鸿蒙新特性 | 日志与断点——console.log 高级玩法

一、我们想解决什么问题
1.1 想象一下这个场景
你是一个餐厅厨师,做了一道新菜,端出去之后客人皱眉退回来了。你问哪里不对,客人只说"不好吃"。不好吃?这信息量太少了——是咸了、淡了、火候过了、还是摆盘不好看?你根本不知道从哪个方向改进。
应用开发也是这样。想象你写了一个 HarmonyOS 应用,用户反馈"卡",你怎么办?用户点击按钮没反应,你怎么办?数据加载失败了,你怎么办?
这时候你有两个武器:日志和断点。日志就像餐厅的服务员记录单——每一道菜出了厨房就登记一下"已出餐、时间:14:32、厨师:张三",出了问题你去查记录就知道哪一步出了问题。断点则像厨房里的暂停键——你让厨师先别动,自己走过去亲眼看他做这一步是怎么做的。
1.2 console.log 的"不好用"时刻
HarmonyOS 本身提供了两套日志体系:
- ArkUI 的 console:适合页面脚本层(ArkTS/ArkUI),用法接近 Web 前端开发者的习惯。
- hilog 日志系统:适合原生模块层(Native/C++),面向系统级开发,性能更强,功能更丰富。
但很多开发者在实际使用中遇到了这些问题:
问题一:日志看不清楚。 打印一个复杂对象,结果只显示 [object Object]。你本想看里面有哪些字段,结果一无所获。
问题二:线上日志太多太杂。 调试的时候拼命加日志,发布的时候全堆在那里,既占空间又影响性能,还容易被别人看到敏感信息。
问题三:异步代码不知道哪一步卡住了。 Promise 链、async/await 写多了,跳来跳去根本不知道哪一行在等待、哪一行已经执行完了。
问题四:日志没有分级。 Info、Debug、Warn、Error 混在一起,真正重要的错误日志被淹没在海量调试信息里。
问题五:断点不会用。 只知道 F8 继续、F9 打点,不知道条件断点、监视表达式、日志点这些东西。
这些问题,我们今天一个个来解决。
二、数据模型设计
2.1 日志条目:一个完整日志长什么样
在动手写日志工具之前,我们先想清楚:一个"好日志"应该包含哪些字段?想象一下医院的病历本——病人姓名、就诊时间、症状描述、主治医生,缺一不可。
同样,一个完整的日志条目至少应该有:
- 时间戳:精确到毫秒,知道什么时候发生的
- 级别:INFO / DEBUG / WARN / ERROR,决定这条日志的重要程度
- 模块名:哪个功能模块打的,方便按领域过滤
- 内容:具体的消息,可以是字符串、数字、对象或错误信息
- 调用栈(可选):在哪一行代码产生的,便于定位源文件
我们用 TypeScript 的 interface 来定义这个数据结构,非常清晰:
// 代码块一:日志条目数据模型
// 位置:src/model/LogEntry.ets
/**
* 日志级别枚举
* 就好像医院的分诊台——不同严重程度的症状走不同的通道
*/
export enum LogLevel {
DEBUG = 0, // 调试级:最详细,开发时用
INFO = 1, // 信息级:一般事件,正常运行时的记录
WARN = 2, // 警告级:潜在问题,需要关注但不必惊慌
ERROR = 3 // 错误级:出了问题了,必须处理
}
/**
* 单条日志条目
* 想象它是一张餐厅厨房的出餐单
*/
export interface LogEntry {
timestamp: number; // 时间戳,毫秒
level: LogLevel; // 日志级别
module: string; // 模块名,比如 "LoginModule" 或 "NetworkService"
message: string; // 日志消息文本
data?: Object; // 附加数据,可选,方便打印复杂对象
stack?: string; // 调用栈信息,定位用
}
2.2 日志配置:日志系统怎么工作
光有日志条目还不够,我们还需要一个"总开关"——日志配置类。就像你家的总电闸,可以统一控制所有电器的开关。
// 代码块二:日志配置与全局管理器
// 位置:src/utils/Logger.ets
import hilog from '@ohos.hilog';
// 日志域 ID,用于区分不同模块的日志
const DOMAIN_ID = 0xFF00;
// 格式化日志输出字符串
function formatEntry(entry: LogEntry): string {
const time = new Date(entry.timestamp).toLocaleTimeString('zh-CN', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
});
const levelTag = ['D', 'I', 'W', 'E'][entry.level];
const dataStr = entry.data ? ` | DATA: ${JSON.stringify(entry.data)}` : '';
return `[${time}][${levelTag}][${entry.module}] ${entry.message}${dataStr}`;
}
export class Logger {
private moduleName: string;
constructor(moduleName: string) {
this.moduleName = moduleName;
}
// 通用的日志打印方法
private log(level: LogLevel, tag: string, message: string, data?: Object): void {
const entry: LogEntry = {
timestamp: Date.now(),
level,
module: this.moduleName,
message,
data,
stack: level === LogLevel.ERROR ? new Error().stack : undefined
};
// 格式化输出到控制台
const formatted = formatEntry(entry);
hilog.print(level, DOMAIN_ID, tag, formatted);
}
debug(tag: string, message: string, data?: Object): void {
this.log(LogLevel.DEBUG, tag, message, data);
}
info(tag: string, message: string, data?: Object): void {
this.log(LogLevel.INFO, tag, message, data);
}
warn(tag: string, message: string, data?: Object): void {
this.log(LogLevel.WARN, tag, message, data);
}
error(tag: string, message: string, data?: Object): void {
this.log(LogLevel.ERROR, tag, message, data);
}
}
2.3 场景枚举:不同场景需要不同日志策略
想象一下,你在不同场合穿不同的衣服——正式会议穿西装,运动时候穿运动服。日志也一样,不同场景需要不同的配置策略:
// 代码块三:场景配置
// 位置:src/config/LogConfig.ets
/**
* 应用场景枚举
* 就好像不同的餐厅——家常小馆和米其林餐厅的后厨规范完全不同
*/
export enum AppScenario {
/** 开发阶段:日志最详细,所有信息都打印 */
DEVELOPMENT = 'dev',
/** 测试阶段:保留关键日志,方便 QA 复现问题 */
TESTING = 'test',
/** 生产环境:只保留警告和错误,节省性能和保护隐私 */
PRODUCTION = 'prod'
}
export interface LoggerConfig {
scenario: AppScenario;
enableConsole: boolean; // 是否输出到控制台
enableFile: boolean; // 是否写入文件(便于崩溃后分析)
enableRemote: boolean; // 是否上报远程日志服务
minLevel: LogLevel; // 最低打印级别
}
export const DEFAULT_CONFIG: Record<AppScenario, LoggerConfig> = {
[AppScenario.DEVELOPMENT]: {
scenario: AppScenario.DEVELOPMENT,
enableConsole: true,
enableFile: false,
enableRemote: false,
minLevel: LogLevel.DEBUG // 开发时连 DEBUG 都打印
},
[AppScenario.TESTING]: {
scenario: AppScenario.TESTING,
enableConsole: true,
enableFile: true,
enableRemote: false,
minLevel: LogLevel.INFO // 测试环境打印 INFO 及以上
},
[AppScenario.PRODUCTION]: {
scenario: AppScenario.PRODUCTION,
enableConsole: false,
enableFile: true,
enableRemote: true,
minLevel: LogLevel.WARN // 线上只打印警告和错误
}
};
三、核心设计决策
3.1 方案对比:为什么选 hilog 而不是 console
HarmonyOS 提供了两套日志 API,该怎么选?我们做一个简单的对比:
| 对比维度 | console API | hilog API |
|---|---|---|
| 使用场景 | 页面层(ArkUI/ArkTS) | 系统层 / 全场景 |
| 性能开销 | 相对较高 | 经过优化,开销更小 |
| 日志级别 | 仅 info / warn / error | DEBUG / INFO / WARN / ERROR / FATAL |
| 域 ID 控制 | 无 | 有(通过 domain 区分模块) |
| 格式化能力 | 弱,复杂对象展示困难 | 强,支持 tag 和 domain 分组 |
| 输出目标 | DevEco Studio 控制台 | 控制台 + 文件 + 远程 |
| Native 层支持 | 不支持 | 原生 C++ 可直接调用 |
打个比方:如果把日志系统比作快递服务,console 就好像普通的平邮小包——简单、够用、但没有追踪和保价功能;hilog 则是顺丰快递——有运单号追踪、有等级分类、有签收状态。
选型结论:推荐在 ArkUI 页面层也优先使用 hilog,原因有三:第一,它与原生系统日志格式一致,方便与系统日志联合分析;第二,性能更好,适合高频日志场景;第三,域 ID 和 tag 机制让日志分类更清晰。console API 可以在快速原型阶段临时使用,但生产代码建议统一迁移到 hilog。
3.2 分级策略:线上线下如何平衡
这个问题的核心矛盾在于:开发者需要详细的调试信息,但线上环境既不需要这些信息,也承受不起它们的性能开销。
我们的策略是"三环境差异化配置"——开发、测试、生产三个环境各有一套配置,通过一个全局开关自动切换。就好像手机的不同模式:飞行模式(全部关闭)、静音模式(只留来电)、正常模式(全开)。
- 开发环境(DEBUG):所有日志都打印,帮助开发者快速定位问题
- 测试环境(INFO):打印业务关键节点日志,方便 QA 复现
- 生产环境(WARN):只保留真正有问题的日志,不干扰正常用户
这里有一个常见的误区需要提醒:有些开发者喜欢在代码里到处写日志,调试完之后就用注释或 #if 0 删掉或禁用。更好的做法是保留日志代码,只通过级别开关来控制它是否输出。这样下次遇到问题时,不需要重新加日志,直接调低级别就能看到所有历史记录——这在排查偶发性 Bug 时特别有用,比如"每天早上九点固定卡顿两秒"这种只有特定时间才触发的诡异问题,事后加日志根本来不及。
3.3 hilog 的 tag 和 domain 参数到底怎么用
用过 hilog.info() 的同学可能注意到,它的签名里有 domain 和 tag 两个参数,很多人不知道该怎么设置,用的时候随便填数字和字符串。
domain 是一个 32 位的整数(通常写成十六进制),用来区分不同业务域的日志。就好像邮政编码——上海的邮编是 200000 开头,北京是 100000 开头,通过邮编就能知道邮件从哪来。hilog 的 domain 也是这个作用:你给自己项目的不同模块分配不同的 domain ID(比如登录模块用 0xFF01,支付模块用 0xFF02),在 DevEco Studio 的日志过滤器里就可以按 domain 一键筛选,只看某个模块的日志。
tag 是一个字符串(通常不超过 20 个字符),是对单次日志操作的描述性标注。比如你在登录模块的请求函数里打日志,tag 可以写成 LoginRequest,在响应函数里打,tag 写成 LoginResponse。这样即使同一个 domain 下有多条日志,通过 tag 也能快速分辨"这是在请求还是在响应"。
一个典型的 hilog 调用长这样:hilog.info(0xFF01, "LoginRequest", "用户登录成功,token已刷新")
3.4 对象打印:如何避免 [object Object]
这是大家最常踩的坑。当你 console.log({ name: "Alice", age: 25 }) 的时候,你以为能看到完整的对象结构,但实际只显示 [object Object]。
原因很简单:hilog 的字符串打印不支持自动序列化复杂对象。你需要手动 JSON.stringify()。
但这里有个坑:循环引用的对象 JSON.stringify() 会直接抛异常。所以我们在打印对象前要做两步处理:第一步尝试序列化,第二步捕获异常兜底。
四、完整代码实现
4.1 业务场景:用户登录流程日志
说了这么多理论,我们用一个实际场景来演示——用户登录流程中的日志记录。想象一下,这个流程就像快递的配送过程:从下单、分拣、装车、运输、到最后签收,每个节点都可能出问题。
// 代码块四:登录模块日志实践
// 位置:src/model/LoginModel.ets
import { Logger } from '../utils/Logger';
// 创建专属于登录模块的日志记录器
const logger = new Logger('LoginModule');
export class LoginModel {
private static instance: LoginModel;
static getInstance(): LoginModel {
if (!LoginModel.instance) {
LoginModel.instance = new LoginModel();
}
return LoginModel.instance;
}
async login(username: string, password: string): Promise<boolean> {
logger.debug('login', `登录请求开始 | 用户名: ${username}`);
// 第一步:参数校验
if (!this.validateInput(username, password)) {
logger.warn('validate', '参数校验失败');
return false;
}
logger.debug('validate', '参数校验通过');
// 第二步:模拟网络请求
try {
logger.info('network', '正在发起登录请求...');
await this.simulateNetworkRequest();
// 模拟登录成功
logger.info('network', '登录成功', { username, token: 'xxx-token-xxx' });
return true;
} catch (err) {
// 注意这里:打印错误时附带错误对象,方便分析根因
logger.error('network', '登录请求失败', { error: (err as Error).message });
return false;
}
}
private validateInput(username: string, password: string): boolean {
return username.length > 0 && password.length >= 6;
}
private simulateNetworkRequest(): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, 1000); // 模拟 1 秒网络延迟
});
}
}
4.2 UI 页面:连接日志与界面
日志最终要呈现在界面上,才方便非开发人员(比如测试工程师)查看。我们来写一个简单的日志查看页面:
// 代码块五:ArkUI 日志查看器页面
// 位置:src/pages/LogViewerPage.ets
import { Logger } from '../utils/Logger';
import { LogLevel } from '../model/LogEntry';
const logger = new Logger('LogViewer');
@Entry
@Component
struct LogViewerPage {
@State logMessages: string[] = [];
@State filterLevel: string = 'ALL';
aboutToAppear(): void {
// 页面加载时打印一条初始化日志
logger.info('ui', '日志查看器页面已加载');
}
build() {
Column() {
// 顶部过滤器
Row() {
Text('日志查看器')
.fontSize(24)
.fontWeight(FontWeight.Bold);
Blank();
Button('清空日志')
.onClick(() => {
this.logMessages = [];
logger.info('ui', '日志已清空');
});
}
.padding(16)
.width('100%');
// 日志列表区域
List() {
ForEach(this.logMessages, (msg: string) => {
ListItem() {
Text(msg)
.fontSize(12)
.fontFamily('monospace')
.width('100%')
.padding({ left: 8, right: 8, top: 4, bottom: 4 });
}
});
}
.layoutWeight(1)
.width('100%');
}
.width('100%')
.height('100%');
}
}
五、深度技术原理
5.1 hilog 底层是怎么工作的
很多同学好奇:hilog 把日志打印出来,背后到底发生了什么?
打个比方:你在饭店点了一道菜,说"加辣",服务员记下这个需求,递给后厨,后厨做完菜之后,服务员把菜端到你面前。hilog 的工作原理与此类似:
第一层——调用层(你的代码):调用 hilog.info() 或 logger.info(),把日志消息扔给系统。
第二层——IPC 通信层:HarmonyOS 是一个分布式操作系统,应用进程和系统服务进程是分离的。日志数据通过 IPC(进程间通信)传递给系统日志服务。这个过程对你透明,但理解它很重要——如果 IPC 出问题了,日志可能会丢失。
第三层——系统日志服务:系统有一个专门的日志服务(hilogd)负责收集、存储和分发所有应用的日志。它就像酒店的前台数据库,把所有服务员的记录单都汇总到这里。
第四层——输出层:日志最终有三个去向:
- DevEco Studio 控制台:开发时实时查看
- 本地日志文件:存储在设备的
/data/log/目录下 - 远程日志服务:企业级应用可将日志上报到日志分析平台(如华为 AppGallery Connect)
5.2 为什么 JSON.stringify 在日志中很重要
前面我们提到对象打印会变成 [object Object],这里来深入解释一下。
JavaScript/ArkTS 中的 console.log 和大多数日志 API 底层都依赖字符串拼接。当你写 logger.info("数据是", {a:1}) 的时候,如果不做特殊处理,系统会调用对象的 .toString() 方法。
普通对象的 .toString() 默认实现就是返回 [object Object]——这就好比你把一箱水果交给快递员,但没拆箱让他看,他只能告诉你"这里有一箱东西",说不出里面是苹果还是香蕉。
JSON.stringify(obj) 就是那个"拆箱"操作。它把对象展开成字符串形式:{"a":1}。这样日志里就能清楚看到里面的内容了。
但要注意三个细节:
细节一:循环引用会炸。如果对象 A 引用了对象 B,而对象 B 又引用了对象 A,JSON.stringify 会抛出一个 TypeError: Converting circular structure to JSON。解法是使用 JSON.stringify(obj, null, 2) 的第二个参数(replacer 函数)来跳过循环引用,或者使用专门的库来处理。
细节二:函数和 undefined 会被丢弃。序列化一个包含函数的对象,JSON.stringify 会把函数字段变成空值。所以如果你想看清楚日志里的完整对象结构,记得先把函数剥离或者单独打印。
细节三:大对象序列化耗时。想象一下一个大数组有十万条数据,JSON.stringify 可能要跑好几秒,这对 UI 线程是致命的。线上环境要慎用,或者用异步方式处理。
5.3 断点调试的内部机制
断点听起来很神奇——代码运行到某一行就自动停下来了,它是怎么做到的?
这里涉及到动态分析技术,我们用生活中的例子来理解:
想象工厂流水线上的质检员。正常情况下,产品从生产线下来就直接打包出货了。但如果质检员在某个环节插了一面小旗子,说"这个环节我得检查一下",那产品流到这里就暂停了,质检员可以拿起放大镜仔细看。
硬件断点和软件断点的区别就在这里:
- 软件断点:编译器在目标行插入一条特殊的指令(比如 x86 的
INT 3,对应的机器码是0xCC)。CPU 执行到这条指令时,会触发一个异常,操作系统接收到异常后就把控制权交给调试器。DevEco Studio 的模拟器和真机调试走的都是这条路。 - 硬件断点:在 CPU 层面设置寄存器,告诉处理器"当这个内存地址被访问时停下来"。适合监控变量变化,但数量有限(通常只有 4 个)。
5.4 条件断点的实现原理
普通的断点是"只要到这里就停",条件断点是"只有满足某个条件才停"。比如你想让断点只在用户 ID 为 123 时触发。
条件断点的实现通常有两种策略:
策略一:条件检查(常见于解释型语言或 JIT 场景)。调试器在断点位置插入一段代码,大致长这样:
if (condition == true) {
pause(); // 触发断点
}
每次经过断点都要执行这个条件判断,有一点点性能损耗,但对大多数调试场景影响可以忽略不计。
策略二:跳过计数(适合"每 N 次停一次"的场景)。调试器维护一个计数器,每次经过断点就加 1,只有当计数器达到目标时才停下来。
六、常见问题解答(FAQ)
Q1:日志打印太多会影响性能吗?
A:确实会影响,但影响程度取决于日志量和打印内容。简单估算一下:一条普通字符串日志大约几十字节,打印一次耗时约 0.01~0.1 毫秒——听起来很快,但如果你在循环里每秒打印一千条,那就有 100 毫秒的损耗,对 UI 流畅度影响就很明显了。建议:生产环境使用 LogLevel.WARN 及以上的日志级别,高频循环中的调试日志一定要加条件判断或用采样方式打印(比如"每 100 次才打印一次")。
Q2:线上日志需要加密吗?
A:如果日志里包含用户名、身份证号、手机号、密码等个人信息,强烈建议加密或脱敏处理。脱敏方案有两种:方案一是在日志输出前做正则替换,比如把手机号 13812345678 变成 138****5678;方案二是分级上报——本地存储明文供调试,远程上报脱敏后的摘要。另外注意 GDPR 和国内的个人信息保护法,敏感日志的存储和传输都要合规。
Q3:异步代码怎么打断点最有效?
A:异步代码最难调试的地方在于"执行顺序不直观"。推荐三个技巧:① 在每个 .then() 和 .catch() 回调的入口处打日志,清楚标注"进入了某某回调";② 使用 async/await 替代 Promise 链式调用,代码更线性的好处是打断点更容易;③ 利用 DevEco Studio 的"异步堆栈"功能,调试时展开异步调用链,能看到完整的跨线程调用路径。
Q4:hilog 和 console.log 在 ArkUI 里可以混用吗?
A:技术上可以,但不推荐混用。混用会导致日志格式不统一、过滤困难、阅读体验差。最佳实践是统一使用 hilog,理由在第三章已经讲过——性能更好、功能更全、格式更规范。console 的使用场景仅限于快速验证某个想法时的临时调试,用完就删。
Q5:真机调试时看不到日志怎么办?
A:这是新手最容易遇到的问题,通常有三个原因:① 日志级别被设成了 ERROR,导致 DEBUG 和 INFO 都不显示——检查 hilog 的域 ID 和日志级别配置;② DevEco Studio 的日志窗口没有打开或被过滤了——确认日志面板已展开,并检查过滤条件;③ 真机开启了隐私模式(部分华为手机有"隐私日志"开关)——在手机设置中允许应用记录完整日志。另外,真机调试建议通过 USB 连接,Wi-Fi 调试时网络不稳定也可能导致日志延迟或丢失。
Q6:如何在崩溃时自动保存日志?
A:可以利用 HarmonyOS 的 app.terminate() 生命周期钩子结合 hilog 的文件输出能力。在 app.terminate() 中写入最近 N 条日志到本地文件,或者利用 AppStorage 维护一个滚动日志缓冲区,崩溃前将缓冲区内容刷到磁盘。当然,如果你的应用接入了华为 AppGallery Connect 的崩溃服务,系统本身就会自动上报崩溃日志和基本信息,比自己写要可靠得多。
Q7:Watch 表达式能帮我们做什么?
A:Watch 是断点调试时最强大的观测工具之一。你可以把自己关心的变量或表达式拖到 Watch 窗口里,然后一边步进调试一边观察它的值如何变化。比如你在调试一个循环,循环变量 i 在不断递增,但你不知道它什么时候会越界——那就把它加到 Watch 里,代码每执行一步你都能看到它当前的值。需要注意的是,Watch 里写的表达式必须是在当前断点作用域内可见的,如果你在一个函数内部打断点却 Watch 了另一个函数的局部变量,那是看不到的。
Q8:日志点(Logpoint)和普通断点有什么区别?
A:日志点是一种"不断点"——代码经过那里不会暂停,但会在控制台打印你指定的消息。就好像你在流水线旁边放了一个自动记录仪,产品经过时它自动记下时间和状态,然后产品继续往前走,不会被拦下来。这个功能特别适合调试那些"必须让它跑一段时间才能暴露问题"的 Bug,比如某个计数器在一百万次循环后溢出,你不可能手动按一百万次 F8 吧?在循环入口设一个日志点,打印当前的计数器值,让程序跑完,结果一目了然。
七、运行效果
7.1 日志输出示例(DevEco Studio 控制台)
[14:32:01.008][D][LoginModule] 登录请求开始 | 用户名: Alice
[14:32:01.012][D][validate] 参数校验通过
[14:32:01.015][I][network] 正在发起登录请求...
[14:32:02.023][I][network] 登录成功 | DATA: {"username":"Alice","token":"xxx-token-xxx"}
[14:32:05.134][I][ui] 日志查看器页面已加载
[14:32:10.567][W][validate] 参数校验失败
[14:32:15.890][E][network] 登录请求失败 | DATA: {"error":"网络超时"}
7.2 断点调试界面 ASCII 草图

7.3 日志级别可视化

八、扩展方向
8.1 日志分析平台集成
单个应用的日志排查效率有限,当你的应用日活达到十万级别时,就需要接入专业的日志分析平台了。推荐几个方向:
华为 AppGallery Connect 的日志分析服务是首选,因为它与 HarmonyOS 生态深度集成,上报 SDK 体积小,平台对日志有自动聚合和异常检测能力。你只需要在日志配置中将 enableRemote: true 打开,并配置好上报地址即可。想象一下,以前你要手动翻几千条日志找 Bug,现在平台会自动告诉你"过去 24 小时内,有 23% 的用户在第三步(支付)崩溃,崩溃原因是空指针异常",这效率差距是指数级的。
8.2 可视化日志面板
目前的日志查看器只是一个简单的列表。进阶方向是可以做一个类似 Chrome DevTools 的 Web 化日志面板:支持按模块名过滤、按关键字搜索、按时间范围筛选、高亮 ERROR 和 WARN 行,甚至可以画一张"请求瀑布图"来展示网络请求的耗时分布。
这类工具的架构通常是:前端 ArkUI 页面负责展示,Native 层的日志收集服务负责采集,IPC 通信负责两者之间的数据传输。如果你的应用面向测试团队或技术支持团队,这个投资是非常值得的。
8.3 性能监控与日志联动
日志不只是查 Bug 的工具,还可以用来做性能监控。思路很简单:在关键路径的入口和出口打上带时间的日志,通过计算时间差就能得到每个步骤的耗时:
// 性能监控的简单实现
const start = Date.now();
await doSomethingHeavy();
logger.info('perf', `doSomethingHeavy 耗时: ${Date.now() - start}ms`);
更进一步,可以将这个模式封装成一个通用的 @timed 装饰器或工具函数,自动测量任何异步方法的执行时间,并将结果上报到性能监控平台。这样你就能在生产环境中持续观测每个功能模块的性能表现,而不需要每次都手动加计时代码。
8.4 结构化日志与 ELK 生态
当应用规模进一步扩大,单机日志文件已经不够用的时候,结构化日志就是下一个台阶。普通日志是纯文本:[INFO] 用户登录成功;结构化日志则是 JSON 对象:{"level":"INFO","event":"user_login","userId":123,"duration":56}。
结构化的好处是:日志分析平台可以自动提取字段、生成仪表盘、设置告警规则。比如你可以配置"当 error_count > 100 且 duration > 500ms 时发送告警",这比在纯文本日志里正则匹配关键词要可靠得多。主流的 ELK(Elasticsearch + Logstash + Kibana)栈和 Splunk 都原生支持 JSON 日志解析,无缝对接。
在 HarmonyOS 中实现结构化日志,只需要把 formatEntry 函数改成输出 JSON 字符串即可:
function formatEntry(entry: LogEntry): string {
return JSON.stringify({
time: new Date(entry.timestamp).toISOString(),
level: LogLevel[entry.level],
module: entry.module,
message: entry.message,
data: entry.data
});
}
这样一条 JSON 日志经过 Logstash 收集后,Elasticsearch 会自动解析出每个字段,你在 Kibana 里就能按模块名、时间范围、数据值做多维度的聚合分析了。
更多推荐

所有评论(0)