veris-ai 1.1.0__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of veris-ai might be problematic. Click here for more details.
- {veris_ai-1.1.0 → veris_ai-1.3.0}/PKG-INFO +14 -5
- {veris_ai-1.1.0 → veris_ai-1.3.0}/README.md +13 -4
- {veris_ai-1.1.0 → veris_ai-1.3.0}/pyproject.toml +1 -1
- {veris_ai-1.1.0 → veris_ai-1.3.0}/src/veris_ai/__init__.py +3 -7
- veris_ai-1.3.0/src/veris_ai/jaeger_interface/README.md +137 -0
- veris_ai-1.3.0/src/veris_ai/jaeger_interface/__init__.py +39 -0
- veris_ai-1.3.0/src/veris_ai/jaeger_interface/client.py +236 -0
- veris_ai-1.3.0/src/veris_ai/jaeger_interface/models.py +78 -0
- veris_ai-1.3.0/src/veris_ai/models.py +11 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/src/veris_ai/tool_mock.py +92 -31
- {veris_ai-1.1.0 → veris_ai-1.3.0}/src/veris_ai/utils.py +1 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/tests/conftest.py +12 -2
- {veris_ai-1.1.0 → veris_ai-1.3.0}/tests/test_tool_mock.py +25 -25
- {veris_ai-1.1.0 → veris_ai-1.3.0}/tests/test_utils.py +22 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/uv.lock +1 -1
- veris_ai-1.1.0/src/veris_ai/jaeger_interface/README.md +0 -109
- veris_ai-1.1.0/src/veris_ai/jaeger_interface/__init__.py +0 -26
- veris_ai-1.1.0/src/veris_ai/jaeger_interface/client.py +0 -133
- veris_ai-1.1.0/src/veris_ai/jaeger_interface/models.py +0 -153
- {veris_ai-1.1.0 → veris_ai-1.3.0}/.github/workflows/release.yml +0 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/.github/workflows/test.yml +0 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/.gitignore +0 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/CHANGELOG.md +0 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/CLAUDE.md +0 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/LICENSE +0 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/examples/__init__.py +0 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/examples/import_options.py +0 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/src/veris_ai/braintrust_tracing.py +0 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/tests/__init__.py +0 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/tests/fixtures/__init__.py +0 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/tests/fixtures/simple_app.py +0 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/tests/fixtures/sse_server.py +0 -0
- {veris_ai-1.1.0 → veris_ai-1.3.0}/tests/test_mcp_protocol_server_mocked.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: veris-ai
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: A Python package for Veris AI tools
|
|
5
5
|
Project-URL: Homepage, https://github.com/veris-ai/veris-python-sdk
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/veris-ai/veris-python-sdk/issues
|
|
@@ -64,7 +64,7 @@ The SDK supports flexible import patterns to minimize dependencies:
|
|
|
64
64
|
|
|
65
65
|
```python
|
|
66
66
|
# These imports only require base dependencies (httpx, pydantic, requests)
|
|
67
|
-
from veris_ai import veris, JaegerClient
|
|
67
|
+
from veris_ai import veris, JaegerClient
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
### Optional Imports (Require Extra Dependencies)
|
|
@@ -434,14 +434,23 @@ This project is licensed under the MIT License - see the LICENSE file for detail
|
|
|
434
434
|
## Jaeger Trace Interface
|
|
435
435
|
|
|
436
436
|
A lightweight, fully-typed wrapper around the Jaeger **Query Service** HTTP API lives under `veris_ai.jaeger_interface`.
|
|
437
|
-
It allows you to **search and retrieve traces** from both Jaeger
|
|
437
|
+
It allows you to **search and retrieve traces** from both Jaeger's default storage back-ends as well as **OpenSearch**, with additional **client-side span filtering** capabilities.
|
|
438
438
|
|
|
439
439
|
```python
|
|
440
|
-
from veris_ai.jaeger_interface import JaegerClient
|
|
440
|
+
from veris_ai.jaeger_interface import JaegerClient
|
|
441
441
|
|
|
442
442
|
client = JaegerClient("http://localhost:16686")
|
|
443
443
|
|
|
444
|
-
|
|
444
|
+
# Search with trace-level filters (server-side)
|
|
445
|
+
traces = client.search(service="veris-agent", limit=2, tags={"error": "true"})
|
|
446
|
+
|
|
447
|
+
# Search with span-level filters (client-side, OR logic)
|
|
448
|
+
filtered = client.search(
|
|
449
|
+
service="veris-agent",
|
|
450
|
+
limit=10,
|
|
451
|
+
span_tags={"http.status_code": 500, "db.error": "timeout"}
|
|
452
|
+
)
|
|
453
|
+
|
|
445
454
|
first_trace = client.get_trace(traces.data[0].traceID)
|
|
446
455
|
```
|
|
447
456
|
|
|
@@ -28,7 +28,7 @@ The SDK supports flexible import patterns to minimize dependencies:
|
|
|
28
28
|
|
|
29
29
|
```python
|
|
30
30
|
# These imports only require base dependencies (httpx, pydantic, requests)
|
|
31
|
-
from veris_ai import veris, JaegerClient
|
|
31
|
+
from veris_ai import veris, JaegerClient
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
### Optional Imports (Require Extra Dependencies)
|
|
@@ -398,14 +398,23 @@ This project is licensed under the MIT License - see the LICENSE file for detail
|
|
|
398
398
|
## Jaeger Trace Interface
|
|
399
399
|
|
|
400
400
|
A lightweight, fully-typed wrapper around the Jaeger **Query Service** HTTP API lives under `veris_ai.jaeger_interface`.
|
|
401
|
-
It allows you to **search and retrieve traces** from both Jaeger
|
|
401
|
+
It allows you to **search and retrieve traces** from both Jaeger's default storage back-ends as well as **OpenSearch**, with additional **client-side span filtering** capabilities.
|
|
402
402
|
|
|
403
403
|
```python
|
|
404
|
-
from veris_ai.jaeger_interface import JaegerClient
|
|
404
|
+
from veris_ai.jaeger_interface import JaegerClient
|
|
405
405
|
|
|
406
406
|
client = JaegerClient("http://localhost:16686")
|
|
407
407
|
|
|
408
|
-
|
|
408
|
+
# Search with trace-level filters (server-side)
|
|
409
|
+
traces = client.search(service="veris-agent", limit=2, tags={"error": "true"})
|
|
410
|
+
|
|
411
|
+
# Search with span-level filters (client-side, OR logic)
|
|
412
|
+
filtered = client.search(
|
|
413
|
+
service="veris-agent",
|
|
414
|
+
limit=10,
|
|
415
|
+
span_tags={"http.status_code": 500, "db.error": "timeout"}
|
|
416
|
+
)
|
|
417
|
+
|
|
409
418
|
first_trace = client.get_trace(traces.data[0].traceID)
|
|
410
419
|
```
|
|
411
420
|
|
|
@@ -5,7 +5,8 @@ from typing import Any
|
|
|
5
5
|
__version__ = "0.1.0"
|
|
6
6
|
|
|
7
7
|
# Import lightweight modules that only use base dependencies
|
|
8
|
-
from .jaeger_interface import JaegerClient
|
|
8
|
+
from .jaeger_interface import JaegerClient
|
|
9
|
+
from .models import ResponseExpectation
|
|
9
10
|
from .tool_mock import veris
|
|
10
11
|
|
|
11
12
|
# Lazy import for modules with heavy dependencies
|
|
@@ -33,9 +34,4 @@ def instrument(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401
|
|
|
33
34
|
return _instrument(*args, **kwargs)
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
__all__ = [
|
|
37
|
-
"veris",
|
|
38
|
-
"JaegerClient",
|
|
39
|
-
"SearchQuery",
|
|
40
|
-
"instrument",
|
|
41
|
-
]
|
|
37
|
+
__all__ = ["veris", "JaegerClient", "instrument", "ResponseExpectation"]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Jaeger Interface
|
|
2
|
+
|
|
3
|
+
This sub-package ships a **thin synchronous wrapper** around the
|
|
4
|
+
[Jaeger Query Service](https://www.jaegertracing.io/docs/) HTTP API so
|
|
5
|
+
that you can **search for and retrieve traces** directly from Python
|
|
6
|
+
with minimal boilerplate. It also provides **client-side span filtering**
|
|
7
|
+
capabilities for more granular control over the returned data.
|
|
8
|
+
|
|
9
|
+
> The client relies on `requests` (already included in the SDK's
|
|
10
|
+
> dependencies) and uses *pydantic* for full type-safety.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
`veris-ai` already lists both `requests` and `pydantic` as hard
|
|
17
|
+
requirements, so **no additional dependencies are required**.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install veris-ai
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quick-start
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from veris_ai.jaeger_interface import JaegerClient
|
|
29
|
+
import json
|
|
30
|
+
from veris_ai.jaeger_interface.models import Trace
|
|
31
|
+
# Replace with the URL of your Jaeger Query Service instance
|
|
32
|
+
client = JaegerClient("http://localhost:16686")
|
|
33
|
+
|
|
34
|
+
# --- 1. Search traces --------------------------------------------------
|
|
35
|
+
resp = client.search(
|
|
36
|
+
service="veris-agent",
|
|
37
|
+
limit=10,
|
|
38
|
+
# operation="CustomSpanData",
|
|
39
|
+
tags={"veris.session_id":"088b5aaf-84bd-4768-9a62-5e981222a9f2"},
|
|
40
|
+
span_tags={"bt.metadata.model":"gpt-4.1-2025-04-14"}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# save to json
|
|
44
|
+
with open("resp.json", "w") as f:
|
|
45
|
+
f.write(resp.model_dump_json(indent=2))
|
|
46
|
+
|
|
47
|
+
# Guard clause
|
|
48
|
+
if not resp or not resp.data:
|
|
49
|
+
print("No data found")
|
|
50
|
+
exit(1)
|
|
51
|
+
|
|
52
|
+
# Print trace ids
|
|
53
|
+
for trace in resp.data:
|
|
54
|
+
if isinstance(trace, Trace):
|
|
55
|
+
print("TRACE ID:", trace.traceID, len(trace.spans), "spans")
|
|
56
|
+
|
|
57
|
+
# --- 2. Retrieve a specific trace -------------------------------------
|
|
58
|
+
if isinstance(resp.data, list):
|
|
59
|
+
trace_id = resp.data[0].traceID
|
|
60
|
+
else:
|
|
61
|
+
trace_id = resp.data.traceID
|
|
62
|
+
|
|
63
|
+
detailed = client.get_trace(trace_id)
|
|
64
|
+
# save detailed to json
|
|
65
|
+
with open("detailed.json", "w") as f:
|
|
66
|
+
f.write(detailed.model_dump_json(indent=2))
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## API Reference
|
|
72
|
+
|
|
73
|
+
### `JaegerClient`
|
|
74
|
+
|
|
75
|
+
| Method | Description |
|
|
76
|
+
| -------- | ----------- |
|
|
77
|
+
| `search(service, *, limit=None, tags=None, operation=None, span_tags=None, **kwargs) -> SearchResponse` | Search for traces with optional span-level filtering. |
|
|
78
|
+
| `get_trace(trace_id: str) -> GetTraceResponse` | Fetch a single trace by ID (wrapper around `/api/traces/{id}`). |
|
|
79
|
+
|
|
80
|
+
### `search()` Parameters
|
|
81
|
+
|
|
82
|
+
The `search()` method now uses a flattened parameter structure:
|
|
83
|
+
|
|
84
|
+
| Parameter | Type | Description |
|
|
85
|
+
| --------- | ---- | ----------- |
|
|
86
|
+
| `service` | `str` | Service name to search for. Optional - if not provided, searches across all services. |
|
|
87
|
+
| `limit` | `int` | Maximum number of traces to return. |
|
|
88
|
+
| `tags` | `Dict[str, Any]` | Trace-level tag filters (AND logic). A trace must have a span matching ALL tags. |
|
|
89
|
+
| `operation` | `str` | Filter by operation name. |
|
|
90
|
+
| `span_tags` | `Dict[str, Any]` | Span-level tag filters (OR logic). Returns only spans matching ANY of these tags. |
|
|
91
|
+
| `span_operations` | `List[str]` | Span-level operation name filters (OR logic). Returns only spans matching ANY of these operations. |
|
|
92
|
+
| `**kwargs` | `Any` | Additional parameters passed directly to Jaeger API. |
|
|
93
|
+
|
|
94
|
+
### Filter Logic
|
|
95
|
+
|
|
96
|
+
The interface provides two levels of filtering:
|
|
97
|
+
|
|
98
|
+
1. **Trace-level filtering** (`tags` parameter):
|
|
99
|
+
- Sent directly to Jaeger API
|
|
100
|
+
- Uses AND logic: all tag key-value pairs must match on a single span
|
|
101
|
+
- Efficient server-side filtering
|
|
102
|
+
|
|
103
|
+
2. **Span-level filtering** (`span_tags` parameter):
|
|
104
|
+
- Applied client-side after retrieving traces
|
|
105
|
+
- Uses OR logic: spans matching ANY of the provided tags are included
|
|
106
|
+
- Traces with no matching spans are excluded from results
|
|
107
|
+
- Useful for finding spans with specific characteristics across different traces
|
|
108
|
+
|
|
109
|
+
### Example: Combining Filters
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
# Find traces in service that have errors, then filter to show only
|
|
113
|
+
# spans with specific HTTP status codes or database errors
|
|
114
|
+
traces = client.search(
|
|
115
|
+
service="my-api",
|
|
116
|
+
tags={"error": "true"}, # Trace must contain an error
|
|
117
|
+
span_tags={
|
|
118
|
+
"http.status_code": 500,
|
|
119
|
+
"http.status_code": 503,
|
|
120
|
+
"db.error": "connection_timeout"
|
|
121
|
+
} # Show only spans with these specific errors
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Compatibility
|
|
128
|
+
|
|
129
|
+
The implementation targets **Jaeger v1.x** REST endpoints. For clusters
|
|
130
|
+
backed by **OpenSearch** storage the same endpoints apply. Should you
|
|
131
|
+
need API v3 support feel free to open an issue or contribution—thanks!
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
This package is released under the **MIT license**.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Jaeger interface for searching and retrieving traces.
|
|
2
|
+
|
|
3
|
+
This sub-package provides a thin synchronous wrapper around the Jaeger
|
|
4
|
+
Query Service HTTP API with client-side span filtering capabilities.
|
|
5
|
+
|
|
6
|
+
Typical usage example::
|
|
7
|
+
|
|
8
|
+
from veris_ai.jaeger_interface import JaegerClient
|
|
9
|
+
|
|
10
|
+
client = JaegerClient("http://localhost:16686")
|
|
11
|
+
|
|
12
|
+
# Search traces with trace-level filters
|
|
13
|
+
traces = client.search(
|
|
14
|
+
service="veris-agent",
|
|
15
|
+
limit=20,
|
|
16
|
+
tags={"error": "true"} # AND logic at trace level
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Search with span-level filtering
|
|
20
|
+
traces_filtered = client.search(
|
|
21
|
+
service="veris-agent",
|
|
22
|
+
limit=20,
|
|
23
|
+
span_tags={
|
|
24
|
+
"http.status_code": 404,
|
|
25
|
+
"db.error": "timeout"
|
|
26
|
+
} # OR logic: spans with either tag are included
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Get a specific trace
|
|
30
|
+
trace = client.get_trace(traces.data[0].traceID)
|
|
31
|
+
|
|
32
|
+
The implementation uses *requests* under the hood and all public functions
|
|
33
|
+
are fully typed using *pydantic* models so that IDEs can provide proper
|
|
34
|
+
autocomplete and type checking.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from .client import JaegerClient as JaegerClient # noqa: F401
|
|
38
|
+
|
|
39
|
+
__all__ = ["JaegerClient"]
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Synchronous Jaeger Query Service client built on **requests**.
|
|
2
|
+
|
|
3
|
+
This implementation keeps dependencies minimal while providing fully-typed
|
|
4
|
+
*pydantic* models for both **request** and **response** bodies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import types
|
|
9
|
+
from typing import Any, Self
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
from .models import GetTraceResponse, SearchResponse, Span, Trace
|
|
14
|
+
|
|
15
|
+
__all__ = ["JaegerClient"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class JaegerClient: # noqa: D101
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
base_url: str,
|
|
22
|
+
*,
|
|
23
|
+
timeout: float | None = 10.0,
|
|
24
|
+
session: requests.Session | None = None,
|
|
25
|
+
headers: dict[str, str] | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Create a new *JaegerClient* instance.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
base_url: Base URL of the Jaeger Query Service (e.g. ``http://localhost:16686``).
|
|
31
|
+
timeout: Request timeout in **seconds** (applied to every call).
|
|
32
|
+
session: Optional pre-configured :class:`requests.Session` to reuse.
|
|
33
|
+
headers: Optional default headers to send with every request.
|
|
34
|
+
"""
|
|
35
|
+
# Normalise to avoid trailing slash duplicates
|
|
36
|
+
self._base_url = base_url.rstrip("/")
|
|
37
|
+
self._timeout = timeout
|
|
38
|
+
self._external_session = session # If provided we won't close it
|
|
39
|
+
self._headers = headers or {}
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------
|
|
42
|
+
# Internal helpers
|
|
43
|
+
# ---------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def _make_session(self) -> tuple[requests.Session, bool]: # noqa: D401
|
|
46
|
+
"""Return a *(session, should_close)* tuple.
|
|
47
|
+
|
|
48
|
+
If an external session was supplied we **must not** close it after the
|
|
49
|
+
request, hence the boolean flag letting callers know whether they are
|
|
50
|
+
responsible for closing the session.
|
|
51
|
+
"""
|
|
52
|
+
if self._external_session is not None:
|
|
53
|
+
return self._external_session, False
|
|
54
|
+
|
|
55
|
+
# Reuse the session opened via the context manager if available
|
|
56
|
+
if hasattr(self, "_session_ctx"):
|
|
57
|
+
return self._session_ctx, False
|
|
58
|
+
|
|
59
|
+
session = requests.Session()
|
|
60
|
+
session.headers.update(self._headers)
|
|
61
|
+
return session, True
|
|
62
|
+
|
|
63
|
+
def _span_matches_tags(self, span: Span, span_tags: dict[str, Any]) -> bool:
|
|
64
|
+
"""Check if a span matches any of the provided tags (OR logic)."""
|
|
65
|
+
if not span.tags or not span_tags:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
# Convert span tags to a dict for easier lookup
|
|
69
|
+
span_tag_dict = {tag.key: tag.value for tag in span.tags}
|
|
70
|
+
|
|
71
|
+
# OR logic: return True if ANY tag matches
|
|
72
|
+
return any(span_tag_dict.get(key) == value for key, value in span_tags.items())
|
|
73
|
+
|
|
74
|
+
def _filter_spans(
|
|
75
|
+
self,
|
|
76
|
+
traces: list[Trace],
|
|
77
|
+
span_tags: dict[str, Any] | None,
|
|
78
|
+
span_operations: list[str] | None = None,
|
|
79
|
+
) -> list[Trace]:
|
|
80
|
+
"""Filter spans within traces based on span_tags and/or span_operations.
|
|
81
|
+
|
|
82
|
+
Uses OR logic within each filter type. If both are provided, a span must
|
|
83
|
+
match at least one tag AND at least one operation.
|
|
84
|
+
"""
|
|
85
|
+
if not span_tags and not span_operations:
|
|
86
|
+
return traces
|
|
87
|
+
|
|
88
|
+
filtered_traces = []
|
|
89
|
+
for trace in traces:
|
|
90
|
+
filtered_spans = []
|
|
91
|
+
for span in trace.spans:
|
|
92
|
+
tag_match = True
|
|
93
|
+
op_match = True
|
|
94
|
+
|
|
95
|
+
if span_tags:
|
|
96
|
+
tag_match = self._span_matches_tags(span, span_tags)
|
|
97
|
+
if span_operations:
|
|
98
|
+
op_match = span.operationName in span_operations
|
|
99
|
+
|
|
100
|
+
# If both filters are provided, require both to match (AND logic)
|
|
101
|
+
if tag_match and op_match:
|
|
102
|
+
filtered_spans.append(span)
|
|
103
|
+
|
|
104
|
+
if filtered_spans:
|
|
105
|
+
filtered_trace = Trace(
|
|
106
|
+
traceID=trace.traceID,
|
|
107
|
+
spans=filtered_spans,
|
|
108
|
+
process=trace.process,
|
|
109
|
+
warnings=trace.warnings,
|
|
110
|
+
)
|
|
111
|
+
filtered_traces.append(filtered_trace)
|
|
112
|
+
|
|
113
|
+
return filtered_traces
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------
|
|
116
|
+
# Public API
|
|
117
|
+
# ---------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
def search( # noqa: PLR0913
|
|
120
|
+
self,
|
|
121
|
+
service: str | None = None,
|
|
122
|
+
*,
|
|
123
|
+
limit: int | None = None,
|
|
124
|
+
tags: dict[str, Any] | None = None,
|
|
125
|
+
operation: str | None = None,
|
|
126
|
+
span_tags: dict[str, Any] | None = None,
|
|
127
|
+
span_operations: list[str] | None = None,
|
|
128
|
+
**kwargs: Any, # noqa: ANN401
|
|
129
|
+
) -> SearchResponse: # noqa: D401
|
|
130
|
+
"""Search traces using the *v1* ``/api/traces`` endpoint with optional span filtering.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
service: Service name to search for. If not provided, searches across all services.
|
|
134
|
+
limit: Maximum number of traces to return.
|
|
135
|
+
tags: Dictionary of tag filters for trace-level filtering (AND-combined).
|
|
136
|
+
operation: Operation name to search for.
|
|
137
|
+
span_tags: Dictionary of tag filters for span-level filtering.
|
|
138
|
+
Uses OR logic. Combined with span_operations using AND.
|
|
139
|
+
Applied client-side after retrieving traces.
|
|
140
|
+
span_operations: List of operation names to search for.
|
|
141
|
+
Uses OR logic. Combined with span_tags using AND.
|
|
142
|
+
**kwargs: Additional parameters to pass to the Jaeger API.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Parsed :class:`~veris_ai.jaeger_interface.models.SearchResponse` model
|
|
146
|
+
with spans filtered according to span_tags if provided.
|
|
147
|
+
"""
|
|
148
|
+
# Build params for the Jaeger API (excluding span_tags)
|
|
149
|
+
params: dict[str, Any] = {}
|
|
150
|
+
|
|
151
|
+
if service is not None:
|
|
152
|
+
params["service"] = service
|
|
153
|
+
|
|
154
|
+
if limit is not None:
|
|
155
|
+
params["limit"] = limit
|
|
156
|
+
|
|
157
|
+
if operation is not None:
|
|
158
|
+
params["operation"] = operation
|
|
159
|
+
|
|
160
|
+
if tags:
|
|
161
|
+
# Convert tags to JSON string as expected by Jaeger API
|
|
162
|
+
params["tags"] = json.dumps(tags)
|
|
163
|
+
|
|
164
|
+
# Add any additional parameters
|
|
165
|
+
params.update(kwargs)
|
|
166
|
+
|
|
167
|
+
session, should_close = self._make_session()
|
|
168
|
+
try:
|
|
169
|
+
url = f"{self._base_url}/api/traces"
|
|
170
|
+
response = session.get(url, params=params, timeout=self._timeout)
|
|
171
|
+
response.raise_for_status()
|
|
172
|
+
data = response.json()
|
|
173
|
+
finally:
|
|
174
|
+
if should_close:
|
|
175
|
+
session.close()
|
|
176
|
+
|
|
177
|
+
# Parse the response
|
|
178
|
+
search_response = SearchResponse.model_validate(data) # type: ignore[arg-type]
|
|
179
|
+
|
|
180
|
+
# Apply span-level filtering if span_tags is provided
|
|
181
|
+
if (
|
|
182
|
+
(span_tags or span_operations)
|
|
183
|
+
and search_response.data
|
|
184
|
+
and isinstance(search_response.data, list)
|
|
185
|
+
):
|
|
186
|
+
filtered_traces = self._filter_spans(search_response.data, span_tags, span_operations)
|
|
187
|
+
search_response.data = filtered_traces
|
|
188
|
+
# Update the total to reflect filtered results
|
|
189
|
+
if search_response.total is not None:
|
|
190
|
+
search_response.total = len(filtered_traces)
|
|
191
|
+
|
|
192
|
+
return search_response
|
|
193
|
+
|
|
194
|
+
def get_trace(self, trace_id: str) -> GetTraceResponse: # noqa: D401
|
|
195
|
+
"""Retrieve a single trace by *trace_id*.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
trace_id: The Jaeger trace identifier.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Parsed :class:`~veris_ai.jaeger_interface.models.GetTraceResponse` model.
|
|
202
|
+
"""
|
|
203
|
+
if not trace_id:
|
|
204
|
+
error_msg = "trace_id must be non-empty"
|
|
205
|
+
raise ValueError(error_msg)
|
|
206
|
+
|
|
207
|
+
session, should_close = self._make_session()
|
|
208
|
+
try:
|
|
209
|
+
url = f"{self._base_url}/api/traces/{trace_id}"
|
|
210
|
+
response = session.get(url, timeout=self._timeout)
|
|
211
|
+
response.raise_for_status()
|
|
212
|
+
data = response.json()
|
|
213
|
+
finally:
|
|
214
|
+
if should_close:
|
|
215
|
+
session.close()
|
|
216
|
+
return GetTraceResponse.model_validate(data) # type: ignore[arg-type]
|
|
217
|
+
|
|
218
|
+
# ------------------------------------------------------------------
|
|
219
|
+
# Context-manager helpers (optional but convenient)
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
def __enter__(self) -> Self:
|
|
223
|
+
"""Enter the context manager."""
|
|
224
|
+
self._session_ctx, self._should_close_ctx = self._make_session()
|
|
225
|
+
return self
|
|
226
|
+
|
|
227
|
+
def __exit__(
|
|
228
|
+
self,
|
|
229
|
+
exc_type: type[BaseException] | None,
|
|
230
|
+
exc: BaseException | None,
|
|
231
|
+
tb: types.TracebackType | None,
|
|
232
|
+
) -> None:
|
|
233
|
+
"""Exit the context manager."""
|
|
234
|
+
# Only close if we created the session
|
|
235
|
+
if getattr(self, "_should_close_ctx", False):
|
|
236
|
+
self._session_ctx.close()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Tag",
|
|
9
|
+
"Process",
|
|
10
|
+
"Span",
|
|
11
|
+
"Trace",
|
|
12
|
+
"SearchResponse",
|
|
13
|
+
"GetTraceResponse",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Tag(BaseModel):
|
|
18
|
+
"""A Jaeger tag key/value pair."""
|
|
19
|
+
|
|
20
|
+
key: str
|
|
21
|
+
value: Any
|
|
22
|
+
type: str | None = None # Jaeger uses an optional *type* field in v1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Process(BaseModel):
|
|
26
|
+
"""Represents the *process* section of a Jaeger trace."""
|
|
27
|
+
|
|
28
|
+
serviceName: str = Field(alias="serviceName") # noqa: N815
|
|
29
|
+
tags: list[Tag] | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Span(BaseModel):
|
|
33
|
+
"""Represents a single Jaeger span."""
|
|
34
|
+
|
|
35
|
+
traceID: str # noqa: N815
|
|
36
|
+
spanID: str # noqa: N815
|
|
37
|
+
operationName: str # noqa: N815
|
|
38
|
+
startTime: int # noqa: N815
|
|
39
|
+
duration: int
|
|
40
|
+
tags: list[Tag] | None = None
|
|
41
|
+
references: list[dict[str, Any]] | None = None
|
|
42
|
+
processID: str | None = None # noqa: N815
|
|
43
|
+
|
|
44
|
+
model_config = ConfigDict(extra="allow")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Trace(BaseModel):
|
|
48
|
+
"""A full Jaeger trace as returned by the Query API."""
|
|
49
|
+
|
|
50
|
+
traceID: str # noqa: N815
|
|
51
|
+
spans: list[Span]
|
|
52
|
+
process: Process | dict[str, Process] | None = None
|
|
53
|
+
warnings: list[str] | None = None
|
|
54
|
+
|
|
55
|
+
model_config = ConfigDict(extra="allow")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _BaseResponse(BaseModel):
|
|
59
|
+
data: list[Trace] | Trace | None = None
|
|
60
|
+
errors: list[str] | None = None
|
|
61
|
+
|
|
62
|
+
# Allow any additional keys returned by Jaeger so that nothing gets
|
|
63
|
+
# silently dropped if the backend adds new fields we don't know about.
|
|
64
|
+
|
|
65
|
+
model_config = ConfigDict(extra="allow")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SearchResponse(_BaseResponse):
|
|
69
|
+
"""Response model for *search* or *find traces* requests."""
|
|
70
|
+
|
|
71
|
+
total: int | None = None
|
|
72
|
+
limit: int | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class GetTraceResponse(_BaseResponse):
|
|
76
|
+
"""Response model for *get trace by id* requests."""
|
|
77
|
+
|
|
78
|
+
# Same as base but alias for clarity
|