Files
mlflow--mlflow/mlflow/utils/databricks_tracing_utils.py
2026-07-13 13:22:34 +08:00

292 lines
11 KiB
Python

import logging
from google.protobuf.duration_pb2 import Duration
from google.protobuf.timestamp_pb2 import Timestamp
from mlflow.entities import Assessment, Span, Trace, TraceData, TraceInfo
from mlflow.entities.trace_info_v2 import _truncate_request_metadata, _truncate_tags
from mlflow.entities.trace_location import (
InferenceTableLocation,
MlflowExperimentLocation,
TraceLocation,
TraceLocationType,
UCSchemaLocation,
UnityCatalog,
)
from mlflow.protos import assessments_pb2
from mlflow.protos import databricks_tracing_pb2 as pb
from mlflow.tracing.utils import (
construct_trace_id_v4,
parse_trace_id_v4,
)
_logger = logging.getLogger(__name__)
def parse_uc_location(location: str) -> tuple[str, str, str | None]:
parts = location.split(".")
if len(parts) == 2:
return parts[0], parts[1], None
if len(parts) == 3:
return parts[0], parts[1], parts[2]
raise ValueError(
f"Invalid UC location: {location}. Expected format: <catalog>.<schema>[.<table_prefix>]."
)
def uc_location_to_str(catalog: str, schema: str, table_prefix: str | None = None) -> str:
return f"{catalog}.{schema}.{table_prefix}" if table_prefix else f"{catalog}.{schema}"
def uc_schema_location_to_proto(
uc_schema_location: UCSchemaLocation,
) -> pb.UCSchemaLocation:
return pb.UCSchemaLocation(
catalog_name=uc_schema_location.catalog_name,
schema_name=uc_schema_location.schema_name,
otel_spans_table_name=uc_schema_location._otel_spans_table_name,
otel_logs_table_name=uc_schema_location._otel_logs_table_name,
)
def uc_schema_location_from_proto(
proto: pb.UCSchemaLocation,
) -> UCSchemaLocation:
location = UCSchemaLocation(
catalog_name=proto.catalog_name,
schema_name=proto.schema_name,
)
if proto.HasField("otel_spans_table_name"):
location._otel_spans_table_name = proto.otel_spans_table_name
if proto.HasField("otel_logs_table_name"):
location._otel_logs_table_name = proto.otel_logs_table_name
return location
def uc_table_prefix_location_to_proto(
location: UnityCatalog,
) -> pb.UcTablePrefixLocation:
proto = pb.UcTablePrefixLocation(
catalog_name=location.catalog_name,
schema_name=location.schema_name,
table_prefix=location.table_prefix,
)
if location._otel_spans_table_name:
proto.spans_table_name = location._otel_spans_table_name
if location._otel_logs_table_name:
proto.logs_table_name = location._otel_logs_table_name
if location._annotations_table_name:
proto.annotations_table_name = location._annotations_table_name
return proto
def uc_table_prefix_location_from_proto(proto: pb.UcTablePrefixLocation) -> UnityCatalog:
location = UnityCatalog(
catalog_name=proto.catalog_name,
schema_name=proto.schema_name,
table_prefix=proto.table_prefix,
)
if proto.HasField("spans_table_name"):
location._otel_spans_table_name = proto.spans_table_name
if proto.HasField("logs_table_name"):
location._otel_logs_table_name = proto.logs_table_name
if proto.HasField("annotations_table_name"):
location._annotations_table_name = proto.annotations_table_name
return location
def inference_table_location_to_proto(
inference_table_location: InferenceTableLocation,
) -> pb.InferenceTableLocation:
return pb.InferenceTableLocation(full_table_name=inference_table_location.full_table_name)
def mlflow_experiment_location_to_proto(
mlflow_experiment_location: MlflowExperimentLocation,
) -> pb.MlflowExperimentLocation:
return pb.MlflowExperimentLocation(experiment_id=mlflow_experiment_location.experiment_id)
def trace_location_to_proto(trace_location: TraceLocation) -> pb.TraceLocation:
if trace_location.type == TraceLocationType.UC_SCHEMA:
return pb.TraceLocation(
type=pb.TraceLocation.TraceLocationType.UC_SCHEMA,
uc_schema=uc_schema_location_to_proto(trace_location.uc_schema),
)
elif trace_location.type == TraceLocationType.UC_TABLE_PREFIX:
return pb.TraceLocation(
type=pb.TraceLocation.TraceLocationType.UC_TABLE_PREFIX,
uc_table_prefix=uc_table_prefix_location_to_proto(trace_location.uc_table_prefix),
)
elif trace_location.type == TraceLocationType.MLFLOW_EXPERIMENT:
return pb.TraceLocation(
type=pb.TraceLocation.TraceLocationType.MLFLOW_EXPERIMENT,
mlflow_experiment=mlflow_experiment_location_to_proto(trace_location.mlflow_experiment),
)
elif trace_location.type == TraceLocationType.INFERENCE_TABLE:
return pb.TraceLocation(
type=pb.TraceLocation.TraceLocationType.INFERENCE_TABLE,
inference_table=inference_table_location_to_proto(trace_location.inference_table),
)
else:
raise ValueError(f"Unsupported trace location type: {trace_location.type}")
def trace_location_type_from_proto(proto: pb.TraceLocation.TraceLocationType) -> TraceLocationType:
return TraceLocationType(pb.TraceLocation.TraceLocationType.Name(proto))
def trace_location_from_proto(proto: pb.TraceLocation) -> TraceLocation:
type_ = trace_location_type_from_proto(proto.type)
if proto.WhichOneof("identifier") == "uc_schema":
return TraceLocation(
type=type_,
uc_schema=uc_schema_location_from_proto(proto.uc_schema),
)
elif proto.WhichOneof("identifier") == "uc_table_prefix":
return TraceLocation(
type=type_,
uc_table_prefix=uc_table_prefix_location_from_proto(proto.uc_table_prefix),
)
elif proto.WhichOneof("identifier") == "mlflow_experiment":
return TraceLocation(
type=type_,
mlflow_experiment=MlflowExperimentLocation.from_proto(proto.mlflow_experiment),
)
elif proto.WhichOneof("identifier") == "inference_table":
return TraceLocation(
type=type_,
inference_table=InferenceTableLocation.from_proto(proto.inference_table),
)
else:
return TraceLocation(TraceLocationType.TRACE_LOCATION_TYPE_UNSPECIFIED)
def trace_info_to_v4_proto(trace_info: TraceInfo) -> pb.TraceInfo:
request_time = Timestamp()
request_time.FromMilliseconds(trace_info.request_time)
execution_duration = None
if trace_info.execution_duration is not None:
execution_duration = Duration()
execution_duration.FromMilliseconds(trace_info.execution_duration)
if trace_info.trace_location.uc_schema or trace_info.trace_location.uc_table_prefix:
_, trace_id = parse_trace_id_v4(trace_info.trace_id)
else:
trace_id = trace_info.trace_id
return pb.TraceInfo(
trace_id=trace_id,
client_request_id=trace_info.client_request_id,
trace_location=trace_location_to_proto(trace_info.trace_location),
request_preview=trace_info.request_preview,
response_preview=trace_info.response_preview,
request_time=request_time,
execution_duration=execution_duration,
state=pb.TraceInfo.State.Value(trace_info.state),
trace_metadata=_truncate_request_metadata(trace_info.trace_metadata),
tags=_truncate_tags(trace_info.tags),
assessments=[assessment_to_proto(a) for a in trace_info.assessments],
)
def trace_to_proto(trace: Trace) -> pb.Trace:
return pb.Trace(
trace_info=trace.info.to_proto(),
spans=[span.to_otel_proto() for span in trace.data.spans],
)
def trace_from_proto(proto: pb.Trace, location_id: str) -> Trace:
return Trace(
info=TraceInfo.from_proto(proto.trace_info),
data=TraceData(spans=[Span.from_otel_proto(span, location_id) for span in proto.spans]),
)
def assessment_to_proto(assessment: Assessment) -> pb.Assessment:
assessment_proto = pb.Assessment()
assessment_proto.assessment_name = assessment.name
location, trace_id = parse_trace_id_v4(assessment.trace_id)
if location:
catalog, schema, table_prefix = parse_uc_location(location)
if table_prefix:
uc_table_prefix = pb.UcTablePrefixLocation(
catalog_name=catalog,
schema_name=schema,
table_prefix=table_prefix,
)
assessment_proto.trace_location.CopyFrom(
pb.TraceLocation(
type=pb.TraceLocation.TraceLocationType.UC_TABLE_PREFIX,
uc_table_prefix=uc_table_prefix,
)
)
else:
uc_schema = pb.UCSchemaLocation(catalog_name=catalog, schema_name=schema)
assessment_proto.trace_location.CopyFrom(
pb.TraceLocation(
type=pb.TraceLocation.TraceLocationType.UC_SCHEMA,
uc_schema=uc_schema,
)
)
assessment_proto.trace_id = trace_id
assessment_proto.source.CopyFrom(assessment.source.to_proto())
# Convert time in milliseconds to protobuf Timestamp
assessment_proto.create_time.FromMilliseconds(assessment.create_time_ms)
assessment_proto.last_update_time.FromMilliseconds(assessment.last_update_time_ms)
if assessment.span_id is not None:
assessment_proto.span_id = assessment.span_id
if assessment.rationale is not None:
assessment_proto.rationale = assessment.rationale
if assessment.assessment_id is not None:
assessment_proto.assessment_id = assessment.assessment_id
if assessment.expectation is not None:
assessment_proto.expectation.CopyFrom(assessment.expectation.to_proto())
elif assessment.feedback is not None:
assessment_proto.feedback.CopyFrom(assessment.feedback.to_proto())
if assessment.metadata:
for key, value in assessment.metadata.items():
assessment_proto.metadata[key] = str(value)
if assessment.overrides:
assessment_proto.overrides = assessment.overrides
if assessment.valid is not None:
assessment_proto.valid = assessment.valid
return assessment_proto
def get_trace_id_from_assessment_proto(proto: pb.Assessment | assessments_pb2.Assessment) -> str:
if "trace_location" in proto.DESCRIPTOR.fields_by_name and proto.trace_location.HasField(
"uc_schema"
):
trace_location = proto.trace_location
return construct_trace_id_v4(
uc_location_to_str(
trace_location.uc_schema.catalog_name,
trace_location.uc_schema.schema_name,
),
proto.trace_id,
)
elif "trace_location" in proto.DESCRIPTOR.fields_by_name and proto.trace_location.HasField(
"uc_table_prefix"
):
trace_location = proto.trace_location
return construct_trace_id_v4(
uc_location_to_str(
trace_location.uc_table_prefix.catalog_name,
trace_location.uc_table_prefix.schema_name,
trace_location.uc_table_prefix.table_prefix,
),
proto.trace_id,
)
else:
return proto.trace_id