- 新增 HRV 监听服务,实时监控心率变异性数据 - 实现 HRV 到压力指数的转换算法和压力等级评估 - 添加智能通知服务,在压力偏高时推送健康建议 - 优化日志系统,修复日志丢失问题并增强刷新机制 - 改进个人页面下拉刷新,支持并行数据加载 - 优化勋章数据缓存策略,减少不必要的网络请求 - 重构应用初始化流程,优化权限服务和健康监听服务的启动顺序 - 移除冗余日志输出,提升应用性能
499 lines
14 KiB
TypeScript
499 lines
14 KiB
TypeScript
import AsyncStorage from '@/utils/kvStore';
|
||
|
||
interface LogEntry {
|
||
id: string;
|
||
timestamp: number;
|
||
level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
|
||
message: string;
|
||
data?: any;
|
||
}
|
||
|
||
/**
|
||
* 改进的日志系统
|
||
* 主要改进:
|
||
* 1. 内存队列和批量写入机制 - 避免频繁存储操作
|
||
* 2. 写入锁机制 - 防止并发写入导致数据丢失
|
||
* 3. 改进的错误处理和重试机制
|
||
* 4. 优化的 ID 生成 - 确保唯一性
|
||
* 5. 写入确认机制 - 返回 Promise 让调用者知道日志是否成功保存
|
||
* 6. 修复日志丢失问题 - 确保所有日志都能被正确保存
|
||
*/
|
||
class Logger {
|
||
private static instance: Logger;
|
||
private readonly maxLogs = 2000; // 最多保存2000条日志
|
||
private readonly storageKey = '@app_logs';
|
||
|
||
// 内存队列相关
|
||
private memoryQueue: LogEntry[] = [];
|
||
private readonly queueMaxSize = 10; // 达到10条日志时触发批量写入
|
||
private readonly flushInterval = 5000; // 5秒自动刷新一次
|
||
private flushTimer: ReturnType<typeof setInterval> | null = null;
|
||
|
||
// 写入锁相关
|
||
private isWriting = false;
|
||
private writePromise: Promise<void> | null = null;
|
||
private pendingFlushes: Array<() => void> = []; // 等待刷新完成的回调队列
|
||
|
||
// ID 生成相关
|
||
private idCounter = 0;
|
||
private lastTimestamp = 0;
|
||
|
||
// 重试相关
|
||
private readonly maxRetries = 3;
|
||
private readonly retryDelay = 1000; // 1秒
|
||
|
||
// 应用退出标志
|
||
private isShuttingDown = false;
|
||
|
||
static getInstance(): Logger {
|
||
if (!Logger.instance) {
|
||
Logger.instance = new Logger();
|
||
}
|
||
return Logger.instance;
|
||
}
|
||
|
||
constructor() {
|
||
// 启动定时刷新机制
|
||
this.startFlushTimer();
|
||
|
||
// 应用退出时确保刷新日志
|
||
if (typeof global !== 'undefined') {
|
||
// 注册应用退出时的清理函数(如果可用)
|
||
this.setupAppExitHandler();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 启动定时刷新机制
|
||
*/
|
||
private startFlushTimer(): void {
|
||
if (this.flushTimer) {
|
||
clearInterval(this.flushTimer);
|
||
}
|
||
|
||
this.flushTimer = setInterval(() => {
|
||
if (this.memoryQueue.length > 0) {
|
||
this.flushQueue().catch(error => {
|
||
console.error('[Logger] Auto flush failed:', error);
|
||
});
|
||
}
|
||
}, this.flushInterval);
|
||
}
|
||
|
||
/**
|
||
* 设置应用退出处理
|
||
*/
|
||
private setupAppExitHandler(): void {
|
||
// 这是一个最佳努力的清理,不是所有场景都能捕获
|
||
if (typeof process !== 'undefined' && process.on) {
|
||
const cleanup = async () => {
|
||
this.isShuttingDown = true;
|
||
|
||
if (this.memoryQueue.length > 0) {
|
||
console.log('[Logger] App exiting, flushing remaining logs...');
|
||
try {
|
||
// 在退出时等待刷新完成
|
||
await this.flushQueue();
|
||
console.log('[Logger] Logs flushed successfully on exit');
|
||
} catch (error) {
|
||
console.error('[Logger] Failed to flush logs on exit:', error);
|
||
}
|
||
}
|
||
};
|
||
|
||
process.on('exit', () => {
|
||
// exit 事件不能异步,只能尝试
|
||
if (this.memoryQueue.length > 0) {
|
||
console.warn('[Logger] Process exiting with unflushed logs');
|
||
}
|
||
});
|
||
|
||
// 这些信号可以异步处理
|
||
process.on('SIGINT', async () => {
|
||
await cleanup();
|
||
process.exit(0);
|
||
});
|
||
|
||
process.on('SIGTERM', async () => {
|
||
await cleanup();
|
||
process.exit(0);
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成唯一 ID
|
||
*/
|
||
private generateId(): string {
|
||
const now = Date.now();
|
||
|
||
// 如果时间戳相同,增加计数器
|
||
if (now === this.lastTimestamp) {
|
||
this.idCounter++;
|
||
} else {
|
||
this.lastTimestamp = now;
|
||
this.idCounter = 0;
|
||
}
|
||
|
||
// 格式:timestamp-counter-random
|
||
const random = Math.random().toString(36).substr(2, 5);
|
||
return `${now}-${this.idCounter}-${random}`;
|
||
}
|
||
|
||
/**
|
||
* 从存储中获取日志(带重试)
|
||
*/
|
||
private async getLogs(retries = 0): Promise<LogEntry[]> {
|
||
try {
|
||
const logsJson = await AsyncStorage.getItem(this.storageKey);
|
||
return logsJson ? JSON.parse(logsJson) : [];
|
||
} catch (error) {
|
||
if (retries < this.maxRetries) {
|
||
console.warn(`[Logger] Failed to get logs, retrying (${retries + 1}/${this.maxRetries})...`);
|
||
await this.delay(this.retryDelay);
|
||
return this.getLogs(retries + 1);
|
||
}
|
||
console.error('[Logger] Failed to get logs after retries:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存日志到存储(带重试)
|
||
*/
|
||
private async saveLogs(logs: LogEntry[], retries = 0): Promise<void> {
|
||
try {
|
||
// 只保留最新的maxLogs条日志
|
||
const trimmedLogs = logs.slice(-this.maxLogs);
|
||
await AsyncStorage.setItem(this.storageKey, JSON.stringify(trimmedLogs));
|
||
} catch (error) {
|
||
if (retries < this.maxRetries) {
|
||
console.warn(`[Logger] Failed to save logs, retrying (${retries + 1}/${this.maxRetries})...`);
|
||
await this.delay(this.retryDelay);
|
||
return this.saveLogs(logs, retries + 1);
|
||
}
|
||
console.error('[Logger] Failed to save logs after retries:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 延迟函数
|
||
*/
|
||
private delay(ms: number): Promise<void> {
|
||
return new Promise(resolve => setTimeout(resolve, ms));
|
||
}
|
||
|
||
/**
|
||
* 安全地序列化数据
|
||
*/
|
||
private serializeData(data: any): any {
|
||
if (!data || typeof data !== 'object') {
|
||
return data;
|
||
}
|
||
|
||
try {
|
||
// 处理 Error 对象
|
||
if (data instanceof Error) {
|
||
return {
|
||
name: data.name,
|
||
message: data.message,
|
||
stack: data.stack
|
||
};
|
||
}
|
||
|
||
// 处理其他对象 - 使用 replacer 函数处理循环引用
|
||
const seen = new WeakSet();
|
||
return JSON.parse(JSON.stringify(data, (key, value) => {
|
||
if (typeof value === 'object' && value !== null) {
|
||
// 检测循环引用
|
||
if (seen.has(value)) {
|
||
return '[Circular Reference]';
|
||
}
|
||
seen.add(value);
|
||
|
||
// 处理特殊对象类型
|
||
if (value.constructor === Object || Array.isArray(value)) {
|
||
return value;
|
||
}
|
||
|
||
// 对于其他对象类型,转换为字符串表示
|
||
return value.toString ? value.toString() : '[Object]';
|
||
}
|
||
return value;
|
||
}));
|
||
} catch (serializeError) {
|
||
// 如果序列化失败,返回基本信息
|
||
return {
|
||
error: 'Failed to serialize data',
|
||
type: typeof data,
|
||
toString: data.toString ? data.toString() : 'N/A'
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 刷新队列到存储(异步,带锁)
|
||
* 修复:确保在写入失败时不丢失日志,并正确处理并发刷新请求
|
||
*/
|
||
private async flushQueue(): Promise<void> {
|
||
// 如果正在写入,返回当前的写入 Promise
|
||
if (this.isWriting && this.writePromise) {
|
||
return this.writePromise;
|
||
}
|
||
|
||
// 如果队列为空,直接返回
|
||
if (this.memoryQueue.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// 设置写入锁
|
||
this.isWriting = true;
|
||
|
||
// 复制要写入的日志(但不立即清空队列,避免失败时丢失)
|
||
const logsToWrite = [...this.memoryQueue];
|
||
|
||
this.writePromise = (async () => {
|
||
try {
|
||
// 获取现有日志
|
||
const existingLogs = await this.getLogs();
|
||
|
||
// 合并日志
|
||
const allLogs = [...existingLogs, ...logsToWrite];
|
||
|
||
// 保存到存储
|
||
await this.saveLogs(allLogs);
|
||
|
||
// 写入成功后才从队列中移除这些日志
|
||
// 使用 filter 而不是直接赋值,因为在写入过程中可能有新日志添加
|
||
const writtenIds = new Set(logsToWrite.map(log => log.id));
|
||
this.memoryQueue = this.memoryQueue.filter(log => !writtenIds.has(log.id));
|
||
|
||
console.log(`[Logger] Successfully flushed ${logsToWrite.length} logs to storage, remaining: ${this.memoryQueue.length}`);
|
||
|
||
// 通知等待的刷新请求
|
||
this.notifyPendingFlushes();
|
||
} catch (error) {
|
||
console.error('[Logger] Failed to flush queue:', error);
|
||
|
||
// 写入失败时,日志已经在队列中,无需重新添加
|
||
// 但要限制队列大小,避免内存溢出
|
||
if (this.memoryQueue.length > this.maxLogs) {
|
||
const overflow = this.memoryQueue.length - this.maxLogs;
|
||
console.warn(`[Logger] Queue overflow, dropping ${overflow} oldest logs`);
|
||
this.memoryQueue = this.memoryQueue.slice(-this.maxLogs);
|
||
}
|
||
|
||
throw error; // 重新抛出错误,让调用者知道刷新失败
|
||
} finally {
|
||
// 释放写入锁
|
||
this.isWriting = false;
|
||
this.writePromise = null;
|
||
}
|
||
})();
|
||
|
||
return this.writePromise;
|
||
}
|
||
|
||
/**
|
||
* 通知等待刷新完成的回调
|
||
*/
|
||
private notifyPendingFlushes(): void {
|
||
const callbacks = [...this.pendingFlushes];
|
||
this.pendingFlushes = [];
|
||
callbacks.forEach(callback => callback());
|
||
}
|
||
|
||
/**
|
||
* 等待当前刷新完成
|
||
*/
|
||
private async waitForFlush(): Promise<void> {
|
||
if (!this.isWriting) {
|
||
return;
|
||
}
|
||
|
||
return new Promise<void>((resolve) => {
|
||
this.pendingFlushes.push(resolve);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 添加日志到队列
|
||
* 修复:确保在达到阈值时正确处理刷新,避免日志丢失
|
||
*/
|
||
private async addLog(level: LogEntry['level'], message: string, data?: any): Promise<void> {
|
||
// 序列化数据
|
||
const safeData = this.serializeData(data);
|
||
|
||
// 创建日志条目
|
||
const logEntry: LogEntry = {
|
||
id: this.generateId(),
|
||
timestamp: Date.now(),
|
||
level,
|
||
message,
|
||
data: safeData
|
||
};
|
||
|
||
// 输出到控制台
|
||
try {
|
||
const logMethod = level === 'ERROR' ? console.error :
|
||
level === 'WARN' ? console.warn :
|
||
level === 'INFO' ? console.info : console.log;
|
||
|
||
logMethod(`[${level}] ${message}`, safeData !== undefined ? safeData : '');
|
||
} catch (consoleError) {
|
||
// 如果控制台输出失败,使用最基本的 console.log
|
||
console.log(`[${level}] ${message}`);
|
||
}
|
||
|
||
// 添加到内存队列
|
||
this.memoryQueue.push(logEntry);
|
||
|
||
// 检查是否需要刷新队列
|
||
if (this.memoryQueue.length >= this.queueMaxSize) {
|
||
try {
|
||
// 等待刷新完成,确保日志不会丢失
|
||
await this.flushQueue();
|
||
} catch (error) {
|
||
// 刷新失败时,日志仍在队列中,会在下次刷新时重试
|
||
console.error('[Logger] Failed to flush queue after size threshold:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 公共日志方法
|
||
*/
|
||
async debug(message: string, data?: any): Promise<void> {
|
||
await this.addLog('DEBUG', message, data);
|
||
}
|
||
|
||
async info(message: string, data?: any): Promise<void> {
|
||
await this.addLog('INFO', message, data);
|
||
}
|
||
|
||
async warn(message: string, data?: any): Promise<void> {
|
||
await this.addLog('WARN', message, data);
|
||
}
|
||
|
||
async error(message: string, data?: any): Promise<void> {
|
||
await this.addLog('ERROR', message, data);
|
||
}
|
||
|
||
/**
|
||
* 获取所有日志(包括内存队列中的)
|
||
*/
|
||
async getAllLogs(): Promise<LogEntry[]> {
|
||
// 先刷新队列
|
||
try {
|
||
await this.flushQueue();
|
||
} catch (error) {
|
||
console.warn('[Logger] Failed to flush before getting logs:', error);
|
||
}
|
||
|
||
// 然后获取存储中的日志
|
||
const storedLogs = await this.getLogs();
|
||
|
||
// 如果还有未刷新的日志(刷新失败的情况),也包含进来
|
||
if (this.memoryQueue.length > 0) {
|
||
return [...storedLogs, ...this.memoryQueue];
|
||
}
|
||
|
||
return storedLogs;
|
||
}
|
||
|
||
/**
|
||
* 清除所有日志
|
||
*/
|
||
async clearLogs(): Promise<void> {
|
||
try {
|
||
// 清空内存队列
|
||
this.memoryQueue = [];
|
||
|
||
// 等待当前写入完成
|
||
await this.waitForFlush();
|
||
|
||
// 清除存储
|
||
await AsyncStorage.removeItem(this.storageKey);
|
||
|
||
console.log('[Logger] All logs cleared successfully');
|
||
} catch (error) {
|
||
console.error('[Logger] Failed to clear logs:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 导出日志
|
||
*/
|
||
async exportLogs(): Promise<string> {
|
||
// 获取所有日志(包括未刷新的)
|
||
const logs = await this.getAllLogs();
|
||
return JSON.stringify(logs, null, 2);
|
||
}
|
||
|
||
/**
|
||
* 手动刷新日志到存储
|
||
*/
|
||
async flush(): Promise<void> {
|
||
await this.flushQueue();
|
||
}
|
||
|
||
/**
|
||
* 获取队列状态(用于调试)
|
||
*/
|
||
getQueueStatus(): { queueSize: number; isWriting: boolean; isShuttingDown: boolean } {
|
||
return {
|
||
queueSize: this.memoryQueue.length,
|
||
isWriting: this.isWriting,
|
||
isShuttingDown: this.isShuttingDown
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 清理资源
|
||
*/
|
||
async destroy(): Promise<void> {
|
||
// 设置关闭标志
|
||
this.isShuttingDown = true;
|
||
|
||
// 停止定时器
|
||
if (this.flushTimer) {
|
||
clearInterval(this.flushTimer);
|
||
this.flushTimer = null;
|
||
}
|
||
|
||
// 最后刷新一次,确保所有日志都被保存
|
||
if (this.memoryQueue.length > 0) {
|
||
try {
|
||
console.log('[Logger] Destroying logger, flushing remaining logs...');
|
||
await this.flushQueue();
|
||
console.log('[Logger] Logger destroyed successfully');
|
||
} catch (error) {
|
||
console.error('[Logger] Failed to flush logs on destroy:', error);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 导出全局日志实例和便捷函数
|
||
export const logger = Logger.getInstance();
|
||
|
||
// 便捷的全局日志函数(返回 Promise 以便调用者可以等待)
|
||
export const log = {
|
||
debug: (message: string, data?: any) => logger.debug(message, data),
|
||
info: (message: string, data?: any) => logger.info(message, data),
|
||
warn: (message: string, data?: any) => logger.warn(message, data),
|
||
error: (message: string, data?: any) => logger.error(message, data),
|
||
|
||
// 额外的工具函数
|
||
flush: () => logger.flush(),
|
||
getQueueStatus: () => logger.getQueueStatus(),
|
||
getAllLogs: () => logger.getAllLogs(),
|
||
clearLogs: () => logger.clearLogs(),
|
||
exportLogs: () => logger.exportLogs(),
|
||
destroy: () => logger.destroy(),
|
||
};
|
||
|
||
export type { LogEntry };
|