agentops-otel 0.1.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.
- agentops_otel-0.1.0/LICENSE +53 -0
- agentops_otel-0.1.0/PKG-INFO +111 -0
- agentops_otel-0.1.0/README.md +89 -0
- agentops_otel-0.1.0/pyproject.toml +48 -0
- agentops_otel-0.1.0/setup.cfg +4 -0
- agentops_otel-0.1.0/src/agentops_otel/__init__.py +46 -0
- agentops_otel-0.1.0/src/agentops_otel/baggage.py +52 -0
- agentops_otel-0.1.0/src/agentops_otel/logging.py +323 -0
- agentops_otel-0.1.0/src/agentops_otel/telemetry.py +274 -0
- agentops_otel-0.1.0/src/agentops_otel.egg-info/PKG-INFO +111 -0
- agentops_otel-0.1.0/src/agentops_otel.egg-info/SOURCES.txt +16 -0
- agentops_otel-0.1.0/src/agentops_otel.egg-info/dependency_links.txt +1 -0
- agentops_otel-0.1.0/src/agentops_otel.egg-info/requires.txt +12 -0
- agentops_otel-0.1.0/src/agentops_otel.egg-info/top_level.txt +1 -0
- agentops_otel-0.1.0/tests/test_baggage.py +43 -0
- agentops_otel-0.1.0/tests/test_import_without_fastapi.py +24 -0
- agentops_otel-0.1.0/tests/test_logging.py +281 -0
- agentops_otel-0.1.0/tests/test_telemetry.py +294 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Komodor AgentOps License
|
|
2
|
+
|
|
3
|
+
The Komodor AgentOps SDK, together with its associated libraries, documentation,
|
|
4
|
+
and files (the "Software"), is provided by Komodor Ltd. ("Komodor") for integration
|
|
5
|
+
with the Komodor service (the "Service"). By installing, copying, or using the
|
|
6
|
+
Software, you agree to these terms.
|
|
7
|
+
|
|
8
|
+
© 2020 - Komodor Ltd.
|
|
9
|
+
|
|
10
|
+
Grant. Subject to your compliance with these terms, Komodor grants you a
|
|
11
|
+
non-exclusive, non-transferable, non-sublicensable, worldwide, revocable, and
|
|
12
|
+
limited license to (a) install and use the Software to develop, test, and operate
|
|
13
|
+
integrations of your applications and services with the Service, or to use
|
|
14
|
+
applications and services that embed the Software, and (b) reproduce
|
|
15
|
+
and distribute verbatim copies of the Software, but only as embedded within your own
|
|
16
|
+
applications, services, or container images, and not on a standalone basis. No
|
|
17
|
+
separate agreement with Komodor is required; anyone who obtains the Software
|
|
18
|
+
receives this license. If you have a separate written agreement with Komodor
|
|
19
|
+
covering the Software, that agreement controls to the extent of any conflict.
|
|
20
|
+
|
|
21
|
+
You may develop and operate your own software that imports, calls, subclasses,
|
|
22
|
+
configures, or otherwise depends on the Software through its published interfaces;
|
|
23
|
+
doing so is permitted use and does not, by itself, create a modified version or
|
|
24
|
+
derivative work of the Software.
|
|
25
|
+
|
|
26
|
+
Restrictions. You shall not: (a) distribute the Software on a standalone basis, or
|
|
27
|
+
resell, lease, or sublicense it; (b) offer the Software itself, or substantially its
|
|
28
|
+
standalone functionality, to third parties as a hosted or managed service — this
|
|
29
|
+
does not restrict you from operating or offering your own applications or services
|
|
30
|
+
that embed the Software; (c) modify the Software or create a derivative work of its
|
|
31
|
+
files or code; (d) remove, alter, or obscure any copyright, license, or proprietary
|
|
32
|
+
notice, all of which you must retain in every copy you distribute; or (e) claim any
|
|
33
|
+
proprietary interest in the Software, or contest or infringe Komodor's intellectual
|
|
34
|
+
property rights in it.
|
|
35
|
+
|
|
36
|
+
Ownership. The Software is licensed, not sold. Komodor retains all right, title, and
|
|
37
|
+
interest in and to the Software and all intellectual property rights in it. No
|
|
38
|
+
rights are granted except as expressly stated here, and no patent rights are
|
|
39
|
+
granted. This license does not permit use of Komodor's names or trademarks, except
|
|
40
|
+
for reasonable and customary use in describing the origin of the Software.
|
|
41
|
+
|
|
42
|
+
Termination. This license terminates automatically if you breach it and do not cure
|
|
43
|
+
the breach within thirty (30) days of Komodor's written notice. On termination you
|
|
44
|
+
must stop installing, using, and distributing the Software, except that copies
|
|
45
|
+
already embedded in products you distributed to end users before termination may
|
|
46
|
+
remain in use. The Restrictions and this "No warranty; no liability" section survive termination.
|
|
47
|
+
|
|
48
|
+
No warranty; no liability. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
|
49
|
+
KIND, EXPRESS OR IMPLIED, INCLUDING THE IMPLIED WARRANTIES OF MERCHANTABILITY,
|
|
50
|
+
FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT. IN NO EVENT SHALL
|
|
51
|
+
KOMODOR OR THE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
|
52
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT
|
|
53
|
+
OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentops-otel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared OpenTelemetry bootstrap for AgentOps (control plane + SDK workers).
|
|
5
|
+
Author: Komodor Ltd.
|
|
6
|
+
License-Expression: LicenseRef-Komodor-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://agentops.komodor.com
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: httpx
|
|
12
|
+
Requires-Dist: opentelemetry-sdk>=1.27
|
|
13
|
+
Requires-Dist: opentelemetry-exporter-otlp>=1.27
|
|
14
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.48b0
|
|
15
|
+
Requires-Dist: opentelemetry-instrumentation-httpx>=0.48b0
|
|
16
|
+
Requires-Dist: opentelemetry-instrumentation-logging>=0.48b0
|
|
17
|
+
Provides-Extra: sqlalchemy
|
|
18
|
+
Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.48b0; extra == "sqlalchemy"
|
|
19
|
+
Provides-Extra: prometheus
|
|
20
|
+
Requires-Dist: opentelemetry-exporter-prometheus>=0.48b0; extra == "prometheus"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# agentops-otel
|
|
24
|
+
|
|
25
|
+
Shared logging + OpenTelemetry bootstrap for AgentOps.
|
|
26
|
+
|
|
27
|
+
## Logging (`configure_logging`, `get_logger`)
|
|
28
|
+
|
|
29
|
+
One entry point sets up the process's logging; every module then grabs a logger
|
|
30
|
+
and lets records propagate to the single root handler:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from agentops_otel import configure_logging, get_logger
|
|
34
|
+
|
|
35
|
+
configure_logging() # once, at process start (idempotent)
|
|
36
|
+
log = get_logger(__name__) # in every module
|
|
37
|
+
log.info("worker started", extra={"worker_id": wid})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`configure_logging()` installs **one** stdout handler on the root logger and
|
|
41
|
+
picks its shape from the environment:
|
|
42
|
+
|
|
43
|
+
- **Local** (`AGENTOPS_ENV` / `AGENTOPS_DD_ENV` / `ENV` unset or `local`) — a
|
|
44
|
+
readable, coloured console line: `time LEVEL name file:line message key=val`.
|
|
45
|
+
- **Anything else** (`staging`, `production`, …) — one-line JSON per event with
|
|
46
|
+
explicit `level`/`status` and Datadog unified-service `dd.*` tags, so a Datadog
|
|
47
|
+
agent tailing container stdout reads the real severity and correlates to traces.
|
|
48
|
+
|
|
49
|
+
Our staging/prod control plane sets `AGENTOPS_DD_ENV`, and worker pods set
|
|
50
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT` — either signal flips a deployed process to JSON
|
|
51
|
+
automatically, with no extra config. Overrides: `json_logs=True|False` forces a
|
|
52
|
+
format; level comes from the arg, then `AGENTOPS_LOG_LEVEL`, then
|
|
53
|
+
`AGENTOPS_DEBUG`, then INFO.
|
|
54
|
+
|
|
55
|
+
**Trace correlation is generic OpenTelemetry.** We read the current span through
|
|
56
|
+
the OTel *API* only. When no tracer/exporter is configured (the default —
|
|
57
|
+
`configure_telemetry` is opt-in), there is no active span and nothing is added,
|
|
58
|
+
so logging needs no running collector. When a span *is* active, each JSON record
|
|
59
|
+
gains `otelTraceID`/`otelSpanID` plus the `dd.trace_id`/`dd.span_id`
|
|
60
|
+
(lower-64-bit) form Datadog uses.
|
|
61
|
+
|
|
62
|
+
### Forwarding worker logs to the control plane (`CPForwardingHandler`)
|
|
63
|
+
|
|
64
|
+
`agentops_otel.CPForwardingHandler` bridges the standard logging module to a
|
|
65
|
+
sink callback — the SDK worker runtime (`komodor_agentops.worker.logging_bridge.install_cp_log_forwarding`)
|
|
66
|
+
uses it to also ship every worker log record to the control plane as a
|
|
67
|
+
`log.created` event, since Komodor has no pod/log access to customer-hosted
|
|
68
|
+
workers. Two env vars control it:
|
|
69
|
+
|
|
70
|
+
- `AGENTOPS_CP_LOG_LEVEL` (default `INFO`) — minimum level forwarded to CP.
|
|
71
|
+
- `AGENTOPS_CP_LOG_CAPTURE_ALL` (default off) — forward only the SDK's own
|
|
72
|
+
loggers (`komodor_agentops`, `agentops`) when unset; set to attach to the
|
|
73
|
+
root logger instead and capture the whole process.
|
|
74
|
+
|
|
75
|
+
## Telemetry (`configure_telemetry`)
|
|
76
|
+
|
|
77
|
+
`configure_telemetry(service_name, *, app=None, engine=None)` builds OTLP
|
|
78
|
+
tracer/meter/logger providers (HTTP/protobuf or gRPC), attaches the OTel logging
|
|
79
|
+
handler, and runs the FastAPI / httpx / logging auto-instrumentors. SQLAlchemy
|
|
80
|
+
instrumentation is the optional `agentops-otel[sqlalchemy]` extra.
|
|
81
|
+
|
|
82
|
+
Off by default: nothing is installed unless `OTEL_EXPORTER_OTLP_ENDPOINT` or
|
|
83
|
+
`AGENTOPS_OTEL_ENABLED` is set. Used by the control plane and by the SDK worker
|
|
84
|
+
runtime; neither duplicates the setup.
|
|
85
|
+
|
|
86
|
+
## Metric naming (`metric_name`, `METRIC_PREFIX`)
|
|
87
|
+
|
|
88
|
+
Every AgentOps metric lives under a single `agentops.` namespace root so the
|
|
89
|
+
whole product's metrics are isolated and trivially sliceable in Datadog. **When
|
|
90
|
+
you create an instrument, name it through `metric_name()`** rather than passing a
|
|
91
|
+
raw string:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from agentops_otel import metric_name
|
|
95
|
+
from opentelemetry import metrics
|
|
96
|
+
|
|
97
|
+
meter = metrics.get_meter("agentops.controlplane.authz")
|
|
98
|
+
hist = meter.create_histogram(metric_name("authz.pdp.duration_ms"), unit="ms")
|
|
99
|
+
# → exported as "agentops.authz.pdp.duration_ms"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`metric_name()` is **idempotent** — a name that already starts with
|
|
103
|
+
`METRIC_PREFIX` (`"agentops."`) is returned unchanged, so passing either the bare
|
|
104
|
+
suffix or the full name is safe.
|
|
105
|
+
|
|
106
|
+
Why a helper and not a global rewrite? OpenTelemetry Python has no
|
|
107
|
+
View/exporter-level facility for *dynamic per-instrument* renaming (a wildcard
|
|
108
|
+
`View`'s `name` is a single static string), so the exported metric name is fixed
|
|
109
|
+
at instrument creation. This helper is therefore the one canonical entry point.
|
|
110
|
+
Note that the meter/instrumentation-scope name is separate from the metric name
|
|
111
|
+
and does not affect it — the `agentops.` prefix must be on the instrument name.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# agentops-otel
|
|
2
|
+
|
|
3
|
+
Shared logging + OpenTelemetry bootstrap for AgentOps.
|
|
4
|
+
|
|
5
|
+
## Logging (`configure_logging`, `get_logger`)
|
|
6
|
+
|
|
7
|
+
One entry point sets up the process's logging; every module then grabs a logger
|
|
8
|
+
and lets records propagate to the single root handler:
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from agentops_otel import configure_logging, get_logger
|
|
12
|
+
|
|
13
|
+
configure_logging() # once, at process start (idempotent)
|
|
14
|
+
log = get_logger(__name__) # in every module
|
|
15
|
+
log.info("worker started", extra={"worker_id": wid})
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`configure_logging()` installs **one** stdout handler on the root logger and
|
|
19
|
+
picks its shape from the environment:
|
|
20
|
+
|
|
21
|
+
- **Local** (`AGENTOPS_ENV` / `AGENTOPS_DD_ENV` / `ENV` unset or `local`) — a
|
|
22
|
+
readable, coloured console line: `time LEVEL name file:line message key=val`.
|
|
23
|
+
- **Anything else** (`staging`, `production`, …) — one-line JSON per event with
|
|
24
|
+
explicit `level`/`status` and Datadog unified-service `dd.*` tags, so a Datadog
|
|
25
|
+
agent tailing container stdout reads the real severity and correlates to traces.
|
|
26
|
+
|
|
27
|
+
Our staging/prod control plane sets `AGENTOPS_DD_ENV`, and worker pods set
|
|
28
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT` — either signal flips a deployed process to JSON
|
|
29
|
+
automatically, with no extra config. Overrides: `json_logs=True|False` forces a
|
|
30
|
+
format; level comes from the arg, then `AGENTOPS_LOG_LEVEL`, then
|
|
31
|
+
`AGENTOPS_DEBUG`, then INFO.
|
|
32
|
+
|
|
33
|
+
**Trace correlation is generic OpenTelemetry.** We read the current span through
|
|
34
|
+
the OTel *API* only. When no tracer/exporter is configured (the default —
|
|
35
|
+
`configure_telemetry` is opt-in), there is no active span and nothing is added,
|
|
36
|
+
so logging needs no running collector. When a span *is* active, each JSON record
|
|
37
|
+
gains `otelTraceID`/`otelSpanID` plus the `dd.trace_id`/`dd.span_id`
|
|
38
|
+
(lower-64-bit) form Datadog uses.
|
|
39
|
+
|
|
40
|
+
### Forwarding worker logs to the control plane (`CPForwardingHandler`)
|
|
41
|
+
|
|
42
|
+
`agentops_otel.CPForwardingHandler` bridges the standard logging module to a
|
|
43
|
+
sink callback — the SDK worker runtime (`komodor_agentops.worker.logging_bridge.install_cp_log_forwarding`)
|
|
44
|
+
uses it to also ship every worker log record to the control plane as a
|
|
45
|
+
`log.created` event, since Komodor has no pod/log access to customer-hosted
|
|
46
|
+
workers. Two env vars control it:
|
|
47
|
+
|
|
48
|
+
- `AGENTOPS_CP_LOG_LEVEL` (default `INFO`) — minimum level forwarded to CP.
|
|
49
|
+
- `AGENTOPS_CP_LOG_CAPTURE_ALL` (default off) — forward only the SDK's own
|
|
50
|
+
loggers (`komodor_agentops`, `agentops`) when unset; set to attach to the
|
|
51
|
+
root logger instead and capture the whole process.
|
|
52
|
+
|
|
53
|
+
## Telemetry (`configure_telemetry`)
|
|
54
|
+
|
|
55
|
+
`configure_telemetry(service_name, *, app=None, engine=None)` builds OTLP
|
|
56
|
+
tracer/meter/logger providers (HTTP/protobuf or gRPC), attaches the OTel logging
|
|
57
|
+
handler, and runs the FastAPI / httpx / logging auto-instrumentors. SQLAlchemy
|
|
58
|
+
instrumentation is the optional `agentops-otel[sqlalchemy]` extra.
|
|
59
|
+
|
|
60
|
+
Off by default: nothing is installed unless `OTEL_EXPORTER_OTLP_ENDPOINT` or
|
|
61
|
+
`AGENTOPS_OTEL_ENABLED` is set. Used by the control plane and by the SDK worker
|
|
62
|
+
runtime; neither duplicates the setup.
|
|
63
|
+
|
|
64
|
+
## Metric naming (`metric_name`, `METRIC_PREFIX`)
|
|
65
|
+
|
|
66
|
+
Every AgentOps metric lives under a single `agentops.` namespace root so the
|
|
67
|
+
whole product's metrics are isolated and trivially sliceable in Datadog. **When
|
|
68
|
+
you create an instrument, name it through `metric_name()`** rather than passing a
|
|
69
|
+
raw string:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from agentops_otel import metric_name
|
|
73
|
+
from opentelemetry import metrics
|
|
74
|
+
|
|
75
|
+
meter = metrics.get_meter("agentops.controlplane.authz")
|
|
76
|
+
hist = meter.create_histogram(metric_name("authz.pdp.duration_ms"), unit="ms")
|
|
77
|
+
# → exported as "agentops.authz.pdp.duration_ms"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`metric_name()` is **idempotent** — a name that already starts with
|
|
81
|
+
`METRIC_PREFIX` (`"agentops."`) is returned unchanged, so passing either the bare
|
|
82
|
+
suffix or the full name is safe.
|
|
83
|
+
|
|
84
|
+
Why a helper and not a global rewrite? OpenTelemetry Python has no
|
|
85
|
+
View/exporter-level facility for *dynamic per-instrument* renaming (a wildcard
|
|
86
|
+
`View`'s `name` is a single static string), so the exported metric name is fixed
|
|
87
|
+
at instrument creation. This helper is therefore the one canonical entry point.
|
|
88
|
+
Note that the meter/instrumentation-scope name is separate from the metric name
|
|
89
|
+
and does not affect it — the `agentops.` prefix must be on the instrument name.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agentops-otel"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Shared OpenTelemetry bootstrap for AgentOps (control plane + SDK workers)."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "LicenseRef-Komodor-Proprietary"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Komodor Ltd." }]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
# HTTPXClientInstrumentor is instrumented unconditionally in configure_telemetry,
|
|
12
|
+
# and opentelemetry-instrumentation-httpx imports httpx at module load — so httpx
|
|
13
|
+
# is a genuine runtime requirement, not optional. (fastapi is not: it's imported
|
|
14
|
+
# lazily and only when configure_telemetry(app=...) is called.)
|
|
15
|
+
"httpx",
|
|
16
|
+
"opentelemetry-sdk>=1.27",
|
|
17
|
+
"opentelemetry-exporter-otlp>=1.27",
|
|
18
|
+
"opentelemetry-instrumentation-fastapi>=0.48b0",
|
|
19
|
+
"opentelemetry-instrumentation-httpx>=0.48b0",
|
|
20
|
+
"opentelemetry-instrumentation-logging>=0.48b0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://agentops.komodor.com"
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
sqlalchemy = [
|
|
28
|
+
"opentelemetry-instrumentation-sqlalchemy>=0.48b0",
|
|
29
|
+
]
|
|
30
|
+
prometheus = [
|
|
31
|
+
"opentelemetry-exporter-prometheus>=0.48b0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["setuptools>=77"]
|
|
36
|
+
build-backend = "setuptools.build_meta"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-dir]
|
|
39
|
+
"" = "src"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = [
|
|
46
|
+
"fastapi",
|
|
47
|
+
"pytest>=8",
|
|
48
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from agentops_otel.baggage import BaggageSpanProcessor, is_agentops_baggage_key
|
|
4
|
+
from agentops_otel.logging import (
|
|
5
|
+
ConsoleFormatter,
|
|
6
|
+
CPForwardingHandler,
|
|
7
|
+
JsonFormatter,
|
|
8
|
+
TraceCorrelationFilter,
|
|
9
|
+
configure_logging,
|
|
10
|
+
get_logger,
|
|
11
|
+
is_local,
|
|
12
|
+
resolve_env,
|
|
13
|
+
resolve_log_level,
|
|
14
|
+
)
|
|
15
|
+
from agentops_otel.telemetry import (
|
|
16
|
+
METRIC_PREFIX,
|
|
17
|
+
TelemetrySettings,
|
|
18
|
+
configure_telemetry,
|
|
19
|
+
metric_name,
|
|
20
|
+
otlp_exporter_classes,
|
|
21
|
+
prometheus_exposition,
|
|
22
|
+
prometheus_metrics_enabled,
|
|
23
|
+
span_baggage_enabled,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"METRIC_PREFIX",
|
|
28
|
+
"BaggageSpanProcessor",
|
|
29
|
+
"CPForwardingHandler",
|
|
30
|
+
"ConsoleFormatter",
|
|
31
|
+
"JsonFormatter",
|
|
32
|
+
"TelemetrySettings",
|
|
33
|
+
"TraceCorrelationFilter",
|
|
34
|
+
"configure_logging",
|
|
35
|
+
"configure_telemetry",
|
|
36
|
+
"get_logger",
|
|
37
|
+
"is_agentops_baggage_key",
|
|
38
|
+
"is_local",
|
|
39
|
+
"metric_name",
|
|
40
|
+
"otlp_exporter_classes",
|
|
41
|
+
"prometheus_exposition",
|
|
42
|
+
"prometheus_metrics_enabled",
|
|
43
|
+
"resolve_env",
|
|
44
|
+
"resolve_log_level",
|
|
45
|
+
"span_baggage_enabled",
|
|
46
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Copy OpenTelemetry baggage onto spans, so tenant/identity context set upstream reaches every span.
|
|
2
|
+
|
|
3
|
+
Span attributes are not inherited by child spans, and identity is often known only after a span
|
|
4
|
+
starts. Baggage solves both: it lives in the context and propagates via the W3C ``baggage`` header, so
|
|
5
|
+
values set once (e.g. by the control plane's auth middleware) reach child spans **and** spans in
|
|
6
|
+
downstream services in the same trace. :class:`BaggageSpanProcessor` promotes those baggage entries to
|
|
7
|
+
span attributes at span start. :func:`configure_telemetry` registers it for every AgentOps service
|
|
8
|
+
(control plane + workers), so a worker span is attributable to the account the control plane set.
|
|
9
|
+
|
|
10
|
+
A tiny in-house equivalent of the OTel-contrib ``BaggageSpanProcessor`` (avoids a new dependency),
|
|
11
|
+
scoped to the AgentOps ``agentops.*`` namespace so unrelated baggage is never promoted to a span tag.
|
|
12
|
+
Only non-PII stable ids belong in baggage — it rides outbound headers to every callee, including
|
|
13
|
+
external ones — so callers must never put email/name there.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
|
|
20
|
+
from opentelemetry.baggage import get_all as get_all_baggage
|
|
21
|
+
from opentelemetry.context import Context
|
|
22
|
+
from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
AGENTOPS_BAGGAGE_PREFIX = "agentops."
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_agentops_baggage_key(key: str) -> bool:
|
|
30
|
+
"""Whether a baggage key is one AgentOps sets (and may be promoted to a span attribute)."""
|
|
31
|
+
return key.startswith(AGENTOPS_BAGGAGE_PREFIX)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BaggageSpanProcessor(SpanProcessor):
|
|
35
|
+
"""Copy ``agentops.*`` baggage entries onto every span at start (see module docstring)."""
|
|
36
|
+
|
|
37
|
+
def on_start(self, span: Span, parent_context: Context | None = None) -> None:
|
|
38
|
+
try:
|
|
39
|
+
for key, value in get_all_baggage(parent_context).items():
|
|
40
|
+
if is_agentops_baggage_key(key):
|
|
41
|
+
span.set_attribute(key, str(value))
|
|
42
|
+
except Exception: # noqa: BLE001 — telemetry must never break the request path
|
|
43
|
+
logger.debug("otel: BaggageSpanProcessor.on_start failed", exc_info=True)
|
|
44
|
+
|
|
45
|
+
def on_end(self, span: ReadableSpan) -> None:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def shutdown(self) -> None:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
52
|
+
return True
|