appoptics-api-ruby 2.1.3

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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +25 -0
  5. data/CHANGELOG.md +184 -0
  6. data/Gemfile +36 -0
  7. data/LICENSE +24 -0
  8. data/README.md +271 -0
  9. data/Rakefile +63 -0
  10. data/appoptics-api-ruby.gemspec +31 -0
  11. data/benchmarks/array_vs_set.rb +29 -0
  12. data/certs/librato-public.pem +20 -0
  13. data/examples/simple.rb +24 -0
  14. data/examples/submit_every.rb +27 -0
  15. data/lib/appoptics/metrics.rb +95 -0
  16. data/lib/appoptics/metrics/aggregator.rb +138 -0
  17. data/lib/appoptics/metrics/annotator.rb +145 -0
  18. data/lib/appoptics/metrics/client.rb +361 -0
  19. data/lib/appoptics/metrics/collection.rb +43 -0
  20. data/lib/appoptics/metrics/connection.rb +101 -0
  21. data/lib/appoptics/metrics/errors.rb +32 -0
  22. data/lib/appoptics/metrics/middleware/count_requests.rb +28 -0
  23. data/lib/appoptics/metrics/middleware/expects_status.rb +38 -0
  24. data/lib/appoptics/metrics/middleware/request_body.rb +18 -0
  25. data/lib/appoptics/metrics/middleware/retry.rb +31 -0
  26. data/lib/appoptics/metrics/persistence.rb +2 -0
  27. data/lib/appoptics/metrics/persistence/direct.rb +73 -0
  28. data/lib/appoptics/metrics/persistence/test.rb +27 -0
  29. data/lib/appoptics/metrics/processor.rb +130 -0
  30. data/lib/appoptics/metrics/queue.rb +191 -0
  31. data/lib/appoptics/metrics/smart_json.rb +43 -0
  32. data/lib/appoptics/metrics/util.rb +25 -0
  33. data/lib/appoptics/metrics/version.rb +5 -0
  34. data/spec/integration/metrics/annotator_spec.rb +190 -0
  35. data/spec/integration/metrics/connection_spec.rb +14 -0
  36. data/spec/integration/metrics/middleware/count_requests_spec.rb +28 -0
  37. data/spec/integration/metrics/queue_spec.rb +96 -0
  38. data/spec/integration/metrics_spec.rb +375 -0
  39. data/spec/rackups/status.ru +30 -0
  40. data/spec/spec_helper.rb +88 -0
  41. data/spec/unit/metrics/aggregator_spec.rb +417 -0
  42. data/spec/unit/metrics/client_spec.rb +127 -0
  43. data/spec/unit/metrics/connection_spec.rb +113 -0
  44. data/spec/unit/metrics/queue/autosubmission_spec.rb +57 -0
  45. data/spec/unit/metrics/queue_spec.rb +593 -0
  46. data/spec/unit/metrics/smart_json_spec.rb +79 -0
  47. data/spec/unit/metrics/util_spec.rb +23 -0
  48. data/spec/unit/metrics_spec.rb +63 -0
  49. metadata +135 -0
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ # Packaging
9
+ Bundler::GemHelper.install_tasks
10
+
11
+ # Gem signing
12
+ task 'before_build' do
13
+ signing_key = File.expand_path("~/.gem/appoptics-private_key.pem")
14
+ if signing_key
15
+ puts "Key found: signing gem..."
16
+ ENV['GEM_SIGNING_KEY'] = signing_key
17
+ else
18
+ puts "WARN: signing key not found, gem not signed"
19
+ end
20
+ end
21
+ task :build => :before_build
22
+
23
+ # Testing
24
+ require 'rspec/core/rake_task'
25
+
26
+ desc "Run all tests"
27
+ task :spec do
28
+ Rake::Task['spec:unit'].execute
29
+ if ENV['TEST_API_USER'] && ENV['TEST_API_KEY']
30
+ Rake::Task['spec:integration'].execute
31
+ else
32
+ puts "TEST_API_USER and TEST_API_KEY not in environment, skipping integration tests..."
33
+ end
34
+ end
35
+
36
+ namespace :spec do
37
+ RSpec::Core::RakeTask.new(:unit) do |t|
38
+ t.rspec_opts = '--color'
39
+ t.pattern = 'spec/unit/**/*_spec.rb'
40
+ end
41
+
42
+ RSpec::Core::RakeTask.new(:integration) do |t|
43
+ t.rspec_opts = '--color'
44
+ t.pattern = 'spec/integration/**/*_spec.rb'
45
+ end
46
+ end
47
+
48
+ task default: :spec
49
+ task test: :spec
50
+
51
+ # Docs
52
+ require 'yard'
53
+ YARD::Rake::YardocTask.new
54
+
55
+ # IRB
56
+ desc "Open an irb session preloaded with this library"
57
+ task :console do
58
+ if !`which pry`.empty?
59
+ sh "pry -r ./lib/appoptics/metrics.rb"
60
+ else
61
+ sh "irb -rubygems -r ./lib/appoptics/metrics.rb"
62
+ end
63
+ end
@@ -0,0 +1,31 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ require 'appoptics/metrics/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'appoptics-api-ruby'
8
+ s.version = '2.1.3'
9
+ s.date = '2017-10-13'
10
+ s.summary = "Ruby bindings for the AppOptics API"
11
+ s.description = "An easy to use ruby wrapper for the AppOptics API"
12
+ s.authors = ["Greg McKeever", "Matt Sanders"]
13
+ s.email = 'greg@solarwinds.cloud'
14
+ s.files = ["lib/appoptics/metrics.rb"]
15
+ s.homepage =
16
+ 'https://github.com/AppOptics/appoptics-api-ruby'
17
+ s.license = 'BSD 3-clause'
18
+ s.require_paths = %w[lib]
19
+
20
+ ## runtime dependencies
21
+ s.add_dependency 'faraday'
22
+ s.add_dependency 'aggregate', '~> 0.2.2'
23
+
24
+ s.files = `git ls-files`.split("\n")
25
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
26
+
27
+ s.cert_chain = ["certs/appoptics-public.pem"]
28
+ if ENV['GEM_SIGNING_KEY']
29
+ s.signing_key = ENV['GEM_SIGNING_KEY']
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'tach'
3
+ require 'set'
4
+
5
+ # Compare speed of simple addition and iteration operations.
6
+ #
7
+ Tach.meter(1000) do
8
+
9
+ tach('array') do
10
+ a = []
11
+ (1..1000).each do |i|
12
+ a << i
13
+ end
14
+ a.each do |i|
15
+ "this is a string with #{i}"
16
+ end
17
+ end
18
+
19
+ tach('set') do
20
+ s = Set.new
21
+ (1...1000).each do |i|
22
+ s << i
23
+ end
24
+ s.each do |i|
25
+ "this is a string with #{i}"
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,20 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDLjCCAhagAwIBAgIBADANBgkqhkiG9w0BAQUFADA9MQ0wCwYDVQQDDARydWJ5
3
+ MRcwFQYKCZImiZPyLGQBGRYHbGlicmF0bzETMBEGCgmSJomT8ixkARkWA2NvbTAe
4
+ Fw0xNzAxMTExODI3MDdaFw0xODAxMTExODI3MDdaMD0xDTALBgNVBAMMBHJ1Ynkx
5
+ FzAVBgoJkiaJk/IsZAEZFgdsaWJyYXRvMRMwEQYKCZImiZPyLGQBGRYDY29tMIIB
6
+ IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA58LirwsWnKL1uuClQ0uwA1XL
7
+ GpxDuFzSosipiPkzZY3hiHazC8SHDREZQXlm7ITX/o+dueNoB7dt0BR3RPVipH7V
8
+ 7cvbCUaZNjEXR5Lal6PsmUsbMTrddkvj3e7flmJv+kMj+teeJ7MDeJTU5wXXV9pD
9
+ ThiCDILJMF5CdP8Jru4rSBOP6RmmzYU+0cN5+5pt9xqrycA+Poo2ZuoUMCMsSBvl
10
+ PimM3PPvoWTuE41GTn/bLoOVoXQmdwZIbwUSVH8rCDjZSlttOst+xrBw4KG0dYUp
11
+ 2UvEe8iCyqEMQ8fEZ7EXuP2WMVICutFbz8Pt4hIMq+OTnDX+mIfma7GvPaKAFwID
12
+ AQABozkwNzAJBgNVHRMEAjAAMB0GA1UdDgQWBBQBSxu9Jj4VTrXTpLujPwk9Kzwp
13
+ 2jALBgNVHQ8EBAMCBLAwDQYJKoZIhvcNAQEFBQADggEBACvNsw1pGv72xp3LiDlZ
14
+ 0tphNP/85RcYyJMklslG3tIYblyo71xHW1UbK5ArUK6k0BN43MuDn3pqLJQttVmi
15
+ bUdA3yYi13GeSrrAMlr4nH8Yt/Bn/XpZGliouJUBwh1BjG6dSj1iuR4/9pt9/LtO
16
+ QTdIc+07qGspypT0Uh/w/BodEcGuAaZZFlkU9vottTe6wWNnM6hfRExiSIsr+oVe
17
+ s8s83ObshjuSzjOqS56IBtNlPEL+D6ghjZZLP3lS6l9p70Pcpcl+IcE4veqZmmKC
18
+ sGepgRclC6UbZh+yQ3alXVghM2qsonAwI/rTNmFJN9kQn6nP9+f1Uf/qZFNcjn4L
19
+ 9bg=
20
+ -----END CERTIFICATE-----
@@ -0,0 +1,24 @@
1
+ require 'appoptics-api-ruby'
2
+
3
+ Appoptics::Metrics.authenticate 'my email', 'my api key'
4
+
5
+ # send a measurement of 12 for 'foo'
6
+ Appoptics::Metrics.submit cpu: 54
7
+
8
+ # submit multiple metrics at once
9
+ Appoptics::Metrics.submit cpu: 63, memory: 213
10
+
11
+ # submit a metric with a custom source
12
+ Appoptics::Metrics.submit cpu: {source: 'myapp', value: 75}
13
+
14
+ # if you are sending many metrics it is much more performant
15
+ # to submit them in sets rather than individually:
16
+
17
+ queue = Appoptics::Metrics::Queue.new
18
+
19
+ queue.add 'disk.free' => 1223121
20
+ queue.add memory: 2321
21
+ queue.add cpu: {source: 'myapp', value: 52}
22
+ #...
23
+
24
+ queue.submit
@@ -0,0 +1,27 @@
1
+ # send a set of metrics every 60 seconds
2
+
3
+ require 'appoptics-api-ruby'
4
+
5
+ Appoptics::Metrics.authenticate 'my email', 'my api key'
6
+ queue = Appoptics::Metrics::Queue.new
7
+
8
+ def sleep_until(time)
9
+ secs = time - Time.now
10
+ puts "sleeping for #{secs}"
11
+ sleep secs
12
+ end
13
+
14
+ loop do
15
+ start = Time.now
16
+
17
+ queue.add 'my.metric' => 1234
18
+ # do work, add more metrics..
19
+
20
+ begin
21
+ queue.submit
22
+ rescue Exception => e
23
+ $stderr.puts e
24
+ end
25
+
26
+ sleep_until(start+60)
27
+ end
@@ -0,0 +1,95 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'base64'
5
+ require 'forwardable'
6
+
7
+ require 'metrics/aggregator'
8
+ require 'metrics/annotator'
9
+ require 'metrics/client'
10
+ require 'metrics/collection'
11
+ require 'metrics/connection'
12
+ require 'metrics/errors'
13
+ require 'metrics/persistence'
14
+ require 'metrics/queue'
15
+ require 'metrics/smart_json'
16
+ require 'metrics/util'
17
+ require 'metrics/version'
18
+
19
+ module Appoptics
20
+
21
+ # Metrics provides a simple wrapper for the Metrics web API with a
22
+ # number of added conveniences for common use cases.
23
+ #
24
+ # See the {file:README.md README} for more information and examples.
25
+ #
26
+ # @example Simple use case
27
+ # Appoptics::Metrics.authenticate 'email', 'api_key'
28
+ #
29
+ # # list current metrics
30
+ # Appoptics::Metrics.metrics
31
+ #
32
+ # # submit a metric immediately
33
+ # Appoptics::Metrics.submit foo: 12712
34
+ #
35
+ # # fetch the last 10 values of foo
36
+ # Appoptics::Metrics.get_measurements :foo, count: 10
37
+ #
38
+ # @example Queuing metrics for submission
39
+ # queue = Appoptics::Metrics::Queue.new
40
+ #
41
+ # # queue some metrics
42
+ # queue.add foo: 12312
43
+ # queue.add bar: 45678
44
+ #
45
+ # # send the metrics
46
+ # queue.submit
47
+ #
48
+ # @example Using a Client object
49
+ # client = Appoptics::Metrics::Client.new
50
+ # client.authenticate 'email', 'api_key'
51
+ #
52
+ # # list client's metrics
53
+ # client.metrics
54
+ #
55
+ # # create an associated queue
56
+ # queue = client.new_queue
57
+ #
58
+ # # queue up some metrics and submit
59
+ # queue.add foo: 12345
60
+ # queue.add bar: 45678
61
+ # queue.submit
62
+ #
63
+ # @note Most of the methods you can call directly on Appoptics::Metrics are
64
+ # delegated to {Client} and are documented there.
65
+ module Metrics
66
+ extend SingleForwardable
67
+
68
+ TYPES = [:counter, :gauge]
69
+ PLURAL_TYPES = TYPES.map { |type| "#{type}s".to_sym }
70
+ MIN_MEASURE_TIME = (Time.now-(3600*24*365)).to_i
71
+
72
+ # Most of the singleton methods of Appoptics::Metrics are actually
73
+ # being called on a global Client instance. See further docs on
74
+ # Client.
75
+ #
76
+ def_delegators :client, :agent_identifier, :annotate,
77
+ :api_endpoint, :api_endpoint=, :authenticate,
78
+ :connection, :create_snapshot, :delete_metrics,
79
+ :faraday_adapter, :faraday_adapter=, :get_composite,
80
+ :get_measurements, :get_metric, :get_series,
81
+ :get_snapshot, :get_source, :metrics,
82
+ :persistence, :persistence=, :persister, :proxy, :proxy=,
83
+ :sources, :submit, :update_metric, :update_metrics,
84
+ :update_source
85
+
86
+ # The Appoptics::Metrics::Client being used by module-level
87
+ # access.
88
+ #
89
+ # @return [Client]
90
+ def self.client
91
+ @client ||= Appoptics::Metrics::Client.new
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,138 @@
1
+ require 'aggregate'
2
+ require 'metrics/processor'
3
+
4
+ module Appoptics
5
+ module Metrics
6
+
7
+ # If you are measuring something very frequently you can sample into
8
+ # an aggregator and it will track and submit a single aggregate
9
+ # measurement
10
+ #
11
+ # @example
12
+ # aggregator = Libato::Metrics::Aggregator.new
13
+ #
14
+ # 40.times do
15
+ # # do work...
16
+ # aggregator.add 'work.time' => work_time
17
+ # end
18
+ #
19
+ # # send directly
20
+ # aggregator.submit
21
+ #
22
+ # # or merge into a queue for submission
23
+ # queue.merge!(aggregator)
24
+ #
25
+ class Aggregator
26
+ SEPARATOR = '%%' # must not be in valid tags and/or source criteria
27
+
28
+ include Processor
29
+
30
+ attr_reader :source
31
+
32
+ # @option opts [Integer] :autosubmit_interval If set the aggregator will auto-submit if the given number of seconds has passed when a new metric is added.
33
+ # @option opts [Boolean] :clear_failures Should the aggregator remove all stored data if it runs into problems with a request? (default: false)
34
+ # @option opts [Client] :client The client object to use to connect to Metrics. (default: Appoptics::Metrics.client)
35
+ # @option opts [Time|Integer] :measure_time A default measure_time to use for measurements added.
36
+ # @option opts [String] :prefix If set will apply the given prefix to all metric names of measurements added.
37
+ # @option opts [String] :source The default source to use for measurements added.
38
+ def initialize(opts={})
39
+ @aggregated = {}
40
+ setup_common_options(opts)
41
+ end
42
+
43
+ # Add a metric entry to the metric set:
44
+ #
45
+ # @example Basic use
46
+ # aggregator.add 'request.time' => 30.24
47
+ #
48
+ # @example With a custom source
49
+ # aggregator.add 'request.time' => {value: 20.52, source: 'staging'}
50
+ #
51
+ # @param [Hash] measurements measurements to add
52
+ # @return [Aggregator] returns self
53
+ def add(measurements)
54
+ measurements.each do |metric, data|
55
+ entry = {}
56
+ if @prefix
57
+ metric = "#{@prefix}.#{metric}"
58
+ end
59
+ entry[:name] = metric.to_s
60
+ if data.respond_to?(:each) # hash form
61
+ validate_parameters(data)
62
+ value = data[:value]
63
+ if data[:source]
64
+ metric = "#{metric}#{SEPARATOR}#{data[:source]}"
65
+ entry[:source] = data[:source].to_s
66
+ elsif data[:tags] && data[:tags].respond_to?(:each)
67
+ metric = Appoptics::Metrics::Util.build_key_for(metric.to_s, data[:tags])
68
+ entry[:tags] = data[:tags]
69
+ end
70
+ else
71
+ value = data
72
+ end
73
+
74
+ @aggregated[metric] = {} unless @aggregated[metric]
75
+ @aggregated[metric][:aggregate] ||= Aggregate.new
76
+ @aggregated[metric][:aggregate] << value
77
+ @aggregated[metric].merge!(entry)
78
+ end
79
+ autosubmit_check
80
+ self
81
+ end
82
+
83
+ # Returns true if aggregate contains no measurements
84
+ #
85
+ # @return [Boolean]
86
+ def empty?
87
+ @aggregated.empty?
88
+ end
89
+
90
+ # Remove all queued metrics
91
+ #
92
+ def clear
93
+ @aggregated = {}
94
+ end
95
+ alias :flush :clear
96
+
97
+ # Returns currently queued data
98
+ #
99
+ def queued
100
+ entries = []
101
+ multidimensional = has_tags?
102
+
103
+ @aggregated.each_value do |data|
104
+ entry = {
105
+ name: data[:name],
106
+ count: data[:aggregate].count,
107
+ sum: data[:aggregate].sum,
108
+ # TODO: make float/non-float consistent in the gem
109
+ min: data[:aggregate].min.to_f,
110
+ max: data[:aggregate].max.to_f
111
+ # TODO: expose v.sum2 and include
112
+ }
113
+ if data[:source]
114
+ entry[:source] = data[:source]
115
+ elsif data[:tags]
116
+ multidimensional = true
117
+ entry[:tags] = data[:tags]
118
+ end
119
+ multidimensional = true if data[:time]
120
+ entries << entry
121
+ end
122
+ time = multidimensional ? :time : :measure_time
123
+ req =
124
+ if multidimensional
125
+ { measurements: entries }
126
+ else
127
+ { gauges: entries }
128
+ end
129
+ req[:source] = @source if @source
130
+ req[:tags] = @tags if has_tags?
131
+ req[time] = @time if @time
132
+
133
+ req
134
+ end
135
+
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,145 @@
1
+ module Appoptics::Metrics
2
+
3
+ # Read & write annotation streams for a given client connection.
4
+ class Annotator
5
+
6
+ # @option options [Client] :client Client instance used to connect to Metrics
7
+ def initialize(options={})
8
+ @client = options[:client] || Appoptics::Metrics.client
9
+ end
10
+
11
+ # Creates a new annotation on the annotation stream
12
+ #
13
+ # @example Simple annotation
14
+ # annotator.add :deployments, 'deployed v45'
15
+ #
16
+ # @example Annotation with start and end times
17
+ # annotator.add :deployments, 'deployed v56', start_time: start,
18
+ # end_time: end_time
19
+ #
20
+ # @example Annotation with a specific source
21
+ # annotator.add :deployments, 'deployed v60', source: 'app12'
22
+ #
23
+ # @example Annotation with a description
24
+ # annotator.add :deployments, 'deployed v61',
25
+ # description: '9b562b2: shipped new feature foo!'
26
+ #
27
+ # @example Annotate with automatic start and end times
28
+ # annotator.add(:deployments, 'deployed v62') do
29
+ # # do work..
30
+ # end
31
+ #
32
+ def add(stream, title, options={})
33
+ options[:title] = title
34
+ options[:start_time] = (options[:start_time] || Time.now).to_i
35
+ if options[:end_time]
36
+ options[:end_time] = options[:end_time].to_i
37
+ end
38
+ payload = SmartJSON.write(options)
39
+ response = connection.post("annotations/#{stream}", payload)
40
+ # will raise exception if not 200 OK
41
+ event = SmartJSON.read(response.body)
42
+ if block_given?
43
+ yield
44
+ update_event stream, event['id'], end_time: Time.now.to_i
45
+ # need to get updated representation
46
+ event = fetch_event stream, event['id']
47
+ end
48
+ event
49
+ end
50
+
51
+ # client instance used by this object
52
+ def client
53
+ @client
54
+ end
55
+
56
+ # Delete an annotation stream
57
+ #
58
+ # @example Delete the 'deployment' annotation stream
59
+ # annotator.delete :deployment
60
+ #
61
+ def delete(stream)
62
+ connection.delete do |request|
63
+ request.url connection.build_url("annotations/#{stream}")
64
+ end
65
+ # expects 204, middleware will raise exception otherwise
66
+ true
67
+ end
68
+
69
+ # Delete an event from a given annotation stream
70
+ #
71
+ # @example Delete event with id 42 from 'deployment'
72
+ # annotator.delete_event :deployment, 42
73
+ #
74
+ def delete_event(stream, id)
75
+ connection.delete do |request|
76
+ request.url connection.build_url("annotations/#{stream}/#{id}")
77
+ end
78
+ # expects 204, middleware will raise exception otherwise
79
+ true
80
+ end
81
+
82
+ # Get a list of annotation events on a given annotation stream
83
+ #
84
+ # @example See properties of the 'deployments' annotation stream
85
+ # annotator.fetch :deployments
86
+ #
87
+ # @example Get events on 'deployments' between start and end times
88
+ # annotator.fetch :deployments, start_time: start,
89
+ # end_time: end_time
90
+ #
91
+ # @example Source-limited listing
92
+ # annotator.fetch :deployments, sources: ['foo','bar','baz'],
93
+ # start_time: start, end_time: end_time
94
+ #
95
+ def fetch(stream, options={})
96
+ response = connection.get("annotations/#{stream}", options)
97
+ SmartJSON.read(response.body)
98
+ end
99
+
100
+ # Get properties for a given annotation stream event
101
+ #
102
+ # @example Get event
103
+ # annotator.fetch :deployments, 23
104
+ #
105
+ def fetch_event(stream, id)
106
+ response = connection.get("annotations/#{stream}/#{id}")
107
+ SmartJSON.read(response.body)
108
+ end
109
+
110
+ # List currently existing annotation streams
111
+ #
112
+ # @example List all annotation streams
113
+ # streams = annotator.list
114
+ #
115
+ # @example List annotator streams with 'deploy' in the name
116
+ # deploy_streams = annotator.list name: 'deploy'
117
+ #
118
+ def list(options={})
119
+ response = connection.get("annotations", options)
120
+ SmartJSON.read(response.body)
121
+ end
122
+
123
+ # Update an event's properties
124
+ #
125
+ # @example Set an end time for a previously submitted event
126
+ # annotator.update_event 'deploys', 'v24', end_time: end_time
127
+ #
128
+ def update_event(stream, id, options={})
129
+ url = "annotations/#{stream}/#{id}"
130
+ connection.put do |request|
131
+ request.url connection.build_url(url)
132
+ request.body = SmartJSON.write(options)
133
+ end
134
+ # expects 204 will raise exception otherwise
135
+ end
136
+
137
+ private
138
+
139
+ def connection
140
+ client.connection
141
+ end
142
+
143
+ end
144
+
145
+ end