
Querying Mlflow Metrics
- 520 installs
- 66 repo stars
- Updated July 30, 2026
- mlflow/skills
querying-mlflow-metrics is a Claude Code skill that runs fetch_metrics.py against MLflow tracking servers so developers who operate traced LLM apps can aggregate token usage, latency, and assessment metrics over time.
About
querying-mlflow-metrics is an MLflow skills workflow centered on scripts/fetch_metrics.py for querying aggregated trace metrics from an MLflow tracking server. It supports five core metrics—trace_count, latency, input_tokens, output_tokens, and total_tokens—with eight aggregations including COUNT, SUM, AVG, MIN, MAX, P50, P95, and P99, plus optional grouping by trace_name or trace_status and hourly or daily buckets via --time-interval. Developers can query SPANS or ASSESSMENTS views for span_count or assessment_value trends, filter windows with --start-time values like -24h or -7d, and emit table or JSON output with -o json. Reach for querying-mlflow-metrics when reviewing LLM cost spikes, P95 latency regressions, error-rate breakdowns by trace_status, or average assessment scores by evaluator name during ML ops reviews. The bundled references/api_reference.md documents filter syntax when you extend queries beyond the default table output examples in the skill README.
- MLflow run and metric queries
- Experiment comparison workflows
- Model performance trend inspection
- Ops-friendly tracking server access
- Supports iterative model evaluation loops
Querying Mlflow Metrics by the numbers
- 520 all-time installs (skills.sh)
- +40 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #441 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mlflow/skills --skill querying-mlflow-metricsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 520 |
|---|---|
| repo stars | ★ 66 |
| Last updated | July 30, 2026 |
| Repository | mlflow/skills ↗ |
How do you query MLflow LLM token usage trends?
Query MLflow runs, compare experiment metrics, and surface model performance trends during ML ops and experiment review.
Who is it for?
Developers operating MLflow-traced LLM applications who need CLI-friendly cost and latency aggregates without building a custom metrics dashboard first.
Skip if: Developers debugging a single failed chat turn who need full span trees rather than aggregated trace metrics across experiments.
When should I use this skill?
The user asks for MLflow token usage trends, latency percentiles, trace counts, or assessment score aggregates over a time range.
What you get
Aggregated metric tables or JSON with SUM, AVG, P95, COUNT breakdowns by time bucket, trace_name, trace_status, or assessment_name.
- aggregated metrics table
- JSON metrics export
- dimensional breakdown reports
By the numbers
- fetch_metrics.py supports 5 trace metrics including total_tokens and latency
- Exposes 8 aggregation functions: COUNT, SUM, AVG, MIN, MAX, P50, P95, P99
- Hourly bucketing uses --time-interval 3600 with relative windows like -24h
Files
MLflow Metrics
Run scripts/fetch_metrics.py to query metrics from an MLflow tracking server.
Examples
Token usage summary:
python scripts/fetch_metrics.py -s http://localhost:5000 -x 1 -m total_tokens -a SUM,AVGOutput: AVG: 223.91 SUM: 7613
Hourly token trend (last 24h):
python scripts/fetch_metrics.py -s http://localhost:5000 -x 1 -m total_tokens -a SUM \
-t 3600 --start-time="-24h" --end-time=nowOutput: Time-bucketed token sums per hour
Latency percentiles by trace:
python scripts/fetch_metrics.py -s http://localhost:5000 -x 1 -m latency -a AVG,P95 -d trace_nameError rate by status:
python scripts/fetch_metrics.py -s http://localhost:5000 -x 1 -m trace_count -a COUNT -d trace_statusQuality scores by evaluator (assessments):
python scripts/fetch_metrics.py -s http://localhost:5000 -x 1 -v ASSESSMENTS \
-m assessment_value -a AVG,P50 -d assessment_nameOutput: Average and median scores for each evaluator (e.g., correctness, relevance)
Assessment count by name:
python scripts/fetch_metrics.py -s http://localhost:5000 -x 1 -v ASSESSMENTS \
-m assessment_count -a COUNT -d assessment_nameJSON output: Add -o json to any command.
Arguments
| Arg | Required | Description |
|---|---|---|
-s, --server | Yes | MLflow server URL |
-x, --experiment-ids | Yes | Experiment IDs (comma-separated) |
-m, --metric | Yes | trace_count, latency, input_tokens, output_tokens, total_tokens |
-a, --aggregations | Yes | COUNT, SUM, AVG, MIN, MAX, P50, P95, P99 |
-d, --dimensions | No | Group by: trace_name, trace_status |
-t, --time-interval | No | Bucket size in seconds (3600=hourly, 86400=daily) |
--start-time | No | -24h, -7d, now, ISO 8601, or epoch ms |
--end-time | No | Same formats as start-time |
-o, --output | No | table (default) or json |
For SPANS metrics (span_count, latency), add -v SPANS. For ASSESSMENTS metrics, add -v ASSESSMENTS.
See references/api_reference.md for filter syntax and full API details.
MLflow Trace Metrics API Reference
Endpoint
POST /api/3.0/mlflow/traces/metrics
Available Metrics
TRACES view (view_type=1)
| Metric | Description | Aggregations |
|---|---|---|
trace_count | Number of traces | COUNT |
latency | Execution time (ms) | AVG, PERCENTILE |
input_tokens | Input token count | SUM, AVG, PERCENTILE |
output_tokens | Output token count | SUM, AVG, PERCENTILE |
total_tokens | Total token count | SUM, AVG, PERCENTILE |
SPANS view (view_type=2)
| Metric | Description | Aggregations |
|---|---|---|
span_count | Number of spans | COUNT |
latency | Span duration (ms) | AVG, PERCENTILE |
ASSESSMENTS view (view_type=3)
| Metric | Description | Aggregations |
|---|---|---|
assessment_count | Number of assessments | COUNT |
assessment_value | Assessment score | AVG, PERCENTILE |
Aggregation Types
| Name | Code | Description |
|---|---|---|
| COUNT | 1 | Count of entities |
| SUM | 2 | Sum of values |
| AVG | 3 | Average of values |
| PERCENTILE | 4 | Percentile (requires percentile_value) |
| MIN | 5 | Minimum value |
| MAX | 6 | Maximum value |
Percentile shorthand: P50, P90, P95, P99, P99.9
Dimensions (grouping)
TRACES
trace_name- Group by trace nametrace_status- Group by status (OK, ERROR)
SPANS
span_name- Group by span namespan_type- Group by span type (LLM, TOOL, etc.)span_status- Group by span status
ASSESSMENTS
assessment_name- Group by assessment nameassessment_value- Group by assessment value
Filter Syntax
trace.status = "OK"
trace.tag.<key> = "<value>"
trace.metadata.<key> = "<value>"
span.name = "<value>"
span.type = "LLM"
assessment.name = "<value>"Time Intervals (seconds)
| Interval | Seconds |
|---|---|
| Minute | 60 |
| Hour | 3600 |
| Day | 86400 |
| Week | 604800 |
Example Request
{
"experiment_ids": ["1"],
"view_type": 1,
"metric_name": "total_tokens",
"aggregations": [
{"aggregation_type": 2},
{"aggregation_type": 3}
],
"dimensions": ["trace_name"],
"time_interval_seconds": 3600,
"start_time_ms": 1737244800000,
"end_time_ms": 1737331200000
}Example Response
{
"data_points": [
{
"metric_name": "total_tokens",
"dimensions": {"time_bucket": "2024-01-19T00:00:00+00:00", "trace_name": "chat"},
"values": {"SUM": 15000, "AVG": 500.0}
}
],
"next_page_token": null
}#!/usr/bin/env python3
"""Fetch MLflow trace metrics from tracking server."""
from __future__ import annotations
import argparse
import json
import re
import sys
import urllib.error
import urllib.request
from datetime import datetime, timezone
# API endpoint path (MLflow 3.0 API)
API_PATH = "/api/3.0/mlflow/traces/metrics"
# Default max results - MLflow server limit is 1000
DEFAULT_MAX_RESULTS = 1000
# Aggregation type codes per MLflow protobuf spec
AGG_TYPES = {"COUNT": 1, "SUM": 2, "AVG": 3, "PERCENTILE": 4, "MIN": 5, "MAX": 6}
# View type codes per MLflow protobuf spec
VIEW_TYPES = {"TRACES": 1, "SPANS": 2, "ASSESSMENTS": 3}
# Valid metrics per view type
VALID_METRICS = {
"TRACES": ["trace_count", "latency", "input_tokens", "output_tokens", "total_tokens"],
"SPANS": ["span_count", "latency"],
"ASSESSMENTS": ["assessment_count", "assessment_value"],
}
# Valid dimensions per view type
VALID_DIMENSIONS = {
"TRACES": ["trace_name", "trace_status"],
"SPANS": ["span_name", "span_type", "span_status"],
"ASSESSMENTS": ["assessment_name", "assessment_value"],
}
# Time unit multipliers (seconds)
TIME_UNITS = {"m": 60, "h": 3600, "d": 86400, "w": 604800}
def parse_time(time_str: str) -> int:
"""Parse time string to epoch milliseconds.
Formats: -24h, -7d, -1w, -30m, now, ISO 8601, epoch ms
"""
if time_str == "now":
return int(datetime.now(timezone.utc).timestamp() * 1000)
# Relative time: -24h, -7d, -1w, -30m
match = re.match(r"^-(\d+)([hdwm])$", time_str)
if match:
value, unit = int(match.group(1)), match.group(2)
offset_seconds = value * TIME_UNITS[unit]
return int((datetime.now(timezone.utc).timestamp() - offset_seconds) * 1000)
# Epoch milliseconds
if time_str.isdigit():
return int(time_str)
# ISO 8601
try:
dt = datetime.fromisoformat(time_str.replace("Z", "+00:00"))
return int(dt.timestamp() * 1000)
except ValueError:
raise ValueError(
f"Invalid time format: '{time_str}'. "
f"Valid formats: relative (-24h, -7d, -30m, now), ISO 8601 (2024-01-01T00:00:00Z), epoch ms"
)
def parse_aggregations(agg_str: str) -> list[dict]:
"""Parse aggregation string. Supports COUNT, SUM, AVG, MIN, MAX, P50, P95, etc."""
result = []
for agg in agg_str.split(","):
agg = agg.strip().upper()
if agg.startswith("P") and agg[1:].replace(".", "", 1).replace("-", "", 1).isdigit():
percentile_value = float(agg[1:])
if not 0 <= percentile_value <= 100:
raise ValueError(f"Percentile must be 0-100, got: {percentile_value}")
result.append({"aggregation_type": AGG_TYPES["PERCENTILE"], "percentile_value": percentile_value})
elif agg in AGG_TYPES:
result.append({"aggregation_type": AGG_TYPES[agg]})
else:
raise ValueError(f"Unknown aggregation: '{agg}'. Valid: {', '.join(AGG_TYPES.keys())}, P<0-100>")
return result
def validate_metric(metric: str, view_type: str) -> None:
"""Validate metric name for view type."""
valid = VALID_METRICS.get(view_type, [])
if metric not in valid:
raise ValueError(f"Invalid metric '{metric}' for {view_type}. Valid: {', '.join(valid)}")
def validate_dimensions(dimensions: list[str] | None, view_type: str) -> None:
"""Validate dimensions for view type."""
if not dimensions:
return
valid = VALID_DIMENSIONS.get(view_type, [])
for dim in dimensions:
if dim not in valid:
raise ValueError(f"Invalid dimension '{dim}' for {view_type}. Valid: {', '.join(valid)}")
def fetch_metrics(
server: str,
experiment_ids: list[str],
metric_name: str,
aggregations: list[dict],
view_type: int = 1,
dimensions: list[str] | None = None,
filters: list[str] | None = None,
time_interval_seconds: int | None = None,
start_time_ms: int | None = None,
end_time_ms: int | None = None,
max_results: int = DEFAULT_MAX_RESULTS,
) -> dict:
"""Fetch metrics from MLflow tracking server."""
url = f"{server.rstrip('/')}{API_PATH}"
payload = {
"experiment_ids": experiment_ids,
"view_type": view_type,
"metric_name": metric_name,
"aggregations": aggregations,
"max_results": max_results,
}
if dimensions:
payload["dimensions"] = dimensions
if filters:
payload["filters"] = filters
if time_interval_seconds:
payload["time_interval_seconds"] = time_interval_seconds
if start_time_ms:
payload["start_time_ms"] = start_time_ms
if end_time_ms:
payload["end_time_ms"] = end_time_ms
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST")
try:
with urllib.request.urlopen(req, timeout=30) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8")
try:
err = json.loads(body)
msg = err.get("message", body)
except json.JSONDecodeError:
msg = body
raise RuntimeError(f"MLflow API error (HTTP {e.code}): {msg}")
except urllib.error.URLError as e:
raise RuntimeError(f"Cannot connect to {server}: {e.reason}")
def format_table(data_points: list[dict]) -> str:
"""Format data points as aligned table."""
if not data_points:
return "No data points found."
first = data_points[0]
dim_keys = list(first.get("dimensions", {}).keys())
value_keys = list(first.get("values", {}).keys())
headers = dim_keys + value_keys
rows = []
for dp in data_points:
row = [str(dp.get("dimensions", {}).get(k, "")) for k in dim_keys]
for k in value_keys:
val = dp.get("values", {}).get(k)
if val is None:
row.append("N/A")
elif isinstance(val, float):
row.append(f"{val:.2f}" if val != int(val) else str(int(val)))
else:
row.append(str(val))
rows.append(row)
widths = [max(len(h), max((len(r[i]) for r in rows), default=0)) for i, h in enumerate(headers)]
lines = [
" ".join(h.ljust(widths[i]) for i, h in enumerate(headers)),
" ".join("-" * w for w in widths),
]
lines.extend(" ".join(cell.ljust(widths[i]) for i, cell in enumerate(row)) for row in rows)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Fetch MLflow trace metrics")
parser.add_argument("-s", "--server", required=True, help="MLflow tracking server URL")
parser.add_argument("-x", "--experiment-ids", required=True, help="Experiment IDs (comma-separated)")
parser.add_argument("-m", "--metric", required=True, help="Metric name")
parser.add_argument("-a", "--aggregations", required=True, help="Aggregations: COUNT,SUM,AVG,MIN,MAX,P50,P95")
parser.add_argument("-v", "--view-type", default="TRACES", choices=VIEW_TYPES.keys(), help="View type")
parser.add_argument("-d", "--dimensions", help="Dimensions to group by (comma-separated)")
parser.add_argument("-f", "--filters", help="Filter expressions (comma-separated)")
parser.add_argument("-t", "--time-interval", type=int, help="Time bucket in seconds (3600=hourly)")
parser.add_argument("--start-time", help="Start time: -24h, -7d, now, ISO 8601, or epoch ms")
parser.add_argument("--end-time", help="End time: same formats as start-time")
parser.add_argument("--max-results", type=int, default=DEFAULT_MAX_RESULTS, help="Max results")
parser.add_argument("-o", "--output", choices=["table", "json"], default="table", help="Output format")
args = parser.parse_args()
try:
# Parse and validate
experiment_ids = [x.strip() for x in args.experiment_ids.split(",")]
aggregations = parse_aggregations(args.aggregations)
validate_metric(args.metric, args.view_type)
dimensions = [x.strip() for x in args.dimensions.split(",")] if args.dimensions else None
validate_dimensions(dimensions, args.view_type)
filters = [x.strip() for x in args.filters.split(",")] if args.filters else None
start_time_ms = parse_time(args.start_time) if args.start_time else None
end_time_ms = parse_time(args.end_time) if args.end_time else None
if args.time_interval and (not start_time_ms or not end_time_ms):
raise ValueError("--start-time and --end-time required with --time-interval")
result = fetch_metrics(
server=args.server,
experiment_ids=experiment_ids,
metric_name=args.metric,
aggregations=aggregations,
view_type=VIEW_TYPES[args.view_type],
dimensions=dimensions,
filters=filters,
time_interval_seconds=args.time_interval,
start_time_ms=start_time_ms,
end_time_ms=end_time_ms,
max_results=args.max_results,
)
if args.output == "json":
print(json.dumps(result, indent=2))
else:
print(format_table(result.get("data_points", [])))
if result.get("next_page_token"):
print(f"\nMore results available (token: {result['next_page_token']})")
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
except RuntimeError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
Related skills
How it compares
Use querying-mlflow-metrics for aggregated cost and latency trends; use trace session skills when you need turn-by-turn conversation debugging.
FAQ
Which metrics does querying-mlflow-metrics support?
querying-mlflow-metrics covers trace_count, latency, input_tokens, output_tokens, and total_tokens. Add -v SPANS for span_count or -v ASSESSMENTS for assessment_value and assessment_count queries.
How does querying-mlflow-metrics bucket time series?
querying-mlflow-metrics passes --time-interval in seconds to fetch_metrics.py, for example 3600 for hourly buckets. Combine it with --start-time -24h and --end-time now for rolling windows.
What script powers querying-mlflow-metrics?
querying-mlflow-metrics runs scripts/fetch_metrics.py with required --server and --experiment-ids flags plus --metric and --aggregations selections, optionally emitting JSON via -o json.