diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f1b1301
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.swp
+*.gem
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..83e16f8
--- /dev/null
+++ b/.rspec
@@ -0,0 +1,2 @@
+--color
+--require spec_helper
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..195539b
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,72 @@
+PATH
+ remote: .
+ specs:
+ fluent-plugin-statsd (1.1.0)
+ fluentd (>= 0.10.8)
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+ coderay (1.1.1)
+ cool.io (1.4.4)
+ diff-lcs (1.2.5)
+ fluentd (0.14.1)
+ cool.io (>= 1.4.3, < 2.0.0)
+ http_parser.rb (>= 0.5.1, < 0.7.0)
+ json (>= 1.4.3)
+ msgpack (>= 0.7.0)
+ serverengine (>= 1.6.4)
+ sigdump (~> 0.2.2)
+ strptime (>= 0.1.7)
+ tzinfo (>= 1.0.0)
+ tzinfo-data (>= 1.0.0)
+ yajl-ruby (~> 1.0)
+ http_parser.rb (0.6.0)
+ json (2.0.1)
+ method_source (0.8.2)
+ msgpack (1.0.0)
+ power_assert (0.3.0)
+ pry (0.10.4)
+ coderay (~> 1.1.0)
+ method_source (~> 0.8.1)
+ slop (~> 3.4)
+ rspec (3.5.0)
+ rspec-core (~> 3.5.0)
+ rspec-expectations (~> 3.5.0)
+ rspec-mocks (~> 3.5.0)
+ rspec-core (3.5.1)
+ rspec-support (~> 3.5.0)
+ rspec-expectations (3.5.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.5.0)
+ rspec-mocks (3.5.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.5.0)
+ rspec-support (3.5.0)
+ serverengine (1.6.4)
+ sigdump (~> 0.2.2)
+ sigdump (0.2.4)
+ slop (3.6.0)
+ statsd-ruby (1.3.0)
+ strptime (0.1.8)
+ test-unit (3.1.8)
+ power_assert
+ thread_safe (0.3.5)
+ tzinfo (1.2.2)
+ thread_safe (~> 0.1)
+ tzinfo-data (1.2016.6)
+ tzinfo (>= 1.0.0)
+ yajl-ruby (1.2.1)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ fluent-plugin-statsd!
+ pry
+ rspec (~> 3.0)
+ statsd-ruby
+ test-unit
+
+BUNDLED WITH
+ 1.10.6
diff --git a/README.md b/README.md
index 96e2a16..34826f0 100644
--- a/README.md
+++ b/README.md
@@ -14,73 +14,28 @@ $ fluent-gem install fluent-plugin-statsd
type statsd
host localhost # optional
- port 8125# optional
+ port 8125 # optional
+ namespace a.b.c # optional
+
+
+ statsd_type timing
+ statsd_key my_app.nginx.response_time
+ statsd_key ${record['response_time']}
+
+
+
+ statsd_type incrument
+ statsd_key my_app.nginx.${response_code.to_i / 100}xx # 2xx 4xx 5xx
+
```
-```ruby
-fluent_logger.post('statsd',
- :statsd_type => 'timing',
- :statsd_key => 'org.foo.timing',
- :statsd_timing => 0.234
-)
-
-fluent_logger.post('statsd',
- :statsd_type => 'gauge',
- :statsd_gauge => 10,
- :statsd_key => 'org.foo.gauge'
-)
-
-fluent_logger.post('statsd',
- :statsd_type => 'count',
- :statsd_gauge => 10,
- :statsd_key => 'org.foo.gauge'
-)
-
-fluent_logger.post('statsd',
- :statsd_type => 'set',
- :statsd_gauge => 10,
- :statsd_key => 'org.foo.gauge'
-)
-
-fluent_logger.post('statsd',
- :statsd_type => 'increment',
- :statsd_key => 'org.foo.counter'
-)
-
-
-fluent_logger.post('statsd',
- :statsd_type => 'decrement',
- :statsd_key => 'org.foo.counter'
-)
-```
-
-# td-agent.conf demo
-
-worked with record_reformer to transform access log request_time into statsd
+# Development
```
-
- type copy
-
- type statsd
- host 127.0.0.1
- port 8125
- flush_interval 1s
-
- # other stores...
-
-
- type record_reformer
- output_tag ${tag}.reformer
- # transform /url1/url2/url3 --> url.urlNN.urlNN
- statsd_key ${"url"+request_uri.gsub(/((\/[^\/]+){2}).*/, '\1').gsub(/([^\?]*)\?.*/,'\1').gsub(/[0-9]+/, "NN").gsub(/\//, ".");}
- statsd_timing ${request_time}
- statsd_type ${"timing"}
-
+$ rspec
```
-
# Copyright
Copyright (c) 2014- Chris Song
diff --git a/Rakefile b/Rakefile
deleted file mode 100644
index f8e784a..0000000
--- a/Rakefile
+++ /dev/null
@@ -1,17 +0,0 @@
-require 'bundler'
-Bundler::GemHelper.install_tasks
-
-require 'rake/testtask'
-
-Rake::TestTask.new(:test) do |test|
- test.libs << 'lib' << 'test'
- test.test_files = FileList['test/plugin/*.rb']
- test.verbose = true
-end
-
-task :coverage do |t|
- ENV['SIMPLE_COV'] = '1'
- Rake::Task["test"].invoke
-end
-
-task :default => [:build]
diff --git a/VERSION b/VERSION
index 6d7de6e..9084fa2 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.0.2
+1.1.0
diff --git a/fluent-plugin-statsd.gemspec b/fluent-plugin-statsd.gemspec
index 58f3522..20eaa5b 100644
--- a/fluent-plugin-statsd.gemspec
+++ b/fluent-plugin-statsd.gemspec
@@ -17,6 +17,8 @@ Gem::Specification.new do |gem|
gem.add_dependency "fluentd", ">= 0.10.8"
- gem.add_development_dependency "rake", ">= 0.9.2"
+ gem.add_development_dependency "rspec", '~> 3.0'
+ gem.add_development_dependency "test-unit"
gem.add_development_dependency "statsd-ruby", ">=1.2.1"
+ gem.add_development_dependency "pry"
end
diff --git a/lib/fluent/plugin/out_statsd.rb b/lib/fluent/plugin/out_statsd.rb
index c8aac4b..a29aa30 100644
--- a/lib/fluent/plugin/out_statsd.rb
+++ b/lib/fluent/plugin/out_statsd.rb
@@ -1,4 +1,6 @@
require 'statsd-ruby'
+require 'ostruct'
+require 'fluent/output'
module Fluent
class StatsdOutput < BufferedOutput
@@ -7,6 +9,13 @@ class StatsdOutput < BufferedOutput
config_param :flush_interval, :time, :default => 1
config_param :host, :string, :default => 'localhost'
config_param :port, :string, :default => '8125'
+ config_param :namespace, :string, :default => nil
+
+ config_section :metric do
+ config_param :statsd_type, :string
+ config_param :statsd_key, :string
+ config_param :statsd_val, :string, default: nil
+ end
attr_reader :statsd
@@ -17,6 +26,11 @@ def initialize
def configure(conf)
super
@statsd = Statsd.new(host, port)
+ @statsd.namespace = namespace if namespace
+ log.info(statsd)
+
+ @metrics = conf.elements.select {|elem| elem.name == 'metric' }
+ log.info(@metrics)
end
def start
@@ -28,29 +42,53 @@ def shutdown
end
def format(tag, time, record)
- record.to_msgpack
+ [tag, record].to_msgpack
end
def write(chunk)
- chunk.msgpack_each {|record|
- if statsd_type = record['statsd_type']
- case statsd_type
- when 'timing'
- @statsd.timing record['statsd_key'], record['statsd_timing'].to_f
- when 'gauge'
- @statsd.gauge record['statsd_key'], record['statsd_gauge'].to_f
- when 'count'
- @statsd.count record['statsd_key'], record['statsd_count'].to_f
- when 'set'
- @statsd.set record['statsd_key'], record['statsd_set']
- when 'increment'
- @statsd.increment record['statsd_key']
- when 'decrement'
- @statsd.decrement record['statsd_key']
- end
+ chunk.msgpack_each do |tag, record|
+ parser = RubyStringParser.new(record: record, tag: tag)
+
+ @metrics.each do |metric|
+ arg_names = %w{statsd_type statsd_key statsd_val}
+ send_to_statsd(*metric.values_at(*arg_names).map {|str| parser.parse(str) })
end
- }
+ end
end
+
+ private
+
+ def send_to_statsd(type, key, val)
+ log.debug([type, key, val])
+
+ case type
+ when 'timing'
+ @statsd.timing key, val.to_f
+ when 'gauge'
+ @statsd.gauge key, val.to_f
+ when 'count'
+ @statsd.count key, val.to_f
+ when 'set'
+ @statsd.set key, val
+ when 'increment'
+ @statsd.increment key
+ when 'decrement'
+ @statsd.decrement key
+ else
+ raise "Invalid statsd type '#{type}'"
+ end
+ end
+
+ class RubyStringParser
+ def initialize(vars = {})
+ @obj = Struct.new(*vars.keys).new(*vars.values)
+ end
+
+ def parse(string)
+ return unless string
+ string.gsub(/\$\{.+\}/) {|str| @obj.instance_eval str[2..-2] }
+ end
+ end
end
end
diff --git a/spec/examples.txt b/spec/examples.txt
new file mode 100644
index 0000000..df82656
--- /dev/null
+++ b/spec/examples.txt
@@ -0,0 +1,3 @@
+example_id | status | run_time |
+------------------------------------- | ------ | --------------- |
+./spec/plugin/out_statsd_spec.rb[1:1] | passed | 0.53516 seconds |
diff --git a/spec/plugin/out_statsd_spec.rb b/spec/plugin/out_statsd_spec.rb
new file mode 100644
index 0000000..e5e678f
--- /dev/null
+++ b/spec/plugin/out_statsd_spec.rb
@@ -0,0 +1,62 @@
+require 'fluent/plugin/out_statsd'
+require 'fluent/test'
+
+RSpec.describe Fluent::StatsdOutput do
+ let(:config) do
+ %{
+ type statsd
+ namespace a.b.c
+
+
+ statsd_type timing
+ statsd_key res_time
+ statsd_val ${record['response_time']}
+
+
+
+ statsd_type increment
+ statsd_key res_code_${record['status'].to_i / 100}xx
+
+ }
+ end
+ let(:driver) { create_driver(config) }
+ let(:statsd) { double('statsd', increment: true, timing: true, 'namespace=' => true) }
+ let(:time) { Time.now.utc }
+
+ before :all do
+ Fluent::Test.setup
+ end
+
+ def create_driver(conf)
+ Fluent::Test::BufferedOutputTestDriver.new(Fluent::StatsdOutput) {
+ }.configure(conf)
+ end
+
+ def emit_events(events)
+ events.each {|e| driver.emit(e, time) }
+ end
+
+
+ it 'should call statsd with events data' do
+ allow(Statsd).to receive(:new).and_return(statsd)
+
+ expect(statsd).to receive(:namespace=).with('a.b.c')
+
+ expect(statsd).to receive(:increment).with('res_code_2xx').twice.times
+ expect(statsd).to receive(:increment).with('res_code_4xx').once.times
+ expect(statsd).to receive(:increment).with('res_code_5xx').once.times
+ expect(statsd).to receive(:timing).with('res_time', 102).ordered
+ expect(statsd).to receive(:timing).with('res_time', 105).ordered
+ expect(statsd).to receive(:timing).with('res_time', 112).ordered
+ expect(statsd).to receive(:timing).with('res_time', 125).ordered
+
+ emit_events([
+ {'response_time' => 102, 'status' => '200'},
+ {'response_time' => 105, 'status' => '200'},
+ {'response_time' => 112, 'status' => '400'},
+ {'response_time' => 125, 'status' => '500'}
+ ])
+
+ driver.run
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..3c8a9ab
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,99 @@
+# This file was generated by the `rspec --init` command. Conventionally, all
+# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
+# The generated `.rspec` file contains `--require spec_helper` which will cause
+# this file to always be loaded, without a need to explicitly require it in any
+# files.
+#
+# Given that it is always loaded, you are encouraged to keep this file as
+# light-weight as possible. Requiring heavyweight dependencies from this file
+# will add to the boot time of your test suite on EVERY test run, even for an
+# individual file that may not need all of that loaded. Instead, consider making
+# a separate helper file that requires the additional dependencies and performs
+# the additional setup, and require it from the spec files that actually need
+# it.
+#
+# The `.rspec` file also contains a few flags that are not defaults but that
+# users commonly want.
+#
+# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+RSpec.configure do |config|
+ # rspec-expectations config goes here. You can use an alternate
+ # assertion/expectation library such as wrong or the stdlib/minitest
+ # assertions if you prefer.
+ config.expect_with :rspec do |expectations|
+ # This option will default to `true` in RSpec 4. It makes the `description`
+ # and `failure_message` of custom matchers include text for helper methods
+ # defined using `chain`, e.g.:
+ # be_bigger_than(2).and_smaller_than(4).description
+ # # => "be bigger than 2 and smaller than 4"
+ # ...rather than:
+ # # => "be bigger than 2"
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ # rspec-mocks config goes here. You can use an alternate test double
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
+ config.mock_with :rspec do |mocks|
+ # Prevents you from mocking or stubbing a method that does not exist on
+ # a real object. This is generally recommended, and will default to
+ # `true` in RSpec 4.
+ mocks.verify_partial_doubles = true
+ end
+
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
+ # have no way to turn it off -- the option exists only for backwards
+ # compatibility in RSpec 3). It causes shared context metadata to be
+ # inherited by the metadata hash of host groups and examples, rather than
+ # triggering implicit auto-inclusion in groups with matching metadata.
+ config.shared_context_metadata_behavior = :apply_to_host_groups
+
+ # This allows you to limit a spec run to individual examples or groups
+ # you care about by tagging them with `:focus` metadata. When nothing
+ # is tagged with `:focus`, all examples get run. RSpec also provides
+ # aliases for `it`, `describe`, and `context` that include `:focus`
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
+ config.filter_run_when_matching :focus
+
+ # Allows RSpec to persist some state between runs in order to support
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
+ # you configure your source control system to ignore this file.
+ config.example_status_persistence_file_path = "spec/examples.txt"
+
+ # Limits the available syntax to the non-monkey patched syntax that is
+ # recommended. For more details, see:
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
+ config.disable_monkey_patching!
+
+ # This setting enables warnings. It's recommended, but in some cases may
+ # be too noisy due to issues in dependencies.
+ config.warnings = true
+
+ # Many RSpec users commonly either run the entire suite or an individual
+ # file, and it's useful to allow more verbose output when running an
+ # individual spec file.
+ if config.files_to_run.one?
+ # Use the documentation formatter for detailed output,
+ # unless a formatter has already been configured
+ # (e.g. via a command-line flag).
+ config.default_formatter = 'doc'
+ end
+
+ # Print the 10 slowest examples and example groups at the
+ # end of the spec run, to help surface which specs are running
+ # particularly slow.
+ config.profile_examples = 10
+
+ # Run specs in random order to surface order dependencies. If you find an
+ # order dependency and want to debug it, you can fix the order by providing
+ # the seed, which is printed after each run.
+ # --seed 1234
+ config.order = :random
+
+ # Seed global randomization in this process using the `--seed` CLI option.
+ # Setting this allows you to use `--seed` to deterministically reproduce
+ # test failures related to randomization by passing the same `--seed` value
+ # as the one that triggered the failure.
+ Kernel.srand config.seed
+end
diff --git a/test/plugin/out_statsd.rb b/test/plugin/out_statsd.rb
deleted file mode 100644
index e4c8a51..0000000
--- a/test/plugin/out_statsd.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'fluent/plugin/out_statsd'
-require 'statsd-ruby'
-
-class StatsdOutputTest < Test::Unit::TestCase
- def setup
- super
- Fluent::Test.setup
- @now = Time.now
- end
-
- def treedown
- end
-
- CONFIG = %[
- type statsd
- ]
-
- def create_driver(conf = CONFIG)
- Fluent::Test::BufferedOutputTestDriver.new(Fluent::StatsdOutput) {
- }.configure(conf)
- end
-
- def test_write
- d = create_driver
- time = Time.at(@now.to_i).utc
- d.emit({ :stastd_type => 'timing', :statsd_key => 'test.statsd.t', :statsd_timing => 100 }, time)
- d.emit({ :stastd_type => 'guage', :statsd_key => 'test.statsd.g', :statsd_gauge => 102 }, time)
- d.emit({ :stastd_type => 'increment', :statsd_key => 'test.statsd.i'}, time)
-
- d.run
- end
-end