%%% @doc Metrics instrumentation for masque tunnels. %%% %%% Uses `instrument_meter' to track tunnel lifecycle, throughput, %%% and rejections. Call `setup/0' from application start. Instruments %%% are stored in `persistent_term' for zero-overhead lookups. -module(masque_metrics). -export([setup/0, tunnel_opened/1, tunnel_closed/2, tunnel_rejected/1, bytes_in/2, bytes_out/2]). %% Simple counters for CONNECT-IP plumbing. These intentionally do %% NOT use `instrument_meter' - they are a lightweight surface for %% downstream consumers (TUN/router, metrics scrapers) and tests to %% read directly via `counters:get/2'. `setup_ip_counters/0' is split %% out from `setup/0' so test environments without the meter system %% running can still exercise the counter API. -export([setup_ip_counters/0, ip_drop_inc/1, ip_drop_count/1, ip_drop_reasons/0, ip_assign_inc/0, ip_assigned_count/0, ip_release_inc/0, ip_released_count/0, ip_advertise_inc/0, ip_advertised_count/0]). -spec setup() -> ok. setup() -> setup_ip_counters(), Meter = instrument_meter:get_meter(<<"masque">>), persistent_term:put(masque_tunnels_total, instrument_meter:create_counter( Meter, <<"masque.tunnels.total">>, #{description => <<"Total tunnels opened">>})), persistent_term:put(masque_tunnels_active, instrument_meter:create_up_down_counter( Meter, <<"masque.tunnels.active">>, #{description => <<"Currently active tunnels">>})), persistent_term:put(masque_tunnels_rejected, instrument_meter:create_counter( Meter, <<"masque.tunnels.rejected">>, #{description => <<"Tunnels rejected by policy">>})), persistent_term:put(masque_bytes_in, instrument_meter:create_counter( Meter, <<"masque.bytes.in">>, #{description => <<"Bytes received from clients">>})), persistent_term:put(masque_bytes_out, instrument_meter:create_counter( Meter, <<"masque.bytes.out">>, #{description => <<"Bytes sent to clients">>})), persistent_term:put(masque_tunnel_duration, instrument_meter:create_histogram( Meter, <<"masque.tunnel.duration_ms">>, #{description => <<"Tunnel duration in milliseconds">>})), ok. %% @doc Idempotent allocator for the IP-side simple counters. Safe to %% call multiple times; only the first call wins (subsequent calls %% keep the existing reference so counts accumulated from earlier %% callers are preserved). -spec setup_ip_counters() -> ok. setup_ip_counters() -> case persistent_term:get(masque_ip_drop_counters, undefined) of undefined -> Ref = counters:new(length(ip_drop_reasons()), [write_concurrency]), persistent_term:put(masque_ip_drop_counters, Ref); _ -> ok end, case persistent_term:get(masque_ip_lifecycle_counters, undefined) of undefined -> %% [assigned, released, advertised] LRef = counters:new(3, [write_concurrency]), persistent_term:put(masque_ip_lifecycle_counters, LRef); _ -> ok end, ok. -spec tunnel_opened(map()) -> ok. tunnel_opened(Attrs) -> instrument_meter:add( persistent_term:get(masque_tunnels_total), 1, Attrs), instrument_meter:add( persistent_term:get(masque_tunnels_active), 1, Attrs). -spec tunnel_closed(number(), map()) -> ok. tunnel_closed(DurationMs, Attrs) -> instrument_meter:add( persistent_term:get(masque_tunnels_active), -1, Attrs), instrument_meter:record( persistent_term:get(masque_tunnel_duration), DurationMs, Attrs). -spec tunnel_rejected(map()) -> ok. tunnel_rejected(Attrs) -> instrument_meter:add( persistent_term:get(masque_tunnels_rejected), 1, normalise_attrs(Attrs)). %% `instrument_meter' labels must be scalars (atom/binary/integer); %% flatten any tuple-shaped reasons (`{other, 401}') into a binary. normalise_attrs(Attrs) when is_map(Attrs) -> maps:map(fun(_K, V) -> normalise_value(V) end, Attrs). normalise_value(V) when is_atom(V); is_binary(V); is_integer(V) -> V; normalise_value(V) -> iolist_to_binary(io_lib:format("~p", [V])). -spec bytes_in(non_neg_integer(), map()) -> ok. bytes_in(Bytes, Attrs) -> instrument_meter:add( persistent_term:get(masque_bytes_in), Bytes, Attrs). -spec bytes_out(non_neg_integer(), map()) -> ok. bytes_out(Bytes, Attrs) -> instrument_meter:add( persistent_term:get(masque_bytes_out), Bytes, Attrs). %%==================================================================== %% IP drop counters - simple, instrument_meter-free. %%==================================================================== %% Ordered list of recognised drop reasons. The position in this list %% is the `counters' index for that reason. `other' captures any %% reason not in this list so a downstream consumer's custom drop %% reasons still get counted. -spec ip_drop_reasons() -> [atom()]. ip_drop_reasons() -> [bcp38, scope_target, scope_ipproto, malformed, forward_drop, ttl_zero, mtu_exceeded, other]. -spec ip_drop_inc(atom()) -> ok. ip_drop_inc(Reason) -> case persistent_term:get(masque_ip_drop_counters, undefined) of undefined -> ok; %% setup/0 not called yet (e.g. in tests) Ref -> counters:add(Ref, reason_index(Reason), 1) end. -spec ip_drop_count(atom()) -> non_neg_integer(). ip_drop_count(Reason) -> case persistent_term:get(masque_ip_drop_counters, undefined) of undefined -> 0; Ref -> counters:get(Ref, reason_index(Reason)) end. reason_index(Reason) -> case index_of(Reason, ip_drop_reasons(), 1) of not_found -> index_of(other, ip_drop_reasons(), 1); I -> I end. index_of(_X, [], _I) -> not_found; index_of(X, [X | _], I) -> I; index_of(X, [_ | T], I) -> index_of(X, T, I + 1). %%==================================================================== %% IP lifecycle counters - assign / release / advertise. %%==================================================================== -spec ip_assign_inc() -> ok. ip_assign_inc() -> lifecycle_inc(1). -spec ip_release_inc() -> ok. ip_release_inc() -> lifecycle_inc(2). -spec ip_advertise_inc() -> ok. ip_advertise_inc() -> lifecycle_inc(3). -spec ip_assigned_count() -> non_neg_integer(). ip_assigned_count() -> lifecycle_get(1). -spec ip_released_count() -> non_neg_integer(). ip_released_count() -> lifecycle_get(2). -spec ip_advertised_count() -> non_neg_integer(). ip_advertised_count() -> lifecycle_get(3). lifecycle_inc(Idx) -> case persistent_term:get(masque_ip_lifecycle_counters, undefined) of undefined -> ok; Ref -> counters:add(Ref, Idx, 1) end. lifecycle_get(Idx) -> case persistent_term:get(masque_ip_lifecycle_counters, undefined) of undefined -> 0; Ref -> counters:get(Ref, Idx) end.