From aa64f084cfeacbed1e9ea656ed307dc14588c19b Mon Sep 17 00:00:00 2001 From: Yotam loewenbach <48534558+yotamloe@users.noreply.github.com> Date: Tue, 24 Dec 2024 17:39:46 +0700 Subject: [PATCH] [Feature] otel context injection (#133) * Add `_addOpentelemetryContext()` * update dependencies * Add unit tests * Update dependencies * Update log * Add `addOtelContext` configuration option * docs * refactor tests --- README.md | 16 +++ lib/logzio-nodejs.js | 71 +++++++++----- package-lock.json | 226 ++++++++++++++++++++++++++++++++++++++++++- package.json | 5 +- test/logger.test.js | 46 +++++++++ 5 files changed, 335 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index f7986de..ec684ae 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ logger.log(obj); * **compress** - If true the the logs are compressed in gzip format. Default: `false` * **internalLogger** - set internal logger that supports the function log. Default: console. * **extraFields** - Adds your own custom fields to each log. Add in JSON Format, for example: `extraFields : { field_1: "val_1", field_2: "val_2" , ... }`. +* **addOtelContext** - Add `trace_id`, `span_id`, `service_name` fields to logs when opentelemetry context is available. Default: `true` ## Using UDP @@ -99,6 +100,17 @@ logger.log('This is a log message'); } ``` +## Add opentelemetry context +If you're sending traces with OpenTelemetry instrumentation (auto or manual), you can correlate your logs with the trace context. That way, your logs will have traces data in it, such as service name, span id and trace id (version >= `2.2.0`). This feature is enabled by default, To disable it, set the `AddOtelContext` param in your handler configuration to `false`, like in this example: + +```javascript +var logger = require('logzio-nodejs').createLogger({ + token: 'token', + type: 'no-otel-context', + addOtelContext: false +}); +``` + ## Build and test locally 1. Clone the repository: ```bash @@ -112,6 +124,10 @@ logger.log('This is a log message'); ``` ## Update log +**2.2.0** +- Add `addOtelContext` configuration option: + - `trace_id`, `span_id`, `service_name` fields to logs when opentelemetry context is available. + **2.1.8** - Make `User-Agent` not optional and add the version to it. diff --git a/lib/logzio-nodejs.js b/lib/logzio-nodejs.js index 5cce963..34f49db 100755 --- a/lib/logzio-nodejs.js +++ b/lib/logzio-nodejs.js @@ -4,7 +4,7 @@ const assign = require('lodash.assign'); const dgram = require('dgram'); const zlib = require('zlib'); const axiosInstance = require('./axiosInstance'); - +const { trace, context } = require('@opentelemetry/api'); const nanoSecDigits = 9; @@ -53,6 +53,7 @@ class LogzioLogger { protocol = 'http', port, timeout, + addOtelContext = true, sleepUntilNextRetry = 2 * 1000, callback = this._defaultCallback, extraFields = {}, @@ -103,11 +104,13 @@ class LogzioLogger { this.timeout = timeout; // build the url for logging - this.messages = []; this.bulkId = 1; this.extraFields = extraFields; this.typeOfIP = 'IPv4'; + + // OpenTelemetry context + this.addOtelContext = addOtelContext } _setProtocol(port) { @@ -231,31 +234,47 @@ class LogzioLogger { } } } - + /** + * Attach OpenTelemetry context to the log record. + * @param msg - The message (Object) to append the OpenTelemetry context to. + * @private + */ + _addOpentelemetryContext(msg) { + if (!this.addOtelContext) { + return; + } + let span = trace.getSpan(context.active()); + if (span) { + msg.trace_id = span.spanContext().traceId; + msg.span_id = span.spanContext().spanId; + msg.service_name = span.resource._attributes['service.name']; + } + } log(msg, obj) { - if (this.closed === true) { - throw new Error('Logging into a logger that has been closed!'); - } - if (![null, undefined].includes(obj)) { - msg += JSON.stringify(obj); - } - if (typeof msg === 'string') { - msg = { - message: msg, - }; - } - this._addSourceIP(msg); - msg = assign(msg, this.extraFields); - if (!msg.type) { - msg.type = this.type; - } - this._addTimestamp(msg); - - this.messages.push(msg); - if (this.messages.length >= this.bufferSize) { - this._debug('Buffer is full - sending bulk'); - this._popMsgsAndSend(); - } + if (this.closed === true) { + throw new Error('Logging into a logger that has been closed!'); + } + if (![null, undefined].includes(obj)) { + msg += JSON.stringify(obj); + } + if (typeof msg === 'string') { + msg = { message: msg }; + } + + this._addSourceIP(msg); + msg = assign(msg, this.extraFields); + if (!msg.type) { + msg.type = this.type; + } + this._addTimestamp(msg); + this._addOpentelemetryContext(msg); + + + this.messages.push(msg); + if (this.messages.length >= this.bufferSize) { + this._debug('Buffer is full - sending bulk'); + this._popMsgsAndSend(); + } } _popMsgsAndSend() { diff --git a/package-lock.json b/package-lock.json index c207076..4271158 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,17 @@ { "name": "logzio-nodejs", - "version": "2.1.6", + "version": "2.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "logzio-nodejs", - "version": "2.1.6", + "version": "2.2.0", "license": "(Apache-2.0)", "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.0", + "@opentelemetry/sdk-trace-node": "^1.30.0", "axios": "^1.6.4", "json-stringify-safe": "5.0.1", "lodash.assign": "4.2.0", @@ -1379,6 +1382,146 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.0.tgz", + "integrity": "sha512-roCetrG/cz0r/gugQm/jFo75UxblVvHaNSRoR0kSSRSzXFAiIBqFCZuH458BHBNRtRe+0yJdIJ21L9t94bw7+g==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.0.tgz", + "integrity": "sha512-Q/3u/K73KUjTCnFUP97ZY+pBjQ1kPEgjOfXj/bJl8zW7GbXdkw6cwuyZk6ZTXkVgCBsYRYUzx4fvYK1jxdb9MA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.30.0.tgz", + "integrity": "sha512-lcobQQmd+hLdtxJJKu/i51lNXmF1PJJ7Y9B97ciHRVQuMI260vSZG7Uf4Zg0fqR8PB+fT/7rnlDwS0M7QldZQQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.30.0.tgz", + "integrity": "sha512-0hdP495V6HPRkVpowt54+Swn5NdesMIRof+rlp0mbnuIUOM986uF+eNxnPo9q5MmJegVBRTxgMHXXwvnXRnKRg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.0.tgz", + "integrity": "sha512-5mGMjL0Uld/99t7/pcd7CuVtJbkARckLVuiOX84nO8RtLtIz0/J6EOHM2TGvPZ6F4K+XjUq13gMx14w80SVCQg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.0", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.0.tgz", + "integrity": "sha512-RKQDaDIkV7PwizmHw+rE/FgfB2a6MBx+AEVVlAHXRG1YYxLiBpPX2KhmoB99R5vA4b72iJrjle68NDWnbrE9Dg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.0", + "@opentelemetry/resources": "1.30.0", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.30.0.tgz", + "integrity": "sha512-MeXkXEdBs9xq1JSGTr/3P1lHBSUBaVmo1+UpoQhUpviPMzDXy0MNsdTC7KKI6/YcG74lTX6eqeNjlC1jV4Rstw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "1.30.0", + "@opentelemetry/core": "1.30.0", + "@opentelemetry/propagator-b3": "1.30.0", + "@opentelemetry/propagator-jaeger": "1.30.0", + "@opentelemetry/sdk-trace-base": "1.30.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -8613,6 +8756,85 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==" + }, + "@opentelemetry/context-async-hooks": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.0.tgz", + "integrity": "sha512-roCetrG/cz0r/gugQm/jFo75UxblVvHaNSRoR0kSSRSzXFAiIBqFCZuH458BHBNRtRe+0yJdIJ21L9t94bw7+g==", + "requires": {} + }, + "@opentelemetry/core": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.0.tgz", + "integrity": "sha512-Q/3u/K73KUjTCnFUP97ZY+pBjQ1kPEgjOfXj/bJl8zW7GbXdkw6cwuyZk6ZTXkVgCBsYRYUzx4fvYK1jxdb9MA==", + "requires": { + "@opentelemetry/semantic-conventions": "1.28.0" + } + }, + "@opentelemetry/propagator-b3": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.30.0.tgz", + "integrity": "sha512-lcobQQmd+hLdtxJJKu/i51lNXmF1PJJ7Y9B97ciHRVQuMI260vSZG7Uf4Zg0fqR8PB+fT/7rnlDwS0M7QldZQQ==", + "requires": { + "@opentelemetry/core": "1.30.0" + } + }, + "@opentelemetry/propagator-jaeger": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.30.0.tgz", + "integrity": "sha512-0hdP495V6HPRkVpowt54+Swn5NdesMIRof+rlp0mbnuIUOM986uF+eNxnPo9q5MmJegVBRTxgMHXXwvnXRnKRg==", + "requires": { + "@opentelemetry/core": "1.30.0" + } + }, + "@opentelemetry/resources": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.0.tgz", + "integrity": "sha512-5mGMjL0Uld/99t7/pcd7CuVtJbkARckLVuiOX84nO8RtLtIz0/J6EOHM2TGvPZ6F4K+XjUq13gMx14w80SVCQg==", + "requires": { + "@opentelemetry/core": "1.30.0", + "@opentelemetry/semantic-conventions": "1.28.0" + } + }, + "@opentelemetry/sdk-trace-base": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.0.tgz", + "integrity": "sha512-RKQDaDIkV7PwizmHw+rE/FgfB2a6MBx+AEVVlAHXRG1YYxLiBpPX2KhmoB99R5vA4b72iJrjle68NDWnbrE9Dg==", + "requires": { + "@opentelemetry/core": "1.30.0", + "@opentelemetry/resources": "1.30.0", + "@opentelemetry/semantic-conventions": "1.28.0" + } + }, + "@opentelemetry/sdk-trace-node": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.30.0.tgz", + "integrity": "sha512-MeXkXEdBs9xq1JSGTr/3P1lHBSUBaVmo1+UpoQhUpviPMzDXy0MNsdTC7KKI6/YcG74lTX6eqeNjlC1jV4Rstw==", + "requires": { + "@opentelemetry/context-async-hooks": "1.30.0", + "@opentelemetry/core": "1.30.0", + "@opentelemetry/propagator-b3": "1.30.0", + "@opentelemetry/propagator-jaeger": "1.30.0", + "@opentelemetry/sdk-trace-base": "1.30.0", + "semver": "^7.5.2" + }, + "dependencies": { + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" + } + } + }, + "@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==" + }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", diff --git a/package.json b/package.json index bf35039..d035ef7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "logzio-nodejs", "description": "A nodejs implementation for sending logs to Logz.IO cloud service Copy of logzio-nodejs", - "version": "2.1.8", + "version": "2.2.0", "author": "Gilly Barr ", "maintainers": [ { @@ -47,6 +47,9 @@ "logzio" ], "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.0", + "@opentelemetry/sdk-trace-node": "^1.30.0", "axios": "^1.6.4", "json-stringify-safe": "5.0.1", "lodash.assign": "4.2.0", diff --git a/test/logger.test.js b/test/logger.test.js index 1a3b2b0..43bdd2e 100644 --- a/test/logger.test.js +++ b/test/logger.test.js @@ -9,6 +9,8 @@ const hrtimemock = require('hrtimemock'); const axiosInstance = require('../lib/axiosInstance.js'); const prop = require('../package.json'); axiosInstance.defaults.adapter = 'http'; +const { trace, context } = require('@opentelemetry/api'); +const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); const dummyHost = 'logz.io'; @@ -32,7 +34,51 @@ const sendLogs = (logger, count = 1, message = 'hello there from test') => { }); }); }; + +const provider = new NodeTracerProvider(); +provider.register(); +const tracer = trace.getTracer('test-tracer'); + + describe('logger', () => { + describe('_addOpentelemetryContext', () => { + it('should attach traceId and spanId when a span is active', () => { + let logger = createLogger({ + bufferSize: 1, + }); + sinon.spy(logger, '_createBulk'); + + let logMessage; + + tracer.startActiveSpan('test-span', (span) => { + logMessage = { + message: 'test message with active span' + }; + logger.log(logMessage); + span.end(); + }); + + const loggedMessage = logger._createBulk.getCall(0).args[0][0]; + assert(loggedMessage.trace_id, 'trace_id should exist'); + assert(loggedMessage.span_id, 'span_id should exist'); + }); + + it('should not attach traceId or spanId when no span is active', () => { + let logger = createLogger({ + bufferSize: 1, + }); + sinon.spy(logger, '_createBulk'); + let logMessage = { + message: 'test message without active span' + }; + + logger.log(logMessage); + + const loggedMessage = logger._createBulk.getCall(0).args[0][0]; + assert.strictEqual(loggedMessage.trace_id, undefined, 'trace_id should not exist'); + assert.strictEqual(loggedMessage.span_id, undefined, 'span_id should not exist'); + }); + }); describe('logs a single line', () => { beforeAll((done) => { sinon