Skip to content

Commit 8f32010

Browse files
authored
Add user-configurable serialization options for span data in Mastra Observability. (#11484)
## Description Add user-configurable serialization options for span data in Mastra Observability. This PR introduces the ability to configure how span data (input, output, attributes) is serialized before export. Users can now customize: - `maxStringLength` - Maximum length for string values (default: 1024) - `maxDepth` - Maximum depth for nested objects (default: 6) - `maxArrayLength` - Maximum number of items in arrays (default: 50) - `maxObjectKeys` - Maximum number of keys in objects (default: 50) These options can be configured via `ObservabilityInstanceConfig.serializationOptions`, allowing users to control truncation limits for large payloads based on their observability backend requirements. ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Code refactoring - [ ] Performance improvement - [ ] Test update ## Checklist - [x] I have made corresponding changes to the documentation (if applicable) - [x] I have added tests that prove my fix is effective or that my feature works <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added configurable serialization options to control span data handling, including customizable limits for string length, object depth, array size, and object key counts. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 12b0cc4 commit 8f32010

File tree

7 files changed

+165
-14
lines changed

7 files changed

+165
-14
lines changed

observability/mastra/src/config.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
ObservabilityBridge,
1313
SpanOutputProcessor,
1414
ConfigSelector,
15+
SerializationOptions,
1516
} from '@mastra/core/observability';
1617
import { z } from 'zod';
1718

@@ -74,6 +75,11 @@ export interface ObservabilityInstanceConfig {
7475
* Supports dot notation for nested values.
7576
*/
7677
requestContextKeys?: string[];
78+
/**
79+
* Options for controlling serialization of span data (input/output/attributes).
80+
* Use these to customize truncation limits for large payloads.
81+
*/
82+
serializationOptions?: SerializationOptions;
7783
}
7884

7985
/**
@@ -114,6 +120,18 @@ export const samplingStrategySchema = z.discriminatedUnion('type', [
114120
}),
115121
]);
116122

123+
/**
124+
* Zod schema for SerializationOptions
125+
*/
126+
export const serializationOptionsSchema = z
127+
.object({
128+
maxStringLength: z.number().int().positive().optional(),
129+
maxDepth: z.number().int().positive().optional(),
130+
maxArrayLength: z.number().int().positive().optional(),
131+
maxObjectKeys: z.number().int().positive().optional(),
132+
})
133+
.optional();
134+
117135
/**
118136
* Zod schema for ObservabilityInstanceConfig
119137
* Note: exporters, spanOutputProcessors, bridge, and configSelector are validated as any
@@ -129,6 +147,7 @@ export const observabilityInstanceConfigSchema = z
129147
spanOutputProcessors: z.array(z.any()).optional(),
130148
includeInternalSpans: z.boolean().optional(),
131149
requestContextKeys: z.array(z.string()).optional(),
150+
serializationOptions: serializationOptionsSchema,
132151
})
133152
.refine(
134153
data => {
@@ -155,6 +174,7 @@ export const observabilityConfigValueSchema = z
155174
spanOutputProcessors: z.array(z.any()).optional(),
156175
includeInternalSpans: z.boolean().optional(),
157176
requestContextKeys: z.array(z.string()).optional(),
177+
serializationOptions: serializationOptionsSchema,
158178
})
159179
.refine(
160180
data => {

observability/mastra/src/instances/base.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export abstract class BaseObservabilityInstance extends MastraBase implements Ob
5353
bridge: config.bridge ?? undefined,
5454
includeInternalSpans: config.includeInternalSpans ?? false,
5555
requestContextKeys: config.requestContextKeys ?? [],
56+
serializationOptions: config.serializationOptions,
5657
};
5758

5859
// Initialize bridge if present

observability/mastra/src/spans/base.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,4 +535,73 @@ describe('Span', () => {
535535
});
536536
});
537537
});
538+
539+
describe('serializationOptions', () => {
540+
it('should use custom maxStringLength from config', () => {
541+
const tracing = new DefaultObservabilityInstance({
542+
serviceName: 'test',
543+
name: 'test',
544+
exporters: [testExporter],
545+
serializationOptions: {
546+
maxStringLength: 10,
547+
},
548+
});
549+
550+
const longString = 'a'.repeat(100);
551+
const span = tracing.startSpan({
552+
type: SpanType.GENERIC,
553+
name: 'test',
554+
input: { data: longString },
555+
});
556+
557+
// String should be truncated to 10 chars + truncation marker
558+
expect(span.input.data.length).toBeLessThanOrEqual(25);
559+
expect(span.input.data).toContain('[truncated]');
560+
span.end();
561+
});
562+
563+
it('should use default options when serializationOptions is not provided', () => {
564+
const tracing = new DefaultObservabilityInstance({
565+
serviceName: 'test',
566+
name: 'test',
567+
exporters: [testExporter],
568+
});
569+
570+
const longString = 'a'.repeat(2000);
571+
const span = tracing.startSpan({
572+
type: SpanType.GENERIC,
573+
name: 'test',
574+
input: { data: longString },
575+
});
576+
577+
// Default maxStringLength is 1024
578+
expect(span.input.data.length).toBeLessThanOrEqual(1024 + 15);
579+
expect(span.input.data).toContain('[truncated]');
580+
span.end();
581+
});
582+
583+
it('should respect custom maxDepth from config', () => {
584+
const tracing = new DefaultObservabilityInstance({
585+
serviceName: 'test',
586+
name: 'test',
587+
exporters: [testExporter],
588+
serializationOptions: {
589+
maxDepth: 2,
590+
},
591+
});
592+
593+
const deepObj: any = { level: 0, nested: { level: 1, nested: { level: 2, nested: { level: 3 } } } };
594+
const span = tracing.startSpan({
595+
type: SpanType.GENERIC,
596+
name: 'test',
597+
input: deepObj,
598+
});
599+
600+
// At maxDepth 2, level 2 values should be [MaxDepth]
601+
expect(span.input.level).toBe(0);
602+
expect(span.input.nested.level).toBe(1);
603+
expect(span.input.nested.nested.level).toBe('[MaxDepth]');
604+
span.end();
605+
});
606+
});
538607
});

observability/mastra/src/spans/base.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import type {
1818

1919
import { SpanType, InternalSpans } from '@mastra/core/observability';
2020
import { ModelSpanTracker } from '../model-tracing';
21-
import { deepClean } from './serialization';
21+
import { deepClean, mergeSerializationOptions } from './serialization';
22+
import type { DeepCleanOptions } from './serialization';
2223

2324
/**
2425
* Determines if a span type should be considered internal based on flags.
@@ -136,12 +137,18 @@ export abstract class BaseSpan<TType extends SpanType = any> implements Span<TTy
136137
public entityName?: string;
137138
/** Parent span ID (for root spans that are children of external spans) */
138139
protected parentSpanId?: string;
140+
/** Deep clean options for serialization */
141+
protected deepCleanOptions: DeepCleanOptions;
139142

140143
constructor(options: CreateSpanOptions<TType>, observabilityInstance: ObservabilityInstance) {
144+
// Get serialization options from observability instance config
145+
const serializationOptions = observabilityInstance.getConfig().serializationOptions;
146+
this.deepCleanOptions = mergeSerializationOptions(serializationOptions);
147+
141148
this.name = options.name;
142149
this.type = options.type;
143-
this.attributes = deepClean(options.attributes) || ({} as SpanTypeMap[TType]);
144-
this.metadata = deepClean(options.metadata);
150+
this.attributes = deepClean(options.attributes, this.deepCleanOptions) || ({} as SpanTypeMap[TType]);
151+
this.metadata = deepClean(options.metadata, this.deepCleanOptions);
145152
this.parent = options.parent;
146153
this.startTime = new Date();
147154
this.observabilityInstance = observabilityInstance;
@@ -158,9 +165,9 @@ export abstract class BaseSpan<TType extends SpanType = any> implements Span<TTy
158165
if (this.isEvent) {
159166
// Event spans don't have endTime or input.
160167
// Event spans are immediately emitted by the BaseObservability class via the end() event.
161-
this.output = deepClean(options.output);
168+
this.output = deepClean(options.output, this.deepCleanOptions);
162169
} else {
163-
this.input = deepClean(options.input);
170+
this.input = deepClean(options.input, this.deepCleanOptions);
164171
}
165172
}
166173

observability/mastra/src/spans/default.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ export class DefaultSpan<TType extends SpanType> extends BaseSpan<TType> {
5757
}
5858
this.endTime = new Date();
5959
if (options?.output !== undefined) {
60-
this.output = deepClean(options.output);
60+
this.output = deepClean(options.output, this.deepCleanOptions);
6161
}
6262
if (options?.attributes) {
63-
this.attributes = { ...this.attributes, ...deepClean(options.attributes) };
63+
this.attributes = { ...this.attributes, ...deepClean(options.attributes, this.deepCleanOptions) };
6464
}
6565
if (options?.metadata) {
66-
this.metadata = { ...this.metadata, ...deepClean(options.metadata) };
66+
this.metadata = { ...this.metadata, ...deepClean(options.metadata, this.deepCleanOptions) };
6767
}
6868
// Tracing events automatically handled by base class
6969
}
@@ -90,10 +90,10 @@ export class DefaultSpan<TType extends SpanType> extends BaseSpan<TType> {
9090

9191
// Update attributes if provided
9292
if (attributes) {
93-
this.attributes = { ...this.attributes, ...deepClean(attributes) };
93+
this.attributes = { ...this.attributes, ...deepClean(attributes, this.deepCleanOptions) };
9494
}
9595
if (metadata) {
96-
this.metadata = { ...this.metadata, ...deepClean(metadata) };
96+
this.metadata = { ...this.metadata, ...deepClean(metadata, this.deepCleanOptions) };
9797
}
9898

9999
if (endSpan) {
@@ -110,16 +110,16 @@ export class DefaultSpan<TType extends SpanType> extends BaseSpan<TType> {
110110
}
111111

112112
if (options.input !== undefined) {
113-
this.input = deepClean(options.input);
113+
this.input = deepClean(options.input, this.deepCleanOptions);
114114
}
115115
if (options.output !== undefined) {
116-
this.output = deepClean(options.output);
116+
this.output = deepClean(options.output, this.deepCleanOptions);
117117
}
118118
if (options.attributes) {
119-
this.attributes = { ...this.attributes, ...deepClean(options.attributes) };
119+
this.attributes = { ...this.attributes, ...deepClean(options.attributes, this.deepCleanOptions) };
120120
}
121121
if (options.metadata) {
122-
this.metadata = { ...this.metadata, ...deepClean(options.metadata) };
122+
this.metadata = { ...this.metadata, ...deepClean(options.metadata, this.deepCleanOptions) };
123123
}
124124
// Tracing events automatically handled by base class
125125
}

observability/mastra/src/spans/serialization.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,28 @@ export const DEFAULT_DEEP_CLEAN_OPTIONS: DeepCleanOptions = Object.freeze({
3434
maxObjectKeys: 50,
3535
});
3636

37+
/**
38+
* Merge user-provided serialization options with defaults.
39+
* Returns a complete DeepCleanOptions object.
40+
*/
41+
export function mergeSerializationOptions(userOptions?: {
42+
maxStringLength?: number;
43+
maxDepth?: number;
44+
maxArrayLength?: number;
45+
maxObjectKeys?: number;
46+
}): DeepCleanOptions {
47+
if (!userOptions) {
48+
return DEFAULT_DEEP_CLEAN_OPTIONS;
49+
}
50+
return {
51+
keysToStrip: DEFAULT_KEYS_TO_STRIP,
52+
maxDepth: userOptions.maxDepth ?? DEFAULT_DEEP_CLEAN_OPTIONS.maxDepth,
53+
maxStringLength: userOptions.maxStringLength ?? DEFAULT_DEEP_CLEAN_OPTIONS.maxStringLength,
54+
maxArrayLength: userOptions.maxArrayLength ?? DEFAULT_DEEP_CLEAN_OPTIONS.maxArrayLength,
55+
maxObjectKeys: userOptions.maxObjectKeys ?? DEFAULT_DEEP_CLEAN_OPTIONS.maxObjectKeys,
56+
};
57+
}
58+
3759
/**
3860
* Hard-cap any string to prevent unbounded growth.
3961
*/

packages/core/src/observability/types/tracing.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,33 @@ export type TracingProperties = {
893893
// Registry Config Interfaces
894894
// ============================================================================
895895

896+
/**
897+
* Options for controlling serialization of span data.
898+
* These options control how input, output, and attributes are cleaned before export.
899+
*/
900+
export interface SerializationOptions {
901+
/**
902+
* Maximum length for string values
903+
* @default 1024
904+
*/
905+
maxStringLength?: number;
906+
/**
907+
* Maximum depth for nested objects
908+
* @default 6
909+
*/
910+
maxDepth?: number;
911+
/**
912+
* Maximum number of items in arrays
913+
* @default 50
914+
*/
915+
maxArrayLength?: number;
916+
/**
917+
* Maximum number of keys in objects
918+
* @default 50
919+
*/
920+
maxObjectKeys?: number;
921+
}
922+
896923
/**
897924
* Configuration for a single observability instance
898925
*/
@@ -917,6 +944,11 @@ export interface ObservabilityInstanceConfig {
917944
* Supports dot notation for nested values.
918945
*/
919946
requestContextKeys?: string[];
947+
/**
948+
* Options for controlling serialization of span data (input/output/attributes).
949+
* Use these to customize truncation limits for large payloads.
950+
*/
951+
serializationOptions?: SerializationOptions;
920952
}
921953

922954
/**

0 commit comments

Comments
 (0)