UNPKG

log4js-appender-cloudwatch

Version:
207 lines (206 loc) 7.02 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfigError = exports.CloudwatchAppender = exports.LogBuffer = void 0; exports.createLogEventHandler = createLogEventHandler; exports.configure = configure; const client_cloudwatch_logs_1 = require("@aws-sdk/client-cloudwatch-logs"); const log4js_layout_json_1 = require("log4js-layout-json"); class LogBuffer { _config; _onReleaseCallback; _timer; _logs; constructor(_config, _onReleaseCallback) { this._config = _config; this._onReleaseCallback = _onReleaseCallback; this._timer = null; this._logs = []; } /** * Pushes a log message to the internal log buffer. * * @param message - The log message to be pushed. If it's an * object, it will be converted to a JSON string. * f * @param timestamp - The timestamp of the log * message. If not provided, the current timestamp will be used. */ push(message, timestamp) { if (typeof message === "object") { message = JSON.stringify(message); } if (timestamp === undefined) { timestamp = Date.now(); } this._logs.push({ message: message, timestamp: timestamp, }); if (this._logs.length >= this._config.batchSize) { this.release(); return; } if (this._timer === null) { this._timer = globalThis.setTimeout(() => { this.release(); }, this._config.bufferTimeout); return; } } /** * Releases the logs and clears the timer. */ release() { if (this._timer) { globalThis.clearTimeout(this._timer); this._timer = null; } this._onReleaseCallback([...this._logs]); this._logs = []; } } exports.LogBuffer = LogBuffer; class CloudwatchAppender { _config; _layout; _logEventBuffer; _cloudwatchClient; constructor(_config, _layout, _logEventBuffer, _cloudwatchClient) { this._config = _config; this._layout = _layout; this._logEventBuffer = _logEventBuffer; this._cloudwatchClient = _cloudwatchClient; if (this._config.createResources) { this.createLogGroups(); } else { this.initializeLogGroups(); } } /** * Creates log groups and streams in CloudWatch if they don't exist. * If resources already exist, the creation requests are silently ignored. */ createLogGroups() { this._cloudwatchClient .createLogGroup({ logGroupName: this._config.logGroupName, }) .catch((error) => { if (error instanceof client_cloudwatch_logs_1.ResourceAlreadyExistsException) { // NOTE: continue, nothing to do } }) .finally(() => { this._cloudwatchClient .createLogStream({ logGroupName: this._config.logGroupName, logStreamName: this._config.logStreamName, }) .catch((error) => { if (error instanceof client_cloudwatch_logs_1.ResourceAlreadyExistsException) { // NOTE: continue, nothing to do } }); }); } /** * Verifies that the configured log groups and streams exist in CloudWatch. * * @throws {ConfigError} If log group/stream doesn't exist or credentials are invalid */ initializeLogGroups() { this._cloudwatchClient .describeLogGroups({ logGroupNamePrefix: this._config.logGroupName, }) .then((group) => { if (group.logGroups?.length === 0) { throw new ConfigError("Log group doesn't exist"); } }) .catch((error) => { if (error instanceof client_cloudwatch_logs_1.ResourceNotFoundException) { // TODO: handle error } }); this._cloudwatchClient .describeLogStreams({ logGroupName: this._config.logGroupName, logStreamNamePrefix: this._config.logStreamName, }) .then((streams) => { if (streams.logStreams?.length) { const streamExists = streams .logStreams .find((s) => s.logStreamName === this._config.logStreamName); if (!streamExists) { throw new ConfigError("Stream name doesn't exist"); } } }) .catch((error) => { if (error instanceof client_cloudwatch_logs_1.UnrecognizedClientException) { throw new ConfigError("Invalid credentials"); } if (error instanceof client_cloudwatch_logs_1.ResourceNotFoundException) { // TODO: handle error } throw error; }); } /** * Returns the appender function that will be used by log4js. * * The function processes logging events by formatting them using the configured layout * and buffering them for batch processing. * * @returns {log4js.AppenderFunction} The function that will handle logging events */ appenderFunction() { return (loggingEvent) => { const message = this._layout(loggingEvent); const time = loggingEvent.startTime.getTime(); this._logEventBuffer.push(message, time); }; } } exports.CloudwatchAppender = CloudwatchAppender; class ConfigError extends Error { constructor(msg, cause) { super(msg); this.cause = cause; this.name = this.constructor.name; } } exports.ConfigError = ConfigError; function createLogEventHandler(cloudwatchClient, config) { return function handleLogRelease(logEventBatch) { cloudwatchClient.putLogEvents({ logEvents: logEventBatch, logGroupName: config.logGroupName, logStreamName: config.logStreamName, }); }; } function configure(config, layouts, _findAppender, _levels) { const cloudwatchClient = new client_cloudwatch_logs_1.CloudWatchLogs({ region: config.region, credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey, sessionToken: config.sessionToken, }, }); const buffer = new LogBuffer(config, createLogEventHandler(cloudwatchClient, config)); let layout; if (config.layout) { // @ts-ignore: bad typings "config: PatternToken" layout = layouts.layout(config.layout.type, config.layout); } else { layout = (0, log4js_layout_json_1.layout)(); } const appender = new CloudwatchAppender(config, layout, buffer, cloudwatchClient); return appender.appenderFunction; }