diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a277b5b3d6..2772fb4f785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,12 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/ * feat(sdk-metrics): apply binary search in histogram recording [#3539](/~https://github.com/open-telemetry/opentelemetry-js/pull/3539) @legendecas * perf(propagator-jaeger): improve deserializeSpanContext performance [#3541](/~https://github.com/open-telemetry/opentelemetry-js/pull/3541) @doochik * feat: support TraceState in SamplingResult [#3530](/~https://github.com/open-telemetry/opentelemetry-js/pull/3530) @raphael-theriault-swi +* feat(sdk-trace-base): add diagnostic logging when spans are dropped [#3610](/~https://github.com/open-telemetry/opentelemetry-js/pull/3610) @neoeinstein ### :bug: (Bug Fix) +* fix(core): added falsy check to make otel core work with browser where webpack config had process as false or null [#3613](/~https://github.com/open-telemetry/opentelemetry-js/issues/3613) @ravindra-dyte + ### :books: (Refine Doc) ### :house: (Internal) diff --git a/api/src/internal/global-utils.ts b/api/src/internal/global-utils.ts index 0753bc27758..b8c5fb16dbf 100644 --- a/api/src/internal/global-utils.ts +++ b/api/src/internal/global-utils.ts @@ -54,7 +54,7 @@ export function registerGlobal( if (api.version !== VERSION) { // All registered APIs must be of the same version exactly const err = new Error( - '@opentelemetry/api: All API registration versions must match' + `@opentelemetry/api: Registration of version v${api.version} for ${type} does not match previously registered API v${VERSION}` ); diag.error(err.stack || err.message); return false; diff --git a/api/test/common/internal/global.test.ts b/api/test/common/internal/global.test.ts index 291f7072336..7a630c22ce1 100644 --- a/api/test/common/internal/global.test.ts +++ b/api/test/common/internal/global.test.ts @@ -84,23 +84,22 @@ describe('Global Utils', () => { assert.notStrictEqual(manager, api1.context['_getContextManager']()); }); - it('should return the module NoOp implementation if the version is a mismatch', () => { - const newContextManager = new NoopContextManager(); - api1.context.setGlobalContextManager(newContextManager); - - // ensure new context manager is returned - assert.strictEqual(api1.context['_getContextManager'](), newContextManager); + it('should not register if the version is a mismatch', () => { + const logger1 = getMockLogger(); + const logger2 = getMockLogger(); - const globalInstance = getGlobal('context'); + api1.diag.setLogger(logger1); + const globalInstance = getGlobal('diag'); assert.ok(globalInstance); // @ts-expect-error we are modifying internals for testing purposes here _globalThis[Symbol.for(GLOBAL_API_SYMBOL_KEY)].version = '0.0.1'; - // ensure new context manager is not returned because version above is incompatible - assert.notStrictEqual( - api1.context['_getContextManager'](), - newContextManager - ); + assert.equal(false, api1.diag.setLogger(logger2)); // won't happen + + api1.diag.info('message'); + sinon.assert.notCalled(logger2.error); + sinon.assert.notCalled(logger2.info); + sinon.assert.notCalled(logger2.warn); }); it('should debug log registrations', () => { diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 5f14e88c50c..b85f4c1d5f5 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -6,9 +6,13 @@ All notable changes to experimental packages in this project will be documented ### :boom: Breaking Change +* feat: remove HTTP/HTTPS prefix from span name [#3603](/~https://github.com/open-telemetry/opentelemetry-js/pull/3603) @Flarna + ### :rocket: (Enhancement) +* feat: use HTTP_ROUTE in span name [#3603](/~https://github.com/open-telemetry/opentelemetry-js/pull/3603) @Flarna * feat: add HTTP_ROUTE attribute to http incoming metrics if present [#3581](/~https://github.com/open-telemetry/opentelemetry-js/pull/3581) @hermogenes +* feat(sdk-node): install diag logger with OTEL_LOG_LEVEL [#3627](/~https://github.com/open-telemetry/opentelemetry-js/pull/3627) @legendecas ### :bug: (Bug Fix) @@ -71,6 +75,7 @@ All notable changes to experimental packages in this project will be documented * deps: remove unused proto-loader dependencies and update grpc-js and proto-loader versions [#3337](/~https://github.com/open-telemetry/opentelemetry-js/pull/3337) @seemk * feat(metrics-exporters): configure temporality via environment variable [#3305](/~https://github.com/open-telemetry/opentelemetry-js/pull/3305) @pichlermarc * feat(console-metric-exporter): add temporality configuration [#3387](/~https://github.com/open-telemetry/opentelemetry-js/pull/3387) @pichlermarc +* feat(otlp-exporter-base): add retries [#3207](/~https://github.com/open-telemetry/opentelemetry-js/pull/3207) @svetlanabrennan ### :bug: (Bug Fix) diff --git a/experimental/packages/exporter-trace-otlp-http/README.md b/experimental/packages/exporter-trace-otlp-http/README.md index ef4259fcd1e..f5944a047bc 100644 --- a/experimental/packages/exporter-trace-otlp-http/README.md +++ b/experimental/packages/exporter-trace-otlp-http/README.md @@ -143,6 +143,21 @@ To override the default timeout duration, use the following options: > Providing `timeoutMillis` with `collectorOptions` takes precedence and overrides timeout set with environment variables. +## OTLP Exporter Retry + +OTLP requires that transient errors be handled with a [retry strategy](/~https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#retry). + +This retry policy has the following configuration, which there is currently no way to customize. + ++ `DEFAULT_EXPORT_MAX_ATTEMPTS`: The maximum number of attempts, including the original request. Defaults to 5. ++ `DEFAULT_EXPORT_INITIAL_BACKOFF`: The initial backoff duration. Defaults to 1 second. ++ `DEFAULT_EXPORT_MAX_BACKOFF`: The maximum backoff duration. Defaults to 5 seconds. ++ `DEFAULT_EXPORT_BACKOFF_MULTIPLIER`: The backoff multiplier. Defaults to 1.5. + +This retry policy first checks if the response has a `'Retry-After'` header. If there is a `'Retry-After'` header, the exporter will wait the amount specified in the `'Retry-After'` header before retrying. If there is no `'Retry-After'` header, the exporter will use an exponential backoff with jitter retry strategy. + + > The exporter will retry exporting within the [exporter timeout configuration](#Exporter-Timeout-Configuration) time. + ## Running opentelemetry-collector locally to see the traces 1. Go to `examples/otlp-exporter-node` diff --git a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts index 81bc6c6b49d..4e8bc1d6e17 100644 --- a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts @@ -582,3 +582,123 @@ describe('when configuring via environment', () => { envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; }); }); + +describe('export with retry - real http request destroyed', () => { + let server: any; + let collectorTraceExporter: OTLPTraceExporter; + let collectorExporterConfig: OTLPExporterConfigBase; + let spans: ReadableSpan[]; + + beforeEach(() => { + server = sinon.fakeServer.create({ + autoRespond: true, + }); + collectorExporterConfig = { + timeoutMillis: 1500, + }; + }); + + afterEach(() => { + server.restore(); + }); + + describe('when "sendBeacon" is NOT available', () => { + beforeEach(() => { + (window.navigator as any).sendBeacon = false; + collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); + }); + it('should log the timeout request error message when retrying with exponential backoff with jitter', done => { + spans = []; + spans.push(Object.assign({}, mockedReadableSpan)); + + let retry = 0; + server.respondWith( + 'http://localhost:4318/v1/traces', + function (xhr: any) { + retry++; + xhr.respond(503); + } + ); + + collectorTraceExporter.export(spans, result => { + assert.strictEqual(result.code, core.ExportResultCode.FAILED); + const error = result.error as OTLPExporterError; + assert.ok(error !== undefined); + assert.strictEqual(error.message, 'Request Timeout'); + assert.strictEqual(retry, 1); + done(); + }); + }).timeout(3000); + + it('should log the timeout request error message when retry-after header is set to 3 seconds', done => { + spans = []; + spans.push(Object.assign({}, mockedReadableSpan)); + + let retry = 0; + server.respondWith( + 'http://localhost:4318/v1/traces', + function (xhr: any) { + retry++; + xhr.respond(503, { 'Retry-After': 3 }); + } + ); + + collectorTraceExporter.export(spans, result => { + assert.strictEqual(result.code, core.ExportResultCode.FAILED); + const error = result.error as OTLPExporterError; + assert.ok(error !== undefined); + assert.strictEqual(error.message, 'Request Timeout'); + assert.strictEqual(retry, 1); + done(); + }); + }).timeout(3000); + it('should log the timeout request error message when retry-after header is a date', done => { + spans = []; + spans.push(Object.assign({}, mockedReadableSpan)); + + let retry = 0; + server.respondWith( + 'http://localhost:4318/v1/traces', + function (xhr: any) { + retry++; + const d = new Date(); + d.setSeconds(d.getSeconds() + 1); + xhr.respond(503, { 'Retry-After': d }); + } + ); + + collectorTraceExporter.export(spans, result => { + assert.strictEqual(result.code, core.ExportResultCode.FAILED); + const error = result.error as OTLPExporterError; + assert.ok(error !== undefined); + assert.strictEqual(error.message, 'Request Timeout'); + assert.strictEqual(retry, 2); + done(); + }); + }).timeout(3000); + it('should log the timeout request error message when retry-after header is a date with long delay', done => { + spans = []; + spans.push(Object.assign({}, mockedReadableSpan)); + + let retry = 0; + server.respondWith( + 'http://localhost:4318/v1/traces', + function (xhr: any) { + retry++; + const d = new Date(); + d.setSeconds(d.getSeconds() + 120); + xhr.respond(503, { 'Retry-After': d }); + } + ); + + collectorTraceExporter.export(spans, result => { + assert.strictEqual(result.code, core.ExportResultCode.FAILED); + const error = result.error as OTLPExporterError; + assert.ok(error !== undefined); + assert.strictEqual(error.message, 'Request Timeout'); + assert.strictEqual(retry, 1); + done(); + }); + }).timeout(3000); + }); +}); diff --git a/experimental/packages/exporter-trace-otlp-http/test/node/CollectorTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-http/test/node/CollectorTraceExporter.test.ts index e7006dbda52..426aed4431f 100644 --- a/experimental/packages/exporter-trace-otlp-http/test/node/CollectorTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-http/test/node/CollectorTraceExporter.test.ts @@ -551,38 +551,3 @@ describe('export - real http request destroyed before response received', () => }, 0); }); }); - -describe('export - real http request destroyed after response received', () => { - let collectorExporter: OTLPTraceExporter; - let collectorExporterConfig: OTLPExporterNodeConfigBase; - let spans: ReadableSpan[]; - - const server = http.createServer((_, res) => { - res.write('writing something'); - }); - before(done => { - server.listen(8081, done); - }); - after(done => { - server.close(done); - }); - it('should log the timeout request error message', done => { - collectorExporterConfig = { - url: 'http://localhost:8081', - timeoutMillis: 300, - }; - collectorExporter = new OTLPTraceExporter(collectorExporterConfig); - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - - setTimeout(() => { - collectorExporter.export(spans, result => { - assert.strictEqual(result.code, core.ExportResultCode.FAILED); - const error = result.error as OTLPExporterError; - assert.ok(error !== undefined); - assert.strictEqual(error.message, 'Request Timeout'); - done(); - }); - }, 0); - }); -}); diff --git a/experimental/packages/exporter-trace-otlp-proto/README.md b/experimental/packages/exporter-trace-otlp-proto/README.md index 0338b4cd936..efd22d2abf9 100644 --- a/experimental/packages/exporter-trace-otlp-proto/README.md +++ b/experimental/packages/exporter-trace-otlp-proto/README.md @@ -72,6 +72,21 @@ To override the default timeout duration, use the following options: > Providing `timeoutMillis` with `collectorOptions` takes precedence and overrides timeout set with environment variables. +## OTLP Exporter Retry + +OTLP requires that transient errors be handled with a [retry strategy](/~https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#retry). + +This retry policy has the following configuration, which there is currently no way to customize. + ++ `DEFAULT_EXPORT_MAX_ATTEMPTS`: The maximum number of attempts, including the original request. Defaults to 5. ++ `DEFAULT_EXPORT_INITIAL_BACKOFF`: The initial backoff duration. Defaults to 1 second. ++ `DEFAULT_EXPORT_MAX_BACKOFF`: The maximum backoff duration. Defaults to 5 seconds. ++ `DEFAULT_EXPORT_BACKOFF_MULTIPLIER`: The backoff multiplier. Defaults to 1.5. + +This retry policy first checks if the response has a `'Retry-After'` header. If there is a `'Retry-After'` header, the exporter will wait the amount specified in the `'Retry-After'` header before retrying. If there is no `'Retry-After'` header, the exporter will use an exponential backoff with jitter retry strategy. + + > The exporter will retry exporting within the [exporter timeout configuration](#Exporter-Timeout-Configuration) time. + ## Running opentelemetry-collector locally to see the traces 1. Go to examples/otlp-exporter-node diff --git a/experimental/packages/opentelemetry-instrumentation-http/src/http.ts b/experimental/packages/opentelemetry-instrumentation-http/src/http.ts index 23a3d2ee98d..7291780e869 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/src/http.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/src/http.ts @@ -58,6 +58,7 @@ import { } from '@opentelemetry/instrumentation'; import { RPCMetadata, RPCType, setRPCMetadata } from '@opentelemetry/core'; import { errorMonitor } from 'events'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; /** * Http instrumentation instrumentation for Opentelemetry @@ -300,14 +301,12 @@ export class HttpInstrumentation extends InstrumentationBase { * Attach event listeners to a client request to end span and add span attributes. * * @param request The original request object. - * @param options The arguments to the original function. * @param span representing the current operation * @param startTime representing the start time of the request to calculate duration in Metric * @param metricAttributes metric attributes */ private _traceClientRequest( request: http.ClientRequest, - hostname: string, span: Span, startTime: HrTime, metricAttributes: MetricAttributes @@ -491,11 +490,7 @@ export class HttpInstrumentation extends InstrumentationBase { utils.getIncomingRequestMetricAttributes(spanAttributes); const ctx = propagation.extract(ROOT_CONTEXT, headers); - const span = instrumentation._startHttpSpan( - `${component.toLocaleUpperCase()} ${method}`, - spanOptions, - ctx - ); + const span = instrumentation._startHttpSpan(method, spanOptions, ctx); const rpcMetadata: RPCMetadata = { type: RPCType.HTTP, span, @@ -624,7 +619,6 @@ export class HttpInstrumentation extends InstrumentationBase { return original.apply(this, [optionsParsed, ...args]); } - const operationName = `${component.toUpperCase()} ${method}`; const { hostname, port } = utils.extractHostnameAndPort(optionsParsed); const attributes = utils.getOutgoingRequestAttributes(optionsParsed, { @@ -645,7 +639,7 @@ export class HttpInstrumentation extends InstrumentationBase { kind: SpanKind.CLIENT, attributes, }; - const span = instrumentation._startHttpSpan(operationName, spanOptions); + const span = instrumentation._startHttpSpan(method, spanOptions); const parentContext = context.active(); const requestContext = trace.setSpan(parentContext, span); @@ -687,7 +681,6 @@ export class HttpInstrumentation extends InstrumentationBase { context.bind(parentContext, request); return instrumentation._traceClientRequest( request, - hostname, span, startTime, metricAttributes @@ -720,6 +713,11 @@ export class HttpInstrumentation extends InstrumentationBase { code: utils.parseResponseStatus(SpanKind.SERVER, response.statusCode), }); + const route = attributes[SemanticAttributes.HTTP_ROUTE]; + if (route) { + span.updateName(`${request.method || 'GET'} ${route}`); + } + if (this._getConfig().applyCustomAttributesOnSpan) { safeExecuteInTheMiddle( () => diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts index 9ebfe41c80f..5a5588993fd 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts @@ -272,6 +272,13 @@ describe('HttpInstrumentation', () => { if (request.url?.includes('/ignored')) { provider.getTracer('test').startSpan('some-span').end(); } + if (request.url?.includes('/setroute')) { + const rpcData = getRPCMetadata(context.active()); + assert.ok(rpcData != null); + assert.strictEqual(rpcData.type, RPCType.HTTP); + assert.strictEqual(rpcData.route, undefined); + rpcData.route = 'TheRoute'; + } response.end('Test Server Response'); }); @@ -339,6 +346,20 @@ describe('HttpInstrumentation', () => { }); }); + it('should respect HTTP_ROUTE', async () => { + await httpRequest.get( + `${protocol}://${hostname}:${serverPort}/setroute` + ); + const span = memoryExporter.getFinishedSpans()[0]; + + assert.strictEqual(span.kind, SpanKind.SERVER); + assert.strictEqual( + span.attributes[SemanticAttributes.HTTP_ROUTE], + 'TheRoute' + ); + assert.strictEqual(span.name, 'GET TheRoute'); + }); + const httpErrorCodes = [ 400, 401, 403, 404, 429, 501, 503, 504, 500, 505, 597, ]; @@ -404,7 +425,7 @@ describe('HttpInstrumentation', () => { assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0); assert.strictEqual(spans.length, 2); - assert.strictEqual(reqSpan.name, 'HTTP GET'); + assert.strictEqual(reqSpan.name, 'GET'); assert.strictEqual( localSpan.spanContext().traceId, reqSpan.spanContext().traceId @@ -449,7 +470,7 @@ describe('HttpInstrumentation', () => { assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0); assert.strictEqual(spans.length, 2); - assert.strictEqual(reqSpan.name, 'HTTP GET'); + assert.strictEqual(reqSpan.name, 'GET'); assert.strictEqual( localSpan.spanContext().traceId, reqSpan.spanContext().traceId @@ -474,7 +495,7 @@ describe('HttpInstrumentation', () => { for (let i = 0; i < num; i++) { await httpRequest.get(`${protocol}://${hostname}${testPath}`); const spans = memoryExporter.getFinishedSpans(); - assert.strictEqual(spans[i].name, 'HTTP GET'); + assert.strictEqual(spans[i].name, 'GET'); assert.strictEqual( span.spanContext().traceId, spans[i].spanContext().traceId diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-package.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-package.test.ts index 51b15dd6f3d..39b288ccdbb 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-package.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-package.test.ts @@ -119,7 +119,7 @@ describe('Packages', () => { }; assert.strictEqual(spans.length, 1); - assert.strictEqual(span.name, 'HTTP GET'); + assert.strictEqual(span.name, 'GET'); switch (name) { case 'axios': diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts index 2f3af2aaeb3..63a0ec00f5d 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts @@ -356,7 +356,7 @@ describe('HttpsInstrumentation', () => { assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0); assert.strictEqual(spans.length, 2); - assert.strictEqual(reqSpan.name, 'HTTPS GET'); + assert.strictEqual(reqSpan.name, 'GET'); assert.strictEqual( localSpan.spanContext().traceId, reqSpan.spanContext().traceId @@ -401,7 +401,7 @@ describe('HttpsInstrumentation', () => { assert.ok(localSpan.name.indexOf('TestRootSpan') >= 0); assert.strictEqual(spans.length, 2); - assert.strictEqual(reqSpan.name, 'HTTPS GET'); + assert.strictEqual(reqSpan.name, 'GET'); assert.strictEqual( localSpan.spanContext().traceId, reqSpan.spanContext().traceId @@ -426,7 +426,7 @@ describe('HttpsInstrumentation', () => { for (let i = 0; i < num; i++) { await httpsRequest.get(`${protocol}://${hostname}${testPath}`); const spans = memoryExporter.getFinishedSpans(); - assert.strictEqual(spans[i].name, 'HTTPS GET'); + assert.strictEqual(spans[i].name, 'GET'); assert.strictEqual( span.spanContext().traceId, spans[i].spanContext().traceId diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-package.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-package.test.ts index 28e9fb98042..dada803e451 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-package.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-package.test.ts @@ -125,7 +125,7 @@ describe('Packages', () => { }; assert.strictEqual(spans.length, 1); - assert.strictEqual(span.name, 'HTTPS GET'); + assert.strictEqual(span.name, 'GET'); switch (name) { case 'axios': diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/integrations/http-enable.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/integrations/http-enable.test.ts index b6a8d18d878..625b8b65499 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/integrations/http-enable.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/integrations/http-enable.test.ts @@ -176,7 +176,7 @@ describe('HttpInstrumentation Integration tests', () => { }; assert.strictEqual(spans.length, 2); - assert.strictEqual(span.name, 'HTTP GET'); + assert.strictEqual(span.name, 'GET'); assertSpan(span, SpanKind.CLIENT, validations); }); @@ -203,7 +203,7 @@ describe('HttpInstrumentation Integration tests', () => { }; assert.strictEqual(spans.length, 2); - assert.strictEqual(span.name, 'HTTP GET'); + assert.strictEqual(span.name, 'GET'); assertSpan(span, SpanKind.CLIENT, validations); }); @@ -233,7 +233,7 @@ describe('HttpInstrumentation Integration tests', () => { }; assert.strictEqual(spans.length, 2); - assert.strictEqual(span.name, 'HTTP GET'); + assert.strictEqual(span.name, 'GET'); assert.strictEqual(result.reqHeaders['x-foo'], 'foo'); assert.strictEqual( span.attributes[SemanticAttributes.HTTP_FLAVOR], @@ -264,7 +264,7 @@ describe('HttpInstrumentation Integration tests', () => { }; assert.strictEqual(spans.length, 2); - assert.strictEqual(span.name, 'HTTP GET'); + assert.strictEqual(span.name, 'GET'); assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT); assertSpan(span, SpanKind.CLIENT, validations); }); @@ -292,7 +292,7 @@ describe('HttpInstrumentation Integration tests', () => { }; assert.strictEqual(spans.length, 2); - assert.strictEqual(span.name, 'HTTP GET'); + assert.strictEqual(span.name, 'GET'); assertSpan(span, SpanKind.CLIENT, validations); }); for (const headers of [ @@ -347,7 +347,7 @@ describe('HttpInstrumentation Integration tests', () => { const span = spans.find(s => s.kind === SpanKind.CLIENT); assert.ok(span); assert.strictEqual(spans.length, 2); - assert.strictEqual(span.name, 'HTTP GET'); + assert.strictEqual(span.name, 'GET'); assert.ok(data); assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]); assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]); @@ -362,7 +362,7 @@ describe('HttpInstrumentation Integration tests', () => { const span = spans.find((s: any) => s.kind === SpanKind.SERVER); assert.ok(span); assert.strictEqual(spans.length, 2); - assert.strictEqual(span.name, 'HTTP GET'); + assert.strictEqual(span.name, 'GET'); }); it('should have correct spans even when request timeout', async () => { @@ -381,7 +381,7 @@ describe('HttpInstrumentation Integration tests', () => { spans = memoryExporter.getFinishedSpans(); const span = spans.find(s => s.kind === SpanKind.CLIENT); assert.ok(span); - assert.strictEqual(span.name, 'HTTP GET'); + assert.strictEqual(span.name, 'GET'); assert.strictEqual( span.attributes[SemanticAttributes.HTTP_HOST], `localhost:${mockServerPort}` diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/integrations/https-enable.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/integrations/https-enable.test.ts index 08d636b1646..10afe76a803 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/integrations/https-enable.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/integrations/https-enable.test.ts @@ -180,7 +180,7 @@ describe('HttpsInstrumentation Integration tests', () => { }; assert.strictEqual(spans.length, 2); - assert.strictEqual(span.name, 'HTTPS GET'); + assert.strictEqual(span.name, 'GET'); assertSpan(span, SpanKind.CLIENT, validations); }); @@ -207,7 +207,7 @@ describe('HttpsInstrumentation Integration tests', () => { }; assert.strictEqual(spans.length, 2); - assert.strictEqual(span.name, 'HTTPS GET'); + assert.strictEqual(span.name, 'GET'); assertSpan(span, SpanKind.CLIENT, validations); }); @@ -237,7 +237,7 @@ describe('HttpsInstrumentation Integration tests', () => { }; assert.strictEqual(spans.length, 2); - assert.strictEqual(span.name, 'HTTPS GET'); + assert.strictEqual(span.name, 'GET'); assert.strictEqual(result.reqHeaders['x-foo'], 'foo'); assert.strictEqual( span.attributes[SemanticAttributes.HTTP_FLAVOR], @@ -268,7 +268,7 @@ describe('HttpsInstrumentation Integration tests', () => { }; assert.strictEqual(spans.length, 2); - assert.strictEqual(span.name, 'HTTPS GET'); + assert.strictEqual(span.name, 'GET'); assert.strictEqual(span.attributes['span kind'], SpanKind.CLIENT); assertSpan(span, SpanKind.CLIENT, validations); }); @@ -296,7 +296,7 @@ describe('HttpsInstrumentation Integration tests', () => { }; assert.strictEqual(spans.length, 2); - assert.strictEqual(span.name, 'HTTPS GET'); + assert.strictEqual(span.name, 'GET'); assertSpan(span, SpanKind.CLIENT, validations); }); for (const headers of [ @@ -351,7 +351,7 @@ describe('HttpsInstrumentation Integration tests', () => { const span = spans.find(s => s.kind === SpanKind.CLIENT); assert.ok(span); assert.strictEqual(spans.length, 2); - assert.strictEqual(span.name, 'HTTPS GET'); + assert.strictEqual(span.name, 'GET'); assert.ok(data); assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]); assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]); diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/utils/assertSpan.ts b/experimental/packages/opentelemetry-instrumentation-http/test/utils/assertSpan.ts index 2576b599d62..1f1b0518c8c 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/utils/assertSpan.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/utils/assertSpan.ts @@ -49,10 +49,7 @@ export const assertSpan = ( assert.strictEqual(span.spanContext().traceId.length, 32); assert.strictEqual(span.spanContext().spanId.length, 16); assert.strictEqual(span.kind, kind); - assert.strictEqual( - span.name, - `${validations.component.toUpperCase()} ${validations.httpMethod}` - ); + assert.strictEqual(span.name, validations.httpMethod); assert.strictEqual( span.attributes[AttributeNames.HTTP_ERROR_MESSAGE], span.status.message diff --git a/experimental/packages/opentelemetry-sdk-node/README.md b/experimental/packages/opentelemetry-sdk-node/README.md index 0bac781143d..693f91ae742 100644 --- a/experimental/packages/opentelemetry-sdk-node/README.md +++ b/experimental/packages/opentelemetry-sdk-node/README.md @@ -143,7 +143,21 @@ Configure the [service name](/~https://github.com/open-telemetry/opentelemetry-spe Disable the SDK by setting the `OTEL_SDK_DISABLED` environment variable to `true`. -## Configure Trace Exporter from Environment +## Configure log level from the environment + +Set the log level by setting the `OTEL_LOG_LEVEL` environment variable to enums: + +- `NONE`, +- `ERROR`, +- `WARN`, +- `INFO`, +- `DEBUG`, +- `VERBOSE`, +- `ALL`. + +The default level is `INFO`. + +## Configure Trace Exporter from environment This is an alternative to programmatically configuring an exporter or span processor. This package will auto setup the default `otlp` exporter with `http/protobuf` protocol if `traceExporter` or `spanProcessor` hasn't been passed into the `NodeSDK` constructor. diff --git a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts index fa14043cd38..faae12dc7d5 100644 --- a/experimental/packages/opentelemetry-sdk-node/src/sdk.ts +++ b/experimental/packages/opentelemetry-sdk-node/src/sdk.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -import { ContextManager, TextMapPropagator, metrics } from '@opentelemetry/api'; +import { + ContextManager, + TextMapPropagator, + metrics, + diag, + DiagConsoleLogger, +} from '@opentelemetry/api'; import { InstrumentationOption, registerInstrumentations, @@ -80,11 +86,17 @@ export class NodeSDK { * Create a new NodeJS SDK instance */ public constructor(configuration: Partial = {}) { - if (getEnv().OTEL_SDK_DISABLED) { + const env = getEnv(); + if (env.OTEL_SDK_DISABLED) { this._disabled = true; // Functions with possible side-effects are set // to no-op via the _disabled flag } + if (env.OTEL_LOG_LEVEL) { + diag.setLogger(new DiagConsoleLogger(), { + logLevel: env.OTEL_LOG_LEVEL, + }); + } this._resource = configuration.resource ?? new Resource({}); this._resourceDetectors = configuration.resourceDetectors ?? [ diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts index 60b493435bd..fb439017105 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts @@ -22,6 +22,7 @@ import { diag, DiagLogLevel, metrics, + DiagConsoleLogger, } from '@opentelemetry/api'; import { AsyncHooksContextManager, @@ -68,6 +69,7 @@ describe('Node SDK', () => { let delegate: any; beforeEach(() => { + diag.disable(); context.disable(); trace.disable(); propagation.disable(); @@ -78,6 +80,10 @@ describe('Node SDK', () => { delegate = (trace.getTracerProvider() as ProxyTracerProvider).getDelegate(); }); + afterEach(() => { + Sinon.restore(); + }); + describe('Basic Registration', () => { it('should not register any unconfigured SDK components', async () => { // need to set OTEL_TRACES_EXPORTER to none since default value is otlp @@ -108,6 +114,25 @@ describe('Node SDK', () => { delete env.OTEL_TRACES_EXPORTER; }); + it('should register a diag logger with OTEL_LOG_LEVEL', () => { + env.OTEL_LOG_LEVEL = 'ERROR'; + + const spy = Sinon.spy(diag, 'setLogger'); + const sdk = new NodeSDK({ + autoDetectResources: false, + }); + + sdk.start(); + + assert.strictEqual(spy.callCount, 1); + assert.ok(spy.args[0][0] instanceof DiagConsoleLogger); + assert.deepStrictEqual(spy.args[0][1], { + logLevel: DiagLogLevel.ERROR, + }); + + delete env.OTEL_LOG_LEVEL; + }); + it('should register a tracer provider if an exporter is provided', async () => { const sdk = new NodeSDK({ traceExporter: new ConsoleSpanExporter(), diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts index a271a3bf5ff..fade4afa88b 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/util.ts @@ -15,6 +15,14 @@ */ import { diag } from '@opentelemetry/api'; import { OTLPExporterError } from '../../types'; +import { + DEFAULT_EXPORT_MAX_ATTEMPTS, + DEFAULT_EXPORT_INITIAL_BACKOFF, + DEFAULT_EXPORT_BACKOFF_MULTIPLIER, + DEFAULT_EXPORT_MAX_BACKOFF, + isExportRetryable, + parseRetryAfterToMills, +} from '../../util'; /** * Send metrics/spans using browser navigator.sendBeacon @@ -57,47 +65,99 @@ export function sendWithXhr( onSuccess: () => void, onError: (error: OTLPExporterError) => void ): void { - let reqIsDestroyed: boolean; + let retryTimer: ReturnType; + let xhr: XMLHttpRequest; + let reqIsDestroyed = false; const exporterTimer = setTimeout(() => { + clearTimeout(retryTimer); reqIsDestroyed = true; - xhr.abort(); + + if (xhr.readyState === XMLHttpRequest.DONE) { + const err = new OTLPExporterError('Request Timeout'); + onError(err); + } else { + xhr.abort(); + } }, exporterTimeout); - const xhr = new XMLHttpRequest(); - xhr.open('POST', url); + const sendWithRetry = ( + retries = DEFAULT_EXPORT_MAX_ATTEMPTS, + minDelay = DEFAULT_EXPORT_INITIAL_BACKOFF + ) => { + xhr = new XMLHttpRequest(); + xhr.open('POST', url); - const defaultHeaders = { - Accept: 'application/json', - 'Content-Type': 'application/json', - }; + const defaultHeaders = { + Accept: 'application/json', + 'Content-Type': 'application/json', + }; - Object.entries({ - ...defaultHeaders, - ...headers, - }).forEach(([k, v]) => { - xhr.setRequestHeader(k, v); - }); + Object.entries({ + ...defaultHeaders, + ...headers, + }).forEach(([k, v]) => { + xhr.setRequestHeader(k, v); + }); - xhr.send(body); + xhr.send(body); - xhr.onreadystatechange = () => { - if (xhr.readyState === XMLHttpRequest.DONE) { - if (xhr.status >= 200 && xhr.status <= 299) { - clearTimeout(exporterTimer); - diag.debug('xhr success', body); - onSuccess(); - } else if (reqIsDestroyed) { - const error = new OTLPExporterError('Request Timeout', xhr.status); - onError(error); - } else { - const error = new OTLPExporterError( - `Failed to export with XHR (status: ${xhr.status})`, - xhr.status - ); - clearTimeout(exporterTimer); - onError(error); + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE && reqIsDestroyed === false) { + if (xhr.status >= 200 && xhr.status <= 299) { + diag.debug('xhr success', body); + onSuccess(); + clearTimeout(exporterTimer); + clearTimeout(retryTimer); + } else if (xhr.status && isExportRetryable(xhr.status) && retries > 0) { + let retryTime: number; + minDelay = DEFAULT_EXPORT_BACKOFF_MULTIPLIER * minDelay; + + // retry after interval specified in Retry-After header + if (xhr.getResponseHeader('Retry-After')) { + retryTime = parseRetryAfterToMills( + xhr.getResponseHeader('Retry-After')! + ); + } else { + // exponential backoff with jitter + retryTime = Math.round( + Math.random() * (DEFAULT_EXPORT_MAX_BACKOFF - minDelay) + minDelay + ); + } + + retryTimer = setTimeout(() => { + sendWithRetry(retries - 1, minDelay); + }, retryTime); + } else { + const error = new OTLPExporterError( + `Failed to export with XHR (status: ${xhr.status})`, + xhr.status + ); + onError(error); + clearTimeout(exporterTimer); + clearTimeout(retryTimer); + } } - } + }; + + xhr.onabort = () => { + if (reqIsDestroyed) { + const err = new OTLPExporterError('Request Timeout'); + onError(err); + } + clearTimeout(exporterTimer); + clearTimeout(retryTimer); + }; + + xhr.onerror = () => { + if (reqIsDestroyed) { + const err = new OTLPExporterError('Request Timeout'); + onError(err); + } + clearTimeout(exporterTimer); + clearTimeout(retryTimer); + }; }; + + sendWithRetry(); } diff --git a/experimental/packages/otlp-exporter-base/src/platform/node/util.ts b/experimental/packages/otlp-exporter-base/src/platform/node/util.ts index d5636d14e65..fd40981e857 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/node/util.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/node/util.ts @@ -24,6 +24,14 @@ import { diag } from '@opentelemetry/api'; import { CompressionAlgorithm } from './types'; import { getEnv } from '@opentelemetry/core'; import { OTLPExporterError } from '../../types'; +import { + DEFAULT_EXPORT_MAX_ATTEMPTS, + DEFAULT_EXPORT_INITIAL_BACKOFF, + DEFAULT_EXPORT_BACKOFF_MULTIPLIER, + DEFAULT_EXPORT_MAX_BACKOFF, + isExportRetryable, + parseRetryAfterToMills, +} from '../../util'; /** * Sends data using http @@ -42,16 +50,21 @@ export function sendWithHttp( ): void { const exporterTimeout = collector.timeoutMillis; const parsedUrl = new url.URL(collector.url); - let reqIsDestroyed: boolean; const nodeVersion = Number(process.versions.node.split('.')[0]); + let retryTimer: ReturnType; + let req: http.ClientRequest; + let reqIsDestroyed = false; const exporterTimer = setTimeout(() => { + clearTimeout(retryTimer); reqIsDestroyed = true; - // req.abort() was deprecated since v14 - if (nodeVersion >= 14) { - req.destroy(); + + if (req.destroyed) { + const err = new OTLPExporterError('Request Timeout'); + onError(err); } else { - req.abort(); + // req.abort() was deprecated since v14 + nodeVersion >= 14 ? req.destroy() : req.abort(); } }, exporterTimeout); @@ -69,61 +82,104 @@ export function sendWithHttp( const request = parsedUrl.protocol === 'http:' ? http.request : https.request; - const req = request(options, (res: http.IncomingMessage) => { - let responseData = ''; - res.on('data', chunk => (responseData += chunk)); + const sendWithRetry = ( + retries = DEFAULT_EXPORT_MAX_ATTEMPTS, + minDelay = DEFAULT_EXPORT_INITIAL_BACKOFF + ) => { + req = request(options, (res: http.IncomingMessage) => { + let responseData = ''; + res.on('data', chunk => (responseData += chunk)); + + res.on('aborted', () => { + if (reqIsDestroyed) { + const err = new OTLPExporterError('Request Timeout'); + onError(err); + } + }); + + res.on('end', () => { + if (reqIsDestroyed === false) { + if (res.statusCode && res.statusCode < 299) { + diag.debug(`statusCode: ${res.statusCode}`, responseData); + onSuccess(); + // clear all timers since request was completed and promise was resolved + clearTimeout(exporterTimer); + clearTimeout(retryTimer); + } else if ( + res.statusCode && + isExportRetryable(res.statusCode) && + retries > 0 + ) { + let retryTime: number; + minDelay = DEFAULT_EXPORT_BACKOFF_MULTIPLIER * minDelay; + + // retry after interval specified in Retry-After header + if (res.headers['retry-after']) { + retryTime = parseRetryAfterToMills(res.headers['retry-after']!); + } else { + // exponential backoff with jitter + retryTime = Math.round( + Math.random() * (DEFAULT_EXPORT_MAX_BACKOFF - minDelay) + + minDelay + ); + } + + retryTimer = setTimeout(() => { + sendWithRetry(retries - 1, minDelay); + }, retryTime); + } else { + const error = new OTLPExporterError( + res.statusMessage, + res.statusCode, + responseData + ); + onError(error); + // clear all timers since request was completed and promise was resolved + clearTimeout(exporterTimer); + clearTimeout(retryTimer); + } + } + }); + }); - res.on('aborted', () => { + req.on('error', (error: Error | any) => { if (reqIsDestroyed) { - const err = new OTLPExporterError('Request Timeout'); + const err = new OTLPExporterError('Request Timeout', error.code); onError(err); + } else { + onError(error); } + clearTimeout(exporterTimer); + clearTimeout(retryTimer); }); - res.on('end', () => { - if (!reqIsDestroyed) { - if (res.statusCode && res.statusCode < 299) { - diag.debug(`statusCode: ${res.statusCode}`, responseData); - onSuccess(); - } else { - const error = new OTLPExporterError( - res.statusMessage, - res.statusCode, - responseData - ); - onError(error); - } - clearTimeout(exporterTimer); + req.on('abort', () => { + if (reqIsDestroyed) { + const err = new OTLPExporterError('Request Timeout'); + onError(err); } + clearTimeout(exporterTimer); + clearTimeout(retryTimer); }); - }); - req.on('error', (error: Error | any) => { - if (reqIsDestroyed) { - const err = new OTLPExporterError('Request Timeout', error.code); - onError(err); - } else { - clearTimeout(exporterTimer); - onError(error); - } - }); - - switch (collector.compression) { - case CompressionAlgorithm.GZIP: { - req.setHeader('Content-Encoding', 'gzip'); - const dataStream = readableFromBuffer(data); - dataStream - .on('error', onError) - .pipe(zlib.createGzip()) - .on('error', onError) - .pipe(req); - - break; + switch (collector.compression) { + case CompressionAlgorithm.GZIP: { + req.setHeader('Content-Encoding', 'gzip'); + const dataStream = readableFromBuffer(data); + dataStream + .on('error', onError) + .pipe(zlib.createGzip()) + .on('error', onError) + .pipe(req); + + break; + } + default: + req.end(data); + break; } - default: - req.end(data); - break; - } + }; + sendWithRetry(); } function readableFromBuffer(buff: string | Buffer): Readable { diff --git a/experimental/packages/otlp-exporter-base/src/util.ts b/experimental/packages/otlp-exporter-base/src/util.ts index 99a9f6e3338..f5dc70c9e88 100644 --- a/experimental/packages/otlp-exporter-base/src/util.ts +++ b/experimental/packages/otlp-exporter-base/src/util.ts @@ -18,6 +18,10 @@ import { diag } from '@opentelemetry/api'; import { getEnv } from '@opentelemetry/core'; const DEFAULT_TRACE_TIMEOUT = 10000; +export const DEFAULT_EXPORT_MAX_ATTEMPTS = 5; +export const DEFAULT_EXPORT_INITIAL_BACKOFF = 1000; +export const DEFAULT_EXPORT_MAX_BACKOFF = 5000; +export const DEFAULT_EXPORT_BACKOFF_MULTIPLIER = 1.5; /** * Parses headers from config leaving only those that have defined values @@ -110,3 +114,26 @@ export function invalidTimeout( return defaultTimeout; } + +export function isExportRetryable(statusCode: number): boolean { + const retryCodes = [429, 502, 503, 504]; + + return retryCodes.includes(statusCode); +} + +export function parseRetryAfterToMills(retryAfter?: string | null): number { + if (retryAfter == null) { + return -1; + } + const seconds = Number.parseInt(retryAfter, 10); + if (Number.isInteger(seconds)) { + return seconds > 0 ? seconds * 1000 : -1; + } + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#directives + const delay = new Date(retryAfter).getTime() - Date.now(); + + if (delay >= 0) { + return delay; + } + return 0; +} diff --git a/experimental/packages/otlp-exporter-base/test/common/util.test.ts b/experimental/packages/otlp-exporter-base/test/common/util.test.ts index d78b719faa9..b00d1f36a5c 100644 --- a/experimental/packages/otlp-exporter-base/test/common/util.test.ts +++ b/experimental/packages/otlp-exporter-base/test/common/util.test.ts @@ -21,6 +21,7 @@ import { parseHeaders, appendResourcePathToUrl, appendRootPathToUrlIfNeeded, + parseRetryAfterToMills, } from '../../src/util'; describe('utils', () => { @@ -117,3 +118,30 @@ describe('utils', () => { }); }); }); + +describe('parseRetryAfterToMills', () => { + // now: 2023-01-20T00:00:00.000Z + const tests = [ + [null, -1], + // duration + ['-100', -1], + ['1000', 1000 * 1000], + // future timestamp + ['Fri, 20 Jan 2023 00:00:01 GMT', 1000], + // Past timestamp + ['Fri, 19 Jan 2023 23:59:59 GMT', 0], + ] as [string | null, number][]; + + afterEach(() => { + sinon.restore(); + }); + + for (const [value, expect] of tests) { + it(`test ${value}`, () => { + sinon.useFakeTimers({ + now: new Date('2023-01-20T00:00:00.000Z'), + }); + assert.strictEqual(parseRetryAfterToMills(value), expect); + }); + } +}); diff --git a/experimental/packages/otlp-exporter-base/test/node/util.test.ts b/experimental/packages/otlp-exporter-base/test/node/util.test.ts index 7d5b03d3eb2..86c8df40ab0 100644 --- a/experimental/packages/otlp-exporter-base/test/node/util.test.ts +++ b/experimental/packages/otlp-exporter-base/test/node/util.test.ts @@ -225,6 +225,9 @@ describe('sendWithHttp', () => { assert.strictEqual(requestData, data); }); + // use fake timers to replace setTimeout in sendWithHttp function + const clock = sinon.useFakeTimers(); + sendWithHttp( exporter, data, @@ -237,6 +240,8 @@ describe('sendWithHttp', () => { assert.fail(err); } ); + + clock.restore(); }); it('should send with gzip compression if configured to do so', () => { @@ -255,6 +260,9 @@ describe('sendWithHttp', () => { assert(Buffer.concat(buffers).equals(compressedData)); }); + // use fake timers to replace setTimeout in sendWithHttp function + const clock = sinon.useFakeTimers(); + sendWithHttp( exporter, data, @@ -267,6 +275,8 @@ describe('sendWithHttp', () => { assert.fail(err); } ); + + clock.restore(); }); it('should work with gzip compression enabled even after multiple requests', () => { @@ -297,6 +307,9 @@ describe('sendWithHttp', () => { assert(Buffer.concat(buffers).equals(compressedData)); }); + // use fake timers to replace setTimeout in sendWithHttp function + const clock = sinon.useFakeTimers(); + sendWithHttp( exporter, data, @@ -309,6 +322,8 @@ describe('sendWithHttp', () => { assert.fail(err); } ); + + clock.restore(); } }); }); diff --git a/packages/opentelemetry-core/src/utils/environment.ts b/packages/opentelemetry-core/src/utils/environment.ts index 01da17f293a..5d529d08a94 100644 --- a/packages/opentelemetry-core/src/utils/environment.ts +++ b/packages/opentelemetry-core/src/utils/environment.ts @@ -333,7 +333,7 @@ export function parseEnvironment(values: RAW_ENVIRONMENT): ENVIRONMENT { * populating default values. */ export function getEnvWithoutDefaults(): ENVIRONMENT { - return typeof process !== 'undefined' + return typeof process !== 'undefined' && process && process.env ? parseEnvironment(process.env as RAW_ENVIRONMENT) : parseEnvironment(_globalThis as typeof globalThis & RAW_ENVIRONMENT); } diff --git a/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts b/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts index 2f14b77c741..d760ff5809a 100644 --- a/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts +++ b/packages/opentelemetry-sdk-trace-base/src/export/BatchSpanProcessorBase.ts @@ -44,6 +44,7 @@ export abstract class BatchSpanProcessorBase private _finishedSpans: ReadableSpan[] = []; private _timer: NodeJS.Timeout | undefined; private _shutdownOnce: BindOnceFuture; + private _droppedSpansCount: number = 0; constructor(private readonly _exporter: SpanExporter, config?: T) { const env = getEnv(); @@ -117,8 +118,23 @@ export abstract class BatchSpanProcessorBase private _addToBuffer(span: ReadableSpan) { if (this._finishedSpans.length >= this._maxQueueSize) { // limit reached, drop span + + if (this._droppedSpansCount === 0) { + diag.debug('maxQueueSize reached, dropping spans'); + } + this._droppedSpansCount++; + return; } + + if (this._droppedSpansCount > 0) { + // some spans were dropped, log once with count of spans dropped + diag.warn( + `Dropped ${this._droppedSpansCount} spans because maxQueueSize reached` + ); + this._droppedSpansCount = 0; + } + this._finishedSpans.push(span); this._maybeStartTimer(); } diff --git a/packages/opentelemetry-sdk-trace-base/test/browser/export/BatchSpanProcessor.test.ts b/packages/opentelemetry-sdk-trace-base/test/browser/export/BatchSpanProcessor.test.ts index c4575fde2c2..f510d518ff2 100644 --- a/packages/opentelemetry-sdk-trace-base/test/browser/export/BatchSpanProcessor.test.ts +++ b/packages/opentelemetry-sdk-trace-base/test/browser/export/BatchSpanProcessor.test.ts @@ -20,11 +20,17 @@ import { SpanExporter } from '../../../src'; import { BatchSpanProcessor } from '../../../src/platform/browser/export/BatchSpanProcessor'; import { TestTracingSpanExporter } from '../../common/export/TestTracingSpanExporter'; +/** + * VisibilityState has been removed from TypeScript 4.6.0+ + * So just defining a simple replacement + */ +type WebVisibilityState = 'visible' | 'hidden'; + const describeDocument = typeof document === 'object' ? describe : describe.skip; describeDocument('BatchSpanProcessor - web main context', () => { - let visibilityState: VisibilityState = 'visible'; + let visibilityState: WebVisibilityState = 'visible'; let exporter: SpanExporter; let processor: BatchSpanProcessor; let forceFlushSpy: sinon.SinonStub; diff --git a/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts b/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts index fd9574537bc..069287fc599 100644 --- a/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts +++ b/packages/opentelemetry-sdk-trace-base/test/common/export/BatchSpanProcessorBase.test.ts @@ -462,6 +462,40 @@ describe('BatchSpanProcessorBase', () => { } assert.equal(processor['_finishedSpans'].length, 6); }); + it('should count and report dropped spans', done => { + const debugStub = sinon.spy(diag, 'debug'); + const warnStub = sinon.spy(diag, 'warn'); + const span = createSampledSpan('test'); + for (let i = 0, j = 6; i < j; i++) { + processor.onStart(span, ROOT_CONTEXT); + processor.onEnd(span); + } + assert.equal(processor['_finishedSpans'].length, 6); + assert.equal(processor['_droppedSpansCount'], 0); + sinon.assert.notCalled(debugStub); + + processor.onStart(span, ROOT_CONTEXT); + processor.onEnd(span); + + assert.equal(processor['_finishedSpans'].length, 6); + assert.equal(processor['_droppedSpansCount'], 1); + sinon.assert.calledOnce(debugStub); + + processor.forceFlush().then(() => { + processor.onStart(span, ROOT_CONTEXT); + processor.onEnd(span); + + assert.equal(processor['_finishedSpans'].length, 1); + assert.equal(processor['_droppedSpansCount'], 0); + + sinon.assert.calledOnceWithExactly( + warnStub, + 'Dropped 1 spans because maxQueueSize reached' + ); + + done(); + }); + }); }); }); diff --git a/packages/opentelemetry-sdk-trace-web/test/NodeGlobalsFoolProofing.test.ts b/packages/opentelemetry-sdk-trace-web/test/NodeGlobalsFoolProofing.test.ts new file mode 100644 index 00000000000..19090be5656 --- /dev/null +++ b/packages/opentelemetry-sdk-trace-web/test/NodeGlobalsFoolProofing.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + context, + propagation, + trace, + ProxyTracerProvider, +} from '@opentelemetry/api'; +import { Resource } from '@opentelemetry/resources'; +import { Tracer } from '@opentelemetry/sdk-trace-base'; +import * as assert from 'assert'; +import { StackContextManager, WebTracerProvider } from '../src'; + +describe('Node Globals Foolproofing', () => { + const originalProcess = globalThis?.process; + before(() => { + Object.assign(globalThis, { process: false }); + }); + + after(() => { + Object.assign(globalThis, { process: originalProcess }); + }); + + beforeEach(() => { + context.disable(); + trace.disable(); + propagation.disable(); + }); + + it('Can get TraceProvider without node globals such as process', () => { + const propagator = propagation['_getGlobalPropagator'](); + const tracerProvider = new WebTracerProvider(); + tracerProvider.register({ + propagator: null, + }); + + assert.strictEqual( + propagation['_getGlobalPropagator'](), + propagator, + 'propagator should not change' + ); + + assert.ok(context['_getContextManager']() instanceof StackContextManager); + const apiTracerProvider = trace.getTracerProvider() as ProxyTracerProvider; + assert.ok(apiTracerProvider.getDelegate() === tracerProvider); + }); + + it('Can get TraceProvider with custom id generator and without node globals such as process', () => { + const getRandomString = (length: number) => { + const alphanumericsList = 'abcdefghijklmnopqrstuvwxyz0123456789'.split( + '' + ); + alphanumericsList.sort(() => 0.5 - Math.random()); + + return alphanumericsList.slice(0, length).join(''); + }; + + const tracer = new WebTracerProvider({ + resource: new Resource({ + 'service.name': 'web-core', + }), + idGenerator: { + generateTraceId: () => getRandomString(32), + generateSpanId: () => getRandomString(16), + }, + }).getTracer('default'); + + assert.ok(tracer instanceof Tracer); + }); +}); diff --git a/selenium-tests/package.json b/selenium-tests/package.json index be4f1d42a1e..f18c0705642 100644 --- a/selenium-tests/package.json +++ b/selenium-tests/package.json @@ -40,7 +40,7 @@ "babel-loader": "8.2.3", "babel-polyfill": "6.26.0", "browserstack-local": "1.4.8", - "chromedriver": "107.0.3", + "chromedriver": "110.0.0", "dotenv": "16.0.0", "fast-safe-stringify": "2.1.1", "geckodriver": "3.0.1",