This guide covers the Elixir query API, all available filters, trace lookup, and query optimization.
# All spans (default limit 100, newest first)
{:ok, result} = TimelessTraces.query()
# Error spans
{:ok, result} = TimelessTraces.query(status: :error)
# Server spans from a specific service
{:ok, result} = TimelessTraces.query(kind: :server, service: "api-gateway")
# Slow spans (> 100ms)
{:ok, result} = TimelessTraces.query(min_duration: 100_000_000)Queries return a TimelessTraces.Result struct:
%TimelessTraces.Result{
entries: [%TimelessTraces.Span{}, ...],
total: 42, # total matching spans (before pagination)
limit: 100, # max entries returned
offset: 0 # entries skipped
}Each span in entries is a TimelessTraces.Span:
| Field | Type | Description |
|---|---|---|
trace_id |
string | 32-character hex trace ID |
span_id |
string | 16-character hex span ID |
parent_span_id |
string or nil | Parent span ID (nil for root spans) |
name |
string | Operation name (e.g. "GET /users") |
kind |
atom | :internal, :server, :client, :producer, :consumer |
start_time |
integer | Start time in nanoseconds |
end_time |
integer | End time in nanoseconds |
duration_ns |
integer | Duration in nanoseconds |
status |
atom | :ok, :error, :unset |
status_message |
string or nil | Error description |
attributes |
map | Span attributes (string keys) |
events |
list | Span events |
resource |
map | OTel resource attributes (string keys) |
instrumentation_scope |
map or nil | %{name: "...", version: "..."} |
| Filter | Type | Description |
|---|---|---|
:name |
string | Case-insensitive substring match on span name |
:service |
string | Exact match on service.name in attributes or resource |
:kind |
atom | :internal, :server, :client, :producer, :consumer |
:status |
atom | :ok, :error, :unset |
:min_duration |
integer | Minimum duration in nanoseconds |
:max_duration |
integer | Maximum duration in nanoseconds |
:since |
DateTime or integer | Start time lower bound (DateTime or unix nanoseconds) |
:until |
DateTime or integer | Start time upper bound (DateTime or unix nanoseconds) |
:trace_id |
string | Exact match on trace ID |
:attributes |
map | All key/value pairs must match (keys and values compared as strings) |
:limit |
integer | Max results (default 100) |
:offset |
integer | Skip N results (default 0) |
:order |
atom | :desc (newest first, default) or :asc (oldest first) |
# Spans from the last hour
one_hour_ago = DateTime.add(DateTime.utc_now(), -3600)
TimelessTraces.query(since: one_hour_ago)
# Spans in a specific window
TimelessTraces.query(
since: ~U[2024-01-15 10:00:00Z],
until: ~U[2024-01-15 11:00:00Z]
)
# Using nanosecond timestamps directly
TimelessTraces.query(since: 1705312200_000_000_000)# Slow spans (> 500ms)
TimelessTraces.query(min_duration: 500_000_000)
# Spans in a duration range (100ms - 1s)
TimelessTraces.query(min_duration: 100_000_000, max_duration: 1_000_000_000)# Match specific attributes
TimelessTraces.query(attributes: %{"http.method" => "POST", "http.status_code" => "500"})# Page 1
{:ok, page1} = TimelessTraces.query(status: :error, limit: 50)
# Page 2
{:ok, page2} = TimelessTraces.query(status: :error, limit: 50, offset: 50)TimelessTraces.query(
service: "api-gateway",
kind: :server,
status: :error,
min_duration: 100_000_000,
since: DateTime.add(DateTime.utc_now(), -86400),
limit: 20,
order: :asc
)Retrieve all spans in a trace, sorted by start time:
{:ok, spans} = TimelessTraces.trace("abc123def456789012345678abcdef01")This uses the trace index for fast lookup -- only blocks known to contain spans for that trace are read.
On disk, trace_index stores packed binary trace IDs for 32-character hex values to reduce index size. Lookups remain backward-compatible with older text rows during migration.
# List all services
{:ok, services} = TimelessTraces.services()
# => {:ok, ["my_app", "api_gateway", "auth_service"]}
# List operations for a service
{:ok, ops} = TimelessTraces.operations("my_app")
# => {:ok, ["GET /users", "POST /orders", "DB query"]}The following filters leverage the inverted term index, narrowing the set of blocks to read:
:service-- matchesservice:<name>terms:kind-- matcheskind:<kind>terms:status-- matchesstatus:<status>terms:name-- matchesname:<name>terms:trace_id-- uses the trace index directly
Time range filters (:since, :until) narrow blocks by timestamp metadata.
Queries with no filters scan all blocks. On large datasets, always include at least one filter to leverage the index.
:min_duration, :max_duration, and :attributes are applied as in-memory filters after block decompression. They don't reduce the number of blocks read. Combine them with indexed filters for best performance.
Query latency on 500K spans, 500 blocks (avg over 3 runs):
| Query | zstd | OpenZL | Speedup |
|---|---|---|---|
| All spans (limit 100) | 945ms | 442ms | 2.1x |
| status=error | 289ms | 148ms | 2.0x |
| service filter | 318ms | 243ms | 1.3x |
| kind=server | 275ms | 225ms | 1.2x |
| Trace lookup | 5.5ms | 5.7ms | 1.0x |
OpenZL columnar format is faster at query time because columns can be selectively decompressed.