Skip to main content

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
Direct link to 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. For the list of metric names you can query, see the Automatic metrics reference.

note

Metric queries are served by the observability domain, which requires an OLAP-capable store (DuckDB locally, ClickHouse in production). See Metrics overview for setup. If the observability store is not configured, getStore('observability') returns null.

Surfaces
Direct link to Surfaces

In-process
Direct link to In-process

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

src/mastra/tools/agent-latency-tool.ts
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
Direct link to 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.

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
Direct link to 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:

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 and the surrounding entries for the full command list.

Queries
Direct link to Queries

getMetricAggregate
Direct link to 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.
  • comparePeriod: Optional 'previous_period' | 'previous_day' | 'previous_week' for period-over-period comparison.

Response:

  • value, previousValue, changePercent.
  • estimatedCost, costUnit, previousEstimatedCost, costChangePercent for token metrics.
src/mastra/tools/token-cost-tool.ts
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
Direct link to 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.

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

getMetricTimeSeries
Direct link to 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 }.

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
Direct link to 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 }.

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

Discovery
Direct link to 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):

MethodArgsPath suffixCLI
getMetricNames{ prefix?, limit? }metric-namesmastra api metric names
getMetricLabelKeys{ metricName }metric-label-keysmastra api metric label-keys
getMetricLabelValues{ metricName, labelKey, prefix?, limit? }metric-label-valuesmastra api metric label-values

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

MethodArgsPath suffix
getEntityTypes{}entity-types
getEntityNames{ entityType? }entity-names
getServiceNames{}service-names
getEnvironments{}environments
getTags{ entityType? }tags

Filtering
Direct link to 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:

// In-process
await observability!.getMetricAggregate({
name: ['mastra_tool_duration_ms'],
aggregation: 'avg',
filters: { entityName: 'weatherTool', labels: { status: 'error' } },
})
# CLI
mastra api metric aggregate \
'{"name":["mastra_tool_duration_ms"],"aggregation":"avg","filters":{"entityName":"weatherTool","labels":{"status":"error"}}}' \
--url http://localhost:4111
# 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
Direct link to 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.

src/mastra/tools/token-kpi-tool.ts
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,
}
},
})