From 522ce49f5714c14ae57822dea856c4e40cec72ab Mon Sep 17 00:00:00 2001 From: Michele Mancioppi Date: Thu, 25 May 2023 14:40:38 +0200 Subject: [PATCH] feat: add support for LUMIGO_DEBUG_SPANDUMP to print to console.log or console.error Fixes #178 --- README.md | 2 +- package-lock.json | 39 ++++++++++++++--- package.json | 1 + src/exporters/FileSpanExporter.test.ts | 59 ++++++++++++++++++++++++++ src/exporters/FileSpanExporter.ts | 34 ++++++++++----- 5 files changed, 119 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index ca40c09f..1258d2a5 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Specifically supported are: * `LUMIGO_TRACER_TOKEN=`: Configure the Lumigo token to enable to upload of telemetry to Lumigo; without this environment variable, your Node.js process will not send telemetry to Lumigo. * `LUMIGO_DEBUG=TRUE`: Enables debug logging -* `LUMIGO_DEBUG_SPANDUMP=`: Log all spans collected to the `` file; this is an option intended only for debugging purposes and should *not* be used in production. +* `LUMIGO_DEBUG_SPANDUMP=`: Log all spans collected to the `` file or, the value is `console:log` or `console:error`, to `console.log` or `console.error`; this is an option intended only for debugging purposes and should *not* be used in production. This setting is independent from `LUMIGO_DEBUG`, that is, `LUMIGO_DEBUG` does not need to additionally be set for `LUMIGO_DEBUG_SPANDUMP` to work. * `LUMIGO_SWITCH_OFF=TRUE`: This option disables the Lumigo OpenTelemetry Distro entirely; no instrumentation will be injected, no tracing data will be collected. * `LUMIGO_SECRET_MASKING_REGEX='["regex1", "regex2"]'`: Prevents Lumigo from sending keys that match the supplied regular expressions in process environment data, HTTP headers, payloads and queries. All regular expressions are case-insensitive. The "magic" value `all` will redact everything. By default, Lumigo applies the following regular expressions: `[".*pass.*", ".*key.*", ".*secret.*", ".*credential.*", ".*passphrase.*"]`. More fine-grained settings can be applied via the following environment variables, which will override `LUMIGO_SECRET_MASKING_REGEX` for a specific type of data: diff --git a/package-lock.json b/package-lock.json index 2778a357..c82b0534 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "jest-chain": "^1.1.6", "jest-expect-message": "^1.1.3", "jest-json": "^2.0.0", + "jest-mock-console": "^2.0.0", "jest-summarizing-reporter": "^1.1.4", "log-timestamp": "^0.3.0", "mock-fs": "^5.2.0", @@ -8931,6 +8932,15 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-mock-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jest-mock-console/-/jest-mock-console-2.0.0.tgz", + "integrity": "sha512-7zrKtXVut+6doalosFxw/2O9spLepQJ9VukODtyLIub2fFkWKe1TyQrxr/GyQogTQcdkHfhvFJdx1OEzLqf/mw==", + "dev": true, + "peerDependencies": { + "jest": ">= 22.4.2" + } + }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", @@ -13369,6 +13379,7 @@ }, "node_modules/npm-install-peers/node_modules/npm/node_modules/lodash._baseindexof": { "version": "3.1.0", + "dev": true, "inBundle": true, "license": "MIT" }, @@ -13384,16 +13395,19 @@ }, "node_modules/npm-install-peers/node_modules/npm/node_modules/lodash._bindcallback": { "version": "3.0.1", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm-install-peers/node_modules/npm/node_modules/lodash._cacheindexof": { "version": "3.0.2", + "dev": true, "inBundle": true, "license": "MIT" }, "node_modules/npm-install-peers/node_modules/npm/node_modules/lodash._createcache": { "version": "3.1.2", + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -13408,6 +13422,7 @@ }, "node_modules/npm-install-peers/node_modules/npm/node_modules/lodash._getnative": { "version": "3.9.1", + "dev": true, "inBundle": true, "license": "MIT" }, @@ -13425,6 +13440,7 @@ }, "node_modules/npm-install-peers/node_modules/npm/node_modules/lodash.restparam": { "version": "3.6.1", + "dev": true, "inBundle": true, "license": "MIT" }, @@ -27192,6 +27208,13 @@ "jest-util": "^29.4.1" } }, + "jest-mock-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jest-mock-console/-/jest-mock-console-2.0.0.tgz", + "integrity": "sha512-7zrKtXVut+6doalosFxw/2O9spLepQJ9VukODtyLIub2fFkWKe1TyQrxr/GyQogTQcdkHfhvFJdx1OEzLqf/mw==", + "dev": true, + "requires": {} + }, "jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", @@ -32208,7 +32231,8 @@ }, "lodash._baseindexof": { "version": "3.1.0", - "bundled": true + "bundled": true, + "dev": true }, "lodash._baseuniq": { "version": "4.6.0", @@ -32221,15 +32245,18 @@ }, "lodash._bindcallback": { "version": "3.0.1", - "bundled": true + "bundled": true, + "dev": true }, "lodash._cacheindexof": { "version": "3.0.2", - "bundled": true + "bundled": true, + "dev": true }, "lodash._createcache": { "version": "3.1.2", "bundled": true, + "dev": true, "requires": { "lodash._getnative": "^3.0.0" } @@ -32241,7 +32268,8 @@ }, "lodash._getnative": { "version": "3.9.1", - "bundled": true + "bundled": true, + "dev": true }, "lodash._root": { "version": "3.0.1", @@ -32255,7 +32283,8 @@ }, "lodash.restparam": { "version": "3.6.1", - "bundled": true + "bundled": true, + "dev": true }, "lodash.union": { "version": "4.6.0", diff --git a/package.json b/package.json index d473136b..6ec3639a 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "jest-chain": "^1.1.6", "jest-expect-message": "^1.1.3", "jest-json": "^2.0.0", + "jest-mock-console": "^2.0.0", "jest-summarizing-reporter": "^1.1.4", "log-timestamp": "^0.3.0", "mock-fs": "^5.2.0", diff --git a/src/exporters/FileSpanExporter.test.ts b/src/exporters/FileSpanExporter.test.ts index 5bd43410..2db6ccb0 100644 --- a/src/exporters/FileSpanExporter.test.ts +++ b/src/exporters/FileSpanExporter.test.ts @@ -1,6 +1,7 @@ import { BasicTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { Span, SpanKind } from '@opentelemetry/api'; import mock from 'mock-fs'; +import mockConsole from 'jest-mock-console'; import { FileSpanExporter } from './index'; @@ -51,6 +52,64 @@ describe('FileSpanExporter tests', () => { ); }); + test('should write one span to console.log', async () => { + const restoreConsole = mockConsole(); + try { + const exporterUnderTest = new FileSpanExporter('console:log'); + + const provider = new BasicTracerProvider(); + provider.addSpanProcessor(new SimpleSpanProcessor(exporterUnderTest)); + + const root: Span = provider.getTracer('default').startSpan('root'); + root.setAttribute('foo', 'bar'); + root.end(); + + await provider.shutdown(); + + expect(console.log).toHaveBeenCalledTimes(1); + const actualSpan = console.log.mock.calls[0][0]; + + expect(JSON.parse(actualSpan)).toEqual( + expect.objectContaining({ + name: 'root', + attributes: { foo: 'bar' }, + kind: SpanKind.INTERNAL, + }) + ); + } finally { + restoreConsole(); + } + }); + + test('should write one span to console error', async () => { + const restoreConsole = mockConsole(); + try { + const exporterUnderTest = new FileSpanExporter('console:error'); + + const provider = new BasicTracerProvider(); + provider.addSpanProcessor(new SimpleSpanProcessor(exporterUnderTest)); + + const root: Span = provider.getTracer('default').startSpan('root'); + root.setAttribute('foo', 'bar'); + root.end(); + + await provider.shutdown(); + + expect(console.error).toHaveBeenCalledTimes(1); + const actualSpan = console.error.mock.calls[0][0]; + + expect(JSON.parse(actualSpan)).toEqual( + expect.objectContaining({ + name: 'root', + attributes: { foo: 'bar' }, + kind: SpanKind.INTERNAL, + }) + ); + } finally { + restoreConsole(); + } + }); + test('should write two spans to file', async () => { const tmpFile = './test-spans.json'; diff --git a/src/exporters/FileSpanExporter.ts b/src/exporters/FileSpanExporter.ts index a9fd3809..467be032 100644 --- a/src/exporters/FileSpanExporter.ts +++ b/src/exporters/FileSpanExporter.ts @@ -31,6 +31,9 @@ import { logger } from '../logging'; * exporter in production. */ +const PRINT_SPANS_TO_CONSOLE_LOG = 'console:log'; +const PRINT_SPANS_TO_CONSOLE_ERROR = 'console:error'; + /* eslint-disable no-console */ export class FileSpanExporter implements SpanExporter { private readonly file: string; @@ -39,8 +42,11 @@ export class FileSpanExporter implements SpanExporter { constructor(file: string) { this.file = file; - this._fd = openSync(file, 'w'); - this._shutdownOnce = new BindOnceFuture(this._shutdown.bind(this), this); + + if (![PRINT_SPANS_TO_CONSOLE_LOG, PRINT_SPANS_TO_CONSOLE_ERROR].includes(file)) { + this._fd = openSync(file, 'w'); + this._shutdownOnce = new BindOnceFuture(this._shutdown.bind(this), this); + } } /** @@ -59,7 +65,13 @@ export class FileSpanExporter implements SpanExporter { spans.map((span) => JSON.stringify(this._exportInfo(span), undefined, 0)).join('\n') + '\n'; try { - appendFileSync(this._fd, spansJson); + if (this._fd) { + appendFileSync(this._fd, spansJson); + } else if (this.file === PRINT_SPANS_TO_CONSOLE_LOG) { + console.log(spansJson); + } else if (this.file === PRINT_SPANS_TO_CONSOLE_ERROR) { + console.error(spansJson); + } } catch (err) { return resultCallback({ code: ExportResultCode.FAILED, @@ -103,7 +115,7 @@ export class FileSpanExporter implements SpanExporter { * Shutdown the exporter. */ shutdown(): Promise { - return this._shutdownOnce.call(); + return this._shutdownOnce?.call(); } /** @@ -136,12 +148,14 @@ export class FileSpanExporter implements SpanExporter { private _flushAll = async (): Promise => new Promise((resolve, reject) => { - try { - fsyncSync(this._fd); - } catch (err) { - logger.error(`An error occured while flushing the spandump to file '${this.file}'`, err); - reject(err); - return; + if (this._fd) { + try { + fsyncSync(this._fd); + } catch (err) { + logger.error(`An error occured while flushing the spandump to file '${this.file}'`, err); + reject(err); + return; + } } resolve();