@chankamlam/nest-jaeger
Version:
Jaeger middleware to request tracing for nest application
199 lines (181 loc) • 6.24 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 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 req_cb: any = undefined;
private 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
var options = {};
var 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();
///////////////////////////////////////////////////
})
);
}
}