This skill teaches the agent to convert log streams into metrics safely: validate the filter, narrow the labels, and avoid creating high-cardinality time series. A log metrics rule is also the way to count or alert on values inside log lines: a monitor alerts on the metric that the rule emits.
Prerequisites
brew install oodle-ai/oodle/oodle
oodle configure
Confirm the log-metrics endpoint works:
oodle log-metrics list -o json | jq 'length'
Command Execution Order
Before running any oodle command:
- Check whether the rule's filter and
labelsare already in context. - If not, run a sample log query (e.g.
oodle logs query, see the oodle-logs skill, or the log explorer) to verify the filter actually matches logs. - Estimate cardinality: the distinct values of each label, multiplied together.
- Run
oodle log-metrics create -f rule.jsononly after the filter is validated. - Do not run
updatewithout first runninggetto capture the existing rule.
Quick Reference
| Task | Command |
|---|---|
| List rules | oodle log-metrics list -o json |
| Get rule | oodle log-metrics get <id> -o json |
| Create rule | oodle log-metrics create -f rule.json |
| Update rule | oodle log-metrics update <id> -f rule.json |
| Delete rule | oodle log-metrics delete <id> --force |
Common Operations
Rule schema
{
"name": "http_errors_from_logs",
"filter": {
"all": [
{"field": "level", "operator": "is", "value": "error"},
{"field": "service", "operator": "is", "value": "api"}
]
},
"labels": [
{"name": "service", "valueExtractor": {"field": "service"}},
{"name": "env", "valueExtractor": {"field": "env"}}
],
"metricDefinitions": [
{"name": "http_errors", "type": "log_count"}
]
}
Field meaning:
| Field | Meaning |
|---|---|
name |
Human identifier for the rule. Required. |
filter |
Which log lines the rule reads. A condition {"field", "operator", "value", "jsonPath"}, or {"all": [...]} (AND), {"any": [...]} (OR), {"not": <filter>}. You can nest them. Operators: is, contains, matches regex, exists. There is no !=; use not. |
labels |
Labels added to the emitted metrics. Each has a name and a static value, or a valueExtractor {"field", "jsonPath", "regex"}. |
metricDefinitions |
The metrics to emit. At least one. Each has a name and a type: log_count (number of matching lines), counter (sum of a value), gauge (last value), or histogram (distribution of a value). counter, gauge, and histogram read the value from field, with an optional jsonPath or regex. |
fieldis a top-level log field: letters, digits, and underscores only. Read a nested JSON value withjsonPath, for example{"field": "log", "jsonPath": "user.id"}.- A
regexuses Rust syntax. Capture group 1 is the value, so put one group around the part you need. In JSON, escape each backslash:\dis"\\d". - When the regex does not match a line, the label is empty, and a value is
0. So make the filter select only the lines that contain the value. - The emitted metric name is
oodle_logs_<name>(oodle_logs_http_errors). Ahistogramemits_bucket,_sum, and_countseries. Querylog_countandcounterwithincrease(...)orrate(...), and aggregate withsum by (...). - A rule applies only to logs that arrive after you create it. For past data, use
oodle logs query. - Put a single condition inside an
alllist, as in the examples. That form works with every CLI and server version.
Creating a rule
# ✅ CORRECT — validate filter first, then create
# (preview with the log explorer or `oodle logs query`)
oodle log-metrics create -f rule.json
# ❌ WRONG — creating with an untested filter; the resulting metric is silently empty
oodle log-metrics create -f <(echo '{"name":"x","filter":{"all":[{"field":"levl","operator":"is","value":"eror"}]},"metricDefinitions":[{"name":"x","type":"log_count"}]}')
Updating a rule
# ✅ CORRECT — get → edit → update
oodle log-metrics get lm_123 -o json > rule.json
jq '.labels = [{"name":"service","valueExtractor":{"field":"service"}},{"name":"env","valueExtractor":{"field":"env"}}]' rule.json > rule.new.json
oodle log-metrics update lm_123 -f rule.new.json
# ❌ WRONG — partial payload removes existing fields (and has no `id`, so it is rejected)
oodle log-metrics update lm_123 -f <(echo '{"labels":[{"name":"service","valueExtractor":{"field":"service"}}]}')
Deleting a rule
# ✅ CORRECT — verify and delete
oodle log-metrics get lm_123 -o json > /dev/null
oodle log-metrics delete lm_123 --force
# ❌ WRONG — speculative delete by name match
oodle log-metrics delete "$(oodle log-metrics list | grep errors | awk '{print $1}')" --force
Alerting on log lines
A monitor runs PromQL, not a log query. To alert on log lines, create a log metrics rule, then a monitor on the metric it emits (see the oodle-monitors skill). A rule reads the raw line text with a regex, so this works for plain-text lines, and you do not need a log transform. A label extracted from the line (user, tool, tenant) names the series in the alert.
Example: one alert per user whose credit ran out, from lines like:
2026-10-07T10:12:03Z WARN credit exhausted for user=alice plan=pro
credit-exhausted.json:
{
"name": "credit-exhausted",
"filter": {"all": [{"field": "message", "operator": "contains", "value": "credit exhausted"}]},
"labels": [
{"name": "user", "valueExtractor": {"field": "message", "regex": "user=(\\S+)"}}
],
"metricDefinitions": [{"name": "credit_exhausted", "type": "log_count"}]
}
credit-exhausted-monitor.json (op 2 is >):
{
"name": "Credit exhausted",
"promql_query": "sum by (user) (increase(oodle_logs_credit_exhausted[10m]))",
"conditions": {"critical": {"op": 2, "value": 0}},
"labels": {"team": "billing", "env": "prod"},
"annotations": {"summary": "User {{ $labels.user }} ran out of credit"}
}
oodle log-metrics create -f credit-exhausted.json
oodle monitors create -f credit-exhausted-monitor.json
# Later: whose credit ran out in the last 24 hours
oodle metrics query --query 'sum by (user) (increase(oodle_logs_credit_exhausted[24h])) > 0' -o json
A label per user is fine here because the event is rare (see "Keep labels to 2–3 low-cardinality labels").
Example: a value inside the line, such as the duration in tool Bash ok in 12345 ms. Use one regex per value, each with one capture group, and a filter that requires the value:
{
"name": "tool-latency",
"filter": {"all": [{"field": "message", "operator": "matches regex", "value": "tool \\S+ (?:ok|failed) in \\d+ ms"}]},
"labels": [
{"name": "tool", "valueExtractor": {"field": "message", "regex": "tool (\\S+) "}},
{"name": "status", "valueExtractor": {"field": "message", "regex": " (ok|failed) in "}}
],
"metricDefinitions": [
{"name": "tool_calls", "type": "log_count"},
{"name": "tool_duration_ms", "type": "histogram", "field": "message", "regex": " in (\\d+) ms"}
]
}
# p95 tool latency in ms, per tool
histogram_quantile(0.95, sum by (le, tool) (rate(oodle_logs_tool_duration_ms_bucket[5m])))
# failed calls per tool
sum by (tool) (increase(oodle_logs_tool_calls{status="failed"}[10m]))
Best Practices
Validate the filter against real log volume before creating the rule
A typo in the filter (levl=eror) creates a rule that silently emits zero data points.
# ✅ CORRECT — preview matching log volume in the log explorer first;
# only then run `oodle log-metrics create`
# (or run `oodle logs query` with the same condition and confirm count > 0)
# ❌ WRONG — create the rule, then notice the dashboard panel is empty next week
oodle log-metrics create -f rule.json
Keep labels to 2–3 low-cardinality labels
Each label multiplies the time-series count. Avoid request_id, user_id, trace_id, path (with IDs). A user label is fine only on a rare event, such as credit refusals.
# ✅ CORRECT — bounded labels
"labels": [{"name": "service", "valueExtractor": {"field": "service"}}, {"name": "env", "valueExtractor": {"field": "env"}}]
# ❌ WRONG — `user_id` on every request blows up cardinality (one series per user)
"labels": [{"name": "service", "valueExtractor": {"field": "service"}}, {"name": "user_id", "valueExtractor": {"field": "user_id"}}]
Always get before update to preserve fields
update replaces the document. Sending a single field nulls everything else.
# ✅ CORRECT
oodle log-metrics get lm_123 -o json > rule.json
jq '.filter.all += [{"field": "status", "operator": "matches regex", "value": "^5"}]' rule.json > rule.new.json
oodle log-metrics update lm_123 -f rule.new.json
# ❌ WRONG — clobbers labels and metricDefinitions
oodle log-metrics update lm_123 -f <(echo '{"filter":{"all":[{"field":"level","operator":"is","value":"error"}]}}')
Use a stable, descriptive metric name
The emitted name is oodle_logs_<name>. A <domain>_<measurement> name makes the metric easy to find in dashboards and drop rules. Use only letters, digits, and underscores.
# ✅ CORRECT
"metricDefinitions": [{"name": "http_errors", "type": "log_count"}]
# ❌ WRONG — generic name collides with other rules
"metricDefinitions": [{"name": "errors", "type": "log_count"}]
Failure Handling
| Error | Cause | Fix |
|---|---|---|
| 401 Unauthorized | Invalid or missing API key | Run oodle configure or set OODLE_API_KEY |
| 404 Not Found | Log-metric rule ID does not exist | Verify with oodle log-metrics list -o json |
| connection refused | Wrong OODLE_DEPLOYMENT URL |
Check OODLE_DEPLOYMENT env var |
400 Match must have a valid Operator |
Operator is not is, contains, matches regex, or exists |
Use one of the four; for "not equal", wrap the condition in not |
400 Field ... must contain only alphanumeric characters and underscores |
Dotted field name in field |
Use the top-level field and put the path in jsonPath |
400 Regex ... is not a valid rust regex |
Regex syntax error, or a JSON escape is missing | Escape backslashes in JSON (\\d); no look-around |
400 'logCount' metric definition has a Field, JSONPath, or Regex |
log_count has field, jsonPath, or regex |
Remove them |
400 Log Metrics ID in path and body don't match |
Body id is missing or differs from <id> |
Run get and edit that output |
| Metric exists but has no data points | Filter doesn't match any logs, or no new lines since the rule was created | Re-test the filter in the log explorer or with oodle logs query; check field names match the log schema. Rules do not backfill |
| Label is empty / values are all 0 | The regex does not match, or group 1 is not a number | Test the regex on sample lines; make the filter require the value |
| Cardinality alarm in the UI | labels includes a high-cardinality field |
Edit the rule to drop that label; old series age out at the regular retention |
| 429 Too Many Requests | Bulk rule creation | Add --retries 3, throttle to <10 creates per second |