@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/README.md +29 -3
- package/exports.ts +7 -0
- package/index.ts +8 -4
- package/lib/exports.d.ts +1 -0
- package/lib/exports.js +2 -0
- package/lib/index.d.ts +8 -4
- package/lib/index.js +7 -3
- package/lib/src/genie.js +3 -2
- package/lib/src/mlflow.d.ts +68 -5
- package/lib/src/mlflow.js +310 -8
- package/lib/src/model.js +4 -1
- package/lib/src/observability.d.ts +2 -1
- package/lib/src/observability.js +18 -6
- package/lib/src/plugin.js +10 -2
- package/lib/src/server.js +12 -1
- package/lib/src/telemetry.d.ts +17 -16
- package/lib/src/telemetry.js +177 -23
- package/lib/src/trace-attributes.d.ts +26 -0
- package/lib/src/trace-attributes.js +27 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +30 -16
- package/src/genie.ts +2 -1
- package/src/mlflow.ts +356 -7
- package/src/model.ts +3 -0
- package/src/observability.ts +22 -5
- package/src/plugin.ts +8 -0
- package/src/server.ts +13 -0
- package/src/telemetry.ts +238 -23
- package/src/trace-attributes.ts +38 -0
package/package.json
CHANGED
|
@@ -19,8 +19,7 @@
|
|
|
19
19
|
"projen": "projen"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@
|
|
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.
|
|
37
|
-
"@dbx-tools/core": "^0.9.
|
|
38
|
-
"@dbx-tools/databricks": "^0.9.
|
|
39
|
-
"@dbx-tools/fs": "^0.9.
|
|
40
|
-
"@dbx-tools/genie": "^0.9.
|
|
41
|
-
"@dbx-tools/model": "^0.9.
|
|
42
|
-
"@dbx-tools/path": "^0.9.
|
|
43
|
-
"@dbx-tools/postgres": "^0.9.
|
|
44
|
-
"@dbx-tools/shared-core": "^0.9.
|
|
45
|
-
"@dbx-tools/shared-fs": "^0.9.
|
|
46
|
-
"@dbx-tools/shared-genie": "^0.9.
|
|
47
|
-
"@dbx-tools/shared-mastra": "^0.9.
|
|
48
|
-
"@dbx-tools/shared-model": "^0.9.
|
|
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.
|
|
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
|
|
221
|
-
*
|
|
222
|
-
*
|
|
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
|
|
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",
|
package/src/observability.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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 && !
|
|
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
|
-
: "
|
|
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:
|
|
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
|
/**
|