HarmonyOS开发:线上监控

📌 核心要点:线上监控是应用的"心电图"——崩溃率、ANR率、启动时间、关键转化率,这些指标实时跳动,异常时立刻告警,让你在用户投诉之前就发现问题。

背景与动机

你发了一个新版本,心里忐忑但自我安慰"应该没问题"。三天后,客服反馈说用户投诉量暴增。你打开后台一看,崩溃率从0.1%飙升到3%,已经炸了三天了。

三天!你居然三天之后才知道出了问题。

没有监控的应用就像蒙着眼开车——你不知道前面是坦途还是悬崖,直到撞上去。

线上监控要解决的核心问题:

  • 崩溃监控:应用崩溃了多少次?什么原因?哪些设备上?
  • ANR监控:应用卡死了多少次?卡在哪个函数?
  • 性能监控:启动时间多少?帧率多少?内存占用多少?
  • 业务监控:关键转化率是否正常?支付成功率有没有下降?
  • 告警通知:指标异常时第一时间通知到人,而不是三天后才知道

鸿蒙应用的线上监控有其特殊性——HiLog日志体系、HiAppEvent事件打点、HiSysEvent系统事件,这些都是鸿蒙特有的监控基础设施。

核心原理

线上监控体系的架构:数据采集 → 数据上报 → 数据分析 → 告警通知

告警层

分析层

上报层

采集层

严重

可自动处理

崩溃采集

ANR采集

性能采集

业务事件采集

批量聚合

压缩加密

网络上报

实时计算

趋势分析

异常检测

指标异常?

钉钉/飞书告警

短信/电话告警

自动回滚

✅ 正常

鸿蒙监控基础设施:

工具 用途 特点
HiLog 日志输出 分级日志(DEBUG/INFO/WARN/ERROR/FATAL)
HiAppEvent 应用事件打点 结构化事件,支持自定义参数
HiSysEvent 系统事件监听 监听系统级事件(崩溃、ANR等)
HiTraceMeter 性能追踪 函数级耗时追踪
HiCollie 看门狗 检测应用卡死(ANR)

代码实战

基础用法:崩溃监控

崩溃是最严重的线上问题,必须第一时间捕获和上报。

// entry/src/main/ets/monitor/CrashMonitor.ets
// 崩溃监控器

import { hiAppEvent } from '@kit.AnalysisHiAppEvent';
import { hilog } from '@kit.PerformanceAnalysisKit';

export class CrashMonitor {
  private static DOMAIN = 0x0001;  // 应用事件域
  private static TAG = 'CrashMonitor';
  
  /**
   * 初始化崩溃监控
   * 在Ability的onCreate中调用
   */
  static init(): void {
    // 1. 配置HiAppEvent
    hiAppEvent.configure({
      disable: false,           // 启用事件打点
      maxStorage: '100M',       // 最大存储空间
    });
    
    // 2. 注册崩溃回调
    // HarmonyOS会在应用崩溃时写入崩溃日志
    // 我们在下次启动时读取并上报
    
    hilog.info(0x0001, this.TAG, '崩溃监控已初始化');
  }
  
  /**
   * 检查上次是否有崩溃
   * 在应用启动时调用
   */
  static async checkLastCrash(): Promise<void> {
    try {
      // 检查崩溃标记文件
      const crashFlag = await this.readCrashFlag();
      
      if (crashFlag && crashFlag.hasCrash) {
        hilog.warn(0x0001, this.TAG, `检测到上次崩溃: ${crashFlag.crashTime}`);
        
        // 读取崩溃堆栈
        const crashStack = await this.readCrashStack();
        
        // 上报崩溃信息
        await this.reportCrash({
          crashTime: crashFlag.crashTime,
          crashType: crashFlag.crashType || 'unknown',
          crashStack: crashStack,
          appVersion: crashFlag.appVersion,
          deviceInfo: await this.getDeviceInfo(),
        });
        
        // 清除崩溃标记
        await this.clearCrashFlag();
      }
      
      // 设置当前运行标记(用于检测下次是否崩溃)
      await this.setRunningFlag();
    } catch (error) {
      hilog.error(0x0001, this.TAG, `崩溃检查失败: ${error}`);
    }
  }
  
  /**
   * 手动上报异常
   * 用于捕获try-catch中的异常
   */
  static async reportError(error: Error, context?: string): Promise<void> {
    const errorInfo = {
      errorType: error.name,
      errorMessage: error.message,
      errorStack: error.stack || '',
      context: context || '',
      timestamp: Date.now(),
      appVersion: await this.getAppVersion(),
      deviceInfo: await this.getDeviceInfo(),
    };
    
    // 使用HiAppEvent打点
    hiAppEvent.write({
      domain: this.DOMAIN,
      name: 'APP_ERROR',
      eventType: hiAppEvent.EventType.FAULT,
      params: errorInfo,
    });
    
    // 同时上报到自建监控平台
    await this.uploadToMonitorServer(errorInfo);
    
    hilog.error(0x0001, this.TAG, `异常已上报: ${error.name} - ${error.message}`);
  }
  
  /**
   * 上报崩溃信息到监控服务端
   */
  private static async reportCrash(crashInfo: CrashInfo): Promise<void> {
    // 使用HiAppEvent打点(华为分析平台可查看)
    hiAppEvent.write({
      domain: this.DOMAIN,
      name: 'APP_CRASH',
      eventType: hiAppEvent.EventType.FAULT,
      params: crashInfo,
    });
    
    // 上报到自建监控平台
    await this.uploadToMonitorServer(crashInfo);
  }
  
  /**
   * 上报到自建监控服务端
   */
  private static async uploadToMonitorServer(data: Record<string, Object>): Promise<void> {
    try {
      // 实际实现使用http模块上报
      // 这里简化处理
      hilog.info(0x0001, this.TAG, '监控数据已上报');
    } catch (error) {
      hilog.error(0x0001, this.TAG, `监控数据上报失败: ${error}`);
    }
  }
  
  // 辅助方法(简化实现)
  private static async readCrashFlag(): Promise<CrashFlag | null> { return null; }
  private static async readCrashStack(): Promise<string> { return ''; }
  private static async clearCrashFlag(): Promise<void> {}
  private static async setRunningFlag(): Promise<void> {}
  private static async getAppVersion(): Promise<string> { return '3.2.0'; }
  private static async getDeviceInfo(): Promise<DeviceInfo> {
    return { model: 'unknown', osVersion: 'unknown', memory: 0 };
  }
}

interface CrashInfo {
  crashTime: number;
  crashType: string;
  crashStack: string;
  appVersion: string;
  deviceInfo: DeviceInfo;
}

interface CrashFlag {
  hasCrash: boolean;
  crashTime: number;
  crashType?: string;
  appVersion?: string;
}

interface DeviceInfo {
  model: string;
  osVersion: string;
  memory: number;
}

进阶用法:ANR监控与性能监控

ANR(Application Not Responding)是用户最反感的体验之一——应用卡住了,啥也干不了。

// entry/src/main/ets/monitor/PerformanceMonitor.ets
// 性能监控器

import { hiAppEvent } from '@kit.AnalysisHiAppEvent';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';

export class PerformanceMonitor {
  private static DOMAIN = 0x0001;
  private static TAG = 'PerfMonitor';
  
  // 性能指标阈值
  private static readonly THRESHOLDS = {
    coldStartMs: 3000,     // 冷启动阈值:3秒
    hotStartMs: 1000,      // 热启动阈值:1秒
    pageRenderMs: 500,     // 页面渲染阈值:500ms
    frameDropRate: 5,      // 掉帧率阈值:5%
    memoryWarningMb: 400,  // 内存告警阈值:400MB
  };
  
  /**
   * 监控冷启动耗时
   */
  static monitorColdStart(): void {
    const startTime = Date.now();
    
    // 在EntryAbility的onWindowStageCreate中结束计时
    // 实际使用时通过回调或全局变量传递startTime
    AppStorage.setOrCreate('cold_start_time', startTime);
  }
  
  /**
   * 记录冷启动完成
   */
  static reportColdStartComplete(): void {
    const startTime = AppStorage.get<number>('cold_start_time', 0);
    if (startTime === 0) return;
    
    const duration = Date.now() - startTime;
    const isSlow = duration > this.THRESHOLDS.coldStartMs;
    
    hiAppEvent.write({
      domain: this.DOMAIN,
      name: 'COLD_START',
      eventType: hiAppEvent.EventType.STATISTIC,
      params: {
        duration: duration,
        isSlow: isSlow,
        threshold: this.THRESHOLDS.coldStartMs,
      }
    });
    
    if (isSlow) {
      hilog.warn(0x0001, this.TAG, `冷启动耗时过长: ${duration}ms (阈值: ${this.THRESHOLDS.coldStartMs}ms)`);
    } else {
      hilog.info(0x0001, this.TAG, `冷启动耗时: ${duration}ms`);
    }
  }
  
  /**
   * 监控页面渲染耗时
   */
  static startPageTrace(pageName: string): string {
    const traceId = `page_${pageName}_${Date.now()}`;
    hiTraceMeter.startTrace(traceId, 1);
    return traceId;
  }
  
  static finishPageTrace(traceId: string, pageName: string): void {
    hiTraceMeter.finishTrace(traceId, 1);
    
    // 记录页面渲染事件
    hiAppEvent.write({
      domain: this.DOMAIN,
      name: 'PAGE_RENDER',
      eventType: hiAppEvent.EventType.STATISTIC,
      params: {
        pageName: pageName,
        timestamp: Date.now(),
      }
    });
  }
  
  /**
   * 监控函数执行耗时
   */
  static async measureFunction<T>(
    funcName: string,
    fn: () => Promise<T>,
    thresholdMs: number = 1000
  ): Promise<T> {
    const startTime = Date.now();
    
    try {
      const result = await fn();
      const duration = Date.now() - startTime;
      
      if (duration > thresholdMs) {
        hilog.warn(0x0001, this.TAG, `函数 ${funcName} 耗时过长: ${duration}ms`);
        
        hiAppEvent.write({
          domain: this.DOMAIN,
          name: 'SLOW_FUNCTION',
          eventType: hiAppEvent.EventType.STATISTIC,
          params: {
            functionName: funcName,
            duration: duration,
            threshold: thresholdMs,
          }
        });
      }
      
      return result;
    } catch (error) {
      const duration = Date.now() - startTime;
      
      // 函数执行异常也要记录
      hiAppEvent.write({
        domain: this.DOMAIN,
        name: 'FUNCTION_ERROR',
        eventType: hiAppEvent.EventType.FAULT,
        params: {
          functionName: funcName,
          duration: duration,
          error: String(error),
        }
      });
      
      throw error;
    }
  }
  
  /**
   * 监控内存使用
   */
  static checkMemoryWarning(): void {
    // 获取当前内存使用
    // 实际实现通过性能API获取
    const currentMemoryMb = 0; // 占位
    
    if (currentMemoryMb > this.THRESHOLDS.memoryWarningMb) {
      hilog.warn(0x0001, this.TAG, `内存使用过高: ${currentMemoryMb}MB`);
      
      hiAppEvent.write({
        domain: this.DOMAIN,
        name: 'MEMORY_WARNING',
        eventType: hiAppEvent.EventType.STATISTIC,
        params: {
          memoryMb: currentMemoryMb,
          threshold: this.THRESHOLDS.memoryWarningMb,
        }
      });
    }
  }
}

完整示例:告警规则与通知

监控数据采集了,但没人看等于白采。告警系统是监控的"嘴巴"——指标异常时主动通知到人。

# alert_service.py - 告警服务
import time
import json
import requests
from datetime import datetime, timedelta
from typing import List, Dict, Optional

class AlertRule:
    """告警规则"""
    def __init__(self, name: str, metric: str, operator: str,
                 threshold: float, duration_minutes: int = 5,
                 severity: str = 'warning', channels: List[str] = None):
        self.name = name
        self.metric = metric
        self.operator = operator  # gt, lt, eq, gte, lte
        self.threshold = threshold
        self.duration_minutes = duration_minutes
        self.severity = severity  # info, warning, critical
        self.channels = channels or ['dingtalk']
    
    def evaluate(self, value: float) -> bool:
        """评估告警条件"""
        ops = {
            'gt': lambda v, t: v > t,
            'lt': lambda v, t: v < t,
            'eq': lambda v, t: v == t,
            'gte': lambda v, t: v >= t,
            'lte': lambda v, t: v <= t,
        }
        return ops.get(self.operator, lambda v, t: False)(value, self.threshold)


class AlertService:
    """告警服务"""
    
    # 预定义告警规则
    DEFAULT_RULES = [
        AlertRule(
            name='崩溃率过高',
            metric='crash_rate',
            operator='gt',
            threshold=1.0,  # 崩溃率 > 1%
            duration_minutes=5,
            severity='critical',
            channels=['dingtalk', 'sms']
        ),
        AlertRule(
            name='ANR率过高',
            metric='anr_rate',
            operator='gt',
            threshold=0.5,  # ANR率 > 0.5%
            duration_minutes=10,
            severity='warning',
            channels=['dingtalk']
        ),
        AlertRule(
            name='冷启动耗时过长',
            metric='cold_start_p99',
            operator='gt',
            threshold=5000,  # P99 > 5秒
            duration_minutes=30,
            severity='warning',
            channels=['dingtalk']
        ),
        AlertRule(
            name='关键转化率下降',
            metric='key_conversion_rate',
            operator='lt',
            threshold=80,  # 转化率 < 80%
            duration_minutes=60,
            severity='warning',
            channels=['dingtalk']
        ),
        AlertRule(
            name='API错误率过高',
            metric='api_error_rate',
            operator='gt',
            threshold=5.0,  # 错误率 > 5%
            duration_minutes=5,
            severity='critical',
            channels=['dingtalk', 'sms']
        ),
    ]
    
    def __init__(self, monitor_api_url: str):
        self.monitor_api_url = monitor_api_url
        self.rules = self.DEFAULT_RULES.copy()
        self.alert_history: List[dict] = []
        self.last_alert_time: Dict[str, datetime] = {}
    
    def add_rule(self, rule: AlertRule):
        """添加自定义告警规则"""
        self.rules.append(rule)
    
    def check_alerts(self, metrics: Dict[str, float]) -> List[dict]:
        """检查所有告警规则"""
        triggered_alerts = []
        
        for rule in self.rules:
            value = metrics.get(rule.metric)
            if value is None:
                continue
            
            if rule.evaluate(value):
                # 检查是否在静默期内(避免频繁告警)
                if self._is_in_silence_period(rule.name):
                    continue
                
                alert = {
                    'rule_name': rule.name,
                    'metric': rule.metric,
                    'current_value': value,
                    'threshold': rule.threshold,
                    'operator': rule.operator,
                    'severity': rule.severity,
                    'channels': rule.channels,
                    'timestamp': datetime.now().isoformat(),
                }
                
                triggered_alerts.append(alert)
                self.last_alert_time[rule.name] = datetime.now()
                self.alert_history.append(alert)
        
        return triggered_alerts
    
    def send_alerts(self, alerts: List[dict]):
        """发送告警通知"""
        for alert in alerts:
            for channel in alert['channels']:
                if channel == 'dingtalk':
                    self._send_dingtalk(alert)
                elif channel == 'sms':
                    self._send_sms(alert)
                elif channel == 'feishu':
                    self._send_feishu(alert)
    
    def _is_in_silence_period(self, rule_name: str) -> bool:
        """检查是否在静默期内(同一规则30分钟内不重复告警)"""
        last_time = self.last_alert_time.get(rule_name)
        if last_time and (datetime.now() - last_time) < timedelta(minutes=30):
            return True
        return False
    
    def _send_dingtalk(self, alert: dict):
        """发送钉钉告警"""
        severity_emoji = {
            'info': 'ℹ️',
            'warning': '⚠️',
            'critical': '🚨',
        }
        
        message = {
            "msgtype": "markdown",
            "markdown": {
                "title": f"{severity_emoji.get(alert['severity'], '⚠️')} {alert['rule_name']}",
                "text": f"""
## {severity_emoji.get(alert['severity'], '⚠️')} {alert['rule_name']}

**严重程度**: {alert['severity'].upper()}
**指标**: {alert['metric']}
**当前值**: {alert['current_value']}
**阈值**: {alert['threshold']}
**时间**: {alert['timestamp']}

> 请尽快处理!
"""
            }
        }
        
        # 发送到钉钉Webhook
        webhook_url = "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN"
        try:
            requests.post(webhook_url, json=message, timeout=5)
        except Exception as e:
            print(f"钉钉告警发送失败: {e}")
    
    def _send_sms(self, alert: dict):
        """发送短信告警(仅critical级别)"""
        if alert['severity'] != 'critical':
            return
        
        # 调用短信API
        print(f"📱 短信告警: {alert['rule_name']} - 当前值 {alert['current_value']}")
    
    def _send_feishu(self, alert: dict):
        """发送飞书告警"""
        print(f"📨 飞书告警: {alert['rule_name']}")


# 告警服务运行循环
def run_alert_service():
    """定时检查告警"""
    alert_service = AlertService(monitor_api_url="https://your-monitor-api.com")
    
    while True:
        try:
            # 1. 获取最新监控指标
            metrics = fetch_latest_metrics()
            
            # 2. 检查告警
            alerts = alert_service.check_alerts(metrics)
            
            # 3. 发送告警
            if alerts:
                alert_service.send_alerts(alerts)
                for alert in alerts:
                    print(f"🚨 告警触发: {alert['rule_name']} (当前值: {alert['current_value']})")
            
        except Exception as e:
            print(f"告警检查异常: {e}")
        
        # 每分钟检查一次
        time.sleep(60)


def fetch_latest_metrics() -> Dict[str, float]:
    """从监控平台获取最新指标"""
    # 实际实现从监控API获取
    return {
        'crash_rate': 0.3,
        'anr_rate': 0.1,
        'cold_start_p99': 2800,
        'key_conversion_rate': 92,
        'api_error_rate': 1.2,
    }


if __name__ == '__main__':
    run_alert_service()

踩坑与注意事项

坑1:监控数据量太大

每个操作都打点,每天产生几亿条事件,存储成本暴增,查询变慢。

解决方案:分级采集,关键事件全量采集,普通事件采样采集。

// 采样采集策略
class SamplingStrategy {
  // 关键事件:100%采集
  static readonly CRITICAL_EVENTS = ['APP_CRASH', 'APP_ANR', 'PAYMENT_FAILED'];
  
  // 普通事件:10%采样
  static shouldSample(eventName: string): boolean {
    if (this.CRITICAL_EVENTS.includes(eventName)) {
      return true;  // 关键事件始终采集
    }
    
    // 基于用户ID哈希采样
    const userId = this.getCurrentUserId();
    const hash = this.simpleHash(userId);
    return (hash % 100) < 10;  // 10%采样率
  }
  
  private static simpleHash(str: string): number {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      hash = ((hash << 5) - hash) + str.charCodeAt(i);
      hash = hash & hash;
    }
    return Math.abs(hash);
  }
  
  private static getCurrentUserId(): string {
    return 'user_id_placeholder';
  }
}

坑2:告警风暴

一个指标异常触发几十条告警,手机被轰炸,反而忽略了真正重要的信息。

解决方案:告警聚合+静默期+分级通知。

# 告警聚合:同一规则30分钟内只告警一次
# critical级别:钉钉+短信
# warning级别:仅钉钉
# info级别:仅记录,不通知

# 静默期配置
SILENCE_PERIODS = {
    'critical': timedelta(minutes=15),  # critical级别15分钟静默
    'warning': timedelta(minutes=30),   # warning级别30分钟静默
    'info': timedelta(hours=1),         # info级别1小时静默
}

坑3:监控影响应用性能

打点操作太频繁,本身就成了性能瓶颈。

解决方案:批量上报、异步写入、限制打点频率。

// 批量上报管理器
class BatchReporter {
  private static eventQueue: Array<Record<string, Object>> = [];
  private static readonly MAX_BATCH_SIZE = 50;  // 最大批量大小
  private static readonly FLUSH_INTERVAL = 30000;  // 30秒上报一次
  private static flushTimer: number | null = null;
  
  /**
   * 添加事件到队列
   */
  static addEvent(event: Record<string, Object>): void {
    this.eventQueue.push(event);
    
    // 队列满了立即上报
    if (this.eventQueue.length >= this.MAX_BATCH_SIZE) {
      this.flush();
    }
    
    // 启动定时上报
    if (this.flushTimer === null) {
      this.flushTimer = setTimeout(() => {
        this.flush();
      }, this.FLUSH_INTERVAL);
    }
  }
  
  /**
   * 批量上报
   */
  static async flush(): Promise<void> {
    if (this.eventQueue.length === 0) return;
    
    const batch = [...this.eventQueue];
    this.eventQueue = [];
    
    // 清除定时器
    if (this.flushTimer !== null) {
      clearTimeout(this.flushTimer);
      this.flushTimer = null;
    }
    
    // 异步上报
    try {
      await this.uploadBatch(batch);
    } catch (error) {
      // 上报失败,重新放回队列(限制重试次数)
      if (this.eventQueue.length < this.MAX_BATCH_SIZE * 2) {
        this.eventQueue.unshift(...batch);
      }
    }
  }
  
  private static async uploadBatch(events: Array<Record<string, Object>>): Promise<void> {
    // 实际上报逻辑
  }
}

坑4:崩溃堆栈被混淆

Release构建的代码是混淆过的,崩溃堆栈里全是a.b.c,根本看不懂。

解决方案:保留sourceMap文件,用反混淆工具还原。

# 构建时保留sourceMap
# build-profile.json5中配置
{
  "arkOptions": {
    "compilerOptions": {
      "sourceMap": true  # 生成sourceMap
    }
  }
}

# sourceMap文件位置
# entry/build/default/outputs/default/sourceMaps/

# 崩溃堆栈反混淆
# 使用DevEco Studio的反混淆工具
# Tools → Reverse Code Obfuscation

坑5:ANR检测不准确

HiCollie看门狗的检测阈值是固定的,有些场景(比如首次加载大量数据)会误报ANR。

解决方案:对已知耗时场景设置白名单,或者调整检测阈值。

// ANR白名单
const ANR_WHITELIST = [
  'FirstLaunchActivity',   // 首次启动允许较长时间
  'DataMigrationPage',     // 数据迁移允许较长时间
];

function shouldReportANR(activityName: string, durationMs: number): boolean {
  if (ANR_WHITELIST.includes(activityName)) {
    return durationMs > 10000;  // 白名单页面10秒才算ANR
  }
  return durationMs > 5000;     // 普通页面5秒算ANR
}

HarmonyOS 6适配说明

HarmonyOS 6对线上监控的改进:

  1. HiAppEvent 2.0:新版事件打点支持更丰富的参数类型和更灵活的事件定义。

  2. 实时崩溃上报:HarmonyOS 6支持崩溃实时上报,不需要等到下次启动。

  3. ANR检测增强:HiCollie 2.0支持自定义检测阈值和主线程卡顿检测。

  4. 性能基线:HarmonyOS 6提供了各设备型号的性能基线数据,可以对比判断应用性能是否正常。

  5. 隐私合规:HiAppEvent 2.0内置隐私合规检查,确保采集的数据不包含用户敏感信息。

总结

线上监控是应用运营的"眼睛"——没有监控,你就是在黑暗中摸索,用户出了问题你比谁都晚知道。

维度 评价
学习难度 ⭐⭐⭐⭐ 监控体系搭建需要理解HiAppEvent、告警规则设计、数据采集策略
使用频率 ⭐⭐⭐⭐⭐ 监控是持续运行的,7x24小时不间断
重要程度 ⭐⭐⭐⭐⭐ 没有监控的应用就是"裸奔",线上问题只能靠用户反馈

几个关键提醒:

  • 崩溃监控是底线,应用上线第一天就要有
  • 告警要精准,告警太多等于没有告警,关键指标异常才告警
  • 监控数据要分级,关键事件全量采集,普通事件采样采集
  • 批量上报,别每次打点都发网络请求,攒一批一起发
  • sourceMap必须保留,混淆后的崩溃堆栈没有sourceMap就是废纸

监控搭好了,出了问题怎么定位?下一篇文章讲故障排查——从日志分析到堆栈解读,从远程调试到故障复盘。

Logo

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

更多推荐