Metrics, Prometheus and statsd¶
Keep counters, gauges and histograms under Envoy's stat names and tags, and read them as Prometheus
text, Envoy's /stats dump or JSON, or push them to statsd and DogStatsD. Use this page to turn
metrics on and understand names, tags and clusters; every stat is listed in the stats
reference.
Metrics (Envoy-compatible stats)¶
With metrics on, the proxy keeps counters, gauges and histograms under Envoy's stat names, so
dashboards and alerts written for Envoy read them: http.<name>.downstream_rq_5xx,
cluster.<cluster>.upstream_cx_connect_fail, listener.<address>.downstream_cx_active, and so on.
They live in a StatsRegistry (package org.microproxy.stats); StatsFormatter renders them as
Prometheus's text exposition format, Envoy's /stats text dump, or its JSON form, and
StatsdSink pushes them to statsd. The admin interface serves them over HTTP
(/stats, /stats/prometheus).
StatsRegistry stats = StatsRegistry.builder().prefix("envoy").build(); // or .withMetrics(true)
HttpProxyServer proxy = MicroProxy.bootstrap().withName("edge").withStats(stats).start();
...
String scrape = StatsFormatter.prometheus(proxy.getStats());
// # TYPE envoy_http_downstream_rq_xx counter
// envoy_http_downstream_rq_xx{envoy_http_conn_manager_prefix="edge",envoy_response_code_class="2"} 1042
// envoy_http_downstream_rq_xx{envoy_http_conn_manager_prefix="edge",envoy_response_code_class="5"} 3
StatsdSink sink = StatsdSink.builder(stats, "127.0.0.1:8125").start(); // every 5 s; close() stops it
java -jar microproxy.jar --port 8080 --name edge --metrics
java -jar microproxy.jar --port 8080 --statsd 127.0.0.1:8125 # implies --metrics
java -jar microproxy.jar --port 8080 --dogstatsd 127.0.0.1:8125 --stats-prefix microproxy
| flag | property | meaning |
|---|---|---|
--metrics |
metrics |
keep the stats |
--statsd <host:port> |
push them to statsd over UDP every 5 seconds, and once more when the proxy stops (implies --metrics) |
|
--dogstatsd <host:port> |
the same with DogStatsD tags (name:1|c|#tag:value) and tag-extracted names |
|
--stats-prefix <prefix> |
stats_prefix |
prefix of exported names: envoy (the default) gives envoy_http_downstream_rq_total and envoy.http.edge.downstream_rq_total; microproxy gives microproxy_... |
--stats-max-clusters <n> |
clusters with stats of their own, besides the fixed ones (default 100) | |
--stats-cluster-lru |
past that, evict the least recently used cluster's stats rather than counting new ones in other |
|
--stats-per-host |
a cluster per server of direct connections (host_port) instead of one direct cluster |
|
--qlog-dir <dir> |
qlog_dir |
write a qlog file per QUIC connection into <dir> (qlog) |
--qlog-max-files <n> |
qlog_max_files |
qlog files the directory keeps; past it, the oldest are deleted (default 1000) |
Names and tags¶
Every stat has Envoy's flat name, with its values in it, which the text dump and
statsd show. When it is registered, Envoy's tag extraction takes those values out as tags, which
Prometheus shows as labels on the tag-extracted name: cluster.direct.upstream_rq_2xx is
envoy_cluster_upstream_rq_xx{envoy_cluster_name="direct",envoy_response_code_class="2"}. The
tags are Envoy's envoy_http_conn_manager_prefix (the proxy's name, withName, --name),
envoy_cluster_name, envoy_listener_address (0.0.0.0_8080, [__1]_8080) and
envoy_response_code_class, and MicroProxy's microproxy_response_flag, plus those of the
per-protocol stats: microproxy_direction (downstream, upstream), microproxy_quic_scope,
microproxy_quic_error, microproxy_congestion_controller, microproxy_drop_reason,
microproxy_socks_version, microproxy_socks_reply, microproxy_reject_reason,
microproxy_tunnel_kind, microproxy_close_code, microproxy_hook, and for scripts'
calls microproxy_script, microproxy_script_hook and microproxy_budget. Histograms are in
milliseconds, with Envoy's buckets (0.5, 1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000,
30000, 60000, 300000, 600000, 1800000, 3600000), shown as _bucket{le="..."}, _sum and _count.
Clusters and cardinality¶
A forward proxy's servers are any number, so they are not clusters
by default. A request's cluster is direct for a direct connection, the chained proxy for one
through a chained proxy (host_port of its address, such as proxy_example_com_3128), cache
for a response from the HTTP cache, and replay for one from server replay. With
perHostClusters(true) (--stats-per-host) each server of a direct connection is a cluster of its
own (api_example_com_443). Chained proxies and servers together are bounded by maxClusters
(--stats-max-clusters, default 100): past it, new ones count in the cluster other, or, with
clusterOverflow(Overflow.LRU) (--stats-cluster-lru), the least recently used cluster loses its
stats (which start from zero if it comes back). A cluster is looked up once per server
connection, never per request.
Script stats¶
A script's stats module makes metrics of its own, named script.<name>:
BLOCKED = stats.counter("blocked") # looked up once, at load time
def on_request(req, ctx):
stats.histogram("path_length").record(len(req.path))
if req.path.startswith("/admin"):
BLOCKED.inc()
return response(403)
A script makes at most maxScriptStats of them (100 by default); past that, new names count
nothing (a warning is logged once), so names made from requests cannot grow the registry without
bound. Without metrics, the module's metrics count nothing. Counters have inc(n=1) and value,
gauges set(v), inc(n=1), dec(n=1) and value, histograms record(x) and count.
Cost¶
Each metric is resolved once and kept in a field: per proxy for the http, listener,
per-protocol, hook and fixed cluster stats, per server connection for its cluster, per client
connection for its own. Counting a request is a few LongAdder adds and a histogram bucket search;
nothing is allocated per request, and nothing goes through ActivityTrackers. Nothing is counted
in the registry per QUIC packet: a connection counts in plain fields of its own, added once when
it closes, and CONNECT-UDP flows and transparent QUIC relays count their datagrams the same way.
Without metrics the registry is StatsRegistry.NOOP, whose metrics do nothing, and the call sites
that would read a clock (the hooks', for one) check first; AllocationTest (HTTP/1, and HTTP/2
and HTTP/3 with metrics against without) and StatsFastPathTest guard both.
Limitations¶
QUIC connections are counted when they close, so quic.* counters lag a
long-lived connection (connections_active and QuicConnection.stats() are live). The proxy's own
connections to servers do not move, so quic.upstream.migrations counts only moves to a server's
preferred address, and a relayed transparent flow whose client moves is no QUIC connection of the
proxy's: it counts in none of these. http2.*.outbound_flood and requests_rejected_with_underscores_in_headers have no
equivalent: writes block rather than queue, and underscores in header names are not refused.
Envoy's upstream_cx_destroy_* and
downstream_cx_destroy_* tell who closed first only where the proxy saw it (an end of stream
before a request, a server that closed its connection). Statsd gets a histogram as one timer per
flush, the mean of its new values sampled at 1/n, which keeps the count and mean, not the spread.