为何lein repl会阻塞STDERR?如何实现双端输出适配systemd日志
这个问题我之前也碰到过——Leiningen默认会把NREPL会话里的System/err输出绑定到当前REPL连接,而不是转发到启动进程的终端(也就是systemd监控的那个stderr流)。要实现两端同时输出,有几个实用的方案,根据你的场景选就行:
方案1:全局配置(所有项目生效)
如果你希望所有Leiningen项目都默认支持这个功能,可以修改你的全局Leiningen配置文件~/.lein/profiles.clj,添加:user配置段,在REPL初始化时包装System/err:
{:user {:repl-options {:init (do ;; 导入Java的TeeOutputStream类,用于复制输出到两个流 (import [java.io TeeOutputStream]) ;; 获取启动终端的stderr流(Linux下是/dev/stderr) (let [terminal-stderr (clojure.java.io/output-stream "/dev/stderr") ;; 当前REPL会话的stderr流 repl-stderr System/err] ;; 把System/err设置为同时输出到两个流的TeeOutputStream (System/setErr (TeeOutputStream. repl-stderr terminal-stderr))))}}}
保存后,不管是在无项目环境下运行lein repl,还是在项目里启动REPL,只要执行(.println System/err "test"),内容会同时出现在REPL会话和启动终端里,systemd也能捕获到日志。
方案2:单个项目配置(仅当前项目生效)
如果只需要给特定项目配置,直接修改项目根目录下的project.clj,添加同样的REPL初始化逻辑:
(defproject your-app "0.1.0-SNAPSHOT" ;; 其他配置... :repl-options {:init (do (import [java.io TeeOutputStream]) (let [terminal-stderr (clojure.java.io/output-stream "/dev/stderr") repl-stderr System/err] (System/setErr (TeeOutputStream. repl-stderr terminal-stderr))))})
方案3:针对lein run的应用代码修改
如果你的场景是用lein run启动应用,并且希望应用运行时(包括远程REPL连接后的输出)的stderr同时输出到终端和systemd,可以在应用的-main函数开头添加包装逻辑:
(ns your-app.core (:import [java.io TeeOutputStream]) (:gen-class)) (defn -main [& args] ;; 包装System/err为双输出流 (let [terminal-stderr (clojure.java.io/output-stream "/dev/stderr") current-err System/err] (System/setErr (TeeOutputStream. current-err terminal-stderr))) ;; 你的应用业务代码... (.println System/err "This will show up in both REPL and systemd logs!") )
关键原理说明
Java的TeeOutputStream会把所有写入它的字节同时复制到两个指定的输出流中——这里我们把它分别指向REPL会话的stderr和启动终端的/dev/stderr(也就是systemd监控的进程stderr)。这样不管是在本地REPL还是远程NREPL连接中输出的stderr内容,都会同时出现在两个地方。
注意:这个方案基于Linux环境(因为用到了/dev/stderr),如果你在Windows上使用,需要替换成对应的标准错误流路径,不过你提到了systemd,应该是Linux环境,所以没问题。
内容的提问来源于stack exchange,提问作者user12769468

