statful-client 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []