로깅 추가
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 path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { config } from './config.js';
|
import { config } from './config.js';
|
||||||
|
import { logger, requestLogger } from './logger.js';
|
||||||
import { sessionsRouter } from './routes/sessions.js';
|
import { sessionsRouter } from './routes/sessions.js';
|
||||||
import { documentsRouter } from './routes/documents.js';
|
import { documentsRouter } from './routes/documents.js';
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +19,7 @@ async function main() {
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
app.use(requestLogger);
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
origin: true,
|
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();
|
startSweeper();
|
||||||
|
|
||||||
app.listen(config.port, () => {
|
app.listen(config.port, () => {
|
||||||
console.log(`[sejong-office] listening on :${config.port}`);
|
logger.info(`Backend listening on :${config.port}`);
|
||||||
console.log(`[sejong-office] tempDir=${config.tempDir}`);
|
logger.info(`Temp directory: ${config.tempDir}`);
|
||||||
console.log(`[sejong-office] publicUrl=${config.publicUrl}`);
|
logger.info(`Public URL: ${config.publicUrl}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err) => {
|
main().catch((err) => {
|
||||||
console.error(err);
|
logger.error('Backend failed to start', { err });
|
||||||
process.exit(1);
|
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 multer from 'multer';
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
import { config, OFFICE_EXTENSIONS } from '../config.js';
|
import { config, OFFICE_EXTENSIONS } from '../config.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
import { requireSession } from '../middleware/session.js';
|
import { requireSession } from '../middleware/session.js';
|
||||||
import {
|
import {
|
||||||
addDocument,
|
addDocument,
|
||||||
@@ -50,7 +51,7 @@ documentsRouter.post('/new', requireSession, async (req, res) => {
|
|||||||
token: doc.token,
|
token: doc.token,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
logger.error('Failed to create blank document', { err });
|
||||||
res.status(500).json({ error: 'create_failed' });
|
res.status(500).json({ error: 'create_failed' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -96,7 +97,7 @@ documentsRouter.post(
|
|||||||
documentType: doc.documentType,
|
documentType: doc.documentType,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
logger.error('Failed to upload document', { err });
|
||||||
res.status(500).json({ error: 'upload_failed' });
|
res.status(500).json({ error: 'upload_failed' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -147,7 +148,7 @@ documentsRouter.post('/:docId/callback', async (req, res) => {
|
|||||||
verifyOnlyOfficeToken(token);
|
verifyOnlyOfficeToken(token);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[callback] jwt invalid', err);
|
logger.warn('Invalid callback JWT', { err, docId: req.params.docId });
|
||||||
res.status(403).json({ error: 1 });
|
res.status(403).json({ error: 1 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -188,7 +189,10 @@ documentsRouter.post('/:docId/callback', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
await persistDocumentFromUrl(doc, fileUrl, filetype);
|
await persistDocumentFromUrl(doc, fileUrl, filetype);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[callback] save error', err);
|
logger.error('Failed to save callback document', {
|
||||||
|
err,
|
||||||
|
docId: req.params.docId,
|
||||||
|
});
|
||||||
res.json({ error: 1 });
|
res.json({ error: 1 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
import type { SessionDoc } from '../types/session.js';
|
import type { SessionDoc } from '../types/session.js';
|
||||||
import { overwriteDocument } from './sessionStore.js';
|
import { overwriteDocument } from './sessionStore.js';
|
||||||
import { signOnlyOfficePayload } from '../utils/jwt.js';
|
import { signOnlyOfficePayload } from '../utils/jwt.js';
|
||||||
@@ -134,7 +135,9 @@ export async function convertToOriginalFormat(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
console.error('[convert] HTTP', res.status);
|
logger.error('OnlyOffice conversion HTTP error', {
|
||||||
|
statusCode: res.status,
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +148,10 @@ export async function convertToOriginalFormat(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (data.error || !data.fileUrl) {
|
if (data.error || !data.fileUrl) {
|
||||||
console.error('[convert] failed', data);
|
logger.error('OnlyOffice conversion failed', {
|
||||||
|
errorCode: data.error,
|
||||||
|
hasFileUrl: Boolean(data.fileUrl),
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +159,7 @@ export async function convertToOriginalFormat(
|
|||||||
if (!fileRes.ok) return null;
|
if (!fileRes.ok) return null;
|
||||||
return Buffer.from(await fileRes.arrayBuffer());
|
return Buffer.from(await fileRes.arrayBuffer());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[convert] error', err);
|
logger.error('OnlyOffice conversion error', { err });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
import type { Session, SessionDoc } from '../types/session.js';
|
import type { Session, SessionDoc } from '../types/session.js';
|
||||||
import { documentTypeFor, mimeFor } from '../utils/formats.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 {
|
export function startSweeper(intervalMs = 60_000): NodeJS.Timeout {
|
||||||
return setInterval(() => {
|
return setInterval(() => {
|
||||||
void sweepExpired().then((n) => {
|
void sweepExpired()
|
||||||
|
.then((n) => {
|
||||||
if (n > 0) {
|
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);
|
}, intervalMs);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user