LPQL Syntax Reference
LPQL (LogPulse Query Language) is a powerful search language for querying, filtering, and analyzing your log data. It supports field expressions, boolean logic, time ranges, and a rich set of pipe commands for transformation and aggregation.
Overview
A LPQL query starts with an optional search expression, followed by optional time range modifiers, and zero or more pipe commands separated by the pipe character (|).
[search] <expression> [earliest=<time>] [latest=<time>] [| command1] [| command2] ...Here are some common query patterns:
level=error
level=error source=api host=prod-*
(level=error OR level=fatal) source=payment earliest=-1h
level=error | stats count by host | sort -count | head 10Search Expressions
Field Expressions
Field expressions compare a field name to a value using comparison operators. Field names can contain letters, digits, underscores, and dots.
| Operator | Description | Example |
|---|---|---|
| = or == | Exact match | level=error |
| != | Not equal | status!=200 |
| > | Greater than | duration>1000 |
| < | Less than | status<400 |
| >= | Greater or equal | host>=prod-01 |
| <= | Less or equal | latency<=100 |
level=error
status!=200
duration>1000
host>=prod-01
field="quoted value"Values support wildcards with * for pattern matching:
source=api-* # starts with "api-"
host=*prod* # contains "prod"
field=* # field exists and is non-empty
field!=* # field does not exist or is emptyText Search
Free-text search matches against the event message field. Unquoted terms are matched individually, quoted strings match exact phrases, and wildcards allow pattern matching.
error # free text search
"connection refused" # exact phrase (double quotes)
'timeout error' # exact phrase (single quotes)
*exception* # wildcard text searchBoolean Logic
Combine expressions with AND, OR, and NOT operators. Spaces between expressions act as implicit AND. Use parentheses to control evaluation order. Operator precedence (lowest to highest): OR, AND, NOT.
# Implicit AND (space-separated)
level=error source=api
# Explicit operators
level=error AND source=api
level=error OR level=warn
NOT debug
# Grouping with parentheses
(level=error OR level=fatal) AND source=api
# Operator precedence: NOT > AND > OR
level=error OR level=warn NOT debugIN Operator
The IN operator matches a field against a list of values. Values can include wildcards.
level IN (error, fatal, critical)
source IN ("api-gateway", "web-server")
host IN (prod-*, dev-*)Time Ranges
Use earliest and latest to limit the time window. Supports relative time offsets and absolute ISO 8601 timestamps.
| Unit | Meaning | Example |
|---|---|---|
| m | Minutes | earliest=-15m |
| h | Hours | earliest=-1h |
| d | Days | earliest=-7d |
| w | Weeks | earliest=-1w |
| M | Months | earliest=-1M |
| y | Years | earliest=-1y |
# Relative time
earliest=-1h
earliest=-15m latest=now
# Absolute time
earliest=2024-01-01T00:00:00Z latest=2024-12-31T23:59:59Z
# Combined with search
earliest=-1h error status>500
level=error earliest=-24h latest=-1hIndex time (_indextime)
Every event has two times. _time is the time of the event itself, taken from the event when a parser reads it. _indextime is when LogPulse indexed the event. _indextime is hidden from results and the field list until you name it in a search, as in Splunk.
Use _index_earliest and _index_latest to filter on when events arrived, on top of the normal time range. They take the same values as earliest and latest. In eval and where, time fields count as epoch seconds, so _indextime-_time is the indexing delay in seconds.
# Everything indexed in the last 15 minutes, whatever its event time
earliest=-7d _index_earliest=-15m
# Late arrivals: indexed in the last hour, but more than an hour old
earliest=-24h _index_earliest=-1h | where _indextime-_time > 3600
# Indexing delay per source
earliest=-1h | eval lag=_indextime-_time | stats avg(lag) max(lag) by source
# Show the index time as a column
earliest=-15m | eval indexed=_indextime | table _time, indexed, sourceEvents indexed before index time was introduced have _indextime equal to _time.
Known Fields
LogPulse recognizes these built-in fields that map directly to stored columns:
| Field | Description |
|---|---|
| event | Log message / event text |
| level | Log level (debug, info, warn, error, fatal) |
| index | Index name |
| source | Source / file name |
| sourcetype | Source type |
| host | Hostname |
| _time / timestamp | Event time (from the event when a parser reads it) |
| _indextime | When LogPulse indexed the event; hidden until named |
| cluster | Kubernetes cluster |
| namespace | Kubernetes namespace |
| pod | Kubernetes pod |
| container | Kubernetes container |
| node | Kubernetes node |
Dynamic fields are stored in map columns and accessed via dot notation:
# Map columns use dot notation
labels.app="my-service"
annotations.owner="platform-team"
attributes.status="active"
parsed_fields.request_id="abc-123"JSON Paths
Events often contain nested objects and arrays. JSON paths let you walk into those structures anywhere a field name is accepted: in search expressions, where, eval, stats, timechart, table, and more. Paths combine dot notation with bracket syntax:
| Syntax | Meaning | Example |
|---|---|---|
| a.b.c | Walk into nested objects | user.profile.country |
| a[N] | Array index (0-based) | io_data[0].value |
| a[*] | Any array item | io_data[*].name |
| a[k="v"] | Filter array by predicate | io_data[io="239"].value |
Use them directly as a field in search expressions and where filters:
# Filter events by a nested value
io_data[io="239"].name=Movement
user.profile.country=NL
# Combine with boolean logic
level=error AND io_data[io="66"].value > 0
# In the where pipe (eval expressions)
| where io_data[io="66"].value > 0
| where user.profile.country != "NL"In eval, a JSON path behaves like any other field: read from it, compute with it, alias it:
# Extract a nested value into a top-level field
| eval voltage = io_data[io="66"].value
# Compute with it
| eval voltage_ok = if(io_data[io="66"].value >= 12, "ok", "low")
# Combine paths from multiple sources
| eval full = user.profile.country . "-" . io_data[0].nameAggregate and group by nested values with stats and timechart:
# Latest voltage reading per device
| stats latest(io_data[io="66"].value) as voltage by device_id
# Average temperature over time
| timechart span=1h avg(sensors[type="temp"].reading)
# Group by a nested value
| stats count by user.profile.countryProject nested values as output columns with table:
# Project nested values as columns
| table timestamp, device_id, io_data[io="66"].value
# Mix with dot-notation and regular fields
| table host, user.profile.country, statusComparison against a number (e.g. value < 12) automatically coerces the extracted string to a number. Predicate filters like [io="239"] match string equality on the field inside each array item.
Pipe Commands
Pipe commands transform, filter, and aggregate search results. Chain multiple commands with the pipe (|) character.
stats
Aggregates results by one or more fields, computing statistical functions.
| stats count by level
| stats avg(duration) as avg_dur by source
| stats count, sum(amount), avg(price) by level, host
| stats dc(host) as unique_hosts by level
| stats latest(io_data[io="66"].value) as voltage by device_idAvailable aggregation functions:
| Category | Functions |
|---|---|
| Count | count, c |
| Average | avg, mean |
| Sum | sum, sumsq |
| Min/Max | min, max |
| Distinct | dc, distinct_count |
| Statistical | median, mode, stdev, stdevp, var, variance, range |
| Selection | values, list, first, last, latest, earliest |
| Percentile | perc50, p95, exactperc99, upperperc75 |
timechart
Creates time-bucketed aggregations for charting. Similar to stats but automatically groups by time intervals.
| timechart count
| timechart span=5m count
| timechart span=1h count by level
| timechart span=1h cont=true avg(duration) by sourceAvailable options:
| Option | Description | Example |
|---|---|---|
| span | Time bucket size | span=5m, span=1h, span=1d |
| cont | Fill gaps with zeros | cont=true (default) |
| by | Split by field (max 1) | by level |
chart
Aggregates over an arbitrary field on the x-axis (over), with an optional split field for the columns. Like stats, but shaped for charting.
| chart count over source
| chart count over source by level
| chart avg(duration) over host limit=10mstats
Aggregates metrics from the dedicated metrics store (OTLP ingest). Filter with where on index and metric_name. rate() and increase() are counter-reset aware; percentiles (p95, perc99) use histogram quantiles.
| mstats avg(value) where metric_name=cpu_usage by host
| mstats span=5m max(value) where index=otlp metric_name=memory_bytes
| mstats rate(value) where metric_name=http_requests_total by route span=1m
| mstats p95(value) where metric_name=http_duration by routempreview
Returns raw metric data points from the metrics store without aggregation. Useful to inspect what a metric looks like before writing an mstats query.
| mpreview index=otlp metric_name=cpu_usageeval
Computes new fields from expressions. Supports arithmetic, string concatenation, comparisons, and a rich set of built-in functions.
| eval ratio = count / total
| eval msg = "Status: " . status
| eval is_error = (status >= 500)
| eval x=1, y=2, z=x+y
| eval voltage = io_data[io="66"].value| Category | Operators |
|---|---|
| Arithmetic | + - * / % |
| Comparison | == != < > <= >= LIKE |
| Boolean | AND OR NOT XOR |
| String | . (concatenation) |
| Unary | - (negation) |
where
Filters results using eval expressions. Only events where the expression evaluates to true are kept.
| where status > 400
| where level == "error" OR level == "fatal"
| where duration/1000 > 5
| where like(source, "api%")
| where io_data[io="66"].value < 12table
Selects specific fields to display in the output. Supports wildcard patterns for field names.
| table level, event, host
| table timestamp, source_*, host
| table *
| table timestamp, device_id, io_data[io="66"].valuetop / rare
top shows the most common values of a field, rare shows the least common. Both support a limit, percentage display toggle, and split-by fields.
| top level
| top 5 host
| top limit=10 showperc=false source
| top level by host
| rare level
| rare 5 host by sourcededup
Removes duplicate events based on one or more fields. Optionally keep a set number of duplicates or sort by a specific field.
| dedup host
| dedup 3 level
| dedup host, source
| dedup keepevents=true sortby=timestamp hostsort
Sorts results by one or more fields. Use - prefix for descending order, + or no prefix for ascending.
| sort timestamp # ascending (default)
| sort -level # descending
| sort limit=100 -duration timestamphead / tail
head returns the first N results, tail returns the last N. Default is 10.
| head # first 10 results (default)
| head 20 # first 20 results
| tail 50 # last 50 resultsfields
Include (+) or exclude (-) specific fields from the output. Supports wildcard patterns.
| fields level, event, host # include only these
| fields +source # include
| fields -attributes # exclude
| fields source_*, host # wildcard includerename
Renames fields in the output using field as new_name syntax.
| rename old_field as new_field
| rename level as log_level, host as hostnamerex
Extracts new fields using regular expressions with named capture groups. Defaults to extracting from the event field.
| rex field=event "(?<app>\w+)-(?<env>\w+)"
| rex "(?<status_code>\d{3})"search
Applies an additional search expression mid-pipeline, using the same syntax as the root search. Useful after commands that create new fields.
| search level="error" OR status>500
| stats count by host | search count > 100fillnull / filldown
fillnull replaces null values with a constant (value=...); filldown carries the previous non-null value forward into nulls, which works well after sort.
| fillnull value="0" count
| fillnull value="unknown" host, source
| sort timestamp | filldown hostbin / bucket
Buckets numeric or time values into discrete groups using span or bins; bucket is an alias. Use as to name the output field, and combine with stats to aggregate per bucket.
| bin response_time_ms span=100 as latency_bucket
| bin timestamp span=1h as hour
| bin duration bins=10 | stats count by durationjoin
Joins results with a subsearch on shared fields. Defaults are type=inner and max=1 match per key; use type=left to keep events without a match.
| join host [search source=metadata | table host, region]
| join type=left user_id [search index=_audit | stats count by user_id]makeresults
Generates synthetic events. Useful for testing eval expressions or producing fixed rows without searching any data.
| makeresults | eval msg="hello"
| makeresults count=3 | eval host="test", level="info"transpose
Pivots rows into columns. Run it after stats or timechart to flip the result orientation, e.g. one column per group instead of one row.
| stats count by level | transpose
| timechart count by source | transpose header_field=_timelookup
Enriches events by joining a lookup dataset (LEFT JOIN on a single key field). OUTPUT selects which dataset columns to add, optionally with an alias. Two lookups are built in and read-only: identities (users, with roles and privileged) and assets (hosts, IPs and other entities), both keyed on key and matched case-insensitively.
| lookup users user_id OUTPUT name, email AS user_email
| lookup ip_allocations src_ip
| lookup assets key AS hostname OUTPUT owner, criticality
| lookup identities key AS actor_email OUTPUT roles, privileged
| lookup identities key AS actor_email OUTPUT roles | where NOT match(roles, "cloudflare_admin")inputlookup / outputlookup
inputlookup emits the rows of a lookup dataset as events and must be the first pipe in the query. outputlookup writes the pipeline result to a lookup dataset and must be the last.
| inputlookup users
| inputlookup ip_allocations where region="EU" | stats count by region
index=apps level=error | stats count by service | outputlookup error_baseline
index=apps | stats count by host | outputlookup append=true host_countsthreatintel / iplookup
Enriches events with reputation columns from the system threat-intel store, matched on the given IP, domain, or hash field. By default it adds threat_verdict, threat_sources, threat_categories, and threat_confidence; iplookup is an alias.
| threatintel src_ip
| threatintel dest_ip OUTPUT threat_verdict AS verdict, threat_confidence
| threatintel src_ip | where threat_verdict = "malicious"iplocation
Geo-enriches events by resolving an IP field (default src_ip) against the GeoIP database, adding country, city, lat, lon, and asn. The lat/lon columns feed the map visualization directly. Unknown IPs yield empty values.
| iplocation
| iplocation client_ip | stats count by country
| iplocation src_ip | stats count by lat, lon
| iplocation src_ip | table src_ip, country, city, asnEval Functions Reference
These functions can be used in eval and where commands:
| Category | Functions |
|---|---|
| Conditional | if(cond, a, b), case(...), coalesce(a, b, ...) |
| String | lower, upper, len, substr, trim, ltrim, rtrim, replace, split, urldecode, urlencode |
| Conversion | tostring, tonumber |
| Math | round, ceil, floor, abs, pow, sqrt, log, ln, exp, pi, min, max |
| Type checks | isnull, isnotnull, isnum, isstr, typeof, null |
| Time | now, strftime, strptime, relative_time, time |
| Pattern | match, like, searchmatch, cidrmatch |
| Multivalue | mvindex, mvcount, mvjoin, mvappend, mvdedup, mvsort, mvfilter, mvfind |
| Crypto | md5, sha1, sha256, sha512 |
| eval greeting = if(level=="error", "ALERT", "OK")
| eval name = lower(host)
| eval dur_sec = round(duration / 1000, 2)
| eval is_prod = like(host, "prod%")
| eval hash = md5(event)