# Derive a metric from your logs

> Count matching logs, or sum one numeric log field, into a metric with fixed buckets, revisions, late accounting and freshness.

Canonical page: https://anectico.com/docs/investigate/log-derived-metrics/


A log-derived metric rule turns your logs into a metric without changing what you ship: it counts
logs that match a condition, or sums one finite numeric field across them, into fixed time buckets.
Your agent creates and revises the rules, and reads the result. The result reads like any other
metric (see [Explore and compare metrics](/docs/investigate/metrics)), through MCP, through the CLI,
or in a dashboard widget. It has its own freshness and completeness reporting, so you always know
whether a bucket is still settling.

## Ask your agent

> Create a metric `checkout.errors` that counts error logs per minute, grouped by route. Show me the
> preview first. Then read it for the last 24 hours and tell me whether the buckets are complete.

| Job | MCP tool or action | CLI command |
| --- | --- | --- |
| List the rules, and read one with every revision | `list_log_metrics`, `get_log_metric` | `anectico metrics rules list`, `anectico metrics rules get`, `anectico metrics rules revisions` |
| Create a rule | `create_log_metric` | `anectico metrics rules create` |
| Check a rule spec against every limit without storing it | the preview of `create_log_metric` | `anectico metrics rules validate` |
| Edit a rule, or roll it back | `revise_log_metric` | `anectico metrics rules revise` |
| Read the published series | `query_log_metric` | `anectico metrics rules query` |
| Delete a rule | `delete_log_metric` | `anectico metrics rules delete` |

Reading needs `metrics:read`. Creating, revising, and deleting need `ingestion:pipelines:write` and
`metrics:read`. MCP calls also need `mcp:read` or `mcp:write`. Every write is a preview and confirm
write.

## What your agent gets back

`query_log_metric` returns the published per-bucket series with its watermark, its completeness, the
windows that are withheld, and the late-log count. Values are bucket counts or sums. They are never
cumulative counters. A rule read returns every immutable revision, newest first, and the instant each
one started applying to received logs.

## Open the proof

A log-derived metric has no page of its own. Ask the agent to chart it on a dashboard and open the
dashboard page from the link the agent returns. See [Investigate service health and
dashboards](/docs/investigate/service-health-and-dashboards).

## What a rule fixes and what stays revisable

A rule fixes four things for its whole life: the **metric name**, the **aggregation** (`count` or
`sum`), the **unit** (a short label such as `1`, `ms` or `By`), and the **bucket width** — one of
`60`, `300`, `600`, `900`, `1800`, `3600` or `86400` seconds. These cannot change after creation;
create a new rule if you need a different one of them.

Everything else is a **revision**, and every edit — including a rollback — appends a new one rather
than changing history in place:

- **Match conditions** (up to 8): each tests one field — `body`, `severity`, `service_name`, an
  attribute (`attributes.<key>`) or a resource field (`resource.<key>`) — for an exact match, a
  regular-expression match, or presence/absence. All conditions must hold; an empty list matches
  every log.
- **Value field** (`sum` rules only): the numeric attribute or resource field to add up. A value that
  is not a finite number (not parseable, `NaN`, or infinite) contributes nothing for that log rather
  than being coerced to zero.
- **Labels** (up to 4): fields whose distinct combination of values becomes a separate series, the
  same way a dashboard groups a metric by a dimension. A label may name `service_name`, `severity`,
  an attribute or a resource field — never a person or session identifier, a URL, an address, a
  credential, or recorded message/prompt content. A rule that tries to label on one of these is
  refused before anything is stored (`DERIVED_LABEL_FORBIDDEN`). A label value longer than 128 bytes
  is stored as the fixed placeholder `__invalid__` rather than being truncated into a different,
  silently-merged value.
- **Series ceiling** (default 100, 1–1000): once a bucket has more distinct label combinations than
  this, the excess collapses into one overflow series labelled `__overflow__: "true"`. A query
  reports when a bucket overflowed so you know some combinations were folded together rather than
  dropped.
- **Late cutoff** (default 300 seconds, 0–3600): how long after a bucket closes a log can still
  arrive and count toward its value. A log that arrives later is recorded in `late_count` and never
  added to any value — a bucket's late count can rise after you first read it, but its stored values
  never change retroactively.
- **Source** (optional): restricts the rule to logs from one intake source instead of every source in
  the project.
- **Enabled**: a disabled revision derives nothing while it is in effect; re-enabling appends another
  revision rather than reactivating the old one.

## When a revision takes effect

A saved revision — an edit or a rollback — starts applying to logs **10 seconds after it is saved**,
never to logs already received. A log keeps the revision that was in effect when your platform
received it, so replaying or reprocessing the same log never recounts it under a different rule.
Because of this, changing a rule can never rewrite an earlier bucket's value; only what is derived
going forward changes.

**Rollback** copies an earlier revision's definition forward as a new revision — it does not delete
or replay history. Roll back to revision `N` and the rule immediately starts deriving from that
definition again, itself effective 10 seconds later, exactly like any other edit.

Every edit and rollback requires the rule's current revision number so a save can never silently
overwrite a change you have not seen; a stale number is refused as a conflict rather than applied.

## Reading the metric

Query a log-derived metric the same way as any other: pick a time range and, optionally, a step (a
whole multiple of the rule's bucket width) and up to one label to group by. Grouping by a subset of
a series' labels **sums the other labels away** rather than merely hiding them — two series that
agree on the kept label become one combined value. A value is always a per-bucket count or sum, not
a running total: reading the same bucket twice returns the same number unless a late log or a
correction changed it.

Every read carries its own freshness:

- **Current** — every bucket in range reflects every log received for it, as of the reported
  watermark.
- **Rebuild pending** — newer logs have arrived for some buckets and are being folded in; the shown
  values are the last settled ones, not stale placeholders.
- **Invalidated** — a deletion affecting this project is being applied, and the affected buckets are
  withheld until it finishes rather than served from a value that might include what was just
  deleted.

A read also reports **complete through** — the instant before which every bucket is settled, current
and past its late cutoff — and the **late count** in range, so a bucket that looks low because logs
are still arriving late is never confused with a bucket that is genuinely low.

## Deletion

Deleting one log removes its contribution to every rule it matched, using the same identity your log
already has — an identical duplicate of a log counts once, and a later log that is byte-for-byte the
same after a capture or transform change is a different log and is counted separately. Deleting a
rule stops deriving anything new from it and its series stop being readable; the rule's history is
not recoverable.

## Limits

| Limit | Value |
| --- | --- |
| Live rules per project | 50 |
| Revisions per rule | 200 |
| Match conditions per revision | 8 |
| Labels per revision | 4 |
| Series ceiling | 1–1000 (default 100) |
| Late cutoff | 0–3600 seconds (default 300) |
| Metric name | lowercase, `[a-z][a-z0-9_.]*`, at most 128 bytes, unique per project |
| Query range | at most 400 bucket windows, at most 2000 steps |

A metric name is reusable once the rule that held it is deleted.

## Manage rules from the CLI and the API

```bash
anectico metrics rules list
anectico metrics rules create --body '{
  "metric_name": "checkout.errors",
  "aggregation": "LOG_METRIC_AGGREGATION_COUNT",
  "unit": "1",
  "bucket_seconds": 60,
  "definition": {
    "match": [{"field": "severity", "equals": "error"}],
    "labels": ["attributes.http.route"],
    "max_series": 100,
    "enabled": true
  }
}'
anectico metrics rules validate --body '{...}'
anectico metrics rules revise <rule-id> --expected-revision 1 --body '{"enabled": false}'
anectico metrics rules revise <rule-id> --expected-revision 2 --rollback-to 1
anectico metrics rules query <rule-id> --window 24h --step 5m --group-by http.route
anectico metrics rules delete <rule-id> --yes
```

`validate` checks a rule spec against every limit above without saving a metric rule — use it before
`create` or `revise` to see the exact diagnostics a save would be refused with. The complete flag
contract is in the [CLI command reference](/docs/reference/cli).

## Resolve empty or unexpected results

- **A rule was refused at creation:** run `validate` first; the diagnostics name the exact field and
  limit.
- **A series is missing right after an edit:** the new revision has not taken effect yet — wait 10
  seconds past the save and query again.
- **A bucket looks lower than expected:** check the late count for that read; logs arriving after the
  late cutoff never add to a value.
- **A series is missing labels you expect:** confirm you did not group by a subset that sums it into
  another series, and check whether the bucket overflowed.
- **A read is withheld:** the project has a deletion in progress; retry once it completes.
- **A step is refused:** it must be a whole multiple of the rule's bucket width.

- [Explore and compare metrics](/docs/investigate/metrics)
- [Metrics REST API](/docs/reference/metrics-api)
- [CLI command reference](/docs/reference/cli)
- [Permission scopes](/docs/reference/permissions)
