说真的,最开始想写 rea 这个工具,是被线上日志搞到没脾气。团队里的系统越拆越细,日志文件散落在各个服务节点上,错误格式五花八门,有 Python 的 traceback,有 Java 的 Exception,有业务自己的 JSON 日志,还有同事偷懒直接 print 出来的诡异文本。每次线上出问题,第一件事就是人手一台服务器,挨个 grep,眼睛都看花了。后来我就琢磨,能不能做一个极轻量的命令行工具,往那一放,告诉它"什么样的日志算错误",它自动把所有异常捞出来、分类、去重、然后按需通知。这个东西就是 rea,Rapid Error Analysis 的缩写。这篇文章是把 rea 从想法到落地、再到上线跑了大半年的完整记录,包括设计取舍、核心实现、规则编写、以及各种我踩过的坑。如果你也是后端开发、运维或者 SRE,每天被日志淹没,又想造一个属于自己的小轮子,那这篇应该能给你不少可以照抄的东西。
1. 项目背景与整体设计
1.1 为什么需要一个错误分析工具
先说背景。我所在的团队维护着一套典型的微服务架构,服务数量不算夸张,但日志规则完全没有统一。有人用了日志框架,有人直接在代码里拼接字符串,更麻烦的是,同一个业务错误在不同服务里会以完全不同的文本出现。举个最简单的例子,调用下游超时,在 A 服务打的是request timeout after 5000ms,在 B 服务打的是context deadline exceeded,到了 C 服务又变成rpc error: timeout。字符串长得完全不像,但本质都是同一个问题。
在流量比较小的时候,这些问题靠人工也能扛。可一旦有活动流量进来,日志量暴涨,问题就开始失控。我记得有次故障,相关错误出现在五个服务的日志里,时间跨度有十几分钟,靠临时写命令去查,根本形不成完整链路。后来我意识到,问题不是日志采集能力不够,而是缺一个能把错误从原始日志里"提取、归类、降噪"的中间层。我们已经有日志平台可以做检索,但检索还是需要人去想关键词,错误分析工具应该反过来:用规则主动找出异常,再把相似的错误合并起来,告诉我"现在到底有哪几类问题、每一类多严重"。这就是 rea 最初定位的雏形。
1.2 核心设计理念:单二进制、零依赖、规则先行
做技术选型时,我几乎没有犹豫就确定了三个原则:单二进制、零依赖、规则先行。
什么叫单二进制?就是最终交付物只有一个可执行文件,不依赖任何运行时。我用 Go 写的,编译出来就是静态链接的二进制,往服务器上一扔,chmod +x就能用。为什么坚持这个?因为在故障现场,环境往往是不可控的。有的机器缺 libc 库,有的机器没有 Docker 权限,有的机器 Python 版本乱得一批,如果工具还依赖一堆运行环境,那还没开始排查就先折腾环境了。单文件直接把这个问题消灭在源头,这也是 rea 在团队里能快速推开的重要原因。
规则先行则是 rea 的核心思想。我不希望工具写死某种日志格式,而是让用户通过一个 YAML 配置文件告诉 rea:"什么样的日志文本是错误?错误类型是什么?从文本里提取哪些字段作为维度?" 这样 rea 就变成了一套可插拔的分析引擎,而不是一个只能处理特定格式的死工具。不同团队、不同语言、不同业务的日志,都可以用一套规则适配。规则先行还带来一个额外好处:所有复杂度都被显式地放在配置里,代码只需要处理"匹配、提取、聚合"三个通用逻辑,这让工具本身保持非常小的体积和极高的稳定性。
1.3 整体架构拆解
rea 的内部结构很清晰,可以分成四条主链路:输入、解析、聚合、输出。
输入层支持三种来源:本地文件、标准输入(stdin)和 HTTP 推送。本地文件适合事故现场直接扫盘;stdin 适合和tail -f或者docker logs等命令组合使用,比如docker logs -f app | rea analyze -c rea.yaml -;HTTP 推送则是给业务侧手动上报用的,适合那些没法通过日志文件暴露的场景。解析层是规则引擎,它读入配置里的正则规则,逐行匹配日志,匹配成功的行会被打上错误级别、错误类型,同时把命名捕获组里的内容提取成结构化字段。聚合层是整个工具最有价值的部分,它会对提取后的错误做指纹计算,把同一个错误的不同出现次数合并成一条摘要,并记录时间范围和数量。输出层支持终端表格、JSON 文件和 Webhook 通知,方便接入不同场景。
这四个模块之间通过简单的接口传递数据,耦合度很低。实际上 rea 的大部分代码都集中在解析和聚合上,输入输出反而非常薄。这也是我在设计时的一个理念:分析类工具的核心价值在分析和判断,不属于核心价值的采集和展示尽量做轻。
2. 核心功能与关键实现
2.1 日志解析器:正则规则引擎
正则规则引擎是整个 rea 的基石。配置里的一条核心规则长这样:
rules: - name: "java exception" pattern: 'Exception: .+' level: error fields: service: '(?P<service>\S+)' type: '(Exception|Error) (?P<type>\w+)'这里我用了命名捕获组(?P<service>\S+)来从日志里抽字段。命名捕获组的好处是,提取出来的字段可以直接作为分类维度。比如我想按服务名和异常类型聚合,那就在规则里声明两个 field,聚合器会自动根据这个规则计算指纹。
写规则时最需要警惕的是灾难性回溯。拿一个用户常见的坑举例:日志里有一长串相同的字符,正则里用了.*和多个嵌套分组,在极端输入下 CPU 占用会直接拉满。默认 Go 的正则引擎是 RE2 语法,它不支持反向引用和前瞻,核心卖点就是保证线性时间匹配,不会出现灾难性回溯。这是我在选型时非常看重的一点。虽然 RE2 不支持一些花哨的语法,但对日志解析这种场景完全够用。如果你用的是语言自带的正则,建议也尽量让规则保持简单,避免写出(a+)+$这种经典灾难性表达式。
除了单行匹配,日志里还有大量多行堆栈。默认 rea 是逐行读取的,如果某一行没有匹配到规则,它会被忽略。但像 Java 的堆栈信息,错误的第一行是Exception: ...,后面跟着十几行调用栈。我实现的方案是规则里加一个multiline: true参数,并在内部维护一个"当前待处理缓冲区"。当匹配到一个新的错误行时,把缓冲区里的内容作为一个完整日志块处理;如果接下来的行不匹配任何错误规则,它们会被追加到当前缓冲区,直到出现下一个错误行或者文件读取结束。处理完一个日志块之后,再对整个块做一次是否包含错误关键字的判断,这样可以把堆栈尾巴干净地归纳到同一类错误下面。
2.2 错误分类与聚合算法
如果只做匹配和提取,rea 充其量就是一个加了正则的 grep。真正的价值在于聚合。
聚合算法的核心是"指纹(fingerprint)"。每一条被识别出的错误,我会把它转换成一组稳定字段,包括错误类型、错误级别、提取到的维度字段(如服务名)、以及从堆栈中提取的文件名和行号。然后使用哈希函数对这组字段计算出一个指纹。关键点在于:只包含稳定标识,不包含会变的参数。举个例子,超时错误的具体时间是变化值,但错误类型upstream time out是稳定的。如果把时间戳也纳入指纹,那同一类错误就会被拆成无数条,完全没有聚合意义。所以我在实现时专门有一层"字段抽象",用户可以在规则里给字段指定一个 key,比如把timeout=5000ms和timeout=8000ms都抽象成timeout-ms这种标签,这样两类日志就会被归到同一组。
聚合还需要考虑时间窗口。早期版本是无限聚合,从程序启动开始累计,但这样问题很明显:一个错误如果持续了一整天,你只会看到一条累计 1 万次的记录,无法感知它是不是最近才爆炸的。后来我加入了可配置的窗口聚合,默认 5 分钟一个窗口。每个窗口维护一组独立的指纹计数,窗口结束时会输出一条聚合记录,标记该错误在窗口内出现的次数、第一次发生时间、最后一次发生时间,以及样本上下文。这样看趋势才有意义:错误次数突增,意味着上游可能挂了;错误零星出现,可能是偶发重试导致。
2.3 告警通知链路:多渠道集成的取舍
告警通知是大家最关心的功能,但我这里的经验是:能少接就少接。一开始我雄心勃勃,想把邮件、短信、各大聊天软件的机器人全接了,结果维护成本爆炸。聊天机器人接口说变就变,邮件网关在故障时经常挤爆,短信的费率又贵,最后我把渠道收敛成了两个:Webhook 和聊天群机器人(通过 Webhook 实现)。
具体的做法是,rea 内部只输出一个统一的事件 JSON,事件包含错误类型、级别、指纹、次数、首次出现时间、最后出现时间、部分样本日志。然后通过配置里的notify.webhook字段把这件事 JSON POST 到任意地址。如果这个地址是聊天群的机器人 Webhook,那自然就实现了群内告警。这里放一个飞书群机器人的配置示例:
notify: webhook: url: "https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx" headers: Content-Type: application/json body_template: | { "msg_type": "text", "content": { "text": "rea 告警:{{.Level}} {{.Type}} 出现 {{.Count}} 次,最近一次 {{.LastTime}},样本:{{.Sample}}" } }body_template 使用了简单模板变量,执行时会从事件数据里做替换。这么设计的好处是,rea 不需要依赖任何供应商 SDK,只要目标系统提供 HTTP 接口就能接入。万一某个机器人接口变得不稳定,我只需要改配置,不用改动代码和重新编译。这里补充一个经验:告警通知一定要加静默窗口配置。我遇到过夜里一个错误高频刷屏,机器人把整个群的人都吵醒了,后面加了notify.silence参数,支持指定时间区间不发送通知,只在恢复时补一条汇总。这个功能虽然不起眼,但上线后好评度非常高。
2.4 配置文件设计:如何做到简单可扩展
配置文件是用户接触 rea 的第一入口,所以我花了很大精力设计它的 schema。我的原则是:尽量扁平、少嵌套、可注释。下面是一个完整的配置示例:
input: files: - "/var/log/app/*.log" - "/data/logs/order/*.log" scan_interval: "30s" rules: - name: "java exception" pattern: 'Exception: .+' level: error fields: service: '(?P<service>\S+)' - name: "timeout" pattern: 'request timeout after (\d+)ms|context deadline exceeded|rpc error: timeout' level: critical fields: service: '(?P<service>\S+)' aggregation: window: "5m" count_threshold: 3 output: format: table json_file: "/tmp/rea-output.json" notify: webhook: url: "https://example.com/hook"配置里最重要的部分是rules,它决定了 rea 能"看到"什么。其次是aggregation和notify,这两个影响结果的展示和触达。input 和 output 则相对直观。为了降低使用门槛,rea 提供了rea validate -c rea.yaml子命令。它会解析配置文件,逐个编译正则表达式,并给出警告提示。比如某个正则在 RE2 里不合法,它会直接告诉你哪一行写错了,而不是等到运行时才报错。
我在实践中建议配置文件不要写得太大。如果规则超过二十条,往往说明日志本身就该治理了。与其用一个万能正则匹配所有异常,不如推动团队把错误日志统一成 JSON 结构化格式,然后再用 rea 做辅助分析。规则先行的确很灵活,但灵活过头也会变成垃圾堆。
3. 实操过程:从零搭建一个 rea 分析任务
3.1 环境准备与安装
rea 的安装过程可以说比任何工具都简单。在 Linux 服务器上,我直接把编译好的二进制包放进/usr/local/bin,然后加上执行权限:
wget https://example.com/download/rea-linux-amd64.tar.gz tar -xzf rea-linux-amd64.tar.gz mv rea /usr/local/bin/ chmod +x /usr/local/bin/rea rea version这里我特意没有选择用 Docker 来分发,因为在真实故障场景里,运维人员有时候连 docker 命令都没权限,而一个纯二进制可以直接放在 PATH 里,任何用户都能调用。如果你用的是 macOS 或者 Windows,也有对应架构的预编译包。还有一点值得说,rea 的默认工作目录是可执行文件所在目录,这样我可以把配置文件和二进制放在同一个目录里直接拷到其他机器,实现了"免安装"运行。团队里另一位同事甚至把这个目录打成 tar 包,遇到问题时解压就能用,非常实用。
如果你想从源码编译,也很简单。项目只有一个 main 包,依赖很少:
git clone https://example.com/rea.git cd rea go build -o rea .编译产物大概只有 8MB 左右。对于命令行工具来说,这个体积完全可以接受。我认为体积小本身就是一种可靠性,因为塞进另一边拷贝到内网机器时可以走最原始的文件传输方式,不会被各种安全限制卡住。
3.2 编写第一份规则文件
现在用一个真实场景演示如何配置。假设我们有这样一段日志:
2025-01-18 10:00:00 ERROR [order-service] [order_001] Failed to call inventory: timeout 2025-01-18 10:00:01 ERROR [order-service] [order_001] Failed to call inventory: timeout 2025-01-18 10:00:05 ERROR [payment-service] [pay_002] Exception: database connection refused我们想让 rea 把timeout和database connection refused分别识别成两类错误。第一份规则文件可以这样写:
rules: - name: "call timeout" pattern: 'Failed to call inventory: timeout' level: error fields: service: '\[(?P<service>\S+)\]'这里有个细节:service字段用了中括号[和],在正则里这两个字符是字符类元字符,需要转义。如果直接写[(?P<service>\S+)],那捕获组会落在字符类里,提取结果会不对。我在配置文件里加了一个注释提醒用户,凡是遇到[]、()、.都建议先用正则工具验证一遍。很多人的第一份规则失败,都是因为转义问题。
接下来是把服务名提取出来,用于聚合。因为原来的日志里服务名被方括号包裹,我写成\[(?P<service>\S+)\],这样 rea 会提取[order-service]中间的内容order-service。提取之后,即使order-service和payment-service都出现timeout,它们也会被聚合到同一个错误类型下,但会按服务名维度分成两条,而不是混在一起。这个维度的选择非常重要,通常我建议至少按服务名和错误类型两个维度做聚合。
3.3 执行分析并解读输出
配置文件写好后,执行分析:
rea analyze -c rea.yaml -f /var/log/app/app.log假设刚才的日志样本只有三条,输出可能是这样的表格:
| 错误类型 | 服务 | 指纹 | 次数 | 首次时间 | 最后时间 | 样本 |
|---|---|---|---|---|---|---|
| call timeout | order-service | a1b2c3 | 2 | 10:00:00 | 10:00:01 | 2025-01-18 10:00:01 ERROR... |
| database connection refused | payment-service | d4e5f6 | 1 | 10:00:05 | 10:00:05 | 2025-01-18 10:00:05 ERROR... |
看到这个表,你就能一眼发现问题:order-service 在 1 秒内连续出现两次超时,支付服务出现一次数据库连接拒绝。如果配置了聚合窗口,这里每一行代表一个窗口内的事件。在真实文件里,同一指纹可能被压缩成一行,次数可能高达上百次。有了这种输出,我们不再需要关心具体某一行日志长什么样,只需要抓住表格里的指纹和次数变化即可。
从输出中还可以做进一步定位。比如我发现call timeout这个指纹在某个时间点开始次数突然从 5 升到 500,那基本可以判断此时下游服务开始不健康。如果时间点的前后有发布操作,那十有八九是新版本引入的问题。这个能力在事故复盘时尤其好用,我通常直接把 rea 输出的 JSON 文件作为附件发到复盘文档里。
3.4 接入 CI/CD 流水线的落地方案
除了在服务器上分析日志,rea 还有一个很有价值的用法:接入到 CI/CD 流水线里做质量门禁。当代码构建完成后,可以跑一轮针对测试日志的分析,如果发现新增的错误签名数量超过阈值,就让构建失败,提前发现问题。
在 GitLab CI 中,我加过这样的一个 job:
analyze: stage: test script: - ./rea analyze -c rea-ci.yaml -f build-test.log --format junit --threshold 5 artifacts: when: always paths: - rea-output.json这里的重点有两个。第一,--format junit让 rea 输出 JUnit 格式的测试报告,这样结果能直接展示在合并请求的检查项里。第二,--threshold 5表示如果新增错误指纹的数量超过 5,则命令退出码不为 0,流水线失败。这个阈值需要根据项目实际情况调整,一开始设成 0 容易被误伤,因为有些历史错误还残留在日志里,后来我改成了只统计最近一次提交引入的新指纹,max 值留给用户自定义。
在实际使用中,我建议 CI 里跑的规则要和服务器的规则分开。CI 规则可以更严格,比如把INFO级别的异常也列为告警;服务器规则则只关注生产环境真正需要人处理的错误。分开维护可以避免线上误报和 CI 误伤之间的互相干扰。
4. 常见问题与排查实录
4.1 规则不生效的三种典型原因
在实际使用中,最常被问到的就是"为什么我写的规则没匹配到日志"。我总结下来,绝大多数离不开三种原因。
第一是转义问题。YAML 文件本身就是转义地狱。比如你从网上抄了一个正则\d+,在 YAML 单引号字符串里不需要额外转义,但如果你用了双引号,\d会被当成d处理,因为 YAML 双引号里\d不是合法转义序列,很多解析器直接报错。更麻烦的是反斜杠和正则的冲突。我建议在 YAML 里统一用单引号写正则表达式,同时如果你要在正则里匹配\.,那就写'\.',解析器不会吞掉这个反斜杠。但有时候单引号在 YAML 里也会有边界问题,所以最可靠的方式是先用rea validate检查,它会告诉你哪条规则编译失败。
第二是日志本身带了特殊字符。最常见的是 UTF-8 BOM 头。某些 Windows 工具生成的日志文件会带一个不可见的 BOM 标记,导致文件第一行永远匹配不上。解决办法很简单,rea 在读取文件时显式跳过了开头的 BOM,但如果你使用的是普通阅读器去分析,很容易忽略这一点,以为规则写错了。
第三是多行堆栈问题。前面提到过,默认是逐行匹配,很多用户的日志第一行是时间戳,第二行才是异常类型。如果不给规则加multiline: true,rea 只会匹配到时间戳行,当然什么也发现不了。我在刚开始写文档时特意用了一个大标题"如果你在匹配多行日志,请务必打开 multiline",这个问题占所有答疑量的一半。
4.2 内存占用过高的问题定位
rea 上线后不久,有同事反馈在处理一个 2GB 的日志文件时,内存占用超过了 1GB,异常吓人。起初我怀疑是聚合结构中保存了太多样本日志,后来加限制只保留前三条样本,但问题依然存在。
真正的原因在输入读取层。当时我图省事用bufio.Scanner每次扫描一行,默认缓冲区只有 64KB。如果某一行日志特别长(比如带序列化大对象的堆栈),Scanner 会反复报错并扩大缓冲区,导致内存抖动。解决办法是把读取换成自管理的bufio.Reader,显式指定缓冲区大小,并且在解析前对单行长度做限制:超过 1MB 的行直接截断,只保留前 1MB 内容。这个限制对错误分析来说完全够用,因为重要的信息通常在行首或者行尾。
另外,正则表达式自身的回溯也会造成 CPU 和内存暴涨。虽然 Go 的 RE2 引擎控制了最坏情况,但如果业务日志量特别大,还是可以通过编译时设置一个并发度来控制资源消耗。默认 rea 用 4 个 goroutine 并发解析文件,如果日志来自多个文件,并发解析非常快。但对于极端的超大日志,我会建议先用split把文件按行切割成多个分片,然后再并行跑分析,最后合并输出。这样最简单的办法反而最稳。
4.3 误报与漏报的平衡
误报和漏报是分析工具永恒的博弈。规则太宽,把正常的 INFO 日志也当成错误,告警轰炸;规则太窄,真正的异常被漏掉,工具形同虚设。
我的经验是分两步解决。第一步,在白名单之外加一层"忽略规则"。rea 支持ignore配置段,里面的规则会在主规则匹配后被检查,如果命中,则跳过这条日志。比如测试环境常见的Connection reset by peer,在生产是偶发正常现象,就在 ignore 里去掉。第二步,把样本日志收集起来做回归测试。我在 rea 仓库里加了一个测试目录,放了几份典型的日志文件,每次改规则或者改代码,都会跑一遍rea test,对比输出结果是否和预期一致。这相当于给规则体系做了自动化测试,极大减少了"改一条规则导致另外一条失效"的情况。
这里也插一句,rea 设计上并不打算取代人工排查,它更像是一个"雷达"。雷达可以帮你发现大量可疑信号,但最终决定要不要处理,还是需要人来判断。所以我在输出中总会带上原始样本日志,方便用户点开看上下文。只给一个错误摘要没有上下文,很容易导致误判。
4.4 与其他工具(日志平台、调用链)的配合方式
有些用户问 rea 和现有日志平台比如 ELK、Loki 是什么关系。我的回答是,这不冲突,而是互补。日志平台负责存储和检索海量日志,rea 更擅长实时分析和聚合。尤其当你在做一个紧急变更时,往往不想去大平台上敲复杂查询,只想快速看一个结论,这种情况 rea 的价值就体现出来了。
一个非常实用的组合是:rea 输出包含 trace_id 的 JSON,然后通过 jq 提取 trace_id,再到日志平台查请求的完整链路。具体命令类似:
rea analyze -c rea.yaml -f app.log --format json | jq -r '.[].fields.trace_id' | sort -u拿到 trace_id 之后,在调用链系统里按 ID 检索,就能看到这个错误在整条请求链路上的上下游。有一次我就是通过这个方式,发现错误日志里的服务是payment-service,但真正导致失败的其实是最底层的库存中心,因为库存中心把错误吞掉了,没有向上抛。只知道payment-service报错还不够,链路追踪帮我把根因定位到了更下游的节点。把 rea 和链路追踪搭配起来,既有了全局视野,也有了聚焦能力。
5. 实战心得与后续扩展
5.1 我在实际使用中踩过的坑
分享几个我为了做这个工具交过学费的地方,这些都属于文档里不一定写的坑。第一个是配置文件路径的问题。当时我习惯把配置放在/etc/rea/rea.yaml,结果在容器环境里,这个路径被映射成了一个目录,rea 尝试读取配置文件时直接报错"is a directory"。后来我把默认配置路径改成了可执行文件目录下的rea.yaml,并且提供--config参数强制指定路径,才避免了这个尴尬。容器环境里路径映射的怪癖数不胜数,我最后建议所有启动命令都用绝对路径显式指定,不要依赖任何隐含规则。
第二个坑是聚合指纹的可变字段问题。前面提过,如果指纹包含时间戳或者请求 ID,那几乎每个错误都会生成新的指纹,根本无法聚合。我在最初设计时确实踩过这个坑,测试时还奇怪为什么输出里每一条都只有一次。后来我引入了字段抽象机制,用户可以把请求 ID 等变量从指纹维度中排除掉,只保留错误类型和服务名,这个问题才真正解决。这个机制很关键,让 rea 能适应真实场景中充满变量日志的环境。
第三个坑是告警疲劳。最开始所有错误都会触发通知,一天能收到几百条消息,没过多久大家都把群静音了。后来我加入了两个机制:一个是聚合窗口内的频率阈值,只有窗口内错误次数超过阈值才发通知;另一个是静默时间窗口,夜间不打扰,但会在早晨补发一份汇总报告。这两个机制上线后,告警数量降到了原来的一成不到,但重要的问题仍然能被捕捉到。很多时候工具太"勤快"反而让人失去警觉,这确实需要经验来平衡。
5.2 项目后续可以扩展的方向
rea 已经稳定跑了大半年,后面我觉得还有几个方向值得探索。最直接的是接入更多数据源,比如 Kafka 和对象存储。目前处理静态文件很顺手,但很多业务日志是实时进入消息队列的,如果能直接从 Kafka 消费数据,就能做到更实时的错误监控。这个功能我已经粗略实现了基础 demo,还没有完全上线,因为要处理消费位点和数据倾斜问题。
另一个方向是内置周期趋势检测。目前 rea 只会告诉你某个窗口有多少次错误,但如果能结合历史数据计算基线,自动发现"今天比上周同期上涨了 300%",就会更有预警价值。再进一步可以引入统计方法或者简单的机器学习模型来做异常检测,但我的建议是先把规则做好,让基础命准率达到 95% 以上,再去谈模型。模型需要大量标注数据,初期投入产出比不高,容易变成负担。
最后是输出 OpenTelemetry 指标的能力。如果能直接把错误计数和指纹维度转为 Prometheus metrics,那么 rea 的分析结果就能被纳管进现有监控体系。这会是一个很自然的演进方向,因为它不需要用户改变工作流,只是把 rea 的输出和主流监控连接起来。
就我个人实际经验来说,rea 这个工具最大的收获不是在算法或者并发上有多高级,而是它逼着我把"到底什么是错误"这个问题想清楚了。在写规则的过程中,团队把这些年所有常见的异常都翻出来重新整理了一遍,还顺手规范了新日志的格式。这个梳理价值远超工具本身。如果你也想做一个类似的工具,我的建议是从自己业务里最高频出现的 20 种错误开始,别急着做通用平台,先把这一小撮错误分析明白,让它真正解决你的痛点,然后再慢慢加功能。