Last active
August 5, 2024 09:50
-
-
Save AliBakerSartawi/c4852279e541904881f45ef4912e33f2 to your computer and use it in GitHub Desktop.
OTEL Attribute Decorator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ATTR_METADATA_KEY } from './constants'; | |
import { AttrKey, AttrMetadata } from './types'; | |
/** | |
* Sets a custom attribute key on a parameter so that it can be picked up | |
* by the Span decorator and mapped to the args values. | |
*/ | |
export function Attr(key: AttrKey): ParameterDecorator { | |
return function ( | |
target: NonNullable<unknown>, | |
propertyKey: string | symbol | undefined, // method name | |
parameterIndex: number, | |
) { | |
if (!propertyKey) { | |
console.log('propertyKey is undefined'); | |
return; | |
} | |
const existingMetadata: AttrMetadata[] = | |
Reflect.getMetadata(ATTR_METADATA_KEY, target, propertyKey) || []; | |
const metadata: AttrMetadata = { | |
type: 'attr', | |
key, | |
parameterIndex, | |
}; | |
const newMetadata = [...existingMetadata, metadata]; | |
Reflect.defineMetadata(ATTR_METADATA_KEY, newMetadata, target, propertyKey); | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const ATTR_METADATA_KEY = Symbol('span:attr'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Logger } from '@nestjs/common'; | |
import { recordSpanExceptionFactory } from './utils'; | |
@Injectable() | |
export class ExampleService { | |
private readonly logger = new Logger(ExampleService.name); | |
private readonly recordException = recordSpanExceptionFactory(this.logger); | |
@Span() | |
async whatchamacallit( | |
@Attr('company.id') companyId: number, | |
@Attr('data') data: SomeDataDto, | |
) { | |
try { | |
/* Your awesome code in here */ | |
} catch (e) { | |
this.recordException(`Failed to do this and that: ${e}`); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'; | |
import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; | |
import { Resource } from '@opentelemetry/resources'; | |
import { NodeSDK } from '@opentelemetry/sdk-node'; | |
import { | |
AlwaysOnSampler, | |
BatchSpanProcessor, | |
NodeTracerProvider, | |
} from '@opentelemetry/sdk-trace-node'; | |
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; | |
import process from 'process'; | |
class Tracer { | |
private sdk: NodeSDK | null = null; | |
private exporter = new OTLPTraceExporter(); | |
private provider = new NodeTracerProvider({ | |
resource: new Resource({ | |
[SemanticResourceAttributes.SERVICE_NAME]: 'qr-messenger', | |
}), | |
}); | |
public init() { | |
try { | |
// export spans to console (useful for debugging) | |
// if (process.env.ENV === 'development') { | |
// this.provider.addSpanProcessor( | |
// new BatchSpanProcessor(new ConsoleSpanExporter()), | |
// ); | |
// } | |
// export spans to opentelemetry collector | |
this.provider.addSpanProcessor(new BatchSpanProcessor(this.exporter)); | |
this.provider.register(); | |
this.sdk = new NodeSDK({ | |
traceExporter: this.exporter, | |
instrumentations: [ | |
// getNodeAutoInstrumentations(), | |
// new HttpInstrumentation(), | |
// new ExpressInstrumentation(), | |
new NestInstrumentation(), | |
], | |
sampler: new AlwaysOnSampler(), | |
}); | |
this.sdk.start(); | |
console.info('The tracer has been initialized'); | |
} catch (e) { | |
console.error('Failed to initialize the tracer', e); | |
} finally { | |
// gracefully shut down the SDK on process exit | |
process.on('SIGTERM', () => { | |
this.sdk | |
?.shutdown() | |
.then( | |
() => console.log('SDK shut down successfully'), | |
(err) => console.log('Error shutting down SDK', err), | |
) | |
.finally(() => process.exit(0)); | |
}); | |
} | |
} | |
} | |
new Tracer().init(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// tracer should be initialized before even importing anything from Nest | |
import './init-tracer'; | |
import { ValidationPipe } from '@nestjs/common'; | |
import { NestFactory } from '@nestjs/core'; | |
import { AppModule } from './app.module'; | |
import { CustomLogger, getPort } from './shared/utils/bootstrap.utils'; | |
import { trace } from '@opentelemetry/api'; | |
import { recordException } from './shared/utils/tracing.utils'; | |
async function bootstrap() { | |
const app = await NestFactory.create(AppModule, { | |
logger: new CustomLogger(), | |
}); | |
/* -------------------------- Global Error Handling ------------------------- */ | |
/** | |
* Necessary to catch an error that is thrown on `disconnected` event | |
* when a business manually logs out from a session that is currently active in the server | |
* or when puppeteer cannot find the chromium executable | |
*/ | |
process.on('unhandledRejection', (reason) => { | |
const tracer = trace.getTracer('default'); | |
tracer.startActiveSpan('unhandledRejection', (span) => { | |
recordException(span, reason); | |
// use console, not the custom logger as it strips away the stack trace | |
console.error('Unhandled Rejection:', reason); | |
span.end(); | |
}); | |
}); | |
/* ------------------------------- Validation ------------------------------- */ | |
app.useGlobalPipes(new ValidationPipe()); | |
/* ------------------------- Global Exception Filter ------------------------ */ | |
// app.useGlobalFilters(new GlobalExceptionFilter()); | |
/* ---------------------------------- CORS ---------------------------------- */ | |
app.enableCors(); | |
/* ---------------------------------- PORT ---------------------------------- */ | |
const PORT = getPort(); | |
await app.listen(PORT); | |
} | |
void bootstrap(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { SpanOptions, trace } from '@opentelemetry/api'; | |
import { | |
copyMetadataFromFunctionToFunction, | |
recordException, | |
setAttribute, | |
setParameterAttributes, | |
} from './utils'; | |
interface ExtendedSpanOptions extends SpanOptions { | |
name?: string; | |
ignoreOutcomeAttr?: boolean; | |
} | |
/** | |
* A modified version of the Span decorator from https://github.com/pragmaticivan/nestjs-otel | |
*/ | |
export function Span(options: ExtendedSpanOptions = {}) { | |
return ( | |
target: NonNullable<unknown>, | |
propertyKey: string, | |
propertyDescriptor: PropertyDescriptor, | |
) => { | |
const originalFunction = propertyDescriptor.value; | |
const wrappedFunction = function PropertyDescriptor( | |
this: NonNullable<unknown>, | |
...args: unknown[] | |
) { | |
const tracer = trace.getTracer('default'); | |
const spanName = | |
options.name || `${target.constructor.name}.${propertyKey}`; | |
return tracer.startActiveSpan(spanName, options, (span) => { | |
setParameterAttributes(span, target, propertyKey, args); | |
if (originalFunction.constructor.name === 'AsyncFunction') { | |
return originalFunction | |
.apply(this, args) | |
.then((result: unknown) => { | |
if (!options.ignoreOutcomeAttr) { | |
setAttribute(propertyKey, span, 'outcome', result); | |
} | |
return result; | |
}) | |
.catch((error: unknown) => { | |
recordException(span, error); | |
// Throw error to propagate it further | |
throw error; | |
}) | |
.finally(() => { | |
span.end(); | |
}); | |
} | |
try { | |
const result = originalFunction.apply(this, args); | |
if (!options.ignoreOutcomeAttr) { | |
setAttribute(propertyKey, span, 'outcome', result); | |
} | |
return result; | |
} catch (error) { | |
recordException(span, error); | |
// Throw error to propagate it further | |
throw error; | |
} finally { | |
span.end(); | |
} | |
}); | |
}; | |
// eslint-disable-next-line no-param-reassign | |
propertyDescriptor.value = wrappedFunction; | |
copyMetadataFromFunctionToFunction(originalFunction, wrappedFunction); | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface AttrMetadata { | |
type: 'attr'; | |
key: AttrKey; | |
parameterIndex: number; | |
} | |
export type AttrKey = | |
/* ---------------------- Frequent ---------------------- */ | |
| 'outcome' | |
| 'company.id' | |
/* ----------------------- PubSub ----------------------- */ | |
| 'pubsub.event' | |
/* ------------------------ Redis ----------------------- */ | |
| 'redis.key' | |
| 'redis.value' | |
| 'redis.ttl.seconds' | |
| 'redis.score' | |
| 'redis.min.score' | |
| 'redis.max.score' | |
/* ----------------------- General ---------------------- */ | |
| 'str' | |
| 'data' | |
| 'reason'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Span, SpanStatusCode } from '@opentelemetry/api'; | |
import { AttrKey, AttrMetadata } from './types'; | |
import { ATTR_METADATA_KEY } from './constants'; | |
import { Logger } from '@nestjs/common'; | |
export function recordException(span: Span, error: unknown) { | |
try { | |
const e = error instanceof Error ? error : new Error(JSON.stringify(error)); | |
span.recordException(e); | |
span.setStatus({ code: SpanStatusCode.ERROR, message: e.message }); | |
} catch (e) { | |
console.error(`Failed to record exception. Error: ${e}`); | |
} | |
} | |
function recordSpanException(error: unknown) { | |
try { | |
const span = trace.getActiveSpan(); | |
if (span) { | |
recordException(span, error); | |
} else { | |
console.error('No active span found to record the exception.'); | |
} | |
} catch (e) { | |
console.error(`Failed to record span exception. Error: ${e}`); | |
} | |
} | |
export function recordSpanExceptionFactory(logger: Logger) { | |
return function recordException(e: unknown) { | |
try { | |
recordSpanException(e); | |
logger.error(e); | |
} catch (e) { | |
logger.error(`Failed to record exception ${e}`); | |
} | |
}; | |
} | |
export function getAttrs( | |
target: NonNullable<unknown>, | |
propertyKey: string, | |
): AttrMetadata[] { | |
try { | |
const metadata: AttrMetadata[] = | |
Reflect.getMetadata(ATTR_METADATA_KEY, target, propertyKey) || []; | |
const filteredMetadata = metadata.filter((item) => item.type === 'attr'); | |
return filteredMetadata; | |
} catch (error) { | |
console.log({ error }); | |
return []; | |
} | |
} | |
export function setAttribute( | |
propertyKey: string, | |
span: Span, | |
key: AttrKey, | |
value: unknown, | |
) { | |
const serialized = (() => { | |
try { | |
if ( | |
typeof value === 'boolean' || | |
typeof value === 'number' || | |
typeof value === 'string' | |
) { | |
return value; | |
} else { | |
// circular objects will fail to stringify | |
return JSON.stringify(value) || 'null'; | |
} | |
} catch (e) { | |
const error = | |
typeof e === 'object' && e !== null && 'message' in e ? e.message : e; | |
console.error( | |
`Failed to stringify value for span ${propertyKey}. Error ${error}`, | |
); | |
return 'un-json-stringify-able'; | |
} | |
})(); | |
try { | |
span.setAttribute(key, serialized); | |
} catch (error) { | |
console.error(error); | |
} | |
} | |
export function setParameterAttributes( | |
span: Span, | |
targetClass: NonNullable<unknown>, | |
propertyKey: string, | |
args: unknown[], | |
) { | |
try { | |
const attrs = getAttrs(targetClass, propertyKey); | |
attrs.forEach((attr) => { | |
const value = args?.[attr.parameterIndex]; | |
setAttribute(propertyKey, span, attr.key, value); | |
}); | |
} catch (error) { | |
console.error(error); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment