honeycomb-beeline 1.2.0 → 1.3.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: 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