Skip to content

Commit 9f7c5f5

Browse files
committed
feat(tracing): support for truncation of span attribute values
1 parent eb35306 commit 9f7c5f5

File tree

11 files changed

+239
-2
lines changed

11 files changed

+239
-2
lines changed

packages/opentelemetry-core/src/common/attributes.ts

+41
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,47 @@ export function isAttributeValue(val: unknown): val is AttributeValue {
4747
return isValidPrimitiveAttributeValue(val);
4848
}
4949

50+
export function truncateValueIfTooLong(
51+
value: AttributeValue,
52+
limit: number | null,
53+
truncationWarningCallback = () => {}
54+
): AttributeValue {
55+
if (limit === null) {
56+
return value;
57+
}
58+
59+
if (limit < 32) {
60+
throw new Error('Value size limit cannot be lower than 32.');
61+
}
62+
63+
if (typeof value === 'boolean' || typeof value === 'number') {
64+
// these types can't exceed the attribute value size limit
65+
return value;
66+
}
67+
68+
if (Array.isArray(value)) {
69+
// note: this is potentially incompatible with a given exporter
70+
const serialized = JSON.stringify(value);
71+
72+
if (serialized.length > limit) {
73+
return truncateValueIfTooLong(
74+
serialized,
75+
limit,
76+
truncationWarningCallback
77+
);
78+
}
79+
80+
return value;
81+
}
82+
83+
if (value.length > limit) {
84+
truncationWarningCallback();
85+
return value.substring(0, limit);
86+
}
87+
88+
return value;
89+
}
90+
5091
function isHomogeneousAttributeValueArray(arr: unknown[]): boolean {
5192
let type: string | undefined;
5293

packages/opentelemetry-core/src/utils/environment.ts

+7
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ export interface ENVIRONMENT {
2525
OTEL_LOG_LEVEL?: LogLevel;
2626
OTEL_NO_PATCH_MODULES?: string;
2727
OTEL_SAMPLING_PROBABILITY?: number;
28+
OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT?: number | null;
2829
}
2930

3031
const ENVIRONMENT_NUMBERS: Partial<keyof ENVIRONMENT>[] = [
3132
'OTEL_SAMPLING_PROBABILITY',
33+
'OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT',
3234
];
3335

3436
/**
@@ -38,6 +40,7 @@ export const DEFAULT_ENVIRONMENT: Required<ENVIRONMENT> = {
3840
OTEL_NO_PATCH_MODULES: '',
3941
OTEL_LOG_LEVEL: LogLevel.INFO,
4042
OTEL_SAMPLING_PROBABILITY: 1,
43+
OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT: null,
4144
};
4245

4346
/**
@@ -116,6 +119,10 @@ export function parseEnvironment(values: ENVIRONMENT_MAP): ENVIRONMENT {
116119
setLogLevelFromEnv(key, environment, values);
117120
break;
118121

122+
case 'OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT':
123+
parseNumber(key, environment, values, 32, Number.MAX_SAFE_INTEGER);
124+
break;
125+
119126
default:
120127
if (ENVIRONMENT_NUMBERS.indexOf(key) >= 0) {
121128
parseNumber(key, environment, values);

packages/opentelemetry-core/test/common/attributes.test.ts

+60
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
*/
1616

1717
import * as assert from 'assert';
18+
import * as sinon from 'sinon';
1819
import {
1920
isAttributeValue,
2021
sanitizeAttributes,
22+
truncateValueIfTooLong,
2123
} from '../../src/common/attributes';
2224

2325
describe('attributes', () => {
@@ -103,4 +105,62 @@ describe('attributes', () => {
103105
assert.strictEqual(attributes.arr[0], 'unmodified');
104106
});
105107
});
108+
describe('#truncateValueIfTooLong', () => {
109+
it('should not truncate any given value if the limit is not set', () => {
110+
assert.strictEqual(truncateValueIfTooLong('a', null), 'a');
111+
assert.strictEqual(truncateValueIfTooLong(1, null), 1);
112+
assert.strictEqual(truncateValueIfTooLong(true, null), true);
113+
114+
const arrayRef: string[] = [];
115+
assert.strictEqual(truncateValueIfTooLong(arrayRef, null), arrayRef);
116+
});
117+
118+
it('passes numbers and bools through', () => {
119+
assert.strictEqual(truncateValueIfTooLong(true, 32), true);
120+
assert.strictEqual(truncateValueIfTooLong(false, 32), false);
121+
assert.strictEqual(truncateValueIfTooLong(1, 32), 1);
122+
});
123+
124+
it('truncates strings if they are longer than the limit', () => {
125+
assert.strictEqual(
126+
truncateValueIfTooLong('a'.repeat(100), 100),
127+
'a'.repeat(100)
128+
);
129+
assert.strictEqual(
130+
truncateValueIfTooLong('a'.repeat(101), 100),
131+
'a'.repeat(100)
132+
);
133+
});
134+
135+
it('serializes and truncates arrays if they are longer than the limit', () => {
136+
assert.strictEqual(
137+
truncateValueIfTooLong(['a'.repeat(100)], 32),
138+
'["' + 'a'.repeat(30)
139+
);
140+
assert.strictEqual(
141+
truncateValueIfTooLong(
142+
[...new Array(10).keys()].map(() => 1000),
143+
32
144+
),
145+
'[' + '1000,'.repeat(6) + '1'
146+
);
147+
assert.strictEqual(
148+
truncateValueIfTooLong(
149+
[...new Array(10).keys()].map(() => true),
150+
32
151+
),
152+
'[' + 'true,'.repeat(6) + 't'
153+
);
154+
});
155+
156+
it('executes callback if a value was truncated', () => {
157+
const fakeCallback = sinon.spy();
158+
159+
truncateValueIfTooLong('a'.repeat(32), 32, fakeCallback);
160+
assert.ok(!fakeCallback.called);
161+
162+
truncateValueIfTooLong('a'.repeat(33), 32, fakeCallback);
163+
assert(fakeCallback.called);
164+
});
165+
});
106166
});

packages/opentelemetry-core/test/utils/environment.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,13 @@ describe('environment', () => {
7777
OTEL_NO_PATCH_MODULES: 'a,b,c',
7878
OTEL_LOG_LEVEL: 'ERROR',
7979
OTEL_SAMPLING_PROBABILITY: '0.5',
80+
OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT: '32',
8081
});
8182
const env = getEnv();
8283
assert.strictEqual(env.OTEL_NO_PATCH_MODULES, 'a,b,c');
8384
assert.strictEqual(env.OTEL_LOG_LEVEL, LogLevel.ERROR);
8485
assert.strictEqual(env.OTEL_SAMPLING_PROBABILITY, 0.5);
86+
assert.strictEqual(env.OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT, 32);
8587
});
8688

8789
it('should parse OTEL_LOG_LEVEL despite casing', () => {

packages/opentelemetry-tracing/src/Span.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616

1717
import * as api from '@opentelemetry/api';
1818
import {
19-
isAttributeValue,
2019
hrTime,
2120
hrTimeDuration,
2221
InstrumentationLibrary,
22+
isAttributeValue,
2323
isTimeInput,
2424
timeInputToHrTime,
25+
truncateValueIfTooLong,
2526
} from '@opentelemetry/core';
2627
import { Resource } from '@opentelemetry/resources';
2728
import {
@@ -56,6 +57,7 @@ export class Span implements api.Span, ReadableSpan {
5657
endTime: api.HrTime = [0, 0];
5758
private _ended = false;
5859
private _duration: api.HrTime = [-1, -1];
60+
private _hasTruncated = false;
5961
private readonly _logger: api.Logger;
6062
private readonly _spanProcessor: SpanProcessor;
6163
private readonly _traceParams: TraceParams;
@@ -112,7 +114,18 @@ export class Span implements api.Span, ReadableSpan {
112114
delete this.attributes[attributeKeyToDelete];
113115
}
114116
}
115-
this.attributes[key] = value;
117+
118+
console.log('limit', this._traceParams.spanAttributeValueSizeLimit);
119+
this.attributes[key] = truncateValueIfTooLong(
120+
value,
121+
this._traceParams.spanAttributeValueSizeLimit || null,
122+
this._hasTruncated
123+
? undefined
124+
: () => {
125+
this._hasTruncated = true;
126+
this._logger.warn(`Span attribute value truncated at key: ${key}.`);
127+
}
128+
);
116129
return this;
117130
}
118131

packages/opentelemetry-tracing/src/Tracer.ts

+9
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ export class Tracer implements api.Tracer {
5555
this.resource = _tracerProvider.resource;
5656
this.instrumentationLibrary = instrumentationLibrary;
5757
this.logger = config.logger || new ConsoleLogger(config.logLevel);
58+
59+
const configuredAttributeLimit =
60+
this._traceParams.spanAttributeValueSizeLimit || null;
61+
if (configuredAttributeLimit !== null && configuredAttributeLimit < 32) {
62+
this.logger.warn(
63+
'OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT was set to a value lower than 32, which is not allowed, limit of 32 will be applied.'
64+
);
65+
this._traceParams.spanAttributeValueSizeLimit = 32;
66+
}
5867
}
5968

6069
/**

packages/opentelemetry-tracing/src/config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export const DEFAULT_CONFIG = {
3636
numberOfAttributesPerSpan: DEFAULT_MAX_ATTRIBUTES_PER_SPAN,
3737
numberOfLinksPerSpan: DEFAULT_MAX_LINKS_PER_SPAN,
3838
numberOfEventsPerSpan: DEFAULT_MAX_EVENTS_PER_SPAN,
39+
spanAttributeValueSizeLimit: getEnv().OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT,
3940
},
4041
gracefulShutdown: true,
4142
};

packages/opentelemetry-tracing/src/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export interface TraceParams {
7474
numberOfLinksPerSpan?: number;
7575
/** numberOfEventsPerSpan is number of message events per span */
7676
numberOfEventsPerSpan?: number;
77+
/** this field defines maximum length of attribute value before it is truncated */
78+
spanAttributeValueSizeLimit?: number | null;
7779
}
7880

7981
/** Interface configuration for a buffer. */

packages/opentelemetry-tracing/src/utility.ts

+3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ export function mergeConfig(userConfig: TracerConfig) {
5656
traceParams.numberOfEventsPerSpan || DEFAULT_MAX_EVENTS_PER_SPAN;
5757
target.traceParams.numberOfLinksPerSpan =
5858
traceParams.numberOfLinksPerSpan || DEFAULT_MAX_LINKS_PER_SPAN;
59+
target.traceParams.spanAttributeValueSizeLimit =
60+
target.traceParams.spanAttributeValueSizeLimit ||
61+
getEnv().OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT;
5962
}
6063
return target;
6164
}

packages/opentelemetry-tracing/test/BasicTracerProvider.test.ts

+18
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ describe('BasicTracerProvider', () => {
7878
numberOfAttributesPerSpan: 32,
7979
numberOfEventsPerSpan: 128,
8080
numberOfLinksPerSpan: 32,
81+
spanAttributeValueSizeLimit: null,
8182
});
8283
});
8384

@@ -91,6 +92,7 @@ describe('BasicTracerProvider', () => {
9192
numberOfAttributesPerSpan: 100,
9293
numberOfEventsPerSpan: 128,
9394
numberOfLinksPerSpan: 32,
95+
spanAttributeValueSizeLimit: null,
9496
});
9597
});
9698

@@ -104,6 +106,7 @@ describe('BasicTracerProvider', () => {
104106
numberOfAttributesPerSpan: 32,
105107
numberOfEventsPerSpan: 300,
106108
numberOfLinksPerSpan: 32,
109+
spanAttributeValueSizeLimit: null,
107110
});
108111
});
109112

@@ -117,6 +120,21 @@ describe('BasicTracerProvider', () => {
117120
numberOfAttributesPerSpan: 32,
118121
numberOfEventsPerSpan: 128,
119122
numberOfLinksPerSpan: 10,
123+
spanAttributeValueSizeLimit: null,
124+
});
125+
});
126+
127+
it('should construct an instance with customized spanAttributeValueSizeLimit trace params', () => {
128+
const tracer = new BasicTracerProvider({
129+
traceParams: {
130+
spanAttributeValueSizeLimit: 100,
131+
},
132+
}).getTracer('default');
133+
assert.deepStrictEqual(tracer.getActiveTraceParams(), {
134+
numberOfAttributesPerSpan: 32,
135+
numberOfEventsPerSpan: 128,
136+
numberOfLinksPerSpan: 32,
137+
spanAttributeValueSizeLimit: 100,
120138
});
121139
});
122140

packages/opentelemetry-tracing/test/Span.test.ts

+81
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
} from '@opentelemetry/core';
3232
import { ExceptionAttribute } from '@opentelemetry/semantic-conventions';
3333
import * as assert from 'assert';
34+
import sinon = require('sinon');
3435
import { BasicTracerProvider, Span, SpanProcessor } from '../src';
3536

3637
const performanceTimeOrigin = hrTime();
@@ -255,6 +256,86 @@ describe('Span', () => {
255256
assert.strictEqual(span.attributes['foo149'], 'bar149');
256257
});
257258

259+
it('should truncate attribute values exceeding length limit', () => {
260+
const tracerWithLimit = new BasicTracerProvider({
261+
logger: new NoopLogger(),
262+
traceParams: {
263+
spanAttributeValueSizeLimit: 100,
264+
},
265+
}).getTracer('default');
266+
267+
const spanWithLimit = new Span(
268+
tracerWithLimit,
269+
name,
270+
spanContext,
271+
SpanKind.CLIENT
272+
);
273+
const spanWithoutLimit = new Span(
274+
tracer,
275+
name,
276+
spanContext,
277+
SpanKind.CLIENT
278+
);
279+
280+
spanWithLimit.setAttribute('attr under limit', 'a'.repeat(100));
281+
assert.strictEqual(
282+
spanWithLimit.attributes['attr under limit'],
283+
'a'.repeat(100)
284+
);
285+
spanWithoutLimit.setAttribute('attr under limit', 'a'.repeat(100));
286+
assert.strictEqual(
287+
spanWithoutLimit.attributes['attr under limit'],
288+
'a'.repeat(100)
289+
);
290+
291+
spanWithLimit.setAttribute('attr over limit', 'b'.repeat(101));
292+
assert.strictEqual(
293+
spanWithLimit.attributes['attr over limit'],
294+
'b'.repeat(100)
295+
);
296+
spanWithoutLimit.setAttribute('attr over limit', 'b'.repeat(101));
297+
assert.strictEqual(
298+
spanWithoutLimit.attributes['attr over limit'],
299+
'b'.repeat(101)
300+
);
301+
});
302+
303+
it('should warn once when truncating attribute values exceeding length limit', () => {
304+
const logger = new NoopLogger();
305+
const loggerWarnSpy = sinon.spy(logger, 'warn');
306+
307+
const tracerWithLimit = new BasicTracerProvider({
308+
logger,
309+
traceParams: {
310+
spanAttributeValueSizeLimit: 100,
311+
},
312+
}).getTracer('default');
313+
314+
const spanWithLimit = new Span(
315+
tracerWithLimit,
316+
name,
317+
spanContext,
318+
SpanKind.CLIENT
319+
);
320+
321+
spanWithLimit.setAttribute('longAttr', 'b'.repeat(100));
322+
assert(!loggerWarnSpy.called);
323+
324+
spanWithLimit.setAttribute('longAttr', 'b'.repeat(101));
325+
assert(
326+
loggerWarnSpy.withArgs('Span attribute value truncated at key: longAttr.')
327+
.calledOnce
328+
);
329+
330+
spanWithLimit.setAttribute('longAttr', 'c'.repeat(102));
331+
assert(
332+
loggerWarnSpy.withArgs('Span attribute value truncated at key: longAttr.')
333+
.calledOnce
334+
);
335+
336+
assert.strictEqual(spanWithLimit.attributes.longAttr, 'c'.repeat(100));
337+
});
338+
258339
it('should set an error status', () => {
259340
const span = new Span(tracer, name, spanContext, SpanKind.CLIENT);
260341
span.setStatus({

0 commit comments

Comments
 (0)