作者:Pavol Loffay,软件工程师 @红帽
在本文中,我们将研究类似代理的测仪(instrumentation)工具T-Trace。该工具为运行在GraalVM上的应用程序提供非侵入性的测仪功能。我们将使用T-Trace和带有Jaeger NodeJS tracer的OpenTracing API来测仪(instrument)一个简单的NodeJS应用程序。
首先,GraalVM是一个多语言的虚拟机。它可以运行任何基于JVM的语言,也可以运行其他主流语言,比如NodeJS、Python和Ruby……它还支持像C和C++这样的LLVM语言。最理想的情况是,这些语言可以组合在一个应用程序中。另一个非常有趣的特性是GraalVM可以为JVM语言生成原生可执行文件。这个特性被Quarkus和Micronaut等现代云原生Java运行时使用。
测仪的风格
在深入研究演示应用程序之前,我们先来看看不同的测仪技术。有几种测仪风格可以应用于代码片段或整个应用程序。在高层次上,我们可以讨论黑盒和白盒工具。黑盒技术不需要对应用程序进行任何修改。监视系统在这情况中仅使用应用程序生成的事件。这些事件可以是日志,也可以是应用程序生成的任何数据。
在本文中,我们将重点讨论白盒技术。这种技术假设应用程序的代码是已知的,并且可以检查。有几种方法可以将测仪点添加到应用程序中:
应用程序
该应用程序是一个简单的NodeJS服务器,只有一个处理程序。完整的演示代码和说明可以在GitHub的pavolloffay/graalvm-t-trace中找到。下面是server.js文件:
function jaegerAvailable(jaeger) {
console.log("Providing Jaeger object to the agent");
}
jaegerAvailable(require("jaeger-client"));
const port = 3000;
const http = require("http");
const srv = http.createServer((req, res) => {
console.log(`server: obtained request ${res.id}`);
setTimeout(() => {
res.write(`OK# ${res.id}`);
console.log(`server: replied to request ${res.id}`);
res.end();
}, 5);
});
srv.listen(port);
本例中唯一与跟踪相关的代码是加载npm模块jaeger-client。这是目前已知的T-Trace的局限性,因为代理脚本无法加载其他库。这个特性被添加到T-Trace中。然后应用程序在请求的开头和结尾打印请求id。代理脚本中设置了res.id。
现在让我们看一下测仪脚本jaegernode.js。有两个函数:initializeJaeger和initializeAgent。第一个函数使用HTTP发送器创建Jaeger跟踪器实例,并将其发送到收集器端口14268,最后调用第二个函数:
let initializeJaeger = function (ctx, frame) {
agent.off('enter', initializeJaeger);
let jaeger = frame.jaeger;
var initTracer = jaeger.initTracer;
console.log('agent: Jaeger tracer obtained');
// See schema https://github.com/jaegertracing/jaeger-client-node/blob/master/src/configuration.js#L37
var config = {
serviceName: 't-trace-demo',
reporter: {
// Provide the traces endpoint; this forces the client to connect directly to the Collector and send
// spans over HTTP
collectorEndpoint: 'http://localhost:14268/api/traces',
// logSpans: true
},
sampler: {
type : 'const',
param : 1
}
};
var options = {
tags: {
't-trace-demo.version': '1.1.2',
},
// metrics: metrics,
logger: console,
sampler: {
type : 'const',
param : 1
}
};
var tracer = initTracer(config, options);
initializeAgent(tracer);
};
agent.on('return', initializeJaeger, {
roots: true,
rootNameFilter: name => name === 'jaegerAvailable'
});
第二个函数是initializeAgent,它测仪应用程序代码:
let initializeAgent = function(tracer) {
agent.on('enter', function(ctx, frame) {
const args = frame.args;
if ('request' !== frame.type || args.length !== 2 || typeof args[0] !== 'object' || typeof args[1] !== 'object') {
return;
}
const req = args[0];
const res = args[1];
const span = tracer.startSpan(req.method);
span.setTag("span.kind", "server");
span.setTag("http.url", req.url);
span.setTag("http.method", req.method);
res.id = span.context().spanIdStr;
res.span = span;
console.log(`agent: handling #${res.id} request for ${req.url}`);
}, {
roots: true,
rootNameFilter: name => name === 'emit',
sourceFilter: src => src.name === 'events.js'
});
agent.on('return', function(ctx, frame) {
var res = frame['this'];
if (res.span) {
res.span.setTag("http.status_code", res.statusCode);
if (res.statusCode >= 400) {
res.span.setTag("error", "true");
}
res.span.finish();
console.log(`agent: finished #${res.id} request`);
} else {
// OK, caused for example by Tracer itself connecting to Jaeger server
}
}, {
roots: true,
rootNameFilter: name => name === 'end',
sourceFilter: src => src.name === '_http_outgoing.js'
});
console.log('agent: ready');
};
测仪是通过agent.on('enter', fn)和agent.on('return', fn)完成的。当调用应用程序中的任何函数时调用第一个测仪点,当函数返回调用者执行时调用第二个测仪点。agent.on函数可以访问frame变量和方法参数。参数用于检查函数是否为HTTP处理程序。你还可以注意到,span对象被注入到响应中。
现在让我们运行应用程序和Jaeger服务器:
docker run --rm -it --net=host jaegertracing/all-in-one:1.16.0
$GRAALVM_HOME/bin/npm install jaeger-client@3.17.2
$GRAALVM_HOME/bin/node --experimental-options --js.print=true --agentscript=jaegernode.js server.js
curl http://localhost:3000
从Jaeger的屏幕截图显示t-trace演示应用程序的跟踪。
总结
我们看到了一个带有Jaeger和OpenTracing的NodeJS hello-world的T-Trace示例。这演示了如何将类似代理的测仪应用到NodeJS应用程序中,而不需要monkey-patching。
可以对代码进行许多改进。例如,我们可以扩展它,不测仪代理脚本中的代码,而是重用NodeJS的OpenTracing测仪,并将其安装到代理脚本中。我们可以做的另一个改进是支持脚本的动态加载。在这种情况下,应用程序将使用代理脚本启动,该脚本暴露用于加载和禁用脚本的REST API。这是一个非常强大的特性,可以动态地更改测仪的粒度,而无需重新编译和重新部署应用程序。
T-Trace还能够将工具语言与主语言混合使用。例如,可以用JavaScript编写代理脚本来跟踪Ruby或C++应用程序。
参考文献