From 7f38150d0a2d55d3df59e427dcfff1a1089d7c57 Mon Sep 17 00:00:00 2001 From: Misaka Date: Sat, 4 Apr 2026 17:52:11 +0800 Subject: [PATCH] feat(logging): add ipAddress to default log metadata Add getLocalIpAddress() that reliably resolves the primary LAN IPv4 address by collecting all non-loopback, non-APIPA addresses and prioritizing RFC 1918 private ranges (192.168.x.x, 10.x.x.x, 172.16-31.x.x) over public IPs. Falls back to any non-internal address or 'N/A'. Co-Authored-By: Claude Opus 4.6 --- src/main/services/logger/index.ts | 49 ++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/main/services/logger/index.ts b/src/main/services/logger/index.ts index 489ee28..6289d40 100644 --- a/src/main/services/logger/index.ts +++ b/src/main/services/logger/index.ts @@ -34,6 +34,52 @@ const PROJECT_LEVELS = { // Cache isProduction() at module load — app.isPackaged never changes at runtime const IS_PROD = isProduction() +/** + * Check if an IPv4 address is an RFC 1918 private address. + * Private ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 + */ +function isPrivateIpv4(ip: string): boolean { + const parts = ip.split('.').map(Number) + if (parts.length !== 4) return false + // 10.0.0.0/8 + if (parts[0] === 10) return true + // 172.16.0.0/12 + if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true + // 192.168.0.0/16 + if (parts[0] === 192 && parts[1] === 168) return true + return false +} + +/** + * Get the primary local IPv4 address (RFC 1918 private address). + * + * Strategy: + * 1. Collect all non-internal, non-APIPA IPv4 addresses from physical adapters + * 2. Return the first private (LAN) address found + * 3. Fallback to any remaining non-internal address + * 4. Returns 'N/A' if none found + */ +function getLocalIpAddress(): string { + const interfaces = os.networkInterfaces() + const candidates: string[] = [] + + for (const [, addrs] of Object.entries(interfaces)) { + if (!addrs) continue + for (const iface of addrs) { + if (iface.family === 'IPv4' && !iface.internal && !iface.address.startsWith('169.254.')) { + candidates.push(iface.address) + } + } + } + + // Prefer private (LAN) addresses + const privateIp = candidates.find(isPrivateIpv4) + if (privateIp) return privateIp + + // Fallback: any non-internal address + return candidates[0] || 'N/A' +} + /** * Check if an error has already been serialized (plain object with name/message but not an Error instance). * Prevents double-serialization when logError() output passes through the format pipeline. @@ -172,7 +218,8 @@ const logger = winston.createLogger({ defaultMeta: { service: 'erpauto', appVersion: app.getVersion(), - computerName: os.hostname() + computerName: os.hostname(), + ipAddress: getLocalIpAddress() }, transports: [ // Console transport - always enabled