Queries
Once the OddDotNet OpenTelemetry Test Harness has received an Export{SIGNAL}ServiceRequest
from your application (where SIGNAL
is either Trace
- see note, Metric
, or Log
), the next step
is to query the harness to confirm that the correct signals have been received.
NOTE: OddDotNet uses Trace
and Span
interchangeably under most circumstances. However,
since the return type of SpanQueryService
is a Span
and not a full Trace
, Span
is used
instead. The name of the request to ingest a trace/span is ExportTraceServiceRequest
, and the
name of the service used to query an individual span is SpanQueryService
. Under most
circumstances, you will not see the Export{SIGNAL}ServiceRequest
, so the SIGNAL
values for
{SIGNAL}QueryService
are Span
, Metric
, and Log
.
Signal Caching
All signals are cached by OddDotNet for 30 seconds. A background service is responsible for cleaning up “expired” signals on a 1-second interval.
The purpose of signal caching is to allow for querying of signals that may have already been received by OddDotNet before the query begins. For example:
// generate some telemetry data as part of a request
await SomeWorkflowThatGeneratesSignals();
// Signals are generated asynchronously, but some time may pass before the query begins
await Task.Delay(TimeSpan.FromSeconds(1));
// Now query for the signal
var response = await client.QueryAsync(request);
Without signal caching, the above code would miss any signals that came in during the 1 second delay between generating the signals and querying for those signals.
When a query is made, the previous 30 seconds of signals are checked in cache first to see if any
matches are found. If they are, they are included in the results. The query then continues to
check more signals as they come in, up to the Take
and Duration
values specified.
Both the signal expiration and the cleanup interval are configurable, although care should be taken when modifying these values as they can have a direct impact on performance. See ODD_CACHE_CLEANUP_INTERVAL and ODD_CACHE_EXPIRATION for details.
{SIGNAL}QueryService
The {SIGNAL}QueryService
handles queries for the specified signal (Span
, Metric
, Log
).
Queries can be made using gRPC or (eventually) HTTP requests. The {SIGNAL}QueryRequest
is used
for every query related to a signal, and each request returns a {SIGNAL}QueryResponse
.
{SIGNAL}QueryResponse
Every query for a signal will return a {SIGNAL}QueryResponse
message. The structure of that
message is (using Span
):
message SpanQueryResponse {
repeated FlatSpan spans = 1;
}
message FlatSpan {
opentelemetry.proto.trace.v1.Span span = 1;
opentelemetry.proto.resource.v1.Resource resource = 2;
opentelemetry.proto.common.v1.InstrumentationScope instrumentationScope = 3;
string resourceSchemaUrl = 4;
string instrumentationScopeSchemaUrl = 5;
}
The signal is de-normalized or “flattened”, hence the Flat{SIGNAL}
name. The
OpenTelemetry data structure for a signal includes the resource that created the signal and the
instrumentation library that generated the signal. To avoid traversing this data structure as
part of OddDotNet testing, the resource and the scope are de-normalized.
The list of signals could be empty if no signals match the filters you provide within the timeout specified.
{SIGNAL}QueryRequest
The SIGNALQueryRequest
object is a simple message that encapsulates all the
query language needed to make a request.
message SpanQueryRequest {
repeated Where filters = 1;
odddotnet.proto.common.v1.Take take = 2;
optional odddotnet.proto.common.v1.Duration duration = 3;
}
message MetricQueryRequest {
repeated Where filters = 1;
odddotnet.proto.common.v1.Take take = 2;
optional odddotnet.proto.common.v1.Duration duration = 3;
}
Take
The Take
property supports three options. Take
is optional, so if no value
is supplied, the query will default to TakeFirst
.
message Take {
oneof value {
TakeFirst takeFirst = 1;
TakeAll takeAll = 2;
TakeExact takeExact = 3;
}
}
message TakeFirst {}
message TakeAll {}
message TakeExact {
int32 count = 1;
}
TakeFirst
finds the first matching signal based on the filters you provide. The
query will return immediately upon finding a match, with the matching signal
contained in the {SIGNAL}QueryResponse
message.
TakeAll
will continue to add signals that match your filters until the request
times out.
Finally, TakeExact
has a single property count
that instructs the test harness
to continue to match signals until it has reached the number specified in count
.
Duration
Duration specifies how long to wait after the query has started before returning the results.
message Duration {
int32 milliseconds = 1;
}
Duration is optional. If a value is not supplied, the query will continue to match spans
until the Take
amount has been met. When supplying a duration, the value is in milliseconds.
If a duration value is supplied, then the query will return either when the Take
criteria has been met, or when the Duration
is met, whichever is first. This means,
for example, that a query with a TakeExact(100)
might not return all 100 if
matches are not found within the Duration
.
Where
The filters
property of the request defines the criteria used to match a signal. For the list
of SIGNAL
-specific filters, see the corresponding query page:
Here is the Where
definition for Metrics, as an example:
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;
}
}
Each filter has logic that evaluates to either true or false. If a filter is true, the span currently being checked passes that filter and moves on to the next filter in line. If all filters pass, the span is considered a match and is included in the results.
Each signal contains the same values that can be queried:
- PropertyFilter - the properties available are specific to each signal
- OrFilter
- InstrumentationScope - for querying details related to the instrumentation library used to create the signal
- Resource - for querying details related to the resource that created the signal
- InstrumentationScopeSchemaUrl
- ResourceSchemaUrl
PropertyFilter
The PropertyFilter
provides the ability to check a specific property of a signal
using the correct data type and a comparison that makes sense for that property.
Notice that the PropertyFilter
contains a map of possible values that are either additional
Filter
types or some data type Property
. When querying a signal, the goal is to build
a query that reduces down to a single property that is being queried. Take metrics as an example.
The PropertyFilter
of the MetricQueryRequest.Wnere
includes a filter for Gauge
:
message PropertyFilter {
reserved 4, 6, 8;
oneof value {
...
GaugeFilter gauge = 5;
...
}
}
Because Gauge
is a GaugeFilter
and not a GaugeProperty
, this indicates that there are additional
levels that can be queried. Here’s the GaugeFilter
:
message GaugeFilter {
oneof value {
NumberDataPointFilter dataPoint = 1;
}
}
Once again, the only property of the GaugeFilter
is itself another filter, the NumberDataPointFilter
.
This filter includes a UInt64Property
called StartTimeUnixNano
.
message NumberDataPointFilter {
reserved 1;
oneof value {
...
odddotnet.proto.common.v1.UInt64Property startTimeUnixNano = 2;
...
}
}
To query for this property with a value of 123, the request would look like the following:
{
"filters": [
{
"property": {
"gauge": {
"dataPoint": {
"startTimeUnixNano": {
"compareAs": "NUMBER_COMPARE_AS_TYPE_EQUALS",
"compare": 123
}
}
}
}
}
]
}
OrFilter
When you are adding a PropertyFilter
to the request, each filter is added as
an “AND”. This is not always desireable, as you may want to check if property ‘x’ matches
“OR” property ‘y’ matches. The OrFilter
provides this ability.
message OrFilter {
repeated Where filters = 1;
}
The “OR” filter is just a list of filters, and it returns true if any of the filters within return true.
InstrumentationScopeFilter
All signals, are generated using an instrumentation library. The
InstrumentationScopeFilter
allows for querying properties
related to the instrumentation scope.
ResourceFilter
All signals, are associated with a resource when they are created. This
resource can be queried using the ResourceFilter
.
InstrumentationScopeSchemaUrl
This property is associated with the Scope{SIGNAL}
message, which is a combination of the
instrumentation scope and the list of signals. It is listed as a separate property since it
is not directly part of the InstrumentationScope
message.
ResourceSchemaUrl
This property is associated with the Resource{SIGNAL}
message, which is a combination of the
resource and the list of Scope{SIGNAL}
. It is listed as a separate property since it
is not directly part of the Resource
message.