Skip to content

Commit 7869057

Browse files
committed
feat: implement query_metrics
query_metrics currently has no implementation, meaning once a metric is emitted there is no way in llama stack to query it from the store. implement query_metrics for the meta_reference provider which follows a similar style to `query_traces`, using the trace_store to format an SQL query and execute it in this case the parameters for the query are `metric.METRIC_NAME, start_time, and end_time`. this required client side changes since the client had no `query_metrics` or any associated resources, so any tests here will fail but I will provider manual execution logs for the new tests I am adding order the metrics by timestamp. Additionally add `unit` to the `MetricDataPoint` class since this adds much more context to the metric being queried. Signed-off-by: Charlie Doern <[email protected]>
1 parent e3928e6 commit 7869057

File tree

5 files changed

+161
-4
lines changed

5 files changed

+161
-4
lines changed

docs/_static/llama-stack-spec.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15846,12 +15846,16 @@
1584615846
"value": {
1584715847
"type": "number",
1584815848
"description": "The numeric value of the metric at this timestamp"
15849+
},
15850+
"unit": {
15851+
"type": "string"
1584915852
}
1585015853
},
1585115854
"additionalProperties": false,
1585215855
"required": [
1585315856
"timestamp",
15854-
"value"
15857+
"value",
15858+
"unit"
1585515859
],
1585615860
"title": "MetricDataPoint",
1585715861
"description": "A single data point in a metric time series."

docs/_static/llama-stack-spec.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11774,10 +11774,13 @@ components:
1177411774
type: number
1177511775
description: >-
1177611776
The numeric value of the metric at this timestamp
11777+
unit:
11778+
type: string
1177711779
additionalProperties: false
1177811780
required:
1177911781
- timestamp
1178011782
- value
11783+
- unit
1178111784
title: MetricDataPoint
1178211785
description: >-
1178311786
A single data point in a metric time series.

llama_stack/apis/telemetry/telemetry.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ class MetricDataPoint(BaseModel):
386386

387387
timestamp: int
388388
value: float
389+
unit: str
389390

390391

391392
@json_schema_type

llama_stack/providers/inline/telemetry/meta_reference/telemetry.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# This source code is licensed under the terms described in the LICENSE file in
55
# the root directory of this source tree.
66

7+
import datetime
78
import logging
89
import threading
910
from typing import Any
@@ -149,7 +150,36 @@ async def query_metrics(
149150
query_type: MetricQueryType = MetricQueryType.RANGE,
150151
label_matchers: list[MetricLabelMatcher] | None = None,
151152
) -> QueryMetricsResponse:
152-
raise NotImplementedError("Querying metrics is not implemented")
153+
"""Query metrics from the telemetry store.
154+
155+
Args:
156+
metric_name: The name of the metric to query (e.g., "prompt_tokens")
157+
start_time: Start time as Unix timestamp
158+
end_time: End time as Unix timestamp (defaults to now if None)
159+
granularity: Time granularity for aggregation (not implemented yet)
160+
query_type: Type of query (RANGE or INSTANT)
161+
label_matchers: Label filters to apply
162+
163+
Returns:
164+
QueryMetricsResponse with metric time series data
165+
"""
166+
# Convert timestamps to datetime objects
167+
start_dt = datetime.datetime.fromtimestamp(start_time, datetime.UTC)
168+
end_dt = datetime.datetime.fromtimestamp(end_time, datetime.UTC) if end_time else None
169+
170+
# Use SQLite trace store if available
171+
if hasattr(self, "trace_store") and self.trace_store:
172+
return await self.trace_store.query_metrics(
173+
metric_name=metric_name,
174+
start_time=start_dt,
175+
end_time=end_dt,
176+
granularity=granularity,
177+
query_type=query_type,
178+
label_matchers=label_matchers,
179+
)
180+
181+
# Fallback to empty response if no trace store
182+
return QueryMetricsResponse(data=[])
153183

154184
def _log_unstructured(self, event: UnstructuredLogEvent, ttl_seconds: int) -> None:
155185
with self._lock:

llama_stack/providers/utils/telemetry/sqlite_trace_store.py

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,23 @@
55
# the root directory of this source tree.
66

77
import json
8-
from datetime import datetime
8+
from datetime import UTC, datetime
99
from typing import Protocol
1010

1111
import aiosqlite
1212

13-
from llama_stack.apis.telemetry import QueryCondition, Span, SpanWithStatus, Trace
13+
from llama_stack.apis.telemetry import (
14+
MetricDataPoint,
15+
MetricLabel,
16+
MetricLabelMatcher,
17+
MetricQueryType,
18+
MetricSeries,
19+
QueryCondition,
20+
QueryMetricsResponse,
21+
Span,
22+
SpanWithStatus,
23+
Trace,
24+
)
1425

1526

1627
class TraceStore(Protocol):
@@ -29,11 +40,119 @@ async def get_span_tree(
2940
max_depth: int | None = None,
3041
) -> dict[str, SpanWithStatus]: ...
3142

43+
async def query_metrics(
44+
self,
45+
metric_name: str,
46+
start_time: datetime,
47+
end_time: datetime | None = None,
48+
granularity: str | None = "1d",
49+
query_type: MetricQueryType = MetricQueryType.RANGE,
50+
label_matchers: list[MetricLabelMatcher] | None = None,
51+
) -> QueryMetricsResponse: ...
52+
3253

3354
class SQLiteTraceStore(TraceStore):
3455
def __init__(self, conn_string: str):
3556
self.conn_string = conn_string
3657

58+
async def query_metrics(
59+
self,
60+
metric_name: str,
61+
start_time: datetime,
62+
end_time: datetime | None = None,
63+
granularity: str | None = "1d",
64+
query_type: MetricQueryType = MetricQueryType.RANGE,
65+
label_matchers: list[MetricLabelMatcher] | None = None,
66+
) -> QueryMetricsResponse:
67+
"""Query metrics from span events stored in SQLite.
68+
69+
Args:
70+
metric_name: The name of the metric to query (e.g., "prompt_tokens")
71+
start_time: Start time for the query range
72+
end_time: End time for the query range (defaults to now if None)
73+
granularity: Time granularity for aggregation (not implemented yet)
74+
query_type: Type of query (RANGE or INSTANT)
75+
label_matchers: Label filters to apply
76+
77+
Returns:
78+
QueryMetricsResponse with metric time series data
79+
"""
80+
if end_time is None:
81+
end_time = datetime.now(UTC)
82+
83+
# Build the base query
84+
query = """
85+
SELECT
86+
se.name,
87+
se.timestamp,
88+
se.attributes
89+
FROM span_events se
90+
WHERE se.name = ?
91+
AND se.timestamp BETWEEN ? AND ?
92+
"""
93+
94+
params = [f"metric.{metric_name}", start_time.isoformat(), end_time.isoformat()]
95+
96+
# Add label matchers if provided
97+
if label_matchers:
98+
for matcher in label_matchers:
99+
if matcher.operator == "=":
100+
query += f" AND json_extract(se.attributes, '$.{matcher.name}') = ?"
101+
params.append(matcher.value)
102+
elif matcher.operator == "!=":
103+
query += f" AND json_extract(se.attributes, '$.{matcher.name}') != ?"
104+
params.append(matcher.value)
105+
elif matcher.operator == "=~":
106+
query += f" AND json_extract(se.attributes, '$.{matcher.name}') LIKE ?"
107+
params.append(f"%{matcher.value}%")
108+
elif matcher.operator == "!~":
109+
query += f" AND json_extract(se.attributes, '$.{matcher.name}') NOT LIKE ?"
110+
params.append(f"%{matcher.value}%")
111+
112+
query += " ORDER BY se.timestamp"
113+
114+
# Execute query
115+
async with aiosqlite.connect(self.conn_string) as conn:
116+
conn.row_factory = aiosqlite.Row
117+
async with conn.execute(query, params) as cursor:
118+
rows = await cursor.fetchall()
119+
120+
if not rows:
121+
return QueryMetricsResponse(data=[])
122+
123+
# Parse metric data
124+
data_points = []
125+
labels: list[MetricLabel] = []
126+
127+
for row in rows:
128+
# Parse JSON attributes
129+
attributes = json.loads(row["attributes"])
130+
131+
# Extract metric value and unit
132+
value = attributes.get("value")
133+
unit = attributes.get("unit", "")
134+
135+
# Extract labels from attributes
136+
metric_labels = []
137+
for key, val in attributes.items():
138+
if key not in ["value", "unit"]:
139+
metric_labels.append(MetricLabel(name=key, value=str(val)))
140+
141+
# Create data point
142+
timestamp = datetime.fromisoformat(row["timestamp"])
143+
data_points.append(
144+
MetricDataPoint(
145+
timestamp=int(timestamp.timestamp()),
146+
value=value,
147+
unit=unit,
148+
)
149+
)
150+
151+
# Create metric series
152+
metric_series = [MetricSeries(metric=metric_name, labels=labels, values=data_points)]
153+
154+
return QueryMetricsResponse(data=metric_series)
155+
37156
async def query_traces(
38157
self,
39158
attribute_filters: list[QueryCondition] | None = None,

0 commit comments

Comments
 (0)