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.
@@ -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()