ログを Datadog に送信するには、ファイルにログを記録し、そのファイルを Datadog Agent で tail します。

一般的な Java ログのスタックトレースは複数の行に分割されているため、元のログイベントに関連付けることが困難です。たとえば、以下のとおりです。

//4 events generated when only one is expected!
Exception in thread "main" java.lang.NullPointerException
        at com.example.myproject.Book.getTitle(Book.java:16)
        at com.example.myproject.Author.getBookTitles(Author.java:25)
        at com.example.myproject.Bootstrap.main(Bootstrap.java:14)

この問題を解決するには、ログを JSON 形式で生成するようにログライブラリを構成します。JSON 形式でログを記録することで、次のことが可能になります。

  • スタックトレースがログイベントに適切にラップされることを確実にします。
  • すべてのログイベント属性 (重大度、ロガー名、スレッド名など) が適切に抽出されることを確実にします。
  • マップされた診断コンテキスト (MDC) 属性にアクセスできます。この属性は、任意のログイベントにアタッチできます。
  • カスタムパースルールが不要になります。

次の手順は、Log4j、Log4j 2、および Logback ログライブラリのセットアップ例を示しています。

ロガーの構成

JSON 形式

Log4j の場合、SLF4J モジュール log4j-over-slf4j を Logback と組み合わせて使用して JSON 形式でログします。log4j-over-slf4j は、アプリケーションの Log4j を完全に置き換えるため、コードを変更する必要はありません。

  1. pom.xml ファイルで、log4j.jar 依存関係を log4j-over-slf4j.jar 依存関係に置き換え、Logback 依存関係を追加します。例:たとえば、以下のとおりです。

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>log4j-over-slf4j</artifactId>
      <version>1.7.32</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.9</version>
    </dependency>
    <dependency>
      <groupId>net.logstash.logback</groupId>
      <artifactId>logstash-logback-encoder</artifactId>
      <version>6.6</version>
    </dependency>
    
  2. logback.xml で JSON レイアウトを使用してアペンダーを構成します。ファイルとコンソール向けのサンプル構成は下記を参照してください。

    ファイル:

    <configuration>
      <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>logs/app.log</file>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder" />
      </appender>
    
      <root level="INFO">
        <appender-ref ref="FILE"/>
      </root>
    </configuration>
    

    For console:

    <configuration>
      <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
          <encoder class="ch.qos.logback.classic.encoder.JsonEncoder"/>
      </appender>
    
      <root>
        <level value="DEBUG"/>
          <appender-ref ref="CONSOLE"/>
        </root>
    </configuration>
    

Log4j 2 には JSON レイアウトが含まれています。

  1. log4j2.xml で JSON レイアウトを使用してアペンダーを構成します。ファイルアペンダーとコンソールアペンダー向けのサンプル構成は下記を参照してください。Log4j プラグインの包括的な説明については、Log4j プラグインリファレンスを参照してください。

log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
  <Configuration>
    <Appenders>
      <File name="FILE" fileName="logs/app.log" >
        <JsonTemplateLayout eventTemplateUri="classpath:MyLayout.json"/>
      </File>
    </Appenders>
    <Loggers>
      <Root level="INFO">
        <AppenderRef ref="FILE"/>
      </Root>
    </Loggers>
  </Configuration>

log4j2.xml

  <?xml version="1.0" encoding="UTF-8"?>
  <Configuration>
    <Appenders>
      <Console name="console" target="SYSTEM_OUT">
        <JsonTemplateLayout eventTemplateUri="classpath:MyLayout.json"/>
      </Console>
    </Appenders>
    <Loggers>
      <Root level="INFO">
        <AppenderRef ref="console"/>
      </Root>
    </Loggers>
  </Configuration>
  1. Java プロジェクトの src/main/resources ディレクトリに JSON レイアウトテンプレートファイル (MyLayout.json など) を追加します。たとえば、以下のとおりです。

    {
       "timestamp":{
          "$resolver":"timestamp",
          "pattern":{
             "format":"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
             "timeZone":"UTC"
          }
       },
       "status":{
          "$resolver":"level",
          "field":"name"
       },
       "thread_name":{
          "$resolver":"thread",
          "field":"name"
       },
       "logger_name":{
          "$resolver":"logger",
          "field":"name"
       },
       "message":{
          "$resolver":"message",
          "stringified":true
       },
       "exception_class":{
          "$resolver":"exception",
          "field":"className"
       },
       "exception_message":{
          "$resolver":"exception",
          "field":"message"
       },
       "stack_trace":{
          "$resolver":"exception",
          "field":"stackTrace",
          "stackTrace":{
             "stringified":true
          }
       },
       "host":"${hostName}",
       "service":"${env:DD_SERVICE}",
       "version":"${env:DD_VERSION}",
       "dd.trace_id":{
          "$resolver":"mdc",
          "key":"dd.trace_id"
       },
       "dd.span_id":{
          "$resolver":"mdc",
          "key":"dd.span_id"
       }
    }
    
  2. pom.xml に JSON レイアウトの依存関係を追加します。たとえば、以下のとおりです。

    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.17.1</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.13.0</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.13.0</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>2.13.0</version>
    </dependency>
    

Logback の JSON 形式のログには、logstash-logback-encoder を使用します。

  1. logback.xml で JSON レイアウトを使用してファイルアペンダーを構成します。たとえば、以下のとおりです。

    <configuration>
      <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>logs/app.log</file>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder" />
      </appender>
    
      <root level="INFO">
        <appender-ref ref="FILE"/>
      </root>
    </configuration>
    
  2. pom.xml ファイルに Logstash エンコーダの依存関係を追加します。たとえば、以下のとおりです。

    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.9</version>
    </dependency>
    <dependency>
      <groupId>net.logstash.logback</groupId>
      <artifactId>logstash-logback-encoder</artifactId>
      <version>6.6</version>
    </dependency>
    

Tinylog 公式ドキュメントに基づいて、JSON ライターの構成を作成します。

tinylog.properties ファイルでは次の形式を使用します。

writer                     = json
writer.file                = log.json
writer.format              = LDJSON
writer.level               = info
writer.field.level         = level
writer.field.source        = {class}.{method}()
writer.field.message       = {message}
writer.field.dd.trace_id   = {context: dd.trace_id}
writer.field.dd.span_id    = {context: dd.span_id}
writer.field.dd.service    = {context: dd.service}
writer.field.dd.version    = {context: dd.version}
writer.field.dd.env        = {context: dd.env}

未加工の形式

log4j.xml でファイルアペンダーを構成します。たとえば、以下のとおりです。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration>

  <appender name="FILE" class="org.apache.log4j.FileAppender">
    <param name="File" value="logs/app.log"/>
    <param name="Append" value="true"/>

    <layout class="org.apache.log4j.PatternLayout">
      <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} %-5p %C:%L - %X{dd.trace_id} %X{dd.span_id} - %m%n"/>
    </layout>
  </appender>

  <root>
    <priority value="INFO"/>
    <appender-ref ref="FILE"/>
  </root>

</log4j:configuration>

log4j2.xml でファイルアペンダーを構成します。たとえば、以下のとおりです。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
  <Appenders>
    <File name="FILE" fileName="logs/app.log">
      <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %C:%L - %X{dd.trace_id} %X{dd.span_id} - %m%n"/>
    </File>
  </Appenders>

  <Loggers>
    <Root level="INFO">
      <AppenderRef ref="FILE"/>
    </Root>
  </Loggers>
</Configuration>

logback.xml でファイルアペンダーを構成します。たとえば、以下のとおりです。

<configuration>
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>${dd.test.logfile}</file>
    <append>false</append>
    <immediateFlush>true</immediateFlush>

    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5p %C:%L - %X{dd.trace_id} %X{dd.span_id} - %m%n</pattern>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="FILE"/>
  </root>
</configuration>

公式 Tinylog ドキュメントに基づき、ファイルへ出力するためのライター構成を作成してください。

tinylog.properties ファイルでは次の形式を使用します。

writer          = file
writer.level    = debug
writer.format   = {level} - {message} - "dd.trace_id":{context: dd.trace_id} - "dd.span_id":{context: dd.span_id}
writer.file     = log.txt

ログへのトレース ID の挿入

このアプリケーションで APM が有効になっている場合は、トレース ID インジェクションを有効にすることで、ログとトレースを相互に関連付けることができます。Java ログとトレースの接続を参照してください。

ログとトレースを_相関させていない_場合は、上記の構成例に含まれているログパターンから MDC プレースホルダー (%X{dd.trace_id} %X{dd.span_id}) を削除します。

たとえば、Log4j 2 を使用していて、ログとトレースを相関させていない場合は、例のログレイアウトテンプレート MyLayout.json から次のブロックを削除します。

"dd.trace_id":{
   "$resolver":"mdc",
   "key":"dd.trace_id"
},
"dd.span_id":{
   "$resolver":"mdc",
   "key":"dd.span_id"
}

Datadog Agent を構成する

ログ収集が有効になったら、ログファイルを追跡して Datadog に送信するカスタムログ収集を設定します。

  1. java.d/ フォルダーを conf.d/Agent 構成ディレクトリに作成します。

  2. java.d/ に以下の内容で conf.yaml ファイルを作成します。

    #Log section
    logs:
    
      - type: file
        path: "<path_to_your_java_log>.log"
        service: <service_name>
        source: java
        sourcecategory: sourcecode
        # For multiline logs, if they start by the date with the format yyyy-mm-dd uncomment the following processing rule
        #log_processing_rules:
        #  - type: multi_line
        #    name: new_log_start_with_date
        #    pattern: \d{4}\-(0?[1-9]|1[012])\-(0?[1-9]|[12][0-9]|3[01])
    
  3. Agent を再起動します。

  4. Agent の status サブコマンドを実行し、Checks セクションで java を探し、ログが Datadog に正常に送信されることを確認します。

ログが JSON 形式の場合、Datadog は自動的にログメッセージをパースし、ログ属性を抽出します。ログエクスプローラーを使用して、ログを表示し、トラブルシューティングを行うことができます。

ログを Agent に直接ストリーミングする

アクセスできない、またはファイルにログを記録できないマシンでアプリケーションが実行されている例外的なケースでは、ログを Datadog または Datadog Agent に直接ストリーミングすることができます。アプリケーションが接続の問題を処理する必要があるため、これは推奨される設定ではありません。

ログを Datadog に直接ストリーミングするには

  1. Logback ログライブラリをコードに追加するか、現在のロガーを Logback にブリッジします。
  2. Logback を構成して Datadog にログを送信します。

Java ロギングライブラリから Logback へのブリッジ

まだ Logback を使用していない場合、ほとんどの一般的なログライブラリは Logback にブリッジすることができます。

SLF4J モジュール log4j-over-slf4j を Logback と共に使用して、ログを別のサーバーに送信します。log4j-over-slf4j は、アプリケーションの Log4j を完全に置き換えるため、コードを変更する必要はありません。

  1. pom.xml ファイルで、log4j.jar 依存関係を log4j-over-slf4j.jar 依存関係に置き換え、Logback 依存関係を追加します。例:たとえば、以下のとおりです。
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>log4j-over-slf4j</artifactId>
      <version>1.7.32</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.9</version>
    </dependency>
    <dependency>
      <groupId>net.logstash.logback</groupId>
      <artifactId>logstash-logback-encoder</artifactId>
      <version>6.6</version>
    </dependency>
    
  2. Logback を構成します。

注: この変更の結果、Log4j コンフィギュレーションファイルは使用されなくなります。Log4j トランスレーターを使用して log4j.properties ファイルを logback.xml に移行してください。

Log4j 2 では、リモートホストへのログ記録が可能ですが、ログの前に API キーを付ける機能はありません。このため、SLF4J モジュール log4j-over-slf4j と Logback を使用してください。log4j-to-slf4j.jar は、アプリケーションの Log4j 2 を完全に置き換えるため、コードを変更する必要はありません。これを使用するには、次を行います。

  1. pom.xml ファイルで、log4j.jar 依存関係を log4j-over-slf4j.jar 依存関係に置き換え、Logback 依存関係を追加します。例:たとえば、以下のとおりです。
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-to-slf4j</artifactId>
        <version>2.17.1</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.9</version>
    </dependency>
    <dependency>
        <groupId>net.logstash.logback</groupId>
        <artifactId>logstash-logback-encoder</artifactId>
        <version>6.6</version>
    </dependency>
    
  2. Logback を構成します。

注:

Logback を構成する

Datadog では、ログを TCP 経由で Datadog インテークに直接送信することはサポートされていません。代わりに、ローカルの Datadog Agent に対する Logback を構成し、Agent から自動エンリッチメントによってログを HTTPS 経由で Datadog に転送します。

  1. ローカルの Datadog Agent をインストールします (v6 以上/v7 以上)。

  2. datadog.yaml でログ収集を有効にし、Agent がログを HTTPS 経由で転送することを確認します (Agent v6.19 以上/v7.19 以上では HTTPS 転送がデフォルトです)。

    logs_enabled: true
    logs_config:
      # HTTPS is the default. Keep or set this to force HTTPS forwarding.
      force_use_http: true
      # (Optional) auto-detect multi-line patterns
      auto_multi_line_detection: true
    
  3. Agent でのログ収集を有効にします。

    # /etc/datadog-agent/conf.d/logback.d/conf.yaml
    logs:
      - type: tcp
        port: 10518           # Port the Agent will listen on
        service: my-java-app  # Your service name (unified service tagging)
        source: java          # Or a more specific source, e.g., "logback"
    
  4. 変更を適用するために Agent を再起動します。

  5. Agent にログを送信するように Logback を構成します。logback.xmllogstash-logback-encoder TCP アペンダーを使用して、ログを Agent に転送します。

    <configuration>
      <appender name="DD_TCP_JSON" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <destination>localhost:10518</destination>
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
          <providers>
            <timestamp/>
            <pattern>
              <pattern>
                {
                  "message": "%message",
                  "level": "%level",
                  "logger": "%logger",
                  "service": "${DD_SERVICE:-my-java-app}",
                  "env": "${DD_ENV:-prod}",
                  "version": "${DD_VERSION:-1.0.0}",
                  "dd.trace_id": "%X{dd.trace_id}",
                  "dd.span_id": "%X{dd.span_id}"
                }
              </pattern>
            </pattern>
            <arguments/>
            <stackTrace/>
          </providers>
        </encoder>
      </appender>
    </configuration>
    

    次に、それをルートロガーで参照します。

    <root level="INFO">
      <appender-ref ref="DD_TCP_JSON"/>
    </root>
    
  6. ログ転送を確認します。datadog-agent status を実行して TCP リスナーを確認し、サービスのタグが付いたエントリが[ログエクスプローラー]に表示されることを確認します。

補足説明

ログイベントをコンテキスト属性で補完することができます。

キー値パーサーの使用

キー値パーサーは、ログイベント内で認識された <KEY>=<VALUE> パターンを抽出します。

Java のログイベントを補完するには、コードでメッセージを書き直し、<KEY>=<VALUE> のシーケンスを挿入します。

たとえば、次のメッセージがあるとします。

logger.info("Emitted 1001 messages during the last 93 seconds for customer scope prod30");

これを次のように変更します。

logger.info("Emitted quantity=1001 messages during the last durationInMs=93180 ms for customer scope=prod30");

キー値パーサーを有効にすると、各ペアが JSON から抽出されます。

{
  "message": "Emitted quantity=1001 messages during the last durationInMs=93180 ms for customer scope=prod30",
  "scope": "prod30",
  "durationInMs": 93180,
  "quantity": 1001
}

これで、scope をフィールド、durationInMsquantity をログメジャーとして利用できます。

MDC

ログを補完するもう 1 つの方法として、Java の マップされた診断コンテキスト (MDC) の利用があります。

SLF4J を使用する場合は、次の Java コードを使用してください。

...
MDC.put("scope", "prod30");
logger.info("Emitted 1001 messages during the last 93 seconds");
...

この JSON を生成するには

{
  "message": "Emitted 1001 messages during the last 93 seconds",
  "scope": "prod30"
}

: MDC は文字列タイプのみを許可するため、数値メトリクスには使用しないでください。

参考資料