sinal

Defines typed telemetry events, attaches handlers to them, and emits them through native BEAM :telemetry.

Describe an event once, as an Event(measurements, metadata) built from sinal/fields codecs, then emit it and observe it:

import sinal
import sinal/fields

let finished =
  sinal.event(
    ["request", "finished"],
    fields.int("duration_ms"),
    fields.string("route"),
  )
let attachment =
  sinal.observe(finished, fn(duration_ms, route) { record(route, duration_ms) })
sinal.emit(finished, 42, "/users")
let _ = sinal.detach(attachment)

measurement_fields and metadata_fields return an event’s codecs, so a test can pin the native maps that emit sends with fields.encode, or assert with fields.check that no value would be skipped, without attaching a handler.

Delivery

Handlers run synchronously in the emitting process, unless the application routed a prefix of the event’s name to a forwarder with sinal/forwarder.route: then emit hands the event to that forwarder’s process and returns without waiting for handlers. Native telemetry catches callback errors, exits and throws; untrappable termination can still stop the executing process. A native map that does not decode skips that one invocation: the handler’s failure observer receives the typed HandlerFailure (observe logs it), and the handler stays attached when failure reporting returns normally. A handler that returns an error or crashes is removed, and telemetry emits its standard [telemetry, handler, failure] event.

Registration

observe attaches an infallible handler to one event. A Subscription describes any registration: subscription for one event, handler for several same-shaped events with a fallible handler and a typed failure observer, with_label to name the handler in :telemetry.list_handlers, and with_id for a stable native handler id. attach installs one for the long term; with_subscriptions installs a group for the duration of one function call. Sinal gives every handler a fresh id unless with_id names one, so duplicate-id refusal requires with_id. Native startup or registry failures can still raise. observe_labelled is observe with a label.

Names are atoms

Each name segment becomes an atom, and the BEAM never frees an atom. A segment must match [a-z][a-z0-9_]{0,62} and must be written in source code, never built from input. An invalid segment, an empty name, an empty with_id or a repeated event in handler is a definition bug: the constructor panics with a message naming it.

The telemetry application

Native :telemetry keeps handlers in a process of the telemetry OTP application. attach, observe and with_subscriptions start that application when it is not running, so a script or a release that does not list it still delivers. Without it, emit reaches no handler and detach returns Error(Nil), because nothing can be attached.

Types

Why attach refused a subscription. The union is closed.

pub type AttachError {
  AlreadyExists(id: String)
}

Constructors

  • AlreadyExists(id: String)

    A handler with this with_id id is already attached.

A registered handler. Keep it to detach the handler.

pub opaque type Attachment

Why a scope could not detach one of its handlers. The union is closed.

pub type CleanupFailure {
  AlreadyDetached
  DetachCrashed(description: String)
}

Constructors

  • AlreadyDetached

    The handler was no longer attached, for example because telemetry removed it after it failed.

  • DetachCrashed(description: String)

    Detaching raised; description is for logs.

A typed event: its name and the codecs of its measurements and metadata.

pub opaque type Event(measurements, metadata)

Why a handler did not run. The union is closed.

pub type HandlerFailure(e) {
  MalformedMeasurements(fields.FieldError)
  MalformedMetadata(fields.FieldError)
  HandlerReturned(e)
}

Constructors

A registration that attach or with_subscriptions installs. Building one installs no handler.

pub opaque type Subscription

A cleanup failure of the subscription at zero-based index.

pub type SubscriptionCleanupFailure {
  SubscriptionCleanupFailure(index: Int, failure: CleanupFailure)
}

Constructors

The work’s result and the handlers that did not detach cleanly. Read it by label; sinal may add fields.

pub type SubscriptionCompletion(a) {
  SubscriptionCompletion(
    work_result: a,
    cleanup_failures: List(SubscriptionCleanupFailure),
  )
}

Constructors

A group of subscriptions for with_subscriptions.

pub opaque type SubscriptionPlan

with_subscriptions could not attach the subscription at index; the work did not run, and earlier subscriptions received cleanup attempts.

pub type SubscriptionScopeError {
  SubscriptionAttachFailed(
    index: Int,
    error: AttachError,
    rollback_failures: List(SubscriptionCleanupFailure),
  )
}

Constructors

Values

pub fn attach(
  subscription: Subscription,
) -> Result(Attachment, AttachError)

Installs a subscription until detach. Starts the telemetry application when it is not running.

pub fn describe_attach_error(error: AttachError) -> String

Describes an attach failure for logs.

pub fn describe_cleanup_failure(
  failure: CleanupFailure,
) -> String

Describes a cleanup failure for logs.

pub fn describe_handler_failure(
  failure: HandlerFailure(e),
  describe: fn(e) -> String,
) -> String

Describes a handler failure for logs, with describe for the handler’s own error.

pub fn describe_scope_error(
  error: SubscriptionScopeError,
) -> String

Describes a scope failure for logs.

pub fn detach(attachment: Attachment) -> Result(Nil, Nil)

Removes a handler. Returns Error(Nil) when it was not attached: it was detached before, or telemetry removed it after it failed.

pub fn emit(
  event: Event(m, d),
  measurements: m,
  metadata: d,
) -> Nil

Emits an event. Handlers run in the caller before emit returns, unless the application routed a prefix of the event’s name with sinal/forwarder.route. A routed event is handed to that forwarder and emit returns without waiting; when the forwarder is full or not running, the event is dropped and counted in the forwarder’s dropped_event. Native telemetry catches callback errors, exits and throws; untrappable termination can still stop the executing process. Custom getters and encoders can raise before dispatch.

pub fn event(
  name: List(String),
  measurements: fields.Fields(measurements),
  metadata: fields.Fields(metadata),
) -> Event(measurements, metadata)

Defines an event. name is the native event name, for example ["http_gun", "request", "stop"].

Panics when name is empty or a segment breaks the name grammar.

pub fn handler(
  events: List(Event(m, d)),
  run: fn(Event(m, d), m, d) -> Result(Nil, e),
  on_failure: fn(Event(m, d), HandlerFailure(e)) -> Nil,
) -> Subscription

Describes one handler of several same-shaped events. run receives the event that fired.

  • When a native map does not decode, on_failure receives MalformedMeasurements or MalformedMetadata, run is skipped for that one event, and the handler stays attached.
  • When run returns an error, on_failure receives HandlerReturned, and telemetry then removes the handler and emits [telemetry, handler, failure]. So does a crash in run or in on_failure.

Panics when events is empty or names one event twice.

pub fn measurement_fields(event: Event(m, d)) -> fields.Fields(m)

The codec of the event’s measurements. A test can encode a value with it to pin the native map that emit sends, without attaching a native handler:

fields.encode(sinal.measurement_fields(finished), 42)
pub fn metadata_fields(event: Event(m, d)) -> fields.Fields(d)

The codec of the event’s metadata, for fields.encode, fields.decode, fields.check and fields.keys.

pub fn name(event: Event(m, d)) -> List(String)

The event’s name.

pub fn observe(
  event: Event(m, d),
  run: fn(m, d) -> Nil,
) -> Attachment

Attaches an infallible handler to one event and returns its attachment. The handler runs in the emitting process (or the forwarder’s, for a routed event). A native map that does not decode skips that one invocation and logs a warning; the handler stays attached. A handler crash removes it and emits telemetry’s [telemetry, handler, failure] event.

pub fn observe_labelled(
  event: Event(m, d),
  label: String,
  run: fn(m, d) -> Nil,
) -> Attachment

observe with a label that names the handler in :telemetry.list_handlers/1, as with_label describes. The id stays fresh, so labels need not be unique. Native startup or registry failures can still raise.

sinal.observe_labelled(finished, "billing_metrics", fn(duration_ms, route) {
  record(route, duration_ms)
})
pub fn subscription(
  event: Event(m, d),
  run: fn(m, d) -> Nil,
) -> Subscription

Describes an infallible handler of one event. When a native map does not decode, the handler skips that one invocation, stays attached, and logs a warning that names the event and the HandlerFailure.

pub fn subscriptions(
  entries: List(Subscription),
) -> SubscriptionPlan

Plans a scope over entries, attached in list order and detached in reverse order.

pub fn with_exception_cleanup_reporter(
  plan: SubscriptionPlan,
  reporter: fn(SubscriptionCleanupFailure) -> Nil,
) -> SubscriptionPlan

Reports cleanup failures that happen while the work’s exception is being re-raised; such failures cannot be returned. The default reporter does nothing. A reporter that crashes does not mask the work’s exception.

pub fn with_id(
  subscription: Subscription,
  id: String,
) -> Subscription

Gives the handler a stable native handler id instead of a fresh one, so that an Erlang or Elixir caller can :telemetry.detach(id) it. attach returns AlreadyExists while another handler holds the id.

Panics when id is empty.

pub fn with_label(
  subscription: Subscription,
  label: String,
) -> Subscription

Names the handler in :telemetry.list_handlers/1, so an application can tell its handlers apart: its native id becomes {sinal_handler, <<"label">>, N} instead of {sinal_handler, N}. The id stays fresh, so a label need not be unique, any string is accepted, and a fresh labelled id avoids duplicate-id refusal. with_id replaces the whole id, so a subscription with both uses the with_id id.

The label is for people reading the handler table; never build it from unbounded input.

pub fn with_subscriptions(
  plan: SubscriptionPlan,
  work: fn() -> a,
) -> Result(SubscriptionCompletion(a), SubscriptionScopeError)

Attaches the plan’s subscriptions in order, runs work, and attempts to detach them in reverse order.

  • When an attach is refused, work does not run, earlier subscriptions receive cleanup attempts, and the error names the failing index and any rollback failures.
  • When work raises a catchable error, exit or throw, every acquired subscription receives a cleanup attempt and the exception is re-raised with its class, reason and stacktrace.
  • Attaching is not atomic: a concurrent emitter can see a partial set. Detaching does not wait for a handler already running in another process. A killed process skips cleanup.
✨ Search Document