前言
在开发小程序的过程中,调试是一个永恒的话题。最初我只会用console.log,后来遇到了各种线上问题,才意识到一个好的调试系统有多重要。
这篇文章将分享我如何从简单的console.log演进到一套完整的三通道日志系统,以及这套系统如何帮助我快速定位和解决线上问题。
一、调试的痛点
1.1 传统调试方式的局限
问题1:console.log的局限
// 这是我最初的调试方式
console.log('请求开始', { name: 'poemEngine' });
console.log('返回结果', { data: [...] });
局限:
- 线上环境看不到
- 无法导出给他人
- 日志分散,难以串联
问题2:断点调试的局限
- 真机调试时无法设置断点
- 复现问题困难(特别是偶发问题)
- 性能开销大
问题3:云函数调试的困难
- 本地调试正常,云端必挂
- 日志在云端,查看不便
- 冷启动问题难以定位
1.2 我的需求
- 多环境支持:开发、测试、生产环境都能用
- 可导出:能把日志导出给他人或保存到文件
- 零性能开销:生产环境完全静默
- 结构化:日志可搜索、可过滤
- 线上可用:用户环境也能收集日志
二、三通道日志系统设计
2.1 架构设计
输入 → createLogger → 三通道输出
↓
1. console(控制台)
2. 内存缓冲(环形队列)
3. 文件输出(可选)
2.2 核心实现
// utils/debug.js
class Logger {
constructor(module) {
this.module = module;
this.buffer = new CircularBuffer(200); // 200条环形缓冲
}
log(...args) {
this.output('log', args);
}
warn(...args) {
this.output('warn', args);
}
error(...args) {
this.output('error', args);
}
output(level, args) {
const entry = {
timestamp: Date.now(),
level,
module: this.module,
args
};
// 通道1:console输出
if (globalThis.__JIA_DEBUG__) {
console.log(`[${this.module}]`, ...args);
}
// 通道2:内存缓冲
this.buffer.push(entry);
// 通道3:文件输出
if (globalThis.__debugWriteFile__) {
globalThis.__debugWriteFile__(entry);
}
}
}
// 环形缓冲(固定大小,先进先出)
class CircularBuffer {
constructor(size) {
this.size = size;
this.buffer = [];
}
push(item) {
this.buffer.push(item);
if (this.buffer.length > this.size) {
this.buffer.shift();
}
}
tail(n) {
return this.buffer.slice(-n);
}
dump() {
return this.buffer.splice(0, this.buffer.length);
}
}
// 全局API
const loggers = new Map();
export function createLogger(module) {
if (!loggers.has(module)) {
loggers.set(module, new Logger(module));
}
return loggers.get(module);
}
// 全局调试API
export function __debugTail(n = 20) {
const allLogs = [];
loggers.forEach(logger => {
allLogs.push(...logger.buffer.tail(n));
});
return allLogs.sort((a, b) => a.timestamp - b.timestamp).slice(-n);
}
export function __debugDump() {
const allLogs = [];
loggers.forEach(logger => {
allLogs.push(...logger.buffer.dump());
});
return allLogs.sort((a, b) => a.timestamp - b.timestamp);
}
2.3 关键设计点
- 变量名禁止用
debug:会和debug.js模块名冲突,统一用dbg - 环形缓冲:固定大小(200条),防止内存泄漏
- 开关控制:
__JIA_DEBUG__控制console输出 - 文件输出回调:
__debugWriteFile__由外部环境注入
三、使用方式
3.1 基础使用
import { createLogger } from '@/utils/debug';
const dbg = createLogger('api');
dbg.log('请求开始', { name: 'poemEngine' });
dbg.warn('警告信息', { ... });
dbg.error('错误信息', { ... });
3.2 开发环境
// tests/setup.js
// 开启console输出
globalThis.__JIA_DEBUG__ = true;
3.3 测试环境
# 带文件日志的测试
export DEBUG_LOG_FILE=./debug_output.log
npm run test:debug
// package.json
{
"scripts": {
"test:debug": "DEBUG_LOG_FILE=./debug_output.log node --import ./tests/setup.js --test tests/**/*.test.js"
}
}
3.4 生产环境
// 默认全静默,零性能开销
// __JIA_DEBUG__ = false(默认)
// __debugWriteFile__ = undefined(默认)
3.5 线上问题排查
// 用户遇到问题时,引导导出日志
// 在页面中添加一个隐藏按钮,长按触发导出
onLongPressExport() {
const logs = __debugTail__(20);
// 保存到本地文件或复制到剪贴板
uni.setClipboardData({
data: JSON.stringify(logs, null, 2)
});
}
四、实际案例
4.1 案例1:云函数超时问题
问题:用户反馈点击生成后一直转圈,最后提示"请求超时"。
排查步骤:
- 让用户导出日志:
__debugTail__(20) - 查看日志发现:
[
{ "timestamp": 1690000000000, "level": "log", "module": "api", "args": ["请求开始", { "name": "poemEngine" }] },
{ "timestamp": 1690000015000, "level": "warn", "module": "api", "args": ["请求超时"] },
{ "timestamp": 1690000015001, "level": "error", "module": "api", "args": ["Error: 请求超时"] }
]
- 分析:从请求开始到超时正好15秒,说明是云函数挂起
- 解决:检查云函数日志,发现
checkContentSecurity调用微信安全检测API hang住 - 修复:添加超时保护 + 降级策略
4.2 案例2:收藏功能401错误
问题:用户反馈收藏功能报错"401"。
排查步骤:
- 导出日志
- 查看日志发现:
[
{ "timestamp": 1690000000000, "level": "log", "module": "api", "args": ["收藏请求", { "name": "张清扬" }] },
{ "timestamp": 1690000000100, "level": "error", "module": "api", "args": ["Error: 401", "未登录"] }
]
- 分析:401是未登录错误,但用户说已登录
- 深入查看:发现登录态过期
- 修复:添加登录态刷新逻辑
4.3 案例3:生僻字显示问题
问题:用户反馈名字显示为"□"。
排查步骤:
- 导出日志
- 查看生成结果的日志
- 发现名字中包含生僻字"龘"
- 修复:添加生僻字过滤规则
const RARE_WORDS = new Set(['龘', '靐', '齉', '爩']);
function filterRareWords(name) {
return !name.split('').some(char => RARE_WORDS.has(char));
}
五、进阶功能
5.1 日志分级
const LEVELS = {
debug: 0,
log: 1,
warn: 2,
error: 3
};
class Logger {
constructor(module, level = 'log') {
this.module = module;
this.level = LEVELS[level];
}
shouldOutput(level) {
return LEVELS[level] >= this.level;
}
debug(...args) {
if (this.shouldOutput('debug')) {
this.output('debug', args);
}
}
}
5.2 日志搜索
function searchLogs(keyword) {
const allLogs = __debugDump();
return allLogs.filter(log =>
JSON.stringify(log.args).includes(keyword)
);
}
// 使用
const errorLogs = searchLogs('error');
const apiLogs = searchLogs('poemEngine');
5.3 日志上报
// 严重错误自动上报到云端
function reportError(error) {
const logs = __debugTail__(20);
uniCloud.callFunction({
name: 'errorReporter',
data: {
error: error.message,
stack: error.stack,
logs
}
});
}
六、与云函数日志的配合
6.1 云函数日志
// cloudfunctions/poemEngine/index.js
exports.main = async (event, context) => {
console.log('云函数开始', { event });
// 业务逻辑...
console.log('云函数结束', { result });
return result;
};
6.2 前后端日志关联
// 前端:生成requestId
const requestId = Date.now().toString();
dbg.log('请求开始', { requestId, ... });
// 云函数:接收并打印requestId
exports.main = async (event, context) => {
console.log('云函数开始', { requestId: event.requestId });
// ...
};
七、最佳实践
7.1 日志规范
- 结构化日志:用对象而不是字符串
- 关键节点必打日志:请求开始/结束、错误、重要状态变化
- 敏感信息脱敏:不要打印密码、token等
- 日志级别合理:debug(开发)、log(常规)、warn(警告)、error(错误)
7.2 性能考虑
- 生产环境静默:
__JIA_DEBUG__ = false - 缓冲大小限制:200条防止内存泄漏
- 避免频繁输出:循环内不要打日志
7.3 调试技巧
- 复现问题前先清空:
__debugDump() - 导出日志:
__debugTail__(20) - 搜索关键词:
searchLogs('error') - 关联前后端:用requestId串联
八、总结
8.1 三通道日志的优势
- console:开发调试,实时查看
- 内存缓冲:线上排查,导出日志
- 文件输出:测试环境,持久化存储
8.2 关键设计要点
- 零性能开销:生产环境完全静默
- 可导出:线上问题也能收集日志
- 结构化:可搜索、可过滤
- 模块化:按模块划分,便于定位
8.3 经验教训
- 不要只用console.log:线上看不到
- 不要用debug变量名:会和模块名冲突
- 缓冲大小要限制:防止内存泄漏
- 关键节点必打日志:否则排查时两眼一抹黑