forgesight-datadog 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.
- forgesight_datadog-0.1.0/.gitignore +38 -0
- forgesight_datadog-0.1.0/PKG-INFO +97 -0
- forgesight_datadog-0.1.0/README.md +70 -0
- forgesight_datadog-0.1.0/pyproject.toml +42 -0
- forgesight_datadog-0.1.0/src/forgesight_datadog/__init__.py +34 -0
- forgesight_datadog-0.1.0/src/forgesight_datadog/_ddtrace.py +95 -0
- forgesight_datadog-0.1.0/src/forgesight_datadog/exporter.py +510 -0
- forgesight_datadog-0.1.0/src/forgesight_datadog/py.typed +0 -0
- forgesight_datadog-0.1.0/src/forgesight_datadog/testing.py +69 -0
- forgesight_datadog-0.1.0/tests/test_exporter.py +520 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# venv / tooling
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
.uv/
|
|
14
|
+
uv.lock
|
|
15
|
+
|
|
16
|
+
# test / type / lint caches
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
.coverage
|
|
21
|
+
.coverage.*
|
|
22
|
+
coverage.xml
|
|
23
|
+
htmlcov/
|
|
24
|
+
|
|
25
|
+
# secrets / local env (never commit)
|
|
26
|
+
.env
|
|
27
|
+
.env.*
|
|
28
|
+
|
|
29
|
+
# editor / OS
|
|
30
|
+
.DS_Store
|
|
31
|
+
.idea/
|
|
32
|
+
.vscode/
|
|
33
|
+
|
|
34
|
+
# local-only session working state (per the workspace pipeline)
|
|
35
|
+
.claude/state/
|
|
36
|
+
|
|
37
|
+
# local-only launch planning (not part of the published repo)
|
|
38
|
+
/launch/
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forgesight-datadog
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ForgeSight Datadog exporter — DD-native APM spans + cost metric, via DD Agent or OTLP intake.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Scaffoldic/forgesight
|
|
6
|
+
Project-URL: Repository, https://github.com/Scaffoldic/forgesight
|
|
7
|
+
Project-URL: Issues, https://github.com/Scaffoldic/forgesight/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Scaffoldic/forgesight/blob/main/docs/releases/v0.1.md
|
|
9
|
+
Author: kjoshi
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
Keywords: ai-agents,apm,datadog,ddtrace,forgesight,observability
|
|
12
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: System :: Monitoring
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: ddtrace>=2
|
|
24
|
+
Requires-Dist: forgesight-core
|
|
25
|
+
Requires-Dist: forgesight-otel
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# forgesight-datadog
|
|
29
|
+
|
|
30
|
+
The Datadog exporter for [ForgeSight](https://github.com/Scaffoldic/forgesight).
|
|
31
|
+
Surfaces agent telemetry in **Datadog APM** with the unified `service` / `env` / `version`
|
|
32
|
+
tags, LLM / tool / MCP calls as child spans, and the SDK's **computed cost** as the
|
|
33
|
+
monitorable DD metric `forgesight.cost_usd` — the same number every other backend reports.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install forgesight-datadog
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import forgesight
|
|
41
|
+
from forgesight_datadog import DatadogExporter
|
|
42
|
+
|
|
43
|
+
forgesight.configure(exporters=[
|
|
44
|
+
DatadogExporter(api_key="...", site="datadoghq.com",
|
|
45
|
+
service="issue-classifier", env="prod"),
|
|
46
|
+
])
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or by name: `exporters: [{name: datadog, config: {api_key: "${DD_API_KEY}", service: ...}}]`.
|
|
50
|
+
|
|
51
|
+
## Two transports
|
|
52
|
+
|
|
53
|
+
- **`agent`** (default) — maps each record to a DD APM span via `ddtrace` and writes it to a
|
|
54
|
+
local DD Agent (`agent_endpoint: http://datadog-agent:8126`), plus emits cost/token DD
|
|
55
|
+
metrics. Direct intake (no `agent_endpoint`) requires `api_key`.
|
|
56
|
+
- **`otlp`** — sends OTLP/HTTP to the DD Agent's OTLP port (`agent_endpoint: http://datadog-agent:4318`)
|
|
57
|
+
with the DD unified tags applied as resource attributes. Reuses `forgesight-otel`.
|
|
58
|
+
|
|
59
|
+
A DD Agent / intake outage makes `export()` return `FAILURE` (counted, never raised — P6);
|
|
60
|
+
it never blocks the agent. Prompt/response content is attached only with
|
|
61
|
+
`capture_content=True` (off by default, P7).
|
|
62
|
+
|
|
63
|
+
## OTLP-native backends need **no package**
|
|
64
|
+
|
|
65
|
+
Because the domain model maps cleanly onto the OTel GenAI conventions, anything that ingests
|
|
66
|
+
OTLP works through `forgesight-otel` with **no dedicated package** — point it at the backend
|
|
67
|
+
and you're done:
|
|
68
|
+
|
|
69
|
+
| Backend | How to send |
|
|
70
|
+
|---|---|
|
|
71
|
+
| Honeycomb | `forgesight-otel` → `api.honeycomb.io:443` + `x-honeycomb-team` header |
|
|
72
|
+
| Jaeger / Tempo / SigNoz | `forgesight-otel` → its OTLP collector |
|
|
73
|
+
| New Relic | `forgesight-otel` → `otlp.nr-data.net:4317` + `api-key` header |
|
|
74
|
+
| AWS X-Ray | `forgesight-otel` → ADOT collector |
|
|
75
|
+
| Arize Phoenix | `forgesight-otel` → Phoenix OTLP endpoint |
|
|
76
|
+
|
|
77
|
+
Datadog earns a package **only** because its richest path (DD-native APM tagging +
|
|
78
|
+
cost-as-DD-metric) is DD-specific. A team that only wants generic spans in Datadog can use
|
|
79
|
+
the OTLP path and skip this package entirely.
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
| Key | Env | Default |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `api_key` | `DD_API_KEY` / `FORGESIGHT_DATADOG_API_KEY` | — (required for direct intake) |
|
|
86
|
+
| `site` | `DD_SITE` / `FORGESIGHT_DATADOG_SITE` | `datadoghq.com` |
|
|
87
|
+
| `service` | `DD_SERVICE` / `FORGESIGHT_DATADOG_SERVICE` | `agentforge` |
|
|
88
|
+
| `env` | `DD_ENV` / `FORGESIGHT_DATADOG_ENV` | — |
|
|
89
|
+
| `version` | `DD_VERSION` / `FORGESIGHT_DATADOG_VERSION` | — |
|
|
90
|
+
| `agent_endpoint` | `FORGESIGHT_DATADOG_AGENT_ENDPOINT` | — |
|
|
91
|
+
| `transport` | `FORGESIGHT_DATADOG_TRANSPORT` | `agent` |
|
|
92
|
+
|
|
93
|
+
Constructor kwargs win over env (FR-12).
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
Apache-2.0
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# forgesight-datadog
|
|
2
|
+
|
|
3
|
+
The Datadog exporter for [ForgeSight](https://github.com/Scaffoldic/forgesight).
|
|
4
|
+
Surfaces agent telemetry in **Datadog APM** with the unified `service` / `env` / `version`
|
|
5
|
+
tags, LLM / tool / MCP calls as child spans, and the SDK's **computed cost** as the
|
|
6
|
+
monitorable DD metric `forgesight.cost_usd` — the same number every other backend reports.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install forgesight-datadog
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
import forgesight
|
|
14
|
+
from forgesight_datadog import DatadogExporter
|
|
15
|
+
|
|
16
|
+
forgesight.configure(exporters=[
|
|
17
|
+
DatadogExporter(api_key="...", site="datadoghq.com",
|
|
18
|
+
service="issue-classifier", env="prod"),
|
|
19
|
+
])
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or by name: `exporters: [{name: datadog, config: {api_key: "${DD_API_KEY}", service: ...}}]`.
|
|
23
|
+
|
|
24
|
+
## Two transports
|
|
25
|
+
|
|
26
|
+
- **`agent`** (default) — maps each record to a DD APM span via `ddtrace` and writes it to a
|
|
27
|
+
local DD Agent (`agent_endpoint: http://datadog-agent:8126`), plus emits cost/token DD
|
|
28
|
+
metrics. Direct intake (no `agent_endpoint`) requires `api_key`.
|
|
29
|
+
- **`otlp`** — sends OTLP/HTTP to the DD Agent's OTLP port (`agent_endpoint: http://datadog-agent:4318`)
|
|
30
|
+
with the DD unified tags applied as resource attributes. Reuses `forgesight-otel`.
|
|
31
|
+
|
|
32
|
+
A DD Agent / intake outage makes `export()` return `FAILURE` (counted, never raised — P6);
|
|
33
|
+
it never blocks the agent. Prompt/response content is attached only with
|
|
34
|
+
`capture_content=True` (off by default, P7).
|
|
35
|
+
|
|
36
|
+
## OTLP-native backends need **no package**
|
|
37
|
+
|
|
38
|
+
Because the domain model maps cleanly onto the OTel GenAI conventions, anything that ingests
|
|
39
|
+
OTLP works through `forgesight-otel` with **no dedicated package** — point it at the backend
|
|
40
|
+
and you're done:
|
|
41
|
+
|
|
42
|
+
| Backend | How to send |
|
|
43
|
+
|---|---|
|
|
44
|
+
| Honeycomb | `forgesight-otel` → `api.honeycomb.io:443` + `x-honeycomb-team` header |
|
|
45
|
+
| Jaeger / Tempo / SigNoz | `forgesight-otel` → its OTLP collector |
|
|
46
|
+
| New Relic | `forgesight-otel` → `otlp.nr-data.net:4317` + `api-key` header |
|
|
47
|
+
| AWS X-Ray | `forgesight-otel` → ADOT collector |
|
|
48
|
+
| Arize Phoenix | `forgesight-otel` → Phoenix OTLP endpoint |
|
|
49
|
+
|
|
50
|
+
Datadog earns a package **only** because its richest path (DD-native APM tagging +
|
|
51
|
+
cost-as-DD-metric) is DD-specific. A team that only wants generic spans in Datadog can use
|
|
52
|
+
the OTLP path and skip this package entirely.
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
| Key | Env | Default |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `api_key` | `DD_API_KEY` / `FORGESIGHT_DATADOG_API_KEY` | — (required for direct intake) |
|
|
59
|
+
| `site` | `DD_SITE` / `FORGESIGHT_DATADOG_SITE` | `datadoghq.com` |
|
|
60
|
+
| `service` | `DD_SERVICE` / `FORGESIGHT_DATADOG_SERVICE` | `agentforge` |
|
|
61
|
+
| `env` | `DD_ENV` / `FORGESIGHT_DATADOG_ENV` | — |
|
|
62
|
+
| `version` | `DD_VERSION` / `FORGESIGHT_DATADOG_VERSION` | — |
|
|
63
|
+
| `agent_endpoint` | `FORGESIGHT_DATADOG_AGENT_ENDPOINT` | — |
|
|
64
|
+
| `transport` | `FORGESIGHT_DATADOG_TRANSPORT` | `agent` |
|
|
65
|
+
|
|
66
|
+
Constructor kwargs win over env (FR-12).
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
Apache-2.0
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "forgesight-datadog"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "ForgeSight Datadog exporter — DD-native APM spans + cost metric, via DD Agent or OTLP intake."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
authors = [{ name = "kjoshi" }]
|
|
9
|
+
keywords = ["observability", "datadog", "apm", "ai-agents", "forgesight", "ddtrace"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Intended Audience :: Information Technology",
|
|
14
|
+
"Topic :: System :: Monitoring",
|
|
15
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
16
|
+
"License :: OSI Approved :: Apache Software License",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["forgesight-core", "forgesight-otel", "ddtrace>=2"]
|
|
23
|
+
|
|
24
|
+
[project.entry-points."forgesight.exporters"]
|
|
25
|
+
datadog = "forgesight_datadog.exporter:DatadogExporter"
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/Scaffoldic/forgesight"
|
|
29
|
+
Repository = "https://github.com/Scaffoldic/forgesight"
|
|
30
|
+
Issues = "https://github.com/Scaffoldic/forgesight/issues"
|
|
31
|
+
Changelog = "https://github.com/Scaffoldic/forgesight/blob/main/docs/releases/v0.1.md"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["hatchling"]
|
|
35
|
+
build-backend = "hatchling.build"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/forgesight_datadog"]
|
|
39
|
+
|
|
40
|
+
[tool.uv.sources]
|
|
41
|
+
forgesight-core = { workspace = true }
|
|
42
|
+
forgesight-otel = { workspace = true }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""ForgeSight Datadog exporter — DD-native APM spans + cost metric (DD Agent or OTLP)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .exporter import (
|
|
6
|
+
COST_METRIC,
|
|
7
|
+
DD_SITES,
|
|
8
|
+
OTLP_NATIVE_BACKENDS,
|
|
9
|
+
TOKENS_METRIC,
|
|
10
|
+
DatadogExporter,
|
|
11
|
+
DatadogMetricSink,
|
|
12
|
+
DatadogSpan,
|
|
13
|
+
DatadogSpanWriter,
|
|
14
|
+
record_to_span,
|
|
15
|
+
)
|
|
16
|
+
from .testing import InMemoryDatadogMetricSink, InMemoryDatadogSpanWriter, MetricCall
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"COST_METRIC",
|
|
22
|
+
"DD_SITES",
|
|
23
|
+
"OTLP_NATIVE_BACKENDS",
|
|
24
|
+
"TOKENS_METRIC",
|
|
25
|
+
"DatadogExporter",
|
|
26
|
+
"DatadogMetricSink",
|
|
27
|
+
"DatadogSpan",
|
|
28
|
+
"DatadogSpanWriter",
|
|
29
|
+
"InMemoryDatadogMetricSink",
|
|
30
|
+
"InMemoryDatadogSpanWriter",
|
|
31
|
+
"MetricCall",
|
|
32
|
+
"__version__",
|
|
33
|
+
"record_to_span",
|
|
34
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Vendor-backed defaults for the ``agent`` transport — ``ddtrace`` + dogstatsd.
|
|
2
|
+
|
|
3
|
+
These touch the live Datadog SDK / Agent, so they are exercised only against a real DD
|
|
4
|
+
Agent (every line is ``pragma: no cover``). The pure record→span mapping lives in
|
|
5
|
+
:mod:`forgesight_datadog.exporter` and is fully unit-tested via injected doubles; this
|
|
6
|
+
module is the thin edge that pushes a mapped :class:`DatadogSpan` onto a ``ddtrace`` span
|
|
7
|
+
and a DD metric onto dogstatsd.
|
|
8
|
+
|
|
9
|
+
Vendor access goes through an ``Any``-typed dynamic boundary on purpose: the package
|
|
10
|
+
supports ``ddtrace>=2`` and the exact span/writer API drifts across major versions, so we
|
|
11
|
+
resolve it at runtime (against whatever ``ddtrace`` is installed) rather than pinning to
|
|
12
|
+
one version's symbols at type-check time.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import contextlib
|
|
18
|
+
import importlib
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from .exporter import DatadogSpan
|
|
23
|
+
|
|
24
|
+
_DD_64BIT_MASK = (1 << 64) - 1
|
|
25
|
+
_NANOS_PER_S = 1_000_000_000
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DDTraceSpanWriter: # pragma: no cover - requires a live DD Agent
|
|
29
|
+
"""Writes mapped spans to a DD Agent via ``ddtrace``'s public tracer."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
service: str,
|
|
35
|
+
api_key: str | None,
|
|
36
|
+
site: str,
|
|
37
|
+
agent_endpoint: str | None,
|
|
38
|
+
) -> None:
|
|
39
|
+
trace_mod: Any = importlib.import_module("ddtrace.trace")
|
|
40
|
+
self._tracer: Any = trace_mod.tracer
|
|
41
|
+
if agent_endpoint:
|
|
42
|
+
# best-effort; the tracer falls back to its default agent url
|
|
43
|
+
with contextlib.suppress(Exception):
|
|
44
|
+
self._tracer.configure(hostname=None, port=None, url=agent_endpoint)
|
|
45
|
+
self._service = service
|
|
46
|
+
|
|
47
|
+
def write(self, span: DatadogSpan) -> None:
|
|
48
|
+
dd = self._tracer.start_span(
|
|
49
|
+
span.name, service=span.service, resource=span.resource, activate=False
|
|
50
|
+
)
|
|
51
|
+
dd.trace_id = int(span.trace_id, 16)
|
|
52
|
+
dd.span_id = int(span.span_id, 16) & _DD_64BIT_MASK
|
|
53
|
+
if span.parent_id:
|
|
54
|
+
dd.parent_id = int(span.parent_id, 16) & _DD_64BIT_MASK
|
|
55
|
+
dd.start_ns = span.start_ns
|
|
56
|
+
dd.error = span.error
|
|
57
|
+
for key, tag in span.meta.items():
|
|
58
|
+
dd.set_tag(key, tag)
|
|
59
|
+
for key, metric in span.metrics.items():
|
|
60
|
+
dd.set_metric(key, metric)
|
|
61
|
+
dd.finish(finish_time=(span.start_ns + span.duration_ns) / _NANOS_PER_S)
|
|
62
|
+
|
|
63
|
+
def flush(self) -> bool:
|
|
64
|
+
flush = getattr(self._tracer, "flush", None)
|
|
65
|
+
if not callable(flush):
|
|
66
|
+
return True
|
|
67
|
+
try:
|
|
68
|
+
flush()
|
|
69
|
+
except Exception:
|
|
70
|
+
return False
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
def stop(self) -> None:
|
|
74
|
+
shutdown = getattr(self._tracer, "shutdown", None)
|
|
75
|
+
if callable(shutdown):
|
|
76
|
+
shutdown()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class DogStatsdMetricSink: # pragma: no cover - requires a live DD Agent / dogstatsd
|
|
80
|
+
"""Emits DD metrics (cost / tokens) to dogstatsd via the DD Agent."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, *, agent_endpoint: str | None) -> None:
|
|
83
|
+
dogstatsd_mod: Any = importlib.import_module("ddtrace.internal.dogstatsd")
|
|
84
|
+
host = "localhost"
|
|
85
|
+
if agent_endpoint:
|
|
86
|
+
host = agent_endpoint.split("://", 1)[-1].split(":", 1)[0]
|
|
87
|
+
self._client: Any = dogstatsd_mod.get_dogstatsd_client(f"udp://{host}:8125")
|
|
88
|
+
|
|
89
|
+
def emit(self, name: str, value: float, tags: Sequence[str]) -> None:
|
|
90
|
+
self._client.distribution(name, value, tags=list(tags))
|
|
91
|
+
|
|
92
|
+
def close(self) -> None:
|
|
93
|
+
close = getattr(self._client, "close", None)
|
|
94
|
+
if callable(close):
|
|
95
|
+
close()
|