@dbx-tools/appkit-mastra 0.9.65 → 0.9.67

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/telemetry.ts CHANGED
@@ -20,13 +20,46 @@
20
20
 
21
21
  import { StringDecoder } from "node:string_decoder";
22
22
 
23
- import { json, log, object, stringUtils } from "@dbx-tools/shared-core";
23
+ import { getRequestTags } from "@dbx-tools/appkit/request-tags";
24
+ import { environmentUtils, json, log, object, stringUtils } from "@dbx-tools/shared-core";
24
25
  import { feedback, thread } from "@dbx-tools/shared-mastra";
25
- import { context, SpanKind, trace, type Span } from "@opentelemetry/api";
26
+ import { context, createContextKey, SpanKind, trace, type Span } from "@opentelemetry/api";
26
27
  import { getRPCMetadata, RPCType } from "@opentelemetry/core";
27
28
  import type express from "express";
28
29
 
29
30
  import { USER_EMAIL_HEADER, USER_ID_HEADER } from "./identity.ts";
31
+ import {
32
+ directMlflowTracingActive,
33
+ updateActiveMlflowTrace,
34
+ updateActiveMlflowTraceTags,
35
+ } from "./mlflow.ts";
36
+ import {
37
+ MLFLOW_AGENT_TAG,
38
+ MLFLOW_AGENT_TAG_ATTR,
39
+ MLFLOW_GENIE_TAG,
40
+ MLFLOW_GENIE_TAG_ATTR,
41
+ MLFLOW_MODEL_TAG,
42
+ MLFLOW_LOCAL_TAG,
43
+ MLFLOW_OBO_AUTH_TAG,
44
+ MLFLOW_SP_AUTH_TAG,
45
+ MLFLOW_TRACE_TAG_PREFIX,
46
+ } from "./trace-attributes.ts";
47
+
48
+ export {
49
+ MLFLOW_AGENT_TAG,
50
+ MLFLOW_AGENT_TAG_ATTR,
51
+ MLFLOW_GENIE_TAG,
52
+ MLFLOW_GENIE_TAG_ATTR,
53
+ MLFLOW_MODEL_TAG,
54
+ MLFLOW_MODEL_TAG_ATTR,
55
+ MLFLOW_LOCAL_TAG,
56
+ MLFLOW_LOCAL_TAG_ATTR,
57
+ MLFLOW_OBO_AUTH_TAG,
58
+ MLFLOW_OBO_AUTH_TAG_ATTR,
59
+ MLFLOW_SP_AUTH_TAG,
60
+ MLFLOW_SP_AUTH_TAG_ATTR,
61
+ MLFLOW_TRACE_TAG_PREFIX,
62
+ } from "./trace-attributes.ts";
30
63
 
31
64
  const logger = log.logger("mastra/telemetry");
32
65
 
@@ -74,24 +107,19 @@ export const GEN_AI_OPERATION_NAME_ATTR = "gen_ai.operation.name";
74
107
  export const MLFLOW_USER_ATTR = "user.id";
75
108
  /** Root-span session id that enables the MLflow Sessions tab. */
76
109
  export const MLFLOW_SESSION_ATTR = "session.id";
77
- /**
78
- * Prefix Databricks/MLflow copies onto the unified-view `tags` map. The
79
- * suffix after this prefix is the tag name shown in the experiment UI.
80
- */
81
- export const MLFLOW_TRACE_TAG_PREFIX = "mlflow.trace.tag.";
82
- /** Experiment-UI tag marking a turn that called Genie. */
83
- export const MLFLOW_GENIE_TAG = "genie";
84
110
  /** Root-span type for a Mastra agent turn. */
85
111
  export const MLFLOW_SPAN_TYPE_AGENT = "AGENT";
86
112
  /** Span type for an `ask_genie` tool call. */
87
113
  export const MLFLOW_SPAN_TYPE_GENIE = "GENIE";
88
- /** Attribute key for the `genie` trace tag. */
89
- export const MLFLOW_GENIE_TAG_ATTR = `${MLFLOW_TRACE_TAG_PREFIX}${MLFLOW_GENIE_TAG}`;
90
114
 
91
115
  /** Root-span metadata known before a chat request is dispatched. */
92
116
  export interface ChatTurnTelemetryOptions {
93
117
  readonly identity?:
94
118
  "obo" | "service-principal" | ((request: express.Request) => "obo" | "service-principal");
119
+ /** Initial trace tags derived from the inbound request. */
120
+ readonly tags?:
121
+ | Readonly<Record<string, ChatTraceTagValue | undefined>>
122
+ | ((request: express.Request) => Readonly<Record<string, ChatTraceTagValue | undefined>>);
95
123
  }
96
124
 
97
125
  interface TraceTarget {
@@ -99,16 +127,20 @@ interface TraceTarget {
99
127
  owned: boolean;
100
128
  }
101
129
 
130
+ interface ChatTraceState {
131
+ readonly models: Set<string>;
132
+ readonly tags: Map<string, string>;
133
+ user?: string;
134
+ }
135
+
136
+ /** Values accepted by request-scoped trace-tag injection helpers. */
137
+ export type ChatTraceTagValue = string | number | boolean;
138
+
139
+ const CHAT_TRACE_STATE_KEY = createContextKey("@dbx-tools/appkit-mastra/chat-trace-state");
140
+
102
141
  /** OpenTelemetry's sentinel for "no valid trace" - 32 zero hex chars. */
103
142
  const INVALID_TRACE_ID = "0".repeat(32);
104
143
 
105
- /**
106
- * Publish the turn's MLflow trace id before the handler writes the body.
107
- *
108
- * The chat UI only shows thumbs when this header is present. Stamping it here
109
- * (on the same span the middleware just resolved) avoids relying on async
110
- * Express continuation still sitting inside `context.with()`.
111
- */
112
144
  /**
113
145
  * Mark the active span as a Genie call so the experiment UI types it `GENIE`
114
146
  * and tags the trace `genie=true`. Safe no-op when no recording span is active.
@@ -135,14 +167,89 @@ function requestQuery(req: express.Request, name: string): string | undefined {
135
167
  return stringUtils.trimToNull(typeof value === "string" ? value : undefined) ?? undefined;
136
168
  }
137
169
 
138
- function stampMlflowActor(span: Span, req: express.Request): void {
170
+ function stampMlflowActor(span: Span, req: express.Request): string | undefined {
139
171
  const user = requestHeader(req, USER_EMAIL_HEADER) ?? requestHeader(req, USER_ID_HEADER);
140
172
  if (user) span.setAttribute(MLFLOW_USER_ATTR, user);
141
173
  const session =
142
174
  requestHeader(req, thread.THREAD_ID_HEADER) ?? requestQuery(req, thread.THREAD_ID_QUERY);
143
175
  if (session) span.setAttribute(MLFLOW_SESSION_ATTR, session);
176
+ return user;
177
+ }
178
+
179
+ function traceId(span: Span | undefined = trace.getActiveSpan()): string | undefined {
180
+ const value = span?.spanContext().traceId;
181
+ return value && value !== INVALID_TRACE_ID ? value : undefined;
182
+ }
183
+
184
+ function activeChatTraceState(): ChatTraceState | undefined {
185
+ return context.active().getValue(CHAT_TRACE_STATE_KEY) as ChatTraceState | undefined;
186
+ }
187
+
188
+ function setChatTraceTag(
189
+ state: ChatTraceState,
190
+ name: string,
191
+ value: ChatTraceTagValue,
192
+ ): void {
193
+ const tag = stringUtils.trimToNull(name);
194
+ if (!tag) return;
195
+ state.tags.set(tag, String(value));
196
+ }
197
+
198
+ /** Inject one MLflow trace tag into the active chat request. */
199
+ export function recordActiveTraceTag(
200
+ name: string,
201
+ value: ChatTraceTagValue = true,
202
+ ): void {
203
+ const state = activeChatTraceState();
204
+ if (state) setChatTraceTag(state, name, value);
205
+ }
206
+
207
+ /** Inject multiple MLflow trace tags into the active chat request. */
208
+ export function recordActiveTraceTags(
209
+ tags: Readonly<Record<string, ChatTraceTagValue | undefined>>,
210
+ ): void {
211
+ const state = activeChatTraceState();
212
+ if (!state) return;
213
+ for (const [name, value] of Object.entries(tags)) {
214
+ if (value !== undefined) setChatTraceTag(state, name, value);
215
+ }
216
+ }
217
+
218
+ /** Record a selected model on the active chat trace, preserving first-use order. */
219
+ export function recordActiveTraceModel(modelId: string): void {
220
+ const value = stringUtils.trimToNull(modelId);
221
+ if (!value) return;
222
+ activeChatTraceState()?.models.add(value);
144
223
  }
145
224
 
225
+ /** Record the best resolved user identifier on the active chat trace. */
226
+ export function recordActiveTraceUser(user: string | undefined): void {
227
+ const value = stringUtils.trimToNull(user);
228
+ if (!value) return;
229
+ const state = activeChatTraceState();
230
+ if (state) state.user = value;
231
+ }
232
+
233
+ /** Record one Databricks authentication mode used during the active chat trace. */
234
+ export function recordActiveTraceAuth(auth: "obo" | "service-principal"): void {
235
+ recordActiveTraceTag(auth === "obo" ? MLFLOW_OBO_AUTH_TAG : MLFLOW_SP_AUTH_TAG);
236
+ }
237
+
238
+ /** Serialize model tags as a scalar or an ordered JSON array for multiple values. */
239
+ export function modelTraceTag(models: Iterable<string>): string | undefined {
240
+ const values: string[] = [];
241
+ const seen = new Set<string>();
242
+ for (const model of models) {
243
+ const value = stringUtils.trimToNull(model);
244
+ if (!value || seen.has(value)) continue;
245
+ seen.add(value);
246
+ values.push(value);
247
+ }
248
+ if (values.length === 0) return undefined;
249
+ return values.length === 1 ? values[0] : JSON.stringify(values);
250
+ }
251
+
252
+ /** Publish the turn's MLflow trace id before the handler writes the body. */
146
253
  function stampMlflowTraceId(res: express.Response, span: Span): void {
147
254
  if (res.headersSent) return;
148
255
  const traceId = span.spanContext().traceId;
@@ -315,6 +422,28 @@ class AssistantResponseCollector {
315
422
  */
316
423
  function resolveTraceTarget(): TraceTarget | undefined {
317
424
  const activeContext = context.active();
425
+ if (directMlflowTracingActive()) {
426
+ const span = trace
427
+ .getTracer(CHAT_TURN_TRACER)
428
+ .startSpan(
429
+ "mastra.chat_turn",
430
+ {
431
+ kind: SpanKind.SERVER,
432
+ attributes: {
433
+ [MLFLOW_AGENT_TAG_ATTR]: "true",
434
+ [MLFLOW_SPAN_TYPE_ATTR]: MLFLOW_SPAN_TYPE_AGENT,
435
+ [GEN_AI_OPERATION_NAME_ATTR]: "invoke_agent",
436
+ },
437
+ },
438
+ trace.deleteSpan(activeContext),
439
+ );
440
+ if (!span.isRecording()) {
441
+ span.end();
442
+ return undefined;
443
+ }
444
+ return { span, owned: true };
445
+ }
446
+
318
447
  const rpc = getRPCMetadata(activeContext);
319
448
  if (rpc?.type === RPCType.HTTP && rpc.span.isRecording()) {
320
449
  return { span: rpc.span, owned: false };
@@ -325,7 +454,18 @@ function resolveTraceTarget(): TraceTarget | undefined {
325
454
 
326
455
  const span = trace
327
456
  .getTracer(CHAT_TURN_TRACER)
328
- .startSpan("mastra.chat_turn", { kind: SpanKind.SERVER }, activeContext);
457
+ .startSpan(
458
+ "mastra.chat_turn",
459
+ {
460
+ kind: SpanKind.SERVER,
461
+ attributes: {
462
+ [MLFLOW_AGENT_TAG_ATTR]: "true",
463
+ [MLFLOW_SPAN_TYPE_ATTR]: MLFLOW_SPAN_TYPE_AGENT,
464
+ [GEN_AI_OPERATION_NAME_ATTR]: "invoke_agent",
465
+ },
466
+ },
467
+ activeContext,
468
+ );
329
469
  if (!span.isRecording()) {
330
470
  span.end();
331
471
  return undefined;
@@ -358,14 +498,40 @@ export function chatTurnTelemetryMiddleware(
358
498
  next();
359
499
  return;
360
500
  }
501
+ const targetTraceId = traceId(target.span);
502
+ if (!targetTraceId) {
503
+ if (target.owned) target.span.end();
504
+ next();
505
+ return;
506
+ }
507
+ const traceState: ChatTraceState = { models: new Set(), tags: new Map() };
508
+ setChatTraceTag(traceState, MLFLOW_AGENT_TAG, true);
509
+ if (!environmentUtils.isDatabricksAppEnv()) {
510
+ setChatTraceTag(traceState, MLFLOW_LOCAL_TAG, true);
511
+ }
512
+ for (const [name, value] of Object.entries(getRequestTags(req))) {
513
+ setChatTraceTag(traceState, name, value);
514
+ }
515
+ const initialTags = typeof options.tags === "function" ? options.tags(req) : options.tags;
516
+ if (initialTags) {
517
+ for (const [name, value] of Object.entries(initialTags)) {
518
+ if (value !== undefined) setChatTraceTag(traceState, name, value);
519
+ }
520
+ }
361
521
  stampMlflowTraceId(res, target.span);
362
- stampMlflowActor(target.span, req);
522
+ traceState.user = stampMlflowActor(target.span, req);
363
523
  target.span.setAttribute(MLFLOW_SPAN_TYPE_ATTR, MLFLOW_SPAN_TYPE_AGENT);
364
524
  target.span.setAttribute(GEN_AI_OPERATION_NAME_ATTR, "invoke_agent");
525
+ target.span.setAttribute(MLFLOW_AGENT_TAG_ATTR, "true");
365
526
  const identity =
366
527
  typeof options.identity === "function" ? options.identity(req) : options.identity;
367
528
  if (identity) {
368
529
  target.span.setAttribute(CHAT_IDENTITY_ATTR, identity);
530
+ setChatTraceTag(
531
+ traceState,
532
+ identity === "obo" ? MLFLOW_OBO_AUTH_TAG : MLFLOW_SP_AUTH_TAG,
533
+ true,
534
+ );
369
535
  }
370
536
  target.span.setAttribute(CHAT_GENIE_USED_ATTR, false);
371
537
 
@@ -382,7 +548,9 @@ export function chatTurnTelemetryMiddleware(
382
548
  const collector = new AssistantResponseCollector();
383
549
  let outputRecorded = false;
384
550
  let fullOutputRecorded = false;
551
+ let genieRecorded = false;
385
552
  let ownedSpanEnded = false;
553
+ let traceFinalized = false;
386
554
  const passThroughWrite = res.write.bind(res) as (...args: unknown[]) => boolean;
387
555
  const passThroughEnd = res.end.bind(res) as (...args: unknown[]) => unknown;
388
556
  const passThroughJson = res.json.bind(res) as (body?: unknown) => express.Response;
@@ -397,9 +565,31 @@ export function chatTurnTelemetryMiddleware(
397
565
  fullOutputRecorded = true;
398
566
  };
399
567
  const recordGenie = (used: boolean): void => {
400
- if (!used) return;
568
+ if (!used || genieRecorded) return;
569
+ genieRecorded = true;
401
570
  target.span.setAttribute(CHAT_GENIE_USED_ATTR, true);
402
571
  target.span.setAttribute(MLFLOW_GENIE_TAG_ATTR, "true");
572
+ setChatTraceTag(traceState, MLFLOW_GENIE_TAG, true);
573
+ context.with(trace.setSpan(context.active(), target.span), () => {
574
+ updateActiveMlflowTraceTags({ [MLFLOW_GENIE_TAG]: "true" });
575
+ });
576
+ };
577
+ const finalizeTrace = (): void => {
578
+ if (traceFinalized) return;
579
+ traceFinalized = true;
580
+ const model = modelTraceTag(traceState.models);
581
+ if (model) setChatTraceTag(traceState, MLFLOW_MODEL_TAG, model);
582
+ const tags = Object.fromEntries(traceState.tags);
583
+ for (const [name, value] of traceState.tags) {
584
+ target.span.setAttribute(`${MLFLOW_TRACE_TAG_PREFIX}${name}`, value);
585
+ }
586
+ if (traceState.user) target.span.setAttribute(MLFLOW_USER_ATTR, traceState.user);
587
+ context.with(trace.setSpan(context.active(), target.span), () => {
588
+ updateActiveMlflowTrace({
589
+ ...(Object.keys(tags).length > 0 ? { tags } : {}),
590
+ ...(traceState.user ? { user: traceState.user } : {}),
591
+ });
592
+ });
403
593
  };
404
594
  const endOwnedSpan = (): void => {
405
595
  if (!target.owned || ownedSpanEnded) return;
@@ -424,6 +614,7 @@ export function chatTurnTelemetryMiddleware(
424
614
  recordOutput(collector.finish());
425
615
  recordFullOutput(collector.rawBody());
426
616
  recordGenie(collector.usedGenie());
617
+ finalizeTrace();
427
618
  try {
428
619
  return passThroughEnd(chunk, ...rest);
429
620
  } finally {
@@ -435,10 +626,17 @@ export function chatTurnTelemetryMiddleware(
435
626
  recordOutput(collector.finish());
436
627
  recordFullOutput(collector.rawBody());
437
628
  recordGenie(collector.usedGenie());
629
+ finalizeTrace();
438
630
  endOwnedSpan();
439
631
  });
440
632
 
441
- context.with(trace.setSpan(context.active(), target.span), next);
633
+ const chatContext = trace
634
+ .setSpan(context.active(), target.span)
635
+ .setValue(CHAT_TRACE_STATE_KEY, traceState);
636
+ context.with(chatContext, () => {
637
+ updateActiveMlflowTraceTags({ [MLFLOW_AGENT_TAG]: "true" });
638
+ next();
639
+ });
442
640
  }
443
641
 
444
642
  /**
@@ -0,0 +1,38 @@
1
+ /** Prefix Databricks/MLflow copies onto the unified-view trace tags map. */
2
+ export const MLFLOW_TRACE_TAG_PREFIX = "mlflow.traceTag.";
3
+
4
+ /** Experiment-UI tag marking every Mastra chat turn as an agent trace. */
5
+ export const MLFLOW_AGENT_TAG = "agent";
6
+
7
+ /** Experiment-UI tag marking a turn that called Genie. */
8
+ export const MLFLOW_GENIE_TAG = "genie";
9
+
10
+ /** Experiment-UI tag recording the model or ordered models used by a turn. */
11
+ export const MLFLOW_MODEL_TAG = "model";
12
+
13
+ /** Experiment-UI tag marking a turn that invoked OBO authentication. */
14
+ export const MLFLOW_OBO_AUTH_TAG = "obo_auth";
15
+
16
+ /** Experiment-UI tag marking a turn that invoked service-principal authentication. */
17
+ export const MLFLOW_SP_AUTH_TAG = "sp_auth";
18
+
19
+ /** Experiment-UI tag marking a turn emitted outside Databricks Apps. */
20
+ export const MLFLOW_LOCAL_TAG = "local";
21
+
22
+ /** Attribute key for the `agent` trace tag. */
23
+ export const MLFLOW_AGENT_TAG_ATTR = `${MLFLOW_TRACE_TAG_PREFIX}${MLFLOW_AGENT_TAG}`;
24
+
25
+ /** Attribute key for the `genie` trace tag. */
26
+ export const MLFLOW_GENIE_TAG_ATTR = `${MLFLOW_TRACE_TAG_PREFIX}${MLFLOW_GENIE_TAG}`;
27
+
28
+ /** Attribute key for the `model` trace tag. */
29
+ export const MLFLOW_MODEL_TAG_ATTR = `${MLFLOW_TRACE_TAG_PREFIX}${MLFLOW_MODEL_TAG}`;
30
+
31
+ /** Attribute key for the `obo_auth` trace tag. */
32
+ export const MLFLOW_OBO_AUTH_TAG_ATTR = `${MLFLOW_TRACE_TAG_PREFIX}${MLFLOW_OBO_AUTH_TAG}`;
33
+
34
+ /** Attribute key for the `sp_auth` trace tag. */
35
+ export const MLFLOW_SP_AUTH_TAG_ATTR = `${MLFLOW_TRACE_TAG_PREFIX}${MLFLOW_SP_AUTH_TAG}`;
36
+
37
+ /** Attribute key for the `local` trace tag. */
38
+ export const MLFLOW_LOCAL_TAG_ATTR = `${MLFLOW_TRACE_TAG_PREFIX}${MLFLOW_LOCAL_TAG}`;