errorlog
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,3 @@
|
||||
node_modules/
|
||||
.env
|
||||
data/
|
||||
data/logs/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import chalk from 'chalk';
|
||||
import { isEmpty, prettyJson } from '../../util/prettyJson.js';
|
||||
import { writeErrorLog } from '../../util/errorLog.js';
|
||||
|
||||
function logField(label, value, { skipEmpty = false } = {}) {
|
||||
if (skipEmpty && isEmpty(value)) return;
|
||||
@@ -56,6 +57,60 @@ export const loggerMiddleware = (req, res, next) => {
|
||||
}
|
||||
logField('body', req.body ?? {}, { skipEmpty: true });
|
||||
logField('response', getBody());
|
||||
|
||||
// Dump full request/response/error details to a log file on 5xx.
|
||||
const statusCode = res.statusCode;
|
||||
if (statusCode >= 500) {
|
||||
const body = getBody();
|
||||
writeErrorLog(
|
||||
`${req.method} ${req.originalUrl} ${statusCode}`,
|
||||
{
|
||||
request: {
|
||||
method: req.method,
|
||||
url: req.originalUrl,
|
||||
headers: req.headers,
|
||||
params: req.params,
|
||||
query: req.query,
|
||||
},
|
||||
body: req.body,
|
||||
files: Array.isArray(req.files)
|
||||
? req.files.map((f) => ({
|
||||
fieldname: f.fieldname,
|
||||
originalname: f.originalname,
|
||||
mimetype: f.mimetype,
|
||||
size: f.size,
|
||||
encoding: f.encoding,
|
||||
bufferHeadHex: f.buffer
|
||||
? [...f.buffer.subarray(0, 32)]
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join(' ')
|
||||
: null,
|
||||
}))
|
||||
: undefined,
|
||||
response: {
|
||||
status: statusCode,
|
||||
body,
|
||||
},
|
||||
error: res.locals?.error
|
||||
? {
|
||||
message: res.locals.error.message,
|
||||
code: res.locals.error.code,
|
||||
number: res.locals.error.number,
|
||||
state: res.locals.error.state,
|
||||
class: res.locals.error.class,
|
||||
lineNumber: res.locals.error.lineNumber,
|
||||
procName: res.locals.error.procName,
|
||||
stack: res.locals.error.stack,
|
||||
precedingErrors: res.locals.error.precedingErrors?.map((e) => ({
|
||||
message: e.message,
|
||||
number: e.number,
|
||||
state: e.state,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import multer from 'multer';
|
||||
import * as db from '../../db/delivery.js';
|
||||
import { writeErrorLog } from '../../util/errorLog.js';
|
||||
|
||||
// Shared memory storage for delivery attachments (max 50 MB)
|
||||
const multerUpload = multer({
|
||||
@@ -105,6 +106,51 @@ function requireSelectedCompany(req) {
|
||||
return { companyId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug logging: dump per-file details before handing them to the store.
|
||||
* Needed to diagnose MSSQL TDS errors on @content ("Data type 0xA5 has an
|
||||
* invalid data length or metadata length").
|
||||
*/
|
||||
function logDeliveryFiles(files, label) {
|
||||
if (!Array.isArray(files) || files.length === 0) {
|
||||
console.log(`[delivery diag] ${label}: no files`);
|
||||
return;
|
||||
}
|
||||
files.forEach((f, idx) => {
|
||||
const content = f?.content;
|
||||
const isBuf = Buffer.isBuffer(content);
|
||||
const hex = isBuf
|
||||
? [...content.subarray(0, 8)].map((b) => b.toString(16).padStart(2, '0')).join(' ')
|
||||
: null;
|
||||
console.log(
|
||||
`[delivery diag] ${label} file[${idx}]:` +
|
||||
` name=${JSON.stringify(f?.name)}` +
|
||||
` mimetype=${JSON.stringify(f?.mimetype)}` +
|
||||
` size=${JSON.stringify(f?.size)}` +
|
||||
` contentType=${isBuf ? 'Buffer' : typeof content}` +
|
||||
` contentLength=${isBuf ? content.length : typeof content === 'string' ? content.length : 'n/a'}` +
|
||||
(hex !== null ? ` headHex=${hex}` : '') +
|
||||
(typeof content === 'string' ? ` head=${JSON.stringify(content.slice(0, 16))}` : '')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug logging: dump everything tedious/mssql puts on a SQL error.
|
||||
*/
|
||||
function logStoreError(label, error) {
|
||||
console.error(`[delivery diag] ${label} store error:`);
|
||||
console.error(` message: ${error.message}`);
|
||||
console.error(` code: ${error.code} number: ${error.number} state: ${error.state} class: ${error.class}`);
|
||||
console.error(` lineNumber: ${error.lineNumber} serverName: ${error.serverName} procName: ${error.procName}`);
|
||||
if (error.precedingErrors?.length) {
|
||||
error.precedingErrors.forEach((e, i) => {
|
||||
console.error(` precedingError[${i}]: ${e.message} (number=${e.number} state=${e.state})`);
|
||||
});
|
||||
}
|
||||
console.error(error.stack);
|
||||
}
|
||||
|
||||
function deliveryErrorStatus(error) {
|
||||
if (error.message === 'No company selected') return 400;
|
||||
if (error.message === 'Not authorized') return 403;
|
||||
@@ -293,6 +339,7 @@ export function register(app, basePath = '') {
|
||||
const deliveries = await db.list(req.user.id);
|
||||
res.status(200).json({ deliveries });
|
||||
} catch (error) {
|
||||
res.locals.error = error;
|
||||
const status = deliveryErrorStatus(error);
|
||||
if (status === 500) console.error('List deliveries failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
@@ -307,6 +354,7 @@ export function register(app, basePath = '') {
|
||||
|
||||
try {
|
||||
const { externalNumber, date, supplierId, lines, files } = parseDeliveryPayload(req);
|
||||
logDeliveryFiles(files, 'create');
|
||||
const delivery = await db.create(req.user.id, {
|
||||
externalNumber,
|
||||
date,
|
||||
@@ -316,8 +364,34 @@ export function register(app, basePath = '') {
|
||||
});
|
||||
res.status(201).json({ delivery });
|
||||
} catch (error) {
|
||||
res.locals.error = error;
|
||||
const status = deliveryErrorStatus(error);
|
||||
if (status === 500) console.error('Create delivery failed:', error);
|
||||
if (status === 500) {
|
||||
logStoreError('Create delivery failed', error);
|
||||
// Dump the full parsed payload (including file metadata) to a log file.
|
||||
try {
|
||||
const { externalNumber, date, supplierId, lines, files } = parseDeliveryPayload(req);
|
||||
writeErrorLog(`POST ${basePath}/delivery 500 (create delivery)`, {
|
||||
payload: { externalNumber, date, supplierId, lines },
|
||||
files: files?.map((f) => ({
|
||||
name: f.name,
|
||||
mimetype: f.mimetype,
|
||||
size: f.size,
|
||||
meta: f.meta,
|
||||
})),
|
||||
error: {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
number: error.number,
|
||||
state: error.state,
|
||||
class: error.class,
|
||||
stack: error.stack,
|
||||
},
|
||||
});
|
||||
} catch (logErr) {
|
||||
console.error('[delivery diag] Failed to dump payload:', logErr.message);
|
||||
}
|
||||
}
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
@@ -526,6 +600,7 @@ export function register(app, basePath = '') {
|
||||
const delivery = await db.getById(req.user.id, id);
|
||||
res.status(200).json({ delivery });
|
||||
} catch (error) {
|
||||
res.locals.error = error;
|
||||
const status = deliveryErrorStatus(error);
|
||||
if (status === 500) console.error('Get delivery failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
@@ -562,11 +637,15 @@ export function register(app, basePath = '') {
|
||||
patch.files = parsed.files;
|
||||
}
|
||||
|
||||
logDeliveryFiles(patch.files, `update ${id}`);
|
||||
const delivery = await db.update(req.user.id, id, patch);
|
||||
res.status(200).json({ delivery });
|
||||
} catch (error) {
|
||||
res.locals.error = error;
|
||||
const status = deliveryErrorStatus(error);
|
||||
if (status === 500) console.error('Update delivery failed:', error);
|
||||
if (status === 500) {
|
||||
logStoreError('Update delivery failed', error);
|
||||
}
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
@@ -586,6 +665,7 @@ export function register(app, basePath = '') {
|
||||
await db.remove(req.user.id, id);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
res.locals.error = error;
|
||||
const status = deliveryErrorStatus(error);
|
||||
if (status === 500) console.error('Delete delivery failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
@@ -729,6 +809,7 @@ export function register(app, basePath = '') {
|
||||
return res.status(200).send(buffer);
|
||||
}
|
||||
} catch (error) {
|
||||
res.locals.error = error;
|
||||
const status = deliveryErrorStatus(error);
|
||||
if (status === 500) console.error('Get delivery file failed:', error);
|
||||
res.status(status).json({ error: error.message });
|
||||
|
||||
@@ -48,12 +48,17 @@ function wrapStoreWithTiming(rawStore) {
|
||||
api[method] = async (...args) => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
return await fn(...args);
|
||||
} finally {
|
||||
const duration = Date.now() - start;
|
||||
const result = await fn(...args);
|
||||
console.log(
|
||||
`${chalk.magenta('libstore')} ${namespace}.${method} ${chalk.gray(`(${duration}ms)`)}`
|
||||
`${chalk.magenta('libstore')} ${namespace}.${method} ${chalk.gray(`(${Date.now() - start}ms)`)}`
|
||||
);
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`${chalk.magenta('libstore')} ${namespace}.${method} ${chalk.red('FAILED')} ` +
|
||||
`${chalk.gray(`(${Date.now() - start}ms)`)} ${chalk.red(err.message)}`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
55
src/util/errorLog.js
Normal file
55
src/util/errorLog.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { appendFileSync, mkdirSync } from 'fs';
|
||||
import { dirname, resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { prettyJson } from '../util/prettyJson.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const LOG_DIR = process.env.ERROR_LOG_DIR || resolve(__dirname, '../../logs/errors');
|
||||
const LOG_PREFIX = 'error-';
|
||||
|
||||
let dirEnsured = false;
|
||||
|
||||
function ensureLogDir() {
|
||||
if (dirEnsured) return;
|
||||
mkdirSync(LOG_DIR, { recursive: true });
|
||||
dirEnsured = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a plain-text error log file (no ANSI colors) to the error log
|
||||
* directory and returns the file path. Never throws.
|
||||
* @param {string} label Short description, e.g. "POST /api/delivery 500"
|
||||
* @param {Record<string, unknown>} sections Named sections (request, body, response, error…)
|
||||
* @returns {string | null} Path of the log file, or null on failure
|
||||
*/
|
||||
export function writeErrorLog(label, sections) {
|
||||
try {
|
||||
ensureLogDir();
|
||||
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const safeLabel = label
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 40);
|
||||
const filePath = resolve(LOG_DIR, `${LOG_PREFIX}${stamp}-${safeLabel}.log`);
|
||||
const divider = '='.repeat(80);
|
||||
|
||||
const parts = [
|
||||
`Timestamp: ${new Date().toISOString()}`,
|
||||
`Label: ${label}`,
|
||||
divider,
|
||||
];
|
||||
|
||||
for (const [name, value] of Object.entries(sections)) {
|
||||
parts.push(`--- ${name} ${'-'.repeat(Math.max(0, 74 - name.length))}`);
|
||||
parts.push(prettyJson(value, { colors: false }));
|
||||
}
|
||||
|
||||
appendFileSync(filePath, `${parts.join('\n')}\n`);
|
||||
console.error(`[error-log] Wrote ${filePath}`);
|
||||
return filePath;
|
||||
} catch (err) {
|
||||
console.error('[error-log] Failed to write error log:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user