Manual Tracing
While MLflow's automatic tracing provides instant observability for supported frameworks, manual tracing gives you complete control over how your GenAI applications are instrumented. This flexibility is essential for building production-ready applications that require detailed monitoring and debugging capabilities.
Prerequisites
- MLflow 3
- MLflow 2.x
This guide requires the following packages:
- mlflow[databricks]>=3.1: Core MLflow functionality with GenAI features and Databricks connectivity.
- openai>=1.0.0: (Optional) Only if your custom code interacts with OpenAI; replace with other SDKs if needed.
Install the basic requirements:
%pip install --upgrade "mlflow[databricks]>=3.1"
# %pip install --upgrade openai>=1.0.0 # Install if needed
This guide requires the following packages:
- mlflow[databricks]>=2.15.0,<3.0.0: Core MLflow functionality with Databricks connectivity.
- openai>=1.0.0: (Optional) Only if your custom code interacts with OpenAI.
Install the basic requirements:
%pip install --upgrade "mlflow[databricks]>=2.15.0,<3.0.0"
# pip install --upgrade openai>=1.0.0 # Install if needed
While manual tracing capabilities are available in MLflow 2.15.0+, it is strongly recommended to install MLflow 3 (specifically 3.1 or newer if using mlflow[databricks]
) for the latest GenAI capabilities, including expanded tracing features, refined span types, improved context propagation, and robust support.
Running in a Databricks notebook? MLflow is pre-installed. You only need to install additional SDKs if your manually traced code uses them.
Running locally? You'll need to install mlflow[databricks]
and any other SDKs your code calls.
When to Use Manual Tracing
Manual tracing is the right choice when you need:
- Fine-Grained Control
- Custom Frameworks
- Advanced Scenarios
Custom Trace Structure
- Define exactly which parts of your code to trace
- Create custom hierarchies of spans
- Control span boundaries and relationships
Example Use Case: Tracing specific business logic within a RAG pipeline where you want to measure retrieval vs. generation latency separately.
Unsupported Libraries
- Instrument proprietary or internal frameworks
- Add tracing to custom LLM wrappers
- Support new libraries before official integration
Example Use Case: Adding tracing to your company's internal LLM gateway or custom agent framework.
Complex Workflows
- Multi-threaded or async operations
- Streaming responses with custom aggregation
- Complex nested operations
- Custom trace metadata and attributes
Example Use Case: Tracing a multi-agent system where agents execute complex workflows with custom business logic.
Manual Tracing Approaches
MLflow provides three levels of abstraction for manual tracing, each suited to different use cases:
1. High-Level APIs (Recommended)
The high-level APIs provide an intuitive way to add tracing with minimal code changes. They automatically handle trace lifecycle, exception tracking, and parent-child relationships.
- Decorator
- Context Manager
Best for: Function-level tracing with minimal code changes
import mlflow
from mlflow.entities import SpanType
@mlflow.trace(span_type=SpanType.CHAIN)
def process_request(query: str) -> str:
# Your code here - automatically traced!
result = generate_response(query)
return result
@mlflow.trace(span_type=SpanType.LLM)
def generate_response(query: str) -> str:
# Nested function - parent-child relationship handled automatically
return llm.invoke(query)
Key Benefits:
- One-line instrumentation
- Automatic exception handling
- Works with async/generator functions
- Compatible with auto-tracing
Best for: Tracing code blocks and complex workflows
import mlflow
with mlflow.start_span(name="data_processing") as span:
# Set inputs at the start
span.set_inputs({"query": query, "filters": filters})
# Your processing logic
data = fetch_data(query, filters)
processed = transform_data(data)
# Set outputs before exiting
span.set_outputs({"count": len(processed), "status": "success"})
Key Benefits:
- Flexible span boundaries
- Dynamic input/output setting
- Fine control over span lifecycle
- Ideal for non-function code blocks
2. Low-Level Client APIs (Advanced)
For scenarios requiring complete control over trace lifecycle, the client APIs provide direct access to MLflow's tracing backend.
from mlflow import MlflowClient
client = MlflowClient()
# Start a trace
root_span = client.start_trace("complex_workflow")
# Create child spans with explicit parent relationships
child_span = client.start_span(
name="data_retrieval",
request_id=root_span.request_id,
parent_id=root_span.span_id,
inputs={"query": query}
)
# End spans explicitly
client.end_span(
request_id=child_span.request_id,
span_id=child_span.span_id,
outputs={"documents": documents}
)
# End the trace
client.end_trace(request_id=root_span.request_id)
When to Use Client APIs:
- Custom trace ID management
- Integration with existing observability systems
- Complex trace lifecycle requirements
- Non-standard tracing patterns
Client APIs require manual management of:
- Parent-child relationships
- Span lifecycle (start/end)
- Exception handling
- Thread safety
Learn more about client APIs →
API Comparison
Feature | Decorator | Context Manager | Client APIs |
---|---|---|---|
Automatic Parent-Child | Yes | Yes | No - manual management |
Exception Handling | Automatic | Automatic | Manual |
Works with Auto-trace | Yes | Yes | No |
Thread Safety | Automatic | Automatic | Manual |
Custom Trace IDs | No | No | Yes |
Best For | Function tracing | Code block tracing | Advanced control |
Common Patterns
Combining with Auto-Tracing
Manual tracing seamlessly integrates with MLflow's auto-tracing capabilities:
import mlflow
import openai
# Enable auto-tracing for OpenAI
mlflow.openai.autolog()
@mlflow.trace(span_type="CHAIN")
def rag_pipeline(query: str):
# Manual span for retrieval
with mlflow.start_span(name="retrieval") as span:
docs = retrieve_documents(query)
span.set_outputs({"doc_count": len(docs)})
# Auto-traced OpenAI call
response = openai.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": f"Answer based on: {docs}\n\nQuery: {query}"}]
)
return response.choices[0].message.content
Complex Workflow Tracing
For complex workflows with multiple steps, use nested spans to capture detailed execution flow:
@mlflow.trace(name="data_pipeline")
def process_data_pipeline(data_source: str):
# Extract phase
with mlflow.start_span(name="extract") as extract_span:
raw_data = extract_from_source(data_source)
extract_span.set_outputs({"record_count": len(raw_data)})
# Transform phase
with mlflow.start_span(name="transform") as transform_span:
transformed = apply_transformations(raw_data)
transform_span.set_outputs({"transformed_count": len(transformed)})
# Load phase
with mlflow.start_span(name="load") as load_span:
result = load_to_destination(transformed)
load_span.set_outputs({"status": "success"})
return result
Customizing Request and Response Previews in the UI
The MLflow UI provides Request
and Response
columns in the Traces tab that show a preview of the overall trace's input and output. By default, these are truncated. When using manual tracing, especially with the @mlflow.trace
decorator or context managers that create the root span of a trace, you can customize these previews using mlflow.update_current_trace()
.
This is useful for complex data structures where the default preview might not be informative.
import mlflow
import openai # Assuming openai is used, replace if not
# This example assumes you have an OpenAI client initialized and API key set up.
# client = openai.OpenAI()
@mlflow.trace
def predict(messages: list[dict]) -> str:
# Customize the request preview to show the first and last messages
custom_preview = f'{messages[0]["content"][:10]} ... {messages[-1]["content"][:10]}'
mlflow.update_current_trace(request_preview=custom_preview)
# Call the model
# response = openai.chat.completions.create(
# model="gpt-4o-mini",
# messages=messages,
# )
# return response.choices[0].message.content
return f"Response based on {len(messages)} messages."
messages = [
{"role": "user", "content": "Hi, how are you?"},
{"role": "assistant", "content": "I'm good, thank you!"},
{"role": "user", "content": "What's your name?"},
# ... (long message history)
{"role": "assistant", "content": "Bye!"},
]
predict(messages)
This allows you to tailor the preview to be more informative for your specific data structures.
Next steps
Continue your journey with these recommended actions and tutorials.
- Decorators & Fluent APIs - Start here for most use cases with high-level APIs
- Low-Level Client APIs - Learn advanced scenarios requiring full control
- Debug & observe your app - Use your manually traced app for debugging
Reference guides
Explore detailed documentation for concepts and features mentioned in this guide.
- Tracing data model - Understand spans and trace structure
- Tracing concepts - Learn the fundamentals of tracing
- Automatic tracing - Combine manual and automatic tracing
Most users should start with the high-level APIs (decorators and context managers). They provide the best balance of ease-of-use and functionality while maintaining compatibility with MLflow's ecosystem.