libhoney 1.12.1 → 1.13.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6c5ff396b0f0a043e1ee4b1ae1d89342fdfbc99861e5cdb22d7c5843acd112b
4
- data.tar.gz: ee861538fa2020c201dc5be4477372c9778c89b910d0f8e2faa2168371ea6f97
3
+ metadata.gz: '085a461457305c6c626057ffa084b66bb3d364d3bf538a66116de45c8195b78e'
4
+ data.tar.gz: 25cdef56d6c38db3b91f411763049acf5f536514002c1ca4ad519c406cb4309c
5
5
  SHA512:
6
- metadata.gz: 3e53c57343b02b2de9b028a06ed6800a78593267169ed9ea302cc2a7783bdc2ea4db4053321bd97162b0afdf9d8a97e3abbcd3c63f599d5c2ef02b69ea7fe886
7
- data.tar.gz: 4d513bbdd0decf922d3a6462a9dd644805cec48d353a2059286b5e408fb3ff94554300fd0ffe1b7028b21de6fbe2970e5a0effa7b1f4647f3fe812db00ab1e2c
6
+ metadata.gz: 5bd46c22d978845d9b124fda903632186ab69003f8a319db662718e222da0d18ce2215a9f54c71e413856a112b32fb6871a5a9d5f1539121120da2ea5476b30e
7
+ data.tar.gz: 8982666d9a862224d7ce2200ac79f0876ded8ecc05a6b010c7ff57e77cb90e978c8ffb64fa1edbc3b1951a2b0ab4c1ba2722c9bacc1a24ee980347535f7caf93
data/.rubocop.yml CHANGED
@@ -10,7 +10,11 @@ Lint/RescueException:
10
10
  Exclude:
11
11
  - 'lib/libhoney/transmission.rb'
12
12
 
13
+ Metrics/BlockLength:
14
+ Max: 35
15
+
13
16
  Metrics/ClassLength:
17
+ Max: 200
14
18
  Exclude:
15
19
  - test/*
16
20
 
@@ -5,11 +5,11 @@ module Libhoney
5
5
  attr_accessor :duration, :status_code, :metadata, :error
6
6
 
7
7
  def initialize(duration: 0,
8
- status_code: HTTP::Response::Status.new(0),
8
+ status_code: 0,
9
9
  metadata: nil,
10
10
  error: nil)
11
11
  @duration = duration
12
- @status_code = status_code
12
+ @status_code = HTTP::Response::Status.new(status_code)
13
13
  @metadata = metadata
14
14
  @error = error
15
15
  end
@@ -1,3 +1,5 @@
1
+ require 'json'
2
+ require 'timeout'
1
3
  require 'libhoney/response'
2
4
 
3
5
  module Libhoney
@@ -17,16 +19,19 @@ module Libhoney
17
19
  @block_on_send = block_on_send
18
20
  @block_on_responses = block_on_responses
19
21
  @max_batch_size = max_batch_size
20
- @send_frequency = send_frequency
22
+ # convert to seconds
23
+ @send_frequency = send_frequency.fdiv(1000)
21
24
  @max_concurrent_batches = max_concurrent_batches
22
25
  @pending_work_capacity = pending_work_capacity
23
26
  @send_timeout = send_timeout
24
27
  @user_agent = build_user_agent(user_agent_addition).freeze
25
28
 
26
- # use a SizedQueue so the producer will block on adding to the send_queue when @block_on_send is true
27
- @send_queue = SizedQueue.new(@pending_work_capacity)
28
- @threads = []
29
- @lock = Mutex.new
29
+ @send_queue = Queue.new
30
+ @threads = []
31
+ @lock = Mutex.new
32
+ # use a SizedQueue so the producer will block on adding to the batch_queue when @block_on_send is true
33
+ @batch_queue = SizedQueue.new(@pending_work_capacity)
34
+ @batch_thread = nil
30
35
  end
31
36
 
32
37
  def add(event)
@@ -35,7 +40,7 @@ module Libhoney
35
40
  raise ArgumentError, "No Dataset for Honeycomb. Can't send datasetless." if event.dataset == ''
36
41
 
37
42
  begin
38
- @send_queue.enq(event, !@block_on_send)
43
+ @batch_queue.enq(event, !@block_on_send)
39
44
  rescue ThreadError
40
45
  # happens if the queue was full and block_on_send = false.
41
46
  end
@@ -44,57 +49,53 @@ module Libhoney
44
49
  end
45
50
 
46
51
  def send_loop
47
- http_clients = Hash.new do |h, api_host|
48
- h[api_host] = HTTP.timeout(connect: @send_timeout, write: @send_timeout, read: @send_timeout)
49
- .persistent(api_host)
50
- .headers(
51
- 'User-Agent' => @user_agent,
52
- 'Content-Type' => 'application/json'
53
- )
54
- end
52
+ http_clients = build_http_clients
55
53
 
56
54
  # eat events until we run out
57
55
  loop do
58
- event = @send_queue.pop
59
- break if event.nil?
56
+ api_host, writekey, dataset, batch = @send_queue.pop
57
+ break if batch.nil?
60
58
 
61
59
  before = Time.now
62
60
 
63
61
  begin
64
- http = http_clients[event.api_host]
65
- url = '/1/events/' + Addressable::URI.escape(event.dataset.dup)
66
-
67
- resp = http.post(url,
68
- json: event.data,
69
- headers: {
70
- 'X-Honeycomb-Team' => event.writekey,
71
- 'X-Honeycomb-SampleRate' => event.sample_rate,
72
- 'X-Event-Time' => event.timestamp.iso8601(3)
73
- })
74
-
75
- # "You must consume response before sending next request via persistent connection"
76
- # https://github.com/httprb/http/wiki/Persistent-Connections-%28keep-alive%29#note-using-persistent-requests-correctly
77
- resp.flush
78
-
79
- response = Response.new(status_code: resp.status)
80
- rescue Exception => error
62
+ http = http_clients[api_host]
63
+ body = serialize_batch(batch)
64
+ next if body.nil?
65
+
66
+ headers = {
67
+ 'Content-Type' => 'application/json',
68
+ 'X-Honeycomb-Team' => writekey
69
+ }
70
+
71
+ response = http.post(
72
+ "/1/batch/#{Addressable::URI.escape(dataset)}",
73
+ body: body,
74
+ headers: headers
75
+ )
76
+ process_response(response, before, batch)
77
+ rescue Exception => e
81
78
  # catch a broader swath of exceptions than is usually good practice,
82
79
  # because this is effectively the top-level exception handler for the
83
80
  # sender threads, and we don't want those threads to die (leaving
84
81
  # nothing consuming the queue).
85
- response = Response.new(error: error)
86
- ensure
87
- if response
88
- response.duration = Time.now - before
89
- response.metadata = event.metadata
82
+ begin
83
+ batch.each do |event|
84
+ # nil events in the batch should already have had an error
85
+ # response enqueued in #serialize_batch
86
+ next if event.nil?
87
+
88
+ Response.new(error: e).tap do |error_response|
89
+ error_response.metadata = event.metadata
90
+ begin
91
+ @responses.enq(error_response, !@block_on_responses)
92
+ rescue ThreadError
93
+ end
94
+ end
95
+ end
96
+ rescue ThreadError
90
97
  end
91
98
  end
92
-
93
- begin
94
- @responses.enq(response, !@block_on_responses) if response
95
- rescue ThreadError
96
- # happens if the queue was full and block_on_send = false.
97
- end
98
99
  end
99
100
  ensure
100
101
  http_clients.each do |_, http|
@@ -108,7 +109,13 @@ module Libhoney
108
109
 
109
110
  def close(drain)
110
111
  # if drain is false, clear the remaining unprocessed events from the queue
111
- @send_queue.clear if drain == false
112
+ unless drain
113
+ @batch_queue.clear
114
+ @send_queue.clear
115
+ end
116
+
117
+ @batch_queue.enq(nil)
118
+ @batch_thread.join
112
119
 
113
120
  # send @threads.length number of nils so each thread will fall out of send_loop
114
121
  @threads.length.times { @send_queue << nil }
@@ -121,8 +128,78 @@ module Libhoney
121
128
  0
122
129
  end
123
130
 
131
+ def batch_loop
132
+ next_send_time = Time.now + @send_frequency
133
+ batched_events = Hash.new do |h, key|
134
+ h[key] = []
135
+ end
136
+
137
+ loop do
138
+ begin
139
+ while (event = Timeout.timeout(@send_frequency) { @batch_queue.pop })
140
+ key = [event.api_host, event.writekey, event.dataset]
141
+ batched_events[key] << event
142
+ end
143
+
144
+ break
145
+ rescue Exception
146
+ ensure
147
+ next_send_time = flush_batched_events(batched_events) if Time.now > next_send_time
148
+ end
149
+ end
150
+
151
+ flush_batched_events(batched_events)
152
+ end
153
+
124
154
  private
125
155
 
156
+ def process_response(http_response, before, batch)
157
+ index = 0
158
+ http_response.parse.each do |event|
159
+ index += 1 while batch[index].nil? && index < batch.size
160
+ break unless (batched_event = batch[index])
161
+
162
+ Response.new(status_code: event['status']).tap do |response|
163
+ response.duration = Time.now - before
164
+ response.metadata = batched_event.metadata
165
+ begin
166
+ @responses.enq(response, !@block_on_responses)
167
+ rescue ThreadError
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ def serialize_batch(batch)
174
+ payload = []
175
+ batch.map! do |event|
176
+ begin
177
+ e = {
178
+ time: event.timestamp.iso8601(3),
179
+ samplerate: event.sample_rate,
180
+ data: event.data
181
+ }
182
+ payload << JSON.generate(e)
183
+
184
+ event
185
+ rescue StandardError => e
186
+ Response.new(error: e).tap do |response|
187
+ response.metadata = event.metadata
188
+ begin
189
+ @responses.enq(response, !@block_on_responses)
190
+ rescue ThreadError
191
+ end
192
+ end
193
+
194
+ nil
195
+ end
196
+ end
197
+
198
+ return if payload.empty?
199
+
200
+ "[#{payload.join(',')}]"
201
+ end
202
+
126
203
  def build_user_agent(user_agent_addition)
127
204
  ua = "libhoney-rb/#{VERSION}"
128
205
  ua << " #{user_agent_addition}" if user_agent_addition
@@ -131,9 +208,32 @@ module Libhoney
131
208
 
132
209
  def ensure_threads_running
133
210
  @lock.synchronize do
211
+ @batch_thread = Thread.new { batch_loop } unless @batch_thread && @batch_thread.alive?
134
212
  @threads.select!(&:alive?)
135
213
  @threads << Thread.new { send_loop } while @threads.length < @max_concurrent_batches
136
214
  end
137
215
  end
216
+
217
+ def flush_batched_events(batched_events)
218
+ batched_events.each do |(api_host, writekey, dataset), events|
219
+ events.each_slice(@max_batch_size) do |batch|
220
+ @send_queue << [api_host, writekey, dataset, batch]
221
+ end
222
+ end
223
+ batched_events.clear
224
+
225
+ Time.now + @send_frequency
226
+ end
227
+
228
+ def build_http_clients
229
+ Hash.new do |h, api_host|
230
+ h[api_host] = HTTP.timeout(connect: @send_timeout, write: @send_timeout, read: @send_timeout)
231
+ .persistent(api_host)
232
+ .headers(
233
+ 'User-Agent' => @user_agent,
234
+ 'Content-Type' => 'application/json'
235
+ )
236
+ end
237
+ end
138
238
  end
139
239
  end
@@ -1,3 +1,3 @@
1
1
  module Libhoney
2
- VERSION = '1.12.1'.freeze
2
+ VERSION = '1.13.0'.freeze
3
3
  end
data/libhoney.gemspec CHANGED
@@ -26,7 +26,9 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency 'bundler'
27
27
  spec.add_development_dependency 'minitest', '~> 5.0'
28
28
  spec.add_development_dependency 'rake', '~> 12.3'
29
- spec.add_development_dependency 'rubocop'
29
+ spec.add_development_dependency 'rubocop', '< 0.69'
30
+ spec.add_development_dependency 'sinatra'
31
+ spec.add_development_dependency 'sinatra-contrib'
30
32
  spec.add_development_dependency 'webmock', '~> 3.4'
31
33
  spec.add_development_dependency 'yard'
32
34
  spec.add_development_dependency 'yardstick', '~> 0.9'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: libhoney
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.12.1
4
+ version: 1.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Honeycomb.io Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-03-13 00:00:00.000000000 Z
11
+ date: 2019-06-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bump
@@ -68,6 +68,34 @@ dependencies:
68
68
  version: '12.3'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "<"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.69'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "<"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.69'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sinatra
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
+ - !ruby/object:Gem::Dependency
98
+ name: sinatra-contrib
71
99
  requirement: !ruby/object:Gem::Requirement
72
100
  requirements:
73
101
  - - ">="
@@ -206,7 +234,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
206
234
  - !ruby/object:Gem::Version
207
235
  version: '0'
208
236
  requirements: []
209
- rubygems_version: 3.0.3
237
+ rubyforge_project:
238
+ rubygems_version: 2.7.7
210
239
  signing_key:
211
240
  specification_version: 4
212
241
  summary: send data to Honeycomb