About oodle-log-metrics
oodle-log-metrics is a skill that guides agents in defining Oodle rules to turn log lines into metrics. It focuses on validating filters, controlling label cardinality, and preserving existing rules when updating them.
Key features
- 01Validate log filters before rule creation
- 02Estimate label cardinality
- 03Define count, counter, gauge, and histogram metrics
- 04Extract labels and values with regex
- 05Retrieve rules before updating
Use cases
- 01Count matching log lines as metrics
- 02Create alerts from log events
- 03Measure tool latency from plain-text logs
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ oodle-ai/ agent-skills --skill oodle-log-metricsAfter installing, type/oodle-log-metricsin Claude Code to confirm it loaded.
Details
From the project READMEOodle Log Metrics — Rules and Cardinality
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 |