Behaviour for log record exporters
(logs/sdk.md §LogRecordExporter L549-L645 + §Concurrency
requirements L647-L660).
Exporters serialize batches of Otel.SDK.Logs.LogRecord to a
protocol and transmit them to a backend. Spec L555-L557:
"The protocol exporter is expected to be primarily a simple
telemetry data encoder and transmitter."
Spec L572-L573 — "Export should not be called concurrently
with other Export calls for the same exporter instance."
Note this is a SHOULD-strength clause (lowercase "should
not" in the spec text), not a MUST. The SDK's
Otel.SDK.Logs.LogRecordProcessor.Simple and
Otel.SDK.Logs.LogRecordProcessor.Batch serialise export
calls through a gen_statem so exporter implementations may
assume single-threaded export/2.
Spec L659-L660 — "ForceFlush and Shutdown MUST be safe
to be called concurrently." The MUST applies independently
of export/2 — the two cleanup callbacks must tolerate
concurrent invocation with each other and with anything
else.
Public API
| Function | Role |
|---|---|
init/1 | SDK (SDK lifecycle) |
export/2 | SDK (OTel API MUST) |
force_flush/1 | SDK (OTel API MUST) |
shutdown/1 | SDK (OTel API MUST) |
Design notes
Four deliberate divergences from
references/opentelemetry-erlang/apps/opentelemetry_experimental/src/otel_exporter_logs.erl:
force_flush/1callback present — erlang'sotel_exporter_logs.erlexports onlyinit/1,export/3,shutdown/1. Spec L564 lists ForceFlush in the MUST-support functions, so we add it here. Erlang has a spec gap; we follow spec.- Resource embedded in
LogRecord— erlang'sexport(list(), otel_resource:t(), term())passes Resource as a separate arg. OurOtel.SDK.Logs.LogRecordalready carriesresourceper spec L283 ("Resource information (implicitly) associated with the LogRecord"), so passing it again would duplicate state. Both shapes satisfy the spec. - Binary
ExportResult— erlang returnsok | success | failed_not_retryable | failed_retryable. Spec L598-L608 definesExportResultas binary Success/Failure with no retry-class signal, and spec L585-L590 makes retry the exporter's responsibility (the processor doesn't dispatch on result). We use:ok | :errorto match spec semantics; richer retry classification stays internal to each exporter implementation. - No
:ignorefrominit/1— erlang'sotel_exporter_logs.erlallowsinit/1to returnignore, which the owning processor maps to a silently-dropping no-op exporter. The OTel spec defines no such mechanism, andcode-conventions.md§"Happy path only" rules out this kind of third-rail branch (neither success nor failure). Soft-disable belongs at the supervision/config layer — omit the exporter from the processor list when disabled, rather than letting the exporter return a "drop everything" signal at runtime. Realinit/1failuresraiseinstead, surfacing immediately via the supervisor.
References
- OTel Logs SDK:
opentelemetry-specification/specification/logs/sdk.md - Erlang reference:
opentelemetry-erlang/apps/opentelemetry_experimental/src/otel_exporter_logs.erl
Summary
Types
Implementer-owned state returned by init/1 and threaded
through export/2, force_flush/1, shutdown/1. Opaque to
the SDK — each exporter implementation defines its own shape
(e.g. HTTP client config, file handles, batch buffers).
Callbacks
SDK (OTel API MUST) — "Export" (logs/sdk.md L566-L612).
SDK (OTel API MUST) — "ForceFlush"
(logs/sdk.md L614-L630).
SDK (SDK lifecycle) — Initializes the exporter from the
caller-supplied config and returns the implementer-owned
state to thread through subsequent calls.
SDK (OTel API MUST) — "Shutdown"
(logs/sdk.md L632-L643).
Types
@type state() :: term()
Implementer-owned state returned by init/1 and threaded
through export/2, force_flush/1, shutdown/1. Opaque to
the SDK — each exporter implementation defines its own shape
(e.g. HTTP client config, file handles, batch buffers).
Callbacks
@callback export(log_records :: [Otel.SDK.Logs.LogRecord.t()], state :: state()) :: :ok | :error
SDK (OTel API MUST) — "Export" (logs/sdk.md L566-L612).
Exports a batch of Otel.SDK.Logs.LogRecord. Spec L582-L583:
"Export MUST NOT block indefinitely, there MUST be a
reasonable upper limit after which the call must time out
with an error result (Failure)."
Spec L598-L608 defines the return as ExportResult:
:ok— Success (batch successfully exported):error— Failure (batch must be dropped, e.g. unserializable)
Spec L585-L590 — concurrent requests and retry logic are the exporter's responsibility, not the processor's.
Spec L637-L639 — after shutdown/1, subsequent export/2
calls SHOULD return :error. Implementations that expose a
shutdown state must enforce this themselves.
SDK (OTel API MUST) — "ForceFlush"
(logs/sdk.md L614-L630).
Hint to flush any buffered records as soon as possible.
Spec L620-L621: "ForceFlush SHOULD provide a way to let
the caller know whether it succeeded, failed or timed out."
:ok— flush completed{:error, reason}— flush failed or timed out (reasonis implementation-defined, e.g.:timeout)
Spec L627-L630 — ForceFlush SHOULD complete or abort
within some timeout; SDK authors MAY make the timeout
configurable.
SDK (SDK lifecycle) — Initializes the exporter from the
caller-supplied config and returns the implementer-owned
state to thread through subsequent calls.
Not in the spec (lifecycle is language-specific). Diverges
from the erlang reference init/1 (which also allows
:ignore) — see the ## Design notes section below for
the rationale.
SDK (OTel API MUST) — "Shutdown"
(logs/sdk.md L632-L643).
Cleans up exporter resources. Spec L637-L639: "Shutdown
SHOULD be called only once for each LogRecordExporter
instance. After the call to Shutdown subsequent calls to
Export are not allowed and SHOULD return a Failure result."
Spec L641-L643: "Shutdown SHOULD NOT block indefinitely
(e.g. if it attempts to flush the data and the destination
is unavailable)."
:ok— shutdown completed{:error, reason}— shutdown failed or timed out