@@ -3,6 +3,8 @@ import logging
|
||||
from io import StringIO
|
||||
|
||||
import structlog
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags
|
||||
|
||||
from app.runtime.logging import configure_logging
|
||||
|
||||
@@ -20,6 +22,8 @@ def test_application_log_is_serialized_as_json() -> None:
|
||||
assert payload["level"] == "info"
|
||||
assert payload["logger"] == "app.test"
|
||||
assert isinstance(payload["timestamp"], str)
|
||||
assert "trace_id" not in payload
|
||||
assert "span_id" not in payload
|
||||
|
||||
_reset_logging()
|
||||
|
||||
@@ -75,6 +79,49 @@ def test_uvicorn_loggers_use_shared_json_logging_setup() -> None:
|
||||
_reset_logging()
|
||||
|
||||
|
||||
def test_active_trace_context_is_added_to_structlog_and_standard_logs() -> None:
|
||||
stream = StringIO()
|
||||
trace_id = 0x1234567890ABCDEF1234567890ABCDEF
|
||||
span_id = 0x1234567890ABCDEF
|
||||
span = NonRecordingSpan(
|
||||
SpanContext(
|
||||
trace_id=trace_id,
|
||||
span_id=span_id,
|
||||
is_remote=False,
|
||||
trace_flags=TraceFlags.SAMPLED,
|
||||
)
|
||||
)
|
||||
|
||||
configure_logging(stream=stream)
|
||||
|
||||
with trace.use_span(span, end_on_exit=False):
|
||||
structlog.get_logger("app.test").info("structured_event")
|
||||
logging.getLogger("app.test").info("standard_event")
|
||||
|
||||
records = [json.loads(line) for line in stream.getvalue().splitlines()]
|
||||
expected_trace_id = "1234567890abcdef1234567890abcdef"
|
||||
expected_span_id = "1234567890abcdef"
|
||||
|
||||
assert [record["trace_id"] for record in records] == [
|
||||
expected_trace_id,
|
||||
expected_trace_id,
|
||||
]
|
||||
assert [record["span_id"] for record in records] == [
|
||||
expected_span_id,
|
||||
expected_span_id,
|
||||
]
|
||||
assert records[0]["event"] == (
|
||||
f'structured_event span_id="{expected_span_id}" '
|
||||
f'trace_id="{expected_trace_id}"'
|
||||
)
|
||||
assert records[1]["event"] == (
|
||||
f'standard_event span_id="{expected_span_id}" '
|
||||
f'trace_id="{expected_trace_id}"'
|
||||
)
|
||||
|
||||
_reset_logging()
|
||||
|
||||
|
||||
def _reset_logging() -> None:
|
||||
structlog.reset_defaults()
|
||||
root_logger = logging.getLogger()
|
||||
|
||||
@@ -6,6 +6,8 @@ import yaml
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
COMPOSE_FILE = PROJECT_ROOT / "docker-compose.yml"
|
||||
INFRA_README = PROJECT_ROOT / "infra" / "README.md"
|
||||
DEPLOY_WORKFLOW = PROJECT_ROOT / ".gitea" / "workflows" / "deploy.yml"
|
||||
COLLECTOR_TEMPLATE = PROJECT_ROOT / "otel-collector-config.template.yaml"
|
||||
|
||||
|
||||
def _load_compose() -> dict:
|
||||
@@ -28,3 +30,26 @@ def test_smoke_command_sequence_is_documented() -> None:
|
||||
assert "docker compose config" in readme
|
||||
assert "docker compose up -d redis" in readme
|
||||
assert "docker compose ps" in readme
|
||||
|
||||
|
||||
def test_deploy_recreates_collector_after_config_update() -> None:
|
||||
workflow = DEPLOY_WORKFLOW.read_text(encoding="utf-8")
|
||||
|
||||
recreate_command = (
|
||||
"docker compose -p $COMPOSE_PROJECT up -d --force-recreate otel-collector"
|
||||
)
|
||||
application_command = "docker compose -p $COMPOSE_PROJECT up -d"
|
||||
|
||||
assert recreate_command in workflow
|
||||
assert workflow.index(recreate_command) < workflow.rindex(application_command)
|
||||
|
||||
|
||||
def test_collector_promotes_log_trace_attributes_to_trace_context() -> None:
|
||||
config = yaml.safe_load(COLLECTOR_TEMPLATE.read_text(encoding="utf-8"))
|
||||
operators = config["receivers"]["filelog"]["operators"]
|
||||
trace_parser = next(
|
||||
operator for operator in operators if operator["type"] == "trace_parser"
|
||||
)
|
||||
|
||||
assert trace_parser["trace_id"]["parse_from"] == "attributes.trace_id"
|
||||
assert trace_parser["span_id"]["parse_from"] == "attributes.span_id"
|
||||
|
||||
Reference in New Issue
Block a user