Sinal

Sinal gives Gleam applications typed events and observers on Erlang/BEAM. Measurements and metadata remain Gleam values in application code and become ordinary names and maps for native Erlang and Elixir :telemetry handlers.

Sinal is unreleased; install it from Git:

[dependencies]
sinal = { git = "https://github.com/gleam-dream/sinal.git", ref = "main" }

Sinal requires Gleam 1.18 or newer and telemetry 1.4.2 through the 1.x series. It supports the Erlang target. Resolve dependencies and commit the generated manifest.toml to lock the selected Git revision.

When to use it

Use Sinal when application or library code should report typed facts through BEAM’s shared telemetry registry. It fits service instrumentation, typed observers for metrics or logging adapters, spans around local work, and correlation values carried between application layers. If callbacks would slow producers, an application can route selected events through a supervised forwarder.

Sinal does not provide an exporter, metric aggregation, durable delivery, remote trace propagation, or business workflow and authorization control. Applications and adapters own those responsibilities.

Capabilities

Define, observe, and emit

Import sinal and sinal/fields, then define an event once and share it between producers and observers:

pub fn observe_request_example() {
  let finished =
    sinal.event(
      ["request", "finished"],
      fields.int("duration_ms"),
      fields.string("route"),
    )
  let attachment =
    sinal.observe(finished, fn(_duration_ms, _route) {
      // Runs in the emitting process, before `emit` returns.
      Nil
    })
  sinal.emit(finished, 42, "/users")
  let assert Ok(Nil) = sinal.detach(attachment)
}

Event(measurements, metadata) pairs the native event name with two Fields codecs. Record codecs use named field bindings and explicit getters. See the record and enum example for the compiled round-trip check.

How dispatch works

The event definition and codecs allocate no process. Sinal encodes values as native maps, then native telemetry selects and invokes the registered handlers. Unrouted callbacks run inline; prefix routing hands an occurrence to the chosen forwarder before the forwarder invokes handlers.

flowchart LR
    P[Producer] --> E{emit}
    E -->|unrouted| N1[Native :telemetry]
    N1 --> H1[Typed handler<br/>producer process]
    E -->|routed prefix| F[Supervised bounded forwarder]
    F --> N2[Native :telemetry]
    N2 --> H2[Typed handler<br/>forwarder process]

Applications supervise and configure forwarders at startup. This excerpt from the forwarding example uses gleam/erlang/process, gleam/otp/static_supervisor, and sinal/forwarder to route a library’s event prefix:

pub fn isolate_library(name: process.Name(forwarder.Message)) -> Nil {
  let observations = forwarder.new(name)
  let assert Ok(_) =
    static_supervisor.new(static_supervisor.OneForOne)
    |> static_supervisor.add(forwarder.supervised(observations))
    |> static_supervisor.start
  // Every `sinal.emit` of an event named `my_library..` now returns as soon
  // as the event is handed to the forwarder.
  forwarder.route(["my_library"], observations)
}

Operational limits

Examples and reference

The compiled usage examples cover handlers, scopes, spans, correlation, and forwarding. See the API docs for events and subscriptions, field codecs, correlation, spans, and forwarding. The design documents ownership and contracts; benchmark notes record a dated local run and its limits.

✨ Search Document