OpenTelemetry JSON Format: OTLP/JSON Spans, Metrics, Logs
Last updated:
OTLP (OpenTelemetry Protocol) supports two encodings: Protobuf binary and JSON — OTLP/JSON sends telemetry data as JSON over HTTP/1.1 to the /v1/traces, /v1/metrics, and /v1/logsendpoints on an OpenTelemetry collector. OTLP/JSON is 2–3× larger than OTLP/Protobuf but requires no schema files and works with any HTTP client — ideal for debugging and HTTP-only environments. The JSON structure mirrors the Protobuf schema: resourceSpans, scopeSpans, and spans arrays nest inside each other to describe a batch of trace data. This guide covers the OTLP/JSON span format, how to export traces as JSON with the Node.js OpenTelemetry SDK, parsing and querying OTLP/JSON payloads with jq, and the JSON format for metrics and logs. Use Jsonic's JSON formatter to validate and explore OTLP/JSON payloads while building your observability pipeline. For related observability patterns, see JSON structured logging.
Need to inspect or pretty-print an OTLP/JSON payload? Paste it into Jsonic and format it instantly.
Open JSON FormatterOTLP/JSON structure: resourceSpans, scopeSpans, spans arrays
Every OTLP/JSON trace payload is a single JSON object with one key: resourceSpans. This array groups spans by the resource (service instance) that produced them, then by instrumentation scope (library), then lists individual spans. The three-level nesting — resource → scope → spans — matches the Protobuf schema exactly and exists to avoid repeating resource attributes on every span. A resource typically carries service.name, service.version, and deployment.environment as semantic convention attributes. Thescope identifies the instrumentation library (e.g. @opentelemetry/instrumentation-http) and its version.
// Full annotated OTLP/JSON trace payload
{
"resourceSpans": [
{
// Resource: the service/process that produced the spans
"resource": {
"attributes": [
{ "key": "service.name", "value": { "stringValue": "checkout-api" } },
{ "key": "service.version", "value": { "stringValue": "2.1.0" } },
{ "key": "deployment.environment", "value": { "stringValue": "production" } }
]
},
// scopeSpans groups spans by instrumentation library
"scopeSpans": [
{
"scope": {
"name": "@opentelemetry/instrumentation-http",
"version": "0.45.0"
},
"spans": [
{
// 32-char hex traceId (128-bit), 16-char hex spanId (64-bit)
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"parentSpanId": "b9c7c989f97918e1", // omit for root span
"name": "POST /checkout",
"kind": 2, // 1=INTERNAL 2=SERVER 3=CLIENT 4=PRODUCER 5=CONSUMER
"startTimeUnixNano": "1700000000000000000",
"endTimeUnixNano": "1700000000250000000",
"status": { "code": 1 }, // 0=UNSET 1=OK 2=ERROR
"attributes": [
{ "key": "http.method", "value": { "stringValue": "POST" } },
{ "key": "http.status_code", "value": { "intValue": "200" } },
{ "key": "http.url", "value": { "stringValue": "https://api.example.com/checkout" } }
],
"events": [
{
"timeUnixNano": "1700000000100000000",
"name": "payment.processed",
"attributes": [
{ "key": "payment.provider", "value": { "stringValue": "stripe" } }
]
}
],
"links": []
}
]
}
]
}
]
}The payload is POSTed to http://collector:4318/v1/traces with Content-Type: application/json. The collector accepts this on TCP port 4318 (HTTP), while the gRPC/Protobuf endpoint is port 4317. You can send multiple resources in a single payload by adding more objects to the resourceSpans array — the SDK batches spans to reduce HTTP request overhead. For context on how JSON compares to binary formats, see JSON to Protobuf.
Span JSON fields: traceId, spanId, kind enum, timestamps in nanoseconds
Understanding each span field is necessary for correctly generating, parsing, and validating OTLP/JSON spans. The most common mistakes involve timestamp precision and ID encoding — both differ from what many developers expect.
| Field | Type | Example | Notes |
|---|---|---|---|
traceId | string (32 hex chars) | "4bf92f3577b34da6a3ce929d0e0e4736" | 128-bit, base16 — NOT base64 |
spanId | string (16 hex chars) | "00f067aa0ba902b7" | 64-bit, base16 — NOT base64 |
parentSpanId | string (16 hex chars) | "b9c7c989f97918e1" | Omit or set to "" for root spans |
name | string | "POST /checkout" | Low-cardinality operation name; avoid user IDs in name |
kind | integer (enum) | 2 | 1=INTERNAL, 2=SERVER, 3=CLIENT, 4=PRODUCER, 5=CONSUMER |
startTimeUnixNano | string (decimal int) | "1700000000000000000" | Nanoseconds since Unix epoch; string to avoid JS precision loss |
endTimeUnixNano | string (decimal int) | "1700000000250000000" | Same format as start; duration = end - start |
status.code | integer (enum) | 1 | 0=UNSET (default), 1=OK, 2=ERROR |
Timestamps are nanoseconds since the Unix epoch represented as decimal strings — not milliseconds, not ISO 8601. The string representation is required because nanosecond-precision Unix timestamps exceed JavaScript's Number.MAX_SAFE_INTEGER (253-1), and JSON numbers would lose precision. To convert from a JavaScript Date: BigInt(Date.now()) * 1_000_000n gives nanoseconds as a BigInt, then convert to string with .toString(). Span kind determines how backends visualize the relationship: SERVER spans represent inbound RPC calls, CLIENT spans represent outbound calls, and INTERNAL spans represent work within a single process. For patterns on structuring JSON for APIs, see REST API JSON response design.
Attributes format: key-value pairs with AnyValue
OTLP/JSON attributes use a typed value wrapper called AnyValue instead of raw JSON primitives. Every attribute is an object with a key string and a value object whose single key indicates the type. This design mirrors the Protobuf AnyValue oneof and makes deserialization unambiguous — a receiver knows whether "42" is a string or the number 42without guessing from context.
// AnyValue type examples
"attributes": [
// String
{ "key": "http.method", "value": { "stringValue": "GET" } },
// Integer (as JSON string to avoid 64-bit precision loss)
{ "key": "http.status_code", "value": { "intValue": "200" } },
// Double (floating point)
{ "key": "db.query_time_ms", "value": { "doubleValue": 12.5 } },
// Boolean
{ "key": "error", "value": { "boolValue": false } },
// Array of AnyValues
{
"key": "http.request.header.accept",
"value": {
"arrayValue": {
"values": [
{ "stringValue": "application/json" },
{ "stringValue": "text/html" }
]
}
}
},
// Key-value list (nested attributes)
{
"key": "db.connection",
"value": {
"kvlistValue": {
"values": [
{ "key": "host", "value": { "stringValue": "postgres.internal" } },
{ "key": "port", "value": { "intValue": "5432" } }
]
}
}
}
]The six AnyValue variants are: stringValue, boolValue, intValue (decimal string to preserve 64-bit precision), doubleValue, arrayValue, and kvlistValue. Follow OpenTelemetry semantic conventions for attribute names — http.method, db.system, messaging.system— to ensure your spans are understood by standard backends like Jaeger, Zipkin, and Datadog. High-cardinality values (user IDs, full URLs with query strings) should go in attributes, not in the span name, to avoid exploding your backend's index cardinality. For background on JSON key-value structures, see JSON structured logging.
Export OTLP/JSON from Node.js: @opentelemetry/exporter-trace-otlp-http setup
The @opentelemetry/exporter-trace-otlp-http package sends spans as OTLP/JSON over HTTP by default. It batches spans using a BatchSpanProcessor and POSTs them to the collector's /v1/traces endpoint. The setup requires three packages: the Node.js SDK, the HTTP exporter, and resource detection for service metadata.
npm install @opentelemetry/sdk-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/resources \
@opentelemetry/semantic-conventions \
@opentelemetry/auto-instrumentations-node// tracing.js — initialize before any other import
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { Resource } from '@opentelemetry/resources'
import { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
const exporter = new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ?? 'http://localhost:4318/v1/traces',
headers: {
// Add auth headers for managed backends (e.g. Honeycomb, Grafana Cloud):
// 'x-honeycomb-team': process.env.HONEYCOMB_API_KEY,
},
// OTLPTraceExporter uses JSON by default (not Protobuf)
})
const sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: 'checkout-api',
[SEMRESATTRS_SERVICE_VERSION]: '2.1.0',
}),
traceExporter: exporter,
instrumentations: [getNodeAutoInstrumentations()],
})
sdk.start()
// Flush spans on process exit
process.on('SIGTERM', () => sdk.shutdown().finally(() => process.exit(0)))// app.js — import tracing FIRST, then your application
import './tracing.js'
import express from 'express'
import { trace } from '@opentelemetry/api'
const app = express()
const tracer = trace.getTracer('checkout-api')
app.post('/checkout', async (req, res) => {
// Auto-instrumented: http span created by auto-instrumentations-node
// Manual child span for business logic:
const span = tracer.startSpan('process-payment', {
attributes: {
'payment.provider': 'stripe',
'payment.amount': req.body.amount,
},
})
try {
const result = await processPayment(req.body)
span.setStatus({ code: 1 }) // OK
res.json(result)
} catch (err) {
span.setStatus({ code: 2, message: err.message }) // ERROR
span.recordException(err)
res.status(500).json({ error: 'Payment failed' })
} finally {
span.end()
}
})To switch from JSON to Protobuf encoding (for production throughput), replace @opentelemetry/exporter-trace-otlp-http with @opentelemetry/exporter-trace-otlp-grpc and change the URL to port 4317. Environment variables override code configuration: OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318 and OTEL_SERVICE_NAME=checkout-api set the endpoint and service name without code changes. For more on JSON handling in Node.js applications, see parsing JSON in Node.js.
Metrics OTLP/JSON: gauge, sum, histogram resource metrics structure
OTLP/JSON metrics payloads use resourceMetrics at the top level, parallel to resourceSpans for traces. Each metric data point carries a timestamp, value, and attributes identifying dimensions (e.g. which CPU core, which HTTP status code). Three metric kinds cover all measurement types: gauge (instantaneous value), sum (cumulative or delta counter), and histogram (distribution with buckets).
// OTLP/JSON metrics payload — POST to /v1/metrics
{
"resourceMetrics": [
{
"resource": {
"attributes": [
{ "key": "service.name", "value": { "stringValue": "checkout-api" } }
]
},
"scopeMetrics": [
{
"scope": { "name": "checkout-api-metrics", "version": "1.0.0" },
"metrics": [
// GAUGE — instantaneous reading (e.g. CPU %, queue depth)
{
"name": "system.cpu.utilization",
"unit": "1",
"description": "CPU utilization fraction (0–1)",
"gauge": {
"dataPoints": [
{
"attributes": [
{ "key": "cpu.core", "value": { "stringValue": "cpu0" } }
],
"timeUnixNano": "1700000000000000000",
"asDouble": 0.42
}
]
}
},
// SUM — monotonic counter (e.g. total requests)
{
"name": "http.server.request.count",
"unit": "{request}",
"sum": {
"dataPoints": [
{
"attributes": [
{ "key": "http.status_code", "value": { "intValue": "200" } }
],
"startTimeUnixNano": "1699999900000000000",
"timeUnixNano": "1700000000000000000",
"asInt": "1523"
}
],
"aggregationTemporality": 2, // 1=DELTA 2=CUMULATIVE
"isMonotonic": true
}
},
// HISTOGRAM — distribution (e.g. request latency)
{
"name": "http.server.duration",
"unit": "ms",
"histogram": {
"dataPoints": [
{
"attributes": [
{ "key": "http.method", "value": { "stringValue": "POST" } }
],
"startTimeUnixNano": "1699999900000000000",
"timeUnixNano": "1700000000000000000",
"count": "843",
"sum": 12650.5,
"explicitBounds": [0, 5, 10, 25, 50, 75, 100, 250, 500, 1000],
"bucketCounts": ["12","87","203","251","164","62","38","17","7","2","1"]
}
],
"aggregationTemporality": 2
}
}
]
}
]
}
]
}aggregationTemporality of 2 (CUMULATIVE) means the counter value represents the total since the process started — backends compute rates by comparing successive snapshots. DELTA (1) sends only the amount that changed since the last export. Most Prometheus-compatible backends prefer CUMULATIVE; some streaming processors prefer DELTA. For a complete understanding of how JSON encodes structured numeric data, see REST API JSON response design.
Logs OTLP/JSON: severity, body, traceId correlation, logRecord format
OTLP/JSON logs use resourceLogs at the top level, with the same resource/scope nesting as traces and metrics. Each log entry is a logRecordcontaining a timestamp, severity, body, and optional traceId and spanId fields for correlating log lines with active spans. This correlation is the key advantage of OTLP logs over traditional log shipping: a single query on a traceId returns both spans and their associated log records from the same request.
// OTLP/JSON logs payload — POST to /v1/logs
{
"resourceLogs": [
{
"resource": {
"attributes": [
{ "key": "service.name", "value": { "stringValue": "checkout-api" } }
]
},
"scopeLogs": [
{
"scope": { "name": "checkout-api", "version": "2.1.0" },
"logRecords": [
{
"timeUnixNano": "1700000000100000000",
"observedTimeUnixNano": "1700000000105000000",
// Severity: numeric level + short name string
// TRACE=1-4 DEBUG=5-8 INFO=9-12 WARN=13-16 ERROR=17-20 FATAL=21-24
"severityNumber": 9,
"severityText": "INFO",
// Body is an AnyValue — can be string or structured object
"body": { "stringValue": "Payment processed successfully" },
// Link to the active span when the log was emitted
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
// Structured fields as attributes (same AnyValue format as spans)
"attributes": [
{ "key": "payment.id", "value": { "stringValue": "pay_abc123" } },
{ "key": "payment.amount", "value": { "doubleValue": 49.99 } },
{ "key": "payment.currency", "value": { "stringValue": "USD" } }
]
}
]
}
]
}
]
}The severityNumber ranges are: TRACE 1–4, DEBUG 5–8, INFO 9–12, WARN 13–16, ERROR 17–20, FATAL 21–24. Each range has four sub-levels (e.g. INFO=9, INFO2=10, INFO3=11, INFO4=12) for fine-grained ordering. Most applications use the base level of each range (9, 13, 17). When emitting OTLP logs from the Node.js SDK, use @opentelemetry/winston-transport to bridge Winston log calls into OTLP log records automatically — span context (traceId, spanId) is injected automatically when logging within an active span. For detailed structured logging patterns, see JSON structured logging.
Debug with OTLP/JSON: curl to collector, jq queries on span data
OTLP/JSON's plain-text nature makes it straightforward to debug without specialized tooling. You can send test payloads with curl, capture collector output to a file, and use jq to filter and transform span data. This workflow is especially valuable when setting up a new instrumentation or diagnosing missing spans in a distributed trace.
# Send a minimal test span to a local collector
curl -X POST http://localhost:4318/v1/traces \
-H "Content-Type: application/json" \
-d '{
"resourceSpans": [{
"resource": {
"attributes": [
{"key":"service.name","value":{"stringValue":"test-service"}}
]
},
"scopeSpans": [{
"scope": {"name":"manual-test"},
"spans": [{
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"name": "test-operation",
"kind": 1,
"startTimeUnixNano": "1700000000000000000",
"endTimeUnixNano": "1700000001000000000",
"status": {"code": 1}
}]
}]
}]
}'# jq queries on saved OTLP/JSON payload (payload.json)
# List all span names
jq '.resourceSpans[].scopeSpans[].spans[].name' payload.json
# Find spans longer than 100ms (100,000,000 nanoseconds)
jq '.resourceSpans[].scopeSpans[].spans[]
| select(
((.endTimeUnixNano | tonumber) - (.startTimeUnixNano | tonumber))
> 100000000
)
| {name, traceId, spanId, durationMs: (
((.endTimeUnixNano | tonumber) - (.startTimeUnixNano | tonumber)) / 1000000
)}' payload.json
# Find all ERROR spans (status.code == 2)
jq '.resourceSpans[].scopeSpans[].spans[]
| select(.status.code == 2)
| {name, traceId, spanId}' payload.json
# Extract all unique attribute keys
jq '[.resourceSpans[].scopeSpans[].spans[].attributes[].key] | unique' payload.json
# Get all spans for a specific traceId
jq --arg tid "4bf92f3577b34da6a3ce929d0e0e4736"
'.resourceSpans[].scopeSpans[].spans[]
| select(.traceId == $tid)
| {spanId, parentSpanId, name}' payload.jsonTo capture OTLP/JSON from your SDK without a real collector, run a local debug receiver: docker run --rm -p 4318:4318 otel/opentelemetry-collector and redirect its stdout to a file. Alternatively, set OTEL_TRACES_EXPORTER=console in your Node.js app to print JSON spans directly to stdout for development inspection. Use Jsonic to pretty-print the console output and verify all expected attributes are present. For jq filter patterns beyond telemetry data, see jq filter examples.
Definitions
- OTLP
- OpenTelemetry Protocol — the wire format specification for transmitting telemetry (traces, metrics, logs) from SDKs to collectors and backends. Supports two encodings: Protobuf binary (gRPC or HTTP) and JSON (HTTP only).
- Span
- A single named, timed operation within a distributed trace. A span records a start timestamp, end timestamp, status, attributes, and events. Spans are linked into a tree by
traceId(shared by all spans in a request) andparentSpanId(linking child to parent). - TraceId
- A 128-bit identifier shared by all spans in a single distributed trace — represented in OTLP/JSON as 32 lowercase hexadecimal characters. A traceId is generated once at the entry point of a request and propagated to all downstream services via the W3C traceparent header or B3 headers.
- SpanId
- A 64-bit identifier unique to a single span — represented in OTLP/JSON as 16 lowercase hexadecimal characters. Combined with a traceId, a spanId uniquely identifies any span in the global trace space. Child spans set their
parentSpanIdto the spanId of their parent. - AnyValue
- The typed value wrapper used for OTLP/JSON attributes and log record bodies. An AnyValue is a JSON object with exactly one of these keys:
stringValue,boolValue,intValue,doubleValue,arrayValue, orkvlistValue. This ensures unambiguous type information without relying on JSON inference. - ResourceSpans
- The top-level array in an OTLP/JSON trace payload. Each element groups all spans emitted by a single resource (service instance) identified by resource attributes such as
service.name,service.version, anddeployment.environment. - ScopeSpans
- A grouping of spans within a
ResourceSpanselement, identified by the instrumentation scope (library name and version) that produced the spans. For example, HTTP spans from@opentelemetry/instrumentation-httpand database spans from@opentelemetry/instrumentation-pgappear in separatescopeSpansentries under the same resource.
Frequently asked questions
What is OTLP/JSON format?
OTLP/JSON (OpenTelemetry Protocol JSON) is the JSON encoding of the OpenTelemetry Protocol. It sends telemetry data — traces, metrics, and logs — as JSON objects over HTTP/1.1 to a collector or backend. The top-level structure mirrors the Protobuf schema: resourceSpans for traces, resourceMetrics for metrics, and resourceLogs for logs. Each payload is POSTed to /v1/traces, /v1/metrics, or /v1/logsrespectively with Content-Type: application/json. OTLP/JSON is 2–3× larger than OTLP/Protobuf because it includes field names as strings rather than compact field numbers, but requires no schema files and works with any standard HTTP client or curl command.
How do I send traces as JSON to an OpenTelemetry collector?
Send a POST request to http://collector:4318/v1/traces with Content-Type: application/json and a body containing a resourceSpans array. With curl: curl -X POST http://collector:4318/v1/traces -H "Content-Type: application/json" -d @payload.json. The collector default HTTP port is 4318 for OTLP/JSON and OTLP/HTTP-Protobuf, and 4317 for OTLP/gRPC. A successful response returns HTTP 200 with an empty JSON object {}. If you receive 400, your payload structure is invalid — paste it into Jsonic and check the resourceSpans nesting. For SDK-based export, see the Node.js export section above.
What is the format of a span in OTLP/JSON?
An OTLP/JSON span has these core fields: traceId (32 hex chars), spanId (16 hex chars), parentSpanId (16 hex chars, omit for root spans), name (operation name string), kind (integer enum: 1=INTERNAL, 2=SERVER, 3=CLIENT, 4=PRODUCER, 5=CONSUMER), startTimeUnixNano and endTimeUnixNano (Unix nanoseconds as decimal strings), status (object with code: 0=UNSET, 1=OK, 2=ERROR), attributes (array of AnyValue key-value pairs), events (timed annotations), links (span links for linked traces), and resource (service attributes, set at the resourceSpans level). Timestamps are nanoseconds represented as strings to preserve 64-bit integer precision.
What encoding does traceId use in OTLP/JSON?
In OTLP/JSON, traceId is base16-encoded (hex), NOT base64. A traceId is exactly 32 lowercase hexadecimal characters representing a 128-bit trace identifier — for example "4bf92f3577b34da6a3ce929d0e0e4736". Similarly, spanId is 16 lowercase hex characters representing a 64-bit identifier — for example "00f067aa0ba902b7". This differs from the Protobuf binary encoding, which stores these as raw byte arrays. The hex encoding was chosen for OTLP/JSON because it is human-readable and consistent with W3C traceparent header format. If your traceId appears to be base64 (e.g. shorter, with + or / chars), you are using the wrong encoding for OTLP/JSON.
What is the difference between OTLP/JSON and OTLP/Protobuf?
OTLP/Protobuf uses compact binary encoding with field numbers instead of names, making it 2–3× smaller than OTLP/JSON and faster to serialize and deserialize. OTLP/JSON uses human-readable text, includes field names as strings, and works over plain HTTP/1.1 — making it easy to inspect, debug, and send with any HTTP client. OTLP/Protobuf requires proto schema files and a Protobuf library; OTLP/JSON has no dependencies beyond a JSON parser. Both use the same data model and the same collector endpoints: port 4318 for HTTP (both JSON and Protobuf with different Content-Typeheaders) and port 4317 for gRPC. Use OTLP/JSON for development and debugging; prefer OTLP/Protobuf for production throughput. See JSON to Protobuf for format comparison.
How do I export OpenTelemetry JSON from Node.js?
Install @opentelemetry/exporter-trace-otlp-http and configure it: import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; const exporter = new OTLPTraceExporter({ url: "http://localhost:4318/v1/traces" });. Wrap it in a NodeSDK: import { NodeSDK } from "@opentelemetry/sdk-node"; const sdk = new NodeSDK({ traceExporter: exporter }); sdk.start();. The HTTP exporter sends OTLP/JSON by default (not Protobuf). Import your tracing initialization file before any other module. Use OTEL_EXPORTER_OTLP_TRACES_ENDPOINT environment variable to override the URL without code changes. For more Node.js JSON patterns, see parsing JSON in Node.js.
How do I query OTLP/JSON spans with jq?
Use jq to filter and transform saved OTLP/JSON payloads. List all span names: jq '.resourceSpans[].scopeSpans[].spans[].name' payload.json. Find spans longer than 100ms: jq '.resourceSpans[].scopeSpans[].spans[] | select(((.endTimeUnixNano | tonumber) - (.startTimeUnixNano | tonumber)) > 100000000)' payload.json. Find error spans (status.code == 2): jq '.resourceSpans[].scopeSpans[].spans[] | select(.status.code == 2)' payload.json. The tonumber filter converts the nanosecond string to a number for arithmetic. Save collector output to a file with curl http://collector:4318/v1/traces ... > payload.json or redirect SDK console output. For more jq patterns, see jq filter examples.
What is the OTLP/JSON format for metrics?
OTLP/JSON metrics payloads use a resourceMetrics array at the top level. Each metric in scopeMetrics has a name, description, unit, and a data field that is one of: gauge (current value snapshot), sum (cumulative or delta counter), or histogram (distribution with explicit bucket boundaries). A gauge example: {"name":"system.cpu.utilization","unit":"1","gauge":{"dataPoints":[{"timeUnixNano":"1700000000000000000","asDouble":0.42}]}}. Counters use sum with isMonotonic: true and aggregationTemporality: 2 (cumulative). All data point timestamps are nanosecond strings, and all values use the same AnyValue-inspired field naming: asDouble or asInt. Post the payload to http://collector:4318/v1/metrics with Content-Type: application/json.
Ready to inspect your OTLP/JSON payloads?
Paste any OTLP/JSON span, metrics, or logs payload into Jsonic to format, validate, and explore the structure — useful for verifying span fields, attribute types, and traceId encoding before sending to production.
Open JSON FormatterFurther reading and primary sources
- OpenTelemetry Protocol Specification — OTLP/JSON — Official specification for OTLP including JSON encoding rules, endpoint paths, and content-type requirements
- OpenTelemetry Proto definitions (GitHub) — Canonical Protobuf definitions that the OTLP/JSON schema mirrors — trace, metrics, and logs proto files
- OpenTelemetry Semantic Conventions — Standard attribute names (http.method, db.system, messaging.system) for consistent span and metric labeling
- OTel JS — @opentelemetry/exporter-trace-otlp-http — npm package documentation for the Node.js OTLP/JSON HTTP trace exporter
- W3C Trace Context Specification — Standard for the traceparent and tracestate HTTP headers used to propagate traceId and spanId across services