Chef のログを Fluentd に流す

Developers Summit 2014 での発表スライド)後、Chef のログを Fluentd に投げてる部分についてツイートをいただいたので、すこしまとめておきます。

Report (Exception) Handler

Chef の Report(Exception) Handler という仕組みを使って、ログを投げています。

  • Report Handler: Chef の実行が成功した場合に実行されるハンドラ
  • Exception Handler: Chef の実行が失敗した場合に実行されるハンドラ

詳しくは About Handlers を参照してください。

実装

実装としてはdataをそのまま投げるというものです。
datarun_statusをハッシュで表現したもので、ノードのアトリビュートや例外の情報などいろいろな情報を含んでいます。

ハンドラは以下の様な実装になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
require 'fluent-logger'

class FluentdHandler < Chef::Handler
  def initialize(tag_prefix, opts)
    @opts = opts
    @tag_prefix = tag_prefix
  end

  def report
    tag = success? ? 'report' : 'exception'
    logger.post(tag, data)
  end

  private
  def logger
    @logger ||= Fluent::Logger::FluentLogger.new(
      @tag_prefix, host: @opts[:host], port: @opts[:port]
    )
  end
end

このハンドラを使うにはsolo.rbなどで以下のようにハンドラを指定します。

1
2
3
handler = FluentHandler.new('chef', host: 'localhost', port: 24224)
report_handlers << handler
exception_handlers << handler

Gem 化しました

この記事を書くついでに Gem 化しておきましたので、ぜひお使いください。
(Pull Request お待ちしております)

GitHub: ryotarai/chef-handler-fluentd

MongoDB に保存する場合

fluent-plugin-mongo を使って MongoDB に保存する場合、dataのキーによっては BSON として invalid になってしまいます。
そのため、fluent-loggerに投げる前にキーに含まれる.と先頭の$を他の文字列に置き換える必要があります。

(おまけ)標準出力も投げる

Chef::Handler#dataで取得できるハッシュではコマンド実行時の標準出力などは取得できません。
そのため、標準出力は別途取得しています。

が、いろいろツライコードなので、もうちょっと良い実装方法はないものかと悩んでいます…


追記 2014/02/14 18:55

せっかく Fluentd, MongoDB つかってるのに全く構造化されてないデータ流すのナンセンスですね。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
$logger_io = StringIO.new

class Chef
  module Mixin
    module ShellOut
      class MultiIO < BasicObject
        attr_reader :ios
        def initialize
          @ios = []
        end
        private
        def method_missing(*args)
          @ios.each do |io|
            io.__send__(*args)
          end
        end
      end

      # This overrides Chef::Mixin::ShellOut#shell_out
      # FIXME
      def shell_out(*command_args)
        cmd = Mixlib::ShellOut.new(*run_command_compatible_options(command_args))
        if STDOUT.tty? && !Chef::Config[:daemon] && Chef::Log.debug?
          multi_io = MultiIO.new
          multi_io.ios << STDOUT
          multi_io.ios << $logger_io
          cmd.live_stream = multi_io
        end
        cmd.run_command
        cmd
      end
    end
  end
end

# この行は Chef::Application#configure_logging のあとに実行する必要がある
Chef::Log.loggers << Logger.new($logger_io)

# ハンドラ
class FluentHandler < Chef::Handler
  def initialize(host, port, tag_prefix)
    @host = host
    @port = port
    @tag_prefix = tag_prefix
  end

  def report
    tag = success? ? 'report' : 'exception'
    logger.post(tag, data_to_post)
  end

  private
  def logger
    @logger ||= Fluent::Logger::FluentLogger.new(@tag_prefix, host: @host, port: @port)
  end

  def data_to_post
    data.tap do |d|
      d['stdout'] = $logger_io.string
    end
  end
end