@chargeuk/log4js-appender-cloudwatch
Version:
Log4js cloudwatch appender
182 lines (181 loc) • 6.66 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigError = void 0;
exports.cloudwatch = cloudwatch;
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;
isReady = false;
constructor(config, _onReleaseCallback) {
this.config = config;
this._onReleaseCallback = _onReleaseCallback;
this._timer = null;
this._logs = new Array();
}
/**
* 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.
*
* @param - 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;
}
this.createTimer();
}
createTimer() {
if (this._timer === null) {
this._timer = globalThis.setTimeout(() => {
this.release();
}, this.config.bufferTimeout);
return;
}
}
/**
* Releases the logs and clears the timer.
*/
release() {
try {
if (this._timer) {
globalThis.clearTimeout(this._timer);
this._timer = null;
}
if (this.isReady) {
this._onReleaseCallback([...this._logs]);
this._logs = [];
}
else if (this._logs.length) {
// we are not ready, so create a new timer
this.createTimer();
}
}
catch (error) {
console.error("Failed to release logs", error);
}
}
}
/**
* TODO: create async method for appender creation
* (for now log4js doesn't support async configure module)
*/
function cloudwatch(config, layout) {
const useCredentials = config.accessKeyId || config.secretAccessKey || config.sessionToken;
const cloudWatchOptions = {
region: config.region,
...(config.endpoint && { endpoint: config.endpoint }),
...(useCredentials && { credentials: {
accessKeyId: config.accessKeyId || '',
secretAccessKey: config.secretAccessKey || '',
sessionToken: config.sessionToken,
} }),
};
const cloudwatch = new client_cloudwatch_logs_1.CloudWatchLogs(cloudWatchOptions);
/**
* TODO: integrate limitation to LogBuffer
*
* constraints:
* - The maximum batch size is 1,048,576 bytes. This size is calculated as the sum of all event messages in UTF-8, plus 26 bytes for each log event.
* - None of the log events in the batch can be more than 2 hours in the future.
* - None of the log events in the batch can be more than 14 days in the past. Also, none of the log events can be from earlier than the retention period of the log group.
* - The log events in the batch must be in chronological order by their timestamp. The timestamp is the time that the event occurred, expressed as the number of milliseconds after Jan 1, 1970 00:00:00 UTC. (In Amazon Web Services Tools for PowerShell and the Amazon Web Services SDK for .NET, the timestamp is specified in .NET format: yyyy-mm-ddThh:mm:ss. For example, 2017-09-15T13:45:30.)
* - Each log event can be no larger than 256 KB.
* - A batch of log events in a single request cannot span more than 24 hours. Otherwise, the operation fails.
* - The maximum number of log events in a batch is 10,000.
*/
const buffer = new LogBuffer(config, (logs) => {
cloudwatch.putLogEvents({
logEvents: logs,
logGroupName: config.logGroupName,
logStreamName: config.logStreamName,
});
});
if (config.createResources) {
cloudwatch.createLogGroup({
logGroupName: config.logGroupName,
}).catch((error) => {
if (error.name === "ResourceAlreadyExistsException") {
// TODO: continue or exit
}
}).finally(() => {
cloudwatch.createLogStream({
logGroupName: config.logGroupName,
logStreamName: config.logStreamName,
}).catch((error) => {
if (error.name === "ResourceAlreadyExistsException") {
// TODO: continue or exit
}
}).finally(() => {
buffer.isReady = true;
});
});
}
else {
cloudwatch.describeLogGroups({
logGroupNamePrefix: config.logGroupName,
}).then((group) => {
if (group.logGroups?.length === 0) {
throw new ConfigError("Log group doesn't exists");
}
});
cloudwatch.describeLogStreams({
logGroupName: config.logGroupName,
logStreamNamePrefix: config.logStreamName,
}).then((streams) => {
if (streams.logStreams?.length) {
const stream = streams
.logStreams
.find((s) => s.logStreamName === config.logStreamName);
if (stream === undefined) {
throw new ConfigError("Stream name doesn't exists");
}
}
}).finally(() => {
buffer.isReady = true;
});
}
return function appender(loggingEvent) {
const msg = layout(loggingEvent);
const time = loggingEvent.startTime.getTime();
buffer.push(msg, time);
};
}
class ConfigError extends Error {
constructor(msg, cause) {
super(msg);
this.cause = cause;
this.name = this.constructor.name;
}
}
exports.ConfigError = ConfigError;
function configure(config, layouts, _findAppender, _levels) {
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 = cloudwatch(config, layout);
return appender;
}