Skip to content

Commit

Permalink
feat(longtasks): allow callback to add span attributes on collection (#…
Browse files Browse the repository at this point in the history
…863)

Co-authored-by: Valentin Marchaud <contact@vmarchaud.fr>
  • Loading branch information
crellison and vmarchaud committed Feb 16, 2022
1 parent 935149c commit 1f68004
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 36 deletions.
25 changes: 24 additions & 1 deletion plugins/web/opentelemetry-instrumentation-long-task/README.md
Expand Up @@ -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: <https://opentelemetry.io/>
Expand Down
Expand Up @@ -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';

Expand All @@ -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);
}

Expand All @@ -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);
Expand Down
43 changes: 43 additions & 0 deletions 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;
}
Expand Up @@ -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<number>(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;
Expand Down Expand Up @@ -65,23 +85,7 @@ describe('LongTaskInstrumentation', () => {
});

it('should report long taking tasks', async () => {
let taskStart: number;
await new Promise<void>(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(
Expand All @@ -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'
);
});
});

0 comments on commit 1f68004

Please sign in to comment.