Files
OpenViking/openviking/observability/context.py
T
Qin Haojie d47f2106ee refactor: remove unused and deprecated APIs (#3272)
Delete dead compatibility paths and test-only helpers so unsupported APIs do not remain as accidental contracts.
2026-07-16 10:49:56 +08:00

352 lines
10 KiB
Python

# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd.
# SPDX-License-Identifier: AGPL-3.0
"""
Unified observability context management.
This module provides a unified context management system for observability,
integrating:
- Root context (HTTP request-level)
- Operation context (business operation-level)
- Execution context (non-request scenarios)
Key features:
- Single contextvar for all observability context
- Unified binding, retrieval, and reset interfaces
- Type-safe data structures
- Automatic OTel trace/span ID integration
"""
from __future__ import annotations
import contextvars
import uuid
from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import Any, Dict, Optional
from openviking.telemetry.span_models import OperationSpanAttributes, RootSpanAttributes
# Try to import opentelemetry
try:
from opentelemetry import trace as otel_trace
from opentelemetry.trace import format_span_id, format_trace_id
except ImportError:
otel_trace = None
format_span_id = None
format_trace_id = None
@dataclass
class ObservabilityContext:
"""
Unified observability context data structure.
This class integrates all observability-related context information:
- Root context (HTTP request-level)
- Operation context (business operation-level)
- Execution context (non-request scenarios)
Attributes:
root: Root span attributes for HTTP requests.
operation: Operation span attributes for business operations.
execution_trace_id: Trace ID for non-request execution contexts.
execution_span_id: Span ID for non-request execution contexts.
extra: Additional custom attributes.
"""
root: Optional[RootSpanAttributes] = None
operation: Optional[OperationSpanAttributes] = None
execution_trace_id: Optional[str] = None
execution_span_id: Optional[str] = None
extra: Dict[str, Any] = field(default_factory=dict)
def get_trace_id(self) -> str:
"""
Get the effective trace ID.
Priority:
1. OTel current span trace_id
2. Execution context trace_id
3. Empty string
Returns:
The effective trace ID, or empty string if not available.
"""
# 1. Check OTel current span
if otel_trace is not None and format_trace_id is not None:
try:
current_span = otel_trace.get_current_span()
if current_span is not None and hasattr(current_span, "context"):
span_context = current_span.get_span_context()
if span_context.is_valid:
return format_trace_id(span_context.trace_id)
except Exception:
pass
# 2. Check execution context
if self.execution_trace_id:
return self.execution_trace_id
# 3. Return empty string
return ""
def get_span_id(self) -> str:
"""
Get the effective span ID.
Priority:
1. OTel current span span_id
2. Execution context span_id
3. Empty string
Returns:
The effective span ID, or empty string if not available.
"""
# 1. Check OTel current span
if otel_trace is not None and format_span_id is not None:
try:
current_span = otel_trace.get_current_span()
if current_span is not None and hasattr(current_span, "context"):
span_context = current_span.get_span_context()
if span_context.is_valid:
return format_span_id(span_context.span_id)
except Exception:
pass
# 2. Check execution context
if self.execution_span_id:
return self.execution_span_id
# 3. Return empty string
return ""
def to_log_fields(self) -> Dict[str, Any]:
"""
Convert the context to log fields.
Returns:
A dictionary of log fields including trace_id, span_id, and other context fields.
"""
fields: Dict[str, Any] = {
"trace_id": self.get_trace_id(),
"span_id": self.get_span_id(),
}
# Add root context fields
if self.root is not None:
root_fields = self.root.to_log_fields()
fields.update(root_fields)
# Add operation context fields
if self.operation is not None:
operation_fields = self.operation.to_log_fields()
fields.update(operation_fields)
# Add extra fields
fields.update(self.extra)
return fields
def bind_root(self, root_attrs: RootSpanAttributes) -> "ObservabilityContext":
"""
Bind root context.
Args:
root_attrs: The root span attributes to bind.
Returns:
Self for method chaining.
"""
self.root = root_attrs
return self
def bind_operation(self, operation_attrs: OperationSpanAttributes) -> "ObservabilityContext":
"""
Bind operation context.
Args:
operation_attrs: The operation span attributes to bind.
Returns:
Self for method chaining.
"""
self.operation = operation_attrs
return self
def bind_execution(
self,
trace_id: Optional[str] = None,
span_id: Optional[str] = None,
) -> "ObservabilityContext":
"""
Bind execution context for non-request scenarios.
Args:
trace_id: Optional trace ID. Generated automatically if not provided.
span_id: Optional span ID. Derived from trace_id if not provided.
Returns:
Self for method chaining.
"""
self.execution_trace_id = trace_id or uuid.uuid4().hex
self.execution_span_id = span_id or self.execution_trace_id[:16]
return self
# Global context variable
_OBSERVABILITY_CONTEXT: contextvars.ContextVar[Optional[ObservabilityContext]] = (
contextvars.ContextVar(
"openviking_observability_context",
default=None,
)
)
def get_observability_context() -> ObservabilityContext:
"""
Get the current observability context.
If no context is bound, a new empty context is created.
Returns:
The current observability context.
"""
ctx = _OBSERVABILITY_CONTEXT.get()
if ctx is None:
ctx = ObservabilityContext()
_OBSERVABILITY_CONTEXT.set(ctx)
return ctx
def set_observability_context(ctx: ObservabilityContext) -> contextvars.Token:
"""
Set the observability context.
Args:
ctx: The observability context to set.
Returns:
A token that can be used to reset the context.
"""
return _OBSERVABILITY_CONTEXT.set(ctx)
def reset_observability_context(token: contextvars.Token) -> None:
"""
Reset the observability context to a previous state.
Args:
token: The token returned by set_observability_context.
"""
_OBSERVABILITY_CONTEXT.reset(token)
def _clone_observability_context(ctx: Optional[ObservabilityContext]) -> ObservabilityContext:
"""
Clone the current observability context.
Note:
ContextVars restore by object reference. If we mutate the existing context
in-place and then call `set()`, a later `reset(token)` would restore the
same mutated object, breaking the expected reset semantics.
"""
if ctx is None:
return ObservabilityContext()
# Shallow copy: span attribute objects are treated as immutable snapshots.
return ObservabilityContext(
root=ctx.root,
operation=ctx.operation,
execution_trace_id=ctx.execution_trace_id,
execution_span_id=ctx.execution_span_id,
extra=dict(ctx.extra),
)
def bind_root_context(root_attrs: RootSpanAttributes) -> contextvars.Token:
"""
Bind root context for HTTP requests.
Args:
root_attrs: The root span attributes to bind.
Returns:
A token that can be used to reset the context.
"""
current = _OBSERVABILITY_CONTEXT.get()
ctx = _clone_observability_context(current)
ctx.bind_root(root_attrs)
return set_observability_context(ctx)
def bind_operation_context(operation_attrs: OperationSpanAttributes) -> contextvars.Token:
"""
Bind operation context for business operations.
Args:
operation_attrs: The operation span attributes to bind.
Returns:
A token that can be used to reset the context.
"""
current = _OBSERVABILITY_CONTEXT.get()
ctx = _clone_observability_context(current)
ctx.bind_operation(operation_attrs)
return set_observability_context(ctx)
@contextmanager
def bind_execution_context(
trace_id: Optional[str] = None,
span_id: Optional[str] = None,
):
"""
Context manager for binding execution context in non-request scenarios.
This is useful for:
- Process startup
- CLI execution
- Background tasks
- Any code that doesn't run under an HTTP request
Args:
trace_id: Optional trace ID. Generated automatically if not provided.
span_id: Optional span ID. Derived from trace_id if not provided.
Yields:
A tuple of (trace_id, span_id).
"""
# Create new context with execution trace/span IDs
ctx = ObservabilityContext()
ctx.bind_execution(trace_id, span_id)
token = set_observability_context(ctx)
try:
yield ctx.execution_trace_id, ctx.execution_span_id
finally:
reset_observability_context(token)
# Convenience aliases for unified context management
bind_root_observability_context = bind_root_context
reset_root_observability_context = reset_observability_context
bind_operation_observability_context = bind_operation_context
def get_root_observability_context() -> Optional[RootSpanAttributes]:
"""Get the root context from the unified observability context."""
ctx = get_observability_context()
return ctx.root
def get_operation_observability_context() -> Optional[OperationSpanAttributes]:
"""Get the operation context from the unified observability context."""
ctx = get_observability_context()
return ctx.operation
def reset_operation_observability_context(token: contextvars.Token) -> None:
"""Reset the observability context."""
reset_observability_context(token)