Dash0 Acquires Lumigo to Expand Agentic Observability Across AWS and Serverless

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.

OpenTelemetry LogRecord data model

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 RangeCategory
1–4TRACE
5–8DEBUG
9–12INFO
13–16WARN
17–20ERROR
21–24FATAL

In the OpenTelemetry Protocol (OTLP) JSON format, a log record is represented as follows:

json
12345678910111213141516171819202122232425262728293031323334353637
{
"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

How the OpenTelemetry Logs API and SDK interact

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 manages Logger instances. Typically, one is configured per process and registered globally for consistent access.

  • Logger: Responsible for emitting logs as LogRecords. 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 LoggerProvider implementation.
  • A LogRecordProcessor that sits between log creation and export and is responsible for enriching and batching LogRecords.
  • A LogRecordExporter that 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:

JavaScript
12345
import pino from "pino";
const logger = pino();
logger.info("hi");

By default, Pino produces JSON logs like this:

json
1234567
{
"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:

JavaScript
123456789101112131415
import { 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):

JavaScript
12345678910
import { 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:

text
12345678910111213141516171819202122232425262728293031
2025-09-22T05:31:27.964Z info ResourceLog #0
Resource 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 #0
ScopeLogs SchemaURL:
InstrumentationScope @opentelemetry/instrumentation-pino 0.49.0
LogRecord #0
ObservedTimestamp: 2025-09-22 05:31:27.924 +0000 UTC
Timestamp: 2025-09-22 05:31:27.924 +0000 UTC
SeverityText: info
SeverityNumber: 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

Correlating logs and traces in OpenTelemetry

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:

JavaScript
1234567
import { 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:

text
123456789
LogRecord #1
ObservedTimestamp: 2025-09-22 05:51:37.685 +0000 UTC
Timestamp: 2025-09-22 05:51:37.685 +0000 UTC
SeverityText: info
SeverityNumber: Info(9)
Body: Str(in a span)
Trace ID: 6691c3b82c157705904ba3b5b921d60a
Span ID: 72efdc9ec81b179a
Flags: 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.

OpenTelemetry-native Log-trace correlation in Dash0

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:

json
12345678
{
"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:

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:

text
1
Aug 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:

text
12345678910
LogRecord #2
ObservedTimestamp: 2025-09-21 17:25:01.598645527 +0000 UTC
Timestamp: 1970-01-01 00:00:00 +0000 UTC
SeverityText:
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 SeverityText and SeverityNumber.
  • 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:

yaml
12345678
# otelcol.yaml
receivers:
filelog:
include: [/var/log/auth.log]
operators:
- type: syslog_parser
protocol: rfc3164
allow_skip_pri_header: true

Produces an updated OpenTelemetry LogRecord:

text
123456789101112131415
LogRecord #2
ObservedTimestamp: 2025-09-21 18:40:22.780865051 +0000 UTC
Timestamp: 2025-08-20 18:23:23 +0000 UTC
SeverityText:
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:

yaml
12345
# otelcol.yaml
- type: regex_parser
parse_from: attributes.message
regex:
'Received disconnect from (?P<client_ip>[\d.]+) port (?P<client_port>\d+)'

Resulting in new attributes:

text
123
Attributes:
-> 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:

yaml
123456789101112131415161718192021222324252627282930
# otelcol.yaml
processors:
transform/auth_logs:
error_mode: ignore
log_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.

text
123456789101112131415161718192021222324
2025-09-22T03:14:29.229Z info ResourceLog #0
Resource SchemaURL:
Resource attributes:
-> host.name: Str(ubuntu-lts)
-> process.executable.name: Str(sshd)
-> process.pid: Int(47339)
ScopeLogs #0
ScopeLogs SchemaURL:
InstrumentationScope
. . .
LogRecord #2
ObservedTimestamp: 2025-09-22 03:14:29.130188792 +0000 UTC
Timestamp: 2025-08-20 18:23:23 +0000 UTC
SeverityText: INFO
SeverityNumber: 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.

Authors
Ayooluwa Isaiah
Ayooluwa Isaiah