fluent-plugin-hrforecast 0.0.1

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: 9ea1bb5340dadf01695d2d08bf52cdf42eb74794
4
+ data.tar.gz: 5107bdce0db47829d7cb3d4dfe7fa9199c363fad
5
+ SHA512:
6
+ metadata.gz: 894030e2d9e94eea7e4933e54d70b20bd71f4816d0181a1e71c998cda4bfda1921996804b4137c01fd09e5e0e207bf92beb2ad6d8c4ef49a9eec0e890f486727
7
+ data.tar.gz: 26f8d3040c64e8b633f977e59013f108c72e670f818fd43d4b7e9eb7bf820544bbc613525ee9b70a8ff1618e9d99ebb554fcbd31e119f62bbb3cc8e855da469a
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ service_name: travis-ci
data/.gitignore ADDED
@@ -0,0 +1,17 @@
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
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.1
6
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fluent-plugin-hrforecast.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "{}"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2014 do_aki
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Fluent::Plugin::Hrforecast
2
+
3
+ fluentd output plugin for HRForecast
4
+
5
+ ## Overview
6
+ Fluented output plugin for sending metrics to [HRForecast](https://github.com/kazeburo/HRForecast).
7
+ This plugin code was wittern based on [fluent-plugin-growthforecast](https://github.com/tagomoris/fluent-plugin-growthforecast)
8
+
9
+ ## Configuration
10
+
11
+ ### Sample2
12
+ Tag is `metrics` and message is `{'field1':10, 'field2':200, 'others':3000}`.
13
+
14
+ By follow config, send 2 numbers and time specified input plugin to HRForecast.
15
+ * send 10 and time to http://hrforecast.local/api/service/section/metrics_field1
16
+ * send 200 and time to http://hrforecast.local/api/service/section/metrics_field2
17
+
18
+ ```
19
+ <match metrics>
20
+ type hrforecast
21
+ hrfapi_url http://hrforecast.local/api/
22
+ graph_path service/section/${tag}_${key_name}
23
+ name_keys field1,field2
24
+ </match>
25
+ ```
26
+
27
+
28
+ ### Sample2
29
+ Tag is `metrics` and message is `{'date_field':'2014/05/23', 'field_hoge':89.3}`.
30
+
31
+ By follow config, send a floating number and formatted date to HRForecast.
32
+ * send 89.3 and '2014-05-23 00:00:00' to http://hrforecast.local/api/service/metrics/hoge
33
+
34
+ ```
35
+ <match metrics>
36
+ type hrforecast
37
+ hrfapi_url http://hrforecast.local/api/
38
+ graph_path service/${tag}/${key_name}
39
+ name_key_pattern ^field_(.*)$
40
+ enable_float_number true
41
+ datetime_key date_field
42
+ datetime_key_format %Y/%m/%d
43
+ datetime_format %Y-%m-%d %H:%M:%S
44
+ </match>
45
+ ```
46
+
47
+
48
+ ## Options
49
+
50
+ * __hrfapi_url__ (required)
51
+
52
+ URL of HRForecast API base like 'http://hrforecast.local/api/'
53
+
54
+ * __graph_path__ (required)
55
+
56
+ Graph Path of HRForecast API endpoint include service_name, section_name and graph_name separated slash.
57
+ e.g. `service/section/graph`
58
+
59
+ You can customize graph_path using `${tag}` and `${key_name}` placeholders.
60
+
61
+ * __name_keys__ (Either of this or __name_key_pattern__ is required)
62
+ Specify field names of sending number. Separate by comma.
63
+
64
+ * __name_key_pattern__ (Either of this or __name_keys__ is required)
65
+ Specify field names of sending number.
66
+
67
+ * __datetime_key__ (default: none)
68
+ Specify field names of sending datetime if you need.
69
+
70
+ * __datetime_key_format__ (required if specified __datetime_key__)
71
+ Parse format for datetime of field specified datetime_key
72
+
73
+ * __datetime_format__ (default: %Y-%m-%d %H:%M:%S %z)
74
+ Format of datetime POST parameter.
75
+
76
+ * __remove_prefix__ (default:none)
77
+
78
+ * __background_post__ (default:false)
79
+
80
+ * __ssl__ (default:false)
81
+
82
+ * __verify_ssl__ (default:false)
83
+
84
+ * __timeout__
85
+
86
+ * __retry__ (default: true)
87
+
88
+ * __keepalive__ (default: true)
89
+
90
+ * __enable_float_number__ (default: false)
91
+
92
+ * __authentication__ (default:none)
93
+
94
+ * __username__ (default:empty)
95
+
96
+ * __password__ (default:empty)
97
+
98
+
99
+ ## License
100
+ Apache License, Version 2.0
101
+
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |test|
5
+ test.libs << 'lib'
6
+ test.libs << 'test'
7
+ test.pattern = 'test/**/test_*.rb'
8
+ test.verbose = true
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "fluent-plugin-hrforecast"
5
+ spec.version = "0.0.1"
6
+ spec.authors = ["do-aki"]
7
+ spec.email = ["do.hiroaki@gmail.com"]
8
+ spec.description = %q{For HRForecast}
9
+ spec.summary = %q{Fluentd output plugin to post to HRForecast}
10
+ spec.homepage = "https://github.com/do-aki/fluent-plugin-hrforecast"
11
+ spec.license = "APLv2"
12
+
13
+ spec.files = `git ls-files`.split($/)
14
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
15
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
+ spec.require_paths = ["lib"]
17
+
18
+ #spec.add_development_dependency "bundler", "~> 1.3"
19
+ spec.add_development_dependency "rake"
20
+ spec.add_development_dependency "simplecov"
21
+ spec.add_development_dependency "coveralls"
22
+ spec.add_development_dependency "net-empty_port"
23
+ spec.add_runtime_dependency "fluent-plugin-growthforecast"
24
+ end
@@ -0,0 +1,254 @@
1
+
2
+ class Fluent::HRForecastOutput < Fluent::Output
3
+ Fluent::Plugin.register_output('hrforecast', self)
4
+
5
+
6
+ def initialize
7
+ super
8
+ require 'net/http'
9
+ require 'uri'
10
+ require 'resolve/hostname'
11
+ end
12
+
13
+ config_param :hrfapi_url, :string
14
+ config_param :graph_path, :string
15
+
16
+ config_param :name_keys, :string, :default => nil
17
+ config_param :name_key_pattern, :string, :default => nil
18
+
19
+ config_param :datetime_key, :string, :default => nil
20
+ config_param :datetime_key_format, :string, :default => nil
21
+ config_param :datetime_format, :string, :default => '%Y-%m-%d %H:%M:%S %z'
22
+
23
+ config_param :remove_prefix, :string, :default => nil
24
+
25
+ config_param :background_post, :bool, :default => false
26
+
27
+ config_param :ssl, :bool, :default => false
28
+ config_param :verify_ssl, :bool, :default => false
29
+
30
+ config_param :timeout, :integer, :default => nil # default 60secs
31
+ config_param :retry, :bool, :default => true
32
+ config_param :keepalive, :bool, :default => true
33
+ config_param :enable_float_number, :bool, :default => false
34
+
35
+ config_param :authentication, :string, :default => nil # nil or 'none' or 'basic'
36
+ config_param :username, :string, :default => ''
37
+ config_param :password, :string, :default => ''
38
+
39
+ # Define `log` method for v0.10.42 or earlier
40
+ unless method_defined?(:log)
41
+ define_method("log") { $log }
42
+ end
43
+
44
+ def configure(conf)
45
+ super
46
+
47
+ if @hrfapi_url !~ %r|/api/\z|
48
+ raise Fluent::ConfigError, "hrfapi_url must end with /api/"
49
+ end
50
+ if @graph_path !~ %r|\A[^/]+/[^/]+/[^/]+\z|
51
+ raise Fluent::ConfigError, "graph_path #{@graph_path} must be like 'service/section/${tag}_${key_name}'"
52
+ end
53
+
54
+ if @name_keys.nil? and @name_key_pattern.nil?
55
+ raise Fluent::ConfigError, "missing both of name_keys and name_key_pattern"
56
+ end
57
+ if not @name_keys.nil? and not @name_key_pattern.nil?
58
+ raise Fluent::ConfigError, "cannot specify both of name_keys and name_key_pattern"
59
+ end
60
+
61
+ if not @datetime_key.nil? and @datetime_key_format.nil?
62
+ raise Fluent::ConfigError, "missing datetime_key_format"
63
+ end
64
+
65
+ url = URI.parse(@hrfapi_url)
66
+ @host = url.host
67
+ @port = url.port
68
+
69
+ if @name_keys
70
+ @name_keys = Hash[
71
+ @name_keys.split(',').map{|k|
72
+ k.split('=>',2).tap{|kv|
73
+ kv.push(kv[0]) if kv.size == 1
74
+ }
75
+ }
76
+ ]
77
+ end
78
+
79
+ if @name_key_pattern
80
+ @name_key_pattern = Regexp.new(@name_key_pattern)
81
+ end
82
+
83
+ if @remove_prefix
84
+ @removed_prefix_string = @remove_prefix + '.'
85
+ @removed_length = @removed_prefix_string.length
86
+ end
87
+
88
+ @auth = case @authentication
89
+ when 'basic' then :basic
90
+ else
91
+ :none
92
+ end
93
+ @resolver = Resolve::Hostname.new(:system_resolver => true)
94
+ end
95
+
96
+ class PostThread
97
+ attr :queue
98
+
99
+ def initialize(plugin)
100
+ require 'thread'
101
+ @queue = Queue.new
102
+ @plugin = plugin
103
+ @thread = Thread.new do
104
+ begin
105
+ post(@queue.deq) while true
106
+ ensure
107
+ post(@queue.deq) while not @queue.empty?
108
+ end
109
+ end
110
+ end
111
+
112
+ def post(events)
113
+ begin
114
+ @plugin.post_events(events) if events.size > 0
115
+ rescue => e
116
+ @plugin.log.warn "HTTP POST in background Error occures to HRforecast server", :error_class => e.class, :error => e.message
117
+ end
118
+ end
119
+
120
+ def shutdown
121
+ @thread.terminate
122
+ @thread.join
123
+ end
124
+ end
125
+
126
+ def start
127
+ super
128
+
129
+ @post_thread = nil
130
+ if @background_post
131
+ @post_thread = PostThread.new(self)
132
+ end
133
+ end
134
+
135
+ def shutdown
136
+ if @post_thread
137
+ @post_thread.shutdown
138
+ end
139
+ super
140
+ end
141
+
142
+ def placeholder_mapping(tag, name)
143
+ if @remove_prefix and
144
+ ( (tag.start_with?(@removed_prefix_string) and tag.length > @removed_length) or tag == @remove_prefix)
145
+ tag = tag[@removed_length..-1]
146
+ end
147
+ {'${tag}' => tag, '${key_name}' => name}
148
+ end
149
+
150
+ def format_url(tag, name)
151
+ graph_path = @graph_path.gsub(/(\${[_a-z]+})/, placeholder_mapping(tag, name))
152
+ return @hrfapi_url + URI.escape(graph_path)
153
+ end
154
+
155
+ def make_http_connection()
156
+ http = Net::HTTP.new(@resolver.getaddress(@host), @port)
157
+ if @timeout
158
+ http.open_timeout = @timeout
159
+ http.read_timeout = @timeout
160
+ end
161
+ if @ssl
162
+ http.use_ssl = true
163
+ unless @verify_ssl
164
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
165
+ end
166
+ end
167
+ http
168
+ end
169
+
170
+ def make_post_request(tag, name, value, time)
171
+ url = URI.parse(format_url(tag,name))
172
+ req = Net::HTTP::Post.new(url.path)
173
+ if @auth and @auth == :basic
174
+ req.basic_auth(@username, @password)
175
+ end
176
+ req['Host'] = url.host
177
+ req['Connection'] = 'Keep-Alive' if @keepalive
178
+ req.set_form_data({
179
+ 'number' => @enable_float_number ? value.to_f : value.to_i,
180
+ 'datetime' => Time.at(time).strftime(@datetime_format),
181
+ })
182
+ req
183
+ end
184
+
185
+ def post_events(events)
186
+ return if events.size < 1
187
+
188
+ requests = events.map do |e|
189
+ make_post_request(e[:tag], e[:name], e[:value], e[:time])
190
+ end
191
+
192
+ http = make_http_connection()
193
+ requests.each do |req|
194
+ begin
195
+ http.start unless http.started?
196
+ res = http.request(req)
197
+ unless res and res.is_a?(Net::HTTPSuccess)
198
+ log.warn "failed to post to HRforecast: #{host}:#{port}#{req.path}, post_data: #{req.body} code: #{res && res.code}"
199
+ end
200
+ rescue IOError, EOFError, Errno::ECONNRESET, Errno::ETIMEDOUT, SystemCallError
201
+ log.warn "net/http POST raises exception: #{$!.class}, '#{$!.message}'"
202
+ http.finish if http.started?
203
+ end
204
+ if not @keepalive and http.started?
205
+ http.finish
206
+ end
207
+ end
208
+ end
209
+
210
+ def decide_time(time, record)
211
+ if @datetime_key && record[@datetime_key]
212
+ time = Time.strptime(record[@datetime_key], @datetime_key_format)
213
+ end
214
+ time
215
+ end
216
+
217
+ def emit(tag, es, chain)
218
+ events = []
219
+ if @name_keys
220
+ es.each {|time,record|
221
+ time = decide_time(time, record)
222
+ @name_keys.each {|key, name|
223
+ if value = record[key]
224
+ events.push({:tag => tag, :name => name, :value => value, :time => time})
225
+ end
226
+ }
227
+ }
228
+ else # for name_key_pattern
229
+ es.each {|time,record|
230
+ time = decide_time(time, record)
231
+ record.keys.each {|key|
232
+ if @name_key_pattern.match(key) and record[key]
233
+ name = Regexp.last_match(1) || key
234
+ events.push({:tag => tag, :name => name, :value => record[key], :time => time})
235
+ end
236
+ }
237
+ }
238
+ end
239
+
240
+ if @post_thread
241
+ @post_thread.queue << events
242
+ else
243
+ begin
244
+ post_events(events)
245
+ rescue => e
246
+ log.warn "HTTP POST Error occures to HRforecast server", :error_class => e.class, :error => e.message
247
+ raise if @retry
248
+ end
249
+ end
250
+
251
+ chain.next
252
+ end
253
+ end
254
+
@@ -0,0 +1,416 @@
1
+ require 'helper'
2
+
3
+ class HRForecastTest < Test::Unit::TestCase
4
+ def setup
5
+ Fluent::Test.setup
6
+ end
7
+
8
+ def create_driver(conf, tag='test.metrics')
9
+ Fluent::Test::OutputTestDriver.new(Fluent::HRForecastOutput, tag).configure(conf)
10
+ end
11
+
12
+ def test_config_default
13
+ d = create_driver(%q[
14
+ hrfapi_url http://127.0.0.1:5125/api/
15
+ graph_path service/metrics/${tag}_${key_name}
16
+ name_keys field1,field2,field3=>otherfield
17
+ ])
18
+ assert_equal 'http://127.0.0.1:5125/api/', d.instance.hrfapi_url
19
+ assert_equal '127.0.0.1', d.instance.instance_eval{ @host }
20
+ assert_equal 5125, d.instance.instance_eval{ @port }
21
+ assert_equal 'service/metrics/${tag}_${key_name}', d.instance.graph_path
22
+ assert_equal _={'field1'=>'field1', 'field2'=>'field2', 'field3'=>'otherfield'}, d.instance.name_keys
23
+ assert_equal '%Y-%m-%d %H:%M:%S %z', d.instance.datetime_format
24
+ assert_nil d.instance.datetime_key
25
+ assert_nil d.instance.datetime_key_format
26
+ assert_nil d.instance.remove_prefix
27
+ assert_equal 'http://127.0.0.1:5125/api/service/metrics/test.data1_field1', d.instance.format_url('test.data1', 'field1')
28
+ end
29
+
30
+ def test_config_name_key_pattern
31
+ config = %q[
32
+ hrfapi_url http://127.0.0.1:5125/api/
33
+ graph_path service/metrics/${tag}_${key_name}
34
+ remove_prefix test
35
+ name_key_pattern ^(field|key)\d+$
36
+ ]
37
+
38
+ d = create_driver(config)
39
+ assert_equal Regexp.new('^(field|key)\d+$'), d.instance.name_key_pattern
40
+ end
41
+
42
+ def test_config_remove_prefix
43
+ config = %q[
44
+ hrfapi_url http://127.0.0.1:5125/api/
45
+ graph_path service/metrics/${tag}_${key_name}
46
+ remove_prefix test
47
+ name_keys field
48
+ ]
49
+
50
+ d = create_driver(config)
51
+ assert_equal 'test', d.instance.remove_prefix
52
+ assert_equal 'test.', d.instance.instance_eval{ @removed_prefix_string }
53
+ end
54
+
55
+ def test_bad_config_invalid_hrfapi_url
56
+ config = %q[
57
+ hrfapi_url http://example.com/
58
+ graph_path service/metrics/${tag}_${key_name}
59
+ name_keys field1,field2,otherfield
60
+ ]
61
+ assert_raise(Fluent::ConfigError) {
62
+ d = create_driver(config)
63
+ }
64
+ end
65
+
66
+ def test_bad_config_invalid_graph_path
67
+ config = %q[
68
+ hrfapi_url http://127.0.0.1:5125/api/
69
+ graph_path service//${tag}_${key_name}
70
+ name_keys field1,field2,otherfield
71
+ ]
72
+ assert_raise(Fluent::ConfigError) {
73
+ d = create_driver(config)
74
+ }
75
+ end
76
+
77
+ def test_bad_config_missing_name_keys
78
+ config = %q[
79
+ hrfapi_url http://127.0.0.1:5125/api/
80
+ graph_path service/metrics/${tag}_${key_name}
81
+ ]
82
+ assert_raise(Fluent::ConfigError) {
83
+ d = create_driver(config)
84
+ }
85
+ end
86
+
87
+ def test_bad_config_specify_both
88
+ config = %q[
89
+ hrfapi_url http://127.0.0.1:5125/api/
90
+ graph_path service/metrics/${tag}_${key_name}
91
+ name_keys field1,field2,otherfield
92
+ name_key_pattern ^field.*$
93
+ ]
94
+ assert_raise(Fluent::ConfigError) {
95
+ d = create_driver(config)
96
+ }
97
+ end
98
+
99
+ def test_bad_config_missing_datetime_key_format
100
+ config = %q[
101
+ hrfapi_url http://127.0.0.1:5125/api/
102
+ graph_path service/metrics/${tag}_${key_name}
103
+ name_keys field1,field2,otherfield
104
+ datetime_key time_field
105
+ ]
106
+ assert_raise(Fluent::ConfigError) {
107
+ d = create_driver(config)
108
+ }
109
+ end
110
+ end
111
+
112
+
113
+
114
+ class HRForecastWithDummryServerTest < Test::Unit::TestCase
115
+
116
+ def setup
117
+ require 'net/http'
118
+ @server = DummyServer.new
119
+ end
120
+
121
+ def teardown
122
+ @server.shutdown
123
+ end
124
+
125
+ def create_driver(conf, tag='test.metrics')
126
+ Fluent::Test::OutputTestDriver.new(Fluent::HRForecastOutput, tag).configure(conf)
127
+ end
128
+
129
+ def test_0dummyserver
130
+ res = Net::HTTP.start('127.0.0.1', @server.port) {|http|
131
+ http.request(Net::HTTP::Get.new('/'))
132
+ }
133
+
134
+ assert_equal '200', res.code
135
+ assert_equal 'running', res.body
136
+ end
137
+
138
+ def test_0dummyserver_invalid_request
139
+ res = Net::HTTP.start('127.0.0.1', @server.port) {|http|
140
+ http.request(Net::HTTP::Get.new('/api'))
141
+ }
142
+
143
+ assert_equal '405', res.code
144
+ assert_equal 'invalid request method', res.body
145
+ end
146
+
147
+ def test_0dummyserver_auth_failure
148
+ @server.setAuth('user', 'pass')
149
+
150
+ res = Net::HTTP.start('127.0.0.1', @server.port) {|http|
151
+ http.request(Net::HTTP::Post.new('/api'))
152
+ }
153
+
154
+ assert_equal '403', res.code
155
+ end
156
+
157
+ def basic_assertion(config, wait_post_thread=false)
158
+ time = Time.now()
159
+ d = create_driver(config, 'test.metrics')
160
+
161
+ d.run do
162
+ d.emit({'field1' => 50, 'field2'=> 20, 'field3' => 10, 'otherfield' => 1}, time)
163
+ sleep 0.3 if wait_post_thread
164
+ end
165
+
166
+ assert_equal 3, @server.posted.size
167
+ v1st = @server.posted[0]
168
+ v2nd = @server.posted[1]
169
+ v3rd = @server.posted[2]
170
+ time_str = time.strftime('%Y-%m-%d %H:%M:%S %z')
171
+
172
+
173
+ assert_equal '50', v1st[:data][:number]
174
+ assert_equal time_str, v1st[:data][:datetime]
175
+ assert_nil v1st[:auth]
176
+ assert_equal 'service/metrics/test.metrics_field1', v1st[:path]
177
+
178
+ assert_equal '20', v2nd[:data][:number]
179
+ assert_equal time_str, v1st[:data][:datetime]
180
+ assert_equal 'service/metrics/test.metrics_field2', v2nd[:path]
181
+
182
+ assert_equal '1', v3rd[:data][:number]
183
+ assert_equal time_str, v1st[:data][:datetime]
184
+ assert_equal 'service/metrics/test.metrics_otherfield', v3rd[:path]
185
+ end
186
+
187
+ def test_emit
188
+ basic_assertion %[
189
+ hrfapi_url http://127.0.0.1:#{@server.port}/api/
190
+ graph_path service/metrics/${tag}_${key_name}
191
+ name_keys field1,field2,otherfield
192
+ ]
193
+ end
194
+
195
+ def test_emit_pattern
196
+ basic_assertion %[
197
+ hrfapi_url http://127.0.0.1:#{@server.port}/api/
198
+ graph_path service/metrics/${tag}_${key_name}
199
+ name_key_pattern ^(?:field[1|2])|(?:field)$
200
+ ]
201
+ end
202
+
203
+ def test_non_keepalive
204
+ basic_assertion %[
205
+ hrfapi_url http://127.0.0.1:#{@server.port}/api/
206
+ graph_path service/metrics/${tag}_${key_name}
207
+ name_keys field1,field2,otherfield
208
+ keepalive false
209
+ ]
210
+ end
211
+
212
+ def test_threading
213
+ basic_assertion %[
214
+ hrfapi_url http://127.0.0.1:#{@server.port}/api/
215
+ graph_path service/metrics/${tag}_${key_name}
216
+ name_keys field1,field2,otherfield
217
+ background_post true
218
+ ], true
219
+ end
220
+
221
+ def test_threading_non_keepalive
222
+ basic_assertion %[
223
+ hrfapi_url http://127.0.0.1:#{@server.port}/api/
224
+ graph_path service/metrics/${tag}_${key_name}
225
+ name_keys field1,field2,otherfield
226
+ background_post true
227
+ keepalive false
228
+ ], true
229
+ end
230
+
231
+ def test_graphpath
232
+ config = %[
233
+ hrfapi_url http://127.0.0.1:#{@server.port}/api/
234
+ graph_path ${tag}/${key_name}_${tag}/${tag}_${key_name}
235
+ name_keys field
236
+ ]
237
+
238
+ d = create_driver(config, 'tag')
239
+ d.run do
240
+ d.emit({'field' => 50, 'otherfield' => 10})
241
+ end
242
+
243
+ assert_equal 1, @server.posted.size
244
+ v1 = @server.posted[0]
245
+
246
+ assert_equal '50', v1[:data][:number]
247
+ assert_equal 'tag/field_tag/tag_field', v1[:path]
248
+ end
249
+
250
+ def test_remove_tag
251
+ config = %[
252
+ hrfapi_url http://127.0.0.1:#{@server.port}/api/
253
+ graph_path service/metrics/${tag}_${key_name}
254
+ name_keys field
255
+ remove_prefix removed
256
+ ]
257
+
258
+ d = create_driver(config, 'removed.tag')
259
+ d.run do
260
+ d.emit({'field' => 50, 'otherfield' => 10})
261
+ end
262
+
263
+ assert_equal 1, @server.posted.size
264
+ v1 = @server.posted[0]
265
+
266
+ assert_equal '50', v1[:data][:number]
267
+ assert_equal 'service/metrics/tag_field', v1[:path]
268
+ end
269
+
270
+ def test_enable_float_number
271
+ config = %[
272
+ hrfapi_url http://127.0.0.1:#{@server.port}/api/
273
+ graph_path service/metrics/${tag}_${key_name}
274
+ name_keys field
275
+ enable_float_number true
276
+ ]
277
+
278
+ d = create_driver(config, 'removed.tag')
279
+ d.run do
280
+ d.emit({'field' => 50.1, 'otherfield' => 10})
281
+ end
282
+
283
+ assert_equal 1, @server.posted.size
284
+ v1 = @server.posted[0]
285
+
286
+ assert_equal '50.1', v1[:data][:number]
287
+ end
288
+
289
+ def test_name_keys_mapping
290
+ config = %[
291
+ hrfapi_url http://127.0.0.1:#{@server.port}/api/
292
+ graph_path service/metrics/${tag}_${key_name}
293
+ name_keys field1=>name,field2
294
+ ]
295
+
296
+ d = create_driver(config, 'test.metrics')
297
+ d.run do
298
+ d.emit({'field1' => 50, 'field2' => 20, 'otherfield' => 10})
299
+ end
300
+
301
+ assert_equal 2, @server.posted.size
302
+ v1 = @server.posted[0]
303
+ v2 = @server.posted[1]
304
+
305
+ assert_equal '50', v1[:data][:number]
306
+ assert_equal 'service/metrics/test.metrics_name', v1[:path]
307
+
308
+ assert_equal '20', v2[:data][:number]
309
+ assert_equal 'service/metrics/test.metrics_field2', v2[:path]
310
+ end
311
+
312
+ def test_name_pattern_mapping
313
+ config = %[
314
+ hrfapi_url http://127.0.0.1:#{@server.port}/api/
315
+ graph_path service/metrics/${tag}_${key_name}
316
+ name_key_pattern field(\\d)
317
+ ]
318
+
319
+ d = create_driver(config, 'test.metrics')
320
+ d.run do
321
+ d.emit({'field1' => 50, 'field2' => 20, 'otherfield' => 10})
322
+ end
323
+
324
+ assert_equal 2, @server.posted.size
325
+ v1 = @server.posted[0]
326
+ v2 = @server.posted[1]
327
+
328
+ assert_equal '50', v1[:data][:number]
329
+ assert_equal 'service/metrics/test.metrics_1', v1[:path]
330
+
331
+ assert_equal '20', v2[:data][:number]
332
+ assert_equal 'service/metrics/test.metrics_2', v2[:path]
333
+ end
334
+
335
+ def test_datetime_format
336
+ config = %[
337
+ hrfapi_url http://127.0.0.1:#{@server.port}/api/
338
+ graph_path service/metrics/${tag}_${key_name}
339
+ name_keys field
340
+ datetime_format %Y%m%d
341
+ ]
342
+
343
+ time = Time.now()
344
+ d = create_driver(config, 'tag')
345
+ d.run do
346
+ d.emit({'field' => 50}, time)
347
+ end
348
+
349
+ assert_equal 1, @server.posted.size
350
+ v1 = @server.posted[0]
351
+
352
+ assert_equal '50', v1[:data][:number]
353
+ assert_equal time.strftime('%Y%m%d'), v1[:data][:datetime]
354
+ end
355
+
356
+ def test_datetime_key
357
+ config = %[
358
+ hrfapi_url http://127.0.0.1:#{@server.port}/api/
359
+ graph_path service/metrics/${tag}_${key_name}
360
+ name_keys field
361
+ datetime_format %Y%m%d
362
+ datetime_key time_field
363
+ datetime_key_format %Y/%m/%d
364
+ ]
365
+
366
+ d = create_driver(config, 'tag')
367
+ d.run do
368
+ d.emit({'time_field' => '2014/05/18', 'field' => 50})
369
+ end
370
+
371
+ assert_equal 1, @server.posted.size
372
+ v1 = @server.posted[0]
373
+
374
+ assert_equal '50', v1[:data][:number]
375
+ assert_equal '20140518', v1[:data][:datetime]
376
+ end
377
+
378
+ def test_auth
379
+ @server.setAuth('user1', 'password!')
380
+
381
+ config = %[
382
+ hrfapi_url http://127.0.0.1:#{@server.port}/api/
383
+ graph_path service/metrics/${tag}_${key_name}
384
+ name_keys field
385
+ authentication basic
386
+ username user1
387
+ password password!
388
+ ]
389
+
390
+ d = create_driver(config, 'tag')
391
+ d.run do
392
+ d.emit({'field' => 50})
393
+ end
394
+
395
+ assert_equal 1, @server.posted.size
396
+ end
397
+
398
+
399
+ def test_bad_connection
400
+ config = %[
401
+ hrfapi_url http://127.0.0.1:#{@server.port}/api/
402
+ graph_path service/metrics/${tag}_${key_name}
403
+ name_keys field
404
+ timeout 1
405
+ ]
406
+ @server.shutdown
407
+
408
+ assert_nothing_raised do
409
+ d = create_driver(config, 'removed.tag')
410
+ d.run do
411
+ d.emit({'field' => 50, 'otherfield' => 10})
412
+ end
413
+ end
414
+
415
+ end
416
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,101 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'simplecov'
4
+ require 'coveralls'
5
+ SimpleCov.start
6
+
7
+ begin
8
+ Bundler.setup(:default, :development)
9
+ rescue Bundler::BundlerError => e
10
+ $stderr.puts e.message
11
+ $stderr.puts "Run `bundle install` to install missin gems"
12
+ exit e.status_code
13
+ end
14
+ require 'test/unit'
15
+
16
+ require 'fluent/test'
17
+ unless ENV.has_key?('VERBOSE')
18
+ nulllogger = Object.new
19
+ nulllogger.instance_eval {|obj|
20
+ def method_missing(method, *args)
21
+ # pass
22
+ end
23
+ }
24
+ $log = nulllogger
25
+ end
26
+
27
+ require 'fluent/plugin/out_hrforecast'
28
+
29
+ class Test::Unit::Testcase
30
+ end
31
+
32
+
33
+ class DummyServer
34
+
35
+ attr :port
36
+ attr :posted
37
+
38
+ def initialize
39
+ require 'net/empty_port'
40
+ @posted = []
41
+ @port = Net::EmptyPort.empty_port
42
+ @auth = nil
43
+ @thread = Thread.new do
44
+ require 'webrick'
45
+
46
+ sv = if ENV['VERBOSE']
47
+ WEBrick::HTTPServer.new({:BindAddress => '127.0.0.1', :Port => @port})
48
+ else
49
+ logger = WEBrick::Log.new('/dev/null', WEBrick::BasicLog::DEBUG)
50
+ WEBrick::HTTPServer.new({:BindAddress => '127.0.0.1', :Port => @port, :Logger => logger, :AccessLog => []})
51
+ end
52
+
53
+ begin
54
+ sv.mount_proc('/api') do |req, res|
55
+ unless req.request_method == 'POST'
56
+ res.status = 405
57
+ res.body = 'invalid request method'
58
+ next
59
+ end
60
+
61
+ if @auth
62
+ if req.header['authorization'][0] != @auth
63
+ res.status = 403
64
+ next
65
+ end
66
+ end
67
+
68
+ req.path =~ %r|^/api/(.*)$|
69
+ path = Regexp.last_match(1)
70
+ post_param = Hash[req.body.split('&').map{|kv| kv.split('=',2)}]
71
+ datetime = URI.decode_www_form_component(post_param['datetime']) if post_param['datetime']
72
+ @posted.push({
73
+ :path => path,
74
+ :data => {:number => post_param['number'], :datetime => datetime},
75
+ })
76
+
77
+ res.status = 200
78
+ end
79
+ sv.mount_proc('/') do |req,res|
80
+ res.status = 200
81
+ res.body = 'running'
82
+ end
83
+ sv.start
84
+ ensure
85
+ sv.shutdown
86
+ end
87
+ end
88
+
89
+ Net::EmptyPort.wait(@port, 3)
90
+ end
91
+
92
+ def shutdown
93
+ @thread.kill
94
+ @thread.join
95
+ end
96
+
97
+ def setAuth(user, password)
98
+ require 'base64'
99
+ @auth = 'Basic ' + Base64.encode64("#{user}:#{password}").chomp
100
+ end
101
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-hrforecast
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - do-aki
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
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: simplecov
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: coveralls
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
+ - !ruby/object:Gem::Dependency
56
+ name: net-empty_port
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: fluent-plugin-growthforecast
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: For HRForecast
84
+ email:
85
+ - do.hiroaki@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - .coveralls.yml
91
+ - .gitignore
92
+ - .travis.yml
93
+ - Gemfile
94
+ - LICENSE
95
+ - README.md
96
+ - Rakefile
97
+ - fluent-plugin-hrforecast.gemspec
98
+ - lib/fluent/plugin/out_hrforecast.rb
99
+ - test/fluent/plugin/test_out_hrforecast.rb
100
+ - test/helper.rb
101
+ homepage: https://github.com/do-aki/fluent-plugin-hrforecast
102
+ licenses:
103
+ - APLv2
104
+ metadata: {}
105
+ post_install_message:
106
+ rdoc_options: []
107
+ require_paths:
108
+ - lib
109
+ required_ruby_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - '>='
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubyforge_project:
121
+ rubygems_version: 2.0.0
122
+ signing_key:
123
+ specification_version: 4
124
+ summary: Fluentd output plugin to post to HRForecast
125
+ test_files:
126
+ - test/fluent/plugin/test_out_hrforecast.rb
127
+ - test/helper.rb