Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0149bcf
feat: initial implementation of restify instrumentation
rauno56 Apr 5, 2021
96f73e6
feat: create spans instead of altering parent spans
rauno56 Apr 6, 2021
4f920f5
feat: clean examples up
rauno56 Apr 6, 2021
92af233
feat: make exporter configurable like other examples
rauno56 Apr 7, 2021
3c37d7f
feat: implement disabling restify instrumentation
rauno56 Apr 8, 2021
32b8931
test: be more verbose in tests and check all spans
rauno56 Apr 8, 2021
ee64e4d
fix: remove the handlers from req, those events seem not to be fired
rauno56 Apr 8, 2021
b6c8318
refactor: export the instrumentation as default
rauno56 Apr 8, 2021
a46adb7
fix: thrown errors are generally not caught in restify
rauno56 Apr 8, 2021
5d67867
feat: enable the instrumentation for all versions after 4.0.0
rauno56 Apr 8, 2021
c4657df
docs: update docs and metadata in package.json and README.md files
rauno56 Apr 8, 2021
cbbdea0
style: fix linting errors
rauno56 Apr 9, 2021
c760527
style: make _moduleVersion optional
rauno56 Apr 12, 2021
4ca8036
feat: use more descriptive span name
rauno56 Apr 13, 2021
e9ab22e
feat: rename HTTP span
rauno56 Apr 13, 2021
0dc22aa
feat: remove creating additional span on the client side
rauno56 Apr 13, 2021
6c380a2
docs: remove TODO
rauno56 Apr 13, 2021
3b8072b
feat: update version
rauno56 Apr 13, 2021
fa55dab
test: test more verbose API
rauno56 Apr 13, 2021
ac8d41b
fix: update version
rauno56 Apr 14, 2021
520d54e
fix: update the example in README
rauno56 Apr 14, 2021
b5be54c
fix: reference @opentelemetry/instrumentation-http correctly
rauno56 Apr 14, 2021
14772ba
feat: remove lodash.once
rauno56 Apr 15, 2021
c8914d1
style: rename constants to be more verbose
rauno56 Apr 15, 2021
88f6052
feat: handle cases with promises
rauno56 Apr 15, 2021
df87efd
feat: support async and promise-returning handlers
rauno56 Apr 15, 2021
4b58434
style: use RestifyInstrumentation as the import name
rauno56 Apr 15, 2021
c4dee5c
feat: let the example be broken until the publish for consistency
rauno56 Apr 15, 2021
f8bcb5c
fix: remove lodash.once types
rauno56 Apr 15, 2021
fd906b1
Merge branch 'main' into feat/restify-instrumentation
vmarchaud Apr 15, 2021
f4f4b08
Merge branch 'main' into feat/restify-instrumentation
rauno56 Apr 16, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
style: fix linting errors
  • Loading branch information
rauno56 committed Apr 15, 2021
commit cbbdea00938fc81de601b04cc94cc3ebbd3a0879
Original file line number Diff line number Diff line change
Expand Up @@ -39,103 +39,141 @@ export class RestifyInstrumentation extends InstrumentationBase<
typeof restify
> {
constructor() {
super(
`@opentelemetry/instrumentation-${MODULE_NAME}`,
VERSION
);
super(`@opentelemetry/instrumentation-${MODULE_NAME}`, VERSION);
}

private _moduleVersion: string | undefined;
private _isDisabled = false;

init() {
const module = new InstrumentationNodeModuleDefinition<typeof restify>(
MODULE_NAME,
SUPPORTED_VERSIONS,
(moduleExports, moduleVersion) => {
this._moduleVersion = moduleVersion;
return moduleExports;
}
);

module.files.push(new InstrumentationNodeModuleFile<typeof restify>(
'restify/lib/server.js',
MODULE_NAME,
SUPPORTED_VERSIONS,
(moduleExports, moduleVersion) => {
diag.debug(`Applying patch for ${MODULE_NAME}@${moduleVersion}`);
this._isDisabled = false;
const Server: any = moduleExports;
for (const name of RESTIFY_METHODS) {
if (isWrapped(Server.prototype[name])) {
this._unwrap(Server.prototype, name);
}
this._wrap(Server.prototype, name as keyof Server, this._methodPatcher.bind(this));
}
for (const name of RESTIFY_MW_METHODS) {
if (isWrapped(Server.prototype[name])) {
this._unwrap(Server.prototype, name);
}
this._wrap(Server.prototype, name as keyof Server, this._middlewarePatcher.bind(this));
}
this._moduleVersion = moduleVersion;
return moduleExports;
},
(moduleExports, moduleVersion) => {
diag.debug(`Removing patch for ${MODULE_NAME}@${moduleVersion}`);
this._isDisabled = true;
if (moduleExports) {
}
);

module.files.push(
new InstrumentationNodeModuleFile<typeof restify>(
'restify/lib/server.js',
SUPPORTED_VERSIONS,
(moduleExports, moduleVersion) => {
diag.debug(`Applying patch for ${MODULE_NAME}@${moduleVersion}`);
this._isDisabled = false;
const Server: any = moduleExports;
for (const name of RESTIFY_METHODS) {
this._unwrap(Server.prototype, name as keyof Server);
if (isWrapped(Server.prototype[name])) {
this._unwrap(Server.prototype, name);
}
this._wrap(
Server.prototype,
name as keyof Server,
this._methodPatcher.bind(this)
);
}
for (const name of RESTIFY_MW_METHODS) {
this._unwrap(Server.prototype, name as keyof Server);
if (isWrapped(Server.prototype[name])) {
this._unwrap(Server.prototype, name);
}
this._wrap(
Server.prototype,
name as keyof Server,
this._middlewarePatcher.bind(this)
);
}
return moduleExports;
},
(moduleExports, moduleVersion) => {
diag.debug(`Removing patch for ${MODULE_NAME}@${moduleVersion}`);
this._isDisabled = true;
if (moduleExports) {
const Server: any = moduleExports;
for (const name of RESTIFY_METHODS) {
this._unwrap(Server.prototype, name as keyof Server);
}
for (const name of RESTIFY_MW_METHODS) {
this._unwrap(Server.prototype, name as keyof Server);
}
}
}
}
));
)
);

return module;
}

private _middlewarePatcher (original: Function, methodName?: string) {
private _middlewarePatcher(original: Function, methodName?: string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think middleware spans should have middleware name as the span name (not sure if possibly with restify), e.g. the operation tree looks like this currently, where each middleware operation name is the route name:
image

However the express instrumentation contains middleware names:
image

Also I think middleware spans should have the incoming request span as its parent.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, it's possible. Express takes the name of the function it seems, which can be undefined at times, but still better than nothing. Thanks!

Also I think middleware spans should have the incoming request span as its parent.

I generally agree but don't want to change that because there might not be a request span if HTTP instrumentation is not enabled, so the structure would be different between HTTP instrumentation enabled and not.

const instrumentation = this;
return function (this: Server, ...handler: restify.RequestHandler[][] | restify.RequestHandler[]) {
return original.call(this, instrumentation._handlerPatcher({ type: types.LayerType.MIDDLEWARE, methodName }, handler));
return function (this: Server, ...handler: types.NestedRequestHandlers) {
return original.call(
this,
instrumentation._handlerPatcher(
{ type: types.LayerType.MIDDLEWARE, methodName },
handler
)
);
};
}

private _methodPatcher (original: Function, methodName?: string) {
private _methodPatcher(original: Function, methodName?: string) {
const instrumentation = this;
return function (this: Server, path: any, ...handler: restify.RequestHandler[][] | restify.RequestHandler[]) {
return original.call(this, path, ...instrumentation._handlerPatcher({ type: types.LayerType.REQUEST_HANDLER, path, methodName }, handler));
return function (
this: Server,
path: any,
...handler: types.NestedRequestHandlers
) {
return original.call(
this,
path,
...instrumentation._handlerPatcher(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how cleanly you've kept the reuse between methodPatcher and middlewarePatcher - not too complex but still keeping the essential bits parameterized instead of copied+pasted.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! :)

{ type: types.LayerType.REQUEST_HANDLER, path, methodName },
handler
)
);
};
}

// will return the same type as `handler`, but all functions recusively patched
private _handlerPatcher (metadata: types.Metadata, handler: restify.RequestHandler | restify.RequestHandler[] | restify.RequestHandler[][]): any {
private _handlerPatcher(
metadata: types.Metadata,
handler: restify.RequestHandler | types.NestedRequestHandlers
): any {
if (Array.isArray(handler)) {
return (handler as restify.RequestHandler[]).map((handler: any) => this._handlerPatcher(metadata, handler));
return handler.map(handler => this._handlerPatcher(metadata, handler));
}
if (typeof handler === 'function') {
return (req: types.Request, res: restify.Response, next: restify.Next) => {
return (
req: types.Request,
res: restify.Response,
next: restify.Next
) => {
if (this._isDisabled) {
return handler(req, res, next);
}
// const parentSpan = api.getSpan(api?.context?.active());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const route = (typeof req.getRoute === 'function' ? req.getRoute()?.path : req.route?.path);
const route =
typeof req.getRoute === 'function'
? req.getRoute()?.path
: req.route?.path;
const spanName = route || metadata.methodName || metadata.type;
const attributes = {
[types.CustomAttributeNames.VERSION]: this._moduleVersion || 'n/a',
[types.CustomAttributeNames.TYPE]: metadata.type,
[types.CustomAttributeNames.METHOD]: metadata.methodName,
[HttpAttribute.HTTP_ROUTE]: route,
}
const span = this.tracer.startSpan(spanName, {
attributes,
}, api.context.active()); // TODO: <- with this I intend to find and attach all consecutive handlers to HTTP span
};
const span = this.tracer.startSpan(
spanName,
{
attributes,
},
api.context.active()
); // TODO: <- with this I intend to find and attach all consecutive handlers to HTTP span
// .. but instead all spans are attached to the previous handler's span.
const endSpan = once(span.end.bind(span));
const patchedNext = (err: any) => {
const patchedNext = (err?: any) => {
endSpan();
next(err);
};
Expand All @@ -157,7 +195,7 @@ export class RestifyInstrumentation extends InstrumentationBase<
req,
res,
patchedNext
);
);
};
}

Expand Down
29 changes: 24 additions & 5 deletions plugins/node/opentelemetry-instrumentation-restify/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
/*
* 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 * as restify from 'restify';

export enum CustomAttributeNames {
Expand All @@ -12,13 +27,17 @@ export enum LayerType {
}

declare interface RequestWithRoute extends restify.Request {
route: { path: string },
getRoute: (() => { path: string }),
route: { path: string };
getRoute: () => { path: string };
}

export declare type Request = RequestWithRoute;
export declare type Metadata = {
path?: string,
methodName?: string,
type: LayerType,
path?: string;
methodName?: string;
type: LayerType;
};

export type NestedRequestHandlers = Array<
NestedRequestHandlers | restify.RequestHandler
>;
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
import RestifyInstrumentation from '../src';
const plugin = new RestifyInstrumentation();

import { strict as assert } from 'assert';
import * as assert from 'assert';
import * as http from 'http';
import { AddressInfo } from 'net';

Expand Down Expand Up @@ -119,37 +119,37 @@ describe('Restify Instrumentation', () => {
await context.with(setSpan(context.active(), rootSpan), async () => {
await httpRequest.get(`http://localhost:${port}/route/foo`);
rootSpan.end();
assert.deepStrictEqual(memoryExporter.getFinishedSpans().length, 4);
assert.strictEqual(memoryExporter.getFinishedSpans().length, 4);

{
// span from pre
const span = memoryExporter
.getFinishedSpans()[0];
assert.notEqual(span, undefined);
assert.equal(span.attributes['http.route'], undefined);
assert.equal(span.attributes['restify.method'], 'pre');
assert.equal(span.attributes['restify.type'], 'middleware');
assert.equal(span.attributes['restify.version'], 'n/a');
const span = memoryExporter.getFinishedSpans()[0];
assert.notStrictEqual(span, undefined);
assert.strictEqual(span.attributes['http.route'], undefined);
assert.strictEqual(span.attributes['restify.method'], 'pre');
assert.strictEqual(span.attributes['restify.type'], 'middleware');
assert.strictEqual(span.attributes['restify.version'], 'n/a');
}
{
// span from use
const span = memoryExporter
.getFinishedSpans()[1];
assert.notEqual(span, undefined);
assert.equal(span.attributes['http.route'], '/route/:param');
assert.equal(span.attributes['restify.method'], 'use');
assert.equal(span.attributes['restify.type'], 'middleware');
assert.equal(span.attributes['restify.version'], 'n/a');
const span = memoryExporter.getFinishedSpans()[1];
assert.notStrictEqual(span, undefined);
assert.strictEqual(span.attributes['http.route'], '/route/:param');
assert.strictEqual(span.attributes['restify.method'], 'use');
assert.strictEqual(span.attributes['restify.type'], 'middleware');
assert.strictEqual(span.attributes['restify.version'], 'n/a');
}
{
// span from get
const span = memoryExporter
.getFinishedSpans()[2];
assert.notEqual(span, undefined);
assert.equal(span.attributes['http.route'], '/route/:param');
assert.equal(span.attributes['restify.method'], 'get');
assert.equal(span.attributes['restify.type'], 'request_handler');
assert.equal(span.attributes['restify.version'], 'n/a');
const span = memoryExporter.getFinishedSpans()[2];
assert.notStrictEqual(span, undefined);
assert.strictEqual(span.attributes['http.route'], '/route/:param');
assert.strictEqual(span.attributes['restify.method'], 'get');
assert.strictEqual(
span.attributes['restify.type'],
'request_handler'
);
assert.strictEqual(span.attributes['restify.version'], 'n/a');
}
});
});
Expand All @@ -160,19 +160,21 @@ describe('Restify Instrumentation', () => {
await context.with(setSpan(context.active(), rootSpan), async () => {
const res = await httpRequest.get(`http://localhost:${port}/not-found`);
rootSpan.end();
assert.deepStrictEqual(memoryExporter.getFinishedSpans().length, 2);
assert.strictEqual(memoryExporter.getFinishedSpans().length, 2);

{
// span from pre
const span = memoryExporter
.getFinishedSpans()[0];
assert.notEqual(span, undefined);
assert.equal(span.attributes['http.route'], undefined);
assert.equal(span.attributes['restify.method'], 'pre');
assert.equal(span.attributes['restify.type'], 'middleware');
assert.equal(span.attributes['restify.version'], 'n/a');
const span = memoryExporter.getFinishedSpans()[0];
assert.notStrictEqual(span, undefined);
assert.strictEqual(span.attributes['http.route'], undefined);
assert.strictEqual(span.attributes['restify.method'], 'pre');
assert.strictEqual(span.attributes['restify.type'], 'middleware');
assert.strictEqual(span.attributes['restify.version'], 'n/a');
}
assert.strictEqual(res, '{"code":"ResourceNotFound","message":"/not-found does not exist"}');
assert.strictEqual(
res,
'{"code":"ResourceNotFound","message":"/not-found does not exist"}'
);
});
});

Expand All @@ -189,9 +191,12 @@ describe('Restify Instrumentation', () => {
const rootSpan = tracer.startSpan('rootSpan');

await context.with(setSpan(context.active(), rootSpan), async () => {
assert.strictEqual(await httpRequest.get(`http://localhost:${port}/route/foo`), '{"route":"foo"}');
assert.strictEqual(
await httpRequest.get(`http://localhost:${port}/route/foo`),
'{"route":"foo"}'
);
rootSpan.end();
assert.deepStrictEqual(memoryExporter.getFinishedSpans().length, 1);
assert.strictEqual(memoryExporter.getFinishedSpans().length, 1);
assert.notStrictEqual(memoryExporter.getFinishedSpans()[0], undefined);
});
});
Expand Down