@servant573/nest-jaeger
Version:
Jaeger middleware to request tracing for nest application
199 lines (181 loc) • 6.27 kB
text/typescript
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { initTracer } from 'jaeger-client';
import { FORMAT_HTTP_HEADERS, Tags } from 'opentracing';
import axios from 'axios';
const TAGS = {
...Tags,
PROTOCAL: 'protocal',
TRACING_TAG: 'tracing-tag',
};
const UNKNOW = 'Unknow';
const DEFAULT_SAMPLER = {
type: 'const',
param: 1,
};
const DEFAULT_REPORTER = {
collectorEndpoint: 'http://localhost:14268/api/traces',
};
const DEFAULT_LOGGER = {
info: (msg) => {
console.log('JAEGER INFO ', msg);
},
error: (msg) => {
console.log('JAEGER ERROR', msg);
},
};
const DEFAULT_OPTION = { logger: DEFAULT_LOGGER };
const DEFAULT_CONFIG = {
serviceName: UNKNOW,
reporter: DEFAULT_REPORTER,
sampler: DEFAULT_SAMPLER,
};
@Injectable()
export class JaegerInterceptor implements NestInterceptor {
// tracer instance, one request one tracer
private readonly tracer: any = undefined;
// master span instance, can have mutilpe chirld span inside
private span: any = undefined;
// tracing tag from request and will pass to remote call
private tracing_tag: any = {};
// callback function form user
private readonly req_cb: any = undefined;
private readonly res_cb: any = undefined;
constructor(cfg?: {}, opt?: {}, req_cb?: any, res_cb?: any) {
// init tracer
if (!this.tracer) {
try {
const config = { ...DEFAULT_CONFIG, ...cfg };
const options = { ...DEFAULT_OPTION, ...opt };
this.tracer = initTracer(config, options);
this.req_cb = req_cb;
this.res_cb = res_cb;
console.log('[*]Init tracer ... [ DONE ] ');
} catch (error) {
console.error('[*]Init tracer ... [ FAILED ] ');
}
} else {
console.log(`[*]Tracer already existed`);
}
}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
//////////////////////////////////////////////////////////////////
// handle metadata
const reflector = new Reflector();
const except = reflector.get<boolean>(
'ExceptJaegerInterceptor',
context.getHandler(),
);
if (except) return next.handle();
////////////////////////////////////////////////////////////////////
// extract parent span from headers of request
if (!this.tracer) return next.handle();
const req = context.switchToHttp().getRequest();
const res = context.switchToHttp().getResponse();
const parent = this.tracer.extract(FORMAT_HTTP_HEADERS, req.headers);
const parentObj = parent ? { childOf: parent } : {};
this.span = this.tracer.startSpan(req.headers.host + req.path, parentObj);
///////////////////////////////////////////////////////////////////////////
// get tracing tag from headers of request
if (!this.span) return next.handle();
if (req.headers && req.headers[TAGS.TRACING_TAG]) {
this.tracing_tag = JSON.parse(req.headers[TAGS.TRACING_TAG]);
}
for (const key in this.tracing_tag) {
const val = this.tracing_tag[key];
this.span.setTag(key, val);
}
////////////////////////////////////////////////////////////////////////////
const createJaegerInstance = () => {
return {
// master span instance
span: this.span,
// tracer instance
tracer: this.tracer,
// TAGS of opentracing
tags: TAGS,
// use for remote call
axios: (opts: any = undefined) => {
if (!opts) return;
// inject tracing tag to remote call
let options = {};
const headers = {};
headers[TAGS.TRACING_TAG] = JSON.stringify(this.tracing_tag);
this.tracer.inject(this.span, FORMAT_HTTP_HEADERS, headers);
opts.headers = { ...opts.headers, ...headers };
options = { ...opts };
///////////////////////////////////////////////////////////
return axios(options);
},
// log
log: (name, content) => {
if (!this.span) return;
this.span.logEvent(name, content);
},
// setup tag
setTag: (tag, val) => {
if (!this.span) return;
this.span.setTag(tag, val);
},
// setup mutiple TAGS
addTags: (obj) => {
if (!this.span && !obj) return;
this.span.addTags(obj);
},
// setup tracing tag which can pass through all remote call by using Jaeger.request
setTracingTag: (tag, val) => {
if (!this.span) return;
this.span.setTag(tag, val);
this.tracing_tag[tag] = val;
},
// finish span job
finish: () => {
if (!this.span) return;
this.span.finish();
},
// create new span under master span
createSpan: (name, parent) => {
const parentObj = parent
? { childOf: parent }
: { childOf: this.span };
if (!this.tracer) return;
return this.tracer.startSpan(name, parentObj);
},
};
};
req.jaeger = createJaegerInstance();
// mark default tag of request
req.jaeger.setTag('request.ip', req.ip || UNKNOW);
req.jaeger.setTag('request.method', req.method || UNKNOW);
req.jaeger.setTag('request.headers', req.headers || UNKNOW);
req.jaeger.setTag('request.path', req.path || UNKNOW);
req.jaeger.setTag('request.body', req.body || UNKNOW);
req.jaeger.setTag('request.query', req.query || UNKNOW);
//////////////////////////////////////////////////
// handle request callback
if (this.req_cb) {
this.req_cb(req, res);
}
///////////////////////////////////////////////
return next.handle().pipe(
tap(() => {
// handle respose callback
if (this.res_cb) {
this.res_cb(req, res);
}
//mark default tag of response
req.jaeger.setTag('response.state', res.statusCode || UNKNOW);
req.jaeger.setTag('response.result', res.statusMessage || UNKNOW);
req.jaeger.finish();
///////////////////////////////////////////////////
}),
);
}
}