Before you dive in to the “traces and spans” concepts outlined below, review these concepts in the official OpenTelemetry documentation, found here.

The ExportTraceServiceRequest

All OpenTelemetry collectors receive OTLP traces from applications in the form of an ExportTraceServiceRequest. This is a well known message, and its structure can be found in the official opentelemetry-proto repository. The diagram below provides the general structure of this message (some properties omitted for brevity).

ExportTraceServiceRequest diagram

When the OpenTelemetry Collector exports traces to an endpoint, it typically batches those traces up and sends them by resource (eg. a microservice), hence the list of resources in an ExportTraceServiceRequest. Each InstrumentationScopeSpan will be associated with one resource, and the same goes for the Spans that are associated with an instrumentation scope.

For each Span, then, you know what instrumentation library was used to create the Span, and you also know which Resource produced the Span.

What are Traces and Spans?

A trace is really just an ID attached to each individual telemetry signal that your application is producing that ties a request together. Think of it like the execution of a workflow, where the trace ID of the workflow is added to each step.

Spans represent the individual steps within the workflow. They contain numerous properties, such as when they started and ended, whether or not they completed successfully, and whether they belong to a parent. The full structure of the span can again be found in the official opentelemetry-proto repository.

In total, including the instrumentation scope properties and the resource properties, there are 25 properties associated with every span. You can find OddDotNet’s span definition here and in the PropertyFilter. Here’s a code snippet for reference:

message Where {
  oneof value {
    PropertyFilter property = 1;
    OrFilter or = 2;
    odddotnet.proto.common.v1.InstrumentationScopeFilter instrumentationScope = 3;
    odddotnet.proto.resource.v1.ResourceFilter resource = 4;
    odddotnet.proto.common.v1.StringProperty instrumentationScopeSchemaUrl = 5;
    odddotnet.proto.common.v1.StringProperty ResourceSchemaUrl = 6;
  }
}

message PropertyFilter {
  oneof value {
    odddotnet.proto.common.v1.ByteStringProperty traceId = 1;
    odddotnet.proto.common.v1.ByteStringProperty spanId = 2;
    odddotnet.proto.common.v1.StringProperty traceState = 3;
    odddotnet.proto.common.v1.ByteStringProperty parentSpanId = 4;
    odddotnet.proto.common.v1.StringProperty name = 5;
    SpanKindProperty kind = 6;
    odddotnet.proto.common.v1.UInt64Property startTimeUnixNano = 7;
    odddotnet.proto.common.v1.UInt64Property endTimeUnixNano = 8;
    odddotnet.proto.common.v1.KeyValueProperty attribute = 9;
    odddotnet.proto.common.v1.UInt32Property droppedAttributesCount = 10;
    EventFilter event = 11;
    odddotnet.proto.common.v1.UInt32Property droppedEventsCount = 12;
    LinkFilter link = 13;
    odddotnet.proto.common.v1.UInt32Property droppedLinksCount = 14;
    StatusFilter status = 15;
    odddotnet.proto.common.v1.UInt32Property flags = 16;
  }
}