97 lines
3.6 KiB
JavaScript
97 lines
3.6 KiB
JavaScript
|
|
"use strict";
|
||
|
|
|
||
|
|
const fs = require("node:fs/promises");
|
||
|
|
const path = require("node:path");
|
||
|
|
|
||
|
|
const { PLUGIN_LOG_MAX_BYTES, PLUGIN_LOG_MAX_FILES } = require("./constants.cjs");
|
||
|
|
const { assertPluginStorageSegment } = require("./paths.cjs");
|
||
|
|
|
||
|
|
const SENSITIVE_KEY = /(?:password|passwd|token|secret|private.?key|credential|authorization|cookie)/iu;
|
||
|
|
|
||
|
|
function redactLogValue(value, key = "", depth = 0) {
|
||
|
|
if (SENSITIVE_KEY.test(key)) return "[REDACTED]";
|
||
|
|
if (depth >= 8) return "[TRUNCATED]";
|
||
|
|
if (value == null || typeof value === "boolean" || typeof value === "number") return value;
|
||
|
|
if (typeof value === "string") return value.slice(0, 2_048);
|
||
|
|
if (Array.isArray(value)) {
|
||
|
|
const output = [];
|
||
|
|
for (let index = 0; index < Math.min(value.length, 64); index += 1) {
|
||
|
|
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
||
|
|
output.push(descriptor && "value" in descriptor
|
||
|
|
? redactLogValue(descriptor.value, "", depth + 1)
|
||
|
|
: "[UNAVAILABLE]");
|
||
|
|
}
|
||
|
|
return output;
|
||
|
|
}
|
||
|
|
if (typeof value === "object") {
|
||
|
|
const output = {};
|
||
|
|
for (const entryKey of Object.keys(value).slice(0, 64)) {
|
||
|
|
if (SENSITIVE_KEY.test(entryKey)) {
|
||
|
|
output[entryKey.slice(0, 128)] = "[REDACTED]";
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const descriptor = Object.getOwnPropertyDescriptor(value, entryKey);
|
||
|
|
output[entryKey.slice(0, 128)] = descriptor && "value" in descriptor
|
||
|
|
? redactLogValue(descriptor.value, entryKey, depth + 1)
|
||
|
|
: "[UNAVAILABLE]";
|
||
|
|
}
|
||
|
|
return output;
|
||
|
|
}
|
||
|
|
return "[UNSUPPORTED]";
|
||
|
|
}
|
||
|
|
|
||
|
|
class PluginLogger {
|
||
|
|
constructor(options) {
|
||
|
|
this.pluginId = assertPluginStorageSegment(options.pluginId, "ID");
|
||
|
|
this.directory = path.join(options.logsDirectory, this.pluginId);
|
||
|
|
this.maxBytes = options.maxBytes ?? PLUGIN_LOG_MAX_BYTES;
|
||
|
|
this.maxFiles = options.maxFiles ?? PLUGIN_LOG_MAX_FILES;
|
||
|
|
this.maxPendingBytes = options.maxPendingBytes ?? Math.min(this.maxBytes, 256 * 1024);
|
||
|
|
this.pendingBytes = 0;
|
||
|
|
this.queue = Promise.resolve();
|
||
|
|
}
|
||
|
|
|
||
|
|
write(level, message, fields) {
|
||
|
|
const record = JSON.stringify({
|
||
|
|
timestamp: new Date().toISOString(),
|
||
|
|
level: ["debug", "info", "warn", "error"].includes(level) ? level : "info",
|
||
|
|
message: String(message ?? "").slice(0, 2_048),
|
||
|
|
...(fields === undefined ? {} : { fields: redactLogValue(fields) }),
|
||
|
|
});
|
||
|
|
const line = `${record}\n`;
|
||
|
|
const bytes = Buffer.byteLength(line);
|
||
|
|
if (this.pendingBytes + bytes > this.maxPendingBytes) return Promise.resolve();
|
||
|
|
this.pendingBytes += bytes;
|
||
|
|
this.queue = this.queue
|
||
|
|
.then(() => this.#append(line))
|
||
|
|
.catch(() => {})
|
||
|
|
.finally(() => { this.pendingBytes -= bytes; });
|
||
|
|
return this.queue;
|
||
|
|
}
|
||
|
|
|
||
|
|
async #append(line) {
|
||
|
|
await fs.mkdir(this.directory, { recursive: true, mode: 0o700 });
|
||
|
|
const activePath = path.join(this.directory, "runtime.log");
|
||
|
|
let size = 0;
|
||
|
|
try { size = (await fs.stat(activePath)).size; } catch {}
|
||
|
|
if (size + Buffer.byteLength(line) > this.maxBytes) await this.#rotate();
|
||
|
|
await fs.appendFile(activePath, line, { encoding: "utf8", mode: 0o600 });
|
||
|
|
}
|
||
|
|
|
||
|
|
async #rotate() {
|
||
|
|
await fs.rm(path.join(this.directory, `runtime.${this.maxFiles}.log`), { force: true });
|
||
|
|
for (let index = this.maxFiles - 1; index >= 1; index -= 1) {
|
||
|
|
try {
|
||
|
|
await fs.rename(
|
||
|
|
path.join(this.directory, index === 1 ? "runtime.log" : `runtime.${index - 1}.log`),
|
||
|
|
path.join(this.directory, `runtime.${index}.log`),
|
||
|
|
);
|
||
|
|
} catch (error) {
|
||
|
|
if (error?.code !== "ENOENT") throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = { PluginLogger, redactLogValue };
|