├── .rspec ├── VERSION ├── AUTHORS ├── .gitignore ├── lib ├── fluent-logger.rb └── fluent │ ├── logger │ ├── version.rb │ ├── null_logger.rb │ ├── text_logger.rb │ ├── logger_base.rb │ ├── fluent_logger │ │ └── cui.rb │ ├── test_logger.rb │ ├── console_logger.rb │ └── fluent_logger.rb │ └── logger.rb ├── .gitmodules ├── spec ├── support │ └── timecop.rb ├── logger_base_spec.rb ├── null_logger_spec.rb ├── spec_helper.rb ├── test_logger_spec.rb ├── logger_spec.rb ├── console_logger_spec.rb └── fluent_logger_spec.rb ├── .travis.yml ├── Gemfile ├── bin └── fluent-post ├── Rakefile ├── COPYING ├── README.rdoc ├── fluent-logger.gemspec └── ChangeLog /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.4.6 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | FURUHASHI Sadayuki 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | pkg/* 3 | coverage/* 4 | coverage.vim 5 | -------------------------------------------------------------------------------- /lib/fluent-logger.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'fluent', 'logger') 2 | -------------------------------------------------------------------------------- /lib/fluent/logger/version.rb: -------------------------------------------------------------------------------- 1 | module Fluent 2 | module Logger 3 | 4 | VERSION = '0.4.6' 5 | 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/fluentd"] 2 | path = vendor/fluentd 3 | url = git://github.com/fluent/fluentd.git 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.8.7 3 | - 1.9.2 4 | - 1.9.3 5 | - 2.0.0 6 | - ree 7 | 8 | before_install: git submodule update -i 9 | 10 | script: bundle exec rake spec 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | 2 | source 'https://rubygems.org/' 3 | 4 | gemspec 5 | 6 | gem "simplecov", :require => false 7 | gem "simplecov-vim" 8 | 9 | gem 'yajl-ruby' # FIXME ruby 1.8.7 don't work add_dependency('yajl-ruby') 10 | gem "fluentd", :path => 'vendor/fluentd' if RUBY_VERSION >= "1.9.2" 11 | 12 | -------------------------------------------------------------------------------- /spec/logger_base_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'spec_helper' 3 | 4 | describe Fluent::Logger::LoggerBase do 5 | context "subclass" do 6 | subject { Class.new(Fluent::Logger::LoggerBase) } 7 | its(:open) { 8 | should be_kind_of(Fluent::Logger::LoggerBase) 9 | } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | logger.post('tag1', {:foo => :bar}).should == false 11 | logger.post('tag2', {:foo => :baz}).should == false 12 | } 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 21 | class NullLogger < LoggerBase 22 | def post_with_time(tag, map, time) 23 | false 24 | end 25 | end 26 | 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /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 | # Requires supporting files with custom matchers and macros, etc, 23 | # in ./support/ and its subdirectories. 24 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} 25 | 26 | require 'fluent-logger' 27 | 28 | RSpec.configure do |config| 29 | config.filter_run :focus => true 30 | config.run_all_when_everything_filtered = true 31 | 32 | config.mock_with :rspec 33 | end 34 | -------------------------------------------------------------------------------- /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 | logger.tag_queue('tag1').size.should == 1 22 | logger.tag_queue('tag2').size.should == 1 23 | logger.tag_queue('tag3').size.should == 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 | -------------------------------------------------------------------------------- /lib/fluent/logger/text_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 | 21 | class TextLogger < LoggerBase 22 | def initialize 23 | require 'yajl' 24 | @time_format = "%b %e %H:%M:%S" 25 | end 26 | 27 | def post_with_time(tag, map, time) 28 | a = [time.strftime(@time_format), " ", tag, ":"] 29 | map.each_pair {|k,v| 30 | a << " #{k}=" 31 | a << Yajl::Encoder.encode(v) 32 | } 33 | post_text a.join 34 | true 35 | end 36 | end 37 | 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /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 | 21 | class LoggerBase 22 | def self.open(*args, &block) 23 | Fluent::Logger.open(self, *args, &block) 24 | end 25 | 26 | def post(tag, map) 27 | raise ArgumentError.new("Second argument should kind of Hash (tag: #{map})") unless map.kind_of? Hash 28 | post_with_time(tag, map, Time.now) 29 | end 30 | 31 | #def post_with_time(tag, map) 32 | #end 33 | 34 | def close 35 | end 36 | end 37 | 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/fluent/logger/fluent_logger/cui.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'fluent/logger' 3 | require 'optparse' 4 | 5 | module Fluent 6 | module Logger 7 | class FluentLogger 8 | 9 | module CUI 10 | def post(args) 11 | options = { 12 | :port => '24224', 13 | :host => 'localhost' 14 | } 15 | 16 | o = OptionParser.new 17 | o.version = Fluent::Logger::VERSION 18 | o.on('-t [tag (default nil)]') {|v| options[:tag] = v } 19 | o.on('-p [port (default 24224)]') {|v| options[:port] = v } 20 | o.on('-h [host (default localhost)]') {|v| options[:host] = v } 21 | o.on('-v [key=value]') {|v| 22 | key, value = v.split('=') 23 | (options[:data] ||= {})[key] = value 24 | } 25 | o.banner = 'Usage: fluent-post -t tag.foo.bar -v key1=value1 -v key2=value2' 26 | args = args.to_a 27 | args << '--help' if args.empty? 28 | o.parse(args) 29 | 30 | f = Fluent::Logger::FluentLogger.new(nil, { 31 | :host => options[:host], 32 | :port => options[:port] 33 | }) 34 | 35 | { 36 | :success => f.post(options[:tag], options[:data]), 37 | :data => options[:data] 38 | } 39 | end 40 | 41 | extend self 42 | end 43 | 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /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 | test_logger.should_receive(:post).with('tag1', {:foo => :bar}) 16 | Fluent::Logger.post('tag1', {:foo => :bar}) 17 | } 18 | 19 | it('close') { 20 | test_logger.should_receive(:close) 21 | Fluent::Logger.close 22 | } 23 | 24 | it('open') { 25 | test_logger.should_receive(:close) 26 | klass = Class.new(Fluent::Logger::LoggerBase) 27 | fluent_logger_logger_io = StringIO.new 28 | Fluent::Logger.open('tag-prefix', { 29 | :logger => ::Logger.new(fluent_logger_logger_io) 30 | }) 31 | # Fluent::Logger::FluentLogger is delegator 32 | Fluent::Logger.default.method_missing(:kind_of?, Fluent::Logger::FluentLogger).should be_true 33 | } 34 | 35 | it('open with BaseLogger class') { 36 | test_logger.should_receive(:close) 37 | klass = Class.new(Fluent::Logger::LoggerBase) 38 | Fluent::Logger.open(klass) 39 | Fluent::Logger.default.class.should == klass 40 | } 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /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 | 21 | class TestLogger < LoggerBase 22 | def initialize(queue=[]) 23 | @queue = queue 24 | @max = 1024 25 | end 26 | 27 | attr_accessor :max 28 | attr_reader :queue 29 | 30 | def post_with_time(tag, map, time) 31 | while @queue.size > @max-1 32 | @queue.shift 33 | end 34 | (class<'localhost', :port=>24224) 12 | log.post("myapp.access", {"agent"=>"foo"}) 13 | 14 | # output: myapp.access {"agent":"foo"} 15 | 16 | === Singleton 17 | 18 | require 'fluent-logger' 19 | 20 | Fluent::Logger::FluentLogger.open(nil, :host=>'localhost', :port=>24224) 21 | Fluent::Logger.post("myapp.access", {"agent"=>"foo"}) 22 | 23 | # output: myapp.access {"agent":"foo"} 24 | 25 | === Tag prefix 26 | 27 | require 'fluent-logger' 28 | 29 | log = Fluent::Logger::FluentLogger.new('myapp', :host=>'localhost', :port=>24224) 30 | log.post("access", {"agent"=>"foo"}) 31 | 32 | # output: myapp.access {"agent":"foo"} 33 | 34 | == Loggers 35 | 36 | === Fluent 37 | 38 | Fluent::Logger::FluentLogger.open('tag_prefix', :host=>'localhost', :port=24224) 39 | 40 | === Console 41 | 42 | Fluent::Logger::ConsoleLogger.open(io) 43 | 44 | === Null 45 | 46 | Fluent::Logger::NullLogger.open 47 | 48 | 49 | Web site:: http://fluent.github.com/ 50 | Documents:: http://fluent.github.com/doc/ 51 | Source repository:: https://github.com/fluent/fluent-logger-ruby 52 | Author:: Sadayuki Furuhashi 53 | Copyright:: (c) 2011 FURUHASHI Sadayuki 54 | License:: Apache License, Version 2.0 55 | 56 | -------------------------------------------------------------------------------- /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 | 23 | class ConsoleLogger < TextLogger 24 | def initialize(out) 25 | super() 26 | require 'time' 27 | 28 | if out.is_a?(String) 29 | @io = File.open(out, "a") 30 | @on_reopen = Proc.new { @io.reopen(out, "a") } 31 | elsif out.respond_to?(:write) 32 | @io = out 33 | @on_reopen = Proc.new { } 34 | else 35 | raise "Invalid output: #{out.inspect}" 36 | end 37 | end 38 | 39 | attr_accessor :time_format 40 | 41 | def reopen! 42 | @on_reopen.call 43 | end 44 | 45 | def post_text(text) 46 | @io.puts text 47 | end 48 | 49 | def close 50 | @io.close 51 | self 52 | end 53 | end 54 | 55 | 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /fluent-logger.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | $:.push File.expand_path('../lib', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | version_file = "lib/fluent/logger/version.rb" 6 | version = File.read("VERSION").strip 7 | File.open(version_file, "w") {|f| 8 | f.write < 1.0' 39 | gem.add_dependency "msgpack", [">= 0.4.4", "!= 0.5.0", "!= 0.5.1", "!= 0.5.2", "!= 0.5.3", "< 0.6.0"] 40 | gem.add_development_dependency 'rake', '>= 0.9.2' 41 | gem.add_development_dependency 'rspec', '>= 2.7.0' 42 | gem.add_development_dependency 'simplecov', '>= 0.5.4' 43 | gem.add_development_dependency 'timecop', '>= 0.3.0' 44 | end 45 | -------------------------------------------------------------------------------- /spec/console_logger_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'spec_helper' 3 | require 'stringio' 4 | require 'tempfile' 5 | require 'pathname' 6 | 7 | describe Fluent::Logger::ConsoleLogger do 8 | before(:each) { 9 | Timecop.freeze Time.local(2008, 9, 1, 10, 5, 0) 10 | } 11 | after(:each) { 12 | Timecop.return 13 | } 14 | 15 | context "IO output" do 16 | let(:io) { StringIO.new } 17 | let(:logger) { Fluent::Logger::ConsoleLogger.new(io) } 18 | 19 | subject { 20 | io 21 | } 22 | 23 | context "post and read" do 24 | before do 25 | logger.post('example', {:foo => :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 67 | } 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | Release 0.4.6 - 2013/06/17 2 | 3 | * Raise an ArgumentError when passes invalid argument to post method 4 | * Relax msgpack gem version 5 | 6 | Release 0.4.5 - 2013/02/26 7 | 8 | * Use https scheme for rubygems source 9 | * Fix broken spec 10 | 11 | Release 0.4.4 - 2012/12/26 12 | 13 | * Change msgpack dependency version == 0.4.7 for avoding 0.4.8 yanked issue 14 | 15 | Release 0.4.3 - 2012/04/24 16 | 17 | * Update yajl dependency version >= 1.0 (thanks shun0102) 18 | 19 | Release 0.4.2 - 2012/03/02 20 | 21 | * Added TestLogger#tag_queue(tag_name) 22 | * Added bin/fluent-post cli command 23 | * Impl default LoggerBase#close 24 | * Don't change logger.level if :debug=> option is sepcified 25 | 26 | Release 0.4.1 - 2011/11/07 27 | 28 | * added Logger#post_with_time(tag, map, time) 29 | * Logger#post(tag, map, time=Time.now) -> Logger#post(tag, map) 30 | * FluentLogger supports :debug=>true option to write all events to STDERR 31 | 32 | 33 | Release 0.4.0 - 2011/11/05 34 | 35 | * Wait before reconnecting to fluentd to prevent burst 36 | * Flush logs when process stops using finalizer 37 | * Added rspec and coverage 38 | * Supports objects that don't support to_msgpack by 39 | JSON.load(JSON.dump(obj)).to_msgpack 40 | * FluentLogger uses IO#sync=true + IO#write instead of IO#syswrite to 41 | avoid unexpected blocking 42 | * Logger#post(tag, map) -> Logger#post(tag, map, time=Time.now) 43 | * Removed Event classes 44 | * Added NullLogger 45 | 46 | 47 | Release 0.3.1 - 2011/08/28 48 | 49 | * FluentLogger#initialize doesn't raise error when connection is failed. 50 | Instead, it tries to reconnect. 51 | 52 | 53 | Release 0.3.0 - 2011/08/21 54 | 55 | * Added 'tag' for event logs 56 | 57 | 58 | Release 0.2.0 - 2011/08/05 59 | 60 | * Redesigned Event class 61 | * Added TestLogger (Fluent.open(:test)) 62 | * Added test programs 63 | 64 | 65 | Release 0.1.0 - 2011/08/04 66 | 67 | * First release 68 | 69 | -------------------------------------------------------------------------------- /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 | 20 | module Logger 21 | autoload :ConsoleLogger , 'fluent/logger/console_logger' 22 | autoload :FluentLogger , 'fluent/logger/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_logger.post(tag, map) 54 | end 55 | 56 | def self.post_with_time(tag, map, time) 57 | @@default_logger.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 | 69 | end 70 | -------------------------------------------------------------------------------- /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 'msgpack' 19 | require 'socket' 20 | require 'monitor' 21 | require 'logger' 22 | require 'yajl' 23 | 24 | module Fluent 25 | module Logger 26 | 27 | 28 | class FluentLogger < LoggerBase 29 | module Finalizable 30 | require 'delegate' 31 | def new(*args, &block) 32 | obj = allocate 33 | obj.instance_eval { initialize(*args, &block) } 34 | dc = DelegateClass(obj.class).new(obj) 35 | ObjectSpace.define_finalizer(dc, finalizer(obj)) 36 | dc 37 | end 38 | 39 | def finalizer(obj) 40 | fin = obj.method(:finalize) 41 | proc {|id| 42 | fin.call 43 | } 44 | end 45 | end 46 | extend Finalizable 47 | 48 | BUFFER_LIMIT = 8*1024*1024 49 | RECONNECT_WAIT = 0.5 50 | RECONNECT_WAIT_INCR_RATE = 1.5 51 | RECONNECT_WAIT_MAX = 60 52 | RECONNECT_WAIT_MAX_COUNT = 53 | (1..100).inject(RECONNECT_WAIT_MAX / RECONNECT_WAIT) {|r,i| 54 | break i + 1 if r < RECONNECT_WAIT_INCR_RATE 55 | r / RECONNECT_WAIT_INCR_RATE 56 | } 57 | 58 | def initialize(tag_prefix, *args) 59 | super() 60 | 61 | options = { 62 | :host => 'localhost', 63 | :port => 24224 64 | } 65 | 66 | case args.first 67 | when String, Symbol 68 | # backward compatible 69 | options[:host] = args[0] 70 | options[:port] = args[1] if args[1] 71 | when Hash 72 | options.update args.first 73 | end 74 | 75 | @tag_prefix = tag_prefix 76 | @host = options[:host] 77 | @port = options[:port] 78 | 79 | @mon = Monitor.new 80 | @pending = nil 81 | @connect_error_history = [] 82 | 83 | @limit = options[:buffer_limit] || BUFFER_LIMIT 84 | @log_reconnect_error_threshold = options[:log_reconnect_error_threshold] || RECONNECT_WAIT_MAX_COUNT 85 | 86 | if logger = options[:logger] 87 | @logger = logger 88 | else 89 | @logger = ::Logger.new(STDERR) 90 | if options[:debug] 91 | @logger.level = ::Logger::DEBUG 92 | else 93 | @logger.level = ::Logger::INFO 94 | end 95 | end 96 | 97 | begin 98 | connect! 99 | rescue 100 | @logger.error "Failed to connect fluentd: #{$!}" 101 | @logger.error "Connection will be retried." 102 | end 103 | end 104 | 105 | attr_accessor :limit, :logger, :log_reconnect_error_threshold 106 | 107 | def post_with_time(tag, map, time) 108 | @logger.debug { "event: #{tag} #{map.to_json}" rescue nil } 109 | tag = "#{@tag_prefix}.#{tag}" if @tag_prefix 110 | write [tag, time.to_i, map] 111 | end 112 | 113 | def close 114 | @mon.synchronize { 115 | if @pending 116 | begin 117 | send_data(@pending) 118 | rescue 119 | @logger.error("FluentLogger: Can't send logs to #{@host}:#{@port}: #{$!}") 120 | end 121 | end 122 | @con.close if connect? 123 | @con = nil 124 | @pending = nil 125 | } 126 | self 127 | end 128 | 129 | def connect? 130 | !!@con 131 | end 132 | 133 | def finalize 134 | close 135 | end 136 | 137 | private 138 | def to_msgpack(msg) 139 | begin 140 | msg.to_msgpack 141 | rescue NoMethodError 142 | Yajl::Parser.parse( Yajl::Encoder.encode(msg) ).to_msgpack 143 | end 144 | end 145 | 146 | def suppress_sec 147 | if (sz = @connect_error_history.size) < RECONNECT_WAIT_MAX_COUNT 148 | RECONNECT_WAIT * (RECONNECT_WAIT_INCR_RATE ** (sz - 1)) 149 | else 150 | RECONNECT_WAIT_MAX 151 | end 152 | end 153 | 154 | def write(msg) 155 | begin 156 | data = to_msgpack(msg) 157 | rescue 158 | @logger.error("FluentLogger: Can't convert to msgpack: #{msg.inspect}: #{$!}") 159 | return false 160 | end 161 | 162 | @mon.synchronize { 163 | if @pending 164 | @pending << data 165 | else 166 | @pending = data 167 | end 168 | 169 | # suppress reconnection burst 170 | if !@connect_error_history.empty? && @pending.bytesize <= @limit 171 | if Time.now.to_i - @connect_error_history.last < suppress_sec 172 | return false 173 | end 174 | end 175 | 176 | begin 177 | send_data(@pending) 178 | @pending = nil 179 | true 180 | rescue 181 | if @pending.bytesize > @limit 182 | @logger.error("FluentLogger: Can't send logs to #{@host}:#{@port}: #{$!}") 183 | @pending = nil 184 | end 185 | @con.close if connect? 186 | @con = nil 187 | false 188 | end 189 | } 190 | end 191 | 192 | def send_data(data) 193 | unless connect? 194 | connect! 195 | end 196 | @con.write data 197 | #while true 198 | # puts "sending #{data.length} bytes" 199 | # if data.length > 32*1024 200 | # n = @con.syswrite(data[0..32*1024]) 201 | # else 202 | # n = @con.syswrite(data) 203 | # end 204 | # puts "sent #{n}" 205 | # if n >= data.bytesize 206 | # break 207 | # end 208 | # data = data[n..-1] 209 | #end 210 | true 211 | end 212 | 213 | def connect! 214 | if defined?(EventMachine::Synchrony) 215 | @con = EventMachine::Synchrony::TCPSocket.new(@host, @port) 216 | else 217 | @con = TCPSocket.new(@host, @port) 218 | end 219 | 220 | @con.sync = true 221 | @connect_error_history.clear 222 | @logged_reconnect_error = false 223 | rescue 224 | @connect_error_history << Time.now.to_i 225 | if @connect_error_history.size > RECONNECT_WAIT_MAX_COUNT 226 | @connect_error_history.shift 227 | end 228 | 229 | if @connect_error_history.size >= @log_reconnect_error_threshold && !@logged_reconnect_error 230 | log_reconnect_error 231 | @logged_reconnect_error = true 232 | end 233 | 234 | raise 235 | end 236 | 237 | def log_reconnect_error 238 | @logger.error("FluentLogger: Can't connect to #{@host}:#{@port}(#{@connect_error_history.size} retried): #{$!}") 239 | end 240 | end 241 | 242 | 243 | end 244 | end 245 | -------------------------------------------------------------------------------- /spec/fluent_logger_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'spec_helper' 3 | if RUBY_VERSION < "1.9.2" 4 | 5 | describe Fluent::Logger::FluentLogger do 6 | pending "fluentd don't work RUBY < 1.9.2" 7 | end 8 | 9 | else 10 | 11 | require 'fluent/load' 12 | require 'tempfile' 13 | require 'logger' 14 | require 'socket' 15 | require 'stringio' 16 | require 'fluent/logger/fluent_logger/cui' 17 | 18 | $log = Fluent::Log.new(StringIO.new) # XXX should remove $log from fluentd 19 | 20 | describe Fluent::Logger::FluentLogger do 21 | WAIT = ENV['WAIT'] ? ENV['WAIT'].to_f : 0.1 22 | 23 | let(:fluentd_port) { 24 | port = 60001 25 | loop do 26 | begin 27 | TCPServer.open('localhost', port).close 28 | break 29 | rescue Errno::EADDRINUSE 30 | port += 1 31 | end 32 | end 33 | port 34 | } 35 | 36 | let(:logger) { 37 | @logger_io = StringIO.new 38 | logger = ::Logger.new(@logger_io) 39 | Fluent::Logger::FluentLogger.new('logger-test', { 40 | :host => 'localhost', 41 | :port => fluentd_port, 42 | :logger => logger, 43 | }) 44 | } 45 | 46 | let(:logger_io) { 47 | @logger_io 48 | } 49 | 50 | let(:output) { 51 | sleep 0.0001 # next tick 52 | Fluent::Engine.match('logger-test').output 53 | } 54 | 55 | let(:queue) { 56 | queue = [] 57 | output.emits.each {|tag, time, record| 58 | queue << [tag, record] 59 | } 60 | queue 61 | } 62 | 63 | after(:each) do 64 | output.emits.clear rescue nil 65 | end 66 | 67 | def wait_transfer 68 | sleep WAIT 69 | end 70 | 71 | context "running fluentd" do 72 | before(:each) do 73 | tmp = Tempfile.new('fluent-logger-config') 74 | tmp.close(false) 75 | 76 | File.open(tmp.path, 'w') {|f| 77 | f.puts < 79 | type tcp 80 | port #{fluentd_port} 81 | 82 | 83 | type test 84 | 85 | EOF 86 | } 87 | Fluent::Test.setup 88 | Fluent::Engine.read_config(tmp.path) 89 | @coolio_default_loop = nil 90 | @thread = Thread.new { 91 | @coolio_default_loop = Coolio::Loop.default 92 | Fluent::Engine.run 93 | } 94 | wait_transfer 95 | end 96 | 97 | after(:each) do 98 | @coolio_default_loop.stop 99 | Fluent::Engine.send :shutdown 100 | @thread.join 101 | end 102 | 103 | context('Post by CUI') do 104 | it('post') { 105 | args = %W(-h localhost -p #{fluentd_port} -t logger-test.tag -v a=b -v foo=bar) 106 | Fluent::Logger::FluentLogger::CUI.post(args) 107 | wait_transfer 108 | queue.last.should == ['logger-test.tag', {'a' => 'b', 'foo' => 'bar'}] 109 | } 110 | end 111 | 112 | context('post') do 113 | it ('success') { 114 | logger.post('tag', {'a' => 'b'}).should be_true 115 | wait_transfer 116 | queue.last.should == ['logger-test.tag', {'a' => 'b'}] 117 | } 118 | 119 | it ('close after post') { 120 | logger.should be_connect 121 | logger.close 122 | logger.should_not be_connect 123 | 124 | logger.post('tag', {'b' => 'c'}) 125 | logger.should be_connect 126 | wait_transfer 127 | queue.last.should == ['logger-test.tag', {'b' => 'c'}] 128 | } 129 | 130 | it ('large data') { 131 | data = {'a' => ('b' * 1000000)} 132 | logger.post('tag', data) 133 | wait_transfer 134 | queue.last.should == ['logger-test.tag', data] 135 | } 136 | 137 | it ('msgpack unsupport data') { 138 | data = { 139 | 'time' => Time.utc(2008, 9, 1, 10, 5, 0), 140 | 'object' => Object.new, 141 | 'proc' => proc { 1 }, 142 | } 143 | logger.post('tag', data) 144 | wait_transfer 145 | logger_data = queue.last.last 146 | logger_data['time'].should == '2008-09-01 10:05:00 UTC' 147 | logger_data['proc'].should be 148 | logger_data['object'].should be 149 | } 150 | 151 | it ('msgpack and JSON unsupport data') { 152 | data = { 153 | 'time' => Time.utc(2008, 9, 1, 10, 5, 0), 154 | 'object' => Object.new, 155 | 'proc' => proc { 1 }, 156 | 'NaN' => (0.0/0.0) # JSON don't convert 157 | } 158 | logger.post('tag', data) 159 | wait_transfer 160 | queue.last.should be_nil 161 | logger_io.rewind 162 | logger_io.read =~ /FluentLogger: Can't convert to msgpack:/ 163 | } 164 | 165 | it ('should raise an error when second argument is non hash object') { 166 | data = 'FooBar' 167 | expect { 168 | logger.post('tag', data) 169 | }.to raise_error(ArgumentError) 170 | 171 | data = nil 172 | expect { 173 | logger.post('tag', data) 174 | }.to raise_error(ArgumentError) 175 | } 176 | end 177 | 178 | context "initializer" do 179 | it "backward compatible" do 180 | port = fluentd_port 181 | fluent_logger = Fluent::Logger::FluentLogger.new('logger-test', 'localhost', port) 182 | fluent_logger.method_missing(:instance_eval) { # fluent_logger is delegetor 183 | @host.should == 'localhost' 184 | @port.should == port 185 | } 186 | end 187 | 188 | it "hash argument" do 189 | port = fluentd_port 190 | fluent_logger = Fluent::Logger::FluentLogger.new('logger-test', { 191 | :host => 'localhost', 192 | :port => port 193 | }) 194 | fluent_logger.method_missing(:instance_eval) { # fluent_logger is delegetor 195 | @host.should == 'localhost' 196 | @port.should == port 197 | } 198 | end 199 | end 200 | end 201 | 202 | context "not running fluentd" do 203 | context('fluent logger interface') do 204 | it ('post & close') { 205 | logger.post('tag', {'a' => 'b'}).should be_false 206 | wait_transfer # even if wait 207 | queue.last.should be_nil 208 | logger.close 209 | logger_io.rewind 210 | log = logger_io.read 211 | log.should =~ /Failed to connect/ 212 | log.should =~ /Can't send logs to/ 213 | } 214 | 215 | it ('post limit over') do 216 | logger.limit = 100 217 | logger.post('tag', {'a' => 'b'}) 218 | wait_transfer # even if wait 219 | queue.last.should be_nil 220 | 221 | logger_io.rewind 222 | logger_io.read.should_not =~ /Can't send logs to/ 223 | 224 | logger.post('tag', {'a' => ('c' * 1000)}) 225 | logger_io.rewind 226 | logger_io.read.should =~ /Can't send logs to/ 227 | end 228 | 229 | it ('log connect error once') do 230 | logger.stub(:suppress_sec).and_return(-1) 231 | logger.log_reconnect_error_threshold = 1 232 | logger.should_receive(:log_reconnect_error).once.and_call_original 233 | 234 | logger.post('tag', {'a' => 'b'}) 235 | wait_transfer # even if wait 236 | logger.post('tag', {'a' => 'b'}) 237 | wait_transfer # even if wait 238 | logger_io.rewind 239 | logger_io.read.should =~ /Can't connect to/ 240 | end 241 | end 242 | end 243 | 244 | end 245 | 246 | end 247 | --------------------------------------------------------------------------------