diff --git a/.changeset/mighty-humans-sparkle.md b/.changeset/mighty-humans-sparkle.md new file mode 100644 index 00000000..139f60ba --- /dev/null +++ b/.changeset/mighty-humans-sparkle.md @@ -0,0 +1,5 @@ +--- +'@hyperdx/node-opentelemetry': minor +--- + +feat: use getSeverityNumber and parseLogAttributes for logger diff --git a/packages/node-opentelemetry/src/otel-logger/__tests__/__snapshots__/index.test.ts.snap b/packages/node-opentelemetry/src/otel-logger/__tests__/__snapshots__/index.test.ts.snap new file mode 100644 index 00000000..413d417d --- /dev/null +++ b/packages/node-opentelemetry/src/otel-logger/__tests__/__snapshots__/index.test.ts.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`otel-logger getSeverityNumber 1`] = ` +{ + "alert": 22, + "debug": 5, + "emerg": 23, + "error": 17, + "info": 9, + "verbose": 6, + "warn": 13, + "warning": 13, +} +`; + +exports[`otel-logger parseLogAttributes 1`] = ` +{ + "a": 1, + "b": "2", + "c": "[{"d":3}]", + "e": [ + 1, + 2, + 3, + ], + "f": [ + "a", + "b", + "c", + ], + "g": { + "h": "i", + }, +} +`; diff --git a/packages/node-opentelemetry/src/otel-logger/__tests__/index.test.ts b/packages/node-opentelemetry/src/otel-logger/__tests__/index.test.ts new file mode 100644 index 00000000..e0c0d6c2 --- /dev/null +++ b/packages/node-opentelemetry/src/otel-logger/__tests__/index.test.ts @@ -0,0 +1,31 @@ +import { getSeverityNumber, parseLogAttributes } from '..'; + +describe('otel-logger', () => { + it('getSeverityNumber', () => { + expect({ + alert: getSeverityNumber('alert'), + debug: getSeverityNumber('debug'), + emerg: getSeverityNumber('emerg'), + error: getSeverityNumber('error'), + info: getSeverityNumber('info'), + verbose: getSeverityNumber('verbose'), + warn: getSeverityNumber('warn'), + warning: getSeverityNumber('warning'), + }).toMatchSnapshot(); + }); + + it('parseLogAttributes', () => { + expect( + parseLogAttributes({ + a: 1, + b: '2', + c: [{ d: 3 }], + e: [1, 2, 3], + f: ['a', 'b', 'c'], + g: { + h: 'i', + }, + }), + ).toMatchSnapshot(); + }); +}); diff --git a/packages/node-opentelemetry/src/otel-logger/index.ts b/packages/node-opentelemetry/src/otel-logger/index.ts index cf6ea365..544239f9 100644 --- a/packages/node-opentelemetry/src/otel-logger/index.ts +++ b/packages/node-opentelemetry/src/otel-logger/index.ts @@ -1,5 +1,10 @@ import { Attributes, diag } from '@opentelemetry/api'; -import { Logger as OtelLogger, logs } from '@opentelemetry/api-logs'; +import { + LogAttributes, + Logger as OtelLogger, + logs, + SeverityNumber, +} from '@opentelemetry/api-logs'; import { getEnvWithoutDefaults } from '@opentelemetry/core'; import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'; import { @@ -16,6 +21,7 @@ import { NoopLogRecordProcessor, } from '@opentelemetry/sdk-logs'; import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions'; +import isPlainObject from 'lodash.isplainobject'; import { version as PKG_VERSION } from '../../package.json'; import { @@ -26,6 +32,7 @@ import { DEFAULT_SEND_INTERVAL_MS, DEFAULT_SERVICE_NAME, } from '../constants'; +import { jsonToString } from '../utils'; const LOG_PREFIX = `⚠️ [LOGGER]`; @@ -41,6 +48,73 @@ export type LoggerOptions = { timeout?: number; // The read/write/connection timeout in milliseconds }; +// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/afccd0d62a0ea81afb8f5609f3ee802c038d11c6/packages/winston-transport/src/utils.ts +const npmLevels: Record = { + error: SeverityNumber.ERROR, + warn: SeverityNumber.WARN, + info: SeverityNumber.INFO, + http: SeverityNumber.DEBUG3, + verbose: SeverityNumber.DEBUG2, + debug: SeverityNumber.DEBUG, + silly: SeverityNumber.TRACE, +}; + +const sysLoglevels: Record = { + emerg: SeverityNumber.FATAL3, + alert: SeverityNumber.FATAL2, + crit: SeverityNumber.FATAL, + error: SeverityNumber.ERROR, + warning: SeverityNumber.WARN, + notice: SeverityNumber.INFO2, + info: SeverityNumber.INFO, + debug: SeverityNumber.DEBUG, +}; + +const cliLevels: Record = { + error: SeverityNumber.ERROR, + warn: SeverityNumber.WARN, + help: SeverityNumber.INFO3, + data: SeverityNumber.INFO2, + info: SeverityNumber.INFO, + debug: SeverityNumber.DEBUG, + prompt: SeverityNumber.TRACE4, + verbose: SeverityNumber.TRACE3, + input: SeverityNumber.TRACE2, + silly: SeverityNumber.TRACE, +}; + +export function getSeverityNumber(level: string): SeverityNumber | undefined { + return npmLevels[level] ?? sysLoglevels[level] ?? cliLevels[level]; +} + +export const parseLogAttributes = ( + meta: Record, +): LogAttributes => { + try { + const attributes: LogAttributes = {}; + for (const key in meta) { + if (Object.prototype.hasOwnProperty.call(meta, key)) { + const value = meta[key]; + // stringify array of objects + if (Array.isArray(value)) { + const firstItem = value[0]; + if (isPlainObject(firstItem)) { + attributes[key] = jsonToString(value); + } + } + + if (attributes[key] === undefined) { + attributes[key] = value; + } + } + } + return attributes; + } catch (error) { + diag.error(`${LOG_PREFIX} Failed to parse log attributes. e = ${error}`); + return meta; + } +}; + export class Logger { private readonly _url: string; @@ -155,15 +229,18 @@ export class Logger { return this.processor.forceFlush(); } - postMessage(level: string, body: string, attributes: Attributes = {}): void { + postMessage( + level: string, + body: string, + meta: Record = {}, + ): void { this.logger.emit({ - // TODO: should map to otel severity number - severityNumber: 0, + severityNumber: getSeverityNumber(level), // TODO: set up the mapping between different downstream log levels severityText: level, body, - attributes, - timestamp: this.parseTimestamp(attributes), + attributes: parseLogAttributes(meta), + timestamp: this.parseTimestamp(meta), }); } }