Packages
crosswake_threadline
0.1.0
Threadline audit and correlation observer for the Crosswake route-policy system.
Current section
Files
Jump to
Current section
Files
lib/crosswake/threadline/telemetry.ex
defmodule Crosswake.Threadline.Telemetry do
@moduledoc """
Telemetry contract for Crosswake Threadline correlation diagnostics.
This module defines the low-cardinality `:telemetry` event-name allowlist
and metadata allowlist guard for request-span events emitted by the
`Crosswake.Plug.Threadline` Plug (Phase 92).
**Diagnostic-only.** Threadline telemetry is PII-free correlation evidence:
it propagates a session-spanning `thread_id` above the per-command
`correlation_id` for operator observability. It is NOT an APM replacement,
NOT a distributed tracing framework, and NOT a generic event bus. It coexists
with any host-side telemetry or tracing pipeline without interfering with them.
All metadata passes through `metadata/1` before emission, which:
- Keeps only the four PROP-02 allowlisted keys (`@metadata_keys`)
- Drops any key in `@forbidden_metadata_keys` (PII denylist) silently
- Drops allowlisted keys whose values are unsafe (nil, oversized binaries, etc.)
- Drops all other unknown keys
Zero new dependencies — only `:telemetry`, already a project dependency.
"""
# PROP-02 fixed allowlist — exactly four keys; :source carries thread provenance
# at the boundary (value domain: :inbound | :minted) and is set by the Phase 92
# Plug emitter. It earns an allowlist slot as the one key not reconstructable from
# the others (D-09, D-10). The value itself is NOT set by this module.
@metadata_keys [:thread_id, :correlation_id, :route_id, :source]
# Three request-span event names only (D-07). Bridge and activation events are NOT
# pre-declared here — that would widen scope beyond PROP-02 (D-08).
@event_names [
[:crosswake, :threadline, :request, :start],
[:crosswake, :threadline, :request, :stop],
[:crosswake, :threadline, :request, :exception]
]
# Sigra's verified 19-key PII denylist plus :actor_ref (RESEARCH A1 — Phase 94's
# PII-adjacent field must never appear in telemetry, even though :actor_ref is not
# in Sigra's list). Total: 20 forbidden keys.
@forbidden_metadata_keys [
:access_token,
:actor_id,
:actor_ref,
:authorization_code,
:credential_id,
:device_id,
:email,
:id_token,
:ip,
:nonce,
:org_id,
:passkey_credential_id,
:pkce_verifier,
:provider_payload,
:raw_return_to,
:refresh_token,
:return_to,
:session_ref,
:subject_ref,
:user_agent
]
defmodule Event do
@moduledoc false
@enforce_keys [:name]
defstruct [:name, :thread_id, :correlation_id, :route_id, :source]
@type t :: %__MODULE__{name: [atom()]}
end
@spec event_names() :: [[atom()]]
def event_names, do: @event_names
@spec metadata_keys() :: [atom()]
def metadata_keys, do: @metadata_keys
@spec forbidden_metadata_keys() :: [atom()]
def forbidden_metadata_keys, do: @forbidden_metadata_keys
@spec valid_event_name?(term()) :: boolean()
def valid_event_name?(name), do: name in @event_names
@spec new_event(keyword()) :: Event.t()
def new_event(attrs) when is_list(attrs) do
name = Keyword.fetch!(attrs, :name)
unless valid_event_name?(name) do
raise ArgumentError, "unknown Threadline telemetry event name: #{inspect(name)}"
end
attrs
|> Keyword.take([:name | @metadata_keys])
|> Map.new()
|> then(&struct!(Event, &1))
end
@spec metadata(Event.t() | map() | keyword()) :: map()
def metadata(%Event{} = event), do: event |> Map.from_struct() |> Map.delete(:name) |> metadata()
def metadata(attrs) when is_list(attrs), do: attrs |> Map.new() |> metadata()
def metadata(attrs) when is_map(attrs) do
attrs
|> Enum.reduce(%{}, fn {key, value}, acc ->
key = normalize_key(key)
cond do
key in @forbidden_metadata_keys ->
acc
key in @metadata_keys and safe_value?(value) ->
Map.put(acc, key, normalize_value(value))
true ->
acc
end
end)
end
@spec to_map(Event.t()) :: map()
def to_map(%Event{name: name} = event) do
event
|> metadata()
|> Map.put(:name, Enum.map_join(name, ".", &Atom.to_string/1))
|> Enum.map(fn {key, value} -> {Atom.to_string(key), value} end)
|> Map.new()
end
@spec execute([atom()], map(), map() | keyword()) :: :ok
def execute(name, measurements \\ %{}, metadata \\ %{}) do
:telemetry.execute(name, measurements, metadata(metadata))
end
defp normalize_key(key) when is_atom(key), do: key
defp normalize_key(key) when is_binary(key) do
if key in Enum.map(@metadata_keys ++ @forbidden_metadata_keys, &Atom.to_string/1) do
String.to_existing_atom(key)
else
key
end
end
defp normalize_key(key), do: key
defp safe_value?(nil), do: false
defp safe_value?(value) when is_atom(value), do: true
defp safe_value?(value) when is_integer(value) and value >= 0, do: true
defp safe_value?(value) when is_binary(value), do: String.length(value) <= 128
defp safe_value?(_value), do: false
defp normalize_value(value) when is_atom(value), do: value
defp normalize_value(value), do: value
end