Skip to content

Commit

Permalink
feat(express): add requestHook support (open-telemetry#1091)
Browse files Browse the repository at this point in the history
  • Loading branch information
luismiramirez authored Aug 10, 2022
1 parent 115aad6 commit bcc048b
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 2 deletions.
31 changes: 31 additions & 0 deletions plugins/node/opentelemetry-instrumentation-express/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Express instrumentation has few options available to choose from. You can set th
| `ignoreLayers` | `IgnoreMatcher[]` | `[/^\/_internal\//]` | Ignore layers that by match. |
| `ignoreLayersType`| `ExpressLayerType[]` | `['request_handler']` | Ignore layers of specified type. |
| `spanNameHook` | `SpanNameHook` | `() => 'my-span-name'` | Can be used to customize span names by returning a new name from the hook. |
| `requestHook` | `ExpressRequestCustomAttributeFunction (function)` | `(span, info) => {}` | Function for adding custom attributes on Express request. Receives params: `Span, ExpressRequestInfo`. |

`ignoreLayers` accepts an array of elements of types:

Expand All @@ -78,6 +79,36 @@ Express instrumentation has few options available to choose from. You can set th
- `info: ExpressRequestInfo` containing the incoming Express.js request, the current route handler creating a span and `ExpressLayerType` - the type of the handling layer or undefined when renaming the root HTTP instrumentation span.
- `defaultName: string` - original name proposed by the instrumentation.

#### Using `requestHook`

Instrumentation configuration accepts a custom "hook" function which will be called for every instrumented Express layer involved in a request. Custom attributes can be set on the span or run any custom logic per layer.

Here is a simple example that adds to the request handler span some attributes based on the Express request attributes:

```javascript
import { ExpressInstrumentation, ExpressLayerType } from "@opentelemetry/instrumentation-express"

const expressInstrumentation = new ExpressInstrumentation({
requestHook: function (
span: Span,
info: ExpressRequestInfo,
) {

if (info.layerType === ExpressLayerType.REQUEST_HANDLER) {
span.setAttribute(
'http.method',
info.request.method
);

span.setAttribute(
'express.base_url',
info.request.baseUrl
);
}
}
});
```

## Useful links

- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@
"@opentelemetry/sdk-trace-node": "^1.3.1",
"@types/mocha": "7.0.2",
"@types/node": "16.11.21",
"@types/sinon": "10.0.9",
"express": "4.17.1",
"gts": "3.1.0",
"mocha": "7.2.0",
"nyc": "15.1.0",
"rimraf": "3.0.2",
"sinon": "14.0.0",
"test-all-versions": "5.0.1",
"ts-mocha": "10.0.0",
"typescript": "4.3.5"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
InstrumentationBase,
InstrumentationNodeModuleDefinition,
isWrapped,
safeExecuteInTheMiddle,
} from '@opentelemetry/instrumentation';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';

Expand Down Expand Up @@ -244,6 +245,24 @@ export class ExpressInstrumentation extends InstrumentationBase<
const span = instrumentation.tracer.startSpan(spanName, {
attributes: Object.assign(attributes, metadata.attributes),
});

if (instrumentation.getConfig().requestHook) {
safeExecuteInTheMiddle(
() =>
instrumentation.getConfig().requestHook!(span, {
request: req,
layerType: type,
route,
}),
e => {
if (e) {
diag.error('express instrumentation: request hook failed', e);
}
},
true
);
}

const startTime = hrTime();
let spanHasEnded = false;
// If we found anything that isnt a middleware, there no point of measuring
Expand Down
15 changes: 14 additions & 1 deletion plugins/node/opentelemetry-instrumentation-express/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { kLayerPatched } from './';
import { Request } from 'express';
import { SpanAttributes } from '@opentelemetry/api';
import { Span, SpanAttributes } from '@opentelemetry/api';
import { InstrumentationConfig } from '@opentelemetry/instrumentation';
import { ExpressLayerType } from './enums/ExpressLayerType';

Expand Down Expand Up @@ -88,6 +88,16 @@ export type SpanNameHook = (
defaultName: string
) => string;

/**
* Function that can be used to add custom attributes to the current span or the root span on
* a Express request
* @param span - The Express middleware layer span.
* @param info - An instance of ExpressRequestInfo that contains info about the request such as the route, and the layer type.
*/
export interface ExpressRequestCustomAttributeFunction {
(span: Span, info: ExpressRequestInfo): void;
}

/**
* Options available for the Express Instrumentation (see [documentation](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-Instrumentation-express#express-Instrumentation-options))
*/
Expand All @@ -97,4 +107,7 @@ export interface ExpressInstrumentationConfig extends InstrumentationConfig {
/** Ignore specific layers based on their type */
ignoreLayersType?: ExpressLayerType[];
spanNameHook?: SpanNameHook;

/** Function for adding custom attributes on Express request */
requestHook?: ExpressRequestCustomAttributeFunction;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ import {
} from '@opentelemetry/sdk-trace-base';
import * as assert from 'assert';
import type * as http from 'http';
import * as sinon from 'sinon';
import { ExpressInstrumentation } from '../src';
import { SpanNameHook } from '../src/types';
import { ExpressRequestInfo, SpanNameHook } from '../src/types';
import { ExpressLayerType } from '../src/enums/ExpressLayerType';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';

const instrumentation = new ExpressInstrumentation();
instrumentation.enable();
Expand Down Expand Up @@ -169,4 +172,97 @@ describe('ExpressInstrumentation hooks', () => {
);
});
});

describe('request hooks', () => {
let server: http.Server;
let port: number;
let rootSpan: Span;

beforeEach(async () => {
rootSpan = tracer.startSpan('rootSpan');

const httpServer = await serverWithMiddleware(tracer, rootSpan, app => {
app.get('*', (req, res) => {
res.send('ok');
});
});
server = httpServer.server;
port = httpServer.port;
});

afterEach(() => {
server.close();
});

it('should call requestHook when set in config', async () => {
const requestHook = sinon.spy((span: Span, info: ExpressRequestInfo) => {
span.setAttribute(SemanticAttributes.HTTP_METHOD, info.request.method);

if (info.layerType) {
span.setAttribute('express.layer_type', info.layerType);
}
});

instrumentation.setConfig({
requestHook,
});

await context.with(
trace.setSpan(context.active(), rootSpan),
async () => {
await httpRequest.get(`http://localhost:${port}/foo/3`);
rootSpan.end();

const spans = memoryExporter.getFinishedSpans();
const requestHandlerSpan = spans.find(
span => span.name === 'request handler - *'
);

assert.strictEqual(spans.length, 2);
sinon.assert.calledOnce(requestHook);
assert.strictEqual(
requestHandlerSpan?.attributes['http.method'],
'GET'
);
assert.strictEqual(
requestHandlerSpan?.attributes['express.layer_type'],
ExpressLayerType.REQUEST_HANDLER
);
}
);
});

it('should ignore requestHook which throws exception', async () => {
const requestHook = sinon.spy((span: Span, info: ExpressRequestInfo) => {
// This is added before the exception is thrown thus we can expect it
span.setAttribute('http.method', info.request.method);
throw Error('error thrown in requestHook');
});

instrumentation.setConfig({
requestHook,
});

await context.with(
trace.setSpan(context.active(), rootSpan),
async () => {
await httpRequest.get(`http://localhost:${port}/foo/3`);
rootSpan.end();

const spans = memoryExporter.getFinishedSpans();
const requestHandlerSpan = spans.find(
span => span.name === 'request handler - *'
);

assert.strictEqual(spans.length, 2);
assert.strictEqual(
requestHandlerSpan?.attributes['http.method'],
'GET'
);

sinon.assert.threw(requestHook);
}
);
});
});
});

0 comments on commit bcc048b

Please sign in to comment.