Files
han-app/VM2_services/codebase/services/message-safety/tests/test_telemetry.py
T

132 lines
4.1 KiB
Python

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