from __future__ import annotations import json import logging from types import SimpleNamespace from opentelemetry import trace from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags, TraceState from app import telemetry def test_telemetry_is_fail_open_without_endpoint(monkeypatch) -> None: monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) monkeypatch.setattr(telemetry, "_runtime", None) assert telemetry.init_telemetry("api") is None def test_resource_uses_canonical_namespace(monkeypatch) -> None: monkeypatch.setenv("APP_ENV", "test") resource = telemetry._resource("worker") assert resource.attributes["service.name"] == "message-safety" assert resource.attributes["service.namespace"] == "han-chat" assert resource.attributes["deployment.environment"] == "test" assert resource.attributes["process.role"] == "worker" def test_json_stdout_redacts_sensitive_data_and_adds_trace_context() -> None: record = logging.LogRecord( "message_safety", logging.INFO, __file__, 1, "Bearer top-secret user@example.org +79991234567 https://host/path?token=x", (), None, ) record.event = "redaction_canary" record.authorization = "secret" telemetry.RedactionFilter().filter(record) context = SpanContext( trace_id=1, span_id=2, is_remote=False, trace_flags=TraceFlags(1), trace_state=TraceState(), ) with trace.use_span(NonRecordingSpan(context)): payload = json.loads(telemetry.JsonFormatter().format(record)) serialized = json.dumps(payload) assert "top-secret" not in serialized assert "user@example.org" not in serialized assert "79991234567" not in serialized assert "?token=x" not in serialized assert payload["trace_id"] == f"{1:032x}" assert payload["span_id"] == f"{2:016x}" assert payload["service.name"] == "message-safety" def test_span_attributes_are_redacted_before_export() -> None: attributes = { "url.full": "https://storage.example/private?token=secret", "db.statement": "SELECT private_value FROM safety_tasks", "safe.outcome": "allow", } telemetry._redact_span_attributes(attributes) assert attributes["url.full"] == "[REDACTED]" assert attributes["db.statement"] == "[REDACTED]" assert attributes["safe.outcome"] == "allow" def test_span_processor_replaces_immutable_sdk_attributes() -> None: exporter = InMemorySpanExporter() processor = telemetry.RedactingBatchSpanProcessor(exporter) span = ReadableSpan( name="database.connect", attributes={"db.statement": "SELECT secret", "safe.outcome": "allow"}, ) processor.on_end(span) processor.shutdown() assert span.attributes["db.statement"] == "[REDACTED]" assert span.attributes["safe.outcome"] == "allow" def test_task_link_rebuilds_valid_remote_context() -> None: task = SimpleNamespace( origin_trace_id=(123).to_bytes(16, "big"), origin_span_id=(456).to_bytes(8, "big"), origin_trace_flags=1, origin_tracestate=None, ) link = telemetry.task_link(task) assert link is not None assert link.context.is_remote assert link.context.trace_id == 123 assert link.context.span_id == 456 def test_current_trace_fields_round_trip_into_task_link() -> None: context = SpanContext( trace_id=123, span_id=456, is_remote=False, trace_flags=TraceFlags(1), trace_state=TraceState(), ) with trace.use_span(NonRecordingSpan(context)): trace_id, span_id, flags, tracestate = telemetry.current_trace_fields() link = telemetry.task_link( SimpleNamespace( origin_trace_id=trace_id, origin_span_id=span_id, origin_trace_flags=flags, origin_tracestate=tracestate, ) ) assert link is not None assert link.context.trace_id == context.trace_id assert link.context.span_id == context.span_id