The Span-specific properties that can be queried are listed below.

SpanQueryRequest

message SpanQueryRequest {
  repeated Where filters = 1;
  odddotnet.proto.common.v1.Take take = 2;
  optional odddotnet.proto.common.v1.Duration duration = 3;
}

Where

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;
  }
}

PropertyFilter

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;
  }
}

EventFilter

message EventFilter {
  oneof value {
    odddotnet.proto.common.v1.UInt64Property timeUnixNano = 1;
    odddotnet.proto.common.v1.StringProperty name = 2;
    odddotnet.proto.common.v1.KeyValueProperty attribute = 3;
    odddotnet.proto.common.v1.UInt32Property droppedAttributesCount = 4;
  }
}

LinkFilter

message LinkFilter {
  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.KeyValueProperty attribute = 4;
    odddotnet.proto.common.v1.UInt32Property droppedAttributesCount = 5;
    odddotnet.proto.common.v1.UInt32Property flags = 6;
  }
}

StatusFilter

message StatusFilter {
  reserved 1;

  oneof value {
    odddotnet.proto.common.v1.StringProperty message = 2;
    SpanStatusCodeProperty code = 3;
  }
}

SpanQueryServiceClient

The SpanQueryServiceClient is automatically generated from the .proto files for the SpanQueryService when you build for a client. Each language will handle this slightly different. In C#, the following can be used:

var channel = GrpcChannel.ForAddress(app.GetEndpoint("odddotnet", "grpc")); // If using .NET Aspire, or just a regular Uri
var spanQueryServiceClient = new SpanQueryService.SpanQueryServiceClient(channel);

The client contains two functions: Query, and Reset.

Query

Passing a SpanQueryRequest to the Query endpoint will kick off the request and return once all spans requested have been matched or the duration has timed out.

Reset

In certain scenarios it might be necessary to clear the cache of spans, such as when the traces generated from two separate tests may conflict with each other and the tests must be ran sequentially. You may reset the list of spans between sequential test runs so that each test is working with a blank slate.

See the Query Overview section for more details around how the test harness caches spans.

An Example

The following code, written in C#, shows how to manually build a request and send it to the test harness. Naturally, the syntax in other languages will be slightly different. Additional examples in other languages are on the roadmap and will be provided soon.

This example also assumes you have the OddDotNet test harness spun up and ready to accept telemetry data. For ideas around how to do this, see the Quick Starts.

// ARRANGE
// Look for spans that have the name "GET /healthz"
var nameFilter = new PropertyFilter
{
  Name = new StringProperty
  {
    Compare = "GET /healthz",
    CompareAs = StringCompareAsType.Equals
  }
};

// Look for spans that were generated using the EFCore instrumentation library
var scopeFilter = new InstrumentationScopeFilter
{
  Name = new StringProperty
  {
    Compare = "OpenTelemetry.Instrumentation.EntityFrameworkCore",
    CompareAs = StringCompareAsType.Equals
  }
};

// We want spans that match either of those, so we're going to add them to an OR filter
var orFilter = new OrFilter
{
  Filters = { nameFilter, scopeFilter }
};

// Create the request
var request = new SpanQueryRequest
{
  Take = new Take
  {
    TakeFirst = new TakeFirst() // Take the first span found
  },
  Duration = new Duration
  {
    Milliseconds = 1000 // Wait up to 1 second to find the span
  },
  Filters = { orFilter } // Add our OR filter to it (which contains the two property filters)
};

// ACT
await TriggerWorkflowThatGeneratesSpans();

// ASSERT
var channel = GrpcChannel.ForAddress("http://localhost:4317");
var client = new SpanQueryService.SpanQueryServiceClient(channel);

SpanQueryResponse response = await client.QueryAsync(request);

// Make some assertions on the spans returned
Assert.Contains(response.Spans, span => span.InstrumentationScope.Name == "OpenTelemetry.Instrumentation.EntityFrameworkCore");
Assert.Contains(response.Spans, span => span.Span.Name == "GET /healthz");

SpanQueryRequestBuilder

The above code snippet is rather verbose. Because the SpanQueryRequest is defined as a message in protobuf, we’re limited in the convenience methods available. The SpanQueryRequestBuilder can simplify the process of building queries.

CSharp

The OddDotNet.Client NuGet package can be added to your project to leverage the pre-built gRPC client and the SpanQueryRequestBuilder.

The builder can streamline the generation of requests to the SpanQueryService. the above snippet of code in An Example can be simplified using the builder.

// ARRANGE
var request = new SpanQueryRequestBuilder()
  .TakeFirst() // Take the first span found
  .Wait(TimeSpan.FromSeconds(1)) // Wait up to 1 second to find the span
  .Where(filters => 
  {
    filters.AddOrFilter(orFilters => 
    {
      orFilters
        .AddNameFilter("GET /healthz", StringCompareAsType.Equals)
        .InstrumentationScope.AddNameFilter("OpenTelemetry.Instrumentation.EntityFrameworkCore", StringCompareAsType.Equals);
    })
  })
  .Build();

// ACT
await TriggerWorkflowThatGeneratesSpans();

// ASSERT
var channel = GrpcChannel.ForAddress("http://localhost:4317");
var client = new SpanQueryService.SpanQueryServiceClient(channel);

SpanQueryResponse response = await client.QueryAsync(request);

// Make some assertions on the spans returned
Assert.Contains(response.Spans, span => span.InstrumentationScope.Name == "OpenTelemetry.Instrumentation.EntityFrameworkCore");
Assert.Contains(response.Spans, span => span.Span.Name == "GET /healthz");

Java

Not yet built. On the roadmap.

Go

Not yet built. On the roadmap.

Other languages

Support for other languages is not currently on the roadmap, but it is desired. Check back often to see the progress on your favorite language, or create a Discussion to advocate for the next supported language.

Remember, this is just for the Builder. Any language that supports gRPC and protobuf (and eventually HTTP and JSON) can use the test harness, it just won’t have the convenience of the Builder, so queries will need to be constructed manually, as outlined in An Example above.