Files
huangserva--3dcellforge/server/logger.mjs
T
2026-07-13 13:15:38 +08:00

122 lines
3.2 KiB
JavaScript

import { appendFile, mkdir, readFile, stat } from 'node:fs/promises'
import { randomUUID } from 'node:crypto'
import path from 'node:path'
import { LOG_DIR, LOG_FILE } from './config.mjs'
const MAX_LOG_READ_BYTES = 768 * 1024
const SENSITIVE_KEYS = new Set([
'authorization',
'cookie',
'imageDataUrl',
'modelBase64',
'TRIPO_API_KEY',
'RODIN_API_KEY',
'FAL_API_KEY',
'OPENAI_API_KEY',
])
export function createRequestId() {
return randomUUID().slice(0, 12)
}
export async function logEvent(level, event, fields = {}) {
const entry = {
ts: new Date().toISOString(),
level,
event,
...sanitizeLogValue(fields),
}
try {
await mkdir(LOG_DIR, { recursive: true })
await appendFile(LOG_FILE, `${JSON.stringify(entry)}\n`, 'utf8')
} catch (error) {
console.warn('log write failed', error)
}
return entry
}
export async function readRecentLogs(limit = 100) {
try {
const fileStat = await stat(LOG_FILE)
const content = await readFile(LOG_FILE, 'utf8')
const slice = content.length > MAX_LOG_READ_BYTES ? content.slice(-MAX_LOG_READ_BYTES) : content
const lines = slice.trim().split(/\r?\n/).filter(Boolean)
const entries = lines.slice(-normalizeLimit(limit)).map(parseLogLine).filter(Boolean)
return {
file: path.relative(process.cwd(), LOG_FILE),
size: fileStat.size,
entries,
}
} catch {
return {
file: path.relative(process.cwd(), LOG_FILE),
size: 0,
entries: [],
}
}
}
export function summarizePayload(payload = {}) {
return {
provider: payload.provider,
modelId: payload.modelId,
fileName: payload.fileName,
hasImage: typeof payload.imageDataUrl === 'string',
imageBytes: estimateDataUrlBytes(payload.imageDataUrl),
promptChars: typeof payload.prompt === 'string' ? payload.prompt.length : 0,
}
}
export function summarizeError(error) {
if (!error) return {}
return {
message: error.message || 'Unknown error',
status: error.status,
detail: sanitizeLogValue(error.detail),
}
}
function normalizeLimit(limit) {
const value = Number(limit)
if (!Number.isFinite(value)) return 100
return Math.max(1, Math.min(500, Math.round(value)))
}
function parseLogLine(line) {
try {
return JSON.parse(line)
} catch {
return null
}
}
function estimateDataUrlBytes(value) {
if (typeof value !== 'string') return 0
const comma = value.indexOf(',')
const base64 = comma === -1 ? value : value.slice(comma + 1)
return Math.round((base64.length * 3) / 4)
}
function sanitizeLogValue(value, key = '') {
if (value === null || value === undefined) return value
if (SENSITIVE_KEYS.has(key)) return '[redacted]'
if (typeof value === 'string') {
if (value.startsWith('data:image/')) return `[image-data:${estimateDataUrlBytes(value)} bytes]`
if (value.length > 900) return `${value.slice(0, 900)}...`
return value
}
if (typeof value !== 'object') return value
if (Array.isArray(value)) return value.slice(0, 30).map((item) => sanitizeLogValue(item))
return Object.fromEntries(
Object.entries(value)
.slice(0, 80)
.map(([entryKey, entryValue]) => [entryKey, sanitizeLogValue(entryValue, entryKey)]),
)
}