diff --git a/packages/opentelemetry-core/src/utils/environment.ts b/packages/opentelemetry-core/src/utils/environment.ts index b6e9dc5db4..7df0be698c 100644 --- a/packages/opentelemetry-core/src/utils/environment.ts +++ b/packages/opentelemetry-core/src/utils/environment.ts @@ -28,6 +28,8 @@ const ENVIRONMENT_NUMBERS_KEYS = [ 'OTEL_BSP_MAX_EXPORT_BATCH_SIZE', 'OTEL_BSP_MAX_QUEUE_SIZE', 'OTEL_BSP_SCHEDULE_DELAY', + 'OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT', + 'OTEL_ATTRIBUTE_COUNT_LIMIT', 'OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT', 'OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT', 'OTEL_SPAN_EVENT_COUNT_LIMIT', @@ -88,6 +90,10 @@ export type RAW_ENVIRONMENT = { [key: string]: string | number | undefined | string[]; }; +export const DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT = Infinity; + +export const DEFAULT_ATTRIBUTE_COUNT_LIMIT = 128; + /** * Default environment variables */ @@ -118,8 +124,10 @@ export const DEFAULT_ENVIRONMENT: Required = { OTEL_PROPAGATORS: ['tracecontext', 'baggage'], OTEL_RESOURCE_ATTRIBUTES: '', OTEL_SERVICE_NAME: '', - OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: Infinity, - OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: 128, + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT, + OTEL_ATTRIBUTE_COUNT_LIMIT: DEFAULT_ATTRIBUTE_COUNT_LIMIT, + OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT , + OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: DEFAULT_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT: 128, OTEL_SPAN_LINK_COUNT_LIMIT: 128, OTEL_TRACES_EXPORTER: 'none', diff --git a/packages/opentelemetry-core/test/utils/environment.test.ts b/packages/opentelemetry-core/test/utils/environment.test.ts index 3f3a709b62..4ab3518645 100644 --- a/packages/opentelemetry-core/test/utils/environment.test.ts +++ b/packages/opentelemetry-core/test/utils/environment.test.ts @@ -84,6 +84,8 @@ describe('environment', () => { OTEL_LOG_LEVEL: 'ERROR', OTEL_NO_PATCH_MODULES: 'a,b,c', OTEL_RESOURCE_ATTRIBUTES: '', + OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT: 40, + OTEL_ATTRIBUTE_COUNT_LIMIT: 50, OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: 100, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: 10, OTEL_SPAN_EVENT_COUNT_LIMIT: 20, @@ -94,6 +96,8 @@ describe('environment', () => { const env = getEnv(); assert.deepStrictEqual(env.OTEL_NO_PATCH_MODULES, ['a', 'b', 'c']); assert.strictEqual(env.OTEL_LOG_LEVEL, DiagLogLevel.ERROR); + assert.strictEqual(env.OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, 40); + assert.strictEqual(env.OTEL_ATTRIBUTE_COUNT_LIMIT, 50); assert.strictEqual(env.OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, 100); assert.strictEqual(env.OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, 10); assert.strictEqual(env.OTEL_SPAN_EVENT_COUNT_LIMIT, 20); diff --git a/packages/opentelemetry-sdk-trace-base/src/Tracer.ts b/packages/opentelemetry-sdk-trace-base/src/Tracer.ts index f39d55d9fd..9960243cba 100644 --- a/packages/opentelemetry-sdk-trace-base/src/Tracer.ts +++ b/packages/opentelemetry-sdk-trace-base/src/Tracer.ts @@ -25,7 +25,7 @@ import { import { Resource } from '@opentelemetry/resources'; import { BasicTracerProvider } from './BasicTracerProvider'; import { Span } from './Span'; -import { SpanLimits, TracerConfig } from './types'; +import { GeneralLimits, SpanLimits, TracerConfig } from './types'; import { mergeConfig } from './utility'; import { SpanProcessor } from './SpanProcessor'; @@ -34,6 +34,7 @@ import { SpanProcessor } from './SpanProcessor'; */ export class Tracer implements api.Tracer { private readonly _sampler: api.Sampler; + private readonly _generalLimits: GeneralLimits; private readonly _spanLimits: SpanLimits; private readonly _idGenerator: IdGenerator; readonly resource: Resource; @@ -49,6 +50,7 @@ export class Tracer implements api.Tracer { ) { const localConfig = mergeConfig(config); this._sampler = localConfig.sampler; + this._generalLimits = localConfig.generalLimits; this._spanLimits = localConfig.spanLimits; this._idGenerator = config.idGenerator || new RandomIdGenerator(); this.resource = _tracerProvider.resource; @@ -212,6 +214,11 @@ export class Tracer implements api.Tracer { return api.context.with(contextWithSpanSet, fn, undefined, span); } + /** Returns the active {@link GeneralLimits}. */ + getGeneralLimits(): GeneralLimits { + return this._generalLimits; + } + /** Returns the active {@link SpanLimits}. */ getSpanLimits(): SpanLimits { return this._spanLimits; diff --git a/packages/opentelemetry-sdk-trace-base/src/config.ts b/packages/opentelemetry-sdk-trace-base/src/config.ts index 7ed67c2b12..4b364b3718 100644 --- a/packages/opentelemetry-sdk-trace-base/src/config.ts +++ b/packages/opentelemetry-sdk-trace-base/src/config.ts @@ -38,6 +38,10 @@ const DEFAULT_RATIO = 1; export const DEFAULT_CONFIG = { sampler: buildSamplerFromEnv(env), forceFlushTimeoutMillis: 30000, + generalLimits: { + attributeValueLengthLimit: getEnv().OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, + attributeCountLimit: getEnv().OTEL_ATTRIBUTE_COUNT_LIMIT, + }, spanLimits: { attributeValueLengthLimit: getEnv().OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, attributeCountLimit: getEnv().OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, diff --git a/packages/opentelemetry-sdk-trace-base/src/types.ts b/packages/opentelemetry-sdk-trace-base/src/types.ts index 91a330d9c9..d60df053ac 100644 --- a/packages/opentelemetry-sdk-trace-base/src/types.ts +++ b/packages/opentelemetry-sdk-trace-base/src/types.ts @@ -29,6 +29,9 @@ export interface TracerConfig { */ sampler?: Sampler; + /** General Limits */ + generalLimits?: GeneralLimits; + /** Span Limits */ spanLimits?: SpanLimits; @@ -61,6 +64,14 @@ export interface SDKRegistrationConfig { contextManager?: ContextManager | null; } +/** Global configuration limits of trace service */ +export interface GeneralLimits { + /** attributeValueLengthLimit is maximum allowed attribute value size */ + attributeValueLengthLimit?: number; + /** attributeCountLimit is number of attributes per trace */ + attributeCountLimit?: number; +} + /** Global configuration of trace service */ export interface SpanLimits { /** attributeValueLengthLimit is maximum allowed attribute value size */ diff --git a/packages/opentelemetry-sdk-trace-base/src/utility.ts b/packages/opentelemetry-sdk-trace-base/src/utility.ts index 542c26925e..44654ae87d 100644 --- a/packages/opentelemetry-sdk-trace-base/src/utility.ts +++ b/packages/opentelemetry-sdk-trace-base/src/utility.ts @@ -14,15 +14,21 @@ * limitations under the License. */ +import { DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT, DEFAULT_ATTRIBUTE_COUNT_LIMIT } from '@opentelemetry/core'; + import { Sampler } from '@opentelemetry/api'; import { buildSamplerFromEnv, DEFAULT_CONFIG } from './config'; -import { SpanLimits, TracerConfig } from './types'; +import { SpanLimits, TracerConfig, GeneralLimits } from './types'; /** * Function to merge Default configuration (as specified in './config') with * user provided configurations. */ -export function mergeConfig(userConfig: TracerConfig): TracerConfig & { sampler: Sampler; spanLimits: SpanLimits } { +export function mergeConfig(userConfig: TracerConfig): TracerConfig & { + sampler: Sampler; + spanLimits: SpanLimits; + generalLimits: GeneralLimits; +} { const perInstanceDefaults: Partial = { sampler: buildSamplerFromEnv(), }; @@ -34,11 +40,33 @@ export function mergeConfig(userConfig: TracerConfig): TracerConfig & { sampler: userConfig ); + target.generalLimits = Object.assign( + {}, + DEFAULT_CONFIG.generalLimits, + userConfig.generalLimits || {} + ); + target.spanLimits = Object.assign( {}, DEFAULT_CONFIG.spanLimits, userConfig.spanLimits || {} ); + /** + * When span attribute count limit is not defined, but general attribute count limit is defined + * Then, span attribute count limit will be same as general one + */ + if (target.spanLimits.attributeCountLimit === DEFAULT_ATTRIBUTE_COUNT_LIMIT && target.generalLimits.attributeCountLimit !== DEFAULT_ATTRIBUTE_COUNT_LIMIT) { + target.spanLimits.attributeCountLimit = target.generalLimits.attributeCountLimit; + } + + /** + * When span attribute value length limit is not defined, but general attribute value length limit is defined + * Then, span attribute value length limit will be same as general one + */ + if (target.spanLimits.attributeValueLengthLimit === DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT && target.generalLimits.attributeValueLengthLimit !== DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT) { + target.spanLimits.attributeValueLengthLimit = target.generalLimits.attributeValueLengthLimit; + } + return target; } diff --git a/packages/opentelemetry-sdk-trace-base/test/common/BasicTracerProvider.test.ts b/packages/opentelemetry-sdk-trace-base/test/common/BasicTracerProvider.test.ts index 581a30f855..1b5e6872e3 100644 --- a/packages/opentelemetry-sdk-trace-base/test/common/BasicTracerProvider.test.ts +++ b/packages/opentelemetry-sdk-trace-base/test/common/BasicTracerProvider.test.ts @@ -85,6 +85,52 @@ describe('BasicTracerProvider', () => { }); }); + describe('generalLimits', () => { + describe('when not defined default values', () => { + it('should have tracer with default values', () => { + const tracer = new BasicTracerProvider({}).getTracer('default'); + assert.deepStrictEqual(tracer.getGeneralLimits(), { + attributeValueLengthLimit: Infinity, + attributeCountLimit: 128, + }); + }); + }); + + describe('when "attributeCountLimit" is defined', () => { + it('should have tracer with defined value', () => { + const tracer = new BasicTracerProvider({ + generalLimits: { + attributeCountLimit: 100, + }, + }).getTracer('default'); + const generalLimits = tracer.getGeneralLimits(); + assert.strictEqual(generalLimits.attributeCountLimit, 100); + }); + }); + + describe('when "attributeValueLengthLimit" is defined', () => { + it('should have tracer with defined value', () => { + const tracer = new BasicTracerProvider({ + generalLimits: { + attributeValueLengthLimit: 10, + }, + }).getTracer('default'); + const generalLimits = tracer.getGeneralLimits(); + assert.strictEqual(generalLimits.attributeValueLengthLimit, 10); + }); + + it('should have tracer with negative "attributeValueLengthLimit" value', () => { + const tracer = new BasicTracerProvider({ + generalLimits: { + attributeValueLengthLimit: -10, + }, + }).getTracer('default'); + const generalLimits = tracer.getGeneralLimits(); + assert.strictEqual(generalLimits.attributeValueLengthLimit, -10); + }); + }); + }); + describe('spanLimits', () => { describe('when not defined default values', () => { it('should have tracer with default values', () => { @@ -155,6 +201,38 @@ describe('BasicTracerProvider', () => { assert.strictEqual(spanLimits.linkCountLimit, 10); }); }); + + describe('when only generalLimits are defined', () => { + it('should have span limits as general limits', () => { + const tracer = new BasicTracerProvider({ + generalLimits: { + attributeValueLengthLimit: 100, + attributeCountLimit: 200, + }, + }).getTracer('default'); + const spanLimits = tracer.getSpanLimits(); + assert.strictEqual(spanLimits.attributeValueLengthLimit, 100); + assert.strictEqual(spanLimits.attributeCountLimit, 200); + }); + }); + + describe('when both generalLimits and spanLimits defined', () => { + it('should have span limits as priority than general limits', () => { + const tracer = new BasicTracerProvider({ + generalLimits: { + attributeValueLengthLimit: 100, + attributeCountLimit: 200, + }, + spanLimits: { + attributeValueLengthLimit: 10, + attributeCountLimit: 20, + }, + }).getTracer('default'); + const spanLimits = tracer.getSpanLimits(); + assert.strictEqual(spanLimits.attributeValueLengthLimit, 10); + assert.strictEqual(spanLimits.attributeCountLimit, 20); + }); + }); }); }); diff --git a/packages/opentelemetry-sdk-trace-base/test/common/Span.test.ts b/packages/opentelemetry-sdk-trace-base/test/common/Span.test.ts index 21e5eb37ff..d9ca0b1301 100644 --- a/packages/opentelemetry-sdk-trace-base/test/common/Span.test.ts +++ b/packages/opentelemetry-sdk-trace-base/test/common/Span.test.ts @@ -267,6 +267,99 @@ describe('Span', () => { }); }); + describe('when generalLimits options set', () => { + describe('when "attributeCountLimit" option defined', () => { + const tracer = new BasicTracerProvider({ + generalLimits: { + // Setting count limit + attributeCountLimit: 100, + }, + }).getTracer('default'); + + const span = new Span( + tracer, + ROOT_CONTEXT, + name, + spanContext, + SpanKind.CLIENT + ); + for (let i = 0; i < 150; i++) { + span.setAttribute('foo' + i, 'bar' + i); + } + span.end(); + + it('should remove / drop all remaining values after the number of values exceeds this limit', () => { + assert.strictEqual(Object.keys(span.attributes).length, 100); + assert.strictEqual(span.attributes['foo0'], 'bar0'); + assert.strictEqual(span.attributes['foo99'], 'bar99'); + assert.strictEqual(span.attributes['foo149'], undefined); + }); + }); + + describe('when "attributeValueLengthLimit" option defined', () => { + const tracer = new BasicTracerProvider({ + generalLimits: { + // Setting attribute value length limit + attributeValueLengthLimit: 5, + }, + }).getTracer('default'); + + const span = new Span( + tracer, + ROOT_CONTEXT, + name, + spanContext, + SpanKind.CLIENT + ); + + it('should truncate value which length exceeds this limit', () => { + span.setAttribute('attr-with-more-length', 'abcdefgh'); + assert.strictEqual(span.attributes['attr-with-more-length'], 'abcde'); + }); + + it('should truncate value of arrays which exceeds this limit', () => { + span.setAttribute('attr-array-of-strings', ['abcdefgh', 'abc', 'abcde', '']); + span.setAttribute('attr-array-of-bool', [true, false]); + assert.deepStrictEqual(span.attributes['attr-array-of-strings'], ['abcde', 'abc', 'abcde', '']); + assert.deepStrictEqual(span.attributes['attr-array-of-bool'], [true, false]); + }); + + it('should not truncate value which length not exceeds this limit', () => { + span.setAttribute('attr-with-less-length', 'abc'); + assert.strictEqual(span.attributes['attr-with-less-length'], 'abc'); + }); + + it('should return same value for non-string values', () => { + span.setAttribute('attr-non-string', true); + assert.strictEqual(span.attributes['attr-non-string'], true); + }); + }); + + describe('when "attributeValueLengthLimit" option is invalid', () => { + const tracer = new BasicTracerProvider({ + generalLimits: { + // Setting invalid attribute value length limit + attributeValueLengthLimit: -5, + }, + }).getTracer('default'); + + const span = new Span( + tracer, + ROOT_CONTEXT, + name, + spanContext, + SpanKind.CLIENT + ); + + it('should not truncate any value', () => { + span.setAttribute('attr-not-truncate', 'abcdefgh'); + span.setAttribute('attr-array-of-strings', ['abcdefgh', 'abc', 'abcde']); + assert.deepStrictEqual(span.attributes['attr-not-truncate'], 'abcdefgh'); + assert.deepStrictEqual(span.attributes['attr-array-of-strings'], ['abcdefgh', 'abc', 'abcde']); + }); + }); + }); + describe('when spanLimits options set', () => { describe('when "attributeCountLimit" option defined', () => { const tracer = new BasicTracerProvider({ @@ -359,6 +452,83 @@ describe('Span', () => { }); }); }); + + describe('when both generalLimits and spanLimits options set', () => { + describe('when "attributeCountLimit" option defined', () => { + const tracer = new BasicTracerProvider({ + generalLimits: { + // Setting count limit + attributeCountLimit: 10, + }, + spanLimits: { + attributeCountLimit: 5, + } + }).getTracer('default'); + + const span = new Span( + tracer, + ROOT_CONTEXT, + name, + spanContext, + SpanKind.CLIENT + ); + for (let i = 0; i < 150; i++) { + span.setAttribute('foo' + i, 'bar' + i); + } + span.end(); + + it('should remove / drop all remaining values after the number of values exceeds span limit', () => { + assert.strictEqual(Object.keys(span.attributes).length, 5); + assert.strictEqual(span.attributes['foo0'], 'bar0'); + assert.strictEqual(span.attributes['foo4'], 'bar4'); + assert.strictEqual(span.attributes['foo5'], undefined); + assert.strictEqual(span.attributes['foo10'], undefined); + }); + }); + + describe('when "attributeValueLengthLimit" option defined', () => { + const tracer = new BasicTracerProvider({ + generalLimits: { + // Setting attribute value length limit + attributeValueLengthLimit: 10, + }, + spanLimits: { + // Setting attribute value length limit + attributeValueLengthLimit: 5, + }, + }).getTracer('default'); + + const span = new Span( + tracer, + ROOT_CONTEXT, + name, + spanContext, + SpanKind.CLIENT + ); + + it('should truncate value which length exceeds span limit', () => { + span.setAttribute('attr-with-more-length', 'abcdefgh'); + assert.strictEqual(span.attributes['attr-with-more-length'], 'abcde'); + }); + + it('should truncate value of arrays which exceeds span limit', () => { + span.setAttribute('attr-array-of-strings', ['abcdefgh', 'abc', 'abcde', '']); + span.setAttribute('attr-array-of-bool', [true, false]); + assert.deepStrictEqual(span.attributes['attr-array-of-strings'], ['abcde', 'abc', 'abcde', '']); + assert.deepStrictEqual(span.attributes['attr-array-of-bool'], [true, false]); + }); + + it('should not truncate value which length not exceeds span limit', () => { + span.setAttribute('attr-with-less-length', 'abc'); + assert.strictEqual(span.attributes['attr-with-less-length'], 'abc'); + }); + + it('should return same value for non-string values', () => { + span.setAttribute('attr-non-string', true); + assert.strictEqual(span.attributes['attr-non-string'], true); + }); + }); + }); }); describe('setAttributes', () => {