wavefront-sdk 0.0.1

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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +20 -0
  3. data/.gitignore +4 -0
  4. data/.rubocop.yml +1157 -0
  5. data/.travis.yml +16 -0
  6. data/Gemfile +2 -0
  7. data/Gemfile.lock +58 -0
  8. data/LICENSE.txt +27 -0
  9. data/README.md +103 -0
  10. data/Rakefile +18 -0
  11. data/lib/wavefront-sdk/alert.rb +195 -0
  12. data/lib/wavefront-sdk/base.rb +251 -0
  13. data/lib/wavefront-sdk/cloudintegration.rb +88 -0
  14. data/lib/wavefront-sdk/credentials.rb +79 -0
  15. data/lib/wavefront-sdk/dashboard.rb +157 -0
  16. data/lib/wavefront-sdk/event.rb +173 -0
  17. data/lib/wavefront-sdk/exception.rb +39 -0
  18. data/lib/wavefront-sdk/externallink.rb +77 -0
  19. data/lib/wavefront-sdk/maintenancewindow.rb +75 -0
  20. data/lib/wavefront-sdk/message.rb +36 -0
  21. data/lib/wavefront-sdk/metric.rb +52 -0
  22. data/lib/wavefront-sdk/mixins.rb +60 -0
  23. data/lib/wavefront-sdk/proxy.rb +95 -0
  24. data/lib/wavefront-sdk/query.rb +96 -0
  25. data/lib/wavefront-sdk/response.rb +56 -0
  26. data/lib/wavefront-sdk/savedsearch.rb +88 -0
  27. data/lib/wavefront-sdk/search.rb +58 -0
  28. data/lib/wavefront-sdk/source.rb +131 -0
  29. data/lib/wavefront-sdk/user.rb +108 -0
  30. data/lib/wavefront-sdk/validators.rb +395 -0
  31. data/lib/wavefront-sdk/version.rb +1 -0
  32. data/lib/wavefront-sdk/webhook.rb +73 -0
  33. data/lib/wavefront-sdk/write.rb +225 -0
  34. data/pkg/wavefront-client-3.5.0.gem +0 -0
  35. data/spec/.rubocop.yml +14 -0
  36. data/spec/spec_helper.rb +157 -0
  37. data/spec/wavefront-sdk/alert_spec.rb +83 -0
  38. data/spec/wavefront-sdk/base_spec.rb +88 -0
  39. data/spec/wavefront-sdk/cloudintegration_spec.rb +54 -0
  40. data/spec/wavefront-sdk/credentials_spec.rb +55 -0
  41. data/spec/wavefront-sdk/dashboard_spec.rb +74 -0
  42. data/spec/wavefront-sdk/event_spec.rb +83 -0
  43. data/spec/wavefront-sdk/externallink_spec.rb +65 -0
  44. data/spec/wavefront-sdk/maintenancewindow_spec.rb +48 -0
  45. data/spec/wavefront-sdk/message_spec.rb +19 -0
  46. data/spec/wavefront-sdk/metric_spec.rb +21 -0
  47. data/spec/wavefront-sdk/mixins_spec.rb +27 -0
  48. data/spec/wavefront-sdk/proxy_spec.rb +41 -0
  49. data/spec/wavefront-sdk/query_spec.rb +51 -0
  50. data/spec/wavefront-sdk/resources/test.conf +10 -0
  51. data/spec/wavefront-sdk/response_spec.rb +47 -0
  52. data/spec/wavefront-sdk/savedsearch_spec.rb +54 -0
  53. data/spec/wavefront-sdk/search_spec.rb +47 -0
  54. data/spec/wavefront-sdk/source_spec.rb +48 -0
  55. data/spec/wavefront-sdk/user_spec.rb +56 -0
  56. data/spec/wavefront-sdk/validators_spec.rb +238 -0
  57. data/spec/wavefront-sdk/webhook_spec.rb +50 -0
  58. data/spec/wavefront-sdk/write_spec.rb +167 -0
  59. data/wavefront-sdk.gemspec +34 -0
  60. metadata +269 -0
@@ -0,0 +1 @@
1
+ WF_SDK_VERSION = '0.0.1'.freeze
@@ -0,0 +1,73 @@
1
+ require_relative './base'
2
+
3
+ module Wavefront
4
+ #
5
+ # Manage and query Wavefront webhooks
6
+ #
7
+ class Webhook < Wavefront::Base
8
+
9
+ # GET /api/v2/webhook
10
+ # Get all webhooks for a customer.
11
+ #
12
+ # @param offset [Integer] webhook at which the list begins
13
+ # @param limit [Integer] the number of webhooks to return
14
+ #
15
+ def list(offset = 0, limit = 100)
16
+ api_get('', { offset: offset, limit: limit })
17
+ end
18
+
19
+ # POST /api/v2/webhook
20
+ # Create a specific webhook.
21
+ #
22
+ # @param body [Hash] a hash of parameters describing the webhook.
23
+ # Please refer to the Wavefront Swagger docs for key:value
24
+ # information
25
+ # @return [Hash]
26
+ #
27
+ def create(body)
28
+ raise ArgumentError unless body.is_a?(Hash)
29
+ api_post('', body, 'application/json')
30
+ end
31
+
32
+ # DELETE /api/v2/webhook/{id}
33
+ # Delete a specific webhook.
34
+ #
35
+ # @param id [String, Integer] ID of the webhook
36
+ # @return [Hash]
37
+ #
38
+ def delete(id)
39
+ wf_webhook_id?(id)
40
+ api_delete(id)
41
+ end
42
+
43
+ # GET /api/v2/webhook/{id}
44
+ # Get a specific webhook.
45
+ #
46
+ # @param id [String, Integer] ID of the webhook
47
+ # @return [Hash]
48
+ #
49
+ def describe(id)
50
+ wf_webhook_id?(id)
51
+ api_get(id)
52
+ end
53
+
54
+ # PUT /api/v2/webhook/{id}
55
+ # Update a specific webhook.
56
+ #
57
+ # @param body [Hash] a hash of parameters describing the webhook.
58
+ # @return [Hash]
59
+ # @raise any validation errors from body
60
+ #
61
+ def update(id, body)
62
+ wf_webhook_id?(id)
63
+ raise ArgumentError unless body.is_a?(Hash)
64
+ api_put(id, body)
65
+ end
66
+ end
67
+
68
+ # A standard response
69
+ #
70
+ class Response
71
+ class Webhook < Base; end
72
+ end
73
+ end
@@ -0,0 +1,225 @@
1
+ require 'socket'
2
+ require_relative './base'
3
+
4
+ HOSTNAME = Socket.gethostname.freeze
5
+
6
+ module Wavefront
7
+ #
8
+ # This class helps you send points to a Wavefront proxy in native
9
+ # format. Usually this is done on port 2878.
10
+ #
11
+ class Write < Wavefront::Base
12
+ attr_reader :sock, :summary
13
+
14
+ # Construct an object which allows us to write points to a
15
+ # Wavefront proxy.
16
+ #
17
+ # @pram creds [Hash] must contain the keys endpoint: and port.
18
+ # @param options [Hash] can contain the following keys:
19
+ # proxy [String] the address of the Wavefront proxy. ('wavefront')
20
+ # port [Integer] the port of the Wavefront proxy (2878)
21
+ # tags [Hash] point tags which will be applied to every point
22
+ # noop [Bool] if true, no proxy connection will be made, and
23
+ # instead of sending the points, they will be printed in
24
+ # Wavefront wire format.
25
+ # novalidate [Bool] if true, points will not be validated.
26
+ # This might make things go marginally quicker if you have
27
+ # done point validation higher up in the chain.
28
+ # verbose [Bool]
29
+ # debug [Bool]
30
+ #
31
+ def post_initialize(_creds = {}, options = {})
32
+ defaults = { tags: nil,
33
+ noop: false,
34
+ novalidate: false,
35
+ verbose: false,
36
+ debug: false }
37
+
38
+ @summary = { sent: 0, rejected: 0, unsent: 0 }
39
+ @opts = setup_options(options, defaults)
40
+
41
+ wf_point_tags?(opts[:tags]) if opts[:tags]
42
+ end
43
+
44
+ def setup_options(user, defaults)
45
+ defaults.merge(user)
46
+ end
47
+
48
+ # Send raw data to a Wavefront proxy, automatically opening and
49
+ # closing a socket.
50
+ #
51
+ # @param point [Array[String]] an array of points in native
52
+ # Wavefront wire format, as described in
53
+ # https://community.wavefront.com/docs/DOC-1031. No validation
54
+ # is performed.
55
+ #
56
+ def raw(points, openclose = true)
57
+ open if openclose
58
+
59
+ begin
60
+ [points].flatten.each{ |p| send_point(p) }
61
+ ensure
62
+ close if openclose
63
+ end
64
+ end
65
+
66
+ # Send multiple points to a Wavefront proxy.
67
+ #
68
+ # @param points [Array[Hash]] an array of points. Each point is
69
+ # defined as a hash with the following keys:
70
+ # path [String] metric path. (mandatory)
71
+ # value [Numeric] value of metric. Numeric. Mandatory.
72
+ # ts [Time, Integer] timestamp for point. Defaults to
73
+ # current UTC time.
74
+ # source [String] originating source of metric. Defaults to
75
+ # the local hostname.
76
+ # tags [Hash] key: value point tags which will be applied in
77
+ # addition to any tags defined in the #initialize()
78
+ # method.
79
+ # @param openclose [Bool] if this is false, you must have
80
+ # already opened a socket to the proxy. If it is true, a
81
+ # connection will be opened for you, used, and closed.
82
+ # @raise any unhandled point validation error is passed through
83
+ # @return true if no points are rejected, otherwise false
84
+ #
85
+ def write(points = [], openclose = true)
86
+ open if openclose
87
+
88
+ begin
89
+ [points].flatten.each do |p|
90
+ p[:ts] = p[:ts].to_i if p[:ts].is_a?(Time)
91
+ valid_point?(p)
92
+ send_point(hash_to_wf(p))
93
+ end
94
+ ensure
95
+ close if openclose
96
+ end
97
+
98
+ Wavefront::Response::Write.new(summary.to_json, nil)
99
+ end
100
+
101
+ def valid_point?(p)
102
+ return true if opts[:novalidate]
103
+
104
+ begin
105
+ wf_point?(p)
106
+ return true
107
+ rescue Wavefront::Exception::InvalidMetricName,
108
+ Wavefront::Exception::InvalidMetricValue,
109
+ Wavefront::Exception::InvalidTimestamp,
110
+ Wavefront::Exception::InvalidSourceId,
111
+ Wavefront::Exception::InvalidTag => e
112
+ log('Invalid point, skipping.', :info)
113
+ log("Invalid point: #{p}. (#{e})", :debug)
114
+ summary[:rejected] += 1
115
+ return false
116
+ end
117
+ end
118
+
119
+ # Convert a validated point has to a string conforming to
120
+ # https://community.wavefront.com/docs/DOC-1031. No validation
121
+ # is done here.
122
+ #
123
+ # @param p [Hash] a hash describing a point. See #write() for
124
+ # the format.
125
+ #
126
+ def hash_to_wf(p)
127
+ unless p.key?(:path) && p.key?(:value)
128
+ raise Wavefront::Exception::InvalidPoint
129
+ end
130
+
131
+ p[:source] = HOSTNAME unless p.key?(:source)
132
+
133
+ m = [p[:path], p[:value]]
134
+ m.<< p[:ts] if p[:ts]
135
+ m.<< 'source=' + p[:source]
136
+ m.<< p[:tags].to_wf_tag if p[:tags]
137
+ m.<< opts[:tags].to_wf_tag if opts[:tags]
138
+ m.join(' ')
139
+ end
140
+
141
+ # Send a point which is already in Wavefront wire format.
142
+ #
143
+ # @param point [String] a point description, probably from
144
+ # #hash_to_wf()
145
+ #
146
+ def send_point(point)
147
+ if opts[:noop]
148
+ log "Would send: #{point}"
149
+ return
150
+ end
151
+
152
+ log("Sending: #{point}", :info)
153
+
154
+ begin
155
+ sock.puts(point)
156
+ rescue => e
157
+ summary[:unsent] += 1
158
+ log('WARNING: failed to send point.')
159
+ log(e.to_s, :debug)
160
+ return false
161
+ end
162
+
163
+ summary[:sent] += 1
164
+ true
165
+ end
166
+
167
+ # Open a socket to a Wavefront proxy, putting the descriptor
168
+ # in instance variable @sock.
169
+ #
170
+ def open
171
+ if opts[:noop]
172
+ log('No-op requested. Not opening connection to proxy.')
173
+ return true
174
+ end
175
+
176
+ log("Connecting to #{net[:proxy]}:#{net[:port]}.", :info)
177
+
178
+ begin
179
+ @sock = TCPSocket.new(net[:proxy], net[:port])
180
+ rescue => e
181
+ log(e, :error)
182
+ raise Wavefront::Exception::InvalidEndpoint
183
+ end
184
+ end
185
+
186
+ # Close the socket described by the @sock instance variable.
187
+ #
188
+ def close
189
+ return if opts[:noop]
190
+ log('Closing connection to proxy.', :info)
191
+ sock.close
192
+ end
193
+
194
+ private
195
+
196
+ # Overload the method which sets an API endpoint. A proxy
197
+ # endpoint has an address and a port, rather than an address and
198
+ # a token.
199
+ #
200
+ def setup_endpoint(creds)
201
+ @net = creds
202
+ end
203
+ end
204
+
205
+ class Response
206
+ # The Write response forges status and response methods to look
207
+ # like other classes and create a more consistent interface. As
208
+ # the request does not happen over HTTP, there's not much to put
209
+ # in the status object. The response object is the contents of
210
+ # the summary variable.
211
+ #
212
+ # @response=#<struct sent=1, rejected=0, unsent=0>,
213
+ # @status=#<struct result="OK", message=nil, code=nil>>
214
+ #
215
+ class Write < Base
216
+ def populate(raw, status)
217
+ @response = Struct.new(*raw.keys).new(*raw.values).freeze
218
+
219
+ ok = raw[:rejected] == 0 && raw[:unsent] == 0 ? 'OK' : 'ERROR'
220
+
221
+ @status = Struct.new(:result, :message, :code).new(ok, nil, nil)
222
+ end
223
+ end
224
+ end
225
+ end
Binary file
data/spec/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ #
2
+ # Allow long blocks of tests
3
+ #
4
+ Metrics/BlockLength:
5
+ Max: 120
6
+
7
+ Metrics/MethodLength:
8
+ Max: 80
9
+
10
+ Metrics/AbcSize:
11
+ Max: 45
12
+
13
+ Metrics/ClassLength:
14
+ Max: 300
@@ -0,0 +1,157 @@
1
+ #
2
+ # Stuff needed by multiple tests
3
+ #
4
+ require 'minitest/autorun'
5
+ require 'webmock/minitest'
6
+
7
+ CREDS = {
8
+ endpoint: 'test.example.com',
9
+ token: '0123456789-ABCDEF'
10
+ }
11
+
12
+ POST_HEADERS = {
13
+ :'Content-Type' => 'text/plain', :Accept => 'application/json'
14
+ }.freeze
15
+
16
+ JSON_POST_HEADERS = {
17
+ :'Content-Type' => 'application/json', :Accept => 'application/json'
18
+ }.freeze
19
+
20
+ DUMMY_RESPONSE = '{"status":{"result":"OK","message":"","code":200},' \
21
+ '"response":{"items":[{"name":"test data"}],"offset":0,' \
22
+ '"limit":100,"totalItems":3,"moreItems":false}}'
23
+
24
+ class WavefrontTestBase < MiniTest::Test
25
+ attr_reader :wf, :wf_noop, :uri_base, :headers
26
+
27
+ def initialize(args)
28
+ require_relative "../lib/wavefront-sdk/#{class_basename.downcase}"
29
+ super(args)
30
+ end
31
+
32
+ def class_basename
33
+ self.class.name.match(/Wavefront(\w+)Test/)[1]
34
+ end
35
+
36
+ def api_base
37
+ class_basename.downcase
38
+ end
39
+
40
+ def setup
41
+ klass = Object.const_get('Wavefront').const_get(class_basename)
42
+ @wf = klass.new(CREDS)
43
+ @uri_base = "https://#{CREDS[:endpoint]}/api/v2/" + api_base
44
+ @headers = { 'Authorization' => "Bearer #{CREDS[:token]}" }
45
+ end
46
+
47
+ def target_uri(path)
48
+ [uri_base, path].join(path.start_with?('?') ? '' : '/')
49
+ end
50
+
51
+ # A shorthand method for very common tests.
52
+ #
53
+ # @param method [String] the method you wish to test
54
+ # @args [String, Integer, Array] arguments with which to call method
55
+ # @path [String] extra API path components (beyond /api/v2/class)
56
+ # @call [Symbol] the type of API call (:get, :put etc.)
57
+ # @more_headers [Hash] any additional headers which should be
58
+ # sent. You will normally need to add these for :put and :post
59
+ # requests.
60
+ # @body [String] a JSON object you expect to be sent as part of
61
+ # the request
62
+ #
63
+ #
64
+ def should_work(method, args, path, call = :get, more_headers = {},
65
+ body = nil, id = nil)
66
+ path = Array(path)
67
+ uri = target_uri(path.first).sub(/\/$/, '')
68
+
69
+ headers = { 'Accept': /.*/,
70
+ 'Accept-Encoding': /.*/,
71
+ 'Authorization': 'Bearer 0123456789-ABCDEF',
72
+ 'User-Agent': "wavefront-sdk #{WF_SDK_VERSION}"
73
+ }.merge(more_headers)
74
+
75
+ if body
76
+ stub_request(call, uri).with(body: body, headers:headers)
77
+ .to_return(body: DUMMY_RESPONSE, status: 200)
78
+ else
79
+ stub_request(call, uri).to_return(body: DUMMY_RESPONSE, status: 200)
80
+ end
81
+
82
+ if args.is_a?(Hash)
83
+ if id
84
+ wf.send(method, id, args)
85
+ else
86
+ wf.send(method, args)
87
+ end
88
+ else
89
+ if id
90
+ wf.send(method, id, *args)
91
+ else
92
+ wf.send(method, *args)
93
+ end
94
+ end
95
+
96
+ assert_requested(call, uri, headers: headers)
97
+ WebMock.reset!
98
+ end
99
+
100
+ def standard_exception
101
+ Object.const_get('Wavefront::Exception')
102
+ .const_get("Invalid#{class_basename}Id")
103
+ end
104
+
105
+ def should_be_invalid(method, args = '!!invalid_val!!')
106
+ assert_raises(standard_exception) { wf.send(method, *args) }
107
+ end
108
+
109
+ # Generic tag method testing.
110
+ #
111
+ def tag_tester(id)
112
+ # Can we get tags? : tests #tags
113
+ #
114
+ should_work('tags', id, "#{id}/tag")
115
+ should_be_invalid('tags')
116
+
117
+ # Can we set tags? tests #tag_set
118
+ #
119
+ should_work('tag_set', [id, 'tag'],
120
+ ["#{id}/tag", ['tag'].to_json], :post, JSON_POST_HEADERS)
121
+ should_work('tag_set', [id, %w(tag1 tag2)],
122
+ ["#{id}/tag", %w(tag1 tag2).to_json], :post,
123
+ JSON_POST_HEADERS)
124
+ should_fail_tags('tag_set', id)
125
+
126
+ # Can we add tags? : tests #tag_add
127
+ #
128
+ should_work('tag_add', [id, 'tagval'],
129
+ ["#{id}/tag/tagval", nil], :put, JSON_POST_HEADERS)
130
+ should_fail_tags('tag_add', id)
131
+
132
+ # Can we delete tags? : tests #tag_delete
133
+ #
134
+ should_work('tag_delete', [id, 'tagval'], "#{id}/tag/tagval", :delete)
135
+ should_fail_tags('tag_delete', id)
136
+ end
137
+
138
+ def should_fail_tags(method, id)
139
+ assert_raises(standard_exception) do
140
+ wf.send(method, '!!invalid!!', 'tag1')
141
+ end
142
+
143
+ assert_raises(Wavefront::Exception::InvalidString) do
144
+ wf.send(method, id, '<!!!>')
145
+ end
146
+ end
147
+ end
148
+
149
+ class Hash
150
+
151
+ # A quick way to deep-copy a hash.
152
+ #
153
+ def dup
154
+ Marshal.load(Marshal.dump(self))
155
+ end
156
+ end
157
+