153 lines
6.9 KiB
Python
153 lines
6.9 KiB
Python
import logging
|
|
import threading
|
|
|
|
from opentelemetry import trace as otel_trace
|
|
from opentelemetry.context import Context
|
|
from opentelemetry.sdk.trace import ReadableSpan as OTelReadableSpan
|
|
from opentelemetry.sdk.trace import Span as OTelSpan
|
|
from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider
|
|
from opentelemetry.sdk.trace.export import SimpleSpanProcessor, SpanExporter
|
|
from opentelemetry.trace import (
|
|
NoOpTracerProvider,
|
|
ProxyTracerProvider,
|
|
get_tracer_provider,
|
|
set_tracer_provider,
|
|
)
|
|
|
|
from mlflow.entities.span import create_mlflow_span
|
|
from mlflow.semantic_kernel.tracing_utils import set_model, set_span_type, set_token_usage
|
|
from mlflow.tracing.constant import SpanAttributeKey
|
|
from mlflow.tracing.provider import _get_tracer, mlflow_runtime_context
|
|
from mlflow.tracing.trace_manager import InMemoryTraceManager
|
|
from mlflow.tracing.utils import (
|
|
_bypass_attribute_guard,
|
|
get_mlflow_span_for_otel_span,
|
|
get_otel_attribute,
|
|
set_span_cost_attribute,
|
|
should_compute_cost_client_side,
|
|
)
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _enable_experimental_genai_tracing():
|
|
# NB: These settings are required to enable the telemetry for genai attributes
|
|
# such as chat completion inputs/outputs, which are currently marked as experimental.
|
|
# We directly update the singleton setting object instead of using env vars,
|
|
# because the object might be already initialized by the time we call this function.
|
|
# https://learn.microsoft.com/en-us/semantic-kernel/concepts/enterprise-readiness/observability/telemetry-with-console
|
|
from semantic_kernel.utils.telemetry.model_diagnostics.decorators import (
|
|
MODEL_DIAGNOSTICS_SETTINGS,
|
|
)
|
|
|
|
MODEL_DIAGNOSTICS_SETTINGS.enable_otel_diagnostics = True
|
|
MODEL_DIAGNOSTICS_SETTINGS.enable_otel_diagnostics_sensitive = True
|
|
|
|
try:
|
|
# This only exists in Semantic Kernel 1.35.1 or later.
|
|
from semantic_kernel.utils.telemetry.agent_diagnostics.decorators import (
|
|
MODEL_DIAGNOSTICS_SETTINGS as AGENT_DIAGNOSTICS_SETTINGS,
|
|
)
|
|
|
|
AGENT_DIAGNOSTICS_SETTINGS.enable_otel_diagnostics = True
|
|
AGENT_DIAGNOSTICS_SETTINGS.enable_otel_diagnostics_sensitive = True
|
|
except ImportError:
|
|
pass
|
|
|
|
_logger.info("Semantic Kernel Otel diagnostics is turned on for enabling tracing.")
|
|
|
|
|
|
def setup_semantic_kernel_tracing():
|
|
_enable_experimental_genai_tracing()
|
|
|
|
# NB: This logic has a known issue that it does not work when Semantic Kernel program is
|
|
# executed before calling this setup is called. This is because Semantic Kernel caches the
|
|
# tracer instance in each module (ref:https://github.com/microsoft/semantic-kernel/blob/6ecf2b9c2c893dc6da97abeb5962dfc49bed062d/python/semantic_kernel/functions/kernel_function.py#L46),
|
|
# which prevent us from updating the span processor setup for the tracer.
|
|
# Therefore, `mlflow.semantic_kernel.autolog()` should always be called before running the
|
|
# Semantic Kernel program.
|
|
|
|
provider = get_tracer_provider()
|
|
sk_processor = SemanticKernelSpanProcessor()
|
|
if isinstance(provider, (NoOpTracerProvider, ProxyTracerProvider)):
|
|
new_provider = SDKTracerProvider()
|
|
new_provider.add_span_processor(sk_processor)
|
|
set_tracer_provider(new_provider)
|
|
else:
|
|
if not any(
|
|
isinstance(p, SemanticKernelSpanProcessor)
|
|
for p in provider._active_span_processor._span_processors
|
|
):
|
|
provider.add_span_processor(sk_processor)
|
|
|
|
|
|
class SemanticKernelSpanProcessor(SimpleSpanProcessor):
|
|
def __init__(self):
|
|
# NB: Dummy NoOp exporter, because OTel span processor requires an exporter
|
|
self.span_exporter = SpanExporter()
|
|
# Store context tokens for each span so we can detach them in on_end
|
|
self._context_tokens: dict[int, object] = {}
|
|
self._processing_local = threading.local()
|
|
|
|
def on_start(self, span: OTelSpan, parent_context: Context | None = None):
|
|
# Recursion guard: with MLFLOW_USE_DEFAULT_TRACER_PROVIDER=false (shared provider),
|
|
# tracer.span_processor.on_start() routes back through the same composite processor,
|
|
# re-entering this method and causing infinite recursion.
|
|
if getattr(self._processing_local, "in_on_start", False):
|
|
return
|
|
self._processing_local.in_on_start = True
|
|
try:
|
|
# Trigger MLflow's span processor
|
|
tracer = _get_tracer(__name__)
|
|
tracer.span_processor.on_start(span, parent_context)
|
|
|
|
trace_id = get_otel_attribute(span, SpanAttributeKey.REQUEST_ID)
|
|
mlflow_span = create_mlflow_span(span, trace_id)
|
|
|
|
# Register new span in the in-memory trace manager
|
|
InMemoryTraceManager.get_instance().register_span(mlflow_span)
|
|
|
|
# Also set this span in MLflow's runtime context so that other autolog integrations
|
|
# (like OpenAI) can correctly parent their spans to Semantic Kernel spans.
|
|
# NB: We use otel_trace.set_span_in_context() directly instead of
|
|
# mlflow.tracing.provider.set_span_in_context() because the latter can produce
|
|
# two separate traces when MLFLOW_USE_DEFAULT_TRACER_PROVIDER is set to False.
|
|
# Using the OpenTelemetry API directly ensures consistent behavior for autologging.
|
|
context = otel_trace.set_span_in_context(span)
|
|
token = mlflow_runtime_context.attach(context)
|
|
self._context_tokens[span.context.span_id] = token
|
|
finally:
|
|
self._processing_local.in_on_start = False
|
|
|
|
def on_end(self, span: OTelReadableSpan) -> None:
|
|
# Recursion guard: with MLFLOW_USE_DEFAULT_TRACER_PROVIDER=false (shared provider),
|
|
# tracer.span_processor.on_end() routes back through the same composite processor,
|
|
# re-entering this method and causing infinite recursion.
|
|
if getattr(self._processing_local, "in_on_end", False):
|
|
return
|
|
|
|
self._processing_local.in_on_end = True
|
|
try:
|
|
# Detach the span from MLflow's runtime context
|
|
token = self._context_tokens.pop(span.context.span_id, None)
|
|
if token is not None:
|
|
mlflow_runtime_context.detach(token)
|
|
|
|
mlflow_span = get_mlflow_span_for_otel_span(span)
|
|
if mlflow_span is None:
|
|
_logger.debug("Span not found in the map. Skipping end.")
|
|
return
|
|
|
|
with _bypass_attribute_guard(mlflow_span._span):
|
|
set_span_type(mlflow_span)
|
|
set_model(mlflow_span)
|
|
set_token_usage(mlflow_span)
|
|
if should_compute_cost_client_side():
|
|
set_span_cost_attribute(mlflow_span)
|
|
|
|
# Export the span using MLflow's span processor
|
|
tracer = _get_tracer(__name__)
|
|
tracer.span_processor.on_end(span)
|
|
finally:
|
|
self._processing_local.in_on_end = False
|