Ankusa.Trace (ankusa v0.7.0)

Copy Markdown View Source

W3C trace context for a hook, and the spans every stage opens (ground rule 8).

The edge reads traceparent / tracestate off the request (from_headers/1) and stores them on the hook as Ankusa.Envelope.trace, a map %{traceparent: "00-<trace-id>-<parent-id>-<flags>", tracestate: String.t() | nil}. Every sink then carries that value to the consumer: traceparent on Ankusa.Sink.Http, the traceparent / tracestate fields and the broker-native header pair on Ankusa.Sink.Message, the CloudEvents traceparent extension on lifecycle events.

No SDK, no cost

Core depends on opentelemetry_api only. Without an SDK started (the default for a library user; ankusa_server starts one when tracing.enabled), span/3 calls its function directly and current/1 returns the context the request carried, so a traceparent sent to the edge reaches the consumer byte for byte and nothing else happens.

With an SDK

  • ankusa.ingest is a :server span, a child of the provider's span (the request's traceparent); ankusa.verify and ankusa.commit are its children. With an SDK the hook stores the ingest span's context (current/1): same trace id, our span as the parent id, tracestate carried through, the sampled flag as the sampler decided it. The value is fixed at ingest, so every retry, replay and sink carries the same one.
  • ankusa.dispatch.attempt (one per attempt, a :producer span) is a child of the stored context and links to it, so the whole life of a hook is one trace however long the retries take. ankusa.claim_check is the pack write of a large body; ankusa.replay is one step of a replay job; ankusa.lifecycle.event builds a lifecycle event.

Only the sampled bit of the trace flags is kept (00 or 01): the other bits are reserved by the specification. A request whose traceparent is malformed, repeated, version ff, or all-zero in its trace or parent id carries no trace (the header stays an ordinary forwarded request header, as before). A tracestate over 512 bytes is dropped; the traceparent still travels.

Summary

Types

Span attributes: string keys, scalar values (an OpenTelemetry attribute).

t()

The trace context stored on a hook (Ankusa.Envelope.trace).

Functions

Whether an SDK is running (spans are recorded); false is the no-op API.

The context to store on a hook being ingested: the current span's, when an SDK runs and the process is inside a valid span; otherwise fallback (the context the request carried, verbatim).

Mark the current span failed. reason is a short class (an atom or string), never a raw error term: those can carry URLs and credentials.

The trace context of a request's headers, or nil. One pass over the list; header names are matched without case. A hook without a traceparent pays for nothing but that pass.

Add attributes to the calling process's current span (a no-op without one).

Run fun inside a span named name; returns what fun returns.

The 32-hex trace id of a trace context, nil for nil. It is what a log line carries as trace_id (Ankusa.Edge.Ingest), so a line can be found from the trace and the other way round.

Types

attributes()

@type attributes() :: %{optional(String.t()) => String.t() | integer() | boolean()}

Span attributes: string keys, scalar values (an OpenTelemetry attribute).

span_opt()

@type span_opt() ::
  {:attributes, attributes() | (-> attributes())}
  | {:kind, :internal | :server | :client | :producer | :consumer}
  | {:parent, t() | nil}
  | {:link, t() | nil}

t()

@type t() :: %{traceparent: String.t(), tracestate: String.t() | nil}

The trace context stored on a hook (Ankusa.Envelope.trace).

Functions

active?()

@spec active?() :: boolean()

Whether an SDK is running (spans are recorded); false is the no-op API.

current(fallback)

@spec current(t() | nil) :: t() | nil

The context to store on a hook being ingested: the current span's, when an SDK runs and the process is inside a valid span; otherwise fallback (the context the request carried, verbatim).

error(reason)

@spec error(atom() | String.t()) :: :ok

Mark the current span failed. reason is a short class (an atom or string), never a raw error term: those can carry URLs and credentials.

from_headers(headers)

@spec from_headers([{String.t(), String.t()}]) :: t() | nil

The trace context of a request's headers, or nil. One pass over the list; header names are matched without case. A hook without a traceparent pays for nothing but that pass.

set_attributes(attributes)

@spec set_attributes(attributes()) :: :ok

Add attributes to the calling process's current span (a no-op without one).

span(name, opts, fun)

@spec span(String.t(), [span_opt()], (-> result)) :: result when result: term()

Run fun inside a span named name; returns what fun returns.

Options: :attributes (a map, or a function that builds it: it runs only when an SDK records the span, so the no-op path allocates nothing), :kind (default :internal), :parent (a stored context to continue; default the calling process's current span) and :link (a stored context to link to). An exception in fun is recorded on the span and re-raised. With no SDK started this is fun.().

trace_id(arg1)

@spec trace_id(t() | nil) :: String.t() | nil

The 32-hex trace id of a trace context, nil for nil. It is what a log line carries as trace_id (Ankusa.Edge.Ingest), so a line can be found from the trace and the other way round.