로깅 추가
This commit is contained in:
1 parent
2967548ff3
commit
5211b0d7ef
5 files changed
+129
-13
No files matched your search
@@ -4,6 +4,7 @@ import cookieParser from 'cookie-parser';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { config } from './config.js';
|
||||
import { logger, requestLogger } from './logger.js';
|
||||
import { sessionsRouter } from './routes/sessions.js';
|
||||
import { documentsRouter } from './routes/documents.js';
|
||||
import {
|
||||
@@ -18,6 +19,7 @@ async function main() {
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(requestLogger);
|
||||
app.use(
|
||||
cors({
|
||||
origin: true,
|
||||
@@ -61,16 +63,23 @@ async function main() {
|
||||
});
|
||||
}
|
||||
|
||||
app.use((err: unknown, req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
||||
logger.error('Unhandled request error', { err, path: req.path });
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ error: 'internal_server_error' });
|
||||
}
|
||||
});
|
||||
|
||||
startSweeper();
|
||||
|
||||
app.listen(config.port, () => {
|
||||
console.log(`[sejong-office] listening on :${config.port}`);
|
||||
console.log(`[sejong-office] tempDir=${config.tempDir}`);
|
||||
console.log(`[sejong-office] publicUrl=${config.publicUrl}`);
|
||||
logger.info(`Backend listening on :${config.port}`);
|
||||
logger.info(`Temp directory: ${config.tempDir}`);
|
||||
logger.info(`Public URL: ${config.publicUrl}`);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
logger.error('Backend failed to start', { err });
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { RequestHandler } from 'express';
|
||||
|
||||
type LogLevel = 'INFO' | 'WARN' | 'ERROR';
|
||||
type LogDetails = Record<string, unknown>;
|
||||
|
||||
function formatDetails(details?: LogDetails): string {
|
||||
if (!details) return '';
|
||||
const normalized = Object.fromEntries(
|
||||
Object.entries(details).map(([key, value]) => [
|
||||
key,
|
||||
value instanceof Error ? value.stack ?? value.message : value,
|
||||
]),
|
||||
);
|
||||
return ` ${JSON.stringify(normalized)}`;
|
||||
}
|
||||
|
||||
function write(level: LogLevel, message: string, details?: LogDetails) {
|
||||
const line = `[${new Date().toISOString()}] [${level}] ${message}${formatDetails(details)}`;
|
||||
if (level === 'ERROR') {
|
||||
console.error(line);
|
||||
} else if (level === 'WARN') {
|
||||
console.warn(line);
|
||||
} else {
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = {
|
||||
info: (message: string, details?: LogDetails) =>
|
||||
write('INFO', message, details),
|
||||
warn: (message: string, details?: LogDetails) =>
|
||||
write('WARN', message, details),
|
||||
error: (message: string, details?: LogDetails) =>
|
||||
write('ERROR', message, details),
|
||||
};
|
||||
|
||||
function browserSummary(userAgent = ''): string {
|
||||
const browsers: [RegExp, string][] = [
|
||||
[/Edg\/([\d.]+)/, 'Edge'],
|
||||
[/OPR\/([\d.]+)/, 'Opera'],
|
||||
[/Chrome\/([\d.]+)/, 'Chrome'],
|
||||
[/Firefox\/([\d.]+)/, 'Firefox'],
|
||||
[/Version\/([\d.]+).*Safari\//, 'Safari'],
|
||||
[/curl\/([\d.]+)/, 'curl'],
|
||||
];
|
||||
const browser = browsers
|
||||
.map(([pattern, name]) => {
|
||||
const match = userAgent.match(pattern);
|
||||
return match ? `${name} ${match[1]}` : null;
|
||||
})
|
||||
.find(Boolean) ?? 'Unknown';
|
||||
|
||||
const os = /Windows NT/.test(userAgent)
|
||||
? 'Windows'
|
||||
: /Android/.test(userAgent)
|
||||
? 'Android'
|
||||
: /iPhone|iPad/.test(userAgent)
|
||||
? 'iOS'
|
||||
: /Mac OS X/.test(userAgent)
|
||||
? 'macOS'
|
||||
: /Linux/.test(userAgent)
|
||||
? 'Linux'
|
||||
: 'Unknown';
|
||||
|
||||
return `${browser} / ${os}`;
|
||||
}
|
||||
|
||||
function requestIp(forwardedFor: string | string[] | undefined, remote?: string) {
|
||||
const forwarded = Array.isArray(forwardedFor)
|
||||
? forwardedFor[0]
|
||||
: forwardedFor?.split(',')[0]?.trim();
|
||||
return (forwarded || remote || '-').replace(/^::ffff:/, '');
|
||||
}
|
||||
|
||||
export const requestLogger: RequestHandler = (req, res, next) => {
|
||||
const startedAt = performance.now();
|
||||
res.on('finish', () => {
|
||||
const durationMs = Math.round(performance.now() - startedAt);
|
||||
const ip = requestIp(
|
||||
req.headers['x-forwarded-for'],
|
||||
req.socket.remoteAddress,
|
||||
);
|
||||
const browser = browserSummary(req.headers['user-agent']);
|
||||
const message =
|
||||
`${req.method} ${req.originalUrl} ${res.statusCode} ${durationMs}ms` +
|
||||
` ip=${ip} browser="${browser}"`;
|
||||
if (res.statusCode >= 500) logger.error(message);
|
||||
else if (res.statusCode >= 400) logger.warn(message);
|
||||
else logger.info(message);
|
||||
});
|
||||
next();
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import fs from 'node:fs/promises';
|
||||
import { config, OFFICE_EXTENSIONS } from '../config.js';
|
||||
import { logger } from '../logger.js';
|
||||
import { requireSession } from '../middleware/session.js';
|
||||
import {
|
||||
addDocument,
|
||||
@@ -50,7 +51,7 @@ documentsRouter.post('/new', requireSession, async (req, res) => {
|
||||
token: doc.token,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
logger.error('Failed to create blank document', { err });
|
||||
res.status(500).json({ error: 'create_failed' });
|
||||
}
|
||||
});
|
||||
@@ -96,7 +97,7 @@ documentsRouter.post(
|
||||
documentType: doc.documentType,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
logger.error('Failed to upload document', { err });
|
||||
res.status(500).json({ error: 'upload_failed' });
|
||||
}
|
||||
},
|
||||
@@ -147,7 +148,7 @@ documentsRouter.post('/:docId/callback', async (req, res) => {
|
||||
verifyOnlyOfficeToken(token);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[callback] jwt invalid', err);
|
||||
logger.warn('Invalid callback JWT', { err, docId: req.params.docId });
|
||||
res.status(403).json({ error: 1 });
|
||||
return;
|
||||
}
|
||||
@@ -188,7 +189,10 @@ documentsRouter.post('/:docId/callback', async (req, res) => {
|
||||
try {
|
||||
await persistDocumentFromUrl(doc, fileUrl, filetype);
|
||||
} catch (err) {
|
||||
console.error('[callback] save error', err);
|
||||
logger.error('Failed to save callback document', {
|
||||
err,
|
||||
docId: req.params.docId,
|
||||
});
|
||||
res.json({ error: 1 });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { config } from '../config.js';
|
||||
import { logger } from '../logger.js';
|
||||
import type { SessionDoc } from '../types/session.js';
|
||||
import { overwriteDocument } from './sessionStore.js';
|
||||
import { signOnlyOfficePayload } from '../utils/jwt.js';
|
||||
@@ -134,7 +135,9 @@ export async function convertToOriginalFormat(
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error('[convert] HTTP', res.status);
|
||||
logger.error('OnlyOffice conversion HTTP error', {
|
||||
statusCode: res.status,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -145,7 +148,10 @@ export async function convertToOriginalFormat(
|
||||
};
|
||||
|
||||
if (data.error || !data.fileUrl) {
|
||||
console.error('[convert] failed', data);
|
||||
logger.error('OnlyOffice conversion failed', {
|
||||
errorCode: data.error,
|
||||
hasFileUrl: Boolean(data.fileUrl),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -153,7 +159,7 @@ export async function convertToOriginalFormat(
|
||||
if (!fileRes.ok) return null;
|
||||
return Buffer.from(await fileRes.arrayBuffer());
|
||||
} catch (err) {
|
||||
console.error('[convert] error', err);
|
||||
logger.error('OnlyOffice conversion error', { err });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { config } from '../config.js';
|
||||
import { logger } from '../logger.js';
|
||||
import type { Session, SessionDoc } from '../types/session.js';
|
||||
import { documentTypeFor, mimeFor } from '../utils/formats.js';
|
||||
|
||||
@@ -151,10 +152,14 @@ export async function sweepExpired(): Promise<number> {
|
||||
|
||||
export function startSweeper(intervalMs = 60_000): NodeJS.Timeout {
|
||||
return setInterval(() => {
|
||||
void sweepExpired().then((n) => {
|
||||
void sweepExpired()
|
||||
.then((n) => {
|
||||
if (n > 0) {
|
||||
console.log(`[session] swept ${n} expired session(s)`);
|
||||
logger.info(`Swept ${n} expired session(s)`);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('Failed to sweep expired sessions', { err });
|
||||
});
|
||||
}, intervalMs);
|
||||
}
|
||||
Reference in new issue
Block a user