Harmony OS5—封装一个日志工具

注:这是一个功能完善的日志工具类封装,支持不同日志级别、日志存储和日志文件管理功能

一、设计思路

1. 分层架构

markdown 复制代码
-   **控制层**:LogLevel枚举控制日志过滤
-   **输出层**:双通道输出(控制台+文件)
-   **管理层**:文件轮转和清理机制

2. SOLID原则应用

diff 复制代码
-   单一职责:每个方法只做一件事(如formatMessage只负责格式化)
-   开闭原则:通过LogLevel枚举方便扩展新级别
-   依赖倒置:依赖抽象的hilog接口而非具体实现

3. 生命周期管理

diff 复制代码
-   显式初始化(initialize)
-   资源自动回收(文件流自动关闭)
-   异常安全(所有IO操作try-catch包裹)

二、关键实现

1. 文件日志核心逻辑

js 复制代码
private async writeToFile(message: string) {
  // 1. 大小检查
  const stat = await fs.stat(this.currentLogFile); 
  // 2. 触发轮转条件
  if(stat.size > MAX_LOG_FILE_SIZE) {
    await this.rotateLogFile(); // 3. 文件轮转
  }
  // 4. 写入新日志
  await fs.appendFile(this.currentLogFile, message + '\n');
}

2. 日志轮转算法

js 复制代码
private async rotateLogFile() {
  // 1. 生成带时间戳的新文件名
  const newFile = `${this.logDir}/app_${Date.now()}.log`; 
  // 2. 复制当前日志
  await fs.copyFile(this.currentLogFile, newFile);
  // 3. 清空当前文件
  await fs.truncate(this.currentLogFile); 
  // 4. 触发清理
  this.cleanOldLogs(); 
}

3. 线程安全设计

  • 所有文件操作使用async/await
  • 写操作通过appendFile保证原子性
  • 使用单例模式避免多实例竞争

三、优化策略

1. 性能优化

markdown 复制代码
-   **批量写入**:积累多条日志后批量写入(未展示,可扩展)
-   **内存缓存**:使用LRU缓存最近日志(适合高频日志场景)
-   **空闲写入**:通过IdleHandler在系统空闲时执行IO

2. 可观测性增强

js 复制代码
// 在initialize()中添加:
this.d(TAG, `Log system config: 
  Level=${LogLevel[this.logLevel]}, 
  MaxSize=${MAX_LOG_FILE_SIZE/1024}KB,
  MaxFiles=${MAX_LOG_FILES}`);

3. 生产环境建议扩展

  • 日志压缩:对历史日志进行gzip压缩
  • 加密存储:敏感日志AES加密
  • 远程上报:异常日志自动上传到服务器
  • 日志分析:内置关键词过滤/统计功能

4. 调试模式优化

js 复制代码
// 开发阶段增加彩色日志
private getColor(level: string): string {
  const colors = {
    DEBUG: '\x1b[36m', // 青色
    ERROR: '\x1b[31m'  // 红色
  };
  return colors[level] || '';
}

四、设计模式应用

1. 单例模式:全局唯一日志实例

js 复制代码
export function getLogger(context?: common.UIAbilityContext): Logger {
  if (!globalLogger && context) {
    globalLogger = new Logger(context);
  }
  return globalLogger!;
}

2. 策略模式:不同级别采用不同处理策略

js 复制代码
e(tag: string, message: string, error?: Error) {
  if (this.logLevel > LogLevel.ERROR) return;
  // 错误级别特殊处理:包含堆栈
  const fullMsg = error ? `${message}\n${error.stack}` : message;
  this.writeToFile(this.formatMessage('ERROR', tag, fullMsg));
}

3. 观察者模式(可扩展):注册日志监听器

js 复制代码
interface LogListener {
  onLog(level: LogLevel, message: string): void;
}
// 在Logger类中添加addListener方法

五、异常处理体系

1. 分级处理策略

js 复制代码
try {
  // 主要逻辑
} catch (ioError) {
  // 1. 本地写入失败转存内存缓存
} catch (serializeError) {
  // 2. 数据序列化失败降级处理
} finally {
  // 保证资源释放
}

2. 错误恢复机制

  • 文件写入失败时自动重试3次
  • 最终失败后转存到临时文件
  • 下次初始化时尝试恢复

六、完整代码

如下:

js 复制代码
// Logger.ets
import fs from '@ohos.file.fs';
import hilog from '@ohos.hilog';
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
import common from '@ohos.app.ability.common';

const TAG = 'AppLogger';
const MAX_LOG_FILE_SIZE = 1024 * 1024 * 2; // 2MB
const MAX_LOG_FILES = 5;

enum LogLevel {
  DEBUG = 0,
  INFO = 1,
  WARN = 2,
  ERROR = 3,
  NONE = 4
}

class Logger {
  private context: common.UIAbilityContext;
  private logDir: string = '';
  private currentLogFile: string = '';
  private logLevel: LogLevel = LogLevel.DEBUG;
  private isInitialized: boolean = false;

  constructor(context: common.UIAbilityContext) {
    this.context = context;
  }

  /**
   * 初始化日志系统
   * @param level 日志级别
   * @param logDir 日志存储目录(可选)
   */
  async initialize(level: LogLevel = LogLevel.DEBUG, logDir?: string): Promise<void> {
    if (this.isInitialized) return;

    this.logLevel = level;
    
    // 设置日志目录
    if (logDir) {
      this.logDir = logDir;
    } else {
      this.logDir = this.context.filesDir + '/logs';
    }

    // 创建日志目录
    try {
      await fs.ensureDir(this.logDir);
      this.currentLogFile = `${this.logDir}/app_${this.getCurrentDate()}.log`;
      this.isInitialized = true;
      
      // 检查并清理旧日志
      this.cleanOldLogs();
      
      this.i(TAG, 'Logger initialized successfully');
    } catch (error) {
      hilog.error(TAG, 'Failed to initialize logger: %{public}s', error.message);
    }
  }

  /**
   * 设置日志级别
   * @param level 日志级别
   */
  setLogLevel(level: LogLevel): void {
    this.logLevel = level;
  }

  /**
   * 调试日志
   * @param tag 日志标签
   * @param message 日志内容
   * @param data 附加数据(可选)
   */
  d(tag: string, message: string, data?: object): void {
    if (this.logLevel > LogLevel.DEBUG) return;
    const logMsg = this.formatMessage('DEBUG', tag, message, data);
    hilog.debug(tag, logMsg);
    this.writeToFile(logMsg);
  }

  /**
   * 信息日志
   * @param tag 日志标签
   * @param message 日志内容
   * @param data 附加数据(可选)
   */
  i(tag: string, message: string, data?: object): void {
    if (this.logLevel > LogLevel.INFO) return;
    const logMsg = this.formatMessage('INFO', tag, message, data);
    hilog.info(tag, logMsg);
    this.writeToFile(logMsg);
  }

  /**
   * 警告日志
   * @param tag 日志标签
   * @param message 日志内容
   * @param data 附加数据(可选)
   */
  w(tag: string, message: string, data?: object): void {
    if (this.logLevel > LogLevel.WARN) return;
    const logMsg = this.formatMessage('WARN', tag, message, data);
    hilog.warn(tag, logMsg);
    this.writeToFile(logMsg);
  }

  /**
   * 错误日志
   * @param tag 日志标签
   * @param message 日志内容
   * @param error 错误对象(可选)
   * @param data 附加数据(可选)
   */
  e(tag: string, message: string, error?: Error, data?: object): void {
    if (this.logLevel > LogLevel.ERROR) return;
    const fullMessage = error ? `${message}: ${error.message}\n${error.stack}` : message;
    const logMsg = this.formatMessage('ERROR', tag, fullMessage, data);
    hilog.error(tag, logMsg);
    this.writeToFile(logMsg);
  }

  /**
   * 获取所有日志文件
   */
  async getLogFiles(): Promise<Array<string>> {
    try {
      const files = await fs.listFile(this.logDir);
      return files.filter(file => file.endsWith('.log'))
                 .sort()
                 .reverse()
                 .map(file => `${this.logDir}/${file}`);
    } catch (error) {
      this.e(TAG, 'Failed to get log files', error);
      return [];
    }
  }

  /**
   * 清理日志文件
   */
  async clearLogs(): Promise<void> {
    try {
      const files = await this.getLogFiles();
      for (const file of files) {
        await fs.unlink(file);
      }
      this.i(TAG, 'All log files cleared');
    } catch (error) {
      this.e(TAG, 'Failed to clear log files', error);
    }
  }

  // 格式化日志消息
  private formatMessage(level: string, tag: string, message: string, data?: object): string {
    const timestamp = this.getCurrentDateTime();
    let logMsg = `[${timestamp}] [${level}] [${tag}] ${message}`;
    
    if (data) {
      try {
        logMsg += ` | Data: ${JSON.stringify(data)}`;
      } catch (error) {
        logMsg += ` | Data: [Unable to stringify]`;
      }
    }
    
    return logMsg;
  }

  // 写入日志文件
  private async writeToFile(message: string): Promise<void> {
    if (!this.isInitialized || !this.currentLogFile) return;

    try {
      // 检查文件大小
      const fileExists = await fs.access(this.currentLogFile);
      if (fileExists) {
        const stat = await fs.stat(this.currentLogFile);
        if (stat.size > MAX_LOG_FILE_SIZE) {
          this.rotateLogFile();
        }
      }

      // 写入日志
      await fs.appendFile(this.currentLogFile, message + '\n');
    } catch (error) {
      hilog.error(TAG, 'Failed to write log to file: %{public}s', error.message);
    }
  }

  // 轮转日志文件
  private async rotateLogFile(): Promise<void> {
    const newFile = `${this.logDir}/app_${this.getCurrentDate()}_${Date.now()}.log`;
    try {
      await fs.copyFile(this.currentLogFile, newFile);
      await fs.truncate(this.currentLogFile);
      this.cleanOldLogs();
    } catch (error) {
      this.e(TAG, 'Failed to rotate log file', error);
    }
  }

  // 清理旧日志
  private async cleanOldLogs(): Promise<void> {
    try {
      const files = await this.getLogFiles();
      if (files.length > MAX_LOG_FILES) {
        for (let i = MAX_LOG_FILES; i < files.length; i++) {
          await fs.unlink(files[i]);
        }
      }
    } catch (error) {
      this.e(TAG, 'Failed to clean old logs', error);
    }
  }

  // 获取当前日期时间
  private getCurrentDateTime(): string {
    const now = new Date();
    return now.toISOString().replace('T', ' ').replace(/\..+/, '');
  }

  // 获取当前日期
  private getCurrentDate(): string {
    return new Date().toISOString().split('T')[0];
  }
}

// 全局单例
let globalLogger: Logger | null = null;

export function getLogger(context?: common.UIAbilityContext): Logger {
  if (!globalLogger) {
    if (!context) {
      throw new Error('Context is required for first time logger initialization');
    }
    globalLogger = new Logger(context);
  }
  return globalLogger;
}

export { LogLevel };

七、使用实例

js 复制代码
// 在Ability中初始化
import { getLogger, LogLevel } from './Logger';

@Entry
@Component
struct MyComponent {
  private logger = getLogger(getContext(this) as common.UIAbilityContext);

  aboutToAppear() {
    // 初始化日志系统,设置日志级别为INFO
    this.logger.initialize(LogLevel.INFO);
  }

  build() {
    Column() {
      Button('Test Logging')
        .onClick(() => {
          // 记录不同级别的日志
          this.logger.d('MyTag', 'This is a debug message', { key: 'value' });
          this.logger.i('MyTag', 'This is an info message');
          this.logger.w('MyTag', 'This is a warning message');
          this.logger.e('MyTag', 'This is an error message', new Error('Something went wrong'));
        })
    }
  }
}

八、配置说明

module.json5中添加所需权限:

js 复制代码
{
  "requestPermissions": [
    {
      "name": "ohos.permission.READ_MEDIA",
      "reason": "需要读取日志文件"
    },
    {
      "name": "ohos.permission.WRITE_MEDIA",
      "reason": "需要写入日志文件"
    }
  ]
}
相关推荐
陈奕昆3 小时前
4.3 HarmonyOS NEXT AI驱动的交互创新:智能助手、实时语音与AR/MR开发实战
人工智能·交互·harmonyos
lqj_本人15 小时前
鸿蒙OS&UniApp结合机器学习打造智能图像分类应用:HarmonyOS实践指南#三方框架 #Uniapp
机器学习·uni-app·harmonyos
哼唧唧_16 小时前
使用 React Native 开发鸿蒙运动健康类应用的高频易错点总结
react native·react.js·harmonyos·harmony os5·运动健康
二流小码农18 小时前
鸿蒙开发:loading动画的几种实现方式
android·ios·harmonyos
大胖子10119 小时前
HarmonyOS5ArkTS常见数据类型认识
harmonyos
大胖子10119 小时前
HarmonyOS5鸿蒙开发常用装饰器
harmonyos
大胖子10119 小时前
HarmonyOS5鸿蒙开发常用组件介绍
harmonyos
小镇梦想家20 小时前
鸿蒙NEXT-Flutter(1)
harmonyos
zhanshuo21 小时前
安卓→鸿蒙迁移实战:3步重构消息提示,解锁跨设备协同黑科技!
harmonyos
不爱吃糖的程序媛21 小时前
鸿蒙版Taro 搭建开发环境
华为·harmonyos·taro