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 }