Module: TraceView::Inst::Redis::Client

Defined in:
lib/traceview/inst/redis.rb

Constant Summary collapse

NO_KEY_OPS =

The operations listed in this constant skip collecting KVKey

[:keys, :randomkey, :scan, :sdiff, :sdiffstore, :sinter,
:sinterstore, :smove, :sunion, :sunionstore, :zinterstore,
:zunionstore, :publish, :select, :eval, :evalsha, :script].freeze
KV_COLLECT_MAP =

Instead of a giant switch statement, we use a hash constant to map out what KVs need to be collected for each of the many many Redis operations Hash formatting by undiagnosed OCD

{
  :brpoplpush  => { :destination  => 2 }, :rpoplpush   => { :destination  => 2 },
  :sdiffstore  => { :destination  => 1 }, :sinterstore => { :destination  => 1 },
  :sunionstore => { :destination  => 1 }, :zinterstore => { :destination  => 1 },
  :zunionstore => { :destination  => 1 }, :publish     => { :channel      => 1 },
  :incrby      => { :increment    => 2 }, :incrbyfloat => { :increment    => 2 },
  :pexpire     => { :milliseconds => 2 }, :pexpireat   => { :milliseconds => 2 },
  :expireat    => { :timestamp    => 2 }, :decrby      => { :decrement    => 2 },
  :psetex      => { :ttl     => 2 },      :restore  => { :ttl     => 2 },
  :setex       => { :ttl     => 2 },      :setnx    => { :ttl     => 2 },
  :move        => { :db      => 2 },      :select   => { :db      => 1 },
  :lindex      => { :index   => 2 },      :getset   => { :value   => 2 },
  :keys        => { :pattern => 1 },      :expire   => { :seconds => 2 },
  :rename      => { :newkey  => 2 },      :renamenx => { :newkey  => 2 },
  :getbit      => { :offset  => 2 },      :setbit   => { :offset  => 2 },
  :setrange    => { :offset  => 2 },      :evalsha  => { :sha     => 1 },
  :getrange    => { :start => 2, :end       => 3 },
  :zrange      => { :start => 2, :end       => 3 },
  :bitcount    => { :start => 2, :stop      => 3 },
  :lrange      => { :start => 2, :stop      => 3 },
  :zrevrange   => { :start => 2, :stop      => 3 },
  :hincrby     => { :field => 2, :increment => 3 },
  :smove           => { :source    => 1, :destination => 2 },
  :bitop           => { :operation => 1, :destkey     => 2 },
  :hincrbyfloat    => { :field     => 2, :increment   => 3 },
  :zremrangebyrank => { :start     => 2, :stop        => 3 }
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(klass) ⇒ Object

The following operations don’t require any special handling. For these, we only collect KVKey and KVOp

:append, :blpop, :brpop, :decr, :del, :dump, :exists, :hgetall, :hkeys, :hlen, :hvals, :hmset, :incr, :linsert, :llen, :lpop, :lpush, :lpushx, :lrem, :lset, :ltrim, :persist, :pttl, :hscan, :rpop, :rpush, :rpushx, :sadd, :scard, :sismember, :smembers, :strlen, :sort, :spop, :srandmember, :srem, :sscan, :ttl, :type, :zadd, :zcard, :zcount, :zincrby, :zrangebyscore, :zrank, :zrem, :zremrangebyscore, :zrevrank, :zrevrangebyscore, :zscore

For the operations in NO_KEY_OPS (above) we only collect KVOp (no KVKey)



59
60
61
62
63
64
# File 'lib/traceview/inst/redis.rb', line 59

def self.included(klass)
  # We wrap two of the Redis methods to instrument
  # operations
  ::TraceView::Util.method_alias(klass, :call, ::Redis::Client)
  ::TraceView::Util.method_alias(klass, :call_pipeline, ::Redis::Client)
end

Instance Method Details

#call_pipeline_with_traceview(pipeline) ⇒ Object

The wrapper method for Redis::Client.call_pipeline. Here (when tracing) we capture KVs to report and pass the call along



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/traceview/inst/redis.rb', line 243

def call_pipeline_with_traceview(pipeline)
  if TraceView.tracing?
    # Fall back to the raw tracing API so we can pass KVs
    # back on exit (a limitation of the TraceView::API.trace
    # block method)  This removes the need for an info
    # event to send additonal KVs
    ::TraceView::API.log_entry(:redis, {})

    report_kvs = extract_pipeline_details(pipeline)

    begin
      call_pipeline_without_traceview(pipeline)
    rescue StandardError => e
      ::TraceView::API.log_exception(:redis, e)
      raise
    ensure
      ::TraceView::API.log_exit(:redis, report_kvs)
    end
  else
    call_pipeline_without_traceview(pipeline)
  end
end

#call_with_traceview(command, &block) ⇒ Object

The wrapper method for Redis::Client.call. Here (when tracing) we capture KVs to report and pass the call along



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/traceview/inst/redis.rb', line 219

def call_with_traceview(command, &block)
  if TraceView.tracing?
    ::TraceView::API.log_entry(:redis, {})

    begin
      r = call_without_traceview(command, &block)
      report_kvs = extract_trace_details(command, r)
      r
    rescue StandardError => e
      ::TraceView::API.log_exception(:redis, e)
      raise
    ensure
      ::TraceView::API.log_exit(:redis, report_kvs)
    end

  else
    call_without_traceview(command, &block)
  end
end

#extract_pipeline_details(pipeline) ⇒ Hash

Extracts the Key/Values to report from a pipelined call to the TraceView dashboard.

Parameters:

  • pipeline (Redis::Pipeline)

    the Redis pipeline instance

Returns:

  • (Hash)

    the Key/Values to report



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/traceview/inst/redis.rb', line 183

def extract_pipeline_details(pipeline)
  kvs = {}

  kvs[:RemoteHost] = @options[:host]
  kvs[:Backtrace] = TraceView::API.backtrace if TraceView::Config[:redis][:collect_backtraces]

  command_count = pipeline.commands.count
  kvs[:KVOpCount] = command_count

  kvs[:KVOp] = if pipeline.commands.first == :multi
    :multi
  else
    :pipeline
  end

  # Report pipelined operations  if the number
  # of ops is reasonable
  if command_count < 12
    ops = []
    pipeline.commands.each do |c|
      ops << c.first
    end
    kvs[:KVOps] = ops.join(', ')
  end
rescue StandardError => e
  TraceView.logger.debug "[traceview/debug] Error extracting pipelined commands: #{e.message}"
  TraceView.logger.debug e.backtrace
ensure
  return kvs
end

#extract_trace_details(command, r) ⇒ Hash

Given any Redis operation command array, this method extracts the Key/Values to report to the TraceView dashboard.

Parameters:

  • command (Array)

    the Redis operation array

  • r (Return)

    the return value from the operation

Returns:

  • (Hash)

    the Key/Values to report



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/traceview/inst/redis.rb', line 73

def extract_trace_details(command, r)
  kvs = {}
  op = command.first

  kvs[:KVOp] = command[0]
  kvs[:RemoteHost] = @options[:host]

  unless NO_KEY_OPS.include?(op) || (command[1].is_a?(Array) && command[1].count > 1)
    if command[1].is_a?(Array)
      kvs[:KVKey] = command[1].first
    else
      kvs[:KVKey] = command[1]
    end
  end

  if KV_COLLECT_MAP[op]
    # Extract KVs from command for this op
    KV_COLLECT_MAP[op].each { |k, v| kvs[k] = command[v] }
  else
    # This case statement handle special cases not handled
    # by KV_COLLECT_MAP
    case op
    when :set
      if command.count > 3
        if command[3].is_a?(Hash)
          options = command[3]
          kvs[:ex] = options[:ex] if options.key?(:ex)
          kvs[:px] = options[:px] if options.key?(:px)
          kvs[:nx] = options[:nx] if options.key?(:nx)
          kvs[:xx] = options[:xx] if options.key?(:xx)
        else
          options = command[3..-1]
          until (opts = options.shift(2)).empty?
            case opts[0]
            when 'EX' then; kvs[:ex] = opts[1]
            when 'PX' then; kvs[:px] = opts[1]
            when 'NX' then; kvs[:nx] = opts[1]
            when 'XX' then; kvs[:xx] = opts[1]
            end
          end
        end
      end

    when :get
      kvs[:KVHit] = r.nil? ? 0 : 1

    when :hdel, :hexists, :hget, :hset, :hsetnx
      kvs[:field] = command[2] unless command[2].is_a?(Array)
      if op == :hget
        kvs[:KVHit] = r.nil? ? 0 : 1
      end

    when :eval
      if command[1].length > 1024
        kvs[:Script] = command[1][0..1023] + '(...snip...)'
      else
        kvs[:Script] = command[1]
      end

    when :script
      kvs[:subcommand] = command[1]
      kvs[:Backtrace] = TraceView::API.backtrace if TraceView::Config[:redis][:collect_backtraces]
      if command[1] == 'load'
        if command[1].length > 1024
          kvs[:Script] = command[2][0..1023] + '(...snip...)'
        else
          kvs[:Script] = command[2]
        end
      elsif command[1] == :exists
        if command[2].is_a?(Array)
          kvs[:KVKey] = command[2].inspect
        else
          kvs[:KVKey] = command[2]
        end
      end

    when :mget
      if command[1].is_a?(Array)
        kvs[:KVKeyCount] = command[1].count
      else
        kvs[:KVKeyCount] = command.count - 1
      end
      values = r.select { |i| i }
      kvs[:KVHitCount] = values.count

    when :hmget
      kvs[:KVKeyCount] = command.count - 2
      values = r.select { |i| i }
      kvs[:KVHitCount] = values.count

    when :mset, :msetnx
      if command[1].is_a?(Array)
        kvs[:KVKeyCount] = command[1].count / 2
      else
        kvs[:KVKeyCount] = (command.count - 1) / 2
      end
    end # case op
  end # if KV_COLLECT_MAP[op]
rescue StandardError => e
  TraceView.logger.debug "Error collecting redis KVs: #{e.message}"
  TraceView.logger.debug e.backtrace.join('\n')
ensure
  return kvs
end