module Oboe::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/oboe/inst/redis.rb, line 59
def self.included(klass)
  # We wrap two of the Redis methods to instrument
  # operations
  ::Oboe::Util.method_alias(klass, :call, ::Redis::Client)
  ::Oboe::Util.method_alias(klass, :call_pipeline, ::Redis::Client)
end

Public Instance Methods

call_pipeline_with_oboe(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/oboe/inst/redis.rb, line 237
def call_pipeline_with_oboe(pipeline)
  if Oboe.tracing?
    # Fall back to the raw tracing API so we can pass KVs
    # back on exit (a limitation of the Oboe::API.trace
    # block method)  This removes the need for an info
    # event to send additonal KVs
    ::Oboe::API.log_entry('redis', {})

    report_kvs = extract_pipeline_details(pipeline)

    begin
      call_pipeline_without_oboe(pipeline)
    rescue StandardError => e
      ::Oboe::API.log_exception('redis', e)
      raise
    ensure
      ::Oboe::API.log_exit('redis', report_kvs)
    end
  else
    call_pipeline_without_oboe(pipeline)
  end
end
call_with_oboe(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/oboe/inst/redis.rb, line 213
def call_with_oboe(command, &block)
  if Oboe.tracing?
    ::Oboe::API.log_entry('redis', {})

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

  else
    call_without_oboe(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/oboe/inst/redis.rb, line 176
def extract_pipeline_details(pipeline)
  kvs = {}

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

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

    if pipeline.commands.first == :multi
      kvs[:KVOp] = :multi
    else
      kvs[:KVOp] = :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
    Oboe.logger.debug "[oboe/debug] Error extracting pipelined commands: #{e.message}"
    Oboe.logger.debug e.backtrace
  end
  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/oboe/inst/redis.rb, line 73
def extract_trace_details(command, r)
  kvs = {}
  op = command.first

  begin
    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
          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)
        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] = Oboe::API.backtrace if Oboe::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
    Oboe.logger.debug "Error collecting redis KVs: #{e.message}"
    Oboe.logger.debug e.backtrace.join('\n')
  end

  kvs
end