mirror of
https://github.com/volcengine/OpenViking.git
synced 2026-10-01 01:38:07 +08:00
Delete dead compatibility paths and test-only helpers so unsupported APIs do not remain as accidental contracts.
352 lines
10 KiB
Python
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)
|