Export MLflow traces to Future AGI over OTLP

Export new MLflow traces to Future AGI over OTLP, with environment-based routing and Python span processors for content and exception filtering.

Send new MLflow 3 traces to Observe using MLflow’s OpenTelemetry (OTLP) export. Environment variables route spans to Future AGI; the Python span processors under Privacy filter recorded content and exception events. This route does not use a traceAI MLflow instrumentor.

This is live export only. Traces already stored in an MLflow tracking server, Databricks or Unity Catalog are not imported. Only spans that end while the variables below are set are sent.

MLflow installs opentelemetry-sdk but not an OTLP exporter. Install opentelemetry-exporter-otlp-proto-http yourself, otherwise MLflow exports nothing and logs nothing about it.

Install

This recipe uses MLflow 3.16.1 on Python 3.12.

pip install mlflow==3.16.1 opentelemetry-exporter-otlp-proto-http==1.45.0 openai==2.54.0

Configure

Set these before the first span starts. MLflow builds its exporter once, on first use.

export FI_API_KEY="your-futureagi-api-key"
export FI_SECRET_KEY="your-futureagi-secret-key"

# The full URL. MLflow does not append /v1/traces to this variable.
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://YOUR_FI_COLLECTOR_ORIGIN/tracer/v1/traces"
export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL="http/protobuf"   # MLflow defaults to gRPC

export OTEL_EXPORTER_OTLP_TRACES_HEADERS="$(python -c 'import os; from urllib.parse import quote; print("x-api-key={0},x-secret-key={1}".format(quote(os.environ["FI_API_KEY"], safe=""), quote(os.environ["FI_SECRET_KEY"], safe="")))')"
export OTEL_RESOURCE_ATTRIBUTES="project_name=my-mlflow-app,project_type=observe"
export OTEL_SERVICE_NAME="my-mlflow-app"

export MLFLOW_ENABLE_OTEL_GENAI_SEMCONV=true   # typed spans, plain model name, tokens
export MLFLOW_DISABLE_TELEMETRY=true
export MLFLOW_MODEL_CATALOG_URI=""              # no model-catalog fetch from github.com

# These make MLflow send nothing, or send it elsewhere. Leave them unset.
unset OTEL_EXPORTER_OTLP_ENDPOINT OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
unset OTEL_EXPORTER_OTLP_HEADERS MLFLOW_TRACING_DESTINATION

The header command percent-encodes the keys so reserved characters do not change the header list.

Before running the example, review the exception-event and content handling in Privacy. Register the processors before the first span starts.

Then trace as usual:

import mlflow

mlflow.openai.autolog()   # OpenAI Chat Completions; openai is installed above

@mlflow.trace(span_type="TOOL")
def get_refund_window(product: str) -> dict:
    return {"product": product, "days": 30}

print(get_refund_window("headphones"))

For a self-hosted Future AGI use your own collector origin in place of YOUR_FI_COLLECTOR_ORIGIN (for example http://localhost:14338). Both /tracer/v1/traces and /v1/traces are accepted by the collector; leave compression off (the exporter default) for compatibility with older self-hosted versions. Future AGI v1.56.0 and later also accept gzip.

What is captured

With MLFLOW_ENABLE_OTEL_GENAI_SEMCONV=true (recommended):

MLflow spanFuture AGI
model call (CHAT_MODEL, LLM)span kind llm, named chat <model>, model and provider without quotes, prompt and completion tokens, cost when the model is in the price table
TOOL functionspan kind tool, named execute_tool
CHAIN, RETRIEVER, EMBEDDING, RERANKER, GUARDRAIL, EVALUATORthe matching span kind
AGENT functionspan invoke_agent, kind unknown
exception in a spanstatus ERROR on that span and its parents (the error text and stack trace too, unless you register the processors under Privacy)
trace hierarchypreserved (parent/child)

In Future AGI 1.56.1, the collector promotes gen_ai.input.messages and gen_ai.output.messages into the input and output columns when those attributes are present, while retaining the raw attributes. The experiment id is not exported unless you add mlflow.experiment_id=<id> to OTEL_RESOURCE_ATTRIBUTES; it is a resource attribute, not a Future AGI session. There is no session or user id.

With the switch off, MLflow exports its own mlflow.* keys, which Future AGI does not map: every span is kind unknown, and autolog model calls carry no model or tokens. Leave the switch on.

Privacy

MLflow has no setting that keeps tracing on and content off. By default MLflow exports the prompts, responses and tool inputs and outputs it records, as span attributes (gen_ai.input.messages, gen_ai.output.messages, gen_ai.system_instructions, tool call arguments and results). This is on by default, like the traceAI instrumentors; you turn it off with a span processor.

Two processors, both registered before the first span:

import mlflow
from mlflow.entities import LiveSpan, SpanStatusCode

def _filter_events(span: LiveSpan, *, content: bool = False) -> None:
    # Read through the public OTel events API. MLflow 3.16.1 / OTel 1.45
    # have no public event-removal API, so replacing the backing collection
    # is the only compatibility fallback. Never silently skip filtering.
    def is_sensitive(event) -> bool:
        if event.name == "exception" or any(
            key.startswith("exception.") for key in event.attributes or {}
        ):
            return True
        if not content:
            return False
        return event.name in {
            "gen_ai.system.message", "gen_ai.user.message", "gen_ai.assistant.message",
            "gen_ai.tool.message", "gen_ai.choice", "gen_ai.input.messages",
            "gen_ai.output.messages", "tool.input", "tool.output",
        } or event.name.startswith("gen_ai.content.") or any(
            key in {"body", "content", "input", "output", "prompt", "completion", "messages"}
            or key.startswith(("gen_ai.input.", "gen_ai.output.", "gen_ai.system_instructions",
                               "gen_ai.tool.call.arguments", "gen_ai.tool.call.result"))
            for key in event.attributes or {}
        )

    try:
        otel_span = span._span
        events = tuple(otel_span.events)
        kept = tuple(event for event in events if not is_sensitive(event))
        if len(kept) != len(events):
            if not hasattr(otel_span, "_events"):
                raise RuntimeError("Event removal is unavailable")
            otel_span._events = list(kept)
            if tuple(otel_span.events) != kept:
                raise RuntimeError("Event removal did not take effect")
    except Exception:
        # MLflow catches Exception from processors and exports the span anyway.
        # SystemExit deliberately stops finalization before the unsafe span is
        # queued for export; neither the event nor its text is logged here.
        raise SystemExit("Cannot safely filter MLflow events; refusing to export this span.") from None


def drop_exception_events(span: LiveSpan) -> None:
    _filter_events(span)


def drop_content(span: LiveSpan) -> None:
    _filter_events(span, content=True)
    if span.inputs is not None:
        span.set_inputs("[REDACTED]")
    if span.outputs is not None:
        span.set_outputs("[REDACTED]")
    if span.status.status_code == SpanStatusCode.ERROR:
        span.set_status("ERROR")

mlflow.tracing.configure(span_processors=[drop_exception_events, drop_content])
  • Use drop_exception_events always to remove exception events while preserving unrelated events and their timestamps. Without it MLflow exports stack traces and local paths. The pinned versions have no public event-removal API, so the processor uses a compatibility fallback after reading events through the public API. If it cannot inspect or filter events, it stops the application before exporting the affected span rather than leaving sensitive events untouched.
  • Add drop_content to keep the trace tree, timings, model, tokens and ERROR status but remove prompts, responses, tool arguments and results and the error text (a provider often puts request details in it). It also removes GenAI message events and events with content-bearing attributes; unrelated operational events stay. Custom attributes with other names are not automatically redacted.
  • Autolog also exports the tool schemas you pass to the model (tools, gen_ai.tool.definitions). They are your code, not user content, and are not removed.

MLflow itself writes a telemetry file under ~/.config/mlflow; MLFLOW_DISABLE_TELEMETRY=true stops that.

Verify

Run the traced function and open Observe in Future AGI, then select the project named by project_name. You should see a trace for the function. Instrumented model calls and tool spans appear nested when they run inside a traced root function.

Troubleshooting

  • Nothing arrives. MLflow gives no error. Check, in order: the traces endpoint is the full URL ending /v1/traces with no trailing slash (a bare origin, a trailing slash or /tracer return 404 and the spans are lost); the protocol is http/protobuf; opentelemetry-exporter-otlp-proto-http is installed; OTEL_RESOURCE_ATTRIBUTES has project_name; the variables were set before the first span.
  • OTEL_EXPORTER_OTLP_ENDPOINT is set. It also turns on gRPC metrics export, and without a gRPC exporter every span fails. Use only the TRACES variables.
  • mlflow.tracing.set_destination(...) or MLFLOW_TRACING_DESTINATION. The destination wins and OTLP can be silent. Remove it.
  • A Databricks tracking URI. MLflow then calls Databricks and a Unity Catalog linked experiment replaces the OTLP export. Not supported by this recipe.
  • A ./mlflow.db file appears. Dual export (MLFLOW_TRACE_ENABLE_OTLP_DUAL_EXPORT=true) with no tracking URI also writes the trace to a local SQLite store.
  • Model shown with quotes and cost 0. The semconv switch is off; MLflow JSON-encodes every attribute value.
  • Old traces are missing. Expected. This is live export, not a backfill.

Limitations

  • Live export only; no historical import, no tracking-server client.
  • AGENT spans are typed unknown; function names of renamed spans are not kept when the semconv switch is on.
  • Content columns reflect the message attributes MLflow exports. Future AGI 1.56.1 promotes those attributes; the privacy processors above replace their content with [REDACTED].
  • Databricks, dual export and code-level set_destination() are outside this recipe’s supported scope.
  • No session id or user id.
  • traceAI instrumentors (for example OpenAI or LangChain) wrap the library and send spans from your process through fi_instrumentation. Here MLflow itself is the exporter and there is no traceAI package.
  • Langfuse import copies historical data from another tool. This page does not import anything.
Was this page helpful?

Questions & Discussion