fluentdでout_fileが出力するファイルのパスを固定するpluginを作成する

# 背景、目的
WindowsServer上のシステムでログファイルが複数個に分かれているものがありました。
それぞれのログファイルを監視したいが、監視システムで監視対象ファイルを追加するためには追加費用が必要なため、ログファイルを一つに集約してファイル監視を行うことにしました。

fluentdの標準範囲で設定をしてみたところ、`Merge.log.20171020.log`というように出力日付がログファイル名に自動的に付与されます。
これでは、監視システムで監視対象のファイル名を固定することができません。

非Windows環境では、`symlink_path`でアクティブなファイルへのシンボリックリンクが作成され、ファイル名を固定化できるようですが、Windows環境は未対応のようです。

[fluentdでout_fileで出力されるファイルのパスを固定する方法]( https://qiita.com/fetaro/items/a6effc7e5d634eb7224e)でプラグインを作られている方がいましたので使わさせて頂きましたが、何かエラーが出たので諦めました。

ちょうどいい解決策を見つけられなかったので、プラグインを書こうと思いここまでに至りました。

# 環境
* Windows
* fluentd-0.14.13
* git

# 作っていきます
## gemスケルトン作成(Generating plugin project skeleton)
Td-agent Command Prompt から実行していきます。

```bat
C:\opt\td-agent>fluent-plugin-generate output fixfile
License: Apache-2.0
        create Gemfile
        create README.md
        create Rakefile
        create fluent-plugin-fixfile.gemspec
        create lib/fluent/plugin/out_fixfile.rb
        create test/helper.rb
        create test/plugin/test_out_fixfile.rb
Initialized empty Git repository in C:/opt/td-agent/fluent-plugin-fixfile/.git/

C:\opt\td-agent>cd fluent-plugin-out-fixfile
C:\opt\td-agent\fluent-plugin-fixfile>git add --all
warning: LF will be replaced by CRLF in Gemfile.
The file will have its original line endings in your working directory.
warning: LF will be replaced by CRLF in Rakefile.
The file will have its original line endings in your working directory.

C:\opt\td-agent\fluent-plugin-fixfile>git commit -m "initial commit"
[master (root-commit) 6545bd6] initial commit
 8 files changed, 338 insertions(+)
 create mode 100644 Gemfile
 create mode 100644 LICENSE
 create mode 100644 README.md
 create mode 100644 Rakefile
 create mode 100644 fluent-plugin-fixfile.gemspec
 create mode 100644 lib/fluent/plugin/out_fixfile.rb
 create mode 100644 test/helper.rb
 create mode 100644 test/plugin/test_out_fixfile.rb
 
 C:\opt\td-agent\fluent-plugin-fixfile>
```

## テストを書く
スケルトンでテストを実行してみました

```bat
C:\opt\td-agent\fluent-plugin-fixfile>rake test
C:/opt/td-agent/embedded/bin/ruby.exe -w -I"lib;lib;test" -I"C:/opt/td-agent/embedded/lib/ruby/gems/2.3.0/gems/rake-10.4.2/lib" "C:/opt/td-agent/embedded/lib/ruby/gems/2.3.0/gems/rake-10.4.2/lib/rake/rake_test_loader.rb" "test/plugin/test_out_fixfile.rb"
Loaded suite C:/opt/td-agent/embedded/lib/ruby/gems/2.3.0/gems/rake-10.4.2/lib/rake/rake_test_loader
Started
F
================================================================================

Failure: Flunked.
test: failure(FixfileOutputTest)
C:/opt/td-agent/fluent-plugin-fixfile/test/plugin/test_out_fixfile.rb:27:in `block in <class:FixfileOutputTest>'
     24:   }
     25:
     26:   test "failure" do
  => 27:     flunk
     28:   end
     29:
     30:   private
================================================================================



Finished in 0.050005 seconds.
--------------------------------------------------------------------------------

1 tests, 1 assertions, 1 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
0% passed
--------------------------------------------------------------------------------

20.00 tests/s, 20.00 assertions/s
rake aborted!
Command failed with status (1): [ruby -w -I"lib;lib;test" -I"C:/opt/td-agent/embedded/lib/ruby/gems/2.3.0/gems/rake-10.4.2/lib" "C:/opt/td-agent/embedded/lib/ruby/gems/2.3.0/gems/rake-10.4.2/lib/rake/rake_test_loader.rb" "test/plugin/test_out_fixfile.rb" ]

Tasks: TOP => test
(See full trace by running task with --trace)

C:\opt\td-agent\fluent-plugin-fixfile>
```


``out_file``のテストを参考に書いてみます。

```rb:test_out_fixfile.rb
require "helper"
require "fluent/plugin/out_fixfile.rb"
require "fileutils"
require "time"
require "timecop"
require "zlib"

class FixfileOutputTest < Test::Unit::TestCase
  setup do
    Fluent::Test.setup
    FileUtils.rm_rf(TMP_DIR)
    FileUtils.mkdir_p(TMP_DIR)
  end

  TMP_DIR = File.expand_path(File.dirname(__FILE__) + "/../tmp/out_fixfile#{ENV['TEST_ENV_NUMBER']}")

  CONFIG = %{
    path #{TMP_DIR}/out_fixfile_test
    compress gz
    utc
    <buffer>
      timekey_use_utc true
    </buffer>
  }

  def check_gzipped_result(path, expect)
    result = ''
    File.open(path, "rb") { |io|
      loop do
        gzr = Zlib::GzipReader.new(io)
        result << gzr.read
        unused = gzr.unused
        gzr.finish
        break if unused.nil?
        io.pos -= unused.length
      end
    }
    assert_equal expect, result
  end

  sub_test_case 'write' do
    test 'basic case' do
      d = create_driver

      assert_false File.exist?("#{TMP_DIR}/out_fixfile_test.20110102_0.log.gz")

      time = event_time("2011-01-02 13:14:15 UTC")
      d.run(default_tag: 'test') do
        d.feed(time, {"a"=>1})
        d.feed(time, {"a"=>2})
      end

      assert File.exist?("#{TMP_DIR}/out_fixfile_test.20110102_0.log.gz")
      check_gzipped_result("#{TMP_DIR}/out_fixfile_test.20110102_0.log.gz", %[2011-01-02T13:14:15Z\ttest\t{"a":1}\n] + %[2011-01-02T13:14:15Z\ttest\t{"a":2}\n])
    end
  end

  private

  def create_driver(conf = CONFIG, opts = {})
    Fluent::Test::Driver::Output.new(Fluent::Plugin::FixfileOutput).configure(conf)
  end
end
```

実行してみます

```bat
C:\opt\td-agent\fluent-plugin-fixfile>rake test
C:/opt/td-agent/embedded/bin/ruby.exe -w -I"lib;lib;test" -I"C:/opt/td-agent/embedded/lib/ruby/gems/2.3.0/gems/rake-10.4.2/lib" "C:/opt/td-agent/embedded/lib/ruby/gems/2.3.0/gems/rake-10.4.2/lib/rake/rake_test_loader.rb" "test/plugin/test_out_fixfile.rb"
Loaded suite C:/opt/td-agent/embedded/lib/ruby/gems/2.3.0/gems/rake-10.4.2/lib/rake/rake_test_loader
Started
E
================================================================================

Error: test: basic case(FixfileOutputTest::write): RuntimeError: BUG: output plugin must implement some methods. see developer documents.
C:/opt/td-agent/embedded/lib/ruby/gems/2.3.0/gems/fluentd-0.14.13/lib/fluent/plugin/output.rb:228:in `configure'
C:/opt/td-agent/embedded/lib/ruby/gems/2.3.0/gems/fluentd-0.14.13/lib/fluent/test/driver/base_owner.rb:57:in `configure'
C:/opt/td-agent/fluent-plugin-fixfile/test/plugin/test_out_fixfile.rb:61:in `create_driver'
C:/opt/td-agent/fluent-plugin-fixfile/test/plugin/test_out_fixfile.rb:43:in `block (2 levels) in <class:FixfileOutputTest>'
================================================================================



Finished in 0.023002 seconds.
--------------------------------------------------------------------------------

1 tests, 0 assertions, 0 failures, 1 errors, 0 pendings, 0 omissions, 0 notifications
0% passed
--------------------------------------------------------------------------------

43.47 tests/s, 0.00 assertions/s
rake aborted!
Command failed with status (1): [ruby -w -I"lib;lib;test" -I"C:/opt/td-agent/embedded/lib/ruby/gems/2.3.0/gems/rake-10.4.2/lib" "C:/opt/td-agent/embedded/lib/ruby/gems/2.3.0/gems/rake-10.4.2/lib/rake/rake_test_loader.rb" "test/plugin/test_out_fixfile.rb" ]

Tasks: TOP => test
(See full trace by running task with --trace)

C:\opt\td-agent\fluent-plugin-fixfile>
```

本体にメソッドを定義しろと言われたので書いていきます。
書くのは、最低限の枠組みだけです。
本件では、``out_file``の出力先を固定したいだけなので、``FileOutput``を継承する形で作ります。

```rb:out_fixfile.rb
require "fluent/plugin/output"
require "fluent/plugin/out_file"

module Fluent
  module Plugin
    class FixfileOutput < Fluent::Plugin::FileOutput
      Fluent::Plugin.register_output("fixfile", self)

      def configure(conf)
        super
      end

      def start
        super
      end

      def shutdown
        super
      end

      def format(tag, time, record)
        super
      end

      def write(chunk)
        super
      end
    end
  end
end

```

テストを実行し、OKだったので本来の実装を始めます。

```bat
Started
.

Finished in 1.393139 seconds.
--------------------------------------------------------------------------------

1 tests, 3 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
--------------------------------------------------------------------------------

0.72 tests/s, 2.15 assertions/s
```

## 実装した
https://github.com/soulhead1mg/fluent-plugin-fixfile

## gemをビルドする

```bat
C:\opt\td-agent\fluent-plugin-fixfile>rake build
fluent-plugin-fixfile 0.1.0 built to pkg/fluent-plugin-fixfile-0.1.0.gem.

C:\opt\td-agent\fluent-plugin-fixfile>
```

## td-agent にインストールする

```bat
C:\opt\td-agent\fluent-plugin-fixfile>fluent-gem install pkg/fluent-plugin-fixfi
le-0.1.0.gem
Successfully installed fluent-plugin-fixfile-0.1.0
Parsing documentation for fluent-plugin-fixfile-0.1.0
Installing ri documentation for fluent-plugin-fixfile-0.1.0
Done installing documentation for fluent-plugin-fixfile after 3 seconds
1 gem installed

C:\opt\td-agent\fluent-plugin-fixfile>
```


# 参考にしたサイト
* [[Ruby]他言語使用者のためのRuby入門知識まとめ](https://qiita.com/cabbage/items/238ebd7b91f83e99681e)
* [すっかり時代に乗り遅れてるけど fluentd が必要になったので test も書きながら output プラグインを書いているその記録](https://qiita.com/norobust/items/8b8f944e72188fb395a0)
* [fluentd/test/plugin/test_out_file.rb](https://github.com/fluent/fluentd/blob/master/test/plugin/test_out_file.rb)
* [Writing plugins](https://docs.fluentd.org/v0.14/articles/plugin-development)

# その他
* ruby, gem などなどよくわかってない状態から手探りで作りました
* ``bundle gem``で始めて一通り作りました
* ここに書くためにもう一度初めから参考サイトなどを見ながらもう一度やり直したら、公式ドキュメントの``fluent-plugin-generate``に気づきやり方を変えました
* ``bundle gem``でやった時に苦労したところに遭遇しないようになりました。以下苦労したところ
 * テストコードで``helper``を読み込めず？``event_time``が使えない　→　``time = Time.parse("2011-01-02 13:14:15 UTC").to_i``と書いた
 * ``create_driver``を2回以上呼び出すと``same buffer``とエラーになる　→　テストのファイル名を``*_test.rb``としておりテストコードと認識されなかった。正しくは、``test_*.rb``と名前をつける必要があった。
http://www.rubydoc.info/gems/fluentd/0.14.4/Fluent/Plugin/FileBuffer#buffer_path_for_test%3F-instance_method


```rb:lib/fluent/plugin/buf_file.rb
# line 95
def buffer_path_for_test?
  caller_locations.each do |location|
    # Thread::Backtrace::Location#path returns base filename or absolute path.
    # #absolute_path returns absolute_path always.
    # https://bugs.ruby-lang.org/issues/12159
    if location.absolute_path =~ /\/test_[^\/]+\.rb$/ # location.path =~ /test_.+\.rb$/
      return true
    end
  end
  false
end
```