Menu

Important: This documentation is about an older version. It's relevant only to the release noted, many of the features and functions have been updated or replaced. Please view the current version.

Open source

Apache Parquet schema

Tempo 2.0 uses Apache Parquet as the default column-formatted block format. Refer to the Parquet configuration options for more information.

This document describes the schema used with the Parquet block format.

Fully nested versus span-oriented schema

There are two overall approaches to a columnar schema: fully nested or span-oriented. Span-oriented means a flattened schema where traces are destructured into rows of spans. A fully nested schema means the current trace structures such as Resource/InstrumentationLibrary/Spans/Events are preserved (nested data is natively supported in Parquet). In both cases, individual leaf values such as span name and duration are individual columns.

We chose the nested schema for several reasons:

  • The block size is much smaller for the nested schema. This is due to the high data duplication incurred when flattening resource-level attributes such as service.name to each individual span.
  • A flat schema is not truly “flat” because each span still contains nested data such as attributes and events.
  • Nested schema is much faster to search for resource-level attributes because the resource-level columns are very small (1 row for each batch).
  • Translation to and from the OpenTelemetry Protocol Specification (OTLP) is straightforward.
  • Easily add computed columns (for example, trace duration) at multiple levels such as per-trace, per-batch, etc.

Static vs dynamic columns

Dynamic vs static columns add another layer to the schema. A dynamic schema stores each attribute such as service.name and http.status_code as its own column and the columns in each parquet file can be different. A static schema is unresponsive to the shape of the data, and all attributes are stored in generic key/value containers.

The dynamic schema is the ultimate dream for a columnar format but it is too complex for a first release. However, the benefits of that approach are also too good to pass up, so we propose a hybrid approach. It is primarily a static schema but with some dynamic columns extracted from trace data based on some heuristics of frequently queried attributes. We plan to continue investing in this direction to implement a fully dynamic schema where trace attributes are blown out into independent Parquet columns at runtime.

For more information, refer to the Parquet design document.

Schema details

The adopted Parquet schema is mostly a direct translation of OTLP but with some key differences.

The table below uses these abbreviations:

  • rs = resource spans
  • ils - InstrumentLibrarySpans
NameTypeDescription
TraceIDbyte arrayThe trace ID in 16-byte binary form.
TraceIDTextstringThe trace ID in hexadecimal text form.
StartTimeUnixNanoint64Start time of the first span in the trace, in nanoseconds since unix epoch.
EndTimeUnixNanoint64End time of the last span in the trace, in nanoseconds since unix epoch.
DurationNanosint64Total trace duration in nanoseconds, computed as difference between EndTimeUnixNano and StartTimeUnixNano.
RootServiceNamestringThe resource-level service.name attribute (rs.Resource.ServiceName) from the root span of the trace if one exists, else null.
RootSpanNamestringThe name (rs.ils.Spans.Name) of the root span if one exists, else null.
rsShort-hand for “ResourceSpans”
rs.Resource.ServiceNamestringA dedicated column for the resource-level service.name attribute if present. https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/#service
rs.Resource.ClusterstringA dedicated column for the resource-level cluster attribute if present and of string type. Values of other types will be stored in the generic attribute columns.
rs.Resource.NamespacestringA dedicated column for the resource-level namespace attribute if present and of string type. Values of other types will be stored in the generic attribute columns.
rs.Resource.PodstringA dedicated column for the resource-level pod attribute if present and of string type. Values of other types will be stored in the generic attribute columns.
rs.Resource.ContainerstringA dedicated column for the resource-level container attribute if present and of string type. Values of other types will be stored in the generic attribute columns.
rs.Resource.K8sClusterNameA dedicated column for the resource-level k8s.cluster.name attribute if present and of string type. Values of other types will be stored in the generic attribute columns. https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/k8s/#cluster
rs.Resource.K8sNamespaceNamestringA dedicated column for the resource-level k8s.namespace.name attribute if present and of string type. Values of other types will be stored in the generic attribute columns. https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/k8s/#namespace
rs.Resource.K8sPodNamestringA dedicated column for the resource-level k8s.pod.name attribute if present and of string type. Values of other types will be stored in the generic attribute columns. https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/k8s/#pod
rs.Resource.K8sContainerNamestringA dedicated column for the resource-level k8s.container.name attribute if present and of string type. Values of other types will be stored in the generic attribute columns. https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/k8s/#container
rs.Resource.Attrs.KeystringAll resource attributes that do not have a dedicated column are stored as a key value pair in these columns. The Key column stores the name, and then one of the Value columns is populated according to the attribute’s data type. The other value columns will contain null.
rs.Resource.Attrs.ValuestringThe attribute value if string type, else null.
rs.Resource.Attrs.ValueIntintThe attribute value if integer type, else null.
rs.Resource.Attrs.ValueDoublefloatThe attribute value if float type, else null.
rs.Resource.Attrs.ValueBoolboolThe attribute value if boolean type, else null.
rs.Resource.Attrs.ValueArraybyte arrayThe attribute value if nested array type, else null. Protocol buffer encoded binary data.
rs.Resource.Attrs.ValueKVListbyte arrayThe attribute value if nested key/value map type, else null. Protocol buffer encoded binary data.
rs.ilsShorthand for “ResourceSpans.InstrumentationLibrarySpans”
rs.ils.ilShorthand for ResourceSpans.InstrumentationLibrarySpans.InstrumentationLibrary
rs.ils.il.NamestringInstrumentationLibrary name if present, else empty string. https://opentelemetry.io/docs/reference/specification/glossary/#instrumentation-library
rs.ils.il.VersionstringThe InstrumentationLibrary version if present, else empty string. https://opentelemetry.io/docs/reference/specification/glossary/#instrumentation-library
rs.ils.Spans.IDbyte arraySpan unique ID
rs.ils.Spans.NamestringSpan name
rs.ils.Spans.ParentSpanIDbyte arrayThe unique ID of the span’s parent. For root spans without a parent this is null.
rs.ils.Spans.StartUnixNanosint64Start time the span in nanoseconds since unix epoch.
rs.ils.Spans.EndUnixNanosint64End time the span in nanoseconds since unix epoch.
rs.ils.Spans.KindintThe span’s kind. Defined values: 0. Unset; 1. Internal; 2. Server; 3. Client; 4. Producer; 5. Consumer; https://opentelemetry.io/docs/reference/specification/trace/api/#spankind
rs.ils.Spans.StatusCodeintThe span status. Defined values: 0: Unset; 1: OK; 2: Error. https://opentelemetry.io/docs/reference/specification/trace/api/#set-status
rs.ils.Spans.StatusMessagestringOptional message to accompany Error status.
rs.ils.Spans.HttpMethodstringA dedicated column for the span-level http.method attribute if present and of string type, else null. Values of other types will be stored in the generic attribute columns. https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#common-attributes
rs.ils.Spans.HttpStatusCodeintA dedicated column for the span-level http.status_code attribute if present and of integer type, else null. Values of other types will be stored in the generic attribute columns. https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#common-attributes
rs.ils.Spans.HttpUrlstringA dedicated column for the span-level http.url attribute if present and of string type, else null. Values of other types will be stored in the generic attribute columns. https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#http-client
rs.ils.Spans.DroppedAttributesCountintNumber of attributes that were dropped
rs.ils.Spans.Attrs.KeystringAll span attributes that do not have a dedicated column are stored as a key value pair in these columns. The Key column stores the name, and then one of the Value columns is populated according to the attribute’s data type. The other value columns will contain null.
rs.ils.Spans.Attrs.ValuestringThe attribute value if string type, else null.
rs.ils.Spans.Attrs.ValueIntintThe attribute value if integer type, else null.
rs.ils.Spans.Attrs.ValueDoublefloatThe attribute value if float type, else null.
rs.ils.Spans.Attrs.ValueBoolboolThe attribute value if boolean type, else null.
rs.ils.Spans.Attrs.ValueArraybyte arrayThe attribute value if nested array type, else null. Protocol buffer encoded binary data.
rs.ils.Spans.Attrs.ValueKVListbyte arrayThe attribute value if nested key/value map type, else null. Protocol buffer encoded binary data.
rs.ils.Spans.DroppedEventsCountintThe number of events that were dropped
rs.ils.Spans.Events.TimeUnixNanoint64The timestamp of the event, as nanoseconds since unix epoch.
rs.ils.Spans.Events.NamestringThe event name or message.
rs.ils.Spans.Events.DroppedAttributesCountintThe number of event attributes that were dropped.
rs.ils.Spans.Events.Attrs.KeystringAll event attributes are stored as a key value pair in these columns. The Key column stores the name.
rs.ils.Spans.Events.Attrs.Valuebyte arrayThe attribute value, Protocol buffer encoded binary data.
rs.ils.Spans.DroppedLinksCountintThe number of links that were dropped.
rs.ils.Spans.Linksbyte arrayProtocol-buffer encoded span links if present, else null.
rs.ils.Spans.TraceStatestringThe span’s TraceState value if present, else empty string.https://opentelemetry.io/docs/reference/specification/trace/api/#tracestate

Block Schema display in Parquet Message format

yaml
message Trace {
  required binary TraceID;
  required binary TraceIDText (STRING);
  required int64 StartTimeUnixNano (INTEGER(64,false));
  required int64 EndTimeUnixNano (INTEGER(64,false));
  required int64 DurationNanos (INTEGER(64,false));
  required binary RootServiceName (STRING);
  required binary RootSpanName (STRING);

  repeated group rs { // Resource spans
    required group Resource {
      repeated group Attrs {
        required binary Key (STRING);
        optional binary Value (STRING);
        optional int64 ValueInt (INTEGER(64,true));
        optional double ValueDouble;
        optional boolean ValueBool;
        optional binary ValueKVList (STRING);
        optional binary ValueArray (STRING);
      }

      required binary ServiceName (STRING);
      optional binary Cluster (STRING);
      optional binary Namespace (STRING);
      optional binary Pod (STRING);
      optional binary Container (STRING);
      optional binary K8sClusterName (STRING);
      optional binary K8sNamespaceName (STRING);
      optional binary K8sPodName (STRING);
      optional binary K8sContainerName (STRING);
      optional binary Test (STRING);
    }
    repeated group ils { // InstrumentationLibrarySpans
      required group il { // InstrumentationLibrary
        required binary Name (STRING);
        required binary Version (STRING);
      }
      repeated group Spans {
        required binary ID;
        required binary Name (STRING);
        required int64 Kind (INTEGER(64,true));
        required binary ParentSpanID;
        required binary TraceState (STRING);
        required int64 StartUnixNanos (INTEGER(64,false));
        required int64 EndUnixNanos (INTEGER(64,false));
        required int64 StatusCode (INTEGER(64,true));
        required binary StatusMessage (STRING);
        repeated group Attrs {
          required binary Key (STRING);
          optional binary Value (STRING);
          optional int64 ValueInt (INTEGER(64,true));
          optional double ValueDouble;
          optional boolean ValueBool;
          optional binary ValueKVList (STRING);
          optional binary ValueArray (STRING);
        }
        required int32 DroppedAttributesCount (INTEGER(32,true));
        repeated group Events {
          required int64 TimeUnixNano (INTEGER(64,false));
          required binary Name (STRING);
          repeated group Attrs {
            required binary Key (STRING);
            required binary Value;
          }
          required int32 DroppedAttributesCount (INTEGER(32,true));
          optional binary Test (STRING);
        }
        required int32 DroppedEventsCount (INTEGER(32,true));
        required binary Links;
        required int32 DroppedLinksCount (INTEGER(32,true));

        optional binary HttpMethod (STRING);
        optional binary HttpUrl (STRING);
        optional int64 HttpStatusCode (INTEGER(64,true));
      }
    }
  }
}

Trace-level attributes

For speed and ease-of-use, we are projecting several values to columns at the trace-level:

  • Trace ID - Don’t store on each span.
  • Root service/span names/StartTimeUnixNano - These are selected properties of the root span in each trace (if there is one). These are used for displaying results in the Grafana UI. These properties are computed at ingest time and stored once for efficiency, so we don’t have to find the root span.
  • DurationNanos - The total trace duration, computed at ingest time. This powers the min/max duration filtering in the current Tempo search and is more efficient than scanning the spans duration column. However, it may go away with TraceQL or we could decide to change it to span-level duration filtering too.

Dedicated columns

Projecting attributes to their own columns has benefits for speed and size. Therefore we are taking an opinionated approach and projecting some common attributes to their own columns. All other attributes are stored in the generic key/value maps and are still searchable, but not as quickly. We chose these attributes based on what we commonly use ourselves (scratching our own itch), but we think they will be useful to most workloads.

Resource-level attributes include the following:

  • service.name
  • cluster and k8s.cluster.name
  • namespace and k8s.namespace.name
  • pod and k8s.pod.name
  • container and k8s.container.name

Span-level attributes include the following:

  • http.method
  • http.url
  • http.status_code (int)

“Any”-type Attributes

OTLP attributes have variable data types, which is easy to accomplish in formats like protocol-buffers, but does not translate directly to Parquet. Each column must have a concrete type. There are several possibilities here but we chose to have optional values for each concrete type. Array and KeyValueList types are stored as protocol-buffer-encoded byte arrays.

yaml
repeated group Attrs {
    required binary  Key (STRING);

    # Only one of these will be set
    optional binary  Value (STRING);
    optional boolean ValueBool;
    optional double  ValueDouble;
    optional int64   ValueInt (INT(64,true));
    optional binary  ValueArray (STRING);
    optional binary  ValueKVList (STRING);
}

Event attributes

Event attributes are stored as protocol-buffer encoded.

yaml
repeated group Attrs {
    required binary Key (STRING);
    required binary Value (STRING);
}

Compression and encoding

Parquet has robust support for many compression algorithms and data encodings. We have found excellent combinations of storage size and performance with the following:

  1. Snappy Compression - Enable on all columns
  2. Dictionary encoding - Enable on all string columns (including byte array ParentSpanID). Most strings are very repetitive so this works well to optimize storage size. However we can greatly speed up search by inspecting the dictionary first and eliminating pages with no matches.
  3. Time and duration unix nanos - Delta encoding
  4. Rarely used columns such as DroppedAttributesCount - These columns are usually all zeroes, RLE works well.

Bloom filters

Parquet has native support for bloom filters. However, Tempo does not use them at this time. Tempo already has sophisticated support for sharding and caching bloom filters.