Clojure中的惯用日志记录

5

我是Clojure的新手,想了解在Clojure中记录日志的方法,我的编程背景是命令式的。在Java生产程序中,我通常会在方法的开始和结束处进行日志记录(调试/信息),类似于这样:

public void foo(){
   logger.debug("starting method to embrace %s", rightIdioms);
   doSomething();
   logger.debug("successfully embraced %s idioms", idioms.length);
}

我熟悉记录日志的优缺点,并知道Clojure中可用的工具,
我还可以找到上述方法中记录日志的一些缺点,这加深了我在非命令式记录日志时感到的紧张情绪。
  1. logging is a side effect and clojure pushes to no-side-effects.
  2. more lines-of-code or 'code-complexity': in java - having big classes is common (getters, setters, constructors), in clojure, expressions return values, logging 'difficults' the process and hardens small functions and namespaces: (one example would be the need to change from if to if-let or if-do to perform a logging):

    (defn foo [x]
      (if (neg? x)
        (inc x)
        x))
    
    (defn foo [x]
      (if (neg? x)
        (let [new-x (inc x)] 
          (logger/debug (format "inc value, now %s" new-x)
          new-x))
        x))
    
我已阅读使用 clojure/taptracing 进行日志记录,但不确定其是否完全有用。
在Clojure中进行日志记录的方法或惯用方式是什么?
5个回答

3
对于具有多个入口/出口和服务的大型项目,我会选择使用Riemann,如JUXT的博客文章所述。
对于单个应用程序或服务,我会使用纯文本日志记录,结合使用Cambium进行JSON日志记录(它提供了用于日志记录的良好编码器和宏),以及Unilog(它允许使用Clojure数据结构配置和启动Logback)。
Logback是Java东西,是流行的Log4j的后继者,但更快,更灵活,具有兼容的API。由于Unilog的存在,无需手工制作其XML格式的配置文件。值得阅读Logback的文档以了解Java日志记录-它甚至包含体系结构图。在30分钟内,基础知识就会清晰,然后可以使用Clojure层。
以下是我的配置示例,其中包括自定义的JSON编码器(标识为:json-console:json-log):
(ns io.randomseed.blabla.logging


(:require   [cheshire.core                   :as cheshire]
            [unilog.config                   :as   unilog]
            [cambium.codec                   :as    codec]
            [cambium.core                    :as      log]
            [cambium.mdc                     :as     mlog]
            [logback-bundle.json.flat-layout :as     flat])

  (:import  [logback_bundle.json                 FlatJsonLayout ValueDecoder]
            [ch.qos.logback.contrib.jackson      JacksonJsonFormatter]
            [ch.qos.logback.core.encoder         LayoutWrappingEncoder]
            [ch.qos.logback.contrib.json.classic JsonLayout]
            [java.nio.charset                    Charset])))

(def config
  {:level      :info
   :console    false
   :appenders  [{:appender       :console
                 :encoder        :pattern
                 :pattern        "%p [%d{yyyy-MM-dd HH:mm:ss.SSS Z}] %t - %c %m%n"}
                {:appender       :rolling-file
                 :file           "/home/users/siefca/.blabla/blabla.log"
                 :pattern        "%p [%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ}] %t - %c %m%n"
                 :rolling-policy {:type              :time-based
                                  :max-history                 7
                                  :pattern     ".%d{yyyy-MM-dd}"}}
                {:appender        :rolling-file
                 :file            "/home/users/siefca/.blabla/blabla-json.log"
                 :encoder         :json-log
                 :rolling-policy  {:type             :time-based
                                   :max-history                7
                                   :pattern    ".%d{yyyy-MM-dd}"}}]
   :overrides  {"org.apache.http"      :debug
                "org.apache.http.wire" :error}})

;;
;; JSON should not be stringified when using flat layout
;;

(flat/set-decoder! codec/destringify-val)

;;
;; Cheshire attached to FlatJsonLayout
;; gives more flexibility when it comes
;; to expressing different objects 
;;

(FlatJsonLayout/setGlobalDecoder
 (reify ValueDecoder
   (decode [this encoded-value]
     (cheshire/parse-string encoded-value))))

;;
;; Custom encoders
;;

(defmethod unilog/build-encoder :json-console
  [config]
  (assoc config :encoder (doto (LayoutWrappingEncoder.)
                           (.setCharset (Charset/forName "UTF-8"))
                           (.setLayout  (doto (FlatJsonLayout.)
                                          (.setIncludeMDC                true)
                                          (.setIncludeException          true)
                                          (.setAppendLineSeparator       true)
                                          (.setTimestampFormatTimezoneId "UTC")
                                          (.setTimestampFormat           "yyyy-MM-dd HH:mm:ss.SSS")
                                          (.setJsonFormatter (doto (JacksonJsonFormatter.)
                                                               (.setPrettyPrint true))))))))

(defmethod unilog/build-encoder :json-log
  [config]
  (assoc config :encoder (doto (LayoutWrappingEncoder.)
                           (.setCharset (Charset/forName "UTF-8"))
                           (.setLayout  (doto (FlatJsonLayout.)
                                          (.setIncludeMDC                 true)
                                          (.setIncludeException           true)
                                          (.setAppendLineSeparator       false)
                                          (.setTimestampFormatTimezoneId "UTC")
                                          (.setJsonFormatter (doto (JacksonJsonFormatter.)
                                                               (.setPrettyPrint false))))))))

;;
;; Let's start logging
;;

(unilog/start-logging! config)

依赖项:

{:deps {logback-bundle/json-bundle                    {:mvn/version "0.3.0"}
        cambium/cambium.core                          {:mvn/version "1.1.0"}
        cambium/cambium.logback.core                  {:mvn/version "0.4.4"}
        cambium/cambium.logback.json                  {:mvn/version "0.4.4"}
        cambium/cambium.codec-cheshire                {:mvn/version "1.0.0"}
        spootnik/unilog                               {:mvn/version "0.7.27"}}}

2

这篇博客文章提供了一些最佳实践,其中建议记录数据而不是字符串,这可能非常有用并符合clojure-style的日志记录。一个日志事件可能如下:

{:service     :user.profile/update
 :level       :debug
 :time        "2020-01-01"
 :description {:before '0
               :after  '1}
 :metric       10ms}

metric可以是任何东西,从更新所需的时间到从数据库中提取的行数。

然后,当您拥有数据时,您可以对其执行任何操作-分析它以获得洞见或按组进行搜索和查找。如果需要进行控制台日志记录,您始终可以将数据结构转换回字符串。


1
编译后的Clojure代码是100%的Java代码。因此,我建议使用标准的Java记录器和Clojure包装器:log4j2(java)+ pedestal.log(Clojure)。这种方法允许混合使用Java项目和Clojure代码。
以下是适用于Clojure的log4j设置示例。Log4j2 example。只需将{{namespace}}占位符替换为您的名称空间即可。这些设置允许将EDN结构作为日志输出。不要使用字符串作为日志数据,因为将数据放入字符串会破坏数据。解析日志字符串是不好的想法。
要将任何数据记录到log4j2,请使用pedestal.log库Pedestal.log 这个包装器永远不会阻塞代码线程,并在不同的线程中执行记录数据。还会打印命名空间(类)名称和源行。
同时,您可以使用clojure.core中的tap>功能记录数据。这里是文档Clojure tap>。这里是描述How is tap> works。tap是一个共享的、全局可访问的系统,用于将一系列信息或诊断值分发给一组(可能具有效果的)处理程序函数。它可以用作更好的debug prn,或者用于类似日志记录等功能。您可以使用不同的日志记录适配器进行调试tap> example

1
我认为目前Clojure中最好的日志记录库是Cambium。我认为它比其(较旧的)竞争对手Timbre更好一些。
为了帮助在记录或调试函数输出时的程序流程,我有时会使用Tupelo Library中的一个小with-result宏。例如:
  (is= 42
    (with-result 42
      (spyx (+ 2 3))))

这个单元测试表明,即使最内层的表达式是 5,结果仍会返回 42。当运行该测试时,调试工具 spyx(代表“spy explicit”)会打印以下内容:
(+ 2 3) => 5

如果您想要一个永久的日志输出,可以使用Cambium,例如:
(log/info "Application started")
(log/info {:args (vec args) :argc (count args)} "Arguments received")

带有结果的:

结果如下:

18:56:42.054 [main] INFO  myapp.main - Application started { ns=myapp.main, line=8, column=3 }
18:56:42.060 [main] INFO  myapp.main - Arguments received { args=["foo" "10"], argc=2, ns=myapp.main, line=9, column=3 }

我会稍微修改这个函数,将最终结果保存到一个变量中,然后以以下两种形式之一进行日志记录:
(ns tst.demo.core
  (:use tupelo.core tupelo.test)
  (:require [cambium.core :as log]))


(defn foo-1 [x]
  (let [result (if (neg? x)
                 (inc x)
                 x)]
    (log/debug (format "foo-1 => %s" result))
    result))

(defn foo-2 [x]
  (let [new-x (if (neg? x)
                (inc x)
                x)]
    (with-result new-x
      (log/debug (format "foo-2 => %s" new-x)))))

(dotest
  (is= 42
    (with-result 42
      (spyx (+ 2 3))))

  (is=  2 (foo-1 2))
  (is= -1 (foo-1 -2))

  (is=  2 (foo-2 2))
  (is= -1 (foo-2 -2))
  )

产生输出:

-------------------------------
   Clojure 1.10.1    Java 13
-------------------------------

Testing tst.demo.core
(+ 2 3) => 5
11:31:47.377 [main] DEBUG tst.demo.core - foo-1 => 2 { ns=tst.demo.core, line=15, column=5 }
11:31:47.378 [main] DEBUG tst.demo.core - foo-1 => -1 { ns=tst.demo.core, line=15, column=5 }
11:31:47.379 [main] DEBUG tst.demo.core - foo-2 => 2 { ns=tst.demo.core, line=23, column=7 }
11:31:47.379 [main] DEBUG tst.demo.core - foo-2 => -1 { ns=tst.demo.core, line=23, column=7 }

Ran 2 tests containing 7 assertions.
0 failures, 0 errors.

对于临时调试打印输出,我会这样做:

(defn foo [x]
  (if (neg? x)
    (spyx :foo-inc (inc x))
    (spyx :foo-noop x)))

测试中:

  (is=  2 (foo 2))
  (is= -1 (foo -2))

和输出

:foo-noop x => 2
:foo-inc (inc x) => -1

示例项目

您可以克隆以下存储库以查看所有设置:


0

记录日志不会对程序数据产生副作用,因此Clojure伦理并不反对它。

为了避免重新设计一个函数以记录其输入和输出,一个快速的方法是滥用前置条件和后置条件:

(defn foo [x]
  {:pre [(do (println "foo" x) 
             true)]
   :post [(do (println "foo" x "==>" %) 
              true)]}
  (if (neg? x)
    (inc x)
    x))

true 使得条件成立。否则,程序将停止。

预先和后置条件在此处有文档记录: https://clojure.org/reference/special_forms#_fn_name_param_condition_map_expr_2

这是增强日志的 foo 在 REPL 中的样子:

user> (foo -7)
foo -7
foo -7 ==> -6
-6

2
记录日志不会对程序数据产生副作用,因此Clojure伦理并不反对它。任何在函数内改变系统状态的操作都是副作用,因此记录日志也是一种副作用。 - m0skit0
2
@m0skit0,在Clojure中,我们只反对那些会在以后给我们带来麻烦的副作用。Clojure对函数纯度有相当宽松的观点。例如,可以看看Clojure的瞬时数据结构 - “如果树倒在森林里,它会发出声音吗?” - 以及Clojure自己的日志记录库clojure.tools.logging - Biped Phill

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接