improved logging
This commit is contained in:
1 parent
7657dc6769
commit
25cca2e2df
4 files changed
+101
-19
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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),
|
||||
};
|
||||
Reference in new issue
Block a user