fluent-plugin-statsite 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2e12dccb3b7db8188b8fc47b913d1ee260773d1e
4
+ data.tar.gz: f09fa661f3b430f1bcc2f1b9b03f3cc4876989ce
5
+ SHA512:
6
+ metadata.gz: 0259eae4b813fa352773f84dbc465008463d4be9f9aa5e2c7f6dc4fa8c31a2e291fe2974b6a0de81cb856e5fbd0f6eae3601b3d0ba07cf7127457fbd42955219
7
+ data.tar.gz: 55b89401b72406cbcbfe3942b5ad5318e897a2e05c21c68d7836d9b7085ec22bc244c7fe6585fa3ee24c0a6e864ff141bd6409d87bf8d6a6047527a57b62c1de
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/.gitmodules ADDED
@@ -0,0 +1,3 @@
1
+ [submodule "vendor/statsite"]
2
+ path = vendor/statsite
3
+ url = https://github.com/armon/statsite
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fluent-plugin-statsite.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2014 OKUNO Akihiro
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.
data/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # Statsite Fluentd Plugin
2
+
3
+ This plugin calculates various useful metrics using [Statsite by armon](http://armon.github.io/statsite/).
4
+
5
+ [Statsite](http://armon.github.io/statsite/) is very cool software. Statsite works as daemon service, receiving events from tcp/udp, aggregating these events with specified methods, and sending the results via pluggable sinks. Statsite is written in C, cpu and memory efficient, and employ some approximate algorithms for unique sets and percentiles.
6
+
7
+ You may think this as standard output plugin which just sends events to a daemon process, such as [mongodb plugin](https://github.com/fluent/fluent-plugin-mongo). It is true that this plugin is registered as output plugin, but this works as the so-called **Filter Plugin**, which means that this plugin sends matched events to Statsite process, recieves results aggregated by the Statsite, then re-emitting these results as events.
8
+
9
+ Statsite process is launched as a child process from this plugin internally. All you have to do place statsite the binary under $PATH, or set the path of statsite binary as parameter. Neither config files or daemon process is not required. Besides, the communication between the plugin and the Statsite process takes place through STDIN/STDOUT, so no network port will be used.
10
+
11
+ ## Installation
12
+
13
+ `$ fluent-gem install fluent-plugin-statsite`
14
+
15
+ ### Statsite Installation
16
+
17
+ Statsite can work as sinble binary with few dependency. You probably could get it working just by downloading source files and executing make command.
18
+
19
+ Please refer to [Statsite official page](http://armon.github.io/statsite/).
20
+
21
+ ## Configuration
22
+
23
+ It is strongly recommended to use '[V1 config format](http://docs.fluentd.org/articles/config-file#v1-format)' because this plugin requires to set deeply nested parameters.
24
+
25
+ ### Example
26
+
27
+ ```
28
+ <match **>
29
+ type statsite
30
+ tag statsite
31
+ metrics [
32
+ "${status}:1|c",
33
+ {"key": "request_time", "value_field": "request_time", "type": "ms"}
34
+ ]
35
+ histograms [
36
+ {"prefix": "request_time" "min": 0, "max": 1, "width": 0.1}
37
+ ]
38
+ statsite_path "statsite"
39
+ statsite_flush_interval 1s
40
+ timer_eps 0.01
41
+ set_eps 0.01
42
+ child_respawn 5
43
+ </match>
44
+ ```
45
+
46
+ ### Parameter
47
+
48
+ TODO
49
+
50
+ ### Metrics Format
51
+
52
+ You can specify metrics in two format, string style, and hash style.
53
+
54
+ #### String style
55
+
56
+ TODO
57
+
58
+ #### Hash style
59
+
60
+ TODO
61
+
62
+ ## Copyright
63
+
64
+ * Copyright (c) 2014- OKUNO Akihiro
65
+ * License
66
+ * Apache License, version 2.0
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'lib' << 'test'
6
+ t.test_files = FileList['test/test*.rb']
7
+ t.verbose = true
8
+ end
9
+ task :test => 'statsite:build'
10
+
11
+ namespace :statsite do
12
+ desc 'Build statsite binary'
13
+ task :'build' => 'vendor/statsite/statsite'
14
+
15
+ desc 'Clean statsite artifacts'
16
+ task :'clean' do
17
+ sh 'make -C vendor/statsite clean'
18
+ end
19
+
20
+ file 'vendor/statsite/statsite' do
21
+ sh 'make -C vendor/statsite'
22
+ end
23
+ end
24
+
25
+ task :default => :test
data/example.conf ADDED
@@ -0,0 +1,27 @@
1
+ <source>
2
+ type forward
3
+ </source>
4
+
5
+ <match event.*>
6
+ type statsite
7
+ tag statsite
8
+ metrics [
9
+ "k1:1|kv",
10
+ "k2:1|g",
11
+ "k3:1|ms",
12
+ "k4:1|c",
13
+ "k5:1|s"
14
+ ]
15
+ histograms [
16
+ {"prefix": "bar", "min": 0, "max": 10, "width": 1.0}
17
+ ]
18
+ statsite_path "vendor/statsite/statsite"
19
+ statsite_flush_interval 1s
20
+ timer_eps 0.01
21
+ set_eps 0.02
22
+ child_respawn 5
23
+ </match>
24
+
25
+ <match *>
26
+ type stdout
27
+ </match>
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "fluent-plugin-statsite"
7
+ spec.version = "0.0.2"
8
+ spec.authors = ["OKUNO Akihiro"]
9
+ spec.email = ["choplin.choplin@gmail.com"]
10
+ spec.summary = %q{Fluentd statsite plugin}
11
+ spec.description = %q{Fluentd plugin which caluculate statistics using statsite}
12
+ spec.homepage = ""
13
+ spec.homepage = "https://github.com/choplin/fluent-plugin-statsite"
14
+ spec.license = "Apache-2.0"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "fluentd"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.6"
24
+ spec.add_development_dependency "rake"
25
+ end
@@ -0,0 +1,156 @@
1
+ require_relative 'statsite/child_process'
2
+ require_relative 'statsite/format'
3
+ require_relative 'statsite/metric'
4
+ require_relative 'statsite/histogram'
5
+
6
+ require 'tempfile'
7
+ require 'timeout'
8
+
9
+ module Fluent
10
+ class StatsiteOutput < Fluent::BufferedOutput
11
+ include Fluent::StatsitePlugin
12
+
13
+ Fluent::Plugin.register_output('statsite', self)
14
+
15
+ # TODO: should be configurable?
16
+ CONFIG_VALIDATION_WAIT = 0.1
17
+
18
+ config_param :tag, :string
19
+ config_param :metrics, :array
20
+ config_param :histograms, :array, :default => []
21
+ config_param :statsite_path, :string, :default => 'statsite'
22
+ config_param :statsite_flush_interval, :time, :default => 10
23
+ config_param :timer_eps, :float, :default => 0.01
24
+ config_param :set_eps, :float, :default => 0.02
25
+ config_param :child_respawn, :string, :default => nil
26
+ # TODO: should support input_counter?
27
+
28
+ def initialize
29
+ super
30
+ end
31
+
32
+ def configure(conf)
33
+ super
34
+ @metrics = validate_metrics
35
+ @metrics.each{|m| @log.info "out_statsite: #{m}"}
36
+
37
+ @histograms = validate_histograms
38
+
39
+ @respawns = configure_respawns
40
+
41
+ @parser = StatsiteParser.new(method(:on_message))
42
+ @formatter = StatsiteFormatter.new(@metrics)
43
+
44
+ $log.info "out_statsite: statsite config\n\n#{config}"
45
+ @conf = Tempfile.new('fluent-plugin-statsite-')
46
+ @conf.puts config
47
+ @conf.flush
48
+
49
+ @child = ChildProcess.new(@parser, @respawns, log)
50
+
51
+ @cmd = "#{@statsite_path} -f #{@conf.path}"
52
+ validate_statsite_confg
53
+ end
54
+
55
+ def start
56
+ super
57
+
58
+ begin
59
+ $log.info "out_statsite: launching statsite process", cmd: @cmd
60
+ @child.start(@cmd)
61
+ rescue
62
+ shutdown
63
+ raise
64
+ end
65
+ end
66
+
67
+ def before_shutdown
68
+ super
69
+ $log.debug "out_statsite#before_shutdown called"
70
+ @child.finished = true
71
+ sleep 0.5 # TODO wait time before killing child process
72
+ end
73
+
74
+ def shutdown
75
+ super
76
+ @conf.close
77
+ @child.shutdown
78
+ end
79
+
80
+ def format(tag, time, record)
81
+ @formatter.call(record)
82
+ end
83
+
84
+ def write(chunk)
85
+ @child.write chunk
86
+ end
87
+
88
+ private
89
+
90
+ def run
91
+ @loop.run
92
+ rescue => e
93
+ log.error "out_statsite: unexpected error", :error => e, :error_class => e.class
94
+ log.error_backtrace
95
+ end
96
+
97
+ def configure_respawns
98
+ if @child_respawn.nil? or @child_respawn == 'none' or @child_respawn == '0'
99
+ 0
100
+ elsif @child_respawn == 'inf' or @child_respawn == '-1'
101
+ -1
102
+ elsif @child_respawn =~ /^\d+$/
103
+ @child_respawn.to_i
104
+ else
105
+ raise ConfigError, "child_respawn option argument invalid: none(or 0), inf(or -1) or positive number"
106
+ end
107
+ end
108
+
109
+ def validate_metrics
110
+ @metrics.map {|m| Metric.validate(m)}
111
+ end
112
+
113
+ def validate_histograms
114
+ @histograms.map {|h| Histogram.validate(h)}
115
+ end
116
+
117
+ def validate_statsite_confg
118
+ $log.debug "lanuch statsite process to validate statsite config"
119
+ pid = spawn(@cmd, out: '/dev/null')
120
+ if pid.nil?
121
+ raise ConfigError, 'failed to launch statsite process', cmd: @cmd
122
+ else
123
+ begin
124
+ Timeout::timeout(CONFIG_VALIDATION_WAIT) do
125
+ Process.waitpid(pid)
126
+ end
127
+ raise ConfigError, 'Statsite process cannot be launched correctly. A config is probably invalid.'
128
+ rescue Timeout::Error
129
+ # launched correctly
130
+ Process.kill(:KILL, pid)
131
+ Process.waitpid(pid)
132
+ end
133
+ end
134
+ end
135
+
136
+ def config
137
+ <<-CONFIG
138
+ [statsite]
139
+ port = 0
140
+ udp_port = 0
141
+ parse_stdin = 1
142
+ log_level = INFO
143
+ flush_interval = #{@statsite_flush_interval}
144
+ timer_eps = #{@timer_eps}
145
+ set_eps = #{@set_eps}
146
+ stream_cmd = cat
147
+
148
+ #{@histograms.map(&:to_ini).join("\n\n")}
149
+ CONFIG
150
+ end
151
+
152
+ def on_message(time, record)
153
+ Engine.emit(@tag, time, record)
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,101 @@
1
+ module Fluent
2
+ module StatsitePlugin
3
+ class ChildProcess
4
+ attr_accessor :finished
5
+
6
+ def initialize(parser, respawns=0, log = $log)
7
+ @pid = nil
8
+ @thread = nil
9
+ @parser = parser
10
+ @respawns = respawns
11
+ @mutex = Mutex.new
12
+ @finished = nil
13
+ @log = log
14
+ end
15
+
16
+ def start(command)
17
+ @command = command
18
+ @mutex.synchronize do
19
+ @io = IO.popen(command, "r+")
20
+ @pid = @io.pid
21
+ @io.sync = true
22
+ @thread = Thread.new(&method(:run))
23
+ end
24
+ @finished = false
25
+ end
26
+
27
+ def kill_child(join_wait)
28
+ begin
29
+ Process.kill(:TERM, @pid)
30
+ rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
31
+ # Errno::ESRCH 'No such process', ignore
32
+ # child process killed by signal chained from fluentd process
33
+ end
34
+ if @thread.join(join_wait)
35
+ # @thread successfully shutdown
36
+ return
37
+ end
38
+ begin
39
+ Process.kill(:KILL, @pid)
40
+ rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
41
+ # ignore if successfully killed by :TERM
42
+ end
43
+ @thread.join
44
+ end
45
+
46
+ def shutdown
47
+ @finished = true
48
+ @mutex.synchronize do
49
+ kill_child(60) # TODO wait time
50
+ end
51
+ end
52
+
53
+ def write(chunk)
54
+ begin
55
+ chunk.write_to(@io)
56
+ rescue Errno::EPIPE => e
57
+ # Broken pipe (child process unexpectedly exited)
58
+ @log.warn "statsite Broken pipe, child process maybe exited.", :command => @command
59
+ if try_respawn
60
+ retry # retry chunk#write_to with child respawned
61
+ else
62
+ raise e # to retry #write with other ChildProcess instance (when num_children > 1)
63
+ end
64
+ end
65
+ end
66
+
67
+ def try_respawn
68
+ return false if @respawns == 0
69
+ @mutex.synchronize do
70
+ return false if @respawns == 0
71
+
72
+ kill_child(5) # TODO wait time
73
+
74
+ @io = IO.popen(@command, "r+")
75
+ @pid = @io.pid
76
+ @io.sync = true
77
+ @thread = Thread.new(&method(:run))
78
+
79
+ @respawns -= 1 if @respawns > 0
80
+ end
81
+ @log.warn "statsite child process successfully respawned.", :command => @command, :respawns => @respawns
82
+ true
83
+ end
84
+
85
+ def run
86
+ @parser.call(@io)
87
+ rescue
88
+ @log.error "statsite thread unexpectedly failed with an error.", :command=>@command, :error=>$!.to_s
89
+ @log.warn_backtrace $!.backtrace
90
+ ensure
91
+ pid, stat = Process.waitpid2(@pid)
92
+ unless @finished
93
+ @log.error "statsite process unexpectedly exited.", :command=>@command, :ecode=>stat.to_i
94
+ unless @respawns == 0
95
+ @log.warn "statsite child process will respawn for next input data (respawns #{@respawns})."
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,47 @@
1
+ module Fluent
2
+ module StatsitePlugin
3
+ class StatsiteParser
4
+ def initialize(on_message)
5
+ @on_message = on_message
6
+ end
7
+
8
+ def call(io)
9
+ io.each_line(&method(:each_line))
10
+ end
11
+
12
+ def each_line(line)
13
+ k,v,t = line.chomp.split('|')
14
+ type, key, statistic, range = k.split(".", 4)
15
+
16
+ record = case type
17
+ when 'timers' then 1
18
+ if statistic == 'histogram'
19
+ {type: type, key: key, value: v.to_i, statistic: statistic, range: range[4..-1]}
20
+ elsif statistic == 'count'
21
+ {type: type, key: key, value: v.to_i, statistic: statistic}
22
+ else
23
+ {type: type, key: key, value: v.to_f, statistic: statistic}
24
+ end
25
+ when 'kv', 'gauges', 'counts'
26
+ {type: type, key: key, value: v.to_f}
27
+ when 'sets'
28
+ {type: type, key: key, value: v.to_i}
29
+ end
30
+
31
+ raise "out_statsite: failed to parse a line. '#{line}'" if record.nil?
32
+
33
+ @on_message.call(t.to_i, record)
34
+ end
35
+ end
36
+
37
+ class StatsiteFormatter
38
+ def initialize(metrics)
39
+ @metrics = metrics
40
+ end
41
+
42
+ def call(record)
43
+ @metrics.map{|m| m.convert(record)}.select{|m| not m.nil?}.join('')
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,65 @@
1
+ module Fluent
2
+ module StatsitePlugin
3
+ class Histogram
4
+ FIELD = %w(
5
+ min
6
+ max
7
+ width
8
+ )
9
+
10
+ OPTIONAL_FIELD = %w(prefix)
11
+
12
+ FLOATING_FIELD = %w(
13
+ min
14
+ max
15
+ width
16
+ )
17
+
18
+ def initialize(prefix, min, max, width)
19
+ @prefix = prefix
20
+ @min = min
21
+ @max = max
22
+ @width = width
23
+
24
+ @section = prefix.nil? ? "default" : prefix
25
+ end
26
+
27
+ def to_ini
28
+ <<-INI
29
+ [histogram_#{@section}]
30
+ prefix=#{@prefix}
31
+ min=#{@min}
32
+ max=#{@max}
33
+ width=#{@width}
34
+ INI
35
+ end
36
+
37
+ def self.validate(h)
38
+ if h.class != Hash
39
+ raise ConfigError, "a type of histogram element must be Hash, but specified as #{h.class}"
40
+ end
41
+
42
+ FIELD.each do |f|
43
+ if not h.has_key?(f)
44
+ raise ConfigError, "histogram element must contain '#{f}'"
45
+ end
46
+ end
47
+
48
+ h.keys.each do |k|
49
+ if not FIELD.member?(k) and not OPTIONAL_FIELD.member?(k)
50
+ raise ConfigError, "invalid histogram hash key: #{k}"
51
+ end
52
+ end
53
+
54
+ FLOATING_FIELD.each do |f|
55
+ cls = h[f].class
56
+ if cls != Fixnum and cls != Float
57
+ raise ConfigError, "#{f} value of histogram must be Fixnum or Float"
58
+ end
59
+ end
60
+
61
+ new(h['prefix'], h['min'], h['max'], h['width'])
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,81 @@
1
+ module Fluent
2
+ module StatsitePlugin
3
+ class Metric
4
+ TYPE = %w(kv g ms h c s)
5
+
6
+ HASH_FIELD = %w(
7
+ type
8
+ key
9
+ key_field
10
+ value
11
+ value_field
12
+ )
13
+
14
+ FIELD = '\w+|\$\{\w+\}'
15
+
16
+ STRING_PATTERN = /^(#{FIELD}):(#{FIELD})\|(#{TYPE.join('|')})$/
17
+ STRING_EXAMPLE = "key_field:value_field|type"
18
+
19
+ def initialize(key, key_field, value, value_field, type)
20
+ @key = key
21
+ @key_field = key_field
22
+ @value = value
23
+ @value_field = value_field
24
+ @type = type
25
+ end
26
+
27
+ def convert(record)
28
+ k = @key.nil? ? record[@key_field] : @key
29
+ v = @value.nil? ? record[@value_field] : @value
30
+ (k.nil? or v.nil?) ? nil : "#{k}:#{v}|#{@type}\n"
31
+ end
32
+
33
+ def to_s
34
+ k = @key.nil? ? "key_field=#{@key_field}" :"key=#{@key}"
35
+ v = @value.nil? ? "value_field=#{@value_field}" :"value=#{@value}"
36
+ "Metric(#{k}, #{v}, type=#{@type})"
37
+ end
38
+
39
+ def self.validate(m)
40
+ if not (m.class == Hash or m.class == String)
41
+ raise ConfigError, "a type of metrics element must be Hash or String, but specified as #{m.class}"
42
+ end
43
+
44
+ case m
45
+ when Hash
46
+ m.keys.each do |k|
47
+ if not HASH_FIELD.member?(k)
48
+ raise ConfigError, "invalid metrics element hash key: #{k}"
49
+ end
50
+ end
51
+
52
+ if not m.has_key?('key') ^ m.has_key?('key_field')
53
+ raise ConfigError, "metrics element must contain either one of 'key' or 'key_field'"
54
+ end
55
+
56
+ if not m.has_key?('value') ^ m.has_key?('value_field')
57
+ raise ConfigError, "metrics element must contain either one of 'value' or 'value_field'"
58
+ end
59
+
60
+ if not m.has_key?('type')
61
+ raise ConfigError, "metrics element must contain 'type'"
62
+ end
63
+
64
+ if not TYPE.member?(m['type'])
65
+ raise ConfigError, "metrics type must be one of the following: #{TYPE.join(' ')}, but specified as #{m['type']}"
66
+ end
67
+
68
+ new(m['key'], m['value_field'], m['value'], m['value_field'], m['type'])
69
+ when String
70
+ if (STRING_PATTERN =~ m).nil?
71
+ raise ConfigError, "metrics string must be #{STRING_PATTERN}, but specified as #{m}"
72
+ end
73
+
74
+ key, key_field = $1.start_with?('$') ? [nil, $1[2..-2]] : [$1, nil]
75
+ value, value_field = $2.start_with?('$') ? [nil, $2[2..-2]] : [$2, nil]
76
+ new(key, key_field, value, value_field, $3)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,62 @@
1
+ require 'test/unit'
2
+ require 'fluent/log'
3
+ require 'fluent/test'
4
+
5
+ $log = Fluent::Log.new(Fluent::Test::DummyLogDevice.new, Fluent::Log::LEVEL_WARN)
6
+
7
+ def records
8
+ [
9
+ {
10
+ "remote_addr" => "114.170.6.118",
11
+ "remote_user" => "-",
12
+ "time_local" => "20/Jul/2014:18:25:50 +0000",
13
+ "request" => "GET /foo HTTP/1.1",
14
+ "status" => "200",
15
+ "body_bytes_sent" => "911",
16
+ "http_referer" => "-",
17
+ "http_user_agent" => "Mozilla/5.0 (Linux; U; Android 4.2.2; ja-jp; SO-04E Build/10.3.1.B.0.256) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
18
+ "request_time" => "0.058",
19
+ "upstream_addr" => "192.168.222.180:80",
20
+ "upstream_response_time" => "0.058"
21
+ },
22
+ {
23
+ "remote_addr" => "180.214.48.86",
24
+ "remote_user" => "-",
25
+ "time_local" => "20/Jul/2014:18:25:50 +0000",
26
+ "request" => "POST /bar HTTP/1.1",
27
+ "status" => "200",
28
+ "body_bytes_sent" => "57",
29
+ "http_referer" => "-",
30
+ "http_user_agent" => "Mozilla/5.0 (Linux; U; Android 4.2.2; ja-jp; SO-04E Build/10.3.1.B.0.256) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
31
+ "request_time" => "0.041",
32
+ "upstream_addr" => "10.0.0.143:80",
33
+ "upstream_response_time" => "0.041"
34
+ },
35
+ {
36
+ "remote_addr" => "153.160.159.80",
37
+ "remote_user" => "-",
38
+ "time_local" => "20/Jul/2014:18:25:50 +0000",
39
+ "request" => "GET /foo HTTP/1.1",
40
+ "status" => "200",
41
+ "body_bytes_sent" => "34139",
42
+ "http_referer" => "/bar",
43
+ "http_user_agent" => "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:30.0) Gecko/20100101 Firefox/30.0",
44
+ "request_time" => "0.000",
45
+ "upstream_addr" => "-",
46
+ "upstream_response_time" => "-"
47
+ },
48
+ {
49
+ "remote_addr" => "172.56.33.226",
50
+ "remote_user" => "-",
51
+ "time_local" => "20/Jul/2014:18:25:50 +0000",
52
+ "request" => "GET /foo HTTP/1.1",
53
+ "status" => "200",
54
+ "body_bytes_sent" => "791",
55
+ "http_referer" => "-",
56
+ "http_user_agent" => "en;Mozilla/5.0 (Linux; U; Android 4.3; en-us; SGH-T999 Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
57
+ "request_time" => "0.073",
58
+ "upstream_addr" => "192.168.222.209:80",
59
+ "upstream_response_time" => "0.073"
60
+ }
61
+ ]
62
+ end
@@ -0,0 +1,25 @@
1
+ require 'helper'
2
+ require 'fluent/plugin/statsite/format'
3
+
4
+ include Fluent::StatsitePlugin
5
+
6
+ class StatsiteFormatterTest < Test::Unit::TestCase
7
+ def setup
8
+ metrics = [
9
+ Metric.validate('${k}:${v}|g'),
10
+ Metric.validate('${k}:v|g')
11
+ ]
12
+ @formatter = StatsiteFormatter.new(metrics)
13
+ end
14
+
15
+ def test_call
16
+ record = {'v' => 'value'}
17
+ assert_equal "", @formatter.call(record)
18
+
19
+ record = {'k' => 'key'}
20
+ assert_equal "key:v|g\n", @formatter.call(record)
21
+
22
+ record = {'k' => 'key', 'v' => 'value'}
23
+ assert_equal "key:value|g\nkey:v|g\n", @formatter.call(record)
24
+ end
25
+ end
@@ -0,0 +1,79 @@
1
+ require 'helper'
2
+ require 'fluent/plugin/statsite/histogram'
3
+
4
+ include Fluent::StatsitePlugin
5
+
6
+ class Histogram
7
+ attr_reader :section, :prefix, :min, :max, :width
8
+ end
9
+
10
+ class HistogramTest < Test::Unit::TestCase
11
+ def valid_config
12
+ {
13
+ 'prefix' => 'pre',
14
+ 'min' => 0.0,
15
+ 'max' => 10.0,
16
+ 'width' => 1.0,
17
+ }
18
+ end
19
+
20
+ def test_validate_object_type
21
+ config = []
22
+ assert_raises(Fluent::ConfigError) { Histogram.validate(config) }
23
+ end
24
+
25
+ def test_validate_mandatory_field
26
+ Histogram::FIELD.each do |f|
27
+ config = valid_config
28
+ config.delete(f)
29
+ assert_raises(Fluent::ConfigError) { Histogram.validate(config) }
30
+ end
31
+ end
32
+
33
+ def test_validate_extra_field
34
+ config = valid_config
35
+ config['foo'] = 'bar'
36
+ assert_raises(Fluent::ConfigError) { Histogram.validate(config) }
37
+ end
38
+
39
+ def test_validate_floating_field
40
+ Histogram::FLOATING_FIELD.each do |f|
41
+ config = valid_config
42
+ config[f] = 'foo'
43
+ assert_raises(Fluent::ConfigError) { Histogram.validate(config) }
44
+ end
45
+ end
46
+
47
+ def test_validate_result
48
+ c = valid_config
49
+ h = Histogram.validate(c)
50
+ assert_equal c['prefix'], h.section
51
+ assert_equal c['prefix'], h.prefix
52
+ assert_equal c['min'], h.min
53
+ assert_equal c['max'], h.max
54
+ assert_equal c['width'], h.width
55
+ end
56
+
57
+ def test_to_init
58
+ c = valid_config
59
+
60
+ ini = Histogram.validate(c).to_ini
61
+ assert_equal ini, <<-INI
62
+ [histogram_#{c['prefix']}]
63
+ prefix=#{c['prefix']}
64
+ min=#{c['min']}
65
+ max=#{c['max']}
66
+ width=#{c['width']}
67
+ INI
68
+
69
+ c.delete('prefix')
70
+ ini = Histogram.validate(c).to_ini
71
+ assert_equal ini, <<-INI
72
+ [histogram_default]
73
+ prefix=
74
+ min=#{c['min']}
75
+ max=#{c['max']}
76
+ width=#{c['width']}
77
+ INI
78
+ end
79
+ end
@@ -0,0 +1,100 @@
1
+ require 'helper'
2
+ require 'fluent/plugin/statsite/metric'
3
+
4
+ include Fluent::StatsitePlugin
5
+
6
+ class Metric
7
+ attr_reader :key, :key_field, :value, :value_field, :type
8
+ end
9
+
10
+ class MetricTest < Test::Unit::TestCase
11
+
12
+ def valid_config
13
+ {'key' => 'k', 'value' => 'v', 'type' => 'kv'}
14
+ end
15
+
16
+ def test_validate_object_type
17
+ config = []
18
+ assert_raises(Fluent::ConfigError) { Metric.validate(config) }
19
+ end
20
+
21
+ def test_validate_extra_field
22
+ config = valid_config
23
+ config['foo'] = 'bar'
24
+ assert_raises(Fluent::ConfigError) { Metric.validate(config) }
25
+ end
26
+
27
+ def test_validate_key
28
+ config = (valid_config)['key_field'] = 'k'
29
+ assert_raises(Fluent::ConfigError) { Metric.validate(config) }
30
+
31
+ config = (valid_config)
32
+ config.delete('key')
33
+ assert_raises(Fluent::ConfigError) { Metric.validate(config) }
34
+ end
35
+
36
+ def test_validate_value
37
+ config = (valid_config)['value_field'] = 'k'
38
+ assert_raises(Fluent::ConfigError) { Metric.validate(config) }
39
+
40
+ config = (valid_config)
41
+ config.delete('value')
42
+ assert_raises(Fluent::ConfigError) { Metric.validate(config) }
43
+ end
44
+
45
+ def test_validate_type
46
+ config = (valid_config)
47
+ config.delete('type')
48
+ assert_raises(Fluent::ConfigError) { Metric.validate(config) }
49
+
50
+ config = (valid_config)
51
+ config['type'] = 'foo'
52
+ assert_raises(Fluent::ConfigError) { Metric.validate(config) }
53
+ end
54
+
55
+ def test_validate_string
56
+ config = "foo"
57
+ assert_raises(Fluent::ConfigError) { Metric.validate(config) }
58
+
59
+ config = "k:v|foo"
60
+ assert_raises(Fluent::ConfigError) { Metric.validate(config) }
61
+ end
62
+
63
+ def test_validate_result
64
+ m = Metric.validate(valid_config)
65
+ assert_equal 'k', m.key
66
+ assert_nil m.key_field
67
+ assert_equal 'v', m.value
68
+ assert_nil m.value_field
69
+ assert_equal 'kv', m.type
70
+ end
71
+
72
+ def test_validate_result_string
73
+ m = Metric.validate('k:v|kv')
74
+ assert_equal 'k', m.key
75
+ assert_nil m.key_field
76
+ assert_equal 'v', m.value
77
+ assert_nil m.value_field
78
+ assert_equal 'kv', m.type
79
+
80
+ m = Metric.validate('${k}:${v}|kv')
81
+ assert_nil m.key
82
+ assert_equal 'k', m.key_field
83
+ assert_nil m.value
84
+ assert_equal 'v', m.value_field
85
+ assert_equal 'kv', m.type
86
+ end
87
+
88
+ def test_convert
89
+ m = Metric.validate('${k}:${v}|kv')
90
+
91
+ record = {'k' => 'key'}
92
+ assert_nil m.convert(record)
93
+
94
+ record = {'v' => 'value'}
95
+ assert_nil m.convert(record)
96
+
97
+ record = {'k' => 'key', 'v' => 'value'}
98
+ assert_equal "key:value|kv\n", m.convert(record)
99
+ end
100
+ end
@@ -0,0 +1,65 @@
1
+ require 'helper'
2
+ require 'fluent/plugin/out_statsite'
3
+
4
+ class Fluent::StatsiteOutput
5
+ attr_reader :respawns
6
+ end
7
+
8
+ class StatsiteOutputTest < Test::Unit::TestCase
9
+ def setup
10
+ Fluent::Test.setup
11
+ end
12
+
13
+ ROOT = File.expand_path('..', File.dirname(__FILE__))
14
+ PATH = ROOT + '/vendor/statsite/statsite'
15
+
16
+ CONFIG = %[
17
+ type statsite
18
+ tag statsite
19
+ metrics [
20
+ "${status}:1|c"
21
+ ]
22
+ histograms [
23
+ {"prefix": "k", "min": 0, "max": 10, "width": 1.0}
24
+ ]
25
+ statsite_path "#{PATH}"
26
+ statsite_flush_interval 1s
27
+ timer_eps 0.01
28
+ set_eps 0.02
29
+ child_respawn 5
30
+ ]
31
+
32
+ RECORDS = [
33
+ ]
34
+
35
+ def create_driver(conf = CONFIG)
36
+ Fluent::Test::OutputTestDriver.new(Fluent::StatsiteOutput).configure(conf, true)
37
+ end
38
+
39
+ def test_configure
40
+ d = create_driver
41
+
42
+ assert_equal 'statsite', d.instance.tag
43
+ assert d.instance.metrics.all?{|m| m.class == Fluent::StatsitePlugin::Metric}
44
+ assert d.instance.histograms.all?{|m| m.class == Fluent::StatsitePlugin::Histogram}
45
+ assert_equal PATH, d.instance.statsite_path
46
+ assert_equal 1, d.instance.statsite_flush_interval
47
+ assert_equal 0.01, d.instance.timer_eps
48
+ assert_equal 0.02, d.instance.set_eps
49
+ assert_equal 5, d.instance.respawns
50
+ end
51
+
52
+ def test_emit
53
+ d = create_driver
54
+
55
+ d.run do
56
+ records.each { |r| d.emit(r, Time.now) }
57
+ end
58
+
59
+ emits = d.emits
60
+
61
+ count_result = emits.pop
62
+ assert_equal 'statsite', count_result[0]
63
+ assert_equal({type: 'counts', key: '200', value: 4.0}, count_result[2])
64
+ end
65
+ end
@@ -0,0 +1,87 @@
1
+ require 'helper'
2
+ require 'fluent/plugin/statsite/format'
3
+
4
+ include Fluent::StatsitePlugin
5
+
6
+ class StatsiteParserTest < Test::Unit::TestCase
7
+ def setup
8
+ @res = []
9
+ proc = Proc.new {|time, record| @res << {time: time, record:record } }
10
+ @parser = StatsiteParser.new(proc)
11
+ end
12
+
13
+ def test_eachline_kv
14
+ line = 'kv.k|1.000000|1405869579'
15
+ @parser.each_line(line)
16
+ expected = {
17
+ time: 1405869579,
18
+ record: { type: 'kv', key: 'k', value: 1.000000 }
19
+ }
20
+ assert_equal expected, @res.pop
21
+ end
22
+
23
+ def test_eachline_gauge
24
+ line = 'gauges.k|1.000000|1405869579'
25
+ @parser.each_line(line)
26
+ expected = {
27
+ time: 1405869579,
28
+ record: { type: 'gauges', key: 'k', value: 1.000000 }
29
+ }
30
+ assert_equal expected, @res.pop
31
+ end
32
+
33
+ def test_eachline_counts
34
+ line = 'counts.k|1.000000|1405869579'
35
+ @parser.each_line(line)
36
+ expected = {
37
+ time: 1405869579,
38
+ record: { type: 'counts', key: 'k', value: 1.000000 }
39
+ }
40
+ assert_equal expected, @res.pop
41
+ end
42
+
43
+ def test_eachline_sets
44
+ line = 'sets.k|1|1405869579'
45
+ @parser.each_line(line)
46
+ expected = {
47
+ time: 1405869579,
48
+ record: { type: 'sets', key: 'k', value: 1 }
49
+ }
50
+ assert_equal expected, @res.pop
51
+ end
52
+
53
+ def test_eachline_timers
54
+ line = 'timers.k.sum|1.000000|1405869579'
55
+ @parser.each_line(line)
56
+ expected = {
57
+ time: 1405869579,
58
+ record: { type: 'timers', key: 'k', value: 1.000000, statistic: 'sum' }
59
+ }
60
+ assert_equal expected, @res.pop
61
+ end
62
+
63
+ def test_eachline_timers_count
64
+ line = 'timers.k.count|1|1405869579'
65
+ @parser.each_line(line)
66
+ expected = {
67
+ time: 1405869579,
68
+ record: { type: 'timers', key: 'k', value: 1, statistic: 'count' }
69
+ }
70
+ assert_equal expected, @res.pop
71
+ end
72
+
73
+ def test_eachline_timers_histogram
74
+ line = 'timers.k.histogram.bin_<0.00|0|1405869579'
75
+ @parser.each_line(line)
76
+ expected = {
77
+ time: 1405869579,
78
+ record: { type: 'timers', key: 'k', value: 0, statistic: 'histogram', range: '<0.00' }
79
+ }
80
+ assert_equal expected, @res.pop
81
+ end
82
+
83
+ def test_eachline_invalid_line
84
+ line = 'test'
85
+ assert_raise(RuntimeError) { @parser.each_line(line) }
86
+ end
87
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-statsite
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - OKUNO Akihiro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-07-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: fluentd
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Fluentd plugin which caluculate statistics using statsite
56
+ email:
57
+ - choplin.choplin@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".gitmodules"
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - example.conf
69
+ - fluent-plugin-statsite.gemspec
70
+ - lib/fluent/plugin/out_statsite.rb
71
+ - lib/fluent/plugin/statsite/child_process.rb
72
+ - lib/fluent/plugin/statsite/format.rb
73
+ - lib/fluent/plugin/statsite/histogram.rb
74
+ - lib/fluent/plugin/statsite/metric.rb
75
+ - test/helper.rb
76
+ - test/test_formatter.rb
77
+ - test/test_histogram.rb
78
+ - test/test_metric.rb
79
+ - test/test_out_statsite.rb
80
+ - test/test_parser.rb
81
+ homepage: https://github.com/choplin/fluent-plugin-statsite
82
+ licenses:
83
+ - Apache-2.0
84
+ metadata: {}
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 2.2.2
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: Fluentd statsite plugin
105
+ test_files:
106
+ - test/helper.rb
107
+ - test/test_formatter.rb
108
+ - test/test_histogram.rb
109
+ - test/test_metric.rb
110
+ - test/test_out_statsite.rb
111
+ - test/test_parser.rb