注:这是一个功能完善的日志工具类封装,支持不同日志级别、日志存储和日志文件管理功能
一、设计思路
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": "需要写入日志文件"
}
]
}