Exporters Guide

View Source

This guide covers exporting telemetry data (spans, metrics, logs) to various backends.

Table of Contents

  1. Overview
  2. Span Exporters
  3. Metrics Exporters
  4. Log Exporters
  5. Custom Exporters
  6. Batch Processing
  7. Configuration
  8. Sending to External Collectors

Overview

The instrument library provides export systems for spans (traces), metrics, and logs. All systems support batch export and multiple simultaneous backends.

  • Span exporters handle trace data via instrument_exporter
  • Metrics exporters handle metric data via instrument_metrics_exporter
  • Log exporters handle log data via instrument_log_exporter

All exporter systems start automatically with the application.

Quick Start

%% Register span exporters
instrument_exporter:register(instrument_exporter_console:new()),
instrument_exporter:register(instrument_exporter_otlp:new(#{
    endpoint => "http://localhost:4318"
})),

%% Register metrics exporters
instrument_metrics_exporter:register(instrument_metrics_exporter_console:new()),
instrument_metrics_exporter:register(instrument_metrics_exporter_otlp:new(#{
    endpoint => "http://localhost:4318"
})),

%% Register log exporters
instrument_log_exporter:register(instrument_log_exporter_console:new()),
instrument_log_exporter:register(instrument_log_exporter_otlp:new(#{
    endpoint => "http://localhost:4318"
})),
%% Enable log export via logger
instrument_logger:install(#{exporter => true}),

%% Spans are exported when they end
instrument_tracer:with_span(<<"my_operation">>, fun() ->
    do_work()
end).

%% Metrics are collected and exported periodically (default: 60s)
instrument_metric:inc_counter(my_counter).

%% Logs are exported when using logger
logger:info("This will be exported with trace context").

Span Exporters

Console Exporter

The console exporter prints spans to stdout or a file. Useful for debugging and development.

Basic Usage

%% Default: text format to stdout
instrument_exporter:register(instrument_exporter_console:new()).

Configuration Options

OptionTypeDefaultDescription
formattext | jsontextOutput format
outputstandard_io | standard_error | {file, Path}standard_ioOutput destination

Text Format

instrument_exporter:register(instrument_exporter_console:new(#{
    format => text
})).

Output example:

=== SPAN ===
Name:       process_order
TraceId:    abc123def456...
SpanId:     789xyz...
ParentId:   none
Kind:       server
Duration:   125.34ms
Status:     OK
Attributes:
  order.id: <<"12345">>
  customer.id: <<"67890">>
Events:
  @2024-01-15T10:30:45.123456Z: order_validated
  @2024-01-15T10:30:45.234567Z: payment_processed
============

JSON Format

instrument_exporter:register(instrument_exporter_console:new(#{
    format => json
})).

Output example (one line per span):

{"name":"process_order","traceId":"abc123...","spanId":"789xyz...","kind":"server","status":{"code":"OK"},...}

Writing to File

instrument_exporter:register(instrument_exporter_console:new(#{
    format => json,
    output => {file, "/var/log/traces.jsonl"}
})).

OTLP Exporter

The OTLP exporter sends spans to an OpenTelemetry Collector or compatible backend using OTLP/HTTP with JSON encoding.

Basic Usage

%% Export to local collector
instrument_exporter:register(instrument_exporter_otlp:new(#{
    endpoint => "http://localhost:4318"
})).

Configuration Options

OptionTypeDefaultDescription
endpointstring | binaryrequiredBase URL of OTLP receiver
headersmap#{}Additional HTTP headers
compressionnone | gzipnoneRequest compression
timeoutinteger10000Request timeout in ms
traces_pathbinary<<"/v1/traces">>API path

With Authentication

instrument_exporter:register(instrument_exporter_otlp:new(#{
    endpoint => "https://otel.example.com:4318",
    headers => #{
        <<"Authorization">> => <<"Bearer your-api-key">>,
        <<"X-Custom-Header">> => <<"value">>
    }
})).

With Compression

instrument_exporter:register(instrument_exporter_otlp:new(#{
    endpoint => "http://localhost:4318",
    compression => gzip
})).

Configuring Service Name

Set the service name via application environment:

%% In sys.config
{instrument, [
    {service_name, <<"my-service">>}
]}

Or at runtime:

application:set_env(instrument, service_name, <<"my-service">>).

Metrics Exporters

The metrics exporter system collects and exports metrics periodically (default: 60 seconds). Metrics are collected from the instrument registry and sent to all registered exporters.

Metrics Console Exporter

The console exporter prints metrics to stdout for debugging.

Basic Usage

%% Default: text format to stdout
instrument_metrics_exporter:register(instrument_metrics_exporter_console:new()).

Configuration Options

OptionTypeDefaultDescription
formattext | jsontextOutput format
outputstandard_io | standard_error | {file, Path}standard_ioOutput destination

Text Format

instrument_metrics_exporter:register(instrument_metrics_exporter_console:new(#{
    format => text
})).

Output example:

# HTTP request counter
[2026-03-31T12:00:00.000000Z] http_requests_total{method="GET",status="200"} 1234
[2026-03-31T12:00:00.000000Z] active_connections{} 42

JSON Format

instrument_metrics_exporter:register(instrument_metrics_exporter_console:new(#{
    format => json
})).

Output example (one line per metric):

{"name":"http_requests_total","type":"counter","dataPoints":[{"attributes":{},"value":1234,"timeUnixNano":1711879200000000000}]}

Metrics OTLP Exporter

The OTLP metrics exporter sends metrics to an OpenTelemetry Collector using OTLP/HTTP with JSON encoding.

Basic Usage

instrument_metrics_exporter:register(instrument_metrics_exporter_otlp:new(#{
    endpoint => "http://localhost:4318"
})).

Configuration Options

OptionTypeDefaultDescription
endpointstring | binaryrequiredBase URL of OTLP receiver
headersmap#{}Additional HTTP headers
compressionnone | gzipnoneRequest compression
timeoutinteger10000Request timeout in ms
metrics_pathbinary<<"/v1/metrics">>API path

With Authentication

instrument_metrics_exporter:register(instrument_metrics_exporter_otlp:new(#{
    endpoint => "https://otel.example.com:4318",
    headers => #{
        <<"Authorization">> => <<"Bearer your-api-key">>
    }
})).

With Compression

instrument_metrics_exporter:register(instrument_metrics_exporter_otlp:new(#{
    endpoint => "http://localhost:4318",
    compression => gzip
})).

Metrics Export Configuration

Configure the export interval via application environment:

%% In sys.config
{instrument, [
    {metrics_export_interval, 30000}  %% 30 seconds
]}

Or at runtime:

application:set_env(instrument, metrics_export_interval, 30000).

Manual Metrics Flush

Force immediate export of metrics:

ok = instrument_metrics_exporter:flush().

Log Exporters

The log exporter system integrates with Erlang's logger to export log records to various backends. Log records include trace context when available, enabling correlation between logs and traces.

Log Console Exporter

The console log exporter prints log records to stdout or stderr. Useful for debugging and development.

Basic Usage

%% Register console exporter for logs
instrument_log_exporter:register(instrument_log_exporter_console:new()),

%% Enable exporter mode in logger
instrument_logger:install(#{exporter => true}).

Configuration Options

OptionTypeDefaultDescription
formattext | jsontextOutput format
outputstandard_io | standard_errorstandard_ioOutput destination

Text Format

instrument_log_exporter:register(instrument_log_exporter_console:new(#{
    format => text
})).

Output example:

[2026-03-31T12:00:00.000000Z] INFO [trace_id=abc123... span_id=def456...] User logged in {"user":"john"}

JSON Format

instrument_log_exporter:register(instrument_log_exporter_console:new(#{
    format => json
})).

Output example (OTLP-compatible, one line per log):

{"timeUnixNano":"...","severityText":"INFO","body":{"stringValue":"User logged in"},"traceId":"...","spanId":"..."}

Log File Exporter

The file log exporter writes log records to a file with optional rotation support.

Basic Usage

instrument_log_exporter:register(instrument_log_exporter_file:new(#{
    path => "/var/log/app.log"
})).

Configuration Options

OptionTypeDefaultDescription
pathstring | binaryrequiredLog file path
formattext | jsontextOutput format
max_sizeinteger10485760Max file size in bytes (10MB), 0 = unlimited
max_filesinteger5Number of rotated files to keep
compressbooleanfalseCompress rotated files with gzip

With Rotation

instrument_log_exporter:register(instrument_log_exporter_file:new(#{
    path => "/var/log/app.log",
    format => json,
    max_size => 52428800,   %% 50MB
    max_files => 10,
    compress => true
})).

File rotation behavior:

  • When max_size is reached: app.log -> app.log.1 -> app.log.2 -> ...
  • If compress=true: rotated files become app.log.1.gz, app.log.2.gz, etc.
  • Oldest files beyond max_files are deleted

Log OTLP Exporter

The OTLP log exporter sends log records to an OpenTelemetry Collector using OTLP/HTTP with JSON encoding.

Basic Usage

instrument_log_exporter:register(instrument_log_exporter_otlp:new(#{
    endpoint => "http://localhost:4318"
})).

Configuration Options

OptionTypeDefaultDescription
endpointstring | binaryrequiredBase URL of OTLP receiver
headersmap#{}Additional HTTP headers
compressionnone | gzipnoneRequest compression
timeoutinteger10000Request timeout in ms
logs_pathbinary<<"/v1/logs">>API path

With Authentication

instrument_log_exporter:register(instrument_log_exporter_otlp:new(#{
    endpoint => "https://otel.example.com:4318",
    headers => #{
        <<"Authorization">> => <<"Bearer your-api-key">>
    },
    compression => gzip
})).

Logger Integration

To enable log export, install the logger integration with exporter mode:

%% Register exporters first
instrument_log_exporter:register(instrument_log_exporter_console:new()),

%% Enable exporter mode (logs go to registered exporters)
instrument_logger:install(#{exporter => true}).

%% Now logs are automatically exported
logger:info("This message will be exported"),
logger:error("This error will be exported with trace context").

Within a span, logs automatically include trace context:

instrument_tracer:with_span(<<"process_order">>, fun() ->
    logger:info("Processing order"),  %% Includes trace_id and span_id
    do_work()
end).

Manual Log Flush

Force immediate export of pending logs:

ok = instrument_log_exporter:flush().

Custom Exporters

You can create custom exporters by implementing the exporter behaviour.

Behaviour Callbacks

-module(my_exporter).

%% Required callbacks
-export([init/1, export/2, shutdown/1, force_flush/1]).

%% Initialize the exporter
%% Config is the map passed during registration
-spec init(Config :: map()) -> {ok, State} | {error, Reason}.
init(Config) ->
    %% Setup your exporter
    {ok, #state{...}}.

%% Export a batch of spans
%% Called periodically or when batch is full
-spec export(Spans :: [#span{}], State) -> {ok, NewState} | {error, Reason, NewState}.
export(Spans, State) ->
    %% Send spans to your backend
    lists:foreach(fun(Span) ->
        send_to_backend(Span, State)
    end, Spans),
    {ok, State}.

%% Shutdown the exporter
%% Called when unregistered or application stops
-spec shutdown(State) -> ok.
shutdown(State) ->
    %% Cleanup resources
    ok.

%% Force flush pending data
%% Called on explicit flush
-spec force_flush(State) -> {ok, NewState}.
force_flush(State) ->
    {ok, State}.

Example: Kafka Exporter

-module(instrument_exporter_kafka).

-export([new/1, init/1, export/2, shutdown/1, force_flush/1]).

-record(state, {
    topic :: binary(),
    producer :: pid()
}).

new(Config) ->
    #{module => ?MODULE, config => Config}.

init(#{topic := Topic, brokers := Brokers}) ->
    {ok, Producer} = kafka_producer:start(Brokers),
    {ok, #state{topic = Topic, producer = Producer}}.

export(Spans, #state{topic = Topic, producer = Producer} = State) ->
    Messages = [encode_span(S) || S <- Spans],
    ok = kafka_producer:send(Producer, Topic, Messages),
    {ok, State}.

shutdown(#state{producer = Producer}) ->
    kafka_producer:stop(Producer),
    ok.

force_flush(State) ->
    {ok, State}.

encode_span(Span) ->
    %% Convert span to your preferred format
    json:encode(span_to_map(Span)).

Registering Custom Exporters

instrument_exporter:register(instrument_exporter_kafka:new(#{
    topic => <<"traces">>,
    brokers => ["localhost:9092"]
})).

Batch Processing

The exporter manager batches spans for efficient export.

Batch Configuration

Default settings:

  • Batch size: 512 spans
  • Batch timeout: 5000ms (5 seconds)

Spans are exported when either threshold is reached.

Manual Flush

Force immediate export of pending spans:

%% Flush all pending spans
ok = instrument_exporter:flush().

Shutdown

Gracefully shutdown all exporters (flushes pending spans first):

ok = instrument_exporter:shutdown().

Configuration

Multiple Exporters

You can register multiple exporters simultaneously:

%% Console for debugging
instrument_exporter:register(instrument_exporter_console:new(#{
    format => text
})),

%% OTLP for collector
instrument_exporter:register(instrument_exporter_otlp:new(#{
    endpoint => "http://localhost:4318"
})),

%% Custom exporter for specific needs
instrument_exporter:register(my_custom_exporter:new(#{...})).

Listing Exporters

%% Get list of registered exporter modules
Exporters = instrument_exporter:list().
%% [instrument_exporter_console, instrument_exporter_otlp, ...]

Unregistering Exporters

%% Unregister a specific exporter
ok = instrument_exporter:unregister(instrument_exporter_console).

Production Recommendations

  1. Use OTLP exporter for production with a collector (Jaeger, Zipkin, etc.)

  2. Enable compression for high-volume scenarios:

    instrument_exporter_otlp:new(#{
        endpoint => "...",
        compression => gzip
    })
  3. Set appropriate timeouts based on your network:

    instrument_exporter_otlp:new(#{
        endpoint => "...",
        timeout => 30000  %% 30 seconds for slow networks
    })
  4. Use console exporter only for debugging - it's not designed for production load

  5. Flush before shutdown in your application stop:

    stop(_State) ->
        instrument_exporter:flush(),
        ok.

Sending to External Collectors

The OTLP exporter can send telemetry to any OTLP-compatible backend. Here are common configurations.

OpenTelemetry Collector

The OpenTelemetry Collector is a vendor-agnostic proxy that receives, processes, and exports telemetry data.

Docker Setup:

# Run the collector
docker run -p 4317:4317 -p 4318:4318 \
  -v $(pwd)/otel-collector-config.yaml:/etc/otelcol/config.yaml \
  otel/opentelemetry-collector:latest

Minimal collector config (otel-collector-config.yaml):

receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [debug]
    metrics:
      receivers: [otlp]
      exporters: [debug]
    logs:
      receivers: [otlp]
      exporters: [debug]

Erlang configuration:

instrument_exporter:register(instrument_exporter_otlp:new(#{
    endpoint => "http://localhost:4318"
})),
instrument_metrics_exporter:register(instrument_metrics_exporter_otlp:new(#{
    endpoint => "http://localhost:4318"
})),
instrument_log_exporter:register(instrument_log_exporter_otlp:new(#{
    endpoint => "http://localhost:4318"
})).

Jaeger

Jaeger natively supports OTLP. Send traces directly to Jaeger's OTLP endpoint.

Docker Setup:

docker run -d --name jaeger \
  -p 4317:4317 \
  -p 4318:4318 \
  -p 16686:16686 \
  jaegertracing/all-in-one:latest

Erlang configuration:

instrument_exporter:register(instrument_exporter_otlp:new(#{
    endpoint => "http://localhost:4318"
})).

Access the Jaeger UI at http://localhost:16686.

Grafana Tempo

Tempo is Grafana's distributed tracing backend with native OTLP support.

Docker Compose Setup:

version: "3"
services:
  tempo:
    image: grafana/tempo:latest
    command: ["-config.file=/etc/tempo.yaml"]
    volumes:
      - ./tempo.yaml:/etc/tempo.yaml
    ports:
      - "4318:4318"   # OTLP HTTP
      - "3200:3200"   # Tempo API

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin

Tempo config (tempo.yaml):

server:
  http_listen_port: 3200

distributor:
  receivers:
    otlp:
      protocols:
        http:

storage:
  trace:
    backend: local
    local:
      path: /tmp/tempo/blocks

Erlang configuration:

instrument_exporter:register(instrument_exporter_otlp:new(#{
    endpoint => "http://localhost:4318"
})).

Cloud Backends

Most cloud observability platforms support OTLP. The pattern is similar: provide the endpoint URL and authentication headers.

Generic OTLP Pattern:

instrument_exporter:register(instrument_exporter_otlp:new(#{
    endpoint => "https://otlp.your-provider.com:4318",
    headers => #{
        <<"Authorization">> => <<"Bearer your-api-key">>,
        <<"X-Custom-Header">> => <<"value">>
    },
    compression => gzip
})).

Datadog (via OTLP):

instrument_exporter:register(instrument_exporter_otlp:new(#{
    endpoint => "https://trace.agent.datadoghq.com:4318",
    headers => #{
        <<"DD-API-KEY">> => <<"your-datadog-api-key">>
    }
})).

Honeycomb:

instrument_exporter:register(instrument_exporter_otlp:new(#{
    endpoint => "https://api.honeycomb.io:443",
    headers => #{
        <<"x-honeycomb-team">> => <<"your-api-key">>,
        <<"x-honeycomb-dataset">> => <<"your-dataset">>
    }
})).

New Relic:

instrument_exporter:register(instrument_exporter_otlp:new(#{
    endpoint => "https://otlp.nr-data.net:4318",
    headers => #{
        <<"api-key">> => <<"your-license-key">>
    }
})).

Environment Variable Configuration

Configure endpoints via environment variables for flexibility across environments:

export OTEL_EXPORTER_OTLP_ENDPOINT="http://otel-collector:4318"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer token"
%% Initialize from environment
instrument_config:init().

%% Get configured endpoint
Endpoint = instrument_config:get_otlp_endpoint().

Span Data Structure

Exporters receive spans with the following structure:

#span{
    name :: binary(),              %% Span name
    ctx :: #span_ctx{              %% Span context
        trace_id :: <<_:128>>,
        span_id :: <<_:64>>,
        trace_flags :: 0 | 1,
        trace_state :: [{binary(), binary()}],
        is_remote :: boolean()
    },
    parent_ctx :: #span_ctx{} | undefined,
    kind :: client | server | producer | consumer | internal,
    start_time :: integer(),       %% Monotonic nanoseconds
    end_time :: integer() | undefined,
    attributes :: map(),
    events :: [#span_event{}],
    links :: [#span_link{}],
    status :: unset | ok | {error, binary()},
    is_recording :: boolean()
}

For events:

#span_event{
    name :: binary(),
    timestamp :: integer(),
    attributes :: map()
}

For links:

#span_link{
    ctx :: #span_ctx{},
    attributes :: map()
}