improved logging

This commit is contained in:
root committed 2026-10-27 17:55:30 -03:00
1 parent 7657dc6769
commit 25cca2e2df
4 files changed
+101 -19

No files matched your search

+10 -2
View File
@@ -20,12 +20,20 @@ const cache = new ProofCache();
const app = express();
app.use(cors());
app.use(express.json());
/** Only what the caller actually sent; empty sections stay out of the line. */
const received = (req: Request): Record<string, unknown> =>
Object.fromEntries(
Object.entries({ query: req.query, body: req.body }).filter(
([, value]) => typeof value === 'object' && value !== null && Object.keys(value).length > 0,
),
);
if (config.debug) {
// Every incoming request and its outcome — success paths log nothing otherwise.
// Every incoming request, its parameters and its outcome — success paths log nothing otherwise.
app.use((req, res, next) => {
const started = Date.now();
res.on('finish', () =>
log.info(`${req.method} ${req.originalUrl} → ${res.statusCode} (${Date.now() - started} ms)`),
log.info(`${req.method} ${req.originalUrl} → ${res.statusCode} (${Date.now() - started} ms)`, received(req)),
);
next();
});
+1 -2
View File
@@ -17,8 +17,7 @@
"dotenv-flow": "^4.1.0",
"ethers": "^5.8.0",
"express": "^5.2.1",
"simple-oauth2": "^5.1.0",
"tslog": "4"
"simple-oauth2": "^5.1.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
+4 -3
View File
@@ -54,8 +54,8 @@ export const loadPrimusConfig = (): Config['primus'] => ({
wallet: new Wallet(required('PRIVATE_KEY')),
});
export const loadConfig = (): Config => {
const primus = loadPrimusConfig();
/** The BB side alone: enough for the admin console, which must boot without the prover's wallet. */
export const loadBBConfig = (): Omit<Config, 'primus'> => {
const apiUrl = required('ITP_API_URL').replace(/\/+$/, '');
const clientCrtPath = optional('CLIENT_CRT', 'cert.pem');
const clientKeyPath = optional('CLIENT_KEY', 'key.pem');
@@ -75,6 +75,7 @@ export const loadConfig = (): Config => {
// Omitted rather than set to undefined: `ca` is optional, not optional-with-undefined.
...(caPath ? { ca: readPem('BB_CA', caPath) } : {}),
},
primus,
};
};
export const loadConfig = (): Config => ({ ...loadBBConfig(), primus: loadPrimusConfig() });
+86 -12
View File
@@ -1,14 +1,88 @@
import { Logger } from 'tslog';
/**
* The one logger: pretty and colored on a terminal, silent unless DEBUG=true,
* and every credential-looking key masked at any depth — the attestation
* record echoes live `Authorization` headers into the logs.
* The one logger: exactly one line per event —
* `2026-10-27 19:31:33.326 LEVEL message {json}` — colored on a terminal.
* Quiet unless DEBUG=true, except errors and fatal: those always reach stderr.
* Every credential-looking value is masked at any
* depth, in keys and in strings alike: the attestation record echoes live
* `Authorization` headers and app-keyed URLs into the logs.
*/
export const log = new Logger({
name: 'zkpix',
minLevel: /^(1|true)$/i.test(process.env.DEBUG ?? '') ? 'DEBUG' : 'FATAL',
maskValuesOfKeys: ['authorization', 'gw-dev-app-key'],
maskValuesOfKeysCaseInsensitive: true,
maskPlaceholder: '…',
});
type Level = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
const enabled = /^(1|true)$/i.test(process.env.DEBUG ?? '');
const colors: Record<Level, string> = { debug: '90', info: '36', warn: '33', error: '31', fatal: '31;1' };
/** Credential keys masked wherever they appear, however deep. */
const secretKey = /^(?:authorization|gw-dev-app-key)$/i;
/** The same credentials escaped into text: raw header lines, query URLs. */
const secretsInText = [
/(authorization["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^\r\n]*)/gi,
/(gw-dev-app-key["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^&\s"']*)/gi,
];
const maskText = (text: string): string =>
secretsInText
.reduce(
(out, pattern) =>
out.replace(pattern, (_match, key: string, value: string) =>
/^["']/.test(value) ? `${key}${value[0]}…${value[0]}` : `${key}…`,
),
text,
)
.replace(/[\r\n]+/g, ' ');
const mask = (value: unknown, seen = new WeakSet<object>()): unknown => {
if (value instanceof Error) return errorText(value);
if (typeof value === 'string') return maskText(value);
if (value === null || typeof value !== 'object') return value;
if (seen.has(value)) return '[circular]';
seen.add(value);
if (Array.isArray(value)) return value.map((item) => mask(item, seen));
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, secretKey.test(key) ? '…' : mask(item, seen)]),
);
};
/** Errors on one line: what failed, and where from — not seven stack frames. */
const errorText = (error: Error): string => {
const frame = error.stack?.split('\n')[1]?.trim().replace(/^at\s+/, '') ?? '';
return maskText(`${error.name}: ${error.message}${frame ? ` @ ${frame}` : ''}`);
};
/** Empty objects are noise; they render as nothing at all. */
const render = (value: unknown): string => {
if (value instanceof Error) return errorText(value);
if (typeof value === 'object' && value !== null) {
if (Object.keys(value).length === 0) return '';
try {
return JSON.stringify(mask(value)) ?? '';
} catch {
return '[unserialisable]';
}
}
return maskText(String(value));
};
const emit = (level: Level, args: unknown[]): void => {
const stderr = level === 'error' || level === 'fatal';
if (!enabled && !stderr) return;
const time = new Date().toISOString().replace('T', ' ').slice(0, 23);
const tag = level.toUpperCase().padEnd(5);
const line = args
.slice()
.sort((a, b) => Number(a instanceof Error) - Number(b instanceof Error)) // errors trail their message
.map(render)
.filter(Boolean)
.join(' ');
const tty = stderr ? process.stderr.isTTY : process.stdout.isTTY;
const text = `${time} ${tty ? `\x1b[${colors[level]}m${tag}\x1b[0m` : tag} ${line}`;
if (stderr) console.error(text);
else console.log(text);
};
export const log = {
debug: (...args: unknown[]): void => emit('debug', args),
info: (...args: unknown[]): void => emit('info', args),
warn: (...args: unknown[]): void => emit('warn', args),
error: (...args: unknown[]): void => emit('error', args),
fatal: (...args: unknown[]): void => emit('fatal', args),
};