StatifierUI.Trace.DeepLink (StatifierUI v0.4.0)

Copy Markdown View Source

Builds a URL into a host's APM backend from the wire format's otel correlation key (ADR-0013), so a rendered step can be followed to the trace that covers it.

This module is the consuming half of the correlation story (StatifierUI.Trace.Otel is the producing half). It calls no OpenTelemetry API, knows no backend, and holds no default URL: the template is host configuration, because only the host knows which backend its spans went to and what that backend's trace URLs look like. A package-supplied default would be a guess that silently sends operators to a URL that does not exist.

The template

A template is a string with {...} variables substituted from the message being rendered:

"https://apm.example.com/trace/{trace_id}?span={span_id}"
VariableFrom
trace_idthe message's otel.trace_id - 32 lowercase hex digits
span_idthe message's otel.span_id - 16 lowercase hex digits
sessionthe message's session
macrostepthe message's macrostep

A template naming neither trace_id nor span_id is rejected: it would render the same URL for every step, which is a configuration mistake and not a deep link. Any other {name} is rejected too, rather than being passed through as a literal - a typo ({traceid}, {trace-id}) that survived into production would produce a plausible-looking URL that never resolves.

Substituted values are percent-encoded to the unreserved set, so a template is safe to write with the variable in a path segment or in a query value. The two ids are hex and pass through unchanged; a session id is arbitrary host text and does not.

Compile once, at configuration time

new/1 returns {:error, reason} for a bad template and new!/1 raises, matching StatifierUI.Trace.Subscriber's treatment of its :otel_context option: a host misconfiguration is loud, immediately, at the point the option is read. What is never loud is a message with no correlation on it - see below.

Absence is not an error

url/2 returns nil when the message carries no otel key, when the key is present but malformed, or when the template asks for a value this context cannot supply. Absence is the documented normal case: the wire format omits otel whenever no bridge is attached (ADR-0013), so most streams a consumer renders carry none, and a renderer asks for a link and gets nil rather than branching on the stream's provenance first.

Malformed is folded into absent deliberately. The wire format is language-neutral (ADR-0005), so an otel object may have been written by a producer this repository never saw; ids that are not W3C Trace Context hex cannot be looked up in any backend, and linking to them would send an operator somewhere worse than nowhere.

Examples

iex> {:ok, template} = StatifierUI.Trace.DeepLink.new("https://apm.example.com/trace/{trace_id}?span={span_id}")
iex> message = %StatifierUI.Trace.Message{
...>   type: "trace.entry_set", session: "sess_1", seq: 7, macrostep: 2,
...>   otel: %{"trace_id" => "4bf92f3577b34da6a3ce929d0e0e4736", "span_id" => "00f067aa0ba902b7"}
...> }
iex> StatifierUI.Trace.DeepLink.url(template, message)
"https://apm.example.com/trace/4bf92f3577b34da6a3ce929d0e0e4736?span=00f067aa0ba902b7"

iex> {:ok, template} = StatifierUI.Trace.DeepLink.new("https://apm.example.com/trace/{trace_id}")
iex> message = %StatifierUI.Trace.Message{type: "trace.entry_set", session: "sess_1", seq: 7, macrostep: 2}
iex> StatifierUI.Trace.DeepLink.url(template, message)
nil

Summary

Types

What a URL is built from: a wire message, or the same three values pulled out of one. :otel is the message's otel object, string-keyed as the wire format writes it.

t()

A compiled template, built by new/1 and reused per message.

Functions

The otel object of context when it is well-formed, nil otherwise.

Compiles template into a t/0.

Compiles template, raising ArgumentError on a bad one.

Builds the URL for context, or nil when there is no link to build.

Types

context()

@type context() ::
  StatifierUI.Trace.Message.t()
  | %{
      optional(:otel) => %{optional(String.t()) => String.t()} | nil,
      optional(:session) => String.t() | nil,
      optional(:macrostep) => non_neg_integer() | nil
    }

What a URL is built from: a wire message, or the same three values pulled out of one. :otel is the message's otel object, string-keyed as the wire format writes it.

error()

@type error() ::
  {:not_a_string, term()}
  | {:unknown_variable, String.t()}
  | {:unbalanced_braces, String.t()}
  | :no_correlation_variable

t()

@type t() :: %StatifierUI.Trace.DeepLink{
  segments: [String.t() | atom()],
  template: String.t()
}

A compiled template, built by new/1 and reused per message.

Functions

correlation(context)

@spec correlation(context()) :: %{optional(String.t()) => String.t()} | nil

The otel object of context when it is well-formed, nil otherwise.

Exposed because "does this step have a trace at all" is a question a renderer asks (to decide whether to draw an affordance) separately from "what is its URL".

Examples

iex> message = %StatifierUI.Trace.Message{
...>   type: "trace.entry_set", session: "s", seq: 1,
...>   otel: %{"trace_id" => "0123456789abcdef0123456789abcdef", "span_id" => "0123456789abcdef"}
...> }
iex> StatifierUI.Trace.DeepLink.correlation(message)
%{"trace_id" => "0123456789abcdef0123456789abcdef", "span_id" => "0123456789abcdef"}

iex> message = %StatifierUI.Trace.Message{
...>   type: "trace.entry_set", session: "s", seq: 1,
...>   otel: %{"trace_id" => "TOO-SHORT", "span_id" => "0123456789abcdef"}
...> }
iex> StatifierUI.Trace.DeepLink.correlation(message)
nil

new(template)

@spec new(term()) :: {:ok, t()} | {:error, error()}

Compiles template into a t/0.

Returns {:error, reason} rather than a template that renders wrongly:

  • {:not_a_string, term} - the option was not a string
  • {:unknown_variable, name} - {name} is not one of trace_id, span_id, session, macrostep
  • {:unbalanced_braces, template} - a { or } outside a variable
  • :no_correlation_variable - neither trace_id nor span_id appears

Examples

iex> {:ok, template} = StatifierUI.Trace.DeepLink.new("https://apm.example.com/t/{trace_id}")
iex> template.template
"https://apm.example.com/t/{trace_id}"

iex> StatifierUI.Trace.DeepLink.new("https://apm.example.com/t/{traceid}")
{:error, {:unknown_variable, "traceid"}}

iex> StatifierUI.Trace.DeepLink.new("https://apm.example.com/session/{session}")
{:error, :no_correlation_variable}

new!(template)

@spec new!(term()) :: t()

Compiles template, raising ArgumentError on a bad one.

For the option-reading path, where a misconfigured host should hear about it at configuration time rather than by getting no links at render time.

url(arg1, context)

@spec url(t() | nil, context()) :: String.t() | nil

Builds the URL for context, or nil when there is no link to build.

nil in place of a template is itself a valid argument and returns nil, so a renderer holding an unconfigured option calls this unconditionally instead of branching around it.

Examples

iex> message = %StatifierUI.Trace.Message{type: "trace.entry_set", session: "s", seq: 1}
iex> StatifierUI.Trace.DeepLink.url(nil, message)
nil