aevum-otel 0.6.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,52 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .venv/
7
+ *.egg-info/
8
+
9
+ # Build
10
+ dist/
11
+ build/
12
+ site/
13
+
14
+ # Tools
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ .pytest_cache/
18
+ .hypothesis/
19
+ .cache/
20
+
21
+ # IDE
22
+ .vscode/
23
+ .idea/
24
+ *.swp
25
+ *.swo
26
+
27
+ # OS
28
+ .DS_Store
29
+ Thumbs.db
30
+
31
+ # Verify scripts (run locally, never commit)
32
+ verify_*.py
33
+ scripts/verify_*.py
34
+
35
+ # Aevum development — never commit (Phase 0+)
36
+ aevum_principles.key
37
+ signed_principles_draft.yaml
38
+ tools/sign_principles.py
39
+
40
+ # Private keys — never commit
41
+ *.key
42
+ *.pem
43
+
44
+ # OpenSSF Scorecard output (Phase 0+)
45
+ results.sarif
46
+ verify_phase3.py
47
+ verify_phase7.py
48
+ verify_phase8.py
49
+ verify_phase*.py
50
+
51
+ # Maintenance generated files — local only, never commit
52
+ maintenance/generated/
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: aevum-otel
3
+ Version: 0.6.0
4
+ Summary: Aevum — OpenTelemetry bridge complication. Routes sigchain events to OTel GenAI spans.
5
+ Project-URL: Homepage, https://aevum.build
6
+ Project-URL: Repository, https://github.com/aevum-labs/aevum
7
+ Project-URL: Issues, https://github.com/aevum-labs/aevum/issues
8
+ License-Expression: Apache-2.0
9
+ Keywords: aevum,audit,genai,observability,opentelemetry,otel
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: aevum-core>=0.6.0
18
+ Requires-Dist: opentelemetry-api>=1.27.0
19
+ Requires-Dist: opentelemetry-sdk>=1.27.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: mypy>=1.9; extra == 'dev'
22
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
23
+ Requires-Dist: pytest>=8.0; extra == 'dev'
24
+ Requires-Dist: ruff>=0.4; extra == 'dev'
25
+ Provides-Extra: otlp
26
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.27.0; extra == 'otlp'
27
+ Provides-Extra: otlp-http
28
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27.0; extra == 'otlp-http'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # aevum-otel
32
+
33
+ OpenTelemetry bridge complication for Aevum. Routes sigchain events to OTel GenAI spans.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install aevum-otel
39
+ # With OTLP HTTP exporter:
40
+ pip install "aevum-otel[otlp-http]"
41
+ # With OTLP gRPC exporter:
42
+ pip install "aevum-otel[otlp]"
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ ```python
48
+ from aevum.core import Engine
49
+ from aevum.otel import AevumOTelBridge
50
+
51
+ bridge = AevumOTelBridge(service_name="my-service")
52
+ engine = Engine()
53
+ engine.install_complication(bridge, auto_approve=True)
54
+
55
+ # All engine calls (ingest, query, etc.) now emit OTel GenAI spans.
56
+ ```
57
+
58
+ ## Privacy defaults
59
+
60
+ By default only `audit_id` is emitted as `gen_ai.content.reference`. No prompt or response content is included.
61
+
62
+ To opt in to richer attributes:
63
+
64
+ ```bash
65
+ export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
66
+ ```
67
+
68
+ ## GenAI semantic conventions
69
+
70
+ For the latest experimental GenAI semconv:
71
+
72
+ ```bash
73
+ export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
74
+ ```
75
+
76
+ See [OTel GenAI semconv documentation](https://opentelemetry.io/docs/specs/semconv/gen-ai/) for details.
77
+
78
+ ## Tested exporters
79
+
80
+ - Console exporter (always available via `opentelemetry-sdk`)
81
+ - Grafana Tempo (document setup if environment permits — otherwise note as untested)
82
+ - Langfuse (document setup if environment permits — otherwise note as untested)
83
+
84
+ ## License
85
+
86
+ Apache-2.0
@@ -0,0 +1,56 @@
1
+ # aevum-otel
2
+
3
+ OpenTelemetry bridge complication for Aevum. Routes sigchain events to OTel GenAI spans.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install aevum-otel
9
+ # With OTLP HTTP exporter:
10
+ pip install "aevum-otel[otlp-http]"
11
+ # With OTLP gRPC exporter:
12
+ pip install "aevum-otel[otlp]"
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ from aevum.core import Engine
19
+ from aevum.otel import AevumOTelBridge
20
+
21
+ bridge = AevumOTelBridge(service_name="my-service")
22
+ engine = Engine()
23
+ engine.install_complication(bridge, auto_approve=True)
24
+
25
+ # All engine calls (ingest, query, etc.) now emit OTel GenAI spans.
26
+ ```
27
+
28
+ ## Privacy defaults
29
+
30
+ By default only `audit_id` is emitted as `gen_ai.content.reference`. No prompt or response content is included.
31
+
32
+ To opt in to richer attributes:
33
+
34
+ ```bash
35
+ export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
36
+ ```
37
+
38
+ ## GenAI semantic conventions
39
+
40
+ For the latest experimental GenAI semconv:
41
+
42
+ ```bash
43
+ export OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
44
+ ```
45
+
46
+ See [OTel GenAI semconv documentation](https://opentelemetry.io/docs/specs/semconv/gen-ai/) for details.
47
+
48
+ ## Tested exporters
49
+
50
+ - Console exporter (always available via `opentelemetry-sdk`)
51
+ - Grafana Tempo (document setup if environment permits — otherwise note as untested)
52
+ - Langfuse (document setup if environment permits — otherwise note as untested)
53
+
54
+ ## License
55
+
56
+ Apache-2.0
@@ -0,0 +1,73 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "aevum-otel"
7
+ version = "0.6.0"
8
+ description = "Aevum — OpenTelemetry bridge complication. Routes sigchain events to OTel GenAI spans."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "Apache-2.0"
12
+ keywords = ["aevum", "opentelemetry", "otel", "observability", "genai", "audit"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
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
+ ]
21
+ dependencies = [
22
+ "aevum-core>=0.6.0",
23
+ "opentelemetry-sdk>=1.27.0",
24
+ "opentelemetry-api>=1.27.0",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ otlp = [
29
+ "opentelemetry-exporter-otlp-proto-grpc>=1.27.0",
30
+ ]
31
+ otlp-http = [
32
+ "opentelemetry-exporter-otlp-proto-http>=1.27.0",
33
+ ]
34
+ dev = [
35
+ "pytest>=8.0",
36
+ "pytest-asyncio>=0.23",
37
+ "mypy>=1.9",
38
+ "ruff>=0.4",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://aevum.build"
43
+ Repository = "https://github.com/aevum-labs/aevum"
44
+ Issues = "https://github.com/aevum-labs/aevum/issues"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/aevum"]
48
+
49
+ [tool.uv.sources]
50
+ aevum-core = { workspace = true }
51
+
52
+ [tool.mypy]
53
+ mypy_path = "src"
54
+ strict = true
55
+ python_version = "3.11"
56
+ ignore_missing_imports = true
57
+
58
+ [tool.ruff]
59
+ line-length = 130
60
+ target-version = "py311"
61
+
62
+ [tool.ruff.lint]
63
+ select = ["E", "F", "UP", "B", "SIM", "I", "ANN"]
64
+ ignore = ["ANN401"]
65
+
66
+ [tool.ruff.lint.per-file-ignores]
67
+ "tests/**" = ["ANN"]
68
+
69
+ [tool.pytest.ini_options]
70
+ testpaths = ["tests"]
71
+ asyncio_mode = "auto"
72
+ addopts = "--tb=short"
73
+ pythonpath = ["src", "tests"]
@@ -0,0 +1,32 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2024-2026 Aevum Labs contributors
3
+ """
4
+ aevum-otel — OpenTelemetry bridge complication for Aevum.
5
+
6
+ Routes sigchain events to OpenTelemetry GenAI spans, publishing to any
7
+ OTLP-compatible backend (Grafana Tempo, Langfuse, Jaeger, etc.).
8
+
9
+ Privacy defaults:
10
+ - Only audit_id is emitted (as gen_ai.content.reference).
11
+ - No prompt, response, or payload content is emitted by default.
12
+ - Set OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true to opt in.
13
+
14
+ GenAI semantic conventions:
15
+ - Set OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental for latest.
16
+
17
+ Usage:
18
+ from aevum.core import Engine
19
+ from aevum.otel import AevumOTelBridge
20
+
21
+ bridge = AevumOTelBridge(service_name="my-service")
22
+ engine = Engine()
23
+ engine.install_complication(bridge, auto_approve=True)
24
+
25
+ # Events from engine.ingest(), engine.query(), etc. will now appear
26
+ # as OTel spans in your configured OTLP backend.
27
+ """
28
+
29
+ from aevum.otel.bridge import AevumOTelBridge
30
+
31
+ __version__ = "0.6.0"
32
+ __all__ = ["AevumOTelBridge"]
@@ -0,0 +1,186 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright 2024-2026 Aevum Labs contributors
3
+ """
4
+ AevumOTelBridge — sigchain events → OTel GenAI spans.
5
+
6
+ Privacy model:
7
+ Default: emit only audit_id as gen_ai.content.reference.
8
+ Opt-in: set OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
9
+ to also emit event_type and actor in span attributes.
10
+
11
+ The bridge registers itself as a ledger observer and emits one OTel span
12
+ per AuditEvent. Span duration is always 0 (events are instantaneous writes).
13
+
14
+ Complication manifest:
15
+ name: "aevum-otel-bridge"
16
+ version: "0.6.0"
17
+ capabilities: ["telemetry.otel"]
18
+
19
+ Install via:
20
+ engine.install_complication(bridge, auto_approve=True)
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+ import os
27
+ from typing import TYPE_CHECKING, Any
28
+
29
+ if TYPE_CHECKING:
30
+ from aevum.core.audit.event import AuditEvent
31
+
32
+ _logger = logging.getLogger("aevum.otel")
33
+
34
+ _MANIFEST: dict[str, Any] = {
35
+ "name": "aevum-otel-bridge",
36
+ "version": "0.6.0",
37
+ "schema_version": "1.0",
38
+ "capabilities": ["telemetry.otel"],
39
+ "description": "Routes Aevum sigchain events to OTel GenAI spans.",
40
+ "author": "Aevum Labs",
41
+ "classification_max": 0,
42
+ "functions": ["ingest", "query", "review", "commit", "replay"],
43
+ "auth": {"public_key": None},
44
+ }
45
+
46
+ # OTel GenAI semantic convention attribute names
47
+ _ATTR_AUDIT_ID = "gen_ai.content.reference"
48
+ _ATTR_EVENT_TYPE = "aevum.event_type"
49
+ _ATTR_ACTOR = "aevum.actor"
50
+ _ATTR_SEQUENCE = "aevum.sequence"
51
+ _ATTR_EPISODE_ID = "aevum.episode_id"
52
+
53
+
54
+ def _capture_content_enabled() -> bool:
55
+ return (
56
+ os.environ.get(
57
+ "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", ""
58
+ ).lower()
59
+ in ("true", "1", "yes")
60
+ )
61
+
62
+
63
+ class AevumOTelBridge:
64
+ """
65
+ OpenTelemetry bridge complication.
66
+
67
+ Subscribes to ledger events and emits OTel GenAI spans to the configured
68
+ OTLP endpoint (or any registered TracerProvider).
69
+
70
+ Args:
71
+ service_name: OTel service name (default: "aevum").
72
+ tracer_provider: Optional pre-configured TracerProvider. If None,
73
+ uses the global OTel TracerProvider.
74
+ endpoint: Optional OTLP endpoint URL. If set, registers an
75
+ OTLP HTTP exporter (requires aevum-otel[otlp-http]).
76
+ If None, uses the global provider (e.g. console exporter
77
+ configured by the host application).
78
+ """
79
+
80
+ name: str = "aevum-otel-bridge"
81
+
82
+ def __init__(
83
+ self,
84
+ *,
85
+ service_name: str = "aevum",
86
+ tracer_provider: Any | None = None,
87
+ endpoint: str | None = None,
88
+ ) -> None:
89
+ from opentelemetry import trace
90
+
91
+ self._service_name = service_name
92
+
93
+ if tracer_provider is not None:
94
+ self._tracer_provider = tracer_provider
95
+ elif endpoint is not None:
96
+ self._tracer_provider = self._build_otlp_provider(service_name, endpoint)
97
+ else:
98
+ self._tracer_provider = trace.get_tracer_provider()
99
+
100
+ self._tracer = self._tracer_provider.get_tracer(
101
+ "aevum.otel.bridge",
102
+ schema_url="https://opentelemetry.io/schemas/1.28.0",
103
+ )
104
+ self._latency_samples: list[float] = []
105
+
106
+ def _build_otlp_provider(self, service_name: str, endpoint: str) -> Any:
107
+ """Build a TracerProvider with OTLP HTTP exporter."""
108
+ from opentelemetry.sdk.resources import Resource
109
+ from opentelemetry.sdk.trace import TracerProvider
110
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
111
+
112
+ try:
113
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
114
+ OTLPSpanExporter,
115
+ )
116
+ except ImportError as exc:
117
+ raise ImportError(
118
+ "OTLP HTTP exporter requires: pip install 'aevum-otel[otlp-http]'"
119
+ ) from exc
120
+
121
+ resource = Resource.create({"service.name": service_name})
122
+ provider = TracerProvider(resource=resource)
123
+ exporter = OTLPSpanExporter(endpoint=endpoint)
124
+ provider.add_span_processor(BatchSpanProcessor(exporter))
125
+ return provider
126
+
127
+ def manifest(self) -> dict[str, Any]:
128
+ return _MANIFEST
129
+
130
+ def set_event_observer(self, ledger: Any) -> None:
131
+ """Called by Engine.install_complication() to hook into ledger events."""
132
+ if hasattr(ledger, "add_observer"):
133
+ ledger.add_observer(self)
134
+ else:
135
+ _logger.warning(
136
+ "Ledger %r does not support add_observer — "
137
+ "AevumOTelBridge will not emit spans",
138
+ ledger,
139
+ )
140
+
141
+ def on_event(self, event: AuditEvent) -> None:
142
+ """
143
+ Called for each AuditEvent appended to the ledger.
144
+ Emits one OTel span per event.
145
+ """
146
+ import time # noqa: PLC0415
147
+
148
+ from opentelemetry.trace import SpanKind, StatusCode
149
+
150
+ t0 = time.monotonic()
151
+ capture = _capture_content_enabled()
152
+
153
+ try:
154
+ with self._tracer.start_as_current_span(
155
+ f"aevum.{event.event_type}",
156
+ kind=SpanKind.INTERNAL,
157
+ ) as span:
158
+ # Always-safe: audit reference only
159
+ span.set_attribute(_ATTR_AUDIT_ID, event.audit_id())
160
+ span.set_attribute(_ATTR_SEQUENCE, event.sequence)
161
+
162
+ if event.episode_id:
163
+ span.set_attribute(_ATTR_EPISODE_ID, event.episode_id)
164
+
165
+ if capture:
166
+ # Opt-in: richer attributes
167
+ span.set_attribute(_ATTR_EVENT_TYPE, event.event_type)
168
+ span.set_attribute(_ATTR_ACTOR, event.actor)
169
+
170
+ span.set_status(StatusCode.OK)
171
+ except Exception as exc: # noqa: BLE001
172
+ _logger.error("OTel span emission failed (suppressed): %s", exc)
173
+ finally:
174
+ elapsed_ms = (time.monotonic() - t0) * 1000
175
+ self._latency_samples.append(elapsed_ms)
176
+
177
+ def latency_p99_ms(self) -> float | None:
178
+ """Return the p99 latency in ms over all observed events, or None if no data."""
179
+ if not self._latency_samples:
180
+ return None
181
+ sorted_samples = sorted(self._latency_samples)
182
+ idx = int(len(sorted_samples) * 0.99)
183
+ return sorted_samples[min(idx, len(sorted_samples) - 1)]
184
+
185
+ def reset_latency_samples(self) -> None:
186
+ self._latency_samples.clear()
File without changes
@@ -0,0 +1,227 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Tests for AevumOTelBridge complication."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from unittest.mock import MagicMock
7
+
8
+ from opentelemetry.sdk.trace import TracerProvider
9
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
10
+ from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
11
+
12
+ from aevum.otel import AevumOTelBridge
13
+
14
+
15
+ def _make_bridge() -> tuple[AevumOTelBridge, InMemorySpanExporter]:
16
+ """Create a bridge wired to an in-memory span exporter for testing."""
17
+ exporter = InMemorySpanExporter()
18
+ provider = TracerProvider()
19
+ provider.add_span_processor(SimpleSpanProcessor(exporter))
20
+ bridge = AevumOTelBridge(service_name="test-service", tracer_provider=provider)
21
+ return bridge, exporter
22
+
23
+
24
+ def _make_mock_event(
25
+ event_type: str = "ingest.accepted",
26
+ actor: str = "test-agent",
27
+ sequence: int = 1,
28
+ episode_id: str | None = None,
29
+ ) -> MagicMock:
30
+ event = MagicMock()
31
+ event.audit_id.return_value = "urn:aevum:audit:test-123"
32
+ event.event_type = event_type
33
+ event.actor = actor
34
+ event.sequence = sequence
35
+ event.episode_id = episode_id
36
+ return event
37
+
38
+
39
+ # ── Manifest ──────────────────────────────────────────────────────────────────
40
+
41
+ class TestManifest:
42
+ def test_manifest_name(self):
43
+ bridge = AevumOTelBridge()
44
+ assert bridge.manifest()["name"] == "aevum-otel-bridge"
45
+
46
+ def test_manifest_capabilities(self):
47
+ bridge = AevumOTelBridge()
48
+ assert "telemetry.otel" in bridge.manifest()["capabilities"]
49
+
50
+ def test_name_attribute(self):
51
+ bridge = AevumOTelBridge()
52
+ assert bridge.name == "aevum-otel-bridge"
53
+
54
+
55
+ # ── Span emission ─────────────────────────────────────────────────────────────
56
+
57
+ class TestSpanEmission:
58
+ def test_emits_span_per_event(self):
59
+ bridge, exporter = _make_bridge()
60
+ bridge.on_event(_make_mock_event())
61
+ spans = exporter.get_finished_spans()
62
+ assert len(spans) == 1
63
+
64
+ def test_span_name_includes_event_type(self):
65
+ bridge, exporter = _make_bridge()
66
+ bridge.on_event(_make_mock_event(event_type="session.start"))
67
+ spans = exporter.get_finished_spans()
68
+ assert spans[0].name == "aevum.session.start"
69
+
70
+ def test_span_has_audit_id_attribute(self):
71
+ bridge, exporter = _make_bridge()
72
+ bridge.on_event(_make_mock_event())
73
+ spans = exporter.get_finished_spans()
74
+ attrs = dict(spans[0].attributes or {})
75
+ assert attrs.get("gen_ai.content.reference") == "urn:aevum:audit:test-123"
76
+
77
+ def test_span_has_sequence_attribute(self):
78
+ bridge, exporter = _make_bridge()
79
+ bridge.on_event(_make_mock_event(sequence=42))
80
+ spans = exporter.get_finished_spans()
81
+ attrs = dict(spans[0].attributes or {})
82
+ assert attrs.get("aevum.sequence") == 42
83
+
84
+ def test_no_content_in_default_mode(self, monkeypatch):
85
+ monkeypatch.delenv("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", raising=False)
86
+ bridge, exporter = _make_bridge()
87
+ bridge.on_event(_make_mock_event(actor="secret-agent"))
88
+ spans = exporter.get_finished_spans()
89
+ attrs = dict(spans[0].attributes or {})
90
+ # actor should NOT be emitted in default mode
91
+ assert "aevum.actor" not in attrs
92
+ assert "aevum.event_type" not in attrs
93
+
94
+ def test_content_emitted_when_opted_in(self, monkeypatch):
95
+ monkeypatch.setenv("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "true")
96
+ bridge, exporter = _make_bridge()
97
+ bridge.on_event(_make_mock_event(event_type="ingest.accepted", actor="my-agent"))
98
+ spans = exporter.get_finished_spans()
99
+ attrs = dict(spans[0].attributes or {})
100
+ assert attrs.get("aevum.actor") == "my-agent"
101
+ assert attrs.get("aevum.event_type") == "ingest.accepted"
102
+
103
+ def test_episode_id_emitted_when_present(self):
104
+ bridge, exporter = _make_bridge()
105
+ bridge.on_event(_make_mock_event(episode_id="ep-001"))
106
+ spans = exporter.get_finished_spans()
107
+ attrs = dict(spans[0].attributes or {})
108
+ assert attrs.get("aevum.episode_id") == "ep-001"
109
+
110
+ def test_episode_id_absent_when_none(self):
111
+ bridge, exporter = _make_bridge()
112
+ bridge.on_event(_make_mock_event(episode_id=None))
113
+ spans = exporter.get_finished_spans()
114
+ attrs = dict(spans[0].attributes or {})
115
+ assert "aevum.episode_id" not in attrs
116
+
117
+
118
+ # ── Observer registration ─────────────────────────────────────────────────────
119
+
120
+ class TestObserverRegistration:
121
+ def test_set_event_observer_registers_with_ledger(self):
122
+ bridge, _ = _make_bridge()
123
+ mock_ledger = MagicMock()
124
+ bridge.set_event_observer(mock_ledger)
125
+ mock_ledger.add_observer.assert_called_once_with(bridge)
126
+
127
+ def test_set_event_observer_warns_when_no_add_observer(self, caplog):
128
+ import logging
129
+ bridge, _ = _make_bridge()
130
+ ledger_without_observer = object() # no add_observer method
131
+ with caplog.at_level(logging.WARNING, logger="aevum.otel"):
132
+ bridge.set_event_observer(ledger_without_observer)
133
+ assert any("add_observer" in r.message for r in caplog.records)
134
+
135
+
136
+ # ── Error resilience ──────────────────────────────────────────────────────────
137
+
138
+ class TestErrorResilience:
139
+ def test_on_event_does_not_raise_on_bad_event(self):
140
+ bridge, _ = _make_bridge()
141
+ bad_event = MagicMock()
142
+ bad_event.audit_id.side_effect = RuntimeError("broken")
143
+ # Should not raise
144
+ bridge.on_event(bad_event)
145
+
146
+
147
+ # ── Latency ───────────────────────────────────────────────────────────────────
148
+
149
+ class TestLatency:
150
+ def test_latency_p99_none_when_no_events(self):
151
+ bridge, _ = _make_bridge()
152
+ assert bridge.latency_p99_ms() is None
153
+
154
+ def test_latency_p99_populated_after_events(self):
155
+ bridge, _ = _make_bridge()
156
+ for _ in range(100):
157
+ bridge.on_event(_make_mock_event())
158
+ p99 = bridge.latency_p99_ms()
159
+ assert p99 is not None
160
+ assert p99 >= 0
161
+
162
+ def test_latency_reset(self):
163
+ bridge, _ = _make_bridge()
164
+ bridge.on_event(_make_mock_event())
165
+ bridge.reset_latency_samples()
166
+ assert bridge.latency_p99_ms() is None
167
+
168
+
169
+ # ── Engine integration ────────────────────────────────────────────────────────
170
+
171
+ class TestEngineIntegration:
172
+ def test_bridge_receives_events_from_engine(self, monkeypatch):
173
+ """Install bridge in Engine, verify spans are emitted."""
174
+ monkeypatch.setenv("AEVUM_DEV", "1")
175
+ from aevum.core.engine import Engine
176
+
177
+ exporter = InMemorySpanExporter()
178
+ provider = TracerProvider()
179
+ provider.add_span_processor(SimpleSpanProcessor(exporter))
180
+ bridge = AevumOTelBridge(service_name="eng-test", tracer_provider=provider)
181
+
182
+ engine = Engine()
183
+ engine.install_complication(bridge, auto_approve=True)
184
+
185
+ engine.ingest(
186
+ data={"note": "otel test"},
187
+ provenance={"source_id": "t", "chain_of_custody": ["t"], "classification": 0},
188
+ purpose="test-otel",
189
+ subject_id="u1",
190
+ actor="a1",
191
+ )
192
+
193
+ spans = exporter.get_finished_spans()
194
+ span_names = [s.name for s in spans]
195
+ # ingest.accepted span should be present
196
+ assert any("ingest" in n for n in span_names), f"No ingest span in: {span_names}"
197
+
198
+ def test_bridge_p99_latency_under_2ms(self, monkeypatch):
199
+ """
200
+ B-14: OTel latency overhead p99 must be < 2ms.
201
+ Uses in-memory span exporter (zero network overhead) to measure
202
+ pure bridge overhead.
203
+ """
204
+ monkeypatch.setenv("AEVUM_DEV", "1")
205
+ from aevum.core.engine import Engine
206
+
207
+ exporter = InMemorySpanExporter()
208
+ provider = TracerProvider()
209
+ provider.add_span_processor(SimpleSpanProcessor(exporter))
210
+ bridge = AevumOTelBridge(service_name="bench", tracer_provider=provider)
211
+
212
+ engine = Engine()
213
+ engine.install_complication(bridge, auto_approve=True)
214
+ bridge.reset_latency_samples()
215
+
216
+ for i in range(200):
217
+ engine.ingest(
218
+ data={"n": i},
219
+ provenance={"source_id": "t", "chain_of_custody": ["t"], "classification": 0},
220
+ purpose="bench",
221
+ subject_id=f"u{i}",
222
+ actor="a1",
223
+ )
224
+
225
+ p99 = bridge.latency_p99_ms()
226
+ assert p99 is not None
227
+ assert p99 < 2.0, f"p99 latency {p99:.3f}ms exceeds 2ms threshold"