Encryptor.Telemetry (Encryptor v0.7.0)

Copy Markdown View Source

The package's :telemetry events.

No event carries a plaintext, a key of any kind, an encryption context value, a :key selector, a partition id, or an Encryptor.Error's :engine term. No event carries a per-scope dimension unless the vault opted in with telemetry_scope_ref: true, and then it is the keyed scope_ref and never the partition id. Handlers run on the calling process, so a slow handler is a slow encrypt.

This module is the single definition site for the vocabulary (ADR-0006 decision 3) and the only module in lib/ that calls :telemetry.execute/3. The event set is closed: adding a name, a measurement, or a metadata key is additive, and removing or renaming one is a breaking change to a consumer's case and to their dashboards, so it takes an amendment to that record.

Attaching

:telemetry.attach_many(
  "my-app-encryptor",
  Encryptor.Telemetry.events(),
  &MyApp.Handler.handle/4,
  nil
)

:telemetry detaches a handler that raises, permanently and silently, for the lifetime of the VM. attach_many/4's detach is total, so one malformed branch takes every event with it; attaching one handler id per name - which is what events/0 is for - bounds that to the event that raised.

The events

:start and :stop name a span pair and nothing else. Every other event is a point event, named in the past tense, so an attach list can be read without reading the record.

EventKindFires
[:encryptor, :vault, :started]pointa vault's supervisor came up with its configuration frozen
[:encryptor, :vault, :stopped]pointEncryptor.Vault.Lifecycle erased the frozen configuration
[:encryptor, :vault, :start_refused]pointconfiguration resolution refused, before any process existed
[:encryptor, :cache, :recycled]pointthe recycler dropped and restarted the cache child
[:encryptor, :suspension, :changed]pointa vault's suspension state changed: see below
[:encryptor, :encrypt, :start] / [..., :stop]spanencrypt/2
[:encryptor, :decrypt, :start] / [..., :stop]spandecrypt/2
[:encryptor, :rekey, :start] / [..., :stop]spanrekey/2
[:encryptor, :provider, :start] / [..., :stop]spanone encryption_key/2 or decryption_keys/2 round trip

The eight span halves - four pairs - are specified by ADR-0006 and emitted by the paths they instrument (that record's decision 10), all of which are written. events/0 returns thirteen names: the five point events above and those eight halves.

Measurements

MeasurementUnitOn
duration:nativeevery span stop, and :recycled
system_time:nativeevery span start and every point event
sizebytes[:encryptor, :encrypt, :start] and [:encryptor, :decrypt, :stop]
candidatescount[:encryptor, :provider, :stop] on a successful decryption_keys/2
countcount[:encryptor, :suspension, :changed] when outcome is :ok: the scopes in the view after the action

Metadata

Metadata is an allow-list. No struct rides verbatim: not a Encryptor.Vault.Config, not a key descriptor, not a keyring, a CMM, a client, a context map, a ciphertext, a plaintext, an Encryptor.Error or its :engine term.

KeyTypeOn
vaultmodule()every event
operationEncryptor.Error.operation/0spans, :start_refused
span_refreference()span halves

| outcome | :ok | :error | span stops, :recycled, :changed | | reason_tag | reason_tag/0 | when outcome is :error | | provider | module() | provider spans | | callback | :encryption_key \| :decryption_keys | provider spans | | cache | boolean() | [:encryptor, :vault, :started] | | profile | :single \| :scoped | [:encryptor, :vault, :started] | | reference_check | :verified \| :unpinned | [:encryptor, :vault, :started] | | scope_ref | String.t() | the four span names' halves, only when :telemetry_scope_ref is on | | action | :suspend | :reinstate | :refresh | [:encryptor, :suspension, :changed] | | store | module() | [:encryptor, :suspension, :changed]: the store module, never its state or options |

Suspension changes

[:encryptor, :suspension, :changed] fires on the process that changed a vault's suspension state (ADR-0010 decision 8):

  • after every Encryptor.Vault.suspend/2 or Encryptor.Vault.reinstate/2 the vault performed, successful or not;
  • under a shared :suspension_store, after the refresh that loads the view, after a refresh that changed the view's membership, after a refresh that failed, and after the first refresh that succeeds following a failure.

Until a shared store's view is loaded, the vault denies every scope, so the refresh that loads it is a change even when the store's set is empty: it emits action: :refresh, outcome: :ok and the loaded count. That is the first successful refresh after the vault starts, and again the first one after a restarted Encryptor.Vault.Lifecycle has recreated the view. Any other refresh that changed nothing emits nothing, so a healthy vault is silent between operator actions. The selector is never in the event, whatever :telemetry_scope_ref says: an operator who needs to know which scope was suspended reads the store.

The opt-in scope dimension

With telemetry_scope_ref: true, every encrypt, decrypt, rekey and provider event carries scope_ref - ADR-0003 decision 5's keyed reference for the scope the call routed to. It is a pseudonym and not an identifier: it does not contain the scope identifier and cannot be reversed into it. Anyone holding the vault's reference subkey can re-identify it, by deriving the reference for a candidate scope and comparing, and so can anyone who can enumerate or guess your scope identifiers. Telemetry metadata is forwarded verbatim by handlers you did not write to vendors whose retention you did not choose. Turning this on is a decision about that, and it is off by default.

It is vault configuration, refused as true on a :single vault, and the reference is derived once per operation and threaded through both halves of the operation span and through the nested provider span's halves. When the option is off the key is absent from the metadata map rather than present as nil: a handler tells "this host did not opt in" from any value by Map.has_key?/2, and a nil in a :telemetry_metrics tag is a dimension value. Cardinality is your scope count, which is what you asked for and is your vendor's bill (ADR-0006 amendment A).

Summary

Types

Every metadata key any event may carry, and nothing else.

The metadata tag for a failure.

The operations that open a span pair.

Functions

Every event name this package emits. The single definition site.

The metadata tag for a reason term. Never the term itself.

Types

metadata()

@type metadata() :: %{
  optional(:vault) => module(),
  optional(:operation) => Encryptor.Error.operation(),
  optional(:span_ref) => reference(),
  optional(:outcome) => :ok | :error,
  optional(:reason_tag) => reason_tag(),
  optional(:provider) => module(),
  optional(:callback) => :encryption_key | :decryption_keys,
  optional(:cache) => boolean(),
  optional(:profile) => Encryptor.Vault.Config.profile(),
  optional(:reference_check) => :verified | :unpinned,
  optional(:scope_ref) => String.t(),
  optional(:action) => :suspend | :reinstate | :refresh,
  optional(:store) => module()
}

Every metadata key any event may carry, and nothing else.

reason_tag()

@type reason_tag() ::
  :decrypt_failed
  | :vault_not_started
  | :missing_config
  | :invalid_config
  | :unknown_key
  | :encryption_context_conflict
  | :reserved_context_key
  | :key_unavailable
  | :invalid_key_descriptor
  | :provider_not_started
  | :missing_optional_dependency
  | :missing_required_context_keys
  | :invalid_context_value
  | :invalid_selector
  | :not_provisionable
  | :suspension_store_unavailable

The metadata tag for a failure.

The head of an Encryptor.Error.reason/0, never the term: every member but :decrypt_failed is a tagged tuple whose second element is caller data

  • a selector, a context key, a config path, a module - and that data does not leave the process in a metric. The set is closed, and extends only when the error vocabulary does, which is itself an ADR-gated act (ADR-0001 decision 10). That closure is what makes it safe as a metric dimension: a backend keying on it has a bounded label set no matter what a caller passes.

span_name()

@type span_name() :: :encrypt | :decrypt | :rekey | :provider

The operations that open a span pair.

Functions

events()

@spec events() :: [[atom(), ...], ...]

Every event name this package emits. The single definition site.

A host attaches against this rather than hand-copying names the record may later extend.

iex> [:encryptor, :cache, :recycled] in Encryptor.Telemetry.events()
true

iex> length(Encryptor.Telemetry.events())
13

reason_tag(arg1)

@spec reason_tag(Encryptor.Error.reason()) :: reason_tag()

The metadata tag for a reason term. Never the term itself.

iex> Encryptor.Telemetry.reason_tag({:key_unavailable, "acct_9f21"})
:key_unavailable

iex> Encryptor.Telemetry.reason_tag(:decrypt_failed)
:decrypt_failed

It is deliberately not elem(reason, 0): :decrypt_failed is a bare atom, and a fallthrough that reached elem/2 on a term the record did not anticipate would either raise inside an emit or leak whatever the term was.