diff --git a/packages/node-opentelemetry/package.json b/packages/node-opentelemetry/package.json index f62947b..caa4436 100644 --- a/packages/node-opentelemetry/package.json +++ b/packages/node-opentelemetry/package.json @@ -34,6 +34,7 @@ "dependencies": { "@hyperdx/instrumentation-exception": "^0.1.0", "@hyperdx/instrumentation-sentry-node": "^0.1.0", + "@loglayer/transport": "^2.2.0", "@opentelemetry/api": "^1.8.0", "@opentelemetry/api-logs": "^0.57.2", "@opentelemetry/auto-instrumentations-node": "^0.56.1", @@ -75,6 +76,7 @@ "ioredis": "^5.4.1", "knex": "^3.1.0", "koa": "^2.15.3", + "loglayer": "^6.4.2", "mongodb": "^6.6.2", "mongoose": "^6.12.8", "mysql": "^2.18.1", diff --git a/packages/node-opentelemetry/src/logger.ts b/packages/node-opentelemetry/src/logger.ts index 2cd3c30..81fe5b6 100644 --- a/packages/node-opentelemetry/src/logger.ts +++ b/packages/node-opentelemetry/src/logger.ts @@ -1,3 +1,4 @@ +import type { LogLevelType } from '@loglayer/transport'; import opentelemetry, { diag } from '@opentelemetry/api'; import { @@ -5,6 +6,8 @@ import { DEFAULT_HDX_NODE_BETA_MODE, DEFAULT_SERVICE_NAME, } from './constants'; +import type { HyperDXLogLayerOptions } from './otel-logger/loglayer'; +import HyperDXLogLayer from './otel-logger/loglayer'; import type { HyperDXPinoOptions } from './otel-logger/pino'; import * as HyperDXPino from './otel-logger/pino'; import type { HyperDXWinstonOptions } from './otel-logger/winston'; @@ -20,6 +23,11 @@ type PinotTransportOptions = Omit< 'apiKey' | 'getCustomMeta' | 'resourceAttributes' >; +type LogLayerTransportOptions = Omit< + HyperDXLogLayerOptions, + 'apiKey' | 'getCustomMeta' | 'resourceAttributes' +>; + const getCustomMeta = () => { //@ts-ignore const contextManager = opentelemetry.context?._getContextManager(); @@ -79,3 +87,22 @@ export const getPinoTransport = ( level: maxLevel, }; }; + +export const getLogLayerTransport = ( + maxLevel: LogLevelType = 'info', + options: LogLayerTransportOptions = {}, +) => { + diag.debug('Initializing LogLayer transport'); + const apiKey = DEFAULT_HDX_API_KEY(); + return new HyperDXLogLayer({ + ...(apiKey && { + headers: { + Authorization: apiKey, + }, + }), + service: DEFAULT_SERVICE_NAME(), + getCustomMeta: DEFAULT_HDX_NODE_BETA_MODE() ? getCustomMeta : () => ({}), + maxLevel, + ...options, + }); +}; diff --git a/packages/node-opentelemetry/src/otel-logger/__tests__/loglayer.test.ts b/packages/node-opentelemetry/src/otel-logger/__tests__/loglayer.test.ts new file mode 100644 index 0000000..4893efa --- /dev/null +++ b/packages/node-opentelemetry/src/otel-logger/__tests__/loglayer.test.ts @@ -0,0 +1,63 @@ +import { LogLayer, LogLevel } from 'loglayer'; + +import { getLogLayerTransport } from '../../logger'; + +describe('LogLayer transport', () => { + it('should initialize and send logs', () => { + const logger = new LogLayer({ + transport: getLogLayerTransport(), + }); + + // Test basic logging + logger.info('test message'); + logger.error('error message'); + logger.warn('warning message'); + logger.debug('debug message'); + }); + + it('should handle metadata', () => { + const logger = new LogLayer({ + transport: getLogLayerTransport(), + }); + + // Test with metadata + logger.withMetadata({ service: 'test' }).info('message with metadata'); + + // Test with multiple metadata calls + logger + .withMetadata({ service: 'test' }) + .withMetadata({ requestId: '123' }) + .info('message with multiple metadata'); + }); + + it('should handle transport cleanup', () => { + const logger = new LogLayer({ + transport: getLogLayerTransport(), + }); + + // Send some logs + logger.info('test message'); + + // Clean up the transport + logger.withFreshTransports([]); + + // This should not throw + logger.info('message after cleanup'); + }); + + it('should respect maxLevel', () => { + const logger = new LogLayer({ + transport: getLogLayerTransport(LogLevel.warn), + }); + + // These should be filtered out + logger.trace('trace message'); + logger.debug('debug message'); + logger.info('info message'); + + // These should be sent + logger.warn('warning message'); + logger.error('error message'); + logger.fatal('fatal message'); + }); +}); diff --git a/packages/node-opentelemetry/src/otel-logger/loglayer.ts b/packages/node-opentelemetry/src/otel-logger/loglayer.ts new file mode 100644 index 0000000..bbd8de4 --- /dev/null +++ b/packages/node-opentelemetry/src/otel-logger/loglayer.ts @@ -0,0 +1,100 @@ +import { + LoggerlessTransport, + type LogLayerTransportParams, + LogLevel, + type LogLevelType, +} from '@loglayer/transport'; +import { Attributes, diag } from '@opentelemetry/api'; + +import type { LoggerOptions } from './'; +import { Logger } from './'; + +// Map LogLayer levels to their numeric values for comparison +const LOG_LEVEL_PRIORITY = { + [LogLevel.trace]: 0, + [LogLevel.debug]: 1, + [LogLevel.info]: 2, + [LogLevel.warn]: 3, + [LogLevel.error]: 4, + [LogLevel.fatal]: 5, +}; + +export type HyperDXLogLayerOptions = LoggerOptions & { + apiKey?: string; + getCustomMeta?: () => Attributes; + maxLevel?: LogLevelType; +}; + +export default class HyperDXLogLayer + extends LoggerlessTransport + implements Disposable +{ + private readonly logger: Logger; + private readonly getCustomMeta: (() => Attributes) | undefined; + private readonly maxLevel: LogLevelType; + private isDisposed = false; + + constructor({ + getCustomMeta, + apiKey, + maxLevel = 'info', + ...options + }: HyperDXLogLayerOptions) { + diag.debug('Initializing HyperDX LogLayer transport...'); + super({ level: maxLevel }); + this.getCustomMeta = getCustomMeta; + this.maxLevel = maxLevel; + this.logger = new Logger({ + ...(apiKey && { + headers: { + Authorization: apiKey, + }, + }), + ...options, + }); + diag.debug('HyperDX LogLayer transport initialized!'); + } + + shipToLogger({ + logLevel, + messages, + data, + hasData, + }: LogLayerTransportParams): string[] { + if (this.isDisposed) return messages; + + // Skip logs below maxLevel + if (LOG_LEVEL_PRIORITY[logLevel] < LOG_LEVEL_PRIORITY[this.maxLevel]) { + return messages; + } + + diag.debug('Received log from LogLayer'); + const message = messages.join(' '); + const meta = { + ...(data && hasData ? data : {}), + ...this.getCustomMeta?.(), + }; + + diag.debug('Sending log to HyperDX'); + this.logger.postMessage(logLevel, message, meta); + diag.debug('Log sent to HyperDX'); + + return messages; + } + + [Symbol.dispose](): void { + if (this.isDisposed) return; + + diag.debug('Closing HyperDX LogLayer transport...'); + this.logger + .shutdown() + .then(() => { + diag.debug('HyperDX LogLayer transport closed!'); + this.isDisposed = true; + }) + .catch((err) => { + console.error('Error closing HyperDX LogLayer transport:', err); + this.isDisposed = true; + }); + } +} diff --git a/yarn.lock b/yarn.lock index 587911b..7a31083 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2698,6 +2698,32 @@ methods "^1.1.2" path-to-regexp "^6.2.1" +"@loglayer/context-manager@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@loglayer/context-manager/-/context-manager-1.1.0.tgz#3dab8b29b55838e2941a69aa20ec4dfbf39bb963" + integrity sha512-kTJ7TgItEvHuQWgGybsCs4TLyUxR4+Jdqq+sj6EWNd8tJVFGZJ/NolfkixZUNNWDcB72+P6oU8TcsnSSLoJT/Q== + dependencies: + "@loglayer/shared" "2.3.0" + +"@loglayer/plugin@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@loglayer/plugin/-/plugin-2.1.0.tgz#76dd34f9addd5f8706c89674bcf993e1789f6b8a" + integrity sha512-MGAjkWPBjChf8B9kDfNy8qkJ/RK0KIaK6m77JkR6aqr2xF4mTV4HhTYom9nM5iq6TJW36PXNWRV5CWDh9HTZQA== + dependencies: + "@loglayer/shared" "2.3.0" + +"@loglayer/shared@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@loglayer/shared/-/shared-2.3.0.tgz#2a4ed585a44ef446fe621ddd309a2508cf4c2d59" + integrity sha512-jBg8RC13ytx7ZfWOAdlYOt61T2GrbsJpNDrAmuK/flOhNW30OENn21l7Ns28L986QfTRPJyVtKxe5lqFN2w3vg== + +"@loglayer/transport@2.2.0", "@loglayer/transport@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@loglayer/transport/-/transport-2.2.0.tgz#a9c880ae02799749325bdb318a3155ca294a5e6e" + integrity sha512-X6j9o9Q+xuGL3HnNu+dcAlBgf+9LBswsVK9MOKQJdOSCwQZpSrEBilaZdecuLhoczZsqIu11cbRXzAsbFj06sA== + dependencies: + "@loglayer/shared" "2.3.0" + "@lukeed/csprng@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe" @@ -4078,13 +4104,12 @@ lru_map "^0.3.3" tslib "^1.9.3" -"@sentry/types-v7@npm:@sentry/types@7.x", "@sentry/types@7.116.0": - name "@sentry/types-v7" +"@sentry/types-v7@npm:@sentry/types@7.x": version "7.116.0" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.116.0.tgz#0be3434e7e53c86db4993e668af1c3a65bfb7519" integrity sha512-QCCvG5QuQrwgKzV11lolNQPP2k67Q6HHD9vllZ/C4dkxkjoIym8Gy+1OgAN3wjsR0f/kG9o5iZyglgNpUVRapQ== -"@sentry/types-v8@npm:@sentry/types@8.x", "@sentry/types@8.7.0", "@sentry/types@^8.7.0": +"@sentry/types-v8@npm:@sentry/types@8.x": version "8.7.0" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.7.0.tgz#92731af32318d6abb8759216cf6c3c5035894e6e" integrity sha512-11KLOKumP6akugVGLvSoEig+JlP0ZEzW3nN9P+ppgdIx9HAxMIh6UvumbieG4/DWjAh2kh6NPNfUw3gk2Gfq1A== @@ -4094,6 +4119,16 @@ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.19.7.tgz#c6b337912e588083fc2896eb012526cf7cfec7c7" integrity sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg== +"@sentry/types@7.116.0": + version "7.116.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.116.0.tgz#0be3434e7e53c86db4993e668af1c3a65bfb7519" + integrity sha512-QCCvG5QuQrwgKzV11lolNQPP2k67Q6HHD9vllZ/C4dkxkjoIym8Gy+1OgAN3wjsR0f/kG9o5iZyglgNpUVRapQ== + +"@sentry/types@8.7.0", "@sentry/types@^8.7.0": + version "8.7.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.7.0.tgz#92731af32318d6abb8759216cf6c3c5035894e6e" + integrity sha512-11KLOKumP6akugVGLvSoEig+JlP0ZEzW3nN9P+ppgdIx9HAxMIh6UvumbieG4/DWjAh2kh6NPNfUw3gk2Gfq1A== + "@sentry/utils@6.19.7": version "6.19.7" resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.19.7.tgz#6edd739f8185fd71afe49cbe351c1bbf5e7b7c79" @@ -10307,6 +10342,16 @@ logform@^2.3.2, logform@^2.4.0: safe-stable-stringify "^2.3.1" triple-beam "^1.3.0" +loglayer@^6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/loglayer/-/loglayer-6.4.2.tgz#fc74389f70fdd3e074b225c48d2583ffb642c179" + integrity sha512-t5L6Nh7uH+f7GGli9UJa2krvzblkiVzllf6+/h5xa26Gr60yDj3lMGH3ZoLyFZjMBbNtpsDs3UVeA5dVl8M2CQ== + dependencies: + "@loglayer/context-manager" "1.1.0" + "@loglayer/plugin" "2.1.0" + "@loglayer/shared" "2.3.0" + "@loglayer/transport" "2.2.0" + long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" @@ -13272,7 +13317,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -13342,7 +13396,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -14411,7 +14472,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -14429,6 +14490,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"