Keyword search falls short when you need aggregation. JUJING OBSERVE ships SQL-like log querying—SELECT, WHERE, GROUP BY, and piped processing—with automatic field parsing and no pre-built indexes. This post walks through real queries for finding slow endpoints and tracing user sessions.
Most teams still use the log search box as a glorified keyword filter. That's fine for "any errors in the last few minutes," but it falls apart the moment you need to answer "which endpoint is slowest" or "what exactly happened during this user's request." Keyword search is great at finding needles, not at telling you how many needles there are and where they cluster.
JUJING OBSERVE ships a SQL-like query language in its log search: SELECT, WHERE, GROUP BY, ORDER BY, plus piped processing. Fields are parsed out of your logs automatically — no pre-built indexes, no writing a separate regex for every log shape your services emit.
The simplest filter: all 5xx errors in the last five minutes.
SELECT * FROM logs
WHERE status >= 500
AND time > now() - 5m
Group by endpoint to find the ten slowest over the past hour:
SELECT api, count(*) AS cnt, p95(latency) AS p95
FROM logs
WHERE time > now() - 1h
GROUP BY api
ORDER BY p95 DESC
LIMIT 10
p95() is a built-in aggregate. latency comes from a numeric field parsed automatically — if your log line contains a key-value pair like latency=182, the platform turns it into a numeric column you can aggregate, normalized to milliseconds. No schema to declare, no mapping file to maintain.
Here's how you reconstruct one user's full path through the system:
SELECT trace_id, min(time) AS start_ts, max(time) AS end_ts
FROM logs
WHERE user_id = 'u_88213'
AND time > now() - 30m
GROUP BY trace_id
ORDER BY start_ts
This is the kind of question that's painful to answer with grep — the user's activity is scattered across a dozen services and thousands of lines — but trivial once trace_id and user_id are first-class columns.
Unlike writing one full SQL statement, pipes match how you actually debug. Chain operations with |; each step's output feeds the next:
FROM logs
| WHERE status >= 400
| SELECT api, status, count(*) GROUP BY api, status
| ORDER BY count(*) DESC
| LIMIT 20
The real advantage: you can delete the later steps to inspect intermediate results without knowing the final answer up front. Start with a broad filter, look at what comes back, then tighten. It turns log analysis from a "compose a perfect query" exercise into a "follow the data" loop.
Not every log is JSON. Say your Nginx access log looks like this:
192.168.1.10 - - [13/Sep/2026:10:02:11] "POST /api/order 200" 0.182
An extraction expression splits the meaningful segments into fields:
FROM logs
| PARSE 'ip - - [date] "method path status" latency'
After that you can WHERE latency > 1 or group by path. Save extraction rules as templates and they apply automatically to matching log shapes, so the whole team shares one definition instead of each person maintaining their own regex with slightly different edge cases.