Skip to content

Commit d019e2b

Browse files
committed
feat(tracing): support for truncatation of span attribute values
1 parent dc8082a commit d019e2b

File tree

5 files changed

+183
-3
lines changed

5 files changed

+183
-3
lines changed

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

+60-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import { AttributeValue, Attributes } from '@opentelemetry/api';
16+
import { AttributeValue, Attributes, Logger } from '@opentelemetry/api';
1717

1818
export function sanitizeAttributes(attributes: unknown): Attributes {
1919
const out: Attributes = {};
@@ -47,6 +47,65 @@ export function isAttributeValue(val: unknown): val is AttributeValue {
4747
return isValidPrimitiveAttributeValue(val);
4848
}
4949

50+
export function truncateValueIfTooLong(
51+
value: AttributeValue,
52+
limit: number | undefined,
53+
logger: Logger
54+
): AttributeValue {
55+
if (limit == null) {
56+
return value;
57+
}
58+
59+
if (typeof value === 'boolean' || typeof value === 'number') {
60+
// these types reasonably can't exceed a limit
61+
// if the limit was set to e.g. 2, then true with a length of 4 will exceed a limit
62+
// however, the purpose of the limit is to prevent accidents such as values being thousands of characters long
63+
return value;
64+
}
65+
66+
if (Array.isArray(value)) {
67+
let accruedLength = 0;
68+
69+
// roughly measure the size of a serialized array
70+
// this is done to avoid running json stringify each time attributes are added
71+
for (const element of value) {
72+
if (element == undefined) {
73+
accruedLength += 4;
74+
} else if (typeof element === 'string') {
75+
accruedLength += element.length;
76+
} else {
77+
accruedLength += element.toString().length;
78+
}
79+
80+
if (accruedLength > limit) {
81+
break;
82+
}
83+
}
84+
85+
if (accruedLength > limit) {
86+
return truncateValueIfTooLong(JSON.stringify(value), limit, logger);
87+
} else {
88+
return value;
89+
}
90+
}
91+
92+
if (typeof value !== 'string') {
93+
logger.warn(
94+
`truncateValueIfTooLong expected a value of type string, but received ${typeof value}.`
95+
);
96+
97+
// while this is an undesireable situation, if a limit was set, then our goal is to prevent large payloads
98+
return truncateValueIfTooLong(JSON.stringify(value), limit, logger);
99+
}
100+
101+
if (value.length > limit) {
102+
logger.warn('Value was truncated.');
103+
return value.substring(0, limit);
104+
} else {
105+
return value;
106+
}
107+
}
108+
50109
function isHomogeneousAttributeValueArray(arr: unknown[]): boolean {
51110
let type: string | undefined;
52111

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

+70
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { Logger } from '@opentelemetry/api';
1718
import * as assert from 'assert';
19+
import * as sinon from 'sinon';
1820
import {
1921
isAttributeValue,
2022
sanitizeAttributes,
23+
truncateValueIfTooLong,
2124
} from '../../src/common/attributes';
2225

2326
describe('attributes', () => {
@@ -103,4 +106,71 @@ describe('attributes', () => {
103106
assert.strictEqual(attributes.arr[0], 'unmodified');
104107
});
105108
});
109+
describe('#truncateValueIfTooLong', () => {
110+
it('should not truncate any given value if the limit is not set', () => {
111+
const fakeLogger: Logger = (null as unknown) as Logger;
112+
assert.strictEqual(
113+
truncateValueIfTooLong('a', undefined, fakeLogger),
114+
'a'
115+
);
116+
assert.strictEqual(truncateValueIfTooLong(1, undefined, fakeLogger), 1);
117+
assert.strictEqual(
118+
truncateValueIfTooLong(true, undefined, fakeLogger),
119+
true
120+
);
121+
122+
const arrayRef: string[] = [];
123+
assert.strictEqual(
124+
truncateValueIfTooLong(arrayRef, undefined, fakeLogger),
125+
arrayRef
126+
);
127+
});
128+
129+
it('passes numbers and bools through regardless of limits', () => {
130+
const fakeLogger: Logger = (null as unknown) as Logger;
131+
assert.strictEqual(truncateValueIfTooLong(true, 3, fakeLogger), true);
132+
assert.strictEqual(truncateValueIfTooLong(false, 3, fakeLogger), false);
133+
assert.strictEqual(truncateValueIfTooLong(100, 3, fakeLogger), 100);
134+
assert.strictEqual(truncateValueIfTooLong(1000, 3, fakeLogger), 1000);
135+
});
136+
137+
it('truncates strings if they are longer then the limit', () => {
138+
const fakeLogger: Logger = ({ warn: () => {} } as unknown) as Logger;
139+
assert.strictEqual(
140+
truncateValueIfTooLong('a'.repeat(10), 10, fakeLogger),
141+
'a'.repeat(10)
142+
);
143+
assert.strictEqual(
144+
truncateValueIfTooLong('a'.repeat(11), 10, fakeLogger),
145+
'a'.repeat(10)
146+
);
147+
assert.strictEqual(
148+
truncateValueIfTooLong('a'.repeat(100), 100, fakeLogger),
149+
'a'.repeat(100)
150+
);
151+
assert.strictEqual(
152+
truncateValueIfTooLong('a'.repeat(101), 100, fakeLogger),
153+
'a'.repeat(100)
154+
);
155+
});
156+
157+
it('truncates arrays if they are longer then the limit', () => {
158+
const fakeLogger: Logger = ({ warn: () => {} } as unknown) as Logger;
159+
assert.strictEqual(
160+
truncateValueIfTooLong(['abcd', 'abcd'], 5, fakeLogger),
161+
'["abc'
162+
);
163+
assert.strictEqual(
164+
truncateValueIfTooLong([1000, 1000], 5, fakeLogger),
165+
'[1000'
166+
);
167+
});
168+
169+
it('warns if a value was truncated', () => {
170+
const warnSpy = sinon.spy();
171+
const fakeLogger: Logger = ({ warn: warnSpy } as unknown) as Logger;
172+
truncateValueIfTooLong('a'.repeat(6), 5, fakeLogger);
173+
assert(warnSpy.calledWith('Value was truncated.'));
174+
});
175+
});
106176
});

packages/opentelemetry-tracing/src/Span.ts

+7-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 {
@@ -112,7 +113,11 @@ export class Span implements api.Span, ReadableSpan {
112113
delete this.attributes[attributeKeyToDelete];
113114
}
114115
}
115-
this.attributes[key] = value;
116+
this.attributes[key] = truncateValueIfTooLong(
117+
value,
118+
this._traceParams.spanAttributeValueSizeLimit,
119+
this._logger
120+
);
116121
return this;
117122
}
118123

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;
7779
}
7880

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

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

+44
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,50 @@ describe('Span', () => {
255255
assert.strictEqual(span.attributes['foo149'], 'bar149');
256256
});
257257

258+
it('should truncate attribute values exceeding length limit', () => {
259+
const tracerWithLimit = new BasicTracerProvider({
260+
logger: new NoopLogger(),
261+
traceParams: {
262+
spanAttributeValueSizeLimit: 100,
263+
},
264+
}).getTracer('default');
265+
266+
const spanWithLimit = new Span(
267+
tracerWithLimit,
268+
name,
269+
spanContext,
270+
SpanKind.CLIENT
271+
);
272+
const spanWithoutLimit = new Span(
273+
tracer,
274+
name,
275+
spanContext,
276+
SpanKind.CLIENT
277+
);
278+
279+
spanWithLimit.setAttribute('attr under limit', 'a'.repeat(100));
280+
assert.strictEqual(
281+
spanWithLimit.attributes['attr under limit'],
282+
'a'.repeat(100)
283+
);
284+
spanWithoutLimit.setAttribute('attr under limit', 'a'.repeat(100));
285+
assert.strictEqual(
286+
spanWithoutLimit.attributes['attr under limit'],
287+
'a'.repeat(100)
288+
);
289+
290+
spanWithLimit.setAttribute('attr over limit', 'b'.repeat(101));
291+
assert.strictEqual(
292+
spanWithLimit.attributes['attr over limit'],
293+
'b'.repeat(100)
294+
);
295+
spanWithoutLimit.setAttribute('attr over limit', 'b'.repeat(101));
296+
assert.strictEqual(
297+
spanWithoutLimit.attributes['attr over limit'],
298+
'b'.repeat(101)
299+
);
300+
});
301+
258302
it('should set an error status', () => {
259303
const span = new Span(tracer, name, spanContext, SpanKind.CLIENT);
260304
span.setStatus({

0 commit comments

Comments
 (0)