├── .github └── workflows │ └── linux.yml ├── .gitignore ├── .rspec ├── AUTHORS ├── COPYING ├── ChangeLog ├── Gemfile ├── Gemfile.v0.12 ├── README.md ├── Rakefile ├── bin └── fluent-post ├── fluent-logger.gemspec ├── lib ├── fluent-logger.rb └── fluent │ ├── logger.rb │ └── logger │ ├── console_logger.rb │ ├── fluent_logger.rb │ ├── fluent_logger │ └── cui.rb │ ├── level_fluent_logger.rb │ ├── logger_base.rb │ ├── null_logger.rb │ ├── test_logger.rb │ ├── text_logger.rb │ └── version.rb └── spec ├── console_logger_spec.rb ├── fluent_logger_spec.rb ├── level_fluent_logger_spec.rb ├── logger_base_spec.rb ├── logger_spec.rb ├── null_logger_spec.rb ├── plugin └── out_test.rb ├── spec_helper.rb ├── support ├── dummy_fluentd.rb ├── dummy_serverengine.rb └── timecop.rb └── test_logger_spec.rb /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: linux 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | ruby: 12 | - '3.2' 13 | - '3.3' 14 | - '3.4' 15 | os: 16 | - ubuntu-latest 17 | name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }} 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: ${{ matrix.ruby }} 23 | - name: unit testing 24 | env: 25 | CI: true 26 | run: | 27 | gem install rake 28 | gem install bundler ${{ matrix.ruby == '2.7' && '-v 2.4.22' || '' }} 29 | bundle install --jobs 4 --retry 3 30 | bundle exec rake spec 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | Gemfile.lock 3 | pkg/* 4 | coverage/* 5 | coverage.vim 6 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | FURUHASHI Sadayuki 2 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 FURUHASHI Sadayuki 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | Release 0.9.1 - 2023/12/20 2 | 3 | * Fix invalid message polluting subsequent message 4 | 5 | Release 0.9.0 - 2020/09/04 6 | 7 | * FluentLogger supports TLS 8 | 9 | Release 0.8.2 - 2019/08/21 10 | 11 | * Block timeout during IO#write to avoid writing invalid bytes 12 | * Fluent::Logger::EventTime#to_json returns String 13 | 14 | Release 0.8.1 - 2019/05/30 15 | 16 | * Improve non-blocking write handling 17 | 18 | Release 0.8.0 - 2019/01/25 19 | 20 | * Add use_nonblock and wait_writeable parameters 21 | 22 | Release 0.7.2 - 2018/01/04 23 | 24 | * Fix EventTime for nanosecond_precision 25 | 26 | Release 0.7.1 - 2017/04/19 27 | 28 | * Fix packer clear bug for non-msgpackable data 29 | 30 | Release 0.7.0 - 2017/04/12 31 | 32 | * Add nanosecond_precision parameter for v0.14 33 | * Update msgpack dependency to v1.0 34 | 35 | Release 0.6.3 - 2017/03/29 36 | 37 | * Add pending_bytesize method 38 | 39 | Release 0.6.2 - 2017/01/17 40 | 41 | * Add unix domain socket support for `in_unix` 42 | * LevelFluentLogger uses standard compatible severity formatting 43 | 44 | Release 0.6.1 - 2016/11/02 45 | 46 | * Add LevelFluentLogger: Standard Logger compatible interface 47 | 48 | Release 0.6.0 - 2016/10/15 49 | 50 | * Relax msgpack dependency 51 | * ConsoleLogger: close io if io is stdout 52 | 53 | Release 0.5.1 - 2015/11/24 54 | 55 | * Allow injection of buffer overflow handler 56 | * tag_prefix now default to nil 57 | * LoggerBase.open follows README 58 | 59 | Release 0.5.0 - 2015/01/25 60 | 61 | * Use json instead of yajl. If you pass an invalid string to post, you may get 'invalid byte sequence' error. 62 | * Support JRuby environment 63 | 64 | Release 0.4.10 - 2015/01/10 65 | 66 | * Call closed? in connect? method to check connection is closed or not 67 | * Add rspec-its for spec 68 | 69 | Release 0.4.9 - 2014/04/17 70 | 71 | * Fix the peformance regression caused by many to_json calls 72 | 73 | Release 0.4.8 - 2014/04/12 74 | 75 | * Use at_exit for logger finalization 76 | * Fix unable to construct a default logger of non-default type 77 | 78 | Release 0.4.7 - 2013/11/21 79 | 80 | * Suppress log error message with :log_reconnect_error_threshold option 81 | * Add FluentLogger#last_error method to get last exception in user code 82 | 83 | Release 0.4.6 - 2013/06/17 84 | 85 | * Raise an ArgumentError when passes invalid argument to post method 86 | * Relax msgpack gem version 87 | 88 | Release 0.4.5 - 2013/02/26 89 | 90 | * Use https scheme for rubygems source 91 | * Fix broken spec 92 | 93 | Release 0.4.4 - 2012/12/26 94 | 95 | * Change msgpack dependency version == 0.4.7 for avoding 0.4.8 yanked issue 96 | 97 | Release 0.4.3 - 2012/04/24 98 | 99 | * Update yajl dependency version >= 1.0 (thanks shun0102) 100 | 101 | Release 0.4.2 - 2012/03/02 102 | 103 | * Added TestLogger#tag_queue(tag_name) 104 | * Added bin/fluent-post cli command 105 | * Impl default LoggerBase#close 106 | * Don't change logger.level if :debug=> option is sepcified 107 | 108 | Release 0.4.1 - 2011/11/07 109 | 110 | * added Logger#post_with_time(tag, map, time) 111 | * Logger#post(tag, map, time=Time.now) -> Logger#post(tag, map) 112 | * FluentLogger supports :debug=>true option to write all events to STDERR 113 | 114 | 115 | Release 0.4.0 - 2011/11/05 116 | 117 | * Wait before reconnecting to fluentd to prevent burst 118 | * Flush logs when process stops using finalizer 119 | * Added rspec and coverage 120 | * Supports objects that don't support to_msgpack by 121 | JSON.load(JSON.dump(obj)).to_msgpack 122 | * FluentLogger uses IO#sync=true + IO#write instead of IO#syswrite to 123 | avoid unexpected blocking 124 | * Logger#post(tag, map) -> Logger#post(tag, map, time=Time.now) 125 | * Removed Event classes 126 | * Added NullLogger 127 | 128 | 129 | Release 0.3.1 - 2011/08/28 130 | 131 | * FluentLogger#initialize doesn't raise error when connection is failed. 132 | Instead, it tries to reconnect. 133 | 134 | 135 | Release 0.3.0 - 2011/08/21 136 | 137 | * Added 'tag' for event logs 138 | 139 | 140 | Release 0.2.0 - 2011/08/05 141 | 142 | * Redesigned Event class 143 | * Added TestLogger (Fluent.open(:test)) 144 | * Added test programs 145 | 146 | 147 | Release 0.1.0 - 2011/08/04 148 | 149 | * First release 150 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | 2 | source 'https://rubygems.org/' 3 | 4 | gem "fluentd", :github => 'fluent/fluentd' 5 | 6 | gemspec 7 | 8 | gem "simplecov", :require => false 9 | gem "simplecov-vim" 10 | 11 | gem 'test-unit' 12 | -------------------------------------------------------------------------------- /Gemfile.v0.12: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'json', '= 1.8.3' 4 | gem 'fluentd', '~> 0.12.0' 5 | 6 | gemspec 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fluent logger 2 | 3 | [![Build Status](https://github.com/fluent/fluent-logger-ruby/actions/workflows/linux.yml/badge.svg?branch=master)](https://github.com/fluent/fluent-logger-ruby/actions/workflows/linux.yml) 4 | 5 | A structured event logger 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'fluent-logger' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle install 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install fluent-logger 22 | 23 | ## Examples 24 | 25 | ### Simple 26 | 27 | ```ruby 28 | require 'fluent-logger' 29 | 30 | # API: FluentLogger.new(tag_prefix, options) 31 | log = Fluent::Logger::FluentLogger.new(nil, :host => 'localhost', :port => 24224) 32 | unless log.post("myapp.access", {"agent" => "foo"}) 33 | p log.last_error # You can get last error object via last_error method 34 | end 35 | 36 | # output: myapp.access {"agent":"foo"} 37 | ``` 38 | 39 | ### UNIX socket 40 | 41 | ```ruby 42 | require 'fluent-logger' 43 | 44 | log = Fluent::Logger::FluentLogger.new(nil, :socket_path => "/tmp/fluent.sock") 45 | unless log.post("myapp.access", {"agent" => "foo"}) 46 | # Passed records are stored into logger's internal buffer so don't re-post same event. 47 | p log.last_error # You can get last error object via last_error method 48 | end 49 | 50 | # output: myapp.access {"agent":"foo"} 51 | ``` 52 | 53 | ### Tag prefix 54 | ```ruby 55 | require 'fluent-logger' 56 | 57 | log = Fluent::Logger::FluentLogger.new('myapp', :host => 'localhost', :port => 24224) 58 | log.post("access", {"agent" => "foo"}) 59 | 60 | # output: myapp.access {"agent":"foo"} 61 | ``` 62 | 63 | ### Nonblocking write 64 | 65 | ```ruby 66 | require 'fluent-logger' 67 | 68 | log = Fluent::Logger::FluentLogger.new(nil, :host => 'localhost', :port => 24224, :use_nonblock => true, :wait_writeable => false) 69 | # When wait_writeable is false 70 | begin 71 | log.post("myapp.access", {"agent" => "foo"}) 72 | rescue IO::EAGAINWaitWritable => e 73 | # wait code for avoding "Resource temporarily unavailable" 74 | # Passed records are stored into logger's internal buffer so don't re-post same event. 75 | end 76 | 77 | # When wait_writeable is true 78 | unless log.post("myapp.access", {"agent" => "foo"}) 79 | # same as other example 80 | end 81 | 82 | # output: myapp.access {"agent":"foo"} 83 | ``` 84 | 85 | ### TLS setting 86 | 87 | ```ruby 88 | require 'fluent-logger' 89 | 90 | tls_opts = { 91 | :ca => '/path/to/cacert.pem', 92 | :cert => '/path/to/client-cert.pem', 93 | :key => '/path/to/client-key.pem', 94 | :key_passphrase => 'test' 95 | } 96 | log = Fluent::Logger::FluentLogger.new(nil, :host => 'localhost', :port => 24224, :tls_options => tls_opts) 97 | ``` 98 | 99 | `in_forward` config example: 100 | 101 | ``` 102 | 103 | @type forward 104 | 105 | version TLS1_2 106 | ca_path /path/to/cacert.pem 107 | cert_path /path/to/server-cert.pem 108 | private_key_path /path/to/server-key.pem 109 | private_key_passphrase test 110 | client_cert_auth true 111 | 112 | 113 | ``` 114 | 115 | ### Singleton 116 | ```ruby 117 | require 'fluent-logger' 118 | 119 | Fluent::Logger::FluentLogger.open(nil, :host => 'localhost', :port => 24224) 120 | Fluent::Logger.post("myapp.access", {"agent" => "foo"}) 121 | 122 | # output: myapp.access {"agent":"foo"} 123 | ``` 124 | 125 | ### Logger options 126 | 127 | #### host (String) 128 | 129 | fluentd instance host 130 | 131 | #### port (Integer) 132 | 133 | fluentd instance port 134 | 135 | #### socket_path (String) 136 | 137 | If specified, fluentd uses unix domain socket instead of TCP. 138 | 139 | #### nanosecond_precision (Bool) 140 | 141 | Use nano second event time instead of epoch. See also "Tips" section. 142 | 143 | #### use_nonblock (Bool) 144 | 145 | Use nonblocking write(`IO#write_nonblock`) instead of normal write(`IO#write`). If `Logger#post` stuck on your environment, specify `true`. Default: `false` 146 | 147 | #### wait_writeable (Bool) 148 | 149 | If `false`, `Logger#post` raises an error when nonblocking write gets `EAGAIN` (i.e. `use_nonblock` must be `true`, otherwise this will have no effect). Default: `true` 150 | 151 | #### buffer_overflow_handler (Proc) 152 | 153 | Pass callback for handling buffer overflow with pending data. See "Buffer overflow" section. 154 | 155 | #### tls_options (Hash) 156 | 157 | Pass TLS related options. 158 | 159 | - use_default_ca: Set `true` if you want to use default CA 160 | - ca: CA file path 161 | - cert: Certificate file path 162 | - key: Private key file path 163 | - key_passphrase: Private key passphrase 164 | - version: TLS version. Default is `OpenSSL::SSL::TLS1_2_VERSION` 165 | - ciphers: The list of cipher suites. Default is `ALL:!aNULL:!eNULL:!SSLv2` 166 | - insecure: Set `true` when `in_forward` uses `insecure true` 167 | 168 | ### Standard ::Logger compatible interface 169 | 170 | #### Example1 171 | 172 | ```ruby 173 | require 'fluent-logger' 174 | f = Fluent::Logger::LevelFluentLogger.new('fluent') 175 | 176 | f.info("some application running.") 177 | # output: fluent.info: {"level":"INFO","message":"some application running."} 178 | 179 | f.warn("some application running.") 180 | # output: fluent.warn: {"level":"WARN","message":"some application running."} 181 | ``` 182 | 183 | #### Example2(add progname) 184 | 185 | ```ruby 186 | require 'fluent-logger' 187 | f = Fluent::Logger::LevelFluentLogger.new('fluent') 188 | f.info("some_application") {"some application running."} 189 | # output: fluent.info: {"level":"INFO","message":"some application running.","progname":"some_application"} 190 | ``` 191 | 192 | #### Example3(set log level) 193 | 194 | ```ruby 195 | require 'fluent-logger' 196 | f = Fluent::Logger::LevelFluentLogger.new('fluent') 197 | f.level = Logger::WARN 198 | f.info("some_application") {"some application running."} 199 | ``` 200 | 201 | Log level is ERROR so no output. 202 | 203 | default log level is debug. 204 | 205 | 206 | #### Example4(customize format for Rails) 207 | 208 | ```ruby 209 | require 'fluent-logger' 210 | f = Fluent::Logger::LevelFluentLogger.new('fluent') 211 | 212 | f.formatter = proc do |severity, datetime, progname, message| 213 | map = { level: severity } 214 | map[:message] = message if message 215 | map[:progname] = progname if progname 216 | map[:stage] = ENV['RAILS_ENV'] 217 | map[:service_name] = "SomeApp" 218 | map 219 | end 220 | 221 | f.info("some_application"){"some application running."} 222 | # output: fluent.info: {"level":"INFO","message":"some application running.","progname":"some_application","stage":"production","service_name":"SomeApp"} 223 | ``` 224 | 225 | ## Loggers 226 | 227 | ### Fluent 228 | ```ruby 229 | Fluent::Logger::FluentLogger.open('tag_prefix', :host => 'localhost', :port => 24224) 230 | ``` 231 | 232 | ### Console 233 | ```ruby 234 | Fluent::Logger::ConsoleLogger.open(io) 235 | ``` 236 | 237 | ### Null 238 | ```ruby 239 | Fluent::Logger::NullLogger.open 240 | ``` 241 | 242 | ## Tips 243 | 244 | ### Use nanosecond-precision time 245 | 246 | To send events with nanosecond-precision time (Fluent 0.14 and up), specify `nanosecond_precision` to `FluentLogger` constructor. 247 | 248 | ```ruby 249 | log = Fluent::Logger::FluentLogger.new(nil, :host => 'localhost', :port => 24224, :nanosecond_precision => true) 250 | # Use nanosecond time instead 251 | log.post("myapp.access", {"agent" => "foo"}) 252 | log.post_with_time("myapp.access", {"agent" => "foo"}, Time.now) # Need Time object for post_with_time 253 | ``` 254 | 255 | ### Buffer overflow 256 | 257 | You can inject your own custom proc to handle buffer overflow in the event of connection failure. This will mitigate the loss of data instead of simply throwing data away. 258 | 259 | Your proc must accept a single argument, which will be the internal buffer of messages from the logger. A typical use-case for this would be writing to disk or possibly writing to Redis. 260 | 261 | ##### Example 262 | ```ruby 263 | class BufferOverflowHandler 264 | attr_accessor :buffer 265 | 266 | def flush(messages) 267 | @buffer ||= [] 268 | MessagePack::Unpacker.new.feed_each(messages) do |msg| 269 | @buffer << msg 270 | end 271 | end 272 | end 273 | 274 | handler = Proc.new { |messages| BufferOverflowHandler.new.flush(messages) } 275 | 276 | Fluent::Logger::FluentLogger.new(nil, 277 | :host => 'localhost', :port => 24224, 278 | :buffer_overflow_handler => handler) 279 | ``` 280 | 281 | ## Information 282 | 283 | |name|description| 284 | |---|---| 285 | |Web site|http://fluentd.org/ | 286 | |Documents|http://docs.fluentd.org/ | 287 | |Source repository|https://github.com/fluent/fluent-logger-ruby | 288 | |Author|Sadayuki Furuhashi| 289 | |Copyright|(c) 2011 FURUHASHI Sadayuki| 290 | |License|Apache License, Version 2.0| 291 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | 2 | require 'bundler' 3 | Bundler::GemHelper.install_tasks 4 | 5 | require 'rspec/core' 6 | require 'rspec/core/rake_task' 7 | 8 | RSpec::Core::RakeTask.new(:spec) do |spec| 9 | spec.pattern = FileList['spec/**/*_spec.rb'] 10 | end 11 | 12 | task :coverage do |t| 13 | ENV['SIMPLE_COV'] = '1' 14 | Rake::Task["spec"].invoke 15 | end 16 | 17 | task :default => :build 18 | 19 | -------------------------------------------------------------------------------- /bin/fluent-post: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'fluent/logger/fluent_logger/cui' 4 | 5 | res = Fluent::Logger::FluentLogger::CUI.post(ARGV) 6 | if res[:success] 7 | warn "post successed. #=> #{res[:data].inspect}" 8 | else 9 | warn "post failed. #=> #{res[:data].inspect}" 10 | exit 1 11 | end 12 | -------------------------------------------------------------------------------- /fluent-logger.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | $:.push File.expand_path('../lib', __FILE__) 3 | require 'fluent/logger/version' 4 | 5 | Gem::Specification.new do |gem| 6 | version_file = "lib/fluent/logger/version.rb" 7 | version = Fluent::Logger::VERSION 8 | 9 | gem.name = %q{fluent-logger} 10 | gem.version = version 11 | # gem.platform = Gem::Platform::RUBY 12 | gem.authors = ["Sadayuki Furuhashi"] 13 | gem.email = %q{frsyuki@gmail.com} 14 | gem.homepage = %q{https://github.com/fluent/fluent-logger-ruby} 15 | gem.description = %q{fluent logger for ruby} 16 | gem.summary = gem.description 17 | 18 | gem.files = `git ls-files`.split("\n") 19 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 20 | gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 21 | gem.require_paths = ['lib'] 22 | gem.license = "Apache-2.0" 23 | 24 | gem.add_dependency "msgpack", ">= 1.0.0", "< 2" 25 | 26 | # logger gem that isn't default gems as of Ruby 3.5 27 | gem.add_runtime_dependency("logger", ["~> 1.6"]) 28 | 29 | gem.add_development_dependency 'rake', '>= 0.9.2' 30 | gem.add_development_dependency 'rspec', '>= 3.0.0' 31 | gem.add_development_dependency 'rspec-its', '>= 1.1.0' 32 | gem.add_development_dependency 'simplecov', '>= 0.5.4' 33 | gem.add_development_dependency 'timecop', '>= 0.3.0' 34 | gem.add_development_dependency 'webrick' 35 | end 36 | -------------------------------------------------------------------------------- /lib/fluent-logger.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'fluent', 'logger') 2 | -------------------------------------------------------------------------------- /lib/fluent/logger.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Fluent 3 | # 4 | # Copyright (C) 2011 FURUHASHI Sadayuki 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | module Fluent 19 | module Logger 20 | autoload :ConsoleLogger , 'fluent/logger/console_logger' 21 | autoload :FluentLogger , 'fluent/logger/fluent_logger' 22 | autoload :LevelFluentLogger , 'fluent/logger/level_fluent_logger' 23 | autoload :LoggerBase , 'fluent/logger/logger_base' 24 | autoload :TestLogger , 'fluent/logger/test_logger' 25 | autoload :TextLogger , 'fluent/logger/text_logger' 26 | autoload :NullLogger , 'fluent/logger/null_logger' 27 | autoload :VERSION , 'fluent/logger/version' 28 | 29 | @@default_logger = nil 30 | 31 | def self.new(*args) 32 | if args.first.is_a?(Class) && args.first.ancestors.include?(LoggerBase) 33 | type = args.shift 34 | else 35 | type = FluentLogger 36 | end 37 | type.new(*args) 38 | end 39 | 40 | def self.open(*args) 41 | close 42 | @@default_logger = new(*args) 43 | end 44 | 45 | def self.close 46 | if @@default_logger 47 | @@default_logger.close 48 | @@default_logger = nil 49 | end 50 | end 51 | 52 | def self.post(tag, map) 53 | default.post(tag, map) 54 | end 55 | 56 | def self.post_with_time(tag, map, time) 57 | default.post_with_time(tag, map, time) 58 | end 59 | 60 | def self.default 61 | @@default_logger ||= ConsoleLogger.new(STDOUT) 62 | end 63 | 64 | def self.default=(logger) 65 | @@default_logger = logger 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/fluent/logger/console_logger.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Fluent 3 | # 4 | # Copyright (C) 2011 FURUHASHI Sadayuki 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | require 'fluent/logger/text_logger' 19 | 20 | module Fluent 21 | module Logger 22 | class ConsoleLogger < TextLogger 23 | def initialize(out) 24 | super() 25 | require 'time' 26 | 27 | if out.is_a?(String) 28 | @io = File.open(out, "a") 29 | @on_reopen = Proc.new { @io.reopen(out, "a") } 30 | elsif out.respond_to?(:write) 31 | @io = out 32 | @on_reopen = Proc.new { } 33 | else 34 | raise "Invalid output: #{out.inspect}" 35 | end 36 | end 37 | 38 | attr_accessor :time_format 39 | 40 | def reopen! 41 | @on_reopen.call 42 | end 43 | 44 | def post_text(text) 45 | @io.puts text 46 | end 47 | 48 | def close 49 | @io.close unless @io == STDOUT 50 | self 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/fluent/logger/fluent_logger.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Fluent 3 | # 4 | # Copyright (C) 2011 FURUHASHI Sadayuki 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | require 'timeout' 19 | require 'msgpack' 20 | require 'socket' 21 | require 'openssl' 22 | require 'monitor' 23 | require 'logger' 24 | require 'json' 25 | 26 | module Fluent 27 | module Logger 28 | class EventTime 29 | TYPE = 0 30 | 31 | def initialize(sec, nsec = 0) 32 | @sec = sec 33 | @nsec = nsec 34 | end 35 | 36 | def to_msgpack(io = nil) 37 | @sec.to_msgpack(io) 38 | end 39 | 40 | def to_msgpack_ext 41 | [@sec, @nsec].pack('NN') 42 | end 43 | 44 | def self.from_msgpack_ext(data) 45 | new(*data.unpack('NN')) 46 | end 47 | 48 | def to_json(*args) 49 | @sec.to_s 50 | end 51 | end 52 | 53 | class FluentLogger < LoggerBase 54 | BUFFER_LIMIT = 8*1024*1024 55 | RECONNECT_WAIT = 0.5 56 | RECONNECT_WAIT_INCR_RATE = 1.5 57 | RECONNECT_WAIT_MAX = 60 58 | RECONNECT_WAIT_MAX_COUNT = 59 | (1..100).inject(RECONNECT_WAIT_MAX / RECONNECT_WAIT) { |r, i| 60 | break i + 1 if r < RECONNECT_WAIT_INCR_RATE 61 | r / RECONNECT_WAIT_INCR_RATE 62 | } 63 | 64 | def initialize(tag_prefix = nil, *args) 65 | super() 66 | 67 | options = { 68 | :host => 'localhost', 69 | :port => 24224, 70 | :use_nonblock => false 71 | } 72 | 73 | case args.first 74 | when String, Symbol 75 | # backward compatible 76 | options[:host] = args[0] 77 | options[:port] = args[1] if args[1] 78 | when Hash 79 | options.update args.first 80 | end 81 | 82 | @tag_prefix = tag_prefix 83 | @host = options[:host] 84 | @port = options[:port] 85 | @socket_path = options[:socket_path] 86 | @nanosecond_precision = options[:nanosecond_precision] 87 | @use_nonblock = options[:use_nonblock] 88 | @tls_options = options[:tls_options] 89 | 90 | @factory = MessagePack::Factory.new 91 | if @nanosecond_precision 92 | @factory.register_type(EventTime::TYPE, EventTime) 93 | end 94 | @packer = @factory.packer 95 | 96 | @mon = Monitor.new 97 | @pending = nil 98 | @connect_error_history = [] 99 | 100 | @limit = options[:buffer_limit] || BUFFER_LIMIT 101 | @log_reconnect_error_threshold = options[:log_reconnect_error_threshold] || RECONNECT_WAIT_MAX_COUNT 102 | 103 | @buffer_overflow_handler = options[:buffer_overflow_handler] 104 | if options[:logger] 105 | @logger = options[:logger] 106 | else 107 | @logger = ::Logger.new(STDERR) 108 | if options[:debug] 109 | @logger.level = ::Logger::DEBUG 110 | else 111 | @logger.level = ::Logger::INFO 112 | end 113 | end 114 | 115 | @wait_writeable = true 116 | @wait_writeable = options[:wait_writeable] if options.key?(:wait_writeable) 117 | 118 | @last_error = {} 119 | 120 | begin 121 | connect! 122 | rescue => e 123 | set_last_error(e) 124 | @logger.error "Failed to connect fluentd: #{$!}" 125 | @logger.error "Connection will be retried." 126 | end 127 | 128 | at_exit { close } 129 | end 130 | 131 | attr_accessor :limit, :logger, :log_reconnect_error_threshold 132 | 133 | def last_error 134 | @last_error[Thread.current.object_id] 135 | end 136 | 137 | def post_with_time(tag, map, time) 138 | @logger.debug { "event: #{tag} #{map.to_json}" rescue nil } if @logger.debug? 139 | tag = "#{@tag_prefix}.#{tag}" if @tag_prefix 140 | if @nanosecond_precision && time.is_a?(Time) 141 | write [tag, EventTime.new(time.to_i, time.nsec), map] 142 | else 143 | write [tag, time.to_i, map] 144 | end 145 | end 146 | 147 | def close 148 | @mon.synchronize { 149 | if @pending 150 | begin 151 | send_data(@pending) 152 | rescue => e 153 | set_last_error(e) 154 | @logger.error("FluentLogger: Can't send logs to #{connection_string}: #{$!}") 155 | call_buffer_overflow_handler(@pending) 156 | end 157 | end 158 | @con.close if connect? 159 | @con = nil 160 | @pending = nil 161 | } 162 | self 163 | end 164 | 165 | def connect? 166 | @con && !@con.closed? 167 | end 168 | 169 | def create_socket! 170 | if @socket_path 171 | @con = UNIXSocket.new(@socket_path) 172 | else 173 | @con = TCPSocket.new(@host, @port) 174 | if @tls_options 175 | context = OpenSSL::SSL::SSLContext.new 176 | if @tls_options[:insecure] 177 | context.verify_mode = OpenSSL::SSL::VERIFY_NONE 178 | else 179 | context.set_params({}) 180 | context.verify_mode = OpenSSL::SSL::VERIFY_PEER 181 | cert_store = OpenSSL::X509::Store.new 182 | if @tls_options[:use_default_ca] 183 | cert_store.set_default_paths 184 | end 185 | if @tls_options[:ca] 186 | cert_store.add_file(@tls_options[:ca]) 187 | end 188 | 189 | context.cert = OpenSSL::X509::Certificate.new(File.read(@tls_options[:cert])) if @tls_options[:cert] 190 | context.key = OpenSSL::PKey::read(File.read(@tls_options[:key]), @tls_options[:key_passphrase]) if @tls_options[:key] 191 | context.ciphers = @tls_options[:ciphers] || "ALL:!aNULL:!eNULL:!SSLv2".freeze 192 | context.cert_store = cert_store 193 | end 194 | set_tls_version(context) 195 | 196 | @con = OpenSSL::SSL::SSLSocket.new(@con, context) 197 | @con.sync_close = true 198 | @con.connect 199 | end 200 | @con 201 | end 202 | end 203 | 204 | def connection_string 205 | @socket_path ? "#{@socket_path}" : "#{@host}:#{@port}" 206 | end 207 | 208 | def pending_bytesize 209 | if @pending 210 | @pending.bytesize 211 | else 212 | 0 213 | end 214 | end 215 | 216 | private 217 | 218 | def set_tls_version(context) 219 | if context.respond_to?(:min_version=) 220 | ver = @tls_options[:version] || OpenSSL::SSL::TLS1_2_VERSION 221 | context.min_version = ver 222 | context.max_version = ver 223 | else 224 | context.ssl_version = @tls_options[:version] || :'TLSv1_2' 225 | end 226 | end 227 | 228 | def to_msgpack(msg) 229 | @mon.synchronize { 230 | res = begin 231 | @packer.pack(msg).to_s 232 | rescue NoMethodError 233 | JSON.parse(JSON.generate(msg)).to_msgpack 234 | ensure 235 | @packer.clear 236 | end 237 | res 238 | } 239 | end 240 | 241 | def suppress_sec 242 | if (sz = @connect_error_history.size) < RECONNECT_WAIT_MAX_COUNT 243 | RECONNECT_WAIT * (RECONNECT_WAIT_INCR_RATE ** (sz - 1)) 244 | else 245 | RECONNECT_WAIT_MAX 246 | end 247 | end 248 | 249 | def write(msg) 250 | begin 251 | data = to_msgpack(msg) 252 | rescue => e 253 | set_last_error(e) 254 | @logger.error("FluentLogger: Can't convert to msgpack: #{msg.inspect}: #{$!}") 255 | return false 256 | end 257 | 258 | @mon.synchronize { 259 | if @pending 260 | @pending << data 261 | else 262 | @pending = data 263 | end 264 | 265 | # suppress reconnection burst 266 | if !@connect_error_history.empty? && pending_bytesize <= @limit 267 | if Time.now.to_i - @connect_error_history.last < suppress_sec 268 | return false 269 | end 270 | end 271 | 272 | begin 273 | written = send_data(@pending) 274 | if @pending.bytesize != written 275 | raise "Actual written data size(#{written} bytes) is different from the received data size(#{@pending.bytesize} bytes)." 276 | end 277 | 278 | @pending = nil 279 | true 280 | rescue => e 281 | unless wait_writeable?(e) 282 | raise e 283 | end 284 | set_last_error(e) 285 | if pending_bytesize > @limit 286 | @logger.error("FluentLogger: Can't send logs to #{connection_string}: #{$!}") 287 | call_buffer_overflow_handler(@pending) 288 | @pending = nil 289 | end 290 | @con.close if connect? 291 | @con = nil 292 | false 293 | end 294 | } 295 | end 296 | 297 | def send_data(data) 298 | unless connect? 299 | connect! 300 | end 301 | if @use_nonblock 302 | send_data_nonblock(data) 303 | else 304 | _, ws = IO.select([], [@con]) 305 | Thread.handle_interrupt(::Timeout::Error => :never) do 306 | # block timeout error during IO#write 307 | ws.first.write(data) 308 | end 309 | end 310 | #while true 311 | # puts "sending #{data.length} bytes" 312 | # if data.length > 32*1024 313 | # n = @con.syswrite(data[0..32*1024]) 314 | # else 315 | # n = @con.syswrite(data) 316 | # end 317 | # puts "sent #{n}" 318 | # if n >= data.bytesize 319 | # break 320 | # end 321 | # data = data[n..-1] 322 | #end 323 | end 324 | 325 | def send_data_nonblock(data) 326 | written = @con.write_nonblock(data) 327 | remaining = data.bytesize - written 328 | 329 | while remaining > 0 330 | len = @con.write_nonblock(data.byteslice(written, remaining)) 331 | remaining -= len 332 | written += len 333 | end 334 | 335 | written 336 | end 337 | 338 | def connect! 339 | create_socket! 340 | @con.sync = true 341 | @connect_error_history.clear 342 | @logged_reconnect_error = false 343 | rescue => e 344 | @connect_error_history << Time.now.to_i 345 | if @connect_error_history.size > RECONNECT_WAIT_MAX_COUNT 346 | @connect_error_history.shift 347 | end 348 | 349 | if @connect_error_history.size >= @log_reconnect_error_threshold && !@logged_reconnect_error 350 | log_reconnect_error 351 | @logged_reconnect_error = true 352 | end 353 | 354 | raise e 355 | end 356 | 357 | def call_buffer_overflow_handler(pending) 358 | if @buffer_overflow_handler 359 | @buffer_overflow_handler.call(pending) 360 | end 361 | rescue Exception => e 362 | @logger.error("FluentLogger: Can't call buffer overflow handler: #{$!}") 363 | end 364 | 365 | def log_reconnect_error 366 | @logger.error("FluentLogger: Can't connect to #{connection_string}(#{@connect_error_history.size} retried): #{$!}") 367 | end 368 | 369 | def set_last_error(e) 370 | # TODO: Check non GVL env 371 | @last_error[Thread.current.object_id] = e 372 | end 373 | 374 | def wait_writeable?(e) 375 | if e.instance_of?(IO::EAGAINWaitWritable) 376 | @wait_writeable 377 | else 378 | true 379 | end 380 | end 381 | end 382 | end 383 | end 384 | -------------------------------------------------------------------------------- /lib/fluent/logger/fluent_logger/cui.rb: -------------------------------------------------------------------------------- 1 | require 'fluent/logger' 2 | require 'optparse' 3 | 4 | module Fluent 5 | module Logger 6 | class FluentLogger 7 | module CUI 8 | def post(args) 9 | options = { 10 | :port => '24224', 11 | :host => 'localhost' 12 | } 13 | 14 | o = OptionParser.new 15 | o.version = Fluent::Logger::VERSION 16 | o.on('-t [tag (default nil)]') {|v| options[:tag] = v } 17 | o.on('-p [port (default 24224)]') {|v| options[:port] = v } 18 | o.on('-h [host (default localhost)]') {|v| options[:host] = v } 19 | o.on('-v [key=value]') {|v| 20 | key, value = v.split('=') 21 | (options[:data] ||= {})[key] = value 22 | } 23 | o.banner = 'Usage: fluent-post -t tag.foo.bar -v key1=value1 -v key2=value2' 24 | args = args.to_a 25 | args << '--help' if args.empty? 26 | o.parse(args) 27 | 28 | f = Fluent::Logger::FluentLogger.new(nil, { 29 | :host => options[:host], 30 | :port => options[:port] 31 | }) 32 | 33 | { 34 | :success => f.post(options[:tag], options[:data]), 35 | :data => options[:data] 36 | } 37 | end 38 | 39 | extend self 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/fluent/logger/level_fluent_logger.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Fluent 3 | # 4 | # Copyright (C) 2011 FURUHASHI Sadayuki 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | require 'msgpack' 19 | require 'socket' 20 | require 'monitor' 21 | require 'logger' 22 | require 'json' 23 | 24 | module Fluent 25 | module Logger 26 | class LevelFluentLogger < ::Logger 27 | 28 | def initialize(tag_prefix = nil, *args) 29 | @level = ::Logger::DEBUG 30 | @default_formatter = proc do |severity, datetime, progname, message| 31 | map = { level: severity } 32 | map[:message] = message if message 33 | map[:progname] = progname if progname 34 | map 35 | end 36 | @fluent_logger = FluentLogger.new(tag_prefix, *args) 37 | end 38 | 39 | def add(severity, message = nil, progname = nil, &block) 40 | severity ||= UNKNOWN 41 | if severity < @level 42 | return true 43 | end 44 | progname ||= @progname 45 | if message.nil? 46 | if block_given? 47 | message = yield 48 | else 49 | message = progname 50 | progname = @progname 51 | end 52 | end 53 | map = format_message(format_severity(severity), Time.now, progname, message) 54 | @fluent_logger.post(format_severity(severity).downcase, map) 55 | true 56 | end 57 | 58 | def close 59 | @fluent_logger.close 60 | end 61 | 62 | def reopen 63 | @fluent_logger.close 64 | # we do not call #connect! here because 1) FluentLogger#connect! is not a public method 65 | # 2) #post automatically connects if its connection is closed 66 | end 67 | 68 | def connect? 69 | @fluent_logger.connect? 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/fluent/logger/logger_base.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Fluent 3 | # 4 | # Copyright (C) 2011 FURUHASHI Sadayuki 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | module Fluent 19 | module Logger 20 | class LoggerBase 21 | def self.open(*args, &block) 22 | Fluent::Logger.open(self, *args, &block) 23 | end 24 | 25 | def post(tag, map) 26 | raise ArgumentError.new("Second argument must be a kind of Hash (#{tag}: #{map})") unless map.kind_of? Hash 27 | post_with_time(tag, map, Time.now) 28 | end 29 | 30 | #def post_with_time(tag, map) 31 | #end 32 | 33 | def close 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/fluent/logger/null_logger.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Fluent 3 | # 4 | # Copyright (C) 2011 FURUHASHI Sadayuki 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | module Fluent 19 | module Logger 20 | class NullLogger < LoggerBase 21 | def post_with_time(tag, map, time) 22 | false 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/fluent/logger/test_logger.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Fluent 3 | # 4 | # Copyright (C) 2011 FURUHASHI Sadayuki 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | module Fluent 19 | module Logger 20 | class TestLogger < LoggerBase 21 | def initialize(queue=[]) 22 | @queue = queue 23 | @max = 1024 24 | end 25 | 26 | attr_accessor :max 27 | attr_reader :queue 28 | 29 | def post_with_time(tag, map, time) 30 | while @queue.size > @max-1 31 | @queue.shift 32 | end 33 | (class< :bar}) 26 | io.rewind 27 | end 28 | its(:read) { should eq %Q!Sep 1 10:05:00 example: foo="bar"\n! } 29 | end 30 | end 31 | 32 | context "Filename output" do 33 | let(:path) { 34 | @tmp = Tempfile.new('fluent-logger') # ref instance var because Tempfile.close(true) check GC 35 | filename = @tmp.path 36 | @tmp.close(true) 37 | Pathname.new(filename) 38 | } 39 | let(:logger) { Fluent::Logger::ConsoleLogger.new(path.to_s) } 40 | 41 | subject { path } 42 | after { path.unlink } 43 | 44 | context "post and read" do 45 | before do 46 | logger.post('example', {:foo => :bar}) 47 | logger.close 48 | end 49 | its(:read) { should eq %Q!Sep 1 10:05:00 example: foo="bar"\n! } 50 | end 51 | 52 | context "reopen" do 53 | before do 54 | logger.post('example', {:foo => :baz}) 55 | logger.close 56 | logger.reopen! 57 | end 58 | its(:read) { should eq %Q!Sep 1 10:05:00 example: foo="baz"\n! } 59 | end 60 | end 61 | 62 | context "Invalid output" do 63 | it { 64 | expect { 65 | Fluent::Logger::ConsoleLogger.new(nil) 66 | }.to raise_error(RuntimeError) 67 | } 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/fluent_logger_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'spec_helper' 3 | require 'support/dummy_serverengine' 4 | require 'support/dummy_fluentd' 5 | 6 | require 'logger' 7 | require 'stringio' 8 | require 'fluent/logger/fluent_logger/cui' 9 | 10 | describe Fluent::Logger::FluentLogger do 11 | let(:fluentd) { 12 | DummyFluentd.new 13 | } 14 | 15 | let(:internal_logger) { 16 | @logger_io = StringIO.new 17 | ::Logger.new(@logger_io) 18 | } 19 | 20 | let(:logger_config) { 21 | { 22 | :host => 'localhost', 23 | :port => fluentd.port, 24 | :logger => internal_logger, 25 | :buffer_overflow_handler => buffer_overflow_handler 26 | } 27 | } 28 | 29 | let(:logger) { 30 | Fluent::Logger::FluentLogger.new('logger-test', logger_config) 31 | } 32 | 33 | let(:logger_with_nanosec) { 34 | Fluent::Logger::FluentLogger.new('logger-test', logger_config.merge(:nanosecond_precision => true)) 35 | } 36 | 37 | let(:buffer_overflow_handler) { nil } 38 | 39 | let(:logger_io) { 40 | @logger_io 41 | } 42 | 43 | context "running fluentd" do 44 | before(:all) do 45 | @serverengine = DummyServerengine.new 46 | @serverengine.startup 47 | end 48 | 49 | before(:each) do 50 | fluentd.startup 51 | end 52 | 53 | after(:each) do 54 | fluentd.shutdown 55 | end 56 | 57 | after(:all) do 58 | @serverengine.shutdown 59 | end 60 | 61 | describe('testing interaction of use_nonblock and wait_writeable') do 62 | before(:example) do 63 | allow_any_instance_of(TCPSocket).to receive(:write_nonblock).and_raise(IO::EAGAINWaitWritable) 64 | allow_any_instance_of(TCPSocket).to receive(:write) { |_, buf| buf.size } 65 | end 66 | 67 | context('use_nonblock is false') do 68 | let(:block_config) { logger_config.merge(use_nonblock: false) } 69 | 70 | it('post returns true when wait_writeable is false') { 71 | cfg = block_config.merge(wait_writeable: false) 72 | l = Fluent::Logger::FluentLogger.new('logger-test', cfg) 73 | expect(l.post('hello', foo: 'bar')).to eq true 74 | } 75 | 76 | it('post returns true when wait_writeable is true') { 77 | cfg = block_config.merge(wait_writeable: true) 78 | l = Fluent::Logger::FluentLogger.new('logger-test', cfg) 79 | expect(l.post('hello', {foo: 'bar'})).to eq true 80 | } 81 | end 82 | 83 | context('use_nonblock is true') do 84 | let(:nonblock_config) { logger_config.merge(use_nonblock: true) } 85 | 86 | it('post raises IO::EAGAINWaitWritable when wait_writeable is false') { 87 | cfg = nonblock_config.merge(wait_writeable: false) 88 | l = Fluent::Logger::FluentLogger.new('logger-test', cfg) 89 | expect { l.post('hello', foo: 'bar') }.to raise_error(IO::EAGAINWaitWritable) 90 | } 91 | 92 | it('post returns false when wait_writeable is true') { 93 | cfg = nonblock_config.merge(wait_writeable: true) 94 | l = Fluent::Logger::FluentLogger.new('logger-test', cfg) 95 | expect(l.post('hello', {foo: 'bar'})).to eq false 96 | } 97 | 98 | context 'when write_nonblock returns the size less than received data' do 99 | before do 100 | allow_any_instance_of(TCPSocket).to receive(:write_nonblock).and_return(1) # write 1 bytes per call 101 | end 102 | 103 | it 'buffering data and flush at closed time' do 104 | logger = Fluent::Logger::FluentLogger.new('logger-test', nonblock_config) 105 | expect(logger.post('hello', foo: 'bar')).to eq(true) 106 | expect(logger.pending_bytesize).to eq(0) 107 | end 108 | end 109 | end 110 | end 111 | 112 | context('Post by CUI') do 113 | it('post') { 114 | args = %W(-h localhost -p #{fluentd.port} -t logger-test.tag -v a=b -v foo=bar) 115 | Fluent::Logger::FluentLogger::CUI.post(args) 116 | fluentd.wait_transfer 117 | expect(fluentd.queue.last).to eq ['logger-test.tag', {'a' => 'b', 'foo' => 'bar'}] 118 | } 119 | end 120 | 121 | context('post') do 122 | it ('success') { 123 | expect(logger.pending_bytesize).to eq 0 124 | expect(logger.post('tag', {'a' => 'b'})).to be true 125 | fluentd.wait_transfer 126 | expect(fluentd.queue.last).to eq ['logger-test.tag', {'a' => 'b'}] 127 | expect(logger.pending_bytesize).to eq 0 128 | } 129 | 130 | if defined?(Fluent::EventTime) 131 | it ('success with nanosecond') { 132 | expect(logger_with_nanosec.pending_bytesize).to eq 0 133 | expect(logger_with_nanosec.post('tag', {'a' => 'b'})).to be true 134 | fluentd.wait_transfer 135 | expect(fluentd.queue.last).to eq ['logger-test.tag', {'a' => 'b'}] 136 | expect(fluentd.output.emits.first[1]).to be_a_kind_of(Fluent::EventTime) 137 | } 138 | end 139 | 140 | context 'when the message has object which does not have #to_msgpack method' do 141 | it 'success with nanosecond' do 142 | expect(logger_with_nanosec.pending_bytesize).to eq(0) 143 | expect(logger_with_nanosec.post('tag', 'a' => Errno::ETIMEDOUT)).to eq(true) 144 | fluentd.wait_transfer 145 | expect(fluentd.queue.last).to eq(['logger-test.tag', { 'a' => 'Errno::ETIMEDOUT' }]) 146 | expect(logger_with_nanosec.pending_bytesize).to eq(0) 147 | end 148 | end 149 | 150 | it ('close after post') { 151 | expect(logger).to be_connect 152 | logger.close 153 | expect(logger).not_to be_connect 154 | 155 | logger.post('tag', {'b' => 'c'}) 156 | expect(logger).to be_connect 157 | fluentd.wait_transfer 158 | expect(fluentd.queue.last).to eq ['logger-test.tag', {'b' => 'c'}] 159 | expect(logger.pending_bytesize).to eq 0 160 | } 161 | 162 | it ('large data') { 163 | data = {'a' => ('b' * 1000000)} 164 | logger.post('tag', data) 165 | fluentd.wait_transfer 166 | expect(fluentd.queue.last).to eq ['logger-test.tag', data] 167 | } 168 | 169 | it ('msgpack unsupport data') { 170 | data = { 171 | 'time' => Time.utc(2008, 9, 1, 10, 5, 0), 172 | 'object' => Object.new, 173 | 'proc' => proc { 1 }, 174 | } 175 | logger.post('tag', data) 176 | fluentd.wait_transfer 177 | logger_data = fluentd.queue.last.last 178 | expect(logger_data['time']).to eq '2008-09-01 10:05:00 UTC' 179 | expect(logger_data['proc']).to be_truthy 180 | expect(logger_data['object']).to be_truthy 181 | } 182 | 183 | it ('msgpack unsupport data and support data') { 184 | logger.post('tag', {'time' => Time.utc(2008, 9, 1, 10, 5, 0)}) 185 | logger.post('tag', {'time' => '2008-09-01 10:05:00 UTC'}) 186 | 187 | fluentd.wait_transfer 188 | 189 | logger_data1 = fluentd.queue.first.last 190 | expect(logger_data1['time']).to eq '2008-09-01 10:05:00 UTC' 191 | 192 | logger_data2 = fluentd.queue.last.last 193 | expect(logger_data2['time']).to eq '2008-09-01 10:05:00 UTC' 194 | } 195 | 196 | it ('msgpack and JSON unsupport data') { 197 | data = { 198 | 'time' => Time.utc(2008, 9, 1, 10, 5, 0), 199 | 'object' => Object.new, 200 | 'proc' => proc { 1 }, 201 | 'NaN' => (0.0/0.0) # JSON don't convert 202 | } 203 | logger.post('tag', data) 204 | fluentd.wait_transfer 205 | expect(fluentd.queue.last).to be_nil 206 | logger_io.rewind 207 | logger_io.read =~ /FluentLogger: Can't convert to msgpack:/ 208 | 209 | logger.post('tag', { 'a' => 'b' }) 210 | fluentd.wait_transfer 211 | expect(fluentd.queue.last).to eq ['logger-test.tag', { 'a' => 'b' }] 212 | } 213 | 214 | it ('should raise an error when second argument is non hash object') { 215 | data = 'FooBar' 216 | expect { 217 | logger.post('tag', data) 218 | }.to raise_error(ArgumentError) 219 | 220 | data = nil 221 | expect { 222 | logger.post('tag', data) 223 | }.to raise_error(ArgumentError) 224 | 225 | fluentd.wait_transfer # ensure the fluentd accepted the connection 226 | } 227 | end 228 | 229 | context "initializer" do 230 | it "backward compatible" do 231 | fluent_logger = Fluent::Logger::FluentLogger.new('logger-test', 'localhost', fluentd.port) 232 | host, port = fluent_logger.instance_eval { [@host, @port] } 233 | expect(host).to eq 'localhost' 234 | expect(port).to eq fluentd.port 235 | fluentd.wait_transfer # ensure the fluentd accepted the connection 236 | end 237 | 238 | it "hash argument" do 239 | fluent_logger = Fluent::Logger::FluentLogger.new('logger-test', { 240 | :host => 'localhost', 241 | :port => fluentd.port 242 | }) 243 | 244 | host, port = fluent_logger.instance_eval { [@host, @port] } 245 | expect(host).to eq 'localhost' 246 | expect(port).to eq fluentd.port 247 | fluentd.wait_transfer # ensure the fluentd accepted the connection 248 | end 249 | end 250 | end 251 | 252 | context "not running fluentd" do 253 | context('fluent logger interface') do 254 | it ('post & close') { 255 | expect(logger.pending_bytesize).to eq 0 256 | expect(logger.post('tag', {'a' => 'b'})).to be false 257 | fluentd.wait_transfer # even if wait 258 | expect(fluentd.queue.last).to be_nil 259 | expect(logger.pending_bytesize).to be > 0 260 | logger.close 261 | logger_io.rewind 262 | log = logger_io.read 263 | expect(log).to match /Failed to connect/ 264 | expect(log).to match /Can't send logs to/ 265 | } 266 | 267 | it ('post limit over') do 268 | logger.limit = 100 269 | logger.post('tag', {'a' => 'b'}) 270 | fluentd.wait_transfer # even if wait 271 | expect(fluentd.queue.last).to be_nil 272 | 273 | logger_io.rewind 274 | expect(logger_io.read).not_to match /Can't send logs to/ 275 | 276 | logger.post('tag', {'a' => ('c' * 1000)}) 277 | logger_io.rewind 278 | expect(logger_io.read).to match /Can't send logs to/ 279 | end 280 | 281 | it ('log connect error once') do 282 | allow_any_instance_of(Fluent::Logger::FluentLogger).to receive(:suppress_sec).and_return(-1) 283 | logger.log_reconnect_error_threshold = 1 284 | expect_any_instance_of(Fluent::Logger::FluentLogger).to receive(:log_reconnect_error).once.and_call_original 285 | 286 | logger.post('tag', {'a' => 'b'}) 287 | fluentd.wait_transfer # even if wait 288 | logger.post('tag', {'a' => 'b'}) 289 | fluentd.wait_transfer # even if wait 290 | logger_io.rewind 291 | expect(logger_io.read).to match /Failed to connect/ 292 | end 293 | end 294 | 295 | context "when a buffer overflow handler is provided" do 296 | class BufferOverflowHandler 297 | attr_accessor :buffer 298 | 299 | def flush(messages) 300 | @buffer ||= [] 301 | MessagePack::Unpacker.new.feed_each(messages) do |msg| 302 | @buffer << msg 303 | end 304 | end 305 | end 306 | 307 | let(:handler) { BufferOverflowHandler.new } 308 | let(:buffer_overflow_handler) { Proc.new { |messages| handler.flush(messages) } } 309 | 310 | it ('post limit over') do 311 | expect(logger.pending_bytesize).to eq 0 312 | logger.limit = 100 313 | event_1 = {'a' => 'b'} 314 | logger.post('tag', event_1) 315 | fluentd.wait_transfer # even if wait 316 | expect(fluentd.queue.last).to be(nil) 317 | expect(logger.pending_bytesize).to be > 0 318 | 319 | logger_io.rewind 320 | expect(logger_io.read).not_to match(/Can't send logs to/) 321 | 322 | event_2 = {'a' => ('c' * 1000)} 323 | logger.post('tag', event_2) 324 | logger_io.rewind 325 | expect(logger.pending_bytesize).to eq 0 326 | expect(logger_io.read).to match(/Can't send logs to/) 327 | 328 | buffer = handler.buffer 329 | 330 | expect(buffer[0][0]).to eq('logger-test.tag') 331 | expect(buffer[0][1].to_s).to match(/\d{10}/) 332 | expect(buffer[0][2]).to eq(event_1) 333 | 334 | expect(buffer[1][0]).to eq('logger-test.tag') 335 | expect(buffer[1][1].to_s).to match(/\d{10}/) 336 | expect(buffer[1][2]).to eq(event_2) 337 | end 338 | end 339 | end 340 | 341 | context "using socket_path" do 342 | 343 | let(:socket_logger) { 344 | @logger_io = StringIO.new 345 | logger = ::Logger.new(@logger_io) 346 | Fluent::Logger::FluentLogger.new('logger-test', { 347 | :socket_path => fluentd.socket_path, 348 | :logger => logger, 349 | :buffer_overflow_handler => buffer_overflow_handler 350 | }) 351 | } 352 | 353 | context "running fluentd" do 354 | before(:all) do 355 | @serverengine = DummyServerengine.new 356 | @serverengine.startup 357 | end 358 | 359 | before(:each) do 360 | fluentd.socket_startup 361 | end 362 | 363 | after(:each) do 364 | fluentd.shutdown 365 | end 366 | 367 | after(:all) do 368 | @serverengine.shutdown 369 | end 370 | 371 | context('post') do 372 | it ('success') { 373 | expect(socket_logger.post('tag', {'b' => 'a'})).to be true 374 | fluentd.wait_transfer 375 | expect(fluentd.queue.last).to eq ['logger-test.tag', {'b' => 'a'}] 376 | } 377 | end 378 | end 379 | end 380 | 381 | context "running fluentd with TLS" do 382 | before(:all) do 383 | @serverengine = DummyServerengine.new 384 | @serverengine.startup 385 | end 386 | 387 | before(:each) do 388 | fluentd.startup(true) 389 | # Make sure to wait TLS server since preparing it takes longer time than 390 | # non-TLS so that it might be too early especially on CI environment 391 | 3.times do 392 | begin 393 | TCPSocket.open('localhost', fluentd.port) {} 394 | break 395 | rescue 396 | fluentd.wait_transfer 397 | end 398 | end 399 | end 400 | 401 | after(:each) do 402 | fluentd.shutdown 403 | end 404 | 405 | after(:all) do 406 | @serverengine.shutdown 407 | end 408 | 409 | let(:logger_config) { 410 | { 411 | :host => 'localhost', 412 | :port => fluentd.port, 413 | :logger => internal_logger, 414 | :buffer_overflow_handler => buffer_overflow_handler, 415 | :tls_options => {:insecure => true} 416 | } 417 | } 418 | 419 | context('post') do 420 | it ('success') { 421 | expect(logger.pending_bytesize).to eq 0 422 | expect(logger.post('tag', {'a' => 'b'})).to be true 423 | fluentd.wait_transfer 424 | expect(fluentd.queue.last).to eq ['logger-test.tag', {'a' => 'b'}] 425 | expect(logger.pending_bytesize).to eq 0 426 | } 427 | 428 | it ('success with nanosecond') { 429 | expect(logger_with_nanosec.pending_bytesize).to eq 0 430 | expect(logger_with_nanosec.post('tag', {'a' => 'b'})).to be true 431 | fluentd.wait_transfer 432 | expect(fluentd.queue.last).to eq ['logger-test.tag', {'a' => 'b'}] 433 | expect(fluentd.output.emits.first[1]).to be_a_kind_of(Fluent::EventTime) 434 | } 435 | end 436 | end 437 | end 438 | -------------------------------------------------------------------------------- /spec/level_fluent_logger_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'spec_helper' 3 | require 'support/dummy_serverengine' 4 | require 'support/dummy_fluentd' 5 | 6 | require 'logger' 7 | require 'stringio' 8 | 9 | describe Fluent::Logger::FluentLogger do 10 | let(:fluentd) { 11 | DummyFluentd.new 12 | } 13 | 14 | let(:level_logger) { 15 | @logger_io = StringIO.new 16 | logger = ::Logger.new(@logger_io) 17 | Fluent::Logger::LevelFluentLogger.new('logger-test', { 18 | :host => 'localhost', 19 | :port => fluentd.port, 20 | :logger => logger, 21 | :buffer_overflow_handler => buffer_overflow_handler 22 | }) 23 | } 24 | 25 | let(:buffer_overflow_handler) { nil } 26 | 27 | let(:logger_io) { 28 | @logger_io 29 | } 30 | 31 | context "running fluentd" do 32 | 33 | before(:all) do 34 | @serverengine = DummyServerengine.new 35 | @serverengine.startup 36 | end 37 | 38 | before(:each) do 39 | fluentd.startup 40 | end 41 | 42 | after(:each) do 43 | fluentd.shutdown 44 | end 45 | 46 | after(:all) do 47 | @serverengine.shutdown 48 | end 49 | 50 | context('::Logger compatible methods') do 51 | it ('initialize default value') { 52 | level_fluent_logger = Fluent::Logger::LevelFluentLogger.new('logger-test', { 53 | :host => 'localhost', 54 | :port => fluentd.port, 55 | :logger => ::Logger.new(StringIO.new), 56 | :buffer_overflow_handler => buffer_overflow_handler 57 | }) 58 | expect(level_fluent_logger.level).to eq 0 59 | expect(level_fluent_logger.progname).to be_nil 60 | fluentd.wait_transfer # ensure the fluentd accepted the connection 61 | } 62 | 63 | it ('close') { 64 | expect(level_logger).to be_connect 65 | level_logger.close 66 | expect(level_logger).not_to be_connect 67 | fluentd.wait_transfer # ensure the fluentd accepted the connection 68 | } 69 | 70 | it ('reopen') { 71 | expect(level_logger).to be_connect 72 | level_logger.reopen 73 | expect(level_logger).not_to be_connect 74 | expect(level_logger.info('logger reopen test')).to be true 75 | fluentd.wait_transfer # ensure the fluentd accepted the connection 76 | } 77 | end 78 | 79 | context('add with level') do 80 | it ('add progname') { 81 | expect(level_logger.info('some_application'){ 'some application running' }).to be true 82 | fluentd.wait_transfer 83 | expect(fluentd.queue.last).to eq ['logger-test.info', {'level' => 'INFO', 'message' => 'some application running', 'progname' => 'some_application' }] 84 | } 85 | 86 | it ('send log debug') { 87 | expect(level_logger.debug('some_application')).to be true 88 | fluentd.wait_transfer 89 | expect(fluentd.queue.last).to eq ['logger-test.debug', {'level' => 'DEBUG', 'message' => 'some_application' }] 90 | } 91 | 92 | it ('send log info') { 93 | expect(level_logger.info('some_application')).to be true 94 | fluentd.wait_transfer 95 | expect(fluentd.queue.last).to eq ['logger-test.info', {'level' => 'INFO', 'message' => 'some_application' }] 96 | } 97 | 98 | it ('send log warn') { 99 | expect(level_logger.warn('some_application')).to be true 100 | fluentd.wait_transfer 101 | expect(fluentd.queue.last).to eq ['logger-test.warn', {'level' => 'WARN', 'message' => 'some_application' }] 102 | } 103 | 104 | it ('send log error') { 105 | expect(level_logger.error('some_application')).to be true 106 | fluentd.wait_transfer 107 | expect(fluentd.queue.last).to eq ['logger-test.error', {'level' => 'ERROR', 'message' => 'some_application' }] 108 | } 109 | 110 | it ('send log fatal') { 111 | expect(level_logger.fatal('some_application')).to be true 112 | fluentd.wait_transfer 113 | expect(fluentd.queue.last).to eq ['logger-test.fatal', {'level' => 'FATAL', 'message' => 'some_application' }] 114 | } 115 | 116 | it ('not send log debug') { 117 | level_logger.level = ::Logger::FATAL 118 | 119 | expect(level_logger.debug('some_application')).to be true 120 | fluentd.wait_transfer 121 | expect(fluentd.queue).to eq [] 122 | } 123 | 124 | it ('not send log info') { 125 | level_logger.level = ::Logger::FATAL 126 | 127 | expect(level_logger.info('some_application')).to be true 128 | fluentd.wait_transfer 129 | expect(fluentd.queue).to eq [] 130 | } 131 | 132 | it ('not send log warn') { 133 | level_logger.level = ::Logger::FATAL 134 | 135 | expect(level_logger.warn('some_application')).to be true 136 | fluentd.wait_transfer 137 | expect(fluentd.queue).to eq [] 138 | } 139 | 140 | it ('not send log error') { 141 | level_logger.level = ::Logger::FATAL 142 | 143 | expect(level_logger.error('some_application')).to be true 144 | fluentd.wait_transfer 145 | expect(fluentd.queue).to eq [] 146 | } 147 | 148 | it ('define formatter') { 149 | level_logger.level = ::Logger::DEBUG 150 | level_logger.formatter = proc do |severity, datetime, progname, message| 151 | map = { level: severity } 152 | map[:message] = message if message 153 | map[:progname] = progname if progname 154 | map[:stage] = "development" 155 | map[:service_name] = "some service" 156 | map 157 | end 158 | 159 | expect(level_logger.error('some_application')).to be true 160 | fluentd.wait_transfer 161 | expect(fluentd.queue.last).to eq [ 162 | 'logger-test.error', 163 | {'level' => 'ERROR', 'message' => 'some_application', 'stage' => 'development', 'service_name' => 'some service'} 164 | ] 165 | } 166 | end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /spec/logger_base_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'spec_helper' 3 | 4 | describe Fluent::Logger::LoggerBase do 5 | context "subclass" do 6 | let(:subclass) { Class.new(Fluent::Logger::LoggerBase) } 7 | let(:other_subclass) { Class.new(Fluent::Logger::LoggerBase) } 8 | 9 | describe ".open" do 10 | subject(:open) { subclass.open } 11 | 12 | it { should be_kind_of(Fluent::Logger::LoggerBase) } 13 | 14 | it "changes Fluent::Logger.default" do 15 | subclass.open 16 | expect(Fluent::Logger.default).to be_kind_of(subclass) 17 | 18 | other_subclass.open 19 | expect(Fluent::Logger.default).to be_kind_of(other_subclass) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/logger_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'spec_helper' 3 | require 'stringio' 4 | 5 | describe Fluent::Logger do 6 | context "default logger" do 7 | let(:test_logger) { 8 | Fluent::Logger::TestLogger.new 9 | } 10 | before(:each) do 11 | Fluent::Logger.default = test_logger 12 | end 13 | 14 | it('post') { 15 | expect(test_logger).to receive(:post).with('tag1', {:foo => :bar}) 16 | #allow_any_instance_of(test_logger).to receive(:post).with('tag1', {:foo => :bar}) 17 | Fluent::Logger.post('tag1', {:foo => :bar}) 18 | } 19 | 20 | it('close') { 21 | expect(test_logger).to receive(:close) 22 | Fluent::Logger.close 23 | } 24 | 25 | it('open') { 26 | expect(test_logger).to receive(:close) 27 | klass = Class.new(Fluent::Logger::LoggerBase) 28 | fluent_logger_logger_io = StringIO.new 29 | Fluent::Logger.open('tag-prefix', { 30 | :logger => ::Logger.new(fluent_logger_logger_io) 31 | }) 32 | expect(Fluent::Logger.default.kind_of?(Fluent::Logger::FluentLogger)).to be true 33 | } 34 | 35 | it('open with BaseLogger class') { 36 | expect(test_logger).to receive(:close) 37 | klass = Class.new(Fluent::Logger::LoggerBase) 38 | Fluent::Logger.open(klass) 39 | expect(Fluent::Logger.default.class).to be klass 40 | } 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/null_logger_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'spec_helper' 3 | 4 | describe Fluent::Logger::NullLogger do 5 | context "logger method" do 6 | let(:logger) { Fluent::Logger::NullLogger.new } 7 | 8 | context "post" do 9 | it('false') { 10 | expect(logger.post('tag1', {:foo => :bar})).to be false 11 | expect(logger.post('tag2', {:foo => :baz})).to be false 12 | } 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/plugin/out_test.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Fluentd 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'fluent/output' 18 | 19 | module Fluent 20 | class TestOutput < Output 21 | Plugin.register_output('test', self) 22 | 23 | def initialize 24 | super 25 | 26 | @emit_streams = [] 27 | @name = nil 28 | end 29 | 30 | attr_reader :emit_streams, :name 31 | 32 | def emits 33 | all = [] 34 | @emit_streams.each {|tag,events| 35 | events.each {|time,record| 36 | all << [tag, time, record] 37 | } 38 | } 39 | all 40 | end 41 | 42 | def events 43 | all = [] 44 | @emit_streams.each {|tag,events| 45 | all.concat events 46 | } 47 | all 48 | end 49 | 50 | def records 51 | all = [] 52 | @emit_streams.each {|tag,events| 53 | events.each {|time,record| 54 | all << record 55 | } 56 | } 57 | all 58 | end 59 | 60 | def configure(conf) 61 | super 62 | 63 | if name = conf['name'] 64 | @name = name 65 | end 66 | end 67 | 68 | def start 69 | super 70 | end 71 | 72 | def shutdown 73 | super 74 | end 75 | 76 | def emit(tag, es, chain) 77 | chain.next 78 | @emit_streams << [tag, es.to_a] 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 3 | 4 | if ENV['SIMPLE_COV'] 5 | require 'simplecov' 6 | require 'simplecov-vim/formatter' 7 | class SimpleCov::Formatter::MergedFormatter 8 | def format(result) 9 | SimpleCov::Formatter::HTMLFormatter.new.format(result) 10 | SimpleCov::Formatter::VimFormatter.new.format(result) 11 | end 12 | end 13 | SimpleCov.start do 14 | formatter SimpleCov::Formatter::MergedFormatter 15 | add_filter 'spec/' 16 | add_filter 'test/' 17 | add_filter 'pkg/' 18 | add_filter 'vendor/' 19 | end 20 | end 21 | 22 | require 'rspec' 23 | require 'rspec/its' 24 | 25 | # Requires supporting files with custom matchers and macros, etc, 26 | # in ./support/ and its subdirectories. 27 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} 28 | 29 | require 'fluent-logger' 30 | 31 | RSpec.configure do |config| 32 | config.filter_run :focus => true 33 | config.run_all_when_everything_filtered = true 34 | 35 | config.mock_with :rspec 36 | end 37 | -------------------------------------------------------------------------------- /spec/support/dummy_fluentd.rb: -------------------------------------------------------------------------------- 1 | require 'fluent/load' 2 | require 'fluent/test' 3 | require 'socket' 4 | require 'plugin/out_test' 5 | require 'stringio' 6 | 7 | class DummyFluentd 8 | def initialize 9 | output.emits.clear rescue nil 10 | end 11 | 12 | WAIT = ENV['WAIT'] ? ENV['WAIT'].to_f : 0.3 13 | SOCKET_PATH = ENV['SOCKET_PATH'] || "/tmp/dummy_fluent.sock" 14 | 15 | def wait_transfer 16 | sleep WAIT 17 | end 18 | 19 | def port 20 | return @port if @port 21 | @port = 60001 22 | loop do 23 | begin 24 | TCPServer.open('localhost', @port).close 25 | break 26 | rescue Errno::EADDRINUSE 27 | @port += 1 28 | end 29 | end 30 | @port 31 | end 32 | 33 | def socket_path 34 | SOCKET_PATH 35 | end 36 | 37 | def output 38 | sleep 0.0001 # next tick 39 | if Fluent::Engine.respond_to?(:match) 40 | Fluent::Engine.match('logger-test').output 41 | else 42 | Fluent::Engine.root_agent.event_router.match('logger-test') 43 | end 44 | end 45 | 46 | def queue 47 | queue = [] 48 | output.emits.each { |tag, time, record| 49 | queue << [tag, record] 50 | } 51 | queue 52 | end 53 | 54 | def startup(with_tls = false) 55 | if with_tls 56 | config = Fluent::Config.parse(< 58 | type forward 59 | port #{port} 60 | 61 | insecure true 62 | 63 | 64 | 65 | type test 66 | 67 | EOF 68 | else 69 | config = Fluent::Config.parse(< 71 | type forward 72 | port #{port} 73 | 74 | 75 | type test 76 | 77 | EOF 78 | end 79 | 80 | Fluent::Test.setup 81 | Fluent::Engine.run_configure(config) 82 | @coolio_default_loop = nil 83 | @thread = Thread.new { 84 | @coolio_default_loop = Coolio::Loop.default 85 | Fluent::Engine.run 86 | } 87 | wait_transfer 88 | end 89 | 90 | def socket_startup 91 | config = Fluent::Config.parse(< 93 | type unix 94 | path #{socket_path} 95 | 96 | 97 | type test 98 | 99 | EOF 100 | Fluent::Test.setup 101 | Fluent::Engine.run_configure(config) 102 | @coolio_default_loop = nil 103 | @thread = Thread.new { 104 | @coolio_default_loop = Coolio::Loop.default 105 | Fluent::Engine.run 106 | } 107 | wait_transfer 108 | end 109 | 110 | def shutdown 111 | @coolio_default_loop.stop rescue nil 112 | begin 113 | Fluent::Engine.stop 114 | rescue => e 115 | # for v0.12, calling stop may cause "loop not running" by internal default loop 116 | if e.message == "loop not running" 117 | Fluent::Engine.send :shutdown 118 | end 119 | end 120 | @thread.join 121 | @coolio_default_loop = @thread = nil 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /spec/support/dummy_serverengine.rb: -------------------------------------------------------------------------------- 1 | class DummyServerengine 2 | def initialize 3 | end 4 | 5 | def startup 6 | @server = nil 7 | if defined?(ServerEngine) # for v0.14. in_forward requires socket manager server 8 | socket_manager_path = ServerEngine::SocketManager::Server.generate_path 9 | @server = ServerEngine::SocketManager::Server.open(socket_manager_path) 10 | ENV['SERVERENGINE_SOCKETMANAGER_PATH'] = socket_manager_path.to_s 11 | end 12 | @server 13 | end 14 | 15 | def shutdown 16 | @server.close if @server 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/support/timecop.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'timecop' 3 | 4 | RSpec.configure do |config| 5 | config.after(:each) do 6 | Timecop.return 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/test_logger_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'spec_helper' 3 | 4 | describe Fluent::Logger::TestLogger do 5 | context "logger method" do 6 | let(:logger) { Fluent::Logger::TestLogger.new } 7 | subject { logger.queue } 8 | 9 | context "post" do 10 | before do 11 | logger.post('tag1', {:foo => :bar}) 12 | logger.post('tag2', {:foo => :baz}) 13 | end 14 | 15 | its(:first) { should == {:foo => :bar } } 16 | its(:last) { should == {:foo => :baz } } 17 | its("first.tag") { should == "tag1" } 18 | its("last.tag") { should == "tag2" } 19 | 20 | it("tag_queue") { 21 | expect(logger.tag_queue('tag1').size).to eq 1 22 | expect(logger.tag_queue('tag2').size).to eq 1 23 | expect(logger.tag_queue('tag3').size).to eq 0 24 | } 25 | end 26 | 27 | context "max" do 28 | before do 29 | logger.max = 2 30 | 10.times {|i| logger.post(i.to_s, {}) } 31 | end 32 | 33 | its(:size) { should == 2 } 34 | its("last.tag") { should == "9" } 35 | end 36 | end 37 | end 38 | --------------------------------------------------------------------------------