로깅 추가

This commit is contained in:
ysj7705 committed 2026-07-28 15:35:21 +09:00
1 parent 2967548ff3
commit 5211b0d7ef
5 files changed
+132 -16

No files matched your search

+13 -4
View File
@@ -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);
});
+92
View File
@@ -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();
};
+8 -4
View File
@@ -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;
}
+9 -3
View File
@@ -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;
}
}
+10 -5
View File
@@ -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) => {
if (n > 0) {
console.log(`[session] swept ${n} expired session(s)`);
}
});
void sweepExpired()
.then((n) => {
if (n > 0) {
logger.info(`Swept ${n} expired session(s)`);
}
})
.catch((err) => {
logger.error('Failed to sweep expired sessions', { err });
});
}, intervalMs);
}