Semantic LoggerのログをDatadog用にカスタマイズしてみた

こんにちは、Plex Job開発チームの種井です。

この記事は、 PLEX Advent Calendar 2024の9日目の記事です。

DBクライアントツールはDataGripを使っています! よく使う機能はダイアグラム機能です。

背景・課題

過去の記事「Railsアプリケーションのログを構造化し、Datadogで活用するまで」の中で、Datadog Logs機能の仕様に合わせてログをフォーマットする方法として、Remapperを使用する方法を紹介しました。

ただ、前回紹介させていただいたRemapperを使用する方法だと、ログのフォーマットに関しての設定がアプリケーションコード(Formatter)とDatadog上の機能(Remapper)にまたがってしまうことになります。 そのため、Formatterの責務がアプリケーションコードと監視ツールに分散してしまい、二重管理による管理コストが懸念点としてありました。

Semantic Loggerには、ログ構造についてのドキュメントがあります。 当時、Semantic Loggerのログ構造にカスタム属性を含めたい場合はnamed_tagspayloadなどの属性にカスタム属性を含める必要があると考え、Remapperを使用して実装することにしました。 改めて調査したところ、Semantic LoggerのFormatterをカスタマイズする方法でも実現することが可能とわかったため、切り替えることにしました。

今回は、前回記事に続いて、Semantic LoggerのカスタムFormatterを作成し、ログをDatadog Logsのエラートラッキング機能で活用できる形にフォーマットするまでの流れについて書いていきたいと思います。

前提の確認

環境

  • Ruby: 3.3.6
  • Rails: 7.1
  • Semantic Logger
    • semantic_logger: 4.16
    • rails_semantic_logger: 4.17

Semantic Loggerとログのフォーマット

Semantic LoggerではAppenderという、ログを異なる出力先に送信するための機能があります。 Appenderには、ログの出力先やログのフォーマットを指定することができます。

例えば、ログをJSON形式でdevelopment.logに書き出したい場合は以下のようになります。

SemanticLogger.add_appender(file_name: "development.log", formatter: :json)

また、ログのフォーマットを独自のものにカスタマイズしたい場合は以下のように書くことができます。

class MyFormatter < SemanticLogger::Formatters::Color
  # Return the complete log level name in uppercase
  def level
    "#{color}log.level.upcase#{clear}"
  end
end

SemanticLogger.add_appender(file_name: "development.log", formatter: MyFormatter.new)

いずれも公式ドキュメントから引用

SemanticLogger::Formatters モジュールのクラスを継承した独自のFormatterを実装し、add_appender の引数として渡すことで、期待する形にログをフォーマットすることができます。

やったこと

  1. Datadog Logs用のFormatterクラスを作成する
  2. Appenderに作成したFormatterクラスを設定する

1. Datadog Logs用のFormatterクラスを作成する

今回の目的は任意のカスタム属性(エラートラッキング用途)をSemantic Loggerのログ構造に追加することです。 Semantic loggerには、デフォルトで複数のFormatterが用意されていますが、Datadog用に対応するものはありませんでした。 ただ、同じ監視ツールであるNewRelicのFomatterがあったので、実装を見てみることにしました。

module SemanticLogger
  module Formatters
    class NewRelicLogs < Raw
      def call(log, logger)
        ...
        if hash[:exception]
          result.merge!(
            "error.message": hash[:exception][:message],
            "error.class":   hash[:exception][:name],
            "error.stack":   hash[:exception][:stack_trace].join("\n")
          )
        end
        ...
      end
    end
  end
end

https://github.com/reidmorrison/semantic_logger/blob/05a00e186c958ddd474285a37aa7e910aa5e9841/lib/semantic_logger/formatters/new_relic_logs.rb#L79

例外時にカスタム属性であるerror.xxx 属性を定義していることがわかります。

同じようにcallメソッドをオーバーライドし、任意の属性を定義していきます。 今回は、例としてapp/lib/log/formatters/datadog_formatter.rb というファイルを作成し、Datadog Logs用に以下のようなカスタムFormatterを作成しました。

※ エラートラッキング機能で要求されるデータ構造については、前回記事を参照ください

require 'rails_semantic_logger'

class DatadogFormatter < SemanticLogger::Formatters::Raw
  def call(log, logger)
    hash = super(log, logger)

    if log.level == :error
      hash = {
        **hash,
        error: {
          kind: log.name,
          message: log.exception&.message,
          stack: log.backtrace.join('\n')
        }
      }
    end

    hash.to_json
  end
end

2. Appenderに作成したFormatterクラスを設定する

カスタムFormatterができたので、設定ファイル(config/environments/任意の環境.rb)にAppenderとして設定していきます。

...
require_relative '../../app/lib/datadog_formatter'

Rails.application.configure do
  # 作成したDatadogFormatterを引数にして、Appenderを設定する
  config.semantic_logger.add_appender(io: $stdout, formatter: DatadogFormatter.new)
end

上記のような実装を行うことにより、無事にRemapperで再構成していたログを、カスタムFormatterでフォーマットすることができました。

まとめ

今回の取り組みにより、ログのフォーマットをアプリケーションコードに一元化し、設定漏れやTerraformのコードとの二重管理など、運用上コストを幾分か削減することができました。 Semantic Loggerには、標準で多様なAppenderが用意されています。 基本的には用途に合うものが見つかりそうですが、今回のようにないものもあるため、その場合は独自のFormatterを作成する必要があります。 その場合でも、用意されているものを参考にすることで、特につまづくこともなくカスタマイズできました。

おわりに

最後にはなりますが、Plex Jobだけでも今回のような監視や可観測性をはじめコスト最適化などの課題があったり、他事業部のプロダクトもGCPへのインフラ移行を控えていたりします。

このような課題にチャレンジしていただけるSREをはじめ、ソフトウェアエンジニアフロントエンドエンジニアを募集しています。

dev.plex.co.jp

少しでも興味を持っていただけた方は業務委託や副業からでも、ぜひご応募いただけると幸いです。