A practical look at JJHub OBSERVE's SQL-like log search: query syntax, index tuning, nested-field queries, and alert integration, so operators can locate issues as easily as querying a table.
Incident response usually hits two walls: logs scattered across dozens of machines, and the limits of keyword matching. grep finds lines containing a keyword, but it struggles when you need "every /api/order request in the last 10 minutes that took longer than 2 seconds." Regex gets you partway, but composing, testing, and reusing those patterns across a team is slow and error-prone.
JJHub OBSERVE treats logs as tables. You filter, aggregate, and sort with SQL-like syntax, getting in one line what used to take dozens of lines of shell scripting. Because the syntax is declarative, the same query reads the same way whether you run it ad hoc, pin it to a dashboard, or attach it to an alert rule. That consistency is the real payoff: a query written during an incident becomes a reusable artifact instead of a one-off command that lives in someone's terminal history.
OBSERVE supports a SELECT ... WHERE ... GROUP BY ... ORDER BY pipeline. Built-in fields include timestamp, level, service, trace_id, and message, plus any custom fields you declared at ingestion time, such as user_id, order_id, or cost_ms. A few queries we see every day:
-- Find error logs
SELECT * FROM log WHERE level = 'ERROR' AND service = 'order-service'
ORDER BY timestamp DESC LIMIT 100
-- Aggregate by status code
SELECT status, count(*) AS cnt FROM log
WHERE service = 'gateway'
GROUP BY status ORDER BY cnt DESC
-- Surface slow requests
SELECT trace_id, cost_ms, path FROM log
WHERE cost_ms > 2000 ORDER BY cost_ms DESC LIMIT 50
Field names are case-insensitive; * returns all fields. Time range is set through the time picker in the UI, so the SQL itself only handles filtering and aggregation—don't hand-write timestamps into the query, or you'll drift from the time picker and confuse anyone reading the dashboard later. Comparison operators (>, <, =, !=, IN) work across numeric, string, and boolean fields; AND/OR combine conditions, and LIMIT caps result size.
OBSERVE infers field types from the ingested data: numbers stay numeric, timestamps stay sortable, and everything else is treated as text. This matters in queries—cost_ms > 2000 only works if cost_ms was emitted as a number, not as a string. If a field arrives as "2000" (quoted), the comparison becomes lexicographic and the results will be wrong in subtle ways. Check the collection's field list to confirm inferred types, and cast with CAST(field AS INT) when the emitter is inconsistent. Getting types right at ingestion is cheaper than casting in every query.
Search speed comes down to indexing. OBSERVE automatically indexes high-frequency fields like level, service, and trace_id, and builds inverted indexes on text fields for full-text matching. Two practical tips:
trace_id and order_id as indexed fields at ingestion time, so queries hit the index instead of doing a full scan.message CONTAINS 'keyword' over message LIKE '%keyword%'; the former goes through the inverted index.For JSON-structured logs, OBSERVE flattens nested fields automatically, so you can drill down with path syntax like request.headers.user_agent CONTAINS 'bot' without a second parse step. In our tests at one billion log lines, an equality query on trace_id returns within one second. An unconstrained full-text scan is far slower, so always narrow the range with field filters first.
A note on cardinality: fields with very high cardinality—a raw request ID that's unique per line, for example—are expensive to index and rarely useful as filters. Index the fields you actually filter on, not every field you emit. You can adjust the indexed-field list in the collection settings, and it applies to newly ingested data.
Query results can be exported as CSV, or saved as a view to pin down a query you run often. A view serves two purposes: it can feed a dashboard, or back an alert rule—for example, "alert when ERROR logs exceed 100 in the last 5 minutes." That turns log search from a manual debugging tool into part of your alerting pipeline: the on-call engineer can open the matching log view straight from an alert and follow the trace_id all the way down. For teams standardizing on a common set of views, this also cuts down on tribal knowledge—new engineers start from the same saved queries instead of re-deriving them.