mirror of
https://gitlab.com/Zer0interrupt/recipe-manager.git
synced 2026-11-02 07:28:25 -05:00
- logger.js: Pino JSON logger with console-style helpers and pino-http request logging (static assets skipped, headers not logged, secrets redacted) - server.js: replace 89 console.* calls with log.info/warn/error; add request logging middleware - docker-compose.yml: json-file rotation (10m x 5, compressed), LOG_LEVEL, 'forkfolio' log tag - observability/: standalone OpenObserve + Fluent Bit stack routing tagged container logs to per-app streams - .dockerignore: exclude observability stack from app image
89 lines
2.9 KiB
JavaScript
89 lines
2.9 KiB
JavaScript
/**
|
|
* Centralized structured logging for ForkFolio.
|
|
*
|
|
* - Emits newline-delimited JSON to stdout (collected by Docker, rotated by the
|
|
* json-file log driver, and forwarded to OpenObserve by Fluent Bit).
|
|
* - `log.*` helpers accept console-style arguments, so existing
|
|
* `console.error('Message:', err)` call sites translate 1:1 while Error
|
|
* objects are serialized with their stack trace under the `err` key.
|
|
* - `httpLogger` is Express middleware that logs one line per HTTP request.
|
|
*
|
|
* Environment variables:
|
|
* LOG_LEVEL trace | debug | info | warn | error | fatal | silent (default: info)
|
|
*/
|
|
const crypto = require('crypto');
|
|
const util = require('util');
|
|
const pino = require('pino');
|
|
const pinoHttp = require('pino-http');
|
|
|
|
const logger = pino({
|
|
level: process.env.LOG_LEVEL || 'info',
|
|
base: { app: 'forkfolio' },
|
|
timestamp: pino.stdTimeFunctions.isoTime,
|
|
formatters: {
|
|
// Use readable level names ("info") instead of numeric levels (30)
|
|
level: (label) => ({ level: label })
|
|
},
|
|
redact: {
|
|
paths: ['password', '*.password', 'token', '*.token', 'apiKey', '*.apiKey'],
|
|
censor: '[REDACTED]'
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Builds a console-compatible log function for the given pino level.
|
|
* The first Error argument is attached as `err`; remaining arguments are
|
|
* formatted into the message exactly like console.log would format them.
|
|
*/
|
|
function consoleStyle(level) {
|
|
return (...args) => {
|
|
const err = args.find((a) => a instanceof Error);
|
|
const rest = err ? args.filter((a) => a !== err) : args;
|
|
const msg = util.format(...rest);
|
|
if (err) {
|
|
// Drop the trailing "Message:" colon since the error is attached separately
|
|
logger[level]({ err }, msg.replace(/:\s*$/, ''));
|
|
} else {
|
|
logger[level](msg);
|
|
}
|
|
};
|
|
}
|
|
|
|
const log = {
|
|
debug: consoleStyle('debug'),
|
|
info: consoleStyle('info'),
|
|
warn: consoleStyle('warn'),
|
|
error: consoleStyle('error')
|
|
};
|
|
|
|
// Static asset requests are high-volume and low-value; skip them in request logs
|
|
const STATIC_ASSET_RE = /\.(?:js|css|png|jpe?g|gif|svg|ico|webp|woff2?|ttf|map|json|html)$/i;
|
|
|
|
const httpLogger = pinoHttp({
|
|
logger,
|
|
genReqId: (req, res) => {
|
|
const id = req.headers['x-request-id'] || crypto.randomUUID();
|
|
res.setHeader('X-Request-Id', id);
|
|
return id;
|
|
},
|
|
autoLogging: {
|
|
ignore: (req) => STATIC_ASSET_RE.test((req.url || '').split('?')[0])
|
|
},
|
|
customLogLevel: (req, res, err) => {
|
|
if (err || res.statusCode >= 500) return 'error';
|
|
if (res.statusCode >= 400) return 'warn';
|
|
return 'info';
|
|
},
|
|
// Keep request logs compact and never record headers (Authorization, cookies)
|
|
serializers: {
|
|
req: (req) => ({
|
|
id: req.id,
|
|
method: req.method,
|
|
url: req.url,
|
|
ip: (req.headers && req.headers['x-forwarded-for']) || req.remoteAddress
|
|
}),
|
|
res: (res) => ({ statusCode: res.statusCode })
|
|
}
|
|
});
|
|
|
|
module.exports = { logger, log, httpLogger };
|