Toyokumo Tech Blog

トヨクモ株式会社の開発本部のブログです。

Clojureで作るAPI ロギングできるようにする

[連載]Clojureで作るAPIの8記事目です。

前回の記事はこちらです。

tech.toyokumo.co.jp

この記事ではロギングをできるようにしていきます。

Javaのロギングの歴史的経緯と現在

Clojureアプリケーションは多くの部分でJavaのライブラリに依存していることから、ClojureアプリケーションのロギングにJavaのロギングの歴史的経緯が関わってきます。

Javaのロギングの歴史的経緯は複雑です。この項ではそれを必要最低限理解するための情報を提供したいと思います。 この項を飛ばして進むことも可能です。興味のある方はお付き合いいただければと思います。

まずJavaのロギングライブラリの構造について理解するためには デザインパターン (ソフトウェア) - Wikipedia の1つである Facadeパターン について理解する必要があります。

リンク先を読んでいただけたけると、Facadeパターンとは実装の詳細を隠蔽し、簡単に使えるインターフェースを用意することだと理解いただけると思います。 理解いただけたという前提で本題のJavaのロギングライブラリの解説に移っていきますが、全てをこの記事に書き切ることはできないので、良い記事を2つ紹介します。

まずこちらの記事の図を見ながらイメージを掴んでいただければと思います。

qiita.com

次にこちらのスライドで登場するライブラリの名前をなんとなくでも頭に入れてください。

www.slideshare.net

これらの記事からわかることをまとめます。

  • よく名前を見るJavaのロギングライブラリにはFacadeと実装がある
  • 今のデファクトスタンダードのFacadeはCommons LoggingではなくSLF4J
  • SLF4Jはさまざまなログ出力実装へのbinding(2.0.0からはproviderと言われる)も同時に提供しており、それらのbindingはログ出力実装をSLF4Jを使用しているように変換し、SLF4J+自分が設定したログ出力実装の組み合わせに適合させてくれる
  • LogbackはSLF4Jと一緒に使うログ出力実装としてSLF4Jの作者が作ったもので、SLF4JとLogbackの組み合わせは広く使われている。SLF4Jの公式にも次の記述がある
    • Logback's ch.qos.logback.classic.Logger class is a direct implementation of SLF4J's org.slf4j.Logger interface. Thus, using SLF4J in conjunction with logback involves strictly zero memory and computational overhead.

簡単ですがここまで理解できれば、実際にログ出力をするための知識としては十分です。

unilogを使って設定する

この連載では上記で説明したように最もスタンダードな組み合わせだと思われるSLF4JとLogbackの組み合わせを使っていきます。

Logbackはクラスパス上の logback.xml というXMLファイルで設定ファイルを記述します。 しかしXMLファイルの記述はちょっと大変です。 また各ログ実装のproviderを1つずつ調べて依存関係に入れていくのも手間です。

そこでClojureのデータとしてLogbackの設定を書けるようにしてくれ、また各ログ実装のproviderをまとめて依存関係に入れてくれるClojureライブラリであるunilogを使います。

github.com

いつもなら deps.edn に依存の記述を追加するところですが、実はunilogはすでにClojureで作るAPI Web サーバーを立ち上げる - Toyokumo Tech Blogで追加していました。

ですのでClojureのロギングのFacadeである clojure.tools.logging だけを新たに依存に加えます。

github.com

;; ./deps.edn
{:paths ["src" "resources"]
 :deps {org.clojure/clojure {:mvn/version "1.11.1"}
        info.sunng/ring-jetty9-adapter {:mvn/version "0.17.6" :exclusions [org.slf4j/slf4j-api]}
        org.clojure/tools.logging {:mvn/version "1.2.4"} ;; 追加
        spootnik/unilog {:mvn/version "0.7.30"}
        com.stuartsierra/component {:mvn/version "1.1.0"}
        aero/aero {:mvn/version "1.1.6"}}
 :aliases {:dev {:extra-paths ["dev"]}
           :build {:deps {io.github.clojure/tools.build {:git/tag "v0.8.2" :git/sha "ba1a2bf"}}
                   :ns-default build}}}

つまり、clojure.tools.logging -> SLF4J -> Logbackという図式を作ります。

ログ出力の設定を config.edn に記載します。

;; ./resources/config.edn
{:logging {:level #profile {:dev :debug
                            :default :info}
           :console #profile {:prod {:encoder :json}
                              :default true}
           :overrides {"org.eclipse.jetty" :info}}

 :server {:opts {:host "localhost"
                 :port #long #profile {:default 5000
                                       :dev 8000}
                 :join? false}}}

:logging というキーのバリューに設定を定義してあります。

  • :level ログレベルです。開発中はdebugレベル以上、それ以外ではinfoレベル以上で出力されるようにしています。
  • :console 標準出力に出す設定です。本番ではJSONで出力し、それ以外ではデフォルトのフォーマットで出力されるようにしています。
  • :overrides ログレベルを特定のパッケージに対して設定しています。 org.eclipse.jetty はWebサーバーですが、開発時にdebugレベルだとログが多すぎるので抑制しています。

Systemと統合する

それではSystemと統合してログ出力を足していきましょう。

;; ./src/cljapi/sytem.clj
(ns cljapi.system
  (:require
   [cljapi.component.handler :as c.handler]
   [cljapi.component.server :as c.server]
   [cljapi.config :as config]
   [clojure.tools.logging :as log]
   [com.stuartsierra.component :as component]
   [unilog.config :as unilog]))

(defn- new-system [config]
  (component/system-map
   :handler (c.handler/map->Handler {})
   :server (component/using
            (c.server/map->Jetty9Server (:server config))
            [:handler])))

(defn- init-logging! [config]
  (unilog/start-logging! (:logging config)))

(defn start [profile]
  (let [config (config/read-config profile)
        system (new-system config)
        _ (init-logging! config)
        _ (log/info "system is ready to start")
        started-system (component/start system)]
    (log/info "system is started")
    started-system))

(defn stop [system]
  (component/stop system))

init-logging! でunilogを使ってロギングの初期化をしています。 これを start に組み込むことで開発時と本番環境のいずれにおいても初期化がされるようになります。

REPLを立ち上げて (go) を評価することで実際にログが出力されることを確かめてみましょう。

INFO [2022-07-01 15:49:00,546] nREPL-session-f03ea922-17bf-447e-987c-43ddca2b2d9d - cljapi.system system is ready to start
INFO [2022-07-01 15:49:00,582] nREPL-session-f03ea922-17bf-447e-987c-43ddca2b2d9d - org.eclipse.jetty.server.Server jetty-10.0.9; built: 2022-03-30T16:46:32.527Z; git: a9eaf8d5d73369acf610ce88f850c0d56c4b1113; jvm 11.0.15+9-LTS
INFO [2022-07-01 15:49:00,650] nREPL-session-f03ea922-17bf-447e-987c-43ddca2b2d9d - org.eclipse.jetty.server.handler.ContextHandler Started o.e.j.s.ServletContextHandler@27fdbafe{/,null,AVAILABLE}
INFO [2022-07-01 15:49:00,657] nREPL-session-f03ea922-17bf-447e-987c-43ddca2b2d9d - org.eclipse.jetty.server.AbstractConnector Started ServerConnector@1eb67498{HTTP/1.1, (http/1.1)}{localhost:8000}
INFO [2022-07-01 15:49:00,666] nREPL-session-f03ea922-17bf-447e-987c-43ddca2b2d9d - org.eclipse.jetty.server.Server Started Server@4b8e5772{STARTING}[10.0.9,sto=0] @6911ms
INFO [2022-07-01 15:49:00,666] nREPL-session-f03ea922-17bf-447e-987c-43ddca2b2d9d - cljapi.system system is started

Jettyが出すログと共に自分で記述したログが同じフォーマットで出力されていることが確認できます。

次にビルドしてJARを実行したときのログを見てみます。

$ make build
$ java -jar target/cljapi.jar
{"@timestamp":"2022-07-01T15:19:41.566+09:00","@version":"1","message":"system is ready to start","logger_name":"cljapi.system","thread_name":"main","level":"INFO","level_value":20000}
{"@timestamp":"2022-07-01T15:19:41.608+09:00","@version":"1","message":"jetty-10.0.9; built: 2022-03-30T16:46:32.527Z; git: a9eaf8d5d73369acf610ce88f850c0d56c4b1113; jvm 11.0.15+9-LTS","logger_name":"org.eclipse.jetty.server.Server","thread_name":"main","level":"INFO","level_value":20000}
{"@timestamp":"2022-07-01T15:19:41.672+09:00","@version":"1","message":"Started o.e.j.s.ServletContextHandler@76e3b45b{/,null,AVAILABLE}","logger_name":"org.eclipse.jetty.server.handler.ContextHandler","thread_name":"main","level":"INFO","level_value":20000}
{"@timestamp":"2022-07-01T15:19:41.678+09:00","@version":"1","message":"Started ServerConnector@5d3b58ca{HTTP/1.1, (http/1.1)}{localhost:5000}","logger_name":"org.eclipse.jetty.server.AbstractConnector","thread_name":"main","level":"INFO","level_value":20000}
{"@timestamp":"2022-07-01T15:19:41.687+09:00","@version":"1","message":"Started Server@7f2d31af{STARTING}[10.0.9,sto=0] @902ms","logger_name":"org.eclipse.jetty.server.Server","thread_name":"main","level":"INFO","level_value":20000}
{"@timestamp":"2022-07-01T15:19:41.687+09:00","@version":"1","message":"system is started","logger_name":"cljapi.system","thread_name":"main","level":"INFO","level_value":20000}

設定通り、JSONで出力されていることが確認できます。

おわりに

ここまでで、ロギングのライブラリと設定を適切に行い、実際にログ出力できていることが確認できました。

コードはGitHubのリポジトリに上げてあります。 08_ロギングできるようにするというタグからご覧ください。

次はテストの設定を行い、簡単なテストを書いてみます。


トヨクモでは一緒に働いてくれる技術が好きなエンジニアを募集しております。

採用に関する情報を公開しております。 気になった方はこちらからご応募ください。