com.github.seancorfield/logging4j2
一个用于 log4j2 的 Clojure 封装库,旨在作为
clojure.tools.logging 的部分替代品,
直接支持 MDC(映射诊断上下文)、NDC(嵌套诊断上下文)
和标记,并以符合 Clojure 习惯的方式提供。
该库显式依赖于 log4j2 以及所有将其他日志框架路由到 log4j2 的桥接库 (jcl、jul、log4j 1.x、slf4j 1.x 和 2.x)。
注意:需要 Clojure 1.11 或更高版本。
安装
将以下依赖项添加到您的 deps.edn 文件中:
com.github.seancorfield/logging4j2 {:mvn/version "1.0.9"}
此版本的库依赖于 log4j2 2.26.1,这是截至 2026 年 5 月的最新稳定版本。
用法
引入主 org.corfield.logging4j2 命名空间(:as 任意你喜欢的别名
-- 以下示例使用 logger):
该库提供以下宏,可用于记录信息:
trace、debug、info、warn、error 和 fatal。此外还有一个
通用的 log 宏,其第一个参数接受一个级别关键字。
(logger/info "Hello, world!")
(logger/log :info "Hello, world!") ; equivalent to the above
对 MDC 和 NDC 的支持通过 with-log-context、with-log-tag 和
with-log-uuid 宏提供(有关更多详细信息,请参阅下文 MDC and NDC):
with-log-context接受一个哈希映射作为其第一个参数,后跟要执行的代码主体。哈希映射中的键和值将作为字符串添加到上下文中,用于执行主体。with-log-tag接受一个关键字或字符串作为其第一个参数,后跟要执行的代码主体。该标签被推送到上下文中,用于执行主体。with-log-uuid接受要执行的代码主体。一个唯一的标签被推送到上下文中,用于执行主体。
(logger/with-log-context {:uid (:id user)}
(logger/info "Hello, world!")) ; INFO {uid=1234} Hello, world!
(logger/with-log-tag (str "user_" (:id user))
(logger/info "Hello, world!")) ; INFO [user_1234] Hello, world!
(logger/with-log-uuid
(logger/info "Hello, world!")) ; is equivalent to
(logger/with-log-tag (str (random-uuid))
(logger/info "Hello, world!")) ; INFO [8b21769c-33c5-42cb-b6c4-146ce8bb875f] Hello, world!
对标记的支持由 as-marker 函数提供,该函数接受一个或多个关键字或字符串,并返回一个标记对象,该对象可以作为任何日志宏的第一个参数(或通用 log 宏的第二个参数)传递。
(as-marker :sql-update :sql) 返回一个标记对象,该对象表示字符串 "SQL_UPDATE",其父级为标记 "SQL"。
可能的参数
大多数日志记录看起来像这样:
(logger/info "some message")
;; or
(logger/info my-exception "some message")
;; or
(logger/info my-marker "some message")
;; or
(logger/info my-marker my-exception "some message")
如果提供了多个消息参数,它们通常会被转换为字符串,并在它们之间用空格连接,但以下情况除外:
- 如果第一个消息参数是字符串,并且包含一个或多个
{}占位符,则其余参数被视为这些占位符的值,并 构造一个ParameterizedMessage。 - 如果只提供了一个消息参数,且它是一个哈希映射,则
构造一个
MapMessage,其中 Clojure 哈希映射的键被转换为字符串, 值保持不变。
你可以直接使用 logger/as-message 构建一个 Message:
(logger/as-message "Hello, {}!" "Parameter")
;; => ParameterizedMessage
(logger/as-message {:hello "world"})
;; => MapMessage
(logger/as-message "Hello," "World!")
;; => SimpleMessage
如果您使用的是 Clojure 1.12(或更高版本),您可以提供一个 (fn [] ...),
它将用作 MessageSupplier 对象:
(logger/info #(logger/as-message "Hello, Supplier!"))
在 Clojure 1.11 中,你必须直接构造 MessageSupplier 对象,
例如通过实现 MessageSupplier 接口并使用 reify 来提供 get 方法:
(logger/info (reify org.apache.logging.log4j.message.MessageSupplier
(get [_] (logger/as-message "Hello, Supplier!"))))
MDC 和 NDC
映射诊断上下文 (MDC) 和嵌套诊断上下文 (NDC) 由三个 with-log-* 宏支持
(如上所述)。对这些宏的嵌套调用
将在当前线程内自动累积映射上下文和标签栈。
log4j2 的底层上下文默认是线程本地的:每个线程
初始时具有空的上下文。该库还使用 Clojure 中的动态 var 来跟踪 MDC 和 NDC,因此你可以在生成的
线程内使用 with-log-inherited 来继承父线程的 MDC 和 NDC。
(logger/with-log-context {:uid (:id user)}
(future
(logger/with-log-inherited
(logger/info "Hello, world!")))) ; INFO {uid=1234} Hello, world!
如果你在线程之间传递函数,你可能需要使用 bound-fn
或 bound-fn* 以便将动态上下文传递到新的线程。
注意:即使子线程中存在未继承上下文的
with-log-context、with-log-tag或with-log-uuid的中间调用,with-log-inherited也会从动态父上下文继承整个 MDC/NDC。
配置
Log4j2 非常灵活,可以通过 XML、JSON、YAML 或 properties 文件进行配置。请参阅 log4j2 配置文档。
配置文件可以位于类路径上(例如,在你的 resources
文件夹中),或者你可以使用 log4j2.configurationFile
JVM 属性或 LOG4J_CONFIGURATION_FILE 环境变量来指定名称/位置。
我更喜欢通过 properties 文件进行配置,因此我通常在 resources/log4j2.properties 中
包含类似以下内容:
rootLogger.level = info
rootLogger.appenderRef.stdout.ref = STDOUT
rootLogger.appenderRef.file.ref = logfile
appender.console.type = Console
appender.console.name = STDOUT
appender.console.filter.threshold.type = ThresholdFilter
appender.console.filter.threshold.level = info
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = [%c] %x %X %highlight{%m}%n
在开发过程中,我通常会有一个仅用于开发的目录(通过别名添加到类路径中)里的第二个属性文件,名为 log4j2-test.properties:
# so that log4j2 checks every 30 seconds for changes in configuration:
monitorInterval = 30
rootLogger.level = debug
rootLogger.appenderRef.stdout.ref = STDOUT
rootLogger.appenderRef.file.ref = logfile
appender.console.type = Console
appender.console.name = STDOUT
appender.console.filter.threshold.type = ThresholdFilter
appender.console.filter.threshold.level = debug
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = [%c] %x %X %highlight{%m}%n
您可能还希望将某些在 info 或 debug 级别非常健谈的库的日志级别阈值配置得更高:
# Chatty libraries we need to suppress:
logger.sshj.name = net.schmizz.sshj
logger.sshj.level = warn
logger.hikari.name = com.zaxxer.hikari
logger.hikari.level = warn
logger.authnet.name = net.authorize
logger.authnet.level = warn
logger.eclipse1.name = com.eclipse
logger.eclipse1.level = warn
logger.eclipse2.name = org.eclipse
logger.eclipse2.level = warn
java.util.logging 桥接
为了正确地将 java.util.logging (JUL) 桥接到 log4j2,您需要
提供某种形式的配置。JUL-to-Log4j bridge docs
中描述了多种选项,同时也解释了 JUL 所施加的、使此操作成为必要的限制。
最简单的方法是设置 java.util.logging.manager JVM 属性。
如果您能够保证在您的应用程序获得
控制权之前(在您的 -main 函数中)不会进行任何日志记录,那么您可以在代码中设置此属性:
(System/setProperty "java.util.logging.manager"
"org.apache.logging.log4j.jul.LogManager")
您可以将此作为顶级表单放在主命名空间中,例如,紧跟在 ns 表单之后。
如果您的代码运行在可能在您的应用程序获得控制权之前初始化 JUL 的上下文中(例如,servlet 容器),则需要在启动 JVM 时将此属性设置为 JVM 属性,例如,
java -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-jar your.jar
如果你正在使用 Clojure CLI,你可以直接设置它,或者通过别名进行设置。使用 CLI 的 -J 选项直接设置:
clojure -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-M -m your.namespace
在 deps.edn 中通过别名设置:
{:aliases
{:jul
{:jvm-opts ["-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"]}}}
然后你可以使用别名运行你的代码:
clojure -M:jul -m your.namespace
如果您为 log4j2 配置了日志级别,但它们似乎未按预期工作,
您可能遇到了这个
log4j-jul 问题,
其中某些代码正在以编程方式设置 JUL 日志级别,
从而覆盖了您的配置。在 log4j2 2.24.0 及更高版本中,
这不应再是一个问题,但如果您希望该编程设置生效,
即覆盖您的配置,您可以将 log4j2.julLoggerAdapter
JVM 属性设置为 org.apache.logging.log4j.jul.CoreLoggerAdapter(
这是 log4j2 2.23.0 中的默认行为)。
如上所述,您可以通过 CLI 的 -J 选项或 deps.edn 中的别名
进行设置,或者直接针对 java 命令进行设置:
java -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-Dlog4j2.julLoggerAdapter=org.apache.logging.log4j.jul.CoreLoggerAdapter \
-jar your.jar
logj42 与 tools.build
如果你正在使用 tools.build (build.clj) 来构建 uberjars,
并且你正在使用本库或直接使用 log4j2,
你可能需要查看这个
log4j2 冲突处理器,用于 tools.build。
如果你的项目有提供 log4j2 插件的依赖项
(例如模板),使用冲突处理器将确保
这些插件的配置被正确合并。
clojure.tools.logging 迁移/常见问题
c.t.l 有 logf,但本库没有。对于大多数用途(配合 %s),你
可以使用一个 ParameterizedMessage -- 将 %s 替换为 {},你
应该会得到相同的行为。对于更复杂的案例,你可以使用
clojure.core/format 配合参数来构建用于日志记录的字符串。
你不再需要 -Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory
因为本库直接使用 log4j2(c.t.l 默认使用 slf4j)。
如果你想直接获取一个 Logger 对象,你可以使用
org.apache.logging.log4j.LogManager/getLogger,传入一个命名空间
名称作为字符串。c.t.l 在其 impl 命名空间中提供了一个通用的 get-logger。
许可证
版权所有 © 2024-2026 Sean Corfield。