Skip to content

feat(appkit): plugin-contributed otel span processors + single-provider agent mlflow tracing - #545

Open
MarioCadenas wants to merge 4 commits into
mainfrom
fix/agents-mlflow-single-provider
Open

feat(appkit): plugin-contributed otel span processors + single-provider agent mlflow tracing#545
MarioCadenas wants to merge 4 commits into
mainfrom
fix/agents-mlflow-single-provider

Conversation

@MarioCadenas

Copy link
Copy Markdown
Collaborator

What

Stacked on #536 (the registerSpanProcessor seam). Wires the agents plugin's MLflow tracing to contribute its span processor to AppKit's single OTel tracer provider, instead of letting mlflow-tracing's init() stand up and globally register a competing provider.

Why

mlflow-tracing 0.1.3's init() builds its own NodeSDK and registers the global tracer provider. OTel's registerGlobal is allowOverride=false, so when both an OTLP endpoint and agent tracing (MLFLOW_EXPERIMENT_ID) are active, AppKit's provider and mlflow's race — whichever registers second is silently dropped, losing either MLflow export or OTLP. This is the bug #536's infrastructure was built to fix; this PR supplies the consumer.

How

  • initAgentTracing() (agents plugin setup()) now builds mlflow's MlflowSpanProcessor itself and contributes it via TelemetryManager.registerSpanProcessor() — no init() here, so no competing registration.
  • mlflow.init() is called lazily on the first trace (after TelemetryManager.start()), purely to seed mlflow's global config; its own provider registration harmlessly loses the already-claimed global slot.
  • GatedMlflowSpanProcessor keeps the contributed processor inert until config is seeded, so AppKit's own spans (HTTP, analytics) created before the first agent turn don't trip mlflow's getConfig() throw. A WeakSet keeps onEnd balanced.
  • The processor/exporter/auth are deep-imported from mlflow-tracing internals (0.1.3 exposes no public SpanProcessor), pinned to the exact version and guarded by a tripwire test that fails loudly if a version bump renames them.

Verification

  • pnpm --filter=@databricks/appkit typecheck — clean
  • mlflow suite — 12 pass (8 behavior + 4 GatedMlflowSpanProcessor gating), including a tripwire that constructs the real deep-imported symbols
  • telemetry-manager + all agents suites — 168 pass
  • oxlint + oxfmt — clean
  • Design validated with a throwaway spike confirming (a) init() seeds global config even when its provider registration loses the global slot, and (b) that duplicate registration is non-fatal.

Manual verification pending: dev-playground with both MLFLOW_EXPERIMENT_ID and OTEL_EXPORTER_OTLP_ENDPOINT set — confirm agent traces reach MLflow and OTLP simultaneously.

Follow-up

The clean long-term fix is upstream: mlflow-tracing exposing a config-only setup (init({ register: false })) or a public span-processor export, which would remove the deep imports.


This pull request and its description were written by Isaac.

Split TelemetryManager into two phases so a single global tracer provider can
carry both the OTLP exporter and plugin-contributed processors (e.g. MLflow),
instead of each SDK racing to register the global provider.

- initialize() registers the meter and logger providers eagerly, because OTel's
  metrics API has no lazy proxy: an instrument bound against the NoOp meter
  stays NoOp for the process lifetime.
- registerSpanProcessor() lets plugins contribute a span processor during
  setup(); ignored with a warning after start() since a started provider's
  processors are immutable in OTel JS 2.x.
- start() (called after plugin setup) builds the global NodeTracerProvider with
  the OTLP processor plus every contributed one, and no-ops when nothing needs
  tracing. Deferring is safe: ProxyTracer rebinds tracers obtained earlier and
  no span is emitted during setup.

Swaps the @opentelemetry/sdk-node dependency for @opentelemetry/sdk-trace-node,
since the tracer provider is now built directly instead of via NodeSDK.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
The mlflow-tracing SDK's init() stands up and globally registers its own
OpenTelemetry tracer provider. OTel's registerGlobal is allowOverride=false,
so when an OTLP endpoint and agent tracing are both active, AppKit's provider
and mlflow's race for the global slot and one exporter is silently dropped.

Instead of calling init() (which self-registers), the agents plugin now builds
mlflow's span processor itself and contributes it to AppKit's single provider
via TelemetryManager.registerSpanProcessor() during setup(). mlflow's global
config is seeded lazily on first trace (after start()), so init()'s own
registration harmlessly loses the already-claimed global slot.

A GatedMlflowSpanProcessor keeps the contributed processor inert until config
is seeded, so AppKit's own spans created before the first agent turn don't hit
mlflow's getConfig() throw. The processor/exporter are deep-imported from
mlflow-tracing internals (no public export in 0.1.3), pinned and guarded by a
tripwire test.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas
MarioCadenas requested a review from a team as a code owner August 21, 2026 13:11
@MarioCadenas
MarioCadenas requested review from calvarjorge and removed request for a team August 21, 2026 13:11
…rom turn 1

The mlflow span processor rides AppKit's shared OTel provider, so it saw every
span — including the exporters' own outbound HTTP calls. Each trace upload became
a new span to trace and upload: a feedback loop that flooded the experiment and
could wedge the process.

- Drop parentless CLIENT spans in the gated processor. An outgoing request made
  outside any agent turn (mlflow/OTLP shipping a trace) is exactly the loop's
  return edge; spans inside a real request tree keep their parent (or are the
  incoming SERVER root), so agent turns are untouched.
- Seed mlflow's config on the "setup:complete" lifecycle event — after
  TelemetryManager.start(), before the server serves — instead of lazily on the
  first trace. The first turn's request-root span is then already forwarded, so
  that turn assembles into a trace instead of being dropped as a cold-start.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas
MarioCadenas changed the base branch from telemetry-manager-refactor to main August 21, 2026 16:53
@MarioCadenas MarioCadenas changed the title fix(agents): route mlflow tracing through AppKit's single otel provider feat(appkit): plugin-contributed otel span processors + single-provider agent mlflow tracing Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant