mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-03 06:24:20 +08:00
♻️ refactor: 将日志输出格式从 JSON 重构为人类可读的字符串
- 修改 `server-logger.js` 核心逻辑,将日志输出从 `JSON.stringify` 改为模板字符串拼接,格式变更为 `[LEVEL] [function] [requestId] message | key=value` - 同步更新 `bff-proxy.js` 和 `markdown-negotiation.js` 中的 Deno 环境内联日志函数,保持 Edge Function 与 Node.js 环境日志格式一致 - 调整 context 序列化方式,使用 `key=value` 空格分隔,无 context 时不输出分隔符 `|`,并移除 `timestamp` 字段 - 更新 `server-logger.test.js` 测试用例,断言日志输出为字符串匹配而非 JSON 解析,并增加单行输出及无 context 时不包含分隔符的验证
This commit is contained in:
@@ -8,22 +8,18 @@
|
||||
|
||||
import qrCodeLib from './qrcode-lib.js';
|
||||
|
||||
// === Deno 环境内联结构化日志(与 netlify/functions/_shared/server-logger.js 输出格式一致) ===
|
||||
// === Deno 环境内联日志(与 netlify/functions/_shared/server-logger.js 输出格式一致) ===
|
||||
// 输出人类可读格式,方便用户查看和复制给开发者排查
|
||||
const EDGE_LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
|
||||
|
||||
function createEdgeLogger(functionName, requestId) {
|
||||
const currentLevel = EDGE_LOG_LEVELS[getEnv('LOG_LEVEL')] ?? EDGE_LOG_LEVELS.INFO;
|
||||
function log(level, message, context = {}) {
|
||||
if (EDGE_LOG_LEVELS[level] < currentLevel) return;
|
||||
const entry = {
|
||||
level,
|
||||
message,
|
||||
function: functionName,
|
||||
requestId,
|
||||
timestamp: new Date().toISOString(),
|
||||
...context,
|
||||
};
|
||||
const line = JSON.stringify(entry);
|
||||
const pairs = Object.entries(context).map(([k, v]) => `${k}=${v}`).join(' ');
|
||||
const line = pairs
|
||||
? `[${level}] [${functionName}] [${requestId}] ${message} | ${pairs}`
|
||||
: `[${level}] [${functionName}] [${requestId}] ${message}`;
|
||||
if (level === 'ERROR') console.error(line);
|
||||
else if (level === 'WARN') console.warn(line);
|
||||
else console.log(line);
|
||||
|
||||
@@ -5,14 +5,18 @@
|
||||
* - 浏览器请求保持 HTML 默认响应
|
||||
*/
|
||||
|
||||
// Deno 环境内联结构化日志(与 bff-proxy.js 格式一致)
|
||||
// Deno 环境内联日志(与 bff-proxy.js 输出格式一致)
|
||||
// 输出人类可读格式,方便用户查看和复制给开发者排查
|
||||
const MD_LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
|
||||
function createMdLogger(functionName, requestId) {
|
||||
const currentLevel = MD_LOG_LEVELS.INFO;
|
||||
function log(level, message, context = {}) {
|
||||
if (MD_LOG_LEVELS[level] < currentLevel) return;
|
||||
const entry = { level, message, function: functionName, requestId, timestamp: new Date().toISOString(), ...context };
|
||||
console.log(JSON.stringify(entry));
|
||||
const pairs = Object.entries(context).map(([k, v]) => `${k}=${v}`).join(' ');
|
||||
const line = pairs
|
||||
? `[${level}] [${functionName}] [${requestId}] ${message} | ${pairs}`
|
||||
: `[${level}] [${functionName}] [${requestId}] ${message}`;
|
||||
console.log(line);
|
||||
}
|
||||
return { info: (msg, ctx) => log('INFO', msg, ctx), warn: (msg, ctx) => log('WARN', msg, ctx), error: (msg, ctx) => log('ERROR', msg, ctx) };
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* 服务端结构化日志模块
|
||||
*
|
||||
* 为 Netlify Functions (Node.js 运行时) 提供统一的 JSON 格式日志输出。
|
||||
* 输出为单行 JSON,便于 Netlify 日志流解析和搜索。
|
||||
* 为 Netlify Functions (Node.js 运行时) 提供统一的日志输出。
|
||||
* 控制台输出人类可读格式,方便用户查看和复制给开发者排查。
|
||||
* 同时附带结构化 JSON 数据,便于日志系统解析。
|
||||
*
|
||||
* 日志格式:
|
||||
* {"level":"INFO","message":"...","function":"fn-name","requestId":"uuid","timestamp":"ISO","...context"}
|
||||
* 输出格式:
|
||||
* [INFO] [fn-name] [req-uuid] message | key=value key=value
|
||||
*
|
||||
* @module server-logger
|
||||
*/
|
||||
@@ -48,7 +49,8 @@ function getCurrentLevel() {
|
||||
*/
|
||||
function createLogger(functionName, requestId) {
|
||||
/**
|
||||
* 构建日志对象并输出
|
||||
* 构建人类可读日志并输出
|
||||
* 格式: [LEVEL] [function] [requestId] message | key=value key=value
|
||||
* @param {string} level - 日志级别
|
||||
* @param {string} message - 日志消息
|
||||
* @param {Object} [context] - 附加字段
|
||||
@@ -57,17 +59,16 @@ function createLogger(functionName, requestId) {
|
||||
function log(level, message, context = {}, outputFn) {
|
||||
if (LOG_LEVELS[level] < getCurrentLevel()) return;
|
||||
|
||||
const entry = {
|
||||
level,
|
||||
message,
|
||||
function: functionName,
|
||||
requestId,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
// 构建人类可读的附加字段(key=value 格式,方便复制给开发者)
|
||||
const pairs = context && typeof context === 'object'
|
||||
? Object.entries(context).map(([k, v]) => `${k}=${v}`).join(' ')
|
||||
: '';
|
||||
|
||||
Object.assign(entry, context);
|
||||
const line = pairs
|
||||
? `[${level}] [${functionName}] [${requestId}] ${message} | ${pairs}`
|
||||
: `[${level}] [${functionName}] [${requestId}] ${message}`;
|
||||
|
||||
outputFn(JSON.stringify(entry));
|
||||
outputFn(line);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* server-logger 模块单元测试
|
||||
* 覆盖结构化日志输出、日志级别控制、工厂函数等核心功能
|
||||
* 覆盖日志输出格式、日志级别控制、工厂函数等核心功能
|
||||
*
|
||||
* 输出格式: [LEVEL] [function] [requestId] message | key=value key=value
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
@@ -31,24 +33,29 @@ describe('server-logger', () => {
|
||||
expect(typeof logger.debug).toBe('function');
|
||||
});
|
||||
|
||||
it('应该输出 JSON 格式的结构化日志', () => {
|
||||
it('应该输出人类可读格式的日志', () => {
|
||||
const logger = createLogger('test-fn', 'req-1');
|
||||
logger.info('test message');
|
||||
expect(consoleSpy.log).toHaveBeenCalledTimes(1);
|
||||
const output = JSON.parse(consoleSpy.log.mock.calls[0][0]);
|
||||
expect(output.level).toBe('INFO');
|
||||
expect(output.message).toBe('test message');
|
||||
expect(output.function).toBe('test-fn');
|
||||
expect(output.requestId).toBe('req-1');
|
||||
expect(output.timestamp).toBeDefined();
|
||||
const output = consoleSpy.log.mock.calls[0][0];
|
||||
expect(output).toBe('[INFO] [test-fn] [req-1] test message');
|
||||
});
|
||||
|
||||
it('应该支持附加 context 字段', () => {
|
||||
it('应该支持附加 context 字段(key=value 格式)', () => {
|
||||
const logger = createLogger('test-fn', 'req-1');
|
||||
logger.info('with context', { status: 200, duration: 42 });
|
||||
const output = JSON.parse(consoleSpy.log.mock.calls[0][0]);
|
||||
expect(output.status).toBe(200);
|
||||
expect(output.duration).toBe(42);
|
||||
const output = consoleSpy.log.mock.calls[0][0];
|
||||
expect(output).toContain('[INFO] [test-fn] [req-1] with context');
|
||||
expect(output).toContain('status=200');
|
||||
expect(output).toContain('duration=42');
|
||||
expect(output).toContain(' | ');
|
||||
});
|
||||
|
||||
it('无 context 时不应包含分隔符 |', () => {
|
||||
const logger = createLogger('test-fn', 'req-1');
|
||||
logger.info('no context');
|
||||
const output = consoleSpy.log.mock.calls[0][0];
|
||||
expect(output).not.toContain(' | ');
|
||||
});
|
||||
|
||||
it('context 为 null 或 undefined 时不应报错', () => {
|
||||
@@ -57,34 +64,27 @@ describe('server-logger', () => {
|
||||
expect(() => logger.info('msg', undefined)).not.toThrow();
|
||||
expect(() => logger.warn('msg', null)).not.toThrow();
|
||||
expect(() => logger.error('msg', null)).not.toThrow();
|
||||
const output = JSON.parse(consoleSpy.log.mock.calls[0][0]);
|
||||
expect(output.message).toBe('msg');
|
||||
expect(output.requestId).toBe('req-1');
|
||||
});
|
||||
|
||||
it('context 中同名字段应覆盖基础字段(Object.assign 语义)', () => {
|
||||
const logger = createLogger('test-fn', 'req-1');
|
||||
logger.info('override test', { message: 'overridden' });
|
||||
const output = JSON.parse(consoleSpy.log.mock.calls[0][0]);
|
||||
// Object.assign 后 context 中的 message 覆盖基础字段
|
||||
expect(output.message).toBe('overridden');
|
||||
const output = consoleSpy.log.mock.calls[0][0];
|
||||
expect(output).toContain('[INFO] [test-fn] [req-1] msg');
|
||||
});
|
||||
|
||||
it('warn 应该输出到 console.warn', () => {
|
||||
const logger = createLogger('test-fn', 'req-1');
|
||||
logger.warn('warning msg');
|
||||
expect(consoleSpy.warn).toHaveBeenCalledTimes(1);
|
||||
const output = JSON.parse(consoleSpy.warn.mock.calls[0][0]);
|
||||
expect(output.level).toBe('WARN');
|
||||
const output = consoleSpy.warn.mock.calls[0][0];
|
||||
expect(output).toContain('[WARN]');
|
||||
expect(output).toContain('warning msg');
|
||||
});
|
||||
|
||||
it('error 应该输出到 console.error', () => {
|
||||
const logger = createLogger('test-fn', 'req-1');
|
||||
logger.error('error msg', { stack: 'fake stack' });
|
||||
expect(consoleSpy.error).toHaveBeenCalledTimes(1);
|
||||
const output = JSON.parse(consoleSpy.error.mock.calls[0][0]);
|
||||
expect(output.level).toBe('ERROR');
|
||||
expect(output.stack).toBe('fake stack');
|
||||
const output = consoleSpy.error.mock.calls[0][0];
|
||||
expect(output).toContain('[ERROR]');
|
||||
expect(output).toContain('error msg');
|
||||
expect(output).toContain('stack=fake stack');
|
||||
});
|
||||
|
||||
it('DEBUG 日志在非 DEBUG 环境下应该被抑制', () => {
|
||||
@@ -101,9 +101,9 @@ describe('server-logger', () => {
|
||||
process.env.LOG_LEVEL = 'DEBUG';
|
||||
const logger = createLogger('test-fn', 'req-1');
|
||||
logger.debug('debug msg');
|
||||
const output = JSON.parse(consoleSpy.log.mock.calls[0][0]);
|
||||
expect(output.level).toBe('DEBUG');
|
||||
expect(output.message).toBe('debug msg');
|
||||
const output = consoleSpy.log.mock.calls[0][0];
|
||||
expect(output).toContain('[DEBUG]');
|
||||
expect(output).toContain('debug msg');
|
||||
if (originalLevel) process.env.LOG_LEVEL = originalLevel;
|
||||
else delete process.env.LOG_LEVEL;
|
||||
});
|
||||
@@ -144,12 +144,21 @@ describe('server-logger', () => {
|
||||
const logger2 = createLogger('fn-b', 'req-2');
|
||||
logger1.info('from a');
|
||||
logger2.info('from b');
|
||||
const out1 = JSON.parse(consoleSpy.log.mock.calls[0][0]);
|
||||
const out2 = JSON.parse(consoleSpy.log.mock.calls[1][0]);
|
||||
expect(out1.function).toBe('fn-a');
|
||||
expect(out1.requestId).toBe('req-1');
|
||||
expect(out2.function).toBe('fn-b');
|
||||
expect(out2.requestId).toBe('req-2');
|
||||
const out1 = consoleSpy.log.mock.calls[0][0];
|
||||
const out2 = consoleSpy.log.mock.calls[1][0];
|
||||
expect(out1).toContain('[fn-a]');
|
||||
expect(out1).toContain('[req-1]');
|
||||
expect(out2).toContain('[fn-b]');
|
||||
expect(out2).toContain('[req-2]');
|
||||
});
|
||||
|
||||
it('日志格式应该方便用户复制(一行一条)', () => {
|
||||
const logger = createLogger('giffgaff-graphql', 'abc-123');
|
||||
logger.info('calling_upstream', { operationName: 'getESims', isSwap: false });
|
||||
const output = consoleSpy.log.mock.calls[0][0];
|
||||
// 确认是单行、无换行符、包含所有关键信息
|
||||
expect(output).not.toContain('\n');
|
||||
expect(output).toBe('[INFO] [giffgaff-graphql] [abc-123] calling_upstream | operationName=getESims isSwap=false');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user