一、uni-app 日志系统
1.1 日志收集
日志等级
// utils/logger.js
class Logger {
constructor() {
this.logs = []
this.maxLogs = 100
this.uploadUrl = 'https://log.api.com/collect'
}
debug(message, ...args) {
this._log('DEBUG', message, args)
}
info(message, ...args) {
this._log('INFO', message, args)
}
warn(message, ...args) {
this._log('WARN', message, args)
}
error(message, ...args) {
this._log('ERROR', message, args)
// 错误日志立即上报
this.flush()
}
_log(level, message, args) {
const log = {
level,
message,
args: this._serialize(args),
timestamp: Date.now(),
page: this._getCurrentPage(),
platform: this._getPlatform(),
userId: this._getUserId(),
deviceInfo: this._getDeviceInfo()
}
console[level.toLowerCase()](message, ...args)
this.logs.push(log)
if (this.logs.length > this.maxLogs) {
this.logs.shift()
}
this._scheduleFlush()
}
_serialize(args) {
return args.map(arg => {
try {
return JSON.stringify(arg)
} catch (e) {
return String(arg)
}
})
}
_getCurrentPage() {
// #ifdef APP-PLUS
const pages = getCurrentPages()
return pages[pages.length - 1]?.route || ''
// #endif
// #ifdef H5
return window.location.pathname
// #endif
// #ifdef MP-WEIXIN
const pages = getCurrentPages()
return pages[pages.length - 1]?.route || ''
// #endif
return ''
}
_getPlatform() {
// #ifdef APP-PLUS
return plus.os.name.toLowerCase()
// #endif
// #ifdef H5
return 'h5'
// #endif
// #ifdef MP-WEIXIN
return 'weixin'
// #endif
return 'unknown'
}
_getUserId() {
return uni.getStorageSync('userId') || 'guest'
}
_getDeviceInfo() {
const info = uni.getSystemInfoSync()
return {
model: info.model,
system: info.system,
platform: info.platform,
version: info.version,
screenWidth: info.screenWidth,
screenHeight: info.screenHeight
}
}
_scheduleFlush() {
if (this.flushTimer) return
this.flushTimer = setTimeout(() => {
this.flush()
}, 10000) // 10秒上报一次
}
flush() {
if (this.logs.length === 0) return
clearTimeout(this.flushTimer)
this.flushTimer = null
const logs = [...this.logs]
this.logs = []
// 批量上报
this._upload(logs)
}
_upload(logs) {
uni.request({
url: this.uploadUrl,
method: 'POST',
data: {
logs,
appVersion: this._getAppVersion(),
uploadTime: Date.now()
},
success: (res) => {
console.log('日志上报成功')
},
fail: (err) => {
console.error('日志上报失败:', err)
// 失败的日志重新加入队列
this.logs.unshift(...logs.slice(0, 50))
}
})
}
_getAppVersion() {
// #ifdef APP-PLUS
return plus.runtime.version
// #endif
return '1.0.0'
}
}
export default new Logger()
使用示例
import logger from '@/utils/logger'
// 业务代码中
logger.info('用户登录', { userId: '123', loginTime: Date.now() })
logger.error('支付失败', { orderId: '456', reason: '余额不足' })
1.2 崩溃日志捕获
// utils/crash-handler.js
class CrashHandler {
constructor() {
this.init()
}
init() {
// JS 错误捕获
this._catchJSError()
// Promise 错误捕获
this._catchPromiseError()
// 资源加载错误
this._catchResourceError()
}
_catchJSError() {
// H5 环境
// #ifdef H5
window.addEventListener('error', (event) => {
if (event.target !== window) return
this._reportCrash({
type: 'JSError',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack
})
}, true)
// #endif
// App 环境
// #ifdef APP-PLUS
// plus.runtime 没有直接的错误捕获,需要在关键代码用 try-catch
// #endif
}
_catchPromiseError() {
// #ifdef H5
window.addEventListener('unhandledrejection', (event) => {
this._reportCrash({
type: 'PromiseError',
message: event.reason?.message || event.reason,
stack: event.reason?.stack
})
})
// #endif
}
_catchResourceError() {
// #ifdef H5
window.addEventListener('error', (event) => {
const target = event.target
if (target === window) return
this._reportCrash({
type: 'ResourceError',
resourceType: target.tagName,
resourceUrl: target.src || target.href
})
}, true)
// #endif
}
_reportCrash(error) {
const crashInfo = {
...error,
timestamp: Date.now(),
platform: this._getPlatform(),
appVersion: this._getAppVersion(),
userId: this._getUserId(),
deviceInfo: this._getDeviceInfo(),
page: this._getCurrentPage()
}
console.error('[Crash]', crashInfo)
// 立即上报
uni.request({
url: 'https://crash.api.com/report',
method: 'POST',
data: crashInfo,
fail: () => {
// 上报失败,保存到本地
this._saveToLocal(crashInfo)
}
})
}
_saveToLocal(crashInfo) {
try {
let crashes = uni.getStorageSync('crashes') || []
crashes.push(crashInfo)
// 只保留最近 10 条
crashes = crashes.slice(-10)
uni.setStorageSync('crashes', crashes)
} catch (e) {
console.error('保存崩溃日志失败:', e)
}
}
// 下次启动时上报本地崩溃日志
uploadLocalCrashes() {
try {
const crashes = uni.getStorageSync('crashes') || []
if (crashes.length === 0) return
uni.request({
url: 'https://crash.api.com/batch',
method: 'POST',
data: { crashes },
success: () => {
uni.removeStorageSync('crashes')
}
})
} catch (e) {
console.error('上报本地崩溃日志失败:', e)
}
}
_getPlatform() {
// #ifdef APP-PLUS
return plus.os.name.toLowerCase()
// #endif
return 'h5'
}
_getAppVersion() {
// #ifdef APP-PLUS
return plus.runtime.version
// #endif
return '1.0.0'
}
_getUserId() {
return uni.getStorageSync('userId') || 'guest'
}
_getDeviceInfo() {
return uni.getSystemInfoSync()
}
_getCurrentPage() {
const pages = getCurrentPages()
return pages[pages.length - 1]?.route || ''
}
}
export default new CrashHandler()
1.3 接口日志拦截
// utils/request-logger.js
import logger from './logger'
// 拦截 uni.request
const originalRequest = uni.request
uni.request = function(options) {
const startTime = Date.now()
const requestId = Math.random().toString(36).substr(2, 9)
// 请求开始日志
logger.debug('API请求开始', {
requestId,
url: options.url,
method: options.method || 'GET',
data: options.data
})
// 保存原始回调
const originalSuccess = options.success
const originalFail = options.fail
const originalComplete = options.complete
// 包装 success 回调
options.success = function(res) {
const duration = Date.now() - startTime
logger.info('API请求成功', {
requestId,
url: options.url,
duration,
statusCode: res.statusCode,
dataSize: JSON.stringify(res.data).length
})
if (originalSuccess) {
originalSuccess(res)
}
}
// 包装 fail 回调
options.fail = function(err) {
const duration = Date.now() - startTime
logger.error('API请求失败', {
requestId,
url: options.url,
duration,
error: err.errMsg
})
if (originalFail) {
originalFail(err)
}
}
// 调用原始方法
return originalRequest(options)
}
1.4 本地存储
// 使用 SQLite(App端)或 IndexedDB(H5)
class LogStorage {
constructor() {
this.dbName = 'logs.db'
this.init()
}
init() {
// #ifdef APP-PLUS
this.db = plus.sqlite.openDatabase({
name: this.dbName,
path: '_doc/' + this.dbName
})
plus.sqlite.executeSql({
name: this.dbName,
sql: `CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
level TEXT,
message TEXT,
data TEXT,
timestamp INTEGER
)`
})
// #endif
}
save(log) {
// #ifdef APP-PLUS
plus.sqlite.executeSql({
name: this.dbName,
sql: 'INSERT INTO logs (level, message, data, timestamp) VALUES (?, ?, ?, ?)',
params: [log.level, log.message, JSON.stringify(log.args), log.timestamp]
})
// #endif
}
query(limit = 100) {
return new Promise((resolve, reject) => {
// #ifdef APP-PLUS
plus.sqlite.selectSql({
name: this.dbName,
sql: 'SELECT * FROM logs ORDER BY timestamp DESC LIMIT ?',
params: [limit],
success: (data) => {
resolve(data)
},
fail: reject
})
// #endif
})
}
clear() {
// #ifdef APP-PLUS
plus.sqlite.executeSql({
name: this.dbName,
sql: 'DELETE FROM logs'
})
// #endif
}
}
二、Flutter 日志系统
2.1 日志收集
// lib/utils/logger.dart
import 'dart:developer' as developer;
import 'package:http/http.dart' as http;
import 'dart:convert';
enum LogLevel { debug, info, warn, error }
class Logger {
static final Logger _instance = Logger._internal();
factory Logger() => _instance;
Logger._internal();
final List<Map<String, dynamic>> _logs = [];
final int _maxLogs = 100;
final String _uploadUrl = 'https://log.api.com/collect';
Timer? _flushTimer;
void debug(String message, [Map<String, dynamic>? extra]) {
_log(LogLevel.debug, message, extra);
}
void info(String message, [Map<String, dynamic>? extra]) {
_log(LogLevel.info, message, extra);
}
void warn(String message, [Map<String, dynamic>? extra]) {
_log(LogLevel.warn, message, extra);
}
void error(String message, [Map<String, dynamic>? extra]) {
_log(LogLevel.error, message, extra);
// 错误日志立即上报
flush();
}
void _log(LogLevel level, String message, Map<String, dynamic>? extra) {
final log = {
'level': level.name,
'message': message,
'extra': extra ?? {},
'timestamp': DateTime.now().millisecondsSinceEpoch,
'platform': Platform.operatingSystem,
'userId': _getUserId(),
'deviceInfo': _getDeviceInfo(),
};
developer.log(message, name: 'Logger', level: level.index);
_logs.add(log);
if (_logs.length > _maxLogs) {
_logs.removeAt(0);
}
_scheduleFlush();
}
void _scheduleFlush() {
_flushTimer?.cancel();
_flushTimer = Timer(const Duration(seconds: 10), flush);
}
Future<void> flush() async {
if (_logs.isEmpty) return;
_flushTimer?.cancel();
final logs = List.from(_logs);
_logs.clear();
try {
await http.post(
Uri.parse(_uploadUrl),
body: jsonEncode({
'logs': logs,
'appVersion': '1.0.0',
'uploadTime': DateTime.now().millisecondsSinceEpoch,
}),
headers: {'Content-Type': 'application/json'},
);
} catch (e) {
// 失败重新加入队列
_logs.insertAll(0, logs.take(50));
}
}
String _getUserId() {
// 从本地存储获取
return 'user123';
}
Map<String, dynamic> _getDeviceInfo() {
return {
'platform': Platform.operatingSystem,
'version': Platform.version,
};
}
}
2.2 崩溃日志捕获
// lib/utils/crash_handler.dart
import 'package:flutter/foundation.dart';
class CrashHandler {
static void init() {
// 捕获 Flutter 框架错误
FlutterError.onError = (FlutterErrorDetails details) {
_reportCrash(details.exception, details.stack, 'FlutterError');
// 开发模式显示红屏
if (kDebugMode) {
FlutterError.dumpErrorToConsole(details);
}
};
// 捕获未处理的异步错误
PlatformDispatcher.instance.onError = (error, stack) {
_reportCrash(error, stack, 'UnhandledError');
return true;
};
}
static void _reportCrash(dynamic exception, StackTrace? stack, String type) {
final crashInfo = {
'type': type,
'message': exception.toString(),
'stack': stack?.toString(),
'timestamp': DateTime.now().millisecondsSinceEpoch,
'platform': Platform.operatingSystem,
'appVersion': '1.0.0',
};
print('[Crash] $crashInfo');
// 立即上报
http.post(
Uri.parse('https://crash.api.com/report'),
body: jsonEncode(crashInfo),
headers: {'Content-Type': 'application/json'},
).catchError((e) {
// 上报失败,保存到本地
_saveToLocal(crashInfo);
});
}
static void _saveToLocal(Map<String, dynamic> crashInfo) {
// 使用 shared_preferences 或 Hive 保存
}
}
// main.dart 中初始化
void main() {
CrashHandler.init();
runApp(MyApp());
}
2.3 HTTP 拦截器
// lib/utils/logging_interceptor.dart
import 'package:dio/dio.dart';
class LoggingInterceptor extends Interceptor {
final Logger _logger = Logger();
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
_logger.debug('API请求', {
'url': options.uri.toString(),
'method': options.method,
'data': options.data,
});
super.onRequest(options, handler);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
_logger.info('API响应', {
'url': response.requestOptions.uri.toString(),
'statusCode': response.statusCode,
'duration': '${response.requestOptions.extra['start_time'] != null ? DateTime.now().millisecondsSinceEpoch - response.requestOptions.extra['start_time'] : 0}ms',
});
super.onResponse(response, handler);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
_logger.error('API错误', {
'url': err.requestOptions.uri.toString(),
'error': err.message,
'statusCode': err.response?.statusCode,
});
super.onError(err, handler);
}
}
// 使用
final dio = Dio();
dio.interceptors.add(LoggingInterceptor());
2.4 本地存储(Hive)
// lib/utils/log_storage.dart
import 'package:hive/hive.dart';
@HiveType(typeId: 0)
class LogEntry extends HiveObject {
@HiveField(0)
String level;
@HiveField(1)
String message;
@HiveField(2)
Map<String, dynamic> extra;
@HiveField(3)
int timestamp;
LogEntry({
required this.level,
required this.message,
required this.extra,
required this.timestamp,
});
}
class LogStorage {
static late Box<LogEntry> _box;
static Future<void> init() async {
await Hive.initFlutter();
Hive.registerAdapter(LogEntryAdapter());
_box = await Hive.openBox<LogEntry>('logs');
}
static Future<void> save(LogEntry log) async {
await _box.add(log);
// 只保留最近 1000 条
if (_box.length > 1000) {
await _box.deleteAt(0);
}
}
static List<LogEntry> query({int limit = 100}) {
return _box.values.toList().reversed.take(limit).toList();
}
static Future<void> clear() async {
await _box.clear();
}
}
三、Electron 日志系统
3.1 主进程日志
// main/logger.js
const fs = require('fs')
const path = require('path')
const { app } = require('electron')
class MainLogger {
constructor() {
this.logDir = path.join(app.getPath('userData'), 'logs')
this.logFile = path.join(this.logDir, 'main.log')
this.maxSize = 10 * 1024 * 1024 // 10MB
this.init()
}
init() {
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true })
}
}
_write(level, message, ...args) {
const timestamp = new Date().toISOString()
const logLine = `[${timestamp}] [${level}] ${message} ${args.map(a => JSON.stringify(a)).join(' ')}\n`
console[level.toLowerCase()](message, ...args)
// 写入文件
fs.appendFileSync(this.logFile, logLine)
// 检查文件大小
this._checkFileSize()
}
_checkFileSize() {
try {
const stats = fs.statSync(this.logFile)
if (stats.size > this.maxSize) {
// 重命名旧文件
const oldFile = path.join(this.logDir, `main.${Date.now()}.log`)
fs.renameSync(this.logFile, oldFile)
// 删除超过 7 天的日志
this._cleanOldLogs()
}
} catch (e) {
console.error('检查日志文件大小失败:', e)
}
}
_cleanOldLogs() {
const files = fs.readdirSync(this.logDir)
const now = Date.now()
const maxAge = 7 * 24 * 60 * 60 * 1000 // 7 天
files.forEach(file => {
const filePath = path.join(this.logDir, file)
const stats = fs.statSync(filePath)
if (now - stats.mtimeMs > maxAge) {
fs.unlinkSync(filePath)
}
})
}
debug(message, ...args) {
this._write('DEBUG', message, ...args)
}
info(message, ...args) {
this._write('INFO', message, ...args)
}
warn(message, ...args) {
this._write('WARN', message, ...args)
}
error(message, ...args) {
this._write('ERROR', message, ...args)
this._upload()
}
_upload() {
// 读取日志文件
const logs = fs.readFileSync(this.logFile, 'utf8')
// 上传到服务器
const https = require('https')
const data = JSON.stringify({ logs })
const options = {
hostname: 'log.api.com',
port: 443,
path: '/collect',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': data.length
}
}
const req = https.request(options, (res) => {
console.log('日志上报成功')
})
req.on('error', (e) => {
console.error('日志上报失败:', e)
})
req.write(data)
req.end()
}
}
module.exports = new MainLogger()
3.2 渲染进程日志
// renderer/logger.js
const { ipcRenderer } = require('electron')
class RendererLogger {
constructor() {
this.logs = []
this.maxLogs = 100
}
_log(level, message, ...args) {
const log = {
level,
message,
args: args.map(a => {
try {
return JSON.stringify(a)
} catch (e) {
return String(a)
}
}),
timestamp: Date.now(),
page: window.location.pathname
}
console[level.toLowerCase()](message, ...args)
this.logs.push(log)
if (this.logs.length > this.maxLogs) {
this.logs.shift()
}
// 发送到主进程
ipcRenderer.send('renderer-log', log)
if (level === 'ERROR') {
this.flush()
}
}
debug(message, ...args) {
this._log('DEBUG', message, ...args)
}
info(message, ...args) {
this._log('INFO', message, ...args)
}
warn(message, ...args) {
this._log('WARN', message, ...args)
}
error(message, ...args) {
this._log('ERROR', message, ...args)
}
flush() {
if (this.logs.length === 0) return
const logs = [...this.logs]
this.logs = []
ipcRenderer.send('flush-logs', logs)
}
}
module.exports = new RendererLogger()
3.3 崩溃捕获
// main.js
const { app, crashReporter } = require('electron')
// 启动崩溃报告
crashReporter.start({
productName: 'MyApp',
companyName: 'MyCompany',
submitURL: 'https://crash.api.com/report',
uploadToServer: true,
compress: true
})
// 主进程崩溃
process.on('uncaughtException', (error) => {
mainLogger.error('主进程崩溃', error.stack)
// 可以选择重启应用
app.relaunch()
app.exit(0)
})
// 渲染进程崩溃
app.on('render-process-gone', (event, webContents, details) => {
mainLogger.error('渲染进程崩溃', details)
// 重新加载页面
if (details.reason !== 'clean-exit') {
webContents.reload()
}
})
3.4 日志查看器
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('logger', {
getLogs: () => ipcRenderer.invoke('get-logs'),
clearLogs: () => ipcRenderer.invoke('clear-logs'),
exportLogs: () => ipcRenderer.invoke('export-logs')
})
// main.js
ipcMain.handle('get-logs', async () => {
const logFile = path.join(app.getPath('userData'), 'logs', 'main.log')
return fs.readFileSync(logFile, 'utf8')
})
ipcMain.handle('export-logs', async () => {
const { dialog } = require('electron')
const { filePath } = await dialog.showSaveDialog({
defaultPath: 'logs.txt',
filters: [{ name: 'Text', extensions: ['txt'] }]
})
if (filePath) {
const logFile = path.join(app.getPath('userData'), 'logs', 'main.log')
fs.copyFileSync(logFile, filePath)
return true
}
return false
})
四、React Native 日志系统
4.1 日志收集
// src/utils/logger.js
class Logger {
constructor() {
this.logs = []
this.maxLogs = 100
this.uploadUrl = 'https://log.api.com/collect'
}
_log(level, message, ...args) {
const log = {
level,
message,
args: this._serialize(args),
timestamp: Date.now(),
platform: Platform.OS,
userId: this._getUserId(),
deviceInfo: this._getDeviceInfo()
}
console[level.toLowerCase()](message, ...args)
this.logs.push(log)
if (this.logs.length > this.maxLogs) {
this.logs.shift()
}
this._scheduleFlush()
}
_serialize(args) {
return args.map(arg => {
try {
return JSON.stringify(arg)
} catch (e) {
return String(arg)
}
})
}
debug(message, ...args) {
this._log('DEBUG', message, ...args)
}
info(message, ...args) {
this._log('INFO', message, ...args)
}
warn(message, ...args) {
this._log('WARN', message, ...args)
}
error(message, ...args) {
this._log('ERROR', message, ...args)
this.flush()
}
_scheduleFlush() {
if (this.flushTimer) return
this.flushTimer = setTimeout(() => {
this.flush()
}, 10000)
}
async flush() {
if (this.logs.length === 0) return
clearTimeout(this.flushTimer)
this.flushTimer = null
const logs = [...this.logs]
this.logs = []
try {
await fetch(this.uploadUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ logs })
})
} catch (error) {
// 失败重新加入队列
this.logs.unshift(...logs.slice(0, 50))
}
}
_getUserId() {
// 从 AsyncStorage 获取
return 'user123'
}
_getDeviceInfo() {
const { width, height } = Dimensions.get('window')
return {
platform: Platform.OS,
version: Platform.Version,
screenWidth: width,
screenHeight: height
}
}
}
export default new Logger()
4.2 崩溃捕获
// src/utils/crash-handler.js
import { Alert } from 'react-native'
class CrashHandler {
init() {
// 全局错误处理
ErrorUtils.setGlobalHandler((error, isFatal) => {
this._reportCrash(error, isFatal)
if (isFatal) {
Alert.alert(
'应用错误',
'应用遇到严重错误需要重启',
[{ text: '重启', onPress: () => RNRestart.Restart() }]
)
}
})
// Promise 错误
this._trackPromiseRejections()
}
_trackPromiseRejections() {
const tracking = require('promise/setimmediate/rejection-tracking')
tracking.enable({
allRejections: true,
onUnhandled: (id, error) => {
this._reportCrash(error, false, 'UnhandledPromise')
}
})
}
_reportCrash(error, isFatal, type = 'JSError') {
const crashInfo = {
type,
message: error.message,
stack: error.stack,
isFatal,
timestamp: Date.now(),
platform: Platform.OS,
appVersion: DeviceInfo.getVersion()
}
console.error('[Crash]', crashInfo)
// 立即上报
fetch('https://crash.api.com/report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(crashInfo)
}).catch(() => {
this._saveToLocal(crashInfo)
})
}
async _saveToLocal(crashInfo) {
try {
const crashes = await AsyncStorage.getItem('crashes')
const list = crashes ? JSON.parse(crashes) : []
list.push(crashInfo)
await AsyncStorage.setItem('crashes', JSON.stringify(list.slice(-10)))
} catch (e) {
console.error('保存崩溃日志失败:', e)
}
}
}
export default new CrashHandler()
4.3 网络拦截
// src/utils/request-interceptor.js
import axios from 'axios'
import logger from './logger'
// 请求拦截器
axios.interceptors.request.use(
config => {
config.metadata = { startTime: Date.now() }
logger.debug('API请求', {
url: config.url,
method: config.method,
data: config.data
})
return config
},
error => {
logger.error('API请求错误', { error: error.message })
return Promise.reject(error)
}
)
// 响应拦截器
axios.interceptors.response.use(
response => {
const duration = Date.now() - response.config.metadata.startTime
logger.info('API响应', {
url: response.config.url,
duration,
status: response.status
})
return response
},
error => {
const duration = error.config?.metadata?.startTime
? Date.now() - error.config.metadata.startTime
: 0
logger.error('API响应错误', {
url: error.config?.url,
duration,
error: error.message,
status: error.response?.status
})
return Promise.reject(error)
}
)
4.4 本地存储
// src/utils/log-storage.js
import AsyncStorage from '@react-native-async-storage/async-storage'
class LogStorage {
async save(log) {
try {
const logs = await this.query()
logs.push(log)
// 只保留最近 1000 条
const latest = logs.slice(-1000)
await AsyncStorage.setItem('logs', JSON.stringify(latest))
} catch (e) {
console.error('保存日志失败:', e)
}
}
async query(limit = 100) {
try {
const logs = await AsyncStorage.getItem('logs')
if (!logs) return []
const list = JSON.parse(logs)
return list.slice(-limit)
} catch (e) {
return []
}
}
async clear() {
await AsyncStorage.removeItem('logs')
}
}
export default new LogStorage()
五、日志上报服务端
5.1 日志接收接口
// server/routes/log.js
const express = require('express')
const router = express.Router()
// 接收日志
router.post('/collect', async (req, res) => {
try {
const { logs, appVersion, uploadTime } = req.body
// 批量写入数据库
await LogModel.insertMany(logs.map(log => ({
...log,
appVersion,
uploadTime,
ip: req.ip
})))
res.json({ success: true })
} catch (error) {
console.error('保存日志失败:', error)
res.status(500).json({ success: false, error: error.message })
}
})
// 崩溃上报
router.post('/crash/report', async (req, res) => {
try {
const crashInfo = req.body
// 写入数据库
await CrashModel.create({
...crashInfo,
ip: req.ip
})
// 发送告警通知
await sendAlert(crashInfo)
res.json({ success: true })
} catch (error) {
res.status(500).json({ success: false })
}
})
module.exports = router
5.2 数据库模型
// models/log.js
const mongoose = require('mongoose')
const LogSchema = new mongoose.Schema({
level: String,
message: String,
args: [String],
timestamp: Number,
platform: String,
userId: String,
appVersion: String,
deviceInfo: Object,
page: String,
ip: String
}, {
timestamps: true
})
// 创建索引
LogSchema.index({ timestamp: -1 })
LogSchema.index({ level: 1 })
LogSchema.index({ userId: 1 })
module.exports = mongoose.model('Log', LogSchema)
5.3 日志查询接口
// routes/log-query.js
router.get('/query', async (req, res) => {
const {
level,
userId,
startTime,
endTime,
page = 1,
limit = 100
} = req.query
const query = {}
if (level) query.level = level
if (userId) query.userId = userId
if (startTime || endTime) {
query.timestamp = {}
if (startTime) query.timestamp.$gte = parseInt(startTime)
if (endTime) query.timestamp.$lte = parseInt(endTime)
}
const logs = await LogModel
.find(query)
.sort({ timestamp: -1 })
.skip((page - 1) * limit)
.limit(limit)
const total = await LogModel.countDocuments(query)
res.json({
logs,
total,
page,
limit
})
})
六、面试回答 SOP
问题1:你们是怎么做日志收集的?
回答模板
我们的日志系统分为三层:收集层、存储层、上报层。
收集层: 封装了统一的 Logger 类,提供 debug、info、warn、error 四个级别。每条日志包含完整的上下文信息:时间戳、用户ID、设备信息、页面路径、平台类型。
存储层
- 内存队列:缓存最近 100 条日志
- 本地存储:uni-app 用 SQLite,Flutter 用 Hive,Electron 用文件,RN 用 AsyncStorage
- 服务端:MongoDB 存储,按时间和级别建索引
上报层: 采用批量上报策略,10 秒或 100 条触发一次上传。error 级别的日志立即上报。上报失败的日志会重新加入队列,下次再试。
我们在 uni-app 项目中,日志命中率 95%,平均延迟 5 秒。每天收集约 50 万条日志。
问题2:崩溃日志怎么处理?
回答模板
我们针对不同平台实现了崩溃捕获:
uni-app: H5 环境监听 window.error 和 unhandledrejection 事件。App 环境在关键代码用 try-catch 包裹,因为 plus.runtime 没有全局错误捕获。
Flutter: 通过 FlutterError.onError 捕获框架错误,通过 PlatformDispatcher.instance.onError 捕获异步错误。
Electron: 主进程监听 uncaughtException,渲染进程监听 render-process-gone。使用 crashReporter 自动上报崩溃堆栈。
React Native: 使用 ErrorUtils.setGlobalHandler 捕获 JS 错误。配合 promise rejection tracking 捕获 Promise 错误。
崩溃信息包含:错误类型、消息、堆栈、是否致命、时间戳、平台、版本。立即上报到服务器,失败则保存本地,下次启动时上报。
我们还集成了 Sentry,实现了崩溃实时告警,崩溃率控制在 0.1% 以下。
问题3:日志数据量大怎么优化?
回答模板
我们做了几个优化:
1. 采样上报 对于高频日志,按 10% 采样。比如列表滑动日志,每 10 次记录 1 次。
2. 日志分级 debug 日志只在开发环境记录。生产环境只记录 info、warn、error。
3. 批量上报 不是每条日志都立即上传,而是累积到 100 条或 10 秒后批量上传,减少请求次数。
4. 压缩传输 上报时对日志 JSON 进行 gzip 压缩,减少 70% 流量。
5. 本地清理 定期清理过期日志,只保留最近 7 天或 1000 条。
6. 数据库优化 MongoDB 按时间分表,每月一张表。对 timestamp、level、userId 建索引,查询速度提升 10 倍。
实施后,服务器存储减少 60%,上报流量减少 70%,查询速度提升 10 倍。
问题4:日志脱敏怎么做?
回答模板
我们有完整的脱敏策略:
1. 敏感字段识别 手机号、身份证号、银行卡号、密码、token 等都是敏感字段。
2. 自动脱敏 在 Logger 的 _serialize 方法中,正则匹配敏感信息并替换:
- 手机号:138****5678
- 身份证:123456********1234
- 密码:******
3. 白名单机制 只记录白名单内的字段。比如用户信息只记录 userId、nickname,不记录真实姓名、地址。
4. 数据加密 上报时对整个 payload 进行 AES 加密,服务端解密后存储。
5. 权限控制 日志查询接口需要权限认证,不同角色看到的字段不同。普通开发只能看脱敏后的数据。
这套方案通过了安全审计,符合 GDPR 和国内数据安全法规。
问题5:线上问题怎么排查?
回答模板
我们有完整的排查流程:
1. 告警触发 用户反馈问题或监控触发告警,我们会收到消息通知。
2. 查询日志 根据用户 ID、时间范围、错误信息,在日志平台查询相关日志。
3. 还原现场 通过日志中的页面路径、操作序列、设备信息,还原用户操作场景。
4. 定位代码 根据错误堆栈,快速定位到具体代码行。
5. 本地复现 根据日志中的参数、环境信息,在本地复现问题。
举个例子: 有次用户反馈支付失败,我们通过 userId 查到这条日志:
ERROR 支付失败 {orderId: 123, amount: 100, error: '余额不足'}
然后查到前面的日志:
INFO 用户余额查询 {userId: 456, balance: 50}
发现是余额查询返回错误,导致支付失败。定位到是缓存数据过期导致的,立即修复。
整个过程 15 分钟,如果没有日志系统,可能要花几个小时。
七、技术选型对比
| 技术栈 | 日志方案 | 崩溃捕获 | 本地存储 | 上报方式 |
|---|---|---|---|---|
| uni-app | Logger 类 | error/unhandledrejection | SQLite/Storage | uni.request |
| Flutter | Logger 类 | FlutterError.onError | Hive | http.post |
| Electron | winston/pino | crashReporter | File/SQLite | https.request |
| React Native | Logger 类 | ErrorUtils | AsyncStorage | fetch |
八、最佳实践总结
- 分级记录:debug < info < warn < error
- 批量上报:减少请求次数
- 本地备份:网络失败时保底
- 实时告警:error 日志立即通知
- 数据脱敏:保护用户隐私
- 定期清理:控制存储大小
- 性能监控:避免日志影响性能
- 完整上下文:用户、设备、页面、时间