log4js-appender-cloudwatch
Version:
Log4js cloudwatch appender
207 lines (206 loc) • 7.02 kB
JavaScript
"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;
}