Simple LogRecordProcessor (logs/sdk.md §Simple processor
L514-L526).
Spec L516-L519 — "passes finished logs and passes the
export-friendly ReadableLogRecord representation to the
configured LogRecordExporter, as soon as they are
finished."
Spec L521-L522 — "The processor MUST synchronize calls to
LogRecordExporter's Export to make sure that they are
not invoked concurrently." — implemented by routing every
emit through :gen_statem.call/2, which is inherently
serial per-process.
Spec L526 — the only configurable parameter is exporter.
Lifecycle ownership
This processor is started by Otel.SDK.Logs.LoggerProvider
(typical OTel SDK pattern, matching erlang's
otel_tracer_server.erl:158-183). The user supplies the
start_link/1 config to LoggerProvider's processors list;
LoggerProvider then calls start_link/1, captures the PID,
links to it, and passes that PID to the behaviour callbacks
via the %{pid: pid} config. The gen_statem is therefore
unregistered (no atom name) — PIDs are first-class.
Non-blocking emit
on_emit/3 is non-blocking per spec §LogRecordProcessor
L394-L396 — "called synchronously on the thread that
emitted the LogRecord, therefore it SHOULD NOT block or
throw exceptions". The processor uses :gen_statem.cast/2
to enqueue the record and returns immediately; the
gen_statem then runs the exporter's export/2 in its own
process. This satisfies §Simple processor L515-L518 ("as
soon as they are finished" — no batching) together with
L521-L522 ("MUST synchronize calls to LogRecordExporter's
Export to make sure that they are not invoked concurrently").
opentelemetry-erlang does not have a separate "simple log
processor"; logs flow through otel_log_handler.erl which
also uses gen_statem:cast for the emit path (matching
L394-L396). The earlier divergence-from-erlang note here
was citing the erlang span simple processor
(apps/opentelemetry/src/otel_simple_processor.erl),
which does block via gen_statem:call — that's a
span-side spec gap, not a logs reference. Our logs
implementation aligns with both spec L394-L396 and the
erlang logs path.
State model and shutdown
One :gen_statem state, :running. The processor accepts
:export and :force_flush requests there. Shutdown
terminates the genstatem rather than transitioning to a
parked state — shutdown/2 calls
`:genstatem.stop(__MODULE, :normal, timeout), which invokesterminate/3(where the exporter'sforce_flush/1andshutdown/1` run, satisfying spec L469
"Shutdown MUST include the effects of ForceFlush") and
then exits the process cleanly.
Late-arriving on_emit/3 after termination is silently
dropped — :gen_statem.cast/2 to a dead pid is dead-lettered
and returns :ok. This satisfies spec §LogRecordProcessor
L462-L464 "SDKs SHOULD ignore these calls gracefully" (the
spec only mentions OnEmit explicitly).
Late force_flush/2 or a second shutdown/2 go through
:gen_statem.call / :gen_statem.stop, catch :exit, {:noproc, _} / :exit, :noproc and return
{:error, :already_shutdown} per spec L466-L467 / L492-L493
("succeeded, failed or timed out" — failed is the right
classification here, not silent success).
When the day comes that we want hung-exporter timeout
isolation, an additional :exporting state with a runner
process would slot in here cleanly. For now, single state.
No child_spec/1 is exposed — the LoggerProvider is the
only supervisor for this processor and it calls
start_link/1 directly. Users who want to put the processor
under their own Supervisor can write a one-line spec inline.
Public API
| Function | Role |
|---|---|
on_emit/3, enabled?/3, shutdown/2, force_flush/2 | SDK (Simple implementation) |
start_link/1 | SDK (lifecycle) |
References
- OTel Logs SDK Simple processor:
opentelemetry-specification/specification/logs/sdk.md§Simple processor - Parent behaviour:
Otel.SDK.Logs.LogRecordProcessor
Summary
Types
start_link/1 configuration map.
Functions
SDK (Simple implementation) — Always returns true; the
Simple processor has no filtering policy of its own
(logs/sdk.md §LogRecordProcessor L420 "MAY implement").
SDK (Simple implementation) — Forwards force_flush/1
to the configured exporter. The processor itself buffers
nothing beyond the gen_statem mailbox (each cast immediately
triggers an export), but spec §LogRecordProcessor L484-L486
makes it a built-in MUST to "invoke ForceFlush on [the
exporter]" — the exporter may have its own buffering (HTTP
keep-alive batching, OS write buffers, etc.).
SDK (Simple implementation) — Cast the emitted record
to the gen_statem for serialised export, satisfying spec
§LogRecordProcessor L394-L396 ("SHOULD NOT block") and
§Simple processor L521-L522 ("MUST synchronize calls to
LogRecordExporter's Export"). Returns :ok immediately;
late casts after termination are silently dead-lettered, per
spec L462-L464.
SDK (Simple implementation) — Synchronously stop the
gen_statem via :gen_statem.stop/3. The terminate/3
callback runs the exporter's force_flush/1 then
shutdown/1 (spec L469) before the process exits.
Types
start_link/1 configuration map.
:exporter(required) —{module, opts}wheremoduleimplementsOtel.SDK.Logs.LogRecordExporterandoptsis passed tomodule.init/1once at startup.
Functions
@spec enabled?( ctx :: Otel.API.Ctx.t(), scope :: Otel.API.InstrumentationScope.t(), opts :: Otel.SDK.Logs.LogRecordProcessor.enabled_opts(), config :: Otel.SDK.Logs.LogRecordProcessor.config() ) :: boolean()
SDK (Simple implementation) — Always returns true; the
Simple processor has no filtering policy of its own
(logs/sdk.md §LogRecordProcessor L420 "MAY implement").
@spec force_flush( config :: Otel.SDK.Logs.LogRecordProcessor.config(), timeout :: timeout() ) :: :ok | {:error, term()}
SDK (Simple implementation) — Forwards force_flush/1
to the configured exporter. The processor itself buffers
nothing beyond the gen_statem mailbox (each cast immediately
triggers an export), but spec §LogRecordProcessor L484-L486
makes it a built-in MUST to "invoke ForceFlush on [the
exporter]" — the exporter may have its own buffering (HTTP
keep-alive batching, OS write buffers, etc.).
timeout (default 30_000ms) bounds the call. Returns
{:error, :timeout} if exceeded, per spec L492-L493 /
L487-L491. Returns {:error, :already_shutdown} when the
gen_statem has already terminated — spec L492-L493
classifies this as failed.
@spec on_emit( log_record :: Otel.SDK.Logs.LogRecord.t(), ctx :: Otel.API.Ctx.t(), config :: Otel.SDK.Logs.LogRecordProcessor.config() ) :: :ok
SDK (Simple implementation) — Cast the emitted record
to the gen_statem for serialised export, satisfying spec
§LogRecordProcessor L394-L396 ("SHOULD NOT block") and
§Simple processor L521-L522 ("MUST synchronize calls to
LogRecordExporter's Export"). Returns :ok immediately;
late casts after termination are silently dead-lettered, per
spec L462-L464.
@spec running( event_type :: :gen_statem.event_type(), event_content :: {:export, Otel.SDK.Logs.LogRecord.t()} | :force_flush, state :: Otel.SDK.Logs.LogRecordProcessor.Simple.State.t() ) :: :gen_statem.event_handler_result( Otel.SDK.Logs.LogRecordProcessor.Simple.State.t() )
@spec shutdown( config :: Otel.SDK.Logs.LogRecordProcessor.config(), timeout :: timeout() ) :: :ok | {:error, term()}
SDK (Simple implementation) — Synchronously stop the
gen_statem via :gen_statem.stop/3. The terminate/3
callback runs the exporter's force_flush/1 then
shutdown/1 (spec L469) before the process exits.
timeout (default 30_000ms) bounds the wait for terminate
to complete. Returns {:error, :timeout} if exceeded,
per spec L466-L467 / L487-L491. Returns
{:error, :already_shutdown} when the gen_statem has
already terminated — the spec L466-L467 result enumeration
("succeeded, failed or timed out") classifies this as
failed rather than silently succeeded.
@spec start_link(config :: start_link_config()) :: :gen_statem.start_ret()