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}"| Variable | From |
|---|---|
trace_id | the message's otel.trace_id - 32 lowercase hex digits |
span_id | the message's otel.span_id - 16 lowercase hex digits |
session | the message's session |
macrostep | the 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.
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
@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.
A compiled template, built by new/1 and reused per message.
Functions
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
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 oftrace_id,span_id,session,macrostep{:unbalanced_braces, template}- a{or}outside a variable:no_correlation_variable- neithertrace_idnorspan_idappears
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}
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.
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