Skip to content

Commit

Permalink
Merge pull request #251 from ilmimris/v3/integrations/nrredis-v8
Browse files Browse the repository at this point in the history
Added support for `v8` of go-redis/redis
  • Loading branch information
RichVanderwal committed Mar 17, 2021
2 parents df4d9c2 + a4b63d0 commit 5739767
Show file tree
Hide file tree
Showing 6 changed files with 393 additions and 0 deletions.
10 changes: 10 additions & 0 deletions v3/integrations/nrredis-v8/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# v3/integrations/nrredis-v8 [![GoDoc](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrredis-v8?status.svg)](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrredis-v8)

Package `nrredis` instruments `"github.com/go-redis/redis/v8"`.

```go
import nrredis "github.com/newrelic/go-agent/v3/integrations/nrredis-v8"
```

For more information, see
[godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrredis-v8).
52 changes: 52 additions & 0 deletions v3/integrations/nrredis-v8/example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2020 New Relic Corporation. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package main

import (
"context"
"fmt"
"os"
"time"

redis "github.com/go-redis/redis/v8"
nrredis "github.com/newrelic/go-agent/v3/integrations/nrredis-v8"
newrelic "github.com/newrelic/go-agent/v3/newrelic"
)

func main() {
app, err := newrelic.NewApplication(
newrelic.ConfigAppName("Redis App"),
newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")),
newrelic.ConfigDebugLogger(os.Stdout),
)
if nil != err {
panic(err)
}
app.WaitForConnection(10 * time.Second)
txn := app.StartTransaction("ping txn")

opts := &redis.Options{
Addr: "localhost:6379",
}
client := redis.NewClient(opts)

//
// Step 1: Add a nrredis.NewHook() to your redis client.
//
client.AddHook(nrredis.NewHook(opts))

//
// Step 2: Ensure that all client calls contain a context which includes
// the transaction.
//
ctx := newrelic.NewContext(context.Background(), txn)
pipe := client.WithContext(ctx).Pipeline()
incr := pipe.Incr(ctx, "pipeline_counter")
pipe.Expire(ctx, "pipeline_counter", time.Hour)
_, err = pipe.Exec(ctx)
fmt.Println(incr.Val(), err)

txn.End()
app.Shutdown(5 * time.Second)
}
10 changes: 10 additions & 0 deletions v3/integrations/nrredis-v8/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/newrelic/go-agent/v3/integrations/nrredis-v8

// As of Jan 2020, go 1.11 is in the go-redis go.mod file:
// https://github.com/go-redis/redis/blob/master/go.mod
go 1.11

require (
github.com/go-redis/redis/v8 v8.4.0
github.com/newrelic/go-agent/v3 v3.0.0
)
99 changes: 99 additions & 0 deletions v3/integrations/nrredis-v8/nrredis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2020 New Relic Corporation. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

// Package nrredis instruments github.com/go-redis/redis/v8.
//
// Use this package to instrument your go-redis/redis/v8 calls without having to
// manually create DatastoreSegments.
package nrredis

import (
"context"
"net"
"strings"

redis "github.com/go-redis/redis/v8"
"github.com/newrelic/go-agent/v3/internal"
newrelic "github.com/newrelic/go-agent/v3/newrelic"
)

func init() { internal.TrackUsage("integration", "datastore", "redis") }

type contextKeyType struct{}

type hook struct {
segment newrelic.DatastoreSegment
}

var (
segmentContextKey = contextKeyType(struct{}{})
)

// NewHook creates a redis.Hook to instrument Redis calls. Add it to your
// client, then ensure that all calls contain a context which includes the
// transaction. The options are optional. Provide them to get instance metrics
// broken out by host and port. The hook returned can be used with
// redis.Client, redis.ClusterClient, and redis.Ring.
func NewHook(opts *redis.Options) redis.Hook {
h := hook{}
h.segment.Product = newrelic.DatastoreRedis
if opts != nil {
// Per https://godoc.org/github.com/go-redis/redis#Options the
// network should either be tcp or unix, and the default is tcp.
if opts.Network == "unix" {
h.segment.Host = "localhost"
h.segment.PortPathOrID = opts.Addr
} else if host, port, err := net.SplitHostPort(opts.Addr); err == nil {
if "" == host {
host = "localhost"
}
h.segment.Host = host
h.segment.PortPathOrID = port
}
}
return h
}

func (h hook) before(ctx context.Context, operation string) (context.Context, error) {
txn := newrelic.FromContext(ctx)
if txn == nil {
return ctx, nil
}
s := h.segment
s.StartTime = txn.StartSegmentNow()
s.Operation = operation
ctx = context.WithValue(ctx, segmentContextKey, &s)
return ctx, nil
}

func (h hook) after(ctx context.Context) {
if segment, ok := ctx.Value(segmentContextKey).(interface{ End() }); ok {
segment.End()
}
}

func (h hook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) {
return h.before(ctx, cmd.Name())
}

func (h hook) AfterProcess(ctx context.Context, cmd redis.Cmder) error {
h.after(ctx)
return nil
}

func pipelineOperation(cmds []redis.Cmder) string {
operations := make([]string, 0, len(cmds))
for _, cmd := range cmds {
operations = append(operations, cmd.Name())
}
return "pipeline:" + strings.Join(operations, ",")
}

func (h hook) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) {
return h.before(ctx, pipelineOperation(cmds))
}

func (h hook) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error {
h.after(ctx)
return nil
}
54 changes: 54 additions & 0 deletions v3/integrations/nrredis-v8/nrredis_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2020 New Relic Corporation. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package nrredis_test

import (
"context"
"fmt"

redis "github.com/go-redis/redis/v8"
nrredis "github.com/newrelic/go-agent/v3/integrations/nrredis-v8"
newrelic "github.com/newrelic/go-agent/v3/newrelic"
)

func getTransaction() *newrelic.Transaction { return nil }

func Example_client() {
opts := &redis.Options{Addr: "localhost:6379"}
client := redis.NewClient(opts)

//
// Step 1: Add a nrredis.NewHook() to your redis client.
//
client.AddHook(nrredis.NewHook(opts))

//
// Step 2: Ensure that all client calls contain a context with includes
// the transaction.
//
txn := getTransaction()
ctx := newrelic.NewContext(context.Background(), txn)
pong, err := client.WithContext(ctx).Ping(ctx).Result()
fmt.Println(pong, err)
}

func Example_clusterClient() {
client := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"},
})

//
// Step 1: Add a nrredis.NewHook() to your redis cluster client.
//
client.AddHook(nrredis.NewHook(nil))

//
// Step 2: Ensure that all client calls contain a context with includes
// the transaction.
//
txn := getTransaction()
ctx := newrelic.NewContext(context.Background(), txn)
pong, err := client.WithContext(ctx).Ping(ctx).Result()
fmt.Println(pong, err)
}
168 changes: 168 additions & 0 deletions v3/integrations/nrredis-v8/nrredis_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Copyright 2020 New Relic Corporation. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package nrredis

import (
"context"
"net"
"testing"

redis "github.com/go-redis/redis/v8"
"github.com/newrelic/go-agent/v3/internal"
"github.com/newrelic/go-agent/v3/internal/integrationsupport"
newrelic "github.com/newrelic/go-agent/v3/newrelic"
)

func emptyDialer(context.Context, string, string) (net.Conn, error) {
return &net.TCPConn{}, nil
}

func TestPing(t *testing.T) {
opts := &redis.Options{
Dialer: emptyDialer,
Addr: "myhost:myport",
}
client := redis.NewClient(opts)

app := integrationsupport.NewTestApp(nil, nil)
txn := app.StartTransaction("txnName")
ctx := newrelic.NewContext(context.Background(), txn)

client.AddHook(NewHook(nil))
client.WithContext(ctx).Ping(ctx)
txn.End()

app.ExpectMetrics(t, []internal.WantMetric{
{Name: "OtherTransaction/Go/txnName", Forced: nil},
{Name: "OtherTransactionTotalTime/Go/txnName", Forced: nil},
{Name: "OtherTransaction/all", Forced: nil},
{Name: "OtherTransactionTotalTime", Forced: nil},
{Name: "Datastore/all", Forced: nil},
{Name: "Datastore/allOther", Forced: nil},
{Name: "Datastore/Redis/all", Forced: nil},
{Name: "Datastore/Redis/allOther", Forced: nil},
{Name: "Datastore/operation/Redis/ping", Forced: nil},
{Name: "Datastore/operation/Redis/ping", Scope: "OtherTransaction/Go/txnName", Forced: nil},
})
}

func TestPingWithOptionsAndAddress(t *testing.T) {
opts := &redis.Options{
Dialer: emptyDialer,
Addr: "myhost:myport",
}
client := redis.NewClient(opts)

app := integrationsupport.NewTestApp(nil, nil)
txn := app.StartTransaction("txnName")
ctx := newrelic.NewContext(context.Background(), txn)

client.AddHook(NewHook(opts))
client.WithContext(ctx).Ping(ctx)
txn.End()

app.ExpectMetrics(t, []internal.WantMetric{
{Name: "OtherTransaction/Go/txnName", Forced: nil},
{Name: "OtherTransactionTotalTime/Go/txnName", Forced: nil},
{Name: "OtherTransaction/all", Forced: nil},
{Name: "OtherTransactionTotalTime", Forced: nil},
{Name: "Datastore/all", Forced: nil},
{Name: "Datastore/allOther", Forced: nil},
{Name: "Datastore/Redis/all", Forced: nil},
{Name: "Datastore/Redis/allOther", Forced: nil},
{Name: "Datastore/instance/Redis/myhost/myport", Forced: nil},
{Name: "Datastore/operation/Redis/ping", Forced: nil},
{Name: "Datastore/operation/Redis/ping", Scope: "OtherTransaction/Go/txnName", Forced: nil},
})
}

func TestPipelineOperation(t *testing.T) {
// As of Jan 16, 2020, it is impossible to test pipeline operations using
// a &net.TCPConn{}, so we will have to make do with this.
if op := pipelineOperation(nil); op != "pipeline:" {
t.Error(op)
}
ctx := context.Background()
cmds := []redis.Cmder{redis.NewCmd(ctx, "GET"), redis.NewCmd(ctx, "SET")}
if op := pipelineOperation(cmds); op != "pipeline:get,set" {
t.Error(op)
}
}

func TestNewHookAddress(t *testing.T) {
testcases := []struct {
network string
address string
expHost string
expPort string
}{
// examples from net.Dial https://godoc.org/net#Dial
{
network: "tcp",
address: "golang.org:http",
expHost: "golang.org",
expPort: "http",
},
{
network: "", // tcp is assumed if missing
address: "golang.org:http",
expHost: "golang.org",
expPort: "http",
},
{
network: "tcp",
address: "192.0.2.1:http",
expHost: "192.0.2.1",
expPort: "http",
},
{
network: "tcp",
address: "198.51.100.1:80",
expHost: "198.51.100.1",
expPort: "80",
},
{
network: "tcp",
address: ":80",
expHost: "localhost",
expPort: "80",
},
{
network: "tcp",
address: "0.0.0.0:80",
expHost: "0.0.0.0",
expPort: "80",
},
{
network: "tcp",
address: "[::]:80",
expHost: "::",
expPort: "80",
},
{
network: "unix",
address: "path/to/socket",
expHost: "localhost",
expPort: "path/to/socket",
},
}

for _, tc := range testcases {
t.Run(tc.network+","+tc.address, func(t *testing.T) {
hk := NewHook(&redis.Options{
Network: tc.network,
Addr: tc.address,
}).(hook)

if hk.segment.Host != tc.expHost {
t.Errorf("incorrect host: expect=%s actual=%s",
tc.expHost, hk.segment.Host)
}
if hk.segment.PortPathOrID != tc.expPort {
t.Errorf("incorrect port: expect=%s actual=%s",
tc.expPort, hk.segment.PortPathOrID)
}
})
}
}

0 comments on commit 5739767

Please sign in to comment.