Files
2026-07-13 13:22:34 +08:00

181 lines
6.6 KiB
Python

from unittest import mock
import pytest
import mlflow
import mlflow.tracking.context.default_context
from mlflow.entities.span import LiveSpan
from mlflow.entities.trace_location import TraceLocationType, UCSchemaLocation
from mlflow.entities.trace_state import TraceState
from mlflow.environment_variables import MLFLOW_TRACKING_USERNAME
from mlflow.exceptions import MlflowException
from mlflow.tracing.constant import TraceMetadataKey
from mlflow.tracing.processor.uc_table import DatabricksUCTableSpanProcessor
from mlflow.tracing.provider import _MLFLOW_TRACE_USER_DESTINATION
from mlflow.tracing.trace_manager import InMemoryTraceManager
from tests.tracing.helper import (
create_mock_otel_span,
create_test_trace_info,
)
@pytest.fixture
def active_uc_schema_destination():
destination = UCSchemaLocation(catalog_name="catalog1", schema_name="schema1")
destination._otel_spans_table_name = "spans_table"
_MLFLOW_TRACE_USER_DESTINATION.set(destination)
try:
yield
finally:
_MLFLOW_TRACE_USER_DESTINATION.reset()
def test_on_start_with_uc_table_name(monkeypatch, active_uc_schema_destination):
monkeypatch.setattr(mlflow.tracking.context.default_context, "_get_source_name", lambda: "test")
monkeypatch.setenv(MLFLOW_TRACKING_USERNAME.name, "alice")
# Root span should create a new trace on start
trace_id = 12345
span = create_mock_otel_span(trace_id=trace_id, span_id=1, parent_id=None, start_time=5_000_000)
processor = DatabricksUCTableSpanProcessor(span_exporter=mock.MagicMock())
processor.on_start(span)
# Check that trace was created in trace manager
trace_manager = InMemoryTraceManager.get_instance()
traces = trace_manager._traces
assert len(traces) == 1
# Get the created trace
created_trace = list(traces.values())[0]
trace_info = created_trace.info
# Verify trace location is UC_SCHEMA type
assert trace_info.trace_location.type == TraceLocationType.UC_SCHEMA
uc_schema = trace_info.trace_location.uc_schema
assert uc_schema.catalog_name == "catalog1"
assert uc_schema.schema_name == "schema1"
# Verify trace state and timing
assert trace_info.state == TraceState.IN_PROGRESS
assert trace_info.request_time == 5 # 5_000_000 nanoseconds -> 5 milliseconds
assert trace_info.execution_duration is None
def test_on_start_without_uc_table_name(monkeypatch):
monkeypatch.setattr(mlflow.tracking.context.default_context, "_get_source_name", lambda: "test")
monkeypatch.setenv(MLFLOW_TRACKING_USERNAME.name, "alice")
# Root span should create a new trace on start
trace_id = 12345
span = create_mock_otel_span(trace_id=trace_id, span_id=1, parent_id=None, start_time=5_000_000)
_MLFLOW_TRACE_USER_DESTINATION.reset()
processor = DatabricksUCTableSpanProcessor(span_exporter=mock.MagicMock())
with pytest.raises(MlflowException, match="Unity Catalog spans table name is not set"):
processor.on_start(span)
# Check that trace was still created in trace manager
trace_manager = InMemoryTraceManager.get_instance()
traces = trace_manager._traces
assert len(traces) == 0
def test_constructor_disables_metrics_export():
mock_exporter = mock.MagicMock()
processor = DatabricksUCTableSpanProcessor(span_exporter=mock_exporter)
# The export_metrics should be False
assert not processor._export_metrics
def test_trace_id_generation_with_uc_schema(active_uc_schema_destination):
trace_id = 12345
span = create_mock_otel_span(trace_id=trace_id, span_id=1, parent_id=None, start_time=5_000_000)
with mock.patch(
"mlflow.tracing.processor.uc_table.generate_trace_id_v4",
return_value="trace:/catalog1.schema1/12345",
) as mock_generate_trace_id:
processor = DatabricksUCTableSpanProcessor(span_exporter=mock.MagicMock())
processor.on_start(span)
# Verify generate_trace_id_v4 was called with correct arguments
mock_generate_trace_id.assert_called_once_with(span, "catalog1.schema1")
def test_on_end():
trace_info = create_test_trace_info("request_id", 0)
trace_manager = InMemoryTraceManager.get_instance()
trace_manager.register_trace("trace_id", trace_info)
otel_span = create_mock_otel_span(
name="foo",
trace_id="trace_id",
span_id=1,
parent_id=None,
start_time=5_000_000,
end_time=9_000_000,
)
span = LiveSpan(otel_span, "request_id")
span.set_status("OK")
span.set_inputs({"input1": "test input"})
span.set_outputs({"output": "test output"})
mock_exporter = mock.MagicMock()
processor = DatabricksUCTableSpanProcessor(span_exporter=mock_exporter)
processor.on_end(otel_span)
# Verify span was exported
mock_exporter.export.assert_called_once_with((otel_span,))
def test_on_end_sets_user_session_span_attributes():
trace_manager = InMemoryTraceManager.get_instance()
with mock.patch.object(trace_manager, "pop_trace", return_value=None):
with mlflow.start_span("foo") as live_span:
mlflow.update_current_trace(
metadata={
TraceMetadataKey.TRACE_USER: "alice",
TraceMetadataKey.TRACE_SESSION: "sess-123",
}
)
otel_span = live_span._span
processor = DatabricksUCTableSpanProcessor(span_exporter=mock.MagicMock())
processor.on_end(otel_span)
assert otel_span.attributes["user.id"] == "alice"
assert otel_span.attributes["session.id"] == "sess-123"
def test_on_end_does_not_set_user_session_attributes_when_missing():
trace_manager = InMemoryTraceManager.get_instance()
with mock.patch.object(trace_manager, "pop_trace", return_value=None):
with mlflow.start_span("foo") as live_span:
otel_span = live_span._span
processor = DatabricksUCTableSpanProcessor(span_exporter=mock.MagicMock())
processor.on_end(otel_span)
assert "user.id" not in otel_span.attributes
assert "session.id" not in otel_span.attributes
def test_trace_metadata_and_tags(active_uc_schema_destination):
trace_id = 12345
span = create_mock_otel_span(trace_id=trace_id, span_id=1, parent_id=None, start_time=5_000_000)
processor = DatabricksUCTableSpanProcessor(span_exporter=mock.MagicMock())
processor.on_start(span)
# Get the created trace
trace_manager = InMemoryTraceManager.get_instance()
traces = trace_manager._traces
created_trace = list(traces.values())[0]
trace_info = created_trace.info
# Check that metadata and tags are present
assert trace_info.trace_metadata is not None
assert trace_info.tags is not None