@dbx-tools/appkit-mastra 0.9.67 → 0.9.69
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/index.ts +2 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +2 -2
- package/lib/src/mlflow.d.ts +20 -0
- package/lib/src/mlflow.js +101 -5
- package/lib/src/observability.js +10 -3
- package/lib/src/telemetry.js +21 -5
- package/package.json +14 -14
- package/src/mlflow.ts +116 -4
- package/src/observability.ts +14 -2
- package/src/telemetry.ts +20 -3
package/src/mlflow.ts
CHANGED
|
@@ -25,10 +25,13 @@
|
|
|
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
|
-
import { asyncUtils,
|
|
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";
|
|
32
35
|
import type { Context } from "@opentelemetry/api";
|
|
33
36
|
import type {
|
|
34
37
|
ReadableSpan,
|
|
@@ -49,6 +52,10 @@ let directMlflow: MlflowModule | undefined;
|
|
|
49
52
|
let directMlflowInitStarted = false;
|
|
50
53
|
let directMlflowProvider: NodeTracerProvider | undefined;
|
|
51
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>>();
|
|
52
59
|
|
|
53
60
|
const NOISY_INSTRUMENTATION_SCOPES = new Set([
|
|
54
61
|
"@opentelemetry/instrumentation-express",
|
|
@@ -295,7 +302,7 @@ function configuredExperiment(): { id?: string; name?: string } {
|
|
|
295
302
|
|
|
296
303
|
/** Tracking URI for direct local MLflow tracing, with CLI-profile auth injected. */
|
|
297
304
|
export function directMlflowTrackingUri(): string | undefined {
|
|
298
|
-
if (
|
|
305
|
+
if (appkit.isDatabricksAppEnv()) return undefined;
|
|
299
306
|
const configured = process.env.MLFLOW_TRACKING_URI?.trim();
|
|
300
307
|
if (configured && configured !== "databricks") return configured;
|
|
301
308
|
const profile = process.env.DATABRICKS_CONFIG_PROFILE?.trim();
|
|
@@ -329,12 +336,116 @@ export function directMlflowTraceLocation():
|
|
|
329
336
|
/** Whether the local direct-to-experiment MLflow SDK has enough configuration. */
|
|
330
337
|
export function directMlflowTracingConfigured(): boolean {
|
|
331
338
|
return Boolean(
|
|
332
|
-
!
|
|
339
|
+
!appkit.isDatabricksAppEnv() &&
|
|
333
340
|
process.env.MLFLOW_EXPERIMENT_ID?.trim() &&
|
|
334
341
|
directMlflowTrackingUri(),
|
|
335
342
|
);
|
|
336
343
|
}
|
|
337
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
|
+
|
|
338
449
|
async function detectedDirectMlflowTraceLocation(
|
|
339
450
|
mlflow: MlflowModule,
|
|
340
451
|
trackingUri: string,
|
|
@@ -449,6 +560,7 @@ export function directMlflowTracingActive(): boolean {
|
|
|
449
560
|
|
|
450
561
|
/** Flush pending direct local trace exports during graceful shutdown. */
|
|
451
562
|
export async function flushDirectMlflowTracing(): Promise<void> {
|
|
563
|
+
await Promise.all([...pendingAppMlflowTraceInfo]);
|
|
452
564
|
await directMlflowProcessor?.forceFlush();
|
|
453
565
|
await directMlflowProvider?.forceFlush();
|
|
454
566
|
}
|
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
|
|
|
@@ -129,6 +133,8 @@ export async function buildObservability(
|
|
|
129
133
|
const otelBase = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
130
134
|
const otelTracesOverride = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
|
|
131
135
|
const otlpConfigured = isOtlpTracingConfigured();
|
|
136
|
+
const appMlflowTraceInfo =
|
|
137
|
+
options?.enabled === false ? false : await initializeAppMlflowTraceInfo();
|
|
132
138
|
const directMlflowConfigured =
|
|
133
139
|
options?.enabled === false || otlpConfigured ? false : await initializeDirectMlflowTracing();
|
|
134
140
|
if (directMlflowConfigured) configureOtelPropagation();
|
|
@@ -169,7 +175,13 @@ export async function buildObservability(
|
|
|
169
175
|
otelBase: otelBase ?? "<unset>",
|
|
170
176
|
resolvedTracesUrl: resolvedTracesUrl ?? "<unset>",
|
|
171
177
|
feedback,
|
|
172
|
-
observability: directMlflowConfigured
|
|
178
|
+
observability: directMlflowConfigured
|
|
179
|
+
? "mlflow-direct"
|
|
180
|
+
: appMlflowTraceInfo
|
|
181
|
+
? "mlflow-apps"
|
|
182
|
+
: feedback
|
|
183
|
+
? "mlflow"
|
|
184
|
+
: "otel",
|
|
173
185
|
});
|
|
174
186
|
|
|
175
187
|
return new Observability({
|
package/src/telemetry.ts
CHANGED
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
|
|
21
21
|
import { StringDecoder } from "node:string_decoder";
|
|
22
22
|
|
|
23
|
+
import { appkit } from "@dbx-tools/appkit";
|
|
23
24
|
import { getRequestTags } from "@dbx-tools/appkit/request-tags";
|
|
24
|
-
import {
|
|
25
|
+
import { json, log, object, stringUtils } from "@dbx-tools/shared-core";
|
|
25
26
|
import { feedback, thread } from "@dbx-tools/shared-mastra";
|
|
26
27
|
import { context, createContextKey, SpanKind, trace, type Span } from "@opentelemetry/api";
|
|
27
28
|
import { getRPCMetadata, RPCType } from "@opentelemetry/core";
|
|
@@ -30,6 +31,7 @@ import type express from "express";
|
|
|
30
31
|
import { USER_EMAIL_HEADER, USER_ID_HEADER } from "./identity.ts";
|
|
31
32
|
import {
|
|
32
33
|
directMlflowTracingActive,
|
|
34
|
+
persistAppMlflowTraceInfo,
|
|
33
35
|
updateActiveMlflowTrace,
|
|
34
36
|
updateActiveMlflowTraceTags,
|
|
35
37
|
} from "./mlflow.ts";
|
|
@@ -505,8 +507,9 @@ export function chatTurnTelemetryMiddleware(
|
|
|
505
507
|
return;
|
|
506
508
|
}
|
|
507
509
|
const traceState: ChatTraceState = { models: new Set(), tags: new Map() };
|
|
510
|
+
const traceRequestTime = Date.now();
|
|
508
511
|
setChatTraceTag(traceState, MLFLOW_AGENT_TAG, true);
|
|
509
|
-
if (!
|
|
512
|
+
if (!appkit.isDatabricksAppEnv()) {
|
|
510
513
|
setChatTraceTag(traceState, MLFLOW_LOCAL_TAG, true);
|
|
511
514
|
}
|
|
512
515
|
for (const [name, value] of Object.entries(getRequestTags(req))) {
|
|
@@ -536,16 +539,19 @@ export function chatTurnTelemetryMiddleware(
|
|
|
536
539
|
target.span.setAttribute(CHAT_GENIE_USED_ATTR, false);
|
|
537
540
|
|
|
538
541
|
const messages = (req.body as { messages?: unknown } | undefined)?.messages;
|
|
542
|
+
let requestPreview: string | undefined;
|
|
539
543
|
if (messages !== undefined) {
|
|
540
544
|
const fullInput = serialized(messages);
|
|
545
|
+
requestPreview = textOnlyChatInput(messages) ?? fullInput;
|
|
541
546
|
target.span.setAttribute(CHAT_MESSAGES_ATTR, fullInput);
|
|
542
547
|
target.span.setAttribute(
|
|
543
548
|
MLFLOW_SPAN_INPUTS_ATTR,
|
|
544
|
-
|
|
549
|
+
requestPreview.slice(0, TRACE_IO_LIMIT),
|
|
545
550
|
);
|
|
546
551
|
}
|
|
547
552
|
|
|
548
553
|
const collector = new AssistantResponseCollector();
|
|
554
|
+
let responsePreview: string | undefined;
|
|
549
555
|
let outputRecorded = false;
|
|
550
556
|
let fullOutputRecorded = false;
|
|
551
557
|
let genieRecorded = false;
|
|
@@ -556,6 +562,7 @@ export function chatTurnTelemetryMiddleware(
|
|
|
556
562
|
const passThroughJson = res.json.bind(res) as (body?: unknown) => express.Response;
|
|
557
563
|
const recordOutput = (answer: string): void => {
|
|
558
564
|
if (outputRecorded || !answer) return;
|
|
565
|
+
responsePreview = answer;
|
|
559
566
|
target.span.setAttribute(MLFLOW_SPAN_OUTPUTS_ATTR, answer.slice(0, TRACE_IO_LIMIT));
|
|
560
567
|
outputRecorded = true;
|
|
561
568
|
};
|
|
@@ -590,6 +597,16 @@ export function chatTurnTelemetryMiddleware(
|
|
|
590
597
|
...(traceState.user ? { user: traceState.user } : {}),
|
|
591
598
|
});
|
|
592
599
|
});
|
|
600
|
+
persistAppMlflowTraceInfo({
|
|
601
|
+
traceId: targetTraceId,
|
|
602
|
+
requestTime: traceRequestTime,
|
|
603
|
+
executionDuration: Date.now() - traceRequestTime,
|
|
604
|
+
error: res.statusCode >= 500,
|
|
605
|
+
tags,
|
|
606
|
+
...(traceState.user ? { user: traceState.user } : {}),
|
|
607
|
+
...(requestPreview ? { requestPreview } : {}),
|
|
608
|
+
...(responsePreview ? { responsePreview } : {}),
|
|
609
|
+
});
|
|
593
610
|
};
|
|
594
611
|
const endOwnedSpan = (): void => {
|
|
595
612
|
if (!target.owned || ownedSpanEnded) return;
|