mirror of
https://github.com/zadam/trilium.git
synced 2025-11-02 20:49:01 +01:00
205 lines
5.6 KiB
TypeScript
205 lines
5.6 KiB
TypeScript
"use strict";
|
|
|
|
import type { Request, Response } from "express";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { EOL } from "os";
|
|
import dataDir from "./data_dir.js";
|
|
import cls from "./cls.js";
|
|
import config, { LOGGING_DEFAULT_RETENTION_DAYS } from "./config.js";
|
|
|
|
if (!fs.existsSync(dataDir.LOG_DIR)) {
|
|
fs.mkdirSync(dataDir.LOG_DIR, 0o700);
|
|
}
|
|
|
|
let logFile: fs.WriteStream | undefined;
|
|
|
|
const SECOND = 1000;
|
|
const MINUTE = 60 * SECOND;
|
|
const HOUR = 60 * MINUTE;
|
|
const DAY = 24 * HOUR;
|
|
|
|
const MINIMUM_FILES_TO_KEEP = 7;
|
|
|
|
let todaysMidnight!: Date;
|
|
|
|
initLogFile();
|
|
|
|
function getTodaysMidnight() {
|
|
const now = new Date();
|
|
|
|
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
}
|
|
|
|
async function cleanupOldLogFiles() {
|
|
try {
|
|
// Get retention days from environment or options
|
|
let retentionDays = LOGGING_DEFAULT_RETENTION_DAYS;
|
|
const customRetentionDays = config.Logging.retentionDays;
|
|
if (customRetentionDays > 0) {
|
|
retentionDays = customRetentionDays;
|
|
} else if (customRetentionDays <= -1){
|
|
info(`Log cleanup: keeping all log files, as specified by configuration.`);
|
|
return
|
|
}
|
|
|
|
const cutoffDate = new Date();
|
|
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
|
|
|
|
// Read all log files
|
|
const files = await fs.promises.readdir(dataDir.LOG_DIR);
|
|
const logFiles: Array<{name: string, mtime: Date, path: string}> = [];
|
|
|
|
for (const file of files) {
|
|
// Security: Only process files matching our log pattern
|
|
if (!/^trilium-\d{4}-\d{2}-\d{2}\.log$/.test(file)) {
|
|
continue;
|
|
}
|
|
|
|
const filePath = path.join(dataDir.LOG_DIR, file);
|
|
|
|
// Security: Verify path stays within LOG_DIR
|
|
const resolvedPath = path.resolve(filePath);
|
|
const resolvedLogDir = path.resolve(dataDir.LOG_DIR);
|
|
if (!resolvedPath.startsWith(resolvedLogDir + path.sep)) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const stats = await fs.promises.stat(filePath);
|
|
logFiles.push({ name: file, mtime: stats.mtime, path: filePath });
|
|
} catch (err) {
|
|
// Skip files we can't stat
|
|
}
|
|
}
|
|
|
|
// Sort by modification time (oldest first)
|
|
logFiles.sort((a, b) => a.mtime.getTime() - b.mtime.getTime());
|
|
|
|
// Keep minimum number of files
|
|
if (logFiles.length <= MINIMUM_FILES_TO_KEEP) {
|
|
return;
|
|
}
|
|
|
|
// Delete old files, keeping minimum
|
|
let deletedCount = 0;
|
|
for (let i = 0; i < logFiles.length - MINIMUM_FILES_TO_KEEP; i++) {
|
|
const file = logFiles[i];
|
|
if (file.mtime < cutoffDate) {
|
|
try {
|
|
await fs.promises.unlink(file.path);
|
|
deletedCount++;
|
|
} catch (err) {
|
|
// Log deletion failed, but continue with others
|
|
}
|
|
}
|
|
}
|
|
|
|
if (deletedCount > 0) {
|
|
info(`Log cleanup: deleted ${deletedCount} old log files`);
|
|
}
|
|
} catch (err) {
|
|
// Cleanup failed, but don't crash the log rotation
|
|
}
|
|
}
|
|
|
|
function initLogFile() {
|
|
todaysMidnight = getTodaysMidnight();
|
|
|
|
const logPath = `${dataDir.LOG_DIR}/trilium-${formatDate()}.log`;
|
|
const isRotating = !!logFile;
|
|
|
|
if (isRotating) {
|
|
logFile!.end();
|
|
}
|
|
|
|
logFile = fs.createWriteStream(logPath, { flags: "a" });
|
|
|
|
// Clean up old log files when rotating to a new file
|
|
if (isRotating) {
|
|
cleanupOldLogFiles().catch(() => {
|
|
// Ignore cleanup errors
|
|
});
|
|
}
|
|
}
|
|
|
|
function checkDate(millisSinceMidnight: number) {
|
|
if (millisSinceMidnight >= DAY) {
|
|
initLogFile();
|
|
|
|
millisSinceMidnight -= DAY;
|
|
}
|
|
|
|
return millisSinceMidnight;
|
|
}
|
|
|
|
function log(str: string | Error) {
|
|
const bundleNoteId = cls.get("bundleNoteId");
|
|
|
|
if (bundleNoteId) {
|
|
str = `[Script ${bundleNoteId}] ${str}`;
|
|
}
|
|
|
|
let millisSinceMidnight = Date.now() - todaysMidnight.getTime();
|
|
|
|
millisSinceMidnight = checkDate(millisSinceMidnight);
|
|
|
|
logFile!.write(`${formatTime(millisSinceMidnight)} ${str}${EOL}`);
|
|
|
|
console.log(str);
|
|
}
|
|
|
|
function info(message: string | Error) {
|
|
log(message);
|
|
}
|
|
|
|
function error(message: string | Error | unknown) {
|
|
log(`ERROR: ${message}`);
|
|
}
|
|
|
|
const requestBlacklist = ["/app", "/images", "/stylesheets", "/api/recent-notes"];
|
|
|
|
function request(req: Request, res: Response, timeMs: number, responseLength: number | string = "?") {
|
|
for (const bl of requestBlacklist) {
|
|
if (req.url.startsWith(bl)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (req.url.includes(".js.map") || req.url.includes(".css.map")) {
|
|
return;
|
|
}
|
|
|
|
info((timeMs >= 10 ? "Slow " : "") + `${res.statusCode} ${req.method} ${req.url} with ${responseLength} bytes took ${timeMs}ms`);
|
|
}
|
|
|
|
function pad(num: number) {
|
|
num = Math.floor(num);
|
|
|
|
return num < 10 ? `0${num}` : num.toString();
|
|
}
|
|
|
|
function padMilli(num: number) {
|
|
if (num < 10) {
|
|
return `00${num}`;
|
|
} else if (num < 100) {
|
|
return `0${num}`;
|
|
} else {
|
|
return num.toString();
|
|
}
|
|
}
|
|
|
|
function formatTime(millisSinceMidnight: number) {
|
|
return `${pad(millisSinceMidnight / HOUR)}:${pad((millisSinceMidnight % HOUR) / MINUTE)}:${pad((millisSinceMidnight % MINUTE) / SECOND)}.${padMilli(millisSinceMidnight % SECOND)}`;
|
|
}
|
|
|
|
function formatDate() {
|
|
return `${pad(todaysMidnight.getFullYear())}-${pad(todaysMidnight.getMonth() + 1)}-${pad(todaysMidnight.getDate())}`;
|
|
}
|
|
|
|
export default {
|
|
info,
|
|
error,
|
|
request
|
|
};
|