honeycomb-beeline 1.2.0 → 1.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 579452ea20fce15d631f43ca535105440ec154e23964e0639ab11d9f81ca6e3d
4
- data.tar.gz: 2497fda72bbbc1f9b0957e17a551677989a2b0b50e30a3356218632b64f47fc8
3
+ metadata.gz: ddd03586fceabfcdb1d51f914e4a61cae2c87bbe295341036969a9fca40b3484
4
+ data.tar.gz: cc72f4183017b81d07ee227a4a5aa48c7e933b24f551b61328575fe5d3cb880b
5
5
  SHA512:
6
- metadata.gz: 8b72e001d1217f09608754912114f7d82d0d712fccf1366da8bf299108f408b2fde3721f061975311768732e98693381ce0c0228fec3230ebecb2c7f2ee01313
7
- data.tar.gz: 2179d51cdaab95f132dde4d2570071b0042bb9c7472e2b1de3243db89b54329c99a4fcd7207c226e398742712b6c868854ce5566997597ffcafeed9798ef41e5
6
+ metadata.gz: 93c1a6e138fb39c9a83591c3c10b7427ff28b4f397c2844f631e22d241976e4d3d4f26bb63bdf2380fb35bfe2ae016ff3e659611808608ffeba36c7e10dd6f6c
7
+ data.tar.gz: ba29c324e453cb42180134008cd8586dba0c1b34ad71a3efc2b4a9165ad380a116d2fd533a9c06cdd7ea2e2fc033a9cf09b9d98f8e68a6ebd242631dba1c4c61
@@ -97,6 +97,14 @@ gemfiles:
97
97
  steps:
98
98
  - ruby:
99
99
  gemfile: gemfiles/rails_6.gemfile
100
+ redis-three: &redis-three
101
+ steps:
102
+ - ruby:
103
+ gemfile: gemfiles/redis_3.gemfile
104
+ redis-four: &redis-four
105
+ steps:
106
+ - ruby:
107
+ gemfile: gemfiles/redis_4.gemfile
100
108
 
101
109
  jobs:
102
110
  publish:
@@ -269,6 +277,30 @@ jobs:
269
277
  rails-six-ruby-two-six:
270
278
  <<: *rails-six
271
279
  executor: ruby-two-six
280
+ redis-three-ruby-two-three:
281
+ <<: *redis-three
282
+ executor: ruby-two-three
283
+ redis-three-ruby-two-four:
284
+ <<: *redis-three
285
+ executor: ruby-two-four
286
+ redis-three-ruby-two-five:
287
+ <<: *redis-three
288
+ executor: ruby-two-five
289
+ redis-three-ruby-two-six:
290
+ <<: *redis-three
291
+ executor: ruby-two-six
292
+ redis-four-ruby-two-three:
293
+ <<: *redis-four
294
+ executor: ruby-two-three
295
+ redis-four-ruby-two-four:
296
+ <<: *redis-four
297
+ executor: ruby-two-four
298
+ redis-four-ruby-two-five:
299
+ <<: *redis-four
300
+ executor: ruby-two-five
301
+ redis-four-ruby-two-six:
302
+ <<: *redis-four
303
+ executor: ruby-two-six
272
304
 
273
305
  workflows:
274
306
  version: 2
@@ -463,6 +495,38 @@ workflows:
463
495
  <<: *tag_filters
464
496
  requires:
465
497
  - lint
498
+ - redis-three-ruby-two-three:
499
+ <<: *tag_filters
500
+ requires:
501
+ - lint
502
+ - redis-three-ruby-two-four:
503
+ <<: *tag_filters
504
+ requires:
505
+ - lint
506
+ - redis-three-ruby-two-five:
507
+ <<: *tag_filters
508
+ requires:
509
+ - lint
510
+ - redis-three-ruby-two-six:
511
+ <<: *tag_filters
512
+ requires:
513
+ - lint
514
+ - redis-four-ruby-two-three:
515
+ <<: *tag_filters
516
+ requires:
517
+ - lint
518
+ - redis-four-ruby-two-four:
519
+ <<: *tag_filters
520
+ requires:
521
+ - lint
522
+ - redis-four-ruby-two-five:
523
+ <<: *tag_filters
524
+ requires:
525
+ - lint
526
+ - redis-four-ruby-two-six:
527
+ <<: *tag_filters
528
+ requires:
529
+ - lint
466
530
  - publish:
467
531
  filters:
468
532
  tags:
@@ -518,3 +582,11 @@ workflows:
518
582
  - rails-five-two-ruby-two-six
519
583
  - rails-six-ruby-two-five
520
584
  - rails-six-ruby-two-six
585
+ - redis-three-ruby-two-three
586
+ - redis-three-ruby-two-four
587
+ - redis-three-ruby-two-five
588
+ - redis-three-ruby-two-six
589
+ - redis-four-ruby-two-three
590
+ - redis-four-ruby-two-four
591
+ - redis-four-ruby-two-five
592
+ - redis-four-ruby-two-six
data/Appraisals CHANGED
@@ -67,3 +67,11 @@ appraise "rails-6" do
67
67
  gem "rails", "~> 6.0.0"
68
68
  gem "warden"
69
69
  end
70
+
71
+ appraise "redis-3" do
72
+ gem "redis", "~> 3"
73
+ end
74
+
75
+ appraise "redis-4" do
76
+ gem "redis", "~> 4"
77
+ end
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- honeycomb-beeline (1.2.0)
4
+ honeycomb-beeline (1.3.0)
5
5
  libhoney (~> 1.8)
6
6
 
7
7
  GEM
@@ -44,7 +44,7 @@ GEM
44
44
  iniparse (1.4.4)
45
45
  jaro_winkler (1.5.3)
46
46
  json (2.2.0)
47
- libhoney (1.14.0)
47
+ libhoney (1.14.1)
48
48
  addressable (~> 2.0)
49
49
  http (>= 2.0, < 5.0)
50
50
  method_source (0.9.2)
data/README.md CHANGED
@@ -20,6 +20,7 @@ Built in instrumentation for:
20
20
  - Faraday
21
21
  - Rack
22
22
  - Rails (tested on versions 4.1 and up)
23
+ - Redis (tested on versions 3.x and 4.x)
23
24
  - Sequel
24
25
  - Sinatra
25
26
 
@@ -16,6 +16,7 @@ module Honeycomb
16
16
  rails
17
17
  railtie
18
18
  rake
19
+ redis
19
20
  sequel
20
21
  sinatra
21
22
  ].freeze
@@ -3,7 +3,7 @@
3
3
  module Honeycomb
4
4
  module Beeline
5
5
  NAME = "honeycomb-beeline".freeze
6
- VERSION = "1.2.0".freeze
6
+ VERSION = "1.3.0".freeze
7
7
  USER_AGENT_SUFFIX = "#{NAME}/#{VERSION}".freeze
8
8
  end
9
9
  end
@@ -0,0 +1,354 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redis"
4
+
5
+ module Honeycomb
6
+ module Redis
7
+ # Patches Redis with the option to configure the Honeycomb client.
8
+ #
9
+ # When you load this integration, each Redis call will be wrapped in a span
10
+ # containing information about the command being invoked.
11
+ #
12
+ # This module automatically gets mixed into the Redis class so you can
13
+ # change the underlying {Honeycomb::Client}. By default, we use the global
14
+ # {Honeycomb.client} to send events. A nil client will disable the
15
+ # integration altogether.
16
+ #
17
+ # @example Custom client
18
+ # Redis.honeycomb_client = Honeycomb::Client.new(...)
19
+ #
20
+ # @example Disabling instrumentation
21
+ # Redis.honeycomb_client = nil
22
+ module Configuration
23
+ attr_writer :honeycomb_client
24
+
25
+ def honeycomb_client
26
+ @honeycomb_client || Honeycomb.client
27
+ end
28
+ end
29
+
30
+ # Patches Redis::Client with Honeycomb instrumentation.
31
+ #
32
+ # Circa versions 3.x and 4.x of their gem, the Redis class is backed by an
33
+ # underlying Redis::Client object. The methods used to send commands to the
34
+ # Redis server - namely Redis::Client#call, Redis::Client#call_loop,
35
+ # Redis::Client#call_pipeline, Redis::Client#call_pipelined,
36
+ # Redis::Client#call_with_timeout, and Redis::Client#call_without_timeout -
37
+ # all eventually wind up calling the Redis::Client#process method to do the
38
+ # "dirty work" of writing commands out to an underlying connection. So this
39
+ # gives us a single point of entry that's ideal for introducing the
40
+ # Honeycomb span.
41
+ #
42
+ # An alternative interface provided since at least version 3.0.0 is
43
+ # Redis::Distributed. Underneath, though, it maintains a collection of
44
+ # Redis objects, and each call is forwarded to one or more members of the
45
+ # collection. So patching Redis::Client still captures spans originating
46
+ # from Redis::Distributed. Typical commands (i.e., ones that aren't
47
+ # "global" like `QUIT` or `FLUSHALL`) forward to just a single node anyway,
48
+ # so there's not much use to wrapping everything up in a span for the
49
+ # Redis::Distributed method call.
50
+ #
51
+ # Another alternative interface provided since v4.0.3 is Redis::Cluster,
52
+ # which you can configure the Redis class to use instead of Redis::Client.
53
+ # Again, though, Redis::Cluster maintains a collection of Redis::Client
54
+ # instances underneath. The tracing needs wind up being pretty much the
55
+ # same as Redis::Distributed, even though the actual architecture is
56
+ # significantly different.
57
+ #
58
+ # An implementation detail of pub/sub commands since v2.0.0 (well below our
59
+ # supported version of the redis gem!) is Redis::SubscribedClient, but that
60
+ # still wraps an underlying Redis::Client or Redis::Cluster instance.
61
+ #
62
+ # @see https://github.com/redis/redis-rb/blob/2e8577ad71d0efc32f31fb034f341e1eb10abc18/lib/redis/client.rb#L77-L180
63
+ # Relevant Redis::Client methods circa v3.0.0
64
+ # @see https://github.com/redis/redis-rb/blob/a2c562c002bc8f86d1f47818d63db2da1c5c3d3f/lib/redis/client.rb#L124-L239
65
+ # Relevant Redis::Client methods circa v4.1.3
66
+ # @see https://github.com/redis/redis-rb/commits/master/lib/redis/client.rb
67
+ # History of Redis::Client
68
+ #
69
+ # @see https://redis.io/topics/partitioning
70
+ # Partitioning (the basis for Redis::Distributed)
71
+ # @see https://github.com/redis/redis-rb/blob/2e8577ad71d0efc32f31fb034f341e1eb10abc18/lib/redis/distributed.rb
72
+ # Redis::Distributed circa v3.0.0
73
+ # @see https://github.com/redis/redis-rb/blob/a2c562c002bc8f86d1f47818d63db2da1c5c3d3f/lib/redis/distributed.rb
74
+ # Redis::Distributed circa v4.1.3
75
+ # @see https://github.com/redis/redis-rb/commits/master/lib/redis/distributed.rb
76
+ # History of Redis::Distributed
77
+ #
78
+ # @see https://redis.io/topics/cluster-spec
79
+ # Clustering (the basis for Redis::Cluster)
80
+ # @see https://github.com/redis/redis-rb/commit/7f48c0b02fa89256167bc481a73ce2e0c8cca89a
81
+ # Initial implementation of Redis::Cluster released in v4.0.3
82
+ # @see https://github.com/redis/redis-rb/blob/a2c562c002bc8f86d1f47818d63db2da1c5c3d3f/lib/redis/cluster.rb
83
+ # Redis::Cluster circa v4.1.3
84
+ # @see https://github.com/redis/redis-rb/commits/master/lib/redis/cluster.rb
85
+ # History of Redis::Cluster
86
+ #
87
+ # @see https://redis.io/topics/pubsub
88
+ # Pub/Sub in Redis
89
+ # @see https://github.com/redis/redis-rb/blob/17d40d80388b536ec53a8f19bb1404e93a61650f/lib/redis/subscribe.rb
90
+ # Redis::SubscribedClient circa v2.0.0
91
+ # @see https://github.com/redis/redis-rb/blob/2e8577ad71d0efc32f31fb034f341e1eb10abc18/lib/redis/subscribe.rb
92
+ # Redis::SubscribedClient circa v3.0.0
93
+ # @see https://github.com/redis/redis-rb/blob/a2c562c002bc8f86d1f47818d63db2da1c5c3d3f/lib/redis/subscribe.rb
94
+ # Redis::SubscribedClient circa v4.1.3
95
+ module Client
96
+ def process(commands)
97
+ return super if ::Redis.honeycomb_client.nil?
98
+
99
+ span = ::Redis.honeycomb_client.start_span(name: "redis")
100
+ begin
101
+ fields = Fields.new(self)
102
+ fields.options = @options
103
+ fields.command = commands
104
+ span.add fields
105
+ super
106
+ rescue StandardError => e
107
+ span.add_field "redis.error", e.class.name
108
+ span.add_field "redis.error_detail", e.message
109
+ raise
110
+ ensure
111
+ span.send
112
+ end
113
+ end
114
+ end
115
+
116
+ # This structure contains the fields we'll add to each Redis span.
117
+ #
118
+ # The logic is in this class to avoid monkey-patching extraneous APIs into
119
+ # the Redis::Client via {Client}.
120
+ #
121
+ # @private
122
+ class Fields
123
+ def initialize(client)
124
+ @client = client
125
+ end
126
+
127
+ def options=(options)
128
+ options.each do |option, value|
129
+ values["redis.#{option}"] ||= value unless ignore?(option)
130
+ end
131
+ end
132
+
133
+ def command=(commands)
134
+ commands = Array(commands)
135
+ values["redis.command"] = commands.map { |cmd| format(cmd) }.join("\n")
136
+ end
137
+
138
+ def to_hash
139
+ values
140
+ end
141
+
142
+ private
143
+
144
+ def values
145
+ @values ||= {
146
+ "meta.package" => "redis",
147
+ "meta.package_version" => ::Redis::VERSION,
148
+ "redis.id" => @client.id,
149
+ "redis.location" => @client.location,
150
+ }
151
+ end
152
+
153
+ # Do we ignore this Redis::Client option?
154
+ #
155
+ # * :url - unsafe because it might contain a password
156
+ # * :password - unsafe
157
+ # * :logger - just some Ruby object, not useful
158
+ # * :_parsed - implementation detail
159
+ def ignore?(option)
160
+ %i[url password logger _parsed].include?(option)
161
+ end
162
+
163
+ def format(cmd)
164
+ name, *args = cmd.flatten(1)
165
+ name = resolve(name)
166
+ sanitize(args) if name.casecmp("auth").zero?
167
+ [name.upcase, *args.map { |arg| prettify(arg) }].join(" ")
168
+ end
169
+
170
+ def resolve(name)
171
+ @client.command_map.fetch(name, name).to_s
172
+ end
173
+
174
+ def sanitize(args)
175
+ args.map! { "[sanitized]" }
176
+ end
177
+
178
+ def prettify(arg)
179
+ quotes = false
180
+ pretty = "".dup
181
+ arg.to_s.each_char do |c|
182
+ quotes ||= needs_quotes?(c)
183
+ pretty << escape(c)
184
+ end
185
+ quotes ? "\"#{pretty}\"" : pretty
186
+ end
187
+
188
+ # This aims to replicate the algorithms used by redis-cli.
189
+ #
190
+ # @see https://github.com/antirez/redis/blob/0f026af185e918a9773148f6ceaa1b084662be88/src/sds.c#L940-L1067
191
+ # The redis-cli parsing algorithm
192
+ #
193
+ # @see https://github.com/antirez/redis/blob/0f026af185e918a9773148f6ceaa1b084662be88/src/sds.c#L878-L907
194
+ # The redis-cli printing algorithm
195
+ def escape(char)
196
+ return escape_with_backslash(char) if escape_with_backslash?(char)
197
+ return escape_with_hex_codes(char) if escape_with_hex_codes?(char)
198
+
199
+ char
200
+ end
201
+
202
+ # A lookup table for backslash-escaped characters.
203
+ #
204
+ # This is used by {#escape_with_backslash?} and {#escape_with_backslash}
205
+ # to replicate the hard-coded `case` statements in redis-cli. As of this
206
+ # writing, Redis recognizes a handful of standard C escape sequences,
207
+ # like "\n" for newlines.
208
+ #
209
+ # Because {#prettify} will output double quoted strings if any escaping
210
+ # is needed, this table must additionally consider the double-quote to be
211
+ # a backslash-escaped character. For example, instead of generating
212
+ #
213
+ # '"hello"'
214
+ #
215
+ # we'll generate
216
+ #
217
+ # "\"hello\""
218
+ #
219
+ # even though redis-cli would technically recognize the single-quoted
220
+ # version.
221
+ #
222
+ # @see https://github.com/antirez/redis/blob/0f026af185e918a9773148f6ceaa1b084662be88/src/sds.c#L888-L896
223
+ # The redis-cli algorithm for outputting standard escape sequences
224
+ BACKSLASHES = {
225
+ "\\" => "\\\\",
226
+ '"' => '\\"',
227
+ "\n" => "\\n",
228
+ "\r" => "\\r",
229
+ "\t" => "\\t",
230
+ "\a" => "\\a",
231
+ "\b" => "\\b",
232
+ }.freeze
233
+
234
+ def escape_with_backslash?(char)
235
+ BACKSLASHES.key?(char)
236
+ end
237
+
238
+ def escape_with_backslash(char)
239
+ BACKSLASHES.fetch(char, char)
240
+ end
241
+
242
+ # Do we need to hex-encode this character?
243
+ #
244
+ # This replicates the C isprint() function that redis-cli uses to decide
245
+ # whether to escape a character in hexadecimal notation, "\xhh". Any
246
+ # non-printable character must be represented as a hex escape sequence.
247
+ #
248
+ # Normally, we could match this using a negated POSIX bracket expression:
249
+ #
250
+ # /[^[:print:]]/
251
+ #
252
+ # You can read that as "not printable".
253
+ #
254
+ # However, in Ruby, these character classes also encompass non-ASCII
255
+ # characters. In contrast, since most platforms have 8-bit `char` types,
256
+ # the C isprint() function generally does not recognize any Unicode code
257
+ # points. This effectively limits the redis-cli interpretation of the
258
+ # printable character range to just printable ASCII characters.
259
+ #
260
+ # Thus, we match using a combination of the previous regexp with a
261
+ # non-POSIX character class that Ruby defines:
262
+ #
263
+ # /[^[:print:]&&[:ascii:]]/
264
+ #
265
+ # You can read this like
266
+ #
267
+ # NOT (printable AND ascii)
268
+ #
269
+ # which by DeMorgan's Law is equivalent to
270
+ #
271
+ # (NOT printable) OR (NOT ascii)
272
+ #
273
+ # That is, if the character is not printable (even in Unicode), we'll
274
+ # escape it; if the character is printable but non-ASCII, we'll also
275
+ # escape it.
276
+ #
277
+ # What's more, Ruby's Regexp#=~ method will blow up if the string does
278
+ # not have a valid encoding (e.g., in UTF-8). In this case, though,
279
+ # {#escape_with_hex_codes} can still convert the bytes that make up the
280
+ # invalid character into a hex code. So we preemptively check for
281
+ # invalidly-encoded characters before testing the above match.
282
+ #
283
+ # @see https://ruby-doc.org/core-2.6.5/Regexp.html
284
+ # @see https://github.com/antirez/redis/blob/0f026af185e918a9773148f6ceaa1b084662be88/src/sds.c#L878-L880
285
+ # @see https://github.com/antirez/redis/blob/0f026af185e918a9773148f6ceaa1b084662be88/src/sds.c#L898-L901
286
+ # @see https://www.justinweiss.com/articles/3-steps-to-fix-encoding-problems-in-ruby/
287
+ def escape_with_hex_codes?(char)
288
+ !char.valid_encoding? || char =~ /[^[:print:]&&[:ascii:]]/
289
+ end
290
+
291
+ # Hex-encodes a (presumably non-printable or non-ASCII) character.
292
+ #
293
+ # Aside from standard backslash escape sequences, redis-cli also
294
+ # recognizes "\xhh" notation, where `hh` is a hexadecimal number.
295
+ #
296
+ # Of note is that redis-cli only recognizes *exactly* two-digit
297
+ # hexadecimal numbers. This is in accordance with IEEE Std 1003.1-2001,
298
+ # Chapter 7, Locale:
299
+ #
300
+ # > A character can be represented as a hexadecimal constant. A
301
+ # > hexadecimal constant shall be specified as the escape character
302
+ # > followed by an 'x' followed by two hexadecimal digits. Each constant
303
+ # > shall represent a byte value. Multi-byte values can be represented by
304
+ # > concatenated constants specified in byte order with the last constant
305
+ # > specifying the least significant byte of the character.
306
+ #
307
+ # Unlike the C `char` type, Ruby's conception of a character can span
308
+ # multiple bytes (and possibly bytes that aren't valid in Ruby's string
309
+ # encoding). So we take care to escape the input properly into the
310
+ # redis-cli compatible version by iterating through each byte and
311
+ # formatting it as a (zero-padded) 2-digit hexadecimal number prefixed by
312
+ # `\x`.
313
+ #
314
+ # @see https://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap07.html
315
+ # @see https://github.com/antirez/redis/blob/0f026af185e918a9773148f6ceaa1b084662be88/src/sds.c#L878-L880
316
+ # @see https://github.com/antirez/redis/blob/0f026af185e918a9773148f6ceaa1b084662be88/src/sds.c#L898-L901
317
+ def escape_with_hex_codes(char)
318
+ char.bytes.map { |b| Kernel.format("\\x%02x", b) }.join
319
+ end
320
+
321
+ def escape?(char)
322
+ escape_with_backslash?(char) || escape_with_hex_codes?(char)
323
+ end
324
+
325
+ # Should this character cause {#prettify} to wrap its output in quotes?
326
+ #
327
+ # The overall string returned by {#prettify} should only be quoted if at
328
+ # least one of the following holds:
329
+ #
330
+ # 1. The string contains a character that needs to be escaped. This
331
+ # includes standard backslash escape sequences (like "\n" and "\t") as
332
+ # well as hex-encoded bytes using the "\x" escape sequence. Since
333
+ # {#prettify} uses double quotes on its output string, we must also
334
+ # force quotes if the string itself contains a literal double quote.
335
+ # This double quote behavior is handled tacitly by {BACKSLASHES}.
336
+ #
337
+ # 2. The string contains a single quote. Since redis-cli recognizes
338
+ # single-quoted strings, we want to wrap the {#prettify} output in
339
+ # double quotes so that the literal single quote character isn't
340
+ # mistaken as the delimiter of a new string.
341
+ #
342
+ # 3. The string contains any whitespace characters. If the {#prettify}
343
+ # output weren't wrapped in quotes, whitespace would act as a
344
+ # separator between arguments to the Redis command. To group things
345
+ # together, we need to quote the string.
346
+ def needs_quotes?(char)
347
+ escape?(char) || char == "'" || char =~ /\s/
348
+ end
349
+ end
350
+ end
351
+ end
352
+
353
+ Redis.extend(Honeycomb::Redis::Configuration)
354
+ Redis::Client.prepend(Honeycomb::Redis::Client)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: honeycomb-beeline
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Holman
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-11-04 00:00:00.000000000 Z
11
+ date: 2019-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: libhoney
@@ -233,6 +233,7 @@ files:
233
233
  - lib/honeycomb/integrations/rails.rb
234
234
  - lib/honeycomb/integrations/railtie.rb
235
235
  - lib/honeycomb/integrations/rake.rb
236
+ - lib/honeycomb/integrations/redis.rb
236
237
  - lib/honeycomb/integrations/sequel.rb
237
238
  - lib/honeycomb/integrations/sinatra.rb
238
239
  - lib/honeycomb/integrations/warden.rb