FuseQL examples and use cases

This page combines a syntax-pattern cheatsheet with worked use-case scenarios. The first sections (Filtering, Aggregations, Pipeline operators) provide short, self-contained snippets built against the sample log lines below. For full operator detail, see the dedicated operator pages — text search, comparison, aggregation, parse, string.

Sample log lines

The examples below are written against these five representative log lines. They cover the most common log shapes you encounter in practice — labeled key=value, structured JSON, free-form text, and delimited records.

nginx access log

A typical web server access log written by nginx or similar reverse proxies. The fields path, status, and duration_ms are key=value facets embedded in the log body, distinct from the source label applied at ingestion.

source="nginx" path=/api/orders status=503 duration_ms=842
none
Java app log

A structured application log. level and msg are common label/facet names. trace_id links this log line to a distributed trace — useful for correlation across services.

source="orders-service" level="ERROR" msg="Connection refused" trace_id=abc123
none
Plain text key=value record

An unstructured log line where fields are embedded inline in key=value format. These fields are not indexed at ingestion — you must extract them with a parse operator at query time before you can aggregate or filter on them.

source="payments" user_id=u123 amount=42.50 latency_ms=420
none
JSON-structured log

A log line whose body is a pure JSON object. source is a label attached by the log shipper at ingestion — it is not part of the log body. level, user.id, and http.status are fields inside the JSON body. Use | json "path" with dot-notation to extract nested fields.

{"level":"warn","user":{"id":"u123"},"http":{"status":500,"path":"/cart/checkout"}}
none

Filter on the source label and then extract body fields:

source="checkout" | json "http.status" as status | where status >= 500
fuseql
Delimited record (used by split examples)

A comma-separated audit record with no field names. The raw log body is the CSV — source is a label attached at ingestion and is not included in __kf_msg (the raw message body). Columns are zero-indexed, so column 0 is the timestamp.

2026-06-06T11:23Z,login,u123,success,10.0.0.7
none

Filter by the source label, then split the CSV body into named columns:

source="audit" | split __kf_msg extract 0 as ts, 1 as event, 2 as user, 3 as outcome, 4 as ip
fuseql

Filtering

Search filters appear before the first | in a FuseQL query. They narrow the set of log lines the pipeline operates on and are evaluated against the index — they do not scan raw log bodies. Filtering early is the single most effective way to keep queries fast.

Two types of fields are filterable before the pipe:

  • Labels — metadata attached to each log line at ingestion time, such as source, level, kube_namespace. Use bare name="value" syntax.

  • Facets — structured fields extracted from the log body at ingestion and indexed separately. Use @name syntax.

Fields that are only present in the raw log body text (like user_id in the key=value line) cannot be filtered before the pipe — extract them with parse first, then use | where to filter.

Label equality and inequality

Labels are the most efficient filter. They are indexed at ingestion, so equality checks against them incur near-zero scan cost regardless of volume. Always start with a label filter (source=, level=, kube_namespace=) to scope the query before adding any pipe stages.

Match a source exactly — restricts all downstream pipe stages to nginx lines only:

source="nginx"
fuseql

Exclude a label value. Returns every line in the query window except those from the orders-service:

source!="orders-service"
fuseql

Combine multiple label filters on the same line — both conditions must be true (implicit and):

source="nginx" level="ERROR"
fuseql

Facet equality, range, and regex

Facets are structured fields extracted from the log body at ingestion. They support equality, numeric comparison, and regex. Use @ prefix to reference a facet.

Exact match on a string facet — find all activity for a specific user:

@user_id="u123"
fuseql

Numeric comparison — find slow requests or error responses. Useful for SLO burn-rate queries where you want only the lines that breach a threshold:

@status>=500
fuseql
@duration_ms>1000
fuseql

Regex on a facet — matches any user_id whose entire value is u followed by one or more digits (for example, u42 or u1000). Patterns are automatically anchored, so u[0-9]+ will not match u123-admin. Use this when values follow a known pattern but you don’t know the exact string. Regex matching is more expensive than equality; prefer equality or range filters where possible:

@user_id=~"u[0-9]+"
fuseql

Substring, prefix, and suffix

These operators match against field values using positional string checks. They are most useful when you know part of a value but not the full string — for example, filtering API paths by prefix or identifying error classes by suffix.

Substring contains (**) — matches any log line where the label kube_deployment contains the substring. The match is case-sensitive. Scoping the search to a label makes it more precise than a free-text search, because the match applies only to that field’s value rather than the full log line:

kube_deployment**"ingress-nginx"
fuseql

Starts-with (*~) — restricts to paths under /api. The match is case-sensitive. Using a facet (@path) makes this more precise than a free-text search, because the match is scoped to a single field rather than the full log line:

@path*~"/api"
fuseql

Ends-with (~*) — matches any path ending with /checkout regardless of the base URL. The match is case-sensitive. Using a facet (@path) scopes the match to that field rather than the full log line:

@path~*"/checkout"
fuseql

Free-text terms and grep

FuseQL has two free-text search operators that both scan the full log line — labels and raw message body — but work differently:

  • Term search (single quotes '…') looks up complete tokens in an inverted index. It is fast and case-insensitive. Tokens are delimited by whitespace and punctuation, so 'error' matches error and error_count (underscore splits the token) but not errored (no boundary after error).

  • Grep (double quotes "…") scans the reconstructed log body character-by-character. It is slower, case-sensitive, and matches any character sequence regardless of boundaries — "error" matches errored, errors, and error_count. Use grep when you need an exact phrase, a partial-word match, or a wildcard pattern.

Term existence — matches any log line where error appears as a complete token. Case-insensitive. Matches error and error_count (punctuation boundary), but not errored (no boundary between error and ed):

'error'
fuseql

Negated term — excludes any line containing the word healthcheck. Use this to suppress high-volume polling traffic that would otherwise dominate your results:

!'healthcheck'
fuseql

Grep — case-sensitive substring match against the raw log body. Matches the exact character sequence Connection refused, including the capital C. Unlike term search, this would also match ConnectionRefused or Connection refused: if those characters appear in sequence:

"Connection refused"
fuseql

Boolean combinators

Use and, or, and parentheses to express compound conditions. The implicit join between adjacent conditions is and, but explicit and is required when mixing it with or to avoid ambiguity.

Scope to nginx lines AND (high-status OR slow response). Without the parentheses, or would bind only to the two adjacent terms:

source="nginx" and (@status>=500 or @duration_ms>=1000)
fuseql

Match any of several sources. Equivalent to SQL WHERE source IN ('checkout', 'payments'):

source="checkout" or source="payments"
fuseql

Facet existence

Test whether a facet is present. In a log-search query (no aggregation), use the bare @facet form — it matches any line where the facet key exists, regardless of value:

@trace_id
fuseql

When combining with aggregation operators, use != "" to filter for lines where the facet is present and non-empty:

@trace_id != "" | count by source
fuseql

trace_id is present only on lines that are part of a traced request. Using this filter before an aggregation scopes the count to only traced activity.

Aggregations

Aggregation operators collapse many log lines into a summary table. They always follow a pipe (|) and are typically the last stage in a query. The by clause breaks results into groups — omitting it returns a single row for all matched lines.

timeslice is not itself an aggregation operator, but it is the standard way to bucket results by time. A timeslice stage adds a _timeslice column that downstream aggregations can group on. Choose the bucket size based on your query window: 1m buckets for a 1-hour window, 5m for 6 hours, 1h for 24+ hours.

Count and distinct count

count answers "how many log lines matched?" — it counts rows, not unique values. Use it to measure event volume, request rates, or error frequency.

Total log volume bucketed by minute — the foundation of any traffic dashboard:

* | timeslice 1m | count by _timeslice
fuseql

count_unique answers "how many distinct values?" — equivalent to COUNT(DISTINCT field) in SQL. Use it for cardinality questions: how many unique users, sessions, trace IDs, or error fingerprints occurred in a window.

Distinct active users over the time range. A sudden drop may indicate an authentication problem; a sudden spike may indicate a bot:

* | count_unique(@user_id)
fuseql

Numeric aggregations

avg, sum, min, and max all operate on numeric fields. They ignore null values, so lines where the field is absent do not affect the result. After using parse to extract a field from the log body, FuseQL automatically coerces the string value to a number.

Average request latency per endpoint per minute. Grouping by both _timeslice and @path reveals whether a latency spike is global or isolated to a specific route:

source="nginx" | timeslice 1m | avg(@duration_ms) by _timeslice, @path
fuseql

sum is most useful for financial totals or byte counts, where you want the running total rather than the average. min and max show the range — a very large gap between min and max often indicates an outlier worth investigating with p99:

source="payments" | timeslice 5m | sum(@amount) by _timeslice, @currency
source="payments" | timeslice 5m | min(@latency_ms) by _timeslice
source="payments" | timeslice 5m | max(@latency_ms) by _timeslice
fuseql

Percentiles

avg hides the distribution — a small number of very slow requests can shift the mean while the majority of users see normal performance. Percentiles expose the tail. Use p50 (the median) as a robust measure of typical behavior, p95 or p99 for SLO thresholds, and compare p50 against p99 to gauge how heavy the tail is.

p95 of request latency per endpoint per minute. If p95 is 10x higher than p50, you have a long-tail distribution:

source="nginx" | timeslice 1m | p95(@duration_ms) by _timeslice, @path
fuseql

Multiple percentiles in one query — shows the full distribution shape without running separate queries. Note that the aliases must differ from the operator names (p50, p95, p99 clash with the operator names themselves):

source="nginx" | timeslice 5m | p50(@duration_ms) as latency_p50, p95(@duration_ms) as latency_p95, p99(@duration_ms) as latency_p99 by _timeslice
fuseql

Multiple aggregations in one query

Comma-separate aggregation operators in a single pipe stage to compute several metrics at once. The result table has one column per operator, and each column appears as a selectable option in the chart’s Aggregate drop-down. This avoids running two separate queries for two related metrics.

Request count alongside the peak response size — useful for capacity planning where you need to know both how many requests came in and how large they were:

* | timeslice 30s |
  count as number,
  max(@_number_0) as maximum
  by (_timeslice)
fuseql

Grouping by multiple dimensions

Adding more fields to by slices the result further. Each unique combination of values produces its own row. Be mindful of cardinality — grouping by a high-cardinality field like user_id or trace_id on a large dataset produces millions of rows and is slow. Group by low-cardinality fields like source, level, kube_namespace, or status.

Error count per service per minute — shows which service started erroring first and how error volume compares across the fleet:

level="ERROR" | timeslice 1m | count by _timeslice, source
fuseql

Pipeline operators

Pipeline operators transform or enrich each log line as it passes through the query. They appear after the | symbol. The most common pattern is: filter with a search expression → extract fields with parse → filter further with where → aggregate.

parse regex

Use parse regex when the field you want to extract has no consistent surrounding delimiter — for example, when the value could be followed by a space, a comma, or end-of-string. Named capture groups ((?P<name>…​)) become new columns. Regex parsing is the most flexible option but also the most expensive — for simple delimited fields, parse anchor is faster. See parse regex for full syntax and options.

parse regex performs a substring match — the pattern only needs to match somewhere within the log line, not the entire line. This is the opposite of the =~ filter operator, which anchors patterns by default. You do not need to add .* around your pattern to find a match within a longer line.

Extract user_id from the key=value line, where the value ends at a space or end-of-line:

* | parse regex "user_id=(?P<user_id>[a-zA-Z0-9]+)"
fuseql

Given the key=value log line:

source="payments" user_id=u123 amount=42.50 latency_ms=420

The parse stage extracts:

Field Value

user_id

u123

Extract multiple fields in one regex from an nginx access log:

source="nginx" | parse regex "(?P<method>GET|POST|DELETE) (?P<path>[^ ]+) HTTP/(?P<version>[0-9.]+)"
fuseql

Given an nginx access log line such as:

203.0.113.42 - - [30/Jun/2026:12:04:11 +0000] "GET /api/orders HTTP/1.1" 200 1482

The parse stage extracts:

Field Value

method

GET

path

/api/orders

version

1.1

parse anchor

Use parse anchor (parse "prefix*suffix" as field) when the field value is bounded by predictable surrounding text. The * wildcard captures everything between the prefix and suffix strings. It is faster than regex and easier to read for simple cases. See parse anchor for full syntax and options.

Extract user_id from the key=value line using the surrounding text as delimiters. The trailing space after user_id= is the end anchor:

* | parse "user_id=* " as user_id
fuseql

Extract multiple fields with one wildcard pattern — useful for fixed-format log lines like nginx access logs:

source="nginx" | parse "* - - [*] \"* *\" * * *" as ip,date,method,url,status,bytes,rest
fuseql

Given an nginx access log line such as:

203.0.113.42 - - [30/Jun/2026:12:04:11 +0000] "GET /api/orders HTTP/1.1" 200 1482 "-" "Mozilla/5.0"

The parse stage extracts:

Field Value

ip

203.0.113.42

date

30/Jun/2026:12:04:11 +0000

method

GET

url

/api/orders HTTP/1.1 (includes the HTTP version — split further if needed)

status

200

bytes

1482

rest

"-" "Mozilla/5.0"

json extraction

Use | json "path" when log lines have a JSON body. The path uses dot notation to navigate nested objects and bracket notation for arrays. JSON extraction is the right choice for structured logging output from frameworks like Logback, Zap, or structlog.

Given the JSON log body:

{"level":"warn","user":{"id":"u123"},"http":{"status":500,"path":"/cart/checkout"}}

Extract a single nested field:

source="checkout" | json "user.id" as user_id
fuseql
Field Value

user_id

u123

Extract multiple fields in one json stage — field paths and aliases are matched by position:

source="checkout" | json "user.id", "http.status", "http.path" as user_id, status, path
fuseql
Field Value

user_id

u123

status

500

path

/cart/checkout

After extraction, the fields behave like any other column — filter with where, aggregate with count, avg, etc.:

source="checkout" | json "http.status" as status | where status >= 500 | count by status
fuseql

split extraction

Use | split when the log body is a positional delimited record with no field names — CSV output, space-separated audit records, or similar. Extract by zero-based column index.

Extract four columns from the CSV audit line — columns are numbered from 0:

source="audit" | split __kf_msg extract 0 as ts, 1 as event, 2 as user, 3 as outcome, 4 as ip
fuseql

Given the CSV audit log line:

2026-06-06T11:23Z,login,u123,success,10.0.0.7

The split stage extracts:

Column Field Value

0

ts

2026-06-06T11:23Z

1

event

login

2

user

u123

3

outcome

success

4

ip

10.0.0.7

where post-filter

Use | where to filter on a value that only exists after a parse stage. The search expression before the pipe only has access to indexed labels and facets; where has access to anything computed in the pipeline.

Keep only 5xx responses after extracting the HTTP status from the JSON body:

source="checkout" | json "http.status" as status | where status >= 500
fuseql

Combine multiple conditions — and and or work the same as in the pre-pipe search expression:

source="nginx" | parse "* - - [*] \"* *\" * * *" as ip,date,method,url,status,bytes,rest
| where (method="GET" or method="POST") and bytes > 10000
| count by method
fuseql

Typecasting

Fields extracted by parse are strings. Aggregation operators like avg, sum, and p99 require numeric input. Use toInt or toFloat to convert before aggregating. FuseQL also coerces automatically in many cases, but explicit conversion makes the intent clear and avoids silent null results when a value is not parseable.

Parse a status code from the log body and convert it to an integer before aggregating:

* | parse regex "status=(?P<status>[0-9]+)" | toInt(status) as status_int | count by status_int
fuseql

Convert a duration string (e.g. "842ms") to a numeric millisecond value for comparison or averaging:

* | toDuration("842ms") as latency_ns
fuseql

Convert to float to preserve decimal precision when computing averages of financial amounts:

source="payments" | toFloat(@amount) as amount_f | avg(amount_f) as avg_amount
fuseql

Scenarios

These scenarios include the query, what to look for in the output, and specific situations where the analysis is useful.

All scenario queries that start with scan every log line in the selected time range across all sources. In production environments this can be a very large dataset. Scope queries to a specific service with a label filter — for example, source="my-service" — whenever possible. Use an unscoped only when you genuinely need fleet-wide visibility, such as when comparing services against each other.

Count all logs

Count the total number of log lines over time, bucketed into 30-second intervals. The resulting time series reveals traffic shape at a glance: ramp-up patterns at service start, flat lines during quiet periods, and sharp drops that may indicate a process crash or network partition.

source="my-service" | timeslice 30s | count by (_timeslice)
fuseql

When to use this query:

Baselining normal traffic patterns

Run this query over a week-long window (with 1h timeslice) to establish what "normal" volume looks like for your system at different times of day. Once you know the baseline, deviations — unexpected spikes or drops — become immediately visible. This baseline also informs alert thresholds so you avoid false positives during expected high-traffic periods.

Capacity planning and scaling decisions

A steadily rising floor on this chart means your overall log (and likely request) volume is growing. Track it weekly to detect organic growth trends and decide when to add capacity before performance degrades. A sudden step-change upward after a deployment often indicates the new version is more verbose or is generating errors.

Post-deployment validation

After a release, run this query scoped to the deployed service (source="my-service") for the 30 minutes before and after the deployment. A significant change in log volume is an early signal of a regression — increased errors inflate the count, while a crashed process drops it to near zero.

Count all fingerprints

Kloudfuse clusters log lines into fingerprints — templates that capture the structural pattern of a log line with values replaced by placeholders. Two log lines that differ only in their variable values (timestamps, IDs, amounts) share the same fingerprint. Counting distinct fingerprints over time is a measure of structural diversity: how many different kinds of things are happening in your system.

* | timeslice 30s | count_unique(fingerprint) by (_timeslice)
fuseql

When to use this query:

Detecting new code paths

When a deployment introduces new log statements — new error handlers, new feature flags, new integrations — the fingerprint count rises. A jump that correlates with a release is usually expected. A jump that does not correlate with any deployment is a signal that something unexpected is happening: a misconfiguration, an untested code path being triggered, or external input causing novel behavior.

Early anomaly detection without alerting rules

Because fingerprints capture structure rather than values, a new error type shows up as a new fingerprint even before you’ve written an alert for it. Watching the fingerprint count is a low-effort early warning: if the count is stable, the system is producing the same kinds of logs it always has; if it rises, something new is happening that warrants investigation.

Count all logs grouped by level

Splitting log count by severity level turns a single total into a stack of signals. The INFO band tells you how busy the system is; the WARN band surfaces degraded-but-not-broken conditions; the ERROR band is your most actionable signal. Watching all three simultaneously helps distinguish "the system is handling more traffic" from "the system is failing more often."

* | timeslice 30s | count by (_timeslice, level)
fuseql

When to use this query:

Identifying the severity of an incident

During an on-call investigation, the first question is whether a traffic spike is load (more INFO logs) or breakage (more ERROR logs). If INFO volume is flat but ERROR volume jumps, the system is not handling more requests — it is failing the requests it has. If both spike together, the system is under load and beginning to fail at the edges. This distinction guides the response: scaling vs. rollback.

Catching warning storms before they become errors

WARN logs often precede ERROR logs by minutes — a connection pool approaching its limit logs warnings before it starts rejecting connections. Monitoring WARN count separately from ERROR count lets you catch degradation in progress. Set an alert on sustained WARN elevation, not just on ERROR spikes, to get earlier notice.

Count all fingerprints grouped by source

This query adds a service dimension to fingerprint diversity. Instead of asking "is something new happening?", it asks "which service is the source of the new behavior?" In a microservices environment with dozens of services, this is the difference between seeing that a problem exists and knowing where to look.

* | timeslice 30s | count_unique(fingerprint) by (_timeslice, source)
fuseql

When to use this query:

Root-cause localization

When the overall fingerprint count spikes, this query reveals which service contributed the new patterns. If a single service accounts for all the new fingerprints, the investigation is immediately scoped. If multiple services spiked simultaneously, look for a shared dependency — a database, a message queue, or a shared library — that all of them depend on.

Identifying verbosity outliers

One service that produces far more distinct fingerprints than others is either doing far more distinct things (expected) or generating a large variety of errors (unexpected). Comparing fingerprint count to total log count for each source reveals the ratio: a service with high total count but low fingerprint count is repetitive (normal operation or a tight loop); a service with low total count but high fingerprint count is encountering many different conditions.

===Average of a duration or number facet

Averaging a numeric facet over time produces a performance trend line. Response times, queue depths, batch durations, and payment amounts are all candidates. The key insight from averaging is trend direction: is performance stable, gradually degrading, or suddenly changing? It does not tell you about distribution shape — combine with percentiles to get the full picture.

* | timeslice 30s | avg(@duration:duration_seconds) by (_timeslice)
fuseql

When to use this query:

Tracking gradual latency degradation

A sudden spike in average duration is easy to spot. Gradual degradation — average response time rising 10ms per hour — is harder. Running this query with a 24-hour window and a 5-minute timeslice makes the slope visible. Common causes include a growing database table without index maintenance, a memory leak causing increased GC pauses, or a third-party API that is slowly becoming overloaded.

Correlating latency with traffic volume

Run this query alongside the count-all-logs query in two panels on the same dashboard. If average latency tracks with request volume (both rise and fall together), the system is under load and needs more capacity. If latency rises while request volume stays flat or falls, the cause is internal — a downstream dependency, a background job, or a resource contention issue.

Anomaly on count of error logs

Threshold-based alerts require you to know in advance what "too many errors" looks like. Anomaly detection removes that requirement — it learns the historical pattern of error counts (including daily and weekly seasonality) and alerts when the current count deviates from what the model predicts, regardless of what the absolute number is.

core:level="error" | timeslice 120s | count by (_timeslice) | anomaly (_count) by 120s, model=agileRobust, seasonality=hourly, bounds=1, band=3
fuseql

The chart overlays a gray expected range band on the error count time series. Data points outside the band are anomalies, shown in red. The bounds parameter controls sensitivity: 1 flags moderate deviations, 3 flags only severe ones. The agile-robust model accounts for hourly patterns, making it suitable for services with predictable daily traffic cycles.

When to use this query:

Detecting incidents without fixed thresholds

At 3am, 50 errors per minute might be alarming. At 2pm on a Monday, 50 errors per minute might be completely normal. A static threshold fires false positives in one case and misses real incidents in the other. The anomaly model adjusts its expected range to the time of day, so the same alert configuration works around the clock without manual tuning. In the screenshot, the spike around 8:40 breaches the band and appears as a red line — that is the signal to investigate, regardless of whether the absolute count crossed a fixed threshold.

Outlier detection

Where anomaly detection asks "is this service behaving differently than usual?", outlier detection asks "is this service behaving differently than its peers?" It compares multiple time series against each other and flags any that deviate from the group, using the DBSCAN clustering algorithm to define "the group."

level="error" | timeslice 120s | count by (_timeslice, kube_namespace) | outlier (_count) by 120s, model=dbscan, eps=3
fuseql

The query groups error counts by kube_namespace, producing one time series per namespace. The outlier operator then identifies which namespaces behave differently from the cluster as a whole. In the chart, solid lines are outliers; dotted lines are non-outliers. The eps (epsilon) parameter controls the tolerance: lower values make the algorithm more sensitive and flag more series as outliers; higher values require a larger deviation to trigger.

When to use this query:

Identifying the affected component in a multi-service incident

In a distributed system, an incident rarely affects all services equally. Outlier detection shows you which namespaces or services have error patterns that differ from the rest of the fleet. The affected service stands out as a solid line that diverges from the dotted non-outlier series — often minutes before users report a problem. This narrows the investigation to a specific component rather than requiring you to check each service manually.

Log math operator to scale down the Y-axis

When log count varies by orders of magnitude across different services or time periods — for example, a high-traffic service generating 10 million logs per minute alongside a low-traffic service generating 100 — plotting both on the same linear scale makes the low-traffic service invisible. Applying a log (logarithm) or cbrt (cube root) transform compresses the scale so that both series remain visible and comparable.

* | timeslice 1m | count as c by (_timeslice, source) | log(c) as log_count
fuseql

When to use this query:

Comparing services with very different volumes

Without the transform, a single high-volume service dominates the chart and makes it impossible to see what lower-volume services are doing. After log, a service generating 10,000 logs/minute (log₁₀ ≈ 4) and one generating 100 logs/minute (log₁₀ ≈ 2) are both clearly visible on the same chart, and their trends can be compared directly.

Revealing trends hidden by outlier spikes

A single brief traffic spike can compress the rest of the time series to a flat line on a linear scale. log reduces the visual dominance of extreme values, making it easier to see gradual trends — a slow ramp, a daily pattern, or a persistent elevated baseline — that would otherwise be obscured. For scale compression without the mathematical properties of logarithms, cbrt (cube root) is a gentler alternative that works well for counts that include zero.