Skip to content

Commit

Permalink
feat(tracing): support for truncation of span attribute values
Browse files Browse the repository at this point in the history
  • Loading branch information
jtmalinowski committed Oct 28, 2020
1 parent 46f31dd commit 139bfcc
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 2 deletions.
41 changes: 41 additions & 0 deletions packages/opentelemetry-core/src/common/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,47 @@ export function isAttributeValue(val: unknown): val is AttributeValue {
return isValidPrimitiveAttributeValue(val);
}

export function truncateValueIfTooLong(
value: AttributeValue,
limit: number | null,
truncationWarningCallback = () => {}
): AttributeValue {
if (limit === null) {
return value;
}

if (limit < 32) {
throw new Error('Value size limit cannot be lower than 32.');

This comment has been minimized.

Copy link
@obecny

obecny Oct 28, 2020

Member

Why such limit, is there a spec which forces that ?,
Besides that Api cannot throw any error that could break the user app

This comment has been minimized.

Copy link
@jtmalinowski

jtmalinowski Oct 28, 2020

Author Contributor

I proposed the limit to be at least 32 in the spec. "attribute value size limit MUST NOT be set to any number lower than 32". The skips some unclear situations like a very low limit of 2-3 characters you mentioned. Btw, Java implementation already skips numbers and bools when truncating attribute values.

}

if (typeof value === 'boolean' || typeof value === 'number') {
// these types can't exceed the attribute value size limit
return value;
}

if (Array.isArray(value)) {
// note: this is potentially incompatible with a given exporter
const serialized = JSON.stringify(value);

if (serialized.length > limit) {
return truncateValueIfTooLong(
serialized,
limit,
truncationWarningCallback
);
}

return value;
}

if (value.length > limit) {
truncationWarningCallback();
return value.substring(0, limit);
}

return value;
}

function isHomogeneousAttributeValueArray(arr: unknown[]): boolean {
let type: string | undefined;

Expand Down
7 changes: 7 additions & 0 deletions packages/opentelemetry-core/src/utils/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ export interface ENVIRONMENT {
OTEL_LOG_LEVEL?: LogLevel;
OTEL_NO_PATCH_MODULES?: string;
OTEL_SAMPLING_PROBABILITY?: number;
OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT?: number | null;
}

const ENVIRONMENT_NUMBERS: Partial<keyof ENVIRONMENT>[] = [
'OTEL_SAMPLING_PROBABILITY',
'OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT',
];

/**
Expand All @@ -38,6 +40,7 @@ export const DEFAULT_ENVIRONMENT: Required<ENVIRONMENT> = {
OTEL_NO_PATCH_MODULES: '',
OTEL_LOG_LEVEL: LogLevel.INFO,
OTEL_SAMPLING_PROBABILITY: 1,
OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT: null,
};

/**
Expand Down Expand Up @@ -116,6 +119,10 @@ export function parseEnvironment(values: ENVIRONMENT_MAP): ENVIRONMENT {
setLogLevelFromEnv(key, environment, values);
break;

case 'OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT':
parseNumber(key, environment, values, 32, Number.MAX_SAFE_INTEGER);
break;

default:
if (ENVIRONMENT_NUMBERS.indexOf(key) >= 0) {
parseNumber(key, environment, values);
Expand Down
60 changes: 60 additions & 0 deletions packages/opentelemetry-core/test/common/attributes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
*/

import * as assert from 'assert';
import * as sinon from 'sinon';
import {
isAttributeValue,
sanitizeAttributes,
truncateValueIfTooLong,
} from '../../src/common/attributes';

describe('attributes', () => {
Expand Down Expand Up @@ -103,4 +105,62 @@ describe('attributes', () => {
assert.strictEqual(attributes.arr[0], 'unmodified');
});
});
describe('#truncateValueIfTooLong', () => {
it('should not truncate any given value if the limit is not set', () => {
assert.strictEqual(truncateValueIfTooLong('a', null), 'a');
assert.strictEqual(truncateValueIfTooLong(1, null), 1);
assert.strictEqual(truncateValueIfTooLong(true, null), true);

const arrayRef: string[] = [];
assert.strictEqual(truncateValueIfTooLong(arrayRef, null), arrayRef);
});

it('passes numbers and bools through', () => {
assert.strictEqual(truncateValueIfTooLong(true, 32), true);
assert.strictEqual(truncateValueIfTooLong(false, 32), false);
assert.strictEqual(truncateValueIfTooLong(1, 32), 1);
});

it('truncates strings if they are longer than the limit', () => {
assert.strictEqual(
truncateValueIfTooLong('a'.repeat(100), 100),
'a'.repeat(100)
);
assert.strictEqual(
truncateValueIfTooLong('a'.repeat(101), 100),
'a'.repeat(100)
);
});

it('serializes and truncates arrays if they are longer than the limit', () => {
assert.strictEqual(
truncateValueIfTooLong(['a'.repeat(100)], 32),
'["' + 'a'.repeat(30)
);
assert.strictEqual(
truncateValueIfTooLong(
[...new Array(10).keys()].map(() => 1000),
32
),
'[' + '1000,'.repeat(6) + '1'
);
assert.strictEqual(
truncateValueIfTooLong(
[...new Array(10).keys()].map(() => true),
32
),
'[' + 'true,'.repeat(6) + 't'
);
});

it('executes callback if a value was truncated', () => {
const fakeCallback = sinon.spy();

truncateValueIfTooLong('a'.repeat(32), 32, fakeCallback);
assert.ok(!fakeCallback.called);

truncateValueIfTooLong('a'.repeat(33), 32, fakeCallback);
assert(fakeCallback.called);
});
});
});
2 changes: 2 additions & 0 deletions packages/opentelemetry-core/test/utils/environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,13 @@ describe('environment', () => {
OTEL_NO_PATCH_MODULES: 'a,b,c',
OTEL_LOG_LEVEL: 'ERROR',
OTEL_SAMPLING_PROBABILITY: '0.5',
OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT: '32',
});
const env = getEnv();
assert.strictEqual(env.OTEL_NO_PATCH_MODULES, 'a,b,c');
assert.strictEqual(env.OTEL_LOG_LEVEL, LogLevel.ERROR);
assert.strictEqual(env.OTEL_SAMPLING_PROBABILITY, 0.5);
assert.strictEqual(env.OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT, 32);
});

it('should parse OTEL_LOG_LEVEL despite casing', () => {
Expand Down
17 changes: 15 additions & 2 deletions packages/opentelemetry-tracing/src/Span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

import * as api from '@opentelemetry/api';
import {
isAttributeValue,
getEnv,
hrTime,
hrTimeDuration,
InstrumentationLibrary,
isAttributeValue,
isTimeInput,
timeInputToHrTime,
truncateValueIfTooLong,
} from '@opentelemetry/core';
import { Resource } from '@opentelemetry/resources';
import {
Expand Down Expand Up @@ -56,6 +58,7 @@ export class Span implements api.Span, ReadableSpan {
endTime: api.HrTime = [0, 0];
private _ended = false;
private _duration: api.HrTime = [-1, -1];
private _hasTruncated = false;
private readonly _logger: api.Logger;
private readonly _spanProcessor: SpanProcessor;
private readonly _traceParams: TraceParams;
Expand Down Expand Up @@ -112,7 +115,17 @@ export class Span implements api.Span, ReadableSpan {
delete this.attributes[attributeKeyToDelete];
}
}
this.attributes[key] = value;
this.attributes[key] = truncateValueIfTooLong(
value,
this._traceParams.spanAttributeValueSizeLimit ||
getEnv().OTEL_SPAN_ATTRIBUTE_VALUE_SIZE_LIMIT,
this._hasTruncated
? undefined
: () => {
this._hasTruncated = true;
this._logger.warn(`Span attribute value truncated at key: ${key}.`);
}
);
return this;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/opentelemetry-tracing/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export interface TraceParams {
numberOfLinksPerSpan?: number;
/** numberOfEventsPerSpan is number of message events per span */
numberOfEventsPerSpan?: number;
/** this field defines maximum length of attribute value before it is truncated */
spanAttributeValueSizeLimit?: number;
}

/** Interface configuration for a buffer. */
Expand Down
81 changes: 81 additions & 0 deletions packages/opentelemetry-tracing/test/Span.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from '@opentelemetry/core';
import { ExceptionAttribute } from '@opentelemetry/semantic-conventions';
import * as assert from 'assert';
import sinon = require('sinon');
import { BasicTracerProvider, Span, SpanProcessor } from '../src';

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

it('should truncate attribute values exceeding length limit', () => {
const tracerWithLimit = new BasicTracerProvider({
logger: new NoopLogger(),
traceParams: {
spanAttributeValueSizeLimit: 100,
},
}).getTracer('default');

const spanWithLimit = new Span(
tracerWithLimit,
name,
spanContext,
SpanKind.CLIENT
);
const spanWithoutLimit = new Span(
tracer,
name,
spanContext,
SpanKind.CLIENT
);

spanWithLimit.setAttribute('attr under limit', 'a'.repeat(100));
assert.strictEqual(
spanWithLimit.attributes['attr under limit'],
'a'.repeat(100)
);
spanWithoutLimit.setAttribute('attr under limit', 'a'.repeat(100));
assert.strictEqual(
spanWithoutLimit.attributes['attr under limit'],
'a'.repeat(100)
);

spanWithLimit.setAttribute('attr over limit', 'b'.repeat(101));
assert.strictEqual(
spanWithLimit.attributes['attr over limit'],
'b'.repeat(100)
);
spanWithoutLimit.setAttribute('attr over limit', 'b'.repeat(101));
assert.strictEqual(
spanWithoutLimit.attributes['attr over limit'],
'b'.repeat(101)
);
});

it('should warn once when truncating attribute values exceeding length limit', () => {
const logger = new NoopLogger();
const loggerWarnSpy = sinon.spy(logger, 'warn');

const tracerWithLimit = new BasicTracerProvider({
logger,
traceParams: {
spanAttributeValueSizeLimit: 100,
},
}).getTracer('default');

const spanWithLimit = new Span(
tracerWithLimit,
name,
spanContext,
SpanKind.CLIENT
);

spanWithLimit.setAttribute('longAttr', 'b'.repeat(100));
assert(!loggerWarnSpy.called);

spanWithLimit.setAttribute('longAttr', 'b'.repeat(101));
assert(
loggerWarnSpy.withArgs('Span attribute value truncated at key: longAttr.')
.calledOnce
);

spanWithLimit.setAttribute('longAttr', 'c'.repeat(102));
assert(
loggerWarnSpy.withArgs('Span attribute value truncated at key: longAttr.')
.calledOnce
);

assert.strictEqual(spanWithLimit.attributes.longAttr, 'c'.repeat(100));
});

it('should set an error status', () => {
const span = new Span(tracer, name, spanContext, SpanKind.CLIENT);
span.setStatus({
Expand Down

0 comments on commit 139bfcc

Please sign in to comment.