diff --git a/README.md b/README.md index 28c1f0b2..da5d0fb4 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ ## Ecosystem -- [redisext](https://github.com/go-redis/redisext) - tracing using OpenTelemetry and OpenCensus. - [Distributed Locks](https://github.com/bsm/redislock). - [Redis Cache](https://github.com/go-redis/cache). - [Rate limiting](https://github.com/go-redis/redis_rate). diff --git a/extra/rediscensus/go.mod b/extra/rediscensus/go.mod new file mode 100644 index 00000000..0bd8adf2 --- /dev/null +++ b/extra/rediscensus/go.mod @@ -0,0 +1,11 @@ +module github.com/go-redis/redis/extra/rediscensus + +go 1.15 + +replace github.com/go-redis/redis/extra/rediscmd => ../rediscmd + +require ( + github.com/go-redis/redis/extra/rediscmd v0.0.0-00010101000000-000000000000 + github.com/go-redis/redis/v8 v8.3.2 + go.opencensus.io v0.22.5 +) diff --git a/extra/rediscensus/rediscensus.go b/extra/rediscensus/rediscensus.go new file mode 100644 index 00000000..48a7ca60 --- /dev/null +++ b/extra/rediscensus/rediscensus.go @@ -0,0 +1,45 @@ +package rediscensus + +import ( + "context" + + "github.com/go-redis/redis/extra/rediscmd" + "github.com/go-redis/redis/v8" + "go.opencensus.io/trace" +) + +type TracingHook struct{} + +var _ redis.Hook = TracingHook{} + +func (TracingHook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) { + ctx, span := trace.StartSpan(ctx, cmd.FullName()) + span.AddAttributes(trace.StringAttribute("db.system", "redis"), + trace.StringAttribute("redis.cmd", rediscmd.CmdString(cmd))) + + return ctx, nil +} + +func (TracingHook) AfterProcess(ctx context.Context, cmd redis.Cmder) error { + span := trace.FromContext(ctx) + if err := cmd.Err(); err != nil { + recordErrorOnOCSpan(ctx, span, err) + } + span.End() + return nil +} + +func (TracingHook) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) { + return ctx, nil +} + +func (TracingHook) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error { + return nil +} + +func recordErrorOnOCSpan(ctx context.Context, span *trace.Span, err error) { + if err != redis.Nil { + span.AddAttributes(trace.BoolAttribute("error", true)) + span.Annotate([]trace.Attribute{trace.StringAttribute("Error", "redis error")}, err.Error()) + } +} diff --git a/extra/rediscmd/go.mod b/extra/rediscmd/go.mod new file mode 100644 index 00000000..2d9f1ac5 --- /dev/null +++ b/extra/rediscmd/go.mod @@ -0,0 +1,9 @@ +module github.com/go-redis/redis/extra/rediscmd + +go 1.15 + +require ( + github.com/go-redis/redis/v8 v8.3.2 + github.com/onsi/ginkgo v1.14.2 + github.com/onsi/gomega v1.10.3 +) diff --git a/extra/rediscmd/rediscmd.go b/extra/rediscmd/rediscmd.go new file mode 100644 index 00000000..12ea39fc --- /dev/null +++ b/extra/rediscmd/rediscmd.go @@ -0,0 +1,149 @@ +package rediscmd + +import ( + "encoding/hex" + "fmt" + "strconv" + "strings" + "time" + + "github.com/go-redis/redis/v8" +) + +func CmdString(cmd redis.Cmder) string { + b := make([]byte, 0, 32) + b = AppendCmd(b, cmd) + return String(b) +} + +func CmdsString(cmds []redis.Cmder) (string, string) { + const numCmdLimit = 100 + const numNameLimit = 10 + + seen := make(map[string]struct{}, numNameLimit) + unqNames := make([]string, 0, numNameLimit) + + b := make([]byte, 0, 32*len(cmds)) + + for i, cmd := range cmds { + if i > numCmdLimit { + break + } + + if i > 0 { + b = append(b, '\n') + } + b = AppendCmd(b, cmd) + + if len(unqNames) >= numNameLimit { + continue + } + + name := cmd.FullName() + if _, ok := seen[name]; !ok { + seen[name] = struct{}{} + unqNames = append(unqNames, name) + } + } + + summary := strings.Join(unqNames, " ") + return summary, String(b) +} + +func AppendCmd(b []byte, cmd redis.Cmder) []byte { + const numArgLimit = 32 + + for i, arg := range cmd.Args() { + if i > numArgLimit { + break + } + if i > 0 { + b = append(b, ' ') + } + b = appendArg(b, arg) + } + + if err := cmd.Err(); err != nil { + b = append(b, ": "...) + b = append(b, err.Error()...) + } + + return b +} + +func appendArg(b []byte, v interface{}) []byte { + const argLenLimit = 64 + + switch v := v.(type) { + case nil: + return append(b, ""...) + case string: + if len(v) > argLenLimit { + v = v[:argLenLimit] + } + return appendUTF8String(b, Bytes(v)) + case []byte: + if len(v) > argLenLimit { + v = v[:argLenLimit] + } + return appendUTF8String(b, v) + case int: + return strconv.AppendInt(b, int64(v), 10) + case int8: + return strconv.AppendInt(b, int64(v), 10) + case int16: + return strconv.AppendInt(b, int64(v), 10) + case int32: + return strconv.AppendInt(b, int64(v), 10) + case int64: + return strconv.AppendInt(b, v, 10) + case uint: + return strconv.AppendUint(b, uint64(v), 10) + case uint8: + return strconv.AppendUint(b, uint64(v), 10) + case uint16: + return strconv.AppendUint(b, uint64(v), 10) + case uint32: + return strconv.AppendUint(b, uint64(v), 10) + case uint64: + return strconv.AppendUint(b, v, 10) + case float32: + return strconv.AppendFloat(b, float64(v), 'f', -1, 64) + case float64: + return strconv.AppendFloat(b, v, 'f', -1, 64) + case bool: + if v { + return append(b, "true"...) + } + return append(b, "false"...) + case time.Time: + return v.AppendFormat(b, time.RFC3339Nano) + default: + return append(b, fmt.Sprint(v)...) + } +} + +func appendUTF8String(dst []byte, src []byte) []byte { + if isSimple(src) { + dst = append(dst, src...) + return dst + } + + s := len(dst) + dst = append(dst, make([]byte, hex.EncodedLen(len(src)))...) + hex.Encode(dst[s:], src) + return dst +} + +func isSimple(b []byte) bool { + for _, c := range b { + if !isSimpleByte(c) { + return false + } + } + return true +} + +func isSimpleByte(c byte) bool { + return c >= 0x21 && c <= 0x7e +} diff --git a/extra/rediscmd/rediscmd_test.go b/extra/rediscmd/rediscmd_test.go new file mode 100644 index 00000000..259a66ac --- /dev/null +++ b/extra/rediscmd/rediscmd_test.go @@ -0,0 +1,32 @@ +package rediscmd + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +func TestGinkgo(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "redisext") +} + +var _ = Describe("AppendArg", func() { + DescribeTable("...", + func(src string, wanted string) { + b := appendArg(nil, src) + Expect(string(b)).To(Equal(wanted)) + }, + + Entry("", "-inf", "-inf"), + Entry("", "+inf", "+inf"), + Entry("", "foo.bar", "foo.bar"), + Entry("", "foo:bar", "foo:bar"), + Entry("", "foo{bar}", "foo{bar}"), + Entry("", "foo-123_BAR", "foo-123_BAR"), + Entry("", "foo\nbar", "666f6f0a626172"), + Entry("", "\000", "00"), + ) +}) diff --git a/extra/rediscmd/safe.go b/extra/rediscmd/safe.go new file mode 100644 index 00000000..efe92f6e --- /dev/null +++ b/extra/rediscmd/safe.go @@ -0,0 +1,11 @@ +// +build appengine + +package rediscmd + +func String(b []byte) string { + return string(b) +} + +func Bytes(s string) []byte { + return []byte(s) +} diff --git a/extra/rediscmd/unsafe.go b/extra/rediscmd/unsafe.go new file mode 100644 index 00000000..a90a48b7 --- /dev/null +++ b/extra/rediscmd/unsafe.go @@ -0,0 +1,20 @@ +// +build !appengine + +package rediscmd + +import "unsafe" + +// String converts byte slice to string. +func String(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} + +// Bytes converts string to byte slice. +func Bytes(s string) []byte { + return *(*[]byte)(unsafe.Pointer( + &struct { + string + Cap int + }{s, len(s)}, + )) +} diff --git a/extra/redisotel/go.mod b/extra/redisotel/go.mod new file mode 100644 index 00000000..e01080c8 --- /dev/null +++ b/extra/redisotel/go.mod @@ -0,0 +1,11 @@ +module github.com/go-redis/redis/extra/rediscensus + +go 1.15 + +replace github.com/go-redis/redis/extra/rediscmd => ../rediscmd + +require ( + github.com/go-redis/redis/extra/rediscmd v0.0.0-00010101000000-000000000000 + github.com/go-redis/redis/v8 v8.3.2 + go.opentelemetry.io/otel v0.13.0 +) diff --git a/extra/redisotel/redisotel.go b/extra/redisotel/redisotel.go new file mode 100644 index 00000000..d0ab602b --- /dev/null +++ b/extra/redisotel/redisotel.go @@ -0,0 +1,72 @@ +package redisotel + +import ( + "context" + + "github.com/go-redis/redis/extra/rediscmd" + "github.com/go-redis/redis/v8" + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/trace" + "go.opentelemetry.io/otel/label" +) + +var tracer = global.Tracer("github.com/go-redis/redis") + +type TracingHook struct{} + +var _ redis.Hook = TracingHook{} + +func (TracingHook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) { + if !trace.SpanFromContext(ctx).IsRecording() { + return ctx, nil + } + + ctx, span := tracer.Start(ctx, cmd.FullName()) + span.SetAttributes( + label.String("db.system", "redis"), + label.String("redis.cmd", rediscmd.CmdString(cmd)), + ) + + return ctx, nil +} + +func (TracingHook) AfterProcess(ctx context.Context, cmd redis.Cmder) error { + span := trace.SpanFromContext(ctx) + if err := cmd.Err(); err != nil { + recordError(ctx, span, err) + } + span.End() + return nil +} + +func (TracingHook) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) { + if !trace.SpanFromContext(ctx).IsRecording() { + return ctx, nil + } + + summary, cmdsString := rediscmd.CmdsString(cmds) + + ctx, span := tracer.Start(ctx, "pipeline "+summary) + span.SetAttributes( + label.String("db.system", "redis"), + label.Int("redis.num_cmd", len(cmds)), + label.String("redis.cmds", cmdsString), + ) + + return ctx, nil +} + +func (TracingHook) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error { + span := trace.SpanFromContext(ctx) + if err := cmds[0].Err(); err != nil { + recordError(ctx, span, err) + } + span.End() + return nil +} + +func recordError(ctx context.Context, span trace.Span, err error) { + if err != redis.Nil { + span.RecordError(ctx, err) + } +} diff --git a/go.sum b/go.sum index eb9e06f9..f76f073d 100644 --- a/go.sum +++ b/go.sum @@ -25,13 +25,11 @@ github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= -github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= -github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -46,6 +44,7 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -57,10 +56,12 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=