plba
This commit is contained in:
16
src/app_runtime/tracing/__init__.py
Normal file
16
src/app_runtime/tracing/__init__.py
Normal 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",
|
||||
]
|
||||
BIN
src/app_runtime/tracing/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
src/app_runtime/tracing/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/app_runtime/tracing/__pycache__/manager.cpython-312.pyc
Normal file
BIN
src/app_runtime/tracing/__pycache__/manager.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/app_runtime/tracing/__pycache__/service.cpython-312.pyc
Normal file
BIN
src/app_runtime/tracing/__pycache__/service.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/app_runtime/tracing/__pycache__/store.cpython-312.pyc
Normal file
BIN
src/app_runtime/tracing/__pycache__/store.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/app_runtime/tracing/__pycache__/transport.cpython-312.pyc
Normal file
BIN
src/app_runtime/tracing/__pycache__/transport.cpython-312.pyc
Normal file
Binary file not shown.
166
src/app_runtime/tracing/service.py
Normal file
166
src/app_runtime/tracing/service.py
Normal 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."""
|
||||
52
src/app_runtime/tracing/store.py
Normal file
52
src/app_runtime/tracing/store.py
Normal 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
|
||||
11
src/app_runtime/tracing/transport.py
Normal file
11
src/app_runtime/tracing/transport.py
Normal 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
|
||||
Reference in New Issue
Block a user