A hands-on look at JJHub OBSERVE's SQL-style log search: operators, aggregation, pipeline functions, and copy-paste queries for common troubleshooting scenarios. Turn log investigation from paging through raw lines into writing queries.
The slowest part of log troubleshooting is rarely finding the bug. It's digging a handful of relevant lines out of hundreds of thousands. Keyword search only tells you whether a given line exists. It can't answer questions like "how are 5xx errors distributed by service over the last hour" or "which span under this traceId took the longest". Answering those with grep or a basic search box means exporting data, opening a spreadsheet, or writing a throwaway script - work that eats the first half hour of every incident.
JJHub OBSERVE stores logs in a columnar layout under the hood and exposes a SQL-like query language on top. Ops and developers write SELECT, WHERE, and GROUP BY directly to filter, aggregate, sort, and join across fields. A line of debugging reasoning becomes a single query. Once a team gets comfortable with the syntax, incident response shifts from "search, export, eyeball" to "run the saved query and look at the result". This matters most for engineers who already think in SQL - database and backend folks pick it up in minutes rather than days.
The difference from full-text search matters in practice. Keyword search treats a log line as an opaque string; SQL treats fields as typed columns. That means you can compare, sort, and aggregate on level, latency, or status_code the way you would on database columns, and the query planner can skip whole partitions using the time range instead of scanning everything.
The syntax mirrors standard SQL, so most engineers have no learning curve. Fields are referenced by name - level, service, trace_id, status_code - and behave like typed columns rather than free text. The operator set covers everything you need for filtering:
A representative query - errors from the payment service in the last hour:
SELECT * FROM logs
WHERE service = 'payment' AND level = 'ERROR'
AND timestamp >= now() - 3600
ORDER BY timestamp DESC
LIMIT 100
The now() function is relative to query time, which makes saved queries reusable: the same statement always means "the last hour" no matter when it runs. Use it instead of hardcoding timestamps.
Beyond row filtering, aggregation is where log search earns its keep. GROUP BY with aggregate functions turns raw lines into a distribution in one shot:
SELECT service, count(*) AS err_count
FROM logs
WHERE level = 'ERROR' AND timestamp >= now() - 900
GROUP BY service
ORDER BY err_count DESC
Pipeline functions handle statistics and time-series on top of that. histogram(timestamp, '1m') buckets events by minute, percentile(latency, 99) computes P99, and topk(5, count) returns the top five. Combined, they let a single query return both a trend and its outliers with no post-processing. This is the difference between "I found a lot of errors" and "orders-service had 143 errors between 14:03 and 14:06, P99 latency jumped to 2.4s, all on the /checkout path".
Slow requests by endpoint, ranked by tail latency:
SELECT path, avg(latency) AS avg_ms, percentile(latency, 99) AS p99
FROM logs
WHERE latency > 1000 AND timestamp >= now() - 1800
GROUP BY path
ORDER BY p99 DESC
Reconstruct a call chain from a traceId, ordered by time:
SELECT timestamp, service, span_name, duration_ms
FROM logs
WHERE trace_id = '8f3a2c...'
ORDER BY timestamp
Find spikes in error logs with a HAVING filter:
SELECT error_type, count(*) AS n
FROM logs
WHERE level = 'ERROR' AND timestamp >= now() - 300
GROUP BY error_type
HAVING n > 20
Save your high-frequency troubleshooting scenarios as saved queries so a problem is one click away instead of written from scratch during an outage. The search box keeps command history and syntax completion, and templates can be shared across the team. For aggregate queries, always bound the time range to avoid full scans; in production, narrow the data with WHERE before layering aggregation on top.