Malachi.Metrics (malachi v0.22.7)

View Source

Real-time operational and security metrics kept in ETS (atomic update_counter), plus a periodically-sampled system snapshot and its recent history.

The increment_*/record_* functions bump counters on the hot path (fast, lock-free), rate-limit and connection-limit blocks, auth failures and account lockouts, audit events, dashboard-auth outcomes, and TLS handshakes; get_system_metrics/0 reads the live BEAM snapshot (memory, processes, io) folded together with those counters, and get_history/1 returns the recent snapshots. A counter is created on first touch, so callers never need to initialize one.

Summary

Functions

Returns a specification to start this module under a supervisor.

The recent per-second history samples for the last seconds (default 60).

A snapshot of system-wide metrics (memory, processes, connections, auth) for the dashboard.

Increment account lockout counter.

Increment account lockout blocked attempt counter.

Increment audit event counter by event type.

Increment connection limit blocked counter.

Increment dashboard authentication blocked (rate limited) counter.

Increment dashboard authentication failure counter.

Increment dashboard authentication success counter.

Increment failed authentication attempt counter.

Increment rate limit blocked counter for specific action.

Increment TLS handshake failure counter.

Increment TLS handshake success counter.

Records an authentication attempt with result (:ok / :error).

Records count records consumed (from the consume telemetry event).

Records that a heal pass finished the seal for count segments whose store was already fenced, which is what lets their ranges take writes again. See record_orphaned_fence/0 for why both halves are counted.

Records one group-commit flush (from the storage flush telemetry event): duration_us into the latency histogram, plus the bytes and records it made durable.

Records a segment that failed checksum verification, by reason (:bad_crc, :bad_magic, :incomplete). Non-zero means data at rest is damaged somewhere on this node, which is otherwise a silent condition: a damaged copy serves short reads without any error.

Records one segment whose store was fenced while recording its seal in the control plane failed, so it is closed to writes while the metadata still calls it active and its range accepts nothing.

Records count records and bytes value bytes produced (from the produce telemetry event).

Records count ticks on which the broker's control plane reconcile did not complete, by reason (:skipped, :down or :timeout; anything else is counted as :other, so the exported label set stays closed). See Malachi.Telemetry.reconcile_degraded/1.

Records a quorum replication with result (:ok / :no_quorum).

Records what one expire answered (from the retention expire telemetry event): an expired segment (:ok) counts itself and its bytes under topic; a segment already gone (:no_such_segment) counts nowhere, since an earlier sweep expired it; anything else is a refusal counted under its reply.

Records count replica directories an expire of topic left behind, because the replica holding them did not answer its delete (from the retention orphan telemetry event). The counterpart of what the orphan sweeper reclaims: the two together say whether the sweeper is keeping up.

Records count replica directories the orphan sweeper reclaimed (from the retention orphan telemetry event). Unlabeled: a reclaimed directory is named by the sweeper's log line, and the segment it belonged to is exactly what the control plane no longer knows.

Records one skip (from the retention skip telemetry event): a reader of group on topic was moved past offsets offsets no longer stored. Counted as one event and as the offsets, under the reader's origin and the skip's span.

Records one retention sweep's duration (from the retention sweep telemetry event). The histogram exists from this server's first start, before the reporter that calls this is attached.

Records that a sweep found topic bound to a policy name it could not resolve, and expired nothing of it. Counted per sweep, so a name that stays unresolved keeps the series moving: the disk it holds is invisible otherwise, and the fix is a binding an operator has to make.

Records one integrity scrub pass: how many segments it verified, repaired, and could not repair. A verified total that stops advancing is how an operator sees that the scrub itself has stopped, which the failure counters alone cannot show (they stay at zero both when all is well and when nothing is checking). The unrepairable total is the one that calls for a human: it counts damage the cluster could not heal by itself, which on a single node is every finding there is.

Records copies copies of sealed segments settled against the recorded length, by the heal pass or by the node that trims, and tells them apart by result (:fenced, :trimmed, :failed or :recorded_size_below), and the records a trim gave up.

Records a storage operation that failed on a segment's copy (Malachi.Cluster.ReplicationServer took the copy out of service), bucketed by reason: :enospc (a full volume), :eio (a failing device), :eacces (a permission problem), and :other for anything else. Buckets rather than the raw reason because a Prometheus label must come from a closed set, and these three are the ones that call for different operator actions.

Record the negotiated TLS version for a connection.

Records one message a long-lived server had no clause for (see Malachi.UnexpectedMessage), by the server's label and the kind it arrived as. A label outside Malachi.UnexpectedMessage.servers/0 is counted as :other, so the exported label set stays closed whatever a caller passes.

How many {topic, group} pairs have their own retention skip label on this node.

The retention counters as the Prometheus exporter needs them: every skip series (topic, reader, group, origin, span, with its events and offsets), the expired segments and bytes per topic, the refusals per reply (every known reply, zero included), the replica directories expiries left behind per topic, the ones the sweeper reclaimed, and the sweep duration histogram in the shape of storage_flush_histogram/0 (its count is the number of sweeps). Read only at scrape time.

Starts the metrics server (owns the ETS counter table).

The storage flush histogram as the Prometheus exporter needs it: the count of flushes at or below each of Malachi.Histogram.edges/0, the total count and the sum in microseconds, the durability totals, and created, the Unix time in seconds when this node's histogram began.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

get_history(seconds \\ 60)

The recent per-second history samples for the last seconds (default 60).

get_system_metrics()

A snapshot of system-wide metrics (memory, processes, connections, auth) for the dashboard.

increment_account_lockout()

Increment account lockout counter.

increment_account_lockout_blocked()

Increment account lockout blocked attempt counter.

increment_audit_event(event_type)

Increment audit event counter by event type.

increment_connection_limit_blocked()

Increment connection limit blocked counter.

increment_dashboard_auth_blocked()

Increment dashboard authentication blocked (rate limited) counter.

increment_dashboard_auth_failed()

Increment dashboard authentication failure counter.

increment_dashboard_auth_success()

Increment dashboard authentication success counter.

increment_failed_auth_attempt()

Increment failed authentication attempt counter.

increment_rate_limit_blocked(action)

Increment rate limit blocked counter for specific action.

increment_tls_handshake_failed()

Increment TLS handshake failure counter.

increment_tls_handshake_success()

Increment TLS handshake success counter.

record_auth(result)

Records an authentication attempt with result (:ok / :error).

record_consume(count)

Records count records consumed (from the consume telemetry event).

record_fences_reconciled(count)

Records that a heal pass finished the seal for count segments whose store was already fenced, which is what lets their ranges take writes again. See record_orphaned_fence/0 for why both halves are counted.

record_flush(duration_us, bytes, records)

@spec record_flush(non_neg_integer(), non_neg_integer(), non_neg_integer()) :: :ok

Records one group-commit flush (from the storage flush telemetry event): duration_us into the latency histogram, plus the bytes and records it made durable.

Four lock-free adds, not one atomic write: a scrape that lands between them can see a flush in the count before its duration is in the sum, which skews a windowed mean by at most one flush. A no-op when Malachi.Metrics never started, so a store used on its own does not need it.

record_integrity_failure(reason)

Records a segment that failed checksum verification, by reason (:bad_crc, :bad_magic, :incomplete). Non-zero means data at rest is damaged somewhere on this node, which is otherwise a silent condition: a damaged copy serves short reads without any error.

record_orphaned_fence()

Records one segment whose store was fenced while recording its seal in the control plane failed, so it is closed to writes while the metadata still calls it active and its range accepts nothing.

Counted separately from record_fences_reconciled/1 because it is the RATIO that carries the meaning: a detection that a later pass reconciles is a split that recovered, and a detection nothing reconciles is a range that is stuck. One counter could not tell those apart.

record_produce(count, bytes)

Records count records and bytes value bytes produced (from the produce telemetry event).

record_reconcile_degraded(reason, count \\ 1)

Records count ticks on which the broker's control plane reconcile did not complete, by reason (:skipped, :down or :timeout; anything else is counted as :other, so the exported label set stays closed). See Malachi.Telemetry.reconcile_degraded/1.

record_replication(result)

Records a quorum replication with result (:ok / :no_quorum).

record_retention_expire(topic, bytes, reply)

@spec record_retention_expire(String.t(), non_neg_integer(), atom()) :: :ok

Records what one expire answered (from the retention expire telemetry event): an expired segment (:ok) counts itself and its bytes under topic; a segment already gone (:no_such_segment) counts nowhere, since an earlier sweep expired it; anything else is a refusal counted under its reply.

record_retention_orphan_left(topic, count)

@spec record_retention_orphan_left(String.t(), non_neg_integer()) :: :ok

Records count replica directories an expire of topic left behind, because the replica holding them did not answer its delete (from the retention orphan telemetry event). The counterpart of what the orphan sweeper reclaims: the two together say whether the sweeper is keeping up.

record_retention_orphan_removed(count)

@spec record_retention_orphan_removed(non_neg_integer()) :: :ok

Records count replica directories the orphan sweeper reclaimed (from the retention orphan telemetry event). Unlabeled: a reclaimed directory is named by the sweeper's log line, and the segment it belonged to is exactly what the control plane no longer knows.

record_retention_skip(topic, group, origin, span, offsets)

@spec record_retention_skip(
  String.t(),
  String.t() | nil,
  atom(),
  atom(),
  non_neg_integer()
) :: :ok

Records one skip (from the retention skip telemetry event): a reader of group on topic was moved past offsets offsets no longer stored. Counted as one event and as the offsets, under the reader's origin and the skip's span.

The series carry WHICH KIND of reader they count (reader) beside the name (group): :group with the name as it is, :none with an empty name for a fetch outside a group (group is nil), and :other with an empty name for a group folded by the cap. Nothing reserves a group name, so the kind has to be its own label: with the name alone, a group a client called "__other__" would share a series with the folded ones, and one called "" with a fetch outside a group.

A {topic, group} pair keeps its own name while fewer than :retention_metrics_max_groups (default

  1. pairs have been admitted on this node, and is folded into :other after that, so a client inventing group names cannot grow the scrape without bound. The per-topic sum stays exact. Admission is check-then-insert across the few processes that report (one skip reporter per data-plane shard), so concurrent first skips can admit a pair or two past the cap: a bound, not an exact limit.

record_retention_sweep(duration_us)

@spec record_retention_sweep(non_neg_integer()) :: :ok

Records one retention sweep's duration (from the retention sweep telemetry event). The histogram exists from this server's first start, before the reporter that calls this is attached.

record_retention_unresolved_policy(topic, count)

@spec record_retention_unresolved_policy(String.t(), non_neg_integer()) :: :ok

Records that a sweep found topic bound to a policy name it could not resolve, and expired nothing of it. Counted per sweep, so a name that stays unresolved keeps the series moving: the disk it holds is invisible otherwise, and the fix is a binding an operator has to make.

record_scrub_pass(verified, repaired, unrepairable)

Records one integrity scrub pass: how many segments it verified, repaired, and could not repair. A verified total that stops advancing is how an operator sees that the scrub itself has stopped, which the failure counters alone cannot show (they stay at zero both when all is well and when nothing is checking). The unrepairable total is the one that calls for a human: it counts damage the cluster could not heal by itself, which on a single node is every finding there is.

record_sealed_copies_settled(result, copies, records)

Records copies copies of sealed segments settled against the recorded length, by the heal pass or by the node that trims, and tells them apart by result (:fenced, :trimmed, :failed or :recorded_size_below), and the records a trim gave up.

Split by result for the same reason record_orphaned_fence/0 is split from record_fences_reconciled/1: settling a copy is overwhelmingly a marker and nothing else, and the rare copy that gave up records was holding data its segment's seal excludes. One counter would make the second unfindable inside the first, and the first pass after an upgrade produces thousands of it.

record_storage_failure(reason)

Records a storage operation that failed on a segment's copy (Malachi.Cluster.ReplicationServer took the copy out of service), bucketed by reason: :enospc (a full volume), :eio (a failing device), :eacces (a permission problem), and :other for anything else. Buckets rather than the raw reason because a Prometheus label must come from a closed set, and these three are the ones that call for different operator actions.

record_tls_version(version)

Record the negotiated TLS version for a connection.

record_unexpected_message(server, kind)

Records one message a long-lived server had no clause for (see Malachi.UnexpectedMessage), by the server's label and the kind it arrived as. A label outside Malachi.UnexpectedMessage.servers/0 is counted as :other, so the exported label set stays closed whatever a caller passes.

retention_group_count()

@spec retention_group_count() :: non_neg_integer()

How many {topic, group} pairs have their own retention skip label on this node.

retention_snapshot()

@spec retention_snapshot() :: %{
  skips: [map()],
  expired: [map()],
  orphans_left: [map()],
  orphans_removed: non_neg_integer(),
  unresolved_policies: [map()],
  failures: %{required(atom()) => non_neg_integer()},
  sweeps: map()
}

The retention counters as the Prometheus exporter needs them: every skip series (topic, reader, group, origin, span, with its events and offsets), the expired segments and bytes per topic, the refusals per reply (every known reply, zero included), the replica directories expiries left behind per topic, the ones the sweeper reclaimed, and the sweep duration histogram in the shape of storage_flush_histogram/0 (its count is the number of sweeps). Read only at scrape time.

start_link(_)

Starts the metrics server (owns the ETS counter table).

storage_flush_histogram()

@spec storage_flush_histogram() :: %{
  buckets: [{float(), non_neg_integer()}],
  count: non_neg_integer(),
  sum_us: non_neg_integer(),
  bytes: non_neg_integer(),
  records: non_neg_integer(),
  created: float()
}

The storage flush histogram as the Prometheus exporter needs it: the count of flushes at or below each of Malachi.Histogram.edges/0, the total count and the sum in microseconds, the durability totals, and created, the Unix time in seconds when this node's histogram began.

created is what tells two scrapes of one histogram from scrapes on either side of a node restart: it changes only when the node boots again, never on a Malachi.Metrics restart, which keeps the samples. Counters alone cannot tell, since a restarted node can flush past its old totals before the next scrape. Kept out of get_system_metrics/0, which is snapshotted every second and sent to the dashboard, neither of which has any use for the buckets. All zeros when Malachi.Metrics never started.