github.com/epsagon/epsagon-go@v1.39.0/wrappers/redis/redis.go (about)

     1  package epsagonredis
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net"
     8  	"runtime/debug"
     9  
    10  	"github.com/epsagon/epsagon-go/epsagon"
    11  	"github.com/epsagon/epsagon-go/protocol"
    12  	"github.com/epsagon/epsagon-go/tracer"
    13  	"github.com/go-redis/redis/v8"
    14  	"github.com/google/uuid"
    15  )
    16  
    17  type epsagonHook struct {
    18  	host    string
    19  	port    string
    20  	dbIndex string
    21  	event   *protocol.Event
    22  	tracer  tracer.Tracer
    23  }
    24  
    25  func NewClient(opt *redis.Options, epsagonCtx context.Context) *redis.Client {
    26  	client := redis.NewClient(opt)
    27  	return wrapClient(client, opt, epsagonCtx)
    28  }
    29  
    30  func wrapClient(client *redis.Client, opt *redis.Options, epsagonCtx context.Context) (wrappedClient *redis.Client) {
    31  	wrappedClient = client
    32  	defer func() { recover() }()
    33  
    34  	currentTracer := epsagon.ExtractTracer([]context.Context{epsagonCtx})
    35  	if currentTracer != nil {
    36  		host, port := getClientHostPort(opt)
    37  		client.AddHook(&epsagonHook{
    38  			host:    host,
    39  			port:    port,
    40  			dbIndex: fmt.Sprint(opt.DB),
    41  			tracer:  currentTracer,
    42  		})
    43  	}
    44  	return wrappedClient
    45  }
    46  
    47  func getClientHostPort(opt *redis.Options) (host, port string) {
    48  	if opt.Network == "unix" {
    49  		return "localhost", opt.Addr
    50  	}
    51  	host, port, err := net.SplitHostPort(opt.Addr)
    52  	if err == nil {
    53  		return host, port
    54  	}
    55  	return opt.Addr, ""
    56  }
    57  
    58  func (epsHook *epsagonHook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (processCtx context.Context, err error) {
    59  	processCtx, err = ctx, nil
    60  	defer func() { recover() }()
    61  
    62  	cmdArgs := safeJsonify(cmd.Args())
    63  	epsHook.before(cmd.Name(), cmdArgs)
    64  	return
    65  }
    66  
    67  func (epsHook *epsagonHook) AfterProcess(ctx context.Context, cmd redis.Cmder) (err error) {
    68  	defer func() { recover() }()
    69  
    70  	var errMsg string
    71  	if err := cmd.Err(); err != nil {
    72  		errMsg = err.Error()
    73  	}
    74  	epsHook.after(cmd.String(), errMsg)
    75  	return
    76  }
    77  
    78  func (epsHook *epsagonHook) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (processCtx context.Context, err error) {
    79  	processCtx, err = ctx, nil
    80  	defer func() { recover() }()
    81  
    82  	cmdArgs := safeJsonify(getPiplineCmdArgs(cmds))
    83  	epsHook.before("Pipeline", cmdArgs)
    84  	return
    85  }
    86  
    87  func (epsHook *epsagonHook) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) (err error) {
    88  	defer func() { recover() }()
    89  
    90  	var errMsg string
    91  	if errors := getPipelineErrors(cmds); len(errors) > 0 {
    92  		errMsg = safeJsonify(errors)
    93  
    94  	}
    95  	response := safeJsonify(getPipelineResponse(cmds))
    96  	epsHook.after(response, errMsg)
    97  	return
    98  }
    99  
   100  func (epsHook *epsagonHook) before(operation, cmdArgs string) {
   101  	metadata := getResourceMetadata(epsHook, cmdArgs)
   102  	epsHook.event = createEvent(epsHook, operation, metadata)
   103  }
   104  
   105  func (epsHook *epsagonHook) after(response, errMsg string) {
   106  	event := epsHook.event
   107  	if event == nil {
   108  		return
   109  	}
   110  	if !epsHook.tracer.GetConfig().MetadataOnly {
   111  		event.Resource.Metadata["redis.response"] = response
   112  	}
   113  
   114  	eventEndTime := tracer.GetTimestamp()
   115  	event.Duration = eventEndTime - event.StartTime
   116  
   117  	if errMsg != "" {
   118  		event.ErrorCode = protocol.ErrorCode_EXCEPTION
   119  		event.Exception = &protocol.Exception{
   120  			Message:   errMsg,
   121  			Traceback: string(debug.Stack()),
   122  			Time:      eventEndTime,
   123  		}
   124  	}
   125  
   126  	epsHook.tracer.AddEvent(event)
   127  }
   128  
   129  func createEvent(epsHook *epsagonHook, operation string, metadata map[string]string) *protocol.Event {
   130  	return &protocol.Event{
   131  		Id:        "redis-" + uuid.New().String(),
   132  		StartTime: tracer.GetTimestamp(),
   133  		Resource: &protocol.Resource{
   134  			Name:      epsHook.host,
   135  			Type:      "redis",
   136  			Operation: operation,
   137  			Metadata:  metadata,
   138  		},
   139  	}
   140  }
   141  
   142  func getResourceMetadata(epsHook *epsagonHook, cmdArgs string) map[string]string {
   143  	metadata := getConnectionMetadata(epsHook)
   144  	if !epsHook.tracer.GetConfig().MetadataOnly {
   145  		metadata["Command Arguments"] = cmdArgs
   146  	}
   147  	return metadata
   148  }
   149  
   150  func getConnectionMetadata(epsHook *epsagonHook) map[string]string {
   151  	return map[string]string{
   152  		"Redis Host":     epsHook.host,
   153  		"Redis Port":     epsHook.port,
   154  		"Redis DB Index": epsHook.dbIndex,
   155  	}
   156  }
   157  
   158  func getPiplineCmdArgs(cmds []redis.Cmder) []interface{} {
   159  	var cmdArgs []interface{}
   160  	for _, cmd := range cmds {
   161  		cmdArgs = append(cmdArgs, cmd.Args())
   162  	}
   163  	return cmdArgs
   164  }
   165  
   166  func getPipelineResponse(cmds []redis.Cmder) []string {
   167  	var responses []string
   168  	for _, cmd := range cmds {
   169  		responses = append(responses, cmd.String())
   170  	}
   171  	return responses
   172  }
   173  
   174  func getPipelineErrors(cmds []redis.Cmder) []string {
   175  	var errors []string
   176  	for _, cmd := range cmds {
   177  		if err := cmd.Err(); err != nil {
   178  			errors = append(errors, err.Error())
   179  		}
   180  	}
   181  	return errors
   182  }
   183  
   184  func safeJsonify(v interface{}) string {
   185  	encodedValue, err := json.Marshal(v)
   186  	if err == nil {
   187  		return string(encodedValue)
   188  	}
   189  	return ""
   190  }