statful-client 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/lib/client.rb +316 -0
  3. data/lib/statful-client.rb +1 -0
  4. metadata +129 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2082d2f2574057de832307766202d6a492f4817c
4
+ data.tar.gz: b9326d3830adb06a4ab25123ca489a1238408a30
5
+ SHA512:
6
+ metadata.gz: 5a09ee6befcecc28200828770b37a1cec711316121b691c617635ade92af7e75b8c0af4ff20d7a2f8bb4dec05d1bd67802e98f883f54664d5867023c647d3dd6
7
+ data.tar.gz: 0f9a337a23504ad89aac1a4b9ac0d9d5b8f9d731a297d42c3b836f4e527d75c23c14abd55029791675e7a959006f5d04e4badec6b2493016888db03c8bd28d04
data/lib/client.rb ADDED
@@ -0,0 +1,316 @@
1
+ require 'socket'
2
+ require 'delegate'
3
+ require 'net/http'
4
+
5
+ # Statful Client Instance
6
+ #
7
+ # @attr_reader config [Hash] Current client config
8
+ class StatfulClient
9
+ attr_reader :config
10
+
11
+ def new
12
+ self
13
+ end
14
+
15
+ # Initialize the client
16
+ #
17
+ # @param [Hash] config Client bootstrap configuration
18
+ # @option config [String] :host Destination host *required*
19
+ # @option config [String] :port Destination port *required*
20
+ # @option config [String] :transport Transport protocol, one of (udp or http) *required*
21
+ # @option config [Integer] :timeout Timeout for http transport
22
+ # @option config [String] :token Authentication account token for http transport
23
+ # @option config [String] :app Global metric app tag
24
+ # @option config [TrueClass/FalseClass] :dryrun Enable dry-run mode
25
+ # @option config [Object] :logger Logger instance that supports debug (if dryrun is enabled) and error methods
26
+ # @option config [Hash] :tags Global list of metric tags
27
+ # @option config [Integer] :sample_rate Global sample rate (as a percentage), between: (1-100)
28
+ # @option config [String] :namespace Global default namespace
29
+ # @option config [Integer] :flush_size Buffer flush upper size limit
30
+ # @return [Object] The Statful client
31
+ def initialize(config = {})
32
+ user_config = MyHash[config].symbolize_keys
33
+
34
+ if !user_config.has_key?(:transport) || !%w(udp http).include?(user_config[:transport])
35
+ raise ArgumentError.new('Transport is missing or invalid')
36
+ end
37
+
38
+ if user_config[:transport] == 'http'
39
+ raise ArgumentError.new('Token is missing') if user_config[:token].nil?
40
+ end
41
+
42
+ if user_config.has_key?(:sample_rate) && !user_config[:sample_rate].between?(1, 100)
43
+ raise ArgumentError.new('Sample rate is not within range (1-100)')
44
+ end
45
+
46
+ default_config = {
47
+ :host => 'api.statful.com',
48
+ :port => 443,
49
+ :transport => 'http',
50
+ :tags => {},
51
+ :sample_rate => 100,
52
+ :namespace => 'application',
53
+ :flush_size => 5
54
+ }
55
+
56
+ @config = default_config.merge(user_config)
57
+ @logger = @config[:logger]
58
+ @buffer = []
59
+ @http = Net::HTTP.new(@config[:host], @config[:port])
60
+
61
+ self
62
+ end
63
+
64
+ # Sends a timer
65
+ #
66
+ # @param name [String] Name of the timer
67
+ # @param value [Numeric] Value of the metric
68
+ # @param [Hash] options The options to apply to the metric
69
+ # @option options [Hash] :tags Tags to associate to the metric
70
+ # @option options [Array<String>] :agg List of aggregations to be applied by Statful
71
+ # @option options [Integer] :agg_freq Aggregation frequency in seconds
72
+ # @option options [String] :namespace Namespace of the metric
73
+ def timer(name, value, options = {})
74
+ tags = @config[:tags].merge({:unit => 'ms'})
75
+ tags = tags.merge(options[:tags]) unless options[:tags].nil?
76
+
77
+ aggregations = %w(avg p90 count)
78
+ aggregations.concat(options[:agg]) unless options[:agg].nil?
79
+
80
+ opts = {
81
+ :agg_freq => 10,
82
+ :namespace => 'application'
83
+ }.merge(MyHash[options].symbolize_keys)
84
+
85
+ opts[:tags] = tags
86
+ opts[:agg] = aggregations
87
+
88
+ _put("timer.#{name}", opts[:tags], value, opts[:agg], opts[:agg_freq], @config[:sample_rate], opts[:namespace])
89
+ end
90
+
91
+ # Sends a counter
92
+ #
93
+ # @param name [String] Name of the counter
94
+ # @param value [Numeric] Increment/Decrement value, this will be truncated with `to_int`
95
+ # @param [Hash] options The options to apply to the metric
96
+ # @option options [Hash] :tags Tags to associate to the metric
97
+ # @option options [Array<String>] :agg List of aggregations to be applied by the Statful
98
+ # @option options [Integer] :agg_freq Aggregation frequency in seconds
99
+ # @option options [String] :namespace Namespace of the metric
100
+ def counter(name, value, options = {})
101
+ tags = @config[:tags]
102
+ tags = tags.merge(options[:tags]) unless options[:tags].nil?
103
+
104
+ aggregations = %w(sum count)
105
+ aggregations.concat(options[:agg]) unless options[:agg].nil?
106
+
107
+ opts = {
108
+ :agg_freq => 10,
109
+ :namespace => 'application'
110
+ }.merge(MyHash[options].symbolize_keys)
111
+
112
+ opts[:tags] = tags
113
+ opts[:agg] = aggregations
114
+
115
+ _put("counter.#{name}", opts[:tags], value.to_i, opts[:agg], opts[:agg_freq], @config[:sample_rate], opts[:namespace])
116
+ end
117
+
118
+ # Sends a gauge
119
+ #
120
+ # @param name [String] Name of the gauge
121
+ # @param value [Numeric] Value of the metric
122
+ # @param [Hash] options The options to apply to the metric
123
+ # @option options [Hash] :tags Tags to associate to the metric
124
+ # @option options [Array<String>] :agg List of aggregations to be applied by Statful
125
+ # @option options [Integer] :agg_freq Aggregation frequency in seconds
126
+ # @option options [String] :namespace Namespace of the metric
127
+ def gauge(name, value, options = {})
128
+ tags = @config[:tags]
129
+ tags = tags.merge(options[:tags]) unless options[:tags].nil?
130
+
131
+ aggregations = %w(last)
132
+ aggregations.concat(options[:agg]) unless options[:agg].nil?
133
+
134
+ opts = {
135
+ :agg_freq => 10,
136
+ :namespace => 'application'
137
+ }.merge(MyHash[options].symbolize_keys)
138
+
139
+ opts[:tags] = tags
140
+ opts[:agg] = aggregations
141
+
142
+ _put("gauge.#{name}", opts[:tags], value, opts[:agg], opts[:agg_freq], @config[:sample_rate], opts[:namespace])
143
+ end
144
+
145
+
146
+ # Flush metrics buffer
147
+ def flush_metrics
148
+ flush
149
+ end
150
+
151
+ # Adds a new metric to the buffer
152
+ #
153
+ # @private
154
+ # @param metric [String] Name of the metric, ex: `response_time`
155
+ # @param value [Numeric] Value of the metric
156
+ # @param tags [Hash] Tags to associate to the metric
157
+ # @param agg [Array<String>] List of aggregations to be applied by Statful
158
+ # @param agg_freq [Integer] Aggregation frequency in seconds
159
+ # @param sample_rate [Integer] Sampling rate, between: (1-100)
160
+ # @param namespace [String] Namespace of the metric
161
+ # @param timestamp [Integer] Timestamp of the metric
162
+ def put(metric, tags, value, agg = [], agg_freq = 10, sample_rate = nil, namespace = 'application', timestamp = nil)
163
+ _tags = @config[:tags]
164
+ _tags = _tags.merge(tags) unless tags.nil?
165
+
166
+ _put(metric, _tags, value, agg, agg_freq, sample_rate, namespace, timestamp)
167
+ end
168
+
169
+ private
170
+
171
+ attr_accessor :buffer
172
+ attr_accessor :logger
173
+
174
+ # Adds a new metric to the buffer
175
+ #
176
+ # @private
177
+ # @param metric [String] Name of the metric, ex: `response_time`
178
+ # @param value [Numeric] Value of the metric
179
+ # @param tags [Hash] Tags to associate to the metric
180
+ # @param agg [Array<String>] List of aggregations to be applied by Statful
181
+ # @param agg_freq [Integer] Aggregation frequency in seconds
182
+ # @param sample_rate [Integer] Sampling rate, between: (1-100)
183
+ # @param namespace [String] Namespace of the metric
184
+ # @param timestamp [Integer] Timestamp of the metric
185
+ def _put(metric, tags, value, agg = [], agg_freq = 10, sample_rate = nil, namespace = 'application', timestamp = nil)
186
+ metric_name = "#{namespace}.#{metric}"
187
+ sample_rate_normalized = sample_rate / 100
188
+ timestamp = Time.now.to_i if timestamp.nil?
189
+
190
+ @config.has_key?(:app) ? merged_tags = tags.merge({:app => @config[:app]}) : merged_tags = tags
191
+
192
+ if Random.new.rand(1..100)*0.01 <= sample_rate_normalized
193
+ flush_line = merged_tags.keys.inject(metric_name) { |previous, tag|
194
+ "#{previous},#{tag.to_s}=#{merged_tags[tag]}"
195
+ }
196
+
197
+ flush_line += " #{value} #{timestamp}"
198
+
199
+ unless agg.empty?
200
+ agg.push(agg_freq)
201
+ flush_line += " #{agg.join(',')}"
202
+ end
203
+
204
+ flush_line += sample_rate ? " #{sample_rate}" : ''
205
+
206
+ put_raw(flush_line)
207
+ end
208
+ end
209
+
210
+ # Add raw metrics directly into the flush buffer
211
+ #
212
+ # @private
213
+ # @param metric_lines
214
+ def put_raw(metric_lines)
215
+ @buffer.push(metric_lines)
216
+ if @buffer.size >= @config[:flush_size]
217
+ flush
218
+ end
219
+ end
220
+
221
+ # Flushed the metrics to the Statful via UDP
222
+ #
223
+ # @private
224
+ def flush
225
+ unless @buffer.empty?
226
+ message = @buffer.join('\n')
227
+
228
+ # Handle socket errors by just logging if we have a logger instantiated
229
+ # We could eventually save the buffer state but that would require us to manage the buffer size etc.
230
+ begin
231
+ @logger.debug("Flushing metrics: #{message}") unless @logger.nil?
232
+
233
+ if !@config.has_key?(:dryrun) || !@config[:dryrun]
234
+ transport_send(message)
235
+ end
236
+ rescue SocketError => ex
237
+ @logger.debug("Statful: #{ex} on #{@config[:host]}:#{@config[:port]}") unless @logger.nil?
238
+ false
239
+ ensure
240
+ @buffer.clear
241
+ end
242
+ end
243
+ end
244
+
245
+ # Delegate flushing messages to the proper transport
246
+ #
247
+ # @private
248
+ # @param message
249
+ def transport_send(message)
250
+ case @config[:transport]
251
+ when 'http'
252
+ http_transport(message)
253
+ when 'udp'
254
+ udp_transport(message)
255
+ else
256
+ @logger.debug("Failed to flush message due to invalid transport: #{@config[:transport]}") unless @logger.nil?
257
+ end
258
+ end
259
+
260
+
261
+ # Sends message via http transport
262
+ #
263
+ # @private
264
+ # @param message
265
+ # :nocov:
266
+ def http_transport(message)
267
+ headers = {
268
+ 'Content-Type' => 'application/json',
269
+ 'M-Api-Token' => @config[:token]
270
+ }
271
+ response = @http.send_request('PUT', '/tel/v2.0/metrics', message, headers)
272
+
273
+ if response.code != '201'
274
+ @logger.debug("Failed to flush message via http with: #{response.code} - #{response.msg}") unless @logger.nil?
275
+ end
276
+ end
277
+
278
+ # Sends message via udp transport
279
+ #
280
+ # @private
281
+ # @param message
282
+ # :nocov:
283
+ def udp_transport(message)
284
+ udp_socket.send(message)
285
+ end
286
+
287
+ # Return a new or existing udp socket
288
+ #
289
+ # @private
290
+ # :nocov:
291
+ def udp_socket
292
+ Thread.current[:statful_socket] ||= UDPSocket.new(Addrinfo.udp(@config[:host], @config[:port]).afamily)
293
+ end
294
+
295
+ # Custom Hash implementation to add a symbolize_keys method
296
+ #
297
+ # @private
298
+ class MyHash < Hash
299
+ # Recursively symbolize an Hash
300
+ #
301
+ # @return [Hash] the symbolized hash
302
+ def symbolize_keys
303
+ symbolize = lambda do |h|
304
+ Hash === h ?
305
+ Hash[
306
+ h.map do |k, v|
307
+ [k.respond_to?(:to_sym) ? k.to_sym : k, symbolize[v]]
308
+ end
309
+ ] : h
310
+ end
311
+
312
+ symbolize[self]
313
+ end
314
+ end
315
+ end
316
+
@@ -0,0 +1 @@
1
+ require 'client'
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: statful-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Miguel Fonseca
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-09-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
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: yard
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: simplecov
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: rake
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: minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest-reporters
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Statful Ruby Client (https://www.statful.com)
98
+ email: miguel.fonseca@mindera.com
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - lib/client.rb
104
+ - lib/statful-client.rb
105
+ homepage: https://github.com/statful/statful-client-ruby
106
+ licenses:
107
+ - MIT
108
+ metadata: {}
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubyforge_project:
125
+ rubygems_version: 2.5.1
126
+ signing_key:
127
+ specification_version: 4
128
+ summary: Statful Ruby Client
129
+ test_files: []