@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/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, environmentUtils, errorUtils, log, object } from "@dbx-tools/shared-core";
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 (environmentUtils.isDatabricksAppEnv()) return undefined;
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
- !environmentUtils.isDatabricksAppEnv() &&
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
  }
@@ -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 { initializeDirectMlflowTracing, 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
 
@@ -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 ? "mlflow-direct" : feedback ? "mlflow" : "otel",
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 { environmentUtils, json, log, object, stringUtils } from "@dbx-tools/shared-core";
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 (!environmentUtils.isDatabricksAppEnv()) {
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
- (textOnlyChatInput(messages) ?? fullInput).slice(0, TRACE_IO_LIMIT),
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;