適切なアプリケーションロギングのための10のヒント

最新のJCPパートナーであるTomasz Nurkiewiczは、適切なアプリケーションロギングの基本原則を説明する多数の投稿を提出しました。 私はそれらを非常に興味深いと思ったので、よりコンパクトな形式でそれらを集約してあなたに提示することにしました。 そう、ここにきれいで、有用な丸太のための彼の提案はある:(ノート: 元の投稿は読みやすさを向上させるために少し編集されています)

1)ジョブに適切なツールを使用する

多くのプログラマは、アプリケーションの動作と現在の活動を記録することがいかに重要であるかを忘れているようです。 誰かが置くとき:

log.info("Happy and carefree logging");

幸いなことに、コードのどこかで、彼はおそらく、メンテナンス、チューニング、および障害識別中のアプリケーションログの重要性を認識していません。 良いログの価値を過小評価することはひどい間違いです。

私の意見では、SLF4Jは、主に優れたパターン置換のサポートのために、利用可能な最高のロギングAPIです:

log.debug("Found {} records matching filter: '{}'", records, filter); 

Log4Jでは、次を使用する必要があります:

log.debug("Found " + records + " records matching filter: '" + filter + "'");

これは、文字列連結の広範な使用のために、より長く読みにくくなるだけでなく、非効率的でもあります。 SLF4Jは素敵な{}置換機能を追加します。 また、文字列の連結は回避され、ロギングステートメントがフィルタリングされた場合はtoString()が呼び出されないため、isDebugEnabled()は不要になりました。 ところで、あなたはフィルタ文字列パラメータの周りの一重引用符に気づいたことがありますか?

SLF4Jは単なるファサードです。 実装として、私はLogbackフレームワークをお勧めします,すでにアドバタイズ,代わりに、十分に確立されたLog4J.それは多くの興味深い機能を持っており、,Log4Jとは対照的に,積極的に開発されています.彼らのモットーを引用すると、

Perf4Jはシステムにあります。log4jとしてcurrentTimeMillis()はシステムにあります。出ろprintln()

私は重い負荷の下である既存のアプリケーションにPerf4Jを追加し、他のいくつかのアプリケーションで動作しているのを見ました。 管理者とビジネスユーザーの両方が、この単純なユーティリティによって生成された素敵なグラフに感銘を受けました。 また、我々はすぐにパフォーマンスの欠陥を発見することができました。 Perf4j自体は、独自の記事に値するが、今のところちょうど彼らの開発者ガイドを確認してください。

さらに、Ceki gülcü(Log4J、SLF4J、Logbackプロジェクトの創設者)は、commons-logging依存関係を取り除くための簡単なアプローチを提案したことに注意してください(彼のコメント

2)忘れないでください、ロギングレベルはあなたのためにあります

ロギングステートメントを作成するたびに、このタイプのイベントに適したロギ どういうわけか、プログラマの90%は、単に同じレベル、通常はINFOまたはDEBUG上のすべてをログ記録する、ロギングレベルに注意を払うことはありません。 どうして? ログフレームワークは、システム上の二つの主な利点があります。出ろ、すなわちカテゴリとレベル。 どちらも、ロギングステートメントを永続的に、または診断時にのみ選択的にフィルタリングできます。 あなたが本当に違いを見ることができない場合は、このテーブルを印刷し、”log”と入力し始めるたびにそれを見てください。”あなたのIDEで:

エラー–ひどく間違った何かが起こった、それはすぐに調査する必要があります。 システムは、このレベルでログに記録された項目を許容できません。 例:NPE、database unavailable、mission criticalユースケースは続行できません。

WARN–プロセスは続行される可能性がありますが、特別な注意が必要です。 実際に私はいつもここに2つのレベルを持っていたいと思っていました:回避策が存在する明白な問題のためのもの(例えば: “現在のデータは利用できません,キャッシュされた値を使用して”)そして第二に(名前を付けます:注意)潜在的な問題や提案のために. 例:”開発モードで実行されているアプリケーション”または”管理コンソールがパスワードで保護されていません”。 アプリケーションは警告メッセージを許容できますが、常に正当化して検査する必要があります。

情報–重要な業務プロセスが終了しました。 理想的な世界では、管理者または上級ユーザーは、情報メッセージを理解し、アプリケーションが何をしているかをすばやく見つけることができるはずです。 たとえば、アプリケーションがすべての航空券の予約に関するものである場合、”booking ticket from to”という情報文は、各チケットごとに一つだけでなければなりません。 情報メッセージのその他の定義:アプリケーションの状態を大幅に変更する各アクション(データベースの更新、外部システム要求)。

デバッグ–開発者のもの。 どのような情報が記録されるに値するかについては、後で説明します。

TRACE–非常に詳細な情報で、開発のみを目的としています。 実稼働環境への展開後、トレースメッセージは短時間保持されますが、これらのログステートメントは一時的なものとして処理され、最終的にはオフにな DEBUGとTRACEの区別は最も難しいですが、機能の開発とテストの後にloggingステートメントを入れて削除すると、おそらくTRACEレベルになるはずです。

上記のリストは単なる提案です、あなたは従うべき命令の独自のセットを作成することができますが、いくつかを持っていることが重要です。 私の経験では、常にすべてが(少なくともアプリケーションコードから)フィルタリングせずに記録されますが、ログをすばやくフィルタリングし、適切な

最後に言及する価値があるのは、悪名高いis*Enabled()条件です。 いくつかは、すべてのロギング文の前にそれを置く:

if(log.isDebugEnabled()) log.debug("Place for your commercial");

個人的に、私はこのイディオムが避けるべきであるちょうど混乱であることを見つけます。 パフォーマンスの向上(特に前に説明したSLF4Jパターン置換を使用する場合)は無関係であり、時期尚早の最適化のようなにおいがします。 また、重複を見つけることができますか? 明示的な条件を持つことが正当化される非常にまれなケースがあります–ロギングメッセージの構築が高価であることを証明できる場合。 他の状況では、ロギングの仕事をして、logging frameworkにその仕事(フィルタリング)をさせてください。

3)何をログに記録しているか知っていますか?

ロギングステートメントを発行するたびに、少し時間をかけて、ログファイルに正確に何が着陸するかを見てください。 その後、あなたのログを読んで、不正な文章を見つけます。 まず第一に、このようなNPEsを避けてください:

log.debug("Processing request with id: {}", request.getId());

ここでrequestがnullではないことを絶対に確信していますか?

もう一つの落とし穴はロギングコレクションです。 Hibernateを使用してデータベースからドメインオブジェクトのコレクションを取得し、ここのように不注意にログに記録した場合:

log.debug("Returning users: {}", users);

SLF4Jは、文が実際に印刷されている場合にのみtoString()を呼び出します。 しかし、それが…メモリ不足エラー、N+1選択問題、スレッド飢餓(ロギングは同期的です!)、遅延初期化例外、ログストレージが完全に満たされた–これらのそれぞれが発生する可能性があります。

たとえば、ドメインオブジェクトのidのみ(またはコレクションのサイズのみ)をログに記録する方がはるかに良い考えです。 しかし、getId()メソッドを持つオブジェクトのコレクションを持つときにidのコレクションを作成することは、Javaでは信じられないほど困難で面倒で Groovyには優れたスプレッド演算子(users*.id)があり、JavaではCommons Beanutilsライブラリを使用してエミュレートできます:

log.debug("Returning user ids: {}", collect(users, "id"));

ここで、collect()メソッドは次のように実装できます:

public static Collection collect(Collection collection, String propertyName) { return CollectionUtils.collect(collection, new BeanToPropertyValueTransformer(propertyName));}

最後に言及するのは、toString()不適切な実装または使用法です。 まず、ロギングステートメントのどこかに表示される各クラスに対して、Tostring()を作成します。ToStringBuilderを使用することをお勧めします(ただし、反射的な対応物ではありません)。 第二に、配列と非典型的なコレクションに注意してください。 配列といくつかの奇妙なコレクションには、各項目のtoString()を呼び出すtostring()が実装されていない可能性があります。 配列#deepToString JDKユーティリティメソッドを使用します。 また、ログを頻繁に読んで、誤ってフォーマットされたメッセージを見つけます。

4)副作用を避ける

ロギングステートメントは、アプリケーションの動作に影響を与えないか、またはほとんど影響しません。 最近、私の友人が、特定の環境で実行されている場合にのみHibernatesのL A Zyinitializationexceptionをスローしたシステムの例を挙げました。 コンテキストから推測したように、いくつかのロギングステートメントにより、sessionがアタッチされたときに遅延初期化されたコレク この環境では、ログレベルが増加し、コレクションは初期化されなくなりました。 この文脈を知らずにバグを見つけるのにどれくらいの時間がかかると思いますか?

もう一つの副作用は、アプリケーションの速度を低下させることです。 簡単な答え:ログが多すぎる場合、またはtoString()や文字列連結を不適切に使用すると、ログにはパフォーマンスの副作用があります。 どのくらいの大きさですか? まあ、私は過度のロギングによって引き起こされるスレッドの飢餓のためにサーバーが15分ごとに再起動するのを見てきました。 今、これは副作用です! 私の経験から、MiBの数百は、おそらくあなたが時間あたりのディスクにログオンすることができますどのくらいの上限です。

もちろん、ロギングステートメント自体が失敗し、例外によりビジネスプロセスが終了する場合、これも大きな副作用です。 私はこれを避けるためにそのような構造を見ました:

try { log.trace("Id=" + request.getUser().getId() + " accesses " + manager.getPage().getUrl().toString())} catch(NullPointerException e) {}

これは実際のコードですが、世界を少し良くして、それをしないでください。

5)簡潔で説明的である

各ロギングステートメントには、データと説明の両方を含める必要があります。 次の例を考えてみましょう:

log.debug("Message processed");log.debug(message.getJMSMessageID());log.debug("Message with id '{}' processed", message.getJMSMessageID());

不明なアプリケーションで障害を診断しているときにどのログを表示しますか? 私を信じて、上記のすべての例はほぼ同じように一般的です。 もう一つの反パターン:

if(message instanceof TextMessage) //...else log.warn("Unknown message type");

実際のメッセージタイプ、メッセージidなどを含めるのはとても難しかったですか? 警告文字列で? 私は何かが間違っていた知っているが、何? 文脈は何でしたか?

第三の反パターンは”マジックログ”です。 実際の例:チームのほとんどのプログラマは、3つのアンパサンドの後に感嘆符、ハッシュ、擬似ランダムな英数字文字列logが続くことは、”XYZ idを受信したメッ 誰もログを変更する気にしない、単に誰かがキーボードを打つと、いくつかのユニークなを選んだ”&&&!#”文字列、それは簡単に自分で見つけることができるように。

結果として、ログファイル全体はランダムな一連の文字のように見えます。 誰かがそのファイルを有効なPerlプログラムと考えるかもしれません。 代わりに、ログファイルは読みやすく、きれいで説明的でなければなりません。 マジックナンバー、ログ値、数字、idを使用せず、それらのコンテキストを含めないでください。 処理中のデータを表示し、その意味を表示します。 プログラムが実際に何をしているかを示します。 良いログは、アプリケーションコード自体の優れた文書として役立ちます。

パスワードや個人情報をログに記録しないことを言及しましたか? やめて!

6)パターンを調整する

ロギングパターンは素晴らしいツールで、作成するすべてのロギング文に意味のあるコンテキストを透過的に追加します。 しかし、どの情報をパターンに含めるかを非常に慎重に検討する必要があります。 たとえば、ログファイル名に日付が既に含まれているため、ログが1時間ごとにロールするときのログ日付は無意味です。 逆に、スレッド名をログに記録しないと、二つのスレッドが同時に動作するときにログを使用してプロセスを追跡することはできません。 これは、シングルスレッドのアプリケーションでは問題ないかもしれません-今日ではほとんど死んでいます。

私の経験から、理想的なログパターンには、現在の時刻(日付、ミリ秒の精度なし)、ログレベル、スレッドの名前、単純なロガー名(完全修飾されていない)、メッ Logbackでは次のようなものです:

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} %-5level %m%n</pattern> </encoder></appender>

ファイル名、クラス名、行番号を含めるべきではありませんが、非常に魅力的です。 私はコードから発行された空のログ文を見てきました:

log.info("");

プログラマは、行番号がロギングパターンの一部であると想定し、”ファイルの67行目(authenticate()メソッド)に空のロギングメッセージが表示された場合、ユーザーが認証され また、クラス名、メソッド名、行番号のロギングは、パフォーマンスに重大な影響を与えます。

ロギングフレームワークのやや高度な機能は、マップされた診断コンテキストの概念です。 MDCは、単にスレッドローカルベースで管理されるマップです。 このマップには任意のキーと値のペアを入れることができ、それ以来、このスレッドから発行されたすべてのロギングステートメントは、この値をパター

7)メソッドの引数と戻り値のログ

開発中にバグが見つかった場合は、通常、潜在的な原因を追跡しようとするデバッガを実行します。 しばらくの間、デバッガを使用できないと想像してください。 たとえば、バグは数日前に顧客環境に現れ、あなたが持っているすべてがログであるためです。 あなたはそれらの中に何かを見つけることができますか?

各メソッドの入力と出力(引数と戻り値)をログに記録するという単純なルールに従うと、デバッガはもう必要ありません。 もちろん、あなたは合理的でなければなりませんが、外部システム(データベースを含む)、ブロック、待機などにアクセスするすべての方法です。 考慮されるべきである。 このパターンに従うだけです:

public String printDocument(Document doc, Mode mode) { log.debug("Entering printDocument(doc={}, mode={})", doc, mode); String id = //Lengthy printing operation log.debug("Leaving printDocument(): {}", id); return id;}

メソッド呼び出しの開始と終了の両方をログに記録しているため、非効率なコードを手動で検出したり、デッドロックや飢餓の原因を検出したりするこ あなたのメソッドに意味のある名前がある場合は、ログを読むのが楽しいでしょう。 また、何が間違っていたのかを分析することは、各ステップで何が処理されたのかを正確に知っているため、はるかに簡単です。 単純なAOPアスペクトを使用して、コード内のさまざまなメソッドをログに記録することもできます。 これにより、コードの重複が軽減されますが、膨大な量の巨大なログにつながる可能性があるため、注意してください。

デバッグレベルまたはトレースレベルは、これらのタイプのログに最適であると考える必要があります。 また、いくつかのメソッドが頻繁に呼び出され、ロギングがパフォーマンスに害を及ぼす可能性がある場合は、そのクラスのログレベルを下げるか、ログを完全に削除します(メソッド呼び出し全体のために1つだけ残しておきますか?)しかし、ログ文が少なすぎるよりも多すぎる方が常に良いです。 単体テストと同じ点でロギングステートメントを扱う–コードは、単体テストと同じようにロギングルーチンでカバーする必要があります。 システムのどの部分もログなしでとどまるべきではありません。 アプリケーションが正常に動作しているのか、永遠にハングしているのかを判断する唯一の方法は、ログのローリングを観察することです。

8)外部システムに気をつけろ

これは前のヒントの特別なケースです:外部システムと通信する場合は、アプリケーションから出てくるすべてのデータをログに記録することを検討してください。 ピリオド。 統合は大変な仕事であり、二つのアプリケーション(二つの異なるベンダー、環境、技術スタック、チームと考える)間の問題を診断することは特に困難です。 たとえば、最近、Apache CXF webサービスのSOAPおよびHTTPヘッダーを含む完全なメッセージの内容をログに記録することは、統合およびシステムテスト中に非常に有

これは大きなオーバーヘッドであり、パフォーマンスに問題がある場合は、いつでもログを無効にすることができます。 しかし、誰も修正できない、高速ではあるが壊れたアプリケーションを持つことのポイントは何ですか? 外部システムと統合するときは特に注意し、そのコストを支払う準備をしてください。 運が良ければ、すべての統合がESBによって処理される場合、バスはおそらくすべての着信要求と応答をログに記録するのに最適な場所です。 例えば、Mulesのlog-componentを参照してください。

外部システムと交換される情報の量がすべてをログに記録することを容認できないことがあります。 一方、テスト中や本番環境での短期間(たとえば、何か問題が発生している場合)は、すべてをログに保存し、パフォーマンスコストを支払う準備ができてい これは、ログレベルを慎重に使用することで実現できます。 ただ、次のイディオムを見てみましょう:

Collection<Integer> requestIds = //...if(log.isDebugEnabled()) log.debug("Processing ids: {}", requestIds);else log.info("Processing ids size: {}", requestIds.size());

この特定のロガーがデバッグメッセージをログに記録するように設定されている場合、requestIdsコレクションの内容全体を出力します。 しかし、INFOメッセージを印刷するように設定されている場合は、コレクションのサイズのみが出力されます。 なぜ私がisInfoEnabled()条件を忘れてしまったのか疑問に思っている場合は、ヒント#2に戻ります。 言及する価値のあることの1つは、この場合、requestIdsコレクションをnullにすべきではないということです。 デバッグが有効になっている場合はnullとして正しくログに記録されますが、loggerがINFOに設定されている場合はbig fat NullPointerExceptionがスローされます。 ヒント#4の副作用についての私の教訓を覚えていますか?

9)例外を正しくログに記録する

まず、例外のログを避けるため、フレームワークやコンテナ(それが何であれ)があなたのためにそれをやらせてくださ 一つ、ekhem、このルールの例外があります: いくつかのリモートサービス(RMI、EJBリモートセッションbeanなど)から例外をスローする場合例外をシリアル化することができる場合は、それらのすべてがクライアントで利用可能であることを確認してください(APIの一部です)。 それ以外の場合、クライアントは”true”エラーの代わりにNoClassDefFoundError:SomeFancyExceptionを受け取ります。

ロギング例外は、ロギングの最も重要な役割の一つですが、多くのプログラマはロギングを例外を処理する方法として扱う傾向があります。 デフォルト値(通常はnull、0、または空の文字列)を返し、何も起こらなかったふりをすることがあります。 それ以外の場合は、最初に例外をログに記録し、それをラップしてスローします:

log.error("IO exception", e);throw new MyCustomException(e);

何かが最終的にMyCustomExceptionをキャッチし、その原因を記録するため、この構造はほとんど常に同じスタックトレースを二回出力します。 ログ、またはラップしてスローバック(これが望ましい)、決して両方、そうでなければあなたのログは混乱します。

しかし、本当に例外をログに記録したいのですか? 何らかの理由で(私たちはApiとドキュメントを読んでいないので?)、私が見るロギング文の約半分が間違っています。 クイッククイズ、次のログステートメントのどれがNPEを適切にログに記録しますか?

try { Integer x = null; ++x;} catch (Exception e) { log.error(e); //A log.error(e, e); //B log.error("" + e); //C log.error(e.toString()); //D log.error(e.getMessage()); //E log.error(null, e); //F log.error("", e); //G log.error("{}", e); //H log.error("{}", e.getMessage()); //I log.error("Error reading configuration file: " + e); //J log.error("Error reading configuration file: " + e.getMessage()); //K log.error("Error reading configuration file", e); //L}

驚くべきことに、Gと好ましくはLだけが正しい! AとBはSLF4Jでコンパイルされず、他の人はスタックトレースを破棄したり、不適切なメッセージを印刷したりします。 たとえば、NPEは通常、例外メッセージを提供せず、スタックトレースも印刷されないため、Eは何も印刷しません。 覚えておいて、最初の引数は常にテキストメッセージであり、問題の性質について何かを書いてください。 例外メッセージは、logステートメントの後、スタックトレースの前に自動的に出力されるため、含めないでください。 しかし、そうするためには、例外自体を2番目の引数として渡す必要があります。

10)ログ読みやすく、解析しやすい

アプリケーションログに特に関心のある受信者のグループは、人間(同意しないかもしれませんが、プログラマもこのグループに属しています)とコンピュータ(通常はシステム管理者によって書かれたシェルスクリプト)の二つのグループがあります。 ログは、これらのグループの両方に適している必要があります。 あなたのアプリケーションログを後ろから見ている人が見ている場合(出典Wikipedia):

その後、あなたはおそらく私のヒントに従っていません。 ログは、コードと同じように読みやすく、理解しやすいものでなければなりません。

一方、アプリケーションが1時間に半分GBのログを生成する場合、誰もグラフィカルテキストエディターがそれらを完全に読み取ることはできません。 これは、古い学校のgrep、sed、awkが便利な場所です。 可能であれば、人間とコンピュータの両方が理解できるような方法でログメッセージを書くようにしてください。 数値の書式設定を避けたり、正規表現で簡単に認識できるパターンを使用したりするなどします。 それが不可能な場合は、2つの形式でデータを印刷します:

log.debug("Request TTL set to: {} ({})", new Date(ttl), ttl);// Request TTL set to: Wed Apr 28 20:14:12 CEST 2010 (1272478452437)final String duration = DurationFormatUtils.formatDurationWords(durationMillis, true, true);log.info("Importing took: {}ms ({})", durationMillis, duration);//Importing took: 123456789ms (1 day 10 hours 17 minutes 36 seconds)

コンピュータは”ms after1970epoch”の時間形式を理解しますが、人々は”1日10時間17分36秒”のテキストを見て喜んでいます。 ところでDurationFormatUtils、素敵なツールを見てみましょう。

それはすべての人、私たちのJCPパートナー、Tomasz Nurkiewiczからの”ロギングのヒントの祭典”です。 共有することを忘れないでください!

関連記事 :
  • ロギングアンチパターン
  • すべてのプログラマが知っておくべきこと
  • ソフトウェア設計の法則
  • Javaのベストプラクティス–Vector vs ArrayList vs HashSet
  • Javaのベス6030>
  • javaのベストプラクティス–文字列のパフォーマンスと正確な文字列の一致
関連するスニペット :
  • ロガーハンドラのカスタムフォーマッタを作成
  • ログメソッドコール
  • ロガーハンドラのフィルタを設定
  • ロガーハンドラのフォーマッタを設定
  • ロガーハンドラのフォーマッタを設定
  • ログアスペクトの例
  • Log4J Mavenの例
  • Hibernateロギング設定–slf4j+log4jとログバック