第12章 Groovyによる設定

満足した豚になるより不満を抱えた人間になるほうがずっと良い。ましてや、満足気な愚か者になるより不満だらけのソクラテス派になるほうがずっと良い。豚や愚か者がこれとは異なる主張をしたとしても、それは彼らに良いとされる側の経験が無いからなのだ。

-ジョン·スチュアート·ミル、 功利主義

ドメイン固有言語やDSLはかなり普及しています。XMLベースのlogbackの設定は、DSLのインスタンスとみなすことができます。XMLの性質上、設定ファイルは非常に冗長でかさばるものになります。さらに、logbackのコードの大部分はXMLベースの設定ファイル処理専用のJoranと呼ばれるものです。Joranは、変数置換や条件分岐、および実行時の拡張など、気の利いた機能をサポートしています。しかし、Joranの問題は複雑さだけではありません。ユーザーエクスペリエンスは不十分ですし、直感とはかけ離れています。

この章で説明するGroovyベースのDSLは、一貫性があり、直感的で、かつ、強力であることを目指しています。XMLの設定ファイルでできることはすべて、それもより短い行数で実現することができます。Groovyスタイルの設定ファイルへの移行を支援するために、既存のlogback.xmlを自動的にlogback.groovyへ変換するツールを用意しました

基本的な哲学

基本的なルールを説明します。logback.groovyはGroovyのプログラムです。Groovy言語はJava言語のスーパーセットなので、Javaにできるあらゆる設定アクションと同じことをlogback.groovyで実行することができます。ですが、logbackをJavaの構文でプログラム的に設定するのはかなり厄介なので、logback専用の拡張構文を追加しました。logback専用の拡張構文はできるだけ少なくなるようにしましたし、実際のところほんのわずかしかありません。Groovyに慣れているとしても本章に目を通してもらって、logback.groovyを書くのは非常に簡単であることを理解してください。Groovyに不慣れな方であっても、logback.xmlを使い続けるよりlogback.groovyの記法のほうがずっとわかりやすいと思えるようになるはずです。

改めて整理します。logback.groovyは最小限のlogback専用の拡張がなされたGroovyプログラムです。logback.groovyの中では、クラスのimportや変数定義、変数評価、GString の${..}記法、if-else構文などのGroovyの機能は全て利用可能です。

自動import

logback1.0.10以降決まりきったものになる共通するクラスやパッケージのimportをしなくてもすむように、自動的にimportします。したがって、組み込みのアペンダーやレイアウトについてはわざわざimport文を書かなくても設定だけでよいのです。もちろん、デフォルトのimportでカバーされていないクラスやパッケージがあるなら、それは自分でやらなければなりません。

デフォルトのimport対象は次のとおりです。

さらに、ch.qos.logback.classic.Levelの全ての定数は、大文字バージョンと小文字バージョンのそれぞれでstatic importされます。つまり、スクリプトではINFOinfoのどちらでも利用できます。

SiftingAppenderはサポートされなくなりました

logback1.0.12以降Groovy設定ファイルではSiftingAppenderはサポートされなくなりました。需要がありそうなら復活するかもしれません。

logback.groovy用の拡張構文

基本的にlogback.groovyの構文は、次に説明する半ダースほどのメソッドで構成されています。これらは実際に定義する順番とは逆順に並んでいます。厳密に言えば、これらのメソッドの呼び出し順序は1つの例外(アペンダーはそれを割り当てるロガーの前に定義しなければならない)を除いて重要ではありません。

root(Level level, List<String> appenderNames = [])

rootメソッドはルートロガーのログレベルを設定するために使用します。第二引数のappenderName(List<String>)は任意で、ルートロガーに割り当てるアペンダーを名前で指定します。引数に値を指定しなければ、空のリストが指定されたものとして扱います。。Groovyでは空のリストを[]で記述します。

ルートロガーのログレベルにWARNを設定するには次のように記述します。

root(WARN)

ルートロガーのログレベルにINFOを設定し、"CONSOLE"アペンダーと"FILE"アペンダーを割り当てるには次のように記述します。

root(INFO, ["CONSOLE", "FILE"])

"CONSOLE"と"FILE"という名前のアペンダーはすでに定義されているものとします。アペンダーの定義の仕方はすぐ後で説明します。

logger(String name, Level level, List<String> appenderNames = [],
         Boolean additivity = null)

logger()メソッドは引数を四つとります。後ろの二つは任意です。第一引数にはロガーの名前を指定します。第二引数にはロガーのログレベルを指定します。ログレベルにnullを指定すると、直近の祖先ロガーに指定されたログレベルを継承するという意味になります。第三引数はList<String>で任意です。省略した場合は空のリストを指定したものとして扱います。リストにはロガーに割り当てるアペンダーの名前を並べます。第四引数はBooleanでこちらも任意です。additivityフラグとして使われます。省略した場合はnullが指定されたものとして扱います。

たとえば、次のスクリプトはロガー名として"com.foo"、ログレベルとしてINFOを設定します。

logger("com.foo", INFO)

次のスクリプトは、ロガー名として"com.foo"、ログレベルとしてDEBUG、そしてアペンダーに"CONSOLE"を割り当てます。

logger("com.foo", DEBUG, ["CONSOLE"])

次のスクリプトは前のスクリプトとほとんど同じですが、additivityフラグにfalseを設定します。

logger("com.foo", DEBUG, ["CONSOLE"], false)

appender(String name, Class clazz, Closure closure = null)

appender()メソッドでは、第一引数にアペンダーの名前を指定します。第二引数は必須で、インスタンス化するアペンダーのクラスを指定します。第三引数には、そのほかの設定をするクロージャーを指定します。省略した場合はnullになります。

ほとんどのアペンダーは、ちゃんと動作するためにプロパティを設定したりサブコンポーネントを注入しなければなりません。プロパティを設定するには '='演算子(代入)を使用します。サブコンポーネントを注入するには、プロパティ名をメソッド名のように記述して、引数にインスタンス化するクラスを指定します。このコーディング規約は、アペンダーのあらゆるサブコンポーネントについても同じように再帰的に適用されるものです。このアプローチはlogback.groovyの中核を為すもので、覚えなければならない唯一の規約となるでしょう。

例を見てみましょう。次のスクリプトは"FILE"という名前のFileAppenderをインスタンス化して、fileプロパティに"testFile.log"を設定し、appendプロパティにfalseを設定しています。encoderにはPatternLayoutEncoderを注入しています。encoderのpatternプロパティには" - %level %logger - %msg%n"を設定しています。そしてこのアペンダーをルートロガーに割り当てます。

appender("FILE", FileAppender) {
  file = "testFile.log"
  append = true
  encoder(PatternLayoutEncoder) {
    pattern = "%level %logger - %msg%n"
  }
}

root(DEBUG, ["FILE"])

timestamp(String datePattern, long timeReference = -1)

timestamp()メソッドは、datePatternに指定された書式文字列でtimeReferenceに指定されたlong値の時間を書式化した文字列を返します。第一引数のdatePatternに指定する書式文字列は、SimpleDateFormatで定義されている規則に従わなければなりません。第二引数のtimeReferenceが省略された場合-1が指定されたものとして扱います。これは設定ファイルを解析しているときの現在日時を表す値です。状況によりますが、基準時間としてcontext.birthTimeを使うこともあるでしょう。

次の例では、 bySecond変数に"yyyyMMdd'T'HHmmss"という書式で文字列化した現在日時を代入していますそして、"bySecond" 変数をfileプロパティの値として使っています。

def bySecond = timestamp("yyyyMMdd'T'HHmmss")

appender("FILE", FileAppender) {
  file = "log-${bySecond}.txt"
  encoder(PatternLayoutEncoder) {
    pattern = "%logger{35} - %msg%n"
  }
}
root(DEBUG, ["FILE"])

conversionRule(String conversionWord, Class converterClass)

変換指定子を自作しても、logbackに教えてあげなければ利用できません。このlogback.groovyでは、logbackが%sampleという変換指定子に対してMySampleConverterを呼び出すようにしています。

import chapters.layouts.MySampleConverter

conversionRule("sample", MySampleConverter)
appender("STDOUT", ConsoleAppender) {
  encoder(PatternLayoutEncoder) {
    pattern = "%-4relative [%thread] %sample - %msg%n"
  }
}
root(DEBUG, ["STDOUT"])

scan(String scanPeriod = null)

scan()メソッドを使うと、logbackが定期的にlogback.groovyの変更を監視するようになります。logbackは変更を検出するたびにlogback.groovyを再読み込みします。

scan()

デフォルトでは、一分ごとに設定ファイルの変更を監視します。監視周期を指定するには、"scanPeriod" 文字列引数を指定します。"scanPeriod" に指定する文字列には、時間単位としてミリ秒、秒、分または時間を含めることができます。例をみてください。

scan("30 seconds")

時間単位がない場合ミリ秒が指定されたものとして扱いますが、ほとんどの場合これは不適切な単位です。デフォルトの監視周期を変更する場合は、時間単位を指定することを忘れないでください。どのように変更を監視するのかについて詳しくは自動再読み込みのセクションを参照してください。

statusListener(Class listenerClass)

statusListener()メソッドは、指定したリスナークラスをステータスリスナーとして追加します。例を見てください。

import chapters.layouts.MySampleConverter

// statusListener()メソッドの呼び出しはimport文の直後、他の式よりも前に置くことを強く推奨します
statusListener(OnConsoleStatusListener)

ステータスリスナーについては前の章で説明しました。

jmxConfigurator(String name)

JMXConfiguratorのMBeanを登録します。MBean名としてデフォルトのオブジェクト名(ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator)を使うには引数を指定せずに呼び出してください。

jmxConfigurator()

Nameキーに"default"以外の値を指定するには、jmxConfigurator()メソッドの引数として指定するだけです。

jmxConfigurator('MyName')

オブジェクト名全体を指定したい場合は、正確なオブジェクト名文字列を引数に指定してください。

jmxConfigurator('myApp:type=LoggerManager')

このメソッドは、指定された文字列をオブジェクト名として使おうとしてから、それが有効なオブジェクト名ではなかったら、フォールバックとして"Name"キーの値にします。

内部DSL、すべてはGroovyの賜物だ!

logback.groovyは内部DSLです。つまり、内容自体が実行可能なGroovyスクリプトなのです。したがって、logback.groovyの中ではGroovy言語に備わっているクラスimport、GString、変数定義、GString文字列中の${..}記法の評価、if-else文などのあらゆる機能を利用することができるのです。以降の説明ではlogback.groovyにおける典型的なGroovy言語の使用例を紹介します。

変数定義、そしてGString

logback.groovyの中ならどこでも変数を定義することができますし、その変数をGString文字列の中で評価することができます。例を見てください。

// USER_HOME 変数にシステムプロパティの "user.home" の値を代入します
def USER_HOME = System.getProperty("user.home")

appender("FILE", FileAppender) {
  // USER_HOME 変数を使います
  file = "${USER_HOME}/myApp.log"
  encoder(PatternLayoutEncoder) {
    pattern = "%msg%n"
  }
}
root(DEBUG, ["FILE"])

コンソールへの出力

Groovyのprintln()メソッドを使ってコンソールに出力することができます。例を見てください。

def USER_HOME = System.getProperty("user.home");
println "USER_HOME=${USER_HOME}"

appender("FILE", FileAppender) {
  println "Setting [file] property to [${USER_HOME}/myApp.log]"
  file = "${USER_HOME}/myApp.log"  
  encoder(PatternLayoutEncoder) {
    pattern = "%msg%n"
  }
}
root(DEBUG, ["FILE"])

自動的に公開されるフィールド

'hostname' 変数

'hostname' 変数にはスクリプトを実行しているホスト名が設定されています。このドキュメントの著者には正確な説明はできませんが、可視範囲のルールがあるため、'hostname'変数が利用できるのは最上位のスコープだけで、ネストされたスコープからは参照できません。次の例を見ればどういうことかわかるでしょう。

// will print "hostname is x" where x is the current host's name
println "Hostname is ${hostname}"

appender("STDOUT", ConsoleAppender) {
  // will print "hostname is null"
  println "Hostname is ${hostname}" 
}

すべてのスコープでhostname変数を使いたいなら、次のように別の変数に代入して参照しなければなりません。

// define HOSTNAME by assigning it hostname
def HOSTNAME=hostname
// will print "hostname is x" where x is the current host's name
println "Hostname is ${HOSTNAME}"

appender("STDOUT", ConsoleAppender) {
  // will print "hostname is x" where x is the current host's name
  println "Hostname is ${HOSTNAME}" 
}

現在のコンテキストを参照するContextAwareが全ての土台になっている

logback.groovyスクリプトはContextAwareオブジェクト上で実行されます。したがって、context変数からいつでも現在のコンテキストにアクセスすることができます。それに、addInfo()メソッドやaddWarn()メソッド、addError()メソッドで、StatusManagerにステータスメッセージを伝えることができます。

// always a good idea to add an on console status listener
statusListener(OnConsoleStatusListener)

// set the context's name to wombat
context.name = "wombat"
// add a status message regarding context's name
addInfo("Context name has been set to ${context.name}")

def USER_HOME = System.getProperty("user.home");
// add a status message regarding USER_HOME
addInfo("USER_HOME=${USER_HOME}")

appender("FILE", FileAppender) {
  // add a status message regarding the file property
  addInfo("Setting [file] property to [${USER_HOME}/myApp.log]")
  file = "${USER_HOME}/myApp.log"  
  encoder(PatternLayoutEncoder) {
    pattern = "%msg%n"
  }
}
root(DEBUG, ["FILE"])

条件付き設定

Groovyは本格的なプログラミング言語なので、条件分岐を使うと1つのlogback.groovyをdevelopment、testing、productionといったいろいろな環境で使い回すことができます。

次のスクリプトでは、ホスト名が本番環境のホスト名である pixie あるいは orion 以外の場合コンソールアペンダーが有効になるようにしています。ローリングファイルアペンダーの出力ディレクトリがホスト名に依存していることにも気をつけてください。

// always a good idea to add an on console status listener
statusListener(OnConsoleStatusListener)

def appenderList = ["ROLLING"]
def WEBAPP_DIR = "."
def consoleAppender = true;

// does hostname match pixie or orion?
if (hostname =~ /pixie|orion/) {
  WEBAPP_DIR = "/opt/myapp"     
  consoleAppender = false   
} else {
  appenderList.add("CONSOLE")
}

if (consoleAppender) {
  appender("CONSOLE", ConsoleAppender) {
    encoder(PatternLayoutEncoder) {
      pattern = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
    }
  }
}

appender("ROLLING", RollingFileAppender) {
  encoder(PatternLayoutEncoder) {
    Pattern = "%d %level %thread %mdc %logger - %m%n"
  }
  rollingPolicy(TimeBasedRollingPolicy) {
    FileNamePattern = "${WEBAPP_DIR}/log/translator-%d{yyyy-MM}.zip"
  }
}

root(INFO, appenderList)