Query APM Measurements
Query groundcover's APM measurements (requests, errors, latency) with gcQL. Two endpoints are available: search for tabular results and search-time-series for charted results over time.
The APM measurements APIs let you query groundcover's pre-aggregated application performance data using gcQL. They back the APM data source in Explore, so anything you can build in the UI you can also run from a script.
Endpoints
Search
POST https://api.groundcover.com/api/apm-measurements/search
Run a gcQL query and get back rows. Use this for catalog-style listings and top-N reports.
Search Time Series
POST https://api.groundcover.com/api/apm-measurements/search-time-series
Run the same gcQL query bucketed over time and get back one series per group. Use this for charts and trends.
Authentication
Both endpoints require an API Key in the Authorization header:
Authorization: Bearer <YOUR_API_KEY>If your account has multiple backends, also send the Backend ID:
X-Backend-Id: <YOUR_BACKEND_ID>To locate your Backend ID, navigate to Settings → Access, open the API Keys tab, and find it in the section header.
The query contract
Every APM query must include two top-level filters, otherwise the request fails with 400:
resource_type- the protocol, using an equality (resource_type:http) or aninfilter (resource_type:in("http","grpc")).A traffic direction - either
is_inbound:trueoris_outbound:true. Pick exactly one.
"Top-level" means the filters must sit in the pre-pipe filter section, not inside an OR subgroup and not after a | filter stage:
Traffic direction
is_inbound:true
The workload as a server, receiving calls
is_outbound:true
The workload as a client, issuing calls to its dependencies
Resource types
resource_type is the protocol the eBPF sensor decoded. The values are:
Web / RPC
http, grpc, graphql
Databases & caches
postgresql, mysql, redis, mongodb
Messaging
kafka, amqp, sqs
Cloud & network
s3, dns, sip
LLM providers
openai, anthropic, bedrock, vertex_ai, google_genai, gen_ai
Which values are present depends on your sensor version and what your workloads actually talk to. A resource_type that matches nothing returns an empty result set rather than an error.
Measurement columns
These are the numeric columns you aggregate over in a | stats stage:
total_counter
Number of requests
success_counter
Requests that completed successfully
error_counter
Requests that completed with an error status
issue_counter
Requests flagged as issues. Every issue is an error, not every error is an issue
total_latency_seconds
Summed request latency, for computing averages
latency_seconds_quantiles
Latency distribution. Read it with quantile(<phi>, latency_seconds_quantiles)
The same traffic can be reported by more than one source, so aggregating across sources double-counts it. Filter to a single source (for example | filter source:eBPF) or add source to your stats by (...) clause.
APM measurements come from two sources:
Source values are case-sensitive, and a value that matches nothing returns an empty result set rather than an error - so note the mixed case on eBPF against the lower case on opentelemetry. Run <filters> | field_values_with_hits source to see which sources are present in your own environment.
eBPF
The groundcover eBPF sensor
opentelemetry
Spans ingested from an OpenTelemetry pipeline
Search Endpoint
Request
Headers
Request Body
start
string
Yes
Range start in RFC3339 format
end
string
Yes
Range end in RFC3339 format. Must be at or after start
query
string
Yes
gcQL query. Required unless you send pipeline instead
pipeline
object
No
Structured pipeline, as an alternative to query. Same top-level filter contract applies
filters
string
No
Extra gcQL filters ANDed onto the query
sources
array
No
Additional filter conditions. Prefer expressing these in query
Response
data
array
One object per result row. Keys are the group-by labels and the aliases from your stats stage
valueFieldHints
array
Names of the numeric columns in data, useful for deciding what to plot
Examples
Top 20 servers by inbound HTTP requests and errors
This is the direct replacement for /api/resources/v3/apis/list:
Slowest HTTP routes by p95 latency
Error rate of outbound PostgreSQL calls, per client
Scoping to a namespace with filters
Discovering filter values
This is the replacement for /api/resources/v3/apis/filters. Use the field_values_with_hits pipe to list the values a label actually takes in the selection, along with how many requests each one accounts for:
Swap server for any label you want to populate a picker with, for example namespace, clustered_path, span_name or response_status_code. The mandatory top-level resource_type and direction filters still apply.
Search Time Series Endpoint
Same body as the search endpoint, plus the bucketing fields. groundcover applies the bucket to your query, so do not add a bucket stage yourself.
limit applies per bucket here, not to the whole result. When the bucket is applied, a trailing | limit N is rewritten to cap rows within each time bucket. With a stats by (...) clause that means you get the top N groups in each bucket independently, so a group that ranks in the top N during some buckets and not others produces a series with gaps, and the set of series can change from bucket to bucket.
If you need a stable set of series, pick the groups first with a search call, then query the time series with those groups pinned in the query filters rather than relying on limit to choose them.
Request
Request Body
start
string
Yes
Range start in RFC3339 format
end
string
Yes
Range end in RFC3339 format
query
string
Conditional
gcQL query. Provide either query or pipeline. Sending neither returns 400
pipeline
object
Conditional
Structured pipeline, as an alternative to query. Same top-level filter contract applies
bucketDuration
string
Yes
Bucket width, for example "1m", "5m", "1h". Also accepts "5 minutes" style values
valueField
string
No
Which numeric column to plot, or a comma-separated list. Resolved automatically from the response when omitted
fillValue
number
No
Value used for empty buckets. Buckets are filled with null when omitted
filters
string
No
Extra gcQL filters ANDed onto the query
sources
array
No
Additional filter conditions. Prefer expressing these in query
Response
A bare top-level array of series, each with its labels and its data points:
velocity
array
Data points for the series, one per bucket
velocity[]
array
A [timestamp, value] pair
metric
object
The group-by labels identifying this series
Each data point contains:
Timestamp: bucket start as a Unix timestamp in seconds
Value: aggregated value for the bucket as a string, or
nullfor an empty bucket whenfillValueis not set
Each series object has the same velocity / metric shape as the metrics range query, but the envelope differs: that endpoint wraps its series in {"velocities": [...], "promql": "..."}, while this one returns the array directly. Parse the response body itself, not response.velocities.
Examples
Hourly inbound HTTP request count per server
This replaces charted reporting off /api/resources/v3/apis/requests. To chart a stable set of series, first pick the servers with a search call (for example, the top 20 by requests), then pin them in the time-series query instead of using a trailing | limit:
p99 latency over time per route
This replaces charted reporting off /api/resources/v3/apis/latencies. Here the trailing | limit 10 is used deliberately for its per-bucket semantics: each 5-minute bucket independently keeps its 10 slowest routes, so the chart surfaces whichever routes were slowest at each point in time rather than a fixed set of series:
Errors over time, replacing /api/resources/v3/apis/errors
Like the latency example, the | limit 10 keeps the 10 highest-error servers within each 15-minute bucket independently. Pin the servers with server:in(...) instead if you need a fixed set of series:
Errors
400
The query is missing a top-level resource_type filter
400
The query is missing a top-level is_inbound:true or is_outbound:true filter
400
gcQL parse error, or an invalid bucketDuration
400
The query exceeded the row cap. Narrow resource_type or the time range, or reduce group-by cardinality
Errors are returned as a JSON object with a human-readable message, a trace_id, and for some cases a stable machine-readable code.
A query missing its resource_type filter:
A query that grouped by too many distinct values:
Best Practices
Always aggregate. These endpoints read pre-aggregated measurements, but a bare filter query over a wide window still returns a lot of rows. Add a
| statsstage and a| limit.Disambiguate the source. Filter to one
sourceor group by it, so eBPF and OpenTelemetry rows are not summed together.Keep group-by cardinality in check. Grouping by a high-cardinality label over a wide window can hit the row cap. Check a candidate label first with
| stats count_uniq(<label>).Match
bucketDurationto the window. A 24-hour range at1mbuckets produces 1440 points per series. Prefer1hfor daily reports and reserve fine buckets for short windows.Set
fillValuefor counters. Counters read better as0in empty buckets than as gaps.Don't lean on
limitto pick series. On the time-series endpoint it caps rows per bucket, so it will not give you a stable global top N. Select the groups with a search call first, then pin them in the time-series query.
Last updated
