@dbx-tools/appkit-mastra 0.9.65 → 0.9.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -19,8 +19,7 @@
19
19
  "projen": "projen"
20
20
  },
21
21
  "devDependencies": {
22
- "@opentelemetry/sdk-trace-base": "2.8.0",
23
- "@opentelemetry/sdk-trace-node": "2.8.0",
22
+ "@mlflow/core": "0.4.0",
24
23
  "@types/express": "^5.0.5",
25
24
  "@types/node": "^24.6.0",
26
25
  "@types/pg": "^8",
@@ -29,23 +28,24 @@
29
28
  "vitest": "3.2.4"
30
29
  },
31
30
  "peerDependencies": {
31
+ "@mlflow/core": "0.4.0",
32
32
  "skills": "^1"
33
33
  },
34
34
  "dependencies": {
35
35
  "@databricks/appkit": "0.81.0",
36
- "@dbx-tools/appkit": "^0.9.65",
37
- "@dbx-tools/core": "^0.9.65",
38
- "@dbx-tools/databricks": "^0.9.65",
39
- "@dbx-tools/fs": "^0.9.65",
40
- "@dbx-tools/genie": "^0.9.65",
41
- "@dbx-tools/model": "^0.9.65",
42
- "@dbx-tools/path": "^0.9.65",
43
- "@dbx-tools/postgres": "^0.9.65",
44
- "@dbx-tools/shared-core": "^0.9.65",
45
- "@dbx-tools/shared-fs": "^0.9.65",
46
- "@dbx-tools/shared-genie": "^0.9.65",
47
- "@dbx-tools/shared-mastra": "^0.9.65",
48
- "@dbx-tools/shared-model": "^0.9.65",
36
+ "@dbx-tools/appkit": "^0.9.68",
37
+ "@dbx-tools/core": "^0.9.68",
38
+ "@dbx-tools/databricks": "^0.9.68",
39
+ "@dbx-tools/fs": "^0.9.68",
40
+ "@dbx-tools/genie": "^0.9.68",
41
+ "@dbx-tools/model": "^0.9.68",
42
+ "@dbx-tools/path": "^0.9.68",
43
+ "@dbx-tools/postgres": "^0.9.68",
44
+ "@dbx-tools/shared-core": "^0.9.68",
45
+ "@dbx-tools/shared-fs": "^0.9.68",
46
+ "@dbx-tools/shared-genie": "^0.9.68",
47
+ "@dbx-tools/shared-mastra": "^0.9.68",
48
+ "@dbx-tools/shared-model": "^0.9.68",
49
49
  "@mastra/ai-sdk": "1.10.5",
50
50
  "@mastra/core": "1.71.0",
51
51
  "@mastra/express": "1.5.15",
@@ -58,6 +58,8 @@
58
58
  "@modelcontextprotocol/sdk": "^1.29.0",
59
59
  "@opentelemetry/api": "^1.9.1",
60
60
  "@opentelemetry/core": "2.11.0",
61
+ "@opentelemetry/sdk-trace-base": "2.8.0",
62
+ "@opentelemetry/sdk-trace-node": "2.8.0",
61
63
  "@pydantic/monty": "0.0.23",
62
64
  "express": "^5.1.0",
63
65
  "pg": "^8.22.0",
@@ -194,6 +196,10 @@
194
196
  "types": "./lib/src/telemetry.d.ts",
195
197
  "default": "./lib/src/telemetry.js"
196
198
  },
199
+ "./trace-attributes": {
200
+ "types": "./lib/src/trace-attributes.d.ts",
201
+ "default": "./lib/src/trace-attributes.js"
202
+ },
197
203
  "./validation": {
198
204
  "types": "./lib/src/validation.d.ts",
199
205
  "default": "./lib/src/validation.js"
@@ -209,7 +215,7 @@
209
215
  "./package.json": "./package.json"
210
216
  }
211
217
  },
212
- "version": "0.9.65",
218
+ "version": "0.9.68",
213
219
  "types": "./lib/index.d.ts",
214
220
  "dbxToolsConfig": {
215
221
  "tags": [
@@ -342,6 +348,10 @@
342
348
  "types": "./lib/src/telemetry.d.ts",
343
349
  "default": "./lib/src/telemetry.js"
344
350
  },
351
+ "./trace-attributes": {
352
+ "types": "./lib/src/trace-attributes.d.ts",
353
+ "default": "./lib/src/trace-attributes.js"
354
+ },
345
355
  "./validation": {
346
356
  "types": "./lib/src/validation.d.ts",
347
357
  "default": "./lib/src/validation.js"
@@ -359,11 +369,15 @@
359
369
  "files": [
360
370
  "index.ts",
361
371
  "src",
372
+ "exports.ts",
362
373
  "lib"
363
374
  ],
364
375
  "peerDependenciesMeta": {
365
376
  "skills": {
366
377
  "optional": true
378
+ },
379
+ "@mlflow/core": {
380
+ "optional": true
367
381
  }
368
382
  },
369
383
  "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"bunx projen\"."
package/src/genie.ts CHANGED
@@ -59,7 +59,7 @@ import { chartPlannerRequestSchema, chartToolOutputSchema, prepareChart } from "
59
59
  import { MASTRA_USER_KEY, resolveUserKey } from "./config.ts";
60
60
  import type { MastraPluginConfig, User } from "./config.ts";
61
61
  import { fetchStatementData } from "./statement.ts";
62
- import { stampGenieToolSpan } from "./telemetry.ts";
62
+ import { recordActiveTraceAuth, stampGenieToolSpan } from "./telemetry.ts";
63
63
  import { safeWriteProgress } from "./writer.ts";
64
64
 
65
65
  const logger = log.logger("mastra/genie");
@@ -128,6 +128,7 @@ function requireClient(
128
128
  "Invoke the tool from an agent turn served by the mastra plugin, which stamps the AppKit user on the Mastra request context.",
129
129
  );
130
130
  }
131
+ recordActiveTraceAuth("isUserContext" in user.executionContext ? "obo" : "service-principal");
131
132
  return { client: user.executionContext.client, requestContext };
132
133
  }
133
134
 
package/src/mlflow.ts CHANGED
@@ -25,15 +25,101 @@
25
25
  * @module
26
26
  */
27
27
 
28
- import { ConfigurationError } from "@databricks/appkit";
28
+ import { ConfigurationError, createWorkspaceClient } from "@databricks/appkit";
29
29
  import { appkit } from "@dbx-tools/appkit";
30
30
  import { asyncUtils, errorUtils, log, object } from "@dbx-tools/shared-core";
31
31
  import { feedback } from "@dbx-tools/shared-mastra";
32
+ import { TraceLocationType, TraceMetadataKey } from "@mlflow/core";
33
+ import { TraceInfo } from "@mlflow/core/dist/core/entities/trace_info";
34
+ import { TraceState } from "@mlflow/core/dist/core/entities/trace_state";
35
+ import type { Context } from "@opentelemetry/api";
36
+ import type {
37
+ ReadableSpan,
38
+ Span as SdkSpan,
39
+ SpanProcessor,
40
+ } from "@opentelemetry/sdk-trace-base";
41
+ import type { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
32
42
  import { z } from "zod";
33
43
  import { databricksFetch, readResponseJson, readResponseText } from "./rest.ts";
44
+ import { MLFLOW_AGENT_TAG_ATTR } from "./trace-attributes.ts";
34
45
 
35
46
  const logger = log.logger("mastra/mlflow");
36
47
 
48
+ type MlflowModule = typeof import("@mlflow/core");
49
+ type UpdateCurrentTraceOptions = Parameters<MlflowModule["updateCurrentTrace"]>[0];
50
+
51
+ let directMlflow: MlflowModule | undefined;
52
+ let directMlflowInitStarted = false;
53
+ let directMlflowProvider: NodeTracerProvider | undefined;
54
+ let directMlflowProcessor: SpanProcessor | undefined;
55
+ let appMlflowTraceInfoInitStarted = false;
56
+ let appMlflowTraceInfoClient: WorkspaceClient | undefined;
57
+ let appMlflowTracePrefix: string | undefined;
58
+ const pendingAppMlflowTraceInfo = new Set<Promise<void>>();
59
+
60
+ const NOISY_INSTRUMENTATION_SCOPES = new Set([
61
+ "@opentelemetry/instrumentation-express",
62
+ "@opentelemetry/instrumentation-http",
63
+ "cache-manager-cache-manager",
64
+ ]);
65
+
66
+ function isAgentTraceRoot(span: SdkSpan): boolean {
67
+ return span.attributes[MLFLOW_AGENT_TAG_ATTR] === "true";
68
+ }
69
+
70
+ function isUsefulAgentDescendant(span: SdkSpan): boolean {
71
+ return !NOISY_INSTRUMENTATION_SCOPES.has(span.instrumentationScope.name);
72
+ }
73
+
74
+ /**
75
+ * Restrict the direct MLflow provider to chat roots and their descendants.
76
+ * AppKit and Mastra create other OTel roots for cache and plugin operations;
77
+ * forwarding those would recreate the one-trace-per-request noise this path is
78
+ * intended to avoid.
79
+ */
80
+ export class AgentTraceSpanProcessor implements SpanProcessor {
81
+ private readonly traceIds = new Set<string>();
82
+ private readonly spanIds = new Set<string>();
83
+
84
+ constructor(private readonly delegate: SpanProcessor) {}
85
+
86
+ onStart(span: SdkSpan, parentContext: Context): void {
87
+ const traceId = span.spanContext().traceId;
88
+ const isRoot = !span.parentSpanContext?.spanId;
89
+ if (isRoot) {
90
+ if (!isAgentTraceRoot(span)) return;
91
+ this.traceIds.add(traceId);
92
+ } else if (!this.traceIds.has(traceId) || !isUsefulAgentDescendant(span)) {
93
+ return;
94
+ }
95
+ this.spanIds.add(span.spanContext().spanId);
96
+ this.delegate.onStart(span, parentContext);
97
+ }
98
+
99
+ onEnding(span: SdkSpan): void {
100
+ if (!this.spanIds.has(span.spanContext().spanId)) return;
101
+ this.delegate.onEnding?.(span);
102
+ }
103
+
104
+ onEnd(span: ReadableSpan): void {
105
+ const traceId = span.spanContext().traceId;
106
+ const spanId = span.spanContext().spanId;
107
+ if (!this.spanIds.delete(spanId)) return;
108
+ this.delegate.onEnd(span);
109
+ if (!span.parentSpanContext?.spanId) this.traceIds.delete(traceId);
110
+ }
111
+
112
+ forceFlush(): Promise<void> {
113
+ return this.delegate.forceFlush();
114
+ }
115
+
116
+ shutdown(): Promise<void> {
117
+ this.traceIds.clear();
118
+ this.spanIds.clear();
119
+ return this.delegate.shutdown();
120
+ }
121
+ }
122
+
37
123
  /** Workspace client carried on an AppKit execution context. */
38
124
  type WorkspaceClient = appkit.WorkspaceClientLike;
39
125
 
@@ -214,14 +300,277 @@ function configuredExperiment(): { id?: string; name?: string } {
214
300
  };
215
301
  }
216
302
 
303
+ /** Tracking URI for direct local MLflow tracing, with CLI-profile auth injected. */
304
+ export function directMlflowTrackingUri(): string | undefined {
305
+ if (appkit.isDatabricksAppEnv()) return undefined;
306
+ const configured = process.env.MLFLOW_TRACKING_URI?.trim();
307
+ if (configured && configured !== "databricks") return configured;
308
+ const profile = process.env.DATABRICKS_CONFIG_PROFILE?.trim();
309
+ return profile ? `databricks://${profile}` : "databricks";
310
+ }
311
+
312
+ /** Parse the optional UC table-prefix location used by direct local tracing. */
313
+ export function directMlflowTraceLocation():
314
+ | { catalogName: string; schemaName: string; tablePrefix: string }
315
+ | undefined {
316
+ const catalogName = process.env.MLFLOW_UC_CATALOG?.trim();
317
+ const schemaName = process.env.MLFLOW_UC_SCHEMA?.trim();
318
+ const tablePrefix = process.env.MLFLOW_UC_TABLE_PREFIX?.trim();
319
+ if (catalogName && schemaName && tablePrefix) {
320
+ return { catalogName, schemaName, tablePrefix };
321
+ }
322
+
323
+ const prefix = normalizeUcPrefix(process.env.MLFLOW_UC_TRACE_PREFIX);
324
+ if (!prefix) return undefined;
325
+ const parts = prefix.split(".");
326
+ if (parts.length !== 3) return undefined;
327
+ const [prefixCatalog, prefixSchema, prefixTable] = parts;
328
+ if (!prefixCatalog || !prefixSchema || !prefixTable) return undefined;
329
+ return {
330
+ catalogName: prefixCatalog,
331
+ schemaName: prefixSchema,
332
+ tablePrefix: prefixTable,
333
+ };
334
+ }
335
+
336
+ /** Whether the local direct-to-experiment MLflow SDK has enough configuration. */
337
+ export function directMlflowTracingConfigured(): boolean {
338
+ return Boolean(
339
+ !appkit.isDatabricksAppEnv() &&
340
+ process.env.MLFLOW_EXPERIMENT_ID?.trim() &&
341
+ directMlflowTrackingUri(),
342
+ );
343
+ }
344
+
345
+ /** Whether a Databricks App can promote OTLP spans into full MLflow trace info. */
346
+ export function appMlflowTraceInfoConfigured(): boolean {
347
+ return Boolean(
348
+ appkit.isDatabricksAppEnv() &&
349
+ (process.env.MLFLOW_EXPERIMENT_ID?.trim() || process.env.MLFLOW_EXPERIMENT_NAME?.trim()) &&
350
+ (process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() ||
351
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim()),
352
+ );
353
+ }
354
+
355
+ /** Initialize the App service-principal client used to persist V4 trace tags and metadata. */
356
+ export async function initializeAppMlflowTraceInfo(
357
+ client?: WorkspaceClient,
358
+ ): Promise<boolean> {
359
+ if (appMlflowTraceInfoClient && appMlflowTracePrefix) return true;
360
+ if (appMlflowTraceInfoInitStarted || !appMlflowTraceInfoConfigured()) return false;
361
+ appMlflowTraceInfoInitStarted = true;
362
+ try {
363
+ const resolvedClient = client ?? (createWorkspaceClient() as WorkspaceClient);
364
+ const prefix = await resolveUcTracePrefix(resolvedClient);
365
+ if (!prefix || prefix.split(".").length !== 3) {
366
+ throw new ConfigurationError(
367
+ "Databricks Apps MLflow trace-info promotion requires a UC-linked experiment or MLFLOW_UC_TRACE_PREFIX.",
368
+ );
369
+ }
370
+ appMlflowTraceInfoClient = resolvedClient;
371
+ appMlflowTracePrefix = prefix;
372
+ logger.info("Databricks Apps MLflow trace-info promotion enabled", { prefix });
373
+ return true;
374
+ } catch (err) {
375
+ logger.warn("Databricks Apps MLflow trace-info promotion disabled", {
376
+ error: errorUtils.errorMessage(err),
377
+ });
378
+ return false;
379
+ }
380
+ }
381
+
382
+ /** Trace-level values persisted beside spans already exported by the Apps OTLP sidecar. */
383
+ export interface AppMlflowTraceInfo {
384
+ traceId: string;
385
+ requestTime: number;
386
+ executionDuration: number;
387
+ error: boolean;
388
+ tags: Record<string, string>;
389
+ user?: string;
390
+ sessionId?: string;
391
+ requestPreview?: string;
392
+ responsePreview?: string;
393
+ }
394
+
395
+ /** Persist MLflow V4 trace info for one Databricks Apps OTel root without re-exporting spans. */
396
+ export function persistAppMlflowTraceInfo(params: AppMlflowTraceInfo): void {
397
+ const client = appMlflowTraceInfoClient;
398
+ const prefix = appMlflowTracePrefix;
399
+ if (!client || !prefix) return;
400
+ const [catalogName, schemaName, tablePrefix] = prefix.split(".");
401
+ if (!catalogName || !schemaName || !tablePrefix) return;
402
+ const traceInfo = new TraceInfo({
403
+ traceId: `trace:/${prefix}/${params.traceId}`,
404
+ traceLocation: {
405
+ type: TraceLocationType.UC_TABLE_PREFIX,
406
+ ucTablePrefix: { catalogName, schemaName, tablePrefix },
407
+ },
408
+ requestTime: params.requestTime,
409
+ executionDuration: params.executionDuration,
410
+ state: params.error ? TraceState.ERROR : TraceState.OK,
411
+ ...(params.requestPreview ? { requestPreview: params.requestPreview } : {}),
412
+ ...(params.responsePreview ? { responsePreview: params.responsePreview } : {}),
413
+ traceMetadata: {
414
+ [TraceMetadataKey.SCHEMA_VERSION]: "4",
415
+ ...(params.user ? { [TraceMetadataKey.TRACE_USER]: params.user } : {}),
416
+ ...(params.sessionId ? { [TraceMetadataKey.TRACE_SESSION]: params.sessionId } : {}),
417
+ },
418
+ tags: params.tags,
419
+ assessments: [],
420
+ });
421
+ const path = `/api/4.0/mlflow/traces/${encodeURIComponent(prefix)}/${params.traceId}/info`;
422
+ const pending = databricksFetch(client, path, { method: "POST", body: traceInfo.toJson() })
423
+ .then(async (response) => {
424
+ if (response.ok) return;
425
+ logger.warn("Databricks Apps MLflow trace-info promotion failed", {
426
+ traceId: params.traceId,
427
+ status: response.status,
428
+ body: await readResponseText(response),
429
+ });
430
+ })
431
+ .catch((err) => {
432
+ logger.warn("Databricks Apps MLflow trace-info promotion failed", {
433
+ traceId: params.traceId,
434
+ error: errorUtils.errorMessage(err),
435
+ });
436
+ })
437
+ .finally(() => pendingAppMlflowTraceInfo.delete(pending));
438
+ pendingAppMlflowTraceInfo.add(pending);
439
+ }
440
+
441
+ /** Reset Apps trace-info promotion state for tests. */
442
+ export function resetAppMlflowTraceInfo(): void {
443
+ appMlflowTraceInfoInitStarted = false;
444
+ appMlflowTraceInfoClient = undefined;
445
+ appMlflowTracePrefix = undefined;
446
+ pendingAppMlflowTraceInfo.clear();
447
+ }
448
+
449
+ async function detectedDirectMlflowTraceLocation(
450
+ mlflow: MlflowModule,
451
+ trackingUri: string,
452
+ experimentId: string,
453
+ host: string | undefined,
454
+ ): Promise<{ catalogName: string; schemaName: string; tablePrefix: string } | undefined> {
455
+ const explicit = directMlflowTraceLocation();
456
+ if (explicit) return explicit;
457
+ try {
458
+ const authProvider = mlflow.createAuthProvider({
459
+ trackingUri,
460
+ ...(host ? { host } : {}),
461
+ });
462
+ const client = new mlflow.MlflowClient({ trackingUri, authProvider });
463
+ const experiment = await client.getExperiment(experimentId);
464
+ if (!experiment) return undefined;
465
+ const { ucLocationFromExperimentTags } = await import("@mlflow/core/dist/core/destination");
466
+ const location = ucLocationFromExperimentTags(experiment.tags);
467
+ if (!location?.tablePrefix) return undefined;
468
+ return {
469
+ catalogName: location.catalogName,
470
+ schemaName: location.schemaName,
471
+ tablePrefix: location.tablePrefix,
472
+ };
473
+ } catch (err) {
474
+ logger.warn("direct MLflow UC trace-location detection failed", {
475
+ experimentId,
476
+ error: errorUtils.errorMessage(err),
477
+ });
478
+ return undefined;
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Start the MLflow Node tracer for local development when AppKit has no OTLP
484
+ * exporter. This provider is never started in the deployed Apps path, avoiding
485
+ * a second global OTel provider racing AppKit's telemetry manager.
486
+ */
487
+ export async function initializeDirectMlflowTracing(): Promise<boolean> {
488
+ if (directMlflow) return true;
489
+ if (directMlflowInitStarted || !directMlflowTracingConfigured()) return false;
490
+ directMlflowInitStarted = true;
491
+
492
+ const experimentId = process.env.MLFLOW_EXPERIMENT_ID!.trim();
493
+ const trackingUri = directMlflowTrackingUri()!;
494
+ try {
495
+ const mlflow = await import("@mlflow/core");
496
+ const rawHost = process.env.DATABRICKS_HOST?.trim();
497
+ const host = rawHost
498
+ ? /^https?:\/\//i.test(rawHost)
499
+ ? rawHost
500
+ : `https://${rawHost}`
501
+ : undefined;
502
+ const traceLocation = await detectedDirectMlflowTraceLocation(
503
+ mlflow,
504
+ trackingUri,
505
+ experimentId,
506
+ host,
507
+ );
508
+ if (!traceLocation) {
509
+ throw new ConfigurationError(
510
+ "Direct local MLflow tracing requires a UC-linked experiment or MLFLOW_UC_TRACE_PREFIX.",
511
+ );
512
+ }
513
+ const [{ DatabricksUCTableSpanExporter, DatabricksUCTableSpanProcessor }, sdk] =
514
+ await Promise.all([
515
+ import("@mlflow/core/dist/exporters/uc_table"),
516
+ import("@opentelemetry/sdk-trace-node"),
517
+ ]);
518
+ const authProvider = mlflow.createAuthProvider({
519
+ trackingUri,
520
+ ...(host ? { host } : {}),
521
+ });
522
+ const client = new mlflow.MlflowClient({ trackingUri, authProvider });
523
+ const exporter = new DatabricksUCTableSpanExporter(client);
524
+ const processor = new AgentTraceSpanProcessor(
525
+ new DatabricksUCTableSpanProcessor(exporter, traceLocation),
526
+ );
527
+ const provider = new sdk.NodeTracerProvider({ spanProcessors: [processor] });
528
+ provider.register();
529
+ directMlflow = mlflow;
530
+ directMlflowProvider = provider;
531
+ directMlflowProcessor = processor;
532
+ logger.info("direct MLflow tracing enabled", {
533
+ experimentId,
534
+ trackingUri,
535
+ traceLocation,
536
+ });
537
+ return true;
538
+ } catch (err) {
539
+ logger.warn("direct MLflow tracing disabled", {
540
+ error: errorUtils.errorMessage(err),
541
+ });
542
+ return false;
543
+ }
544
+ }
545
+
546
+ /** Persist trace-level fields on the direct MLflow trace in the current OTel context. */
547
+ export function updateActiveMlflowTrace(options: UpdateCurrentTraceOptions): void {
548
+ directMlflow?.updateCurrentTrace(options);
549
+ }
550
+
551
+ /** Persist tags on the direct MLflow trace active in the current OTel context. */
552
+ export function updateActiveMlflowTraceTags(tags: Record<string, string>): void {
553
+ updateActiveMlflowTrace({ tags });
554
+ }
555
+
556
+ /** Whether this process owns the filtered local MLflow tracer provider. */
557
+ export function directMlflowTracingActive(): boolean {
558
+ return directMlflowProcessor !== undefined;
559
+ }
560
+
561
+ /** Flush pending direct local trace exports during graceful shutdown. */
562
+ export async function flushDirectMlflowTracing(): Promise<void> {
563
+ await Promise.all([...pendingAppMlflowTraceInfo]);
564
+ await directMlflowProcessor?.forceFlush();
565
+ await directMlflowProvider?.forceFlush();
566
+ }
567
+
217
568
  /**
218
569
  * Whether MLflow feedback logging is available for this deployment.
219
570
  *
220
- * Enabled when an OTLP exporter endpoint is configured (traces are
221
- * actually shipped somewhere) AND an MLflow experiment is named - the
222
- * two signals that the OTLP backend is MLflow and traces will
223
- * materialize there. Both are standard env vars, so no plugin config is
224
- * required; a deployment opts in simply by wiring MLflow tracing.
571
+ * Enabled when an MLflow experiment is configured and traces have an export
572
+ * path: AppKit OTLP in a Databricks App, or the direct MLflow Node SDK outside
573
+ * one. Both paths are selected from standard environment values.
225
574
  */
226
575
  export function mlflowEnabled(): boolean {
227
576
  const hasExporter = Boolean(
@@ -231,7 +580,7 @@ export function mlflowEnabled(): boolean {
231
580
  const hasExperiment = Boolean(
232
581
  process.env.MLFLOW_EXPERIMENT_ID?.trim() || process.env.MLFLOW_EXPERIMENT_NAME?.trim(),
233
582
  );
234
- return hasExporter && hasExperiment;
583
+ return hasExperiment && (hasExporter || directMlflowTracingConfigured());
235
584
  }
236
585
 
237
586
  /**
package/src/model.ts CHANGED
@@ -37,6 +37,7 @@ import {
37
37
  rewriteServingResponseStream,
38
38
  } from "./serving-sanitize.ts";
39
39
  import { MASTRA_MODEL_OVERRIDE_KEY, resolveServingConfig } from "./serving.ts";
40
+ import { recordActiveTraceAuth, recordActiveTraceModel } from "./telemetry.ts";
40
41
 
41
42
  type ModelClass = model.ModelClass;
42
43
  const { parseModelClass } = classes;
@@ -135,6 +136,7 @@ export async function buildModel(
135
136
  // (the active OBO scope, or the service principal) when it's absent.
136
137
  const user = requestContext.get(MASTRA_USER_KEY) as User | undefined;
137
138
  const executionContext = user?.executionContext ?? getExecutionContext();
139
+ recordActiveTraceAuth("isUserContext" in executionContext ? "obo" : "service-principal");
138
140
  const clientConfig = executionContext.client.config;
139
141
  const host = (await clientConfig.getHost()).toString();
140
142
  const headers = new Headers();
@@ -165,6 +167,7 @@ export async function buildModel(
165
167
  selectionInput(config, requested, overrides.modelClass, serving),
166
168
  );
167
169
  logger.debug("model selected", { modelId, source, requested });
170
+ recordActiveTraceModel(modelId);
168
171
 
169
172
  return {
170
173
  providerId: config.providerId ?? "databricks",
@@ -49,7 +49,11 @@ import { OtelBridge } from "@mastra/otel-bridge";
49
49
  import { propagation } from "@opentelemetry/api";
50
50
 
51
51
  import { TRACE_REQUEST_CONTEXT_KEYS } from "./config.ts";
52
- import { mlflowEnabled } from "./mlflow.ts";
52
+ import {
53
+ initializeAppMlflowTraceInfo,
54
+ initializeDirectMlflowTracing,
55
+ mlflowEnabled,
56
+ } from "./mlflow.ts";
53
57
 
54
58
  const logger = log.logger("mastra/observability");
55
59
 
@@ -117,7 +121,8 @@ export function configureOtelPropagation(): boolean {
117
121
  * OTel pipeline via `@mastra/otel-bridge`.
118
122
  *
119
123
  * Returns `undefined` when tracing is off: either the caller passed
120
- * `enabled: false`, or auto mode finds no OTLP endpoint (the default).
124
+ * `enabled: false`, or auto mode finds neither AppKit OTLP nor a local MLflow
125
+ * experiment.
121
126
  * Skipping the bridge in that case avoids `[OtelBridge] No OTEL span
122
127
  * found` log spam from Mastra spans that have no parent on the noop
123
128
  * tracer.
@@ -128,13 +133,19 @@ export async function buildObservability(
128
133
  const otelBase = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
129
134
  const otelTracesOverride = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
130
135
  const otlpConfigured = isOtlpTracingConfigured();
136
+ const appMlflowTraceInfo =
137
+ options?.enabled === false ? false : await initializeAppMlflowTraceInfo();
138
+ const directMlflowConfigured =
139
+ options?.enabled === false || otlpConfigured ? false : await initializeDirectMlflowTracing();
140
+ if (directMlflowConfigured) configureOtelPropagation();
141
+ const tracingConfigured = otlpConfigured || directMlflowConfigured;
131
142
 
132
- if (options?.enabled === false || (options?.enabled !== true && !otlpConfigured)) {
143
+ if (options?.enabled === false || (options?.enabled !== true && !tracingConfigured)) {
133
144
  logger.info("Mastra observability off", {
134
145
  reason:
135
146
  options?.enabled === false
136
147
  ? "disabled in plugin config"
137
- : "OTEL_EXPORTER_OTLP_ENDPOINT unset",
148
+ : "no OTLP endpoint or direct MLflow experiment configured",
138
149
  });
139
150
  return undefined;
140
151
  }
@@ -164,7 +175,13 @@ export async function buildObservability(
164
175
  otelBase: otelBase ?? "<unset>",
165
176
  resolvedTracesUrl: resolvedTracesUrl ?? "<unset>",
166
177
  feedback,
167
- observability: feedback ? "mlflow" : "otel",
178
+ observability: directMlflowConfigured
179
+ ? "mlflow-direct"
180
+ : appMlflowTraceInfo
181
+ ? "mlflow-apps"
182
+ : feedback
183
+ ? "mlflow"
184
+ : "otel",
168
185
  });
169
186
 
170
187
  return new Observability({
package/src/plugin.ts CHANGED
@@ -100,6 +100,7 @@ import {
100
100
  import { buildMcpServer, type ResolvedMcp } from "./mcp.ts";
101
101
  import { createMemoryBuilder, createServicePrincipalPool, needsLakebase } from "./memory.ts";
102
102
  import {
103
+ flushDirectMlflowTracing,
103
104
  logFeedback,
104
105
  mlflowExperimentManagerUrl,
105
106
  resolveFeedbackEnabled,
@@ -304,6 +305,13 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
304
305
  * shutdown budget.
305
306
  */
306
307
  async shutdown(): Promise<void> {
308
+ try {
309
+ await flushDirectMlflowTracing();
310
+ } catch (err) {
311
+ this.logger.warn("error flushing direct MLflow traces", {
312
+ error: errorUtils.errorMessage(err),
313
+ });
314
+ }
307
315
  const pool = this.servicePrincipalPool;
308
316
  if (!pool) return;
309
317
  this.servicePrincipalPool = null;
package/src/server.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  } from "./config.ts";
30
30
  import { requestUserEmail, requestUserId } from "./identity.ts";
31
31
  import { resolveFeedbackEnabled } from "./mlflow.ts";
32
+ import { recordActiveTraceAuth, recordActiveTraceUser } from "./telemetry.ts";
32
33
 
33
34
  import {
34
35
  extractModelOverride,
@@ -222,6 +223,18 @@ export class MastraServer extends ExpressMastraServer {
222
223
  userId: requestUserId(req),
223
224
  email: requestUserEmail(req),
224
225
  });
226
+ const user = requestContext.get(MASTRA_USER_KEY) as User | undefined;
227
+ if (user) {
228
+ recordActiveTraceAuth(
229
+ "isUserContext" in user.executionContext ? "obo" : "service-principal",
230
+ );
231
+ }
232
+ const traceUser = [
233
+ requestContext.get(MASTRA_USER_EMAIL_KEY),
234
+ requestContext.get(MASTRA_USER_NAME_KEY),
235
+ requestContext.get(MASTRA_RESOURCE_ID_KEY),
236
+ ].find((value): value is string => typeof value === "string" && value.trim().length > 0);
237
+ recordActiveTraceUser(traceUser);
225
238
  }
226
239
 
227
240
  /**