OpenTelemetry JSON: OTLP JSON Traces, Metrics & Logs Format
Last updated:
OpenTelemetry exports traces, metrics, and logs as OTLP (OpenTelemetry Protocol) JSON over HTTP/1.1 — a POST to /v1/traces with a ResourceSpans JSON array containing every span collected since the last export flush. A single OTLP JSON trace export contains up to 512 spans by default; each span JSON object has 12 required fields including traceId (32-hex), spanId (16-hex), startTimeUnixNano, endTimeUnixNano, and an attributes array of key-value pairs. This guide covers the OTLP JSON payload structure for traces, metrics, and logs, semantic convention attributes, OpenTelemetry Collector YAML configuration for JSON export, Node.js SDK instrumentation, and querying OTLP JSON in Jaeger and Tempo.
OTLP JSON Payload Structure: ResourceSpans, ScopeSpans, Spans
The OTLP JSON trace payload is a three-level hierarchy: resourceSpans (one entry per service instance) › scopeSpans (one entry per instrumentation library) › spans (the individual span objects). A POST to /v1/traces carries a JSON body with a single top-level resourceSpans array. Each resource describes the entity producing telemetry via a resource.attributes array containing key-value pairs such as service.name, service.version, and deployment.environment. The scopeSpans array groups spans by the instrumentation scope (the library that created them), identified by scope.name and scope.version. Each span carries a full set of timing, identity, and metadata fields.
// POST /v1/traces Content-Type: application/json
{
"resourceSpans": [
{
"resource": {
"attributes": [
{ "key": "service.name", "value": { "stringValue": "checkout-api" } },
{ "key": "service.version", "value": { "stringValue": "2.4.1" } },
{ "key": "deployment.environment", "value": { "stringValue": "production" } },
{ "key": "host.name", "value": { "stringValue": "ip-10-0-1-42" } }
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "@opentelemetry/instrumentation-http",
"version": "0.51.0"
},
"spans": [
{
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"parentSpanId": "",
"traceState": "",
"name": "POST /checkout",
"kind": 2,
"startTimeUnixNano": "1716124800000000000",
"endTimeUnixNano": "1716124800123456789",
"attributes": [
{ "key": "http.request.method", "value": { "stringValue": "POST" } },
{ "key": "url.path", "value": { "stringValue": "/checkout" } },
{ "key": "http.response.status_code", "value": { "intValue": "200" } },
{ "key": "server.address", "value": { "stringValue": "api.example.com" } }
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"links": [],
"droppedLinksCount": 0,
"status": { "code": 1, "message": "" }
}
]
}
],
"schemaUrl": "https://opentelemetry.io/schemas/1.24.0"
}
]
}Span kind is an integer: 0 = unspecified, 1 = internal, 2 = server, 3 = client, 4 = producer, 5 = consumer. Nanosecond timestamps are encoded as decimal strings (e.g., "1716124800000000000") rather than JSON numbers because 64-bit nanosecond values exceed JavaScript's safe integer range. Status code 0 = unset, 1 = ok, 2 = error. A root span has an empty parentSpanId; child spans reference their parent's 16-hex spanId. The schemaUrl field on both resourceSpans and scopeSpans identifies the semantic convention version used for attribute naming.
Span Attributes and Semantic Conventions
OpenTelemetry semantic conventions define standardized attribute names so that spans from any SDK or instrumentation library carry consistent, cross-vendor meaning. Each attribute is an AnyValue JSON object with exactly one typed value key. Following semantic conventions enables automatic service maps, dashboards, and alerting rules in Jaeger, Grafana, and other backends without custom field mapping.
// AnyValue JSON encoding — exactly one typed value key per attribute
{ "key": "http.request.method", "value": { "stringValue": "GET" } }
{ "key": "http.response.status_code","value": { "intValue": "404" } }
{ "key": "http.request.body.size", "value": { "intValue": "1024" } }
{ "key": "db.query.text", "value": { "stringValue": "SELECT * FROM users WHERE id = ?" } }
{ "key": "error", "value": { "boolValue": true } }
{ "key": "net.sock.peer.addr", "value": { "stringValue": "192.168.1.1" } }
// AnyValue types: stringValue | intValue | boolValue | doubleValue | arrayValue | kvlistValue
// arrayValue example — list of tags
{
"key": "user.tags",
"value": {
"arrayValue": {
"values": [
{ "stringValue": "admin" },
{ "stringValue": "beta-tester" }
]
}
}
}
// ── HTTP semantic conventions (stable) ───────────────────────────
// http.request.method (GET, POST, PUT, DELETE, PATCH…)
// url.full (https://api.example.com/checkout)
// url.path (/checkout)
// http.response.status_code (200, 404, 500…)
// server.address (api.example.com)
// server.port (443)
// network.protocol.name (http)
// network.protocol.version (1.1, 2)
// ── Database semantic conventions ────────────────────────────────
// db.system (postgresql, mysql, redis, mongodb, elasticsearch)
// db.name (my_database)
// db.operation.name (SELECT, INSERT, UPDATE, DELETE)
// db.query.text (SELECT id, name FROM users WHERE active = $1)
// db.server.address (db.internal)
// db.server.port (5432)
// ── RPC semantic conventions ─────────────────────────────────────
// rpc.system (grpc, json_rpc, apache_dubbo)
// rpc.service (checkout.CheckoutService)
// rpc.method (PlaceOrder)
// rpc.grpc.status_code (0=OK, 2=UNKNOWN, 14=UNAVAILABLE)
// ── Exception event (recorded on span, not as attribute) ─────────
{
"timeUnixNano": "1716124800050000000",
"name": "exception",
"attributes": [
{ "key": "exception.type", "value": { "stringValue": "Error" } },
{ "key": "exception.message", "value": { "stringValue": "Connection refused" } },
{ "key": "exception.stacktrace", "value": { "stringValue": "Error: Connection refused\n at connect..." } }
]
}Custom attributes are valid alongside semantic convention attributes — prefix them with your organization's namespace to avoid collisions (e.g., myapp.user.id, myapp.tenant.id). Do not use the http.*, db.*, or rpc.* prefixes for custom attributes; these are reserved by the OpenTelemetry specification. Exception events should be recorded as span events (not attributes) using span.recordException(error) in the SDK, which populates the standardized exception.type, exception.message, and exception.stacktrace event attributes automatically.
OTLP JSON Metrics Format: Sum, Gauge, Histogram
OTLP JSON metrics follow the same three-level hierarchy as traces but with different signal-specific objects: resourceMetrics › scopeMetrics › metrics. Each metric object has a name, description, unit, and exactly one data type field: sum, gauge, or histogram. The data type field contains a dataPoints array and aggregation configuration. Exemplars within data points link metric observations to the trace spans that produced them, enabling drill-down from a metric spike to the individual trace.
// POST /v1/metrics Content-Type: application/json
{
"resourceMetrics": [
{
"resource": {
"attributes": [
{ "key": "service.name", "value": { "stringValue": "checkout-api" } }
]
},
"scopeMetrics": [
{
"scope": { "name": "checkout-api-metrics", "version": "1.0.0" },
"metrics": [
// ── Sum (monotonic counter) ───────────────────────────
{
"name": "http.server.request.duration",
"description": "Duration of HTTP server requests",
"unit": "s",
"sum": {
"dataPoints": [
{
"attributes": [
{ "key": "http.request.method", "value": { "stringValue": "POST" } },
{ "key": "http.response.status_code", "value": { "intValue": "200" } }
],
"startTimeUnixNano": "1716124800000000000",
"timeUnixNano": "1716124860000000000",
"asDouble": 142.35,
"exemplars": [
{
"timeUnixNano": "1716124830000000000",
"asDouble": 0.123,
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7"
}
]
}
],
"aggregationTemporality": 2,
"isMonotonic": true
}
},
// ── Gauge (instantaneous value) ───────────────────────
{
"name": "process.runtime.jvm.memory.usage",
"description": "JVM heap memory used",
"unit": "By",
"gauge": {
"dataPoints": [
{
"attributes": [
{ "key": "pool.name", "value": { "stringValue": "heap" } }
],
"timeUnixNano": "1716124860000000000",
"asInt": "104857600"
}
]
}
},
// ── Histogram (distribution) ──────────────────────────
{
"name": "http.server.response.size",
"description": "HTTP response body size distribution",
"unit": "By",
"histogram": {
"dataPoints": [
{
"attributes": [],
"startTimeUnixNano": "1716124800000000000",
"timeUnixNano": "1716124860000000000",
"count": "1200",
"sum": 945000.0,
"bucketCounts": ["10","120","480","480","90","20"],
"explicitBounds": [100, 1000, 10000, 100000, 1000000]
}
],
"aggregationTemporality": 2
}
}
]
}
]
}
]
}
// aggregationTemporality:
// 1 = DELTA — counts since last export flush
// 2 = CUMULATIVE — counts since process start (monotonically increasing)Use Sum with isMonotonic=true for counters that only increase (request counts, error counts, bytes sent). Use Gauge for values that can go up or down (memory usage, active connections, queue depth). Use Histogram for latency and size distributions — the explicitBounds array defines bucket boundaries and bucketCounts has one more element than explicitBounds (the overflow bucket). Delta temporality is preferred by most backends (Prometheus, Datadog) because it avoids server-side subtraction; cumulative temporality is easier to reason about in client code. Exemplars require the SDK to be configured with an exemplar filter — set OTEL_METRICS_EXEMPLAR_FILTER=trace_based to attach trace context only to sampled spans.
OTLP JSON Logs Format: LogRecord Structure
OTLP JSON logs POST to /v1/logs with a resourceLogs array following the same hierarchy as traces and metrics. Each LogRecord JSON object carries a nanosecond timestamp, severity level, body, attributes, and crucially the traceId and spanId fields that correlate the log to the active trace at the time of emission. This correlation makes it possible to jump from a log line directly to its parent trace in Grafana or Jaeger.
// POST /v1/logs Content-Type: application/json
{
"resourceLogs": [
{
"resource": {
"attributes": [
{ "key": "service.name", "value": { "stringValue": "checkout-api" } }
]
},
"scopeLogs": [
{
"scope": { "name": "checkout-api", "version": "2.4.1" },
"logRecords": [
{
"timeUnixNano": "1716124830050000000",
"observedTimeUnixNano": "1716124830051000000",
"severityNumber": 17,
"severityText": "ERROR",
"body": { "stringValue": "Payment gateway timeout after 5000ms" },
"attributes": [
{ "key": "user.id", "value": { "stringValue": "user-42" } },
{ "key": "order.id", "value": { "stringValue": "ord-9182" } },
{ "key": "gateway.name", "value": { "stringValue": "stripe" } },
{ "key": "timeout.ms", "value": { "intValue": "5000" } }
],
"droppedAttributesCount": 0,
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"traceFlags": 1
},
{
"timeUnixNano": "1716124800010000000",
"observedTimeUnixNano": "1716124800011000000",
"severityNumber": 9,
"severityText": "INFO",
"body": { "stringValue": "Checkout initiated" },
"attributes": [
{ "key": "order.id", "value": { "stringValue": "ord-9182" } }
],
"droppedAttributesCount": 0,
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"traceFlags": 1
}
]
}
]
}
]
}
// Severity number mapping (1-24):
// 1-4 TRACE (TRACE, TRACE2, TRACE3, TRACE4)
// 5-8 DEBUG (DEBUG, DEBUG2, DEBUG3, DEBUG4)
// 9-12 INFO (INFO, INFO2, INFO3, INFO4)
// 13-16 WARN (WARN, WARN2, WARN3, WARN4)
// 17-20 ERROR (ERROR, ERROR2, ERROR3, ERROR4)
// 21-24 FATAL (FATAL, FATAL2, FATAL3, FATAL4)The timeUnixNano field is the timestamp when the log event occurred; observedTimeUnixNano is when the SDK or collector received it — these differ when logs are buffered or batched. The traceFlags field (1 = sampled, 0 = not sampled) mirrors the W3C TraceContext traceparent flags byte, allowing log backends to know whether the associated trace was sampled and stored. When using the OpenTelemetry Pino or Winston log bridge, trace context injection into traceId and spanId log record fields is automatic — no manual extraction of the span context is needed. See the JSON structured logging guide for log format comparison across frameworks.
OpenTelemetry Collector Configuration for JSON Export
The OpenTelemetry Collector is a vendor-agnostic proxy that receives, processes, and exports telemetry. Configure it with a YAML pipeline that connects receivers (where data arrives), processors (transformation and filtering), and exporters (where data goes). For JSON debug output, the debug exporter writes structured JSON to stdout; the file exporter writes rotating JSON files to disk. Both are useful during development and for one-time trace captures without running a full backend.
# collector.yaml — OpenTelemetry Collector configuration
receivers:
otlp:
protocols:
http:
endpoint: "0.0.0.0:4318"
# SDK connects to http://localhost:4318/v1/traces (JSON or protobuf)
grpc:
endpoint: "0.0.0.0:4317"
processors:
batch:
# Batch up to 512 spans before exporting (matches SDK default)
send_batch_size: 512
# Wait up to 5s for a full batch before flushing
timeout: 5s
send_batch_max_size: 1000
memory_limiter:
# Prevent OOM if telemetry volume spikes
check_interval: 1s
limit_mib: 512
spike_limit_mib: 128
exporters:
# Debug exporter — JSON to stdout (verbosity: detailed shows all attributes)
debug:
verbosity: detailed
sampling_initial: 5
sampling_thereafter: 200
# File exporter — JSON to disk, one span batch per line
file:
path: /var/log/otel/traces.json
rotation:
max_megabytes: 100
max_days: 3
max_backups: 3
# OTLP HTTP to a backend (Jaeger, Tempo, etc.)
otlphttp:
endpoint: "https://tempo.internal:4318"
headers:
Authorization: "Bearer ${env:TEMPO_TOKEN}"
# Prometheus metrics exporter
prometheus:
endpoint: "0.0.0.0:8889"
namespace: "otel"
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [debug, file, otlphttp]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [prometheus, otlphttp]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [debug, otlphttp]
telemetry:
logs:
level: "info"
metrics:
level: "detailed"
address: "0.0.0.0:8888"Run the collector with otelcol --config collector.yaml. The memory_limiter processor must be the first processor in every pipeline — it protects against OOM conditions when telemetry volume spikes unexpectedly. The batch processor should follow immediately after; it reduces the number of export calls significantly for high-throughput services. For the file exporter, each line in the output file is a complete JSON object representing one batch — parse it with jq -c . traces.json | head -1 | jq . to pretty-print the first batch. See the JSON monitoring and observability guide for a broader collector architecture overview.
Node.js SDK: Instrumenting for OTLP JSON Export
The OpenTelemetry Node.js SDK auto-instruments popular libraries (http, express, pg, redis, grpc) and exports spans as OTLP JSON with minimal configuration. The key is to load the SDK before any application code so that module patching occurs at import time. Set OTEL_EXPORTER_OTLP_PROTOCOL=http/json in the environment to force JSON encoding instead of the default protobuf.
// tracing.ts — load with: node --require ./tracing.js app.js
// or: NODE_OPTIONS="--require ./tracing.js" node app.js
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { Resource } from '@opentelemetry/resources'
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'
const traceExporter = new OTLPTraceExporter({
// Collector OTLP HTTP endpoint
url: 'http://localhost:4318/v1/traces',
headers: {
// Force JSON encoding (default is protobuf)
'Content-Type': 'application/json',
},
})
const metricExporter = new OTLPMetricExporter({
url: 'http://localhost:4318/v1/metrics',
headers: { 'Content-Type': 'application/json' },
})
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: 'checkout-api',
[ATTR_SERVICE_VERSION]: '2.4.1',
'deployment.environment': process.env.NODE_ENV ?? 'development',
}),
traceExporter,
metricReader: new PeriodicExportingMetricReader({
exporter: metricExporter,
exportIntervalMillis: 5000, // flush metrics every 5 seconds
}),
instrumentations: [
getNodeAutoInstrumentations({
// Disable noisy fs instrumentation in production
'@opentelemetry/instrumentation-fs': { enabled: false },
'@opentelemetry/instrumentation-http': {
// Only trace requests with latency > 100ms for sampling
ignoreIncomingRequestHook: (req) => {
return req.url?.startsWith('/health') ?? false
},
},
}),
],
})
sdk.start()
// Graceful shutdown — flush remaining spans before process exits
process.on('SIGTERM', () => {
sdk.shutdown().then(() => process.exit(0))
})
// ── Manual span creation ───────────────────────────────────────
import { trace, context, SpanStatusCode } from '@opentelemetry/api'
const tracer = trace.getTracer('checkout-api', '2.4.1')
async function processPayment(orderId: string, amount: number) {
return tracer.startActiveSpan('payment.process', async (span) => {
try {
span.setAttribute('order.id', orderId)
span.setAttribute('payment.amount', amount)
span.setAttribute('payment.currency', 'USD')
const result = await chargeCard(orderId, amount)
span.setAttribute('payment.transaction_id', result.transactionId)
span.setStatus({ code: SpanStatusCode.OK })
return result
} catch (err) {
span.recordException(err as Error)
span.setStatus({ code: SpanStatusCode.ERROR, message: (err as Error).message })
throw err
} finally {
span.end()
}
})
}
// ── W3C TraceContext propagation ──────────────────────────────
// Auto-instrumentation injects traceparent header automatically.
// For manual HTTP calls with fetch:
import { propagation, ROOT_CONTEXT } from '@opentelemetry/api'
async function callDownstreamService(url: string) {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
// Inject current trace context into outgoing headers
propagation.inject(context.active(), headers)
// headers now contains: traceparent: "00-{32hexTraceId}-{16hexSpanId}-01"
return fetch(url, { method: 'GET', headers })
}Set the OTEL_EXPORTER_OTLP_ENDPOINT environment variable to override the exporter URL without code changes — useful for switching between local collector and production endpoint across environments. The OTEL_TRACES_SAMPLER variable controls sampling: always_on (100%), always_off (0%), traceidratio (percentage-based, e.g., OTEL_TRACES_SAMPLER_ARG=0.1 for 10%). For production Node.js services, see the JSON microservices patterns guide for service mesh context propagation.
Querying OTLP JSON in Jaeger, Tempo, and Elasticsearch
Once OTLP JSON is ingested by a backend, each system exposes its own query interface. Jaeger provides a UI and REST API; Grafana Tempo adds TraceQL for programmatic span filtering; Elasticsearch stores span fields as document fields queryable via DSL. For local debugging of raw OTLP JSON files, jq is the most powerful tool available without running any backend infrastructure.
# ── jq queries on local OTLP JSON files ───────────────────────
# List all span names in a trace export
cat traces.json | jq -r '
.resourceSpans[].scopeSpans[].spans[].name'
# Find all ERROR spans (status.code == 2)
cat traces.json | jq '
.resourceSpans[].scopeSpans[].spans[]
| select(.status.code == 2)'
# Extract traceId + duration for all spans (duration in milliseconds)
cat traces.json | jq -r '
.resourceSpans[].scopeSpans[].spans[]
| [
.traceId,
.name,
((.endTimeUnixNano | tonumber) - (.startTimeUnixNano | tonumber)) / 1000000
]
| @tsv'
# Find HTTP 500 spans by attribute value
cat traces.json | jq '
.resourceSpans[].scopeSpans[].spans[]
| select(
.attributes[]?
| select(.key == "http.response.status_code")
| .value.intValue == "500"
)'
# ── Jaeger REST API ─────────────────────────────────────────────
# Fetch a specific trace by ID (returns Jaeger-native JSON)
curl "http://localhost:16686/api/traces/4bf92f3577b34da6a3ce929d0e0e4736" | jq .
# Search traces by service and operation
curl "http://localhost:16686/api/traces?service=checkout-api&operation=POST+/checkout&limit=20&lookback=1h" | jq .data[].spans | length
# ── Grafana Tempo TraceQL ───────────────────────────────────────
# All spans with HTTP 500 errors in last 1h
{ span.http.response.status_code = 500 && resource.service.name = "checkout-api" }
# Spans with duration > 1 second
{ duration > 1s && resource.service.name = "checkout-api" }
# Find traces containing a specific error message
{ span.exception.message =~ "timeout" }
# Tempo REST API — fetch trace JSON
curl "http://localhost:3200/api/traces/4bf92f3577b34da6a3ce929d0e0e4736" -H "Accept: application/json" | jq .
# ── Elasticsearch OTLP field mapping ───────────────────────────
# Query for all traces with errors in the last 24h
curl -X POST "http://localhost:9200/traces-*/_search" -H "Content-Type: application/json" -d '{
"query": {
"bool": {
"must": [
{ "term": { "status.code": 2 } },
{ "term": { "resource.service.name": "checkout-api" } },
{ "range": { "startTimeUnixNano": { "gte": "now-24h" } } }
]
}
},
"sort": [{ "startTimeUnixNano": "desc" }],
"_source": ["traceId", "spanId", "name", "status", "startTimeUnixNano"]
}' | jq .hits.hitsGrafana Tempo's TraceQL is the most concise way to search OTLP spans — it operates directly on the OTLP data model, using span.* for span attributes and resource.* for resource attributes. Combine Tempo with Grafana Loki for log correlation: configure Loki's derived fields to extract the traceId from log lines and link to the Tempo trace viewer. For Elasticsearch, the OpenTelemetry Collector's elasticsearchexporter (in collector-contrib) handles field mapping from OTLP JSON to Elasticsearch documents automatically. See the JSON analytics events guide for event-level analysis patterns that complement trace-level observability.
Key Terms
- OTLP
- OpenTelemetry Protocol — the wire protocol used by OpenTelemetry SDKs and the Collector to transmit traces, metrics, and logs. OTLP has two encodings: protobuf (binary, compact, default) and JSON (text, human-readable, ~3× larger). OTLP HTTP/JSON is transmitted as POST requests to
/v1/traces,/v1/metrics, or/v1/logswithContent-Type: application/json. OTLP is the recommended export format for all OpenTelemetry SDKs and is supported natively by Jaeger, Grafana Tempo, Datadog, Honeycomb, and most modern observability backends. The protocol is defined in theopentelemetry-protoGitHub repository as Protocol Buffer schemas that are also the source of the JSON field names. - ResourceSpans
- The top-level element in an OTLP JSON trace export — a JSON object containing a
resource(describing the service instance producing telemetry, with attributes likeservice.nameanddeployment.environment), ascopeSpansarray (grouping spans by instrumentation library), and an optionalschemaUrlidentifying the semantic convention version. A single export request body contains aresourceSpansarray where each element represents one resource. In practice, a single-process application produces one ResourceSpans element per export; a batch processor or collector aggregating from multiple services may produce multiple ResourceSpans elements in one request body. - semantic convention
- A standardized attribute name and value definition maintained by the OpenTelemetry project that gives span and resource attributes consistent cross-vendor meaning. Semantic conventions are organized by technology domain (HTTP, database, RPC, messaging, cloud, container) and published as versioned specifications in the
opentelemetry-specificationrepository. Examples:http.request.methodfor the HTTP verb,db.systemfor the database technology,service.namefor the logical service identifier. Using semantic conventions enables automatic service topology maps, SLO dashboards, and alerting rules in observability backends because they can query by well-known attribute names without custom field configuration per service. - exemplar
- A sample data point within a metric that carries a reference to the trace span that produced it, enabling navigation from a metric anomaly to the individual trace that caused it. In OTLP JSON, an exemplar is an object within a metric data point's
exemplarsarray, containingtimeUnixNano,asDouble(the observed value),traceId(32 hex chars), andspanId(16 hex chars). For example, a histogram data point tracking HTTP request duration may include an exemplar pointing to the trace of the slowest request in that bucket. Exemplars require the SDK to have an exemplar filter configured and the backend (Prometheus with exemplar storage, Grafana Tempo) to support exemplar ingestion and display. - aggregation temporality
- A property of OTLP metric data points that specifies whether the reported value is cumulative (accumulated since process start) or delta (accumulated since the last export). In OTLP JSON, represented as the integer
aggregationTemporalityfield:1= DELTA,2= CUMULATIVE. Prometheus expects cumulative (monotonically increasing) counters; Datadog and most cloud metrics systems prefer delta. The OpenTelemetry SDK defaults to cumulative for all instruments; the Collector can convert between temporalities using thecumulativetodeltaprocessor in collector-contrib. Delta temporality reduces the risk of counter resets on restart appearing as large negative values in dashboards. - context propagation
- The mechanism by which trace context (traceId and spanId) is transmitted across process and service boundaries so that spans created in different services can be linked into a single distributed trace. OpenTelemetry uses the W3C TraceContext standard: the
traceparentHTTP header carries the format00-{32hexTraceId}-{16hexSpanId}-{flags}, where flags is a two-hex-digit bitmask (01 = sampled). A receiving service extracts the traceId and spanId fromtraceparentand uses them as theparentSpanIdof its root span, linking the two services' spans into one trace. OpenTelemetry auto-instrumentation for HTTP clients and servers handles injection and extraction automatically; manual propagation requires callingpropagation.inject()on outgoing requests andpropagation.extract()on incoming requests.
FAQ
What is the OTLP JSON format for traces?
OTLP JSON is the text encoding of the OpenTelemetry Protocol, transmitted as a POST to /v1/traces with Content-Type: application/json. The body is a JSON object with a resourceSpans array. Each element contains a resource object (service attributes), a scopeSpans array (grouped by instrumentation library), and within each scope a spans array. Each span has 12 required fields: traceId (32 hex chars), spanId (16 hex chars), name, kind (integer 0-5), startTimeUnixNano and endTimeUnixNano (decimal strings), attributes (key-value array), droppedAttributesCount, events, droppedEventsCount, links, and status. The parentSpanId field is optional — omit it for root spans. Nanosecond timestamps are strings, not numbers, to avoid JSON integer precision loss for 64-bit values.
What is the difference between OTLP JSON and OTLP protobuf?
Both encode the same OpenTelemetry data model but use different wire formats. OTLP protobuf (Content-Type: application/x-protobuf) is binary, compact, and ~3× smaller than OTLP JSON for the same trace data — it encodes field names as integer tags and binary IDs as raw bytes rather than hex strings. OTLP JSON is human-readable, debuggable with curl and jq, and works in environments where binary protocols are blocked by proxies. Use protobuf in production for bandwidth efficiency; use JSON for local development, debugging, and environments that cannot handle binary payloads. Switch the SDK encoding by setting OTEL_EXPORTER_OTLP_PROTOCOL=http/json (JSON) or http/protobuf (binary). The OpenTelemetry Collector accepts both on the same port, selecting the encoding from the request's Content-Type header.
How do I configure the OpenTelemetry Collector to output JSON?
The Collector ships with two JSON output exporters. The debug exporter writes structured JSON to stdout — set verbosity: detailed to include all span attributes. The file exporter writes batches as JSON lines to a rotating file on disk — set path: /var/log/otel/traces.json and configure rotation.max_megabytes and max_days. Add the exporter to the exporters section of collector.yaml and reference it in the appropriate service.pipelines list alongside your receiver (otlp) and processors (memory_limiter, batch). Run the collector with otelcol --config collector.yaml. You can also use the logging exporter (the predecessor to debug) with loglevel: debug in older collector versions. The debug exporter is the recommended choice in collector v0.87.0 and later.
What are OpenTelemetry semantic convention attributes?
Semantic conventions are standardized attribute names defined by the OpenTelemetry project that give span attributes consistent cross-vendor meaning. They are grouped by technology domain: HTTP (http.request.method, http.response.status_code, url.path, server.address), database (db.system, db.name, db.operation.name, db.query.text), RPC (rpc.system, rpc.service, rpc.method), messaging (messaging.system, messaging.destination.name), and exceptions (exception.type, exception.message, exception.stacktrace). All values are transmitted as AnyValue JSON objects with exactly one typed key: stringValue, intValue, boolValue, doubleValue, arrayValue, or kvlistValue. Following conventions enables automatic service maps and dashboards in observability backends without custom field mapping.
How do I correlate logs and traces using JSON fields?
OTLP JSON log records include traceId (32 hex chars) and spanId (16 hex chars) fields that match the active span at log emission time. To achieve correlation: use an OpenTelemetry log bridge (Pino bridge: pino-opentelemetry-transport; Winston bridge: @opentelemetry/winston-transport) that automatically injects trace context into log records without manual coding. Configure your log exporter to send OTLP JSON to the same collector pipeline as your traces. In Grafana, add a Loki data source derived field that extracts traceId from log lines and links to Tempo with the URL /explore?datasource=tempo&queries[0].traceId=${__value.raw}. In Elasticsearch, query by traceId field to retrieve all logs from a specific trace. The 32-character hex traceId string is the universal correlation key between all three OTLP signals.
How do I instrument a Node.js application to export OTLP JSON traces?
Install three packages: @opentelemetry/sdk-node, @opentelemetry/auto-instrumentations-node, and @opentelemetry/exporter-trace-otlp-http. Create a tracing.ts file that instantiates OTLPTraceExporter with url: 'http://localhost:4318/v1/traces' and headers: { "Content-Type": "application/json" }. Pass the exporter and getNodeAutoInstrumentations() to NodeSDK along with a Resource defining service.name. Call sdk.start() at the top of the file and sdk.shutdown() on SIGTERM. Load this file before your application with node --require ./tracing.js app.js or the NODE_OPTIONS='--require ./tracing.js' environment variable. For manual spans, obtain a tracer with trace.getTracer('my-service') and create spans with tracer.startActiveSpan().
What JSON fields are required in an OpenTelemetry span?
A valid OTLP JSON span requires 12 fields: traceId (32-char hex, 128-bit trace identifier), spanId (16-char hex, 64-bit span identifier), name (operation name string), kind (integer 0-5), startTimeUnixNano (decimal string, nanoseconds since Unix epoch), endTimeUnixNano (decimal string), attributes (array of key-value objects, may be empty), droppedAttributesCount (integer, typically 0), events (array, may be empty), droppedEventsCount (integer), links (array, may be empty), and status (object with code integer: 0=unset, 1=ok, 2=error). Optional fields include parentSpanId (for child spans), traceState (W3C TraceState header value), and droppedLinksCount. Nanosecond timestamps must be decimal strings to avoid JavaScript's 64-bit integer precision limit.
How do I query OTLP trace JSON with Jaeger or Grafana Tempo?
Jaeger accepts OTLP JSON via its HTTP receiver on port 4318 (/v1/traces) and stores spans in its backend. Query via the Jaeger UI by searching service, operation, tags, and time range. The Jaeger REST API returns full trace JSON at /api/traces/{traceId}. Grafana Tempo accepts OTLP JSON directly and supports TraceQL for span-level filtering: { span.http.response.status_code = 500 } finds all HTTP 500 spans; { duration > 1s } finds slow spans. The Tempo REST API returns trace JSON at /api/traces/{traceId}. For local OTLP JSON files without a backend, use jq: cat traces.json | jq '.resourceSpans[].scopeSpans[].spans[] | select(.status.code == 2)' lists all error spans. Combine jq with tonumber arithmetic on the nanosecond timestamp strings to compute span durations in milliseconds.
Further reading and primary sources
- OpenTelemetry Protocol (OTLP) Specification — Official OTLP spec defining the JSON and protobuf wire formats for traces, metrics, and logs
- OpenTelemetry Semantic Conventions — Standardized attribute names for HTTP, database, RPC, messaging, and cloud resources
- OpenTelemetry Collector Documentation — Collector architecture, YAML configuration, receivers, processors, and exporters reference
- OpenTelemetry Node.js SDK Getting Started — Node.js auto-instrumentation, OTLPTraceExporter setup, and manual span creation guide
- Grafana Tempo TraceQL Documentation — TraceQL query language for searching OTLP spans stored in Grafana Tempo