@unhandled/instrumentation-mastra 0.0.0-stage → 0.0.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.
- package/README.md +78 -2
- package/build/attributes.d.ts +12 -0
- package/build/attributes.js +12 -0
- package/build/evaluations.d.ts +9 -0
- package/build/evaluations.js +34 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +3 -0
- package/build/integration.d.ts +4 -0
- package/build/integration.js +13 -0
- package/build/model_metadata.d.ts +13 -0
- package/build/model_metadata.js +99 -0
- package/build/normalize.d.ts +4 -0
- package/build/normalize.js +44 -0
- package/build/observability_config.d.ts +6 -0
- package/build/observability_config.js +8 -0
- package/build/span_processor.d.ts +10 -0
- package/build/span_processor.js +41 -0
- package/build/types.d.ts +33 -0
- package/build/types.js +1 -0
- package/package.json +66 -3
package/README.md
CHANGED
|
@@ -1,3 +1,79 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @unhandled/instrumentation-mastra
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Reshapes spans produced by Mastra's OpenTelemetry bridge into the attributes Unhandled's AI views read. It does not create spans and it does not create the bridge: you still configure `OtelBridge` yourself (the helper below does it for you).
|
|
4
|
+
|
|
5
|
+
Peers: `@mastra/core`, `@mastra/otel-bridge` (and `@mastra/observability`, which the bridge already needs).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @unhandled/instrumentation-mastra
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Register
|
|
14
|
+
|
|
15
|
+
With the AdonisJS agent, add the integration. It only registers the span processor:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { mastra } from '@unhandled/instrumentation-mastra/integration'
|
|
19
|
+
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
integrations: [mastra({ recordInputs: true, recordOutputs: true })],
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Other Node apps, add the processor to your tracer provider:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { MastraSpanProcessor } from '@unhandled/instrumentation-mastra'
|
|
29
|
+
|
|
30
|
+
const provider = new NodeTracerProvider({ spanProcessors: [new MastraSpanProcessor()] })
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then hand Mastra a bridge config:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { Observability, SamplingStrategyType } from '@mastra/observability'
|
|
37
|
+
import { createMastraObservabilityConfig } from '@unhandled/instrumentation-mastra'
|
|
38
|
+
|
|
39
|
+
export const observability = new Observability({
|
|
40
|
+
configs: {
|
|
41
|
+
default: createMastraObservabilityConfig({
|
|
42
|
+
serviceName: 'my-app',
|
|
43
|
+
sampling: { type: SamplingStrategyType.ALWAYS },
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`createMastraObservabilityConfig` adds an `OtelBridge` and sets `excludeSpanTypes` to `MODEL_STEP` and `MODEL_CHUNK`. Pass your own `excludeSpanTypes` (replaces the default) or `bridge` (for example an `OtelBridge` with a custom tracer provider) to override. Everything else in the options is forwarded as is.
|
|
50
|
+
|
|
51
|
+
## Options
|
|
52
|
+
|
|
53
|
+
| Option | Where | Default | Effect |
|
|
54
|
+
| -------------------------- | -------------------- | ------- | ------------------------------------------------------------------- |
|
|
55
|
+
| `recordInputs` | integration, processor | `false` | `true` keeps input content attributes. |
|
|
56
|
+
| `recordOutputs` | integration, processor | `false` | `true` keeps output content attributes and response previews. |
|
|
57
|
+
| `enabled` | integration | `true` | `false` skips the integration. |
|
|
58
|
+
| `maxBufferedTraces` | processor | `500` | Traces tracked for metadata propagation; the oldest is dropped first. |
|
|
59
|
+
| `maxBufferedSpansPerTrace` | processor | `256` | Spans tracked per trace before the oldest entry is dropped. |
|
|
60
|
+
|
|
61
|
+
Prompts and responses are dropped unless you opt in. With `recordInputs` or `recordOutputs` off, the processor also removes Mastra's own `mastra.*.input` and `mastra.*.output` attributes. These flags are independent of the Vercel AI integration's.
|
|
62
|
+
|
|
63
|
+
## What the processor changes
|
|
64
|
+
|
|
65
|
+
Mastra already writes most semconv names. The processor renames the rest, keeping a semconv value that is already set:
|
|
66
|
+
|
|
67
|
+
| Mastra attribute | Renamed to |
|
|
68
|
+
| ------------------------------------------- | --------------------------------------- |
|
|
69
|
+
| `gen_ai.usage.cached_input_tokens` | `gen_ai.usage.cache_read.input_tokens` |
|
|
70
|
+
| `gen_ai.usage.cache_creation.input_tokens` | `gen_ai.usage.cache_write.input_tokens` |
|
|
71
|
+
| `gen_ai.usage.reasoning_tokens` | `gen_ai.usage.reasoning.output_tokens` |
|
|
72
|
+
|
|
73
|
+
`gen_ai.agent.name` is also copied to `gen_ai.function_id` when that is not set.
|
|
74
|
+
|
|
75
|
+
Model metadata (request and response model, provider and, with `recordOutputs`, the response preview) is copied from each `chat` span to its nearest `invoke_agent` ancestor when the agent span lacks it, so an agent run shows its model. Usage stays on the `chat` span, so tokens and cost are counted once. Children are buffered per trace until the agent span ends, within the limits above. When several model calls run under one agent, the first one to end supplies each value.
|
|
76
|
+
|
|
77
|
+
## Evaluations
|
|
78
|
+
|
|
79
|
+
Span events named `gen_ai.evaluation.result` are recognised. `extractEvaluations(span)` returns `{ name, scoreValue, scoreLabel, explanation, scorerId, scorerVersion, targetTraceId }` per event, read from `gen_ai.evaluation.name`, `.score.value`, `.score.label`, `.explanation`, `.scorer.id`, `.scorer.version` and `.target.trace_id`. A numeric string score is exported as a number.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const MASTRA_ATTR: {
|
|
2
|
+
readonly agentName: "gen_ai.agent.name";
|
|
3
|
+
readonly cachedInputTokens: "gen_ai.usage.cached_input_tokens";
|
|
4
|
+
readonly reasoningTokens: "gen_ai.usage.reasoning_tokens";
|
|
5
|
+
readonly cacheCreationInputTokens: "gen_ai.usage.cache_creation.input_tokens";
|
|
6
|
+
};
|
|
7
|
+
export declare const EVALUATION_ATTR: {
|
|
8
|
+
readonly scorerId: "gen_ai.evaluation.scorer.id";
|
|
9
|
+
readonly scorerVersion: "gen_ai.evaluation.scorer.version";
|
|
10
|
+
readonly targetTraceId: "gen_ai.evaluation.target.trace_id";
|
|
11
|
+
};
|
|
12
|
+
export declare const MASTRA_CONTENT_PATTERN: RegExp;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const MASTRA_ATTR = {
|
|
2
|
+
agentName: "gen_ai.agent.name",
|
|
3
|
+
cachedInputTokens: "gen_ai.usage.cached_input_tokens",
|
|
4
|
+
reasoningTokens: "gen_ai.usage.reasoning_tokens",
|
|
5
|
+
cacheCreationInputTokens: "gen_ai.usage.cache_creation.input_tokens",
|
|
6
|
+
};
|
|
7
|
+
export const EVALUATION_ATTR = {
|
|
8
|
+
scorerId: "gen_ai.evaluation.scorer.id",
|
|
9
|
+
scorerVersion: "gen_ai.evaluation.scorer.version",
|
|
10
|
+
targetTraceId: "gen_ai.evaluation.target.trace_id",
|
|
11
|
+
};
|
|
12
|
+
export const MASTRA_CONTENT_PATTERN = /^mastra\.[a-z_]+\.(input|output)$/;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Attributes } from "@opentelemetry/api";
|
|
2
|
+
import type { TimedEvent } from "@opentelemetry/sdk-trace-base";
|
|
3
|
+
import type { MastraEvaluation } from "./types.ts";
|
|
4
|
+
export declare const isEvaluationEvent: (event: TimedEvent) => boolean;
|
|
5
|
+
export declare const readEvaluation: (attributes: Attributes) => MastraEvaluation;
|
|
6
|
+
export declare const extractEvaluations: (span: {
|
|
7
|
+
events: readonly TimedEvent[];
|
|
8
|
+
}) => MastraEvaluation[];
|
|
9
|
+
export declare const coerceEvaluationScores: (events: readonly TimedEvent[]) => void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ATTR, GEN_AI_EVENTS } from "@unhandled/protocol";
|
|
2
|
+
import { EVALUATION_ATTR } from "./attributes.js";
|
|
3
|
+
const text = (value) => (typeof value === "string" && value.length > 0 ? value : undefined);
|
|
4
|
+
const score = (value) => {
|
|
5
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
if (typeof value === "string" && value.trim().length > 0 && Number.isFinite(Number(value))) {
|
|
9
|
+
return Number(value);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
export const isEvaluationEvent = (event) => event.name === GEN_AI_EVENTS.evaluationResult;
|
|
13
|
+
export const readEvaluation = (attributes) => ({
|
|
14
|
+
name: text(attributes[ATTR.genAiEvaluationName]),
|
|
15
|
+
scoreValue: score(attributes[ATTR.genAiEvaluationScoreValue]),
|
|
16
|
+
scoreLabel: text(attributes[ATTR.genAiEvaluationScoreLabel]),
|
|
17
|
+
explanation: text(attributes[ATTR.genAiEvaluationExplanation]),
|
|
18
|
+
scorerId: text(attributes[EVALUATION_ATTR.scorerId]),
|
|
19
|
+
scorerVersion: text(attributes[EVALUATION_ATTR.scorerVersion]),
|
|
20
|
+
targetTraceId: text(attributes[EVALUATION_ATTR.targetTraceId]),
|
|
21
|
+
});
|
|
22
|
+
export const extractEvaluations = (span) => span.events.filter(isEvaluationEvent).map((event) => readEvaluation(event.attributes ?? {}));
|
|
23
|
+
export const coerceEvaluationScores = (events) => {
|
|
24
|
+
for (const event of events) {
|
|
25
|
+
if (!isEvaluationEvent(event) || event.attributes === undefined) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const raw = event.attributes[ATTR.genAiEvaluationScoreValue];
|
|
29
|
+
const value = score(raw);
|
|
30
|
+
if (typeof raw === "string" && value !== undefined) {
|
|
31
|
+
event.attributes[ATTR.genAiEvaluationScoreValue] = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { extractEvaluations } from "./evaluations.ts";
|
|
2
|
+
export type { MastraObservabilityConfig, MastraObservabilityConfigOptions, } from "./observability_config.ts";
|
|
3
|
+
export { createMastraObservabilityConfig, DEFAULT_EXCLUDED_SPAN_TYPES, } from "./observability_config.ts";
|
|
4
|
+
export { MastraSpanProcessor } from "./span_processor.ts";
|
|
5
|
+
export type { MastraEvaluation, MastraIntegrationOptions, MastraRecordingOptions, MastraSpanProcessorOptions, } from "./types.ts";
|
package/build/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { UnhandledIntegration } from "@unhandled/node/types";
|
|
2
|
+
import type { MastraIntegrationOptions } from "./types.ts";
|
|
3
|
+
export declare const mastra: (options?: MastraIntegrationOptions) => UnhandledIntegration;
|
|
4
|
+
export type { MastraIntegrationOptions } from "./types.ts";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { MastraSpanProcessor } from "./span_processor.js";
|
|
2
|
+
export const mastra = (options = {}) => ({
|
|
3
|
+
name: "mastra",
|
|
4
|
+
enabled: options.enabled ?? true,
|
|
5
|
+
setup: () => ({
|
|
6
|
+
spanProcessors: [
|
|
7
|
+
new MastraSpanProcessor({
|
|
8
|
+
recordInputs: options.recordInputs,
|
|
9
|
+
recordOutputs: options.recordOutputs,
|
|
10
|
+
}),
|
|
11
|
+
],
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MutableSpan, ResolvedRecording } from "./types.ts";
|
|
2
|
+
export interface ModelMetadataBufferOptions {
|
|
3
|
+
maxSpansPerTrace?: number;
|
|
4
|
+
maxTraces?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class ModelMetadataBuffer {
|
|
7
|
+
#private;
|
|
8
|
+
constructor(options?: ModelMetadataBufferOptions);
|
|
9
|
+
get traceCount(): number;
|
|
10
|
+
remember(traceId: string, spanId: string, parentSpanId: string | undefined, modelSpan: MutableSpan | undefined, recording: ResolvedRecording): void;
|
|
11
|
+
fillParent(traceId: string, parent: MutableSpan, parentSpanId: string): void;
|
|
12
|
+
endTrace(traceId: string): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ATTR } from "@unhandled/protocol";
|
|
2
|
+
const DEFAULT_MAX_TRACES = 500;
|
|
3
|
+
const DEFAULT_MAX_SPANS_PER_TRACE = 256;
|
|
4
|
+
const MAX_ANCESTOR_DEPTH = 32;
|
|
5
|
+
const MODEL_KEYS = [ATTR.genAiRequestModel, ATTR.genAiResponseModel, ATTR.genAiProviderName];
|
|
6
|
+
const RESPONSE_KEYS = [ATTR.genAiOutputMessages];
|
|
7
|
+
export class ModelMetadataBuffer {
|
|
8
|
+
#maxTraces;
|
|
9
|
+
#maxSpansPerTrace;
|
|
10
|
+
#traces = new Map();
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this.#maxTraces = options.maxTraces ?? DEFAULT_MAX_TRACES;
|
|
13
|
+
this.#maxSpansPerTrace = options.maxSpansPerTrace ?? DEFAULT_MAX_SPANS_PER_TRACE;
|
|
14
|
+
}
|
|
15
|
+
get traceCount() {
|
|
16
|
+
return this.#traces.size;
|
|
17
|
+
}
|
|
18
|
+
remember(traceId, spanId, parentSpanId, modelSpan, recording) {
|
|
19
|
+
const spans = this.#spansFor(traceId);
|
|
20
|
+
if (spans.size >= this.#maxSpansPerTrace) {
|
|
21
|
+
const oldest = spans.keys().next();
|
|
22
|
+
if (!oldest.done) {
|
|
23
|
+
spans.delete(oldest.value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
spans.set(spanId, {
|
|
27
|
+
parentSpanId,
|
|
28
|
+
metadata: modelSpan === undefined ? undefined : pickMetadata(modelSpan, recording),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
fillParent(traceId, parent, parentSpanId) {
|
|
32
|
+
const spans = this.#traces.get(traceId);
|
|
33
|
+
if (spans === undefined) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const claimed = [];
|
|
37
|
+
for (const [spanId, buffered] of spans) {
|
|
38
|
+
if (buffered.metadata === undefined) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!descendsFrom(spans, spanId, parentSpanId)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
for (const [key, value] of Object.entries(buffered.metadata)) {
|
|
45
|
+
if (value !== undefined && parent.attributes[key] === undefined) {
|
|
46
|
+
parent.attributes[key] = value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
claimed.push(spanId);
|
|
50
|
+
}
|
|
51
|
+
for (const spanId of claimed) {
|
|
52
|
+
const buffered = spans.get(spanId);
|
|
53
|
+
if (buffered !== undefined) {
|
|
54
|
+
buffered.metadata = undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
endTrace(traceId) {
|
|
59
|
+
this.#traces.delete(traceId);
|
|
60
|
+
}
|
|
61
|
+
#spansFor(traceId) {
|
|
62
|
+
const existing = this.#traces.get(traceId);
|
|
63
|
+
if (existing !== undefined) {
|
|
64
|
+
this.#traces.delete(traceId);
|
|
65
|
+
this.#traces.set(traceId, existing);
|
|
66
|
+
return existing;
|
|
67
|
+
}
|
|
68
|
+
if (this.#traces.size >= this.#maxTraces) {
|
|
69
|
+
const oldest = this.#traces.keys().next();
|
|
70
|
+
if (!oldest.done) {
|
|
71
|
+
this.#traces.delete(oldest.value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const created = new Map();
|
|
75
|
+
this.#traces.set(traceId, created);
|
|
76
|
+
return created;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const descendsFrom = (spans, spanId, ancestorId) => {
|
|
80
|
+
let cursor = spans.get(spanId)?.parentSpanId;
|
|
81
|
+
for (let depth = 0; cursor !== undefined && depth < MAX_ANCESTOR_DEPTH; depth += 1) {
|
|
82
|
+
if (cursor === ancestorId) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
cursor = spans.get(cursor)?.parentSpanId;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
};
|
|
89
|
+
const pickMetadata = (span, recording) => {
|
|
90
|
+
const picked = {};
|
|
91
|
+
for (const [key, value] of Object.entries(span.attributes)) {
|
|
92
|
+
const isModel = MODEL_KEYS.includes(key);
|
|
93
|
+
const isResponse = RESPONSE_KEYS.includes(key) && recording.recordOutputs;
|
|
94
|
+
if ((isModel || isResponse) && value !== undefined) {
|
|
95
|
+
picked[key] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return picked;
|
|
99
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MutableSpan, ResolvedRecording } from "./types.ts";
|
|
2
|
+
export declare const applyMappings: (span: MutableSpan) => void;
|
|
3
|
+
export declare const stripUnrecordedContent: (span: MutableSpan, recording: ResolvedRecording) => void;
|
|
4
|
+
export declare const isOperation: (span: MutableSpan, operation: string) => boolean;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ATTR } from "@unhandled/protocol";
|
|
2
|
+
import { MASTRA_ATTR, MASTRA_CONTENT_PATTERN } from "./attributes.js";
|
|
3
|
+
const RENAMES = {
|
|
4
|
+
[MASTRA_ATTR.cachedInputTokens]: ATTR.genAiUsageCacheReadInputTokens,
|
|
5
|
+
[MASTRA_ATTR.reasoningTokens]: ATTR.genAiUsageReasoningOutputTokens,
|
|
6
|
+
[MASTRA_ATTR.cacheCreationInputTokens]: ATTR.genAiUsageCacheWriteInputTokens,
|
|
7
|
+
};
|
|
8
|
+
const INPUT_KEYS = new Set([
|
|
9
|
+
ATTR.genAiInputMessages,
|
|
10
|
+
ATTR.genAiSystemInstructions,
|
|
11
|
+
ATTR.genAiToolCallArguments,
|
|
12
|
+
]);
|
|
13
|
+
const OUTPUT_KEYS = new Set([ATTR.genAiOutputMessages, ATTR.genAiToolCallResult]);
|
|
14
|
+
export const applyMappings = (span) => {
|
|
15
|
+
const { attributes } = span;
|
|
16
|
+
for (const [source, target] of Object.entries(RENAMES)) {
|
|
17
|
+
const value = attributes[source];
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (attributes[target] === undefined) {
|
|
22
|
+
attributes[target] = value;
|
|
23
|
+
}
|
|
24
|
+
delete attributes[source];
|
|
25
|
+
}
|
|
26
|
+
const agentName = attributes[MASTRA_ATTR.agentName];
|
|
27
|
+
if (attributes[ATTR.genAiFunctionId] === undefined && agentName !== undefined) {
|
|
28
|
+
attributes[ATTR.genAiFunctionId] = agentName;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
export const stripUnrecordedContent = (span, recording) => {
|
|
32
|
+
if (recording.recordInputs && recording.recordOutputs) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const key of Object.keys(span.attributes)) {
|
|
36
|
+
const mastraContent = MASTRA_CONTENT_PATTERN.exec(key);
|
|
37
|
+
const isInput = INPUT_KEYS.has(key) || mastraContent?.[1] === "input";
|
|
38
|
+
const isOutput = OUTPUT_KEYS.has(key) || mastraContent?.[1] === "output";
|
|
39
|
+
if ((isInput && !recording.recordInputs) || (isOutput && !recording.recordOutputs)) {
|
|
40
|
+
delete span.attributes[key];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
export const isOperation = (span, operation) => span.attributes[ATTR.genAiOperationName] === operation;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SpanType } from "@mastra/core/observability";
|
|
2
|
+
import type { ObservabilityInstanceConfig } from "@mastra/observability";
|
|
3
|
+
export type MastraObservabilityConfig = Omit<ObservabilityInstanceConfig, "name">;
|
|
4
|
+
export type MastraObservabilityConfigOptions = Pick<MastraObservabilityConfig, "serviceName"> & Partial<Omit<MastraObservabilityConfig, "serviceName">>;
|
|
5
|
+
export declare const DEFAULT_EXCLUDED_SPAN_TYPES: readonly SpanType[];
|
|
6
|
+
export declare const createMastraObservabilityConfig: (options: MastraObservabilityConfigOptions) => MastraObservabilityConfig;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SpanType } from "@mastra/core/observability";
|
|
2
|
+
import { OtelBridge } from "@mastra/otel-bridge";
|
|
3
|
+
export const DEFAULT_EXCLUDED_SPAN_TYPES = [SpanType.MODEL_STEP, SpanType.MODEL_CHUNK];
|
|
4
|
+
export const createMastraObservabilityConfig = (options) => ({
|
|
5
|
+
...options,
|
|
6
|
+
bridge: options.bridge ?? new OtelBridge(),
|
|
7
|
+
excludeSpanTypes: options.excludeSpanTypes ?? [...DEFAULT_EXCLUDED_SPAN_TYPES],
|
|
8
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReadableSpan, SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
2
|
+
import type { MastraSpanProcessorOptions } from "./types.ts";
|
|
3
|
+
export declare class MastraSpanProcessor implements SpanProcessor {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(options?: MastraSpanProcessorOptions);
|
|
6
|
+
onStart(): void;
|
|
7
|
+
onEnd(span: ReadableSpan): void;
|
|
8
|
+
forceFlush(): Promise<void>;
|
|
9
|
+
shutdown(): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { GEN_AI_OPERATIONS } from "@unhandled/protocol";
|
|
2
|
+
import { coerceEvaluationScores } from "./evaluations.js";
|
|
3
|
+
import { ModelMetadataBuffer } from "./model_metadata.js";
|
|
4
|
+
import { applyMappings, isOperation, stripUnrecordedContent } from "./normalize.js";
|
|
5
|
+
export class MastraSpanProcessor {
|
|
6
|
+
#recording;
|
|
7
|
+
#buffer;
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.#recording = {
|
|
10
|
+
recordInputs: options.recordInputs ?? false,
|
|
11
|
+
recordOutputs: options.recordOutputs ?? false,
|
|
12
|
+
};
|
|
13
|
+
this.#buffer = new ModelMetadataBuffer({
|
|
14
|
+
maxTraces: options.maxBufferedTraces,
|
|
15
|
+
maxSpansPerTrace: options.maxBufferedSpansPerTrace,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
// biome-ignore lint/suspicious/noEmptyBlockStatements: SpanProcessor requires onStart; this processor only rewrites ended spans
|
|
19
|
+
onStart() { }
|
|
20
|
+
onEnd(span) {
|
|
21
|
+
const { traceId, spanId } = span.spanContext();
|
|
22
|
+
const parentSpanId = span.parentSpanContext?.spanId;
|
|
23
|
+
applyMappings(span);
|
|
24
|
+
if (isOperation(span, GEN_AI_OPERATIONS.invokeAgent)) {
|
|
25
|
+
this.#buffer.fillParent(traceId, span, spanId);
|
|
26
|
+
}
|
|
27
|
+
coerceEvaluationScores(span.events);
|
|
28
|
+
const isModelCall = isOperation(span, GEN_AI_OPERATIONS.chat);
|
|
29
|
+
this.#buffer.remember(traceId, spanId, parentSpanId, isModelCall ? span : undefined, this.#recording);
|
|
30
|
+
if (parentSpanId === undefined) {
|
|
31
|
+
this.#buffer.endTrace(traceId);
|
|
32
|
+
}
|
|
33
|
+
stripUnrecordedContent(span, this.#recording);
|
|
34
|
+
}
|
|
35
|
+
forceFlush() {
|
|
36
|
+
return Promise.resolve();
|
|
37
|
+
}
|
|
38
|
+
shutdown() {
|
|
39
|
+
return Promise.resolve();
|
|
40
|
+
}
|
|
41
|
+
}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Attributes, HrTime, SpanStatus } from "@opentelemetry/api";
|
|
2
|
+
import type { TimedEvent } from "@opentelemetry/sdk-trace-base";
|
|
3
|
+
export interface MastraRecordingOptions {
|
|
4
|
+
recordInputs?: boolean;
|
|
5
|
+
recordOutputs?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface MastraSpanProcessorOptions extends MastraRecordingOptions {
|
|
8
|
+
maxBufferedSpansPerTrace?: number;
|
|
9
|
+
maxBufferedTraces?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface MastraIntegrationOptions extends MastraRecordingOptions {
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ResolvedRecording {
|
|
15
|
+
recordInputs: boolean;
|
|
16
|
+
recordOutputs: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface MutableSpan {
|
|
19
|
+
attributes: Attributes;
|
|
20
|
+
endTime: HrTime;
|
|
21
|
+
events: TimedEvent[];
|
|
22
|
+
name: string;
|
|
23
|
+
status: SpanStatus;
|
|
24
|
+
}
|
|
25
|
+
export interface MastraEvaluation {
|
|
26
|
+
explanation?: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
scoreLabel?: string;
|
|
29
|
+
scorerId?: string;
|
|
30
|
+
scorerVersion?: string;
|
|
31
|
+
scoreValue?: number;
|
|
32
|
+
targetTraceId?: string;
|
|
33
|
+
}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unhandled/instrumentation-mastra",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Mastra span normalization for Unhandled",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/treboryx/unhandled.git",
|
|
8
|
+
"directory": "packages/instrumentation-mastra"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./build/index.d.ts",
|
|
15
|
+
"default": "./build/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./integration": {
|
|
18
|
+
"types": "./build/integration.d.ts",
|
|
19
|
+
"default": "./build/integration.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"build"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@opentelemetry/api": "~1.9.1",
|
|
27
|
+
"@opentelemetry/sdk-trace-base": "^2.12.0",
|
|
28
|
+
"@unhandled/protocol": "0.0.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@adonisjs/tsconfig": "^2.0.0",
|
|
32
|
+
"@mastra/core": "^1.78.0",
|
|
33
|
+
"@mastra/observability": "^1.20.1",
|
|
34
|
+
"@mastra/otel-bridge": "^1.5.17",
|
|
35
|
+
"@opentelemetry/context-async-hooks": "^2.12.0",
|
|
36
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.223.0",
|
|
37
|
+
"@types/node": "^25.9.9",
|
|
38
|
+
"@unhandled/node": "0.0.1",
|
|
39
|
+
"@unhandled/otlp-receiver": "0.0.0",
|
|
40
|
+
"import-in-the-middle": "^3.5.2",
|
|
41
|
+
"typescript": "~6.0.2"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@mastra/core": ">=1.0.0",
|
|
45
|
+
"@mastra/observability": ">=1.0.0",
|
|
46
|
+
"@mastra/otel-bridge": ">=1.0.0",
|
|
47
|
+
"@unhandled/node": "^0.0.1"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@mastra/observability": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@unhandled/node": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=24.0.0"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsc --project tsconfig.build.json",
|
|
65
|
+
"test": "node --test 'tests/**/*.test.ts'",
|
|
66
|
+
"typecheck": "tsrs --noEmit",
|
|
67
|
+
"typecheck:tsc": "tsc --noEmit"
|
|
68
|
+
}
|
|
6
69
|
}
|