Remedia

Structured logging and OpenTelemetry

Configure Remedia tracing, bounded diagnostic events, and optional OTLP export.

Configure structured Rust diagnostics, console logging, and optional OTLP traces and metrics. Server operators usually need the environment-variable table below. Rust embedders can create an owned observability context. Node.js applications can subscribe to embedded debug events.

Environment variables

VariableDefaultBehavior
REMEDIA_DEBUGunset (0)1 enables debug logs for Remedia components.
REMEDIA_LOGwarnTracing filter directives, such as remedia_core=debug,remedia_discord=info. Takes precedence over REMEDIA_DEBUG.
REMEDIA_LOG_FORMATtextSelects text or one-event-per-line json console output.
REMEDIA_OTEL_ENABLEDfalseEnables OTLP traces and metrics only when set to true.
OTEL_SERVICE_NAMESDK defaultSets the OpenTelemetry service.name resource attribute.
OTEL_RESOURCE_ATTRIBUTESunsetAdds standard comma-separated resource attributes.
OTEL_EXPORTER_OTLP_ENDPOINTOTLP HTTP defaultStandard endpoint shared by traces and metrics unless a signal-specific endpoint is set.
OTEL_EXPORTER_OTLP_TRACES_ENDPOINTinheritedOverrides the general endpoint for traces.
OTEL_EXPORTER_OTLP_METRICS_ENDPOINTinheritedOverrides the general endpoint for metrics.
OTEL_EXPORTER_OTLP_PROTOCOLhttp/protobufSelects the general OTLP protocol.
OTEL_EXPORTER_OTLP_TRACES_PROTOCOLinheritedOverrides the protocol for traces.
OTEL_EXPORTER_OTLP_METRICS_PROTOCOLinheritedOverrides the protocol for metrics.
OTEL_TRACES_SAMPLERparentbased_always_onUses the OpenTelemetry SDK sampler. Supported values are always_on, always_off, traceidratio, parentbased_always_on, parentbased_always_off, and parentbased_traceidratio.
OTEL_TRACES_SAMPLER_ARGSDK defaultSampling ratio for a ratio sampler. Remedia validates values are between 0.0 and 1.0.

Supported OTLP protocols are grpc, http/protobuf, and http/json. The default HTTP/protobuf exporter follows the standard OTLP endpoint convention. Signal-specific endpoint and protocol variables take precedence over their general counterparts. GRPC exporter creation requires an active Tokio runtime. Invalid Remedia filters, booleans, formats, protocols, sampler names, and sampler ratios fail initialization with a typed configuration error. Invalid endpoint configuration is reported by the exporter builder.

REMEDIA_LOG filters console lines and the bounded diagnostic receiver. When OTLP is enabled, Remedia lifecycle spans and events at INFO or above are exported independently of that filter, so the default warn console setting does not disable traces.

Native FFmpeg diagnostics

FFmpeg has a process-wide native logging callback separate from Rust tracing. When the core first initializes FFmpeg, Remedia installs one centralized callback for the process. Additional sessions and Remedia instances reuse it. FFmpeg calls made synchronously inside Remedia are routed to the current Remedia tracing dispatcher with target remedia_core::ffmpeg and event name core.ffmpeg.log.

Native error, warning, info, debug, and trace levels map to the corresponding tracing severities. They use the same REMEDIA_LOG filter as the structured Rust events. Debug and trace native messages are requested from FFmpeg only when REMEDIA_DEBUG=1 or a matching REMEDIA_LOG directive asks for them. Remedia does not change FFmpeg verbosity during playback. The native verbosity choice is read once at the process's first Remedia FFmpeg initialization. Changing environment variables afterward does not mutate FFmpeg's global setting.

Repeated identical messages from the same FFmpeg context and severity are rate-limited for ten seconds. The next forwarded event reports how many repeats were suppressed. Messages are bounded and redacted for URI values and credential-like key/value fields. FFmpeg skip-sample and discard-padding messages are diagnostics, not playback errors. The decoder supplies the selected stream's packet time base so FFmpeg can adjust timestamps when it removes encoder delay or padding described by codec metadata.

FFmpeg exposes no getter for a previously installed logging callback. Remedia installs its callback once and forwards calls outside a Remedia-marked operation to FFmpeg's default callback up to the verbosity level that was active at installation. This preserves default FFmpeg behavior, but an application's custom native FFmpeg callback cannot be discovered or chained. FFmpeg can also emit messages from internal native worker threads, where thread-local Remedia attribution is unavailable. Those are treated as unmarked. Applications embedding another FFmpeg consumer with a custom callback should coordinate native logging ownership before initializing Remedia. The Node binding does not install a Rust tracing subscriber, but the native FFmpeg callback itself is process-global.

The embedded Node binding disables console output to avoid duplicating events that the application handles through JavaScript. Its REMEDIA_LOG, REMEDIA_DEBUG, and OTLP settings still control filtering and export. REMEDIA_LOG_FORMAT is validated but does not change the structured JavaScript event object.

Examples:

REMEDIA_DEBUG=1 REMEDIA_LOG_FORMAT=json cargo run -p remedia-core --example playback -- file ./audio.wav
REMEDIA_LOG='remedia_core=trace,remedia_discord=info' cargo run -p remedia-discord --example play_file
REMEDIA_OTEL_ENABLED=true \
OTEL_SERVICE_NAME=remedia-core \
OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318 \
OTEL_TRACES_SAMPLER=parentbased_traceidratio \
OTEL_TRACES_SAMPLER_ARG=0.25 \
cargo run -p remedia-core --example playback -- file ./audio.wav

Events, spans, and metrics

Events carry a stable callsite name, target, severity, human-readable message, and structured fields. The bounded event route inherits correlation IDs from the active Remedia span when the event does not set one directly. Playback operations use the internal playback_id. Discord operations use voice_connection_id. These process-unique numeric IDs correlate Remedia work and are unrelated to Discord's secret voice session ID. Decoder events identify only the source kind (file, http, http_sequential, buffer or byte_stream), not its path or URL.

The Rust DiagnosticEvent carries this structured shape (shown here as JSON-like data):

{
  "timestamp": "2026-11-02T10:00:00Z",
  "severity": "Info",
  "target": "remedia_core::playback",
  "event": "core.playback.started",
  "message": "playback started",
  "playback_id": 42,
  "voice_connection_id": null,
  "fields": { "codec": "flac", "duration_known": true }
}

Spans and packet diagnostics

Lifecycle spans cover media opening, playback worker lifetime, voice connection establishment, DAVE initialization, and disconnection. Packet-level RTP and Opus details use trace and include only duration, byte count, scheduling delay, and drop reason. Remedia does not emit debug events for every media or network packet.

remedia-server additionally emits server.plugin_host.discovered, server.plugin_host.discovery_failed, server.plugin_host.operation_completed and server.plugin_host.operation_failed (target remedia_server::plugin_host). They carry the configured host ID, plugin ID, operation name, attempt number, duration and a low-cardinality error label. They never contain endpoint or media URLs, tokens or operation input. Per-host health and counters are also in node.stats. See Playing audio through plugins.

Metric instruments

OTLP metrics use low-cardinality attributes only. They do not include guild IDs, playback IDs, voice connection IDs, source paths, or URLs. The current instruments include:

  • Active playback sessions and voice connections (up/down counters).
  • Playback starts, natural completions, and failures.
  • Voice connection and reconnect attempts.
  • RTP packets sent and dropped, plus packet underruns.
  • Decoder startup and voice connection establishment durations.
  • RTP scheduling delay.

Flush and shut down exporters

Providers use the OpenTelemetry SDK's asynchronous trace batch processor and periodic metric reader. Console output has a bounded 1,024-line queue and uses lossy non-blocking writes. Inspect dropped_console_lines() when diagnosing log pressure. Structured event delivery is bounded too. Call force_flush() and shutdown() from normal application shutdown code so pending telemetry is exported and owned provider threads are released. These methods may block and do not belong in an audio callback or frame consumer.

Security and embedding

Remedia does not log Discord tokens, voice session IDs, encryption keys, MLS material, authenticated URLs, PCM sample values, or Opus payload bytes. Structured event routing redacts sensitive field names and URL values as a defense in depth. Applications should follow the same rule when adding their own tracing fields. Never place credentials in an event message.

In a native application, use a local ObservabilityHandle to share one metrics provider and bounded diagnostic receiver across sessions. The Node binding owns one such context per Remedia instance. It does not install or replace a process-global tracing subscriber. Its JavaScript callback runs through a bounded ThreadsafeFunction, never directly on a media worker. The standalone server initializes the same context in its native runtime.

OTLP log export is unavailable. Console logs and the bounded structured Rust event route remain available. Node diagnostic events preserve structured fields; 64-bit integer field values are delivered as decimal strings to avoid JavaScript precision loss.

Initialize an application context

An embedded application can pass one owned context to independent playback sessions and voice connections. This keeps diagnostics associated with each Remedia component without taking over the host application's global subscriber.

use remedia_core::PlaybackSession;
use remedia_observability::Observability;

let mut observability = Observability::from_env()?;
let diagnostics = observability.diagnostics();
let handle = observability.handle();
let playback = PlaybackSession::with_observability(handle.clone());

// Pass `handle` to DiscordVoiceConnection::connect_with_observability(info, handle).await
// when the application has obtained voice session information from its own Gateway client.

// Drain `diagnostics` on an application task. Do not drain it on a decoder or audio worker.
let _ = (playback, diagnostics);
observability.force_flush()?;
observability.shutdown()?;
# Ok::<(), Box<dyn std::error::Error>>(())

Observability::from_env() creates a local dispatcher. It does not affect code using the process default subscriber unless that code runs through a Remedia component given the returned handle. Direct AudioDecoder use follows the application's current tracing subscriber. An application may explicitly call try_install_global() if it owns global subscriber initialization.

The call fails with GlobalSubscriberError when another subscriber is already installed and never replaces it. Global installation cannot be undone because tracing has no subscriber-uninstall operation. Keep the Observability owner alive while the installed subscriber is in use, then flush and shut it down during application shutdown. When the host already owns tracing, it can keep that subscriber and pass a local Remedia handle to playback and voice connections instead.

DiagnosticReceiver is a bounded Rust event route for native hosts. Cloned receivers compete for events. The queue holds at most 256 events and uses non-blocking sends; dropped_events() reports overflow. The Node binding creates one receiver per Remedia instance and forwards events from a dedicated worker through a bounded N-API callback queue. remedia.on('debug', listener) receives structured events, and remedia.droppedDebugEvents counts drops in both bounded queues.

On this page