Learn JJWhale OBSERVE's log query syntax: field filters, full-text matching, and pipeline aggregation, plus how to turn queries into alerts and dashboards, with practical examples and performance tips.
How pleasant your log platform's query syntax is often decides how fast you can resolve an incident. JJWhale OBSERVE models its search language on SQL, so anyone who has written a database query can pick it up in minutes. This article walks through the core syntax and hands you ready-made queries for the most common scenarios.
Every log line is parsed into fields on ingestion, such as level, service, trace_id, and status. Where those fields come from matters: the platform applies parsing rules (JSON, key-value, regex) at the collector or gateway, so a single log line can surface dozens of queryable fields. A line like {"level":"ERROR","service":"order-api","status":500,"latency":1240} becomes five fields you can filter on immediately, with no extra configuration. Field filters use the field = value form, while full-text matching just takes a bare keyword. A few common patterns:
level = "ERROR" AND service = "order-api"
message CONTAINS "timeout"
trace_id = "8f3a2c1d9e7b..."
status IN (500, 502, 503)
Operators include =, !=, >, <, >=, <=, CONTAINS, and IN (...). Combine conditions with AND / OR / NOT, and use parentheses to control precedence. Quote string values to avoid clashing with reserved keywords. Field names are case-sensitive; if a query returns nothing when you expected results, the first thing to check is whether the field actually exists in the parsed schema for that log source.
Filtering alone is rarely enough. Incident work usually starts with "count first, then drill down." JJWhale OBSERVE offers pipeline syntax that chains stages with |:
service = "order-api" AND level = "ERROR"
| stats count() by error_code
| sort count desc
| limit 10
This filters order-api errors, groups by error_code, sorts by count descending, and returns the top ten. stats supports far more than count(): avg(), p50(), p99(), sum(), and distinct_count() all work the same way. For latency, service = "order-api" | stats p99(latency) by endpoint | sort p99 desc gives you a ranked list of slow endpoints in one expression. Unlike exporting logs and processing them in a script, pipeline aggregation runs server-side, so even tens of billions of lines come back within seconds.
Reconstruct a call chain by trace_id: once you have the trace_id from an error, search all correlated logs and append | sort timestamp asc to rebuild the exact sequence of a single request. This is usually faster than clicking through the trace view when you need every log line, not just the spans.
Find slow requests: | stats avg(latency) by endpoint | sort avg desc immediately shows which endpoint is dragging everything down. If health checks pollute the numbers, add a filter to exclude them.
See the error distribution: | stats count() by level | sort count desc tells you at a glance whether the problem is concentrated in one place or spread everywhere.
Time range and sampling: the default window is the last 15 minutes; older data automatically falls back to archive storage. When a query feels slow, narrow the time window and add field filters first, before resorting to a full scan.
Any query you run in the search page can be pinned to a dashboard or turned into an alert rule in two clicks. Pin the "P99 by endpoint" query to a service dashboard and it refreshes on every render; attach the error-rate query to an alert threshold and you get paged before customers notice. Because search, dashboards, and alerting share one syntax, you learn it once and use it everywhere, and the alert condition is always exactly the query you already tested — there is no translation gap.
Think about what you are actually looking for before typing. Use the narrowest field filter to bound the result set, prefer equality filters over CONTAINS, and put filter conditions at the front of a pipeline so downstream stages see less data. CONTAINS on a high-cardinality field is the single most common cause of slow queries. A real example: searching message CONTAINS "user_87321" across a billion lines forces a scan of every message; adding a parsing rule that extracts user_id turns it into user_id = "87321", which hits an index and returns in milliseconds. Once these habits become second nature, incident debugging gets dramatically faster.