From 3f4623bb36b6079b3dc59560a9b3ffa1043e3474 Mon Sep 17 00:00:00 2001 From: Andy Green Date: Wed, 6 Jan 2021 15:08:22 +0000 Subject: [PATCH] lws_metrics There are a few build options that are trying to keep and report various statistics - DETAILED_LATENCY - SERVER_STATUS - WITH_STATS remove all those and establish a generic rplacement, lws_metrics. lws_metrics makes its stats available via an lws_system ops function pointer that the user code can set. Openmetrics export is supported, for, eg, prometheus scraping. --- .sai.json | 18 +- CMakeLists-implied-options.txt | 5 +- CMakeLists.txt | 4 +- README.md | 3 +- READMEs/README.detailed-latency.md | 117 -- READMEs/README.lws_metrics.md | 245 ++++ cmake/lws_config.h.in | 1 + doc-assets/lws_metrics-decimation.png | Bin 0 -> 73917 bytes doc-assets/lws_metrics-policy.png | Bin 0 -> 120375 bytes include/libwebsockets.h | 3 +- include/libwebsockets/lws-context-vhost.h | 19 +- include/libwebsockets/lws-detailed-latency.h | 140 -- include/libwebsockets/lws-lejp.h | 2 +- include/libwebsockets/lws-metrics.h | 329 +++++ include/libwebsockets/lws-protocols-plugins.h | 15 + .../libwebsockets/lws-secure-streams-policy.h | 20 + include/libwebsockets/lws-smd.h | 5 + include/libwebsockets/lws-stats.h | 81 -- include/libwebsockets/lws-system.h | 8 +- lib/core-net/CMakeLists.txt | 16 - lib/core-net/adopt.c | 21 +- lib/core-net/client/connect.c | 25 +- lib/core-net/client/connect2.c | 38 +- lib/core-net/client/connect3.c | 51 +- lib/core-net/client/connect4.c | 6 +- lib/core-net/close.c | 32 +- lib/core-net/detailed-latency.c | 79 -- lib/core-net/network.c | 2 +- lib/core-net/output.c | 71 +- lib/core-net/pollfd.c | 16 - lib/core-net/private-lib-core-net.h | 100 +- lib/core-net/server.c | 326 ----- lib/core-net/service.c | 26 +- lib/core-net/sorted-usec-list.c | 2 + lib/core-net/stats.c | 273 ---- lib/core-net/vhost.c | 64 +- lib/core-net/wsi-timeout.c | 3 - lib/core-net/wsi.c | 4 +- lib/core/context.c | 124 +- lib/core/logs.c | 36 + lib/core/private-lib-core.h | 98 +- lib/plat/freertos/freertos-service.c | 10 - lib/plat/unix/unix-service.c | 35 +- lib/roles/h1/ops-h1.c | 14 +- lib/roles/h2/hpack.c | 4 +- lib/roles/h2/http2.c | 23 +- lib/roles/h2/ops-h2.c | 8 +- lib/roles/http/client/client-http.c | 25 +- lib/roles/http/header.c | 7 +- lib/roles/http/parsers.c | 12 +- lib/roles/http/private-lib-roles-http.h | 3 + lib/roles/http/server/server.c | 41 +- lib/roles/mqtt/client/client-mqtt.c | 11 - lib/roles/mqtt/mqtt.c | 10 +- lib/roles/raw-skt/ops-raw-skt.c | 14 +- lib/roles/ws/client-ws.c | 5 - lib/secure-streams/policy-common.c | 28 + lib/secure-streams/policy-json.c | 81 +- .../private-lib-secure-streams.h | 33 + lib/secure-streams/protocols/ss-h1.c | 32 +- lib/secure-streams/protocols/ss-mqtt.c | 6 + lib/secure-streams/protocols/ss-raw.c | 20 +- lib/secure-streams/protocols/ss-ws.c | 6 + lib/secure-streams/secure-streams-client.c | 27 + lib/secure-streams/secure-streams-process.c | 25 +- lib/secure-streams/secure-streams-serialize.c | 11 +- lib/secure-streams/secure-streams.c | 16 + lib/system/CMakeLists.txt | 2 + lib/system/async-dns/async-dns-parse.c | 4 +- lib/system/async-dns/async-dns.c | 14 + lib/system/async-dns/private-lib-async-dns.h | 3 + lib/system/metrics/CMakeLists.txt | 10 + lib/system/metrics/metrics.c | 891 ++++++++++++ .../metrics/private-lib-system-metrics.h | 124 ++ lib/system/smd/smd.c | 4 + lib/tls/mbedtls/mbedtls-client.c | 70 +- lib/tls/mbedtls/mbedtls-ssl.c | 61 +- lib/tls/openssl/openssl-client.c | 72 +- lib/tls/openssl/openssl-ssl.c | 57 +- lib/tls/tls-client.c | 12 +- lib/tls/tls-network.c | 4 - lib/tls/tls-server.c | 30 - lwsws/main.c | 2 +- .../api-tests/api-test-lws_tokenize/main.c | 6 +- .../minimal-http-client-multi.c | 34 +- .../minimal-http-server-tls.c | 39 +- .../minimal-secure-streams-alexa/main.c | 5 - .../minimal-secure-streams-avs/main-client.c | 5 - .../minimal-secure-streams-avs/main.c | 5 - .../metrics-proxy-policy.json | 59 + .../minimal-secure-streams-post.c | 4 - .../minimal-secure-streams-proxy/main.c | 35 +- .../minimal-secure-streams-smd.c | 1 + .../minimal-secure-streams.c | 4 - .../minimal-secure-streams-testsfail.c | 105 ++ .../minimal-secure-streams.c | 36 +- plugins/CMakeLists.txt | 14 +- plugins/protocol_lws_openmetrics_export.c | 1200 +++++++++++++++++ plugins/protocol_lws_server_status.c | 218 --- plugins/server-status.html | 25 - plugins/server-status.js | 249 ---- 101 files changed, 4066 insertions(+), 2273 deletions(-) delete mode 100644 READMEs/README.detailed-latency.md create mode 100644 READMEs/README.lws_metrics.md create mode 100644 doc-assets/lws_metrics-decimation.png create mode 100644 doc-assets/lws_metrics-policy.png delete mode 100644 include/libwebsockets/lws-detailed-latency.h create mode 100644 include/libwebsockets/lws-metrics.h delete mode 100644 include/libwebsockets/lws-stats.h delete mode 100644 lib/core-net/detailed-latency.c delete mode 100644 lib/core-net/server.c delete mode 100644 lib/core-net/stats.c create mode 100644 lib/system/metrics/CMakeLists.txt create mode 100644 lib/system/metrics/metrics.c create mode 100644 lib/system/metrics/private-lib-system-metrics.h create mode 100644 minimal-examples/secure-streams/minimal-secure-streams-metrics-proxy/metrics-proxy-policy.json create mode 100644 plugins/protocol_lws_openmetrics_export.c delete mode 100644 plugins/protocol_lws_server_status.c delete mode 100644 plugins/server-status.html delete mode 100644 plugins/server-status.js diff --git a/.sai.json b/.sai.json index 7b08cf71e..26a2de8a2 100644 --- a/.sai.json +++ b/.sai.json @@ -54,7 +54,7 @@ "build": "mkdir build destdir; cd build; export LD_LIBRARY_PATH=../destdir/usr/local/share/libwebsockets-test-server/plugins:../destdir/usr/local/lib;export SAI_CPACK=\"-G ZIP\";export MACOSX_DEPLOYMENT_TARGET=10.15 ; cmake .. -DCMAKE_MAKE_PROGRAM=/usr/bin/make -DLWS_OPENSSL_INCLUDE_DIRS=/usr/local/opt/openssl@1.1/include -DLWS_OPENSSL_LIBRARIES=\"/usr/local/opt/openssl/lib/libssl.dylib;/usr/local/opt/openssl/lib/libcrypto.dylib\" ${cmake} && make -j4 && make -j DESTDIR=../destdir install && ctest -j2 --output-on-failure ${cpack}" }, "netbsd-OSX-bigsur/aarch64-apple-m1/llvm": { - "build": "mkdir build destdir; cd build; export LD_LIBRARY_PATH=../destdir/usr/local/share/libwebsockets-test-server/plugins:../destdir/usr/local/lib;export SAI_CPACK=\"-G ZIP\";export MACOSX_DEPLOYMENT_TARGET=10.15 ; cmake .. -DLWS_WITH_SUL_DEBUGGING=1 -DCMAKE_SYSTEM_PREFIX_PATH=/opt/homebrew -DLWS_OPENSSL_INCLUDE_DIRS=/opt/homebrew/Cellar/openssl@1.1/1.1.1h/include '-DLWS_OPENSSL_LIBRARIES=/opt/homebrew/Cellar/openssl@1.1/1.1.1h/lib/libssl.dylib;/opt/homebrew/Cellar/openssl@1.1/1.1.1h/lib/libcrypto.dylib' -DLWS_WITH_MINIMAL_EXAMPLES=1 ${cmake} && make -j6 && rm -rf ../destdir && make -j DESTDIR=../destdir install && ctest -j3 --output-on-failure ${cpack}" + "build": "mkdir build destdir; cd build; export LD_LIBRARY_PATH=../destdir/usr/local/share/libwebsockets-test-server/plugins:../destdir/usr/local/lib;export SAI_CPACK=\"-G ZIP\";export MACOSX_DEPLOYMENT_TARGET=10.15 ; cmake .. -DLWS_WITH_SUL_DEBUGGING=1 -DCMAKE_SYSTEM_PREFIX_PATH=/opt/homebrew -DLWS_OPENSSL_INCLUDE_DIRS=/opt/homebrew/Cellar/openssl@1.1/1.1.1h/include '-DLWS_OPENSSL_LIBRARIES=/opt/homebrew/Cellar/openssl@1.1/1.1.1h/lib/libssl.dylib;/opt/homebrew/Cellar/openssl@1.1/1.1.1h/lib/libcrypto.dylib' ${cmake} && make -j6 && rm -rf ../destdir && make -j DESTDIR=../destdir install && ctest -j3 --output-on-failure ${cpack}" }, "solaris/x86_64-amd/gcc": { "build": "mkdir build destdir; cd build; export SAI_CPACK=\"-G ZIP\";cmake .. ${cmake} && make -j 4 && make install DESTDIR=../destdir && ctest -j2 --output-on-failure ${cpack}", @@ -115,7 +115,7 @@ }, "default-noudp": { "cmake": "-DLWS_WITH_UDP=0", - "platforms": "w10/x86_64-amd/msvc, w10/x86_64-amd/noptmsvc, freertos-linkit/arm32-m4-mt7697-usi/gcc, linux-ubuntu-2004/aarch64-a72-bcm2711-rpi4/gcc, w10/x86_64-amd/mingw32, w10/x86_64-amd/mingw64, netbsd/aarch64BE-bcm2837-a53/gcc, w10/x86_64-amd/wmbedtls-msvc" + "platforms": "w10/x86_64-amd/msvc, w10/x86_64-amd/noptmsvc, freertos-linkit/arm32-m4-mt7697-usi/gcc, linux-ubuntu-2004/aarch64-a72-bcm2711-rpi4/gcc, w10/x86_64-amd/mingw32, w10/x86_64-amd/mingw64, netbsd/aarch64BE-bcm2837-a53/gcc, w10/x86_64-amd/wmbedtlsmsvc" }, "esp32-heltec": { "cmake": "", @@ -181,7 +181,10 @@ "cmake": "-DLWS_WITH_SECURE_STREAMS=1 -DLWS_WITH_SECURE_STREAMS_PROXY_API=1 -DLWS_WITH_MINIMAL_EXAMPLES=1 -DLWS_WITH_SECURE_STREAMS_AUTH_SIGV4=1", "platforms": "not w10/x86_64-amd/msvc, netbsd/aarch64BE-bcm2837-a53/gcc, openbsd/x86_64-amd/llvm, solaris/x86_64-amd/gcc" }, - + "secure-streams-proxy-metrics": { + "cmake": "-DLWS_WITH_SECURE_STREAMS=1 -DLWS_WITH_SECURE_STREAMS_PROXY_API=1 -DLWS_WITH_MINIMAL_EXAMPLES=1 -DLWS_WITH_SECURE_STREAMS_AUTH_SIGV4=1 -DLWS_WITH_SYS_METRICS=1", + "platforms": "not w10/x86_64-amd/msvc, netbsd/aarch64BE-bcm2837-a53/gcc" + }, "distro_recommended": { # minimal examples also needed for ctest "cmake": "-DLWS_WITH_DISTRO_RECOMMENDED=1 -DLWS_WITH_MINIMAL_EXAMPLES=1", "platforms": "not freebsd-12/x86_64-amd/llvm, not linkit-cross, not w10/x86_64-amd/msvc, linux-ubuntu-2004/aarch64-a72-bcm2711-rpi4/gcc, linux-fedora-32/riscv64-virt/gcc", @@ -193,6 +196,11 @@ # no distro -devel package for libuv "platforms": "not linux-centos-8/x86_64-amd/gcc" }, + "lwsws-nometrics": { + "cmake": "-DLWS_WITH_LWSWS=ON -DLWS_WITHOUT_EXTENSIONS=0 -DLWS_WITH_HTTP2=1 -DLWS_WITH_ACME=1 -DLWS_WITH_MINIMAL_EXAMPLES=1 -DCMAKE_BUILD_TYPE=DEBUG -DLWS_WITH_GENCRYPTO=1 -DLWS_WITH_JOSE=1 -DLWS_WITH_SYS_ASYNC_DNS=1 -DLWS_WITH_SYS_NTPCLIENT=1 -DLWS_WITH_SYS_METRICS=0", + # no distro -devel package for libuv + "platforms": "not linux-centos-8/x86_64-amd/gcc" + }, "lwsws2": { "cmake": "-DLWS_WITH_LWSWS=ON -DLWS_WITHOUT_EXTENSIONS=0 -DLWS_WITH_HTTP2=1 -DLWS_WITH_ACME=1 -DLWS_WITH_MINIMAL_EXAMPLES=1 -DCMAKE_BUILD_TYPE=DEBUG -DLWS_WITH_LWS_DSH=1", # no distro -devel package for libuv @@ -207,6 +215,10 @@ # no distro -devel package for mbedtls "platforms": "not linux-centos-7/x86_64-amd/gcc, not linux-centos-8/x86_64-amd/gcc" }, + "mbedtls-metrics": { + "cmake": "-DLWS_WITH_MBEDTLS=1 -DLWS_WITH_HTTP2=1 -DLWS_WITH_LWSWS=1 -DLWS_WITH_MINIMAL_EXAMPLES=1 -DLWS_WITH_JOSE=1 -DCMAKE_BUILD_TYPE=DEBUG -DLWS_WITH_SYS_METRICS=1", + "platforms": "not linux-centos-7/x86_64-amd/gcc, not linux-centos-8/x86_64-amd/gcc" + }, "noserver": { "cmake": "-DLWS_WITHOUT_SERVER=ON -DLWS_WITH_MINIMAL_EXAMPLES=1 -DLWS_WITH_SECURE_STREAMS=1", "platforms": "w10/x86_64-amd/msvc, w10/x86_64-amd/noptmsvc" diff --git a/CMakeLists-implied-options.txt b/CMakeLists-implied-options.txt index 6c98359ce..89e319e1f 100644 --- a/CMakeLists-implied-options.txt +++ b/CMakeLists-implied-options.txt @@ -83,7 +83,7 @@ if(LWS_WITH_DISTRO_RECOMMENDED) set(LWS_WITH_SOCKS5 1) # selfcontained set(LWS_WITH_RANGES 1) # selfcontained set(LWS_WITH_ACME 1) # selfcontained / tls - set(LWS_WITH_SERVER_STATUS 1) # selfcontained + set(LWS_WITH_SYS_METRICS 1) # selfcontained set(LWS_WITH_GLIB 1) # glib set(LWS_WITH_LIBUV 1) # libuv set(LWS_WITH_LIBEV 1) # libev @@ -128,6 +128,7 @@ endif() if (LWS_WITH_SECURE_STREAMS_PROXY_API) set(LWS_WITH_LWS_DSH 1) set(LWS_WITH_UNIX_SOCK 1) + set(LWS_WITH_SYS_SMD 1) endif() if (NOT LWS_WITH_NETWORK) @@ -210,7 +211,7 @@ if (LWS_WITH_LWSWS) set(LWS_WITH_LIBUV_INTERNAL 1) set(LWS_WITH_EVENT_LIBS 1) # implied by LIBUV_INTERNAL set(LWS_WITH_ACCESS_LOG 1) - set(LWS_WITH_SERVER_STATUS 1) + set(LWS_WITH_SYS_METRICS 1) set(LWS_WITH_LEJP 1) set(LWS_WITH_LEJP_CONF 1) set(LWS_WITH_PEER_LIMITS 1) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9be767fc6..996a54511 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,7 +112,6 @@ option(LWS_WITH_SOCKS5 "Allow use of SOCKS5 proxy on client connections" OFF) option(LWS_WITH_PEER_LIMITS "Track peers and restrict resources a single peer can allocate" OFF) option(LWS_WITH_ACCESS_LOG "Support generating Apache-compatible access logs" OFF) option(LWS_WITH_RANGES "Support http ranges (RFC7233)" OFF) -option(LWS_WITH_SERVER_STATUS "Support json + jscript server monitoring" OFF) option(LWS_WITH_THREADPOOL "Managed worker thread pool support (relies on pthreads)" OFF) option(LWS_WITH_HTTP_STREAM_COMPRESSION "Support HTTP stream compression" OFF) option(LWS_WITH_HTTP_BROTLI "Also offer brotli http stream compression (requires LWS_WITH_HTTP_STREAM_COMPRESSION)" OFF) @@ -134,6 +133,7 @@ else() option(LWS_WITH_RFC6724 "Enable RFC6724 DNS result sorting" OFF) endif() option(LWS_WITH_SYS_FAULT_INJECTION "Enable fault injection support" OFF) +option(LWS_WITH_SYS_METRICS "Lws Metrics API" OFF) # # Secure Streams @@ -250,7 +250,6 @@ set(LWS_LOGGING_BITFIELD_CLEAR 0 CACHE STRING "Bitfield describing which log lev option(LWS_LOGS_TIMESTAMP "Timestamp at start of logs" ON) option(LWS_LOG_TAG_LIFECYCLE "Log tagged object lifecycle as NOTICE" ON) option(LWS_AVOID_SIGPIPE_IGN "Android 7+ reportedly needs this" OFF) -option(LWS_WITH_STATS "Keep statistics of lws internal operations" OFF) option(LWS_WITH_JOSE "JSON Web Signature / Encryption / Keys (RFC7515/6/) API" OFF) option(LWS_WITH_GENCRYPTO "Enable support for Generic Crypto apis independent of TLS backend" OFF) option(LWS_WITH_SELFTESTS "Selftests run at context creation" OFF) @@ -272,7 +271,6 @@ option(LWS_WITH_EXTERNAL_POLL "Support external POLL integration using callback option(LWS_WITH_LWS_DSH "Support lws_dsh_t Disordered Shared Heap" OFF) option(LWS_CLIENT_HTTP_PROXYING "Support external http proxies for client connections" ON) option(LWS_WITH_FILE_OPS "Support file operations vfs" ON) -option(LWS_WITH_DETAILED_LATENCY "Record detailed latency stats for each read and write" OFF) option(LWS_WITH_UDP "Platform supports UDP" ON) option(LWS_WITH_SPAWN "Spawn subprocesses with piped stdin/out/stderr" OFF) option(LWS_WITH_FSMOUNT "Overlayfs and fallback mounting apis" OFF) diff --git a/README.md b/README.md index 3aa3c97b9..2013f436d 100644 --- a/README.md +++ b/README.md @@ -378,7 +378,8 @@ with `api-tests/api-test-async-dns` minimal example. You can now opt to measure and store us-resolution statistics on effective latencies for client operations, and easily spool them to a file in a format suitable for gnuplot, or handle in your own callback. Enable -`-DLWS_WITH_DETAILED_LATENCY=1` in cmake to build it into lws. +`-DLWS_WITH_DETAILED_LATENCY=1` in cmake to build it into lws. (NB 2021-01-12 +this has been replaced by the lws_metrics support) If you are concerned about operation latency or potential blocking from user code, or behaviour under load, or latency variability on specific diff --git a/READMEs/README.detailed-latency.md b/READMEs/README.detailed-latency.md deleted file mode 100644 index 29e29d03a..000000000 --- a/READMEs/README.detailed-latency.md +++ /dev/null @@ -1,117 +0,0 @@ -# lws detailed latency - -![lws detailed latency example plot](../doc-assets/lws-detailed-latency-example.png) - -## Introduction - -lws has the capability to make detailed latency measurements and -report them in realtime to a specified callback. - -A default callback is provided that renders the data as text in -space-separated format suitable for gnuplot, to a specified file. - -## Configuring - -Enable `LWS_WITH_DETAILED_LATENCY` at cmake. - -Create your context with something similar to this - -``` -#if defined(LWS_WITH_DETAILED_LATENCY) - info.detailed_latency_cb = lws_det_lat_plot_cb; - info.detailed_latency_filepath = "/tmp/lws-latency-results"; -#endif -``` - -`lws_det_lat_plot_cb` is provided by lws as a convenience to convert -the stuct data provided at the callback interface to space-separated -text data that is easy to process with shell commands and gnuplot. - -## `lws_det_lat_plot_cb` format - -``` -728239173547 N 23062 0 0 23062 0 0 0 -728239192554 C 18879 0 0 18879 0 0 0 -728239217894 T 25309 0 0 25309 0 0 0 -728239234998 r 0 0 0 0 271 172 256 -728239250611 r 0 0 0 0 69 934 4096 -728239255679 w 19 122 18 159 20 80 80 -728239275718 w 20 117 15 152 18 80 80 -728239295578 w 10 73 7 90 7 80 80 -728239315567 w 9 67 5 81 7 80 80 -728239335745 w 23 133 9 165 14 80 80 -... -``` - -Each event is shown in 9 columns - - - unix time in us - - event type - - N = Name resolution - - C = TCP Connection - - T = TLS negotiation server - - t = TLS negotiation client - - r = Read - - w = Write - - us duration, for w time client spent waiting to write - - us duration, for w time data spent in transit to proxy - - us duration, for w time proxy waited to send data - - as a convenience, sum of last 3 columns above - - us duration, time spent in callback - - last 2 are actual / requested size in bytes - -## Processing captured data with ministat - -Eg, to summarize overall latencies on all captured writes - -``` - $ cat /tmp/lws-latency-results | grep " w " | cut -d' ' -f6 | ministat -... - N Min Max Median Avg Stddev -x 1000 43 273 141 132.672 32.471693 -``` - -## Processing captured data with gnuplot - -### Gnuplot plotting script - -Create a gnuplot script, eg myscript.gp - -``` -reset -set term pngcairo enhanced nocrop font "OpenSans, 12" size 800,600#output terminal and file -set output "lws-latency.png" -#set yrange [0:10000] -#to put an empty boundary around the -#data inside an autoscaled graph. -set offset graph 0.05,0.05,0.05,0.0 -set style fill transparent solid 0.5 #fillstyle -set tics out nomirror -set xlabel "event" -set ylabel "latency (us)" -set format x "" -set title "Write latency" -set key invert reverse Right inside nobox -set key autotitle columnheader -set style data histogram -set style histogram rowstacked -set style fill solid border -1 -set boxwidth 0.75 -set style fill solid 1.00 noborder -set tic scale 0 -set grid ytics lc rgb "#505050" -unset border -unset xtics - -plot '/tmp/1' \ - using ($3 + $4 + $5):xtic(1) w boxes lt rgbcolor "blue" title 'prox wr wait', \ - '' using ($3 + $4):xtic(1) w boxes lt rgbcolor "green" title 'txfr to prox', \ - '' using 3:xtic(1) w boxes lt rgbcolor "red" title 'cli wri wait' -``` - -### gnuplot invocation - -``` - $ cat /tmp/lws-latency-results | grep " w " \>/tmp/1 ; gnuplot myscript.gp && eog lws-latency.png -``` - diff --git a/READMEs/README.lws_metrics.md b/READMEs/README.lws_metrics.md new file mode 100644 index 000000000..82cd2a525 --- /dev/null +++ b/READMEs/README.lws_metrics.md @@ -0,0 +1,245 @@ +## `lws_metrics` + +### Introduction + +`lws_metrics` records and aggregates **events** at all lws layers. + +There are three distinct parts: + + - the architecture inside lws for collecting and aggregating / decimating the + events and maintaining statistics about them, these are lws_metric objects + + - an external handler for forwarding aggregated metrics. An lws_system ops + interface to pass on the aggregated metrics to an external backend. lws + presents its own public metrics objects and leaves it to the external + code to have a shim to marry the lws metrics up to whatever is needed in the + metrics backend + + - a policy for when to emit each type of aggregated information to the external + handler. This can be specified in the generic Secure Streams policy, or + a linked-list of lws_metric_policy_t object passed it at context creation in + `info.metrics_policies`. + +The external backend interface code may itself make use of lws connectivity apis +including Secure Streams itself, and lws metrics are available on that too. + +### `lws_metrics` policy-based reporting + +Normally metrics implementations are fixed at build-time and cannot change +without a coordinated reflash of devices along with a change of backend schema. + +`lws_metrics` separates out the objects and code necessary to collect and +aggregate the data cheaply, and the reporting policy that controls if, or how +often, the results are reported to the external handler. + +![policy based metrics](/doc-assets/lws_metrics-policy.png) + +Metrics are created with a namespace name and the policy applies itself to those +by listing the names, with wildcards allowed, the policy applies to, eg if +specified in the Secure Streams JSON policy + +``` + ... + "metrics": [ + { + "name": "tensecs", + "us_schedule": 10000000, + "report": "cpu.*" + }, { + "name": "30secs", + "us_schedule": 30000000, + "report": "n.cn.*, n.http.*, n.ss.*, vh.*" + } + ], + ... +``` + +Metrics that do not have a reporting policy do not report, but continue to +aggregate measurements in case they are bound to a policy dynamically later. + +### Freeform metrics naming + +There is no predefined metrics schema, metrics objects, including those created +by applications, can independently choose their own name in a namespace like +"cpu.srv" or "n.cn.dns", and can set a prefix for all metrics names created in a +context (by setting `info.metrics_prefix` at context creation time). + +This allows multiple processes in a single device to expose copies of the same +metrics in an individually addressable way, eg, if the UI process specifies the +prefix "ui", then its lws metrics like "cpu.srv" will actually be created as +"ui.cpu.srv". + +Applications can freely define their own `lws_metrics` measurements with their +own names in the namespace too, without central registration, and refer to those +names in the reporting policy same as any other metric names. + +If the metrics backend requires a fixed schema, the mapping between the +`lws_metrics` names and the backend schema indexes will be done in the +`lws_system` external reporting api implementation alone. Metrics objects +contain a `void * backend_opaque` that is ignored by lws and can be set and +read by the external reporting handler implementation to facilitate that. + +### Histogram metrics tagging + +Histogram metrics track differently-qualified results in the same metric, for +example the metric `n.cn.failures` maintains separate result counts for all +variations and kinds of failure. + +``` +[2021/03/01 06:34:05:6570] U: my_metric_report: ssproxy.n.cn.failures{ss="badcert_selfsigned",hostname="invalidca.badcert.warmcat.com",peer="46.105.127.147",tls="invalidca"} 2 +[2021/03/01 06:34:05:6573] U: my_metric_report: ssproxy.n.cn.failures{hostname="invalidca.badcert.warmcat.com",peer="46.105.127.147",tls="invalidca"} 1 +[2021/03/01 06:34:05:6576] U: my_metric_report: ssproxy.n.cn.failures{ss="badcert_expired",hostname="warmcat.com",peer="46.105.127.147",tls="expired"} 2 +[2021/03/01 06:34:05:6578] U: my_metric_report: ssproxy.n.cn.failures{hostname="warmcat.com",peer="46.105.127.147",tls="expired"} 1 +[2021/03/01 06:34:05:6580] U: my_metric_report: ssproxy.n.cn.failures{ss="badcert_hostname",hostname="hostname.badcert.warmcat.com",peer="46.105.127.147",tls="hostname"} 2 +[2021/03/01 06:34:05:6583] U: my_metric_report: ssproxy.n.cn.failures{hostname="hostname.badcert.warmcat.com",peer="46.105.127.147",tls="hostname"} 1 +[2021/03/01 06:34:05:6585] U: my_metric_report: ssproxy.n.cn.failures{dns="nores -2"} 8 +``` + +The user handler for metrics is expected to iterate these, in the provided +examples (eg, minimal-secure-streams-testsfail) + +``` +#if defined(LWS_WITH_SYS_METRICS) +static int +my_metric_report(lws_metric_pub_t *mp) +{ + lws_metric_bucket_t *sub = mp->u.hist.head; + char buf[192]; + + do { + if (lws_metrics_format(mp, &sub, buf, sizeof(buf))) + lwsl_user("%s: %s\n", __func__, buf); + } while ((mp->flags & LWSMTFL_REPORT_HIST) && sub); + + /* 0 = leave metric to accumulate, 1 = reset the metric */ + + return 1; +} + +static const lws_system_ops_t system_ops = { + .metric_report = my_metric_report, +}; + +#endif +``` + +### `lws_metrics` decimation + +Event information can easily be produced faster than it can be transmitted, or +is useful to record if everything is working. In the case that things are not +working, then eventually the number of events that are unable to be forwarded +to the backend would overwhelm the local storage. + +For that reason, the metrics objects are designed to absorb and summarize a +potentially large number of events cheaply by aggregating them, so even extreme +situations can be tracked meaningfully inbetween dumps to the backend. + +There are two approaches: + + - "aggregation": decimate keeping a uint64 mean + sum, along with a max and min + + - "histogram": keep a linked-list of different named buckets, with a 64-bit + counter for the number of times an event in each bucket was observed + +A single metric aggregation object has separate "go / no-go" counters, since +most operations can fail, and failing operations act differently. + +`lws_metrics` 'aggregation' supports decimation by + + - a mean of a 64-bit event metric, separate for go and no-go events + - counters of go and no-go events + - a min and max of the metric + - keeping track of when the sample period started + +![metrics decimation](/doc-assets/lws_metrics-decimation.png) + +In addition, the policy defines a percentage variance from the mean that +optionally qualifies events to be reported individually. + +The `lws_metrics` 'histogram' allows monitoring of different outcomes to +produce counts of each outcome in the "bucket". + +### `lws_metrics` flags + +When the metrics object is created, flags are used to control how it will be +used and consumed. + +For example to create a histogram metrics object rather than the default +aggregation type, you would give the flag `LWSMTFL_REPORT_HIST` at creation +time. + +|Flag|Meaning| +|---|---| +|`LWSMTFL_REPORT_OUTLIERS`|track outliers and report them internally| +|`LWSMTFL_REPORT_OUTLIERS_OOB`|report each outlier externally as they happen| +|`LWSMTFL_REPORT_INACTIVITY_AT_PERIODIC`|explicitly externally report no activity at periodic cb, by default no events in the period is just not reported| +|`LWSMTFL_REPORT_MEAN`|the mean is interesting for this metric| +|`LWSMTFL_REPORT_ONLY_GO`|no-go pieces invalid and should be ignored, used for simple counters| +|`LWSMTFL_REPORT_DUTY_WALLCLOCK_US`|the aggregated sum or mean can be compared to wallclock time| +|`LWSMTFL_REPORT_HIST`|object is a histogram (else aggregator)| + +### Built-in lws-layer metrics + +lws creates and maintains various well-known metrics when you enable build +with cmake `-DLWS_WITH_SYS_METRICS=1`: + +#### Aggregation metrics +|metric name|scope|type|meaning| +---|---|---|---| +`cpu.svc`|context|monotonic over time|time spent servicing, outside of event loop wait| +`n.cn.dns`|context|go/no-go mean|duration of blocking libc DNS lookup| +`n.cn.adns`|context|go/no-go mean|duration of SYS_ASYNC_DNS lws DNS lookup| +`n.cn.tcp`|context|go/no-go mean|duration of tcp connection until accept| +`n.cn.tls`|context|go/no-go mean|duration of tls connection until accept| +`n.http.txn`|context|go (2xx)/no-go mean|duration of lws http transaction| +`n.ss.conn`|context|go/no-go mean|duration of Secure Stream transaction| +`n.ss.cliprox.conn`|context|go/no-go mean|time taken for client -> proxy connection| +`vh.[vh-name].rx`|vhost|go/no-go sum|received data on the vhost| +`vh.[vh-name].tx`|vhost|go/no-go sum|transmitted data on the vhost| + +#### Histogram metrics +|metric name|scope|type|meaning| +|---|---|---|---| +`n.cn.failures`|context|histogram|Histogram of connection attempt failure reasons| + +#### Connection failure histogram buckets +|Bucket name|Meaning| +|---|---| +`tls/invalidca`|Peer certificate CA signature missing or not trusted| +`tls/hostname`|Peer certificate CN or SAN doesn't match the endpoint we asked for| +`tls/notyetvalid`|Peer certificate start date is in the future (time wrong?)| +`tls/expired`|Peer certificate is expiry date is in the past| +`dns/badsrv`|No DNS result because couldn't talk to the server| +`dns/nxdomain`|No DNS result because server says no result| + +The `lws-minimal-secure-streams` example is able to report the aggregated +metrics at the end of execution, eg + +``` +[2021/01/13 11:47:19:9145] U: my_metric_report: cpu.svc: 137.045ms / 884.563ms (15%) +[2021/01/13 11:47:19:9145] U: my_metric_report: n.cn.dns: Go: 4, mean: 3.792ms, min: 2.470ms, max: 5.426ms +[2021/01/13 11:47:19:9145] U: my_metric_report: n.cn.tcp: Go: 4, mean: 40.633ms, min: 17.107ms, max: 94.560ms +[2021/01/13 11:47:19:9145] U: my_metric_report: n.cn.tls: Go: 3, mean: 91.232ms, min: 30.322ms, max: 204.635ms +[2021/01/13 11:47:19:9145] U: my_metric_report: n.http.txn: Go: 4, mean: 63.089ms, min: 20.184ms, max: 125.474ms +[2021/01/13 11:47:19:9145] U: my_metric_report: n.ss.conn: Go: 4, mean: 161.740ms, min: 42.937ms, max: 429.510ms +[2021/01/13 11:47:19:9145] U: my_metric_report: vh._ss_default.rx: Go: (1) 102, NoGo: (1) 0 +[2021/01/13 11:47:19:9145] U: my_metric_report: vh.le_via_dst.rx: Go: (22) 28.165Ki +[2021/01/13 11:47:19:9145] U: my_metric_report: vh.le_via_dst.tx: Go: (1) 267 +[2021/01/13 11:47:19:9145] U: my_metric_report: vh.api_amazon_com.rx: Go: (1) 1.611Ki, NoGo: (1) 0 +[2021/01/13 11:47:19:9145] U: my_metric_report: vh.api_amazon_com.tx: Go: (3) 1.505Ki +``` + +lws-minimal-secure-stream-testsfail which tests various kinds of connection failure +reports histogram results like this + +``` +[2021/01/15 13:10:16:0933] U: my_metric_report: n.cn.failures: tot: 36, [ tls/invalidca: 5, tls/expired: 5, tls/hostname: 5, dns/nxdomain: 21 ] +``` + +## Support for openmetrics + +Openmetrics https://tools.ietf.org/html/draft-richih-opsawg-openmetrics-00 +defines a textual metrics export format comaptible with Prometheus. Lws +provides a protocol plugin in `./plugins/protocol_lws_openmetrics_export` +that enables direct export for prometheus scraping, and also protocols to +proxy openmetrics export for unreachable servers. diff --git a/cmake/lws_config.h.in b/cmake/lws_config.h.in index 3d4b867da..cd9aa2b3b 100644 --- a/cmake/lws_config.h.in +++ b/cmake/lws_config.h.in @@ -195,6 +195,7 @@ #cmakedefine LWS_WITH_SQLITE3 #cmakedefine LWS_WITH_SYS_DHCP_CLIENT #cmakedefine LWS_WITH_SYS_FAULT_INJECTION +#cmakedefine LWS_WITH_SYS_METRICS #cmakedefine LWS_WITH_SYS_NTPCLIENT #cmakedefine LWS_WITH_SYS_STATE #cmakedefine LWS_WITH_THREADPOOL diff --git a/doc-assets/lws_metrics-decimation.png b/doc-assets/lws_metrics-decimation.png new file mode 100644 index 0000000000000000000000000000000000000000..e791608cd03c1598543d096474e477e1a2b19cd9 GIT binary patch literal 73917 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 literal 0 HcmV?d00001 diff --git a/doc-assets/lws_metrics-policy.png b/doc-assets/lws_metrics-policy.png new file mode 100644 index 0000000000000000000000000000000000000000..d1d5766ca00e84c4fadedccddb46967c6fc86273 GIT binary patch literal 120375 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�AK| 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 $i_h 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�lyxm!A=8eB9%QUa4u)FXJ*r2hw#mfl`}|X* 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 literal 0 HcmV?d00001 diff --git a/include/libwebsockets.h b/include/libwebsockets.h index ae4ab6b38..63dfa4f32 100644 --- a/include/libwebsockets.h +++ b/include/libwebsockets.h @@ -575,8 +575,8 @@ struct lws; #include #include #include +#include #include -#include #include #include #include @@ -605,7 +605,6 @@ struct lws; #include #endif #include -#include #include #include #include diff --git a/include/libwebsockets/lws-context-vhost.h b/include/libwebsockets/lws-context-vhost.h index b90876a01..c72ded05a 100644 --- a/include/libwebsockets/lws-context-vhost.h +++ b/include/libwebsockets/lws-context-vhost.h @@ -245,6 +245,7 @@ struct lws_plat_file_ops; struct lws_ss_policy; struct lws_ss_plugin; +struct lws_metric_policy; typedef int (*lws_context_ready_cb_t)(struct lws_context *context); @@ -725,13 +726,6 @@ struct lws_context_creation_info { const lws_system_ops_t *system_ops; /**< CONTEXT: hook up lws_system_ apis to system-specific * implementations */ -#if defined(LWS_WITH_DETAILED_LATENCY) - det_lat_buf_cb_t detailed_latency_cb; - /**< CONTEXT: NULL, or callback to receive detailed latency information - * collected for each read and write */ - const char *detailed_latency_filepath; - /**< CONTEXT: NULL, or filepath to put latency data into */ -#endif const lws_retry_bo_t *retry_and_idle_policy; /**< VHOST: optional retry and idle policy to apply to this vhost. * Currently only the idle parts are applied to the connections. @@ -840,6 +834,17 @@ struct lws_context_creation_info { * (20 for FREERTOS) */ #endif +#if defined(LWS_WITH_SYS_METRICS) + const struct lws_metric_policy *metrics_policies; + /**< non-SS policy metrics policies */ + const char *metrics_prefix; + /**< prefix for this context's metrics, used to distinguish metrics + * pooled from different processes / applications, so, eg what would + * be "cpu.svc" if this is NULL becomes "myapp.cpu.svc" is this is + * set to "myapp". Policies are applied using the name with the prefix, + * if present. + */ +#endif /* Add new things just above here ---^ * This is part of the ABI, don't needlessly break compatibility diff --git a/include/libwebsockets/lws-detailed-latency.h b/include/libwebsockets/lws-detailed-latency.h deleted file mode 100644 index 1b352c7a6..000000000 --- a/include/libwebsockets/lws-detailed-latency.h +++ /dev/null @@ -1,140 +0,0 @@ -/* - * libwebsockets - small server side websockets and web server implementation - * - * Copyright (C) 2010 - 2019 Andy Green - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - * - * included from libwebsockets.h - */ - -enum { - - /* types of latency, all nonblocking except name resolution */ - - LDLT_READ, /* time taken to read LAT_DUR_PROXY_RX_TO_CLIENT_WRITE */ - LDLT_WRITE, - LDLT_NAME_RESOLUTION, /* BLOCKING: LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE */ - LDLT_CONNECTION, /* conn duration: LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE */ - LDLT_TLS_NEG_CLIENT, /* tls conn duration: LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE */ - LDLT_TLS_NEG_SERVER, /* tls conn duration: LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE */ - - LDLT_USER, - - /* interval / duration elements in latencies array */ - - LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE = 0, - /* us the client spent waiting to write to proxy */ - LAT_DUR_PROXY_CLIENT_WRITE_TO_PROXY_RX, - /* us the packet took to be received by proxy */ - LAT_DUR_PROXY_PROXY_REQ_TO_WRITE, - /* us the proxy has to wait before it could write */ - LAT_DUR_PROXY_RX_TO_ONWARD_TX, - /* us the proxy spent waiting to write to destination, or - * if nonproxied, then time between write request and write */ - - LAT_DUR_USERCB, /* us duration of user callback */ - - LAT_DUR_STEPS /* last */ -}; - -typedef struct lws_detlat { - lws_usec_t earliest_write_req; - lws_usec_t earliest_write_req_pre_write; - /**< use this for interval comparison */ - const char *aux; /* name for name resolution timing */ - int type; - uint32_t latencies[LAT_DUR_STEPS]; - size_t req_size; - size_t acc_size; -} lws_detlat_t; - -typedef int (*det_lat_buf_cb_t)(struct lws_context *context, - const lws_detlat_t *d); - -/** - * lws_det_lat_cb() - inject your own latency records - * - * \param context: the lws_context - * \param d: the lws_detlat_t you have prepared - * - * For proxying or similar cases where latency information is available from - * user code rather than lws itself, you can generate your own latency callback - * events with your own lws_detlat_t. - */ - -LWS_VISIBLE LWS_EXTERN int -lws_det_lat_cb(struct lws_context *context, lws_detlat_t *d); - -/* - * detailed_latency_plot_cb() - canned save to file in plottable format cb - * - * \p context: the lws_context - * \p d: the detailed latency event information - * - * This canned callback makes it easy to export the detailed latency information - * to a file. Just set the context creation members like this - * - * #if defined(LWS_WITH_DETAILED_LATENCY) - * info.detailed_latency_cb = lws_det_lat_plot_cb; - * info.detailed_latency_filepath = "/tmp/lws-latency-results"; - * #endif - * - * and you will get a file containing information like this - * - * 718823864615 N 10589 0 0 10589 0 0 0 - * 718823880837 C 16173 0 0 16173 0 0 0 - * 718823913063 T 32212 0 0 32212 0 0 0 - * 718823931835 r 0 0 0 0 232 30 256 - * 718823948757 r 0 0 0 0 40 30 256 - * 718823948799 r 0 0 0 0 83 30 256 - * 718823965602 r 0 0 0 0 27 30 256 - * 718823965617 r 0 0 0 0 43 30 256 - * 718823965998 r 0 0 0 0 12 28 256 - * 718823983887 r 0 0 0 0 74 3 4096 - * 718823986411 w 16 87 7 110 9 80 80 - * 718824006358 w 8 68 6 82 6 80 80 - * - * which is easy to grep and pass to gnuplot. - * - * The columns are - * - * - unix time in us - * - N = Name resolution, C = TCP Connection, T = TLS negotiation server, - * t = TLS negotiation client, r = Read, w = Write - * - us duration, for w time client spent waiting to write - * - us duration, for w time data spent in transit to proxy - * - us duration, for w time proxy waited to send data - * - as a convenience, sum of last 3 columns above - * - us duration, time spent in callback - * - last 2 are actual / requested size in bytes - */ -LWS_VISIBLE LWS_EXTERN int -lws_det_lat_plot_cb(struct lws_context *context, const lws_detlat_t *d); - -/** - * lws_det_lat_active() - indicates if latencies are being measured - * - * \context: lws_context - * - * Returns 0 if latency measurement has not been set up (the callback is NULL). - * Otherwise returns 1 - */ -LWS_VISIBLE LWS_EXTERN int -lws_det_lat_active(struct lws_context *context); diff --git a/include/libwebsockets/lws-lejp.h b/include/libwebsockets/lws-lejp.h index 55c4dc0ee..6ea6d3dc2 100644 --- a/include/libwebsockets/lws-lejp.h +++ b/include/libwebsockets/lws-lejp.h @@ -182,7 +182,7 @@ typedef signed char (*lejp_callback)(struct lejp_ctx *ctx, char reason); #define LEJP_MAX_DEPTH 12 #endif #ifndef LEJP_MAX_INDEX_DEPTH -#define LEJP_MAX_INDEX_DEPTH 6 +#define LEJP_MAX_INDEX_DEPTH 8 #endif #ifndef LEJP_MAX_PATH #define LEJP_MAX_PATH 128 diff --git a/include/libwebsockets/lws-metrics.h b/include/libwebsockets/lws-metrics.h new file mode 100644 index 000000000..4df7a266d --- /dev/null +++ b/include/libwebsockets/lws-metrics.h @@ -0,0 +1,329 @@ + /* + * libwebsockets - small server side websockets and web server implementation + * + * Copyright (C) 2010 - 2021 Andy Green + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * Public apis related to metric collection and reporting + */ + +/* lws_metrics public part */ + +typedef uint64_t u_mt_t; + +enum { + LWSMTFL_REPORT_OUTLIERS = (1 << 0), + /**< track outliers and report them internally */ + LWSMTFL_REPORT_OOB = (1 << 1), + /**< report events as they happen */ + LWSMTFL_REPORT_INACTIVITY_AT_PERIODIC = (1 << 2), + /**< explicitly externally report no activity at periodic cb, by + * default no events in the period is just not reported */ + LWSMTFL_REPORT_MEAN = (1 << 3), + /**< average/min/max is meaningful, else only sum is meaningful */ + LWSMTFL_REPORT_ONLY_GO = (1 << 4), + /**< no-go pieces invalid */ + LWSMTFL_REPORT_DUTY_WALLCLOCK_US = (1 << 5), + /**< aggregate compares to wallclock us for duty cycle */ + LWSMTFL_REPORT_HIST = (1 << 6), + /**< our type is histogram (otherwise, sum / mean aggregation) */ +}; + +/* + * lws_metrics_tag allows your object to accumulate OpenMetrics-style + * descriptive tags before accounting for it with a metrics object at the end. + * + * Tags should represent low entropy information that is likely to repeat + * identically, so, eg, http method name, not eg, latency in us which is + * unlikely to be seen the same twice. + * + * Tags are just a list of name=value pairs, used for qualifying the final + * metrics entry with decorations in additional dimensions. For example, + * rather than keep individual metrics on methods, scheme, mountpoint, result + * code, you can keep metrics on http transactions only, and qualify the + * transaction metrics entries with tags that can be queried on the metrics + * backend to get the finer-grained information. + * + * http_srv{code="404",mount="/",method="GET",scheme="http"} 3 + * + * For OpenMetrics the tags are converted to a { list } and appended to the base + * metrics name before using with actual metrics objects, the same set of tags + * on different transactions resolve to the same qualification string. + */ + +typedef struct lws_metrics_tag { + lws_dll2_t list; + + const char *name; /* tag, intended to be in .rodata, not copied */ + /* overallocated value */ +} lws_metrics_tag_t; + +LWS_EXTERN LWS_VISIBLE int +lws_metrics_tag_add(lws_dll2_owner_t *owner, const char *name, const char *val); + +#if defined(LWS_WITH_SYS_METRICS) +/* + * wsi-specific version that also appends the tag value to the lifecycle tag + * used for logging the wsi identity + */ +LWS_EXTERN LWS_VISIBLE int +lws_metrics_tag_wsi_add(struct lws *wsi, const char *name, const char *val); +#else +#define lws_metrics_tag_wsi_add(_a, _b, _c) +#endif + +#if defined(LWS_WITH_SECURE_STREAMS) +/* + * ss-specific version that also appends the tag value to the lifecycle tag + * used for logging the ss identity + */ +#if defined(LWS_WITH_SYS_METRICS) +LWS_EXTERN LWS_VISIBLE int +lws_metrics_tag_ss_add(struct lws_ss_handle *ss, const char *name, const char *val); +#else +#define lws_metrics_tag_ss_add(_a, _b, _c) +#endif +#endif + +LWS_EXTERN LWS_VISIBLE void +lws_metrics_tags_destroy(lws_dll2_owner_t *owner); + +LWS_EXTERN LWS_VISIBLE size_t +lws_metrics_tags_serialize(lws_dll2_owner_t *owner, char *buf, size_t len); + +LWS_EXTERN LWS_VISIBLE const char * +lws_metrics_tag_get(lws_dll2_owner_t *owner, const char *name); + +/* histogram bucket */ + +typedef struct lws_metric_bucket { + struct lws_metric_bucket *next; + uint64_t count; + + /* name + NUL is overallocated */ +} lws_metric_bucket_t; + +/* get overallocated name of bucket from bucket pointer */ +#define lws_metric_bucket_name_len(_b) (*((uint8_t *)&(_b)[1])) +#define lws_metric_bucket_name(_b) (((const char *)&(_b)[1]) + 1) + +/* + * These represent persistent local event measurements. They may aggregate + * a large number of events inbetween external dumping of summaries of the + * period covered, in two different ways + * + * 1) aggregation by sum or mean, to absorb multiple scalar readings + * + * - go / no-go ratio counting + * - mean averaging for, eg, latencies + * - min / max for averaged values + * - period the stats covers + * + * 2) aggregation by histogram, to absorb a range of outcomes that may occur + * multiple times + * + * - add named buckets to histogram + * - bucket has a 64-bit count + * - bumping a bucket just increments the count if already exists, else adds + * a new one with count set to 1 + * + * The same type with a union covers both cases. + * + * The lws_system ops api that hooks lws_metrics up to a metrics backend is + * given a pointer to these according to the related policy, eg, hourly, or + * every event passed straight through. + */ + +typedef struct lws_metric_pub { + const char *name; + /**< eg, "n.cn.dns", "vh.myendpoint" */ + void *backend_opaque; + /**< ignored by lws, backend handler completely owns it */ + + lws_usec_t us_first; + /**< us time metric started collecting, reset to us_dumped at dump */ + lws_usec_t us_last; + /**< 0, or us time last event, reset to 0 at last dump */ + lws_usec_t us_dumped; + /**< 0 if never, else us time of last dump to external api */ + + /* scope of data in .u is "since last dump" --> */ + + union { + /* aggregation, by sum or mean */ + + struct { + u_mt_t sum[2]; + /**< go, no-go summed for mean or plan sum */ + u_mt_t min; + /**< smallest individual measurement */ + u_mt_t max; + /**< largest individual measurement */ + + uint32_t count[2]; + /**< go, no-go count of measurements in sum */ + } agg; + + /* histogram with dynamic named buckets */ + + struct { + lws_metric_bucket_t *head; + /**< first bucket in our bucket list */ + + uint64_t total_count; + /**< total count in all of our buckets */ + uint32_t list_size; + /**< number of buckets in our bucket list */ + } hist; + } u; + + uint8_t flags; + +} lws_metric_pub_t; + +LWS_EXTERN LWS_VISIBLE void +lws_metrics_hist_bump_priv_tagged(lws_metric_pub_t *mt, lws_dll2_owner_t *tow, + lws_dll2_owner_t *tow2); + + +/* + * Calipers are a helper struct for implementing "hanging latency" detection, + * where setting the start time and finding the end time may happen in more than + * one place. + * + * There are convenience wrappers to eliminate caliper definitions and code + * cleanly if WITH_SYS_METRICS is disabled for the build. + */ + +struct lws_metric; + +typedef struct lws_metric_caliper { + struct lws_dll2_owner mtags_owner; /**< collect tags here during + * caliper lifetime */ + struct lws_metric *mt; /**< NULL == inactive */ + lws_usec_t us_start; +} lws_metric_caliper_t; + +#if defined(LWS_WITH_SYS_METRICS) +#define lws_metrics_caliper_compose(_name) \ + lws_metric_caliper_t _name; +#define lws_metrics_caliper_bind(_name, _mt) \ + { if (_name.mt) { \ + lwsl_err("caliper: overwrite %s\n", \ + lws_metrics_priv_to_pub(_name.mt)->name); \ + assert(0); } \ + _name.mt = _mt; _name.us_start = lws_now_usecs(); } +#define lws_metrics_caliper_declare(_name, _mt) \ + lws_metric_caliper_t _name = { .mt = _mt, .us_start = lws_now_usecs() } +#define lws_metrics_caliper_report(_name, _go_nogo) \ + { if (_name.us_start) { lws_metric_event(_name.mt, _go_nogo, \ + (u_mt_t)(lws_now_usecs() - \ + _name.us_start)); \ + } lws_metrics_caliper_done(_name); } +#define lws_metrics_caliper_report_hist(_name, pwsi) if (_name.mt) { \ + lws_metrics_hist_bump_priv_tagged(lws_metrics_priv_to_pub(_name.mt), \ + &_name.mtags_owner, \ + pwsi ? &((pwsi)->cal_conn.mtags_owner) : NULL); \ + lws_metrics_caliper_done(_name); } + +#define lws_metrics_caliper_cancel(_name) { lws_metrics_caliper_done(_name); } +#define lws_metrics_hist_bump(_mt, _name) \ + lws_metrics_hist_bump_(_mt, _name) +#define lws_metrics_hist_bump_priv(_mt, _name) \ + lws_metrics_hist_bump_(lws_metrics_priv_to_pub(_mt), _name) +#define lws_metrics_caliper_done(_name) { \ + _name.us_start = 0; _name.mt = NULL; \ + lws_metrics_tags_destroy(&_name.mtags_owner); } +#else +#define lws_metrics_caliper_compose(_name) +#define lws_metrics_caliper_bind(_name, _mt) +#define lws_metrics_caliper_declare(_name, _mp) +#define lws_metrics_caliper_report(_name, _go_nogo) +#define lws_metrics_caliper_report_hist(_name, pwsiconn) +#define lws_metrics_caliper_cancel(_name) +#define lws_metrics_hist_bump(_mt, _name) +#define lws_metrics_hist_bump_priv(_mt, _name) +#define lws_metrics_caliper_done(_name) +#endif + +/** + * lws_metrics_format() - helper to format a metrics object for logging + * + * \param pub: public part of metrics object + * \param buf: output buffer to place string in + * \param len: available length of \p buf + * + * Helper for describing the state of a metrics object as a human-readable + * string, accounting for how its flags indicate what it contains. This is not + * how you would report metrics, but during development it can be useful to + * log them inbetween possibily long report intervals. + * + * It uses the metric's flags to adapt the format shown appropriately, eg, + * as a histogram if LWSMTFL_REPORT_HIST etc + */ +LWS_EXTERN LWS_VISIBLE int +lws_metrics_format(lws_metric_pub_t *pub, lws_metric_bucket_t **sub, + char *buf, size_t len); + +/** + * lws_metrics_hist_bump() - add or increment histogram bucket + * + * \param pub: public part of metrics object + * \param name: bucket name to increment + * + * Either increment the count of an existing bucket of the right name in the + * metrics object, or add a new bucket of the given name and set its count to 1. + * + * The metrics object must have been created with flag LWSMTFL_REPORT_HIST + * + * Normally, you will actually use the preprocessor wrapper + * lws_metrics_hist_bump() defined above, since this automatically takes care of + * removing itself from the build if WITH_SYS_METRICS is not defined, without + * needing any preprocessor conditionals. + */ +LWS_EXTERN LWS_VISIBLE int +lws_metrics_hist_bump_(lws_metric_pub_t *pub, const char *name); + +LWS_VISIBLE LWS_EXTERN int +lws_metrics_foreach(struct lws_context *ctx, void *user, + int (*cb)(lws_metric_pub_t *pub, void *user)); + +LWS_VISIBLE LWS_EXTERN int +lws_metrics_hist_bump_describe_wsi(struct lws *wsi, lws_metric_pub_t *pub, + const char *name); + +enum { + LMT_NORMAL = 0, /* related to successful events */ + LMT_OUTLIER, /* related to successful events outside of bounds */ + + LMT_FAIL, /* related to failed events */ + + LMT_COUNT, +}; + +typedef enum lws_metric_rpt { + LMR_PERIODIC = 0, /* we are reporting on a schedule */ + LMR_OUTLIER, /* we are reporting the last outlier */ +} lws_metric_rpt_kind_t; + +#define METRES_GO 0 +#define METRES_NOGO 1 + + diff --git a/include/libwebsockets/lws-protocols-plugins.h b/include/libwebsockets/lws-protocols-plugins.h index b39bf3b7c..ec9d8d8c8 100644 --- a/include/libwebsockets/lws-protocols-plugins.h +++ b/include/libwebsockets/lws-protocols-plugins.h @@ -359,7 +359,22 @@ extern const struct lws_protocols lws_sshd_demo_protocols[1]; extern const struct lws_protocols lws_acme_client_protocols[1]; extern const struct lws_protocols client_loopback_test_protocols[1]; extern const struct lws_protocols fulltext_demo_protocols[1]; +extern const struct lws_protocols lws_openmetrics_export_protocols[ +#if defined(LWS_WITH_SERVER) && defined(LWS_WITH_CLIENT) && defined(LWS_ROLE_WS) + 4 +#else +#if defined(LWS_WITH_SERVER) + 3 +#else + 1 +#endif +#endif + ]; +#define LWSOMPROIDX_DIRECT_HTTP_SERVER 0 +#define LWSOMPROIDX_PROX_HTTP_SERVER 1 +#define LWSOMPROIDX_PROX_WS_SERVER 2 +#define LWSOMPROIDX_PROX_WS_CLIENT 3 #endif diff --git a/include/libwebsockets/lws-secure-streams-policy.h b/include/libwebsockets/lws-secure-streams-policy.h index d22b23e00..f3924605d 100644 --- a/include/libwebsockets/lws-secure-streams-policy.h +++ b/include/libwebsockets/lws-secure-streams-policy.h @@ -77,6 +77,25 @@ typedef struct lws_ss_plugin { } lws_ss_plugin_t; #endif +/* the public, const metrics policy definition */ + +typedef struct lws_metric_policy { + /* order of first two mandated by JSON policy parsing scope union */ + const struct lws_metric_policy *next; + const char *name; + + const char *report; + + /**< the metrics policy name in the policy, used to bind to it */ + uint32_t us_schedule; + /**< us interval between lws_system metrics api reports */ + + uint32_t us_decay_unit; + /**< how many us to decay avg by half, 0 = no decay */ + uint8_t min_contributors; + /**< before we can judge something is an outlier */ +} lws_metric_policy_t; + typedef struct lws_ss_x509 { struct lws_ss_x509 *next; const char *vhost_name; /**< vhost name using cert ctx */ @@ -226,6 +245,7 @@ typedef struct lws_ss_policy { const char *payload_fmt; const char *socks5_proxy; lws_ss_metadata_t *metadata; /* linked-list of metadata */ + const lws_metric_policy_t *metrics; /* linked-list of metric policies */ const lws_ss_auth_t *auth; /* NULL or auth object we bind to */ /* protocol-specific connection policy details */ diff --git a/include/libwebsockets/lws-smd.h b/include/libwebsockets/lws-smd.h index f5188b5af..50dbc9ecf 100644 --- a/include/libwebsockets/lws-smd.h +++ b/include/libwebsockets/lws-smd.h @@ -52,6 +52,11 @@ enum { * Something happened on the network, eg, link-up or DHCP, or captive * portal state update */ + LWSSMDCL_METRICS = (1 << 3), + /**< + * An SS client process is reporting a metric to the proxy (this class + * is special in that it is not rebroadcast by the proxy) + */ LWSSMDCL_USER_BASE_BITNUM = 24 }; diff --git a/include/libwebsockets/lws-stats.h b/include/libwebsockets/lws-stats.h deleted file mode 100644 index ca9a4e625..000000000 --- a/include/libwebsockets/lws-stats.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * libwebsockets - small server side websockets and web server implementation - * - * Copyright (C) 2010 - 2019 Andy Green - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -/* - * Stats are all uint64_t numbers that start at 0. - * Index names here have the convention - * - * _C_ counter - * _B_ byte count - * _MS_ millisecond count - */ - -enum { - LWSSTATS_C_CONNECTIONS, /**< count incoming connections */ - LWSSTATS_C_API_CLOSE, /**< count calls to close api */ - LWSSTATS_C_API_READ, /**< count calls to read from socket api */ - LWSSTATS_C_API_LWS_WRITE, /**< count calls to lws_write API */ - LWSSTATS_C_API_WRITE, /**< count calls to write API */ - LWSSTATS_C_WRITE_PARTIALS, /**< count of partial writes */ - LWSSTATS_C_WRITEABLE_CB_REQ, /**< count of writable callback requests */ - LWSSTATS_C_WRITEABLE_CB_EFF_REQ, /**< count of effective writable callback requests */ - LWSSTATS_C_WRITEABLE_CB, /**< count of writable callbacks */ - LWSSTATS_C_SSL_CONNECTIONS_FAILED, /**< count of failed SSL connections */ - LWSSTATS_C_SSL_CONNECTIONS_ACCEPTED, /**< count of accepted SSL connections */ - LWSSTATS_C_SSL_ACCEPT_SPIN, /**< count of SSL_accept() attempts */ - LWSSTATS_C_SSL_CONNS_HAD_RX, /**< count of accepted SSL conns that have had some RX */ - LWSSTATS_C_TIMEOUTS, /**< count of timed-out connections */ - LWSSTATS_C_SERVICE_ENTRY, /**< count of entries to lws service loop */ - LWSSTATS_B_READ, /**< aggregate bytes read */ - LWSSTATS_B_WRITE, /**< aggregate bytes written */ - LWSSTATS_B_PARTIALS_ACCEPTED_PARTS, /**< aggreate of size of accepted write data from new partials */ - LWSSTATS_US_SSL_ACCEPT_LATENCY_AVG, /**< aggregate delay in accepting connection */ - LWSSTATS_US_WRITABLE_DELAY_AVG, /**< aggregate delay between asking for writable and getting cb */ - LWSSTATS_US_WORST_WRITABLE_DELAY, /**< single worst delay between asking for writable and getting cb */ - LWSSTATS_US_SSL_RX_DELAY_AVG, /**< aggregate delay between ssl accept complete and first RX */ - LWSSTATS_C_PEER_LIMIT_AH_DENIED, /**< number of times we would have given an ah but for the peer limit */ - LWSSTATS_C_PEER_LIMIT_WSI_DENIED, /**< number of times we would have given a wsi but for the peer limit */ - LWSSTATS_C_CONNS_CLIENT, /**< attempted client conns */ - LWSSTATS_C_CONNS_CLIENT_FAILED, /**< failed client conns */ - - /* Add new things just above here ---^ - * This is part of the ABI, don't needlessly break compatibility - * - * UPDATE stat_names in stats.c in sync with this! - */ - LWSSTATS_SIZE -}; - -#if defined(LWS_WITH_STATS) - -LWS_VISIBLE LWS_EXTERN uint64_t -lws_stats_get(struct lws_context *context, int index); -LWS_VISIBLE LWS_EXTERN void -lws_stats_log_dump(struct lws_context *context); -#else -static LWS_INLINE uint64_t -lws_stats_get(struct lws_context *context, int index) { (void)context; (void)index; return 0; } -static LWS_INLINE void -lws_stats_log_dump(struct lws_context *context) { (void)context; } -#endif diff --git a/include/libwebsockets/lws-system.h b/include/libwebsockets/lws-system.h index 1feef7967..5d57a1f7a 100644 --- a/include/libwebsockets/lws-system.h +++ b/include/libwebsockets/lws-system.h @@ -1,7 +1,7 @@ /* * libwebsockets - small server side websockets and web server implementation * - * Copyright (C) 2010 - 2020 Andy Green + * Copyright (C) 2010 - 2021 Andy Green * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -151,7 +151,6 @@ typedef enum { LWS_CPD_NO_INTERNET, /* we couldn't touch anything */ } lws_cpd_result_t; - typedef void (*lws_attach_cb_t)(struct lws_context *context, int tsi, void *opaque); struct lws_attach_item; @@ -182,6 +181,11 @@ typedef struct lws_system_ops { * by calling lws_captive_portal_detect_result() api */ + int (*metric_report)(lws_metric_pub_t *mdata); + /**< metric \p item is reporting an event of kind \p rpt, + * held in \p mdata... return 0 to leave the metric object as it is, + * or nonzero to reset it. */ + uint32_t wake_latency_us; /**< time taken for this device to wake from suspend, in us */ diff --git a/lib/core-net/CMakeLists.txt b/lib/core-net/CMakeLists.txt index e401506d8..96c742ed7 100644 --- a/lib/core-net/CMakeLists.txt +++ b/lib/core-net/CMakeLists.txt @@ -51,11 +51,6 @@ if (LWS_WITH_NETLINK) ) endif() -if (LWS_WITH_DETAILED_LATENCY) - list(APPEND SOURCES - core-net/detailed-latency.c) -endif() - if (LWS_WITH_LWS_DSH) list(APPEND SOURCES core-net/lws-dsh.c) @@ -77,20 +72,9 @@ if (LWS_WITH_CLIENT) ) endif() -if (NOT LWS_WITHOUT_SERVER) - list(APPEND SOURCES - core-net/server.c) -endif() - if (LWS_WITH_SOCKS5 AND NOT LWS_WITHOUT_CLIENT) list(APPEND SOURCES core-net/socks5-client.c) endif() -if (LWS_WITH_NETWORK AND LWS_WITH_STATS) - list(APPEND SOURCES - core-net/stats.c - ) -endif() - exports_to_parent_scope() diff --git a/lib/core-net/adopt.c b/lib/core-net/adopt.c index 8c2c05573..98c92d348 100644 --- a/lib/core-net/adopt.c +++ b/lib/core-net/adopt.c @@ -66,7 +66,11 @@ lws_create_new_server_wsi(struct lws_vhost *vhost, int fixed_tsi, const char *de return NULL; } - __lws_lc_tag(&vhost->context->lcg[LWSLCG_WSI_SERVER], &new_wsi->lc, desc); + __lws_lc_tag(&vhost->context->lcg[ +#if defined(LWS_ROLE_H2) || defined(LWS_ROLE_MQTT) + strcmp(desc, "adopted") ? LWSLCG_WSI_MUX : +#endif + LWSLCG_WSI_SERVER], &new_wsi->lc, desc); new_wsi->wsistate |= LWSIFR_SERVER; new_wsi->tsi = (char)n; @@ -77,11 +81,6 @@ lws_create_new_server_wsi(struct lws_vhost *vhost, int fixed_tsi, const char *de new_wsi->rxflow_change_to = LWS_RXFLOW_ALLOW; new_wsi->retry_policy = vhost->retry_policy; -#if defined(LWS_WITH_DETAILED_LATENCY) - if (vhost->context->detailed_latency_cb) - new_wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); -#endif - /* initialize the instance struct */ lwsi_set_state(new_wsi, LRS_UNCONNECTED); @@ -145,8 +144,6 @@ lws_adopt_descriptor_vhost1(struct lws_vhost *vh, lws_adoption_type type, pt = &context->pt[(int)new_wsi->tsi]; lws_pt_lock(pt, __func__); - lws_stats_bump(pt, LWSSTATS_C_CONNECTIONS, 1); - if (parent) { new_wsi->parent = parent; new_wsi->sibling_list = parent->child_list; @@ -176,6 +173,11 @@ lws_adopt_descriptor_vhost1(struct lws_vhost *vh, lws_adoption_type type, goto bail; } +#if defined(LWS_WITH_SERVER) + if (new_wsi->role_ops) + lws_metrics_tag_wsi_add(new_wsi, "role", new_wsi->role_ops->name); +#endif + lws_pt_unlock(pt); /* @@ -494,9 +496,6 @@ lws_adopt_descriptor_vhost_via_info(const lws_adopt_desc_t *info) peer->count_wsi >= info->vh->context->ip_limit_wsi) { lwsl_info("Peer reached wsi limit %d\n", info->vh->context->ip_limit_wsi); - lws_stats_bump(&info->vh->context->pt[0], - LWSSTATS_C_PEER_LIMIT_WSI_DENIED, - 1); if (info->vh->context->pl_notify_cb) info->vh->context->pl_notify_cb( info->vh->context, diff --git a/lib/core-net/client/connect.c b/lib/core-net/client/connect.c index dd3ca3516..edc4b9a5b 100644 --- a/lib/core-net/client/connect.c +++ b/lib/core-net/client/connect.c @@ -86,8 +86,12 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i) struct lws *wsi, *safe = NULL; const struct lws_protocols *p; const char *cisin[CIS_COUNT]; - int tid = 0, n, tsi = 0; struct lws_vhost *vh; + int +#if LWS_MAX_SMP > 1 + tid = 0, +#endif + n, tsi = 0; size_t size; char *pc; @@ -105,9 +109,6 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i) if (i->local_protocol_name) local = i->local_protocol_name; - lws_stats_bump(&i->context->pt[tid], LWSSTATS_C_CONNS_CLIENT, 1); - - lws_context_lock(i->context, __func__); /* * PHASE 1: if SMP, find out the tsi related to current service thread @@ -161,10 +162,6 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i) lws_fi_import(&wsi->fi, i->fi); #endif -#if defined(LWS_WITH_DETAILED_LATENCY) && LWS_MAX_SMP > 1 - wsi->detlat.tsi = tsi; -#endif - /* * Until we exit, we can report connection failure directly to the * caller without needing to call through to protocol CONNECTION_ERROR. @@ -186,11 +183,6 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i) else wsi->retry_policy = &i->context->default_retry; -#if defined(LWS_WITH_DETAILED_LATENCY) - if (i->context->detailed_latency_cb) - wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); -#endif - if (i->ssl_connection & LCCSCF_WAKE_SUSPEND__VALIDITY) wsi->conn_validity_wakesuspend = 1; @@ -370,7 +362,8 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i) &wsi->lc, "%s/%s/%s/(%s)", i->method ? i->method : "WS", wsi->role_ops->name, i->address, #if defined(LWS_WITH_SECURE_STREAMS_PROXY_API) - wsi->client_bound_sspc ? lws_sspc_tag((lws_sspc_handle_t *)i->opaque_user_data) : + wsi->client_bound_sspc ? + lws_sspc_tag((lws_sspc_handle_t *)i->opaque_user_data) : #endif lws_ss_tag(((lws_ss_handle_t *)i->opaque_user_data))); } else @@ -379,6 +372,8 @@ lws_client_connect_via_info(const struct lws_client_connect_info *i) "%s/%s/%s", i->method ? i->method : "WS", wsi->role_ops->name, i->address); + lws_metrics_tag_wsi_add(wsi, "vh", wsi->a.vhost->name); + pc = (char *)&wsi->stash[1]; for (n = 0; n < CIS_COUNT; n++) @@ -533,7 +528,5 @@ bail2: if (i->pwsi) *i->pwsi = NULL; - lws_stats_bump(&i->context->pt[tid], LWSSTATS_C_CONNS_CLIENT_FAILED, 1); - return NULL; } diff --git a/lib/core-net/client/connect2.c b/lib/core-net/client/connect2.c index 9e1bbe5d4..2e7033454 100644 --- a/lib/core-net/client/connect2.c +++ b/lib/core-net/client/connect2.c @@ -32,7 +32,11 @@ static int lws_getaddrinfo46(struct lws *wsi, const char *ads, struct addrinfo **result) { + lws_metrics_caliper_declare(cal, wsi->a.context->mt_conn_dns); struct addrinfo hints; +#if defined(LWS_WITH_SYS_METRICS) + char buckname[32]; +#endif int n; memset(&hints, 0, sizeof(hints)); @@ -79,12 +83,26 @@ lws_getaddrinfo46(struct lws *wsi, const char *ads, struct addrinfo **result) #endif wsi->dns_reachability = 1; - lwsl_notice("%s: asking to recheck CPD in 1ms\n", __func__); - lws_system_cpd_start_defer(wsi->a.context, LWS_US_PER_MS); + lws_metrics_caliper_report(cal, METRES_NOGO); +#if defined(LWS_WITH_SYS_METRICS) + lws_snprintf(buckname, sizeof(buckname), "dns=\"unreachable %d\"", n); + lws_metrics_hist_bump_priv_wsi(wsi, mth_conn_failures, buckname); +#endif + lwsl_notice("%s: asking to recheck CPD in 1s\n", __func__); + lws_system_cpd_start_defer(wsi->a.context, LWS_US_PER_SEC); } lwsl_info("%s: getaddrinfo '%s' says %d\n", __func__, ads, n); +#if defined(LWS_WITH_SYS_METRICS) + if (n < 0) { + lws_snprintf(buckname, sizeof(buckname), "dns=\"nores %d\"", n); + lws_metrics_hist_bump_priv_wsi(wsi, mth_conn_failures, buckname); + } +#endif + + lws_metrics_caliper_report(cal, n >= 0 ? METRES_GO : METRES_NOGO); + return n; } #endif @@ -260,19 +278,6 @@ solo: } #endif -#if defined(LWS_WITH_DETAILED_LATENCY) - if (lwsi_state(wsi) == LRS_WAITING_DNS && - wsi->a.context->detailed_latency_cb) { - wsi->detlat.type = LDLT_NAME_RESOLUTION; - wsi->detlat.latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] = - (uint32_t)(lws_now_usecs() - - wsi->detlat.earliest_write_req_pre_write); - wsi->detlat.latencies[LAT_DUR_USERCB] = 0; - lws_det_lat_cb(wsi->a.context, &wsi->detlat); - wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); - } -#endif - #if defined(LWS_CLIENT_HTTP_PROXYING) && \ (defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2)) @@ -313,9 +318,6 @@ solo: lwsl_info("%s: %s: lookup %s:%u\n", __func__, wsi->lc.gutag, ads, port); wsi->conn_port = (uint16_t)port; -#if defined(LWS_WITH_DETAILED_LATENCY) - wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); -#endif #if !defined(LWS_WITH_SYS_ASYNC_DNS) n = 0; if (!wsi->dns_sorted_list.count) { diff --git a/lib/core-net/client/connect3.c b/lib/core-net/client/connect3.c index 5418cfd5d..ec1b250c3 100644 --- a/lib/core-net/client/connect3.c +++ b/lib/core-net/client/connect3.c @@ -203,6 +203,7 @@ lws_client_connect_3_connect(struct lws *wsi, const char *ads, default: lwsl_debug("%s: getsockopt check: conn fail: errno %d\n", __func__, LWS_ERRNO); + lws_metrics_caliper_report(wsi->cal_conn, METRES_NOGO); goto try_next_dns_result_fds; } } @@ -236,19 +237,6 @@ lws_client_connect_3_connect(struct lws *wsi, const char *ads, } #endif -#if defined(LWS_WITH_DETAILED_LATENCY) - if (lwsi_state(wsi) == LRS_WAITING_DNS && - wsi->a.context->detailed_latency_cb) { - wsi->detlat.type = LDLT_NAME_RESOLUTION; - wsi->detlat.latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] = - (uint32_t)(lws_now_usecs() - - wsi->detlat.earliest_write_req_pre_write); - wsi->detlat.latencies[LAT_DUR_USERCB] = 0; - lws_det_lat_cb(wsi->a.context, &wsi->detlat); - wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); - } -#endif - /* * Let's try directly connecting to each of the results in turn until * one works, or we run out of results... @@ -393,11 +381,6 @@ ads_known: * The actual connection attempt */ -#if defined(LWS_WITH_DETAILED_LATENCY) - wsi->detlat.earliest_write_req = - wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); -#endif - #if defined(LWS_ESP_PLATFORM) errno = 0; #endif @@ -412,6 +395,12 @@ ads_known: * Finally, make the actual connection attempt */ +#if defined(LWS_WITH_SYS_METRICS) + if (wsi->cal_conn.mt) + lws_metrics_caliper_report(wsi->cal_conn, METRES_NOGO); + lws_metrics_caliper_bind(wsi->cal_conn, wsi->a.context->mt_conn_tcp); +#endif + m = connect(wsi->desc.sockfd, (const struct sockaddr *)psa, (unsigned int)n); if (m == -1) { /* @@ -438,6 +427,8 @@ ads_known: * The connect() failed immediately... */ + lws_metrics_caliper_report(wsi->cal_conn, METRES_NOGO); + #if defined(_DEBUG) #if defined(LWS_WITH_UNIX_SOCK) if (!wsi->unix_skt) { @@ -503,7 +494,12 @@ conn_good: &salen) == -1) lwsl_warn("getsockname: %s\n", strerror(LWS_ERRNO)); #if defined(_DEBUG) - lws_sa46_write_numeric_address(&wsi->sa46_local, buf, sizeof(buf)); +#if defined(LWS_WITH_UNIX_SOCK) + if (wsi->unix_skt) + buf[0] = '\0'; + else +#endif + lws_sa46_write_numeric_address(&wsi->sa46_local, buf, sizeof(buf)); lwsl_info("%s: %s: source ads %s\n", __func__, wsi->lc.gutag, buf); #endif @@ -511,20 +507,7 @@ conn_good: #endif lws_sul_cancel(&wsi->sul_connect_timeout); - -#if defined(LWS_WITH_DETAILED_LATENCY) - if (wsi->a.context->detailed_latency_cb) { - wsi->detlat.type = LDLT_CONNECTION; - wsi->detlat.latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] = - (uint32_t)(lws_now_usecs() - - wsi->detlat.earliest_write_req_pre_write); - wsi->detlat.latencies[LAT_DUR_USERCB] = 0; - lws_det_lat_cb(wsi->a.context, &wsi->detlat); - wsi->detlat.earliest_write_req = - wsi->detlat.earliest_write_req_pre_write = - lws_now_usecs(); - } -#endif + lws_metrics_caliper_report(wsi->cal_conn, METRES_GO); lws_addrinfo_clean(wsi); @@ -550,6 +533,8 @@ oom4: /* do the full wsi close flow */ goto failed1; + lws_metrics_caliper_report(wsi->cal_conn, METRES_NOGO); + /* * We can't be an active client connection any more, if we thought * that was what we were going to be doing. It should be if we are diff --git a/lib/core-net/client/connect4.c b/lib/core-net/client/connect4.c index eaa05e2cc..89611202f 100644 --- a/lib/core-net/client/connect4.c +++ b/lib/core-net/client/connect4.c @@ -156,11 +156,7 @@ send_hs: * wait in the queue until it's possible to send them. */ lws_callback_on_writable(wsi_piggyback); -#if defined(LWS_WITH_DETAILED_LATENCY) - wsi->detlat.earliest_write_req = - wsi->detlat.earliest_write_req_pre_write = - lws_now_usecs(); -#endif + lwsl_info("%s: %s: waiting to send hdrs (par state 0x%x)\n", __func__, wsi->lc.gutag, lwsi_state(wsi_piggyback)); } else { diff --git a/lib/core-net/close.c b/lib/core-net/close.c index fe01dff51..806d772b6 100644 --- a/lib/core-net/close.c +++ b/lib/core-net/close.c @@ -238,8 +238,6 @@ lws_inform_client_conn_fail(struct lws *wsi, void *arg, size_t len) return; wsi->already_did_cce = 1; - lws_stats_bump(&wsi->a.context->pt[(int)wsi->tsi], - LWSSTATS_C_CONNS_CLIENT_FAILED, 1); if (!wsi->a.protocol) return; @@ -293,6 +291,20 @@ __lws_close_free_wsi(struct lws *wsi, enum lws_close_status reason, context = wsi->a.context; pt = &context->pt[(int)wsi->tsi]; +#if defined(LWS_WITH_SYS_METRICS) && \ + (defined(LWS_WITH_CLIENT) || defined(LWS_WITH_SERVER)) + /* wsi level: only reports if dangling caliper */ + if (wsi->cal_conn.mt && wsi->cal_conn.us_start) { + if ((lws_metrics_priv_to_pub(wsi->cal_conn.mt)->flags) & LWSMTFL_REPORT_HIST) { + lws_metrics_caliper_report_hist(wsi->cal_conn, (struct lws *)NULL); + } else { + lws_metrics_caliper_report(wsi->cal_conn, METRES_NOGO); + lws_metrics_caliper_done(wsi->cal_conn); + } + } else + lws_metrics_caliper_done(wsi->cal_conn); +#endif + #if defined(LWS_WITH_SYS_ASYNC_DNS) if (wsi == context->async_dns.wsi) context->async_dns.wsi = NULL; @@ -300,8 +312,6 @@ __lws_close_free_wsi(struct lws *wsi, enum lws_close_status reason, lws_pt_assert_lock_held(pt); - lws_stats_bump(pt, LWSSTATS_C_API_CLOSE, 1); - #if defined(LWS_WITH_CLIENT) lws_free_set_NULL(wsi->cli_hostname_copy); @@ -719,6 +729,14 @@ async_close: lws_sspc_handle_t *h = (lws_sspc_handle_t *)wsi->a.opaque_user_data; if (h) { // && (h->info.flags & LWSSSINFLAGS_ACCEPTED)) { + +#if defined(LWS_WITH_SYS_METRICS) + /* + * If any hanging caliper measurement, dump it, and free any tags + */ + lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL); +#endif + h->cwsi = NULL; //wsi->a.opaque_user_data = NULL; } @@ -729,6 +747,12 @@ async_close: if (h) { // && (h->info.flags & LWSSSINFLAGS_ACCEPTED)) { + /* + * ss level: only reports if dangling caliper + * not already reported + */ + lws_metrics_caliper_report_hist(h->cal_txn, wsi); + h->wsi = NULL; wsi->a.opaque_user_data = NULL; diff --git a/lib/core-net/detailed-latency.c b/lib/core-net/detailed-latency.c deleted file mode 100644 index a8c1d0988..000000000 --- a/lib/core-net/detailed-latency.c +++ /dev/null @@ -1,79 +0,0 @@ -/* - * libwebsockets - small server side websockets and web server implementation - * - * Copyright (C) 2010 - 2019 Andy Green - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -#include "private-lib-core.h" - -int -lws_det_lat_active(struct lws_context *context) -{ - return !!context->detailed_latency_cb; -} - -int -lws_det_lat_cb(struct lws_context *context, lws_detlat_t *d) -{ - int n; - - if (!context->detailed_latency_cb) - return 0; - - n = context->detailed_latency_cb(context, d); - - memset(&d->latencies, 0, sizeof(d->latencies)); - - return n; -} - -static const char types[] = "rwNCTt????"; -int -lws_det_lat_plot_cb(struct lws_context *context, const lws_detlat_t *d) -{ - char buf[80], *p = buf, *end = &p[sizeof(buf) - 1]; - - if (!context->detailed_latency_filepath) - return 1; - - if (context->latencies_fd == -1) { - context->latencies_fd = open(context->detailed_latency_filepath, - LWS_O_CREAT | LWS_O_TRUNC | LWS_O_WRONLY, 0600); - if (context->latencies_fd == -1) - return 1; - } - - p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), - "%llu %c %u %u %u %u %u %zu %zu\n", - (unsigned long long)lws_now_usecs(), types[d->type], - d->latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE], - d->latencies[LAT_DUR_PROXY_CLIENT_WRITE_TO_PROXY_RX], - d->latencies[LAT_DUR_PROXY_PROXY_REQ_TO_WRITE], - d->latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] + - d->latencies[LAT_DUR_PROXY_CLIENT_WRITE_TO_PROXY_RX] + - d->latencies[LAT_DUR_PROXY_PROXY_REQ_TO_WRITE], - d->latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX], - d->acc_size, d->req_size); - - write(context->latencies_fd, buf, lws_ptr_diff_size_t(p, buf)); - - return 0; -} diff --git a/lib/core-net/network.c b/lib/core-net/network.c index 72d5db99f..866ff3c33 100644 --- a/lib/core-net/network.c +++ b/lib/core-net/network.c @@ -890,7 +890,7 @@ lws_sa46_write_numeric_address(lws_sockaddr46 *sa46, char *buf, size_t len) return lws_snprintf(buf, len, "(unset)"); if (sa46->sa4.sin_family == AF_INET6) - lws_snprintf(buf, len, "(ipv6 unsupp)"); + return lws_snprintf(buf, len, "(ipv6 unsupp)"); lws_snprintf(buf, len, "(AF%d unsupp)", (int)sa46->sa4.sin_family); diff --git a/lib/core-net/output.c b/lib/core-net/output.c index 71e59122f..8a2fd901e 100644 --- a/lib/core-net/output.c +++ b/lib/core-net/output.c @@ -31,7 +31,6 @@ int lws_issue_raw(struct lws *wsi, unsigned char *buf, size_t len) { struct lws_context *context = lws_get_context(wsi); - struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi]; size_t real_len = len; unsigned int n, m; @@ -59,8 +58,6 @@ lws_issue_raw(struct lws *wsi, unsigned char *buf, size_t len) (unsigned long)len); } - lws_stats_bump(pt, LWSSTATS_C_API_WRITE, 1); - /* just ignore sends after we cleared the truncation buffer */ if (lwsi_state(wsi) == LRS_FLUSHING_BEFORE_CLOSE && !lws_has_buffered_out(wsi) @@ -215,9 +212,6 @@ lws_issue_raw(struct lws *wsi, unsigned char *buf, size_t len) real_len - m) < 0) return -1; - lws_stats_bump(pt, LWSSTATS_C_WRITE_PARTIALS, 1); - lws_stats_bump(pt, LWSSTATS_B_PARTIALS_ACCEPTED_PARTS, m); - #if defined(LWS_WITH_UDP) if (lws_wsi_is_udp(wsi)) /* stash original destination for fulfilling UDP partials */ @@ -234,57 +228,30 @@ int lws_write(struct lws *wsi, unsigned char *buf, size_t len, enum lws_write_protocol wp) { - struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi]; -#if defined(LWS_WITH_DETAILED_LATENCY) - lws_usec_t us; -#endif int m; - lws_stats_bump(pt, LWSSTATS_C_API_LWS_WRITE, 1); - if ((int)len < 0) { lwsl_err("%s: suspicious len int %d, ulong %lu\n", __func__, (int)len, (unsigned long)len); return -1; } - lws_stats_bump(pt, LWSSTATS_B_WRITE, len); - #ifdef LWS_WITH_ACCESS_LOG wsi->http.access_log.sent += len; #endif -#if defined(LWS_WITH_SERVER_STATUS) - if (wsi->a.vhost) - wsi->a.vhost->conn_stats.tx += len; -#endif -#if defined(LWS_WITH_DETAILED_LATENCY) - us = lws_now_usecs(); -#endif assert(wsi->role_ops); if (!lws_rops_fidx(wsi->role_ops, LWS_ROPS_write_role_protocol)) - return lws_issue_raw(wsi, buf, len); + m = lws_issue_raw(wsi, buf, len); + else + m = lws_rops_func_fidx(wsi->role_ops, LWS_ROPS_write_role_protocol). + write_role_protocol(wsi, buf, len, &wp); - m = lws_rops_func_fidx(wsi->role_ops, LWS_ROPS_write_role_protocol). - write_role_protocol(wsi, buf, len, &wp); - if (m < 0) - return m; - -#if defined(LWS_WITH_DETAILED_LATENCY) - if (wsi->a.context->detailed_latency_cb) { - wsi->detlat.req_size = len; - wsi->detlat.acc_size = (unsigned int)m; - wsi->detlat.type = LDLT_WRITE; - if (wsi->detlat.earliest_write_req_pre_write) - wsi->detlat.latencies[LAT_DUR_PROXY_PROXY_REQ_TO_WRITE] = - (uint32_t)(us - wsi->detlat.earliest_write_req_pre_write); - else - wsi->detlat.latencies[LAT_DUR_PROXY_PROXY_REQ_TO_WRITE] = 0; - wsi->detlat.latencies[LAT_DUR_USERCB] = (uint32_t)(lws_now_usecs() - us); - lws_det_lat_cb(wsi->a.context, &wsi->detlat); - - } +#if defined(LWS_WITH_SYS_METRICS) + if (wsi->a.vhost) + lws_metric_event(wsi->a.vhost->mt_traffic_tx, (char) + (m < 0 ? METRES_NOGO : METRES_GO), len); #endif return m; @@ -316,19 +283,20 @@ lws_ssl_capable_read_no_ssl(struct lws *wsi, unsigned char *buf, size_t len) en = LWS_ERRNO; if (n >= 0) { - if (!n && wsi->unix_skt) - return LWS_SSL_CAPABLE_ERROR; + //if (!n && wsi->unix_skt) + // goto do_err; /* * See https://libwebsockets.org/ * pipermail/libwebsockets/2019-March/007857.html */ - if (!n) - return LWS_SSL_CAPABLE_ERROR; + if (!n && !wsi->unix_skt) + goto do_err; -#if defined(LWS_WITH_SERVER_STATUS) +#if defined(LWS_WITH_SYS_METRICS) && defined(LWS_WITH_SERVER) if (wsi->a.vhost) - wsi->a.vhost->conn_stats.rx = (unsigned long long)(wsi->a.vhost->conn_stats.rx + (unsigned long long)(long long)n); + lws_metric_event(wsi->a.vhost->mt_traffic_rx, + METRES_GO /* rx */, (unsigned int)n); #endif return n; @@ -339,7 +307,14 @@ lws_ssl_capable_read_no_ssl(struct lws *wsi, unsigned char *buf, size_t len) en == LWS_EINTR) return LWS_SSL_CAPABLE_MORE_SERVICE; - lwsl_info("error on reading from skt : %d\n", en); +do_err: +#if defined(LWS_WITH_SYS_METRICS) && defined(LWS_WITH_SERVER) + if (wsi->a.vhost) + lws_metric_event(wsi->a.vhost->mt_traffic_rx, METRES_NOGO, 0u); +#endif + + lwsl_info("%s: error on reading from skt : %d, errno %d\n", + __func__, n, en); return LWS_SSL_CAPABLE_ERROR; } diff --git a/lib/core-net/pollfd.c b/lib/core-net/pollfd.c index f3ecb61cf..a59f86629 100644 --- a/lib/core-net/pollfd.c +++ b/lib/core-net/pollfd.c @@ -515,7 +515,6 @@ lws_change_pollfd(struct lws *wsi, int _and, int _or) int lws_callback_on_writable(struct lws *wsi) { - struct lws_context_per_thread *pt; struct lws *w = wsi; if (lwsi_state(wsi) == LRS_SHUTDOWN) @@ -524,21 +523,6 @@ lws_callback_on_writable(struct lws *wsi) if (wsi->socket_is_permanently_unusable) return 0; - pt = &wsi->a.context->pt[(int)wsi->tsi]; - -#if defined(LWS_WITH_DETAILED_LATENCY) - if (!wsi->detlat.earliest_write_req) - wsi->detlat.earliest_write_req = lws_now_usecs(); -#endif - - lws_stats_bump(pt, LWSSTATS_C_WRITEABLE_CB_REQ, 1); -#if defined(LWS_WITH_STATS) - if (!wsi->active_writable_req_us) { - wsi->active_writable_req_us = lws_now_usecs(); - lws_stats_bump(pt, LWSSTATS_C_WRITEABLE_CB_EFF_REQ, 1); - } -#endif - if (lws_rops_fidx(wsi->role_ops, LWS_ROPS_callback_on_writable)) { int q = lws_rops_func_fidx(wsi->role_ops, LWS_ROPS_callback_on_writable). diff --git a/lib/core-net/private-lib-core-net.h b/lib/core-net/private-lib-core-net.h index a489fc45d..86094fb34 100644 --- a/lib/core-net/private-lib-core-net.h +++ b/lib/core-net/private-lib-core-net.h @@ -264,41 +264,6 @@ struct lws_timed_vh_protocol { #endif -/* - * lws_dsh -*/ - -typedef struct lws_dsh_obj_head { - lws_dll2_owner_t owner; - size_t total_size; /* for this kind in dsh */ - int kind; -} lws_dsh_obj_head_t; - -typedef struct lws_dsh_obj { - lws_dll2_t list; /* must be first */ - struct lws_dsh *dsh; /* invalid when on free list */ - size_t size; /* invalid when on free list */ - size_t asize; - int kind; /* so we can account at free */ -} lws_dsh_obj_t; - -typedef struct lws_dsh { - lws_dll2_t list; - uint8_t *buf; - lws_dsh_obj_head_t *oha; /* array of object heads/kind */ - size_t buffer_size; - size_t locally_in_use; - size_t locally_free; - int count_kinds; - uint8_t being_destroyed; - /* - * Overallocations at create: - * - * - the buffer itself - * - the object heads array - */ -} lws_dsh_t; - /* * lws_async_dns */ @@ -371,11 +336,6 @@ struct lws_context_per_thread { #if defined(LWS_ROLE_CGI) lws_sorted_usec_list_t sul_cgi; #endif -#if defined(LWS_WITH_STATS) - uint64_t lws_stats[LWSSTATS_SIZE]; - int updated; - lws_sorted_usec_list_t sul_stats; -#endif #if defined(LWS_WITH_PEER_LIMITS) lws_sorted_usec_list_t sul_peer_limits; #endif @@ -419,10 +379,6 @@ struct lws_context_per_thread { void *evlib_pt; /* overallocated */ #endif -#if defined(LWS_WITH_DETAILED_LATENCY) - lws_usec_t ust_left_poll; -#endif - /* --- */ unsigned long count_conns; @@ -454,14 +410,6 @@ struct lws_context_per_thread { unsigned char is_destroyed:1; }; -#if defined(LWS_WITH_SERVER_STATUS) -struct lws_conn_stats { - unsigned long long rx, tx; - unsigned long h1_conn, h1_trans, h2_trans, ws_upg, h2_alpn, h2_subs, - h2_upg, rejected, mqtt_subs; -}; -#endif - /* * virtual host -related context information * vhostwide SSL context @@ -510,8 +458,9 @@ struct lws_vhost { #if defined(LWS_WITH_EVENT_LIBS) void *evlib_vh; /* overallocated */ #endif -#if defined(LWS_WITH_SERVER_STATUS) - struct lws_conn_stats conn_stats; +#if defined(LWS_WITH_SYS_METRICS) + lws_metric_t *mt_traffic_rx; + lws_metric_t *mt_traffic_tx; #endif #if defined(LWS_WITH_SYS_FAULT_INJECTION) @@ -707,10 +656,6 @@ struct lws { void *evlib_wsi; /* overallocated */ #endif -#if defined(LWS_WITH_DETAILED_LATENCY) - lws_detlat_t detlat; -#endif - lws_sorted_usec_list_t sul_timeout; lws_sorted_usec_list_t sul_hrtimer; lws_sorted_usec_list_t sul_validity; @@ -728,6 +673,8 @@ struct lws { struct lws_dll2 dll2_cli_txn_queue; struct lws_dll2_owner dll2_cli_txn_queue_owner; + /**< caliper is reused for tcp, tls and txn conn phases */ + lws_dll2_t speculative_list; lws_dll2_owner_t speculative_connect_owner; /* wsis: additional connection candidates */ @@ -741,6 +688,10 @@ struct lws { /**< Fault Injection ctx for the wsi, hierarchy wsi->vhost->context */ #endif +#if defined(LWS_WITH_SYS_METRICS) + lws_metrics_caliper_compose(cal_conn) +#endif + lws_sockaddr46 sa46_local; lws_sockaddr46 sa46_peer; @@ -779,12 +730,7 @@ struct lws { #endif lws_sock_file_fd_type desc; /* .filefd / .sockfd */ -#if defined(LWS_WITH_STATS) - uint64_t active_writable_req_us; -#if defined(LWS_WITH_TLS) - uint64_t accept_start_us; -#endif -#endif + lws_wsi_state_t wsistate; lws_wsi_state_t wsistate_pre_close; @@ -909,9 +855,6 @@ struct lws { #if defined(LWS_WITH_CGI) || defined(LWS_WITH_CLIENT) char reason_bf; /* internal writeable callback reason bitfield */ #endif -#if defined(LWS_WITH_STATS) && defined(LWS_WITH_TLS) - char seen_rx; -#endif #if defined(LWS_WITH_NETLINK) lws_route_uidx_t peer_route_uidx; /**< unique index of the route the connection is estimated to take */ @@ -1224,11 +1167,6 @@ lws_destroy_event_pipe(struct lws *wsi); int lws_socks5c_generate_msg(struct lws *wsi, enum socks_msg_type type, ssize_t *msg_len); -#if defined(LWS_WITH_SERVER_STATUS) -void -lws_sum_stats(const struct lws_context *ctx, struct lws_conn_stats *cs); -#endif - #if defined(LWS_WITH_DEPRECATED_THINGS) int __lws_timed_callback_remove(struct lws_vhost *vh, struct lws_timed_vh_protocol *p); @@ -1433,21 +1371,6 @@ lws_sort_dns(struct lws *wsi, const struct addrinfo *result); int lws_broadcast(struct lws_context_per_thread *pt, int reason, void *in, size_t len); -#if defined(LWS_WITH_STATS) - void - lws_stats_bump(struct lws_context_per_thread *pt, int i, uint64_t bump); - void - lws_stats_max(struct lws_context_per_thread *pt, int index, uint64_t val); -#else - static LWS_INLINE uint64_t lws_stats_bump( - struct lws_context_per_thread *pt, int index, uint64_t bump) { - (void)pt; (void)index; (void)bump; return 0; } - static LWS_INLINE uint64_t lws_stats_max( - struct lws_context_per_thread *pt, int index, uint64_t val) { - (void)pt; (void)index; (void)val; return 0; } -#endif - - #if defined(LWS_WITH_PEER_LIMITS) void @@ -1500,6 +1423,9 @@ extern const struct lws_protocols protocol_abs_client_raw_skt, void __lws_reset_wsi(struct lws *wsi); +void +lws_metrics_dump(struct lws_context *ctx); + void lws_inform_client_conn_fail(struct lws *wsi, void *arg, size_t len); diff --git a/lib/core-net/server.c b/lib/core-net/server.c deleted file mode 100644 index dff334849..000000000 --- a/lib/core-net/server.c +++ /dev/null @@ -1,326 +0,0 @@ -/* - * libwebsockets - small server side websockets and web server implementation - * - * Copyright (C) 2010 - 2019 Andy Green - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -#include "private-lib-core.h" - -#if defined(LWS_WITH_SERVER_STATUS) - -void -lws_sum_stats(const struct lws_context *ctx, struct lws_conn_stats *cs) -{ - const struct lws_vhost *vh = ctx->vhost_list; - - while (vh) { - - cs->rx += vh->conn_stats.rx; - cs->tx += vh->conn_stats.tx; - cs->h1_conn += vh->conn_stats.h1_conn; - cs->h1_trans += vh->conn_stats.h1_trans; - cs->h2_trans += vh->conn_stats.h2_trans; - cs->ws_upg += vh->conn_stats.ws_upg; - cs->h2_upg += vh->conn_stats.h2_upg; - cs->h2_alpn += vh->conn_stats.h2_alpn; - cs->h2_subs += vh->conn_stats.h2_subs; - cs->rejected += vh->conn_stats.rejected; - - vh = vh->vhost_next; - } -} - -int -lws_json_dump_vhost(const struct lws_vhost *vh, char *buf, int len) -{ -#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) - static const char * const prots[] = { - "http://", - "https://", - "file://", - "cgi://", - ">http://", - ">https://", - "callback://" - }; -#endif - char *orig = buf, *end = buf + len - 1, first; - int n; - - if (len < 100) - return 0; - - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), - "{\n \"name\":\"%s\",\n" - " \"port\":\"%d\",\n" - " \"use_ssl\":\"%d\",\n" - " \"sts\":\"%d\",\n" - " \"rx\":\"%llu\",\n" - " \"tx\":\"%llu\",\n" - " \"h1_conn\":\"%lu\",\n" - " \"h1_trans\":\"%lu\",\n" - " \"h2_trans\":\"%lu\",\n" - " \"ws_upg\":\"%lu\",\n" - " \"rejected\":\"%lu\",\n" - " \"h2_upg\":\"%lu\",\n" - " \"h2_alpn\":\"%lu\",\n" - " \"h2_subs\":\"%lu\"" - , - vh->name, vh->listen_port, -#if defined(LWS_WITH_TLS) - vh->tls.use_ssl & LCCSCF_USE_SSL, -#else - 0, -#endif - !!(vh->options & LWS_SERVER_OPTION_STS), - vh->conn_stats.rx, vh->conn_stats.tx, - vh->conn_stats.h1_conn, - vh->conn_stats.h1_trans, - vh->conn_stats.h2_trans, - vh->conn_stats.ws_upg, - vh->conn_stats.rejected, - vh->conn_stats.h2_upg, - vh->conn_stats.h2_alpn, - vh->conn_stats.h2_subs - ); -#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) - if (vh->http.mount_list) { - const struct lws_http_mount *m = vh->http.mount_list; - - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ",\n \"mounts\":["); - first = 1; - while (m) { - if (!first) - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ","); - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), - "\n {\n \"mountpoint\":\"%s\",\n" - " \"origin\":\"%s%s\",\n" - " \"cache_max_age\":\"%d\",\n" - " \"cache_reuse\":\"%d\",\n" - " \"cache_revalidate\":\"%d\",\n" - " \"cache_intermediaries\":\"%d\"\n" - , - m->mountpoint, - prots[m->origin_protocol], - m->origin, - m->cache_max_age, - m->cache_reusable, - m->cache_revalidate, - m->cache_intermediaries); - if (m->def) - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), - ",\n \"default\":\"%s\"", - m->def); - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "\n }"); - first = 0; - m = m->mount_next; - } - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "\n ]"); - } -#endif - if (vh->protocols) { - n = 0; - first = 1; - - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ",\n \"ws-protocols\":["); - while (n < vh->count_protocols) { - if (!first) - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ","); - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), - "\n {\n \"%s\":{\n" - " \"status\":\"ok\"\n }\n }" - , - vh->protocols[n].name); - first = 0; - n++; - } - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "\n ]"); - } - - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "\n}"); - - return lws_ptr_diff(buf, orig); -} - - -int -lws_json_dump_context(const struct lws_context *context, char *buf, int len, - int hide_vhosts) -{ - char *orig = buf, *end = buf + len - 1, first = 1; - const struct lws_vhost *vh = context->vhost_list; - const struct lws_context_per_thread *pt; - int n, listening = 0, cgi_count = 0, fd; - struct lws_conn_stats cs; - double d = 0; -#ifdef LWS_WITH_CGI - struct lws_cgi * const *pcgi; -#endif - -//#ifdef LWS_WITH_LIBUV && -// uv_uptime(&d); -//#endif - - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "{ " - "\"version\":\"%s\",\n" - "\"uptime\":\"%ld\",\n", - lws_get_library_version(), - (long)d); - -#ifdef LWS_HAVE_GETLOADAVG -#if defined(__sun) -#include -#endif - { - double d[3]; - int m; - - m = getloadavg(d, 3); - for (n = 0; n < m; n++) { - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), - "\"l%d\":\"%.2f\",\n", - n + 1, d[n]); - } - } -#endif - - fd = lws_open("/proc/self/statm", LWS_O_RDONLY); - if (fd >= 0) { - char contents[96], pure[96]; - n = (int)read(fd, contents, sizeof(contents) - 1); - if (n > 0) { - contents[n] = '\0'; - if (contents[n - 1] == '\n') - contents[--n] = '\0'; - lws_json_purify(pure, contents, sizeof(pure), NULL); - - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), - "\"statm\": \"%s\",\n", pure); - } - close(fd); - } - - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "\"heap\":%lld,\n\"contexts\":[\n", - (long long)lws_get_allocated_heap()); - - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "{ " - "\"context_uptime\":\"%llu\",\n" - "\"cgi_spawned\":\"%d\",\n" - "\"pt_fd_max\":\"%d\",\n" - "\"ah_pool_max\":\"%d\",\n" - "\"deprecated\":\"%d\",\n" - "\"wsi_alive\":\"", - (unsigned long long)(lws_now_usecs() - context->time_up) / - LWS_US_PER_SEC, - context->count_cgi_spawned, - context->fd_limit_per_thread, - context->max_http_header_pool, - context->deprecated); - - for (n = 0; n < LWSLCG_COUNT; n++) - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "%u ", - context->lcg[n].owner.count); - - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "\", \"pt\":[\n "); - for (n = 0; n < context->count_threads; n++) { - pt = &context->pt[n]; - if (n) - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ","); - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), - "\n {\n" - " \"fds_count\":\"%d\",\n" - " \"ah_pool_inuse\":\"%d\",\n" - " \"ah_wait_list\":\"%d\"\n" - " }", - pt->fds_count, - pt->http.ah_count_in_use, - pt->http.ah_wait_list_length); - } - - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "]"); - - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ", \"vhosts\":[\n "); - - first = 1; - vh = context->vhost_list; - listening = 0; - cs = context->conn_stats; - lws_sum_stats(context, &cs); - while (vh) { - - if (!hide_vhosts) { - if (!first) - if (buf != end) - *buf++ = ','; - buf += lws_json_dump_vhost(vh, buf, lws_ptr_diff(end, buf)); - first = 0; - } - if (vh->lserv_wsi) - listening++; - vh = vh->vhost_next; - } - - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), - "],\n\"listen_wsi\":\"%d\",\n" - " \"rx\":\"%llu\",\n" - " \"tx\":\"%llu\",\n" - " \"h1_conn\":\"%lu\",\n" - " \"h1_trans\":\"%lu\",\n" - " \"h2_trans\":\"%lu\",\n" - " \"ws_upg\":\"%lu\",\n" - " \"rejected\":\"%lu\",\n" - " \"h2_alpn\":\"%lu\",\n" - " \"h2_subs\":\"%lu\",\n" - " \"h2_upg\":\"%lu\"", - listening, cs.rx, cs.tx, - cs.h1_conn, - cs.h1_trans, - cs.h2_trans, - cs.ws_upg, - cs.rejected, - cs.h2_alpn, - cs.h2_subs, - cs.h2_upg); - -#ifdef LWS_WITH_CGI - for (n = 0; n < context->count_threads; n++) { - pt = &context->pt[n]; - pcgi = &pt->http.cgi_list; - - while (*pcgi) { - pcgi = &(*pcgi)->cgi_list; - - cgi_count++; - } - } -#endif - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ",\n \"cgi_alive\":\"%d\"\n ", - cgi_count); - - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "}"); - - - buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "]}\n "); - - return lws_ptr_diff(buf, orig); -} - -#endif diff --git a/lib/core-net/service.c b/lib/core-net/service.c index 62a73a4a1..5e0370521 100644 --- a/lib/core-net/service.c +++ b/lib/core-net/service.c @@ -27,31 +27,8 @@ int lws_callback_as_writeable(struct lws *wsi) { - struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi]; int n, m; - lws_stats_bump(pt, LWSSTATS_C_WRITEABLE_CB, 1); -#if defined(LWS_WITH_STATS) - if (wsi->active_writable_req_us) { - uint64_t ul = lws_now_usecs() - - wsi->active_writable_req_us; - - lws_stats_bump(pt, LWSSTATS_US_WRITABLE_DELAY_AVG, ul); - lws_stats_max(pt, LWSSTATS_US_WORST_WRITABLE_DELAY, ul); - wsi->active_writable_req_us = 0; - } -#endif -#if defined(LWS_WITH_DETAILED_LATENCY) - if (wsi->a.context->detailed_latency_cb && lwsi_state_est(wsi)) { - lws_usec_t us = lws_now_usecs(); - - wsi->detlat.earliest_write_req_pre_write = - wsi->detlat.earliest_write_req; - wsi->detlat.earliest_write_req = 0; - wsi->detlat.latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX] = - (uint32_t)(us - wsi->detlat.earliest_write_req_pre_write); - } -#endif n = wsi->role_ops->writeable_cb[lwsi_role_server(wsi)]; m = user_callback_handle_rxflow(wsi->a.protocol->callback, wsi, (enum lws_callback_reasons) n, @@ -827,7 +804,8 @@ lws_service(struct lws_context *context, int timeout_ms) } n = lws_plat_service(context, timeout_ms); - pt->inside_service = 0; + if (n != -1) + pt->inside_service = 0; return n; } diff --git a/lib/core-net/sorted-usec-list.c b/lib/core-net/sorted-usec-list.c index 4b3122122..16ff617f1 100644 --- a/lib/core-net/sorted-usec-list.c +++ b/lib/core-net/sorted-usec-list.c @@ -147,6 +147,8 @@ __lws_sul_service_ripe(lws_dll2_owner_t *own, int own_len, lws_usec_t usnow) lws_dll2_remove(&hit->list); hit->us = 0; + // lwsl_notice("%s: sul: %p\n", __func__, hit->cb); + pt->inside_lws_service = 1; hit->cb(hit); pt->inside_lws_service = 0; diff --git a/lib/core-net/stats.c b/lib/core-net/stats.c deleted file mode 100644 index 0a7e7b6b3..000000000 --- a/lib/core-net/stats.c +++ /dev/null @@ -1,273 +0,0 @@ -/* - * libwebsockets - small server side websockets and web server implementation - * - * Copyright (C) 2010 - 2019 Andy Green - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -#include "private-lib-core.h" - -#if defined(LWS_WITH_STATS) - -uint64_t -lws_stats_get(struct lws_context *context, int index) -{ - struct lws_context_per_thread *pt = &context->pt[0]; - - if (index >= LWSSTATS_SIZE) - return 0; - - return pt->lws_stats[index]; -} - -static const char * stat_names[] = { - "C_CONNECTIONS", - "C_API_CLOSE", - "C_API_READ", - "C_API_LWS_WRITE", - "C_API_WRITE", - "C_WRITE_PARTIALS", - "C_WRITEABLE_CB_REQ", - "C_WRITEABLE_CB_EFF_REQ", - "C_WRITEABLE_CB", - "C_SSL_CONNECTIONS_FAILED", - "C_SSL_CONNECTIONS_ACCEPTED", - "C_SSL_CONNECTIONS_ACCEPT_SPIN", - "C_SSL_CONNS_HAD_RX", - "C_TIMEOUTS", - "C_SERVICE_ENTRY", - "B_READ", - "B_WRITE", - "B_PARTIALS_ACCEPTED_PARTS", - "US_SSL_ACCEPT_LATENCY_AVG", - "US_WRITABLE_DELAY_AVG", - "US_WORST_WRITABLE_DELAY", - "US_SSL_RX_DELAY_AVG", - "C_PEER_LIMIT_AH_DENIED", - "C_PEER_LIMIT_WSI_DENIED", - "C_CONNECTIONS_CLIENT", - "C_CONNECTIONS_CLIENT_FAILED", -}; - -static int -quantify(struct lws_context *context, int tsi, char *p, int len, int idx, - uint64_t *sum) -{ - const lws_humanize_unit_t *schema = humanize_schema_si; - struct lws_context_per_thread *pt = &context->pt[tsi]; - uint64_t u, u1; - - lws_pt_stats_lock(pt); - u = pt->lws_stats[idx]; - - /* it's supposed to be an average? */ - - switch (idx) { - case LWSSTATS_US_SSL_ACCEPT_LATENCY_AVG: - u1 = pt->lws_stats[LWSSTATS_C_SSL_CONNECTIONS_ACCEPTED]; - if (u1) - u = u / u1; - break; - case LWSSTATS_US_SSL_RX_DELAY_AVG: - u1 = pt->lws_stats[LWSSTATS_C_SSL_CONNS_HAD_RX]; - if (u1) - u = u / u1; - break; - case LWSSTATS_US_WRITABLE_DELAY_AVG: - u1 = pt->lws_stats[LWSSTATS_C_WRITEABLE_CB]; - if (u1) - u = u / u1; - break; - } - lws_pt_stats_unlock(pt); - - *sum += u; - - switch (stat_names[idx][0]) { - case 'U': - schema = humanize_schema_us; - break; - case 'B': - schema = humanize_schema_si_bytes; - break; - } - - return lws_humanize(p, len, u, schema); -} - - -void -lws_stats_log_dump(struct lws_context *context) -{ - struct lws_vhost *v = context->vhost_list; - uint64_t summary[LWSSTATS_SIZE]; - char bufline[128], *p, *end = bufline + sizeof(bufline) - 1; - int n, m; - - if (!context->updated) - return; - - context->updated = 0; - memset(summary, 0, sizeof(summary)); - - lwsl_notice("\n"); - lwsl_notice("LWS internal statistics dump ----->\n"); - for (n = 0; n < (int)LWS_ARRAY_SIZE(stat_names); n++) { - uint64_t u = 0; - - /* if it's all zeroes, don't report it */ - - for (m = 0; m < context->count_threads; m++) { - struct lws_context_per_thread *pt = &context->pt[m]; - - u |= pt->lws_stats[n]; - } - if (!u) - continue; - - p = bufline; - p += lws_snprintf(p, lws_ptr_diff(end, p), "%28s: ", - stat_names[n]); - - for (m = 0; m < context->count_threads; m++) - quantify(context, m, p, lws_ptr_diff(end, p), n, &summary[n]); - - lwsl_notice("%s\n", bufline); - } - - lwsl_notice("Simultaneous SSL restriction: %8d/%d\n", - context->simultaneous_ssl, - context->simultaneous_ssl_restriction); - - while (v) { - if (v->lserv_wsi && - v->lserv_wsi->position_in_fds_table != LWS_NO_FDS_POS) { - - struct lws_context_per_thread *pt = - &context->pt[(int)v->lserv_wsi->tsi]; - struct lws_pollfd *pfd; - - pfd = &pt->fds[v->lserv_wsi->position_in_fds_table]; - - lwsl_notice(" Listen port %d actual POLLIN: %d\n", - v->listen_port, - (int)pfd->events & LWS_POLLIN); - } - - v = v->vhost_next; - } - - for (n = 0; n < context->count_threads; n++) { - struct lws_context_per_thread *pt = &context->pt[n]; - struct lws *wl; - int m = 0; - - lwsl_notice("PT %d\n", n + 1); - - lws_pt_lock(pt, __func__); - - lwsl_notice(" AH in use / max: %d / %d\n", - pt->http.ah_count_in_use, - context->max_http_header_pool); - - wl = pt->http.ah_wait_list; - while (wl) { - m++; - wl = wl->http.ah_wait_list; - } - - lwsl_notice(" AH wait list count / actual: %d / %d\n", - pt->http.ah_wait_list_length, m); - - lws_pt_unlock(pt); - } - -#if defined(LWS_WITH_PEER_LIMITS) - m = 0; - for (n = 0; n < (int)context->pl_hash_elements; n++) { - lws_start_foreach_llp(struct lws_peer **, peer, - context->pl_hash_table[n]) { - m++; - } lws_end_foreach_llp(peer, next); - } - - lwsl_notice(" Peers: total active %d\n", m); - if (m > 10) { - m = 10; - lwsl_notice(" (showing 10 peers only)\n"); - } - - if (m) { - for (n = 0; n < (int)context->pl_hash_elements; n++) { - char buf[72]; - - lws_start_foreach_llp(struct lws_peer **, peer, - context->pl_hash_table[n]) { - struct lws_peer *df = *peer; - - if (!lws_plat_inet_ntop(df->af, df->addr, buf, - sizeof(buf) - 1)) - strcpy(buf, "unknown"); -#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) - lwsl_notice(" peer %s: count wsi: %d, count ah: %d\n", - buf, df->count_wsi, - df->http.count_ah); -#else - lwsl_notice(" peer %s: count wsi: %d\n", - buf, df->count_wsi); -#endif - - if (!--m) - break; - } lws_end_foreach_llp(peer, next); - } - } -#endif - - lwsl_notice("\n"); -} - -void -lws_stats_bump(struct lws_context_per_thread *pt, int i, uint64_t bump) -{ - lws_pt_stats_lock(pt); - pt->lws_stats[i] += bump; - if (i != LWSSTATS_C_SERVICE_ENTRY) { - pt->updated = 1; - pt->context->updated = 1; - } - lws_pt_stats_unlock(pt); -} - -void -lws_stats_max(struct lws_context_per_thread *pt, int index, uint64_t val) -{ - lws_pt_stats_lock(pt); - if (val > pt->lws_stats[index]) { - pt->lws_stats[index] = val; - pt->updated = 1; - pt->context->updated = 1; - } - lws_pt_stats_unlock(pt); -} - -#endif - - diff --git a/lib/core-net/vhost.c b/lib/core-net/vhost.c index a2ce30b5e..144b6eaa1 100644 --- a/lib/core-net/vhost.c +++ b/lib/core-net/vhost.c @@ -1,7 +1,7 @@ /* * libwebsockets - small server side websockets and web server implementation * - * Copyright (C) 2010 - 2019 Andy Green + * Copyright (C) 2010 - 2021 Andy Green * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -119,9 +119,13 @@ lws_role_call_alpn_negotiated(struct lws *wsi, const char *alpn) LWS_FOR_EVERY_AVAILABLE_ROLE_START(ar) if (ar->alpn && !strcmp(ar->alpn, alpn) && - lws_rops_fidx(ar, LWS_ROPS_alpn_negotiated)) + lws_rops_fidx(ar, LWS_ROPS_alpn_negotiated)) { +#if defined(LWS_WITH_SERVER) + lws_metrics_tag_wsi_add(wsi, "upg", ar->name); +#endif return (lws_rops_func_fidx(ar, LWS_ROPS_alpn_negotiated)). alpn_negotiated(wsi, alpn); + } LWS_FOR_EVERY_AVAILABLE_ROLE_END; #endif return 0; @@ -305,6 +309,8 @@ lws_vhd_find_by_pvo(struct lws_context *cx, const char *protname, vh = cx->vhost_list; while (vh) { + if (vh->protocol_vh_privs) { + for (n = 0; n < vh->count_protocols; n++) { const struct lws_protocol_vhost_options *pv; @@ -313,7 +319,11 @@ lws_vhd_find_by_pvo(struct lws_context *cx, const char *protname, /* this vh has an instance of the required protocol */ - pv = lws_pvo_search(vh->pvo, pvo_name); + pv = lws_pvo_search(vh->pvo, protname); + if (!pv) + continue; + + pv = lws_pvo_search(pv->options, pvo_name); if (!pv) continue; @@ -326,6 +336,8 @@ lws_vhd_find_by_pvo(struct lws_context *cx, const char *protname, */ return vh->protocol_vh_privs[n]; } + } else + lwsl_notice("%s: no privs yet on %s\n", __func__, lws_vh_tag(vh)); vh = vh->vhost_next; } @@ -460,7 +472,7 @@ lws_protocol_init(struct lws_context *context) context->doing_protocol_init = 1; - lwsl_notice("%s\n", __func__); + lwsl_info("%s\n", __func__); while (vh) { @@ -538,10 +550,7 @@ lws_create_vhost(struct lws_context *context, struct lws_protocols *lwsp; int m, f = !info->pvo, fx = 0, abs_pcol_count = 0, sec_pcol_count = 0; char buf[96]; -#if defined(LWS_CLIENT_HTTP_PROXYING) && defined(LWS_WITH_CLIENT) \ - && defined(LWS_HAVE_GETENV) char *p; -#endif #if defined(LWS_WITH_SYS_ASYNC_DNS) extern struct lws_protocols lws_async_dns_protocol; #endif @@ -572,6 +581,19 @@ lws_create_vhost(struct lws_context *context, vh->name = "default"; else vh->name = info->vhost_name; + { + char *end = buf + sizeof(buf) - 1; + p = buf; + + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "%s", vh->name); + if (info->iface) + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "|%s", info->iface); + if (info->port && !(info->port & 0xffff)) + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "|%u", info->port); + } + + __lws_lc_tag(&context->lcg[LWSLCG_VHOST], &vh->lc, "%s|%s|%d", buf, + info->iface ? info->iface : "", info->port); #if defined(LWS_WITH_SYS_FAULT_INJECTION) vh->fi.name = "vh"; @@ -584,9 +606,6 @@ lws_create_vhost(struct lws_context *context, lws_fi_import(&vh->fi, info->fi); #endif - __lws_lc_tag(&context->lcg[LWSLCG_VHOST], &vh->lc, "%s|%s|%d", vh->name, - info->iface ? info->iface : "", info->port); - #if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) vh->http.error_document_404 = info->error_document_404; #endif @@ -804,6 +823,23 @@ lws_create_vhost(struct lws_context *context, vh->http.mount_list = info->mounts; #endif +#if defined(LWS_WITH_SYS_METRICS) && defined(LWS_WITH_SERVER) + { + char *end = buf + sizeof(buf) - 1; + p = buf; + + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "vh.%s", vh->name); + if (info->iface) + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), ".%s", info->iface); + if (info->port && !(info->port & 0xffff)) + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), ".%u", info->port); + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), ".rx"); + vh->mt_traffic_rx = lws_metric_create(context, 0, buf); + p[-2] = 't'; + vh->mt_traffic_tx = lws_metric_create(context, 0, buf); + } +#endif + #ifdef LWS_WITH_UNIX_SOCK if (LWS_UNIX_SOCK_ENABLED(vh)) { lwsl_info("Creating Vhost '%s' path \"%s\", %d protocols\n", @@ -909,7 +945,7 @@ lws_create_vhost(struct lws_context *context, goto bail1; } #if defined(LWS_WITH_SERVER) - lws_context_lock(context, "create_vhost"); + lws_context_lock(context, __func__); n = _lws_vhost_init_server(info, vh); lws_context_unlock(context); if (n < 0) { @@ -1397,6 +1433,11 @@ __lws_vhost_destroy2(struct lws_vhost *vh) lws_dll2_foreach_safe(&vh->abstract_instances_owner, NULL, destroy_ais); #endif +#if defined(LWS_WITH_SERVER) && defined(LWS_WITH_SYS_METRICS) + lws_metric_destroy(&vh->mt_traffic_rx, 0); + lws_metric_destroy(&vh->mt_traffic_tx, 0); +#endif + lws_dll2_remove(&vh->vh_being_destroyed_list); #if defined(LWS_WITH_SYS_FAULT_INJECTION) @@ -1404,6 +1445,7 @@ __lws_vhost_destroy2(struct lws_vhost *vh) #endif __lws_lc_untag(&vh->lc); + memset(vh, 0, sizeof(*vh)); lws_free(vh); } diff --git a/lib/core-net/wsi-timeout.c b/lib/core-net/wsi-timeout.c index a51a34ec9..70ef6d88e 100644 --- a/lib/core-net/wsi-timeout.c +++ b/lib/core-net/wsi-timeout.c @@ -81,9 +81,6 @@ lws_sul_wsitimeout_cb(lws_sorted_usec_list_t *sul) struct lws *wsi = lws_container_of(sul, struct lws, sul_timeout); struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi]; - if (wsi->pending_timeout != PENDING_TIMEOUT_USER_OK) - lws_stats_bump(pt, LWSSTATS_C_TIMEOUTS, 1); - /* no need to log normal idle keepalive timeout */ // if (wsi->pending_timeout != PENDING_TIMEOUT_HTTP_KEEPALIVE_IDLE) #if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) diff --git a/lib/core-net/wsi.c b/lib/core-net/wsi.c index 4bad83da8..5392ef71a 100644 --- a/lib/core-net/wsi.c +++ b/lib/core-net/wsi.c @@ -342,9 +342,9 @@ lws_rx_flow_control(struct lws *wsi, int _enable) /* any bit set in rxflow_bitmap DISABLEs rxflow control */ if (en & LWS_RXFLOW_REASON_APPLIES_ENABLE_BIT) - wsi->rxflow_bitmap &= (uint8_t)~(en & 0xff); + wsi->rxflow_bitmap = (uint8_t)(wsi->rxflow_bitmap & ~(en & 0xff)); else - wsi->rxflow_bitmap |= (uint8_t)(en & 0xff); + wsi->rxflow_bitmap = (uint8_t)(wsi->rxflow_bitmap | (en & 0xff)); if ((LWS_RXFLOW_PENDING_CHANGE | (!wsi->rxflow_bitmap)) == wsi->rxflow_change_to) diff --git a/lib/core/context.c b/lib/core/context.c index e556953ea..1fddce673 100644 --- a/lib/core/context.c +++ b/lib/core/context.c @@ -53,20 +53,6 @@ lws_get_library_version(void) return library_version; } -#if defined(LWS_WITH_STATS) -static void -lws_sul_stats_cb(lws_sorted_usec_list_t *sul) -{ - struct lws_context_per_thread *pt = lws_container_of(sul, - struct lws_context_per_thread, sul_stats); - - lws_stats_log_dump(pt->context); - - __lws_sul_insert_us(&pt->pt_sul_owner[LWSSULLI_MISS_IF_SUSPENDED], - &pt->sul_stats, 10 * LWS_US_PER_SEC); -} -#endif - #if defined(LWS_WITH_NETWORK) #if defined(LWS_WITH_SYS_STATE) @@ -379,10 +365,11 @@ lws_create_context(const struct lws_context_creation_info *info) pid_t pid_daemon = get_daemonize_pid(); #endif #if defined(LWS_WITH_NETWORK) + const lws_plugin_evlib_t *plev = NULL; unsigned short count_threads = 1; uint8_t *u; uint16_t us_wait_resolution = 0; -#endif + #if defined(__ANDROID__) struct rlimit rt; #endif @@ -394,9 +381,10 @@ lws_create_context(const struct lws_context_creation_info *info) s1 = 4096, #endif size = sizeof(struct lws_context); +#endif + int n; unsigned int lpf = info->fd_limit_per_thread; - const lws_plugin_evlib_t *plev = NULL; #if defined(LWS_WITH_EVLIB_PLUGINS) && defined(LWS_WITH_EVENT_LIBS) struct lws_plugin *evlib_plugin_list = NULL; #if defined(_DEBUG) @@ -429,10 +417,6 @@ lws_create_context(const struct lws_context_creation_info *info) s = "IPV6-off"; #endif -#if defined(LWS_WITH_STATS) - lwsl_info(" LWS_WITH_STATS : on\n"); -#endif - lwsl_notice("%s%s\n", opts_str, s); if (lws_plat_context_early_init()) @@ -453,7 +437,6 @@ lws_create_context(const struct lws_context_creation_info *info) #if !defined(LWS_PLAT_FREERTOS) size += (count_threads * sizeof(struct lws)); #endif -#endif /* network */ #if defined(LWS_WITH_POLL) { @@ -655,6 +638,10 @@ lws_create_context(const struct lws_context_creation_info *info) context->lcg[LWSLCG_VHOST].tag_prefix = "vh"; context->lcg[LWSLCG_WSI_SERVER].tag_prefix = "wsisrv"; /* adopted */ +#if defined(LWS_ROLE_H2) || defined(LWS_ROLE_MQTT) + context->lcg[LWSLCG_WSI_MUX].tag_prefix = "mux", /* a mux child wsi */ +#endif + #if defined(LWS_WITH_CLIENT) context->lcg[LWSLCG_WSI_CLIENT].tag_prefix = "wsicli"; #endif @@ -675,6 +662,77 @@ lws_create_context(const struct lws_context_creation_info *info) #endif #endif +#if defined(LWS_WITH_SYS_METRICS) + /* + * If we're not using secure streams, we can still pass in a linked- + * list of metrics policies + */ + context->metrics_policies = info->metrics_policies; + context->metrics_prefix = info->metrics_prefix; + + context->mt_service = lws_metric_create(context, + LWSMTFL_REPORT_DUTY_WALLCLOCK_US | + LWSMTFL_REPORT_ONLY_GO, "cpu.svc"); + +#if defined(LWS_WITH_CLIENT) + + context->mt_conn_dns = lws_metric_create(context, + LWSMTFL_REPORT_MEAN | + LWSMTFL_REPORT_DUTY_WALLCLOCK_US, + "n.cn.dns"); + context->mt_conn_tcp = lws_metric_create(context, + LWSMTFL_REPORT_MEAN | + LWSMTFL_REPORT_DUTY_WALLCLOCK_US, + "n.cn.tcp"); + context->mt_conn_tls = lws_metric_create(context, + LWSMTFL_REPORT_MEAN | + LWSMTFL_REPORT_DUTY_WALLCLOCK_US, + "n.cn.tls"); +#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) + context->mt_http_txn = lws_metric_create(context, + LWSMTFL_REPORT_MEAN | + LWSMTFL_REPORT_DUTY_WALLCLOCK_US, + "n.http.txn"); +#endif + + context->mth_conn_failures = lws_metric_create(context, + LWSMTFL_REPORT_HIST, "n.cn.failures"); + +#if defined(LWS_WITH_SYS_ASYNC_DNS) + context->mt_adns_cache = lws_metric_create(context, + LWSMTFL_REPORT_MEAN | + LWSMTFL_REPORT_DUTY_WALLCLOCK_US, + "n.cn.adns"); +#endif +#if defined(LWS_WITH_SECURE_STREAMS) + context->mth_ss_conn = lws_metric_create(context, LWSMTFL_REPORT_HIST, + "n.ss.conn"); +#endif +#if defined(LWS_WITH_SECURE_STREAMS_PROXY_API) + context->mt_ss_cliprox_conn = lws_metric_create(context, + LWSMTFL_REPORT_HIST, + "n.ss.cliprox.conn"); + context->mt_ss_cliprox_paylat = lws_metric_create(context, + LWSMTFL_REPORT_MEAN | + LWSMTFL_REPORT_DUTY_WALLCLOCK_US, + "n.ss.cliprox.paylat"); + context->mt_ss_proxcli_paylat = lws_metric_create(context, + LWSMTFL_REPORT_MEAN | + LWSMTFL_REPORT_DUTY_WALLCLOCK_US, + "n.ss.proxcli.paylat"); +#endif + +#endif /* network + metrics + client */ + +#if defined(LWS_WITH_SERVER) + context->mth_srv = lws_metric_create(context, + LWSMTFL_REPORT_HIST, "n.srv"); +#endif /* network + metrics + server */ + +#endif /* network + metrics */ + +#endif /* network */ + /* * Proxy group */ @@ -713,11 +771,7 @@ lws_create_context(const struct lws_context_creation_info *info) #if defined(LWS_WITH_NETWORK) context->undestroyed_threads = count_threads; context->count_threads = count_threads; -#if defined(LWS_WITH_DETAILED_LATENCY) - context->detailed_latency_cb = info->detailed_latency_cb; - context->detailed_latency_filepath = info->detailed_latency_filepath; - context->latencies_fd = -1; -#endif + #if defined(LWS_ROLE_WS) && defined(LWS_WITHOUT_EXTENSIONS) if (info->extensions) lwsl_warn("%s: LWS_WITHOUT_EXTENSIONS but extensions ptr set\n", __func__); @@ -1144,12 +1198,6 @@ lws_create_context(const struct lws_context_creation_info *info) #endif #endif -#if defined(LWS_WITH_STATS) - context->pt[0].sul_stats.cb = lws_sul_stats_cb; - __lws_sul_insert_us(&context->pt[0].pt_sul_owner[LWSSULLI_MISS_IF_SUSPENDED], - &context->pt[0].sul_stats, 10 * LWS_US_PER_SEC); -#endif - #if defined(LWS_HAVE_SYS_CAPABILITY_H) && defined(LWS_HAVE_LIBCAP) memcpy(context->caps, info->caps, sizeof(context->caps)); context->count_caps = info->count_caps; @@ -1364,8 +1412,10 @@ bail_libuv_aware: return NULL; #endif +#if defined(LWS_WITH_NETWORK) fail_event_libs: lwsl_err("Requested event library support not configured\n"); +#endif free_context_fail: lws_free(context); @@ -1598,6 +1648,11 @@ lws_context_destroy(struct lws_context *context) context->being_destroyed = 1; #if defined(LWS_WITH_NETWORK) + +#if defined(LWS_WITH_SYS_METRICS) + lws_metrics_dump(context); +#endif + /* * Close any vhost listen wsi * @@ -1819,7 +1874,6 @@ next: lwsl_debug("%p: post pdl\n", __func__); #endif - lws_stats_log_dump(context); #if defined(LWS_WITH_NETWORK) lws_ssl_context_destroy(context); #endif @@ -1982,6 +2036,10 @@ next: lws_mutex_refcount_destroy(&context->mr); #endif +#if defined(LWS_WITH_SYS_METRICS) && defined(LWS_WITH_NETWORK) + lws_metrics_destroy(context); +#endif + if (context->external_baggage_free_on_destroy) free(context->external_baggage_free_on_destroy); diff --git a/lib/core/logs.c b/lib/core/logs.c index 019858ddf..e6b6025c5 100644 --- a/lib/core/logs.c +++ b/lib/core/logs.c @@ -55,6 +55,42 @@ __lws_lc_tag(lws_lifecycle_group_t *grp, lws_lifecycle_t *lc, va_list ap; int n = 1; + if (*lc->gutag == '[') { + /* appending inside [] */ + + char *cp = strchr(lc->gutag, ']'); + char rend[96]; + size_t ll, k; + int n; + + if (!cp) + return; + + /* length of closing brace and anything else after it */ + k = strlen(cp); + + /* compute the remaining gutag unused */ + ll = sizeof(lc->gutag) - lws_ptr_diff_size_t(cp, lc->gutag) - k - 1; + if (ll > sizeof(rend) - 1) + ll = sizeof(rend) - 1; + va_start(ap, format); + n = vsnprintf(rend, ll, format, ap); + va_end(ap); + + if ((unsigned int)n > ll) + n = (int)ll; + + /* shove the trailer up by what we added */ + memmove(cp + n, cp, k); + assert(k + (unsigned int)n < sizeof(lc->gutag)); + cp[k + (unsigned int)n] = '\0'; + /* copy what we added into place */ + memcpy(cp, rend, (unsigned int)n); + + return; + } + + assert(grp); assert(grp->tag_prefix); /* lc group must have a tag prefix string */ lc->gutag[0] = '['; diff --git a/lib/core/private-lib-core.h b/lib/core/private-lib-core.h index 47ad0b5e1..0ec908963 100644 --- a/lib/core/private-lib-core.h +++ b/lib/core/private-lib-core.h @@ -139,6 +139,41 @@ #include "libwebsockets.h" +/* + * lws_dsh +*/ + +typedef struct lws_dsh_obj_head { + lws_dll2_owner_t owner; + size_t total_size; /* for this kind in dsh */ + int kind; +} lws_dsh_obj_head_t; + +typedef struct lws_dsh_obj { + lws_dll2_t list; /* must be first */ + struct lws_dsh *dsh; /* invalid when on free list */ + size_t size; /* invalid when on free list */ + size_t asize; + int kind; /* so we can account at free */ +} lws_dsh_obj_t; + +typedef struct lws_dsh { + lws_dll2_t list; + uint8_t *buf; + lws_dsh_obj_head_t *oha; /* array of object heads/kind */ + size_t buffer_size; + size_t locally_in_use; + size_t locally_free; + int count_kinds; + uint8_t being_destroyed; + /* + * Overallocations at create: + * + * - the buffer itself + * - the object heads array + */ +} lws_dsh_t; + /* * * ------ lifecycle defines ------ @@ -285,6 +320,9 @@ struct lws; #include "private-lib-system-fault-injection.h" #endif +#include "private-lib-system-metrics.h" + + struct lws_foreign_thread_pollfd { struct lws_foreign_thread_pollfd *next; int fd_index; @@ -328,6 +366,10 @@ enum { LWSLCG_WSI_SERVER, /* server wsi */ +#if defined(LWS_ROLE_H2) || defined(LWS_ROLE_MQTT) + LWSLCG_WSI_MUX, /* a mux child wsi */ +#endif + #if defined(LWS_WITH_CLIENT) LWSLCG_WSI_CLIENT, /* client wsi */ #endif @@ -418,21 +460,53 @@ struct lws_context { struct http2_settings set; #endif -#if defined(LWS_WITH_SERVER_STATUS) - struct lws_conn_stats conn_stats; -#endif #if LWS_MAX_SMP > 1 struct lws_mutex_refcount mr; #endif -#if defined(LWS_WITH_NETWORK) +#if defined(LWS_WITH_SYS_METRICS) + lws_dll2_owner_t owner_mtr_dynpol; + /**< owner for lws_metric_policy_dyn_t (dynamic part of metric pols) */ + lws_dll2_owner_t owner_mtr_no_pol; + /**< owner for lws_metric_pub_t with no policy to bind to */ +#endif +#if defined(LWS_WITH_NETWORK) /* * LWS_WITH_NETWORK =====> */ lws_dll2_owner_t owner_vh_being_destroyed; + lws_metric_t *mt_service; /* doing service */ + const lws_metric_policy_t *metrics_policies; + const char *metrics_prefix; + +#if defined(LWS_WITH_SYS_METRICS) && defined(LWS_WITH_CLIENT) + lws_metric_t *mt_conn_tcp; /* client tcp conns */ + lws_metric_t *mt_conn_tls; /* client tcp conns */ + lws_metric_t *mt_conn_dns; /* client dns external lookups */ + lws_metric_t *mth_conn_failures; /* histogram of conn failure reasons */ +#if defined(LWS_ROLE_H1) || defined(LWS_ROLE_H2) + lws_metric_t *mt_http_txn; /* client http transaction */ +#endif +#if defined(LWS_WITH_SYS_ASYNC_DNS) + lws_metric_t *mt_adns_cache; /* async dns lookup lat */ +#endif +#if defined(LWS_WITH_SECURE_STREAMS) + lws_metric_t *mth_ss_conn; /* SS connection outcomes */ +#endif +#if defined(LWS_WITH_SECURE_STREAMS_PROXY_API) + lws_metric_t *mt_ss_cliprox_conn; /* SS cli->prox conn */ + lws_metric_t *mt_ss_cliprox_paylat; /* cli->prox payload latency */ + lws_metric_t *mt_ss_proxcli_paylat; /* prox->cli payload latency */ +#endif +#endif /* client */ + +#if defined(LWS_WITH_SERVER) + lws_metric_t *mth_srv; +#endif + #if defined(LWS_WITH_EVENT_LIBS) struct lws_plugin *evlib_plugin_list; void *evlib_ctx; /* overallocated */ @@ -498,9 +572,6 @@ struct lws_context { const struct lws_tls_ops *tls_ops; #endif -#if defined(LWS_WITH_DETAILED_LATENCY) - det_lat_buf_cb_t detailed_latency_cb; -#endif #if defined(LWS_WITH_PLUGINS) struct lws_plugin *plugin_list; #endif @@ -531,9 +602,6 @@ struct lws_context { #if !defined(LWS_PLAT_FREERTOS) const char *username, *groupname; #endif -#if defined(LWS_WITH_DETAILED_LATENCY) - const char *detailed_latency_filepath; -#endif #if defined(LWS_AMAZON_RTOS) && defined(LWS_WITH_MBEDTLS) mbedtls_entropy_context mec; @@ -589,6 +657,9 @@ struct lws_context { uint64_t options; time_t last_ws_ping_pong_check_s; +#if defined(LWS_WITH_SECURE_STREAMS) + time_t last_policy; +#endif #if defined(LWS_PLAT_FREERTOS) unsigned long time_last_state_dump; @@ -607,9 +678,6 @@ struct lws_context { int count_cgi_spawned; #endif -#if defined(LWS_WITH_DETAILED_LATENCY) - int latencies_fd; -#endif unsigned int fd_limit_per_thread; unsigned int timeout_secs; unsigned int pt_serv_buf_size; @@ -667,10 +735,6 @@ struct lws_context { uint8_t captive_portal_detect_type; uint8_t destroy_state; /* enum lws_context_destroy */ - -#if defined(LWS_WITH_STATS) - uint8_t updated; -#endif }; #define lws_get_context_protocol(ctx, x) ctx->vhost_list->protocols[x] diff --git a/lib/plat/freertos/freertos-service.c b/lib/plat/freertos/freertos-service.c index 1d79301b4..1bea562a9 100644 --- a/lib/plat/freertos/freertos-service.c +++ b/lib/plat/freertos/freertos-service.c @@ -50,7 +50,6 @@ _lws_plat_service_tsi(struct lws_context *context, int timeout_ms, int tsi) return 1; pt = &context->pt[tsi]; - lws_stats_bump(pt, LWSSTATS_C_SERVICE_ENTRY, 1); { unsigned long m = lws_now_secs(); @@ -142,15 +141,6 @@ again: n = select(max_fd + 1, &readfds, &writefds, &errfds, ptv); n = 0; - #if defined(LWS_WITH_DETAILED_LATENCY) - /* - * so we can track how long it took before we actually read a POLLIN - * that was signalled when we last exited poll() - */ - if (context->detailed_latency_cb) - pt->ust_left_poll = lws_now_usecs(); - #endif - for (m = 0; m < (int)pt->fds_count; m++) { c = 0; if (FD_ISSET(pt->fds[m].fd, &readfds)) { diff --git a/lib/plat/unix/unix-service.c b/lib/plat/unix/unix-service.c index c11bbad80..49d06920b 100644 --- a/lib/plat/unix/unix-service.c +++ b/lib/plat/unix/unix-service.c @@ -71,6 +71,9 @@ _lws_plat_service_tsi(struct lws_context *context, int timeout_ms, int tsi) volatile struct lws_context_per_thread *vpt; struct lws_context_per_thread *pt; lws_usec_t timeout_us, us; +#if defined(LWS_WITH_SYS_METRICS) + lws_usec_t a, b; +#endif int n; #if (defined(LWS_ROLE_WS) && !defined(LWS_WITHOUT_EXTENSIONS)) || defined(LWS_WITH_TLS) int m; @@ -81,11 +84,14 @@ _lws_plat_service_tsi(struct lws_context *context, int timeout_ms, int tsi) if (!context) return 1; +#if defined(LWS_WITH_SYS_METRICS) + b = +#endif + us = lws_now_usecs(); + pt = &context->pt[tsi]; vpt = (volatile struct lws_context_per_thread *)pt; - lws_stats_bump(pt, LWSSTATS_C_SERVICE_ENTRY, 1); - if (timeout_ms < 0) timeout_ms = 0; else @@ -108,7 +114,6 @@ _lws_plat_service_tsi(struct lws_context *context, int timeout_ms, int tsi) pt->service_tid_detected = 1; } - us = lws_now_usecs(); lws_pt_lock(pt, __func__); /* * service ripe scheduled events, and limit wait to next expected one @@ -144,15 +149,9 @@ _lws_plat_service_tsi(struct lws_context *context, int timeout_ms, int tsi) vpt->inside_poll = 0; lws_memory_barrier(); - #if defined(LWS_WITH_DETAILED_LATENCY) - /* - * so we can track how long it took before we actually read a - * POLLIN that was signalled when we last exited poll() - */ - if (context->detailed_latency_cb) - pt->ust_left_poll = lws_now_usecs(); +#if defined(LWS_WITH_SYS_METRICS) + b = lws_now_usecs(); #endif - /* Collision will be rare and brief. Spin until it completes */ while (vpt->foreign_spinlock) ; @@ -207,14 +206,16 @@ _lws_plat_service_tsi(struct lws_context *context, int timeout_ms, int tsi) #if (defined(LWS_ROLE_WS) && !defined(LWS_WITHOUT_EXTENSIONS)) || defined(LWS_WITH_TLS) !m && #endif - !n) { /* nothing to do */ + !n) /* nothing to do */ lws_service_do_ripe_rxflow(pt); + else + if (_lws_plat_service_forced_tsi(context, tsi) < 0) + return -1; - return 0; - } - - if (_lws_plat_service_forced_tsi(context, tsi) < 0) - return -1; +#if defined(LWS_WITH_SYS_METRICS) + lws_metric_event(context->mt_service, METRES_GO, + (u_mt_t) (a + (lws_now_usecs() - b))); +#endif if (pt->destroy_self) { lws_context_destroy(pt->context); diff --git a/lib/roles/h1/ops-h1.c b/lib/roles/h1/ops-h1.c index ea69163f4..a6e53e40c 100644 --- a/lib/roles/h1/ops-h1.c +++ b/lib/roles/h1/ops-h1.c @@ -521,19 +521,6 @@ try_pollout: return LWS_HPI_RET_HANDLED; } - lws_stats_bump(pt, LWSSTATS_C_WRITEABLE_CB, 1); -#if defined(LWS_WITH_STATS) - if (wsi->active_writable_req_us) { - uint64_t ul = lws_now_usecs() - - wsi->active_writable_req_us; - - lws_stats_bump(pt, LWSSTATS_US_WRITABLE_DELAY_AVG, ul); - lws_stats_max(pt, - LWSSTATS_US_WORST_WRITABLE_DELAY, ul); - wsi->active_writable_req_us = 0; - } -#endif - n = user_callback_handle_rxflow(wsi->a.protocol->callback, wsi, LWS_CALLBACK_HTTP_WRITEABLE, wsi->user_space, NULL, 0); @@ -934,6 +921,7 @@ rops_adoption_bind_h1(struct lws *wsi, int type, const char *vh_prot_name) #if defined(LWS_WITH_HTTP2) if ((!(type & LWS_ADOPT_ALLOW_SSL)) && (wsi->a.vhost->options & LWS_SERVER_OPTION_H2_PRIOR_KNOWLEDGE)) { lwsl_info("http/2 prior knowledge\n"); + lws_metrics_tag_wsi_add(wsi, "upg", "h2_prior"); lws_role_call_alpn_negotiated(wsi, "h2"); } else diff --git a/lib/roles/h2/hpack.c b/lib/roles/h2/hpack.c index 326f9dd05..68629e6f4 100644 --- a/lib/roles/h2/hpack.c +++ b/lib/roles/h2/hpack.c @@ -1422,7 +1422,7 @@ int lws_add_http2_header_by_name(struct lws *wsi, const unsigned char *name, *((*p)++) = 0; /* literal hdr, literal name, */ - *((*p)++) = 0 | (uint8_t)lws_h2_num_start(7, (unsigned long)len); /* non-HUF */ + *((*p)++) = (uint8_t)(0 | (uint8_t)lws_h2_num_start(7, (unsigned long)len)); /* non-HUF */ if (lws_h2_num(7, (unsigned long)len, p, end)) return 1; @@ -1432,7 +1432,7 @@ int lws_add_http2_header_by_name(struct lws *wsi, const unsigned char *name, while(len--) *((*p)++) = (uint8_t)tolower((int)*name++); - *((*p)++) = 0 | (uint8_t)lws_h2_num_start(7, (unsigned long)length); /* non-HUF */ + *((*p)++) = (uint8_t)(0 | (uint8_t)lws_h2_num_start(7, (unsigned long)length)); /* non-HUF */ if (lws_h2_num(7, (unsigned long)length, p, end)) return 1; diff --git a/lib/roles/h2/http2.c b/lib/roles/h2/http2.c index 07d0ef6fe..ba836e958 100644 --- a/lib/roles/h2/http2.c +++ b/lib/roles/h2/http2.c @@ -261,6 +261,11 @@ lws_wsi_server_new(struct lws_vhost *vh, struct lws *parent_wsi, return NULL; } +#if defined(LWS_WITH_SERVER) + if (lwsi_role_server(parent_wsi)) + lws_metrics_caliper_bind(wsi->cal_conn, wsi->a.context->mth_srv); +#endif + h2n->highest_sid_opened = sid; lws_wsi_mux_insert(wsi, parent_wsi, sid); @@ -281,10 +286,6 @@ lws_wsi_server_new(struct lws_vhost *vh, struct lws *parent_wsi, if (lws_ensure_user_space(wsi)) goto bail1; -#if defined(LWS_WITH_SERVER_STATUS) - wsi->a.vhost->conn_stats.h2_subs++; -#endif - #if defined(LWS_WITH_SERVER) && defined(LWS_WITH_SECURE_STREAMS) if (lws_adopt_ss_server_accept(wsi)) goto bail1; @@ -362,10 +363,6 @@ lws_wsi_h2_adopt(struct lws *parent_wsi, struct lws *wsi) lws_callback_on_writable(wsi); -#if defined(LWS_WITH_SERVER_STATUS) - wsi->a.vhost->conn_stats.h2_subs++; -#endif - return wsi; bail1: @@ -818,9 +815,6 @@ int lws_h2_do_pps_send(struct lws *wsi) h2n->swsi->h2.END_STREAM = 1; lwsl_info("servicing initial http request\n"); -#if defined(LWS_WITH_SERVER_STATUS) - wsi->a.vhost->conn_stats.h2_trans++; -#endif #if defined(LWS_WITH_SERVER) if (lws_http_action(h2n->swsi)) goto bail; @@ -1713,9 +1707,6 @@ lws_h2_parse_end_of_frame(struct lws *wsi) lws_http_compression_validate(h2n->swsi); #endif -#if defined(LWS_WITH_SERVER_STATUS) - wsi->a.vhost->conn_stats.h2_trans++; -#endif p = lws_hdr_simple_ptr(h2n->swsi, WSI_TOKEN_HTTP_COLON_METHOD); /* * duplicate :path into the individual method uri header @@ -2532,10 +2523,6 @@ lws_h2_client_handshake(struct lws *wsi) if (lws_finalize_http_header(wsi, &p, end)) goto fail_length; -#if defined(LWS_WITH_DETAILED_LATENCY) - wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); -#endif - m = LWS_WRITE_HTTP_HEADERS; #if defined(LWS_WITH_CLIENT) /* below is not needed in spec, indeed it destroys the long poll diff --git a/lib/roles/h2/ops-h2.c b/lib/roles/h2/ops-h2.c index d5a2eba3a..f46cfcca0 100644 --- a/lib/roles/h2/ops-h2.c +++ b/lib/roles/h2/ops-h2.c @@ -521,13 +521,12 @@ rops_check_upgrades_h2(struct lws *wsi) if (!p || strcmp(p, "websocket")) return LWS_UPG_RET_CONTINUE; -#if defined(LWS_WITH_SERVER_STATUS) - wsi->a.vhost->conn_stats.ws_upg++; -#endif lwsl_info("Upgrade h2 to ws\n"); lws_mux_mark_immortal(wsi); wsi->h2_stream_carries_ws = 1; + lws_metrics_tag_wsi_add(wsi, "upg", "ws_over_h2"); + if (lws_process_ws_upgrade(wsi)) return LWS_UPG_RET_BAIL; @@ -1254,9 +1253,6 @@ rops_alpn_negotiated_h2(struct lws *wsi, const char *alpn) #endif wsi->upgraded_to_http2 = 1; -#if defined(LWS_WITH_SERVER_STATUS) - wsi->a.vhost->conn_stats.h2_alpn++; -#endif /* adopt the header info */ diff --git a/lib/roles/http/client/client-http.c b/lib/roles/http/client/client-http.c index dfb2105f1..1e6da7962 100644 --- a/lib/roles/http/client/client-http.c +++ b/lib/roles/http/client/client-http.c @@ -67,7 +67,10 @@ lws_http_client_socket_service(struct lws *wsi, struct lws_pollfd *pollfd) * timeout protection set in client-handshake.c */ if (pollfd->revents & LWS_POLLOUT) - lws_client_connect_3_connect(wsi, NULL, NULL, 0, NULL); + if (lws_client_connect_3_connect(wsi, NULL, NULL, 0, NULL) == NULL) { + lwsl_client("closed\n"); + return -1; + } break; #if defined(LWS_WITH_SOCKS5) @@ -203,16 +206,6 @@ start_ws_handshake: } } #endif -#if defined(LWS_WITH_DETAILED_LATENCY) - if (context->detailed_latency_cb) { - wsi->detlat.type = LDLT_TLS_NEG_CLIENT; - wsi->detlat.latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] = - (uint32_t)(lws_now_usecs() - - wsi->detlat.earliest_write_req_pre_write); - wsi->detlat.latencies[LAT_DUR_USERCB] = 0; - lws_det_lat_cb(wsi->a.context, &wsi->detlat); - } -#endif #if defined (LWS_WITH_HTTP2) if (wsi->client_h2_alpn && lwsi_state(wsi) != LRS_H1C_ISSUE_HANDSHAKE2) { @@ -271,9 +264,7 @@ hs2: "(wsistate 0x%lx), w sock %d\n", __func__, lws_wsi_tag(wsi), (unsigned long)wsi->wsistate, wsi->desc.sockfd); -#if defined(LWS_WITH_DETAILED_LATENCY) - wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); -#endif + n = lws_ssl_capable_write(wsi, (unsigned char *)sb, lws_ptr_diff_size_t(p, sb)); switch (n) { case LWS_SSL_CAPABLE_ERROR: @@ -485,6 +476,10 @@ lws_http_transaction_completed_client(struct lws *wsi) lwsl_info("%s: %s (%s)\n", __func__, lws_wsi_tag(wsi), wsi->a.protocol->name); + // if (wsi->http.ah && wsi->http.ah->http_response) + /* we're only judging if any (200, or 500 etc) http txn completed */ + lws_metrics_caliper_report(wsi->cal_conn, METRES_GO); + if (user_callback_handle_rxflow(wsi->a.protocol->callback, wsi, LWS_CALLBACK_COMPLETED_CLIENT_HTTP, wsi->user_space, NULL, 0)) { @@ -1210,6 +1205,8 @@ lws_generate_client_handshake(struct lws *wsi, char *pkt) if (wsi->client_http_body_pending) lws_callback_on_writable(wsi); + lws_metrics_caliper_bind(wsi->cal_conn, wsi->a.context->mt_http_txn); + // puts(pkt); return p; diff --git a/lib/roles/http/header.c b/lib/roles/http/header.c index eb84d7cf1..83983a573 100644 --- a/lib/roles/http/header.c +++ b/lib/roles/http/header.c @@ -125,9 +125,6 @@ lws_finalize_write_http_header(struct lws *wsi, unsigned char *start, p = *pp; len = lws_ptr_diff(p, start); -#if defined(LWS_WITH_DETAILED_LATENCY) - wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); -#endif if (lws_write(wsi, start, (unsigned int)len, LWS_WRITE_HTTP_HEADERS) != len) return 1; @@ -316,6 +313,7 @@ lws_add_http_header_status(struct lws *wsi, unsigned int _code, unsigned char code_and_desc[60]; int n; + wsi->http.response_code = code; #ifdef LWS_WITH_ACCESS_LOG wsi->http.access_log.response = (int)code; #endif @@ -482,9 +480,6 @@ lws_return_http_status(struct lws *wsi, unsigned int code, * * Solve it by writing the headers now... */ -#if defined(LWS_WITH_DETAILED_LATENCY) - wsi->detlat.earliest_write_req_pre_write = lws_now_usecs(); -#endif m = lws_write(wsi, start, lws_ptr_diff_size_t(p, start), LWS_WRITE_HTTP_HEADERS); if (m != lws_ptr_diff(p, start)) diff --git a/lib/roles/http/parsers.c b/lib/roles/http/parsers.c index 778d96a58..2999faa2b 100644 --- a/lib/roles/http/parsers.c +++ b/lib/roles/http/parsers.c @@ -236,11 +236,8 @@ lws_header_table_attach(struct lws *wsi, int autoservice) n = pt->http.ah_count_in_use == (int)context->max_http_header_pool; #if defined(LWS_WITH_PEER_LIMITS) - if (!n) { + if (!n) n = lws_peer_confirm_ah_attach_ok(context, wsi->peer); - if (n) - lws_stats_bump(pt, LWSSTATS_C_PEER_LIMIT_AH_DENIED, 1); - } #endif if (n) { /* @@ -375,12 +372,7 @@ int __lws_header_table_detach(struct lws *wsi, int autoservice) wsi = *pwsi; pwsi_eligible = pwsi; } -#if defined(LWS_WITH_PEER_LIMITS) - else - if (!(*pwsi)->http.ah_wait_list) - lws_stats_bump(pt, - LWSSTATS_C_PEER_LIMIT_AH_DENIED, 1); -#endif + pwsi = &(*pwsi)->http.ah_wait_list; } diff --git a/lib/roles/http/private-lib-roles-http.h b/lib/roles/http/private-lib-roles-http.h index d8d9fc17e..94ee87689 100644 --- a/lib/roles/http/private-lib-roles-http.h +++ b/lib/roles/http/private-lib-roles-http.h @@ -248,6 +248,9 @@ struct _lws_http_mode_related { #ifdef LWS_WITH_ACCESS_LOG struct lws_access_log access_log; #endif +#if defined(LWS_WITH_SERVER) + unsigned int response_code; +#endif #ifdef LWS_WITH_CGI struct lws_cgi *cgi; /* wsi being cgi stream have one of these */ #endif diff --git a/lib/roles/http/server/server.c b/lib/roles/http/server/server.c index c93d43c49..c6b4bf632 100644 --- a/lib/roles/http/server/server.c +++ b/lib/roles/http/server/server.c @@ -797,6 +797,10 @@ lws_find_mount(struct lws *wsi, const char *uri_ptr, int uri_len) uri_ptr[hm->mountpoint_len] == '/' || hm->mountpoint_len == 1) ) { +#if defined(LWS_WITH_SYS_METRICS) + lws_metrics_tag_wsi_add(wsi, "mnt", hm->mountpoint); +#endif + if (hm->origin_protocol == LWSMPRO_CALLBACK || ((hm->origin_protocol == LWSMPRO_CGI || lws_hdr_total_length(wsi, WSI_TOKEN_GET_URI) || @@ -1416,6 +1420,9 @@ lws_http_action(struct lws *wsi) if (meth < 0 || meth >= (int)LWS_ARRAY_SIZE(method_names)) goto bail_nuke_ah; + lws_metrics_tag_wsi_add(wsi, "vh", wsi->a.vhost->name); + lws_metrics_tag_wsi_add(wsi, "meth", method_names[meth]); + /* we insist on absolute paths */ if (!uri_ptr || uri_ptr[0] != '/') { @@ -2076,17 +2083,9 @@ raw_transition: } else lwsl_info("no host\n"); - if (!lwsi_role_h2(wsi) || !lwsi_role_server(wsi)) { -#if defined(LWS_WITH_SERVER_STATUS) - wsi->a.vhost->conn_stats.h1_trans++; -#endif - if (!wsi->conn_stat_done) { -#if defined(LWS_WITH_SERVER_STATUS) - wsi->a.vhost->conn_stats.h1_conn++; -#endif - wsi->conn_stat_done = 1; - } - } + if ((!lwsi_role_h2(wsi) || !lwsi_role_server(wsi)) && + (!wsi->conn_stat_done)) + wsi->conn_stat_done = 1; /* check for unwelcome guests */ #if defined(LWS_WITH_HTTP_UNCOMMON_HEADERS) @@ -2121,9 +2120,6 @@ raw_transition: uri_ptr, uri_len, meth); /* wsi close will do the log */ -#endif -#if defined(LWS_WITH_SERVER_STATUS) - wsi->a.vhost->conn_stats.rejected++; #endif /* * We don't want anything from @@ -2215,18 +2211,14 @@ raw_transition: if (!strcasecmp(up, "websocket")) { #if defined(LWS_ROLE_WS) -#if defined(LWS_WITH_SERVER_STATUS) - wsi->a.vhost->conn_stats.ws_upg++; -#endif + lws_metrics_tag_wsi_add(wsi, "upg", "ws"); lwsl_info("Upgrade to ws\n"); goto upgrade_ws; #endif } #if defined(LWS_WITH_HTTP2) if (!strcasecmp(up, "h2c")) { -#if defined(LWS_WITH_SERVER_STATUS) - wsi->a.vhost->conn_stats.h2_upg++; -#endif + lws_metrics_tag_wsi_add(wsi, "upg", "h2c"); lwsl_info("Upgrade to h2c\n"); goto upgrade_h2c; } @@ -2379,6 +2371,15 @@ lws_http_transaction_completed(struct lws *wsi) return 0; } +#if defined(LWS_WITH_SYS_METRICS) + { + char tmp[10]; + + lws_snprintf(tmp, sizeof(tmp), "%u", wsi->http.response_code); + lws_metrics_tag_wsi_add(wsi, "status", tmp); + } +#endif + lwsl_info("%s: %s\n", __func__, lws_wsi_tag(wsi)); #if defined(LWS_WITH_HTTP_STREAM_COMPRESSION) diff --git a/lib/roles/mqtt/client/client-mqtt.c b/lib/roles/mqtt/client/client-mqtt.c index 8e87218f8..93b539eb3 100644 --- a/lib/roles/mqtt/client/client-mqtt.c +++ b/lib/roles/mqtt/client/client-mqtt.c @@ -264,17 +264,6 @@ lws_mqtt_client_socket_service(struct lws *wsi, struct lws_pollfd *pollfd, wsi->tls.ssl = NULL; #endif /* LWS_WITH_TLS */ -#if defined(LWS_WITH_DETAILED_LATENCY) - if (context->detailed_latency_cb) { - wsi->detlat.type = LDLT_TLS_NEG_CLIENT; - wsi->detlat.latencies[LAT_DUR_PROXY_CLIENT_REQ_TO_WRITE] = - (uint32_t)(lws_now_usecs() - - wsi->detlat.earliest_write_req_pre_write); - wsi->detlat.latencies[LAT_DUR_USERCB] = 0; - lws_det_lat_cb(wsi->a.context, &wsi->detlat); - } -#endif - /* fallthru */ #if defined(LWS_WITH_SOCKS5) diff --git a/lib/roles/mqtt/mqtt.c b/lib/roles/mqtt/mqtt.c index b47bbe93e..02a648263 100644 --- a/lib/roles/mqtt/mqtt.c +++ b/lib/roles/mqtt/mqtt.c @@ -992,7 +992,7 @@ cmd_completion: lws_set_timeout(wsi, 0, 0); w = lws_create_new_server_wsi(wsi->a.vhost, - wsi->tsi, "mqtt"); + wsi->tsi, "mqtt_sid1"); if (!w) { lwsl_notice("%s: sid 1 migrate failed\n", __func__); @@ -1042,10 +1042,6 @@ cmd_completion: __func__, lws_wsi_tag(wsi), lws_wsi_tag(w)); - #if defined(LWS_WITH_SERVER_STATUS) - wsi->a.vhost->conn_stats.h2_subs++; - #endif - /* * It was the last thing we were waiting for * before we can be fully ESTABLISHED @@ -2107,10 +2103,6 @@ lws_wsi_mqtt_adopt(struct lws *parent_wsi, struct lws *wsi) lws_mqtt_set_client_established(wsi); lws_callback_on_writable(wsi); -#if defined(LWS_WITH_SERVER_STATUS) - wsi->a.vhost->conn_stats.mqtt_subs++; -#endif - return wsi; bail1: diff --git a/lib/roles/raw-skt/ops-raw-skt.c b/lib/roles/raw-skt/ops-raw-skt.c index 229f5af7b..cbe927cf4 100644 --- a/lib/roles/raw-skt/ops-raw-skt.c +++ b/lib/roles/raw-skt/ops-raw-skt.c @@ -120,6 +120,8 @@ rops_handle_POLLIN_raw_skt(struct lws_context_per_thread *pt, struct lws *wsi, buffered = lws_buflist_aware_read(pt, wsi, &ebuf, 1, __func__); switch (ebuf.len) { case 0: + if (wsi->unix_skt) + break; lwsl_info("%s: read 0 len\n", __func__); wsi->seen_zero_length_recv = 1; if (lws_change_pollfd(wsi, LWS_POLLIN, 0)) @@ -202,18 +204,6 @@ try_pollout: /* clear back-to-back write detection */ wsi->could_have_pending = 0; - lws_stats_bump(pt, LWSSTATS_C_WRITEABLE_CB, 1); -#if defined(LWS_WITH_STATS) - if (wsi->active_writable_req_us) { - uint64_t ul = lws_now_usecs() - - wsi->active_writable_req_us; - - lws_stats_bump(pt, LWSSTATS_US_WRITABLE_DELAY_AVG, ul); - lws_stats_max(pt, - LWSSTATS_US_WORST_WRITABLE_DELAY, ul); - wsi->active_writable_req_us = 0; - } -#endif n = user_callback_handle_rxflow(wsi->a.protocol->callback, wsi, LWS_CALLBACK_RAW_WRITEABLE, wsi->user_space, NULL, 0); diff --git a/lib/roles/ws/client-ws.c b/lib/roles/ws/client-ws.c index af733cc7c..7a795b615 100644 --- a/lib/roles/ws/client-ws.c +++ b/lib/roles/ws/client-ws.c @@ -251,11 +251,6 @@ lws_client_ws_upgrade(struct lws *wsi, const char **cce) char ignore; #endif -#if defined(LWS_WITH_DETAILED_LATENCY) - wsi->detlat.earliest_write_req = 0; - wsi->detlat.earliest_write_req_pre_write = 0; -#endif - if (wsi->client_mux_substream) {/* !!! client ws-over-h2 not there yet */ lwsl_warn("%s: client ws-over-h2 upgrade not supported yet\n", __func__); diff --git a/lib/secure-streams/policy-common.c b/lib/secure-streams/policy-common.c index 14de047ce..18c05ddab 100644 --- a/lib/secure-streams/policy-common.c +++ b/lib/secure-streams/policy-common.c @@ -297,6 +297,17 @@ lws_ss_policy_set(struct lws_context *context, const char *name) if (context->ac_policy) { int n; +#if defined(LWS_WITH_SYS_METRICS) + lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, + context->owner_mtr_dynpol.head) { + lws_metric_policy_dyn_t *dm = + lws_container_of(d, lws_metric_policy_dyn_t, list); + + lws_metric_policy_dyn_destroy(dm, 1); /* keep */ + + } lws_end_foreach_dll_safe(d, d1); +#endif + /* * any existing ss created with the old policy have to go away * now, since they point to the shortly-to-be-destroyed old @@ -431,10 +442,27 @@ lws_ss_policy_set(struct lws_context *context, const char *name) x = x->next; } + context->last_policy = time(NULL); +#if defined(LWS_WITH_SYS_METRICS) + if (context->pss_policies) + ((lws_ss_policy_t *)context->pss_policies)->metrics = + args->heads[LTY_METRICS].m; +#endif + /* and we can discard the parsing args object now, invalidating args */ lws_free_set_NULL(context->pol_args); #endif +#if defined(LWS_WITH_SYS_METRICS) + lws_metric_rebind_policies(context); +#endif + +#if defined(LWS_WITH_SYS_SMD) + (void)lws_smd_msg_printf(context, LWSSMDCL_SYSTEM_STATE, + "{\"policy\":\"updated\",\"ts\":%lu}", + (long)context->last_policy); +#endif + return ret; } diff --git a/lib/secure-streams/policy-json.c b/lib/secure-streams/policy-json.c index d4e3bb31e..3876ae2c9 100644 --- a/lib/secure-streams/policy-json.c +++ b/lib/secure-streams/policy-json.c @@ -41,6 +41,11 @@ static const char * const lejp_tokens_policy[] = { "certs[].*", "trust_stores[].name", "trust_stores[].stack", + "metrics[].name", + "metrics[].us_schedule", + "metrics[].us_halflife", + "metrics[].min_outlier", + "metrics[].report", "s[].*.endpoint", "s[].*.via-socks5", "s[].*.protocol", @@ -135,6 +140,11 @@ typedef enum { LSSPPT_CERTS, LSSPPT_TRUST_STORES_NAME, LSSPPT_TRUST_STORES_STACK, + LSSPPT_METRICS_NAME, + LSSPPT_METRICS_US_SCHEDULE, + LSSPPT_METRICS_US_HALFLIFE, + LSSPPT_METRICS_MIN_OUTLIER, + LSSPPT_METRICS_REPORT, LSSPPT_ENDPOINT, LSSPPT_VH_VIA_SOCKS5, LSSPPT_PROTOCOL, @@ -225,6 +235,7 @@ static uint16_t sizes[] = { sizeof(lws_ss_trust_store_t), sizeof(lws_ss_policy_t), sizeof(lws_ss_auth_t), + sizeof(lws_metric_policy_t), }; static const char * const protonames[] = { @@ -253,6 +264,25 @@ lws_ss_policy_find_auth_by_name(struct policy_cb_args *a, return NULL; } +static int +lws_ss_policy_alloc_helper(struct policy_cb_args *a, int type) +{ + /* + * We do the pointers always as .b union member, all of the + * participating structs begin with .next and .name the same + */ + + a->curr[type].b = lwsac_use_zero(&a->ac, + sizes[type], POL_AC_GRAIN); + if (!a->curr[type].b) + return 1; + + a->curr[type].b->next = a->heads[type].b; + a->heads[type].b = a->curr[type].b; + + return 0; +} + static signed char lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason) { @@ -291,6 +321,13 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason) case LSSPPT_AUTH: n = LTY_AUTH; break; + case LSSPPT_METRICS_NAME: + case LSSPPT_METRICS_US_SCHEDULE: + case LSSPPT_METRICS_US_HALFLIFE: + case LSSPPT_METRICS_MIN_OUTLIER: + case LSSPPT_METRICS_REPORT: + n = LTY_METRICS; + break; } if (reason == LEJPCB_ARRAY_START && @@ -300,13 +337,8 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason) a->count = 0; if (reason == LEJPCB_OBJECT_START && n == LTY_AUTH) { - a->curr[n].b = lwsac_use_zero(&a->ac, sizes[n], POL_AC_GRAIN); - if (!a->curr[n].b) + if (lws_ss_policy_alloc_helper(a, LTY_AUTH)) goto oom; - - a->curr[n].b->next = a->heads[n].b; - a->heads[n].b = a->curr[n].b; - return 0; } @@ -360,7 +392,7 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason) } if (reason == LEJPCB_PAIR_NAME && n != -1 && - (n != LTY_TRUSTSTORE && n != LTY_AUTH)) { + (n != LTY_TRUSTSTORE && n != LTY_AUTH && n != LTY_METRICS)) { p2 = NULL; if (n == LTY_POLICY) { @@ -490,18 +522,10 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason) break; case LSSPPT_TRUST_STORES_NAME: - /* - * We do the pointers always as .b, all of the participating - * structs begin with .next and .name - */ - a->curr[LTY_TRUSTSTORE].b = lwsac_use_zero(&a->ac, - sizes[LTY_TRUSTSTORE], POL_AC_GRAIN); - if (!a->curr[LTY_TRUSTSTORE].b) + if (lws_ss_policy_alloc_helper(a, LTY_TRUSTSTORE)) goto oom; a->count = 0; - a->curr[LTY_TRUSTSTORE].b->next = a->heads[LTY_TRUSTSTORE].b; - a->heads[LTY_TRUSTSTORE].b = a->curr[LTY_TRUSTSTORE].b; pp = (char **)&a->curr[LTY_TRUSTSTORE].b->name; goto string2; @@ -528,6 +552,31 @@ lws_ss_policy_parser_cb(struct lejp_ctx *ctx, char reason) lwsl_err("%s: unknown trust store entry %s\n", __func__, dotstar); goto oom; +#if defined(LWS_WITH_SYS_METRICS) + case LSSPPT_METRICS_NAME: + if (lws_ss_policy_alloc_helper(a, LTY_METRICS)) + goto oom; + + pp = (char **)&a->curr[LTY_METRICS].b->name; + + goto string2; + + case LSSPPT_METRICS_US_SCHEDULE: + a->curr[LTY_METRICS].m->us_schedule = (uint32_t)atol(ctx->buf); + break; + + case LSSPPT_METRICS_US_HALFLIFE: + a->curr[LTY_METRICS].m->us_decay_unit = (uint32_t)atol(ctx->buf); + break; + + case LSSPPT_METRICS_MIN_OUTLIER: + a->curr[LTY_METRICS].m->min_contributors = (uint8_t)atoi(ctx->buf); + break; + + case LSSPPT_METRICS_REPORT: + pp = (char **)&a->curr[LTY_METRICS].m->report; + goto string2; +#endif case LSSPPT_SERVER_CERT: case LSSPPT_SERVER_KEY: diff --git a/lib/secure-streams/private-lib-secure-streams.h b/lib/secure-streams/private-lib-secure-streams.h index ab0b661e4..b386776de 100644 --- a/lib/secure-streams/private-lib-secure-streams.h +++ b/lib/secure-streams/private-lib-secure-streams.h @@ -49,6 +49,10 @@ typedef struct lws_ss_handle { lws_lifecycle_t lc; +#if defined(LWS_WITH_SYS_METRICS) + lws_metrics_caliper_compose(cal_txn) +#endif + struct lws_dll2 list; /**< pt lists active ss */ struct lws_dll2 to_list; /**< pt lists ss with pending to-s */ #if defined(LWS_WITH_SERVER) @@ -290,6 +294,10 @@ typedef struct lws_sspc_handle { struct lws_dll2 client_list; struct lws_tx_credit txc; +#if defined(LWS_WITH_SYS_METRICS) + lws_metrics_caliper_compose(cal_txn) +#endif + struct lws *cwsi; struct lws_dsh *dsh; @@ -333,6 +341,7 @@ union u { lws_ss_trust_store_t *t; lws_ss_policy_t *p; lws_ss_auth_t *a; + lws_metric_policy_t *m; }; enum { @@ -341,6 +350,7 @@ enum { LTY_TRUSTSTORE, LTY_POLICY, LTY_AUTH, + LTY_METRICS, _LTY_COUNT /* always last */ }; @@ -510,6 +520,29 @@ struct ss_pcols { secstream_protocol_get_txcr_t tx_cr_est; }; +/* + * Because both sides of the connection share the conn, we allocate it + * during accepted adoption, and both sides point to it. + * + * When .ss or .wsi close, they must NULL their entry here so no dangling + * refereneces. + * + * The last one of the accepted side and the onward side to close frees it. + */ + + + + +struct conn { + struct lws_ss_serialization_parser parser; + + lws_dsh_t *dsh; /* unified buffer for both sides */ + struct lws *wsi; /* the proxy's client side */ + lws_ss_handle_t *ss; /* the onward, ss side */ + + lws_ss_conn_states_t state; +}; + extern const struct ss_pcols ss_pcol_h1; extern const struct ss_pcols ss_pcol_h2; extern const struct ss_pcols ss_pcol_ws; diff --git a/lib/secure-streams/protocols/ss-h1.c b/lib/secure-streams/protocols/ss-h1.c index 77c8971e3..0311dd885 100644 --- a/lib/secure-streams/protocols/ss-h1.c +++ b/lib/secure-streams/protocols/ss-h1.c @@ -415,6 +415,7 @@ secstream_h1(struct lws *wsi, enum lws_callback_reasons reason, void *user, break; } assert(h->policy); + lws_metrics_caliper_report_hist(h->cal_txn, wsi); lwsl_info("%s: %s CLIENT_CONNECTION_ERROR: %s\n", __func__, h->lc.gutag, in ? (const char *)in : "none"); /* already disconnected, no action for DISCONNECT_ME */ @@ -445,8 +446,11 @@ secstream_h1(struct lws *wsi, enum lws_callback_reasons reason, void *user, break; lws_sul_cancel(&h->sul_timeout); - lwsl_notice("%s: %s LWS_CALLBACK_CLOSED_CLIENT_HTTP\n", - __func__, wsi->lc.gutag); + + lws_metrics_caliper_report_hist(h->cal_txn, wsi); + //lwsl_notice("%s: %s LWS_CALLBACK_CLOSED_CLIENT_HTTP\n", + // __func__, wsi->lc.gutag); + h->wsi = NULL; #if defined(LWS_WITH_SERVER) @@ -487,6 +491,13 @@ secstream_h1(struct lws *wsi, enum lws_callback_reasons reason, void *user, /* it's just telling use we connected / joined the nwsi */ // break; +#if defined(LWS_WITH_SYS_METRICS) + if (status) { + lws_snprintf((char *)buf, 10, "%d", status); + lws_metrics_tag_ss_add(h, "http_resp", (char *)buf); + } +#endif + if (status == HTTP_STATUS_SERVICE_UNAVAILABLE /* 503 */ || status == 429 /* Too many requests */) { /* @@ -552,6 +563,7 @@ secstream_h1(struct lws *wsi, enum lws_callback_reasons reason, void *user, lws_sul_cancel(&h->sul); if (h->prev_ss_state != LWSSSCS_CONNECTED) { + lws_metrics_caliper_report_hist(h->cal_txn, wsi); r = lws_ss_event_helper(h, LWSSSCS_CONNECTED); if (r != LWSSSSRET_OK) return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h); @@ -722,6 +734,12 @@ malformed: h->being_serialized && ( !strcmp(h->policy->u.http.method, "PUT") || !strcmp(h->policy->u.http.method, "POST"))) { +#if defined(LWS_WITH_SYS_METRICS) + /* + * If any hanging caliper measurement, dump it, and free any tags + */ + lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL); +#endif r = lws_ss_event_helper(h, LWSSSCS_CONNECTED); if (r) return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h); @@ -773,7 +791,7 @@ malformed: return 0; /* don't passthru */ case LWS_CALLBACK_COMPLETED_CLIENT_HTTP: - lwsl_debug("%s: LWS_CALLBACK_COMPLETED_CLIENT_HTTP\n", __func__); + // lwsl_debug("%s: LWS_CALLBACK_COMPLETED_CLIENT_HTTP\n", __func__); if (!h) return -1; @@ -919,7 +937,7 @@ malformed: } #if defined(LWS_WITH_SERVER) - if (!(h->info.flags & LWSSSINFLAGS_ACCEPTED) && + if ((h->info.flags & LWSSSINFLAGS_ACCEPTED) /* server */ && (f & LWSSS_FLAG_EOM) && lws_http_transaction_completed(wsi)) return -1; @@ -973,6 +991,12 @@ malformed: } if (!h->ss_dangling_connected) { +#if defined(LWS_WITH_SYS_METRICS) + /* + * If any hanging caliper measurement, dump it, and free any tags + */ + lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL); +#endif r = lws_ss_event_helper(h, LWSSSCS_CONNECTED); if (r) return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h); diff --git a/lib/secure-streams/protocols/ss-mqtt.c b/lib/secure-streams/protocols/ss-mqtt.c index 1e1d1cdb4..0d719d568 100644 --- a/lib/secure-streams/protocols/ss-mqtt.c +++ b/lib/secure-streams/protocols/ss-mqtt.c @@ -94,6 +94,12 @@ secstream_mqtt(struct lws *wsi, enum lws_callback_reasons reason, void *user, h->retry = 0; h->seqstate = SSSEQ_CONNECTED; lws_sul_cancel(&h->sul); +#if defined(LWS_WITH_SYS_METRICS) + /* + * If any hanging caliper measurement, dump it, and free any tags + */ + lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL); +#endif r = lws_ss_event_helper(h, LWSSSCS_CONNECTED); if (r != LWSSSSRET_OK) return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h); diff --git a/lib/secure-streams/protocols/ss-raw.c b/lib/secure-streams/protocols/ss-raw.c index aa020a233..69d96bbf5 100644 --- a/lib/secure-streams/protocols/ss-raw.c +++ b/lib/secure-streams/protocols/ss-raw.c @@ -60,8 +60,7 @@ secstream_raw(struct lws *wsi, enum lws_callback_reasons reason, void *user, if (!h) break; lws_sul_cancel(&h->sul_timeout); - lwsl_info("%s: %s, %s LWS_CALLBACK_CLOSED_CLIENT_HTTP\n", - __func__, lws_ss_tag(h), + lwsl_info("%s: %s, %s RAW_CLOSE\n", __func__, lws_ss_tag(h), h->policy ? h->policy->streamtype : "no policy"); h->wsi = NULL; #if defined(LWS_WITH_SERVER) @@ -69,6 +68,12 @@ secstream_raw(struct lws *wsi, enum lws_callback_reasons reason, void *user, lws_dll2_remove(&h->cli_list); lws_pt_unlock(pt); #endif + + /* wsi is going down anyway */ + r = lws_ss_event_helper(h, LWSSSCS_DISCONNECTED); + if (r == LWSSSSRET_DESTROY_ME) + return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h); + if (h->policy && !(h->policy->flags & LWSSSPOLF_OPPORTUNISTIC) && #if defined(LWS_WITH_SERVER) !(h->info.flags & LWSSSINFLAGS_ACCEPTED) && /* not server */ @@ -79,10 +84,7 @@ secstream_raw(struct lws *wsi, enum lws_callback_reasons reason, void *user, return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h); break; } - /* wsi is going down anyway */ - r = lws_ss_event_helper(h, LWSSSCS_DISCONNECTED); - if (r == LWSSSSRET_DESTROY_ME) - return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h); + break; case LWS_CALLBACK_RAW_CONNECTED: @@ -91,6 +93,12 @@ secstream_raw(struct lws *wsi, enum lws_callback_reasons reason, void *user, h->retry = 0; h->seqstate = SSSEQ_CONNECTED; lws_sul_cancel(&h->sul); +#if defined(LWS_WITH_SYS_METRICS) + /* + * If any hanging caliper measurement, dump it, and free any tags + */ + lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL); +#endif r = lws_ss_event_helper(h, LWSSSCS_CONNECTED); if (r != LWSSSSRET_OK) return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h); diff --git a/lib/secure-streams/protocols/ss-ws.c b/lib/secure-streams/protocols/ss-ws.c index fb25a28a0..ad24c40c6 100644 --- a/lib/secure-streams/protocols/ss-ws.c +++ b/lib/secure-streams/protocols/ss-ws.c @@ -106,6 +106,12 @@ secstream_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, h->retry = 0; h->seqstate = SSSEQ_CONNECTED; lws_sul_cancel(&h->sul); +#if defined(LWS_WITH_SYS_METRICS) + /* + * If any hanging caliper measurement, dump it, and free any tags + */ + lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL); +#endif r = lws_ss_event_helper(h, LWSSSCS_CONNECTED); if (r != LWSSSSRET_OK) return _lws_ss_handle_state_ret_CAN_DESTROY_HANDLE(r, wsi, &h); diff --git a/lib/secure-streams/secure-streams-client.c b/lib/secure-streams/secure-streams-client.c index e0477553e..21bcade3c 100644 --- a/lib/secure-streams/secure-streams-client.c +++ b/lib/secure-streams/secure-streams-client.c @@ -71,9 +71,23 @@ lws_sspc_sul_retry_cb(lws_sorted_usec_list_t *sul) i.pwsi = &h->cwsi; i.opaque_user_data = (void *)h; i.ssl_connection = LCCSCF_SECSTREAM_PROXY_LINK; + + lws_metrics_caliper_bind(h->cal_txn, h->context->mt_ss_cliprox_conn); +#if defined(LWS_WITH_SYS_METRICS) + lws_metrics_tag_add(&h->cal_txn.mtags_owner, "ss", h->ssi.streamtype); +#endif + /* this wsi is the link to the proxy */ if (!lws_client_connect_via_info(&i)) { + +#if defined(LWS_WITH_SYS_METRICS) + /* + * If any hanging caliper measurement, dump it, and free any tags + */ + lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL); +#endif + lws_sul_schedule(h->context, 0, &h->sul_retry, lws_sspc_sul_retry_cb, LWS_US_PER_SEC); @@ -147,6 +161,12 @@ callback_sspc_client(struct lws *wsi, enum lws_callback_reasons reason, case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: lwsl_warn("%s: CONNECTION_ERROR\n", __func__); +#if defined(LWS_WITH_SYS_METRICS) + /* + * If any hanging caliper measurement, dump it, and free any tags + */ + lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL); +#endif lws_set_opaque_user_data(wsi, NULL); h->cwsi = NULL; lws_sul_schedule(h->context, 0, &h->sul_retry, @@ -600,6 +620,13 @@ lws_sspc_destroy(lws_sspc_handle_t **ph) h->destroying = 1; + /* if this caliper is still dangling at destroy, we failed */ +#if defined(LWS_WITH_SYS_METRICS) + /* + * If any hanging caliper measurement, dump it, and free any tags + */ + lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL); +#endif if (h->ss_dangling_connected && h->ssi.state) { lws_sspc_event_helper(h, LWSSSCS_DISCONNECTED, 0); h->ss_dangling_connected = 0; diff --git a/lib/secure-streams/secure-streams-process.c b/lib/secure-streams/secure-streams-process.c index 306d11d39..6bfa5d6df 100644 --- a/lib/secure-streams/secure-streams-process.c +++ b/lib/secure-streams/secure-streams-process.c @@ -51,26 +51,6 @@ #include -/* - * Because both sides of the connection share the conn, we allocate it - * during accepted adoption, and both sides point to it. - * - * When .ss or .wsi close, they must NULL their entry here so no dangling - * refereneces. - * - * The last one of the accepted side and the onward side to close frees it. - */ - -struct conn { - struct lws_ss_serialization_parser parser; - - lws_dsh_t *dsh; /* unified buffer for both sides */ - struct lws *wsi; /* the proxy's client side */ - lws_ss_handle_t *ss; /* the onward, ss side */ - - lws_ss_conn_states_t state; -}; - struct raw_pss { struct conn *conn; }; @@ -313,9 +293,6 @@ callback_ss_proxy(struct lws *wsi, enum lws_callback_reasons reason, lws_ss_metadata_t *md; lws_ss_info_t ssi; const uint8_t *cp; -#if defined(LWS_WITH_DETAILED_LATENCY) - lws_usec_t us; -#endif char s[256]; uint8_t *p; size_t si; @@ -588,7 +565,7 @@ callback_ss_proxy(struct lws *wsi, enum lws_callback_reasons reason, cp = p; -#if defined(LWS_WITH_DETAILED_LATENCY) +#if 0 if (cp[0] == LWSSS_SER_RXPRE_RX_PAYLOAD && wsi->a.context->detailed_latency_cb) { diff --git a/lib/secure-streams/secure-streams-serialize.c b/lib/secure-streams/secure-streams-serialize.c index a9e7469bd..0cbf08821 100644 --- a/lib/secure-streams/secure-streams-serialize.c +++ b/lib/secure-streams/secure-streams-serialize.c @@ -224,7 +224,7 @@ lws_ss_deserialize_tx_payload(struct lws_dsh *dsh, struct lws *wsi, *flags = (int)lws_ser_ru32be(&p[3]); -#if defined(LWS_WITH_DETAILED_LATENCY) +#if 0 if (wsi && wsi->a.context->detailed_latency_cb) { /* * use the proxied latency information to compute the client @@ -725,7 +725,7 @@ payload_ff: } } -#if defined(LWS_WITH_DETAILED_LATENCY) +#if 0 if (lws_det_lat_active(context)) { lws_detlat_t d; @@ -1233,6 +1233,13 @@ payload_ff: * CREATING now so we'll know the metadata to sync. */ +#if defined(LWS_WITH_SYS_METRICS) + /* + * If any hanging caliper measurement, dump it, and free any tags + */ + lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL); +#endif + if (!h->creating_cb_done) { if (lws_ss_check_next_state(&h->lc, &h->prev_ss_state, LWSSSCS_CREATING)) diff --git a/lib/secure-streams/secure-streams.c b/lib/secure-streams/secure-streams.c index 1cc7e6343..3d5f58f5e 100644 --- a/lib/secure-streams/secure-streams.c +++ b/lib/secure-streams/secure-streams.c @@ -159,6 +159,7 @@ static const uint32_t ss_state_txn_validity[] = { (1 << LWSSSCS_POLL) | (1 << LWSSSCS_TIMEOUT) | (1 << LWSSSCS_DISCONNECTED) | + (1 << LWSSSCS_UNREACHABLE) | (1 << LWSSSCS_DESTROYING), [LWSSSCS_SERVER_TXN] = (1 << LWSSSCS_DISCONNECTED) | @@ -671,6 +672,14 @@ _lws_ss_client_connect(lws_ss_handle_t *h, int is_retry, void *conn_if_sspc_onw) lwsl_info("%s: connecting %s, '%s' '%s' %s\n", __func__, i.method, i.alpn, i.address, i.path); +#if defined(LWS_WITH_SYS_METRICS) + /* possibly already hanging connect retry... */ + if (!h->cal_txn.mt) + lws_metrics_caliper_bind(h->cal_txn, h->context->mth_ss_conn); + + lws_metrics_tag_add(&h->cal_txn.mtags_owner, "ss", h->policy->streamtype); +#endif + h->txn_ok = 0; r = lws_ss_event_helper(h, LWSSSCS_CONNECTING); if (r) { @@ -1181,6 +1190,13 @@ lws_ss_destroy(lws_ss_handle_t **ppss) lws_fi_destroy(&h->fi); #endif +#if defined(LWS_WITH_SYS_METRICS) + /* + * If any hanging caliper measurement, dump it, and free any tags + */ + lws_metrics_caliper_report_hist(h->cal_txn, (struct lws *)NULL); +#endif + lws_sul_cancel(&h->sul_timeout); /* confirm no sul left scheduled in handle or user allocation object */ diff --git a/lib/system/CMakeLists.txt b/lib/system/CMakeLists.txt index 9ad9d7094..654264b4a 100644 --- a/lib/system/CMakeLists.txt +++ b/lib/system/CMakeLists.txt @@ -63,6 +63,8 @@ if (LWS_WITH_NETWORK) system/fault-injection/fault-injection.c) endif() + add_subdir_include_dirs(metrics) + endif() # diff --git a/lib/system/async-dns/async-dns-parse.c b/lib/system/async-dns/async-dns-parse.c index c0dd9ae1a..001455550 100644 --- a/lib/system/async-dns/async-dns-parse.c +++ b/lib/system/async-dns/async-dns-parse.c @@ -1,7 +1,7 @@ /* * libwebsockets - small server side websockets and web server implementation * - * Copyright (C) 2010 - 2019 Andy Green + * Copyright (C) 2010 - 2021 Andy Green * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -690,6 +690,8 @@ lws_adns_parse_udp(lws_async_dns_t *dns, const uint8_t *pkt, size_t len) c->incomplete = 0; lws_async_dns_complete(q, q->firstcache); + q->go_nogo = METRES_GO; + /* * the query is completely finished with */ diff --git a/lib/system/async-dns/async-dns.c b/lib/system/async-dns/async-dns.c index 429d85aeb..ed13fd740 100644 --- a/lib/system/async-dns/async-dns.c +++ b/lib/system/async-dns/async-dns.c @@ -34,6 +34,8 @@ static const lws_retry_bo_t retry_policy = { void lws_adns_q_destroy(lws_adns_q_t *q) { + lws_metrics_caliper_report(q->metcal, (char)q->go_nogo); + lws_sul_cancel(&q->sul); lws_sul_cancel(&q->write_sul); lws_dll2_remove(&q->list); @@ -703,6 +705,10 @@ lws_async_dns_query(struct lws_context *context, int tsi, const char *name, if (c->results) c->refcount++; +#if defined(LWS_WITH_SYS_METRICS) + lws_metric_event(context->mt_adns_cache, METRES_GO, 0); +#endif + if (cb(wsi, name, c->results, m, opaque) == NULL) return LADNS_RET_FAILED_WSI_CLOSED; @@ -710,6 +716,10 @@ lws_async_dns_query(struct lws_context *context, int tsi, const char *name, } else lwsl_info("%s: %s uncached\n", __func__, name); +#if defined(LWS_WITH_SYS_METRICS) + lws_metric_event(context->mt_adns_cache, METRES_NOGO, 0); +#endif + /* * It's a 1.2.3.4 or ::1 type IP address already? We don't need a dns * server set up to be able to create an addrinfo result for that. @@ -876,6 +886,10 @@ lws_async_dns_query(struct lws_context *context, int tsi, const char *name, lws_dll2_add_head(&q->list, &dns->waiting); + lws_metrics_caliper_bind(q->metcal, context->mt_conn_dns); + q->go_nogo = METRES_NOGO; + /* caliper is reported in lws_adns_q_destroy */ + lwsl_info("%s: created new query: %s\n", __func__, name); lws_adns_dump(dns); diff --git a/lib/system/async-dns/private-lib-async-dns.h b/lib/system/async-dns/private-lib-async-dns.h index 969833be8..f213448ab 100644 --- a/lib/system/async-dns/private-lib-async-dns.h +++ b/lib/system/async-dns/private-lib-async-dns.h @@ -58,6 +58,8 @@ typedef struct { lws_sorted_usec_list_t write_sul; /* fail if unable to write by this time */ lws_dll2_t list; + lws_metrics_caliper_compose(metcal) + lws_dll2_owner_t wsi_adns; lws_async_dns_cb_t standalone_cb; /* if not associated to wsi */ struct lws_context *context; @@ -83,6 +85,7 @@ typedef struct { uint8_t recursion; uint8_t tids; + uint8_t go_nogo; uint8_t is_retry:1; diff --git a/lib/system/metrics/CMakeLists.txt b/lib/system/metrics/CMakeLists.txt new file mode 100644 index 000000000..3ed7f3f3c --- /dev/null +++ b/lib/system/metrics/CMakeLists.txt @@ -0,0 +1,10 @@ +include_directories(.) + +if (LWS_WITH_SYS_METRICS) + list(APPEND SOURCES + system/metrics/metrics.c + ) +endif() + +exports_to_parent_scope() + diff --git a/lib/system/metrics/metrics.c b/lib/system/metrics/metrics.c new file mode 100644 index 000000000..95a599f3c --- /dev/null +++ b/lib/system/metrics/metrics.c @@ -0,0 +1,891 @@ +/* + * lws Generic Metrics + * + * Copyright (C) 2019 - 2021 Andy Green + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include "private-lib-core.h" +#include + +int +lws_metrics_tag_add(lws_dll2_owner_t *owner, const char *name, const char *val) +{ + size_t vl = strlen(val); + lws_metrics_tag_t *tag; + + // lwsl_notice("%s: adding %s=%s\n", __func__, name, val); + + /* + * Remove (in order to replace) any existing tag of same name + */ + + lws_start_foreach_dll(struct lws_dll2 *, d, owner->head) { + tag = lws_container_of(d, lws_metrics_tag_t, list); + + if (!strcmp(name, tag->name)) { + lws_dll2_remove(&tag->list); + lws_free(tag); + break; + } + + } lws_end_foreach_dll(d); + + /* + * Create the new tag + */ + + tag = lws_malloc(sizeof(*tag) + vl + 1, __func__); + if (!tag) + return 1; + + lws_dll2_clear(&tag->list); + tag->name = name; + memcpy(&tag[1], val, vl + 1); + + lws_dll2_add_tail(&tag->list, owner); + + return 0; +} + +int +lws_metrics_tag_wsi_add(struct lws *wsi, const char *name, const char *val) +{ + __lws_lc_tag(NULL, &wsi->lc, "|%s", val); + + return lws_metrics_tag_add(&wsi->cal_conn.mtags_owner, name, val); +} + +#if defined(LWS_WITH_SECURE_STREAMS) +int +lws_metrics_tag_ss_add(struct lws_ss_handle *ss, const char *name, const char *val) +{ + __lws_lc_tag(NULL, &ss->lc, "|%s", val); + return lws_metrics_tag_add(&ss->cal_txn.mtags_owner, name, val); +} +#if defined(LWS_WITH_SECURE_STREAMS) +int +lws_metrics_tag_sspc_add(struct lws_sspc_handle *sspc, const char *name, + const char *val) +{ + __lws_lc_tag(NULL, &sspc->lc, "|%s", val); + return lws_metrics_tag_add(&sspc->cal_txn.mtags_owner, name, val); +} +#endif +#endif + +void +lws_metrics_tags_destroy(lws_dll2_owner_t *owner) +{ + lws_metrics_tag_t *t; + + lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, owner->head) { + t = lws_container_of(d, lws_metrics_tag_t, list); + + lws_dll2_remove(&t->list); + lws_free(t); + + } lws_end_foreach_dll_safe(d, d1); +} + +size_t +lws_metrics_tags_serialize(lws_dll2_owner_t *owner, char *buf, size_t len) +{ + char *end = buf + len - 1, *p = buf; + lws_metrics_tag_t *t; + + lws_start_foreach_dll(struct lws_dll2 *, d, owner->head) { + t = lws_container_of(d, lws_metrics_tag_t, list); + + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), + "%s=\"%s\"", t->name, (const char *)&t[1]); + + if (d->next && p + 2 < end) + *p++ = ','; + + } lws_end_foreach_dll(d); + + *p = '\0'; + + return lws_ptr_diff_size_t(p, buf); +} + +const char * +lws_metrics_tag_get(lws_dll2_owner_t *owner, const char *name) +{ + lws_metrics_tag_t *t; + + lws_start_foreach_dll(struct lws_dll2 *, d, owner->head) { + t = lws_container_of(d, lws_metrics_tag_t, list); + + if (!strcmp(name, t->name)) + return (const char *)&t[1]; + + } lws_end_foreach_dll(d); + + return NULL; +} + +static int +lws_metrics_dump_cb(lws_metric_pub_t *pub, void *user); + +static void +lws_metrics_report_and_maybe_clear(struct lws_context *ctx, lws_metric_pub_t *pub) +{ + if (!pub->us_first || pub->us_last == pub->us_dumped) + return; + + lws_metrics_dump_cb(pub, ctx); +} + +static void +lws_metrics_periodic_cb(lws_sorted_usec_list_t *sul) +{ + lws_metric_policy_dyn_t *dmp = lws_container_of(sul, + lws_metric_policy_dyn_t, sul); + struct lws_context *ctx = lws_container_of(dmp->list.owner, + struct lws_context, owner_mtr_dynpol); + + if (!ctx->system_ops || !ctx->system_ops->metric_report) + return; + + lws_start_foreach_dll(struct lws_dll2 *, d, dmp->owner.head) { + lws_metric_t *mt = lws_container_of(d, lws_metric_t, list); + lws_metric_pub_t *pub = lws_metrics_priv_to_pub(mt); + + lws_metrics_report_and_maybe_clear(ctx, pub); + + } lws_end_foreach_dll(d); + +#if defined(LWS_WITH_SYS_SMD) && defined(LWS_WITH_SECURE_STREAMS) + (void)lws_smd_msg_printf(ctx, LWSSMDCL_METRICS, + "{\"dump\":\"%s\",\"ts\":%lu}", + dmp->policy->name, + (long)ctx->last_policy); +#endif + + if (dmp->policy->us_schedule) + lws_sul_schedule(ctx, 0, &dmp->sul, + lws_metrics_periodic_cb, + dmp->policy->us_schedule); +} + +/* + * Policies are in two pieces, a const policy and a dynamic part that contains + * lists and sul timers for the policy etc. This creates a dynmic part + * corresponding to the static part. + * + * Metrics can exist detached from being bound to any policy about how to + * report them, these are collected but not reported unless they later become + * bound to a reporting policy dynamically. + */ + +lws_metric_policy_dyn_t * +lws_metrics_policy_dyn_create(struct lws_context *ctx, + const lws_metric_policy_t *po) +{ + lws_metric_policy_dyn_t *dmet; + + dmet = lws_zalloc(sizeof(*dmet), __func__); + if (!dmet) + return NULL; + + dmet->policy = po; + lws_dll2_add_tail(&dmet->list, &ctx->owner_mtr_dynpol); + + if (po->us_schedule) + lws_sul_schedule(ctx, 0, &dmet->sul, + lws_metrics_periodic_cb, + po->us_schedule); + + return dmet; +} + +/* + * Get a dynamic metrics policy from the const one, may return NULL if OOM + */ + +lws_metric_policy_dyn_t * +lws_metrics_policy_get_dyn(struct lws_context *ctx, + const lws_metric_policy_t *po) +{ + lws_start_foreach_dll(struct lws_dll2 *, d, ctx->owner_mtr_dynpol.head) { + lws_metric_policy_dyn_t *dm = + lws_container_of(d, lws_metric_policy_dyn_t, list); + + if (dm->policy == po) + return dm; + + } lws_end_foreach_dll(d); + + /* + * no dyn policy part for this const policy --> create one + * + * We want a dynamic part for listing metrics that bound to the policy + */ + + return lws_metrics_policy_dyn_create(ctx, po); +} + +static int +lws_metrics_check_in_policy(const char *polstring, const char *name) +{ + struct lws_tokenize ts; + + memset(&ts, 0, sizeof(ts)); + + ts.start = polstring; + ts.len = strlen(polstring); + ts.flags = (uint16_t)(LWS_TOKENIZE_F_MINUS_NONTERM | + LWS_TOKENIZE_F_ASTERISK_NONTERM | + LWS_TOKENIZE_F_COMMA_SEP_LIST | + LWS_TOKENIZE_F_NO_FLOATS | + LWS_TOKENIZE_F_DOT_NONTERM); + + do { + ts.e = (int8_t)lws_tokenize(&ts); + + if (ts.e == LWS_TOKZE_TOKEN) { + if (!lws_strcmp_wildcard(ts.token, ts.token_len, name)) + /* yes, we are mentioned in this guy's policy */ + return 0; + } + } while (ts.e > 0); + + /* no, this policy doesn't apply to a metric with our name */ + + return 1; +} + +static const lws_metric_policy_t * +lws_metrics_find_policy(struct lws_context *ctx, const char *name) +{ + const lws_metric_policy_t *mp = ctx->metrics_policies; + + if (!mp) { +#if defined(LWS_WITH_SECURE_STREAMS) + if (ctx->pss_policies) + mp = ctx->pss_policies->metrics; +#endif + if (!mp) + return NULL; + } + + while (mp) { + if (mp->report && !lws_metrics_check_in_policy(mp->report, name)) + return mp; + + mp = mp->next; + } + + return NULL; +} + +/* + * Create a lws_metric_t, bind to a named policy if possible (or add to the + * context list of unbound metrics) and set its lws_system + * idx. The metrics objects themselves are typically composed into other + * objects and are well-known composed members of them. + */ + +lws_metric_t * +lws_metric_create(struct lws_context *ctx, uint8_t flags, const char *name) +{ + const lws_metric_policy_t *po; + lws_metric_policy_dyn_t *dmp; + lws_metric_pub_t *pub; + lws_metric_t *mt; + char pname[32]; + size_t nl; + + if (ctx->metrics_prefix) { + + /* + * In multi-process case, we want to prefix metrics from this + * process / context with a string distinguishing which + * application they came from + */ + + nl = (size_t)lws_snprintf(pname, sizeof(pname) - 1, "%s.%s", + ctx->metrics_prefix, name); + name = pname; + } else + nl = strlen(name); + + mt = (lws_metric_t *)lws_zalloc(sizeof(*mt) /* private */ + + sizeof(lws_metric_pub_t) + + nl + 1 /* copy of metric name */, + __func__); + if (!mt) + return NULL; + + pub = lws_metrics_priv_to_pub(mt); + pub->name = (char *)pub + sizeof(lws_metric_pub_t); + memcpy((char *)pub->name, name, nl + 1); + pub->flags = flags; + + /* after these common members, we have to use the right type */ + + if (!(flags & LWSMTFL_REPORT_HIST)) { + /* anything is smaller or equal to this */ + pub->u.agg.min = ~(u_mt_t)0; + pub->us_first = lws_now_usecs(); + } + + mt->ctx = ctx; + + /* + * Let's see if we can bind to a reporting policy straight away + */ + + po = lws_metrics_find_policy(ctx, name); + if (po) { + dmp = lws_metrics_policy_get_dyn(ctx, po); + if (dmp) { + lwsl_notice("%s: metpol %s\n", __func__, name); + lws_dll2_add_tail(&mt->list, &dmp->owner); + + return 0; + } + } + + /* + * If not, well, let's go on without and maybe later at runtime, he'll + * get interested in us and apply a reporting policy + */ + + lws_dll2_add_tail(&mt->list, &ctx->owner_mtr_no_pol); + + return mt; +} + +/* + * If our metric is bound to a reporting policy, return a pointer to it, + * otherwise NULL + */ + +const lws_metric_policy_t * +lws_metric_get_policy(lws_metric_t *mt) +{ + lws_metric_policy_dyn_t *dp; + + /* + * Our metric must either be on the "no policy" context list or + * listed by the dynamic part of the policy it is bound to + */ + assert(mt->list.owner); + + if ((char *)mt->list.owner >= (char *)mt->ctx && + (char *)mt->list.owner < (char *)mt->ctx + sizeof(struct lws_context)) + /* we are on the "no policy" context list */ + return NULL; + + /* we are listed by a dynamic policy owner */ + + dp = lws_container_of(mt->list.owner, lws_metric_policy_dyn_t, owner); + + /* return the const policy the dynamic policy represents */ + + return dp->policy; +} + +void +lws_metric_rebind_policies(struct lws_context *ctx) +{ + const lws_metric_policy_t *po; + lws_metric_policy_dyn_t *dmp; + + lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, + ctx->owner_mtr_no_pol.head) { + lws_metric_t *mt = lws_container_of(d, lws_metric_t, list); + lws_metric_pub_t *pub = lws_metrics_priv_to_pub(mt); + + po = lws_metrics_find_policy(ctx, pub->name); + if (po) { + dmp = lws_metrics_policy_get_dyn(ctx, po); + if (dmp) { + lwsl_info("%s: %s <- pol %s\n", __func__, + pub->name, po->name); + lws_dll2_remove(&mt->list); + lws_dll2_add_tail(&mt->list, &dmp->owner); + } + } else + lwsl_debug("%s: no pol for %s\n", __func__, pub->name); + + } lws_end_foreach_dll_safe(d, d1); +} + +int +lws_metric_destroy(lws_metric_t **pmt, int keep) +{ + lws_metric_t *mt = *pmt; + lws_metric_pub_t *pub = lws_metrics_priv_to_pub(mt); + + if (!mt) + return 0; + + lws_dll2_remove(&mt->list); + + if (keep) { + lws_dll2_add_tail(&mt->list, &mt->ctx->owner_mtr_no_pol); + + return 0; + } + + if (pub->flags & LWSMTFL_REPORT_HIST) { + lws_metric_bucket_t *b = pub->u.hist.head, *b1; + + pub->u.hist.head = NULL; + + while (b) { + b1 = b->next; + lws_free(b); + b = b1; + } + } + + lws_free(mt); + *pmt = NULL; + + return 0; +} + +/* + * Allow an existing metric to have its reporting policy changed at runtime + */ + +int +lws_metric_switch_policy(lws_metric_t *mt, const char *polname) +{ + const lws_metric_policy_t *po; + lws_metric_policy_dyn_t *dmp; + + po = lws_metrics_find_policy(mt->ctx, polname); + if (!po) + return 1; + + dmp = lws_metrics_policy_get_dyn(mt->ctx, po); + if (!dmp) + return 1; + + lws_dll2_remove(&mt->list); + lws_dll2_add_tail(&mt->list, &dmp->owner); + + return 0; +} + +/* + * If keep is set, don't destroy existing metrics objects, just detach them + * from the policy being deleted and keep track of them on ctx-> + * owner_mtr_no_pol + */ + +void +lws_metric_policy_dyn_destroy(lws_metric_policy_dyn_t *dm, int keep) +{ + lws_sul_cancel(&dm->sul); + + lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, dm->owner.head) { + lws_metric_t *m = lws_container_of(d, lws_metric_t, list); + + lws_metric_destroy(&m, keep); + + } lws_end_foreach_dll_safe(d, d1); + + lws_sul_cancel(&dm->sul); + + lws_dll2_remove(&dm->list); + lws_free(dm); +} + +/* + * Destroy all dynamic metrics policies, deinit any metrics still using them + */ + +void +lws_metrics_destroy(struct lws_context *ctx) +{ + lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, + ctx->owner_mtr_dynpol.head) { + lws_metric_policy_dyn_t *dm = + lws_container_of(d, lws_metric_policy_dyn_t, list); + + lws_metric_policy_dyn_destroy(dm, 0); /* don't keep */ + + } lws_end_foreach_dll_safe(d, d1); + + /* destroy metrics with no current policy too... */ + + lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, + ctx->owner_mtr_no_pol.head) { + lws_metric_t *mt = lws_container_of(d, lws_metric_t, list); + + lws_metric_destroy(&mt, 0); /* don't keep */ + + } lws_end_foreach_dll_safe(d, d1); + + /* ... that's the whole allocated metrics footprint gone... */ +} + +int +lws_metrics_hist_bump_(lws_metric_pub_t *pub, const char *name) +{ + lws_metric_bucket_t *buck = pub->u.hist.head; + size_t nl = strlen(name); + char *nm; + + if (!(pub->flags & LWSMTFL_REPORT_HIST)) { + lwsl_err("%s: %s not histogram: flags %d\n", __func__, + pub->name, pub->flags); + assert(0); + } + assert(nl < 255); + + pub->us_last = lws_now_usecs(); + if (!pub->us_first) + pub->us_first = pub->us_last; + + while (buck) { + if (lws_metric_bucket_name_len(buck) == nl && + !strcmp(name, lws_metric_bucket_name(buck))) { + buck->count++; + goto happy; + } + buck = buck->next; + } + + buck = lws_malloc(sizeof(*buck) + nl + 2, __func__); + if (!buck) + return 1; + + nm = (char *)buck + sizeof(*buck); + /* length byte at beginning of name, avoid struct alignment overhead */ + *nm = (char)nl; + memcpy(nm + 1, name, nl + 1); + + buck->next = pub->u.hist.head; + pub->u.hist.head = buck; + buck->count = 1; + pub->u.hist.list_size++; + +happy: + pub->u.hist.total_count++; + + return 0; +} + +int +lws_metrics_hist_bump_describe_wsi(struct lws *wsi, lws_metric_pub_t *pub, + const char *name) +{ + char desc[192], d1[48], *p = desc, *end = desc + sizeof(desc); + +#if defined(LWS_WITH_SECURE_STREAMS) +#if defined(LWS_WITH_SECURE_STREAMS_PROXY_API) + if (wsi->client_bound_sspc) { + lws_sspc_handle_t *h = (lws_sspc_handle_t *)wsi->a.opaque_user_data; + if (h) + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "ss=\"%s\",", + h->ssi.streamtype); + } else + if (wsi->client_proxy_onward) { + struct conn *conn = (struct conn *)wsi->a.opaque_user_data; + + if (conn && conn->ss) + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "ss=\"%s\",", + conn->ss->info.streamtype); + } else +#endif + if (wsi->for_ss) { + lws_ss_handle_t *h = (lws_ss_handle_t *)wsi->a.opaque_user_data; + if (h) + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "ss=\"%s\",", + h->info.streamtype); + } +#endif + +#if defined(LWS_WITH_CLIENT) + if (wsi->stash && wsi->stash->cis[CIS_HOST]) + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "hostname=\"%s\",", + wsi->stash->cis[CIS_HOST]); +#endif + + lws_sa46_write_numeric_address(&wsi->sa46_peer, d1, sizeof(d1)); + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "peer=\"%s\",", d1); + + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "%s", name); + + lws_metrics_hist_bump_(pub, desc); + + return 0; +} + +int +lws_metrics_foreach(struct lws_context *ctx, void *user, + int (*cb)(lws_metric_pub_t *pub, void *user)) +{ + int n; + + lws_start_foreach_dll_safe(struct lws_dll2 *, d, d1, + ctx->owner_mtr_no_pol.head) { + lws_metric_t *mt = lws_container_of(d, lws_metric_t, list); + + n = cb(lws_metrics_priv_to_pub(mt), user); + if (n) + return n; + + } lws_end_foreach_dll_safe(d, d1); + + lws_start_foreach_dll_safe(struct lws_dll2 *, d2, d3, + ctx->owner_mtr_dynpol.head) { + lws_metric_policy_dyn_t *dm = + lws_container_of(d2, lws_metric_policy_dyn_t, list); + + lws_start_foreach_dll_safe(struct lws_dll2 *, e, e1, + dm->owner.head) { + + lws_metric_t *mt = lws_container_of(e, lws_metric_t, list); + + n = cb(lws_metrics_priv_to_pub(mt), user); + if (n) + return n; + + } lws_end_foreach_dll_safe(e, e1); + + } lws_end_foreach_dll_safe(d2, d3); + + return 0; +} + +static int +lws_metrics_dump_cb(lws_metric_pub_t *pub, void *user) +{ + struct lws_context *ctx = (struct lws_context *)user; + int n; + + if (!ctx->system_ops || !ctx->system_ops->metric_report) + return 0; + + /* + * return nonzero to reset stats + */ + + n = ctx->system_ops->metric_report(pub); + + /* track when we dumped it... */ + + pub->us_first = pub->us_dumped = lws_now_usecs(); + pub->us_last = 0; + + if (!n) + return 0; + + /* ... and clear it back to 0 */ + + if (pub->flags & LWSMTFL_REPORT_HIST) { + lws_metric_bucket_t *b = pub->u.hist.head, *b1; + pub->u.hist.head = NULL; + + while (b) { + b1 = b->next; + lws_free(b); + b = b1; + } + pub->u.hist.total_count = 0; + pub->u.hist.list_size = 0; + } else + memset(&pub->u.agg, 0, sizeof(pub->u.agg)); + + return 0; +} + +void +lws_metrics_dump(struct lws_context *ctx) +{ + lws_metrics_foreach(ctx, ctx, lws_metrics_dump_cb); +} + +static int +_lws_metrics_format(lws_metric_pub_t *pub, lws_usec_t now, int gng, + char *buf, size_t len) +{ + const lws_humanize_unit_t *schema = humanize_schema_si; + char *end = buf + len - 1, *obuf = buf; + + if (pub->flags & LWSMTFL_REPORT_DUTY_WALLCLOCK_US) + schema = humanize_schema_us; + + if (!(pub->flags & LWSMTFL_REPORT_MEAN)) { + /* only the sum is meaningful */ + if (pub->flags & LWSMTFL_REPORT_DUTY_WALLCLOCK_US) { + + buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), " %u, ", + (unsigned int)pub->u.agg.count[gng]); + + buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf), + (uint64_t)pub->u.agg.sum[gng], + humanize_schema_us); + + buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), " / "); + + buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf), + (uint64_t)(now - pub->us_first), + humanize_schema_us); + + buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), + " (%d%%)", (int)((100 * pub->u.agg.sum[gng]) / + (unsigned long)(now - pub->us_first))); + } else { + /* it's a monotonic ordinal, like total tx */ + buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "(%u) ", + (unsigned int)pub->u.agg.count[gng]); + buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf), + (uint64_t)pub->u.agg.sum[gng], + humanize_schema_si); + } + + } else { + buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "%u, mean: ", (unsigned int)pub->u.agg.count[gng]); + /* the average over the period is meaningful */ + buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf), + (uint64_t)(pub->u.agg.count[gng] ? + pub->u.agg.sum[gng] / pub->u.agg.count[gng] : 0), + schema); + } + + return lws_ptr_diff(buf, obuf); +} + +int +lws_metrics_format(lws_metric_pub_t *pub, lws_metric_bucket_t **sub, char *buf, size_t len) +{ + char *end = buf + len - 1, *obuf = buf; + lws_usec_t t = lws_now_usecs(); + const lws_humanize_unit_t *schema = humanize_schema_si; + + if (pub->flags & LWSMTFL_REPORT_DUTY_WALLCLOCK_US) + schema = humanize_schema_us; + + if (pub->flags & LWSMTFL_REPORT_HIST) { + + if (*sub == NULL) + return 0; + + if (*sub) { + buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), + "%s{%s} %llu", pub->name, + lws_metric_bucket_name(*sub), + (unsigned long long)(*sub)->count); + + *sub = (*sub)->next; + } + + goto happy; + } + + buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), "%s: ", + pub->name); + + if (!pub->u.agg.count[METRES_GO] && !pub->u.agg.count[METRES_NOGO]) + return 0; + + if (pub->u.agg.count[METRES_GO]) { + if (!(pub->flags & LWSMTFL_REPORT_ONLY_GO)) + buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), + "Go: "); + buf += _lws_metrics_format(pub, t, METRES_GO, buf, + lws_ptr_diff_size_t(end, buf)); + } + + if (!(pub->flags & LWSMTFL_REPORT_ONLY_GO) && pub->u.agg.count[METRES_NOGO]) { + buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ", NoGo: "); + buf += _lws_metrics_format(pub, t, METRES_NOGO, buf, + lws_ptr_diff_size_t(end, buf)); + } + + if (pub->flags & LWSMTFL_REPORT_MEAN) { + buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ", min: "); + buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf), pub->u.agg.min, + schema); + buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), ", max: "); + buf += lws_humanize(buf, lws_ptr_diff_size_t(end, buf), pub->u.agg.max, + schema); + } + +happy: + if (pub->flags & LWSMTFL_REPORT_HIST) + return 1; + + *sub = NULL; + + return lws_ptr_diff(buf, obuf); +} + +/* + * We want to, at least internally, record an event... depending on the policy, + * that might cause us to call through to the lws_system apis, or just update + * our local stats about it and dump at the next periodic chance (also set by + * the policy) + */ + +void +lws_metric_event(lws_metric_t *mt, char go_nogo, u_mt_t val) +{ + lws_metric_pub_t *pub; + + assert((go_nogo & 0xfe) == 0); + + if (!mt) + return; + + pub = lws_metrics_priv_to_pub(mt); + assert(!(pub->flags & LWSMTFL_REPORT_HIST)); + + pub->us_last = lws_now_usecs(); + if (!pub->us_first) + pub->us_first = pub->us_last; + pub->u.agg.count[(int)go_nogo]++; + pub->u.agg.sum[(int)go_nogo] += val; + if (val > pub->u.agg.max) + pub->u.agg.max = val; + if (val < pub->u.agg.min) + pub->u.agg.min = val; + + if (pub->flags & LWSMTFL_REPORT_OOB) + lws_metrics_report_and_maybe_clear(mt->ctx, pub); +} + + +void +lws_metrics_hist_bump_priv_tagged(lws_metric_pub_t *mt, lws_dll2_owner_t *tow, + lws_dll2_owner_t *tow2) +{ + char qual[192]; + size_t p; + + p = lws_metrics_tags_serialize(tow, qual, sizeof(qual)); + if (tow2) + lws_metrics_tags_serialize(tow2, qual + p, + sizeof(qual) - p); + + lwsl_warn("%s: '%s'\n", __func__, qual); + + lws_metrics_hist_bump(mt, qual); +} diff --git a/lib/system/metrics/private-lib-system-metrics.h b/lib/system/metrics/private-lib-system-metrics.h new file mode 100644 index 000000000..07f4daf43 --- /dev/null +++ b/lib/system/metrics/private-lib-system-metrics.h @@ -0,0 +1,124 @@ +/* + * lws System Metrics + * + * Copyright (C) 2021 Andy Green + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +/* + * Const struct that describes a policy for processing raw metrics to turn them + * into events. + * + * Typically although we want to monitor every event, the data produced can be + * too large, and many events that are "normal" just need to be counted as such; + * outliers or change-to-continuous outliers may deserve closer recording as + * events in their own right. + * + * Mean computation must "decay" as it ages, we do this by halving the sum and + * count after .us_decay_unit us. + * + * We don't acknowledge outliers until there are at least .min_contributors + * in the current mean (which is subject to decaying) + * + * We decide something is an outlier event if it deviates from the mean by + * .pc_outlier_deviation %. + */ + +/* + * The dynamic counterpart for each static metric policy, this is on heap + * one per const lws_metric_policy_t. It's listed in context->owner_mtr_dynpol + */ + +typedef struct lws_metric_policy_dyn { + const lws_metric_policy_t *policy; + /**< the static part of the policy we belong to... can be NULL if no + * policy matches or the policy was invalidated */ + + lws_dll2_owner_t owner; + /**< list of metrics that are using this policy */ + + lws_dll2_t list; + /**< context owns us */ + + lws_sorted_usec_list_t sul; + /**< schedule periodic reports for metrics using this policy */ +} lws_metric_policy_dyn_t; + +/* + * A metrics private part, encapsulating the public part + */ + +typedef struct lws_metric { + + lws_dll2_t list; + /**< owned by either 1) ctx.lws_metric_policy_dyn_t.owner, or + * 2) ctx.owner_mtr_no_pol */ + + struct lws_context *ctx; + + /* public part overallocated */ +} lws_metric_t; + + +#if defined(LWS_WITH_SYS_METRICS) +#define lws_metrics_hist_bump_priv(_mt, _name) \ + lws_metrics_hist_bump_(lws_metrics_priv_to_pub(_mt), _name) +#define lws_metrics_hist_bump_priv_wsi(_wsi, _hist, _name) \ + lws_metrics_hist_bump_(lws_metrics_priv_to_pub(_wsi->a.context->_hist), _name) +#define lws_metrics_hist_bump_priv_ss(_ss, _hist, _name) \ + lws_metrics_hist_bump_(lws_metrics_priv_to_pub(_ss->context->_hist), _name) +#define lws_metrics_priv_to_pub(_x) ((lws_metric_pub_t *)&(_x)[1]) +#else +#define lws_metrics_hist_bump_priv(_mt, _name) +#define lws_metrics_hist_bump_priv_wsi(_wsi, _hist, _name) +#define lws_metrics_hist_bump_priv_ss(_ss, _hist, _name) +#define lws_metrics_priv_to_pub(_x) ((lws_metric_pub_t *)NULL) +#endif + +#if defined(LWS_WITH_SECURE_STREAMS_PROXY_API) +/* + * sspc-specific version that also appends the tag value to the lifecycle tag + * used for logging the sspc identity + */ +int +lws_metrics_tag_sspc_add(struct lws_sspc_handle *ss, const char *name, const char *val); +#endif + +int +lws_metrics_register_policy(struct lws_context *ctx, + const lws_metric_policy_t *head); + +void +lws_metrics_destroy(struct lws_context *ctx); + +void +lws_metric_event(lws_metric_t *mt, char go_nogo, u_mt_t val); + +lws_metric_t * +lws_metric_create(struct lws_context *ctx, uint8_t flags, const char *name); + +int +lws_metric_destroy(lws_metric_t **mt, int keep); + +void +lws_metric_policy_dyn_destroy(lws_metric_policy_dyn_t *dm, int keep); + +void +lws_metric_rebind_policies(struct lws_context *ctx); diff --git a/lib/system/smd/smd.c b/lib/system/smd/smd.c index 6ba05f986..763070169 100644 --- a/lib/system/smd/smd.c +++ b/lib/system/smd/smd.c @@ -377,6 +377,10 @@ _lws_smd_ss_rx_forward(struct lws_context *ctx, const char *tag, _class = (lws_smd_class_t)lws_ser_ru64be(buf); + if (_class == LWSSMDCL_METRICS) { + + } + /* only locally forward messages that we care about in this process */ if (!(ctx->smd._class_filter & _class)) diff --git a/lib/tls/mbedtls/mbedtls-client.c b/lib/tls/mbedtls/mbedtls-client.c index 38f8a33b9..cd9a585b8 100644 --- a/lib/tls/mbedtls/mbedtls-client.c +++ b/lib/tls/mbedtls/mbedtls-client.c @@ -223,47 +223,75 @@ int lws_tls_client_confirm_peer_cert(struct lws *wsi, char *ebuf, size_t ebuf_len) { int n; + unsigned int avoid = 0; X509 *peer = SSL_get_peer_certificate(wsi->tls.ssl); struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi]; + const char *type = ""; char *sb = (char *)&pt->serv_buf[0]; if (!peer) { +#if defined(LWS_WITH_SYS_METRICS) + lws_metrics_hist_bump_describe_wsi(wsi, lws_metrics_priv_to_pub( + wsi->a.context->mth_conn_failures), + "tls=\"nocert\""); +#endif lwsl_info("peer did not provide cert\n"); lws_snprintf(ebuf, ebuf_len, "no peer cert"); return -1; } - lwsl_info("peer provided cert\n"); n = (int)SSL_get_verify_result(wsi->tls.ssl); - lwsl_debug("get_verify says %d\n", n); + lwsl_debug("get_verify says %d\n", n); - if (n == X509_V_OK) + switch (n) { + case X509_V_OK: return 0; - if (n == X509_V_ERR_HOSTNAME_MISMATCH && - (wsi->tls.use_ssl & LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK)) { - lwsl_info("accepting certificate for invalid hostname\n"); + case X509_V_ERR_HOSTNAME_MISMATCH: + type = "hostname"; + avoid = LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK; + break; + + case X509_V_ERR_INVALID_CA: + type = "invalidca"; + avoid = LCCSCF_ALLOW_SELFSIGNED; + break; + + case X509_V_ERR_CERT_NOT_YET_VALID: + type = "notyetvalid"; + avoid = LCCSCF_ALLOW_EXPIRED; + break; + + case X509_V_ERR_CERT_HAS_EXPIRED: + type = "expired"; + avoid = LCCSCF_ALLOW_EXPIRED; + break; + } + + lwsl_info("%s: cert problem: %s\n", __func__, type); +#if defined(LWS_WITH_SYS_METRICS) + { + char buckname[64]; + lws_snprintf(buckname, sizeof(buckname), "tls=\"%s\"", type); + lws_metrics_hist_bump_describe_wsi(wsi, + lws_metrics_priv_to_pub(wsi->a.context->mth_conn_failures), + buckname); + } +#endif + if (wsi->tls.use_ssl & avoid) { + lwsl_info("%s: allowing anyway\n", __func__); + return 0; } - if (n == X509_V_ERR_INVALID_CA && - (wsi->tls.use_ssl & LCCSCF_ALLOW_SELFSIGNED)) { - lwsl_info("accepting certificate from untrusted CA\n"); - return 0; - } - - if ((n == X509_V_ERR_CERT_NOT_YET_VALID || - n == X509_V_ERR_CERT_HAS_EXPIRED) && - (wsi->tls.use_ssl & LCCSCF_ALLOW_EXPIRED)) { - lwsl_info("accepting expired or not yet valid certificate\n"); - - return 0; - } lws_snprintf(ebuf, ebuf_len, - "server's cert didn't look good, (use_ssl 0x%x) X509_V_ERR = %d: %s\n", - (unsigned int)wsi->tls.use_ssl, n, ERR_error_string((unsigned long)n, sb)); + "server's cert didn't look good, %s (use_ssl 0x%x) X509_V_ERR = %d: %s\n", + type, (unsigned int)wsi->tls.use_ssl, n, + ERR_error_string((unsigned long)n, sb)); + lwsl_info("%s\n", ebuf); + lws_tls_err_describe_clear(); return -1; diff --git a/lib/tls/mbedtls/mbedtls-ssl.c b/lib/tls/mbedtls/mbedtls-ssl.c index 16fe2261f..240480643 100644 --- a/lib/tls/mbedtls/mbedtls-ssl.c +++ b/lib/tls/mbedtls/mbedtls-ssl.c @@ -51,8 +51,6 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len) if (!wsi->tls.ssl) return lws_ssl_capable_read_no_ssl(wsi, buf, len); - lws_stats_bump(pt, LWSSTATS_C_API_READ, 1); - errno = 0; n = SSL_read(wsi->tls.ssl, buf, (int)len); #if defined(LWS_PLAT_FREERTOS) @@ -61,15 +59,6 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len) return LWS_SSL_CAPABLE_ERROR; } #endif -#if defined(LWS_WITH_STATS) - if (!wsi->seen_rx && wsi->accept_start_us) { - lws_stats_bump(pt, LWSSTATS_US_SSL_RX_DELAY_AVG, - lws_now_usecs() - wsi->accept_start_us); - lws_stats_bump(pt, LWSSTATS_C_SSL_CONNS_HAD_RX, 1); - wsi->seen_rx = 1; - } -#endif - lwsl_debug("%s: %s: SSL_read says %d\n", __func__, lws_wsi_tag(wsi), n); /* manpage: returning 0 means connection shut down */ @@ -82,14 +71,13 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len) if (n < 0) { m = SSL_get_error(wsi->tls.ssl, n); lwsl_debug("%s: %s: ssl err %d errno %d\n", __func__, lws_wsi_tag(wsi), m, errno); - if (errno == LWS_ENOTCONN) { + if (errno == LWS_ENOTCONN) /* If the socket isn't connected anymore, bail out. */ - wsi->socket_is_permanently_unusable = 1; - return LWS_SSL_CAPABLE_ERROR; - } + goto do_err1; + if (m == SSL_ERROR_ZERO_RETURN || m == SSL_ERROR_SYSCALL) - return LWS_SSL_CAPABLE_ERROR; + goto do_err; if (m == SSL_ERROR_WANT_READ || SSL_want_read(wsi->tls.ssl)) { lwsl_debug("%s: WANT_READ\n", __func__); @@ -101,8 +89,16 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len) lwsl_debug("%s: LWS_SSL_CAPABLE_MORE_SERVICE\n", lws_wsi_tag(wsi)); return LWS_SSL_CAPABLE_MORE_SERVICE; } + +do_err1: wsi->socket_is_permanently_unusable = 1; +do_err: +#if defined(LWS_WITH_SYS_METRICS) + if (wsi->a.vhost) + lws_metric_event(wsi->a.vhost->mt_traffic_rx, METRES_NOGO, 0); +#endif + return LWS_SSL_CAPABLE_ERROR; } @@ -115,23 +111,12 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len) lwsl_hexdump_notice(buf, n); #endif - lws_stats_bump(pt, LWSSTATS_B_READ, (uint64_t)n); - -#if defined(LWS_WITH_SERVER_STATUS) +#if defined(LWS_WITH_SYS_METRICS) if (wsi->a.vhost) - wsi->a.vhost->conn_stats.rx = wsi->a.vhost->conn_stats.rx + (unsigned long long)n; -#endif -#if defined(LWS_WITH_DETAILED_LATENCY) - if (context->detailed_latency_cb) { - wsi->detlat.req_size = len; - wsi->detlat.acc_size = n; - wsi->detlat.type = LDLT_READ; - wsi->detlat.latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX] = - lws_now_usecs() - pt->ust_left_poll; - wsi->detlat.latencies[LAT_DUR_USERCB] = 0; - lws_det_lat_cb(wsi->a.context, &wsi->detlat); - } + lws_metric_event(wsi->a.vhost->mt_traffic_rx, + METRES_GO /* rx */, (u_mt_t)n); #endif + /* * if it was our buffer that limited what we read, * check if SSL has additional data pending inside SSL buffers. @@ -186,8 +171,14 @@ lws_ssl_capable_write(struct lws *wsi, unsigned char *buf, size_t len) return lws_ssl_capable_write_no_ssl(wsi, buf, len); n = SSL_write(wsi->tls.ssl, buf, (int)len); - if (n > 0) + if (n > 0) { +#if defined(LWS_WITH_SYS_METRICS) + if (wsi->a.vhost) + lws_metric_event(wsi->a.vhost->mt_traffic_tx, + METRES_GO, (u_mt_t)n); +#endif return n; + } m = SSL_get_error(wsi->tls.ssl, n); if (m != SSL_ERROR_SYSCALL) { @@ -208,6 +199,12 @@ lws_ssl_capable_write(struct lws *wsi, unsigned char *buf, size_t len) lwsl_debug("%s failed: %d\n",__func__, m); wsi->socket_is_permanently_unusable = 1; +#if defined(LWS_WITH_SYS_METRICS) + if (wsi->a.vhost) + lws_metric_event(wsi->a.vhost->mt_traffic_tx, + METRES_NOGO, (u_mt_t)n); +#endif + return LWS_SSL_CAPABLE_ERROR; } diff --git a/lib/tls/openssl/openssl-client.c b/lib/tls/openssl/openssl-client.c index 1c6e08a05..590ed86b9 100644 --- a/lib/tls/openssl/openssl-client.c +++ b/lib/tls/openssl/openssl-client.c @@ -125,6 +125,18 @@ OpenSSL_client_verify_callback(int preverify_ok, X509_STORE_CTX *x509_ctx) lwsl_err("SSL error: %s (preverify_ok=%d;err=%d;" "depth=%d)\n", msg, preverify_ok, err, depth); +#if defined(LWS_WITH_SYS_METRICS) + { + char buckname[64]; + + lws_snprintf(buckname, sizeof(buckname), + "tls=\"%s\"", msg); + lws_metrics_hist_bump_describe_wsi(wsi, + lws_metrics_priv_to_pub(wsi->a.context->mth_conn_failures), + buckname); + } +#endif + return preverify_ok; // not ok } } @@ -480,7 +492,8 @@ lws_tls_client_confirm_peer_cert(struct lws *wsi, char *ebuf, size_t ebuf_len) #if !defined(USE_WOLFSSL) struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi]; char *p = (char *)&pt->serv_buf[0]; - const char *es; + const char *es, *type = ""; + unsigned int avoid = 0; char *sb = p; long n; @@ -488,29 +501,46 @@ lws_tls_client_confirm_peer_cert(struct lws *wsi, char *ebuf, size_t ebuf_len) ERR_clear_error(); n = SSL_get_verify_result(wsi->tls.ssl); - lwsl_debug("get_verify says %ld\n", n); - - if (n == X509_V_OK) + switch (n) { + case X509_V_OK: return 0; - if ((n == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT || - n == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN) && - (wsi->tls.use_ssl & LCCSCF_ALLOW_SELFSIGNED)) { - lwsl_info("accepting self-signed certificate\n"); + case X509_V_ERR_HOSTNAME_MISMATCH: + type = "tls=hostname"; + avoid = LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK; + break; + + case X509_V_ERR_INVALID_CA: + case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT: + case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN: + type = "tls=invalidca"; + avoid = LCCSCF_ALLOW_SELFSIGNED; + break; + + case X509_V_ERR_CERT_NOT_YET_VALID: + type = "tls=notyetvalid"; + avoid = LCCSCF_ALLOW_EXPIRED; + break; + + case X509_V_ERR_CERT_HAS_EXPIRED: + type = "tls=expired"; + avoid = LCCSCF_ALLOW_EXPIRED; + break; + } + + lwsl_info("%s: cert problem: %s\n", __func__, type); + +#if defined(LWS_WITH_SYS_METRICS) + lws_metrics_hist_bump_describe_wsi(wsi, + lws_metrics_priv_to_pub(wsi->a.context->mth_conn_failures), type); +#endif + + if (wsi->tls.use_ssl & avoid) { + lwsl_info("%s: allowing anyway\n", __func__); return 0; } - if ((n == X509_V_ERR_CERT_NOT_YET_VALID || - n == X509_V_ERR_CERT_HAS_EXPIRED) && - (wsi->tls.use_ssl & LCCSCF_ALLOW_EXPIRED)) { - lwsl_info("accepting expired certificate\n"); - return 0; - } - if (n == X509_V_ERR_CERT_NOT_YET_VALID) { - lwsl_info("Cert is from the future... " - "probably our clock... accepting...\n"); - return 0; - } + es = ERR_error_string( #if defined(LWS_WITH_BORINGSSL) (uint32_t) @@ -519,8 +549,8 @@ lws_tls_client_confirm_peer_cert(struct lws *wsi, char *ebuf, size_t ebuf_len) #endif n, sb); lws_snprintf(ebuf, ebuf_len, - "server's cert didn't look good, X509_V_ERR = %ld: %s\n", - n, es); + "server's cert didn't look good, %s X509_V_ERR = %ld: %s\n", + type, n, es); lwsl_info("%s\n", ebuf); lws_tls_err_describe_clear(); diff --git a/lib/tls/openssl/openssl-ssl.c b/lib/tls/openssl/openssl-ssl.c index a593d8768..562c352dc 100644 --- a/lib/tls/openssl/openssl-ssl.c +++ b/lib/tls/openssl/openssl-ssl.c @@ -207,8 +207,6 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len) if (!wsi->tls.ssl) return lws_ssl_capable_read_no_ssl(wsi, buf, len); - lws_stats_bump(pt, LWSSTATS_C_API_READ, 1); - errno = 0; ERR_clear_error(); n = SSL_read(wsi->tls.ssl, buf, (int)(ssize_t)len); @@ -218,16 +216,6 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len) return LWS_SSL_CAPABLE_ERROR; } #endif -#if defined(LWS_WITH_STATS) - if (!wsi->seen_rx && wsi->accept_start_us) { - lws_stats_bump(pt, LWSSTATS_US_SSL_RX_DELAY_AVG, - lws_now_usecs() - - wsi->accept_start_us); - lws_stats_bump(pt, LWSSTATS_C_SSL_CONNS_HAD_RX, 1); - wsi->seen_rx = 1; - } -#endif - lwsl_debug("%s: SSL_read says %d\n", lws_wsi_tag(wsi), n); /* manpage: returning 0 means connection shut down @@ -258,7 +246,7 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len) m = lws_ssl_get_error(wsi, n); lwsl_debug("%s: ssl err %d errno %d\n", lws_wsi_tag(wsi), m, errno); if (m == SSL_ERROR_ZERO_RETURN) /* cleanly shut down */ - return LWS_SSL_CAPABLE_ERROR; + goto do_err; /* hm not retryable.. could be 0 size pkt or error */ @@ -268,7 +256,12 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len) /* unclean, eg closed conn */ wsi->socket_is_permanently_unusable = 1; - +do_err: +#if defined(LWS_WITH_SYS_METRICS) + if (wsi->a.vhost) + lws_metric_event(wsi->a.vhost->mt_traffic_rx, + METRES_NOGO, 0); +#endif return LWS_SSL_CAPABLE_ERROR; } @@ -294,26 +287,12 @@ lws_ssl_capable_read(struct lws *wsi, unsigned char *buf, size_t len) * paths to dump what was received as decrypted data from the tls tunnel */ lwsl_notice("%s: len %d\n", __func__, n); - lwsl_hexdump_notice(buf, n); + lwsl_hexdump_notice(buf, (unsigned int)n); #endif - lws_stats_bump(pt, LWSSTATS_B_READ, (unsigned int)n); - -#if defined(LWS_WITH_SERVER_STATUS) +#if defined(LWS_WITH_SYS_METRICS) if (wsi->a.vhost) - wsi->a.vhost->conn_stats.rx = (unsigned long long)(wsi->a.vhost->conn_stats.rx + (unsigned long long)(long long)n); -#endif - -#if defined(LWS_WITH_DETAILED_LATENCY) - if (context->detailed_latency_cb) { - wsi->detlat.req_size = len; - wsi->detlat.acc_size = (unsigned int)n; - wsi->detlat.type = LDLT_READ; - wsi->detlat.latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX] = - (uint32_t)(lws_now_usecs() - pt->ust_left_poll); - wsi->detlat.latencies[LAT_DUR_USERCB] = 0; - lws_det_lat_cb(wsi->a.context, &wsi->detlat); - } + lws_metric_event(wsi->a.vhost->mt_traffic_rx, METRES_GO, (u_mt_t)n); #endif /* @@ -363,7 +342,7 @@ lws_ssl_capable_write(struct lws *wsi, unsigned char *buf, size_t len) * paths before sending data into the tls tunnel, where you can dump it * and see what is being sent. */ - lwsl_notice("%s: len %d\n", __func__, len); + lwsl_notice("%s: len %u\n", __func__, (unsigned int)len); lwsl_hexdump_notice(buf, len); #endif @@ -373,8 +352,14 @@ lws_ssl_capable_write(struct lws *wsi, unsigned char *buf, size_t len) errno = 0; ERR_clear_error(); n = SSL_write(wsi->tls.ssl, buf, (int)(ssize_t)len); - if (n > 0) + if (n > 0) { +#if defined(LWS_WITH_SYS_METRICS) + if (wsi->a.vhost) + lws_metric_event(wsi->a.vhost->mt_traffic_tx, + METRES_GO, (u_mt_t)n); +#endif return n; + } m = lws_ssl_get_error(wsi, n); if (m != SSL_ERROR_SYSCALL) { @@ -398,6 +383,12 @@ lws_ssl_capable_write(struct lws *wsi, unsigned char *buf, size_t len) wsi->socket_is_permanently_unusable = 1; +#if defined(LWS_WITH_SYS_METRICS) + if (wsi->a.vhost) + lws_metric_event(wsi->a.vhost->mt_traffic_tx, + METRES_NOGO, 0); +#endif + return LWS_SSL_CAPABLE_ERROR; } diff --git a/lib/tls/tls-client.c b/lib/tls/tls-client.c index b8f7a5112..e576f8cef 100644 --- a/lib/tls/tls-client.c +++ b/lib/tls/tls-client.c @@ -34,6 +34,7 @@ lws_ssl_client_connect1(struct lws *wsi, char *errbuf, size_t len) case LWS_SSL_CAPABLE_ERROR: return -1; case LWS_SSL_CAPABLE_DONE: + lws_metrics_caliper_report(wsi->cal_conn, METRES_GO); return 1; /* connected */ case LWS_SSL_CAPABLE_MORE_SERVICE_WRITE: lws_callback_on_writable(wsi); @@ -73,8 +74,12 @@ lws_ssl_client_connect2(struct lws *wsi, char *errbuf, size_t len) } } - if (lws_tls_client_confirm_peer_cert(wsi, errbuf, len)) + if (lws_tls_client_confirm_peer_cert(wsi, errbuf, len)) { + lws_metrics_caliper_report(wsi->cal_conn, METRES_NOGO); return -1; + } + + lws_metrics_caliper_report(wsi->cal_conn, METRES_GO); return 1; } @@ -187,6 +192,9 @@ lws_client_create_tls(struct lws *wsi, const char **pcce, int do_c1) if (!do_c1) return 0; + lws_metrics_caliper_report(wsi->cal_conn, METRES_GO); + lws_metrics_caliper_bind(wsi->cal_conn, wsi->a.context->mt_conn_tls); + n = lws_ssl_client_connect1(wsi, (char *)wsi->a.context->pt[(int)wsi->tsi].serv_buf, wsi->a.context->pt_serv_buf_size); lwsl_debug("%s: lws_ssl_client_connect1: %d\n", __func__, n); @@ -194,8 +202,10 @@ lws_client_create_tls(struct lws *wsi, const char **pcce, int do_c1) return CCTLS_RETURN_RETRY; /* caller should return 0 */ if (n < 0) { *pcce = (const char *)wsi->a.context->pt[(int)wsi->tsi].serv_buf; + lws_metrics_caliper_report(wsi->cal_conn, METRES_NOGO); return CCTLS_RETURN_ERROR; } + /* ...connect1 already handled caliper if SSL_accept done */ } else wsi->tls.ssl = NULL; diff --git a/lib/tls/tls-network.c b/lib/tls/tls-network.c index 5deab5b6c..c90faa78e 100644 --- a/lib/tls/tls-network.c +++ b/lib/tls/tls-network.c @@ -204,10 +204,6 @@ lws_gate_accepts(struct lws_context *context, int on) lwsl_notice("%s: on = %d\n", __func__, on); -#if defined(LWS_WITH_STATS) - context->updated = 1; -#endif - while (v) { if (v->tls.use_ssl && v->lserv_wsi && lws_change_pollfd(v->lserv_wsi, (LWS_POLLIN) * !on, diff --git a/lib/tls/tls-server.c b/lib/tls/tls-server.c index c4983dfd1..887159fa2 100644 --- a/lib/tls/tls-server.c +++ b/lib/tls/tls-server.c @@ -157,9 +157,6 @@ lws_server_socket_service_ssl(struct lws *wsi, lws_sockfd_type accept_fd, char f goto fail; } -#if defined(LWS_WITH_STATS) - context->updated = 1; -#endif /* * we are not accepted yet, but we need to enter ourselves * as a live connection. That way we can retry when more @@ -318,20 +315,13 @@ punt: /* normal SSL connection processing path */ -#if defined(LWS_WITH_STATS) - /* only set this the first time around */ - if (!wsi->accept_start_us) - wsi->accept_start_us = lws_now_usecs(); -#endif errno = 0; - lws_stats_bump(pt, LWSSTATS_C_SSL_ACCEPT_SPIN, 1); n = lws_tls_server_accept(wsi); lwsl_info("SSL_accept says %d\n", n); switch (n) { case LWS_SSL_CAPABLE_DONE: break; case LWS_SSL_CAPABLE_ERROR: - lws_stats_bump(pt, LWSSTATS_C_SSL_CONNECTIONS_FAILED, 1); lwsl_info("%s: SSL_accept failed socket %u: %d\n", __func__, wsi->desc.sockfd, n); wsi->socket_is_permanently_unusable = 1; @@ -341,26 +331,6 @@ punt: return 0; } - lws_stats_bump(pt, LWSSTATS_C_SSL_CONNECTIONS_ACCEPTED, 1); -#if defined(LWS_WITH_STATS) - if (wsi->accept_start_us) - lws_stats_bump(pt, - LWSSTATS_US_SSL_ACCEPT_LATENCY_AVG, - lws_now_usecs() - - wsi->accept_start_us); - wsi->accept_start_us = lws_now_usecs(); -#endif -#if defined(LWS_WITH_DETAILED_LATENCY) - if (context->detailed_latency_cb) { - wsi->detlat.type = LDLT_TLS_NEG_SERVER; - wsi->detlat.latencies[LAT_DUR_PROXY_RX_TO_ONWARD_TX] = - (uint32_t)(lws_now_usecs() - - wsi->detlat.earliest_write_req_pre_write); - wsi->detlat.latencies[LAT_DUR_USERCB] = 0; - lws_det_lat_cb(wsi->a.context, &wsi->detlat); - } -#endif - /* adapt our vhost to match the SNI SSL_CTX that was chosen */ vh = context->vhost_list; while (vh) { diff --git a/lwsws/main.c b/lwsws/main.c index d2b70b71c..eb19a0421 100644 --- a/lwsws/main.c +++ b/lwsws/main.c @@ -64,7 +64,7 @@ static uv_signal_t signal_outer[2]; static int pids[32]; void lwsl_emit_stderr(int level, const char *line); -#define LWSWS_CONFIG_STRING_SIZE (32 * 1024) +#define LWSWS_CONFIG_STRING_SIZE (64 * 1024) static const struct lws_extension exts[] = { #if !defined(LWS_WITHOUT_EXTENSIONS) diff --git a/minimal-examples/api-tests/api-test-lws_tokenize/main.c b/minimal-examples/api-tests/api-test-lws_tokenize/main.c index b55678958..d69dc2fee 100644 --- a/minimal-examples/api-tests/api-test-lws_tokenize/main.c +++ b/minimal-examples/api-tests/api-test-lws_tokenize/main.c @@ -204,7 +204,7 @@ struct tests tests[] = { }, { "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5", expected7, LWS_ARRAY_SIZE(expected7), - LWS_TOKENIZE_F_RFC7230_DELIMS + LWS_TOKENIZE_F_ASTERISK_NONTERM | LWS_TOKENIZE_F_RFC7230_DELIMS }, { " Οὐχὶ ταὐτὰ παρίσταταί μοι γιγνώσκειν, ὦ ἄνδρες ᾿Αθηναῖοι, greek", @@ -720,6 +720,10 @@ int main(int argc, const char **argv) lwsl_user("%s: wc 15 fail\n", __func__); fail++; } + if (lws_strcmp_wildcard("ssproxy.n.cn.*", 14, "ssproxy.n.cn.failures")) { + lwsl_user("%s: wc 16 fail\n", __func__); + fail++; + } lwsl_user("Completed: PASS: %d, FAIL: %d\n", ok, fail); diff --git a/minimal-examples/http-client/minimal-http-client-multi/minimal-http-client-multi.c b/minimal-examples/http-client/minimal-http-client-multi/minimal-http-client-multi.c index 626e28049..8a447f2f4 100644 --- a/minimal-examples/http-client/minimal-http-client-multi/minimal-http-client-multi.c +++ b/minimal-examples/http-client/minimal-http-client-multi/minimal-http-client-multi.c @@ -217,6 +217,30 @@ static const struct lws_protocols protocols[] = { { NULL, NULL, 0, 0 } }; +#if defined(LWS_WITH_SYS_METRICS) + +static int +my_metric_report(lws_metric_pub_t *mp) +{ + lws_metric_bucket_t *sub = mp->u.hist.head; + char buf[192]; + + do { + if (lws_metrics_format(mp, &sub, buf, sizeof(buf))) + lwsl_user("%s: %s\n", __func__, buf); + } while ((mp->flags & LWSMTFL_REPORT_HIST) && sub); + + /* 0 = leave metric to accumulate, 1 = reset the metric */ + + return 1; +} + +static const lws_system_ops_t system_ops = { + .metric_report = my_metric_report, +}; + +#endif + static void signal_cb(void *handle, int signum) { @@ -362,6 +386,11 @@ int main(int argc, const char **argv) * network wsi) that we will use. */ info.fd_limit_per_thread = 1 + COUNT + 1; + info.pcontext = &context; + +#if defined(LWS_WITH_SYS_METRICS) + info.system_ops = &system_ops; +#endif #if defined(LWS_WITH_MBEDTLS) || defined(USE_WOLFSSL) /* @@ -374,11 +403,6 @@ int main(int argc, const char **argv) if ((p = lws_cmdline_option(argc, argv, "--limit"))) info.simultaneous_ssl_restriction = atoi(p); -#if defined(LWS_WITH_DETAILED_LATENCY) - info.detailed_latency_cb = lws_det_lat_plot_cb; - info.detailed_latency_filepath = "/tmp/lws-latency-results"; -#endif - context = lws_create_context(&info); if (!context) { lwsl_err("lws init failed\n"); diff --git a/minimal-examples/http-server/minimal-http-server-tls/minimal-http-server-tls.c b/minimal-examples/http-server/minimal-http-server-tls/minimal-http-server-tls.c index dbc4e7248..0049980c0 100644 --- a/minimal-examples/http-server/minimal-http-server-tls/minimal-http-server-tls.c +++ b/minimal-examples/http-server/minimal-http-server-tls/minimal-http-server-tls.c @@ -23,8 +23,41 @@ static int interrupted; -static const struct lws_http_mount mount = { +#if defined(LWS_WITH_PLUGINS) +static const char * const plugin_dirs[] = { + LWS_INSTALL_DATADIR"/libwebsockets-test-server/plugins/", + NULL +}; +#endif + +static const struct lws_http_mount +#if defined(LWS_WITH_SYS_METRICS) + mount_metrics = { /* .mount_next */ NULL, /* linked-list "next" */ + /* .mountpoint */ "/metrics", /* mountpoint URL */ + /* .origin */ "lws-openmetrics", /* serve from dir */ + /* .def */ "x", /* default filename */ + /* .protocol */ "lws-openmetrics", + /* .cgienv */ NULL, + /* .extra_mimetypes */ NULL, + /* .interpret */ NULL, + /* .cgi_timeout */ 0, + /* .cache_max_age */ 0, + /* .auth_mask */ 0, + /* .cache_reusable */ 0, + /* .cache_revalidate */ 0, + /* .cache_intermediaries */ 0, + /* .origin_protocol */ LWSMPRO_CALLBACK, /* bind to callback */ + /* .mountpoint_len */ 8, /* char count */ + /* .basic_auth_login_file */ NULL, + }, +#endif + mount = { +#if defined(LWS_WITH_SYS_METRICS) + /* .mount_next */ &mount_metrics, /* linked-list "next" */ +#else + /* .mount_next */ NULL, /* linked-list "next" */ +#endif /* .mountpoint */ "/", /* mountpoint URL */ /* .origin */ "./mount-origin", /* serve from dir */ /* .def */ "index.html", /* default filename */ @@ -94,6 +127,10 @@ int main(int argc, const char **argv) info.ssl_cert_filepath = "localhost-100y.cert"; info.ssl_private_key_filepath = "localhost-100y.key"; +#if defined(LWS_WITH_PLUGINS) + info.plugin_dirs = plugin_dirs; +#endif + if (lws_cmdline_option(argc, argv, "-h")) info.options |= LWS_SERVER_OPTION_VHOST_UPG_STRICT_HOST_CHECK; diff --git a/minimal-examples/secure-streams/minimal-secure-streams-alexa/main.c b/minimal-examples/secure-streams/minimal-secure-streams-alexa/main.c index d07c4c723..f6a24d7bf 100644 --- a/minimal-examples/secure-streams/minimal-secure-streams-alexa/main.c +++ b/minimal-examples/secure-streams/minimal-secure-streams-alexa/main.c @@ -389,11 +389,6 @@ int main(int argc, const char **argv) info.port = CONTEXT_PORT_NO_LISTEN; info.pprotocols = protocols; -#if defined(LWS_WITH_DETAILED_LATENCY) - info.detailed_latency_cb = lws_det_lat_plot_cb; - info.detailed_latency_filepath = "/tmp/lws-latency-ssproxy"; -#endif - /* integrate us with lws system state management when context created */ nl.name = "app"; nl.notify_cb = app_system_state_nf; diff --git a/minimal-examples/secure-streams/minimal-secure-streams-avs/main-client.c b/minimal-examples/secure-streams/minimal-secure-streams-avs/main-client.c index bb9b8947b..0f62d43bf 100644 --- a/minimal-examples/secure-streams/minimal-secure-streams-avs/main-client.c +++ b/minimal-examples/secure-streams/minimal-secure-streams-avs/main-client.c @@ -100,11 +100,6 @@ int main(int argc, const char **argv) info.protocols = lws_sspc_protocols; info.port = CONTEXT_PORT_NO_LISTEN; -#if defined(LWS_WITH_DETAILED_LATENCY) - info.detailed_latency_cb = lws_det_lat_plot_cb; - info.detailed_latency_filepath = "/tmp/lws-latency-ssproxy"; -#endif - /* integrate us with lws system state management when context created */ nl.name = "app"; nl.notify_cb = app_system_state_nf; diff --git a/minimal-examples/secure-streams/minimal-secure-streams-avs/main.c b/minimal-examples/secure-streams/minimal-secure-streams-avs/main.c index b3b18f6f5..56ca531fa 100644 --- a/minimal-examples/secure-streams/minimal-secure-streams-avs/main.c +++ b/minimal-examples/secure-streams/minimal-secure-streams-avs/main.c @@ -341,11 +341,6 @@ int main(int argc, const char **argv) } #endif -#if defined(LWS_WITH_DETAILED_LATENCY) - info.detailed_latency_cb = lws_det_lat_plot_cb; - info.detailed_latency_filepath = "/tmp/lws-latency-ssproxy"; -#endif - /* integrate us with lws system state management when context created */ nl.name = "app"; nl.notify_cb = app_system_state_nf; diff --git a/minimal-examples/secure-streams/minimal-secure-streams-metrics-proxy/metrics-proxy-policy.json b/minimal-examples/secure-streams/minimal-secure-streams-metrics-proxy/metrics-proxy-policy.json new file mode 100644 index 000000000..078b025c6 --- /dev/null +++ b/minimal-examples/secure-streams/minimal-secure-streams-metrics-proxy/metrics-proxy-policy.json @@ -0,0 +1,59 @@ +{ + "release":"01234567", + "product":"myproduct", + "schema-version":1, + "retry": [{ + "default": { + "backoff": [1000,2000,3000,5000,10000], + "conceal":5, + "jitterpc":20, + "svalidping":300, + "svalidhup":310 + }}], + "certs": [{ + "dst_root_x3": "MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ"},{"self_localhost": "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"},{"self_localhost_key": "MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCjYtuWaICCY0tJPubxpIgIL+WWmz/fmK8IQr11Wtee6/IUyUlo5I602mq1qcLhT/kmpoR8Di3DAmHKnSWdPWtn1BtXLErLlUiHgZDrZWInmEBjKM1DZf+CvNGZ+EzPgBv5nTekLWcfI5ZZtoGuIP1Dl/IkNDw8zFz4cpiMe/BFGemyxdHhLrKHSm8Eo+nT734tItnHKT/m6DSU0xlZ13d6ehLRm7/+Nx47M3XMTRH5qKP/7TTE2s0U6+M0tsGI2zpRi+m6jzhNyMBTJ1u58qAe3ZW5/+YAiuZYAB6n5bhUp4oFuB5wYbcBywVR8ujInpF8buWQUjy5N8pSNp7szdYsnLJpvAd0sibrNPjC0FQCNrpNjgJmIK3+mKk4kXX7ZTwefoAzTK4l2pHNuC53QVc/EF++GBLAxmvCDq9ZpMIYi7OmzkkAKKC9Ue6Ef217LFQCFIBKIzv9cgi9fwPMLhrKleoVRNsecBsCP569WgJXhUnwf2lon4fEZr3+vRuc9shfqnV0nPN1IMSnzXCast7I2fiuRXdIz96KjlGQpP4XfNVA+RGL7aMnWOFIaVrKWLzAtgzoGMTvP/AuehKXncBJhYtW0ltTioVx+5yTYSAZWl+IssmXjefxJqYi2/7QWmv1QC9psNcjTMaBQLN03T1Qelbs7Y27sxdEnNUth4kI+wIDAQABAoICAFWe8MQZb37k2gdAV3Y6aq8fqokKQqbCNLd3giGFwYkezHXoJfg6Di7oZxNcKyw35LFEghkgtQqErQqo35VPIoH+vXUpWOjnCmM4muFA9/cX6mYMc8TmJsg0ewLdBCOZVw+wPABlaqz+0UOiSMMftpk9fz9JwGd8ERyBsT+tk3Qi6D0vPZVsC1KqxxL/cwIFd3Hf2ZBtJXe0KBn1pktWht5AKqx9mld2Ovl7NjgiC1Fx9r+fZw/iOabFFwQA4dr+R8mEMK/7bd4VXfQ1o/QGGbMTG+ulFrsiDyP+rBIAaGC0i7gDjLAIBQeDhP409ZhswIEc/GBtODU372a2CQK/u4Q/HBQvuBtKFNkGUooLgCCbFxzgNUGc83GB/6IwbEM7R5uXqsFiE71LpmroDyjKTlQ8YZkpIcLNVLw0usoGYHFm2rvCyEVlfsE3Ub8cFyTFk50SeOcF2QL2xzKmmbZEpXglxBHR0hjgon0IKJDGfor4bHO7Nt+1Ece8u2oTEKvpz5aIn44OeC5mApRGy83/0bvsesnWjDE/bGpoT8qFuy+0urDEPNId44XcJm1IRIlG56ErxC3l0s11wrIpTmXXckqwzFR9s2z7f0zjeyxqZg4NTPI7wkM3M8BXlvp2GTBIeoxrWB4V3YArwu8QF80QBgVzmgHl24nTg00UH1OjZsABAoIBAQDOxftSDbSqGytcWqPYP3SZHAWDA0O4ACEM+eCwau9ASutl0IDlNDMJ8nC2ph25BMe5hHDWp2cGQJog7pZ/3qQogQho2gUniKDifN7740QdykllTzTVROqmP8+efreIvqlzHmuqaGfGs5oTkZaWj5su+B+bT+9rIwZcwfs5YRINhQRx17qa++xh5mfE25c+M9fiIBTiNSo4lTxWMBShnK8xrGaMEmN7W0qTMbFHPgQz5FcxRjCCqwHilwNBeLDTp/ZECEB7y34khVh531mBE2mNzSVIQcGZP1I/DvXjW7UUNdgFwii/GW+6M0uUDy23UVQpbFzcV8o1C2nZc4Fb4zwBAoIBAQDKSJkFwwuRnaVJS6WxOKjX8MCu9/cKPnwBv2mmI2jgGxHTw5sr3ahmF5eTb8Zo19BowytN+tr62ZFoIBA9Ubc9esEAU8l3fggdfM82cuR9sGcfQVoCh8tMg6BP8IBLOmbSUhN3PG2m39I802u0fFNVQCJKhx1m1MFFLOu7lVcDS9JN+oYVPb6MDfBLm5jOiPuYkFZ4gH79J7gXI0/YKhaJ7yXthYVkdrSF6Eooer4RZgma62Dd1VNzSq3JBo6rYjF7Lvd+RwDCR1thHrmf/IXplxpNVkoMVxtzbrrbgnC25QmvRYc0rlS/kvM4yQhMH3eA7IycDZMpY+0xm7I7jTT7AoIBAGKzKIMDXdCxBWKhNYJ8z7hiItNl1IZZMW2TPUiY0rl6yaChBVXjM9W0r07QPnHZsUiByqb743adkbTUjmxdJzjaVtxN7ZXwZvOVrY7I7fPWYnCEfXCr4+IVpZI/ZHZWpGX6CGSgT6EOjCZ5IUufIvEpqVSmtF8MqfXO9o9uIYLokrWQx1dBl5UnuTLDqw8bChq7O5y6yfuWaOWvL7nxI8NvSsfj4y635gIa/0dFeBYZEfHIUlGdNVomwXwYEzgE/c19ruIowX7HU/NgxMWTMZhpazlxgesXybel+YNcfDQ4e3RMOMz3ZFiaMaJsGGNf4++d9TmMgk4Ns6oDs6Tb9AECggEBAJYzd+SOYo26iBu3nw3L65uEeh6xou8pXH0Tu4gQrPQTRZZ/nT3iNgOwqu1gRuxcq7TOjt41UdqIKO8vN7/AaJavCpaKoIMowy/aGCbvAvjNPpU3unU8jdl/t08EXs79S5IKPcgAx87sTTi7KDN5SYt4tr2uPEe53NTXuSatilG5QCyExIELOuzWAMKzg7CAiIlNS9foWeLyVkBgCQ6Sme/L8ta+mUDy37K6vC34jh9vK9yrwF6X44ItRoOJafCaVfGI+175q/eWcqTX4q+IG4tKls4sL4mgOJLq+ra50aYMxbcuommctPMXU6CrrYyQpPTHMNVDQy2ttFdsq9iKTncCggEBAMmt/8yvPflS+xv3kg/ZBvR9JB1In2n3rUCYYD47ReKFqJ03Vmq5C9nY56s9w7OUO8perBXlJYmKZQhO4293lvxZD2Iq4NcZbVSCMoHAUzhzY3brdgtSIxa2gGveGAezZ38qKIU26dkz7deECY4vrsRkwhpTW0LGVCpjcQoaKvymAoCmAs8V2oMrZiw1YQ9uOUoWwOqm1wZqmVcOXvPIS2gWAs3fQlWjH9hkcQTMsUaXQDOD0aqkSY3ENqOvbCV1/oUpRi3076khCoAXI1bKSn/AvR3KDP14B5toHI/F5OTSEiGhhHesgRrsfBrpEY1IATtPq1taBZZogRqI3rOkkPk=" + }], + "trust_stores": [ + {"name": "le_via_dst", + "stack": ["dst_root_x3"] + } + ], "s": [ + { + "mintest": { + "endpoint":"warmcat.com", + "port":443, + "protocol":"h2", + "http_method":"GET", + "http_url":"index.html", + "tls":true, + "retry":"default", + "tls_trust_store":"le_via_dst" + }},{ + "forscraper": { + "server":true, + "port":19090, + "protocol":"h1", + "metadata": [{ + "mime": "Content-Type:", + "method": "", + "path": "" + } + ] + }},{ + "forclients": { + "server":true, + "port":19091, + "protocol":"h1", + "metadata": [{ + "mime": "Content-Type:", + "method": "", + "path": "" + }], + "tls":true, + "ws_subprotocol":"lws-metrics-proxy", + "server_cert":"self_localhost", + "server_key":"self_localhost_key" + }} + ] +} + diff --git a/minimal-examples/secure-streams/minimal-secure-streams-post/minimal-secure-streams-post.c b/minimal-examples/secure-streams/minimal-secure-streams-post/minimal-secure-streams-post.c index 8a649ad6d..a213135a1 100644 --- a/minimal-examples/secure-streams/minimal-secure-streams-post/minimal-secure-streams-post.c +++ b/minimal-examples/secure-streams/minimal-secure-streams-post/minimal-secure-streams-post.c @@ -500,10 +500,6 @@ int main(int argc, const char **argv) info.options = LWS_SERVER_OPTION_EXPLICIT_VHOSTS | LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; #endif -#if defined(LWS_WITH_DETAILED_LATENCY) - info.detailed_latency_cb = lws_det_lat_plot_cb; - info.detailed_latency_filepath = "/tmp/lws-latency-ssproxy"; -#endif /* integrate us with lws system state management when context created */ diff --git a/minimal-examples/secure-streams/minimal-secure-streams-proxy/main.c b/minimal-examples/secure-streams/minimal-secure-streams-proxy/main.c index b8f428f70..122cf7c96 100644 --- a/minimal-examples/secure-streams/minimal-secure-streams-proxy/main.c +++ b/minimal-examples/secure-streams/minimal-secure-streams-proxy/main.c @@ -206,6 +206,30 @@ static lws_state_notify_link_t * const app_notifier_list[] = { &nl, NULL }; +#if defined(LWS_WITH_SYS_METRICS) + +static int +my_metric_report(lws_metric_pub_t *mp) +{ + lws_metric_bucket_t *sub = mp->u.hist.head; + char buf[192]; + + do { + if (lws_metrics_format(mp, &sub, buf, sizeof(buf))) + lwsl_user("%s: %s\n", __func__, buf); + } while ((mp->flags & LWSMTFL_REPORT_HIST) && sub); + + /* 0 = leave metric to accumulate, 1 = reset the metric */ + + return 1; +} + +static const lws_system_ops_t system_ops = { + .metric_report = my_metric_report, +}; + +#endif + static void sigint_handler(int sig) { @@ -262,13 +286,9 @@ int main(int argc, const char **argv) info.options = LWS_SERVER_OPTION_EXPLICIT_VHOSTS | LWS_SERVER_OPTION_H2_JUST_FIX_WINDOW_UPDATE_OVERFLOW | LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; - info.fd_limit_per_thread = 1 + 32 + 1; + info.fd_limit_per_thread = 1 + 6 + 1; info.pss_policies_json = default_ss_policy; info.port = CONTEXT_PORT_NO_LISTEN; -#if defined(LWS_WITH_DETAILED_LATENCY) - info.detailed_latency_cb = lws_det_lat_plot_cb; - info.detailed_latency_filepath = "/tmp/lws-latency-ssproxy"; -#endif /* integrate us with lws system state management when context created */ nl.name = "app"; @@ -278,6 +298,11 @@ int main(int argc, const char **argv) info.pt_serv_buf_size = (unsigned int)((6144 * 2) + 2048); info.max_http_header_data = (unsigned short)(6144 + 2048); +#if defined(LWS_WITH_SYS_METRICS) + info.system_ops = &system_ops; + info.metrics_prefix = "ssproxy"; +#endif + context = lws_create_context(&info); if (!context) { lwsl_err("lws init failed\n"); diff --git a/minimal-examples/secure-streams/minimal-secure-streams-smd/minimal-secure-streams-smd.c b/minimal-examples/secure-streams/minimal-secure-streams-smd/minimal-secure-streams-smd.c index a0d028635..741e6932d 100644 --- a/minimal-examples/secure-streams/minimal-secure-streams-smd/minimal-secure-streams-smd.c +++ b/minimal-examples/secure-streams/minimal-secure-streams-smd/minimal-secure-streams-smd.c @@ -178,6 +178,7 @@ static const lws_ss_info_t ssi_lws_smd = { .user_alloc = sizeof(myss_t), .streamtype = LWS_SMD_STREAMTYPENAME, .manual_initial_tx_credit = LWSSMDCL_SYSTEM_STATE | + LWSSMDCL_METRICS | LWSSMDCL_NETWORK, }; diff --git a/minimal-examples/secure-streams/minimal-secure-streams-staticpolicy/minimal-secure-streams.c b/minimal-examples/secure-streams/minimal-secure-streams-staticpolicy/minimal-secure-streams.c index d276c2164..1cffe3681 100644 --- a/minimal-examples/secure-streams/minimal-secure-streams-staticpolicy/minimal-secure-streams.c +++ b/minimal-examples/secure-streams/minimal-secure-streams-staticpolicy/minimal-secure-streams.c @@ -230,10 +230,6 @@ int main(int argc, const char **argv) info.options = LWS_SERVER_OPTION_EXPLICIT_VHOSTS | LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; #endif -#if defined(LWS_WITH_DETAILED_LATENCY) - info.detailed_latency_cb = lws_det_lat_plot_cb; - info.detailed_latency_filepath = "/tmp/lws-latency-ssproxy"; -#endif /* integrate us with lws system state management when context created */ diff --git a/minimal-examples/secure-streams/minimal-secure-streams-testsfail/minimal-secure-streams-testsfail.c b/minimal-examples/secure-streams/minimal-secure-streams-testsfail/minimal-secure-streams-testsfail.c index 95876847a..e5339e147 100644 --- a/minimal-examples/secure-streams/minimal-secure-streams-testsfail/minimal-secure-streams-testsfail.c +++ b/minimal-examples/secure-streams/minimal-secure-streams-testsfail/minimal-secure-streams-testsfail.c @@ -350,6 +350,58 @@ static const char * const default_ss_policy = "\"opportunistic\": true," "\"retry\": \"default\"," "\"tls_trust_store\": \"arca1\"" + + "}},{" + + /* + * Various kinds of tls failure + * + * hostname.badcert.warmcat.com: serves valid cert but for + * warmcat.com + * + * warmcat.com:446: serves valid but expired cert + * + * I don't have an easy way to make the test for "not valid yet" + * cert without root + * + * invalidca.badcert.warmcat.com: selfsigned cert for that + * hostname + */ + + "\"badcert_hostname\": {" + "\"endpoint\": \"hostname.badcert.warmcat.com\"," + "\"port\": 443," + "\"protocol\": \"h1\"," + "\"http_method\": \"GET\"," + "\"http_url\": \"/\"," + "\"tls\": true," + "\"opportunistic\": true," + "\"retry\": \"default\"," + "\"tls_trust_store\": \"le_via_dst\"" + "}},{" + "\"badcert_expired\": {" + "\"endpoint\": \"warmcat.com\"," + "\"port\": 446," + "\"protocol\": \"h1\"," + "\"http_method\": \"GET\"," + "\"http_url\": \"/\"," + "\"tls\": true," + "\"opportunistic\": true," + "\"retry\": \"default\"," + "\"tls_trust_store\": \"le_via_dst\"" + "}},{" + "\"badcert_selfsigned\": {" + "\"endpoint\": \"invalidca.badcert.warmcat.com\"," + "\"port\": 443," + "\"protocol\": \"h1\"," + "\"http_method\": \"GET\"," + "\"http_url\": \"/\"," + "\"tls\": true," + "\"nghttp2_quirk_end_stream\": true," + "\"h2q_oflow_txcr\": true," + "\"opportunistic\": true," + "\"retry\": \"default\"," + "\"tls_trust_store\": \"le_via_dst\"" "}}" "]}" ; @@ -495,6 +547,29 @@ struct tests_seq { 12345 }, + /* + * Let's fail at the tls negotiation various ways + */ + + { + "h1:badcert_hostname", + "badcert_hostname", 5 * LWS_US_PER_SEC, LWSSSCS_TIMEOUT, + (1 << LWSSSCS_QOS_NACK_REMOTE) | + (1 << LWSSSCS_ALL_RETRIES_FAILED) + }, + { + "h1:badcert_expired", + "badcert_expired", 5 * LWS_US_PER_SEC, LWSSSCS_TIMEOUT, + (1 << LWSSSCS_QOS_NACK_REMOTE) | + (1 << LWSSSCS_ALL_RETRIES_FAILED) + }, + { + "h1:badcert_selfsigned", + "badcert_selfsigned", 5 * LWS_US_PER_SEC, LWSSSCS_TIMEOUT, + (1 << LWSSSCS_QOS_NACK_REMOTE) | + (1 << LWSSSCS_ALL_RETRIES_FAILED) + }, + }; typedef struct myss { @@ -675,6 +750,29 @@ static lws_state_notify_link_t * const app_notifier_list[] = { &nl, NULL }; +#if defined(LWS_WITH_SYS_METRICS) +static int +my_metric_report(lws_metric_pub_t *mp) +{ + lws_metric_bucket_t *sub = mp->u.hist.head; + char buf[192]; + + do { + if (lws_metrics_format(mp, &sub, buf, sizeof(buf))) + lwsl_user("%s: %s\n", __func__, buf); + } while ((mp->flags & LWSMTFL_REPORT_HIST) && sub); + + /* 0 = leave metric to accumulate, 1 = reset the metric */ + + return 1; +} + +static const lws_system_ops_t system_ops = { + .metric_report = my_metric_report, +}; + +#endif + static void sigint_handler(int sig) { @@ -740,6 +838,13 @@ main(int argc, const char **argv) nl.notify_cb = app_system_state_nf; info.register_notifier_list = app_notifier_list; +#if defined(LWS_WITH_SYS_METRICS) + info.system_ops = &system_ops; +#if defined(LWS_WITH_SECURE_STREAMS_PROXY_API) + info.metrics_prefix = "ssmex"; +#endif +#endif + /* create the context */ context = lws_create_context(&info); diff --git a/minimal-examples/secure-streams/minimal-secure-streams/minimal-secure-streams.c b/minimal-examples/secure-streams/minimal-secure-streams/minimal-secure-streams.c index cd3f73adc..700062942 100644 --- a/minimal-examples/secure-streams/minimal-secure-streams/minimal-secure-streams.c +++ b/minimal-examples/secure-streams/minimal-secure-streams/minimal-secure-streams.c @@ -125,7 +125,7 @@ static const char * const default_ss_policy = #if defined(VIA_LOCALHOST_SOCKS) "\"http_url\":" "\"policy/minimal-proxy-socks.json\"," #else - "\"http_url\":" "\"policy/minimal-proxy-2.json\"," + "\"http_url\":" "\"policy/minimal-proxy-v4.2.json\"," #endif "\"tls\":" "true," "\"opportunistic\":" "true," @@ -363,6 +363,30 @@ static lws_state_notify_link_t * const app_notifier_list[] = { &nl, NULL }; +#if defined(LWS_WITH_SYS_METRICS) + +static int +my_metric_report(lws_metric_pub_t *mp) +{ + lws_metric_bucket_t *sub = mp->u.hist.head; + char buf[192]; + + do { + if (lws_metrics_format(mp, &sub, buf, sizeof(buf))) + lwsl_user("%s: %s\n", __func__, buf); + } while ((mp->flags & LWSMTFL_REPORT_HIST) && sub); + + /* 0 = leave metric to accumulate, 1 = reset the metric */ + + return 1; +} + +static const lws_system_ops_t system_ops = { + .metric_report = my_metric_report, +}; + +#endif + static void sigint_handler(int sig) { @@ -425,10 +449,6 @@ int main(int argc, const char **argv) LWS_SERVER_OPTION_H2_JUST_FIX_WINDOW_UPDATE_OVERFLOW | LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; #endif -#if defined(LWS_WITH_DETAILED_LATENCY) - info.detailed_latency_cb = lws_det_lat_plot_cb; - info.detailed_latency_filepath = "/tmp/lws-latency-ssproxy"; -#endif /* integrate us with lws system state management when context created */ @@ -436,6 +456,12 @@ int main(int argc, const char **argv) nl.notify_cb = app_system_state_nf; info.register_notifier_list = app_notifier_list; + +#if defined(LWS_WITH_SYS_METRICS) + info.system_ops = &system_ops; + info.metrics_prefix = "ssmex"; +#endif + /* create the context */ context = lws_create_context(&info); diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 2f252f9e2..15acd5b96 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -147,13 +147,12 @@ if (LWS_ROLE_WS) endif() endif() - if (LWS_WITH_SERVER_STATUS) - create_plugin(protocol_lws_server_status "" - "protocol_lws_server_status.c" "" "") + if (LWS_WITH_SYS_METRICS) + create_plugin(protocol_lws_openmetrics_export "" + "protocol_lws_openmetrics_export.c" "" "") if (NOT LWS_WITH_PLUGINS_BUILTIN) - target_compile_definitions(protocol_lws_server_status PRIVATE LWS_BUILDING_SHARED) + target_compile_definitions(protocol_lws_openmetrics_export PRIVATE LWS_BUILDING_SHARED) endif() - endif() if (NOT LWS_WITHOUT_CLIENT) @@ -237,11 +236,6 @@ if (LWS_WITH_PLUGINS AND NOT LWS_WITH_PLUGINS_BUILTIN) endif() -if (LWS_WITH_SERVER_STATUS) - install(FILES server-status.html;server-status.js;server-status.css;lwsws-logo.png - DESTINATION share/libwebsockets-test-server/server-status - COMPONENT examples) -endif() endif() export_to_parent_intermediate() diff --git a/plugins/protocol_lws_openmetrics_export.c b/plugins/protocol_lws_openmetrics_export.c new file mode 100644 index 000000000..ba79fef33 --- /dev/null +++ b/plugins/protocol_lws_openmetrics_export.c @@ -0,0 +1,1200 @@ +/* + * libwebsockets-test-server - libwebsockets test implementation + * + * Written in 2010-2021 by Andy Green + * + * This file is made available under the Creative Commons CC0 1.0 + * Universal Public Domain Dedication. + * + * The person who associated a work with this deed has dedicated + * the work to the public domain by waiving all of his or her rights + * to the work worldwide under copyright law, including all related + * and neighboring rights, to the extent allowed by law. You can copy, + * modify, distribute and perform the work, even for commercial purposes, + * all without asking permission. + * + * The test apps are intended to be adapted for use in your code, which + * may be proprietary. So unlike the library itself, they are licensed + * Public Domain. + * + * Scrapeable, proxiable OpenMetrics metrics (compatible with Prometheus) + * + * https://tools.ietf.org/html/draft-richih-opsawg-openmetrics-00 + * + * This plugin provides four protocols related to openmetrics handling: + * + * 1) "lws-openmetrics" direct http listener so scraper can directly get metrics + * + * 2) "lws-openmetrics-prox-agg" metrics proxy server that scraper can connect + * to locally to proxy through to connected remote clients at 3) + * + * 3) "lws-openmetrics-prox-server" metrics proxy server that remote clients can + * connect to, providing a path where scrapers at 2) can get metrics from + * clients connected us + * + * 4) "lws-openmetrics-prox-client" nailed-up metrics proxy client that tries to + * keep up a connection to the server at 3), allowing to scraper to reach + * clients that have no reachable way to serve. + * + * These are provided like this to maximize flexibility in being able to add + * openmetrics serving, proxying, or client->proxy to existing lws code. + * + * Openmetrics supports a "metric" at the top of its report that describes the + * source aka "target metadata". + * + * Since we want to enable collection from devices that are not externally + * reachable, we must provide a reachable server that the clients can attach to + * and have their stats aggregated and then read by Prometheus or whatever. + * Openmetrics says that it wants to present the aggregated stats in a flat + * summary with only the aggregator's "target metadata" and contributor targets + * getting their data tagged with the source + * + * "The above discussion is in the context of individual exposers. An + * exposition from a general purpose monitoring system may contain + * metrics from many individual targets, and thus may expose multiple + * target info Metrics. The metrics may already have had target + * metadata added to them as labels as part of ingestion. The metric + * names MUST NOT be varied based on target metadata. For example it + * would be incorrect for all metrics to end up being prefixed with + * staging_ even if they all originated from targets in a staging + * environment)." + */ + +#if !defined (LWS_PLUGIN_STATIC) +#if !defined(LWS_DLL) +#define LWS_DLL +#endif +#if !defined(LWS_INTERNAL) +#define LWS_INTERNAL +#endif +#include +#endif +#include +#include +#include +#include +#if !defined(WIN32) +#include +#endif +#include + +struct vhd { + struct lws_context *cx; + struct lws_vhost *vhost; + + char ws_server_uri[128]; + char metrics_proxy_path[128]; + char ba_secret[128]; + + const char *proxy_side_bind_name; + /**< name used to bind the two halves of the proxy together, must be + * the same name given in a pvo for both "lws-openmetrics-prox-agg" + * (the side local to the scraper) and "lws-openmetrics-prox-server" + * (the side the clients connect to) + */ + + char sanity[8]; + + lws_dll2_owner_t clients; + + lws_sorted_usec_list_t sul; /* schedule connection retry */ + + struct vhd *bind_partner_vhd; + + struct lws *wsi; /* related wsi if any */ + uint16_t retry_count; /* count of consequetive retries */ +}; + +struct pss { + lws_dll2_t list; + char proxy_path[64]; + struct lwsac *ac; /* the translated metrics, one ac per line */ + struct lwsac *walk; /* iterator for ac when writing */ + size_t tot; /* content-length computation */ + struct lws *wsi; + + uint8_t greet:1; /* set if client needs to send proxy path */ + uint8_t trigger:1; /* we want to ask the client to dump */ +}; + +#if defined(LWS_WITH_CLIENT) +static const uint32_t backoff_ms[] = { 1000, 2000, 3000, 4000, 5000 }; + +static const lws_retry_bo_t retry = { + .retry_ms_table = backoff_ms, + .retry_ms_table_count = LWS_ARRAY_SIZE(backoff_ms), + .conceal_count = LWS_ARRAY_SIZE(backoff_ms), + + .secs_since_valid_ping = 400, /* force PINGs after secs idle */ + .secs_since_valid_hangup = 400, /* hangup after secs idle */ + + .jitter_percent = 0, +}; + +static void +omc_connect_client(lws_sorted_usec_list_t *sul) +{ + struct vhd *vhd = lws_container_of(sul, struct vhd, sul); + struct lws_client_connect_info i; + const char *prot; + char url[128]; + + memset(&i, 0, sizeof(i)); + + lwsl_notice("%s: %s %s %s\n", __func__, vhd->ws_server_uri, vhd->metrics_proxy_path, vhd->ba_secret); + + lws_strncpy(url, vhd->ws_server_uri, sizeof(url)); + + if (lws_parse_uri(url, &prot, &i.address, &i.port, &i.path)) { + lwsl_err("%s: unable to parse uri %s\n", __func__, + vhd->ws_server_uri); + return; + } + + i.context = vhd->cx; + i.origin = i.address; + i.host = i.address; + i.ssl_connection = LCCSCF_USE_SSL; + i.protocol = "lws-openmetrics-prox-server"; /* public subprot */ + i.local_protocol_name = "lws-openmetrics-prox-client"; + i.pwsi = &vhd->wsi; + i.retry_and_idle_policy = &retry; + i.userdata = vhd; + i.vhost = vhd->vhost; + + lwsl_notice("%s: %s %u %s\n", __func__, i.address, i.port, i.path); + + if (lws_client_connect_via_info(&i)) + return; + + /* + * Failed... schedule a retry... we can't use the _retry_wsi() + * convenience wrapper api here because no valid wsi at this + * point. + */ + if (!lws_retry_sul_schedule(vhd->cx, 0, sul, &retry, + omc_connect_client, &vhd->retry_count)) + return; + + vhd->retry_count = 0; + lws_retry_sul_schedule(vhd->cx, 0, sul, &retry, + omc_connect_client, &vhd->retry_count); +} +#endif + +static void +openmetrics_san(char *nm, size_t nl) +{ + size_t m; + + /* Openmetrics has a very restricted token charset */ + + for (m = 0; m < nl; m++) + if ((nm[m] < 'A' || nm[m] > 'Z') && + (nm[m] < 'a' || nm[m] > 'z') && + (nm[m] < '0' || nm[m] > '9') && + nm[m] != '_') + nm[m] = '_'; +} + +static int +lws_metrics_om_format_agg(lws_metric_pub_t *pub, const char *nm, lws_usec_t now, + int gng, char *buf, size_t len) +{ + const char *_gng = gng ? "_nogo" : "_go"; + char *end = buf + len - 1, *obuf = buf; + + if (pub->flags & LWSMTFL_REPORT_ONLY_GO) + _gng = ""; + + if (!(pub->flags & LWSMTFL_REPORT_MEAN)) { + /* only the sum is meaningful */ + if (pub->flags & LWSMTFL_REPORT_DUTY_WALLCLOCK_US) { + buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), + "%s_count %u\n" + "%s_us_sum %llu\n" + "%s_created %lu.%06u\n", + nm, (unsigned int)pub->u.agg.count[gng], + nm, (unsigned long long)pub->u.agg.sum[gng], + nm, (unsigned long)(pub->us_first / 1000000), + (unsigned int)(pub->us_first % 1000000)); + + return lws_ptr_diff(buf, obuf); + } + + /* it's a monotonic ordinal, like total tx */ + buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), + "%s%s_count %u\n" + "%s%s_sum %llu\n", + nm, _gng, + (unsigned int)pub->u.agg.count[gng], + nm, _gng, + (unsigned long long)pub->u.agg.sum[gng]); + + } else + buf += lws_snprintf(buf, lws_ptr_diff_size_t(end, buf), + "%s%s_count %u\n" + "%s%s_mean %llu\n", + nm, _gng, + (unsigned int)pub->u.agg.count[gng], + nm, _gng, (unsigned long long) + (pub->u.agg.count[gng] ? + pub->u.agg.sum[gng] / + pub->u.agg.count[gng] : 0)); + + return lws_ptr_diff(buf, obuf); +} + +static int +lws_metrics_om_ac_stash(struct pss *pss, const char *buf, size_t len) +{ + char *q; + + q = lwsac_use(&pss->ac, LWS_PRE + len + 2, LWS_PRE + len + 2); + if (!q) { + lwsac_free(&pss->ac); + + return -1; + } + q[LWS_PRE] = (char)((len >> 8) & 0xff); + q[LWS_PRE + 1] = (char)(len & 0xff); + memcpy(q + LWS_PRE + 2, buf, len); + pss->tot += len; + + return 0; +} + +/* + * We have to do the ac listing at this level, because there can be too large + * a number to metrics tags to iterate that can fit in a reasonable buffer. + */ + +static int +lws_metrics_om_format(struct pss *pss, lws_metric_pub_t *pub, const char *nm) +{ + char buf[1200], *p = buf, *end = buf + sizeof(buf) - 1, tmp[512]; + lws_usec_t t = lws_now_usecs(); + + if (pub->flags & LWSMTFL_REPORT_HIST) { + lws_metric_bucket_t *buck = pub->u.hist.head; + + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), + "%s_count %llu\n", + nm, (unsigned long long) + pub->u.hist.total_count); + + while (buck) { + lws_strncpy(tmp, lws_metric_bucket_name(buck), + sizeof(tmp)); + + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), + "%s{%s} %llu\n", nm, tmp, + (unsigned long long)buck->count); + + lws_metrics_om_ac_stash(pss, buf, + lws_ptr_diff_size_t(p, buf)); + p = buf; + + buck = buck->next; + } + + goto happy; + } + + if (!pub->u.agg.count[METRES_GO] && !pub->u.agg.count[METRES_NOGO]) + return 0; + + if (pub->u.agg.count[METRES_GO]) + p += lws_metrics_om_format_agg(pub, nm, t, METRES_GO, p, + lws_ptr_diff_size_t(end, p)); + + if (!(pub->flags & LWSMTFL_REPORT_ONLY_GO) && + pub->u.agg.count[METRES_NOGO]) + p += lws_metrics_om_format_agg(pub, nm, t, METRES_NOGO, p, + lws_ptr_diff_size_t(end, p)); + + if (pub->flags & LWSMTFL_REPORT_MEAN) + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), + "%s_min %llu\n" + "%s_max %llu\n", + nm, (unsigned long long)pub->u.agg.min, + nm, (unsigned long long)pub->u.agg.max); + +happy: + return lws_metrics_om_ac_stash(pss, buf, lws_ptr_diff_size_t(p, buf)); +} + +static int +append_om_metric(lws_metric_pub_t *pub, void *user) +{ + struct pss *pss = (struct pss *)user; + char nm[64]; + size_t nl; + + /* + * Convert lws_metrics to openmetrics metrics data, stashing into an + * lwsac without backfill. Since it's not backfilling, use areas are in + * linear sequence simplifying walking them. Limiting the lwsac alloc + * to less than a typical mtu means we can write one per write + * efficiently + */ + + lws_strncpy(nm, pub->name, sizeof(nm)); + nl = strlen(nm); + + openmetrics_san(nm, nl); + + return lws_metrics_om_format(pss, pub, nm); +} + +#if defined(__linux__) +static int +grabfile(const char *fi, char *buf, size_t len) +{ + int n, fd = lws_open(fi, LWS_O_RDONLY); + + buf[0] = '\0'; + if (fd < 0) + return -1; + + n = (int)read(fd, buf, len - 1); + close(fd); + if (n < 0) { + buf[0] = '\0'; + return -1; + } + + buf[n] = '\0'; + if (n > 0 && buf[n - 1] == '\n') + buf[--n] = '\0'; + + return n; +} +#endif + +/* + * Let's pregenerate the output into an lwsac all at once and + * then spool it back to the peer afterwards + * + * - there's not going to be that much of it (a few kB) + * - we then know the content-length for the headers + * - it's stretchy to arbitrary numbers of metrics + * - lwsac block list provides the per-metric structure to + * hold the data in a way we can walk to write it simply + */ + +int +ome_prepare(struct lws_context *ctx, struct pss *pss) +{ + char buf[1224], *start = buf + LWS_PRE, *p = start, + *end = buf + sizeof(buf) - 1; + char hn[64]; + + pss->tot = 0; + + /* + * Target metadata + */ + + hn[0] = '\0'; + gethostname(hn, sizeof(hn) - 1); + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), + "# TYPE target info\n" + "# HELP target Target metadata\n" + "target_info{hostname=\"%s\"", hn); + +#if defined(__linux__) + if (grabfile("/proc/self/cmdline", hn, sizeof(hn))) + p += lws_snprintf((char *)p, lws_ptr_diff_size_t(end, p), + ",cmdline=\"%s\"", hn); +#endif + + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "} 1\n"); + + if (lws_metrics_om_ac_stash(pss, (const char *)buf + LWS_PRE, + lws_ptr_diff_size_t(p, buf + LWS_PRE))) + return 1; + + /* lws version */ + + p = start; + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), + "# TYPE lws_info info\n" + "# HELP lws_info Version of lws producing this\n" + "lws_info{version=\"%s\"} 1\n", LWS_BUILD_HASH); + if (lws_metrics_om_ac_stash(pss, (const char *)buf + LWS_PRE, + lws_ptr_diff_size_t(p, buf + LWS_PRE))) + return 1; + + /* system scalars */ + +#if defined(__linux__) + if (grabfile("/proc/loadavg", hn, sizeof(hn))) { + char *sp = strchr(hn, ' '); + if (sp) { + p = start; + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), + "load_1m %.*s\n", + lws_ptr_diff(sp, hn), hn); + if (lws_metrics_om_ac_stash(pss, + (char *)buf + LWS_PRE, + lws_ptr_diff_size_t(p, + start))) + return 1; + } + } +#endif + + if (lws_metrics_foreach(ctx, pss, append_om_metric)) + return 1; + + p = start; + p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), + "# EOF\n"); + if (lws_metrics_om_ac_stash(pss, (char *)buf + LWS_PRE, + lws_ptr_diff_size_t(p, buf + LWS_PRE))) + return 1; + + pss->walk = pss->ac; + + return 0; +} + +#if defined(LWS_WITH_SERVER) + +/* 1) direct http export for scraper */ + +static int +callback_lws_openmetrics_export(struct lws *wsi, + enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + unsigned char buf[1224], *start = buf + LWS_PRE, *p = start, + *end = buf + sizeof(buf) - 1, *ip; + struct lws_context *cx = lws_get_context(wsi); + struct pss *pss = (struct pss *)user; + unsigned int m, wm; + + switch (reason) { + case LWS_CALLBACK_HTTP: + + ome_prepare(cx, pss); + + p = start; + if (lws_add_http_common_headers(wsi, HTTP_STATUS_OK, + "application/openmetrics-text; " + "version=1.0.0; charset=utf-8", + pss->tot, &p, end) || + lws_finalize_write_http_header(wsi, start, &p, end)) + return 1; + + lws_callback_on_writable(wsi); + + return 0; + + case LWS_CALLBACK_CLOSED_HTTP: + lwsac_free(&pss->ac); + break; + + case LWS_CALLBACK_HTTP_WRITEABLE: + if (!pss->walk) + return 0; + + do { + ip = (uint8_t *)pss->walk + + lwsac_sizeof(pss->walk == pss->ac) + LWS_PRE; + m = (unsigned int)((ip[0] << 8) | ip[1]); + + /* coverity */ + if (m > lwsac_get_tail_pos(pss->walk) - + lwsac_sizeof(pss->walk == pss->ac)) + return -1; + + if (lws_ptr_diff_size_t(end, p) < m) + break; + + memcpy(p, ip + 2, m); + p += m; + + pss->walk = lwsac_get_next(pss->walk); + } while (pss->walk); + + if (!lws_ptr_diff_size_t(p, start)) { + lwsl_err("%s: stuck\n", __func__); + return -1; + } + + wm = pss->walk ? LWS_WRITE_HTTP : LWS_WRITE_HTTP_FINAL; + + if (lws_write(wsi, start, lws_ptr_diff_size_t(p, start), + (enum lws_write_protocol)wm) < 0) + return 1; + + if (!pss->walk) { + if (lws_http_transaction_completed(wsi)) + return -1; + } else + lws_callback_on_writable(wsi); + + return 0; + + default: + break; + } + + return lws_callback_http_dummy(wsi, reason, user, in, len); +} + +static struct pss * +omc_lws_om_get_other_side_pss_client(struct vhd *vhd, struct pss *pss) +{ + /* + * Search through our partner's clients list looking for one with the + * same proxy path + */ + lws_start_foreach_dll(struct lws_dll2 *, d, + vhd->bind_partner_vhd->clients.head) { + struct pss *apss = lws_container_of(d, struct pss, list); + + if (!strcmp(pss->proxy_path, apss->proxy_path)) + return apss; + + } lws_end_foreach_dll(d); + + return NULL; +} + +/* 2) "lws-openmetrics-prox-agg": http server export via proxy to connected clients */ + +static int +callback_lws_openmetrics_prox_agg(struct lws *wsi, + enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + unsigned char buf[1224], *start = buf + LWS_PRE, *p = start, + *end = buf + sizeof(buf) - 1, *ip; + struct vhd *vhd = (struct vhd *)lws_protocol_vh_priv_get( + lws_get_vhost(wsi), lws_get_protocol(wsi)); + struct lws_context *cx = lws_get_context(wsi); + struct pss *pss = (struct pss *)user, *partner_pss; + unsigned int m, wm; + + switch (reason) { + + case LWS_CALLBACK_PROTOCOL_INIT: + lwsl_notice("%s: PROTOCOL_INIT on %s\n", __func__, lws_vh_tag(lws_get_vhost(wsi))); + /* + * We get told what to do when we are bound to the vhost + */ + vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), + lws_get_protocol(wsi), sizeof(struct vhd)); + if (!vhd) { + lwsl_err("%s: vhd alloc failed\n", __func__); + return 0; + } + + vhd->cx = cx; + + /* + * Try to bind to the counterpart server in the proxy, binding + * to the right one by having a common bind name set in a pvo. + * We don't know who will get instantiated last, so both parts + * try to bind if not already bound + */ + + if (!lws_pvo_get_str(in, "proxy-side-bind-name", + &vhd->proxy_side_bind_name)) { + /* + * Attempt to find the vhd that belongs to a vhost + * that has instantiated protocol + * "lws-openmetrics-prox-server", and has set pvo + * "proxy-side-bind-name" on it to whatever our + * vhd->proxy_side_bind_name was also set to. + * + * If found, inform the two sides of the same proxy + * what their partner vhd is + */ + lws_strncpy(vhd->sanity, "isagg", sizeof(vhd->sanity)); + vhd->bind_partner_vhd = lws_vhd_find_by_pvo(cx, + "lws-openmetrics-prox-server", + "proxy-side-bind-name", + vhd->proxy_side_bind_name); + if (vhd->bind_partner_vhd) { + assert(!strcmp(vhd->bind_partner_vhd->sanity, "isws")); + lwsl_notice("%s: proxy binding OK\n", __func__); + vhd->bind_partner_vhd->bind_partner_vhd = vhd; + } + } else { + lwsl_warn("%s: proxy-side-bind-name required\n", __func__); + return 1; + } + + break; + + case LWS_CALLBACK_PROTOCOL_DESTROY: + if (vhd) + lws_sul_cancel(&vhd->sul); + break; + + case LWS_CALLBACK_HTTP: + + /* + * The scraper has connected to us, the local side of the proxy, + * we need to match what it wants to + */ + + if (!vhd->bind_partner_vhd) + return 0; + + lws_strnncpy(pss->proxy_path, (const char *)in, len, + sizeof(pss->proxy_path)); + + if (pss->list.owner) { + lwsl_warn("%s: double HTTP?\n", __func__); + return 0; + } + + pss->wsi = wsi; + + lws_start_foreach_dll(struct lws_dll2 *, d, + vhd->bind_partner_vhd->clients.head) { + struct pss *apss = lws_container_of(d, struct pss, list); + + if (!strcmp((const char *)in, apss->proxy_path)) { + apss->trigger = 1; + lws_callback_on_writable(apss->wsi); + + /* let's add him on the http server vhd list */ + + lws_dll2_add_tail(&pss->list, &vhd->clients); + return 0; + } + + } lws_end_foreach_dll(d); + + return 0; + + case LWS_CALLBACK_CLOSED_HTTP: + lwsac_free(&pss->ac); + lws_dll2_remove(&pss->list); + break; + + case LWS_CALLBACK_HTTP_WRITEABLE: + + if (!pss->walk) + return 0; + + /* locate the wss side if it's still around */ + + partner_pss = omc_lws_om_get_other_side_pss_client(vhd, pss); + if (!partner_pss) + return -1; + + do { + ip = (uint8_t *)pss->walk + + lwsac_sizeof(pss->walk == partner_pss->ac) + LWS_PRE; + m = (unsigned int)((ip[0] << 8) | ip[1]); + + /* coverity */ + if (m > lwsac_get_tail_pos(pss->walk) - + lwsac_sizeof(pss->walk == partner_pss->ac)) + return -1; + + if (lws_ptr_diff_size_t(end, p) < m) + break; + + memcpy(p, ip + 2, m); + p += m; + + pss->walk = lwsac_get_next(pss->walk); + } while (pss->walk); + + if (!lws_ptr_diff_size_t(p, start)) { + lwsl_err("%s: stuck\n", __func__); + return -1; + } + + wm = pss->walk ? LWS_WRITE_HTTP : LWS_WRITE_HTTP_FINAL; + + if (lws_write(wsi, start, lws_ptr_diff_size_t(p, start), + (enum lws_write_protocol)wm) < 0) + return 1; + + if (!pss->walk) { + lwsl_info("%s: whole msg proxied to scraper\n", __func__); + lws_dll2_remove(&pss->list); + lwsac_free(&partner_pss->ac); +// if (lws_http_transaction_completed(wsi)) + return -1; + } else + lws_callback_on_writable(wsi); + + return 0; + + default: + break; + } + + return lws_callback_http_dummy(wsi, reason, user, in, len); +} + +/* 3) "lws-openmetrics-prox-server": ws server side of metrics proxy, for + * ws clients to connect to */ + +static int +callback_lws_openmetrics_prox_server(struct lws *wsi, + enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + unsigned char buf[1224], *start = buf + LWS_PRE, *p = start, + *end = buf + sizeof(buf) - 1; + struct vhd *vhd = (struct vhd *)lws_protocol_vh_priv_get( + lws_get_vhost(wsi), lws_get_protocol(wsi)); + struct lws_context *cx = lws_get_context(wsi); + struct pss *pss = (struct pss *)user, *partner_pss; + + switch (reason) { + + case LWS_CALLBACK_PROTOCOL_INIT: + /* + * We get told what to do when we are bound to the vhost + */ + + lwsl_notice("%s: PROTOCOL_INIT on %s\n", __func__, lws_vh_tag(lws_get_vhost(wsi))); + + vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), + lws_get_protocol(wsi), sizeof(struct vhd)); + if (!vhd) { + lwsl_err("%s: vhd alloc failed\n", __func__); + return 0; + } + + vhd->cx = cx; + + /* + * Try to bind to the counterpart server in the proxy, binding + * to the right one by having a common bind name set in a pvo. + * We don't know who will get instantiated last, so both parts + * try to bind if not already bound + */ + + if (!lws_pvo_get_str(in, "proxy-side-bind-name", + &vhd->proxy_side_bind_name)) { + /* + * Attempt to find the vhd that belongs to a vhost + * that has instantiated protocol + * "lws-openmetrics-prox-server", and has set pvo + * "proxy-side-bind-name" on it to whatever our + * vhd->proxy_side_bind_name was also set to. + * + * If found, inform the two sides of the same proxy + * what their partner vhd is + */ + lws_strncpy(vhd->sanity, "isws", sizeof(vhd->sanity)); + vhd->bind_partner_vhd = lws_vhd_find_by_pvo(cx, + "lws-openmetrics-prox-agg", + "proxy-side-bind-name", + vhd->proxy_side_bind_name); + if (vhd->bind_partner_vhd) { + assert(!strcmp(vhd->bind_partner_vhd->sanity, "isagg")); + lwsl_notice("%s: proxy binding OK\n", __func__); + vhd->bind_partner_vhd->bind_partner_vhd = vhd; + } + } else { + lwsl_warn("%s: proxy-side-bind-name required\n", __func__); + return 1; + } + + break; + + case LWS_CALLBACK_PROTOCOL_DESTROY: + break; + + case LWS_CALLBACK_ESTABLISHED: + /* + * a client has joined... we need to add his pss to our list + * of live, joined clients + */ + + /* mark us as waiting for the reference name from the client */ + pss->greet = 1; + pss->wsi = wsi; + lws_validity_confirmed(wsi); + + return 0; + + case LWS_CALLBACK_CLOSED: + /* + * a client has parted + */ + lws_dll2_remove(&pss->list); + lwsl_warn("%s: client %s left (%u)\n", __func__, + pss->proxy_path, + (unsigned int)vhd->clients.count); + lwsac_free(&pss->ac); + + /* let's kill the scraper connection accordingly, if still up */ + partner_pss = omc_lws_om_get_other_side_pss_client(vhd, pss); + if (partner_pss) + lws_wsi_close(partner_pss->wsi, LWS_TO_KILL_ASYNC); + break; + + case LWS_CALLBACK_RECEIVE: + if (pss->greet) { + pss->greet = 0; + lws_strnncpy(pss->proxy_path, (const char *)in, len, + sizeof(pss->proxy_path)); + + lws_validity_confirmed(wsi); + lwsl_notice("%s: received greet '%s'\n", __func__, + pss->proxy_path); + /* + * we need to add his pss to our list of configured, + * live, joined clients + */ + lws_dll2_add_tail(&pss->list, &vhd->clients); + return 0; + } + + /* + * He's sending us his results... let's collect chunks into the + * pss lwsac before worrying about anything else + */ + + if (lws_is_first_fragment(wsi)) + pss->tot = 0; + + lws_metrics_om_ac_stash(pss, (const char *)in, len); + + if (lws_is_final_fragment(wsi)) { + struct pss *partner_pss; + + lwsl_info("%s: ws side received complete msg\n", + __func__); + + /* the lwsac is complete */ + pss->walk = pss->ac; + partner_pss = omc_lws_om_get_other_side_pss_client(vhd, pss); + if (!partner_pss) { + lwsl_notice("%s: no partner A\n", __func__); + return -1; + } + + /* indicate to scraper side we want to issue now */ + + p = start; + if (lws_add_http_common_headers(partner_pss->wsi, HTTP_STATUS_OK, + "application/openmetrics-text; " + "version=1.0.0; charset=utf-8", + pss->tot, &p, end) || + lws_finalize_write_http_header(partner_pss->wsi, + start, &p, end)) + return -1; + + /* indicate to scraper side we want to issue now */ + + partner_pss->walk = pss->ac; + partner_pss->trigger = 1; + lws_callback_on_writable(partner_pss->wsi); + } + + return 0; + + case LWS_CALLBACK_SERVER_WRITEABLE: + if (!pss->trigger) + return 0; + + pss->trigger = 0; + + partner_pss = omc_lws_om_get_other_side_pss_client(vhd, pss); + if (!partner_pss) { + lwsl_err("%s: no partner\n", __func__); + return 0; + } + + lwsl_info("%s: sending trigger to client\n", __func__); + + *start = 'x'; + if (lws_write(wsi, start, 1, + (enum lws_write_protocol)LWS_WRITE_TEXT) < 0) + return 1; + + lws_validity_confirmed(wsi); + + return 0; + + default: + break; + } + + return lws_callback_http_dummy(wsi, reason, user, in, len); +} +#endif + +#if defined(LWS_WITH_CLIENT) && defined(LWS_ROLE_WS) + +/* 4) ws client that keeps wss connection up to metrics proxy ws server */ + +static int +callback_lws_openmetrics_prox_client(struct lws *wsi, + enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + unsigned char buf[1224], *start = buf + LWS_PRE, *p = start, + *end = buf + sizeof(buf) - 1, *ip; + struct vhd *vhd = (struct vhd *)lws_protocol_vh_priv_get( + lws_get_vhost(wsi), lws_get_protocol(wsi)); + struct lws_context *cx = lws_get_context(wsi); + struct pss *pss = (struct pss *)user; + unsigned int m, wm; + const char *cp; + char first; + + switch (reason) { + + case LWS_CALLBACK_PROTOCOL_INIT: + + lwsl_notice("%s: PROTOCOL_INIT on %s\n", __func__, + lws_vh_tag(lws_get_vhost(wsi))); + + + /* + * We get told what to do when we are bound to the vhost + */ + vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), + lws_get_protocol(wsi), sizeof(struct vhd)); + if (!vhd) + return 0; + + vhd->cx = cx; + vhd->vhost = lws_get_vhost(wsi); + + /* the proxy server uri */ + + if (lws_pvo_get_str(in, "ws-server-uri", &cp)) { + lwsl_err("%s: ws-server-uri pvo required\n", __func__); + + return 1; + } + lws_strncpy(vhd->ws_server_uri, cp, sizeof(vhd->ws_server_uri)); + + /* how we should be referenced at the proxy */ + + if (lws_pvo_get_str(in, "metrics-proxy-path", &cp)) { + lwsl_err("%s: metrics-proxy-path pvo required\n", __func__); + + return 1; + } + lws_strncpy(vhd->metrics_proxy_path, cp, sizeof(vhd->metrics_proxy_path)); + + /* the shared secret to authenticate us as allowed to join */ + + if (lws_pvo_get_str(in, "ba-secret", &cp)) { + lwsl_err("%s: ba-secret pvo required\n", __func__); + + return 1; + } + lws_strncpy(vhd->ba_secret, cp, sizeof(vhd->ba_secret)); + + lwsl_notice("%s: scheduling connect %s %s %s\n", __func__, + vhd->ws_server_uri, vhd->metrics_proxy_path, vhd->ba_secret); + + lws_validity_confirmed(wsi); + lws_sul_schedule(cx, 0, &vhd->sul, omc_connect_client, 1); + break; + + case LWS_CALLBACK_PROTOCOL_DESTROY: + if (vhd) + lws_sul_cancel(&vhd->sul); + break; + + case LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER: + { + unsigned char **pp = (unsigned char **)in, *pend = (*pp) + len; + char b[128]; + + /* authorize ourselves to the metrics proxy using basic auth */ + + if (lws_http_basic_auth_gen("metricsclient", vhd->ba_secret, + b, sizeof(b))) + break; + + if (lws_add_http_header_by_token(wsi, + WSI_TOKEN_HTTP_AUTHORIZATION, + (unsigned char *)b, + (int)strlen(b), pp, pend)) + return -1; + + break; + } + + case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: + lwsl_err("CLIENT_CONNECTION_ERROR: %s\n", + in ? (char *)in : "(null)"); + goto do_retry; + + case LWS_CALLBACK_CLIENT_ESTABLISHED: + lwsl_warn("%s: connected to ws metrics agg server\n", __func__); + pss->greet = 1; + lws_callback_on_writable(wsi); + lws_validity_confirmed(wsi); + return 0; + + case LWS_CALLBACK_CLIENT_CLOSED: + lwsl_notice("%s: client closed\n", __func__); + lwsac_free(&pss->ac); + goto do_retry; + + case LWS_CALLBACK_CLIENT_RECEIVE: + /* + * Proxy serverside sends us something to trigger us to create + * our metrics message and send it back over the ws link + */ + ome_prepare(cx, pss); + pss->walk = pss->ac; + lws_callback_on_writable(wsi); + lwsl_info("%s: dump requested\n", __func__); + break; + + case LWS_CALLBACK_CLIENT_WRITEABLE: + if (pss->greet) { + /* + * At first after establishing the we link, we send a + * message indicating to the metrics proxy how we + * should be referred to by the scraper to particularly + * select to talk to us + */ + lwsl_info("%s: sending greet '%s'\n", __func__, + vhd->metrics_proxy_path); + lws_strncpy((char *)start, vhd->metrics_proxy_path, + sizeof(buf) - LWS_PRE); + if (lws_write(wsi, start, + strlen(vhd->metrics_proxy_path), + LWS_WRITE_TEXT) < 0) + return 1; + + lws_validity_confirmed(wsi); + + pss->greet = 0; + return 0; + } + + if (!pss->walk) + return 0; + + /* + * We send the metrics dump in a single logical ws message, + * using ws fragmentation to split it around 1 mtu boundary + * and keep coming back until it's finished + */ + + first = pss->walk == pss->ac; + + do { + ip = (uint8_t *)pss->walk + + lwsac_sizeof(pss->walk == pss->ac) + LWS_PRE; + m = (unsigned int)((ip[0] << 8) | ip[1]); + + /* coverity */ + if (m > lwsac_get_tail_pos(pss->walk) - + lwsac_sizeof(pss->walk == pss->ac)) { + lwsl_err("%s: size blow\n", __func__); + return -1; + } + + if (lws_ptr_diff_size_t(end, p) < m) + break; + + memcpy(p, ip + 2, m); + p += m; + + pss->walk = lwsac_get_next(pss->walk); + } while (pss->walk); + + if (!lws_ptr_diff_size_t(p, start)) { + lwsl_err("%s: stuck\n", __func__); + return -1; + } + + wm = (unsigned int)lws_write_ws_flags(LWS_WRITE_TEXT, first, + !pss->walk); + + if (lws_write(wsi, start, lws_ptr_diff_size_t(p, start), + (enum lws_write_protocol)wm) < 0) { + lwsl_notice("%s: write fail\n", __func__); + return 1; + } + + lws_validity_confirmed(wsi); + lwsl_info("%s: forwarded %d\n", __func__, lws_ptr_diff(p, start)); + + if (!pss->walk) { + lwsl_info("%s: dump send completed\n", __func__); + lwsac_free(&pss->ac); + } else + lws_callback_on_writable(wsi); + + return 0; + + default: + break; + } + + return lws_callback_http_dummy(wsi, reason, user, in, len); + +do_retry: + if (!lws_retry_sul_schedule(cx, 0, &vhd->sul, &retry, + omc_connect_client, &vhd->retry_count)) + return 0; + + vhd->retry_count = 0; + lws_retry_sul_schedule(cx, 0, &vhd->sul, &retry, + omc_connect_client, &vhd->retry_count); + + return 0; +} +#endif + + +LWS_VISIBLE const struct lws_protocols lws_openmetrics_export_protocols[] = { +#if defined(LWS_WITH_SERVER) + { /* for scraper directly: http export on listen socket */ + "lws-openmetrics", + callback_lws_openmetrics_export, + sizeof(struct pss), + 1024, + }, + { /* for scraper via ws proxy: http export on listen socket */ + "lws-openmetrics-prox-agg", + callback_lws_openmetrics_prox_agg, + sizeof(struct pss), + 1024, + }, + { /* metrics proxy server side: ws server for clients to connect to */ + "lws-openmetrics-prox-server", + callback_lws_openmetrics_prox_server, + sizeof(struct pss), + 1024, + }, +#endif +#if defined(LWS_WITH_CLIENT) && defined(LWS_ROLE_WS) + { /* client to metrics proxy: ws client to connect to metrics proxy*/ + "lws-openmetrics-prox-client", + callback_lws_openmetrics_prox_client, + sizeof(struct pss), + 1024, + }, +#endif +}; + +LWS_VISIBLE const lws_plugin_protocol_t lws_openmetrics_export = { + .hdr = { + "lws OpenMetrics export", + "lws_protocol_plugin", + LWS_BUILD_HASH, + LWS_PLUGIN_API_MAGIC + }, + + .protocols = lws_openmetrics_export_protocols, + .count_protocols = LWS_ARRAY_SIZE(lws_openmetrics_export_protocols), +}; diff --git a/plugins/protocol_lws_server_status.c b/plugins/protocol_lws_server_status.c deleted file mode 100644 index 497f59efd..000000000 --- a/plugins/protocol_lws_server_status.c +++ /dev/null @@ -1,218 +0,0 @@ -/* - * libwebsockets-test-server - libwebsockets test implementation - * - * Written in 2010-2019 by Andy Green - * - * This file is made available under the Creative Commons CC0 1.0 - * Universal Public Domain Dedication. - * - * The person who associated a work with this deed has dedicated - * the work to the public domain by waiving all of his or her rights - * to the work worldwide under copyright law, including all related - * and neighboring rights, to the extent allowed by law. You can copy, - * modify, distribute and perform the work, even for commercial purposes, - * all without asking permission. - * - * The test apps are intended to be adapted for use in your code, which - * may be proprietary. So unlike the library itself, they are licensed - * Public Domain. - */ - -#define LWS_DLL -#define LWS_INTERNAL -#include -#include -#include -#include -#include -#include -#include - -struct lws_ss_filepath { - struct lws_ss_filepath *next; - char filepath[128]; -}; - -struct lws_ss_dumps { - char buf[32768]; - int length; -}; - -struct pss { - int ver; - int pos; -}; - -struct vhd { - struct lws_context *context; - struct lws_vhost *vhost; - const struct lws_protocols *protocol; - lws_sorted_usec_list_t sul; - int hide_vhosts; - int tow_flag; - int period_s; - int clients; - struct lws_ss_dumps d; - struct lws_ss_filepath *fp; -}; - -static const struct lws_protocols protocols[1]; - -static void -update(struct lws_sorted_usec_list *sul) -{ - struct vhd *v = lws_container_of(sul, struct vhd, sul); - struct lws_ss_filepath *fp; - char contents[256], pure[256], *p = v->d.buf + LWS_PRE, - *end = v->d.buf + sizeof(v->d.buf) - LWS_PRE - 1; - int n, first = 1, fd; - - p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "{\"i\":"); - p += lws_json_dump_context(v->context, p, lws_ptr_diff(end, p), - v->hide_vhosts); - p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), ", \"files\": ["); - - fp = v->fp; - while (fp) { - if (!first) - p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), ","); - - strcpy(pure, "(unknown)"); - fd = lws_open(fp->filepath, LWS_O_RDONLY); - if (fd >= 0) { - n = (int)read(fd, contents, sizeof(contents) - 1); - close(fd); - if (n >= 0) { - contents[n] = '\0'; - lws_json_purify(pure, contents, sizeof(pure), NULL); - } - } - - p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), - "{\"path\":\"%s\",\"val\":\"%s\"}", - fp->filepath, pure); - first = 0; - - fp = fp->next; - } - p += lws_snprintf(p, lws_ptr_diff_size_t(end, p), "]}"); - v->d.length = lws_ptr_diff(p, (v->d.buf + LWS_PRE)); - - lws_callback_on_writable_all_protocol(v->context, &protocols[0]); - - lws_sul_schedule(v->context, 0, &v->sul, update, v->period_s * LWS_US_PER_SEC); -} - -static int -callback_lws_server_status(struct lws *wsi, enum lws_callback_reasons reason, - void *user, void *in, size_t len) -{ - const struct lws_protocol_vhost_options *pvo = - (const struct lws_protocol_vhost_options *)in; - struct vhd *v = (struct vhd *) - lws_protocol_vh_priv_get(lws_get_vhost(wsi), - lws_get_protocol(wsi)); - struct lws_ss_filepath *fp, *fp1, **fp_old; - int m; - - switch (reason) { - - case LWS_CALLBACK_ESTABLISHED: - lwsl_info("%s: LWS_CALLBACK_ESTABLISHED\n", __func__); - if (!v->clients++) { - lws_sul_schedule(lws_get_context(wsi), 0, &v->sul, update, 1); - lwsl_info("%s: starting updates\n", __func__); - } - break; - - case LWS_CALLBACK_CLOSED: - if (!--v->clients) - lwsl_notice("%s: stopping updates\n", __func__); - - break; - - case LWS_CALLBACK_PROTOCOL_INIT: /* per vhost */ - if (v) - break; - - lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), - lws_get_protocol(wsi), - sizeof(struct vhd)); - v = (struct vhd *)lws_protocol_vh_priv_get(lws_get_vhost(wsi), - lws_get_protocol(wsi)); - - fp_old = &v->fp; - - while (pvo) { - if (!strcmp(pvo->name, "hide-vhosts")) - v->hide_vhosts = atoi(pvo->value); - if (!strcmp(pvo->name, "update-ms")) - v->period_s = (atoi(pvo->value) + 500) / 1000; - else - v->period_s = 5; - if (!strcmp(pvo->name, "filepath")) { - fp = malloc(sizeof(*fp)); - if (!fp) - return -1; - fp->next = NULL; - lws_snprintf(&fp->filepath[0], - sizeof(fp->filepath), "%s", - pvo->value); - *fp_old = fp; - fp_old = &fp->next; - } - pvo = pvo->next; - } - v->context = lws_get_context(wsi); - v->vhost = lws_get_vhost(wsi); - v->protocol = lws_get_protocol(wsi); - - lws_sul_schedule(lws_get_context(wsi), 0, &v->sul, update, 1); - break; - - case LWS_CALLBACK_PROTOCOL_DESTROY: /* per vhost */ - if (!v) - break; - fp = v->fp; - while (fp) { - fp1= fp->next; - free(fp); - fp = fp1; - } - break; - - case LWS_CALLBACK_SERVER_WRITEABLE: - m = lws_write(wsi, (unsigned char *)v->d.buf + LWS_PRE, - (size_t)v->d.length, LWS_WRITE_TEXT); - if (m < 0) - return -1; - break; - - default: - break; - } - - return 0; -} - -static const struct lws_protocols protocols[] = { - { - "lws-server-status", - callback_lws_server_status, - sizeof(struct pss), - 1024, - }, -}; - -LWS_VISIBLE const lws_plugin_protocol_t lws_server_status = { - .hdr = { - "lws server status", - "lws_protocol_plugin", - LWS_PLUGIN_API_MAGIC - }, - - .protocols = protocols, - .count_protocols = LWS_ARRAY_SIZE(protocols), - .extensions = NULL, - .count_extensions = 0, -}; diff --git a/plugins/server-status.html b/plugins/server-status.html deleted file mode 100644 index 0c0386335..000000000 --- a/plugins/server-status.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - LWS Server Status - - - -
-
- - - - -
-Server status
-
...
-
-
-
- - diff --git a/plugins/server-status.js b/plugins/server-status.js deleted file mode 100644 index ddc7796df..000000000 --- a/plugins/server-status.js +++ /dev/null @@ -1,249 +0,0 @@ -(function() { - -/* - * We display untrusted stuff in html context... reject anything - * that has HTML stuff in it - */ - -function san(s) -{ - if (s.search("<") !== -1) - return "invalid string"; - - return s; -} - -function humanize(s) -{ - var i = parseInt(s, 10); - - if (i >= (1024 * 1024 * 1024)) - return (i / (1024 * 1024 * 1024)).toFixed(3) + "Gi"; - - if (i >= (1024 * 1024)) - return (i / (1024 * 1024)).toFixed(3) + "Mi"; - - if (i > 1024) - return (i / 1024).toFixed(3) + "Ki"; - - return s; -} - -function get_appropriate_ws_url() -{ - var pcol; - var u = document.URL; - - /* - * We open the websocket encrypted if this page came on an - * https:// url itself, otherwise unencrypted - */ - - if (u.substring(0, 5) === "https") { - pcol = "wss://"; - u = u.substr(8); - } else { - pcol = "ws://"; - if (u.substring(0, 4) === "http") - u = u.substr(7); - } - - u = u.split("/"); - - /* + "/xxx" bit is for IE10 workaround */ - - return pcol + u[0] + "/xxx"; -} - - - var socket_status, jso, s; - -function ws_open_server_status() -{ - socket_status = new WebSocket(get_appropriate_ws_url(), - "lws-server-status"); - - try { - socket_status.onopen = function() { - document.getElementById("title").innerHTML = "Server Status (Active)"; - lws_gray_out(false); - }; - - socket_status.onmessage =function got_packet(msg) { - var u, ci, n; - // document.getElementById("json").innerHTML = "
"+msg.data+"
"; - if (msg.data.length < 100) - return; - jso = JSON.parse(msg.data); - u = parseInt(san(jso.i.uptime), 10); - - if (parseInt(jso.i.contexts[0].deprecated, 10) === 0) - s = "
"; - else - s = ""; - - for (ci = 0; ci < jso.i.contexts.length; ci++) { - - if (parseInt(jso.i.contexts[ci].deprecated, 10) === 0) - s += ""; - - } // context - s = s + "
"; - s += - "Server" + - "Server Version: " + - san(jso.i.version) + "
" + - "Host Uptime: " + - ((u / (24 * 3600)) | 0) + "d " + - (((u % (24 * 3600)) / 3600) | 0) + "h " + - (((u % 3600) / 60) | 0) + "m"; - if (jso.i.l1) - s = s + ", Host Load: " + san(jso.i.l1) + " "; - if (jso.i.l2) - s = s + san(jso.i.l2) + " "; - if (jso.i.l3) - s = s + san(jso.i.l3); - if (jso.i.l1) - s =s + ""; - - if (jso.i.statm) { - var sm = jso.i.statm.split(" "); - s += ", Virt stack + heap Usage: " + - humanize(parseInt(sm[5], 10) * 4096) + "B"; - } - s += ", lws heap usage: " + - humanize(jso.i.heap) + "B"; - - - for (n = 0; n < jso.files.length; n++) { - s += "
" + san(jso.files[n].path) + ":
" + san(jso.files[n].val); - } - s += "
" + - "Active Context"; - else - s += "
" + - "Deprecated Context " + ci + ""; - - u = parseInt(san(jso.i.contexts[ci].context_uptime), 10); - s += "Server Uptime: " + - ((u / (24 * 3600)) | 0) + "d " + - (((u % (24 * 3600)) / 3600) | 0) + "h " + - (((u % 3600) / 60) | 0) + "m"; - - s = s + - "
" + - "Tagged objects alive: " + san(jso.i.contexts[ci].wsi_alive) + "
" + - "Total Rx: " + humanize(san(jso.i.contexts[ci].rx)) +"B, " + - "Total Tx: " + humanize(san(jso.i.contexts[ci].tx)) +"B
" + - - "CONNECTIONS: HTTP/1.x: " + san(jso.i.contexts[ci].h1_conn) +", " + - "Websocket: " + san(jso.i.contexts[ci].ws_upg) +", " + - "H2 upgrade: " + san(jso.i.contexts[ci].h2_upg) +", " + - "H2 ALPN: " + san(jso.i.contexts[ci].h2_alpn) +", " + - "Rejected: " + san(jso.i.contexts[ci].rejected) +"
" + - - "TRANSACTIONS: HTTP/1.x: " + san(jso.i.contexts[ci].h1_trans) + ", " + - "H2: " + san(jso.i.contexts[ci].h2_trans) +", " + - "Total H2 substreams: " + san(jso.i.contexts[ci].h2_subs) +"
" + - - "CGI: alive: " + san(jso.i.contexts[ci].cgi_alive) + ", " + - "spawned: " + san(jso.i.contexts[ci].cgi_spawned) + - ""; - - for (n = 0; n < jso.i.contexts[ci].pt.length; n++) { - - if (parseInt(jso.i.contexts[ci].deprecated, 10) === 0) - s += ""; - - } - for (n = 0; n < jso.i.contexts[ci].vhosts.length; n++) { - if (parseInt(jso.i.contexts[ci].deprecated, 10) === 0) - s += ""; - } - - s += "
  service thread " + (n + 1); - else - s += "
  service thread " + (n + 1); - s += "" + - "fds: " + san(jso.i.contexts[ci].pt[n].fds_count) + " / " + - san(jso.i.contexts[ci].pt_fd_max) + ", "; - s = s + "ah pool: " + san(jso.i.contexts[ci].pt[n].ah_pool_inuse) + " / " + - san(jso.i.contexts[ci].ah_pool_max) + ", " + - "ah waiting list: " + san(jso.i.contexts[ci].pt[n].ah_wait_list); - - s = s + "
  vhost " + (n + 1); - else - s += "
  vhost " + (n + 1); - s += ""; - if (jso.i.contexts[ci].vhosts[n].use_ssl === "1") - s = s + "https://"; - else - s = s + "http://"; - s = s + san(jso.i.contexts[ci].vhosts[n].name) + ":" + - san(jso.i.contexts[ci].vhosts[n].port) + ""; - if (jso.i.contexts[ci].vhosts[n].sts === "1") - s = s + " (STS)"; - s = s +"
" + - - "Total Rx: " + humanize(san(jso.i.contexts[ci].vhosts[n].rx)) +"B, " + - "Total Tx: " + humanize(san(jso.i.contexts[ci].vhosts[n].tx)) +"B
" + - - "CONNECTIONS: HTTP/1.x: " + san(jso.i.contexts[ci].vhosts[n].h1_conn) +", " + - "Websocket: " + san(jso.i.contexts[ci].vhosts[n].ws_upg) +", " + - "H2 upgrade: " + san(jso.i.contexts[ci].vhosts[n].h2_upg) +", " + - "H2 ALPN: " + san(jso.i.contexts[ci].vhosts[n].h2_alpn) +", " + - "Rejected: " + san(jso.i.contexts[ci].vhosts[n].rejected) +"
" + - - "TRANSACTIONS: HTTP/1.x: " + san(jso.i.contexts[ci].vhosts[n].h1_trans) + ", " + - "H2: " + san(jso.i.contexts[ci].vhosts[n].h2_trans) +", " + - "Total H2 substreams: " + san(jso.i.contexts[ci].vhosts[n].h2_subs) +"
"; - - if (jso.i.contexts[ci].vhosts[n].mounts) { - s = s + ""; - - var m; - for (m = 0; m < jso.i.contexts[ci].vhosts[n].mounts.length; m++) { - s = s + ""; - } - s = s + "
MountpointOriginCache Policy
"; - s = s + "" + san(jso.i.contexts[ci].vhosts[n].mounts[m].mountpoint) + - "" + - san(jso.i.contexts[ci].vhosts[n].mounts[m].origin) + - ""; - if (parseInt(san(jso.i.contexts[ci].vhosts[n].mounts[m].cache_max_age), 10)) - s = s + "max-age: " + - san(jso.i.contexts[ci].vhosts[n].mounts[m].cache_max_age) + - ", reuse: " + - san(jso.i.contexts[ci].vhosts[n].mounts[m].cache_reuse) + - ", reval: " + - san(jso.i.contexts[ci].vhosts[n].mounts[m].cache_revalidate) + - ", inter: " + - san(jso.i.contexts[ci].vhosts[n].mounts[m].cache_intermediaries); - s = s + "
"; - } - s = s + "
"; - - document.getElementById("conninfo").innerHTML = s; - }; - - socket_status.onclose = function(){ - document.getElementById("title").innerHTML = "Server Status (Disconnected)"; - lws_gray_out(true,{"zindex":"499"}); - }; - } catch(exception) { - alert("

Error" + exception); - } -} - -/* stuff that has to be delayed until all the page assets are loaded */ - -window.addEventListener("load", function() { - - lws_gray_out(true,{"zindex":"499"}); - - ws_open_server_status(); - -}, false); - -}()); -