trodo-python 2.3.0__tar.gz → 2.4.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.
- {trodo_python-2.3.0/trodo_python.egg-info → trodo_python-2.4.0}/PKG-INFO +38 -1
- trodo_python-2.3.0/PKG-INFO → trodo_python-2.4.0/README.md +33 -27
- {trodo_python-2.3.0 → trodo_python-2.4.0}/pyproject.toml +8 -1
- trodo_python-2.4.0/tests/test_register_otel.py +183 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/__init__.py +46 -1
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/client.py +19 -1
- trodo_python-2.4.0/trodo/otel/register.py +200 -0
- trodo_python-2.4.0/trodo/otel/transport.py +64 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/otel/wrap_agent.py +109 -1
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/session/server_session.py +1 -0
- trodo_python-2.3.0/README.md → trodo_python-2.4.0/trodo_python.egg-info/PKG-INFO +64 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo_python.egg-info/SOURCES.txt +3 -0
- trodo_python-2.4.0/trodo_python.egg-info/requires.txt +15 -0
- trodo_python-2.3.0/trodo_python.egg-info/requires.txt +0 -10
- {trodo_python-2.3.0 → trodo_python-2.4.0}/setup.cfg +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/tests/test_cross_process_session.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/tests/test_end_run.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/tests/test_processor_methods.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/tests/test_start_run.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/tests/test_wrap_agent_unchanged.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/api/__init__.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/api/async_client.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/api/endpoints.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/api/http_client.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/auto/__init__.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/auto/auto_event_manager.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/managers/__init__.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/managers/group_manager.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/managers/people_manager.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/otel/__init__.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/otel/auto_instrument.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/otel/context.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/otel/helpers.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/otel/processor.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/queue/__init__.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/queue/batch_flusher.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/queue/event_queue.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/session/__init__.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/session/session_manager.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/types.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo/user_context.py +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo_python.egg-info/dependency_links.txt +0 -0
- {trodo_python-2.3.0 → trodo_python-2.4.0}/trodo_python.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trodo-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Trodo Analytics SDK for Python — server-side event tracking
|
|
5
5
|
License: ISC
|
|
6
6
|
Keywords: analytics,tracking,trodo,server-side
|
|
@@ -19,6 +19,10 @@ Description-Content-Type: text/markdown
|
|
|
19
19
|
Requires-Dist: requests>=2.28.0
|
|
20
20
|
Provides-Extra: async
|
|
21
21
|
Requires-Dist: httpx>=0.27.0; extra == "async"
|
|
22
|
+
Provides-Extra: otlp
|
|
23
|
+
Requires-Dist: opentelemetry-api>=1.20.0; extra == "otlp"
|
|
24
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == "otlp"
|
|
25
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0; extra == "otlp"
|
|
22
26
|
Provides-Extra: dev
|
|
23
27
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
24
28
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -37,6 +41,39 @@ pip install trodo-python
|
|
|
37
41
|
|
|
38
42
|
Requires Python 3.8+.
|
|
39
43
|
|
|
44
|
+
## OpenTelemetry / OTLP path (NEW in 2.4.0)
|
|
45
|
+
|
|
46
|
+
Already running OTel? Skip the Trodo SDK install entirely and point your existing pipeline at Trodo. Two env vars:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT=https://sdkapi.trodo.ai
|
|
50
|
+
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer ${TRODO_SITE_ID}"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The Bearer token is your **site_id** — same value you'd pass to `trodo.init(site_id=...)`. Get it from the [Integration Manager](https://app.trodo.ai/dashboard/integrations).
|
|
54
|
+
|
|
55
|
+
Use this when **you already run OTel** (Datadog, Jaeger, Honeycomb) and want Trodo as an additional destination. Install `trodo-python` and call `trodo.register_otel(site_id=..., mode='otlp')` to attach our OTLP exporter without replacing your existing setup. `wrap_agent` then routes through OTel so auto-instrumented children share the same trace.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# At app startup (after your existing OTel provider is registered)
|
|
59
|
+
import os, trodo
|
|
60
|
+
|
|
61
|
+
trodo.register_otel(
|
|
62
|
+
site_id=os.environ['TRODO_SITE_ID'],
|
|
63
|
+
mode='otlp',
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`mode='otlp'` requires the optional `[otlp]` extras:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install 'trodo-python[otlp]'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The SDK raises a friendly install hint if you call `mode='otlp'` without them.
|
|
74
|
+
|
|
75
|
+
For richer Trodo features on top (`wrap_agent`, `feedback`, `track_mcp`), continue with the SDK quick start below.
|
|
76
|
+
|
|
40
77
|
## Quick Start
|
|
41
78
|
|
|
42
79
|
```python
|
|
@@ -1,30 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: trodo-python
|
|
3
|
-
Version: 2.3.0
|
|
4
|
-
Summary: Trodo Analytics SDK for Python — server-side event tracking
|
|
5
|
-
License: ISC
|
|
6
|
-
Keywords: analytics,tracking,trodo,server-side
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
-
Classifier: License :: OSI Approved :: ISC License (ISCL)
|
|
14
|
-
Classifier: Operating System :: OS Independent
|
|
15
|
-
Classifier: Intended Audience :: Developers
|
|
16
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
-
Requires-Python: >=3.8
|
|
18
|
-
Description-Content-Type: text/markdown
|
|
19
|
-
Requires-Dist: requests>=2.28.0
|
|
20
|
-
Provides-Extra: async
|
|
21
|
-
Requires-Dist: httpx>=0.27.0; extra == "async"
|
|
22
|
-
Provides-Extra: dev
|
|
23
|
-
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
24
|
-
Requires-Dist: pytest-cov; extra == "dev"
|
|
25
|
-
Requires-Dist: responses>=0.25.0; extra == "dev"
|
|
26
|
-
Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
27
|
-
|
|
28
1
|
# trodo-python
|
|
29
2
|
|
|
30
3
|
Server-side Python SDK for [Trodo Analytics](https://trodo.ai). Track backend events, identify users, manage people/groups, and instrument AI agents — all unified with your frontend data under the same `site_id`.
|
|
@@ -37,6 +10,39 @@ pip install trodo-python
|
|
|
37
10
|
|
|
38
11
|
Requires Python 3.8+.
|
|
39
12
|
|
|
13
|
+
## OpenTelemetry / OTLP path (NEW in 2.4.0)
|
|
14
|
+
|
|
15
|
+
Already running OTel? Skip the Trodo SDK install entirely and point your existing pipeline at Trodo. Two env vars:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT=https://sdkapi.trodo.ai
|
|
19
|
+
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer ${TRODO_SITE_ID}"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The Bearer token is your **site_id** — same value you'd pass to `trodo.init(site_id=...)`. Get it from the [Integration Manager](https://app.trodo.ai/dashboard/integrations).
|
|
23
|
+
|
|
24
|
+
Use this when **you already run OTel** (Datadog, Jaeger, Honeycomb) and want Trodo as an additional destination. Install `trodo-python` and call `trodo.register_otel(site_id=..., mode='otlp')` to attach our OTLP exporter without replacing your existing setup. `wrap_agent` then routes through OTel so auto-instrumented children share the same trace.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
# At app startup (after your existing OTel provider is registered)
|
|
28
|
+
import os, trodo
|
|
29
|
+
|
|
30
|
+
trodo.register_otel(
|
|
31
|
+
site_id=os.environ['TRODO_SITE_ID'],
|
|
32
|
+
mode='otlp',
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`mode='otlp'` requires the optional `[otlp]` extras:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install 'trodo-python[otlp]'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The SDK raises a friendly install hint if you call `mode='otlp'` without them.
|
|
43
|
+
|
|
44
|
+
For richer Trodo features on top (`wrap_agent`, `feedback`, `track_mcp`), continue with the SDK quick start below.
|
|
45
|
+
|
|
40
46
|
## Quick Start
|
|
41
47
|
|
|
42
48
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "trodo-python"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.4.0"
|
|
8
8
|
description = "Trodo Analytics SDK for Python — server-side event tracking"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "ISC" }
|
|
@@ -28,6 +28,13 @@ classifiers = [
|
|
|
28
28
|
|
|
29
29
|
[project.optional-dependencies]
|
|
30
30
|
async = ["httpx>=0.27.0"]
|
|
31
|
+
# OTLP-mode dependencies for register_otel(mode='otlp'). Install with:
|
|
32
|
+
# pip install 'trodo-python[otlp]'
|
|
33
|
+
otlp = [
|
|
34
|
+
"opentelemetry-api>=1.20.0",
|
|
35
|
+
"opentelemetry-sdk>=1.20.0",
|
|
36
|
+
"opentelemetry-exporter-otlp-proto-http>=1.20.0",
|
|
37
|
+
]
|
|
31
38
|
dev = [
|
|
32
39
|
"pytest>=7.0",
|
|
33
40
|
"pytest-cov",
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Tests for register_otel + transport dispatch (Python parity with the TS SDK)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from trodo.otel.register import register_otel, _reset_for_tests
|
|
7
|
+
from trodo.otel.transport import (
|
|
8
|
+
get_transport_mode,
|
|
9
|
+
get_otel_tracer,
|
|
10
|
+
set_otel_transport,
|
|
11
|
+
_reset_transport_for_tests,
|
|
12
|
+
)
|
|
13
|
+
from trodo.otel.processor import TrodoSpanProcessor
|
|
14
|
+
from trodo.otel.wrap_agent import wrap_agent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(autouse=True)
|
|
18
|
+
def reset_state():
|
|
19
|
+
_reset_for_tests()
|
|
20
|
+
yield
|
|
21
|
+
_reset_for_tests()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# register_otel — argument validation + idempotency
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_raises_when_site_id_missing(processor):
|
|
30
|
+
with pytest.raises(ValueError, match="site_id"):
|
|
31
|
+
register_otel(site_id="", processor=processor)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_raises_when_processor_missing():
|
|
35
|
+
with pytest.raises(ValueError, match="processor"):
|
|
36
|
+
register_otel(site_id="site_x", processor=None) # type: ignore[arg-type]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_idempotency_warns_on_second_call(processor):
|
|
40
|
+
first = register_otel(site_id="site_x", processor=processor, mode="trodo")
|
|
41
|
+
assert first.mode == "trodo"
|
|
42
|
+
assert first.fresh is True
|
|
43
|
+
|
|
44
|
+
with pytest.warns(UserWarning, match="already called"):
|
|
45
|
+
second = register_otel(site_id="site_x", processor=processor, mode="trodo")
|
|
46
|
+
assert second is first
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_unknown_mode_raises(processor):
|
|
50
|
+
with pytest.raises(ValueError, match="unknown mode"):
|
|
51
|
+
register_otel(site_id="site_x", processor=processor, mode="bogus")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Mode dispatch — 'trodo' default, 'otlp' raises if peer deps missing
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_trodo_mode_succeeds_without_otlp_peer_deps(processor):
|
|
60
|
+
result = register_otel(site_id="site_x", processor=processor, mode="trodo")
|
|
61
|
+
assert result.mode == "trodo"
|
|
62
|
+
# active_instrumentations is best-effort; just confirm it's a list.
|
|
63
|
+
assert isinstance(result.active_instrumentations, list)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_otlp_mode_emits_install_hint_when_peer_deps_missing(processor, monkeypatch):
|
|
67
|
+
# Ensure opentelemetry imports fail to simulate a fresh install without
|
|
68
|
+
# the 'otlp' extras. We do this by replacing opentelemetry.sdk.trace's
|
|
69
|
+
# spec in sys.modules with None, mimicking ImportError at import time.
|
|
70
|
+
import sys
|
|
71
|
+
|
|
72
|
+
sentinel = "opentelemetry.exporter.otlp.proto.http.trace_exporter"
|
|
73
|
+
monkeypatch.setitem(sys.modules, sentinel, None)
|
|
74
|
+
|
|
75
|
+
with pytest.raises(RuntimeError, match=r"register_otel\(mode='otlp'\) requires"):
|
|
76
|
+
register_otel(site_id="site_x", processor=processor, mode="otlp")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Transport dispatch — wrap_agent in 'otlp' mode goes through the OTel tracer
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class RecordedSpan:
|
|
85
|
+
def __init__(self, name: str, trace_id: int, span_id: int):
|
|
86
|
+
self.name = name
|
|
87
|
+
self.trace_id = trace_id
|
|
88
|
+
self.span_id = span_id
|
|
89
|
+
self.attrs = {}
|
|
90
|
+
self.ended = False
|
|
91
|
+
self.exception = None
|
|
92
|
+
self.status = None
|
|
93
|
+
|
|
94
|
+
def set_attribute(self, key, value):
|
|
95
|
+
self.attrs[key] = value
|
|
96
|
+
|
|
97
|
+
def record_exception(self, e):
|
|
98
|
+
self.exception = e
|
|
99
|
+
|
|
100
|
+
def set_status(self, s):
|
|
101
|
+
self.status = s
|
|
102
|
+
|
|
103
|
+
def end(self):
|
|
104
|
+
self.ended = True
|
|
105
|
+
|
|
106
|
+
def get_span_context(self):
|
|
107
|
+
class C:
|
|
108
|
+
pass
|
|
109
|
+
c = C()
|
|
110
|
+
c.trace_id = self.trace_id
|
|
111
|
+
c.span_id = self.span_id
|
|
112
|
+
return c
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class MockTracer:
|
|
116
|
+
def __init__(self):
|
|
117
|
+
self.spans: list = []
|
|
118
|
+
self._next_id = 1
|
|
119
|
+
|
|
120
|
+
def start_span(self, name):
|
|
121
|
+
span = RecordedSpan(name, self._next_id, self._next_id + 1000)
|
|
122
|
+
self._next_id += 1
|
|
123
|
+
self.spans.append(span)
|
|
124
|
+
return span
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_wrap_agent_dispatches_to_otel_when_transport_set(processor):
|
|
128
|
+
"""wrap_agent in 'otlp' transport routes through tracer.start_span and
|
|
129
|
+
sets Trodo attributes the backend OTLP controller understands."""
|
|
130
|
+
tracer = MockTracer()
|
|
131
|
+
|
|
132
|
+
class _NoopCM:
|
|
133
|
+
def __enter__(self):
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
def __exit__(self, *a):
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def fake_use_span(span, end_on_exit=False):
|
|
140
|
+
return _NoopCM()
|
|
141
|
+
|
|
142
|
+
set_otel_transport(tracer, use_span=fake_use_span)
|
|
143
|
+
|
|
144
|
+
with wrap_agent(
|
|
145
|
+
processor=processor,
|
|
146
|
+
team_site_id="site-x",
|
|
147
|
+
agent_name="support-bot",
|
|
148
|
+
distinct_id="user-1",
|
|
149
|
+
conversation_id="conv-1",
|
|
150
|
+
metadata={"experimentId": "v3", "tier": "enterprise"},
|
|
151
|
+
) as run:
|
|
152
|
+
run.set_input({"q": "hello"})
|
|
153
|
+
run.set_output({"a": "world"})
|
|
154
|
+
|
|
155
|
+
assert len(tracer.spans) == 1
|
|
156
|
+
span = tracer.spans[0]
|
|
157
|
+
assert span.name == "support-bot"
|
|
158
|
+
assert span.ended is True
|
|
159
|
+
assert span.attrs["trodo.agent_name"] == "support-bot"
|
|
160
|
+
assert span.attrs["trodo.distinct_id"] == "user-1"
|
|
161
|
+
assert span.attrs["trodo.conversation_id"] == "conv-1"
|
|
162
|
+
assert span.attrs["trodo.metadata.experimentId"] == "v3"
|
|
163
|
+
assert span.attrs["trodo.metadata.tier"] == "enterprise"
|
|
164
|
+
assert "ai.prompt" in span.attrs
|
|
165
|
+
assert "ai.response.text" in span.attrs
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_wrap_agent_legacy_path_when_no_transport(processor, http):
|
|
169
|
+
"""No register_otel call → transport mode stays 'trodo' → wrap_agent
|
|
170
|
+
uses TrodoSpanProcessor + HTTP API as in 2.3.x."""
|
|
171
|
+
assert get_transport_mode() == "trodo"
|
|
172
|
+
assert get_otel_tracer() is None
|
|
173
|
+
|
|
174
|
+
with wrap_agent(
|
|
175
|
+
processor=processor,
|
|
176
|
+
team_site_id="site-x",
|
|
177
|
+
agent_name="legacy-agent",
|
|
178
|
+
) as run:
|
|
179
|
+
run.set_output("done")
|
|
180
|
+
|
|
181
|
+
# Trodo HTTP API path used as in 2.3.x.
|
|
182
|
+
assert len(http.run_ingest) == 1
|
|
183
|
+
assert http.run_ingest[0]["run"]["agent_name"] == "legacy-agent"
|
|
@@ -40,7 +40,7 @@ Downstream microservice (join the caller's run instead of making a new one):
|
|
|
40
40
|
|
|
41
41
|
from __future__ import annotations
|
|
42
42
|
|
|
43
|
-
__version__ = "2.
|
|
43
|
+
__version__ = "2.4.0"
|
|
44
44
|
|
|
45
45
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
46
46
|
|
|
@@ -67,6 +67,7 @@ __all__ = [
|
|
|
67
67
|
"SpanHandle",
|
|
68
68
|
"FeedbackProps",
|
|
69
69
|
"init",
|
|
70
|
+
"register_otel",
|
|
70
71
|
"for_user",
|
|
71
72
|
"track",
|
|
72
73
|
"identify",
|
|
@@ -129,6 +130,7 @@ def init(
|
|
|
129
130
|
on_error: Optional[Any] = None,
|
|
130
131
|
debug: bool = False,
|
|
131
132
|
auto_instrument: bool = True,
|
|
133
|
+
otel_mode: str = "trodo",
|
|
132
134
|
) -> TrodoClient:
|
|
133
135
|
"""Initialise the singleton SDK instance.
|
|
134
136
|
|
|
@@ -150,10 +152,53 @@ def init(
|
|
|
150
152
|
on_error=on_error,
|
|
151
153
|
debug=debug,
|
|
152
154
|
auto_instrument=auto_instrument,
|
|
155
|
+
otel_mode=otel_mode,
|
|
153
156
|
)
|
|
154
157
|
return _client
|
|
155
158
|
|
|
156
159
|
|
|
160
|
+
def register_otel(
|
|
161
|
+
*,
|
|
162
|
+
site_id: Optional[str] = None,
|
|
163
|
+
mode: str = "trodo",
|
|
164
|
+
endpoint: Optional[str] = None,
|
|
165
|
+
service_name: str = "trodo-agent",
|
|
166
|
+
) -> Any:
|
|
167
|
+
"""Configure the OTel pipeline with one call.
|
|
168
|
+
|
|
169
|
+
Auto-creates the singleton TrodoClient if init() hasn't been called yet,
|
|
170
|
+
so this can replace init() for OTel-first deployments.
|
|
171
|
+
|
|
172
|
+
Modes:
|
|
173
|
+
- 'trodo' (default): bridges OTel auto-instrumentations into
|
|
174
|
+
TrodoSpanProcessor (HTTP API path). Same as init() with default config.
|
|
175
|
+
- 'otlp': registers an OTel TracerProvider with an OTLP/protobuf
|
|
176
|
+
exporter pointed at Trodo's /v1/traces. Use alongside Datadog/Jaeger
|
|
177
|
+
or for vanilla OTel-native setups.
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
import trodo
|
|
181
|
+
trodo.register_otel(site_id=os.environ['TRODO_SITE_ID'], mode='otlp')
|
|
182
|
+
"""
|
|
183
|
+
global _client
|
|
184
|
+
from .otel.register import register_otel as _impl
|
|
185
|
+
if _client is None:
|
|
186
|
+
if not site_id:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
"register_otel: pass site_id, or call trodo.init(site_id=...) first."
|
|
189
|
+
)
|
|
190
|
+
# Suppress legacy auto_instrument inside the constructor — register_otel
|
|
191
|
+
# below is what wires the pipeline.
|
|
192
|
+
_client = TrodoClient(site_id=site_id, auto_instrument=False)
|
|
193
|
+
return _impl(
|
|
194
|
+
site_id=site_id or _client.site_id,
|
|
195
|
+
processor=_client._get_span_processor(),
|
|
196
|
+
mode=mode,
|
|
197
|
+
endpoint=endpoint,
|
|
198
|
+
service_name=service_name,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
157
202
|
def for_user(
|
|
158
203
|
distinct_id: str,
|
|
159
204
|
session_id: Optional[str] = None,
|
|
@@ -23,6 +23,7 @@ from .otel.wrap_agent import (
|
|
|
23
23
|
current_span_id as _current_span_id,
|
|
24
24
|
)
|
|
25
25
|
from .otel.auto_instrument import enable_auto_instrument
|
|
26
|
+
from .otel.register import register_otel as _register_otel
|
|
26
27
|
from .otel.helpers import (
|
|
27
28
|
tool as tool_decorator,
|
|
28
29
|
track_llm_call as track_llm_call_fn,
|
|
@@ -52,11 +53,13 @@ class TrodoClient:
|
|
|
52
53
|
on_error: Optional[Any] = None,
|
|
53
54
|
debug: bool = False,
|
|
54
55
|
auto_instrument: bool = True,
|
|
56
|
+
otel_mode: str = "trodo",
|
|
55
57
|
) -> None:
|
|
56
58
|
if not site_id:
|
|
57
59
|
raise ValueError("trodo-python: site_id is required")
|
|
58
60
|
|
|
59
61
|
self.site_id = site_id
|
|
62
|
+
self._api_base = api_base
|
|
60
63
|
|
|
61
64
|
self._http = HttpClient(
|
|
62
65
|
api_base=api_base,
|
|
@@ -90,7 +93,22 @@ class TrodoClient:
|
|
|
90
93
|
|
|
91
94
|
self._span_processor = TrodoSpanProcessor(http_client=self._http)
|
|
92
95
|
if auto_instrument:
|
|
93
|
-
|
|
96
|
+
import os
|
|
97
|
+
if os.environ.get("TRODO_LEGACY_INIT") == "1":
|
|
98
|
+
# Pre-2.4 escape hatch for one minor version.
|
|
99
|
+
enable_auto_instrument(self._span_processor)
|
|
100
|
+
else:
|
|
101
|
+
_register_otel(
|
|
102
|
+
site_id=site_id,
|
|
103
|
+
processor=self._span_processor,
|
|
104
|
+
mode=otel_mode,
|
|
105
|
+
endpoint=api_base,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def _get_span_processor(self) -> TrodoSpanProcessor:
|
|
109
|
+
"""Internal: expose the span processor to register_otel. Not part of
|
|
110
|
+
the public surface; subject to change between minors."""
|
|
111
|
+
return self._span_processor
|
|
94
112
|
|
|
95
113
|
# --------------------------------------------------------------------------
|
|
96
114
|
# Primary pattern: for_user()
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
register_otel — one-call setup for Trodo's tracing pipeline.
|
|
3
|
+
|
|
4
|
+
Mirrors trodo-node's registerOTel. Two modes:
|
|
5
|
+
|
|
6
|
+
- 'trodo' (default) — uses the existing enable_auto_instrument bridge so spans
|
|
7
|
+
from opentelemetry-instrumentation-{anthropic,openai,langchain,...} flow
|
|
8
|
+
into TrodoSpanProcessor (HTTP API path). Functionally equivalent to today's
|
|
9
|
+
init() with auto_instrument=True, exposed as a one-line OTel-native call.
|
|
10
|
+
|
|
11
|
+
- 'otlp' — registers an OTel TracerProvider with an OTLP/protobuf exporter
|
|
12
|
+
pointed at Trodo's /v1/traces ingest. Use when you already have an OTel
|
|
13
|
+
pipeline (Datadog, Jaeger, Honeycomb) or want a vanilla OTel-native setup.
|
|
14
|
+
In 'otlp' mode, wrap_agent / span dispatch through the OTel tracer so
|
|
15
|
+
auto-instrumented children (Anthropic, LangChain, etc.) join the same
|
|
16
|
+
trace; backend OTLP controller groups them into one run.
|
|
17
|
+
|
|
18
|
+
Idempotent — second call returns the cached configuration with a warning.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
import warnings
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Any, List, Optional
|
|
24
|
+
|
|
25
|
+
from .processor import TrodoSpanProcessor
|
|
26
|
+
from .auto_instrument import enable_auto_instrument
|
|
27
|
+
from .transport import set_otel_transport, _reset_transport_for_tests
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class RegisterOtelResult:
|
|
32
|
+
mode: str
|
|
33
|
+
active_instrumentations: List[str]
|
|
34
|
+
fresh: bool
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_registered: Optional[RegisterOtelResult] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def register_otel(
|
|
41
|
+
*,
|
|
42
|
+
site_id: str,
|
|
43
|
+
processor: TrodoSpanProcessor,
|
|
44
|
+
mode: str = "trodo",
|
|
45
|
+
endpoint: Optional[str] = None,
|
|
46
|
+
service_name: str = "trodo-agent",
|
|
47
|
+
disable_instrumentations: Optional[List[str]] = None,
|
|
48
|
+
) -> RegisterOtelResult:
|
|
49
|
+
"""Configure Trodo's OTel pipeline. See module doc for mode semantics."""
|
|
50
|
+
global _registered
|
|
51
|
+
if _registered is not None:
|
|
52
|
+
warnings.warn(
|
|
53
|
+
"[trodo] register_otel already called; returning existing "
|
|
54
|
+
"configuration. Pass {mode} only at startup.",
|
|
55
|
+
stacklevel=2,
|
|
56
|
+
)
|
|
57
|
+
return _registered
|
|
58
|
+
if not site_id:
|
|
59
|
+
raise ValueError("register_otel: site_id is required")
|
|
60
|
+
if processor is None:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"register_otel: processor is required (typically passed "
|
|
63
|
+
"automatically via init() / TrodoClient).",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
endpoint = (endpoint or "https://sdkapi.trodo.ai").rstrip("/")
|
|
67
|
+
|
|
68
|
+
if mode == "trodo":
|
|
69
|
+
active = enable_auto_instrument(processor)
|
|
70
|
+
elif mode == "otlp":
|
|
71
|
+
active = _setup_otlp_mode(
|
|
72
|
+
site_id=site_id,
|
|
73
|
+
endpoint=endpoint,
|
|
74
|
+
service_name=service_name,
|
|
75
|
+
disable=disable_instrumentations or [],
|
|
76
|
+
processor=processor,
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
raise ValueError(f"register_otel: unknown mode {mode!r}")
|
|
80
|
+
|
|
81
|
+
_registered = RegisterOtelResult(
|
|
82
|
+
mode=mode, active_instrumentations=active, fresh=True
|
|
83
|
+
)
|
|
84
|
+
return _registered
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _reset_for_tests() -> None:
|
|
88
|
+
"""Reset internal state — for tests only. Not exported from __init__.py."""
|
|
89
|
+
global _registered
|
|
90
|
+
_registered = None
|
|
91
|
+
_reset_transport_for_tests()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# 'otlp' mode setup
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _setup_otlp_mode(
|
|
100
|
+
*,
|
|
101
|
+
site_id: str,
|
|
102
|
+
endpoint: str,
|
|
103
|
+
service_name: str,
|
|
104
|
+
disable: List[str],
|
|
105
|
+
processor: TrodoSpanProcessor,
|
|
106
|
+
) -> List[str]:
|
|
107
|
+
"""Lazy-import OTel SDK + protobuf exporter; emit friendly install hint
|
|
108
|
+
if not installed (only users who pick 'otlp' mode need them)."""
|
|
109
|
+
try:
|
|
110
|
+
from opentelemetry import trace as otel_trace
|
|
111
|
+
from opentelemetry.trace import Status, StatusCode, use_span
|
|
112
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
113
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
114
|
+
from opentelemetry.sdk.resources import Resource
|
|
115
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
116
|
+
OTLPSpanExporter,
|
|
117
|
+
)
|
|
118
|
+
except ImportError as e:
|
|
119
|
+
raise RuntimeError(
|
|
120
|
+
"register_otel(mode='otlp') requires the OTel SDK + protobuf "
|
|
121
|
+
"exporter. Install with:\n\n"
|
|
122
|
+
" pip install 'trodo-python[otlp]'\n\n"
|
|
123
|
+
"or directly:\n\n"
|
|
124
|
+
" pip install opentelemetry-sdk "
|
|
125
|
+
"opentelemetry-exporter-otlp-proto-http\n\n"
|
|
126
|
+
f"Original error: {e}",
|
|
127
|
+
) from e
|
|
128
|
+
|
|
129
|
+
exporter = OTLPSpanExporter(
|
|
130
|
+
endpoint=f"{endpoint}/v1/traces",
|
|
131
|
+
headers={"Authorization": f"Bearer {site_id}"},
|
|
132
|
+
)
|
|
133
|
+
batch_processor = BatchSpanProcessor(exporter)
|
|
134
|
+
|
|
135
|
+
# Coexistence: if a non-default TracerProvider is already registered,
|
|
136
|
+
# attach our exporter to it instead of replacing it.
|
|
137
|
+
existing = otel_trace.get_tracer_provider()
|
|
138
|
+
existing_class = type(existing).__name__
|
|
139
|
+
if existing_class not in ("ProxyTracerProvider", "NoOpTracerProvider"):
|
|
140
|
+
# User's pipeline owns the provider — just attach our processor.
|
|
141
|
+
if hasattr(existing, "add_span_processor"):
|
|
142
|
+
existing.add_span_processor(batch_processor)
|
|
143
|
+
# Auto-instrumentations are presumably already wired by the existing
|
|
144
|
+
# pipeline. We still set the transport so wrap_agent dispatches.
|
|
145
|
+
set_otel_transport(
|
|
146
|
+
otel_trace.get_tracer("trodo-agent"),
|
|
147
|
+
use_span=use_span,
|
|
148
|
+
status_cls=Status,
|
|
149
|
+
status_code=StatusCode,
|
|
150
|
+
)
|
|
151
|
+
return _build_instrumentations(disable)
|
|
152
|
+
|
|
153
|
+
# Fresh pipeline.
|
|
154
|
+
resource = Resource.create({
|
|
155
|
+
"service.name": service_name,
|
|
156
|
+
"trodo.sdk.mode": "otlp",
|
|
157
|
+
})
|
|
158
|
+
provider = TracerProvider(resource=resource)
|
|
159
|
+
provider.add_span_processor(batch_processor)
|
|
160
|
+
otel_trace.set_tracer_provider(provider)
|
|
161
|
+
|
|
162
|
+
active = _build_instrumentations(disable)
|
|
163
|
+
|
|
164
|
+
# Activate the OTel transport so wrap_agent / span dispatch via tracer.
|
|
165
|
+
set_otel_transport(otel_trace.get_tracer("trodo-agent"))
|
|
166
|
+
return active
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _build_instrumentations(disable: List[str]) -> List[str]:
|
|
170
|
+
"""Best-effort install of OTel auto-instrumentations.
|
|
171
|
+
|
|
172
|
+
Mirrors auto_instrument.py's loader list but instruments via the OTel
|
|
173
|
+
tracer (which is now configured to ship to Trodo via OTLP), not via
|
|
174
|
+
TrodoSpanProcessor's adapter.
|
|
175
|
+
"""
|
|
176
|
+
disabled = set(disable)
|
|
177
|
+
active: List[str] = []
|
|
178
|
+
candidates = [
|
|
179
|
+
("anthropic", "opentelemetry.instrumentation.anthropic", "AnthropicInstrumentor"),
|
|
180
|
+
("openai", "opentelemetry.instrumentation.openai", "OpenAIInstrumentor"),
|
|
181
|
+
("langchain", "opentelemetry.instrumentation.langchain", "LangChainInstrumentor"),
|
|
182
|
+
("llama-index", "opentelemetry.instrumentation.llamaindex", "LlamaIndexInstrumentor"),
|
|
183
|
+
("bedrock", "opentelemetry.instrumentation.bedrock", "BedrockInstrumentor"),
|
|
184
|
+
("cohere", "opentelemetry.instrumentation.cohere", "CohereInstrumentor"),
|
|
185
|
+
("google-generativeai", "opentelemetry.instrumentation.google_generativeai", "GoogleGenerativeAiInstrumentor"),
|
|
186
|
+
("vertexai", "opentelemetry.instrumentation.vertexai", "VertexAIInstrumentor"),
|
|
187
|
+
("requests", "opentelemetry.instrumentation.requests", "RequestsInstrumentor"),
|
|
188
|
+
("httpx", "opentelemetry.instrumentation.httpx", "HTTPXClientInstrumentor"),
|
|
189
|
+
]
|
|
190
|
+
for short_id, mod_path, cls_name in candidates:
|
|
191
|
+
if short_id in disabled:
|
|
192
|
+
continue
|
|
193
|
+
try:
|
|
194
|
+
mod = __import__(mod_path, fromlist=[cls_name])
|
|
195
|
+
getattr(mod, cls_name)().instrument()
|
|
196
|
+
active.append(short_id)
|
|
197
|
+
except Exception:
|
|
198
|
+
# Package not installed or instrumentation already attached.
|
|
199
|
+
pass
|
|
200
|
+
return active
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transport mode + OTel tracer registry for wrap_agent / span dispatch.
|
|
3
|
+
|
|
4
|
+
Set by register_otel() at startup. Read by wrap_agent / span to decide whether
|
|
5
|
+
to use the legacy TrodoSpanProcessor + Trodo HTTP API path, or to route
|
|
6
|
+
through an OTel tracer (so auto-instrumented children join the same OTel
|
|
7
|
+
trace and the backend OTLP controller groups them into one run).
|
|
8
|
+
|
|
9
|
+
Default state: 'trodo' mode + None tracer = legacy behavior, identical to
|
|
10
|
+
the pre-2.4.0 SDK. Existing 2.3.x users see no change.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
# Transport state — module-level singletons.
|
|
16
|
+
_mode: str = "trodo" # 'trodo' | 'otlp'
|
|
17
|
+
_tracer: Optional[Any] = None
|
|
18
|
+
# Optional helpers loaded once at register_otel time. Stored here so
|
|
19
|
+
# wrap_agent / span don't have to do `from opentelemetry import ...` inside
|
|
20
|
+
# the hot path (which would force opentelemetry to be installed even in tests
|
|
21
|
+
# that mock the tracer).
|
|
22
|
+
_use_span: Optional[Any] = None # callable: (span, end_on_exit=False) -> ContextManager
|
|
23
|
+
_status_cls: Optional[Any] = None # opentelemetry.trace.Status (or shim)
|
|
24
|
+
_status_code: Optional[Any] = None # opentelemetry.trace.StatusCode (or shim)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_transport_mode() -> str:
|
|
28
|
+
"""Read by wrap_agent / span. Returns 'trodo' if register_otel never called."""
|
|
29
|
+
return _mode
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_otel_tracer() -> Optional[Any]:
|
|
33
|
+
"""Read by wrap_agent / span in 'otlp' mode. None when transport is 'trodo'."""
|
|
34
|
+
return _tracer
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_otel_helpers() -> tuple:
|
|
38
|
+
"""Returns (use_span, Status, StatusCode) — all callable, all may be None
|
|
39
|
+
when the tracer was set without helpers (e.g. in tests)."""
|
|
40
|
+
return _use_span, _status_cls, _status_code
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def set_otel_transport(tracer: Any, *, use_span=None, status_cls=None, status_code=None) -> None:
|
|
44
|
+
"""Internal: register_otel calls this once mode='otlp' setup completes.
|
|
45
|
+
|
|
46
|
+
Helpers are optional so tests can stub a tracer without installing the
|
|
47
|
+
full opentelemetry stack.
|
|
48
|
+
"""
|
|
49
|
+
global _mode, _tracer, _use_span, _status_cls, _status_code
|
|
50
|
+
_mode = "otlp"
|
|
51
|
+
_tracer = tracer
|
|
52
|
+
_use_span = use_span
|
|
53
|
+
_status_cls = status_cls
|
|
54
|
+
_status_code = status_code
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _reset_transport_for_tests() -> None:
|
|
58
|
+
"""Reset for tests. Not exported from trodo/__init__.py."""
|
|
59
|
+
global _mode, _tracer, _use_span, _status_cls, _status_code
|
|
60
|
+
_mode = "trodo"
|
|
61
|
+
_tracer = None
|
|
62
|
+
_use_span = None
|
|
63
|
+
_status_cls = None
|
|
64
|
+
_status_code = None
|
|
@@ -32,6 +32,30 @@ from typing import Any, Callable, Dict, Optional
|
|
|
32
32
|
|
|
33
33
|
from .context import ActiveSpanContext, get_active_context, run_with_context
|
|
34
34
|
from .processor import TrodoSpanProcessor, TrodoRun, TrodoSpan
|
|
35
|
+
from .transport import get_transport_mode, get_otel_tracer, get_otel_helpers
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _hex_to_uuid(hex_str: str) -> str:
|
|
39
|
+
"""Format a 32-char hex (OTel traceId) into a UUID. Mirrors
|
|
40
|
+
backend/agentOtlpController.hexToUuid so the run_id returned to the user
|
|
41
|
+
matches the run_id the backend computes from the OTLP payload."""
|
|
42
|
+
h = hex_str.replace("-", "")
|
|
43
|
+
if len(h) != 32:
|
|
44
|
+
return str(uuid.uuid4())
|
|
45
|
+
return f"{h[0:8]}-{h[8:12]}-{h[12:16]}-{h[16:20]}-{h[20:32]}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _serialize_attr(value: Any) -> Any:
|
|
49
|
+
"""Coerce a Python value into something OTel attribute setters accept
|
|
50
|
+
(str / int / float / bool). Complex objects → JSON string."""
|
|
51
|
+
if value is None:
|
|
52
|
+
return ""
|
|
53
|
+
if isinstance(value, (str, int, float, bool)):
|
|
54
|
+
return value
|
|
55
|
+
try:
|
|
56
|
+
return json.dumps(value, default=str)
|
|
57
|
+
except Exception:
|
|
58
|
+
return str(value)
|
|
35
59
|
|
|
36
60
|
|
|
37
61
|
def _now_iso() -> str:
|
|
@@ -262,8 +286,27 @@ class wrap_agent:
|
|
|
262
286
|
self._started_ms: float = 0.0
|
|
263
287
|
self._started_iso: str = ""
|
|
264
288
|
self.handle: Optional[RunHandle] = None
|
|
289
|
+
# OTel-mode state — populated only when transport mode is 'otlp'.
|
|
290
|
+
self._otel_span: Optional[Any] = None
|
|
291
|
+
self._otel_token: Optional[Any] = None
|
|
265
292
|
|
|
266
293
|
def __enter__(self) -> RunHandle:
|
|
294
|
+
# Dispatch: 'otlp' transport routes through OTel tracer so auto-
|
|
295
|
+
# instrumented children (Anthropic / LangChain) join the same trace.
|
|
296
|
+
if get_transport_mode() == "otlp" and get_otel_tracer() is not None:
|
|
297
|
+
return self._enter_otel()
|
|
298
|
+
return self._enter_trodo()
|
|
299
|
+
|
|
300
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
301
|
+
if self._otel_span is not None:
|
|
302
|
+
return self._exit_otel(exc_type, exc, tb)
|
|
303
|
+
return self._exit_trodo(exc_type, exc, tb)
|
|
304
|
+
|
|
305
|
+
# ----------------------------------------------------------------------
|
|
306
|
+
# Legacy 'trodo' transport — TrodoSpanProcessor + Trodo HTTP API path.
|
|
307
|
+
# ----------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
def _enter_trodo(self) -> RunHandle:
|
|
267
310
|
run_id = str(uuid.uuid4())
|
|
268
311
|
root_span_id = str(uuid.uuid4())
|
|
269
312
|
self._started_iso = _now_iso()
|
|
@@ -281,7 +324,7 @@ class wrap_agent:
|
|
|
281
324
|
self._ctx_mgr.__enter__()
|
|
282
325
|
return self.handle
|
|
283
326
|
|
|
284
|
-
def
|
|
327
|
+
def _exit_trodo(self, exc_type, exc, tb) -> None:
|
|
285
328
|
assert self.handle is not None
|
|
286
329
|
ended_iso = _now_iso()
|
|
287
330
|
duration_ms = int(time.time() * 1000.0 - self._started_ms)
|
|
@@ -321,6 +364,71 @@ class wrap_agent:
|
|
|
321
364
|
self._ctx_mgr.__exit__(exc_type, exc, tb)
|
|
322
365
|
return None
|
|
323
366
|
|
|
367
|
+
# ----------------------------------------------------------------------
|
|
368
|
+
# 'otlp' transport — OTel tracer.start_as_current_span path.
|
|
369
|
+
# ----------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
def _enter_otel(self) -> RunHandle:
|
|
372
|
+
tracer = get_otel_tracer()
|
|
373
|
+
otel_span = tracer.start_span(self._agent_name)
|
|
374
|
+
# Make this span the current span for the duration of the with-block
|
|
375
|
+
# so auto-instrumented children attach to it via OTel context. The
|
|
376
|
+
# use_span helper is captured at register_otel time so wrap_agent
|
|
377
|
+
# doesn't need to import opentelemetry at runtime — keeps the test
|
|
378
|
+
# path mockable and the dependency truly optional in 'trodo' mode.
|
|
379
|
+
use_span_helper, _, _ = get_otel_helpers()
|
|
380
|
+
if use_span_helper is not None:
|
|
381
|
+
cm = use_span_helper(otel_span, end_on_exit=False)
|
|
382
|
+
cm.__enter__()
|
|
383
|
+
self._otel_token = cm # store CM so __exit__ can release the context
|
|
384
|
+
|
|
385
|
+
trace_id_int = otel_span.get_span_context().trace_id
|
|
386
|
+
trace_id_hex = format(trace_id_int, "032x") if isinstance(trace_id_int, int) else str(trace_id_int)
|
|
387
|
+
run_id = _hex_to_uuid(trace_id_hex)
|
|
388
|
+
|
|
389
|
+
otel_span.set_attribute("trodo.agent_name", self._agent_name)
|
|
390
|
+
if self._distinct_id:
|
|
391
|
+
otel_span.set_attribute("trodo.distinct_id", str(self._distinct_id))
|
|
392
|
+
if self._conversation_id:
|
|
393
|
+
otel_span.set_attribute("trodo.conversation_id", str(self._conversation_id))
|
|
394
|
+
if self._parent_run_id:
|
|
395
|
+
otel_span.set_attribute("trodo.parent_run_id", str(self._parent_run_id))
|
|
396
|
+
if self._metadata:
|
|
397
|
+
for k, v in self._metadata.items():
|
|
398
|
+
otel_span.set_attribute(f"trodo.metadata.{k}", _serialize_attr(v))
|
|
399
|
+
|
|
400
|
+
self._otel_span = otel_span
|
|
401
|
+
self.handle = RunHandle(run_id, self._agent_name)
|
|
402
|
+
return self.handle
|
|
403
|
+
|
|
404
|
+
def _exit_otel(self, exc_type, exc, tb) -> None:
|
|
405
|
+
assert self.handle is not None
|
|
406
|
+
assert self._otel_span is not None
|
|
407
|
+
otel_span = self._otel_span
|
|
408
|
+
try:
|
|
409
|
+
if self.handle.input is not None:
|
|
410
|
+
otel_span.set_attribute("ai.prompt", self.handle.input)
|
|
411
|
+
if self.handle.output is not None:
|
|
412
|
+
otel_span.set_attribute("ai.response.text", self.handle.output)
|
|
413
|
+
for k, v in self.handle.metadata.items():
|
|
414
|
+
otel_span.set_attribute(f"trodo.metadata.{k}", _serialize_attr(v))
|
|
415
|
+
if exc is not None:
|
|
416
|
+
otel_span.record_exception(exc)
|
|
417
|
+
_, status_cls, status_code = get_otel_helpers()
|
|
418
|
+
if status_cls is not None and status_code is not None:
|
|
419
|
+
otel_span.set_status(status_cls(status_code.ERROR, _truncate(str(exc), 4_000) or ""))
|
|
420
|
+
finally:
|
|
421
|
+
otel_span.end()
|
|
422
|
+
# Best-effort close of the use_span CM if it was created.
|
|
423
|
+
if self._otel_token is not None:
|
|
424
|
+
try:
|
|
425
|
+
self._otel_token.__exit__(exc_type, exc, tb) # type: ignore[union-attr]
|
|
426
|
+
except Exception:
|
|
427
|
+
pass
|
|
428
|
+
self._otel_span = None
|
|
429
|
+
self._otel_token = None
|
|
430
|
+
return None
|
|
431
|
+
|
|
324
432
|
|
|
325
433
|
class join_run:
|
|
326
434
|
"""Join an existing agent run owned by a remote service.
|
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trodo-python
|
|
3
|
+
Version: 2.4.0
|
|
4
|
+
Summary: Trodo Analytics SDK for Python — server-side event tracking
|
|
5
|
+
License: ISC
|
|
6
|
+
Keywords: analytics,tracking,trodo,server-side
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: License :: OSI Approved :: ISC License (ISCL)
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: requests>=2.28.0
|
|
20
|
+
Provides-Extra: async
|
|
21
|
+
Requires-Dist: httpx>=0.27.0; extra == "async"
|
|
22
|
+
Provides-Extra: otlp
|
|
23
|
+
Requires-Dist: opentelemetry-api>=1.20.0; extra == "otlp"
|
|
24
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == "otlp"
|
|
25
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0; extra == "otlp"
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
29
|
+
Requires-Dist: responses>=0.25.0; extra == "dev"
|
|
30
|
+
Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
31
|
+
|
|
1
32
|
# trodo-python
|
|
2
33
|
|
|
3
34
|
Server-side Python SDK for [Trodo Analytics](https://trodo.ai). Track backend events, identify users, manage people/groups, and instrument AI agents — all unified with your frontend data under the same `site_id`.
|
|
@@ -10,6 +41,39 @@ pip install trodo-python
|
|
|
10
41
|
|
|
11
42
|
Requires Python 3.8+.
|
|
12
43
|
|
|
44
|
+
## OpenTelemetry / OTLP path (NEW in 2.4.0)
|
|
45
|
+
|
|
46
|
+
Already running OTel? Skip the Trodo SDK install entirely and point your existing pipeline at Trodo. Two env vars:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT=https://sdkapi.trodo.ai
|
|
50
|
+
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer ${TRODO_SITE_ID}"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The Bearer token is your **site_id** — same value you'd pass to `trodo.init(site_id=...)`. Get it from the [Integration Manager](https://app.trodo.ai/dashboard/integrations).
|
|
54
|
+
|
|
55
|
+
Use this when **you already run OTel** (Datadog, Jaeger, Honeycomb) and want Trodo as an additional destination. Install `trodo-python` and call `trodo.register_otel(site_id=..., mode='otlp')` to attach our OTLP exporter without replacing your existing setup. `wrap_agent` then routes through OTel so auto-instrumented children share the same trace.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# At app startup (after your existing OTel provider is registered)
|
|
59
|
+
import os, trodo
|
|
60
|
+
|
|
61
|
+
trodo.register_otel(
|
|
62
|
+
site_id=os.environ['TRODO_SITE_ID'],
|
|
63
|
+
mode='otlp',
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`mode='otlp'` requires the optional `[otlp]` extras:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install 'trodo-python[otlp]'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The SDK raises a friendly install hint if you call `mode='otlp'` without them.
|
|
74
|
+
|
|
75
|
+
For richer Trodo features on top (`wrap_agent`, `feedback`, `track_mcp`), continue with the SDK quick start below.
|
|
76
|
+
|
|
13
77
|
## Quick Start
|
|
14
78
|
|
|
15
79
|
```python
|
|
@@ -3,6 +3,7 @@ pyproject.toml
|
|
|
3
3
|
tests/test_cross_process_session.py
|
|
4
4
|
tests/test_end_run.py
|
|
5
5
|
tests/test_processor_methods.py
|
|
6
|
+
tests/test_register_otel.py
|
|
6
7
|
tests/test_start_run.py
|
|
7
8
|
tests/test_wrap_agent_unchanged.py
|
|
8
9
|
trodo/__init__.py
|
|
@@ -23,6 +24,8 @@ trodo/otel/auto_instrument.py
|
|
|
23
24
|
trodo/otel/context.py
|
|
24
25
|
trodo/otel/helpers.py
|
|
25
26
|
trodo/otel/processor.py
|
|
27
|
+
trodo/otel/register.py
|
|
28
|
+
trodo/otel/transport.py
|
|
26
29
|
trodo/otel/wrap_agent.py
|
|
27
30
|
trodo/queue/__init__.py
|
|
28
31
|
trodo/queue/batch_flusher.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|