Skip to content

Commit

Permalink
feat(tracing): add utils for using spans (#58)
Browse files Browse the repository at this point in the history
* feat(tracing): add utils for using spans

* docs: add comments for typedoc

* feat(tracing): add decorators for tracing

* fix: cr

* fix: lint

* fix: export utils and decorators

* fix: cr

* fix: pass span instance to callback
  • Loading branch information
rannyeli authored Jan 2, 2024
1 parent 52f5874 commit 2257d1a
Show file tree
Hide file tree
Showing 8 changed files with 4,339 additions and 947 deletions.
5,070 changes: 4,143 additions & 927 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,20 @@
"devDependencies": {
"@commitlint/cli": "^11.0.0",
"@commitlint/config-conventional": "^11.0.0",
"@map-colonies/eslint-config": "^2.2.0",
"@map-colonies/eslint-config": "^4.0.0",
"@opentelemetry/context-async-hooks": "^1.3.0",
"@types/express": "^4.17.12",
"@types/faker": "^5.5.3",
"@types/node": "^14.14.12",
"commitlint": "^11.0.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.8.1",
"eslint": "^8.56.0",
"husky": "^4.3.5",
"pino": "^8.14.1",
"prettier": "^2.2.1",
"pretty-quick": "^3.1.0",
"standard-version": "^9.0.0",
"ts-node": "^9.1.1",
"typescript": "^4.9.5"
"typescript": "^5.3.3"
}
}
34 changes: 20 additions & 14 deletions src/metrics/middleware/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ export function metricsMiddleware(
if (shouldCollectDefaultMetrics) {
collectDefaultMetrics({ prefix: defaultMetricsPrefix, register: registry, labels: defaultMetricsLabels });
}
return async (req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> => {
try {
res.set('Content-Type', registry.contentType);
res.end(await registry.metrics());
} catch (error) {
return next(error);
}
return (req: express.Request, res: express.Response, next: express.NextFunction): void => {
registry
.metrics()
.then((metrics) => {
res.set('Content-Type', registry.contentType);
res.end(metrics);
})
.catch((error) => {
next(error);
});
};
}

Expand All @@ -38,13 +41,16 @@ export function metricsMiddleware(
export function defaultMetricsMiddleware(prefix?: string, labels?: Record<string, string>): express.RequestHandler {
const register = new Registry();
collectDefaultMetrics({ prefix, register, labels });
return async (req: express.Request, res: express.Response, next: express.NextFunction): Promise<void> => {
try {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
} catch (error) {
return next(error);
}
return (req: express.Request, res: express.Response, next: express.NextFunction): void => {
register
.metrics()
.then((metrics) => {
res.set('Content-Type', register.contentType);
res.end(metrics);
})
.catch((error) => {
next(error);
});
};
}

Expand Down
56 changes: 56 additions & 0 deletions src/tracing/decorators/v4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Tracer } from '@opentelemetry/api';
import { asyncCallWithSpan, callWithSpan } from '../utils/tracing';

/**
* Decorator that creates a trace span for the decorated method logic.
* using the typescript decorators stage 2.
* requires the "experimentalDecorators" compiler option to be true.
* @param _target the class prototype
* @param propertyKey the name of the decorated method
* @param descriptor the method descriptor
* @returns the decorated descriptor
*/
export function withSpan<This extends { tracer: Tracer }, Args extends unknown[], Return>(
_target: This,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Return>
): TypedPropertyDescriptor<(this: This, ...args: Args) => Return> {
const originalMethod = descriptor.value;

if (originalMethod === undefined) {
throw new Error('Decorated method is undefined');
}

descriptor.value = function (this: This, ...args: Args): Return {
return callWithSpan(() => originalMethod.call(this, ...args), this.tracer, String(propertyKey));
};

return descriptor;
}

/**
* Decorator that creates a trace span for the decorated async method logic.
* using the typescript decorators stage 2.
* requires the "experimentalDecorators" compiler option to be true.
* @param _target the class prototype
* @param propertyKey the name of the decorated async method
* @param descriptor the async method descriptor
* @returns the decorated descriptor
*/
export function withSpanAsync<This extends { tracer: Tracer }, Args extends unknown[], Return>(
_target: This,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>>
): TypedPropertyDescriptor<(this: This, ...args: Args) => Promise<Return>> {
const originalMethod = descriptor.value;

if (originalMethod === undefined) {
throw new Error('Decorated method is undefined');
}

descriptor.value = async function (this: This, ...args: Args): Promise<Return> {
return asyncCallWithSpan(async () => originalMethod.call(this, ...args), this.tracer, String(propertyKey));
};

return descriptor;
}
36 changes: 36 additions & 0 deletions src/tracing/decorators/v5.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Tracer } from '@opentelemetry/api';
import { asyncCallWithSpan, callWithSpan } from '../utils/tracing';

/**
* Decorator that creates a trace span for the decorated method logic.
* using the typescript decorators stage 3, which available in typescript v5 and above.
* requires the "experimentalDecorators" compiler option to be false.
* @param target the method to decorate
* @param context the class method decorator context
* @returns the decorated method
*/
export function withSpan<This extends { tracer: Tracer }, Args extends unknown[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
return function (this: This, ...args: Args): Return {
return callWithSpan(() => target.call(this, ...args), this.tracer, String(context.name));
};
}

/**
* Decorator that creates a trace span for the decorated async method logic.
* using the typescript decorators stage 3, which available in typescript v5 and above.
* requires the "experimentalDecorators" compiler option to be false.
* @param target the async method to decorate
* @param context the class method decorator context
* @returns the decorated async method
*/
export function withSpanAsync<This extends { tracer: Tracer }, Args extends unknown[], Return>(
target: (this: This, ...args: Args) => Promise<Return>,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Promise<Return>>
) {
return async function (this: This, ...args: Args): Promise<Return> {
return asyncCallWithSpan(async () => target.call(this, ...args), this.tracer, String(context.name));
};
}
4 changes: 3 additions & 1 deletion src/tracing/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export { Tracing } from './tracing';
export { getTraceContexHeaderMiddleware } from './middleware/traceOnHeaderMiddleware';
export { contexBindingHelper, ignoreIncomingRequestUrl, ignoreOutgoingRequestPath } from './utils/tracing';
export * from './utils/tracing';
export { getOtelMixin } from './mixin';
export { logMethod } from './loggerHook';
export { withSpan, withSpanAsync } from './decorators/v5';
export { withSpan as withSpanV4, withSpanAsync as withSpanAsyncV4 } from './decorators/v4';
2 changes: 1 addition & 1 deletion src/tracing/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class Tracing implements TelemetryBase<void> {
instrumentations: [
...(getNodeAutoInstrumentations({
...this.autoInstrumentationsConfigMap,
'@opentelemetry/instrumentation-pino': { enabled: false },
['@opentelemetry/instrumentation-pino']: { enabled: false },
}) as InstrumentationOption[]),
...(this.instrumentations ?? []),
],
Expand Down
78 changes: 77 additions & 1 deletion src/tracing/utils/tracing.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { IncomingMessage, RequestOptions } from 'http';
import api, { Span } from '@opentelemetry/api';
import api, { Span, SpanStatusCode, Tracer } from '@opentelemetry/api';
import { getTracingConfig } from '../config';

const tracingConfig = getTracingConfig();

export const contexBindingHelper = <T>(parentSpan: Span, func: T): T => {
const ctx = api.trace.setSpan(api.context.active(), parentSpan);
Expand All @@ -13,3 +16,76 @@ export const ignoreIncomingRequestUrl = (urlsToIgnore: RegExp[]): ((request: Inc
export const ignoreOutgoingRequestPath = (pathsToIgnore: RegExp[]): ((request: RequestOptions) => boolean) => {
return (request): boolean => pathsToIgnore.some((regex) => regex.test(request.path ?? ''));
};

/**
* Calls the given asynchronous function with oTel tracing span instrumentation
* @param fn function to be called
* @param tracer tracer to be used
* @param spanName name of the span to be created
* @returns the result of the original function
*/
export const asyncCallWithSpan = async <T>(fn: (span?: Span) => Promise<T>, tracer: Tracer, spanName: string): Promise<T> => {
if (!tracingConfig.isEnabled) {
return fn();
}
return new Promise((resolve, reject) => {
return tracer.startActiveSpan(spanName, (span) => {
fn(span)
.then((result) => {
handleSpanOnSuccess(span);
return resolve(result);
})
.catch((error) => {
handleSpanOnError(span, error);
return reject(error);
});
});
});
};

/**
* Calls the given function with oTel tracing span instrumentation
* @param fn function to be called
* @param tracer tracer to be used
* @param spanName name of the span to be created
* @returns the result of the original function
*/
export const callWithSpan = <T>(fn: (span?: Span) => T, tracer: Tracer, spanName: string): T => {
if (!tracingConfig.isEnabled) {
return fn();
}
return tracer.startActiveSpan(spanName, (span) => {
try {
const result = fn(span);
handleSpanOnSuccess(span);
return result;
} catch (error) {
handleSpanOnError(span, error);
throw error;
}
});
};

/**
* Ends the given span with status OK
* @param span span to be ended
*/
export const handleSpanOnSuccess = (span: Span): void => {
span.setStatus({ code: SpanStatusCode.OK });
span.end();
};

/**
* Ends the given span with status ERROR and records the error
* @param span span to be ended
* @param error error to be recorded
*/
export const handleSpanOnError = (span: Span, error?: unknown): void => {
span.setStatus({ code: SpanStatusCode.ERROR });

if (error instanceof Error) {
span.recordException(error);
}

span.end();
};

0 comments on commit 2257d1a

Please sign in to comment.