module TraceView::Inst::Redis::Client

Constants

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

NO_KEY_OPS

The operations listed in this constant skip collecting KVKey

Public Class Methods

included(klass) click to toggle source

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)

# 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

Public Instance Methods

call_pipeline_with_traceview(pipeline) click to toggle source

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

# 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) click to toggle source

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

# 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) click to toggle source

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

@param pipeline [Redis::Pipeline] the Redis pipeline instance @return [Hash] the Key/Values to report

# 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) click to toggle source

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

@param command [Array] the Redis operation array @param r [Return] the return value from the operation @return [Hash] the Key/Values to report

# 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