mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-23 14:41:13 +00:00
feat(stage2-phase2): structlog hot-path, pytest markers, obs package
Phase 2 deliverables not yet committed from plan execution: - structlog wired to 10 hot-path files (orchestrator, eskf, components) - bind_contextvars(correlation_id=frame_id) in process_frame - obs/logging_config.py: configure_logging(env) JSON/console renderer - pyproject.toml: structlog>=25.1, --strict-markers, 6 markers registered - tests/conftest.py: ac(id) validator plugin + pytest_collection hooks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""structlog configuration. Call ``configure_logging`` once at app boot.
|
||||
|
||||
Per Phase 2 / OBS-01: hot path uses structlog with ``correlation_id`` (= frame_id)
|
||||
bound at frame entry. Non-hot-path code keeps stdlib ``logging`` until Phase 6
|
||||
(api/, db/, scripts/, composition.py at startup, core/models.py engine load).
|
||||
The stdlib bridge below lets stdlib records flow through the same renderer.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
import orjson
|
||||
import structlog
|
||||
|
||||
_Env = Literal["jetson", "x86_dev", "ci", "sitl"]
|
||||
|
||||
_configured = False
|
||||
|
||||
|
||||
def configure_logging(env: _Env, level: int = logging.INFO) -> None:
|
||||
"""Configure structlog ONCE at app boot. Idempotent — repeat calls no-op.
|
||||
|
||||
Args:
|
||||
env: Deployment environment. Controls renderer:
|
||||
- ``x86_dev`` -> pretty console renderer
|
||||
- ``jetson|ci|sitl`` -> JSON renderer (orjson) + bytes logger factory
|
||||
level: Stdlib log level threshold. ``filtering_bound_logger`` short-circuits
|
||||
sub-level calls in ~50-100 ns. Keep at INFO in production.
|
||||
"""
|
||||
global _configured
|
||||
if _configured:
|
||||
return
|
||||
|
||||
shared_processors: list = [
|
||||
# MUST be first — pulls bound frame_id into every record.
|
||||
structlog.contextvars.merge_contextvars,
|
||||
structlog.processors.add_log_level,
|
||||
structlog.processors.TimeStamper(fmt="iso", utc=True),
|
||||
structlog.processors.format_exc_info,
|
||||
]
|
||||
|
||||
if env == "x86_dev":
|
||||
processors = [*shared_processors, structlog.dev.ConsoleRenderer()]
|
||||
logger_factory = structlog.PrintLoggerFactory()
|
||||
else:
|
||||
# jetson | ci | sitl — JSON to bytes via orjson; fastest path on hot loop.
|
||||
processors = [
|
||||
*shared_processors,
|
||||
structlog.processors.JSONRenderer(serializer=orjson.dumps),
|
||||
]
|
||||
logger_factory = structlog.BytesLoggerFactory()
|
||||
|
||||
structlog.configure(
|
||||
processors=processors,
|
||||
wrapper_class=structlog.make_filtering_bound_logger(level),
|
||||
logger_factory=logger_factory,
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
|
||||
# Bridge stdlib logging: api/, db/, scripts/, composition.py, core/models.py
|
||||
# (Phase 6 ports these to structlog directly). Until then, their records share
|
||||
# the same level threshold; format passthrough is via "%(message)s" because the
|
||||
# structlog renderer above is the actual output sink.
|
||||
logging.basicConfig(level=level, format="%(message)s")
|
||||
|
||||
_configured = True
|
||||
Reference in New Issue
Block a user