# groundcover Query Language (gcQL) Reference

### Overview

gcQL is a pipeline-based query language for searching, filtering, aggregating, and transforming your logs, traces, and events in groundcover. Queries flow left to right using the pipe operator `|`, where each stage processes the output of the previous stage.

```
<filters> | <pipe> | <pipe> | ...
```

You can use gcQL in the Data Explorer, Monitors, Dashboards, and the groundcover API.

{% hint style="info" %}
gcQL was inspired by [LogsQL](https://docs.victoriametrics.com/victorialogs/logsql/) from VictoriaMetrics VictoriaLogs. If you are familiar with LogsQL, many gcQL concepts will feel familiar. See the Filters page for key differences between gcQL and LogsQL.
{% endhint %}

***

### Filters

Filters narrow down which records your query returns. They appear at the beginning of a query, before any pipe operations. Use `*` to match all records when you don't need filtering.

***

### time

Narrow your query to a specific time window. By default, the time range is controlled by the UI time picker, but you can override it directly in the query.

**Syntax**

```
_time:duration
_time:[start, end]
_time:YYYY-MM-DD
_time:duration offset duration
```

| Argument       | Type            | Description                                                 |
| -------------- | --------------- | ----------------------------------------------------------- |
| `duration`     | duration string | Time window relative to now. Units: `s`, `m`, `h`, `d`, `w` |
| `start`, `end` | date string     | Absolute range in `YYYY-MM-DD` format                       |
| `offset`       | duration string | Shifts the window backwards by the specified duration       |

**Examples**

```
_time:5m
```

*All records from the last 5 minutes*

```
_time:1h | stats by (workload) count() as cnt
```

*Count records per workload in the last hour*

```
_time:[2026-01-01, 2026-01-18] | stats by (level) count() as cnt
```

*Count records per level in a specific date range*

```
_time:5m offset 1h | stats by (workload) count() as cnt
```

*Records from 65–60 minutes ago (5-minute window, shifted back 1 hour)*

***

### in

Match any of multiple values for a field, similar to SQL `IN`.

**Syntax**

```
field:in(value1, value2, ...)
```

**Examples**

```
level:in(error, fatal, critical) | fields timestamp, level, content | limit 20
```

*Logs with level error, fatal, or critical*

```
workload:in(api, gateway, auth) | stats by (workload) count() as cnt
```

*Count logs for specific workloads*

```
workload:in(level:error | fields workload) | stats by (workload) count() as cnt
```

*Logs from workloads that have at least one error — uses a subquery to dynamically build the value list*

**Notes**

* The `in` filter accepts both static value lists and subqueries.
* When using a subquery, it must return a single field.

***

### eq\_field

Match records where two fields have equal values. Useful for finding records where related fields match (e.g., container name equals workload name).

**Syntax**

```
field1:eq_field(field2)
```

**Examples**

```
container_name:eq_field(workload) | fields timestamp, container_name, workload | limit 10
```

*Records where the container name matches the workload name*

```
namespace:eq_field(workload) | stats by (namespace) count() as cnt
```

*Records where namespace and workload have the same value*

***

### le\_field

Match records where the value of one field is less than or equal to the value of another field.

**Syntax**

```
field1:le_field(field2)
```

**Examples**

```
request.size_bytes:le_field(response.size_bytes) | fields start_timestamp, resource, request.size_bytes, response.size_bytes | limit 10
```

*Traces where the request was smaller than or equal to the response*

```
min_latency:le_field(max_latency) | fields timestamp, workload, min_latency, max_latency | limit 10
```

*Records where min\_latency does not exceed max\_latency*

***

### lt\_field

Match records where the value of one field is strictly less than the value of another field.

**Syntax**

```
field1:lt_field(field2)
```

**Examples**

```
request.size_bytes:lt_field(response.size_bytes) | fields start_timestamp, resource, request.size_bytes, response.size_bytes | limit 10
```

*Traces where the response was strictly larger than the request*

```
p50:lt_field(p99) | fields timestamp, workload, p50, p99 | limit 10
```

*Records where p50 latency is strictly below p99 latency*

**Notes**

* `eq_field`, `le_field`, and `lt_field` compare field values within the same record.
* For numeric fields, the comparison is numeric. For string fields, the comparison is lexicographic.

***

### is\_ipv4

Filter for records containing valid IPv4 addresses in a field.

**Examples**

```
is_ipv4(client_ip) | fields timestamp, client_ip, content | limit 10
```

*Records where client\_ip contains a valid IPv4 address*

```
is_ipv4(source_address) | stats by (source_address) count() as cnt
```

*Count records grouped by IPv4 source address*

***

### is\_ipv6

Filter for records containing valid IPv6 addresses in a field.

**Examples**

```
is_ipv6(client_ip) | fields timestamp, client_ip, content | limit 10
```

*Records where client\_ip contains a valid IPv6 address*

```
is_ipv6(source_address) | stats by (source_address) count() as cnt
```

*Count records grouped by IPv6 source address*

***

### range

Match numeric field values within a range. Use `[]` for inclusive bounds and `()` for exclusive bounds.

**Syntax**

```
field:range[min, max]
field:range(min, max)
```

**Examples**

```
latency:range[100, 500] | fields timestamp, workload, latency | limit 20
```

*Records where latency is between 100 and 500 (inclusive)*

```
duration_seconds:range(0.1, 1.0) | stats by (workload) count() as cnt
```

*Count records per workload where duration is between 0.1 and 1.0 (exclusive)*

**Notes**

* Mixed brackets are supported: `[min, max)` or `(min, max]`.

***

### regexp

Match field values using regular expressions (RE2 syntax). Use `~` for positive match and `!~` for negation.

**Syntax**

```
field:~"pattern"
field:!~"pattern"
```

**Examples**

```
workload:~"gc-.*" | stats by (workload) count() as cnt
```

*All workloads starting with "gc-"*

```
workload:~"(?i)GROUNDCOVER.*" | fields timestamp, workload, content | limit 10
```

*Case-insensitive match for workloads starting with "groundcover"*

```
level:!~"debug|trace" | stats by (level) count() as cnt
```

*Exclude debug and trace logs*

**Notes**

* Uses [RE2 regex syntax](https://github.com/google/re2/wiki/Syntax).
* Prefix the pattern with `(?i)` for case-insensitive matching.
* The field must be a string-type column.

***

### Pipe Operations

Pipe operations transform, reshape, or aggregate your query results. They appear after filters, separated by the `|` operator. You can chain multiple pipes together — each one processes the output of the previous stage.

***

### datetime\_extract

Extract date/time components (hour, day, weekday, etc.) from a timestamp field. Useful for time-based grouping and analysis.

**Syntax**

```
datetime_extract(field, 'part') as alias
```

| Argument | Type            | Description                                                                                           |
| -------- | --------------- | ----------------------------------------------------------------------------------------------------- |
| `field`  | timestamp field | The timestamp to extract from (typically `_time`)                                                     |
| `part`   | string          | One of: `year`, `month`, `day`, `hour`, `minute`, `second`, `weekday`, `dayofyear`, `quarter`, `week` |

**Examples**

```
* | datetime_extract(_time, 'hour') as hour | stats by (hour) count()
```

*Distribution of logs by hour of day*

```
* | datetime_extract(_time, 'weekday') as weekday | stats by (weekday) count()
```

*Distribution of logs by day of week*

{% hint style="warning" %}
`datetime_extract` is a pipe operation — it must appear after a filter or `*`. Use it **before** `stats`, not inside `stats by()`.

**Correct:** `* | datetime_extract(_time, 'hour') as hour | stats by (hour) count()`

**Incorrect:** `stats by (datetime_extract(_time, 'hour')) count()`
{% endhint %}

***

### extract\_regexp

Extract fields from a string using regex with named capture groups.

{% hint style="warning" %}
`extract_regexp` is not currently usable via the HTTP API. The `<>` characters in the `(?P<name>...)` capture-group syntax are stripped by the server-side input sanitizer, causing parse errors. Use `grok` as an alternative for text extraction.
{% endhint %}

**Syntax**

```
extract_regexp 'pattern' from field
```

**Examples**

```
* | extract_regexp '(?P<ip>\d+\.\d+\.\d+\.\d+):(?P<port>\d+)' from message | limit 10
```

*Extract IP address and port from message field*

```
* | extract_regexp '(?P<method>\w+) (?P<path>/\S+)' from request | limit 10
```

*Extract HTTP method and path from request field*

**Notes**

* Uses `(?P<name>...)` syntax for named capture groups.
* Due to the HTTP API limitation above, prefer `grok` for field extraction.

***

### field\_names

Discover which fields are available in your data. Useful for exploring unfamiliar data sources.

**Examples**

```
field_names
```

*List all field names across all records*

```
field_names limit 50
```

*List up to 50 field names*

```
level:error | field_names
```

*List field names that appear in error logs*

***

### field\_names\_with\_hits

Like `field_names`, but also shows how many records contain each field.

**Examples**

```
field_names_with_hits limit 20
```

*Top 20 field names with occurrence counts*

```
workload:api | field_names_with_hits
```

*Field names and counts for the "api" workload*

***

### field\_values

Get the unique values for a specific field, similar to SQL `SELECT DISTINCT`.

**Examples**

```
field_values workload limit 100
```

*List up to 100 unique workload names*

```
field_values level
```

*List all log levels present in the data*

```
level:error | field_values workload
```

*Which workloads have error logs?*

***

### field\_values\_with\_hits

Like `field_values`, but also shows the occurrence count for each value.

**Examples**

```
field_values_with_hits workload limit 20
```

*Top 20 workloads with their log counts*

```
level:error | field_values_with_hits workload limit 10
```

*Top 10 workloads by error count*

***

### fields

Select specific columns to include in the output. Use this to narrow down results or inspect specific data.

**Examples**

```
* | fields timestamp, level, content, workload | limit 10
```

*Show only timestamp, level, content, and workload columns*

```
level:error | fields timestamp, instance, workload, content | limit 50
```

*Inspect the last 50 errors with key context fields*

```
* | fields start_timestamp, resource, duration_seconds, status_code, trace_id | limit 10
```

*Select trace-specific fields*

***

### filter

Apply a filter after aggregation. Use this when you need to filter on computed values like counts or averages.

**Examples**

```
* | stats by (workload) count() as total | filter total>1000
```

*Workloads with more than 1,000 records*

```
* | stats by (workload) avg(duration_seconds) as avg_dur | filter avg_dur>0.5
```

*Workloads where average latency exceeds 500ms*

**Notes**

* An implicit syntax without the `filter` keyword is also supported: `| total>1000`.

***

### format

Create formatted strings using a template with field placeholders. Useful for building human-readable summaries.

**Syntax**

```
format "template with <field> placeholders" as alias
```

Use `<field_name>` to insert field values into the template.

**Examples**

```
* | stats by (workload) count() as cnt | format "<workload>: <cnt>" as summary | limit 10
```

*Build "workload: count" summary strings*

```
* | format "<resource> returned <status_code>" as request_summary | limit 10
```

*Create request summary lines from trace fields*

{% hint style="info" %}
On logs, `format` works most reliably after a `stats` aggregation. On traces, it works on both raw and aggregated results.
{% endhint %}

***

### grok

Parse unstructured text using Logstash-style grok patterns. This is the recommended way to extract structured fields from log messages.

**Syntax**

```
grok [field=source_field] "pattern"
```

If `field` is not specified, grok parses the default message field (`content`/`_msg`).

**Examples**

```
* | grok "%{IP:client_ip} - - \[%{HTTPDATE:timestamp}\] \"%{WORD:method} %{URIPATHPARAM:request}\"" | limit 10
```

*Parse Apache/Nginx access log lines*

```
* | grok field=content "%{TIMESTAMP_ISO8601:ts} %{LOGLEVEL:level} \[%{DATA:logger}\] %{GREEDYDATA:message}" | limit 10
```

*Parse structured application logs*

```
level:error | grok "Exception: %{GREEDYDATA:exception_type}: %{GREEDYDATA:error_msg}" | limit 10
```

*Extract exception type and message from error logs*

```
* | grok "user=%{USERNAME:user} action=%{WORD:action} status=%{WORD:status}" | stats by (user, action) count()
```

*Parse key=value pairs and aggregate by user and action*

**Common grok patterns**

| Pattern                | Matches                       |
| ---------------------- | ----------------------------- |
| `%{IP}`                | IPv4 or IPv6 address          |
| `%{WORD}`              | Single word                   |
| `%{NUMBER}`            | Integer or float              |
| `%{GREEDYDATA}`        | Rest of line                  |
| `%{DATA}`              | Non-greedy match              |
| `%{TIMESTAMP_ISO8601}` | ISO 8601 timestamp            |
| `%{HTTPDATE}`          | Apache-style date             |
| `%{LOGLEVEL}`          | Log level (INFO, ERROR, etc.) |
| `%{POSINT}`            | Positive integer              |
| `%{URIPATHPARAM}`      | URI path with params          |

**Notes**

* Use `%{PATTERN:field_name}` to extract into a named field. Unnamed `%{PATTERN}` matches but does not extract.
* For custom regex patterns, `extract_regexp` supports arbitrary named capture groups (see `extract_regexp` for current limitations).

***

### join

Join your query results with results from another query based on matching field values.

**Syntax**

```
join [by (field1, field2)] (subquery) [prefix "prefix_"]
```

| Argument    | Type                   | Description                                              |
| ----------- | ---------------------- | -------------------------------------------------------- |
| `by` fields | field names (optional) | Fields to join on. Performs a `FULL JOIN` when specified |
| `subquery`  | gcQL query             | The right-side query to join with                        |
| `prefix`    | string (optional)      | Prefix for subquery fields to avoid name conflicts       |

**Examples**

```
* | join by (trace_id) (span_type:http | fields trace_id, status_code)
```

*Enrich results with HTTP status codes from related spans*

***

### len

Calculate the byte length of a string field.

**Examples**

```
* | len(content) as content_length | fields timestamp, content_length | limit 10
```

*Show the byte length of each log message*

```
* | len(content) as content_length | stats avg(content_length) as avg_log_size
```

*Average log message size in bytes*

**Notes**

* Returns length in bytes, not characters.

***

### limit

Limit the number of results returned.

**Examples**

```
* | fields timestamp, level, content | limit 10
```

*First 10 log records*

```
level:error | fields timestamp, workload, content | limit 50
```

*First 50 error records*

```
* | stats by (workload) count() as cnt | sort by (cnt desc) | limit 10
```

*Top 10 workloads by record count*

{% hint style="info" %}
When querying raw log records, use the `fields` pipe before `limit` to select columns first (e.g., `* | fields timestamp, level, content | limit 10`). After `stats` and on traces, `limit` works directly.
{% endhint %}

***

### math

Perform arithmetic calculations on fields. Use this to derive new values from existing fields.

**Syntax**

```
math expression as alias
```

**Operators:** `+`, `-`, `*`, `/`, `%` (modulo), `^` (power)

**Functions:** `max(a, b)`, `min(a, b)`, `abs(x)`, `round(x)`, `ceil(x)`, `floor(x)`, `ln(x)`, `exp(x)`, `now()`, `rand()`

**Examples**

```
* | stats by (workload) count() as total | math total * 2 as doubled | limit 10
```

*Double the count for each workload*

```
* | stats by (workload) avg(duration_seconds) as avg_dur | math round(avg_dur * 1000) as avg_ms | limit 10
```

*Convert average latency from seconds to milliseconds*

```
* | stats by (workload) sum(bytes_sent) as sent, sum(bytes_received) as received | math sent + received as total_bytes | limit 10
```

*Total bytes transferred per workload*

***

### offset

Skip a number of results for pagination. Typically used with `limit`.

**Examples**

```
* | fields timestamp, level, content | offset 20 | limit 10
```

*Records 21–30 (skip the first 20)*

```
* | stats by (workload) count() as cnt | sort by (cnt desc) | offset 10 | limit 10
```

*Workloads ranked 11th–20th by count*

***

### rename

Rename fields in the output.

**Syntax**

```
rename old_name as new_name [, old_name2 as new_name2, ...]
```

**Examples**

```
* | rename duration_seconds as latency | fields timestamp, latency, workload | limit 10
```

*Rename duration\_seconds to latency for readability*

```
* | stats by (workload) count() as cnt | rename cnt as total_count | limit 10
```

*Rename the count alias*

***

### sort

Sort results by one or more fields. Default order is ascending.

**Syntax**

```
sort by (field1 [asc|desc], field2 [asc|desc], ...)
```

**Examples**

```
* | stats by (workload) count() as cnt | sort by (cnt desc) | limit 10
```

*Top 10 workloads by log count*

```
* | stats by (level) count() as cnt | sort by (cnt asc) | limit 10
```

*Levels sorted from least to most frequent*

```
* | stats by (workload) avg(duration_seconds) as avg_dur | sort by (avg_dur desc) | limit 10
```

*Slowest workloads by average latency*

***

### split

Split a string field by a separator into an array. Often used with `unroll` to expand the array into separate rows.

**Syntax**

```
split 'separator' from field [as alias]
```

**Examples**

```
* | split ',' from tags as tag_array | limit 10
```

*Split comma-separated tags into an array*

```
* | split ':' from key_value as parts | limit 10
```

*Split a key:value string on the colon*

***

### stats

Aggregate data using statistical functions, optionally grouped by one or more fields. This is the primary aggregation pipe in gcQL.

**Syntax**

```
stats [by (field1, field2, ...)] func1() [as alias1], func2() [as alias2], ...
```

**Examples**

```
* | stats count()
```

*Total record count*

```
* | stats by (workload) count() as total
```

*Record count per workload*

```
* | stats by (env, cluster, namespace, workload) avg(duration_seconds) as avg_latency
```

*Average latency broken down by environment, cluster, namespace, and workload*

```
* | stats by (resource) quantile(0.99, duration_seconds) as p99
```

*p99 latency per resource*

**Notes**

* Only `count()` takes no arguments. All other stats functions require a field name.
* Use `quantile(phi, field)` for percentiles — see the `quantile` section below.

***

### total\_stats

Compute running totals across the result set. Each output row includes the cumulative aggregate up to that point.

**Syntax**

```
total_stats func1() as alias1 [, func2() as alias2]
```

**Examples**

```
* | stats by (workload) count() as cnt | total_stats count() as running | limit 10
```

*Running count across workload groups*

```
* | total_stats count() as running_total | limit 5
```

*Cumulative record count*

{% hint style="info" %}
On logs, `total_stats` works most reliably after a `stats` aggregation. On traces, it works on both raw and aggregated results.
{% endhint %}

***

### uniq

Get unique combinations of field values, similar to SQL `SELECT DISTINCT`.

**Examples**

```
* | uniq by (workload)
```

*List all unique workload names*

```
* | uniq by (namespace, workload)
```

*All unique namespace + workload combinations*

```
* | uniq by (workload) limit 100
```

*Up to 100 unique workloads*

***

### unpack\_json

Parse a JSON string field into queryable subfields. After unpacking, access fields with dot notation (`prefix.field_name`).

**Syntax**

```
unpack_json from field as prefix
```

**Examples**

```
* | unpack_json from payload as payload | limit 10
```

*Parse the payload field as JSON*

```
* | unpack_json from request_body as req | fields req.user_id, req.action | limit 10
```

*Extract user\_id and action from a JSON request body*

```
* | unpack_json from metadata as meta | stats by (meta.action) count()
```

*Count records grouped by the action field inside a JSON metadata column*

***

### unroll

Expand an array field so each element becomes a separate row. Use after `split` to flatten delimited strings.

**Syntax**

```
unroll field [as alias]
```

**Examples**

```
* | split ',' from content as arr | unroll arr as item | limit 10
```

*Split content by comma, then create one row per item*

```
* | split '/' from resource as parts | unroll parts as part | stats by (part) count()
```

*Split URL paths by "/" and count occurrences of each path segment*

**Notes**

* The target field must be an array. Use `split` first to create an array from a string field.
* Use a different alias for the `unroll` output than the `split` output to avoid column name conflicts.

***

### Stats Functions

Stats functions are used inside the `stats` pipe (and related pipes like `total_stats`) to compute aggregate values. You can use multiple stats functions in a single `stats` call.

***

### avg

Average (mean) of a numeric field.

**Examples**

```
* | stats by (workload) avg(duration_seconds) as avg_latency
```

*Average latency per workload*

```
* | stats avg(duration_seconds) as overall_avg
```

*Overall average latency*

***

### count

Count the number of records. The only stats function that takes no arguments.

**Examples**

```
* | stats count()
```

*Total record count*

```
* | stats by (workload) count() as total
```

*Records per workload*

```
* | stats by (level) count() as log_count
```

*Records per log level*

***

### count\_empty

Count records where a field is empty or missing.

**Examples**

```
* | stats by (workload) count_empty(error_message) as missing_errors
```

*How many records per workload have no error message?*

```
* | stats count_empty(trace_id) as logs_without_trace
```

*Logs that are not associated with a trace*

***

### count\_uniq

Count the number of unique (distinct) values for a field.

**Examples**

```
* | stats by (workload) count_uniq(user_id) as unique_users
```

*Unique users per workload*

```
* | stats by (namespace) count_uniq(pod) as pod_count
```

*Number of distinct pods per namespace*

**Notes**

* To see the actual unique values (not just the count), use `uniq_values` instead.

***

### max

Maximum value of a numeric field.

**Examples**

```
* | stats by (workload) max(duration_seconds) as max_latency
```

*Slowest request per workload*

```
* | stats by (resource) max(response_time) as slowest
```

*Slowest response time per resource*

***

### median

Median (50th percentile) of a numeric field. Equivalent to `quantile(0.50, field)`.

**Examples**

```
* | stats by (workload) median(duration_seconds) as median_latency
```

*Median latency per workload*

```
* | stats median(duration_seconds) as overall_median
```

*Overall median latency*

***

### min

Minimum value of a numeric field.

**Examples**

```
* | stats by (workload) min(duration_seconds) as min_latency
```

*Fastest request per workload*

```
* | stats by (resource) min(response_time) as fastest
```

*Fastest response time per resource*

***

### quantile

Calculate percentile values. Use `phi` between 0 and 1 (e.g., 0.50 = p50, 0.95 = p95, 0.99 = p99).

**Syntax**

```
quantile(phi, field)
```

**Examples**

```
* | stats by (workload) quantile(0.50, duration_seconds) as p50
```

*p50 latency per workload*

```
* | stats by (workload) quantile(0.95, duration_seconds) as p95
```

*p95 latency per workload*

```
* | stats by (resource) quantile(0.99, duration_seconds) as p99
```

*p99 latency per resource*

{% hint style="warning" %}
Do **not** use `p50()`, `p95()`, or `p99()` — these functions do not exist in gcQL. Always use `quantile()`.
{% endhint %}

***

### row\_any

Return any single (sample) value from a field within each group. Useful for grabbing an example value alongside an aggregation.

**Examples**

```
* | stats by (workload) row_any(content) as sample_log, count() as cnt
```

*One sample log line per workload, plus the count*

```
* | stats by (level) row_any(workload) as example_workload, count() as cnt
```

*An example workload for each log level*

**Notes**

* The returned value is arbitrary and non-deterministic.

***

### sum

Sum of values for a numeric field.

**Examples**

```
* | stats by (workload) sum(bytes_sent) as total_bytes
```

*Total bytes sent per workload*

```
* | stats by (namespace) sum(request_count) as total_requests
```

*Total requests per namespace*

***

### sum\_len

Sum the byte lengths of all values in a string field.

**Examples**

```
* | stats by (workload) sum_len(content) as total_log_bytes
```

*Total log volume in bytes per workload*

```
* | stats sum_len(content) as total_bytes
```

*Total log volume across all records*

***

### uniq\_values

Collect unique values from a field into an array.

**Examples**

```
* | stats by (workload) uniq_values(level) as log_levels
```

*Which log levels does each workload produce?*

```
* | stats by (namespace) uniq_values(pod) limit 10 as sample_pods
```

*Up to 10 unique pod names per namespace*

**Notes**

* Returns an array of distinct values. Use `limit` inside the function to cap the array size.

***

### values

Collect all values from a field into an array, including duplicates.

**Examples**

```
* | stats by (workload) values(status_code) as all_status_codes
```

*All status codes seen per workload (with duplicates)*

```
* | stats by (workload) values(level) as all_levels
```

*All log levels per workload (with duplicates)*

**Notes**

* For distinct values only, use `uniq_values` instead.
* The `limit N` modifier is accepted syntactically but may not reliably cap the number of values in the returned array. Use `uniq_values(field) limit N` for bounded results.
