This commit is contained in:
2026-03-04 10:01:49 +03:00
commit de787ce7ee
107 changed files with 2801 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
from app_runtime.contracts.trace import TraceContext, TraceContextRecord, TraceLogMessage, TraceTransport
from app_runtime.tracing.service import TraceManager, TraceService
from app_runtime.tracing.store import ActiveTraceContext, TraceContextStore
from app_runtime.tracing.transport import NoOpTraceTransport
__all__ = [
"ActiveTraceContext",
"NoOpTraceTransport",
"TraceContext",
"TraceContextRecord",
"TraceContextStore",
"TraceLogMessage",
"TraceManager",
"TraceService",
"TraceTransport",
]

View File

@@ -0,0 +1,166 @@
from __future__ import annotations
import logging
from contextlib import contextmanager
from typing import Any, Iterator
from uuid import uuid4
from app_runtime.contracts.trace import (
TraceContext,
TraceContextFactory,
TraceContextRecord,
TraceLogMessage,
TraceTransport,
utc_now,
)
from app_runtime.tracing.store import TraceContextStore
from app_runtime.tracing.transport import NoOpTraceTransport
class TraceRecordWriter:
def __init__(self, transport: TraceTransport) -> None:
self._logger = logging.getLogger(__name__)
self._transport = transport
def write_context(self, record: TraceContextRecord) -> None:
try:
self._transport.write_context(record)
except Exception:
self._logger.exception("Trace transport failed to write context %s", record.trace_id)
def write_message(self, record: TraceLogMessage) -> None:
try:
self._transport.write_message(record)
except Exception:
self._logger.exception("Trace transport failed to write message for %s", record.trace_id)
class TraceService(TraceContextFactory):
def __init__(self, transport: TraceTransport | None = None, store: TraceContextStore | None = None) -> None:
self.transport = transport or NoOpTraceTransport()
self.store = store or TraceContextStore()
self._writer = TraceRecordWriter(self.transport)
def create_context(
self,
*,
alias: str,
parent_id: str | None = None,
kind: str | None = None,
attrs: dict[str, Any] | None = None,
) -> str:
record = TraceContextRecord(
trace_id=uuid4().hex,
alias=str(alias or ""),
parent_id=parent_id,
type=kind,
event_time=utc_now(),
attrs=dict(attrs or {}),
)
self.store.push(record)
self._writer.write_context(record)
return record.trace_id
@contextmanager
def open_context(
self,
*,
alias: str,
parent_id: str | None = None,
kind: str | None = None,
attrs: dict[str, Any] | None = None,
) -> Iterator[str]:
trace_id = self.create_context(alias=alias, parent_id=parent_id, kind=kind, attrs=attrs)
try:
yield trace_id
finally:
self.store.pop()
def current_trace_id(self) -> str | None:
return self.store.current_trace_id()
def close_context(self) -> str | None:
previous = self.store.pop()
return previous.record.trace_id if previous else None
def step(self, name: str) -> None:
self.store.set_step(str(name or ""))
def info(self, message: str, *, status: str, attrs: dict[str, Any] | None = None) -> None:
self._write_message("INFO", message, status, attrs)
def warning(self, message: str, *, status: str, attrs: dict[str, Any] | None = None) -> None:
self._write_message("WARNING", message, status, attrs)
def error(self, message: str, *, status: str, attrs: dict[str, Any] | None = None) -> None:
self._write_message("ERROR", message, status, attrs)
def exception(self, message: str, *, status: str = "failed", attrs: dict[str, Any] | None = None) -> None:
self._write_message("ERROR", message, status, attrs)
def new_root(self, operation: str) -> TraceContext:
trace_id = self.create_context(alias=operation, kind="source", attrs={"operation": operation})
return TraceContext(trace_id=trace_id, span_id=trace_id, attributes={"operation": operation})
def child_of(self, parent: TraceContext, operation: str) -> TraceContext:
trace_id = self.create_context(
alias=operation,
parent_id=parent.trace_id,
kind="worker",
attrs={"operation": operation},
)
return TraceContext(
trace_id=trace_id,
span_id=trace_id,
parent_span_id=parent.span_id,
attributes={"operation": operation},
)
def attach(self, task_metadata: dict[str, object], context: TraceContext) -> dict[str, object]:
updated = dict(task_metadata)
updated["trace_id"] = context.trace_id
updated["span_id"] = context.span_id
updated["parent_span_id"] = context.parent_span_id
return updated
def resume(self, task_metadata: dict[str, object], operation: str) -> TraceContext:
trace_id = str(task_metadata.get("trace_id") or uuid4().hex)
span_id = str(task_metadata.get("span_id") or trace_id)
parent_id = task_metadata.get("parent_span_id")
self.create_context(
alias=operation,
parent_id=str(parent_id) if parent_id else None,
kind="handler",
attrs=dict(task_metadata),
)
return TraceContext(
trace_id=trace_id,
span_id=span_id,
parent_span_id=str(parent_id) if parent_id else None,
attributes={"operation": operation},
)
def _write_message(
self,
level: str,
message: str,
status: str,
attrs: dict[str, Any] | None,
) -> None:
active = self.store.current()
if active is None:
raise RuntimeError("Trace context is not bound. Call create_context() first.")
record = TraceLogMessage(
trace_id=active.record.trace_id,
step=active.step,
status=str(status or ""),
message=str(message or ""),
level=level,
event_time=utc_now(),
attrs=dict(attrs or {}),
)
self._writer.write_message(record)
class TraceManager(TraceService):
"""Compatibility alias during the transition from ConfigManager-shaped naming."""

View File

@@ -0,0 +1,52 @@
from __future__ import annotations
from contextvars import ContextVar
from dataclasses import dataclass, replace
from app_runtime.contracts.trace import TraceContextRecord
@dataclass(frozen=True)
class ActiveTraceContext:
record: TraceContextRecord
step: str = ""
class TraceContextStore:
def __init__(self) -> None:
self._current: ContextVar[ActiveTraceContext | None] = ContextVar("trace_current", default=None)
self._stack: ContextVar[tuple[ActiveTraceContext, ...]] = ContextVar("trace_stack", default=())
def current(self) -> ActiveTraceContext | None:
return self._current.get()
def current_trace_id(self) -> str | None:
active = self.current()
return active.record.trace_id if active else None
def push(self, record: TraceContextRecord) -> ActiveTraceContext:
active = self.current()
stack = self._stack.get()
if active is not None:
self._stack.set(stack + (active,))
updated = ActiveTraceContext(record=record)
self._current.set(updated)
return updated
def pop(self) -> ActiveTraceContext | None:
stack = self._stack.get()
if not stack:
self._current.set(None)
return None
previous = stack[-1]
self._stack.set(stack[:-1])
self._current.set(previous)
return previous
def set_step(self, step: str) -> ActiveTraceContext | None:
active = self.current()
if active is None:
return None
updated = replace(active, step=step)
self._current.set(updated)
return updated

View File

@@ -0,0 +1,11 @@
from __future__ import annotations
from app_runtime.contracts.trace import TraceContextRecord, TraceLogMessage, TraceTransport
class NoOpTraceTransport(TraceTransport):
def write_context(self, record: TraceContextRecord) -> None:
del record
def write_message(self, record: TraceLogMessage) -> None:
del record