@mieweb/wikigdrive
Version:
Google Drive to MarkDown synchronization
262 lines (224 loc) • 8.05 kB
text/typescript
import type {ClientRequest, IncomingMessage} from 'node:http';
import * as path from 'node:path';
import process from 'node:process';
import opentelemetry, {SpanKind} from '@opentelemetry/api';
import type {Span} from '@opentelemetry/api';
import {ZipkinExporter} from '@opentelemetry/exporter-zipkin';
import {InstrumentationBase, registerInstrumentations} from '@opentelemetry/instrumentation';
import {HttpInstrumentation} from '@opentelemetry/instrumentation-http';
import {Resource} from '@opentelemetry/resources';
import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node';
import {AlwaysOnSampler, SimpleSpanProcessor} from '@opentelemetry/sdk-trace-base';
import {SemanticResourceAttributes} from '@opentelemetry/semantic-conventions';
const __dirname = import.meta.dirname;
let mainDirLength = 0;
let provider: NodeTracerProvider;
export function TelemetryClass() {
return function (classObj) {
if (!process.env.ZIPKIN_URL) {
return;
}
const stackTrace = new Error().stack.split('\n');
if (stackTrace.length < 3) {
return;
}
const classFileName = stackTrace[2]
.replace(/^.*file:\/\//, '')
.replace(/:.*$/, '');
if (!classFileName) {
return;
}
registerInstrumentations({
tracerProvider: provider,
instrumentations: [
new ClassInstrumentation(classObj.prototype, classFileName.substring(mainDirLength)),
]
});
};
}
export function TelemetryMethod(config: { paramsCount: number }) {
return function (classPrototype, methodFunc: string) {
const method = classPrototype[methodFunc] || methodFunc;
method.telemetryParamCount = config.paramsCount;
};
}
export function TelemetryMethodDisable() {
return function (classPrototype, methodFunc: string) {
const method = classPrototype[methodFunc] || methodFunc;
method.telemetryDisable = true;
};
}
export async function instrumentAndWrap(spanName, req, res, func) {
const tracer = opentelemetry.trace.getTracer(
provider.resource.attributes[SemanticResourceAttributes.SERVICE_NAME].toString(),
'1.0'
);
const traceparent = req.header('traceparent');
const input = { traceparent };
const activeContext = opentelemetry.propagation.extract(opentelemetry.context.active(), input);
return tracer.startActiveSpan(
spanName,
{ kind: SpanKind.SERVER },
activeContext,
async (span) => {
try {
const traceId = span.spanContext().traceId;
res.header('trace-id', traceId);
return await func();
} catch (err) {
// err.stack = [err.message].concat(stackTrace).join('\n');
span.recordException(err);
throw err;
} finally {
span.end();
}
});
}
export class ClassInstrumentation extends InstrumentationBase {
private className: string;
constructor(private classPrototype: unknown, private path: string) {
super('opentelemetry-instrumentation-class', '1.0', { enabled: false });
this.className = classPrototype.constructor.name;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
protected init() {
}
enable() {
super.enable();
// eslint-disable-next-line @typescript-eslint/no-this-alias
const instrumentation = this;
for (const methodName of Object.getOwnPropertyNames(this.classPrototype)) {
if ('function' !== typeof this.classPrototype[methodName]) {
continue;
}
const origMethod = this.classPrototype[methodName];
if (origMethod.telemetryDisable) {
continue;
}
this.classPrototype[methodName] = async function (...args) {
const stackTrace = new Error().stack.split('\n').splice(2);
const tracer = opentelemetry.trace.getTracer(
provider.resource.attributes[SemanticResourceAttributes.SERVICE_NAME].toString(),
'1.0'
);
let spanName = `${instrumentation.className}.${methodName}`;
if (origMethod.telemetryParamCount) {
spanName += '(';
for (let i = 0; i < origMethod.telemetryParamCount; i++) {
if (i > 0) {
spanName += ', ';
}
spanName += args[0];
}
spanName += ')';
}
return tracer.startActiveSpan(
spanName,
{ kind: SpanKind.INTERNAL },
async (span) => {
try {
const branch = 'develop';
const url = `https://github.com/mieweb/wikiGDrive/blob/${branch}/src${instrumentation.path}`;
span.setAttribute('src', url);
return await origMethod.apply(this, args);
} catch (err) {
err.stack = [err.message].concat(stackTrace).join('\n');
span.recordException(err);
throw err;
} finally {
span.end();
}
});
};
this.classPrototype[methodName].origMethod = origMethod;
}
}
}
export function instrumentFunction<K>(func, telemetryParamCount = 0, telemetryParamOffset = 0) {
return async function (...args) {
if (!process.env.ZIPKIN_URL) {
return await func(...args);
}
const stackTrace = new Error().stack.split('\n').splice(2);
const tracer = opentelemetry.trace.getTracer(
provider.resource.attributes[SemanticResourceAttributes.SERVICE_NAME].toString(),
'1.0'
);
let spanName = func.name;
if (telemetryParamCount) {
spanName += '(';
for (let i = telemetryParamOffset; i < telemetryParamCount + telemetryParamOffset; i++) {
if (i > 0) {
spanName += ', ';
}
spanName += args[0];
}
spanName += ')';
}
return tracer.startActiveSpan(
spanName,
{ kind: SpanKind.INTERNAL },
async (span) => {
try {
return await func(...args);
} catch (err) {
err.stack = [err.message].concat(stackTrace).join('\n');
span.recordException(err);
throw err;
} finally {
span.end();
}
});
};
}
export async function addTelemetry(serviceName: string, mainDir: string) {
if (!process.env.ZIPKIN_URL) {
return;
}
mainDirLength = mainDir.length;
provider = new NodeTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0'
}),
sampler: new AlwaysOnSampler(),
});
console.log('Initializing telemetry', serviceName, process.env.ZIPKIN_URL);
registerInstrumentations({
tracerProvider: provider,
instrumentations: [
new HttpInstrumentation({
requireParentforOutgoingSpans: true,
ignoreIncomingPaths: [ /^\/assets/ ],
ignoreOutgoingUrls: [ (url) => url.startsWith(process.env.ZIPKIN_URL) ],
applyCustomAttributesOnSpan(span: Span, request: ClientRequest | IncomingMessage) {
if (request['path']) {
span.updateName('http_client ' + request.method + ' ' + request['host'] + request['path'].replace(/\?.*/, ''));
}
if (request['originalUrl']) {
span.updateName('http_server ' + request.method + ' ' + request['originalUrl'].replace(/\?.*/, ''));
}
},
})
],
});
const {GitScanner} = await import('./git/GitScanner.ts');
const {GoogleDriveService} = await import('./google/GoogleDriveService.ts');
registerInstrumentations({
tracerProvider: provider,
instrumentations: [
new ClassInstrumentation(GitScanner.prototype, path.resolve(__dirname, './git/GitScanner.ts').substring(mainDir.length)),
new ClassInstrumentation(GoogleDriveService.prototype, path.resolve(__dirname, './google/GoogleDriveService').substring(mainDir.length)),
]
});
provider.addSpanProcessor(
new SimpleSpanProcessor(
new ZipkinExporter({
serviceName,
url: process.env.ZIPKIN_URL + '/api/v2/spans'
})
)
);
provider.register();
return opentelemetry.trace.getTracer(serviceName);
}