132 lines
4.1 KiB
Python
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
|