# 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)