Final conclusions and interface specifications only. No decision rationale, no Rust structs. Currently supports 7 backends: VictoriaTraces, Jaeger, Grafana Tempo, OpenSearch, Elasticsearch, Datadog, and SLS.
Implementation status: The
--viewflag is not yet implemented. The current version outputs all fields (equivalent to Full View). The Agent View / Full View differences described below are planned behavior.--fieldsand--truncateare implemented: field projection onspans[]and truncation of retained string values are supported;trace_detailtop-level summary fields are not affected by--fields. Seeoutput-projection.mdfor details.
{
"status": "success",
"metadata": {
"provider": "prod-dd",
"total_count": 3
},
"data": {
"result_type": "spans",
"spans": [
{
"trace_id": "abc123def456789012345678",
"span_id": "def4567890123456",
"parent_span_id": "aabb112233445566",
"name": "HTTP GET /api/users",
"service": "api-gateway",
"kind": "server",
"status": "ok",
"start_time": 1711234800,
"duration_us": 45230,
"attributes": {
"http.method": "GET",
"http.status_code": "200",
"http.url": "https://api.example.com/users"
}
},
{
"trace_id": "abc123def456789012345678",
"span_id": "1234abcd5678ef90",
"parent_span_id": "def4567890123456",
"name": "SELECT * FROM users",
"service": "user-service",
"kind": "client",
"status": "ok",
"start_time": 1711234800,
"duration_us": 12300,
"attributes": {
"db.system": "postgresql",
"db.statement": "SELECT * FROM users WHERE id = ?"
}
},
{
"trace_id": "ff00112233445566aabbccdd",
"span_id": "9988776655443322",
"name": "POST /api/orders",
"service": "order-service",
"kind": "server",
"status": "error",
"start_time": 1711234795,
"duration_us": 1523700,
"attributes": {
"http.method": "POST",
"http.status_code": "500",
"error.message": "Connection refused"
}
}
]
}
}{
"status": "success",
"metadata": {
"provider": "prod-dd",
"total_count": 4
},
"data": {
"result_type": "trace_detail",
"trace_id": "abc123def456789012345678",
"span_count": 4,
"service_count": 3,
"duration_us": 245500,
"services": ["api-gateway", "user-service", "postgres"],
"spans": [
{
"trace_id": "abc123def456789012345678",
"span_id": "root001aabbccddee",
"name": "HTTP GET /api/users",
"service": "api-gateway",
"kind": "server",
"status": "ok",
"start_time": 1711234800,
"duration_us": 245500,
"attributes": {
"http.method": "GET",
"http.status_code": "200"
}
},
{
"trace_id": "abc123def456789012345678",
"span_id": "child002ffeeddcc",
"parent_span_id": "root001aabbccddee",
"name": "user.getById",
"service": "user-service",
"kind": "internal",
"status": "ok",
"start_time": 1711234800,
"duration_us": 50200,
"attributes": {}
},
{
"trace_id": "abc123def456789012345678",
"span_id": "child003aabb1122",
"parent_span_id": "child002ffeeddcc",
"name": "SELECT * FROM users WHERE id = ?",
"service": "user-service",
"kind": "client",
"status": "ok",
"start_time": 1711234800,
"duration_us": 12300,
"attributes": {
"db.system": "postgresql"
}
},
{
"trace_id": "abc123def456789012345678",
"span_id": "child004ccdd3344",
"parent_span_id": "root001aabbccddee",
"name": "redis.GET user:123",
"service": "api-gateway",
"kind": "client",
"status": "ok",
"start_time": 1711234800,
"duration_us": 2100,
"attributes": {
"db.system": "redis"
}
}
]
}
}
trace_detailhas additional top-level summary fields compared tospans:trace_id,span_count,service_count,duration_us,services. These fields let agents get a trace overview without iterating through all spans.
{
"status": "success",
"metadata": {
"provider": "prod-dd",
"provider_type": "datadog",
"query_language": "Span Search Syntax",
"query": "service:api-gateway @http.status_code:500",
"time_range": {"start": 1711231200, "end": 1711234800},
"total_count": 3,
"is_complete": true,
"cursor": "eyJhZnRlciI6IkFRQUFBR..."
},
"data": {
"result_type": "spans",
"spans": [
{
"trace_id": "abc123def456789012345678",
"span_id": "def4567890123456",
"parent_span_id": "aabb112233445566",
"name": "HTTP GET /api/users",
"service": "api-gateway",
"kind": "server",
"status": "ok",
"start_time": 1711234800,
"duration_us": 45230,
"attributes": {
"http.method": "GET",
"http.status_code": "200",
"http.url": "https://api.example.com/users"
},
"events": [],
"resource": {
"hostname": "web01",
"env": "prod",
"version": "1.2.3"
},
"extensions": {
"datadog.type": "web",
"datadog.meta": {
"runtime-id": "abc-123"
}
}
}
]
}
}Full View adds the following compared to Agent View:
metadata.provider_type/query_language/query/time_range/is_complete/cursor, and per-spanevents/resource/extensions.
trace search:
TRACE_ID SERVICE NAME STATUS DURATION_MS START_TIME
abc123def456789012345678 api-gateway HTTP GET /api/users ok 45.23 2024-03-24 10:00:00
abc123def456789012345678 user-service SELECT * FROM users ok 12.30 2024-03-24 10:00:00
ff00112233445566aabbccdd order-service POST /api/orders error 1523.70 2024-03-24 09:59:55
trace get:
Trace: abc123def456789012345678 | 4 spans | 3 services | 245.5 ms
SPAN_ID PARENT SERVICE NAME STATUS DURATION_MS
root001aabbcc… — api-gateway HTTP GET /api/users ok 245.50
child002ffeedd… root001aabbcc… user-service user.getById ok 50.20
child003aabb11… child002ffeedd… user-service SELECT * FROM users WHERE… ok 12.30
child004ccdd33… root001aabbcc… api-gateway redis.GET user:123 ok 2.10
In table mode, trace_id and span_id are truncated, and attributes are not shown.
| Field | Type | Agent View | Full View | Description |
|---|---|---|---|---|
trace_id |
string | required | required | Hex format |
span_id |
string | required | required | Hex format |
parent_span_id |
string? | present when set | present when set | Omitted for root spans |
name |
string | required | required | Operation name (Jaeger operationName, DD operation_name) |
service |
string | required | required | Service name |
kind |
string? | present when set | present when set | Normalized SpanKind (see §3.1) |
status |
string | required | required | ok / error / unset |
start_time |
int64 | required | required | Unix seconds |
duration_us |
i64 | required | required | Duration in microseconds |
attributes |
{key: string} |
present when non-empty | always present | Flattened to strings (consistent with log) |
events |
[SpanEvent]? |
omitted | present when set | Span Events (see §2.2) |
resource |
{key: string}? |
omitted | always present | Resource attributes (hostname, env, etc.) |
extensions |
{key: any}? |
omitted | always present | Provider-specific information |
| Field | Type | Description |
|---|---|---|
name |
string | Event name |
timestamp |
int64 | Unix seconds |
attributes |
{key: string}? |
Event attributes |
Datadog does not support span events. Jaeger/Tempo span events are supported.
When result_type: "trace_detail", data contains these summary fields in addition to spans:
| Field | Type | Description |
|---|---|---|
trace_id |
string | The queried traceID |
span_count |
int | Total span count for this trace |
service_count |
int | Number of services involved |
duration_us |
i64 | End-to-end duration (root span, or earliest start to latest end) |
services |
[string] |
List of services involved (deduplicated, alphabetical) |
These summary fields are computed by the obz CLI and don't depend on the backend API. Agents can use these fields directly for decision-making without iterating through the spans array.
| Field | Length | Format | Description |
|---|---|---|---|
trace_id |
16 or 32 hex chars | lowercase hex | OTel 128-bit = 32 chars; Jaeger/Zipkin compatible 64-bit = 16 chars |
span_id |
16 hex chars | lowercase hex | 64-bit |
Datadog: trace_id is hex from the API; span_id/parent_id are decimal strings, passed through as-is by design. No decimal-to-hex conversion is performed because parent-child relationships are resolved within the same provider using the original format, and obz does not perform cross-provider span joins.
Unified to the 5 OTel-defined SpanKind values (lowercase). Values that can't be normalized are preserved as-is:
| Normalized Output | Description | Datadog Mapping |
|---|---|---|
client |
Initiator of a synchronous remote call | custom.span.kind = "client" |
server |
Receiver of a synchronous remote call | custom.span.kind = "server" |
producer |
Sender of an asynchronous message | custom.span.kind = "producer" |
consumer |
Receiver of an asynchronous message | custom.span.kind = "consumer" |
internal |
In-process operation | custom.span.kind = "internal" |
Datadog: kind is extracted from
custom.span.kindin the span's custom attributes (OTel-instrumented spans). The Datadogtypefield (web/http/db/cache/custom) is preserved inextensions["datadog.type"]but not used for kind normalization. Spans withoutcustom.span.kindhavekind: null. VictoriaTraces (Jaeger API) kinds are already OTel standard values and map directly. SLS trace extracts kind from log fields.
Unified to 3 statuses (lowercase), aligned with OTel StatusCode:
| Normalized Output | Description | Datadog Mapping |
|---|---|---|
ok |
Normal | attrs.status == "ok" |
error |
Error | attrs.status == "error" |
unset |
Not set | anything else or missing |
Jaeger/Zipkin backends determine status via the
errortag; Tempo/OTel hasstatus.codedirectly.
Consistent with log-model.md §3.4:
- Primitive types (string/number/bool) → converted to string
- Nested objects/arrays → serialized as JSON string
- Full nested structures are preserved in
extensions(Full View)
| Backend | Source |
|---|---|
| Datadog | attributes.service |
| VT / Jaeger | processes.{processID}.serviceName |
| Tempo | OTel resource.service.name |
| OpenSearch | resource.service.name (camelCase structure) |
| Elasticsearch | resource.attributes.service.name (nested structure) |
| SLS | Heuristically extracted from attributes |
| Backend | Source |
|---|---|
| Datadog | attributes.operation_name |
| VT / Jaeger | operationName |
| Tempo | OTel name |
| OpenSearch | name |
| Elasticsearch | name |
| SLS | Heuristically extracted from attributes |
VictoriaTraces and Jaeger share the jaegerapi module; conversion rules are identical. The only differences are API path prefixes and some request parameters.
| Jaeger Field | obz Field | Conversion |
|---|---|---|
traceID |
trace_id |
Direct mapping (already hex) |
spanID |
span_id |
Direct mapping |
references[0].spanID (CHILD_OF) |
parent_span_id |
Direct mapping; no reference → omitted |
operationName |
name |
Direct mapping |
processes.{pID}.serviceName |
service |
Resolved via processID |
tags["span.kind"] |
kind |
Direct mapping (already OTel standard values) |
tags["error"]=true |
status |
true→"error", otherwise→"ok" |
startTime |
start_time |
Microseconds → Unix seconds (÷ 1,000,000) |
duration |
duration_us |
Direct mapping (Jaeger native microseconds) |
tags |
attributes |
Flat key=value mapping, excluding system tags |
VictoriaTraces and Jaeger both support two extension commands:
trace services: VT usesGET /select/jaeger/api/services, Jaeger usesGET /api/servicestrace operations --service <name>: VT usesGET /select/jaeger/api/services/{service}/operations, Jaeger usesGET /api/services/{service}/operations
VT vs Jaeger differences:
- VT API path prefix is
/select/jaeger/api/, Jaeger uses/api/ - VT
trace getpassesstart/endparameters, Jaeger does not - Jaeger
trace searchrequires-qto be a service name (mandatory, cannot be empty)
| Datadog Field | obz Field | Conversion |
|---|---|---|
attributes.trace_id |
trace_id |
Direct mapping (hex string from API) |
attributes.span_id |
span_id |
Direct mapping (decimal string, passed through as-is) |
attributes.parent_id |
parent_span_id |
Direct mapping; "0" → omitted |
attributes.operation_name |
name |
Direct mapping |
attributes.service |
service |
Direct mapping |
custom.span.kind |
kind |
OTel SpanKind value from custom attributes (§3.1) |
attributes.start_timestamp |
start_time |
ISO 8601 → Unix seconds |
custom.duration or timestamps |
duration_us |
custom.duration / 1000 (ns→us), or (end_ms - start_ms) * 1000 (§5.3) |
attributes.status |
status |
"error"→error, "ok"→ok, else→unset |
custom.* |
attributes |
Flattened to {key: string} |
| — | events |
Datadog has no events, Full View shows [] |
Provider-specific information preserved in Full View:
| Field | Description |
|---|---|
datadog.type |
Datadog original span type (web/http/db/cache/custom) |
datadog.resource_name |
Datadog resource name (e.g. "GET /api/users") |
datadog.env |
Datadog environment (e.g. "prod", "staging") |
Note: In the current implementation, only
datadog.type,datadog.resource_name, anddatadog.envare populated.datadog.metaanddatadog.metricsare reserved keys for future use.
Datadog has no dedicated "fetch full trace by traceID" API. The obz trace get <trace_id> command is implemented as:
POST /api/v2/spans/events/search
{
"data": {
"type": "search_request",
"attributes": {
"filter": {
"query": "trace_id:<trace_id>",
"from": "now-15m",
"to": "now"
},
"sort": "timestamp",
"page": {"limit": 1000}
}
}
}
The obz CLI then computes the trace_detail summary fields (span_count, service_count, duration_us, services).
Note:
trace getdefaults to searching spans within the last 15 minutes. For a wider time range, use--from/--to.
Tempo returns OTLP protobuf-JSON format (OtlpTraceResponse), which obz converts to the unified Span model:
| Tempo (OTLP) Field | obz Field | Conversion |
|---|---|---|
resourceSpans[].resource.attributes[].key/value |
resource |
Flattened key-value array |
resourceSpans[].resource.attributes["service.name"] |
service |
Extracted from resource |
scopeSpans[].spans[].traceId |
trace_id |
base64 or hex → standard hex |
scopeSpans[].spans[].spanId |
span_id |
base64 or hex → standard hex |
scopeSpans[].spans[].parentSpanId |
parent_span_id |
Same as above; empty or all-zeros → omitted |
scopeSpans[].spans[].name |
name |
Direct mapping |
scopeSpans[].spans[].kind |
kind |
OTel SpanKind enum → standard value |
scopeSpans[].spans[].status.code |
status |
STATUS_CODE_OK→ok, STATUS_CODE_ERROR→error, others→unset |
scopeSpans[].spans[].startTimeUnixNano |
start_time |
Nanoseconds → Unix seconds |
scopeSpans[].spans[].endTimeUnixNano - startTimeUnixNano |
duration_us |
Nanosecond difference → microseconds |
scopeSpans[].spans[].attributes[] |
attributes |
Flattened key-value array |
scopeSpans[].spans[].events[] |
events |
Mapped to SpanEvent |
Tempo also supports two extension commands:
trace tags:GET /api/v2/search/tags, returns tag names grouped by scopetrace tag-values --tag <name>:GET /api/v2/search/tag/{tag}/values
OpenSearch uses the OTel data model (camelCase field names):
| OpenSearch Field | obz Field | Conversion |
|---|---|---|
_source.traceId |
trace_id |
Direct mapping (camelCase) |
_source.spanId |
span_id |
Direct mapping |
_source.parentSpanId |
parent_span_id |
Direct mapping |
_source.name |
name |
Direct mapping |
_source.resource.service.name |
service |
Extracted from resource |
_source.kind |
kind |
Normalized |
_source.status.code |
status |
Normalized |
_source.startTime |
start_time |
ISO 8601 → Unix seconds |
_source.endTime - _source.startTime |
duration_us |
Time difference → microseconds |
_source.attributes.* |
attributes |
Flat mapping |
_source.resource.* |
resource |
Flat mapping |
Elasticsearch also uses the OTel data model, but with snake_case fields:
| Elasticsearch Field | obz Field | Conversion |
|---|---|---|
_source.trace_id |
trace_id |
snake_case |
_source.span_id |
span_id |
snake_case |
_source.parent_span_id |
parent_span_id |
snake_case |
_source.name |
name |
Direct mapping |
_source.resource.attributes.service.name |
service |
Nested under resource.attributes |
_source.kind |
kind |
Normalized |
_source.status.code |
status |
Unset represented as {} (empty object) |
_source.@timestamp |
start_time |
epoch_millis → Unix seconds |
_source.duration |
duration_us |
Nanoseconds → microseconds (÷ 1000); explicit field, not computed |
_source.attributes.* |
attributes |
Flat mapping |
_source.resource.attributes.* |
resource |
Nested under resource.attributes |
SLS uses the GetLogs API; span data is stored in a logstore:
| SLS Field | obz Field | Conversion |
|---|---|---|
traceID / trace_id |
trace_id |
Extracted from attributes |
spanID / span_id |
span_id |
Extracted from attributes |
parentSpanID / parent_span_id |
parent_span_id |
Extracted from attributes |
| Heuristic name field | name |
Heuristically extracted |
| Heuristic service field | service |
Heuristically extracted |
__time__ |
start_time |
string → i64 |
duration |
duration_us |
Extracted from attributes and converted |
| All non-system fields | attributes |
Flat mapping |
trace get is implemented by querying traceID:{trace_id}, returning up to 1000 spans.
SLS progress retry follows the same mechanism as log search (up to 10 retries with exponential backoff).
Datadog trace_id is a hex string from the API. span_id and parent_id are decimal strings, passed through as-is by design. Converting to hex would break parent-child span matching (both sides must use the same format), and obz does not perform cross-provider span joins where format normalization would matter.
Datadog root spans have parent_id set to "0". In obz, the parent_span_id field is omitted (no "parent_span_id": "0000000000000000" in the output).
Datadog duration is computed with two fallback paths:
-
If
custom.duration(nanoseconds) is present in the span's custom attributes:duration_us = custom.duration / 1_000 -
Otherwise, from ISO 8601 timestamps:
duration_us = (end_timestamp_ms - start_timestamp_ms) * 1_000
The first path is preferred because custom.duration provides nanosecond precision from OTel-instrumented spans.
| Source | Original Format | Conversion |
|---|---|---|
| Datadog | ISO 8601 string | parse → Unix seconds |
| VT / Jaeger | Microsecond int64 | ÷ 1,000,000 → Unix seconds |
| Tempo / OTel | Nanosecond int64 | ÷ 1,000,000,000 → Unix seconds |
| OpenSearch | ISO 8601 string | parse → Unix seconds |
| Elasticsearch | epoch_millis | ÷ 1,000 → Unix seconds |
| SLS | Second string | parse i64 |
Consistent with metric/log,
start_timeis always Unix seconds. Sub-second precision is preserved induration_usas an integer in microseconds.
| Platform | Max Per Request | Pagination Method |
|---|---|---|
| Datadog | page.limit max 1000 |
cursor-based (meta.page.after), CLI auto-paginates |
| VT / Jaeger | limit parameter (default 20) |
Single request returns full trace |
| Tempo | limit parameter |
Single request returns |
| OpenSearch | size parameter (trace get max 1000) |
size/from |
| Elasticsearch | size parameter (trace get max 1000) |
size/from |
| SLS | line parameter (trace get max 1000) |
offset + line, progress retry |
The CLI's --limit controls the final number of spans returned to the user; the adapter handles multiple internal requests automatically.
The is_complete semantics for trace search are the same as for log search (see log-model.md §5.6). Type is Option<bool> with three-state semantics: true (complete), false (incomplete), omitted (backend provides no signal).
Per-platform behavior:
| Platform | is_complete Source |
Description |
|---|---|---|
| ES / OpenSearch | hits.total.value + hits.total.relation |
Same as log search |
| SLS | x-log-progress response header |
Progress signal is no longer discarded |
| Datadog | meta.page.after cursor |
No cursor → true, has cursor → false |
| VT / Jaeger / Tempo | omitted | Backend does not provide a truncation indicator |
Spans in trace_detail are sorted by start_time ascending (ties broken by duration_us descending), ensuring the call chain reads top-to-bottom.
| result_type | Command | Data fields in data |
|---|---|---|
spans |
trace search |
data.spans: [Span, ...] |
trace_detail |
trace get |
data.trace_id, data.span_count, data.service_count, data.duration_us, data.services, data.spans: [Span, ...] |