@dbx-tools/appkit-mastra 0.1.111 → 0.3.1

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.
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Mastra observability wired through the same OTel pipeline AppKit's
3
+ * built-in plugins (e.g. `agents`) use, via `@mastra/otel-bridge`.
4
+ *
5
+ * How traces flow:
6
+ *
7
+ * 1. `@databricks/appkit` boots a global `NodeSDK` in
8
+ * `TelemetryManager.initialize()` (during `createApp`) when
9
+ * `OTEL_EXPORTER_OTLP_ENDPOINT` is set in the process env.
10
+ * 2. Every AppKit plugin span (e.g. the `agents` plugin's
11
+ * `executeStream`) is created via the global OTel tracer
12
+ * (`trace.getTracer(<plugin>)`), so it lands on that NodeSDK and
13
+ * is shipped through its OTLP exporter.
14
+ * 3. The Mastra `OtelBridge` ALSO creates real OTel spans on the same
15
+ * global tracer for every Mastra operation (agent runs, model
16
+ * calls, tool invocations, workflow steps). They inherit the
17
+ * ambient OTel context, so when Mastra is invoked from inside an
18
+ * AppKit HTTP span the trace stays connected.
19
+ *
20
+ * Net effect: Mastra spans get exactly the treatment AppKit's
21
+ * `agents` plugin gets. No custom OTLP pipeline lives in this
22
+ * package; the OTLP endpoint, headers, and resource attributes are
23
+ * driven by the standard OTel env vars
24
+ * (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_HEADERS`,
25
+ * `OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`, ...) and consumed
26
+ * by AppKit's `TelemetryManager`. Set those once and both AppKit and
27
+ * Mastra spans end up at the same backend.
28
+ *
29
+ * When `OTEL_EXPORTER_OTLP_ENDPOINT` is unset the bridge is not
30
+ * registered at all (unless `observability: true` forces it on), so
31
+ * Mastra does not emit `[OtelBridge] No OTEL span found` warnings.
32
+ */
33
+
34
+ import { Observability } from "@mastra/observability";
35
+ import { OtelBridge } from "@mastra/otel-bridge";
36
+
37
+ import { TRACE_REQUEST_CONTEXT_KEYS } from "./config";
38
+ import { log } from "@dbx-tools/shared-core";
39
+ import { project } from "@dbx-tools/core";
40
+
41
+ const logger = log.logger("mastra/observability");
42
+
43
+ const DEFAULT_SERVICE_NAME = "mastra";
44
+
45
+ export interface BuildObservabilityOptions {
46
+ /**
47
+ * Whether to wire the Mastra `OtelBridge`. Mirrors
48
+ * {@link MastraPluginConfig.observability}:
49
+ *
50
+ * - `undefined` (auto): on only when `OTEL_EXPORTER_OTLP_ENDPOINT` or
51
+ * `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` is set (same signal AppKit's
52
+ * `TelemetryManager` uses to register a real tracer). When unset,
53
+ * skip the bridge entirely so Mastra does not emit
54
+ * `[OtelBridge] No OTEL span found` warnings on the noop tracer.
55
+ * - `true`: force on even without an OTLP endpoint.
56
+ * - `false`: force off.
57
+ */
58
+ enabled?: boolean;
59
+ /**
60
+ * Service name attached to the Mastra `Observability` config. Used
61
+ * as the tracer scope name on bridged OTel spans (the `service.name`
62
+ * resource attribute is owned by AppKit's `TelemetryManager` instead
63
+ * - it reads `OTEL_SERVICE_NAME` / `DATABRICKS_APP_NAME` at
64
+ * `createApp` time).
65
+ *
66
+ * Defaults to project name then `"mastra"`.
67
+ */
68
+ serviceName?: string;
69
+ /**
70
+ * `RequestContext` keys to extract as span metadata on every Mastra
71
+ * trace. Defaults to {@link TRACE_REQUEST_CONTEXT_KEYS} (user id,
72
+ * thread id, request id, environment, model override, ...).
73
+ *
74
+ * Supports dot notation for nested values per the Mastra docs.
75
+ */
76
+ requestContextKeys?: readonly string[];
77
+ }
78
+
79
+ /** True when AppKit's global OTel pipeline is configured to export traces. */
80
+ export function isOtlpTracingConfigured(): boolean {
81
+ return Boolean(
82
+ process.env.OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Build a Mastra `Observability` whose spans ride AppKit's global
88
+ * OTel pipeline via `@mastra/otel-bridge`.
89
+ *
90
+ * Returns `undefined` when tracing is off: either the caller passed
91
+ * `enabled: false`, or auto mode finds no OTLP endpoint (the default).
92
+ * Skipping the bridge in that case avoids `[OtelBridge] No OTEL span
93
+ * found` log spam from Mastra spans that have no parent on the noop
94
+ * tracer.
95
+ */
96
+ export async function buildObservability(
97
+ options?: BuildObservabilityOptions,
98
+ ): Promise<Observability | undefined> {
99
+ const otelBase = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
100
+ const otelTracesOverride = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
101
+ const otlpConfigured = isOtlpTracingConfigured();
102
+
103
+ if (options?.enabled === false || (options?.enabled !== true && !otlpConfigured)) {
104
+ logger.info("Mastra observability off", {
105
+ reason:
106
+ options?.enabled === false
107
+ ? "disabled in plugin config"
108
+ : "OTEL_EXPORTER_OTLP_ENDPOINT unset",
109
+ });
110
+ return undefined;
111
+ }
112
+
113
+ const serviceName = options?.serviceName ?? (await project.name()) ?? DEFAULT_SERVICE_NAME;
114
+ const requestContextKeys = [...(options?.requestContextKeys ?? TRACE_REQUEST_CONTEXT_KEYS)];
115
+
116
+ // The OTel HTTP exporter treats `OTEL_EXPORTER_OTLP_ENDPOINT` as a
117
+ // *base* URL and appends the signal path itself (e.g.
118
+ // `http://localhost:6006` -> `http://localhost:6006/v1/traces`). Log
119
+ // the resolved POST URL so misconfigurations (e.g. accidentally
120
+ // setting the base var to a `/v1/traces`-suffixed URL, which makes
121
+ // the SDK POST to `.../v1/traces/v1/traces` and Phoenix 404s) are
122
+ // obvious in startup output.
123
+ const resolvedTracesUrl = otelTracesOverride
124
+ ? otelTracesOverride
125
+ : otelBase
126
+ ? `${otelBase.replace(/\/+$/, "")}/v1/traces`
127
+ : undefined;
128
+ logger.info("Mastra observability wired through OTel bridge", {
129
+ serviceName,
130
+ requestContextKeys,
131
+ otelBase: otelBase ?? "<unset>",
132
+ resolvedTracesUrl: resolvedTracesUrl ?? "<unset>",
133
+ });
134
+
135
+ return new Observability({
136
+ configs: {
137
+ serviceName: {
138
+ serviceName,
139
+ bridge: new OtelBridge(),
140
+ requestContextKeys,
141
+ },
142
+ },
143
+ });
144
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Query-parameter coercion shared by the paginated custom routes
3
+ * (`history` / `threads`). Each route keeps its own page-size default
4
+ * and hard cap and passes them in, so the coercion rules stay in one
5
+ * place without collapsing the routes' distinct sizing.
6
+ */
7
+
8
+ /** Bounds for {@link clampPerPage}. */
9
+ export interface PerPageBounds {
10
+ /** Page size used for empty / non-positive / non-numeric inputs. */
11
+ fallback: number;
12
+ /** Hard cap so a misbehaving client can't fetch everything at once. */
13
+ max: number;
14
+ }
15
+
16
+ /** Coerce / clamp a `perPage` value, falling back to `bounds.fallback`. */
17
+ export function clampPerPage(value: number | undefined, bounds: PerPageBounds): number {
18
+ if (value === undefined || Number.isNaN(value)) return bounds.fallback;
19
+ const n = Math.trunc(value);
20
+ if (n <= 0) return bounds.fallback;
21
+ return Math.min(n, bounds.max);
22
+ }
23
+
24
+ /**
25
+ * Coerce a Hono query value into a non-negative integer. Returns
26
+ * `undefined` for empty / non-numeric / negative inputs so the caller
27
+ * can apply its built-in defaults.
28
+ */
29
+ export function parseIntParam(value: string | undefined): number | undefined {
30
+ if (!value) return undefined;
31
+ const n = Number(value);
32
+ if (!Number.isFinite(n) || n < 0) return undefined;
33
+ return Math.trunc(n);
34
+ }