honeycomb-beeline 2.1.2 → 2.4.2

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.
data/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![Build Status](https://circleci.com/gh/honeycombio/beeline-ruby.svg?style=svg)](https://circleci.com/gh/honeycombio/beeline-ruby)
4
4
  [![Gem Version](https://badge.fury.io/rb/honeycomb-beeline.svg)](https://badge.fury.io/rb/honeycomb-beeline)
5
+ [![codecov](https://codecov.io/gh/honeycombio/beeline-ruby/branch/main/graph/badge.svg)](https://codecov.io/gh/honeycombio/beeline-ruby)
5
6
 
6
7
  This package makes it easy to instrument your Ruby web app to send useful events to [Honeycomb](https://www.honeycomb.io), a service for debugging your software in production.
7
8
  - [Usage and Examples](https://docs.honeycomb.io/getting-data-in/beelines/ruby-beeline/)
@@ -24,6 +25,13 @@ Built in instrumentation for:
24
25
  - Sequel
25
26
  - Sinatra
26
27
 
28
+ ## Testing
29
+ Find `rspec` test files in the `spec` directory.
30
+
31
+ To run tests on gem-specific instrumentations or across various dependency versions, use [appraisal](https://github.com/thoughtbot/appraisal) (further instructions in the readme for that gem). Find gem sets in the `Appraisals` config.
32
+
33
+ To run a specific file: `bundle exec appraisal <gem set> rspec <path/to/file>`
34
+
27
35
  ## Get in touch
28
36
 
29
37
  Please reach out to [support@honeycomb.io](mailto:support@honeycomb.io) or ping
data/UPGRADING.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Upgrade Guide
2
2
 
3
+ ## 1.0.0 - 2.0.0
4
+
5
+ 1. See release notes: https://github.com/honeycombio/beeline-ruby/releases/tag/v2.0.0
6
+ 1. This update requires no code changes, but you must be aware of certain instrumentation changes. New fields will be added to your dataset and other fields will be removed.
7
+ 1. ActionController::Parameters will now result in extra fields, or nested json, depending on your unfurl settings.
8
+ 1. aws.params are now exploded into separate fields.
9
+ 1. request.error becomes error.
10
+ 1. request.error_detail becomes error_detail
11
+ 1. request.protocol becomes request.scheme
12
+
3
13
  ## 0.8.0 - 1.0.0
4
14
 
5
15
  1. If you have a web application, remove beeline configuration from the `config.ru` file
@@ -42,11 +42,13 @@ Gem::Specification.new do |spec|
42
42
  spec.add_development_dependency "appraisal"
43
43
  spec.add_development_dependency "bump"
44
44
  spec.add_development_dependency "bundler"
45
+ spec.add_development_dependency "codecov"
45
46
  spec.add_development_dependency "overcommit", "~> 0.46.0"
46
47
  spec.add_development_dependency "pry", "< 0.13.0"
47
48
  spec.add_development_dependency "pry-byebug", "~> 3.6.0"
48
49
  spec.add_development_dependency "rake"
49
50
  spec.add_development_dependency "rspec", "~> 3.0"
51
+ spec.add_development_dependency "rspec_junit_formatter"
50
52
  spec.add_development_dependency "rubocop", "< 0.69"
51
53
  spec.add_development_dependency "rubocop-performance", "< 1.3.0"
52
54
  spec.add_development_dependency "simplecov"
@@ -26,7 +26,8 @@ module Honeycomb
26
26
  attr_reader :client
27
27
 
28
28
  def_delegators :@client, :libhoney, :start_span, :add_field,
29
- :add_field_to_trace, :current_span, :current_trace
29
+ :add_field_to_trace, :current_span, :current_trace,
30
+ :with_field, :with_trace_field
30
31
 
31
32
  def configure
32
33
  Configuration.new.tap do |config|
@@ -3,7 +3,7 @@
3
3
  module Honeycomb
4
4
  module Beeline
5
5
  NAME = "honeycomb-beeline".freeze
6
- VERSION = "2.1.2".freeze
6
+ VERSION = "2.4.2".freeze
7
7
  USER_AGENT_SUFFIX = "#{NAME}/#{VERSION}".freeze
8
8
  end
9
9
  end
@@ -35,6 +35,8 @@ module Honeycomb
35
35
  @additional_trace_options = {
36
36
  presend_hook: configuration.presend_hook,
37
37
  sample_hook: configuration.sample_hook,
38
+ parser_hook: configuration.http_trace_parser_hook,
39
+ propagation_hook: configuration.http_trace_propagation_hook,
38
40
  }
39
41
 
40
42
  configuration.after_initialize(self)
@@ -87,6 +89,14 @@ module Honeycomb
87
89
  context.current_span.trace.add_field("app.#{key}", value)
88
90
  end
89
91
 
92
+ def with_field(key)
93
+ yield.tap { |value| add_field(key, value) }
94
+ end
95
+
96
+ def with_trace_field(key)
97
+ yield.tap { |value| add_field_to_trace(key, value) }
98
+ end
99
+
90
100
  private
91
101
 
92
102
  attr_reader :context
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "socket"
4
+ require "honeycomb/propagation/honeycomb"
4
5
 
5
6
  module Honeycomb
6
7
  # Used to configure the Honeycomb client
@@ -60,5 +61,27 @@ module Honeycomb
60
61
  @sample_hook
61
62
  end
62
63
  end
64
+
65
+ def http_trace_parser_hook(&hook)
66
+ if block_given?
67
+ @http_trace_parser_hook = hook
68
+ elsif @http_trace_parser_hook
69
+ @http_trace_parser_hook
70
+ else
71
+ # by default we try to parse incoming honeycomb traces
72
+ HoneycombPropagation::UnmarshalTraceContext.method(:parse_rack_env)
73
+ end
74
+ end
75
+
76
+ def http_trace_propagation_hook(&hook)
77
+ if block_given?
78
+ @http_trace_propagation_hook = hook
79
+ elsif @http_trace_propagation_hook
80
+ @http_trace_propagation_hook
81
+ else
82
+ # by default we send outgoing honeycomb trace headers
83
+ HoneycombPropagation::MarshalTraceContext.method(:parse_faraday_env)
84
+ end
85
+ end
63
86
  end
64
87
  end
@@ -22,7 +22,9 @@ module Honeycomb
22
22
  span.add_field "meta.package", "faraday"
23
23
  span.add_field "meta.package_version", ::Faraday::VERSION
24
24
 
25
- env.request_headers["X-Honeycomb-Trace"] = span.to_trace_header
25
+ if (headers = span.trace_headers(env)).is_a?(Hash)
26
+ env.request_headers.merge!(headers)
27
+ end
26
28
 
27
29
  @app.call(env).tap do |response|
28
30
  span.add_field "response.status_code", response.status
@@ -17,6 +17,7 @@ module Honeycomb
17
17
  ["HTTP_X_FORWARDED_PROTO", "request.header.x_forwarded_proto"],
18
18
  ["HTTP_X_FORWARDED_PORT", "request.header.x_forwarded_port"],
19
19
  ["HTTP_ACCEPT", "request.header.accept"],
20
+ ["HTTP_ACCEPT_ENCODING", "request.header.accept_encoding"],
20
21
  ["HTTP_ACCEPT_LANGUAGE", "request.header.accept_language"],
21
22
  ["CONTENT_TYPE", "request.header.content_type"],
22
23
  ["HTTP_USER_AGENT", "request.header.user_agent"],
@@ -32,8 +33,10 @@ module Honeycomb
32
33
 
33
34
  def call(env)
34
35
  req = ::Rack::Request.new(env)
35
- hny = env["HTTP_X_HONEYCOMB_TRACE"]
36
- client.start_span(name: "http_request", serialized_trace: hny) do |span|
36
+ client.start_span(
37
+ name: "http_request",
38
+ serialized_trace: env,
39
+ ) do |span|
37
40
  add_field = lambda do |key, value|
38
41
  unless value.nil? || (value.respond_to?(:empty?) && value.empty?)
39
42
  span.add_field(key, value)
@@ -45,11 +48,12 @@ module Honeycomb
45
48
  span.add_field("request.secure", req.ssl?)
46
49
  span.add_field("request.xhr", req.xhr?)
47
50
 
48
- status, headers, body = app.call(env)
49
-
50
- add_package_information(env, &add_field)
51
-
52
- extract_user_information(env, &add_field)
51
+ begin
52
+ status, headers, body = call_with_hook(env, span, &add_field)
53
+ ensure
54
+ add_package_information(env, &add_field)
55
+ extract_user_information(env, &add_field)
56
+ end
53
57
 
54
58
  span.add_field("response.status_code", status)
55
59
  span.add_field("response.content_type", headers["Content-Type"])
@@ -69,6 +73,12 @@ module Honeycomb
69
73
  end
70
74
  end
71
75
 
76
+ private
77
+
78
+ def call_with_hook(env, _span, &_add_field)
79
+ app.call(env)
80
+ end
81
+
72
82
  # Rack middleware
73
83
  class Middleware
74
84
  include Rack
@@ -89,6 +89,16 @@ module Honeycomb
89
89
  include Rack
90
90
  include Warden
91
91
  include Rails
92
+
93
+ def call_with_hook(env, span, &_add_field)
94
+ super
95
+ rescue StandardError => e
96
+ wrapped = ActionDispatch::ExceptionWrapper.new(nil, e)
97
+
98
+ span.add_field "response.status_code", wrapped.status_code
99
+
100
+ raise e
101
+ end
92
102
  end
93
103
  end
94
104
  end
@@ -159,7 +159,17 @@ module Honeycomb
159
159
  # * :logger - just some Ruby object, not useful
160
160
  # * :_parsed - implementation detail
161
161
  def ignore?(option)
162
- %i[url password logger _parsed].include?(option)
162
+ # Redis options may be symbol or string keys.
163
+ #
164
+ # This normalizes `option` using `to_sym` as benchmarking on Ruby MRI
165
+ # v2.6.6 and v2.7.3 has shown that was faster compared to `to_s`.
166
+ # However, `nil` does not support `to_sym`. This uses a guard clause to
167
+ # handle the `nil` case because this is still faster than safe
168
+ # navigation. Also this lib still supports Ruby 2.2.0; which does not
169
+ # include safe navigation.
170
+ return true unless option
171
+
172
+ %i[url password logger _parsed].include?(option.to_sym)
163
173
  end
164
174
 
165
175
  def format(cmd)
@@ -177,16 +187,6 @@ module Honeycomb
177
187
  args.map! { "[sanitized]" }
178
188
  end
179
189
 
180
- def prettify(arg)
181
- quotes = false
182
- pretty = "".dup
183
- arg.to_s.each_char do |c|
184
- quotes ||= needs_quotes?(c)
185
- pretty << escape(c)
186
- end
187
- quotes ? "\"#{pretty}\"" : pretty
188
- end
189
-
190
190
  # This aims to replicate the algorithms used by redis-cli.
191
191
  #
192
192
  # @see https://github.com/antirez/redis/blob/0f026af185e918a9773148f6ceaa1b084662be88/src/sds.c#L940-L1067
@@ -194,54 +194,15 @@ module Honeycomb
194
194
  #
195
195
  # @see https://github.com/antirez/redis/blob/0f026af185e918a9773148f6ceaa1b084662be88/src/sds.c#L878-L907
196
196
  # The redis-cli printing algorithm
197
- def escape(char)
198
- return escape_with_backslash(char) if escape_with_backslash?(char)
199
- return escape_with_hex_codes(char) if escape_with_hex_codes?(char)
200
-
201
- char
202
- end
203
-
204
- # A lookup table for backslash-escaped characters.
205
- #
206
- # This is used by {#escape_with_backslash?} and {#escape_with_backslash}
207
- # to replicate the hard-coded `case` statements in redis-cli. As of this
208
- # writing, Redis recognizes a handful of standard C escape sequences,
209
- # like "\n" for newlines.
210
- #
211
- # Because {#prettify} will output double quoted strings if any escaping
212
- # is needed, this table must additionally consider the double-quote to be
213
- # a backslash-escaped character. For example, instead of generating
214
- #
215
- # '"hello"'
216
- #
217
- # we'll generate
218
- #
219
- # "\"hello\""
220
- #
221
- # even though redis-cli would technically recognize the single-quoted
222
- # version.
223
- #
224
- # @see https://github.com/antirez/redis/blob/0f026af185e918a9773148f6ceaa1b084662be88/src/sds.c#L888-L896
225
- # The redis-cli algorithm for outputting standard escape sequences
226
- BACKSLASHES = {
227
- "\\" => "\\\\",
228
- '"' => '\\"',
229
- "\n" => "\\n",
230
- "\r" => "\\r",
231
- "\t" => "\\t",
232
- "\a" => "\\a",
233
- "\b" => "\\b",
234
- }.freeze
235
-
236
- def escape_with_backslash?(char)
237
- BACKSLASHES.key?(char)
238
- end
239
-
240
- def escape_with_backslash(char)
241
- BACKSLASHES.fetch(char, char)
197
+ def prettify(arg)
198
+ pretty = arg.to_s.dup
199
+ pretty.encode!("UTF-8", "binary", fallback: ->(c) { hex(c) })
200
+ pretty.gsub!(NEEDS_BACKSLASH, BACKSLASH)
201
+ pretty.gsub!(NEEDS_HEX) { |c| hex(c) }
202
+ pretty =~ NEEDS_QUOTES ? "\"#{pretty}\"" : pretty
242
203
  end
243
204
 
244
- # Do we need to hex-encode this character?
205
+ # A regular expression matching characters that need to be hex-encoded.
245
206
  #
246
207
  # This replicates the C isprint() function that redis-cli uses to decide
247
208
  # whether to escape a character in hexadecimal notation, "\xhh". Any
@@ -277,18 +238,95 @@ module Honeycomb
277
238
  # escape it.
278
239
  #
279
240
  # What's more, Ruby's Regexp#=~ method will blow up if the string does
280
- # not have a valid encoding (e.g., in UTF-8). In this case, though,
281
- # {#escape_with_hex_codes} can still convert the bytes that make up the
282
- # invalid character into a hex code. So we preemptively check for
283
- # invalidly-encoded characters before testing the above match.
241
+ # not have a valid encoding (e.g., in UTF-8). We handle this case
242
+ # separately, though, using String#encode! with a :fallback option to
243
+ # hex-encode invalid UTF-8 byte sequences with {#hex}.
284
244
  #
285
245
  # @see https://ruby-doc.org/core-2.6.5/Regexp.html
286
246
  # @see https://github.com/antirez/redis/blob/0f026af185e918a9773148f6ceaa1b084662be88/src/sds.c#L878-L880
287
247
  # @see https://github.com/antirez/redis/blob/0f026af185e918a9773148f6ceaa1b084662be88/src/sds.c#L898-L901
288
248
  # @see https://www.justinweiss.com/articles/3-steps-to-fix-encoding-problems-in-ruby/
289
- def escape_with_hex_codes?(char)
290
- !char.valid_encoding? || char =~ /[^[:print:]&&[:ascii:]]/
291
- end
249
+ NEEDS_HEX = /[^[:print:]&&[:ascii:]]/.freeze
250
+
251
+ # A regular expression for characters that need to be backslash-escaped.
252
+ #
253
+ # Any match of this regexp will be substituted according to the
254
+ # {BACKSLASH} table. This includes standard C escape sequences (newlines,
255
+ # tabs, etc) as well as a couple special considerations:
256
+ #
257
+ # 1. Because {#prettify} will output double quoted strings if any
258
+ # escaping is needed, we must match double quotes (") so they'll be
259
+ # replaced by escaped quotes (\").
260
+ #
261
+ # 2. Backslashes themselves get backslash-escaped, so \ becomes \\.
262
+ # However, strings with invalid UTF-8 encoding will blow up when we
263
+ # try to use String#gsub!, so {#prettify} must first use
264
+ # String#encode! to scrub out invalid characters. It does this by
265
+ # replacing invalid bytes with hex-encoded escape sequences using
266
+ # {#hex}. This will insert sequences like \xhh, which contains a
267
+ # backslash that we *don't* want to escape.
268
+ #
269
+ # Unfortunately, this regexp can't really distinguish between
270
+ # backslashes in the original input vs backslashes resulting from the
271
+ # UTF-8 fallback. We make an effort by using a negative lookahead.
272
+ # That way, only backslashes that *aren't* followed by x + hex digit +
273
+ # hex digit will be escaped.
274
+ NEEDS_BACKSLASH = /["\n\r\t\a\b]|\\(?!x\h\h)/.freeze
275
+
276
+ # A lookup table for backslash-escaped characters.
277
+ #
278
+ # This is used by {#prettify} to replicate the hard-coded `case`
279
+ # statements in redis-cli. As of this writing, Redis recognizes a handful
280
+ # of standard C escape sequences, like "\n" for newlines.
281
+ #
282
+ # Because {#prettify} will output double quoted strings if any escaping
283
+ # is needed, this table must additionally consider the double-quote to be
284
+ # a backslash-escaped character. For example, instead of generating
285
+ #
286
+ # '"hello"'
287
+ #
288
+ # we'll generate
289
+ #
290
+ # "\"hello\""
291
+ #
292
+ # even though redis-cli would technically recognize the single-quoted
293
+ # version.
294
+ #
295
+ # @see https://github.com/antirez/redis/blob/0f026af185e918a9773148f6ceaa1b084662be88/src/sds.c#L888-L896
296
+ # The redis-cli algorithm for outputting standard escape sequences
297
+ BACKSLASH = {
298
+ "\\" => "\\\\",
299
+ '"' => '\\"',
300
+ "\n" => "\\n",
301
+ "\r" => "\\r",
302
+ "\t" => "\\t",
303
+ "\a" => "\\a",
304
+ "\b" => "\\b",
305
+ }.freeze
306
+
307
+ # If the final escaped string needs quotes, it will match this regexp.
308
+ #
309
+ # The overall string returned by {#prettify} should only be quoted if at
310
+ # least one of the following holds:
311
+ #
312
+ # 1. The string contains an escape sequence, broadly demarcated by a
313
+ # backslash. This includes standard escape sequences like "\n" and
314
+ # "\t" as well as hex-encoded bytes using the "\x" escape sequence.
315
+ # Since {#prettify} uses double quotes on its output string, we must
316
+ # also force quotes if the string itself contains a literal
317
+ # double quote. This double quote behavior is handled tacitly by the
318
+ # {NEEDS_BACKSLASH} + {BACKSLASH} replacement.
319
+ #
320
+ # 2. The string contains a single quote. Since redis-cli recognizes
321
+ # single-quoted strings, we want to wrap the {#prettify} output in
322
+ # double quotes so that the literal single quote character isn't
323
+ # mistaken as the delimiter of a new string.
324
+ #
325
+ # 3. The string contains any whitespace characters. If the {#prettify}
326
+ # output weren't wrapped in quotes, whitespace would act as a
327
+ # separator between arguments to the Redis command. To group things
328
+ # together, we need to quote the string.
329
+ NEEDS_QUOTES = /[\\'\s]/.freeze
292
330
 
293
331
  # Hex-encodes a (presumably non-printable or non-ASCII) character.
294
332
  #
@@ -316,38 +354,9 @@ module Honeycomb
316
354
  # @see https://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap07.html
317
355
  # @see https://github.com/antirez/redis/blob/0f026af185e918a9773148f6ceaa1b084662be88/src/sds.c#L878-L880
318
356
  # @see https://github.com/antirez/redis/blob/0f026af185e918a9773148f6ceaa1b084662be88/src/sds.c#L898-L901
319
- def escape_with_hex_codes(char)
357
+ def hex(char)
320
358
  char.bytes.map { |b| Kernel.format("\\x%02x", b) }.join
321
359
  end
322
-
323
- def escape?(char)
324
- escape_with_backslash?(char) || escape_with_hex_codes?(char)
325
- end
326
-
327
- # Should this character cause {#prettify} to wrap its output in quotes?
328
- #
329
- # The overall string returned by {#prettify} should only be quoted if at
330
- # least one of the following holds:
331
- #
332
- # 1. The string contains a character that needs to be escaped. This
333
- # includes standard backslash escape sequences (like "\n" and "\t") as
334
- # well as hex-encoded bytes using the "\x" escape sequence. Since
335
- # {#prettify} uses double quotes on its output string, we must also
336
- # force quotes if the string itself contains a literal double quote.
337
- # This double quote behavior is handled tacitly by {BACKSLASHES}.
338
- #
339
- # 2. The string contains a single quote. Since redis-cli recognizes
340
- # single-quoted strings, we want to wrap the {#prettify} output in
341
- # double quotes so that the literal single quote character isn't
342
- # mistaken as the delimiter of a new string.
343
- #
344
- # 3. The string contains any whitespace characters. If the {#prettify}
345
- # output weren't wrapped in quotes, whitespace would act as a
346
- # separator between arguments to the Redis command. To group things
347
- # together, we need to quote the string.
348
- def needs_quotes?(char)
349
- escape?(char) || char == "'" || char =~ /\s/
350
- end
351
360
  end
352
361
  end
353
362
  end