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 thisDirect 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.
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.
SurfacesDirect link to Surfaces
In-processDirect link to In-process
Inside a tool, server route, or workflow step, get the observability store from the Mastra storage:
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.
HTTPDirect 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/aggregatePOST /api/observability/metrics/breakdownPOST /api/observability/metrics/timeseriesPOST /api/observability/metrics/percentilesGET /api/observability/metrics(raw rows, paginated)GET /api/observability/discovery/metric-namesGET /api/observability/discovery/metric-label-keysGET /api/observability/discovery/metric-label-values
The @mastra/client-js SDK wraps the same routes as mastraClient.getMetricAggregate(...), getMetricBreakdown(...), and so on.
CLIDirect 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.
QueriesDirect link to Queries
getMetricAggregateDirect 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,costChangePercentfor token metrics.
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)
getMetricBreakdownDirect 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-cardinalitygroupBy.orderDirection:'ASC' | 'DESC'(defaults toDESC).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',
})
getMetricTimeSeriesDirect 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) },
},
})
getMetricPercentilesDirect 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 between0and1, for example[0.5, 0.95, 0.99].interval: Same enum asgetMetricTimeSeries.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',
})
DiscoveryDirect 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):
| 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 |
FilteringDirect link to Filtering
Every query accepts the same filters object. The most useful fields:
name: Restrict to specific metric names. (Top-levelnamealready does this for aggregate/breakdown/timeseries; usefilters.namewhen you want to mix multiple metrics under a single query.)timestamp:{ start, end, startExclusive, endExclusive }. Both bounds are optional; omitendfor "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 tileDirect 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.
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,
}
},
})