Skip to content

Commit 8714dba

Browse files
authored
Add traces resource for list and get (#119)
* Add traces resource for list and get * Cleanup trace obj * Add time range conversion
1 parent 8429fc5 commit 8714dba

File tree

4 files changed

+212
-2
lines changed

4 files changed

+212
-2
lines changed

quotientai/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ def __init__(self, api_key: Optional[str] = None, lazy_init: bool = False):
618618
self.auth = None
619619
self.logs = None
620620
self.tracing = None
621+
self.traces = None
621622
self.logger = None
622623

623624
# Always create a tracer instance for lazy_init mode to avoid decorator errors
@@ -660,6 +661,7 @@ def _ensure_initialized(self):
660661
self.auth = resources.AuthResource(_client)
661662
self.logs = resources.LogsResource(_client)
662663
self.tracing = resources.TracingResource(_client)
664+
self.traces = resources.TracesResource(_client)
663665

664666
# Create an unconfigured logger instance.
665667
self.logger = QuotientLogger(self.logs)

quotientai/resources/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from quotientai.resources.auth import AuthResource
22
from quotientai.resources.logs import LogsResource
33
from quotientai.resources.logs import AsyncLogsResource
4+
from quotientai.resources.tracing import TracesResource
45
from quotientai.tracing.core import TracingResource
56

67
__all__ = [
78
"AuthResource",
89
"LogsResource",
910
"AsyncLogsResource",
11+
"TracesResource",
1012
"TracingResource",
1113
]

quotientai/resources/tracing.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import json
2+
import re
3+
4+
from dataclasses import dataclass
5+
from datetime import datetime, timedelta
6+
from typing import Any, Dict, List, Optional
7+
8+
from quotientai.exceptions import logger
9+
10+
11+
@dataclass
12+
class Trace:
13+
"""
14+
Represents a trace from the QuotientAI API
15+
"""
16+
17+
trace_id: str
18+
root_span: Optional[Dict[str, Any]] = None
19+
total_duration_ms: float = 0
20+
start_time: Optional[datetime] = None
21+
end_time: Optional[datetime] = None
22+
span_list: List[Dict[str, Any]] = None
23+
24+
def __post_init__(self):
25+
if self.span_list is None:
26+
self.span_list = []
27+
28+
def __rich_repr__(self): # pragma: no cover
29+
yield "id", self.trace_id
30+
yield "total_duration_ms", self.total_duration_ms
31+
32+
if self.start_time:
33+
yield "start_time", self.start_time
34+
if self.end_time:
35+
yield "end_time", self.end_time
36+
37+
38+
class Traces:
39+
"""
40+
Container for traces that matches the API response schema.
41+
"""
42+
43+
def __init__(self, data: List[Trace], count: int):
44+
self.data = data
45+
self.count = count
46+
47+
def __repr__(self):
48+
return f"Traces(count={self.count}, data=[{type(self.data[0] if self.data else None)}])"
49+
50+
def to_jsonl(self, filename: Optional[str] = None) -> str:
51+
"""
52+
Export traces to JSON Lines format.
53+
54+
Args:
55+
filename: Optional filename to save the JSON Lines data to
56+
57+
Returns:
58+
String containing JSON Lines data
59+
"""
60+
jsonl_lines = []
61+
for trace in self.data:
62+
# Convert Trace object to dict for JSON serialization
63+
trace_dict = {
64+
"trace_id": trace.trace_id,
65+
"root_span": trace.root_span,
66+
"total_duration_ms": trace.total_duration_ms,
67+
"start_time": trace.start_time.isoformat() if trace.start_time else None,
68+
"end_time": trace.end_time.isoformat() if trace.end_time else None,
69+
"span_list": trace.span_list,
70+
}
71+
jsonl_lines.append(json.dumps(trace_dict))
72+
73+
jsonl_data = "\n".join(jsonl_lines)
74+
75+
if filename:
76+
with open(filename, 'w') as f:
77+
f.write(jsonl_data)
78+
79+
return jsonl_data
80+
81+
82+
class TracesResource:
83+
"""
84+
Resource for interacting with traces in the Quotient API.
85+
"""
86+
87+
def __init__(self, client):
88+
self._client = client
89+
90+
def list(
91+
self,
92+
*,
93+
time_range: Optional[str] = None,
94+
app_name: Optional[str] = None,
95+
environments: Optional[List[str]] = None,
96+
compress: bool = True,
97+
) -> Traces:
98+
"""
99+
List traces with optional filtering parameters.
100+
101+
Args:
102+
time_range: Optional time range filter (e.g., "1d", "1h", "1m")
103+
app_name: Optional app name filter
104+
environments: Optional list of environments to filter by
105+
compress: Whether to request compressed response
106+
107+
Returns:
108+
Traces object containing traces and total count
109+
"""
110+
try:
111+
params = {}
112+
if time_range:
113+
params["time_range"] = time_range
114+
# convert time range from 1d / 1h / 1m to 1 DAY / 1 HOUR / 1 MINUTE, months to MONTHS
115+
params["time_range"] = params["time_range"].replace("d", " DAY").replace("h", " HOUR").replace("m", " MINUTE").replace("M", " MONTHS")
116+
# add a space between the number and the unit
117+
params["time_range"] = re.sub(r'(\d+)([a-zA-Z]+)', r'\1 \2', params["time_range"])
118+
119+
if app_name:
120+
params["app_name"] = app_name
121+
if environments:
122+
params["environments"] = environments
123+
if compress:
124+
params["compress"] = "true"
125+
126+
headers = {}
127+
if compress:
128+
headers["Accept-Encoding"] = "gzip"
129+
130+
# the response is already decompressed by httpx
131+
# https://www.python-httpx.org/quickstart/#binary-response-content
132+
response = self._client._get("/traces", params=params)
133+
134+
# Convert trace dictionaries to Trace objects
135+
trace_objects = []
136+
for trace_dict in response.get("traces", []):
137+
# Parse datetime fields
138+
start_time = None
139+
if trace_dict.get("start_time"):
140+
start_time = datetime.fromisoformat(trace_dict["start_time"].replace('Z', '+00:00'))
141+
142+
end_time = None
143+
if trace_dict.get("end_time"):
144+
end_time = datetime.fromisoformat(trace_dict["end_time"].replace('Z', '+00:00'))
145+
146+
trace = Trace(
147+
trace_id=trace_dict["trace_id"],
148+
root_span=trace_dict.get("root_span"),
149+
total_duration_ms=trace_dict.get("total_duration_ms", 0),
150+
start_time=start_time,
151+
end_time=end_time,
152+
span_list=trace_dict.get("span_list", []),
153+
)
154+
trace_objects.append(trace)
155+
156+
traces = Traces(
157+
data=trace_objects,
158+
count=len(trace_objects),
159+
)
160+
161+
except Exception as e:
162+
logger.error(f"Error listing traces: {str(e)}")
163+
raise
164+
165+
# Return Traces object with structured response
166+
return traces
167+
168+
def get(self, trace_id: str) -> Trace:
169+
"""
170+
Get a specific trace by its ID.
171+
172+
Args:
173+
trace_id: The ID of the trace to retrieve
174+
175+
Returns:
176+
Trace object containing the trace data
177+
"""
178+
try:
179+
response = self._client._get(f"/traces/{trace_id}")
180+
181+
# Response is already parsed JSON from @handle_errors decorator
182+
trace_dict = response
183+
184+
# Parse datetime fields
185+
start_time = None
186+
if trace_dict.get("start_time"):
187+
start_time = datetime.fromisoformat(trace_dict["start_time"].replace('Z', '+00:00'))
188+
189+
end_time = None
190+
if trace_dict.get("end_time"):
191+
end_time = datetime.fromisoformat(trace_dict["end_time"].replace('Z', '+00:00'))
192+
193+
trace = Trace(
194+
trace_id=trace_dict["trace_id"],
195+
root_span=trace_dict.get("root_span"),
196+
total_duration_ms=trace_dict.get("total_duration_ms", 0),
197+
start_time=start_time,
198+
end_time=end_time,
199+
span_list=trace_dict.get("span_list", []),
200+
)
201+
except Exception as e:
202+
logger.error(f"Error getting trace {trace_id}: {str(e)}")
203+
raise
204+
205+
return trace
206+

quotientai/tracing/core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1+
import atexit
12
import contextlib
23
import functools
34
import inspect
45
import json
56
import os
6-
import atexit
77
import time
8+
89
from enum import Enum
910
from typing import Optional
1011

11-
1212
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
1313

1414
from opentelemetry.sdk.trace import TracerProvider

0 commit comments

Comments
 (0)