OpenTelemetry Logs: Send Existing Logs Over OTLP
Last verified on Nov 8, 2026.
OpenTelemetry (OTel) treats logs as a first-class signal with a defined record format, and it is designed to work with the logs you already produce. There are two ways in. You can attach a bridge to the logging library you already use so that records leave the process over OTLP, or you can run a Collector that tails log files. This page covers the record format, both routes, and the settings that trip people up, with configurations run against Collector v0.165.0 and Python SDK 1.45.2.
The Log Record Data Model
A log record in the OTel data model has these fields:
Severity numbers fall in ranges: 1-4 TRACE, 5-8 DEBUG, 9-12 INFO, 13-16 WARN, 17-20 ERROR, 21-24 FATAL. Every source level maps into one of these, which is what lets a query like "errors and worse" work across a Python service and an nginx file. Resource is the part that stays fixed for a source, so service.name belongs there and not in each record's attributes. For how severity maps to your own code, see log levels.
The docs make one point worth knowing: a log encoded as JSON is not automatically structured. Structured logs imply a consistent schema, and a JSON blob with ad hoc keys is only semistructured. See structured logging and the JSON logging guide.
Route 1: Bridge Your Existing Logger
The Logs Bridge API is meant for authors of logging libraries and appenders, not for application code. In an application you keep calling your usual logger, and a handler (the docs call it an appender or bridge) forwards each record to the SDK. Language SDKs provide these. At the time of writing the OpenTelemetry docs list logs as stable for C++, .NET, Java, PHP, and Rust, as a release candidate for Go, and as in development for Python, JavaScript, Ruby, Swift, and Erlang/Elixir. Check the status table for your language before you depend on an API.
This Python example bridges the standard logging module:
import logging
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.logging.handler import LoggingHandler
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
provider = LoggerProvider(resource=Resource.create({"service.name": "checkout-api"}))
provider.add_log_record_processor(
BatchLogRecordProcessor(OTLPLogExporter(endpoint="http://localhost:4318/v1/logs"))
)
logging.getLogger().addHandler(LoggingHandler(level=logging.INFO, logger_provider=provider))
logging.getLogger().setLevel(logging.INFO)
logging.getLogger("checkout").error("payment failed", extra={"order_id": "o-123"})
provider.shutdown() # flush the last batchSending that record, inside an active span, to a Collector with a debug exporter printed:
InstrumentationScope checkout
LogRecord #0
SeverityText: ERROR
SeverityNumber: Error(17)
Body: Str(payment failed)
Attributes:
-> order_id: Str(o-123)
-> code.file.path: Str(/tmp/otel/app.py)
Trace ID: 1e2457d646b370647745fc4ebe734647
Span ID: 95e1bae9ba2b5274The extra field became an attribute, the logger name became the scope, and because the call happened inside a span the record carries its Trace ID and Span ID, which is how a log line is joined to a trace. Two things to remember. The root logger defaults to WARNING, so call setLevel or INFO records never reach the handler. And the _logs module path has an underscore in the version tested here, so expect it to change as the Python API matures. Call provider.shutdown() on exit, or the last batch may be lost. The LoggingHandler import comes from the opentelemetry-instrumentation-logging package, and the older copy in opentelemetry-sdk is deprecated.
You can configure the exporter with environment variables instead of code. OTEL_EXPORTER_OTLP_ENDPOINT sets the destination for all signals, OTEL_EXPORTER_OTLP_PROTOCOL takes grpc, http/protobuf, or http/json, and OTEL_EXPORTER_OTLP_LOGS_ENDPOINT overrides the destination for logs only.
Route 2: Read Log Files With the Collector
When you cannot change the application, a Collector reads the files. The filelog receiver reads any file and can parse formats. This configuration tails JSON lines, turns the level field into a severity and the time field into the timestamp, and lifts a trace ID into the record's trace context:
receivers:
filelog:
include: [/var/log/myapp/*.log]
start_at: beginning
operators:
- type: json_parser
timestamp:
parse_from: attributes.time
layout: '%Y-%m-%dT%H:%M:%SZ'
severity:
parse_from: attributes.level
- type: trace_parser
trace_id:
parse_from: attributes.trace_id
exporters:
debug:
verbosity: detailed
service:
pipelines:
logs:
receivers: [filelog]
exporters: [debug]Run against sample JSON lines, an error line came out with SeverityText: error, SeverityNumber: Error(17), the original line as Body, the parsed fields as attributes, and the Trace ID set. The settings that matter:
start_atdefaults toend. The receiver reads only lines written after it starts. Usebeginningto read what is already in the file, and remember it will then re-read on a fresh start unless you configure persistent storage.includeandexcludeare glob lists. A glob that does not match the real path is the most common reason for an empty pipeline.multilinetakes exactly one ofline_start_patternorline_end_pattern, so stack traces stay in one record.- Rotation is handled for both move/create and copy/truncate. The receiver tracks files by inode and fingerprint. See logrotate.
json_parserwrites parsed keys toattributesby default, and the original line stays inBody.
A Collector v0.165.0 run also printed a warning that the filelog name is an alias and that file_log is preferred. The filelog name still worked in that run and is what the OpenTelemetry and Sazabi docs use, so check your version's release notes if you upgrade.
Export Over OTLP
Replace debug with otlphttp to send logs onward:
exporters:
otlphttp/backend:
endpoint: https://otlp.example.com
service:
pipelines:
logs:
receivers: [filelog, otlp]
processors: [batch]
exporters: [otlphttp/backend]An otlp receiver in the same pipeline accepts records from SDKs, so one Collector can carry both routes. Add a batch processor before exporting. Details of the Collector side are in the OpenTelemetry Collector integration. If volume is a concern, log sampling shows how to drop records in the Collector without losing errors.
Sending OpenTelemetry Logs to Sazabi
Sazabi, an AI-native observability and security platform, accepts OTLP over HTTP. For SDKs, its OpenTelemetry source says to set two environment variables and redeploy:
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=<your intake URL>The intake URL comes from the log source's setup screen and carries your project key, so no extra headers are needed. Its Python example attaches a LoggingHandler to a LoggerProvider, as above. For a Collector, the Collector source adds an otlphttp/sazabi exporter with the intake URL as its endpoint and lists it in the logs pipeline next to your existing exporters. Logs and traces arrive as separate streams, and metrics sent to this source are not stored. The Sazabi docs also note that SDK batch processors drop records when their queue fills, which defaults to 2048 per process through OTEL_BLRP_MAX_QUEUE_SIZE. See the OpenTelemetry integration page, then microservices logging for propagating trace context between services.
Frequently Asked Questions
What is an OpenTelemetry log record?
A log record is a timestamped event with a body, a severity, attributes, a resource describing the source, and optional trace and span IDs. The data model is stable and lets logs from files, SDKs, and system sources share one representation.
Do I have to change my logging code to use OpenTelemetry logs?
No. You keep your logging library and add an appender or bridge that hands records to the OpenTelemetry SDK. Alternatively, leave the app alone and have a Collector read its log files with the filelog receiver.
Why does the filelog receiver show no logs?
start_at defaults to end, so the receiver reads only lines written after it starts. Set start_at: beginning to read existing content, and check that the include glob matches the file path.
How do logs get trace IDs?
When a log is emitted inside an active span through an OpenTelemetry bridge, the SDK copies the span context into the record as TraceId and SpanId. For file logs, parse an ID from the line with the trace_parser operator.
Sources
- OpenTelemetry logs work with existing logging libraries through appenders or bridges, the Collector filelog receiver reads any file, and logs are correlated with active traces and spans (accessed Nov 8, 2026)
- Log record fields (Timestamp, ObservedTimestamp, TraceId, SpanId, SeverityText, SeverityNumber, Body, Resource, InstrumentationScope, Attributes) and the SeverityNumber ranges (accessed Nov 8, 2026)
- The Logs Bridge API is for authors of logging libraries and appenders, not for application code (accessed Nov 8, 2026)
- filelog receiver options include, exclude, start_at (default end), operators, multiline, and rotation handling (accessed Nov 8, 2026)
- OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL values grpc, http/protobuf, and http/json, and signal-specific OTEL_EXPORTER_OTLP_LOGS_ variables (accessed Nov 8, 2026)
- Setting OTEL_EXPORTER_OTLP_ENDPOINT to the Sazabi intake URL with http/protobuf sends SDK logs and traces to Sazabi, and Python LoggingHandler setup (accessed Nov 8, 2026)
- An otlphttp exporter in an OpenTelemetry Collector forwards logs to Sazabi (accessed Nov 8, 2026)