Otel.SDK.Logs.LogRecordProcessor.Simple (otel v0.2.0)

Copy Markdown View Source

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

FunctionRole
on_emit/3, enabled?/3, shutdown/2, force_flush/2SDK (Simple implementation)
start_link/1SDK (lifecycle)

References

Summary

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

Functions

enabled?(ctx, scope, opts, config)

SDK (Simple implementation) — Always returns true; the Simple processor has no filtering policy of its own (logs/sdk.md §LogRecordProcessor L420 "MAY implement").

force_flush(map, timeout \\ 30000)

@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.

on_emit(log_record, ctx, map)

@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.

running(arg1, arg2, state)

@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()
  )

shutdown(map, timeout \\ 30000)

@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.

start_link(config)

@spec start_link(config :: start_link_config()) :: :gen_statem.start_ret()