kakudooo docs

Rails.loggerのjson formatterを自作してみる | その④

タグを構造化ログに追加する

RailsのActiveSupport::TaggedLoggingモジュールを参考にして、以下のようなタグを構造化ログに設定できるようにする。

{
  ...
  "tags": [
    "0123456789"
  ]
}

大まかな処理の流れとしては、以下

  1. ThreadやFiberを使用してローカル変数を定義する
  2. 1.をストレージとして、タグを記録する
  3. Formatterで保存されているタグを含めてログを出力する

実装

以前のpartで作成したconfig/structured_logger.rbをタグ付けに対応させてみる。 ActiveSupport::TaggedLoggingからタグを追加する最低限の機能だけを雑に実装してみると以下のようになる。

※ 本記事のタグ管理は検証用の簡易実装であり、タグのpopやブロック定義の対応など本番運用向けの設計は考慮していない。

class StructuredLogger < ::Logger
  include ActiveSupport::LoggerSilence

  class TagStack
    attr_reader :tags

    def initialize
      @tags = []
    end

    def format_message(message)
      if @tags.empty?
        message
      else
        {
          **message,
          tags: @tags
        }
      end
    end

    def push_tags(tags)
      tags.flatten!
      tags.reject!(&:blank?)
      @tags.concat(tags)
      tags
    end
  end

  class JsonFormatter < ::Logger::Formatter
    def call(severity, timestamp, progname, msg)
      base = {
        severity:  severity.to_s,
        timestamp: timestamp.utc.iso8601(6),
        progname:  progname
      }

      # TODO: データ型によって、紐づけ方を変える必要がある
      payload =
        case msg
        when Hash      then tag_stack.format_message(msg) # MEMO: 一旦のところHashの場合のみタグに対応。ログのフォーマット時に合わせて保持しているタグを`tags`属性に設定する
        when Exception then { exception: { error_class: msg.class.name, message: msg.message, backtrace: msg.backtrace } }
        else               { message: msg.to_s }
        end

      (base.merge(payload)).to_json << "\n"
    end

    # Thread or Fiber(デフォルトはThread)を使用して、タグをスレッド内で保持する
    def tag_stack
      # We use our object ID here to avoid conflicting with other instances
      @thread_key ||= "structured_tagged_logging_tags:#{object_id}"
      ActiveSupport::IsolatedExecutionState[@thread_key] ||= TagStack.new
    end

    def current_tags
      tag_stack.tags
    end

    def push_tags(*tags)
      tag_stack.push_tags(tags)
    end
  end

  # logger.tagged("タグ").info(...)のようなインターフェースでタグを登録できるようにする
  def tagged(*tags)
    formatter.push_tags(*formatter.current_tags, *tags)
    self
  end
end

呼び出し側

logger.tagged("sample").info({
  message: "hoge!!",
  data: {
    hoge: "fuga"
  }
})

出力結果

{"severity":"INFO","timestamp":"2025-09-28T02:59:20.222436Z","progname":null,"message":"hoge!!","data":{"hoge": "fuga"},"tags":["sample"]}

参考