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_idid 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
-
AlreadyDetachedThe handler was no longer attached, for example because telemetry removed it after it failed.
-
DetachCrashed(description: String)Detaching raised;
descriptionis 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
-
MalformedMeasurements(fields.FieldError) -
MalformedMetadata(fields.FieldError) -
HandlerReturned(e)
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
-
SubscriptionCleanupFailure(index: Int, failure: CleanupFailure)
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
-
SubscriptionCompletion( work_result: a, cleanup_failures: List(SubscriptionCleanupFailure), )
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
-
SubscriptionAttachFailed( index: Int, error: AttachError, rollback_failures: List(SubscriptionCleanupFailure), )
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_failurereceivesMalformedMeasurementsorMalformedMetadata,runis skipped for that one event, and the handler stays attached. - When
runreturns an error,on_failurereceivesHandlerReturned, and telemetry then removes the handler and emits[telemetry, handler, failure]. So does a crash inrunor inon_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 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,
workdoes not run, earlier subscriptions receive cleanup attempts, and the error names the failing index and any rollback failures. - When
workraises 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.