HarmonyOS日志系统实战:从HiLog到云日志分析

一、HiLog日志系统基础

1.1 HiLog是什么?

HiLog是HarmonyOS原生提供的日志系统,专为分布式应用场景设计。与传统的System.out.println或Android的Log.d不同,HiLog在设计之初就考虑了多设备协同、性能开销和安全隐私等维度。

HiLog的核心设计目标:

  • 分级日志:支持DEBUG、INFO、WARN、ERROR、FATAL五级日志
  • 标签化分类:通过domain和tag双维度标识日志来源
  • 性能友好:异步写入机制,避免阻塞主线程
  • 安全打印:内置隐私标识符,防止敏感信息泄露
  • 跨设备聚合:结合分布式软总线,支持多设备日志关联

1.2 快速上手:ArkTS环境下的HiLog使用

1.2.1 导入模块
import { hilog } from '@kit.PerformanceAnalysisKit';
1.2.2 基础API示例
// 定义日志标签
const DOMAIN: number = 0xF811;  // 业务领域标识,范围0x0000~0xFFFF
const TAG: string = 'LoginModule';

@Entry
@Component
struct Index {
  build() {
    Column() {
      Button('用户登录')
        .onClick(() => {
          // DEBUG级别:开发调试用
          hilog.debug(DOMAIN, TAG, '用户点击登录按钮,时间戳:%{public}d', Date.now());
          
          // 模拟登录逻辑
          this.login('test_user_123');
        })
    }
  }
  
  login(userId: string) {
    // INFO级别:记录关键业务流程
    hilog.info(DOMAIN, TAG, '开始处理登录请求,userId:%{private}s', userId);
    
    try {
      // 模拟网络请求
      let result = this.callLoginApi(userId);
      
      if (result.success) {
        hilog.info(DOMAIN, TAG, '登录成功,userId:%{private}s', userId);
      } else {
        // WARN级别:潜在问题但业务可继续
        hilog.warn(DOMAIN, TAG, '登录返回异常状态,错误码:%{public}d', result.code);
      }
    } catch (error) {
      // ERROR级别:记录异常信息
      hilog.error(DOMAIN, TAG, '登录过程异常:%{public}s', error.message);
    }
  }
  
  callLoginApi(userId: string) {
    // 模拟实现
    return { success: true, code: 200 };
  }
}

1.3 C++环境下的HiLog使用(Native层)

对于性能敏感或涉及Native代码的场景,HiLog同样提供了C/C++接口。

1.3.1 CMake配置
# CMakeLists.txt
target_link_libraries(entry PUBLIC libhilog_ndk.z.so)
1.3.2 代码实现
#include "hilog/log.h"

#undef LOG_DOMAIN
#undef LOG_TAG
#define LOG_DOMAIN 0x3200  // 业务领域
#define LOG_TAG "NativeEngine"

void ProcessVideoFrame(const char* frameData, int length) {
    // 检查日志级别是否可用
    if (OH_LOG_IsLoggable(LOG_DOMAIN, LOG_TAG, LOG_INFO)) {
        OH_LOG_INFO(LOG_APP, "开始处理视频帧,长度:%{public}d", length);
    }
    
    // 业务处理逻辑
    int result = DecodeFrame(frameData, length);
    
    if (result < 0) {
        // 使用隐私标识:%{private}s 会输出 <private>
        OH_LOG_ERROR(LOG_APP, "视频解码失败,帧数据:%{private}s,错误码:%{public}d", 
                     frameData, result);
    } else {
        OH_LOG_DEBUG(LOG_APP, "视频解码成功,耗时:%{public}dms", result);
    }
}

运行结果示例(DevEco Studio控制台):

12-11 12:21:47.579 2695 2695 I A03200/NativeEngine: 开始处理视频帧,长度:1024
12-11 12:21:47.592 2695 2695 E A03200/NativeEngine: 视频解码失败,帧数据:<private>,错误码:-1

1.4 核心参数详解

参数 说明 使用建议
domain 业务领域标识,取值范围0x0000~0xFFFF 按模块分配固定区间,如登录模块0xF801~0xF820
tag 日志标签,字符串类型,最长31字节 采用“模块_子功能”命名,如Login_Network
level 日志级别:DEBUG/INFO/WARN/ERROR/FATAL 按场景选择,生产环境关闭DEBUG
隐私标识 %{public}s明文显示,%{private}s显示为<private> 敏感信息用private,非敏感用public

二、分级日志规范与性能开销

2.1 五级日志的合理使用场景

很多团队日志写得乱,根本原因是没有明确各级日志的使用场景。以下是基于项目实践总结的使用规范:

2.1.1 DEBUG级别

使用场景:开发阶段临时调试、详细的方法入参、循环内的中间状态。

规范要求

  • 仅在开发环境开启,Release版本必须关闭
  • 避免打印循环体内高频日志
  • 用完及时清理或降级
// 不推荐:循环内打DEBUG日志
for (let i = 0; i < 1000; i++) {
    hilog.debug(DOMAIN, TAG, '当前循环次数:%{public}d', i);  // 1000条日志,性能灾难
}

// 推荐:只在关键节点打印
hilog.debug(DOMAIN, TAG, '开始批量处理,总数量:%{public}d', items.length);
let successCount = 0;
for (let item of items) {
    // 处理逻辑
    successCount++;
}
hilog.debug(DOMAIN, TAG, '批量处理完成,成功数:%{public}d', successCount);
2.1.2 INFO级别

使用场景:关键业务流程节点、状态变更、服务启动/停止、用户可感知的操作。

规范要求

  • 生产环境保留
  • 每条日志有明确的业务含义
  • 配合关键参数打印
// 推荐的INFO日志
hilog.info(DOMAIN, TAG, '应用冷启动完成,耗时:%{public}dms', costTime);
hilog.info(DOMAIN, TAG, '用户【%{private}s】登出系统', userId);
hilog.info(DOMAIN, TAG, '支付订单【%{private}s】状态变更为:%{public}s', orderId, status);
2.1.3 WARN级别

使用场景:预期内的异常情况、降级处理、重试机制触发、配置加载失败但使用默认值。

规范要求

  • 业务仍可继续,不需要立即人工介入
  • 通常伴随重试或降级策略
// 推荐的WARN日志
if (network.isWeak()) {
    hilog.warn(DOMAIN, TAG, '网络信号弱,启用图片降级加载模式');
}

try {
    config = await loadConfig();
} catch (error) {
    hilog.warn(DOMAIN, TAG, '配置加载失败,使用默认配置,错误:%{public}s', error.message);
    config = DEFAULT_CONFIG;
}
2.1.4 ERROR级别

使用场景:业务无法正常进行、关键功能不可用、需要立即定位的异常、捕获的Exception。

规范要求

  • 必须包含可定位的上下文信息(请求ID、关键参数)
  • 需要触发告警
  • 记录堆栈信息
try {
    let result = await api.request(orderData);
} catch (error) {
    // 必须包含traceId或requestId
    hilog.error(DOMAIN, TAG, '下单接口调用失败,订单号:%{private}s,错误:%{public}s,traceId:%{public}s', 
                orderId, error.message, traceId);
    // 触发上报
    reportErrorToServer(error);
}
2.1.5 FATAL级别

使用场景:应用即将崩溃、不可恢复的系统级错误、内存溢出、关键资源无法获取。

规范要求

  • 伴随应用重启或退出
  • 通常由框架层抛出
  • 必须全量上报

2.2 性能开销分析与优化

日志系统如果设计不当,会成为性能瓶颈。以下是HiLog的性能特点及优化建议:

2.2.1 日志打印的性能成本
操作 耗时(μs) 说明
普通函数调用 0.01~0.05 基准参考
DEBUG日志(关闭状态) 0.1~0.5 级别检查开销
INFO日志打印 10~50 格式化+写入缓冲区
ERROR日志打印 20~80 包含额外堆栈信息
字符串拼接 随长度变化 避免在参数中拼接
2.2.2 常见性能陷阱

陷阱1:日志参数预拼接

// 不推荐:无论日志是否打印,都要执行字符串拼接
hilog.debug(DOMAIN, TAG, '用户信息:' + JSON.stringify(userInfo) + ',时间:' + Date.now());

// 推荐:使用格式化参数,只在真正打印时处理
hilog.debug(DOMAIN, TAG, '用户信息:%{public}s,时间:%{public}d', 
            JSON.stringify(userInfo), Date.now());

陷阱2:高频循环打印

// 不推荐:1000次循环打印1000条日志
items.forEach(item => {
    hilog.info(DOMAIN, TAG, '处理项:%{public}s', item.id);
    process(item);
});

// 推荐:聚合打印
hilog.info(DOMAIN, TAG, '开始批量处理,数量:%{public}d', items.length);
let start = Date.now();
items.forEach(item => process(item));
hilog.info(DOMAIN, TAG, '批量处理完成,总耗时:%{public}dms', Date.now() - start);

陷阱3:对象深度序列化

// 不推荐:大对象递归序列化
hilog.error(DOMAIN, TAG, '错误状态:%{public}s', JSON.stringify(complexObject));

// 推荐:只打印关键字段
hilog.error(DOMAIN, TAG, '错误码:%{public}d,错误类型:%{public}s', 
            complexObject.code, complexObject.type);
2.2.3 动态日志级别开关

通过系统配置实现运行时动态调整日志级别,既保证开发期可调试,又保障生产环境性能。

// 日志级别管理器
export class LogManager {
  private static instance: LogManager;
  private logLevelMap: Map<string, number> = new Map();
  private defaultLevel: number = 3; // 默认INFO级别
  
  static getInstance(): LogManager {
    if (!LogManager.instance) {
      LogManager.instance = new LogManager();
    }
    return LogManager.instance;
  }
  
  // 设置模块日志级别
  setModuleLevel(module: string, level: number) {
    this.logLevelMap.set(module, level);
  }
  
  // 检查是否可打印
  isLoggable(module: string, level: number): boolean {
    // 调试模式全开
    if (this.isDebugMode()) return true;
    
    let moduleLevel = this.logLevelMap.get(module) ?? this.defaultLevel;
    return level >= moduleLevel;
  }
  
  // 从云端拉取配置
  async syncConfigFromCloud() {
    try {
      let config = await requestLogConfig();
      Object.entries(config).forEach(([module, level]) => {
        this.setModuleLevel(module, level as number);
      });
    } catch (error) {
      // 使用默认配置
    }
  }
  
  private isDebugMode(): boolean {
    // 判断是否为Debug包
    return __DEV__ === true;
  }
}

// 封装日志工具
export class Logger {
  static debug(module: string, tag: string, msg: string, ...args: any[]) {
    if (LogManager.getInstance().isLoggable(module, 0)) {
      hilog.debug(0xF811, `[${module}]${tag}`, msg, ...args);
    }
  }
  
  static info(module: string, tag: string, msg: string, ...args: any[]) {
    if (LogManager.getInstance().isLoggable(module, 1)) {
      hilog.info(0xF811, `[${module}]${tag}`, msg, ...args);
    }
  }
  
  static error(module: string, tag: string, msg: string, ...args: any[]) {
    if (LogManager.getInstance().isLoggable(module, 4)) {
      hilog.error(0xF811, `[${module}]${tag}`, msg, ...args);
    }
  }
}

// 使用示例
Logger.debug('Login', 'Network', '请求参数:%{public}s', JSON.stringify(params));

三、日志采集与上传策略

3.1 日志采集架构设计

一套完整的日志采集系统需要考虑端侧缓存、网络适配、压缩加密等多个维度。

[应用层] → [日志缓冲区] → [压缩加密] → [网络判断] → [上报服务]
                 ↓
            [本地文件] ← [失败重试]

3.2 本地日志收集实现

3.2.1 使用AVLoggerConnection采集日志

HarmonyOS提供了AVLoggerConnection框架用于日志采集。

// 日志收集管理器
import { AVLoggerConnection } from '@ohos.multimedia.avlogger';

export class LogCollector {
  private loggerConnection: AVLoggerConnection;
  private logBuffer: string[] = [];
  private readonly MAX_BUFFER_SIZE = 100;
  private readonly MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
  
  constructor(context: Context) {
    this.initLoggerConnection(context);
    this.initFileSystem(context);
  }
  
  private initLoggerConnection(context: Context) {
    this.loggerConnection = new AVLoggerConnection(context, {
      onLoggerConnected: () => {
        console.log('日志服务连接成功');
        this.startCollecting();
      },
      onLoggerError: (error) => {
        console.error('日志服务异常:', error);
      }
    });
  }
  
  private startCollecting() {
    // 开始采集系统日志
    this.loggerConnection.on('log', (logData: string) => {
      this.appendToBuffer(logData);
    });
  }
  
  private appendToBuffer(log: string) {
    this.logBuffer.push(log);
    
    // 达到阈值触发写入
    if (this.logBuffer.length >= this.MAX_BUFFER_SIZE) {
      this.flushToFile();
    }
  }
  
  private async flushToFile() {
    if (this.logBuffer.length === 0) return;
    
    try {
      let logs = this.logBuffer.join('\n');
      await this.writeToFile(logs);
      this.logBuffer = [];
    } catch (error) {
      console.error('写入日志文件失败:', error);
    }
  }
  
  private async writeToFile(content: string) {
    // 实现文件写入逻辑
    // 检查文件大小,超过阈值则滚动
  }
  
  // 获取日志文件列表
  async getLogFiles(): Promise<string[]> {
    // 返回日志文件路径列表
    return [];
  }
}
3.2.2 日志文件管理策略
export class LogFileManager {
  private logDir: string;
  private currentFile: string;
  private maxFiles: number = 5; // 最多保留5个文件
  private maxSize: number = 10 * 1024 * 1024; // 单个文件10MB
  
  constructor(context: Context) {
    this.logDir = context.filesDir + '/logs/';
    this.initLogDirectory();
  }
  
  private async initLogDirectory() {
    try {
      let file = await fileIo.access(this.logDir);
      if (!file) {
        await fileIo.mkdir(this.logDir);
      }
      this.cleanExpiredFiles();
    } catch (error) {
      console.error('初始化日志目录失败:', error);
    }
  }
  
  private async cleanExpiredFiles() {
    try {
      let files = await fileIo.listFile(this.logDir);
      let logFiles = files.filter(f => f.endsWith('.log'))
                          .map(f => ({
                            name: f,
                            time: this.extractFileTime(f)
                          }))
                          .sort((a, b) => b.time - a.time);
      
      // 删除超过数量限制的旧文件
      if (logFiles.length >= this.maxFiles) {
        for (let i = this.maxFiles - 1; i < logFiles.length; i++) {
          await fileIo.unlink(this.logDir + logFiles[i].name);
        }
      }
    } catch (error) {
      console.error('清理日志文件失败:', error);
    }
  }
  
  // 获取当前可用的日志文件
  async getWritableFile(): Promise<string> {
    if (!this.currentFile) {
      this.currentFile = this.logDir + `app_${Date.now()}.log`;
    }
    
    // 检查当前文件大小
    let stat = await fileIo.stat(this.currentFile);
    if (stat.size >= this.maxSize) {
      // 滚动新文件
      this.currentFile = this.logDir + `app_${Date.now()}.log`;
    }
    
    return this.currentFile;
  }
}

3.3 日志上传策略

合理的上传策略需要在实时性和成本之间取得平衡。

3.3.1 网络状态感知上传
import { connection } from '@ohos.net.connection';

export class LogUploadStrategy {
  private isWifiConnected: boolean = false;
  private isCharging: boolean = false;
  private uploadQueue: string[] = [];
  private isUploading: boolean = false;
  
  constructor() {
    this.monitorNetworkState();
    this.monitorPowerState();
  }
  
  private monitorNetworkState() {
    connection.on('netAvailable', (data) => {
      let netHandle = data.netHandle;
      // 判断网络类型
      if (netHandle.netId === 100) { // WIFI
        this.isWifiConnected = true;
        this.tryUpload();
      } else {
        this.isWifiConnected = false;
      }
    });
  }
  
  private monitorPowerState() {
    // 监听充电状态
    // 实现略
  }
  
  // 添加日志到上传队列
  addToUploadQueue(logFile: string) {
    this.uploadQueue.push(logFile);
    this.tryUpload();
  }
  
  private tryUpload() {
    // 上传条件判断
    if (this.isUploading) return;
    if (this.uploadQueue.length === 0) return;
    
    // WIFI环境下立即上传
    if (this.isWifiConnected) {
      this.startUpload();
    } 
    // 移动网络下延迟上传并限制大小
    else if (this.isCharging) {
      // 充电时可以使用移动网络上传
      this.startUploadWithLimit();
    }
    // 其他情况等待合适时机
  }
  
  private async startUpload() {
    this.isUploading = true;
    
    while (this.uploadQueue.length > 0) {
      let logFile = this.uploadQueue.shift();
      try {
        await this.uploadLogFile(logFile);
        // 上传成功后删除本地文件
        await fileIo.unlink(logFile);
      } catch (error) {
        console.error('上传失败:', error);
        // 失败重新入队
        this.uploadQueue.unshift(logFile);
        break;
      }
    }
    
    this.isUploading = false;
  }
  
  private async uploadLogFile(filePath: string): Promise<boolean> {
    // 读取文件
    let file = await fileIo.open(filePath);
    let stat = await fileIo.stat(filePath);
    let buffer = new ArrayBuffer(stat.size);
    await fileIo.read(file.fd, buffer);
    await fileIo.close(file.fd);
    
    // 压缩
    let compressed = await this.compressData(buffer);
    
    // 上传
    let response = await request.upload({
      url: 'https://log.agc.example.com/upload',
      files: [{ filename: 'log.gz', uri: `data:application/octet-stream;base64,${compressed}` }],
      data: [{ name: 'appId', value: getAppId() }]
    });
    
    return response.code === 200;
  }
  
  private async compressData(data: ArrayBuffer): Promise<string> {
    // 使用gzip压缩
    // 实际实现需引入zlib或系统压缩库
    return data as any;
  }
}
3.3.2 采样上报策略

对于高频日志,采用采样上报可以有效降低开销。

export class SampleStrategy {
  private sampleRate: number = 0.1; // 10%采样率
  private errorSampleRate: number = 1.0; // 错误日志100%采样
  private slowSampleRate: number = 0.01; // 慢操作1%采样
  
  // 是否上报
  shouldUpload(level: number, eventType: string): boolean {
    // 错误日志全量
    if (level >= 3) { // ERROR及以上
      return true;
    }
    
    // 慢操作低采样
    if (eventType === 'slow_operation') {
      return Math.random() < this.slowSampleRate;
    }
    
    // 普通日志按采样率
    return Math.random() < this.sampleRate;
  }
  
  // 动态调整采样率
  async adjustSampleRate() {
    // 根据当前设备状态调整
    let batteryLevel = await this.getBatteryLevel();
    let memoryLevel = await this.getMemoryLevel();
    
    if (batteryLevel < 20) {
      // 低电量降低采样
      this.sampleRate = 0.01;
    } else if (memoryLevel > 0.8) {
      // 高内存降低采样
      this.sampleRate = 0.05;
    } else {
      // 正常状态
      this.sampleRate = 0.1;
    }
  }
}

3.4 使用LTS Harmony SDK

华为云日志服务(LTS)提供了专门的Harmony SDK,简化了日志上报流程。

3.4.1 集成SDK
# 通过ohpm安装
ohpm install lts_harmony_sdk
// oh-package.json5
{
  "dependencies": {
    "lts_harmony_sdk": "0.0.1"
  }
}
3.4.2 初始化与上报
import { LTSSDK, ConfigParam } from 'lts_harmony_sdk';
import { BusinessError } from '@kit.BasicServicesKit';

export class LTSManager {
  private ltsSDK: LTSSDK | null = null;
  private context: Context;
  
  constructor(context: Context) {
    this.context = context;
  }
  
  init() {
    const config: ConfigParam = {
      // 必填参数
      region: 'cn-north-4',          // 上报region
      projectId: 'your_project_id',   // 华为云项目ID
      groupId: 'your_log_group_id',   // LTS日志组ID
      streamId: 'your_log_stream_id', // LTS日志流ID
      
      // 选填参数
      cacheThreshold: 100,            // 上报条数阈值
      timeInterval: 30,               // 上报时间阈值(秒)
      isReportBackground: true        // 开启后台上报
    };
    
    try {
      let applicationContext = this.context.getApplicationContext();
      this.ltsSDK = new LTSSDK(applicationContext, config);
    } catch (error) {
      console.error('LTS SDK初始化失败:', (error as BusinessError).message);
    }
  }
  
  // 上报单条日志
  reportLog(event: string, data: object) {
    if (!this.ltsSDK) return;
    
    let content = {
      event: event,
      timestamp: Date.now(),
      ...data
    };
    
    let labels = {
      appVersion: getAppVersion(),
      deviceType: getDeviceType(),
      userId: getCurrentUserId()
    };
    
    this.ltsSDK.report(content, labels);
  }
  
  // 立即上报
  reportImmediately(event: string, data: object) {
    if (!this.ltsSDK) return;
    
    this.ltsSDK.reportImmediately({
      event: event,
      timestamp: Date.now(),
      ...data
    });
  }
}

四、AppGallery Connect日志分析

4.1 AGC性能管理服务概述

AppGallery Connect(AGC)提供了开箱即用的性能管理服务(APMS),无需集成SDK即可自动采集应用的崩溃、ANR、页面性能等数据。

核心能力

  • 异常管理:自动采集崩溃、应用无响应等
  • 性能管理:启动耗时、页面切换耗时、网络请求分析
  • 单点查询:查看特定用户的性能数据
  • 日志回捞:按需拉取指定设备的日志

4.2 异常管理:零代码接入

从HarmonyOS 5.0.0 Release开始,应用无需主动集成SDK即可使用异常管理功能。

// 无需额外代码,系统自动采集
// 崩溃发生后,数据会自动上报到AGC

验证上报是否成功

// 使用hilog过滤崩溃上报日志
// 搜索关键字:C04707.*report\s(error|success)

hilog.info(0, 'APMS', 'report success');  // 看到此日志表示上报成功

4.3 单点查询:定位特定用户问题

当遇到特定用户反馈问题时,可以通过单点查询功能分析该用户的性能数据。

4.3.1 设置用户标识
import { apms } from '@kit. PerformanceAnalysisKit';

// 在用户登录后设置
export function setUserIdentifier(userId: string) {
  try {
    apms.setUserIdentifier(userId);
    hilog.info(0xF811, 'APMS', '设置用户标识:%{private}s', userId);
  } catch (error) {
    hilog.error(0xF811, 'APMS', '设置用户标识失败:%{public}s', error.message);
  }
}

// 用户登出时清除
export function clearUserIdentifier() {
  apms.setUserIdentifier('');
}
4.3.2 在AGC控制台查询

登录AppGallery Connect,进入“性能管理” > “单点查询”,输入用户标识即可查看:

  • ANR分析:应用无响应事件的堆栈和上下文
  • 页面分析:各页面的加载耗时、呈现耗时
  • 慢启动追踪:冷启动/热启动的耗时分布
  • 网络分析:接口请求的耗时、成功率

4.4 日志回捞:按需获取详细日志

日志回捞功能允许开发者在需要时,主动拉取指定用户终端上的日志,解决偶现问题定位难的问题。

4.4.1 启用自定义日志
import { apms } from '@kit. PerformanceAnalysisKit';

export class CustomLogger {
  // 记录自定义日志,供回捞使用
  static log(tag: string, message: string) {
    // 使用hilog正常打印
    hilog.info(0xF811, tag, message);
    
    // 同时记录到APMS自定义日志缓冲区
    try {
      apms.recordLog(tag, message);
    } catch (error) {
      // APMS日志缓冲区可能满,忽略错误
    }
  }
  
  // 记录关键事件
  static recordEvent(eventName: string, properties: object) {
    try {
      apms.recordEvent(eventName, JSON.stringify(properties));
    } catch (error) {
      // 忽略
    }
  }
}
4.4.2 创建回捞任务

在AGC控制台操作步骤:

  1. 进入“性能管理” > “日志回捞”
  2. 点击“创建任务”
  3. 填写任务信息:
    • 任务名称:如“定位登录失败问题-用户张三”
    • 拉取时间范围:问题发生前后30分钟
    • 用户标识:输入setUserIdentifier设置的值
  4. 任务完成后,可在详情页查看或下载日志
4.4.3 回捞日志示例
2026-03-09 10:23:15.123 [INFO] [LoginModule] 用户点击登录按钮
2026-03-09 10:23:15.245 [DEBUG] [LoginModule] 请求参数:{"userId":"user_123","timestamp":1741501395245}
2026-03-09 10:23:16.012 [ERROR] [NetworkModule] 登录接口超时,错误码:-1001
2026-03-09 10:23:16.013 [WARN] [LoginModule] 触发重试机制,重试次数:1
2026-03-09 10:23:17.856 [INFO] [LoginModule] 重试成功,登录完成

4.5 崩溃分析实战

4.5.1 崩溃数据采集

系统自动采集的崩溃数据包含以下信息:

{
  "crash_type": "Java Crash",
  "thread_name": "main",
  "exception_type": "NullPointerException",
  "message": "Attempt to invoke virtual method on a null object reference",
  "stacktrace": [
    "at com.example.app.LoginActivity.onClick(LoginActivity.java:125)",
    "at android.view.View.performClick(View.java:7448)"
  ],
  "device_info": {
    "model": "HUAWEI P50",
    "os_version": "HarmonyOS 5.0.0",
    "memory": 6144
  },
  "app_info": {
    "version": "1.2.3",
    "build_id": "20260201"
  }
}
4.5.2 使用hilog辅助分析

当AGC未显示崩溃数据时,可以通过hilog排查:

// 过滤崩溃上报日志
// 命令:hilog | grep C04707

// 成功上报
// 12-11 12:21:47.579 2695 2695 I C04707: report success, reqId: 123456

// 上报失败(网络问题)
// 12-11 12:21:47.579 2695 2695 E C04707: report error, statusCode: 12, errMsg: Rcp error:1007900006

五、实战案例:构建完整的日志系统

5.1 系统架构

结合以上知识点,构建一个完整的日志系统架构:

┌─────────────────────────────────────────────────────────────┐
│                        应用端                                │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐        │
│  │  HiLog API  │  │  自定义Logger│  │ 性能监控    │        │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘        │
│         │                │                 │                │
│         └────────────────┼─────────────────┘                │
│                          ▼                                   │
│  ┌─────────────────────────────────────────────┐           │
│  │          日志处理管道                         │           │
│  │  ┌─────────┐ ┌─────────┐ ┌──────────────┐  │           │
│  │  │级别过滤 │ │采样过滤 │ │隐私脱敏      │  │           │
│  │  └────┬────┘ └────┬────┘ └──────┬───────┘  │           │
│  └───────┼────────────┼──────────────┼─────────┘           │
│          │            │              │                       │
│          ▼            ▼              ▼                       │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐           │
│  │本地文件存储 │ │实时控制台   │ │LTS/AGC上报  │           │
│  └─────────────┘ └─────────────┘ └──────┬──────┘           │
└──────────────────────────────────────────┼──────────────────┘
                                           │
                                           ▼
┌─────────────────────────────────────────────────────────────┐
│                          云端                                │
│  ┌─────────────────────────────────────────────┐           │
│  │      AppGallery Connect                       │           │
│  │  ┌─────────────┐ ┌─────────────┐            │           │
│  │  │ 异常管理    │ │ 性能管理    │            │           │
│  │  └─────────────┘ └─────────────┘            │           │
│  │  ┌─────────────┐ ┌─────────────┐            │           │
│  │  │ 日志回捞    │ │ 单点查询    │            │           │
│  │  └─────────────┘ └─────────────┘            │           │
│  └─────────────────────────────────────────────┘           │
│                                                             │
│  ┌─────────────────────────────────────────────┐           │
│  │      华为云LTS(可选)                         │           │
│  │  实时日志分析/告警/可视化                      │           │
│  └─────────────────────────────────────────────┘           │
└─────────────────────────────────────────────────────────────┘

5.2 完整实现代码

5.2.1 日志配置管理
// log-config.ts
export interface LogConfig {
  domain: number;
  globalTag: string;
  logDir: string;
  maxFileSize: number;      // 字节
  maxFileCount: number;
  uploadInterval: number;    // 秒
  uploadThreshold: number;   // 条数
  sampleRate: number;        // 0-1
}

export class LogConfigManager {
  private static instance: LogConfigManager;
  private config: LogConfig;
  
  private constructor() {
    // 默认配置
    this.config = {
      domain: 0xF811,
      globalTag: 'MyApp',
      logDir: '/logs/',
      maxFileSize: 10 * 1024 * 1024,
      maxFileCount: 5,
      uploadInterval: 30,
      uploadThreshold: 100,
      sampleRate: 0.1
    };
  }
  
  static getInstance(): LogConfigManager {
    if (!LogConfigManager.instance) {
      LogConfigManager.instance = new LogConfigManager();
    }
    return LogConfigManager.instance;
  }
  
  async loadConfig() {
    try {
      // 尝试从本地加载配置
      let localConfig = await this.loadLocalConfig();
      if (localConfig) {
        this.config = { ...this.config, ...localConfig };
      }
      
      // 从云端加载远程配置(可选)
      let remoteConfig = await this.fetchRemoteConfig();
      if (remoteConfig) {
        this.config = { ...this.config, ...remoteConfig };
      }
    } catch (error) {
      console.error('加载日志配置失败,使用默认配置', error);
    }
  }
  
  getConfig(): LogConfig {
    return { ...this.config };
  }
  
  private async loadLocalConfig(): Promise<Partial<LogConfig> | null> {
    // 从首选项或文件读取
    return null;
  }
  
  private async fetchRemoteConfig(): Promise<Partial<LogConfig> | null> {
    // 从AGC远程配置拉取
    return null;
  }
}
5.2.2 统一的日志管理器
// log-manager.ts
import { hilog } from '@kit.PerformanceAnalysisKit';
import { LogConfig, LogConfigManager } from './log-config';
import { LogFileManager } from './log-file-manager';
import { LogUploadManager } from './log-upload';
import { BusinessError } from '@kit.BasicServicesKit';

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

export class LogManager {
  private static instance: LogManager;
  private config: LogConfig;
  private fileManager: LogFileManager;
  private uploadManager: LogUploadManager;
  private moduleLevels: Map<string, LogLevel> = new Map();
  private context: Context;
  
  private constructor(context: Context) {
    this.context = context;
    this.config = LogConfigManager.getInstance().getConfig();
    this.fileManager = new LogFileManager(context);
    this.uploadManager = new LogUploadManager(context);
    this.init();
  }
  
  static getInstance(context?: Context): LogManager {
    if (!LogManager.instance && context) {
      LogManager.instance = new LogManager(context);
    }
    return LogManager.instance;
  }
  
  private async init() {
    await LogConfigManager.getInstance().loadConfig();
    this.config = LogConfigManager.getInstance().getConfig();
    
    // 初始化上传定时器
    setInterval(() => {
      this.uploadManager.tryUpload();
    }, this.config.uploadInterval * 1000);
  }
  
  // 设置模块日志级别
  setModuleLevel(module: string, level: LogLevel) {
    this.moduleLevels.set(module, level);
  }
  
  // 检查是否可打印
  isLoggable(module: string, level: LogLevel): boolean {
    // Debug包全开
    if (this.isDebugMode()) return true;
    
    let moduleLevel = this.moduleLevels.get(module) ?? LogLevel.INFO;
    return level >= moduleLevel;
  }
  
  private isDebugMode(): boolean {
    // 判断是否为Debug包
    return __DEV__ === true;
  }
  
  // 核心日志方法
  log(level: LogLevel, module: string, tag: string, message: string, ...args: any[]) {
    if (!this.isLoggable(module, level)) return;
    
    // 处理隐私标识
    let formattedMsg = this.formatMessage(message, args);
    
    // 构建完整日志
    let logEntry = {
      timestamp: Date.now(),
      level: LogLevel[level],
      module: module,
      tag: tag,
      pid: ProcessManager.getPid(),
      tid: ThreadManager.getTid(),
      message: formattedMsg,
      traceId: this.getTraceId()
    };
    
    // 输出到hilog
    this.outputToHilog(level, module, tag, message, args);
    
    // 写入文件
    this.fileManager.appendLog(JSON.stringify(logEntry) + '\n');
    
    // 错误日志立即触发上传条件
    if (level >= LogLevel.ERROR) {
      this.uploadManager.markHasError();
    }
  }
  
  private outputToHilog(level: LogLevel, module: string, tag: string, msg: string, args: any[]) {
    let fullTag = `[${module}]${tag}`;
    
    switch (level) {
      case LogLevel.DEBUG:
        hilog.debug(this.config.domain, fullTag, msg, ...args);
        break;
      case LogLevel.INFO:
        hilog.info(this.config.domain, fullTag, msg, ...args);
        break;
      case LogLevel.WARN:
        hilog.warn(this.config.domain, fullTag, msg, ...args);
        break;
      case LogLevel.ERROR:
        hilog.error(this.config.domain, fullTag, msg, ...args);
        break;
      case LogLevel.FATAL:
        hilog.fatal(this.config.domain, fullTag, msg, ...args);
        break;
    }
  }
  
  private formatMessage(msg: string, args: any[]): string {
    // 简单实现,实际应替换占位符
    return msg;
  }
  
  private getTraceId(): string {
    // 从上下文获取或生成traceId
    return TraceContext.getCurrentId() || 'no-trace';
  }
  
  // 快捷方法
  debug(module: string, tag: string, msg: string, ...args: any[]) {
    this.log(LogLevel.DEBUG, module, tag, msg, ...args);
  }
  
  info(module: string, tag: string, msg: string, ...args: any[]) {
    this.log(LogLevel.INFO, module, tag, msg, ...args);
  }
  
  warn(module: string, tag: string, msg: string, ...args: any[]) {
    this.log(LogLevel.WARN, module, tag, msg, ...args);
  }
  
  error(module: string, tag: string, msg: string, ...args: any[]) {
    this.log(LogLevel.ERROR, module, tag, msg, ...args);
  }
  
  fatal(module: string, tag: string, msg: string, ...args: any[]) {
    this.log(LogLevel.FATAL, module, tag, msg, ...args);
  }
}

// 导出便捷函数
export const log = LogManager.getInstance();

// 使用示例
// log.debug('Login', 'UI', '按钮点击 %{public}s', '登录');
5.2.3 应用入口初始化
// EntryAbility.ets
import { LogManager } from './utils/log-manager';
import { apms } from '@kit.PerformanceAnalysisKit';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
    // 初始化日志系统
    const logManager = LogManager.getInstance(this.context);
    
    // 设置模块日志级别
    if (launchParam.launchReason === AbilityConstant.LaunchReason.NORMAL) {
      logManager.setModuleLevel('Network', LogLevel.INFO);
      logManager.setModuleLevel('Database', LogLevel.WARN);
    }
    
    // 设置APMS用户标识(如果已登录)
    let userId = this.getStoredUserId();
    if (userId) {
      apms.setUserIdentifier(userId);
    }
    
    logManager.info('System', 'Startup', '应用启动,原因:%{public}s', launchParam.launchReason);
  }
  
  onDestroy() {
    LogManager.getInstance().info('System', 'Lifecycle', '应用销毁');
  }
}

5.3 最佳实践总结

5.3.1 Domain和Tag命名规范

根据社区实践,推荐以下命名规范:

// domain按业务线分配
export enum DomainConstant {
  SYSTEM = 0xF001,    // 系统基础
  LOGIN = 0xF101,     // 登录模块
  PAY = 0xF201,       // 支付模块
  MESSAGE = 0xF301,   // 消息模块
  PROFILE = 0xF401,   // 个人中心
}

// tag采用"模块_子功能"格式
// Login_UI, Login_Network, Login_Storage
// Pay_Order, Pay_Result, Pay_WebView
5.3.2 敏感信息处理规则
数据类型 处理方式 示例
用户ID 脱敏显示前3后4 user_***_1234
手机号 完全脱敏 <phone>
Token 完全脱敏 <token>
订单号 显示后6位 ******123456
IP地址 显示前3段 192.168.1.***
请求参数 结构化脱敏 {"password":"<private>"}
5.3.3 性能优化清单
  • ✅ DEBUG日志只在开发环境开启
  • ✅ 循环内避免打印日志
  • ✅ 使用参数占位符代替字符串拼接
  • ✅ 大对象只打印关键字段
  • ✅ 设置合理的缓冲区大小
  • ✅ 异步写入文件
  • ✅ 采样上报高频日志
  • ✅ 压缩后再上传

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐