wavefront-sdk 1.6.2 → 2.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/HISTORY.md +39 -13
  4. data/README.md +75 -28
  5. data/Rakefile +1 -1
  6. data/lib/wavefront-sdk/alert.rb +113 -17
  7. data/lib/wavefront-sdk/cloudintegration.rb +8 -8
  8. data/lib/wavefront-sdk/core/api.rb +99 -0
  9. data/lib/wavefront-sdk/core/api_caller.rb +211 -0
  10. data/lib/wavefront-sdk/{exception.rb → core/exception.rb} +11 -6
  11. data/lib/wavefront-sdk/{logger.rb → core/logger.rb} +2 -3
  12. data/lib/wavefront-sdk/{response.rb → core/response.rb} +69 -52
  13. data/lib/wavefront-sdk/credentials.rb +6 -3
  14. data/lib/wavefront-sdk/dashboard.rb +14 -14
  15. data/lib/wavefront-sdk/{constants.rb → defs/constants.rb} +1 -0
  16. data/lib/wavefront-sdk/defs/version.rb +1 -0
  17. data/lib/wavefront-sdk/derivedmetric.rb +14 -14
  18. data/lib/wavefront-sdk/distribution.rb +75 -0
  19. data/lib/wavefront-sdk/event.rb +13 -13
  20. data/lib/wavefront-sdk/externallink.rb +8 -8
  21. data/lib/wavefront-sdk/integration.rb +9 -9
  22. data/lib/wavefront-sdk/maintenancewindow.rb +54 -8
  23. data/lib/wavefront-sdk/message.rb +4 -4
  24. data/lib/wavefront-sdk/metric.rb +3 -3
  25. data/lib/wavefront-sdk/notificant.rb +9 -9
  26. data/lib/wavefront-sdk/paginator/base.rb +148 -0
  27. data/lib/wavefront-sdk/paginator/delete.rb +11 -0
  28. data/lib/wavefront-sdk/paginator/get.rb +11 -0
  29. data/lib/wavefront-sdk/paginator/post.rb +64 -0
  30. data/lib/wavefront-sdk/paginator/put.rb +11 -0
  31. data/lib/wavefront-sdk/proxy.rb +7 -7
  32. data/lib/wavefront-sdk/query.rb +4 -4
  33. data/lib/wavefront-sdk/report.rb +9 -25
  34. data/lib/wavefront-sdk/savedsearch.rb +8 -8
  35. data/lib/wavefront-sdk/search.rb +16 -13
  36. data/lib/wavefront-sdk/source.rb +14 -14
  37. data/lib/wavefront-sdk/{mixins.rb → support/mixins.rb} +8 -2
  38. data/lib/wavefront-sdk/{parse_time.rb → support/parse_time.rb} +2 -0
  39. data/lib/wavefront-sdk/types/status.rb +52 -0
  40. data/lib/wavefront-sdk/user.rb +8 -8
  41. data/lib/wavefront-sdk/validators.rb +52 -3
  42. data/lib/wavefront-sdk/webhook.rb +8 -8
  43. data/lib/wavefront-sdk/write.rb +153 -52
  44. data/lib/wavefront-sdk/writers/api.rb +38 -0
  45. data/lib/wavefront-sdk/writers/core.rb +146 -0
  46. data/lib/wavefront-sdk/writers/http.rb +42 -0
  47. data/lib/wavefront-sdk/writers/socket.rb +66 -0
  48. data/lib/wavefront-sdk/writers/summary.rb +39 -0
  49. data/lib/wavefront_sdk.rb +9 -0
  50. data/spec/spec_helper.rb +3 -0
  51. data/spec/wavefront-sdk/alert_spec.rb +6 -0
  52. data/spec/wavefront-sdk/{base_spec.rb → core/api_caller_spec.rb} +28 -41
  53. data/spec/wavefront-sdk/core/api_spec.rb +31 -0
  54. data/spec/wavefront-sdk/{logger_spec.rb → core/logger_spec.rb} +3 -3
  55. data/spec/wavefront-sdk/core/response_spec.rb +77 -0
  56. data/spec/wavefront-sdk/credentials_spec.rb +15 -10
  57. data/spec/wavefront-sdk/distribution_spec.rb +78 -0
  58. data/spec/wavefront-sdk/paginator/base_spec.rb +67 -0
  59. data/spec/wavefront-sdk/paginator/post_spec.rb +53 -0
  60. data/spec/wavefront-sdk/report_spec.rb +3 -1
  61. data/spec/wavefront-sdk/search_spec.rb +25 -0
  62. data/spec/wavefront-sdk/stdlib/array_spec.rb +2 -1
  63. data/spec/wavefront-sdk/stdlib/hash_spec.rb +6 -1
  64. data/spec/wavefront-sdk/stdlib/string_spec.rb +2 -0
  65. data/spec/wavefront-sdk/{mixins_spec.rb → support/mixins_spec.rb} +2 -2
  66. data/spec/wavefront-sdk/{parse_time_spec.rb → support/parse_time_spec.rb} +2 -2
  67. data/spec/wavefront-sdk/validators_spec.rb +64 -1
  68. data/spec/wavefront-sdk/write_spec.rb +55 -77
  69. data/spec/wavefront-sdk/writers/api_spec.rb +45 -0
  70. data/spec/wavefront-sdk/writers/core_spec.rb +49 -0
  71. data/spec/wavefront-sdk/writers/http_spec.rb +69 -0
  72. data/spec/wavefront-sdk/writers/socket_spec.rb +104 -0
  73. data/spec/wavefront-sdk/writers/summary_spec.rb +38 -0
  74. data/wavefront-sdk.gemspec +1 -1
  75. metadata +52 -24
  76. data/lib/wavefront-sdk/base.rb +0 -264
  77. data/lib/wavefront-sdk/base_write.rb +0 -185
  78. data/lib/wavefront-sdk/stdlib.rb +0 -5
  79. data/lib/wavefront-sdk/version.rb +0 -1
  80. data/spec/wavefront-sdk/base_write_spec.rb +0 -82
  81. data/spec/wavefront-sdk/response_spec.rb +0 -39
@@ -1,7 +1,9 @@
1
1
  require 'date'
2
- require_relative 'exception'
3
2
  require_relative 'parse_time'
4
- require_relative 'stdlib'
3
+ require_relative '../core/exception'
4
+ require_relative '../stdlib/string'
5
+ require_relative '../stdlib/array'
6
+ require_relative '../stdlib/hash'
5
7
 
6
8
  module Wavefront
7
9
  #
@@ -85,5 +87,9 @@ module Wavefront
85
87
  return u[suffix.to_sym] if u.key?(suffix.to_sym)
86
88
  raise Wavefront::Exception::InvalidTimeUnit
87
89
  end
90
+
91
+ def log(message, severity = :info)
92
+ logger.log(message, severity)
93
+ end
88
94
  end
89
95
  end
@@ -1,3 +1,5 @@
1
+ require 'time'
2
+
1
3
  module Wavefront
2
4
  #
3
5
  # Parse various times into integers. This class is not for direct
@@ -0,0 +1,52 @@
1
+ module Wavefront
2
+ #
3
+ # Status types are used by the Wavefront::Response class. They
4
+ # represent the success or failure of an API call.
5
+ #
6
+ #
7
+ module Type
8
+ #
9
+ # An object which provides information about whether the request
10
+ # was successful or not. Ordinarily this is easy to construct
11
+ # from the API's JSON response, but some classes, for instance
12
+ # Wavefront::Write fake it by constructing their own.
13
+ #
14
+ # @!attribute [r] result
15
+ # @return [OK, ERROR] a string telling us how the request went
16
+ # @!attribute [r] message
17
+ # @return [String] Any informational message from the API
18
+ # @!attribute [r] code
19
+ # @return [Integer] the HTTP response code from the API
20
+ # request
21
+ #
22
+ class Status
23
+ attr_reader :obj, :status
24
+
25
+ # @param response [Hash] the API response, turned into a hash
26
+ # @param status [Integer] HTTP status code
27
+ #
28
+ def initialize(response, status)
29
+ @obj = response.fetch(:status, response)
30
+ @status = status
31
+ end
32
+
33
+ def to_s
34
+ obj.inspect.to_s
35
+ end
36
+
37
+ def message
38
+ obj[:message] || nil
39
+ end
40
+
41
+ def code
42
+ obj[:code] || status
43
+ end
44
+
45
+ def result
46
+ return obj[:result] if obj[:result]
47
+ return 'OK' if status.between?(200, 299)
48
+ 'ERROR'
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,15 +1,15 @@
1
- require_relative 'base'
1
+ require_relative 'core/api'
2
2
 
3
3
  module Wavefront
4
4
  #
5
5
  # Manage and query Wavefront users
6
6
  #
7
- class User < Base
7
+ class User < CoreApi
8
8
  # GET /api/v2/user
9
9
  # Get all users.
10
10
  #
11
11
  def list
12
- api_get('')
12
+ api.get('')
13
13
  end
14
14
 
15
15
  # POST /api/v2/user
@@ -22,7 +22,7 @@ module Wavefront
22
22
  #
23
23
  def create(body, send_email = false)
24
24
  raise ArgumentError unless body.is_a?(Hash)
25
- api_post("?sendEmail=#{send_email}", body, 'application/json')
25
+ api.post("?sendEmail=#{send_email}", body, 'application/json')
26
26
  end
27
27
 
28
28
  # DELETE /api/v2/user/id
@@ -33,7 +33,7 @@ module Wavefront
33
33
  #
34
34
  def delete(id)
35
35
  wf_user_id?(id)
36
- api_delete(id)
36
+ api.delete(id)
37
37
  end
38
38
 
39
39
  # GET /api/v2/user/id
@@ -44,7 +44,7 @@ module Wavefront
44
44
  #
45
45
  def describe(id)
46
46
  wf_user_id?(id)
47
- api_get(id)
47
+ api.get(id)
48
48
  end
49
49
 
50
50
  # PUT /api/v2/user/id/grant
@@ -63,7 +63,7 @@ module Wavefront
63
63
  def grant(id, group)
64
64
  wf_user_id?(id)
65
65
  raise ArgumentError unless group.is_a?(String)
66
- api_post([id, 'grant'].uri_concat, "group=#{group}",
66
+ api.post([id, 'grant'].uri_concat, "group=#{group}",
67
67
  'application/x-www-form-urlencoded')
68
68
  end
69
69
 
@@ -79,7 +79,7 @@ module Wavefront
79
79
  def revoke(id, group)
80
80
  wf_user_id?(id)
81
81
  raise ArgumentError unless group.is_a?(String)
82
- api_post([id, 'revoke'].uri_concat, "group=#{group}",
82
+ api.post([id, 'revoke'].uri_concat, "group=#{group}",
83
83
  'application/x-www-form-urlencoded')
84
84
  end
85
85
 
@@ -1,5 +1,5 @@
1
- require_relative 'constants'
2
- require_relative 'exception'
1
+ require_relative 'defs/constants'
2
+ require_relative 'core/exception'
3
3
 
4
4
  module Wavefront
5
5
  #
@@ -409,7 +409,7 @@ module Wavefront
409
409
  # https://community.wavefront.com/docs/DOC-1031
410
410
  #
411
411
  # @param point [Hash] description of point
412
- # @return true if valie
412
+ # @return true if valid
413
413
  # @raise whichever exception is thrown first when validating
414
414
  # each component of the point.
415
415
  #
@@ -422,6 +422,35 @@ module Wavefront
422
422
  true
423
423
  end
424
424
 
425
+ # Validate a distribution description
426
+ # @param dist [Hash] description of distribution
427
+ # @return true if valid
428
+ # @raise whichever exception is thrown first when validating
429
+ # each component of the distribution.
430
+ #
431
+ def wf_distribution?(dist)
432
+ wf_metric_name?(dist[:path])
433
+ wf_distribution_values?(dist[:value])
434
+ wf_epoch?(dist[:ts]) if dist[:ts]
435
+ wf_source_id?(dist[:source]) if dist[:source]
436
+ wf_point_tags?(dist[:tags]) if dist[:tags]
437
+ true
438
+ end
439
+
440
+ # Validate an array of distribution values
441
+ # @param vals [Array[Array]] [count, value]
442
+ # @return true if valid
443
+ # @raise whichever exception is thrown first when validating
444
+ # each component of the distribution.
445
+ #
446
+ def wf_distribution_values?(vals)
447
+ vals.each do |times, val|
448
+ wf_distribution_count?(times)
449
+ wf_value?(val)
450
+ end
451
+ true
452
+ end
453
+
425
454
  # Ensure the given argument is a valid Wavefront notificant ID.
426
455
  #
427
456
  # @param id [String] the notificant ID to validate
@@ -446,6 +475,26 @@ module Wavefront
446
475
  return true if id.is_a?(String) && id =~ /^[a-z0-9]+$/
447
476
  raise Wavefront::Exception::InvalidIntegrationId
448
477
  end
478
+
479
+ # Ensure the given argument is a valid distribution interval.
480
+ # @param interval [Symbol]
481
+ # @raise Wavefront::Exception::InvalidDistributionInterval if the
482
+ # interval is not valid
483
+ #
484
+ def wf_distribution_interval?(interval)
485
+ return true if %i[m h d].include?(interval)
486
+ raise Wavefront::Exception::InvalidDistributionInterval
487
+ end
488
+
489
+ # Ensure the given argument is a valid distribution count.
490
+ # @param count [Numeric]
491
+ # @raise Wavefront::Exception::InvalidDistributionCount if the
492
+ # count is not valid
493
+ #
494
+ def wf_distribution_count?(count)
495
+ return true if count.is_a?(Integer) && count > 0
496
+ raise Wavefront::Exception::InvalidDistributionCount
497
+ end
449
498
  end
450
499
  # rubocop:enable Metrics/ModuleLength
451
500
  end
@@ -1,10 +1,10 @@
1
- require_relative 'base'
1
+ require_relative 'core/api'
2
2
 
3
3
  module Wavefront
4
4
  #
5
5
  # Manage and query Wavefront webhooks
6
6
  #
7
- class Webhook < Base
7
+ class Webhook < CoreApi
8
8
  def update_keys
9
9
  %i[title description template title triggers recipient]
10
10
  end
@@ -16,7 +16,7 @@ module Wavefront
16
16
  # @param limit [Integer] the number of webhooks to return
17
17
  #
18
18
  def list(offset = 0, limit = 100)
19
- api_get('', offset: offset, limit: limit)
19
+ api.get('', offset: offset, limit: limit)
20
20
  end
21
21
 
22
22
  # POST /api/v2/webhook
@@ -29,7 +29,7 @@ module Wavefront
29
29
  #
30
30
  def create(body)
31
31
  raise ArgumentError unless body.is_a?(Hash)
32
- api_post('', body, 'application/json')
32
+ api.post('', body, 'application/json')
33
33
  end
34
34
 
35
35
  # DELETE /api/v2/webhook/id
@@ -40,7 +40,7 @@ module Wavefront
40
40
  #
41
41
  def delete(id)
42
42
  wf_webhook_id?(id)
43
- api_delete(id)
43
+ api.delete(id)
44
44
  end
45
45
 
46
46
  # GET /api/v2/webhook/id
@@ -51,7 +51,7 @@ module Wavefront
51
51
  #
52
52
  def describe(id)
53
53
  wf_webhook_id?(id)
54
- api_get(id)
54
+ api.get(id)
55
55
  end
56
56
 
57
57
  # PUT /api/v2/webhook/id
@@ -69,9 +69,9 @@ module Wavefront
69
69
  wf_webhook_id?(id)
70
70
  raise ArgumentError unless body.is_a?(Hash)
71
71
 
72
- return api_put(id, body, 'application/json') unless modify
72
+ return api.put(id, body, 'application/json') unless modify
73
73
 
74
- api_put(id, hash_for_update(describe(id).response, body),
74
+ api.put(id, hash_for_update(describe(id).response, body),
75
75
  'application/json')
76
76
  end
77
77
  end
@@ -1,66 +1,127 @@
1
- require_relative 'base_write'
1
+ require 'socket'
2
+ require_relative 'core/exception'
3
+ require_relative 'core/logger'
4
+ require_relative 'defs/constants'
5
+ require_relative 'validators'
6
+ require 'spy'
7
+ require 'spy/integration'
8
+
9
+ HOSTNAME = Socket.gethostname.freeze
2
10
 
3
11
  module Wavefront
4
12
  #
5
- # This class helps you send points to a Wavefront proxy in native
6
- # format. Usually this is done on port 2878.
7
- #
8
- # The points are prepped in the BaseWrite class, which this
9
- # extends. This class provides the transport mechanism.
13
+ # This class helps you send points to Wavefront. It is extended by
14
+ # the Write and Report classes, which respectively handle point
15
+ # ingestion by a proxy and directly to the API.
10
16
  #
11
- class Write < BaseWrite
12
- def really_send_point(point)
13
- begin
14
- sock.puts(point)
15
- rescue StandardError => e
16
- summary[:unsent] += 1
17
- log('WARNING: failed to send point.')
18
- log(e.to_s, :debug)
19
- return false
20
- end
17
+ class Write
18
+ attr_reader :creds, :opts, :writer, :logger
19
+
20
+ include Wavefront::Validators
21
+
22
+ # Construct an object which gives the user an interface for
23
+ # writing points to Wavefront. The actual writing is handled by
24
+ # a Wavefront::Writer:: subclass.
25
+ #
26
+ # @param creds [Hash] credentials
27
+ # signature.
28
+ # @param options [Hash] can contain the following keys:
29
+ # proxy [String] the address of the Wavefront proxy. ('wavefront')
30
+ # port [Integer] the port of the Wavefront proxy
31
+ # tags [Hash] point tags which will be applied to every point
32
+ # noop [Bool] if true, no proxy connection will be made, and
33
+ # instead of sending the points, they will be printed in
34
+ # Wavefront wire format.
35
+ # novalidate [Bool] if true, points will not be validated.
36
+ # This might make things go marginally quicker if you have
37
+ # done point validation higher up in the chain. Invalid
38
+ # points are dropped, logged, and reported in the summary.
39
+ # verbose [Bool]
40
+ # debug [Bool]
41
+ # writer [Symbol, String] the name of the writer class to use.
42
+ # Defaults to :socket
43
+ #
44
+ def initialize(creds = {}, opts = {})
45
+ defaults = { tags: nil,
46
+ writer: :socket,
47
+ noop: false,
48
+ novalidate: false,
49
+ verbose: false,
50
+ debug: false }
51
+
52
+ @opts = setup_options(opts, defaults)
53
+ @creds = creds
54
+ wf_point_tags?(opts[:tags]) if opts[:tags]
55
+ @logger = Wavefront::Logger.new(opts)
56
+ @writer = setup_writer
57
+ end
21
58
 
22
- summary[:sent] += 1
23
- true
59
+ def setup_options(user, defaults)
60
+ defaults.merge(user)
24
61
  end
25
62
 
26
- # Open a socket to a Wavefront proxy, putting the descriptor
27
- # in instance variable @sock.
63
+ # Wrapper to the writer class's #open method. Using this you can
64
+ # manually open a connection and re-use it.
28
65
  #
29
66
  def open
30
- if opts[:noop]
31
- log('No-op requested. Not opening connection to proxy.')
32
- return true
33
- end
67
+ writer.open
68
+ end
34
69
 
35
- port = net[:port] || 2878
36
- log("Connecting to #{net[:proxy]}:#{port}.", :debug)
70
+ # Wrapper to the writer class's #close method.
71
+ #
72
+ def close
73
+ writer.close
74
+ end
37
75
 
38
- begin
39
- @sock = TCPSocket.new(net[:proxy], port)
40
- rescue StandardError => e
41
- log(e, :error)
42
- raise Wavefront::Exception::InvalidEndpoint
43
- end
76
+ # A wrapper to the writer class's #write method.
77
+ # Writers implement this method differently, Check the
78
+ # appropriate class documentation for @return information etc.
79
+ # The signature is always the same.
80
+ #
81
+ def write(points = [], openclose = true, prefix = nil)
82
+ writer.write(points, openclose, prefix)
44
83
  end
45
84
 
46
- # Close the socket described by the @sock instance variable.
85
+ # A wrapper method around #write() which guarantees all points
86
+ # will be sent as deltas. You can still manually prefix any
87
+ # metric with a delta symbol and use #write(), but depending on
88
+ # your use-case, this method may be safer. It's easy to forget
89
+ # the delta.
47
90
  #
48
- def close
49
- return if opts[:noop]
50
- log('Closing connection to proxy.', :debug)
51
- sock.close
91
+ # @param points [Array[Hash]] see #write()
92
+ # @param openclose [Bool] see #write()
93
+ #
94
+ def write_delta(points, openclose = true)
95
+ write(paths_to_deltas(points), openclose)
96
+ end
97
+
98
+ # Prefix all paths in a points array (as passed to
99
+ # #write_delta() with a delta symbol
100
+ #
101
+ # @param points [Array[Hash]] see #write()
102
+ # @return [Array[Hash]]
103
+ #
104
+ def paths_to_deltas(points)
105
+ [points].flatten.map { |p| p.tap { p[:path] = DELTA + p[:path] } }
52
106
  end
53
107
 
54
- # Overload the method which sets an API endpoint. A proxy
55
- # endpoint has an address and a port, rather than an address and
56
- # a token.
108
+ # Wrapper for the writer class's #send_point method
109
+ # @param point [String] a point description, probably from
110
+ # #hash_to_wf()
57
111
  #
58
- def setup_endpoint(creds)
59
- @net = creds
112
+ def send_point(point)
113
+ if opts[:noop]
114
+ logger.log "Would send: #{point}"
115
+ return
116
+ end
117
+
118
+ logger.log("Sending: #{point}", :debug)
119
+ writer.send_point(point)
60
120
  end
61
121
 
62
- # Send raw data to a Wavefront proxy, automatically opening and
63
- # closing a socket.
122
+ # Send raw data to a Wavefront proxy, optionally automatically
123
+ # opening and closing the connection. (Or not, if that does not
124
+ # make sense in the context of the writer.)
64
125
  #
65
126
  # @param points [Array[String]] an array of points in native
66
127
  # Wavefront wire format, as described in
@@ -71,22 +132,62 @@ module Wavefront
71
132
  # afterwards, close it.
72
133
  #
73
134
  def raw(points, openclose = true)
74
- open if openclose
135
+ writer.open if openclose && writer.respond_to?(:open)
75
136
 
76
137
  begin
77
- [points].flatten.each { |p| send_point(p) }
138
+ [points].flatten.each { |p| writer.send_point(p) }
78
139
  ensure
79
- close if openclose
140
+ writer.close if openclose && writer.respond_to?(:close)
80
141
  end
81
142
  end
82
143
 
144
+ # The method used to validate the data we wish to write.
145
+ #
146
+ def validation
147
+ :wf_point?
148
+ end
149
+
150
+ # Convert a validated point to a string conforming to
151
+ # https://community.wavefront.com/docs/DOC-1031. No validation
152
+ # is done here.
153
+ #
154
+ # @param point [Hash] a hash describing a point. See #write() for
155
+ # the format.
156
+ #
157
+ def hash_to_wf(point)
158
+ format('%s %s %s source=%s %s %s',
159
+ *point_array(point)).squeeze(' ').strip
160
+ rescue StandardError
161
+ raise Wavefront::Exception::InvalidPoint
162
+ end
163
+
164
+ # Make an array which can be used by #hash_to_wf.
165
+ # @param point [Hash] a hash describing a point. See #write() for
166
+ # the format.
167
+ # @raise
168
+ #
169
+ def point_array(point)
170
+ [point[:path] || raise,
171
+ point[:value] || raise,
172
+ point.fetch(:ts, nil),
173
+ point.fetch(:source, HOSTNAME),
174
+ point[:tags] && point[:tags].to_wf_tag,
175
+ opts[:tags] && opts[:tags].to_wf_tag]
176
+ end
177
+
83
178
  private
84
179
 
85
- def _write_loop(points)
86
- points.each do |p|
87
- p[:ts] = p[:ts].to_i if p[:ts].is_a?(Time)
88
- send_point(hash_to_wf(p))
89
- end
180
+ # @return [Object] appropriate subclass of Wavefront::Writer
181
+ # @raise [Wavefront::Exception::UnsupportedWriter] if requested
182
+ # writer cannot be loaded
183
+ #
184
+ def setup_writer
185
+ writer = opts[:writer].to_s
186
+ require_relative File.join('writers', writer)
187
+ Object.const_get(format('Wavefront::Writer::%s',
188
+ writer.capitalize)).new(self)
189
+ rescue LoadError
190
+ raise(Wavefront::Exception::UnsupportedWriter, writer)
90
191
  end
91
192
  end
92
193
  end