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 Formatter

OTLP/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.

FieldTypeExampleNotes
traceIdstring (32 hex chars)"4bf92f3577b34da6a3ce929d0e0e4736"128-bit, base16 — NOT base64
spanIdstring (16 hex chars)"00f067aa0ba902b7"64-bit, base16 — NOT base64
parentSpanIdstring (16 hex chars)"b9c7c989f97918e1"Omit or set to "" for root spans
namestring"POST /checkout"Low-cardinality operation name; avoid user IDs in name
kindinteger (enum)21=INTERNAL, 2=SERVER, 3=CLIENT, 4=PRODUCER, 5=CONSUMER
startTimeUnixNanostring (decimal int)"1700000000000000000"Nanoseconds since Unix epoch; string to avoid JS precision loss
endTimeUnixNanostring (decimal int)"1700000000250000000"Same format as start; duration = end - start
status.codeinteger (enum)10=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.json

To 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) and parentSpanId (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 parentSpanId to 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, or kvlistValue. 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, and deployment.environment.
ScopeSpans
A grouping of spans within a ResourceSpans element, identified by the instrumentation scope (library name and version) that produced the spans. For example, HTTP spans from @opentelemetry/instrumentation-http and database spans from @opentelemetry/instrumentation-pg appear in separate scopeSpans entries 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 Formatter

Further reading and primary sources