664 lines
24 KiB
Python
664 lines
24 KiB
Python
import uuid
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
from opentelemetry import trace as otel_trace
|
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
from opentelemetry.sdk.resources import Resource as OTelSDKResource
|
|
from opentelemetry.sdk.trace import TracerProvider
|
|
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
from opentelemetry.trace import Status, StatusCode
|
|
from opentelemetry.util._once import Once
|
|
|
|
import mlflow
|
|
from mlflow.entities import SpanStatusCode
|
|
from mlflow.entities.assessment import AssessmentSource, Expectation, Feedback
|
|
from mlflow.entities.assessment_source import AssessmentSourceType
|
|
from mlflow.server import handlers
|
|
from mlflow.server.fastapi_app import app
|
|
from mlflow.server.handlers import initialize_backend_stores
|
|
from mlflow.tracing.constant import SpanAttributeKey, SpansLocation, TraceTagKey
|
|
from mlflow.tracing.otel.translation.base import OtelSchemaTranslator
|
|
from mlflow.tracing.otel.translation.genai_semconv import GenAiTranslator
|
|
from mlflow.tracing.otel.translation.open_inference import OpenInferenceTranslator
|
|
from mlflow.tracing.otel.translation.traceloop import TraceloopTranslator
|
|
from mlflow.tracing.provider import _get_trace_exporter
|
|
from mlflow.tracing.utils import encode_trace_id
|
|
from mlflow.tracing.utils.otlp import MLFLOW_EXPERIMENT_ID_HEADER
|
|
from mlflow.tracking._tracking_service.utils import _use_tracking_uri
|
|
from mlflow.version import IS_TRACING_SDK_ONLY
|
|
|
|
from tests.helper_functions import get_safe_port
|
|
from tests.tracking.integration_test_utils import ServerThread
|
|
|
|
if IS_TRACING_SDK_ONLY:
|
|
pytest.skip("OTel get_trace tests require full MLflow server", allow_module_level=True)
|
|
|
|
|
|
@pytest.fixture
|
|
def mlflow_server(tmp_path: Path, db_uri: str):
|
|
artifact_uri = tmp_path.joinpath("artifacts").as_uri()
|
|
|
|
# Force-reset backend stores before each test
|
|
handlers._tracking_store = None
|
|
handlers._model_registry_store = None
|
|
initialize_backend_stores(db_uri, default_artifact_root=artifact_uri)
|
|
|
|
with ServerThread(app, get_safe_port()) as url:
|
|
yield url
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def tracking_uri_setup(mlflow_server):
|
|
with _use_tracking_uri(mlflow_server):
|
|
yield
|
|
|
|
|
|
@pytest.fixture(params=[True, False])
|
|
def is_async(request, monkeypatch):
|
|
monkeypatch.setenv("MLFLOW_ASYNC_TRACE_LOGGING", "true" if request.param else "false")
|
|
|
|
|
|
def _flush_async_logging():
|
|
exporter = _get_trace_exporter()
|
|
assert hasattr(exporter, "_async_queue"), "Async queue is not initialized"
|
|
exporter._async_queue.flush(terminate=True)
|
|
|
|
|
|
def create_tracer(mlflow_server: str, experiment_id: str, service_name: str = "test-service"):
|
|
resource = OTelSDKResource.create({"service.name": service_name, "service.version": "1.0.0"})
|
|
tracer_provider = TracerProvider(resource=resource)
|
|
|
|
exporter = OTLPSpanExporter(
|
|
endpoint=f"{mlflow_server}/v1/traces",
|
|
headers={MLFLOW_EXPERIMENT_ID_HEADER: experiment_id},
|
|
timeout=10,
|
|
)
|
|
|
|
span_processor = SimpleSpanProcessor(exporter)
|
|
tracer_provider.add_span_processor(span_processor)
|
|
|
|
# Reset the global tracer provider
|
|
otel_trace._TRACER_PROVIDER_SET_ONCE = Once()
|
|
otel_trace._TRACER_PROVIDER = None
|
|
otel_trace.set_tracer_provider(tracer_provider)
|
|
|
|
return otel_trace.get_tracer(__name__)
|
|
|
|
|
|
def test_get_trace_for_otel_sent_span(mlflow_server: str, is_async):
|
|
experiment = mlflow.set_experiment("otel-get-trace-test")
|
|
experiment_id = experiment.experiment_id
|
|
|
|
tracer = create_tracer(mlflow_server, experiment_id, "test-service-get-trace")
|
|
|
|
# Create a span with various attributes to test conversion
|
|
with tracer.start_as_current_span("otel-test-span") as span:
|
|
span.set_attribute("test.string", "string-value")
|
|
span.set_attribute("test.number", 42)
|
|
span.set_attribute("test.boolean", True)
|
|
span.set_attribute("operation.type", "llm_request")
|
|
|
|
# Capture the OTel trace ID
|
|
otel_trace_id = span.get_span_context().trace_id
|
|
assert span.get_span_context().is_valid
|
|
assert otel_trace_id != 0
|
|
|
|
if is_async:
|
|
_flush_async_logging()
|
|
|
|
traces = mlflow.search_traces(
|
|
locations=[experiment_id], include_spans=False, return_type="list"
|
|
)
|
|
|
|
assert len(traces) > 0, "No traces found in the database"
|
|
|
|
trace_id = traces[0].info.trace_id
|
|
retrieved_trace = mlflow.get_trace(trace_id)
|
|
|
|
assert retrieved_trace.info.trace_id == trace_id
|
|
assert retrieved_trace.info.trace_location.mlflow_experiment.experiment_id == experiment_id
|
|
|
|
assert len(retrieved_trace.data.spans) == 1
|
|
span = retrieved_trace.data.spans[0]
|
|
|
|
assert span.name == "otel-test-span"
|
|
assert span.trace_id == trace_id
|
|
# OTel spans default to UNSET status if not explicitly set
|
|
assert span.status.status_code == SpanStatusCode.UNSET
|
|
|
|
# Verify attributes were converted correctly
|
|
assert span.attributes["test.string"] == "string-value"
|
|
assert span.attributes["test.number"] == 42
|
|
assert span.attributes["test.boolean"] is True
|
|
assert span.attributes["operation.type"] == "llm_request"
|
|
|
|
# Verify the trace ID matches the expected format
|
|
expected_trace_id = f"tr-{encode_trace_id(otel_trace_id)}"
|
|
assert trace_id == expected_trace_id
|
|
|
|
|
|
def test_rest_client_reads_archived_trace_via_server(mlflow_server: str, tmp_path: Path, is_async):
|
|
experiment = mlflow.set_experiment("otel-archived-trace-rest-test")
|
|
experiment_id = experiment.experiment_id
|
|
|
|
tracer = create_tracer(mlflow_server, experiment_id, "test-service-archive-read")
|
|
|
|
with tracer.start_as_current_span("archived-otel-span") as span:
|
|
span.set_attribute("archived.attribute", "server-owned")
|
|
|
|
if is_async:
|
|
_flush_async_logging()
|
|
|
|
traces = mlflow.search_traces(
|
|
locations=[experiment_id], include_spans=False, return_type="list"
|
|
)
|
|
assert len(traces) == 1, "Expected a single trace to archive in this isolated test"
|
|
|
|
trace_info = traces[0].info
|
|
trace_id = trace_info.trace_id
|
|
|
|
store = handlers._get_tracking_store()
|
|
archive_root = tmp_path.joinpath("archive")
|
|
archive_root.mkdir()
|
|
with mock.patch.object(
|
|
store,
|
|
"_get_archive_traces_now_millis",
|
|
return_value=trace_info.request_time + 2 * 60 * 1000,
|
|
):
|
|
archived = store.archive_traces(
|
|
resolved_trace_archival_location=archive_root.as_uri(),
|
|
broader_retention="1m",
|
|
long_retention_allowlist=None,
|
|
max_traces_per_pass=100,
|
|
)
|
|
|
|
assert archived == 1
|
|
archived_trace_info = store.get_trace_info(trace_id)
|
|
assert archived_trace_info.tags[TraceTagKey.SPANS_LOCATION] == SpansLocation.ARCHIVE_REPO.value
|
|
assert TraceTagKey.ARCHIVE_LOCATION in archived_trace_info.tags
|
|
|
|
client = mlflow.MlflowClient(mlflow_server)
|
|
with (
|
|
mock.patch.object(
|
|
client._tracing_client,
|
|
"_download_trace_data",
|
|
side_effect=AssertionError("client-side archive download should not be used"),
|
|
) as mock_download_trace_data,
|
|
mock.patch.object(
|
|
client._tracing_client,
|
|
"_download_spans_from_artifact_repo",
|
|
side_effect=AssertionError("artifact-repo batch download should not be used"),
|
|
) as mock_download_spans_from_artifact_repo,
|
|
):
|
|
retrieved_trace = client.get_trace(trace_id)
|
|
searched_traces = client.search_traces(locations=[experiment_id], include_spans=True)
|
|
|
|
assert retrieved_trace.info.trace_id == trace_id
|
|
assert len(retrieved_trace.data.spans) == 1
|
|
assert retrieved_trace.data.spans[0].name == "archived-otel-span"
|
|
assert retrieved_trace.data.spans[0].attributes["archived.attribute"] == "server-owned"
|
|
|
|
assert len(searched_traces) == 1
|
|
assert searched_traces[0].info.trace_id == trace_id
|
|
assert len(searched_traces[0].data.spans) == 1
|
|
assert searched_traces[0].data.spans[0].name == "archived-otel-span"
|
|
|
|
mock_download_trace_data.assert_not_called()
|
|
mock_download_spans_from_artifact_repo.assert_not_called()
|
|
|
|
|
|
def test_get_trace_for_otel_nested_spans(mlflow_server: str, is_async):
|
|
experiment = mlflow.set_experiment("otel-nested-spans-test")
|
|
experiment_id = experiment.experiment_id
|
|
|
|
tracer = create_tracer(mlflow_server, experiment_id, "nested-test-service")
|
|
|
|
# Create nested spans
|
|
with tracer.start_as_current_span("parent-span") as parent_span:
|
|
parent_span.set_attribute("span.level", "parent")
|
|
|
|
with tracer.start_as_current_span("child-span") as child_span:
|
|
child_span.set_attribute("span.level", "child")
|
|
child_span.set_attribute("child.operation", "process_data")
|
|
|
|
if is_async:
|
|
_flush_async_logging()
|
|
|
|
traces = mlflow.search_traces(
|
|
locations=[experiment_id], include_spans=False, return_type="list"
|
|
)
|
|
|
|
assert len(traces) > 0, "No traces found in the database"
|
|
|
|
trace_id = traces[0].info.trace_id
|
|
retrieved_trace = mlflow.get_trace(trace_id)
|
|
|
|
assert len(retrieved_trace.data.spans) == 2
|
|
|
|
spans_by_name = {span.name: span for span in retrieved_trace.data.spans}
|
|
|
|
assert "parent-span" in spans_by_name
|
|
assert "child-span" in spans_by_name
|
|
|
|
parent_span = spans_by_name["parent-span"]
|
|
child_span = spans_by_name["child-span"]
|
|
|
|
assert parent_span.attributes["span.level"] == "parent"
|
|
assert parent_span.parent_id is None # Root span has no parent
|
|
|
|
assert child_span.attributes["span.level"] == "child"
|
|
assert child_span.attributes["child.operation"] == "process_data"
|
|
assert child_span.parent_id == parent_span.span_id # Child should reference parent
|
|
|
|
|
|
def test_get_trace_with_otel_span_events(mlflow_server: str, is_async):
|
|
experiment = mlflow.set_experiment("otel-events-test")
|
|
experiment_id = experiment.experiment_id
|
|
|
|
tracer = create_tracer(mlflow_server, experiment_id, "events-test-service")
|
|
|
|
# Create span with events using OTel SDK
|
|
with tracer.start_as_current_span("span-with-events") as span:
|
|
span.add_event("test_event", attributes={"event.type": "processing"})
|
|
|
|
if is_async:
|
|
_flush_async_logging()
|
|
|
|
traces = mlflow.search_traces(
|
|
locations=[experiment_id], include_spans=False, return_type="list"
|
|
)
|
|
|
|
trace_id = traces[0].info.trace_id
|
|
retrieved_trace = mlflow.get_trace(trace_id)
|
|
|
|
assert len(retrieved_trace.data.spans) == 1
|
|
retrieved_span = retrieved_trace.data.spans[0]
|
|
|
|
assert retrieved_span.name == "span-with-events"
|
|
assert len(retrieved_span.events) == 1
|
|
event = retrieved_span.events[0]
|
|
assert event.name == "test_event"
|
|
assert event.attributes["event.type"] == "processing"
|
|
|
|
|
|
def test_get_trace_nonexistent_otel_trace(mlflow_server: str):
|
|
# Create a fake trace ID in OTel format
|
|
fake_otel_trace_id = uuid.uuid4().hex
|
|
fake_trace_id = f"tr-{fake_otel_trace_id}"
|
|
|
|
# MLflow get_trace returns None for non-existent traces
|
|
trace = mlflow.get_trace(fake_trace_id)
|
|
assert trace is None
|
|
|
|
|
|
def test_get_trace_with_otel_span_status(mlflow_server: str, is_async):
|
|
experiment = mlflow.set_experiment("otel-status-test")
|
|
experiment_id = experiment.experiment_id
|
|
|
|
tracer = create_tracer(mlflow_server, experiment_id, "status-test-service")
|
|
|
|
# Create span with error status using OTel SDK
|
|
with tracer.start_as_current_span("error-span") as span:
|
|
span.set_status(Status(StatusCode.ERROR, "Something went wrong"))
|
|
|
|
if is_async:
|
|
_flush_async_logging()
|
|
|
|
traces = mlflow.search_traces(
|
|
locations=[experiment_id], include_spans=False, return_type="list"
|
|
)
|
|
|
|
trace_id = traces[0].info.trace_id
|
|
retrieved_trace = mlflow.get_trace(trace_id)
|
|
|
|
assert len(retrieved_trace.data.spans) == 1
|
|
retrieved_span = retrieved_trace.data.spans[0]
|
|
|
|
assert retrieved_span.name == "error-span"
|
|
assert retrieved_span.status.status_code == SpanStatusCode.ERROR
|
|
assert "Something went wrong" in retrieved_span.status.description
|
|
|
|
|
|
def test_set_trace_tag_on_otel_trace(mlflow_server: str, is_async):
|
|
experiment = mlflow.set_experiment("otel-tag-test")
|
|
experiment_id = experiment.experiment_id
|
|
|
|
tracer = create_tracer(mlflow_server, experiment_id, "tag-test-service")
|
|
|
|
with tracer.start_as_current_span("tagged-span") as span:
|
|
span.set_attribute("test.attribute", "value")
|
|
|
|
if is_async:
|
|
_flush_async_logging()
|
|
|
|
traces = mlflow.search_traces(
|
|
locations=[experiment_id], include_spans=False, return_type="list"
|
|
)
|
|
trace_id = traces[0].info.trace_id
|
|
|
|
mlflow.set_trace_tag(trace_id, "environment", "test")
|
|
mlflow.set_trace_tag(trace_id, "version", "1.0.0")
|
|
|
|
retrieved_trace = mlflow.get_trace(trace_id)
|
|
assert retrieved_trace.info.tags["environment"] == "test"
|
|
assert retrieved_trace.info.tags["version"] == "1.0.0"
|
|
|
|
|
|
def test_log_expectation_on_otel_trace(mlflow_server: str, is_async):
|
|
experiment = mlflow.set_experiment("otel-expectation-test")
|
|
experiment_id = experiment.experiment_id
|
|
|
|
tracer = create_tracer(mlflow_server, experiment_id, "expectation-test-service")
|
|
|
|
# Create a span that represents a question-answer scenario
|
|
with tracer.start_as_current_span("qa-span") as span:
|
|
span.set_attribute("question", "What is MLflow?")
|
|
span.set_attribute("answer", "MLflow is an open-source ML platform")
|
|
|
|
if is_async:
|
|
_flush_async_logging()
|
|
|
|
traces = mlflow.search_traces(
|
|
locations=[experiment_id], include_spans=False, return_type="list"
|
|
)
|
|
trace_id = traces[0].info.trace_id
|
|
|
|
expectation_source = AssessmentSource(
|
|
source_type=AssessmentSourceType.HUMAN, source_id="test_user@example.com"
|
|
)
|
|
|
|
logged_assessment = mlflow.log_expectation(
|
|
trace_id=trace_id,
|
|
name="expected_answer",
|
|
value="MLflow is an open-source machine learning platform",
|
|
source=expectation_source,
|
|
metadata={"confidence": "high", "reviewed_by": "expert"},
|
|
)
|
|
expectation = mlflow.get_assessment(
|
|
trace_id=trace_id, assessment_id=logged_assessment.assessment_id
|
|
)
|
|
assert expectation.name == "expected_answer"
|
|
assert expectation.value == "MLflow is an open-source machine learning platform"
|
|
assert expectation.source.source_type == AssessmentSourceType.HUMAN
|
|
assert expectation.metadata["confidence"] == "high"
|
|
|
|
|
|
def test_log_feedback_on_otel_trace(mlflow_server: str, is_async):
|
|
experiment = mlflow.set_experiment("otel-feedback-test")
|
|
experiment_id = experiment.experiment_id
|
|
|
|
tracer = create_tracer(mlflow_server, experiment_id, "feedback-test-service")
|
|
|
|
# Create a span representing a model prediction
|
|
with tracer.start_as_current_span("prediction-span") as span:
|
|
span.set_attribute("model", "gpt-4")
|
|
span.set_attribute("prediction", "The weather is sunny")
|
|
|
|
if is_async:
|
|
_flush_async_logging()
|
|
|
|
traces = mlflow.search_traces(
|
|
locations=[experiment_id], include_spans=False, return_type="list"
|
|
)
|
|
assert len(traces) > 0, "No traces found in the database"
|
|
trace_id = traces[0].info.trace_id
|
|
|
|
llm_source = AssessmentSource(
|
|
source_type=AssessmentSourceType.LLM_JUDGE, source_id="gpt-4o-mini"
|
|
)
|
|
|
|
logged_quality = mlflow.log_feedback(
|
|
trace_id=trace_id,
|
|
name="quality_score",
|
|
value=8.5,
|
|
source=llm_source,
|
|
metadata={"scale": "1-10", "criterion": "accuracy"},
|
|
)
|
|
feedback = mlflow.get_assessment(trace_id=trace_id, assessment_id=logged_quality.assessment_id)
|
|
assert feedback.name == "quality_score"
|
|
assert feedback.value == 8.5
|
|
assert feedback.source.source_type == AssessmentSourceType.LLM_JUDGE
|
|
|
|
human_source = AssessmentSource(
|
|
source_type=AssessmentSourceType.HUMAN, source_id="reviewer@example.com"
|
|
)
|
|
|
|
logged_approval = mlflow.log_feedback(
|
|
trace_id=trace_id,
|
|
name="approved",
|
|
value=True,
|
|
source=human_source,
|
|
metadata={"review_date": "2024-01-15"},
|
|
)
|
|
feedback = mlflow.get_assessment(trace_id=trace_id, assessment_id=logged_approval.assessment_id)
|
|
assert feedback.name == "approved"
|
|
assert feedback.value is True
|
|
assert feedback.source.source_type == AssessmentSourceType.HUMAN
|
|
|
|
|
|
def test_multiple_assessments_on_otel_trace(mlflow_server: str, is_async):
|
|
experiment = mlflow.set_experiment("otel-multi-assessment-test")
|
|
experiment_id = experiment.experiment_id
|
|
|
|
tracer = create_tracer(mlflow_server, experiment_id, "multi-assessment-test-service")
|
|
|
|
# Create a complex trace with nested spans
|
|
with tracer.start_as_current_span("conversation") as parent_span:
|
|
parent_span.set_attribute("user_query", "Explain quantum computing")
|
|
|
|
with tracer.start_as_current_span("retrieval") as retrieval_span:
|
|
retrieval_span.set_attribute("documents_found", 5)
|
|
|
|
with tracer.start_as_current_span("generation") as generation_span:
|
|
generation_span.set_attribute("model", "gpt-4")
|
|
generation_span.set_attribute("response", "Quantum computing uses quantum mechanics...")
|
|
|
|
if is_async:
|
|
_flush_async_logging()
|
|
|
|
traces = mlflow.search_traces(
|
|
locations=[experiment_id], include_spans=False, return_type="list"
|
|
)
|
|
trace_id = traces[0].info.trace_id
|
|
|
|
mlflow.set_trace_tag(trace_id, "topic", "quantum_computing")
|
|
mlflow.set_trace_tag(trace_id, "complexity", "high")
|
|
|
|
human_source = AssessmentSource(AssessmentSourceType.HUMAN, "expert@physics.edu")
|
|
llm_source = AssessmentSource(AssessmentSourceType.LLM_JUDGE, "claude-3")
|
|
|
|
expectation = Expectation(
|
|
name="expected_quality",
|
|
value="Should explain quantum superposition and entanglement",
|
|
source=human_source,
|
|
)
|
|
mlflow.log_assessment(trace_id=trace_id, assessment=expectation)
|
|
feedback_items = [
|
|
Feedback(name="accuracy", value=9.0, source=llm_source, metadata={"max_score": "10"}),
|
|
Feedback(name="clarity", value=8.5, source=llm_source, metadata={"max_score": "10"}),
|
|
Feedback(
|
|
name="helpfulness",
|
|
value=True,
|
|
source=human_source,
|
|
metadata={"reviewer_expertise": "quantum_physics"},
|
|
),
|
|
Feedback(
|
|
name="contains_errors",
|
|
value=False,
|
|
source=human_source,
|
|
metadata={"fact_checked": "True"},
|
|
),
|
|
]
|
|
|
|
for feedback in feedback_items:
|
|
mlflow.log_assessment(trace_id=trace_id, assessment=feedback)
|
|
|
|
retrieved_trace = mlflow.get_trace(trace_id)
|
|
assessments = retrieved_trace.info.assessments
|
|
assert len(assessments) == 5
|
|
assert [a.name for a in assessments] == [
|
|
"expected_quality",
|
|
"accuracy",
|
|
"clarity",
|
|
"helpfulness",
|
|
"contains_errors",
|
|
]
|
|
|
|
assert retrieved_trace.info.tags["topic"] == "quantum_computing"
|
|
assert retrieved_trace.info.tags["complexity"] == "high"
|
|
|
|
assert len(retrieved_trace.data.spans) == 3
|
|
span_names = {span.name for span in retrieved_trace.data.spans}
|
|
assert span_names == {"conversation", "retrieval", "generation"}
|
|
|
|
tagged_traces = mlflow.search_traces(
|
|
locations=[experiment_id],
|
|
filter_string='tags.topic = "quantum_computing"',
|
|
return_type="list",
|
|
)
|
|
assert len(tagged_traces) == 1
|
|
assert tagged_traces[0].info.trace_id == trace_id
|
|
|
|
|
|
def test_span_kind_translation(mlflow_server: str, is_async):
|
|
experiment = mlflow.set_experiment("span-kind-translation-test")
|
|
experiment_id = experiment.experiment_id
|
|
|
|
tracer = create_tracer(mlflow_server, experiment_id, "span-kind-translation-test-service")
|
|
|
|
with tracer.start_as_current_span("llm-call") as span:
|
|
span.set_attribute(OpenInferenceTranslator.SPAN_KIND_ATTRIBUTE_KEY, "LLM")
|
|
|
|
with tracer.start_as_current_span("retriever-call") as span:
|
|
span.set_attribute(OpenInferenceTranslator.SPAN_KIND_ATTRIBUTE_KEY, "RETRIEVER")
|
|
|
|
with tracer.start_as_current_span("tool-call") as span:
|
|
span.set_attribute(TraceloopTranslator.SPAN_KIND_ATTRIBUTE_KEY, "tool")
|
|
|
|
if is_async:
|
|
_flush_async_logging()
|
|
|
|
traces = mlflow.search_traces(
|
|
locations=[experiment_id], include_spans=False, return_type="list"
|
|
)
|
|
|
|
assert len(traces) == 3
|
|
for trace_info in traces:
|
|
retrieved_trace = mlflow.get_trace(trace_info.info.trace_id)
|
|
for span in retrieved_trace.data.spans:
|
|
if span.name == "llm-call":
|
|
assert span.span_type == "LLM"
|
|
elif span.name == "retriever-call":
|
|
assert span.span_type == "RETRIEVER"
|
|
elif span.name == "tool-call":
|
|
assert span.span_type == "TOOL"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"translator", [GenAiTranslator, OpenInferenceTranslator, TraceloopTranslator]
|
|
)
|
|
def test_span_inputs_outputs_translation(
|
|
mlflow_server: str, is_async, translator: OtelSchemaTranslator
|
|
):
|
|
experiment = mlflow.set_experiment("span-inputs-outputs-translation-test")
|
|
experiment_id = experiment.experiment_id
|
|
|
|
tracer = create_tracer(
|
|
mlflow_server, experiment_id, "span-inputs-outputs-translation-test-service"
|
|
)
|
|
|
|
with tracer.start_as_current_span("llm-call") as span:
|
|
span.set_attribute(translator.INPUT_VALUE_KEYS[0], "Hello, world!")
|
|
span.set_attribute(translator.OUTPUT_VALUE_KEYS[0], "Bye!")
|
|
|
|
if is_async:
|
|
_flush_async_logging()
|
|
|
|
traces = mlflow.search_traces(
|
|
locations=[experiment_id], include_spans=False, return_type="list"
|
|
)
|
|
assert len(traces) == 1
|
|
retrieved_trace = mlflow.get_trace(traces[0].info.trace_id)
|
|
assert retrieved_trace.data.spans[0].inputs == "Hello, world!"
|
|
assert retrieved_trace.data.spans[0].outputs == "Bye!"
|
|
assert retrieved_trace.info.request_preview == '"Hello, world!"'
|
|
assert retrieved_trace.info.response_preview == '"Bye!"'
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"translator", [GenAiTranslator, OpenInferenceTranslator, TraceloopTranslator]
|
|
)
|
|
def test_span_token_usage_translation(
|
|
mlflow_server: str, is_async, translator: OtelSchemaTranslator
|
|
):
|
|
experiment = mlflow.set_experiment("span-token-usage-translation-test")
|
|
experiment_id = experiment.experiment_id
|
|
|
|
tracer = create_tracer(
|
|
mlflow_server, experiment_id, "span-token-usage-translation-test-service"
|
|
)
|
|
|
|
with tracer.start_as_current_span("llm-call") as span:
|
|
span.set_attribute(translator.INPUT_TOKEN_KEY, 100)
|
|
span.set_attribute(translator.OUTPUT_TOKEN_KEY, 50)
|
|
|
|
if is_async:
|
|
_flush_async_logging()
|
|
|
|
traces = mlflow.search_traces(
|
|
locations=[experiment_id], include_spans=False, return_type="list"
|
|
)
|
|
assert len(traces) > 0
|
|
for trace_info in traces:
|
|
assert trace_info.info.token_usage == {
|
|
"input_tokens": 100,
|
|
"output_tokens": 50,
|
|
"total_tokens": 150,
|
|
}
|
|
retrieved_trace = mlflow.get_trace(trace_info.info.trace_id)
|
|
assert (
|
|
retrieved_trace.data.spans[0].attributes[SpanAttributeKey.CHAT_USAGE]
|
|
== trace_info.info.token_usage
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"translator", [GenAiTranslator, OpenInferenceTranslator, TraceloopTranslator]
|
|
)
|
|
def test_aggregated_token_usage_from_multiple_spans(
|
|
mlflow_server: str, is_async, translator: OtelSchemaTranslator
|
|
):
|
|
experiment = mlflow.set_experiment("aggregated-token-usage-test")
|
|
experiment_id = experiment.experiment_id
|
|
|
|
tracer = create_tracer(mlflow_server, experiment_id, "token-aggregation-service")
|
|
|
|
with tracer.start_as_current_span("parent-llm-call") as parent:
|
|
parent.set_attribute(translator.INPUT_TOKEN_KEY, 100)
|
|
parent.set_attribute(translator.OUTPUT_TOKEN_KEY, 50)
|
|
|
|
with tracer.start_as_current_span("child-llm-call-1") as child1:
|
|
child1.set_attribute(translator.INPUT_TOKEN_KEY, 200)
|
|
child1.set_attribute(translator.OUTPUT_TOKEN_KEY, 75)
|
|
|
|
with tracer.start_as_current_span("child-llm-call-2") as child2:
|
|
child2.set_attribute(translator.INPUT_TOKEN_KEY, 150)
|
|
child2.set_attribute(translator.OUTPUT_TOKEN_KEY, 100)
|
|
|
|
if is_async:
|
|
_flush_async_logging()
|
|
|
|
traces = mlflow.search_traces(
|
|
locations=[experiment_id], include_spans=False, return_type="list"
|
|
)
|
|
|
|
trace_id = traces[0].info.trace_id
|
|
retrieved_trace = mlflow.get_trace(trace_id)
|
|
|
|
assert retrieved_trace.info.token_usage is not None
|
|
assert retrieved_trace.info.token_usage["input_tokens"] == 450
|
|
assert retrieved_trace.info.token_usage["output_tokens"] == 225
|
|
assert retrieved_trace.info.token_usage["total_tokens"] == 675
|