# Querying metrics

Mastra exposes the same five OLAP queries (`getMetricAggregate`, `getMetricBreakdown`, `getMetricTimeSeries`, `getMetricPercentiles`, and discovery helpers) through three surfaces: an in-process store accessor, the runtime HTTP API, and the `mastra api metric` CLI. All three accept the same Zod-validated input shapes, so you can move from a one-off CLI investigation to a programmatic dashboard tool without re-learning the API.

## When to use this

- Build a custom dashboard or KPI tile alongside Studio.
- Power a scheduled alert that fires when token cost or latency crosses a threshold.
- Give an agent a tool that reads its own performance metrics and explains them in chat.
- Run one-off investigations from a terminal with `mastra api metric ...`.

For setup of the observability store itself, see the [Metrics overview](https://mastra.ai/docs/observability/metrics/overview). For the list of metric names you can query, see the [Automatic metrics reference](https://mastra.ai/reference/observability/metrics/automatic-metrics).

> **Note:** Metric queries are served by the observability domain, which requires an OLAP-capable store (DuckDB locally, ClickHouse in production). See [Metrics overview](https://mastra.ai/docs/observability/metrics/overview) for setup. If the observability store is not configured, `getStore('observability')` returns `null`.

## Surfaces

### In-process

Inside a tool, server route, or workflow step, get the observability store from the Mastra storage:

```typescript
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'

export const agentLatencyTool = createTool({
  id: 'agentLatency',
  description: 'Average agent latency over the last hour.',
  inputSchema: z.object({}),
  execute: async (_input, context) => {
    const observability = await context.mastra!.getStorage()!.getStore('observability')
    if (!observability) {
      throw new Error('Observability domain is not configured (requires DuckDB or ClickHouse)')
    }

    const result = await observability.getMetricAggregate({
      name: ['mastra_agent_duration_ms'],
      aggregation: 'avg',
      filters: {
        timestamp: { start: new Date(Date.now() - 60 * 60 * 1000) },
      },
    })

    return { averageMs: result.value }
  },
})
```

`getStore('observability')` returns `null` when the configured backend does not support OLAP queries.

### HTTP

The `mastra dev` server (and any deployed Mastra runtime) exposes the same queries under `/api/observability/metrics/*`. Aggregate, breakdown, time series, and percentile endpoints take a JSON body with `POST`. Discovery endpoints use `GET` with query parameters.

```bash
curl -sS -X POST http://localhost:4111/api/observability/metrics/aggregate \
  -H "content-type: application/json" \
  -d '{"name":["mastra_agent_duration_ms"],"aggregation":"avg"}'
```

Available routes:

- `POST /api/observability/metrics/aggregate`
- `POST /api/observability/metrics/breakdown`
- `POST /api/observability/metrics/timeseries`
- `POST /api/observability/metrics/percentiles`
- `GET /api/observability/metrics` (raw rows, paginated)
- `GET /api/observability/discovery/metric-names`
- `GET /api/observability/discovery/metric-label-keys`
- `GET /api/observability/discovery/metric-label-values`

The `@mastra/client-js` SDK wraps the same routes as `mastraClient.getMetricAggregate(...)`, `getMetricBreakdown(...)`, and so on.

### CLI

`mastra api metric ...` calls the same endpoints with a single JSON argument, so an agent or shell script can fetch metrics without writing any code:

```bash
mastra api metric aggregate \
  '{"name":["mastra_agent_duration_ms"],"aggregation":"avg"}' \
  --url http://localhost:4111 --pretty
```

By default the CLI targets hosted Mastra observability (`https://observability.mastra.ai`). Pass `--url http://localhost:4111` to query a local `mastra dev` server. See [`mastra api metric aggregate`](https://mastra.ai/reference/cli/mastra) and the surrounding entries for the full command list.

## Queries

### `getMetricAggregate`

Returns a single scalar — the building block for KPI cards.

Inputs:

- `name`: Array of one or more metric names.
- `aggregation`: One of `'sum' | 'avg' | 'min' | 'max' | 'count' | 'count_distinct' | 'last'`.
- `filters`: Optional [filter object](#filtering).
- `comparePeriod`: Optional `'previous_period' | 'previous_day' | 'previous_week'` for period-over-period comparison.

Response:

- `value`, `previousValue`, `changePercent`.
- `estimatedCost`, `costUnit`, `previousEstimatedCost`, `costChangePercent` for token metrics.

```typescript
const observability = await mastra.getStorage()!.getStore('observability')

const cost = await observability!.getMetricAggregate({
  name: ['mastra_model_total_input_tokens', 'mastra_model_total_output_tokens'],
  aggregation: 'sum',
  comparePeriod: 'previous_day',
})

console.log(cost.value, cost.estimatedCost, cost.costUnit, cost.changePercent)
```

### `getMetricBreakdown`

Groups rows by one or more dimensions and aggregates each group — the building block for top-N tables (e.g. "tokens by agent").

Inputs:

- `name`: Array of metric names.
- `groupBy`: Array of fields to group by (for example `['entityName']`).
- `aggregation`: Same enum as above.
- `limit`: Server-side top-K cap. Required for high-cardinality `groupBy`.
- `orderDirection`: `'ASC' | 'DESC'` (defaults to `DESC`).
- `filters`: Optional.

Response: `groups[]`, each with `dimensions` (record of group keys to values), `value`, and `estimatedCost`.

```typescript
const byAgent = await observability!.getMetricBreakdown({
  name: ['mastra_model_total_input_tokens'],
  groupBy: ['entityName'],
  aggregation: 'sum',
  limit: 10,
  orderDirection: 'DESC',
})
```

### `getMetricTimeSeries`

Buckets values by a fixed interval — the building block for line and bar charts.

Inputs:

- `name`: Array of metric names.
- `interval`: One of `'1m' | '5m' | '15m' | '1h' | '1d'`.
- `aggregation`: Same enum.
- `groupBy`: Optional. When omitted, multiple metric names are summed into one series; use one call per metric to keep them separate.
- `filters`: Optional.

Response: `series[]`, each with `name`, `costUnit`, and `points[]` of `{ timestamp, value, estimatedCost }`.

```typescript
const inputTokens = await observability!.getMetricTimeSeries({
  name: ['mastra_model_total_input_tokens'],
  aggregation: 'sum',
  interval: '1h',
  filters: {
    timestamp: { start: new Date(Date.now() - 24 * 60 * 60 * 1000) },
  },
})
```

### `getMetricPercentiles`

Returns percentile values bucketed by time — the building block for latency charts.

Inputs:

- `name`: Single metric name (string, not array).
- `percentiles`: Array of numbers between `0` and `1`, for example `[0.5, 0.95, 0.99]`.
- `interval`: Same enum as `getMetricTimeSeries`.
- `filters`: Optional.

Response: `series[]`, each with `percentile` and `points[]` of `{ timestamp, value }`.

```typescript
const latency = await observability!.getMetricPercentiles({
  name: 'mastra_agent_duration_ms',
  percentiles: [0.5, 0.95],
  interval: '1h',
})
```

### Discovery

Use these endpoints to populate dropdowns or to give an agent the menu of values it can filter by. All discovery routes are `GET` and live under `/api/observability/discovery/`.

**Metric-specific** (also exposed as `mastra api metric` subcommands):

| Method                 | Args                                        | Path suffix           | CLI                              |
| ---------------------- | ------------------------------------------- | --------------------- | -------------------------------- |
| `getMetricNames`       | `{ prefix?, limit? }`                       | `metric-names`        | `mastra api metric names`        |
| `getMetricLabelKeys`   | `{ metricName }`                            | `metric-label-keys`   | `mastra api metric label-keys`   |
| `getMetricLabelValues` | `{ metricName, labelKey, prefix?, limit? }` | `metric-label-values` | `mastra api metric label-values` |

**Shared with traces and logs** (HTTP-only, no dedicated CLI subcommand):

| Method            | Args              | Path suffix     |
| ----------------- | ----------------- | --------------- |
| `getEntityTypes`  | `{}`              | `entity-types`  |
| `getEntityNames`  | `{ entityType? }` | `entity-names`  |
| `getServiceNames` | `{}`              | `service-names` |
| `getEnvironments` | `{}`              | `environments`  |
| `getTags`         | `{ entityType? }` | `tags`          |

## Filtering

Every query accepts the same `filters` object. The most useful fields:

- `name`: Restrict to specific metric names. (Top-level `name` already does this for aggregate/breakdown/timeseries; use `filters.name` when you want to mix multiple metrics under a single query.)
- `timestamp`: `{ start, end, startExclusive, endExclusive }`. Both bounds are optional; omit `end` for "until now".
- `provider`, `model`, `costUnit`: For token and cost metrics.
- `labels`: Exact key-value match on metric labels, for example `{ status: 'error' }` for duration metrics.
- Correlation fields: `entityType`, `entityName`, `parentEntityName`, `rootEntityName`, `userId`, `organizationId`, `resourceId`, `runId`, `sessionId`, `threadId`, `requestId`, `executionSource`, `environment`, `serviceName`, `experimentId`, `tags`.

The same `filters` shape works across all three surfaces:

```typescript
// In-process
await observability!.getMetricAggregate({
  name: ['mastra_tool_duration_ms'],
  aggregation: 'avg',
  filters: { entityName: 'weatherTool', labels: { status: 'error' } },
})
```

```bash
# CLI
mastra api metric aggregate \
  '{"name":["mastra_tool_duration_ms"],"aggregation":"avg","filters":{"entityName":"weatherTool","labels":{"status":"error"}}}' \
  --url http://localhost:4111
```

```bash
# HTTP
curl -sS -X POST http://localhost:4111/api/observability/metrics/aggregate \
  -H "content-type: application/json" \
  -d '{"name":["mastra_tool_duration_ms"],"aggregation":"avg","filters":{"entityName":"weatherTool","labels":{"status":"error"}}}'
```

## Example: build a custom KPI tile

The following tool returns input-token volume and estimated cost for the last hour. An agent or a dashboard can call it as `structuredContent` without re-implementing the query.

```typescript
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'

export const tokenKpiTool = createTool({
  id: 'tokenKpi',
  description: 'Returns input-token volume and estimated cost for the last hour.',
  inputSchema: z.object({}),
  outputSchema: z.object({
    inputTokens: z.number().nullable(),
    estimatedCost: z.number().nullable(),
    costUnit: z.string().nullable(),
    changePercent: z.number().nullable(),
  }),
  execute: async (_input, context) => {
    const observability = await context.mastra!.getStorage()!.getStore('observability')
    if (!observability) {
      throw new Error('Observability domain is not configured (requires DuckDB or ClickHouse)')
    }

    const result = await observability.getMetricAggregate({
      name: ['mastra_model_total_input_tokens'],
      aggregation: 'sum',
      filters: {
        timestamp: { start: new Date(Date.now() - 60 * 60 * 1000) },
      },
      comparePeriod: 'previous_period',
    })

    return {
      inputTokens: result.value,
      estimatedCost: result.estimatedCost ?? null,
      costUnit: result.costUnit ?? null,
      changePercent: result.changePercent ?? null,
    }
  },
})
```

## Related

- [Metrics overview](https://mastra.ai/docs/observability/metrics/overview)
- [Automatic metrics reference](https://mastra.ai/reference/observability/metrics/automatic-metrics)
- [CLI: `mastra api metric ...`](https://mastra.ai/reference/cli/mastra)
- [Studio observability](https://mastra.ai/docs/studio/observability)