computer-agent-py 0.1.0__tar.gz → 0.1.3__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.
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/PKG-INFO +3 -2
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/README.md +2 -1
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/pyproject.toml +1 -1
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/_proxy/client.py +9 -1
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/_proxy/query.py +33 -7
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/telemetry/config.py +8 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/telemetry/event.py +8 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/telemetry/sinks/agentos.py +20 -3
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/telemetry/sinks/otel.py +303 -11
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/__init__.py +0 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/_proxy/__init__.py +0 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/policy/__init__.py +0 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/policy/authorizer.py +0 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/policy/cedar.py +0 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/policy/opa.py +0 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/policy/types.py +0 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/py.typed +0 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/telemetry/__init__.py +0 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/telemetry/middleware/__init__.py +0 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/telemetry/middleware/guardrails.py +0 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/telemetry/middleware/pii.py +0 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/telemetry/pipeline.py +0 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/telemetry/sinks/__init__.py +0 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/telemetry/sinks/message_archive.py +0 -0
- {computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: computer-agent-py
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Drop-in replacement for claude-agent-sdk that adds a proxied telemetry pipeline (PII redaction + guardrails) with OpenTelemetry and AgentOS sinks.
|
|
5
5
|
Keywords: computeragent,claude-agent-sdk,claude,agent,telemetry,otel,opentelemetry,agentos,pii
|
|
6
6
|
Author: Abhi Bhat
|
|
@@ -114,10 +114,11 @@ That's it — same shape as `claude-agent-sdk`. Telemetry is configured from env
|
|
|
114
114
|
|
|
115
115
|
| Variable | Effect |
|
|
116
116
|
|---|---|
|
|
117
|
-
| `OTEL_EXPORTER_OTLP_ENDPOINT` |
|
|
117
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Base URL of an OTLP/HTTP backend. The sink appends `/v1/traces` and `/v1/metrics` automatically — pass the base. Unset → console exporter (debug). |
|
|
118
118
|
| `OTEL_EXPORTER_OTLP_HEADERS` | Comma-separated `key=value` (e.g. `api-key=NRRX-...` for New Relic). |
|
|
119
119
|
| `OTEL_SERVICE_NAME` | `service.name` attribute on every span. Default: `computeragent`. |
|
|
120
120
|
| `COMPUTERAGENT_OTEL` | `disabled` to suppress OtelSink. |
|
|
121
|
+
| `COMPUTERAGENT_OTEL_TEMPORALITY` | `delta` (default) or `cumulative`. SaaS OTLP intakes (New Relic, Datadog direct, Honeycomb) require delta; cumulative is only correct when exporting to a collector that aggregates downstream. |
|
|
121
122
|
| `AGENTOS_MONGO_URL` | When set + `[agentos]` installed, attaches AgentOS sinks (registry, logs, sessions, agent_messages, slack_threads). |
|
|
122
123
|
| `AGENTOS_MONGO_DB` | Mongo database name. Default: `agentos`. |
|
|
123
124
|
| `COMPUTERAGENT_CAPTURE_CONTENT` | `1` to include prompts/responses on OTel spans. Default: off. |
|
|
@@ -71,10 +71,11 @@ That's it — same shape as `claude-agent-sdk`. Telemetry is configured from env
|
|
|
71
71
|
|
|
72
72
|
| Variable | Effect |
|
|
73
73
|
|---|---|
|
|
74
|
-
| `OTEL_EXPORTER_OTLP_ENDPOINT` |
|
|
74
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Base URL of an OTLP/HTTP backend. The sink appends `/v1/traces` and `/v1/metrics` automatically — pass the base. Unset → console exporter (debug). |
|
|
75
75
|
| `OTEL_EXPORTER_OTLP_HEADERS` | Comma-separated `key=value` (e.g. `api-key=NRRX-...` for New Relic). |
|
|
76
76
|
| `OTEL_SERVICE_NAME` | `service.name` attribute on every span. Default: `computeragent`. |
|
|
77
77
|
| `COMPUTERAGENT_OTEL` | `disabled` to suppress OtelSink. |
|
|
78
|
+
| `COMPUTERAGENT_OTEL_TEMPORALITY` | `delta` (default) or `cumulative`. SaaS OTLP intakes (New Relic, Datadog direct, Honeycomb) require delta; cumulative is only correct when exporting to a collector that aggregates downstream. |
|
|
78
79
|
| `AGENTOS_MONGO_URL` | When set + `[agentos]` installed, attaches AgentOS sinks (registry, logs, sessions, agent_messages, slack_threads). |
|
|
79
80
|
| `AGENTOS_MONGO_DB` | Mongo database name. Default: `agentos`. |
|
|
80
81
|
| `COMPUTERAGENT_CAPTURE_CONTENT` | `1` to include prompts/responses on OTel spans. Default: off. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "computer-agent-py"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
description = "Drop-in replacement for claude-agent-sdk that adds a proxied telemetry pipeline (PII redaction + guardrails) with OpenTelemetry and AgentOS sinks."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Abhi Bhat", email = "abhishek.bhat@lyzr.ai" },
|
|
@@ -20,7 +20,7 @@ import claude_agent_sdk
|
|
|
20
20
|
from claude_agent_sdk.types import AssistantMessage, ResultMessage, TextBlock
|
|
21
21
|
|
|
22
22
|
from ..telemetry import TelemetryEvent, get_pipeline, new_session_id
|
|
23
|
-
from .query import _derive_agent_name
|
|
23
|
+
from .query import _derive_agent_description, _derive_agent_name
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
26
|
from collections.abc import AsyncIterable, AsyncIterator
|
|
@@ -50,6 +50,7 @@ class ClaudeSDKClient:
|
|
|
50
50
|
# placeholder-vs-upstream rationale).
|
|
51
51
|
self._upstream_session_id: str | None = None
|
|
52
52
|
self._agent_name = _derive_agent_name(options)
|
|
53
|
+
self._agent_description = _derive_agent_description(options)
|
|
53
54
|
self._started_at: float | None = None
|
|
54
55
|
self._last_result: ResultMessage | None = None
|
|
55
56
|
self._last_assistant_text: str = ""
|
|
@@ -75,6 +76,7 @@ class ClaudeSDKClient:
|
|
|
75
76
|
TelemetryEvent.session_started(
|
|
76
77
|
session_id=self._session_id,
|
|
77
78
|
agent_name=self._agent_name,
|
|
79
|
+
agent_description=self._agent_description,
|
|
78
80
|
options=self._options,
|
|
79
81
|
prompt=prompt if prompt is not None else "<connect>",
|
|
80
82
|
)
|
|
@@ -122,6 +124,8 @@ class ClaudeSDKClient:
|
|
|
122
124
|
for ev in TelemetryEvent.from_message(
|
|
123
125
|
msg, session_id=self._session_id, agent_name=self._agent_name
|
|
124
126
|
):
|
|
127
|
+
if self._agent_description is not None:
|
|
128
|
+
ev.agent_description = self._agent_description
|
|
125
129
|
await pipeline.emit(ev)
|
|
126
130
|
if isinstance(msg, AssistantMessage):
|
|
127
131
|
for block in getattr(msg, "content", []) or []:
|
|
@@ -145,6 +149,8 @@ class ClaudeSDKClient:
|
|
|
145
149
|
for ev in TelemetryEvent.from_message(
|
|
146
150
|
msg, session_id=self._session_id, agent_name=self._agent_name
|
|
147
151
|
):
|
|
152
|
+
if self._agent_description is not None:
|
|
153
|
+
ev.agent_description = self._agent_description
|
|
148
154
|
await pipeline.emit(ev)
|
|
149
155
|
if isinstance(msg, AssistantMessage):
|
|
150
156
|
for block in getattr(msg, "content", []) or []:
|
|
@@ -203,6 +209,7 @@ class ClaudeSDKClient:
|
|
|
203
209
|
duration_ms=duration_ms,
|
|
204
210
|
last_assistant_text=self._last_assistant_text,
|
|
205
211
|
)
|
|
212
|
+
ev.agent_description = self._agent_description
|
|
206
213
|
if self._upstream_session_id is not None:
|
|
207
214
|
ev.payload["upstream_session_id"] = self._upstream_session_id
|
|
208
215
|
await pipeline.emit(ev)
|
|
@@ -212,6 +219,7 @@ class ClaudeSDKClient:
|
|
|
212
219
|
kind="session_ended",
|
|
213
220
|
session_id=self._session_id,
|
|
214
221
|
agent_name=self._agent_name,
|
|
222
|
+
agent_description=self._agent_description,
|
|
215
223
|
payload={
|
|
216
224
|
"is_error": False,
|
|
217
225
|
"subtype": "no_result",
|
|
@@ -39,6 +39,7 @@ async def query( # noqa: PLR0912 - mirror upstream's tolerant param surface
|
|
|
39
39
|
pipeline = get_pipeline()
|
|
40
40
|
session_id = new_session_id()
|
|
41
41
|
agent_name = _derive_agent_name(options)
|
|
42
|
+
agent_description = _derive_agent_description(options)
|
|
42
43
|
started_at = time.monotonic()
|
|
43
44
|
last_assistant_text = ""
|
|
44
45
|
|
|
@@ -47,6 +48,7 @@ async def query( # noqa: PLR0912 - mirror upstream's tolerant param surface
|
|
|
47
48
|
TelemetryEvent.session_started(
|
|
48
49
|
session_id=session_id,
|
|
49
50
|
agent_name=agent_name,
|
|
51
|
+
agent_description=agent_description,
|
|
50
52
|
options=options,
|
|
51
53
|
prompt=prompt,
|
|
52
54
|
)
|
|
@@ -78,6 +80,11 @@ async def query( # noqa: PLR0912 - mirror upstream's tolerant param surface
|
|
|
78
80
|
for ev in TelemetryEvent.from_message(
|
|
79
81
|
msg, session_id=session_id, agent_name=agent_name
|
|
80
82
|
):
|
|
83
|
+
# Carry the description forward on every event so sinks that
|
|
84
|
+
# group by description (or want to surface it on per-message
|
|
85
|
+
# telemetry) don't have to back-reference session_started.
|
|
86
|
+
if agent_description is not None:
|
|
87
|
+
ev.agent_description = agent_description
|
|
81
88
|
await pipeline.emit(ev)
|
|
82
89
|
|
|
83
90
|
if isinstance(msg, AssistantMessage):
|
|
@@ -100,6 +107,7 @@ async def query( # noqa: PLR0912 - mirror upstream's tolerant param surface
|
|
|
100
107
|
duration_ms=duration_ms,
|
|
101
108
|
last_assistant_text=last_assistant_text,
|
|
102
109
|
)
|
|
110
|
+
ev.agent_description = agent_description
|
|
103
111
|
if upstream_session_id is not None:
|
|
104
112
|
ev.payload["upstream_session_id"] = upstream_session_id
|
|
105
113
|
await pipeline.emit(ev)
|
|
@@ -110,6 +118,7 @@ async def query( # noqa: PLR0912 - mirror upstream's tolerant param surface
|
|
|
110
118
|
kind="session_ended",
|
|
111
119
|
session_id=session_id,
|
|
112
120
|
agent_name=agent_name,
|
|
121
|
+
agent_description=agent_description,
|
|
113
122
|
payload={
|
|
114
123
|
"is_error": False,
|
|
115
124
|
"subtype": "no_result",
|
|
@@ -124,14 +133,14 @@ async def query( # noqa: PLR0912 - mirror upstream's tolerant param surface
|
|
|
124
133
|
except BaseException as exc:
|
|
125
134
|
duration_ms = (time.monotonic() - started_at) * 1000.0
|
|
126
135
|
try:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
duration_ms=duration_ms,
|
|
133
|
-
)
|
|
136
|
+
err_ev = TelemetryEvent.session_ended_error(
|
|
137
|
+
session_id=session_id,
|
|
138
|
+
agent_name=agent_name,
|
|
139
|
+
exc=exc,
|
|
140
|
+
duration_ms=duration_ms,
|
|
134
141
|
)
|
|
142
|
+
err_ev.agent_description = agent_description
|
|
143
|
+
await pipeline.emit(err_ev)
|
|
135
144
|
except Exception: # noqa: BLE001
|
|
136
145
|
logger.debug("error-path telemetry emit failed", exc_info=True)
|
|
137
146
|
raise
|
|
@@ -163,3 +172,20 @@ def _derive_agent_name(options: Any) -> str | None:
|
|
|
163
172
|
if first:
|
|
164
173
|
return first[:80]
|
|
165
174
|
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _derive_agent_description(options: Any) -> str | None:
|
|
178
|
+
"""Pick an agent description from options.
|
|
179
|
+
|
|
180
|
+
Mirrors :func:`_derive_agent_name` — the description is purely opt-in.
|
|
181
|
+
Callers set it on options via ``options.agent_description = "..."`` and
|
|
182
|
+
the proxy threads it through to the telemetry pipeline. Unlike the
|
|
183
|
+
name, there's no implicit fallback to system_prompt: a description is
|
|
184
|
+
a human label, not something we want to synthesize from the prompt.
|
|
185
|
+
"""
|
|
186
|
+
if options is None:
|
|
187
|
+
return None
|
|
188
|
+
desc = getattr(options, "agent_description", None)
|
|
189
|
+
if isinstance(desc, str) and desc:
|
|
190
|
+
return desc
|
|
191
|
+
return None
|
|
@@ -45,6 +45,7 @@ def configure(
|
|
|
45
45
|
capture_content_mode: Literal["events", "attributes", "both"] = "events",
|
|
46
46
|
max_attribute_length: int = 8192,
|
|
47
47
|
fail_open: bool = False,
|
|
48
|
+
temporality: Literal["delta", "cumulative"] | None = None,
|
|
48
49
|
) -> Pipeline:
|
|
49
50
|
"""Programmatic configuration. Idempotent — replaces the active pipeline.
|
|
50
51
|
|
|
@@ -52,6 +53,11 @@ def configure(
|
|
|
52
53
|
and :class:`~computeragent.telemetry.sinks.agentos.AgentRegistrySink` if their
|
|
53
54
|
extras are installed and not already in ``sinks``. Pass ``sinks=[]`` to
|
|
54
55
|
disable all auto-attachment.
|
|
56
|
+
|
|
57
|
+
``temporality`` controls metric aggregation temporality. Defaults to
|
|
58
|
+
delta (set by OtelSink) which is what every major SaaS OTLP intake
|
|
59
|
+
requires — pass ``"cumulative"`` only when exporting to a collector that
|
|
60
|
+
aggregates downstream.
|
|
55
61
|
"""
|
|
56
62
|
global _PIPELINE, _CONFIG
|
|
57
63
|
_CONFIG = {
|
|
@@ -65,6 +71,7 @@ def configure(
|
|
|
65
71
|
"capture_content": capture_content,
|
|
66
72
|
"capture_content_mode": capture_content_mode,
|
|
67
73
|
"max_attribute_length": max_attribute_length,
|
|
74
|
+
"temporality": temporality,
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
effective_sinks: list[TelemetrySink] = list(sinks) if sinks is not None else _auto_sinks()
|
|
@@ -106,6 +113,7 @@ def _build_default_pipeline() -> Pipeline:
|
|
|
106
113
|
"endpoint": os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"),
|
|
107
114
|
"headers": _parse_headers(os.environ.get("OTEL_EXPORTER_OTLP_HEADERS")),
|
|
108
115
|
"sample_rate": 1.0,
|
|
116
|
+
"temporality": os.environ.get("COMPUTERAGENT_OTEL_TEMPORALITY"),
|
|
109
117
|
"capture_content": _env_bool("COMPUTERAGENT_CAPTURE_CONTENT", default=False),
|
|
110
118
|
"capture_content_mode": os.environ.get("COMPUTERAGENT_CAPTURE_CONTENT_MODE", "events"),
|
|
111
119
|
"max_attribute_length": int(os.environ.get("COMPUTERAGENT_MAX_ATTRIBUTE_LENGTH", "8192")),
|
|
@@ -46,6 +46,12 @@ class TelemetryEvent:
|
|
|
46
46
|
session_id: str
|
|
47
47
|
timestamp: datetime = field(default_factory=_utcnow)
|
|
48
48
|
agent_name: str | None = None
|
|
49
|
+
# Human-readable description of the agent. Mirrors `agent_name`'s
|
|
50
|
+
# opt-in shape: derived from ``options.agent_description`` (set via
|
|
51
|
+
# setattr by the caller) and propagated through the same event path.
|
|
52
|
+
# ``None`` when the caller doesn't supply one — sinks fall back to an
|
|
53
|
+
# empty string in the registry doc.
|
|
54
|
+
agent_description: str | None = None
|
|
49
55
|
payload: dict[str, Any] = field(default_factory=dict)
|
|
50
56
|
raw: Any = None
|
|
51
57
|
|
|
@@ -60,6 +66,7 @@ class TelemetryEvent:
|
|
|
60
66
|
*,
|
|
61
67
|
session_id: str,
|
|
62
68
|
agent_name: str | None,
|
|
69
|
+
agent_description: str | None = None,
|
|
63
70
|
options: Any,
|
|
64
71
|
prompt: Any,
|
|
65
72
|
) -> TelemetryEvent:
|
|
@@ -86,6 +93,7 @@ class TelemetryEvent:
|
|
|
86
93
|
kind="session_started",
|
|
87
94
|
session_id=session_id,
|
|
88
95
|
agent_name=agent_name,
|
|
96
|
+
agent_description=agent_description,
|
|
89
97
|
payload=payload,
|
|
90
98
|
raw=options,
|
|
91
99
|
)
|
{computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/telemetry/sinks/agentos.py
RENAMED
|
@@ -151,6 +151,7 @@ class AgentRegistrySink:
|
|
|
151
151
|
|
|
152
152
|
async def _upsert_registry(self, event: TelemetryEvent) -> None:
|
|
153
153
|
name = event.agent_name or _agent_name_from_event(event)
|
|
154
|
+
description = event.agent_description or ""
|
|
154
155
|
now = _utcnow()
|
|
155
156
|
await self._registry.update_one(
|
|
156
157
|
{"_id": name},
|
|
@@ -161,8 +162,12 @@ class AgentRegistrySink:
|
|
|
161
162
|
},
|
|
162
163
|
"$set": {
|
|
163
164
|
"harness": "claude-agent-sdk",
|
|
164
|
-
|
|
165
|
+
# `source.manifest` carries the description too so any
|
|
166
|
+
# downstream consumer that reads the inline manifest
|
|
167
|
+
# gets the same value as the top-level `description`.
|
|
168
|
+
"source": _inline_source_for(name, event.payload, description),
|
|
165
169
|
"model": event.payload.get("model"),
|
|
170
|
+
"description": description,
|
|
166
171
|
"registeredBy": self._registered_by,
|
|
167
172
|
"updatedAt": now,
|
|
168
173
|
"lastSeen": now,
|
|
@@ -359,7 +364,11 @@ DEFAULT_AGENT_INSTRUCTIONS = (
|
|
|
359
364
|
)
|
|
360
365
|
|
|
361
366
|
|
|
362
|
-
def _inline_source_for(
|
|
367
|
+
def _inline_source_for(
|
|
368
|
+
name: str,
|
|
369
|
+
options_payload: dict[str, Any],
|
|
370
|
+
description: str = "",
|
|
371
|
+
) -> dict[str, Any]:
|
|
363
372
|
"""Build a complete inline ``IdentitySource`` the harness can clone into a
|
|
364
373
|
sandbox workdir for live chat.
|
|
365
374
|
|
|
@@ -386,11 +395,16 @@ def _inline_source_for(name: str, options_payload: dict[str, Any]) -> dict[str,
|
|
|
386
395
|
allowed_tools=allowed_tools,
|
|
387
396
|
max_turns=max_turns,
|
|
388
397
|
permission_mode=permission_mode,
|
|
398
|
+
description=description,
|
|
389
399
|
)
|
|
390
400
|
|
|
401
|
+
manifest: dict[str, Any] = {"name": name, "version": "0.1.0"}
|
|
402
|
+
if description:
|
|
403
|
+
manifest["description"] = description
|
|
404
|
+
|
|
391
405
|
return {
|
|
392
406
|
"type": "inline",
|
|
393
|
-
"manifest":
|
|
407
|
+
"manifest": manifest,
|
|
394
408
|
"model": model,
|
|
395
409
|
"files": {
|
|
396
410
|
"agent.yaml": agent_yaml,
|
|
@@ -406,6 +420,7 @@ def _build_agent_yaml(
|
|
|
406
420
|
allowed_tools: list[str],
|
|
407
421
|
max_turns: int | None,
|
|
408
422
|
permission_mode: str,
|
|
423
|
+
description: str = "",
|
|
409
424
|
) -> str:
|
|
410
425
|
"""Render the agent.yaml the harness's inline loader expects.
|
|
411
426
|
|
|
@@ -419,6 +434,8 @@ def _build_agent_yaml(
|
|
|
419
434
|
f"name: {safe_name}",
|
|
420
435
|
"version: 0.1.0",
|
|
421
436
|
]
|
|
437
|
+
if description:
|
|
438
|
+
lines.append(f"description: {_yaml_str(description)}")
|
|
422
439
|
if model:
|
|
423
440
|
lines += [
|
|
424
441
|
"model:",
|
{computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/telemetry/sinks/otel.py
RENAMED
|
@@ -22,7 +22,8 @@ from __future__ import annotations
|
|
|
22
22
|
|
|
23
23
|
import contextlib
|
|
24
24
|
import logging
|
|
25
|
-
|
|
25
|
+
import os
|
|
26
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
26
27
|
|
|
27
28
|
try:
|
|
28
29
|
from opentelemetry import metrics, trace # noqa: F401 - probe imports only
|
|
@@ -33,7 +34,26 @@ try:
|
|
|
33
34
|
OTLPSpanExporter as HttpSpanExporter,
|
|
34
35
|
)
|
|
35
36
|
from opentelemetry.sdk.metrics import MeterProvider
|
|
37
|
+
from opentelemetry.sdk.metrics._internal.instrument import (
|
|
38
|
+
Counter as _SDKCounter,
|
|
39
|
+
)
|
|
40
|
+
from opentelemetry.sdk.metrics._internal.instrument import (
|
|
41
|
+
Histogram as _SDKHistogram,
|
|
42
|
+
)
|
|
43
|
+
from opentelemetry.sdk.metrics._internal.instrument import (
|
|
44
|
+
ObservableCounter as _SDKObservableCounter,
|
|
45
|
+
)
|
|
46
|
+
from opentelemetry.sdk.metrics._internal.instrument import (
|
|
47
|
+
ObservableGauge as _SDKObservableGauge,
|
|
48
|
+
)
|
|
49
|
+
from opentelemetry.sdk.metrics._internal.instrument import (
|
|
50
|
+
ObservableUpDownCounter as _SDKObservableUpDownCounter,
|
|
51
|
+
)
|
|
52
|
+
from opentelemetry.sdk.metrics._internal.instrument import (
|
|
53
|
+
UpDownCounter as _SDKUpDownCounter,
|
|
54
|
+
)
|
|
36
55
|
from opentelemetry.sdk.metrics.export import (
|
|
56
|
+
AggregationTemporality,
|
|
37
57
|
ConsoleMetricExporter,
|
|
38
58
|
PeriodicExportingMetricReader,
|
|
39
59
|
)
|
|
@@ -49,6 +69,8 @@ except ImportError as _exc: # pragma: no cover - guarded by lazy export
|
|
|
49
69
|
"OtelSink requires the [otel] extra. Install with: pip install 'computer-agent-py[otel]'"
|
|
50
70
|
) from _exc
|
|
51
71
|
|
|
72
|
+
Temporality = Literal["delta", "cumulative"]
|
|
73
|
+
|
|
52
74
|
if TYPE_CHECKING:
|
|
53
75
|
from opentelemetry.trace import Span
|
|
54
76
|
|
|
@@ -62,6 +84,7 @@ logger = logging.getLogger("computeragent.telemetry")
|
|
|
62
84
|
# constants for these keys move between alpha versions; literals are safer.
|
|
63
85
|
GEN_AI_SYSTEM = "gen_ai.system"
|
|
64
86
|
GEN_AI_AGENT_NAME = "gen_ai.agent.name"
|
|
87
|
+
GEN_AI_AGENT_DESCRIPTION = "gen_ai.agent.description"
|
|
65
88
|
GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id"
|
|
66
89
|
GEN_AI_REQUEST_MODEL = "gen_ai.request.model"
|
|
67
90
|
GEN_AI_OPERATION_NAME = "gen_ai.operation.name"
|
|
@@ -93,19 +116,61 @@ class OtelSink:
|
|
|
93
116
|
exporter: str | None = None,
|
|
94
117
|
endpoint: str | None = None,
|
|
95
118
|
headers: dict[str, str] | None = None,
|
|
119
|
+
temporality: Temporality | None = None,
|
|
96
120
|
tracer_provider: Any | None = None,
|
|
97
121
|
meter_provider: Any | None = None,
|
|
98
122
|
) -> None:
|
|
99
|
-
#
|
|
123
|
+
# Resolve effective config. The user pattern is
|
|
124
|
+
# ``configure(sinks=[OtelSink(), ...])``: Python builds the sinks list
|
|
125
|
+
# (running this __init__) BEFORE configure() populates _CONFIG, so
|
|
126
|
+
# falling back through cfg alone misses env vars. Resolution order:
|
|
127
|
+
# explicit kwarg → configure()'d _CONFIG → env var → final default.
|
|
100
128
|
from ..config import get_config
|
|
101
129
|
|
|
102
130
|
cfg = get_config()
|
|
103
|
-
self._service_name =
|
|
104
|
-
|
|
105
|
-
|
|
131
|
+
self._service_name = (
|
|
132
|
+
service_name
|
|
133
|
+
or cfg.get("service_name")
|
|
134
|
+
or os.environ.get("OTEL_SERVICE_NAME")
|
|
135
|
+
or "computeragent"
|
|
136
|
+
)
|
|
137
|
+
self._endpoint = (
|
|
138
|
+
endpoint or cfg.get("endpoint") or os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
139
|
+
)
|
|
140
|
+
if headers is not None:
|
|
141
|
+
self._headers: dict[str, str] = headers
|
|
142
|
+
elif cfg.get("headers"):
|
|
143
|
+
self._headers = cfg["headers"]
|
|
144
|
+
else:
|
|
145
|
+
self._headers = _parse_otlp_headers_env(os.environ.get("OTEL_EXPORTER_OTLP_HEADERS"))
|
|
146
|
+
self._exporter = exporter or cfg.get("exporter") or _infer_exporter(self._endpoint)
|
|
147
|
+
self._temporality: Temporality = _resolve_temporality(temporality or cfg.get("temporality"))
|
|
148
|
+
|
|
149
|
+
# Content capture — opt-in. When on, the sink writes prompt /
|
|
150
|
+
# completion / tool input / tool output to spans per the GenAI
|
|
151
|
+
# semconv. Middleware (PiiRedactor, GuardrailFilter) has already
|
|
152
|
+
# mutated the payload by this point, so what lands on spans is the
|
|
153
|
+
# redacted projection — never raw content from the agent stream.
|
|
154
|
+
self._capture_content = bool(
|
|
155
|
+
cfg.get("capture_content")
|
|
156
|
+
if "capture_content" in cfg
|
|
157
|
+
else _env_bool("COMPUTERAGENT_CAPTURE_CONTENT", False)
|
|
106
158
|
)
|
|
107
|
-
|
|
108
|
-
|
|
159
|
+
mode = cfg.get("capture_content_mode") or os.environ.get(
|
|
160
|
+
"COMPUTERAGENT_CAPTURE_CONTENT_MODE", "events"
|
|
161
|
+
)
|
|
162
|
+
if mode not in ("attributes", "events", "both"):
|
|
163
|
+
mode = "events"
|
|
164
|
+
self._capture_mode: str = mode
|
|
165
|
+
# Cap attribute size so a runaway prompt doesn't blow OTLP packet
|
|
166
|
+
# limits. Honors the same env that GuardrailFilter consumes.
|
|
167
|
+
try:
|
|
168
|
+
self._max_attr_length = int(
|
|
169
|
+
cfg.get("max_attribute_length")
|
|
170
|
+
or os.environ.get("COMPUTERAGENT_MAX_ATTRIBUTE_LENGTH", "8192")
|
|
171
|
+
)
|
|
172
|
+
except (TypeError, ValueError):
|
|
173
|
+
self._max_attr_length = 8192
|
|
109
174
|
|
|
110
175
|
self._tracer_provider = tracer_provider or _build_tracer_provider(
|
|
111
176
|
service_name=self._service_name,
|
|
@@ -118,6 +183,7 @@ class OtelSink:
|
|
|
118
183
|
exporter=self._exporter,
|
|
119
184
|
endpoint=self._endpoint,
|
|
120
185
|
headers=self._headers,
|
|
186
|
+
temporality=self._temporality,
|
|
121
187
|
)
|
|
122
188
|
self._tracer = self._tracer_provider.get_tracer("computeragent")
|
|
123
189
|
self._meter = self._meter_provider.get_meter("computeragent")
|
|
@@ -171,37 +237,148 @@ class OtelSink:
|
|
|
171
237
|
|
|
172
238
|
# ── handlers (one per event kind) --------------------------------------
|
|
173
239
|
|
|
240
|
+
def _emit_content(
|
|
241
|
+
self,
|
|
242
|
+
span: Any,
|
|
243
|
+
role: str,
|
|
244
|
+
content: str,
|
|
245
|
+
attr_prefix: str,
|
|
246
|
+
event_name: str,
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Write content to ``span`` per ``capture_content_mode``.
|
|
249
|
+
|
|
250
|
+
* ``attributes`` — ``<attr_prefix>.role`` + ``<attr_prefix>.content``
|
|
251
|
+
per the GenAI semconv 1.27 ``gen_ai.prompt.{i}.*`` / ``gen_ai.completion.{i}.*``
|
|
252
|
+
pattern. Easy to query in NRQL, lossy beyond ~32 KB.
|
|
253
|
+
* ``events`` — ``span.add_event(event_name, {role, content})``.
|
|
254
|
+
Preserves multi-message history without overwriting prior attrs.
|
|
255
|
+
* ``both`` — write both shapes. Useful while migrating backends.
|
|
256
|
+
|
|
257
|
+
Content is truncated to ``max_attribute_length`` chars to stay
|
|
258
|
+
under OTLP per-attribute limits. The middleware
|
|
259
|
+
(``PiiRedactor`` / ``GuardrailFilter``) has already mutated the
|
|
260
|
+
payload by this point, so the redacted projection is what lands.
|
|
261
|
+
"""
|
|
262
|
+
if not self._capture_content or not content:
|
|
263
|
+
return
|
|
264
|
+
text = (
|
|
265
|
+
content
|
|
266
|
+
if len(content) <= self._max_attr_length
|
|
267
|
+
else (content[: self._max_attr_length - 14] + "...[truncated]")
|
|
268
|
+
)
|
|
269
|
+
if self._capture_mode in ("attributes", "both"):
|
|
270
|
+
span.set_attribute(f"{attr_prefix}.role", role)
|
|
271
|
+
span.set_attribute(f"{attr_prefix}.content", text)
|
|
272
|
+
if self._capture_mode in ("events", "both"):
|
|
273
|
+
try:
|
|
274
|
+
span.add_event(event_name, attributes={"role": role, "content": text})
|
|
275
|
+
except Exception: # noqa: BLE001 — never break a session for an event-emit
|
|
276
|
+
logger.debug("add_event(%s) failed", event_name, exc_info=True)
|
|
277
|
+
|
|
278
|
+
def _parent_ctx(self, session_id: str) -> Any:
|
|
279
|
+
"""Build an OTel Context pinned to the active invoke_agent span so
|
|
280
|
+
child spans (chat / execute_tool / policy_decision) nest correctly.
|
|
281
|
+
|
|
282
|
+
``invoke_agent`` is created via ``start_span`` (not
|
|
283
|
+
``start_as_current_span``) because its lifetime spans many event
|
|
284
|
+
handler calls — we can't hold a ``with`` block across them. The
|
|
285
|
+
trade-off is that the span is never the "current" span by default, so
|
|
286
|
+
children would orphan into new traces unless we re-pin context here.
|
|
287
|
+
"""
|
|
288
|
+
parent = self._invoke_spans.get(session_id)
|
|
289
|
+
if parent is None:
|
|
290
|
+
return None
|
|
291
|
+
return trace.set_span_in_context(parent)
|
|
292
|
+
|
|
174
293
|
def _on_session_started(self, event: TelemetryEvent) -> None:
|
|
175
294
|
span = self._tracer.start_span(name=f"invoke_agent {event.agent_name or 'agent'}")
|
|
176
295
|
span.set_attribute(GEN_AI_SYSTEM, "claude-agent-sdk")
|
|
177
296
|
if event.agent_name:
|
|
178
297
|
span.set_attribute(GEN_AI_AGENT_NAME, event.agent_name)
|
|
298
|
+
if event.agent_description:
|
|
299
|
+
span.set_attribute(GEN_AI_AGENT_DESCRIPTION, event.agent_description)
|
|
179
300
|
span.set_attribute(GEN_AI_CONVERSATION_ID, event.session_id)
|
|
180
301
|
span.set_attribute(GEN_AI_OPERATION_NAME, "invoke_agent")
|
|
181
302
|
if event.payload.get("model"):
|
|
182
303
|
span.set_attribute(GEN_AI_REQUEST_MODEL, str(event.payload["model"]))
|
|
304
|
+
# Content capture — system prompt + initial user prompt, both on the
|
|
305
|
+
# invoke_agent root so a single span query surfaces what the agent
|
|
306
|
+
# was asked to do.
|
|
307
|
+
system_prompt = event.payload.get("system_prompt")
|
|
308
|
+
if isinstance(system_prompt, str) and system_prompt:
|
|
309
|
+
self._emit_content(
|
|
310
|
+
span,
|
|
311
|
+
"system",
|
|
312
|
+
system_prompt,
|
|
313
|
+
"gen_ai.prompt.0",
|
|
314
|
+
"gen_ai.system.message",
|
|
315
|
+
)
|
|
316
|
+
prompt = event.payload.get("prompt")
|
|
317
|
+
if isinstance(prompt, str) and prompt and prompt != "<streaming>":
|
|
318
|
+
# When the caller streams the prompt (AsyncIterable), the proxy
|
|
319
|
+
# writes the sentinel "<streaming>" instead of the body — skip
|
|
320
|
+
# capture in that case because there's nothing to record yet.
|
|
321
|
+
self._emit_content(
|
|
322
|
+
span,
|
|
323
|
+
"user",
|
|
324
|
+
prompt,
|
|
325
|
+
"gen_ai.prompt.1",
|
|
326
|
+
"gen_ai.user.message",
|
|
327
|
+
)
|
|
183
328
|
self._invoke_spans[event.session_id] = span
|
|
184
329
|
|
|
185
330
|
def _on_assistant_message(self, event: TelemetryEvent) -> None:
|
|
186
331
|
# Open or reuse a chat span; spec models chat as one span per LLM call,
|
|
187
332
|
# but the upstream stream gives us one AssistantMessage per LLM call,
|
|
188
333
|
# so open/close per event.
|
|
189
|
-
chat = self._tracer.start_span(
|
|
334
|
+
chat = self._tracer.start_span(
|
|
335
|
+
name=f"chat {event.payload.get('model') or 'unknown'}",
|
|
336
|
+
context=self._parent_ctx(event.session_id),
|
|
337
|
+
)
|
|
190
338
|
chat.set_attribute(GEN_AI_OPERATION_NAME, "chat")
|
|
191
339
|
if event.payload.get("model"):
|
|
192
340
|
chat.set_attribute(GEN_AI_REQUEST_MODEL, str(event.payload["model"]))
|
|
193
341
|
chat.set_attribute(GEN_AI_CONVERSATION_ID, event.session_id)
|
|
342
|
+
text = event.payload.get("text")
|
|
343
|
+
if isinstance(text, str) and text:
|
|
344
|
+
self._emit_content(
|
|
345
|
+
chat,
|
|
346
|
+
"assistant",
|
|
347
|
+
text,
|
|
348
|
+
"gen_ai.completion.0",
|
|
349
|
+
"gen_ai.assistant.message",
|
|
350
|
+
)
|
|
194
351
|
chat.end()
|
|
195
352
|
|
|
196
353
|
def _on_tool_use(self, event: TelemetryEvent) -> None:
|
|
197
354
|
tool_name = str(event.payload.get("tool_name") or "tool")
|
|
198
|
-
span = self._tracer.start_span(
|
|
355
|
+
span = self._tracer.start_span(
|
|
356
|
+
name=f"execute_tool {tool_name}",
|
|
357
|
+
context=self._parent_ctx(event.session_id),
|
|
358
|
+
)
|
|
199
359
|
span.set_attribute(GEN_AI_OPERATION_NAME, "execute_tool")
|
|
200
360
|
span.set_attribute(GEN_AI_TOOL_NAME, tool_name)
|
|
201
361
|
call_id = str(event.payload.get("tool_use_id") or "")
|
|
202
362
|
if call_id:
|
|
203
363
|
span.set_attribute(GEN_AI_TOOL_CALL_ID, call_id)
|
|
204
364
|
span.set_attribute(GEN_AI_CONVERSATION_ID, event.session_id)
|
|
365
|
+
# Tool arguments — JSON-encoded so multi-key inputs survive the
|
|
366
|
+
# attribute-string projection.
|
|
367
|
+
tool_input = event.payload.get("tool_input")
|
|
368
|
+
if isinstance(tool_input, dict) and tool_input:
|
|
369
|
+
import json
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
serialised = json.dumps(tool_input, default=str)
|
|
373
|
+
except (TypeError, ValueError):
|
|
374
|
+
serialised = str(tool_input)
|
|
375
|
+
self._emit_content(
|
|
376
|
+
span,
|
|
377
|
+
"tool",
|
|
378
|
+
serialised,
|
|
379
|
+
"gen_ai.tool.call.arguments",
|
|
380
|
+
"gen_ai.tool.call.start",
|
|
381
|
+
)
|
|
205
382
|
self._tool_spans.setdefault(event.session_id, {})[call_id] = span
|
|
206
383
|
|
|
207
384
|
def _on_tool_result(self, event: TelemetryEvent) -> None:
|
|
@@ -211,6 +388,27 @@ class OtelSink:
|
|
|
211
388
|
return
|
|
212
389
|
if event.payload.get("is_error"):
|
|
213
390
|
span.set_attribute("error.type", "tool_error")
|
|
391
|
+
# Tool output — agent-readable text projection. The block may be a
|
|
392
|
+
# str OR a list[dict] (Anthropic tool-result content blocks); we
|
|
393
|
+
# serialise dicts to JSON, leave strings alone.
|
|
394
|
+
content = event.payload.get("content")
|
|
395
|
+
if content:
|
|
396
|
+
if isinstance(content, str):
|
|
397
|
+
serialised = content
|
|
398
|
+
else:
|
|
399
|
+
import json
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
serialised = json.dumps(content, default=str)
|
|
403
|
+
except (TypeError, ValueError):
|
|
404
|
+
serialised = str(content)
|
|
405
|
+
self._emit_content(
|
|
406
|
+
span,
|
|
407
|
+
"tool",
|
|
408
|
+
serialised,
|
|
409
|
+
"gen_ai.tool.call.result",
|
|
410
|
+
"gen_ai.tool.call.end",
|
|
411
|
+
)
|
|
214
412
|
span.end()
|
|
215
413
|
|
|
216
414
|
def _on_usage_snapshot(self, event: TelemetryEvent) -> None:
|
|
@@ -269,6 +467,18 @@ class OtelSink:
|
|
|
269
467
|
v = usage.get(key)
|
|
270
468
|
if isinstance(v, int):
|
|
271
469
|
invoke.set_attribute(attr, v)
|
|
470
|
+
# Final answer — surface the ResultMessage.result (or last assistant
|
|
471
|
+
# text fallback) on the invoke_agent root so a single span query
|
|
472
|
+
# returns the agent's response.
|
|
473
|
+
result_text = event.payload.get("result")
|
|
474
|
+
if isinstance(result_text, str) and result_text:
|
|
475
|
+
self._emit_content(
|
|
476
|
+
invoke,
|
|
477
|
+
"assistant",
|
|
478
|
+
result_text,
|
|
479
|
+
"gen_ai.completion.final",
|
|
480
|
+
"gen_ai.assistant.message",
|
|
481
|
+
)
|
|
272
482
|
invoke.end()
|
|
273
483
|
duration_s = float(event.payload.get("duration_ms", 0.0) or 0.0) / 1000.0
|
|
274
484
|
if duration_s > 0:
|
|
@@ -341,6 +551,78 @@ def _infer_exporter(endpoint: str | None) -> str:
|
|
|
341
551
|
return "otlp-http" if endpoint else "console"
|
|
342
552
|
|
|
343
553
|
|
|
554
|
+
def _signal_url(endpoint: str | None, signal: str) -> str | None:
|
|
555
|
+
"""Append ``/v1/<signal>`` if ``endpoint`` looks like a base URL.
|
|
556
|
+
|
|
557
|
+
OTLP HTTP exporters treat a non-None ``endpoint`` constructor arg as the
|
|
558
|
+
full URL — no automatic path appending — so passing the documented base
|
|
559
|
+
URL (e.g. New Relic's ``https://otlp.nr-data.net``) produces 404s. When
|
|
560
|
+
the caller has already supplied a signal-specific URL, return it
|
|
561
|
+
unchanged so we never double-append.
|
|
562
|
+
"""
|
|
563
|
+
if endpoint is None:
|
|
564
|
+
return None
|
|
565
|
+
e = endpoint.rstrip("/")
|
|
566
|
+
if any(e.endswith(suffix) for suffix in ("/v1/traces", "/v1/metrics", "/v1/logs")):
|
|
567
|
+
return endpoint
|
|
568
|
+
return f"{e}/v1/{signal}"
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _env_bool(name: str, default: bool) -> bool:
|
|
572
|
+
raw = os.environ.get(name)
|
|
573
|
+
if raw is None:
|
|
574
|
+
return default
|
|
575
|
+
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _parse_otlp_headers_env(raw: str | None) -> dict[str, str]:
|
|
579
|
+
"""Parse ``OTEL_EXPORTER_OTLP_HEADERS`` per the OTel env-spec format.
|
|
580
|
+
|
|
581
|
+
Comma-separated ``key=value`` pairs; whitespace around either side is
|
|
582
|
+
stripped. Lines without an ``=`` are dropped silently.
|
|
583
|
+
"""
|
|
584
|
+
if not raw:
|
|
585
|
+
return {}
|
|
586
|
+
out: dict[str, str] = {}
|
|
587
|
+
for pair in raw.split(","):
|
|
588
|
+
if "=" not in pair:
|
|
589
|
+
continue
|
|
590
|
+
k, _, v = pair.partition("=")
|
|
591
|
+
k = k.strip()
|
|
592
|
+
v = v.strip()
|
|
593
|
+
if k:
|
|
594
|
+
out[k] = v
|
|
595
|
+
return out
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
_VALID_TEMPORALITIES: frozenset[str] = frozenset(("delta", "cumulative"))
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _resolve_temporality(value: Temporality | str | None) -> Temporality:
|
|
602
|
+
"""Pick a temporality, falling back through env then to the default.
|
|
603
|
+
|
|
604
|
+
Default is ``"delta"`` because every major SaaS OTLP intake (New Relic,
|
|
605
|
+
Datadog direct, Honeycomb) requires delta for histograms — cumulative
|
|
606
|
+
silently ingests but renders as zero. Users running their own collector
|
|
607
|
+
can opt into cumulative.
|
|
608
|
+
"""
|
|
609
|
+
candidate = value or os.environ.get("COMPUTERAGENT_OTEL_TEMPORALITY") or "delta"
|
|
610
|
+
if candidate not in _VALID_TEMPORALITIES:
|
|
611
|
+
return "delta"
|
|
612
|
+
return candidate # type: ignore[return-value]
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _delta_temporality_map() -> dict[Any, AggregationTemporality]:
|
|
616
|
+
return {
|
|
617
|
+
_SDKCounter: AggregationTemporality.DELTA,
|
|
618
|
+
_SDKUpDownCounter: AggregationTemporality.DELTA,
|
|
619
|
+
_SDKHistogram: AggregationTemporality.DELTA,
|
|
620
|
+
_SDKObservableCounter: AggregationTemporality.DELTA,
|
|
621
|
+
_SDKObservableUpDownCounter: AggregationTemporality.DELTA,
|
|
622
|
+
_SDKObservableGauge: AggregationTemporality.DELTA,
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
|
|
344
626
|
def _build_tracer_provider(
|
|
345
627
|
*,
|
|
346
628
|
service_name: str,
|
|
@@ -351,7 +633,10 @@ def _build_tracer_provider(
|
|
|
351
633
|
resource = Resource.create({"service.name": service_name})
|
|
352
634
|
provider = TracerProvider(resource=resource)
|
|
353
635
|
if exporter == "otlp-http":
|
|
354
|
-
span_exporter = HttpSpanExporter(
|
|
636
|
+
span_exporter = HttpSpanExporter(
|
|
637
|
+
endpoint=_signal_url(endpoint, "traces"),
|
|
638
|
+
headers=headers or None,
|
|
639
|
+
)
|
|
355
640
|
provider.add_span_processor(BatchSpanProcessor(span_exporter))
|
|
356
641
|
else:
|
|
357
642
|
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
|
|
@@ -364,11 +649,18 @@ def _build_meter_provider(
|
|
|
364
649
|
exporter: str,
|
|
365
650
|
endpoint: str | None,
|
|
366
651
|
headers: dict[str, str],
|
|
652
|
+
temporality: Temporality = "delta",
|
|
367
653
|
) -> MeterProvider:
|
|
368
654
|
resource = Resource.create({"service.name": service_name})
|
|
369
655
|
metric_exporter: Any
|
|
370
656
|
if exporter == "otlp-http":
|
|
371
|
-
|
|
657
|
+
kwargs: dict[str, Any] = {
|
|
658
|
+
"endpoint": _signal_url(endpoint, "metrics"),
|
|
659
|
+
"headers": headers or None,
|
|
660
|
+
}
|
|
661
|
+
if temporality == "delta":
|
|
662
|
+
kwargs["preferred_temporality"] = _delta_temporality_map()
|
|
663
|
+
metric_exporter = HttpMetricExporter(**kwargs)
|
|
372
664
|
else:
|
|
373
665
|
metric_exporter = ConsoleMetricExporter()
|
|
374
666
|
reader = PeriodicExportingMetricReader(metric_exporter, export_interval_millis=5000)
|
|
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
|
{computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/telemetry/middleware/pii.py
RENAMED
|
File without changes
|
|
File without changes
|
{computer_agent_py-0.1.0 → computer_agent_py-0.1.3}/src/computeragent/telemetry/sinks/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|