12、日志与输出:Logging级别、Project.logger、标准输出捕获、自定义日志实现、构建扫描
日志这东西,说实话,平时开发时你不太会注意到它。但一旦构建出了问题,它就是你的救命稻草。我见过太多人对着控制台里那一大堆红色错误信息干瞪眼,其实很多时候,只要你会看日志、会用日志,问题早就定位到了。
这一节,我们就来聊聊 Gradle 里的日志体系。我会从最基础的 Logging 级别讲起,再到 Project.logger 怎么用,然后说说标准输出捕获的那些坑,最后带你手写一个自定义日志实现,顺便提一嘴构建扫描。嗯,内容不少,但都很实用。
12.1 Logging 级别:Gradle 的六档变速
Gradle 的日志系统,说白了就是六个级别。你可以把它想象成汽车的档位,从最安静到最啰嗦,依次是:
| 级别 | 用途 | 命令行参数 |
|---|---|---|
| ERROR | 只输出错误信息 | 默认显示 |
| QUIET | 重要的提示信息 | -q 或 --quiet |
| WARNING | 警告信息 | 默认显示 |
| LIFECYCLE | 构建进度信息(默认级别) | 默认显示 |
| INFO | 详细信息 | -i 或 --info |
| DEBUG | 调试信息(最详细) | -d 或 --debug |
我个人习惯,平时用默认的 LIFECYCLE 就够了。但如果你在调试插件,或者排查某个诡异的依赖冲突,那就果断上 -d。我曾经有一次被一个第三方库的版本问题折磨了一下午,最后开了 DEBUG 日志,才发现是某个传递依赖在作祟。嗯,从那以后,我排查问题第一件事就是开 DEBUG。
--info 或 -i 来查看 INFO 级别的日志。DEBUG 级别信息量巨大,建议配合 --stacktrace 一起使用。
12.2 Project.logger:你的随身日志本
在 Gradle 脚本或自定义插件里,你可以通过 project.logger 来输出日志。它提供了对应六个级别的方法:
// 在 build.gradle.kts 中
project.logger.error("这是错误信息")
project.logger.quiet("这是重要提示")
project.logger.warn("这是警告")
project.logger.lifecycle("这是生命周期信息")
project.logger.info("这是详细信息")
project.logger.debug("这是调试信息")
你想想看,如果你在自定义 Task 里想输出一些中间结果,用 println 当然也行,但它会直接打到标准输出,不受日志级别控制。而用 project.logger,你可以精确控制信息在什么级别下显示。比如,调试信息只在 -d 模式下才出现,不会污染正常的构建输出。
我在项目中遇到过这样一个场景:写了一个代码生成插件,生成逻辑比较复杂。我就在关键步骤加了 logger.info 输出,平时构建安安静静,只有出问题时开 --info 才能看到详细流程。这样既保持了控制台整洁,又保留了排查能力。
12.3 标准输出捕获:别让 println 毁了你的构建
这里有个坑,我必须提醒你。Gradle 默认会捕获 Task 中的 println 输出,并将其重定向到 QUIET 级别。也就是说,你在 Task 里写 println("hello"),它其实是以 QUIET 级别输出的。
为什么会这样?因为 Gradle 想把所有输出都纳入自己的日志体系,方便统一管理。但这也带来一个问题:如果你在 Task 里大量使用 println,即使你加了 -q 参数,这些输出依然会显示。因为 QUIET 级别默认就是显示的。
logger 而不是 println。否则你的日志输出会不受控制,该隐藏的隐藏不了,该显示的又显示不出来。
如果你确实需要捕获标准输出,可以用 StandardOutputCapture:
// 在 Task 中
val outputCapture = project.logging.captureStandardOutput(LogLevel.INFO)
// 执行一些操作
outputCapture.stop()
嗯,这个功能我用的不多,但在写测试用例时偶尔会用到。比如你想验证某个 Task 是否输出了预期的信息,就可以用这个来捕获并断言。
12.4 自定义日志实现:打造你的专属日志器
Gradle 的日志系统是可扩展的。如果你觉得内置的日志格式不够用,或者想输出到文件、发送到远程服务器,你可以实现自己的 LoggingManager 或 StandardOutputListener。
这里我给出一个简单的例子:把日志同时输出到控制台和一个文件。
// 自定义日志监听器
class FileLogListener(private val file: File) : StandardOutputListener {
override fun onOutput(character: CharSequence) {
file.appendText(character.toString())
}
}
// 在 build.gradle.kts 中注册
val logFile = project.layout.buildDirectory.file("build.log").get().asFile
val fileListener = FileLogListener(logFile)
project.logging.addStandardOutputListener(fileListener)
// 记得在构建结束时移除
project.gradle.buildFinished {
project.logging.removeStandardOutputListener(fileListener)
}
这个例子虽然简单,但思路是通用的。你可以把日志写到数据库、发到 Slack、或者集成到你的监控系统里。我曾经在一个大型项目中,把构建日志实时推送到 ELK 栈,方便团队统一查看构建历史。嗯,效果还不错。
StandardOutputListener 或 LoggingOutputInternal 接口,然后通过 project.logging 注册进去。Gradle 会负责把所有的日志输出都路由到你的监听器上。
12.5 构建扫描:可视化你的构建过程
构建扫描(Build Scan)是 Gradle 提供的一个非常强大的功能。它能把你的构建过程变成一个可交互的网页,里面包含了所有任务的耗时、依赖关系、测试结果、甚至每个 Task 的输入输出。
启用方式很简单,在 build.gradle.kts 中添加:
// 在 settings.gradle.kts 中
plugins {
id("com.gradle.enterprise") version "3.16.1"
}
gradleEnterprise {
buildScan {
termsOfServiceUrl = "https://gradle.com/terms-of-service"
termsOfServiceAgree = "yes"
}
}
然后执行构建时加上 --scan 参数:
./gradlew build --scan
构建完成后,控制台会输出一个链接。打开它,你会看到一个非常详细的构建报告。我个人特别喜欢它的「性能」页面,能一眼看出哪个 Task 最耗时,哪个 Task 没有缓存命中。我曾经用这个功能优化过一个项目的构建速度,从 5 分钟降到了 2 分钟,全靠构建扫描帮我找到了瓶颈。
12.6 知识体系总览
说了这么多,我们来梳理一下本章的核心逻辑。下面这张图可以帮你快速回顾:
从这张图可以看出,Gradle 的日志体系是分层的。底层是六个日志级别,中间是核心 API,上层是扩展方式,最上面是高级工具。你掌握了这些,基本上就能应对 90% 的日志相关需求了。
好了,这一节的内容就到这里。日志这东西,平时不起眼,但关键时刻能救命。我建议你在自己的项目里试试这些技巧,特别是构建扫描,用一次你就会爱上它。
公众号:蓝海资料掘金营,微信deep3321