This skill teaches the agent to search log data using the OpenSearch-compatible query DSL and to discover available log index patterns.
Prerequisites
brew install oodle-ai/oodle/oodle
oodle configure
Confirm the logs endpoint works:
oodle logs index-patterns -o json | jq 'length'
Command Execution Order
Before running any log query:
- Run
oodle logs index-patterns -o jsonto discover available log index patterns. - Pick the appropriate index pattern
titlefor your query. - Build the NDJSON query file using the discovered index pattern and OpenSearch Query DSL.
- Set
--startand--endto cover the time range you need. They default to-1handnow. - Do not guess index names — always confirm with
index-patternsfirst.
Quick Reference
| Task | Command |
|---|---|
| List log index patterns | oodle logs index-patterns -o json |
| Search logs | oodle logs query -f query.ndjson --start <start> --end <end> -o json |
Common Operations
List log index patterns
Discover available log index patterns before building log queries. The returned title field is used as the index value in NDJSON query files.
# CORRECT — list all available index patterns
oodle logs index-patterns -o json
# CORRECT — extract just the pattern titles
oodle logs index-patterns -o json | jq '.[].title'
# CORRECT — use a discovered pattern in a log query
oodle logs index-patterns -o json | jq '.[].title'
# Then use the title in your NDJSON file:
# {"index": "logs-*"}
# {"query": {"match_all": {}}, "size": 10}
Log query
Search log data using the OpenSearch-compatible multi-search API. The request body uses NDJSON format with exactly two JSON lines: the first selects the index, the second contains the search query using OpenSearch Query DSL. Pass the body via -f <file>.
Important constraints:
- The CLI only supports a single search body (exactly 2 NDJSON lines: one header + one search). Multiple search pairs are not supported.
- Use
--startand--endflags for time range filtering. Values can be epoch milliseconds (e.g.,1716825600000), RFC 3339 (e.g.,2026-01-02T00:00:00Z),now, or relative expressions (e.g.,-1h,-24h,-7d). --startdefaults to-1hand--enddefaults tonow. If you are searching for data older than 1 hour, you must pass an explicit--startor the query will silently return 0 results.- Do not put range filters on
timestampin the query body — the CLI auto-injects atimestamprange filter from--start/--end.
# CORRECT — basic query (searches last 1 hour by default)
cat > query.ndjson <<'EOF'
{"index": "logs-*"}
{"query": {"match_all": {}}, "size": 10}
EOF
oodle logs query -f query.ndjson -o json
# CORRECT — search for specific log messages with explicit time range
cat > query.ndjson <<'EOF'
{"index": "logs-*"}
{"query": {"bool": {"must": [{"match_phrase": {"message": "error"}}]}}, "size": 50, "sort": [{"timestamp": {"order": "desc"}}]}
EOF
oodle logs query -f query.ndjson --start -6h --end now -o json
# CORRECT — search older data using relative time
cat > query.ndjson <<'EOF'
{"index": "logs-*"}
{"query": {"bool": {"must": [{"match_phrase": {"message": "OOMKilled"}}]}}, "size": 100}
EOF
oodle logs query -f query.ndjson --start -7d --end -6d -o json
# CORRECT — search older data using epoch milliseconds
cat > query.ndjson <<'EOF'
{"index": "logs-*"}
{"query": {"match_phrase": {"message": "connection refused"}}, "size": 50}
EOF
oodle logs query -f query.ndjson --start 1716825600000 --end 1716912000000 -o json
# WRONG — omitting --start when searching for data older than 1 hour
cat > query.ndjson <<'EOF'
{"index": "logs-*"}
{"query": {"match_phrase": {"message": "yesterday's deploy error"}}, "size": 50}
EOF
oodle logs query -f query.ndjson -o json
# ^^^ Returns 0 results! --start defaults to -1h, so older data is excluded.
# FIX: add --start -24h (or whatever range covers the target timeframe)
# WRONG — putting a range filter on timestamp in the query body
cat > query.ndjson <<'EOF'
{"index": "logs-*"}
{"query": {"bool": {"must": [{"range": {"timestamp": {"gte": "now-1h", "lte": "now"}}}]}}, "size": 100}
EOF
# The CLI already injects a timestamp range from --start/--end. In-query range filters conflict.
# FIX: use --start and --end flags instead.
# WRONG — passing the query inline without -f
oodle logs query --body '{"index":"logs-*"}'
# WRONG — using JSON instead of NDJSON format
oodle logs query -f query.json # file must be valid NDJSON (exactly two lines)
Aggregations
Put OpenSearch aggregations in an aggs block. Set "size": 0 when you need only the aggregation. Read the result with jq '.responses[0].aggregations'. The field names below are examples; confirm them with oodle logs index-patterns. Numeric aggregations need a numeric field, not text inside message.
# CORRECT — error lines per service (top 20)
cat > query.ndjson <<'EOF'
{"index": "logs-*"}
{"size": 0, "query": {"term": {"level": "error"}}, "aggs": {"by_service": {"terms": {"field": "service_name", "size": 20}}}}
EOF
oodle logs query -f query.ndjson --start -24h --end now -o json \
| jq -r '.responses[0].aggregations.by_service.buckets[] | "\(.key)\t\(.doc_count)"'
# CORRECT — error lines per hour
cat > query.ndjson <<'EOF'
{"index": "logs-*"}
{"size": 0, "query": {"match_phrase": {"message": "error"}}, "aggs": {"per_hour": {"date_histogram": {"field": "timestamp", "fixed_interval": "1h"}}}}
EOF
oodle logs query -f query.ndjson --start -24h --end now -o json \
| jq -r '.responses[0].aggregations.per_hour.buckets[] | "\(.key_as_string)\t\(.doc_count)"'
Values inside plain-text log lines
regexp queries, scripts, and runtime fields are not supported. So a log query cannot filter on, or group by, a value that is only inside the text of a plain-text line. For lines like tool Bash ok in 12345 ms, a query cannot select "more than 10000 ms" or group by the tool name. Do not tell the user that Oodle cannot do it. Offer one of these:
- Log metrics rule (oodle-log-metrics skill): extracts the value with a regex into a metric with labels. Use it for counts, latency, and alerts on new logs.
- Past data: find the lines with
match_phrase, and extract the value on the client withjq.
| Need | Use |
|---|---|
| Find lines, read events, investigate the past | oodle logs query |
| Count or group by a real field, over the past | oodle logs query with aggs |
| Filter or group by a value only inside plain text | Log metrics rule (new data), or jq on the client (past data) |
| Alert on log lines (count, rate, latency, per user) | Log metrics rule + monitor |
| Trend over weeks on a dashboard | Log metrics rule |
Worked example: whose credit ran out in the last 24 hours?
First check if a log metrics rule already counts these lines: oodle log-metrics list -o json. If one does, read its metric, for example oodle metrics query --query 'sum by (user) (increase(oodle_logs_credit_exhausted[24h])) > 0' -o json. A rule counts only lines that arrived after it was made, so use the logs below for older data.
cat > credit.ndjson <<'EOF'
{"index": "logs-*"}
{"query": {"match_phrase": {"message": "credit exhausted"}}, "size": 1000, "sort": [{"timestamp": {"order": "desc"}}]}
EOF
# If the user is a real field (for example user_id): use a terms aggregation on it.
# If the user is only inside the text ("... for user=alice ..."): extract it on the client.
oodle logs query -f credit.ndjson --start -24h --end now -o json \
| jq -r '.responses[0].hits.hits[]._source.message | capture("user=(?<user>[^ ]+)").user' \
| sort | uniq -c | sort -rn
If the query returns size lines, there can be more: split the time range (for example, four 6-hour queries) and combine the results. To alert per user from now on, create a log metrics rule that extracts user with the regex user=(\S+), and a monitor on sum by (user) (increase(oodle_logs_credit_exhausted[10m])). The oodle-log-metrics skill has the rule JSON.
Best Practices
Discover index patterns before querying
Always run oodle logs index-patterns to find the correct index name. Guessing index names may return empty results or errors.
# CORRECT — discover first, then query
oodle logs index-patterns -o json | jq '.[].title'
# Use the discovered title in your NDJSON file
# WRONG — guessing index names
cat > query.ndjson <<'EOF'
{"index": "my-logs"}
{"query": {"match_all": {}}, "size": 10}
EOF
oodle logs query -f query.ndjson -o json
Always use --start and --end for time range filtering
The CLI injects a timestamp range filter automatically from --start/--end. Do not add your own range filter on timestamp in the query body — it conflicts with the CLI-injected one.
# CORRECT — use CLI flags for time range
oodle logs query -f query.ndjson --start -24h --end now -o json
# CORRECT — use epoch milliseconds for precise ranges
oodle logs query -f query.ndjson --start 1716825600000 --end 1716912000000 -o json
# WRONG — relying on the default 1-hour window for older data
oodle logs query -f query.ndjson -o json
# ^^^ Only searches the last 1 hour. If the data you need is older, you get 0 results.
# WRONG — putting range filters on timestamp in the query body
{"query": {"bool": {"must": [{"range": {"timestamp": {"gte": "now-6h"}}}]}}}
# FIX: remove the range from the query body and use --start -6h instead.
Use -o json for log query results
Log query responses contain nested JSON structures. Always use -o json and pipe through jq for extraction.
# CORRECT — extract log messages from the response
oodle logs query -f query.ndjson -o json | jq '.responses[].hits.hits[]._source.message'
# WRONG — using table output for complex nested log data
oodle logs query -f query.ndjson -o table
Use index pattern fields to build precise queries
Each index pattern includes a fields array with field names and types. Use these to build targeted queries instead of match_all.
# CORRECT — check available fields first
oodle logs index-patterns -o json | jq '.[0].fields[].name'
# Then build a query using confirmed field names
Failure Handling
| Error | Cause | Fix |
|---|---|---|
| 401 Unauthorized | Invalid or missing API key | Run oodle configure or set OODLE_API_KEY |
| 400 Bad Request | Invalid NDJSON body or Query DSL | Validate the NDJSON file has exactly two lines; check OpenSearch Query DSL syntax |
| Empty result (not an error) | No matching logs in the index or time range | Widen the time range or check the index name with oodle logs index-patterns |
| 0 results for older data | --start defaults to -1h; data outside that window is excluded |
Pass explicit --start for older data, e.g. --start -24h or --start <epoch_ms> |
| connection refused | Wrong OODLE_DEPLOYMENT URL |
Check OODLE_DEPLOYMENT env var, ensure no trailing slash |
| 429 Too Many Requests | Rate limited | Add --retries 3, back off, reduce query frequency |
400 on regexp, script, or runtime field |
Not supported | Use match_phrase, or a log metrics rule (see "Values inside plain-text log lines") |
required flag(s) "file" not set |
Missing required flag for log query | Add -f flag with path to NDJSON file |