From 1f68004ef9b25b0d260159f4b1e2f279b1a64649 Mon Sep 17 00:00:00 2001 From: Cole Ellison Date: Wed, 16 Feb 2022 12:58:01 -0800 Subject: [PATCH] feat(longtasks): allow callback to add span attributes on collection (#863) Co-authored-by: Valentin Marchaud --- .../README.md | 25 +++++- .../src/instrumentation.ts | 33 ++++--- .../src/types.ts | 43 ++++++++++ .../test/longTask.test.ts | 85 +++++++++++++++---- 4 files changed, 150 insertions(+), 36 deletions(-) create mode 100644 plugins/web/opentelemetry-instrumentation-long-task/src/types.ts diff --git a/plugins/web/opentelemetry-instrumentation-long-task/README.md b/plugins/web/opentelemetry-instrumentation-long-task/README.md index 6a6659ee97..642bc8cfe3 100644 --- a/plugins/web/opentelemetry-instrumentation-long-task/README.md +++ b/plugins/web/opentelemetry-instrumentation-long-task/README.md @@ -31,11 +31,34 @@ provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); registerInstrumentations({ tracerProvider: provider, instrumentations: [ - new LongTaskInstrumentation(), + new LongTaskInstrumentation({ + // see under for available configuration + }), ], }); ``` +### longtask Instrumentation Options + +| Options | Type | Description | +| --- | --- | --- | +| `observerCallback` | `ObserverCallback` | Callback executed on observed `longtask`, allowing additional attributes to be attached to the span. | + +The `observerCallback` function is passed the created span and the `longtask` `PerformanceEntry`, +allowing the user to add custom attributes to the span with any logic. +For example, a webapp with client-side routing can add contextual information on the current page, +even if the tracer was instantiated before navigation. + +Usage Example: + +```js +longtaskInstrumentationConfig = { + observerCallback: (span, longtaskEvent) => { + span.setAttribute('location.pathname', window.location.pathname) + } +} +``` + ## Useful links - For more information on OpenTelemetry, visit: diff --git a/plugins/web/opentelemetry-instrumentation-long-task/src/instrumentation.ts b/plugins/web/opentelemetry-instrumentation-long-task/src/instrumentation.ts index 1bebc4d70c..d39122d745 100644 --- a/plugins/web/opentelemetry-instrumentation-long-task/src/instrumentation.ts +++ b/plugins/web/opentelemetry-instrumentation-long-task/src/instrumentation.ts @@ -13,25 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { hrTime } from '@opentelemetry/core'; -import { - InstrumentationBase, - InstrumentationConfig, -} from '@opentelemetry/instrumentation'; +import { diag } from '@opentelemetry/api'; +import { InstrumentationBase } from '@opentelemetry/instrumentation'; import { VERSION } from './version'; - -// Currently missing in typescript DOM definitions -interface PerformanceLongTaskTiming extends PerformanceEntry { - attribution: TaskAttributionTiming[]; -} - -interface TaskAttributionTiming extends PerformanceEntry { - containerType: string; - containerSrc: string; - containerId: string; - containerName: string; -} +import type { + PerformanceLongTaskTiming, + LongtaskInstrumentationConfig, +} from './types'; const LONGTASK_PERFORMANCE_TYPE = 'longtask'; @@ -41,12 +30,13 @@ export class LongTaskInstrumentation extends InstrumentationBase { moduleName = this.component; private _observer?: PerformanceObserver; + override _config!: LongtaskInstrumentationConfig; /** * * @param config */ - constructor(config: InstrumentationConfig = {}) { + constructor(config: LongtaskInstrumentationConfig = {}) { super('@opentelemetry/instrumentation-long-task', VERSION, config); } @@ -69,6 +59,13 @@ export class LongTaskInstrumentation extends InstrumentationBase { const span = this.tracer.startSpan(LONGTASK_PERFORMANCE_TYPE, { startTime: hrTime(entry.startTime), }); + if (this._config.observerCallback) { + try { + this._config.observerCallback(span, { longtaskEntry: entry }); + } catch (err) { + diag.error('longtask instrumentation: observer callback failed', err); + } + } span.setAttribute('component', this.component); span.setAttribute('longtask.name', entry.name); span.setAttribute('longtask.entry_type', entry.entryType); diff --git a/plugins/web/opentelemetry-instrumentation-long-task/src/types.ts b/plugins/web/opentelemetry-instrumentation-long-task/src/types.ts new file mode 100644 index 0000000000..141135873e --- /dev/null +++ b/plugins/web/opentelemetry-instrumentation-long-task/src/types.ts @@ -0,0 +1,43 @@ +/* + * 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 type { Span } from '@opentelemetry/api'; +import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +// Currently missing in typescript DOM definitions +export interface PerformanceLongTaskTiming extends PerformanceEntry { + attribution: TaskAttributionTiming[]; +} + +export interface TaskAttributionTiming extends PerformanceEntry { + containerType: string; + containerSrc: string; + containerId: string; + containerName: string; +} + +export interface ObserverCallbackInformation { + longtaskEntry: PerformanceLongTaskTiming; +} + +export type ObserverCallback = ( + span: Span, + information: ObserverCallbackInformation +) => void; + +export interface LongtaskInstrumentationConfig extends InstrumentationConfig { + /** Callback for adding custom attributes to span */ + observerCallback?: ObserverCallback; +} diff --git a/plugins/web/opentelemetry-instrumentation-long-task/test/longTask.test.ts b/plugins/web/opentelemetry-instrumentation-long-task/test/longTask.test.ts index fabb02e469..c65ed71e63 100644 --- a/plugins/web/opentelemetry-instrumentation-long-task/test/longTask.test.ts +++ b/plugins/web/opentelemetry-instrumentation-long-task/test/longTask.test.ts @@ -30,6 +30,26 @@ function generateLongTask() { while (performance.now() - startingTimestamp < LONGTASK_DURATION) {} } +async function waitForLongTask(exportSpy: sinon.SinonStub) { + let taskStart: number; + return await new Promise(resolve => { + // Resolve promise when export gets called + exportSpy.callsFake(() => { + if (!taskStart) { + // Hasn't generated expected longtask yet + return; + } + resolve(taskStart); + }); + setTimeout(() => { + // Cleanup any past longtasks + exportSpy.resetHistory(); + taskStart = performance.timeOrigin + performance.now(); + generateLongTask(); + }, 1); + }); +} + describe('LongTaskInstrumentation', () => { let longTaskInstrumentation: LongTaskInstrumentation; let sandbox: sinon.SinonSandbox; @@ -65,23 +85,7 @@ describe('LongTaskInstrumentation', () => { }); it('should report long taking tasks', async () => { - let taskStart: number; - await new Promise(resolve => { - // Resolve promise when export gets called - exportSpy.callsFake(() => { - if (!taskStart) { - // Hasn't generated expected longtask yet - return; - } - resolve(); - }); - setTimeout(() => { - // Cleanup any past longtasks - exportSpy.resetHistory(); - taskStart = performance.timeOrigin + performance.now(); - generateLongTask(); - }, 1); - }); + const taskStart = await waitForLongTask(exportSpy); assert.strictEqual(exportSpy.args.length, 1, 'should export once'); assert.strictEqual( @@ -101,4 +105,51 @@ describe('LongTaskInstrumentation', () => { "span duration should be longtask's" ); }); + + it('should attach additional attributes from callback', async () => { + deregister(); + const additionalAttributes = { foo: 'bar' }; + longTaskInstrumentation = new LongTaskInstrumentation({ + enabled: false, + observerCallback: span => { + span.setAttributes(additionalAttributes); + }, + }); + deregister = registerInstrumentations({ + instrumentations: [longTaskInstrumentation], + }); + + await waitForLongTask(exportSpy); + const span: ReadableSpan = exportSpy.args[0][0][0]; + assert.ok( + Object.entries(additionalAttributes).every(([key, value]) => { + return span.attributes[key] === value; + }), + 'span should have key/value pairs from additional attributes' + ); + }); + + it('should not fail to export span if observerCallback throws', async () => { + deregister(); + const errorCallback = sandbox.stub().throws(); + longTaskInstrumentation = new LongTaskInstrumentation({ + enabled: false, + observerCallback: errorCallback, + }); + deregister = registerInstrumentations({ + instrumentations: [longTaskInstrumentation], + }); + + await waitForLongTask(exportSpy); + assert.strictEqual( + errorCallback.threw(), + true, + 'expected callback to throw error' + ); + assert.strictEqual( + exportSpy.callCount, + 1, + 'expected export to be called once' + ); + }); });