Last updated: January 27, 2026
OpenTelemetry Logging Concepts and Data Model
Logs have always been essential for understanding how software behaves, but the traditional model quickly breaks down in modern distributed environments. Since services are split across languages and runtimes, logs are emitted in different formats, and correlating events across a single request is nigh impossible.
OpenTelemetry logging was developed to address this problem. Rather than introducing another logging framework, OpenTelemetry defines a standard, vendor-neutral way to represent log events, enrich them with execution context, and correlate them with traces and metrics. Existing logging libraries remain in place, but their output is normalized into a consistent model that works across services and ecosystems.
Once logs follow the OpenTelemetry model, they're transformed from isolated text lines to structured events that can be processed, queried, and correlated as part of a unified observability strategy.
This article provides an overview of OpenTelemetry logging, and cover its core concepts, data model, and how logs integrate with traces and metrics in a unified observability system.
What are OpenTelemetry logs?
OpenTelemetry logs are structured log records that follow a standardized, vendor-neutral data model and can be correlated with traces and metrics.
At a high level, OpenTelemetry treats logs as first-class telemetry signals, alongside traces and metrics. A log is no longer just a string written to files; it's a well-defined event with:
- A precise timestamp.
- A severity level with consistent semantics.
- Structured attributes for context.
- Metadata describing where the log came from.
- Optional trace and span identifiers that link the log to a request’s execution path.
This design allows logs from diverse sources to be processed uniformly, even when they originate from different logging libraries or runtimes.
OpenTelemetry logs vs the traditional logging model
Traditional logging approaches focus on local output: writing text to files or stdout, often in ad-hoc formats. This works well for single applications, but breaks down in distributed environments where each service produces logs with diverse formats and conventions.
OpenTelemetry logging addresses this by separating log production from log representation. Applications can continue using their existing logging libraries, but the resulting logs are then normalized into a common model with consistent semantics.
The most important innovation is automatic correlation with traces. When logs are emitted within an active trace, OpenTelemetry automatically attaches trace and span identifiers to each log record, making it possible to:
- Jump from a log entry directly to the trace that produced it.
- See associated logs in the exact execution context of a request.
- Correlate errors, latency spikes, and anomalous behavior across signals.
To support these capabilities, OpenTelemetry defines a logs data model that standardizes how log records are represented internally.
The rest of this guide explores that model in detail, explaining how its fields work together, why they exist, and how OpenTelemetry uses them to turn logs into reliable, high-signal observability signals.
Understanding the OpenTelemetry logging specification
The OpenTelemetry logs data model is designed to accommodate logs from a wide range of sources while preserving their original meaning. Existing log formats can be mapped into the model without ambiguity and, in most cases, reconstructed without loss of information.
An OpenTelemetry log record consists of a small set of core fields with well-defined semantics, complemented by flexible attributes that provide event-specific context. The most important fields are:
-
Timestamp: The time when the event originally occurred at the source. -
Body: The primary log content, usually a human-readable message, though it may also contain structured data. -
Attributes: Arbitrary key-value pairs that capture machine-readable context specific to the event. -
Resource: Describes the entity that produced the log, such as the application, host, or Kubernetes pod.
To enable trace–log correlation, the model also incorporates fields from the W3C Trace Context specification:
TraceId: The unique identifier for a distributed trace.SpanId: The identifier for a specific span (operation) within that trace.TraceFlags: Flags providing metadata about the trace, such as whether it was sampled.
When present, these fields allow an individual log record to be directly linked to the trace that produced it, enabling fast navigation between logs and their execution context in distributed systems.
The data model also standardizes how log severity is expressed by decoupling severity semantics from language-specific logging conventions:
-
SeverityText: The original severity label emitted by the source. -
SeverityNumber: A numeric value that enables consistent comparison and filtering across systems. Smaller numbers represent less severe events, larger numbers more severe ones.
| SeverityNumber Range | Category |
|---|---|
| 1–4 | TRACE |
| 5–8 | DEBUG |
| 9–12 | INFO |
| 13–16 | WARN |
| 17–20 | ERROR |
| 21–24 | FATAL |
In the OpenTelemetry Protocol (OTLP) JSON format, a log record is represented as follows:
json12345678910111213141516171819202122232425262728293031323334353637{"resourceLogs": [{"resource": {"attributes": [{"key": "service.name","value": { "stringValue": "checkoutservice" }}]},"scopeLogs": [{"scope": { "name": "logback", "version": "1.4.0" },"logRecords": [{"timeUnixNano": "1756571696706248000","observedTimeUnixNano": "1756571696710000000","severityNumber": 17,"severityText": "ERROR","body": { "stringValue": "Database connection failed" },"attributes": [{ "key": "thread.id", "value": { "intValue": 42 } },{"key": "exception.type","value": { "stringValue": "SQLException" }}],"traceId": "da5b97cecb0fe7457507a876944b3cf","spanId": "fa7f0ea9cb73614c"}]}]}]}
At the top level, resourceLogs groups records emitted by the same resource
which in this case is a single microservice identified by service.name. Within
each resource, scopeLogs group records by instrumentation scope, indicating
which library or module produced them. The logRecords array then contains the
individual events, each enriched with timestamps, severity, context, and
optional trace identifiers.
By enforcing a consistent representation, OpenTelemetry makes logs from heterogeneous sources interoperable and directly correlatable.
However, a data model alone does not produce structured logs. To generate records that conform to it in practice, applications must be instrumented, which is where the OpenTelemetry SDKs and log bridges come into play.
Integrating application logs with OpenTelemetry
Unlike traces and metrics, which rely on OpenTelemetry-specific APIs for instrumentation, logging follows a different model. Given the long history and diversity of logging frameworks, OpenTelemetry is designed to integrate with existing libraries rather than replace them.
Application logs enter the OpenTelemetry ecosystem through log bridges; adapters that forward records from familiar libraries such as Python’s logging, Java’s SLF4J or Logback, and .NET’s Serilog.
This design means you can keep your existing logging code and tooling, while benefiting from the OpenTelemetry log data model, trace correlation, and consistent export to observability backends.
Understanding the OpenTelemetry logging components
The Logs API defines the contract for passing log records into the OpenTelemetry pipeline. It's primarily intended for library authors to build appenders or handlers, but it can also be called directly from instrumentation libraries or application code.
It consists of the following core components:
-
LoggerProvider: Creates and managesLoggerinstances. Typically, one is configured per process and registered globally for consistent access. -
Logger: Responsible for emitting logs asLogRecords. In practice, your existing logging library (via a bridge) will call it for you. -
LogRecord: The data structure representing a single log event, with all the fields defined in the logs data model described earlier.
While the Logs API defines how logs are created, the Logs SDK is responsible for processing and exporting them. It provides:
- A concrete
LoggerProviderimplementation. - A
LogRecordProcessorthat sits between log creation and export and is responsible for enriching and batchingLogRecords. - A
LogRecordExporterthat takes processed records and exports them to set destinations (often an OTLP endpoint).
OpenTelemetry logging example using log bridges
The OpenTelemetry Logs SDK does not automatically capture application logs. It provides the processing and export pipeline, but log records must be explicitly fed into it through a log bridge.
A log bridge (or appender) connects an existing logging framework to the OpenTelemetry Logs API. Rather than rewriting applications to emit logs through OpenTelemetry directly, you only need to attach a bridge to the logger you already use.
For example, consider a Node.js application using Pino:
JavaScript12345import pino from "pino";const logger = pino();logger.info("hi");
By default, Pino produces JSON logs like this:
json1234567{"level": 30,"time": 1758515262941,"pid": 55904,"hostname": "falcon","msg": "hi"}
To bring these logs into an OpenTelemetry pipeline, you must configure the
OpenTelemetry SDK,
register a LogRecordProcessor and LogRecordExporter, and include the Pino
log bridge via the
@opentelemetry/instrumentation-pino
package:
JavaScript123456789101112131415import { PinoInstrumentation } from "@opentelemetry/instrumentation-pino";import { logs, NodeSDK } from "@opentelemetry/sdk-node";import pino from "pino";const sdk = new NodeSDK({logRecordProcessor: new logs.SimpleLogRecordProcessor(new logs.ConsoleLogRecordExporter(),),instrumentations: [new PinoInstrumentation()],});sdk.start();const logger = pino();logger.info("hi");
The SimpleLogRecordProcessor immediately exports each log, which is useful for
development and debugging. In production, it is typically replaced with a
BatchLogRecordProcessor to reduce overhead, and the ConsoleLogRecordExporter
is swapped for an OTLPLogExporter that streams logs to an OTLP endpoint
(typically the
OpenTelemetry Collector):
JavaScript12345678910import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";import { PinoInstrumentation } from "@opentelemetry/instrumentation-pino";import { logs, NodeSDK } from "@opentelemetry/sdk-node";import pino from "pino";const sdk = new NodeSDK({logRecordProcessor: new logs.BatchLogRecordProcessor(new OTLPLogExporter()),instrumentations: [new PinoInstrumentation()],});sdk.start();
When viewed through the Collector using the debug exporter, the resulting log record appears as follows:
text123456789101112131415161718192021222324252627282930312025-09-22T05:31:27.964Z info ResourceLog #0Resource SchemaURL:Resource attributes:-> host.name: Str(falcon)-> host.arch: Str(amd64)-> host.id: Str(4a3dc42bf0564d50807d1553f485552a)-> process.pid: Int(59532)-> process.executable.name: Str(node)-> process.executable.path: Str(/home/ayo/.local/share/mise/installs/node/24.8.0/bin/node)-> process.command_args: Slice(["/home/ayo/.local/share/mise/installs/node/24.8.0/bin/node","--experimental-loader=@opentelemetry/instrumentation/hook.mjs","/home/ayo/dev/dash0/repro-contrib-2838/index.js"])-> process.runtime.version: Str(24.8.0)-> process.runtime.name: Str(nodejs)-> process.runtime.description: Str(Node.js)-> process.command: Str(/home/ayo/dev/dash0/repro-contrib-2838/index.js)-> process.owner: Str(ayo)-> service.name: Str(unknown_service:node)-> telemetry.sdk.language: Str(nodejs)-> telemetry.sdk.name: Str(opentelemetry)-> telemetry.sdk.version: Str(2.0.1)ScopeLogs #0ScopeLogs SchemaURL:InstrumentationScope @opentelemetry/instrumentation-pino 0.49.0LogRecord #0ObservedTimestamp: 2025-09-22 05:31:27.924 +0000 UTCTimestamp: 2025-09-22 05:31:27.924 +0000 UTCSeverityText: infoSeverityNumber: Info(9)Body: Str(hi)Trace ID:Span ID:Flags: 0
This output illustrates how the bridge and Logs SDK work together. The original
Pino log is translated into an OpenTelemetry LogRecord, enriched with resource
metadata, mapped to standardized severity fields, and annotated with
instrumentation scope information. Trace correlation fields are present, though
unset in this example because no active span was in scope.
While this is the standard integration pattern, the availability and maturity of log bridges varies by language and framework. Always consult the OpenTelemetry documentation for your language to confirm what is supported and how it should be configured.
Correlating OTel logs and traces
OpenTelemetry offers a capability that structured logging alone cannot: when you use the OTel SDK for both tracing and logging, it automatically correlates the two.
For this to occur, logs must be emitted within an active span. When they are, the SDK attaches the current trace and span identifiers to each log record automatically.
In practice, spans are usually created via zero-code instrumentation around common operations such as HTTP requests or database calls. Spans can also be created manually when needed:
JavaScript1234567import { api, logs, NodeSDK } from "@opentelemetry/sdk-node";const tracer = api.trace.getTracer("example");tracer.startActiveSpan("manual-span", (span) => {logger.info("in a span");});
The resulting log record now includes the active trace context:
text123456789LogRecord #1ObservedTimestamp: 2025-09-22 05:51:37.685 +0000 UTCTimestamp: 2025-09-22 05:51:37.685 +0000 UTCSeverityText: infoSeverityNumber: Info(9)Body: Str(in a span)Trace ID: 6691c3b82c157705904ba3b5b921d60aSpan ID: 72efdc9ec81b179aFlags: 1
This creates a bidirectional debugging workflow: from a trace, you can navigate directly to the logs emitted within its spans; from a log entry, you can pivot back to the full distributed trace that produced it.
This is the core value proposition of using the full OpenTelemetry ecosystem with an OpenTelemetry-native observability tool. It elevates logs from isolated event records to fully contextualized signals embedded within a system-wide execution narrative.
When a log bridge isn't available
If your logging library currently lacks a log bridge, you can still enrich your logs with trace context and let the Collector do the mapping.
Most logging libraries allow additional fields to be injected into log output.
By including trace_id, span_id, and trace_flags with each log record,
trace–log correlation can be preserved:
json12345678{"level": "ERROR","timestamp": "2025-10-05T15:34:11.428Z","message": "Payment authorization failed","trace_id": "c8f4a2171adf3de0a2c0b2e8f649a21f","span_id": "d6e2b6c1a2f53e4b","user_id": "user-1234"}
Once ingested by the Collector, these fields can be parsed and mapped to canonical OpenTelemetry attributes, preserving trace correlation across logs and traces without changing existing logging calls.
How the OpenTelemetry Collector ingests and transforms logs
So far, we’ve focused on applications emitting OpenTelemetry-native logs directly via SDKs and log bridges. In practice, however, not every component in a system is instrumented or even under your control. Legacy applications, third-party software, and infrastructure components typically emit logs in their own formats, with no awareness of OpenTelemetry.
The OpenTelemetry Collector addresses this gap. It can ingest logs from a wide range of sources, parse them, and map them into the OpenTelemetry logs data model before export. This allows systems that know nothing about OpenTelemetry to still participate in the same observability pipeline as instrumented applications.
Log ingestion via receivers
The Collector ingests logs through receivers, each designed to handle a specific input source or protocol. Common examples include:
-
filelogreceiver for tailing local log files.
-
awscloudwatchreceiver for AWS CloudWatch log groups and streams.
-
syslogreceiver for network-delivered syslog messages.
-
fluentforwardreceiver for integration with Fluentd or Vector.
Once logs are ingested by a receiver, they're represented as OpenTelemetry log records. However, this does not mean they are immediately useful.
From raw log lines to structured records
Consider the following Linux authentication log entry:
text1Aug 20 18:23:23 ubuntu-lts sshd[47339]: Received disconnect from 180.101.88.228 port 11349:11: [preauth]
When ingested by the filelogreceiver and viewed with the debug exporter, it
appears as:
text12345678910LogRecord #2ObservedTimestamp: 2025-09-21 17:25:01.598645527 +0000 UTCTimestamp: 1970-01-01 00:00:00 +0000 UTCSeverityText:SeverityNumber: Unspecified(0)Body: Str(Aug 20 18:23:23 ubuntu-lts sshd[47339]: Received disconnect from 180.101.88.228 port 11349:11: [preauth])Attributes:-> log.file.name: Str(auth.log)Trace ID:Span ID:
At this stage, the log is little more than an unstructured string stored in the
Body, accompanied by minimal metadata. Important fields such as Timestamp,
SeverityNumber, and contextual attributes remain unset. This is expected as
receivers prioritize ingestion, not interpretation.
To make logs usable, they must be parsed, normalized, and enriched.
Parsing and enrichment with operators and processors
Transforming raw logs into high-signal OpenTelemetry records typically involves:
- Extracting timestamps and mapping them to
Timestamp. - Identifying severity levels (or heuristics) and mapping them to
SeverityTextandSeverityNumber. - Extracting contextual fields into structured
Attributes - Enriching records with resource metadata (host, process, Kubernetes, cloud)
- Populating trace context fields if available
This work is performed using:
- Operators, which act on individual log entries during ingestion
- Processors, which operate on batches of telemetry regardless of source
For example, applying the syslog_parser operator within the filelog
receiver:
yaml12345678# otelcol.yamlreceivers:filelog:include: [/var/log/auth.log]operators:- type: syslog_parserprotocol: rfc3164allow_skip_pri_header: true
Produces an updated OpenTelemetry LogRecord:
text123456789101112131415LogRecord #2ObservedTimestamp: 2025-09-21 18:40:22.780865051 +0000 UTCTimestamp: 2025-08-20 18:23:23 +0000 UTCSeverityText:SeverityNumber: Unspecified(0)Body: Str(Aug 20 18:23:23 ubuntu-lts sshd[47339]: Received disconnect from 180.101.88.228 port 11349:11: [preauth])Attributes:-> log.file.name: Str(auth.log)-> message: Str(Received disconnect from 180.101.88.228 port 11349:11: [preauth])-> hostname: Str(ubuntu-lts)-> appname: Str(sshd)-> proc_id: Str(47339)Trace ID:Span ID:Flags: 0
The timestamp is now correctly parsed, and key fields from the syslog prefix
have been extracted into the Attributes map.
Additional operators can be chained to extract domain-specific details. For
example, a regex_parser can extract the client IP and port:
yaml12345# otelcol.yaml- type: regex_parserparse_from: attributes.messageregex:'Received disconnect from (?P<client_ip>[\d.]+) port (?P<client_port>\d+)'
Resulting in new attributes:
text123Attributes:-> client_ip: Str(180.101.88.228)-> client_port: Str(11349)
Advanced transformations with OTTL
For more complex transformations, the
OpenTelemetry Transformation Language (OTTL)
is used via the transform processor. OTTL enables conditional logic, field
restructuring, semantic normalization, and severity mapping:
yaml123456789101112131415161718192021222324252627282930# otelcol.yamlprocessors:transform/auth_logs:error_mode: ignorelog_statements:# Move host and process attributes to the Resource- set(resource.attributes["host.name"], log.attributes["hostname"])- set(resource.attributes["process.executable.name"],log.attributes["appname"])- set(resource.attributes["process.pid"], Int(log.attributes["proc_id"]))# Conform attributes to semantic conventions- set(log.attributes["client.address"], log.attributes["client_ip"])- set(log.attributes["client.port"], Int(log.attributes["client_port"]))- set(log.attributes["log.record.original"], log.body)- set(log.body, log.attributes["message"])# Severity mapping- set(log.severity_number, SEVERITY_NUMBER_INFO) where IsMatch(log.body,"^Received disconnect")- set(log.severity_text, "INFO") where log.severity_number >=SEVERITY_NUMBER_INFO and log.severity_number <= SEVERITY_NUMBER_INFO4# Delete the old, non-compliant attributes- delete_key(log.attributes, "hostname")- delete_key(log.attributes, "appname")- delete_key(log.attributes, "proc_id")- delete_key(log.attributes, "client_ip")- delete_key(log.attributes, "client_port")- delete_key(log.attributes, "message")
After applying such transformations, the final log record becomes a fully populated OpenTelemetry log with consistent structure, normalized severity, and rich context.
text1234567891011121314151617181920212223242025-09-22T03:14:29.229Z info ResourceLog #0Resource SchemaURL:Resource attributes:-> host.name: Str(ubuntu-lts)-> process.executable.name: Str(sshd)-> process.pid: Int(47339)ScopeLogs #0ScopeLogs SchemaURL:InstrumentationScope. . .LogRecord #2ObservedTimestamp: 2025-09-22 03:14:29.130188792 +0000 UTCTimestamp: 2025-08-20 18:23:23 +0000 UTCSeverityText: INFOSeverityNumber: Info(9)Body: Str(Received disconnect from 180.101.88.228 port 11349:11: [preauth])Attributes:-> client.port: Int(11349)-> client.address: Str(180.101.88.228)-> log.file.name: Str(auth.log)-> log.record.original: Str(Aug 20 18:23:23 ubuntu-lts sshd[47339]: Received disconnect from 180.101.88.228 port 11349:11: [preauth])Trace ID:Span ID:Flags: 0
By composing receivers, operators, and processors into a pipeline, the Collector can systematically convert raw, heterogeneous log streams into structured OpenTelemetry records that are consistent, correlatable, and ready for analysis regardless of their original source or format.
Best practices for OpenTelemetry logging
Rolling out OpenTelemetry logging in production requires more than just turning on a bridge or deploying a Collector. To produce logs that are reliable, cost-effective, and operationally useful, follow these practices.
1. Start with structure
When logs are unstructured, like the raw sshd example, you end up stacking
operators and transform rules just to pull out basics like timestamp or client
IP.
That effort largely disappears if your application emits structured logs from the outset. With structure in place, log bridges and the Collector can map fields directly into the OpenTelemetry data model without inference or cleanup.
The rule of thumb is simple: reserve parsers and regex operators for legacy systems you cannot change, and ensure new services emit structured logs in JSON by default.
2. Embrace high-cardinality attributes
High-cardinality attributes in logs and traces are essential for cross-signal correlation and effective root-cause analysis. They enable the most important debugging distinction: is this affecting everyone, or only a specific subset?
Without high cardinality, you can see that you have a spike in errors. With it,
you can see that the error spike is coming from user:8675309 on the canary
deployment in eu-west-1 who has the new-checkout-flow feature flag enabled.
Of course, this means you have to avoid observability backends that penalize high-cardinality data through cost models that discourage attaching meaningful context.
3. Use semantic conventions
Whenever possible, rely on OpenTelemetry’s semantic conventions for contextual fields. Doing so ensures logs remain interpretable across OTel-compliant backends and align cleanly with traces and metrics.
It also eliminates the need for post-ingestion cleanup, where attributes must be renamed or normalized after parsing.
4. Always include resource attributes
Resource attributes anchor logs to the services and environments that produced them. Set these attributes in the SDK Resource where possible, or enrich them in the Collector using processors such as resourcedetection or k8sattributes.
Without consistent resource metadata, even well-structured logs lose much of their diagnostic value.
5. Scrub sensitive data
Logs frequently capture sensitive information (often unintentionally) such as tokens, credentials, emails, or other PII. Ensure sensitive values are redacted before logs leave your environment.
Use Collector processors
such as transform or redaction, and configure logging libraries to avoid
emitting secrets in the first place. This keeps logs safe to share across teams
and compliant with security and privacy requirements.
6. Control your log volume
Production systems do not need the same volume of DEBUG logs as development
environments. Set appropriate log levels in application frameworks, and use the
Collector to
filter or sample
logs where necessary.
This keeps ingestion costs predictable and prevents backend overload, while preserving high-value logs associated with errors, traces, and critical execution paths.
Final thoughts and next steps
At this point, you understand how OpenTelemetry logging works conceptually: the data model, log bridges, trace correlation, and the role of the Collector. The next step depends on where you are in your adoption journey.
If you’re instrumenting applications directly, start with language-specific logging guides that show how to wire existing logging frameworks into OpenTelemetry using log bridges and SDKs.
For legacy systems or third-party logs, focus instead on configuring the OpenTelemetry Collector to ingest raw logs, then incrementally add parsing, normalization, and enrichment until logs conform cleanly to the OpenTelemetry data model.
Once logs are flowing, validate correlation by navigating between logs and traces in your backend. This is where gaps surface quickly: missing resource attributes, inconsistent severity mapping, or vendor limitations due to legacy architecture.
An OpenTelemetry-native backend treats the OpenTelemetry Protocol as its primary ingestion interface, not a compatibility layer, allowing telemetry to flow end-to-end exactly as defined without losing structure, context, or correlations.
Dash0 is one such backend. Built from the ground up to be OpenTelemetry-native, it accepts OTLP out of the box and preserves the full fidelity of the data model, making it possible to explore logs with complete context, correlate them seamlessly with traces and metrics, and work with high-cardinality data without a cost penalty.
From here, OpenTelemetry logging stops being a concept and becomes an operational capability you can depend on. To see this in practice, start a free trial with Dash0 today.




