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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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