from __future__ import annotations import json import logging from typing import Any import pytest from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from app import telemetry @pytest.mark.parametrize( "canary", [ "Bearer canary-authorization-value", "person@example.test", "+7 (999) 123-45-67", "https://portal.example/path?token=canary", "aaaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbb.cccccccccc", ], ) def test_redaction_removes_canaries_from_json_stdout(canary: str) -> None: record = logging.LogRecord( "test", logging.INFO, __file__, 1, "safe event %s", (canary,), None, ) record.event_name = "test.canary" record.telemetry_attributes = { "authorization": canary, "nested": {"email": canary, "safe": canary}, } telemetry.RedactionFilter().filter(record) output = telemetry.JsonFormatter().format(record) assert canary not in output assert json.loads(output)["event"] == "test.canary" def test_redaction_blocks_sensitive_keys_and_bounds_collections() -> None: result = telemetry.redact( { "db.statement": "SELECT secret FROM users", "object_key": "private/file.txt", "safe": list(range(100)), } ) assert result["db.statement"] == "[REDACTED]" assert result["object_key"] == "[REDACTED]" assert len(result["safe"]) == 32 def test_redaction_drops_exception_text_before_otlp() -> None: try: raise RuntimeError("secret-token-in-exception") except RuntimeError: record = logging.LogRecord( "test", logging.ERROR, __file__, 1, "operation failed", (), exc_info=__import__("sys").exc_info(), ) telemetry.RedactionFilter().filter(record) assert record.exc_info is None assert record.telemetry_attributes["error.type"] == "RuntimeError" assert "secret-token-in-exception" not in telemetry.JsonFormatter().format(record) def test_span_processor_replaces_immutable_sdk_attributes() -> None: exporter = InMemorySpanExporter() processor = telemetry.RedactingBatchSpanProcessor(exporter) span = ReadableSpan( name="crm.request", attributes={"url.full": "https://crm.example/?token=secret", "safe.outcome": "success"}, ) processor.on_end(span) processor.shutdown() assert span.attributes["url.full"] == "[REDACTED]" assert span.attributes["safe.outcome"] == "success" def test_init_without_endpoint_is_backward_compatible(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) monkeypatch.setattr(telemetry, "_runtime", None) runtime = telemetry.init_telemetry("bitrix-sync-worker") assert runtime.tracer_provider is None assert runtime.meter_provider is None assert runtime.logger_provider is None telemetry.shutdown_telemetry() def test_each_process_has_distinct_service_resource() -> None: names = { telemetry._resource(name).attributes["service.name"] # noqa: SLF001 for name in telemetry.SERVICE_NAMES } assert names == { "bitrix-sync-api", "bitrix-sync-worker", "bitrix-sync-reconciliation", } def test_init_is_fail_open_when_exporter_construction_fails( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4317") monkeypatch.setattr(telemetry, "_runtime", None) def fail_exporter(**_kwargs: Any) -> None: raise RuntimeError("collector unavailable") monkeypatch.setattr(telemetry, "OTLPSpanExporter", fail_exporter) runtime = telemetry.init_telemetry("bitrix-sync-reconciliation") assert runtime.tracer_provider is None telemetry.shutdown_telemetry() def test_business_metric_attributes_are_bounded(monkeypatch: pytest.MonkeyPatch) -> None: captured: list[dict[str, str]] = [] class Instrument: def add(self, _value: int, attributes: dict[str, str]) -> None: captured.append(attributes) def record(self, _value: float, attributes: dict[str, str]) -> None: captured.append(attributes) class Meter: def create_counter(self, *_args: Any, **_kwargs: Any) -> Instrument: return Instrument() def create_histogram(self, *_args: Any, **_kwargs: Any) -> Instrument: return Instrument() monkeypatch.setattr(telemetry.metrics, "get_meter", lambda _name: Meter()) telemetry.record_workflow("user-2f3c0a5e-identifier", "novel-state", 0.5) telemetry.record_crm("unknown.dynamic.method", "novel-state", 0.1) assert captured assert all("user-2f3c0a5e-identifier" not in values.values() for values in captured) assert all("novel-state" not in values.values() for values in captured) def test_business_metrics_are_fail_open(monkeypatch: pytest.MonkeyPatch) -> None: def unavailable(_name: str) -> None: raise RuntimeError("metrics unavailable") monkeypatch.setattr(telemetry.metrics, "get_meter", unavailable) telemetry.record_claim("task", 1)