Documentation
¶
Overview ¶
Package expression defines the structured trace-query filter of Jaeger RFC 0005, "Structured Query Filters for Trace Search": https://github.com/jaegertracing/jaeger/blob/main/docs/rfc/0005-structured-query-filters.md Section references (§N) below point into that document.
These are plain Go types, deliberately independent of the proto definitions the filter travels over the wire in. The AST is the contract, and it is one contract: the same types describe a filter arriving on the public query API, reaching a storage backend, and being gated by a query interceptor, so nothing in that path has to translate between two representations of the same tree. Converting to and from the wire is the business of whoever owns a wire.
Index ¶
- Constants
- Variables
- func SpanKinds() []string
- func SpanStatuses() []string
- func ValidateFilter(filter *Call) error
- type AnyValue
- type AttributeRef
- type BoolValue
- type Call
- type DoubleValue
- type DurationValue
- type Expression
- type Field
- type FieldRef
- type FieldType
- type IntValue
- type Level
- type List
- type NestedRef
- type Operator
- type StringValue
- type TimestampValue
- type ValueType
Constants ¶
const ( SpanFieldTraceID = "traceID" SpanFieldSpanID = "spanID" SpanFieldParentSpanID = "parentSpanID" SpanFieldTraceState = "traceState" SpanFieldName = "name" SpanFieldKind = "kind" SpanFieldStartTime = "startTime" SpanFieldEndTime = "endTime" SpanFieldDuration = "duration" SpanFieldStatus = "status" SpanFieldStatusMessage = "statusMessage" ResourceFieldService = "service" ResourceFieldSchemaURL = "schemaURL" ScopeFieldName = "name" ScopeFieldVersion = "version" ScopeFieldSchemaURL = "schemaURL" EventFieldName = "name" EventFieldTime = "time" EventFieldTimeSinceStart = "timeSinceStart" LinkFieldTraceID = "traceID" LinkFieldSpanID = "spanID" LinkFieldTraceState = "traceState" )
Built-in fields are the values a span carries directly, rather than entries in one of its attribute maps. A FieldRef names one by giving its level and its name.
The set is defined here rather than left to each backend, because which fields a query may name is part of the query API: a caller writes one query against Jaeger, not a different one per storage backend. Which of them a given backend can actually serve is the separate question that SearchCapabilities answers, so a field being valid here does not promise that every deployment can filter on it.
Most fields are a field of the corresponding OTLP message. A few are derived, computed from the OTLP data rather than stored in it — a span's duration from its two timestamps, an event's offset from its span's start — and are named because they are what people actually filter on.
Field names are camelCase — `startTime`, `traceID`, not the proto's `start_time_unix_nano` and `trace_id` — because that is how proto3 JSON renders a message field, and how this API's own query parameters are already named, so a field reads like the rest of the JSON surface. The operators are snake_case (`not_in`) without contradicting that: those are values, not field names.
A constant compared against a field is written the way that field's values are written, and for the two that measure time that means two different formats, neither of them a bare number in an assumed unit:
- A duration — `duration`, `timeSinceStart` — carries its unit, in Go duration syntax: "2s", "1h30m", "50us". This is what `duration_min`/`duration_max` have always accepted, and RFC 0005 §5.3 requires the unit rather than leaving nanoseconds and milliseconds to be guessed at.
- A timestamp — `startTime`, `endTime`, `time` — is RFC 3339 with nanosecond precision: "2026-08-16T18:56:20.123456789Z". That is what api_v3 already accepts for the query's own time range, so a caller writes an instant one way throughout.
Each level has its own vocabulary, so the constants are named for the level they belong to: a span's startTime and an event's time are different fields, and the three levels that each have a `name` have three different fields under that one name.
const MaxNestingDepth = 20
MaxNestingDepth is how deeply calls may nest, counting the filter itself as the first level. A filter is a tree a person or a query builder wrote, and twenty levels of nesting is far past anything either produces.
The bound is what makes walking a filter safe rather than merely usual. The AST is built from pointers, so a caller can hand a call itself as one of its own arguments, and nothing about the types prevents it; a walk without a bound would follow that cycle until the stack ran out. With the bound, a cycle is refused for being too deep, and so is an honest tree that no consumer downstream could walk either.
Variables ¶
var ErrTooDeeplyNested = fmt.Errorf("filter nests calls more than %d deep", MaxNestingDepth)
ErrTooDeeplyNested is returned for a filter that nests calls beyond MaxNestingDepth, which is also how a filter that contains itself is answered.
Functions ¶
func SpanKinds ¶
func SpanKinds() []string
SpanKinds returns the words span.kind holds, in the order OTLP declares them. It returns a copy, like Fields, because the validator reads the same words: a caller that could append one would change what every filter after it means.
func SpanStatuses ¶
func SpanStatuses() []string
SpanStatuses returns the words span.status holds. It returns a copy, for the reason SpanKinds does.
func ValidateFilter ¶
ValidateFilter checks that a filter is well formed: every operator is one this package defines and has the number and kind of arguments it takes, every level and value type is a defined one, every reference names something, a reference to a built-in field names one this API defines (see Field), and the quantifier's binding rules hold (RFC 0005 §5.5).
What it deliberately leaves alone is a constant's text — whether "banana" is a duration is answered by ResolveConstants, which knows the field it is compared against — and which of the valid things a given backend can serve, which is what a backend's declared capabilities are for.
Types ¶
type AnyValue ¶
type AnyValue struct {
Value string
// contains filtered or unexported fields
}
AnyValue is a constant under no type constraint: the caller wrote a value and said nothing about how to read it, so a backend matches it at whatever type the value was stored. It is also what an unhinted duration or timestamp arrives as, until it is resolved against the field it is compared with (see ResolveConstants).
type AttributeRef ¶
type AttributeRef struct {
Key string
// Level is empty for the unqualified span-or-resource search, or one of the five levels.
Level Level
// contains filtered or unexported fields
}
AttributeRef names an entry in one of the span's attribute maps. See RFC 0005 §5.1.
type BoolValue ¶
type BoolValue struct {
Value bool
// contains filtered or unexported fields
}
BoolValue is a constant to be matched as a boolean.
type Call ¶
type Call struct {
Op Operator
Args []Expression
// contains filtered or unexported fields
}
Call applies Op to Args. The arity follows the operator: OpNot and OpExists are unary, the comparisons and OpIn/OpNotIn are binary, and OpAnd/OpOr take two or more. Because an argument is itself an Expression, a comparison reads two references as readily as a reference and a constant — what it requires is that both operands hold the same kind of value (see ValidateFilter).
func Finalize ¶
Finalize prepares a decoded filter for everything downstream of it: it checks the structure, reads every constant against the field it is compared to, and puts the reference first in each comparison. What it returns is a new tree; the one it was given is untouched.
Running it again on its own result changes nothing, which is what lets each boundary finalize a filter it did not build — the query service after an interceptor has edited one, and the remote-storage server on whatever a client sent it (RFC 0005 §7).
func ResolveConstants ¶
ResolveConstants reads every unconstrained constant that is compared against a built-in field as that field's declared type, and refuses one whose text will not parse — a duration of "banana" is answered at the query boundary rather than passed to a backend to interpret. It is stage 3 of RFC 0005 §7 and expects a filter ValidateFilter has accepted.
A constant compared against an *attribute* is left alone, because only storage knows how that attribute was written. Resolution rewrites the nodes it changes and returns a new tree rather than annotating the one it was given, so nothing it produces can go stale when a query interceptor edits a predicate afterwards.
It also puts the reference first in every comparison, so each consumer downstream reads one orientation rather than handling both.
type DoubleValue ¶
type DoubleValue struct {
Value float64
// contains filtered or unexported fields
}
DoubleValue is a constant to be matched as a floating-point number.
type DurationValue ¶
DurationValue is a length of time, which is what a duration field is compared against.
The wire has no duration type. One travels as an unhinted constant written in Go duration syntax, "2s" or "50us", so this node is reached by resolving that constant against the field it is compared to (see ResolveConstants).
Beside an attribute reference there is no field to resolve against, so a round trip through the wire hands the receiver an AnyValue holding the same text. Nothing is lost that an attribute had to begin with: only storage knows what type it was written as.
type Expression ¶
type Expression interface {
// contains filtered or unexported methods
}
Expression is a node in a structured filter: an atom — a reference to a value on the span, or a constant — or a Call applying an operator to argument expressions. Only the types in this package implement it, so a backend can switch on the concrete type and cover every case. See RFC 0005 §6.
func ReadConstant ¶ added in v0.11.1
func ReadConstant(t FieldType, text string) (Expression, error)
ReadConstant reads text as the type a built-in field holds, which is what finalizing a filter does to every constant compared against one. A consumer that needs the typed value of something the wire carried as text — a list element, or a tree that reached it without being finalized — reads it here rather than parsing it again itself.
func ReadElement ¶ added in v0.11.1
func ReadElement(list *List, fieldType FieldType, element string) (Expression, error)
ReadElement reads one element of a list as the type the list is read at: Type where the list declares one, and otherwise the type of the built-in field it is compared against, which a caller passes as fieldType. It is the reading a consumer would otherwise write for itself, and the same one finalizing a filter already did, so on a finalized filter it cannot fail.
A list compared against an attribute always declares its type (see List), so a caller lowering one passes an empty fieldType.
type Field ¶
type Field struct {
Level Level
Name string
// Type is what a constant compared against this field is read as.
Type FieldType
// Derived is true when the field is computed from the OTLP data rather than being a field
// of it. A backend has to be able to compute it to serve a predicate on it, which is why
// it is worth knowing apart.
Derived bool
// contains filtered or unexported fields
}
Field is a built-in field: its name paired with the level it belongs to, and the type it holds. The name and level travel together because neither identifies a field on its own — `name` is a field of the span, the event and the instrumentation scope alike, and `traceID` of the span and the link.
type FieldRef ¶
FieldRef names a built-in field — a value the data model defines directly rather than an attribute-map entry, such as a span's duration. Level is never empty. See RFC 0005 §5.2 and Field.
type FieldType ¶
type FieldType string
FieldType is the type a built-in field holds, and so the type a constant compared against that field is read as. It is what ResolveConstants rewrites an unconstrained constant into, and what makes `span.duration > "banana"` refusable at the query boundary.
It is a smaller vocabulary than it might be, because the fields below are the only ones that exist: IDs, a status, a span kind and a trace state are all text this API checks, and a distinct type only pays once something wants the parsed form (RFC 0005 §5.4). A level gains numeric fields the day one is defined, and the type for it is added here with the rule that parses it.
const ( FieldTypeString FieldType = "string" FieldTypeDuration FieldType = "duration" FieldTypeTimestamp FieldType = "timestamp" // FieldTypeSpanKind and FieldTypeSpanStatus hold one of a closed set of words, so a // constant compared against one is refused unless it is a member. An ID is a string // rather than a type of its own: a span kind outside the set can never match any span, // while an ID nobody recorded is indistinguishable from one the caller is looking for. FieldTypeSpanKind FieldType = "spanKind" FieldTypeSpanStatus FieldType = "spanStatus" )
type IntValue ¶
type IntValue struct {
Value int64
// contains filtered or unexported fields
}
IntValue is a constant to be matched as an integer.
type Level ¶
type Level string
Level is the scope a referenced value lives in. The five levels are the OTLP attribute maps; an attribute reference may also leave it empty, which searches the span and resource levels. See RFC 0005 §5.1.
type List ¶
List is a homogeneous list constant, the right-hand argument of OpIn and OpNotIn. Its elements stay as the caller wrote them, and every one of them is read as a single type.
That type is always known: Type declares it, or the built-in field the list is compared against supplies it. Compared against an attribute, which declares nothing itself, the list has to declare it — and it is worth declaring anyway, because a list matches only values of the type it names.
Unlike a constant, a list is not rewritten into typed elements when a filter is finalized. Two reasons. A backend that indexes a value as text matches the text a caller wrote, and re-writing "1.50" as a number and back would hand it "1.5" instead. And an element that cannot be read as the list's type is refused while finalizing, so what a consumer holds is text already known to be readable: ReadElement turns one into the typed node, and nothing has to parse it defensively.
type NestedRef ¶
type NestedRef struct {
Level Level
// contains filtered or unexported fields
}
NestedRef names a span's events or links collection, which is what OpSome quantifies over and the only place it may appear. See RFC 0005 §5.5.
type Operator ¶
type Operator string
Operator is what a Call applies to its arguments: a boolean combinator, a comparison, a set-membership test, or the existential quantifier over a span's events or links. See RFC 0005 §5.3 and §5.5.
const ( OpAnd Operator = "and" OpOr Operator = "or" OpNot Operator = "not" OpEq Operator = "eq" OpNe Operator = "ne" OpGt Operator = "gt" OpLt Operator = "lt" OpGte Operator = "gte" OpLte Operator = "lte" OpRegex Operator = "regex" OpExists Operator = "exists" OpIn Operator = "in" OpNotIn Operator = "not_in" OpSome Operator = "some" )
type StringValue ¶
type StringValue struct {
Value string
// contains filtered or unexported fields
}
StringValue is a constant to be matched as text.
type TimestampValue ¶
TimestampValue is an instant, which is what a timestamp field is compared against.
Like DurationValue it has no wire type of its own, arrives as an unhinted RFC 3339 constant, and comes back as an AnyValue when it travels beside an attribute.
type ValueType ¶
type ValueType string
ValueType is the type a constant declares on the wire. It is optional there: empty means the backend matches the value at whatever type it was stored, and a type that is set is authoritative, so the backend matches only values of that type. In this AST a constant is a typed node instead (see AnyValue and the values beside it), so the vocabulary is left for List, whose elements are strings, and for whoever converts a wire message into a node. See RFC 0005 §5.4.