forked from phoenix-oss/llama-stack-mirror
# What does this PR do? Change the Telemetry API to be able to support different use cases like returning traces for the UI and ability to export for Evals. Other changes: * Add a new trace_protocol decorator to decorate all our API methods so that any call to them will automatically get traced across all impls. * There is some issue with the decorator pattern of span creation when using async generators, where there are multiple yields with in the same context. I think its much more explicit by using the explicit context manager pattern using with. I moved the span creations in agent instance to be using with * Inject session id at the turn level, which should quickly give us all traces across turns for a given session Addresses #509 ## Test Plan ``` llama stack run /Users/dineshyv/.llama/distributions/llamastack-together/together-run.yaml PYTHONPATH=. python -m examples.agents.rag_with_memory_bank localhost 5000 curl -X POST 'http://localhost:5000/alpha/telemetry/query-traces' \ -H 'Content-Type: application/json' \ -d '{ "attribute_filters": [ { "key": "session_id", "op": "eq", "value": "dd667b87-ca4b-4d30-9265-5a0de318fc65" }], "limit": 100, "offset": 0, "order_by": ["start_time"] }' | jq . [ { "trace_id": "6902f54b83b4b48be18a6f422b13e16f", "root_span_id": "5f37b85543afc15a", "start_time": "2024-12-04T08:08:30.501587", "end_time": "2024-12-04T08:08:36.026463" }, { "trace_id": "92227dac84c0615ed741be393813fb5f", "root_span_id": "af7c5bb46665c2c8", "start_time": "2024-12-04T08:08:36.031170", "end_time": "2024-12-04T08:08:41.693301" }, { "trace_id": "7d578a6edac62f204ab479fba82f77b6", "root_span_id": "1d935e3362676896", "start_time": "2024-12-04T08:08:41.695204", "end_time": "2024-12-04T08:08:47.228016" }, { "trace_id": "dbd767d76991bc816f9f078907dc9ff2", "root_span_id": "f5a7ee76683b9602", "start_time": "2024-12-04T08:08:47.234578", "end_time": "2024-12-04T08:08:53.189412" } ] curl -X POST 'http://localhost:5000/alpha/telemetry/get-span-tree' \ -H 'Content-Type: application/json' \ -d '{ "span_id" : "6cceb4b48a156913", "max_depth": 2, "attributes_to_return": ["input"] }' | jq . % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 875 100 790 100 85 18462 1986 --:--:-- --:--:-- --:--:-- 20833 { "span_id": "6cceb4b48a156913", "trace_id": "dafa796f6aaf925f511c04cd7c67fdda", "parent_span_id": "892a66d726c7f990", "name": "retrieve_rag_context", "start_time": "2024-12-04T09:28:21.781995", "end_time": "2024-12-04T09:28:21.913352", "attributes": { "input": [ "{\"role\":\"system\",\"content\":\"You are a helpful assistant\"}", "{\"role\":\"user\",\"content\":\"What are the top 5 topics that were explained in the documentation? Only list succinct bullet points.\",\"context\":null}" ] }, "children": [ { "span_id": "1a2df181854064a8", "trace_id": "dafa796f6aaf925f511c04cd7c67fdda", "parent_span_id": "6cceb4b48a156913", "name": "MemoryRouter.query_documents", "start_time": "2024-12-04T09:28:21.787620", "end_time": "2024-12-04T09:28:21.906512", "attributes": { "input": null }, "children": [], "status": "ok" } ], "status": "ok" } ``` <img width="1677" alt="Screenshot 2024-12-04 at 9 42 56 AM" src="https://github.com/user-attachments/assets/4d3cea93-05ce-415a-93d9-4b1628631bf8">
177 lines
6.2 KiB
Python
177 lines
6.2 KiB
Python
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
# All rights reserved.
|
|
#
|
|
# This source code is licensed under the terms described in the LICENSE file in
|
|
# the root directory of this source tree.
|
|
|
|
import json
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
|
|
import aiosqlite
|
|
|
|
from llama_stack.apis.telemetry import (
|
|
QueryCondition,
|
|
SpanWithChildren,
|
|
Trace,
|
|
TraceStore,
|
|
)
|
|
|
|
|
|
class SQLiteTraceStore(TraceStore):
|
|
def __init__(self, conn_string: str):
|
|
self.conn_string = conn_string
|
|
|
|
async def query_traces(
|
|
self,
|
|
attribute_filters: Optional[List[QueryCondition]] = None,
|
|
attributes_to_return: Optional[List[str]] = None,
|
|
limit: Optional[int] = 100,
|
|
offset: Optional[int] = 0,
|
|
order_by: Optional[List[str]] = None,
|
|
) -> List[Trace]:
|
|
print(attribute_filters, attributes_to_return, limit, offset, order_by)
|
|
|
|
def build_attribute_select() -> str:
|
|
if not attributes_to_return:
|
|
return ""
|
|
return "".join(
|
|
f", json_extract(s.attributes, '$.{key}') as attr_{key}"
|
|
for key in attributes_to_return
|
|
)
|
|
|
|
def build_where_clause() -> tuple[str, list]:
|
|
if not attribute_filters:
|
|
return "", []
|
|
|
|
conditions = [
|
|
f"json_extract(s.attributes, '$.{condition.key}') {condition.op} ?"
|
|
for condition in attribute_filters
|
|
]
|
|
params = [condition.value for condition in attribute_filters]
|
|
where_clause = " WHERE " + " AND ".join(conditions)
|
|
return where_clause, params
|
|
|
|
def build_order_clause() -> str:
|
|
if not order_by:
|
|
return ""
|
|
|
|
order_clauses = []
|
|
for field in order_by:
|
|
desc = field.startswith("-")
|
|
clean_field = field[1:] if desc else field
|
|
order_clauses.append(f"t.{clean_field} {'DESC' if desc else 'ASC'}")
|
|
return " ORDER BY " + ", ".join(order_clauses)
|
|
|
|
# Build the main query
|
|
base_query = """
|
|
WITH matching_traces AS (
|
|
SELECT DISTINCT t.trace_id
|
|
FROM traces t
|
|
JOIN spans s ON t.trace_id = s.trace_id
|
|
{where_clause}
|
|
),
|
|
filtered_traces AS (
|
|
SELECT t.trace_id, t.root_span_id, t.start_time, t.end_time
|
|
{attribute_select}
|
|
FROM matching_traces mt
|
|
JOIN traces t ON mt.trace_id = t.trace_id
|
|
LEFT JOIN spans s ON t.trace_id = s.trace_id
|
|
{order_clause}
|
|
)
|
|
SELECT DISTINCT trace_id, root_span_id, start_time, end_time
|
|
FROM filtered_traces
|
|
LIMIT {limit} OFFSET {offset}
|
|
"""
|
|
|
|
where_clause, params = build_where_clause()
|
|
query = base_query.format(
|
|
attribute_select=build_attribute_select(),
|
|
where_clause=where_clause,
|
|
order_clause=build_order_clause(),
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
|
|
# Execute query and return results
|
|
async with aiosqlite.connect(self.conn_string) as conn:
|
|
conn.row_factory = aiosqlite.Row
|
|
async with conn.execute(query, params) as cursor:
|
|
rows = await cursor.fetchall()
|
|
return [
|
|
Trace(
|
|
trace_id=row["trace_id"],
|
|
root_span_id=row["root_span_id"],
|
|
start_time=datetime.fromisoformat(row["start_time"]),
|
|
end_time=datetime.fromisoformat(row["end_time"]),
|
|
)
|
|
for row in rows
|
|
]
|
|
|
|
async def get_materialized_span(
|
|
self,
|
|
span_id: str,
|
|
attributes_to_return: Optional[List[str]] = None,
|
|
max_depth: Optional[int] = None,
|
|
) -> SpanWithChildren:
|
|
# Build the attributes selection
|
|
attributes_select = "s.attributes"
|
|
if attributes_to_return:
|
|
json_object = ", ".join(
|
|
f"'{key}', json_extract(s.attributes, '$.{key}')"
|
|
for key in attributes_to_return
|
|
)
|
|
attributes_select = f"json_object({json_object})"
|
|
|
|
# SQLite CTE query with filtered attributes
|
|
query = f"""
|
|
WITH RECURSIVE span_tree AS (
|
|
SELECT s.*, 1 as depth, {attributes_select} as filtered_attributes
|
|
FROM spans s
|
|
WHERE s.span_id = ?
|
|
|
|
UNION ALL
|
|
|
|
SELECT s.*, st.depth + 1, {attributes_select} as filtered_attributes
|
|
FROM spans s
|
|
JOIN span_tree st ON s.parent_span_id = st.span_id
|
|
WHERE (? IS NULL OR st.depth < ?)
|
|
)
|
|
SELECT *
|
|
FROM span_tree
|
|
ORDER BY depth, start_time
|
|
"""
|
|
|
|
async with aiosqlite.connect(self.conn_string) as conn:
|
|
conn.row_factory = aiosqlite.Row
|
|
async with conn.execute(query, (span_id, max_depth, max_depth)) as cursor:
|
|
rows = await cursor.fetchall()
|
|
|
|
if not rows:
|
|
raise ValueError(f"Span {span_id} not found")
|
|
|
|
# Build span tree
|
|
spans_by_id = {}
|
|
root_span = None
|
|
|
|
for row in rows:
|
|
span = SpanWithChildren(
|
|
span_id=row["span_id"],
|
|
trace_id=row["trace_id"],
|
|
parent_span_id=row["parent_span_id"],
|
|
name=row["name"],
|
|
start_time=datetime.fromisoformat(row["start_time"]),
|
|
end_time=datetime.fromisoformat(row["end_time"]),
|
|
attributes=json.loads(row["filtered_attributes"]),
|
|
status=row["status"].lower(),
|
|
children=[],
|
|
)
|
|
|
|
spans_by_id[span.span_id] = span
|
|
|
|
if span.span_id == span_id:
|
|
root_span = span
|
|
elif span.parent_span_id in spans_by_id:
|
|
spans_by_id[span.parent_span_id].children.append(span)
|
|
|
|
return root_span
|