AuroraMeter.Telemetry (Aurora Meter v1.0.0)

View Source

The telemetry contract: the event catalogue as data, the tag rules, the redaction helper and the on-demand gauges.

Aurora Meter emits :telemetry events and nothing else. It starts no exporter, opens no socket and sends nothing anywhere: what a host does with these events is entirely the host's decision. AuroraMeter.Telemetry.Metrics offers Telemetry.Metrics presets for hosts that want a reporter, and it is compiled only when that optional dependency is present.

Why the catalogue is data

events/0 returns one entry per event, with its real measurement and metadata keys and the closed set of metadata keys that are safe to use as a metric tag. Documentation drifts from code when the only thing joining them is a person reading both; AuroraMeter.TelemetryContractTest compares this catalogue against the emit sites in lib/, against docs/api.md and against docs/telemetry.md, in both directions, so a new event that is not documented and a documented event that is not emitted both fail the suite.

Cardinality is the reason for the tag rules

A metric tagged on tenant_key creates one time series per tenant, and one tagged on any _id creates one per object. Both are unbounded, and the cost lands on the host's monitoring bill and on its reporter's memory rather than here, which is exactly why the library must not teach it by example.

tag_allow_list/0 is closed. Every tag a preset uses is one of those names, and the value set behind each is bounded by something nameable in the source:

TagBounded by
:resultthe documented outcome set of the operation that emits it
:kindAuroraMeter.Schema.CreditTransaction.kinds/0, the retention table allow list, the cluster batch kinds, the span exception kinds
:statethe state machine that owns the event
:exporterthe exporter modules a host configures
:workerthe AuroraMeter.Oban.* module list
:featurethe host's compiled plan definitions, which is why it is opt-in

forbidden_tags/0 and forbidden_tag_suffixes/0 are the other half: names that must never become a tag, whatever a preset believes about them.

The library polices its own presets and can do nothing about a handler a host writes. That is a real limit and it is stated plainly rather than implied away: attach whatever you like, but redact/2 is here so you do not have to invent the rule yourself.

Gauges

Three measurements have no natural event because nothing happens when they change: how much is buffered, how old the oldest buffered thing is, and how far behind the cluster is. They are sampled on a timer inside processes that already exist (AuroraMeter.Store and AuroraMeter.Cluster), every :metrics_interval milliseconds. Set metrics_interval: 0 to switch the timers off and drive emit_gauges/0 from telemetry_poller or any other scheduler.

Handlers attached to a gauge run inside the emitting process. A slow handler on [:aurora_meter, :store, :gauge] delays flush batch snapshots; do no I/O in one.

Summary

Types

One entry in the catalogue.

Functions

Emits every gauge once, synchronously, from the caller's process.

Every concrete event name in the catalogue, flattened.

The event catalogue.

The opt-in tag, :feature.

Name suffixes that must never be a metric tag, whatever the rest of the name.

Metadata keys that must never be a metric tag, named one by one.

The most recent gauge sample of each gauge, with how long ago it was taken.

A copy of metadata safe to put in a log line or a span attribute.

The closed set of metadata keys a preset may use as a metric tag.

Whether tag may be used as a metric tag.

Types

event_doc()

@type event_doc() :: %{
  event: [atom()],
  form: :execute | :span | :family,
  names: [[atom()]],
  measurements: [atom()],
  metadata: [atom()],
  tags: [atom()],
  description: String.t(),
  since: String.t(),
  emitter: module()
}

One entry in the catalogue.

:event is the literal name at the emit site and :form is how it is emitted. A segment computed at the emit site appears as :"<kind>" with form: :family, and :names then lists the concrete names a handler attaches to. A form: :span entry emits the :start, :stop and :exception names under :event, and :names lists all three.

Functions

emit_gauges()

@spec emit_gauges() :: :ok

Emits every gauge once, synchronously, from the caller's process.

For a host that has set metrics_interval: 0 and drives the gauges from telemetry_poller or its own scheduler. Each gauge is emitted by the process that owns the state behind it, so this is a message to AuroraMeter.Store and another to AuroraMeter.Cluster, not a read of their internals.

A process that is not running contributes no sample, rather than a zero: a gauge that reports zero when nothing is watching is worse than a gauge that reports nothing, because zero looks healthy.

Examples

iex> AuroraMeter.Telemetry.emit_gauges()
:ok

event_names()

@spec event_names() :: [[atom()]]

Every concrete event name in the catalogue, flattened.

Examples

iex> [:aurora_meter, :credits, :grant] in AuroraMeter.Telemetry.event_names()
true

events()

@spec events() :: [event_doc()]

The event catalogue.

Every entry names measurement and metadata keys that the emitter really writes. :tags is the subset of :metadata a preset may tag on, before the opt-in :feature tag is added.

Examples

iex> AuroraMeter.Telemetry.events() |> Enum.find(&(&1.event == [:aurora_meter, :flush])) |> Map.get(:measurements)
[:count, :delta_sum]

feature_tag()

@spec feature_tag() :: atom()

The opt-in tag, :feature.

Examples

iex> AuroraMeter.Telemetry.feature_tag()
:feature

forbidden_tag_suffixes()

@spec forbidden_tag_suffixes() :: [String.t()]

Name suffixes that must never be a metric tag, whatever the rest of the name.

Examples

iex> AuroraMeter.Telemetry.forbidden_tag_suffixes()
["_key", "_id", "_secret", "_token", "_ref"]

forbidden_tags()

@spec forbidden_tags() :: [atom()]

Metadata keys that must never be a metric tag, named one by one.

Examples

iex> :tenant_key in AuroraMeter.Telemetry.forbidden_tags()
true

gauges()

@spec gauges() :: [%{event: [atom()], measurements: map(), age_ms: non_neg_integer()}]

The most recent gauge sample of each gauge, with how long ago it was taken.

For a reader that wants the figure without emitting one: a dashboard refreshing every second would otherwise fire a gauge event per refresh per connected operator, and every handler a host has attached would run inside AuroraMeter.Store each time.

Each entry is %{event: name, measurements: map, age_ms: integer}. A gauge whose owning process is not running, or that has never sampled, contributes no entry rather than a zero-filled one: a gauge that reports zero when nothing is watching is worse than a gauge that reports nothing, because zero looks healthy. The same rule as emit_gauges/0.

age_ms is a monotonic span inside this node (AuroraMeter.Clock.monotonic_ms/0). It is never persisted and never compared across nodes, which is the one reading that cannot step backwards.

Examples

iex> is_list(AuroraMeter.Telemetry.gauges())
true

redact(metadata, opts \\ [])

@spec redact(
  map(),
  keyword()
) :: map()

A copy of metadata safe to put in a log line or a span attribute.

Everything that identifies a customer, a payment or an object is dropped, and so is anything whose name ends in one of forbidden_tag_suffixes/0. error becomes error_class: the exception module, or the outcome tag, and never the message, which is where a query, a key or a customer's data ends up.

:tenant chooses what happens to tenant_key:

  • :drop (default) removes it.
  • :digest replaces it with tenant_digest, the first eight bytes of its SHA-256 in lower-case hex. That is pseudonymous, not anonymous: it is stable, so it still links a person's activity across records, and several regimes treat it as personal data.
  • :raw keeps it. It is spelled out so that carrying an identifier into a monitoring pipeline is a decision somebody wrote down.

Examples

iex> AuroraMeter.Telemetry.redact(%{tenant_key: "acme", feature: :api_calls})
%{feature: :api_calls}

iex> AuroraMeter.Telemetry.redact(%{tenant_key: "acme"}, tenant: :raw)
%{tenant_key: "acme"}

iex> AuroraMeter.Telemetry.redact(%{error: %RuntimeError{message: "boom"}})
%{error_class: "RuntimeError"}

tag_allow_list()

@spec tag_allow_list() :: [atom()]

The closed set of metadata keys a preset may use as a metric tag.

:feature is not on it. It is offered separately, behind metrics_feature_label: true, because its bound is the host's plan definitions and only the host knows how many that is.

Examples

iex> AuroraMeter.Telemetry.tag_allow_list()
[:result, :kind, :exporter, :state, :worker]

tag_allowed?(tag, feature_label?)

@spec tag_allowed?(atom(), boolean()) :: boolean()

Whether tag may be used as a metric tag.

feature_label? says whether the host has turned the opt-in :feature tag on; with it false, :feature is refused like any other name.

Examples

iex> AuroraMeter.Telemetry.tag_allowed?(:result, false)
true

iex> AuroraMeter.Telemetry.tag_allowed?(:tenant_key, true)
false

iex> AuroraMeter.Telemetry.tag_allowed?(:feature, false)
false