Skip to content

Commit 5739767

Browse files
Merge pull request #251 from ilmimris/v3/integrations/nrredis-v8
Added support for `v8` of go-redis/redis
2 parents df4d9c2 + a4b63d0 commit 5739767

File tree

6 files changed

+393
-0
lines changed

6 files changed

+393
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# 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)
2+
3+
Package `nrredis` instruments `"github.com/go-redis/redis/v8"`.
4+
5+
```go
6+
import nrredis "github.com/newrelic/go-agent/v3/integrations/nrredis-v8"
7+
```
8+
9+
For more information, see
10+
[godocs](https://godoc.org/github.com/newrelic/go-agent/v3/integrations/nrredis-v8).
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2020 New Relic Corporation. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package main
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"os"
10+
"time"
11+
12+
redis "github.com/go-redis/redis/v8"
13+
nrredis "github.com/newrelic/go-agent/v3/integrations/nrredis-v8"
14+
newrelic "github.com/newrelic/go-agent/v3/newrelic"
15+
)
16+
17+
func main() {
18+
app, err := newrelic.NewApplication(
19+
newrelic.ConfigAppName("Redis App"),
20+
newrelic.ConfigLicense(os.Getenv("NEW_RELIC_LICENSE_KEY")),
21+
newrelic.ConfigDebugLogger(os.Stdout),
22+
)
23+
if nil != err {
24+
panic(err)
25+
}
26+
app.WaitForConnection(10 * time.Second)
27+
txn := app.StartTransaction("ping txn")
28+
29+
opts := &redis.Options{
30+
Addr: "localhost:6379",
31+
}
32+
client := redis.NewClient(opts)
33+
34+
//
35+
// Step 1: Add a nrredis.NewHook() to your redis client.
36+
//
37+
client.AddHook(nrredis.NewHook(opts))
38+
39+
//
40+
// Step 2: Ensure that all client calls contain a context which includes
41+
// the transaction.
42+
//
43+
ctx := newrelic.NewContext(context.Background(), txn)
44+
pipe := client.WithContext(ctx).Pipeline()
45+
incr := pipe.Incr(ctx, "pipeline_counter")
46+
pipe.Expire(ctx, "pipeline_counter", time.Hour)
47+
_, err = pipe.Exec(ctx)
48+
fmt.Println(incr.Val(), err)
49+
50+
txn.End()
51+
app.Shutdown(5 * time.Second)
52+
}

v3/integrations/nrredis-v8/go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/newrelic/go-agent/v3/integrations/nrredis-v8
2+
3+
// As of Jan 2020, go 1.11 is in the go-redis go.mod file:
4+
// https://github.com/go-redis/redis/blob/master/go.mod
5+
go 1.11
6+
7+
require (
8+
github.com/go-redis/redis/v8 v8.4.0
9+
github.com/newrelic/go-agent/v3 v3.0.0
10+
)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright 2020 New Relic Corporation. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Package nrredis instruments github.com/go-redis/redis/v8.
5+
//
6+
// Use this package to instrument your go-redis/redis/v8 calls without having to
7+
// manually create DatastoreSegments.
8+
package nrredis
9+
10+
import (
11+
"context"
12+
"net"
13+
"strings"
14+
15+
redis "github.com/go-redis/redis/v8"
16+
"github.com/newrelic/go-agent/v3/internal"
17+
newrelic "github.com/newrelic/go-agent/v3/newrelic"
18+
)
19+
20+
func init() { internal.TrackUsage("integration", "datastore", "redis") }
21+
22+
type contextKeyType struct{}
23+
24+
type hook struct {
25+
segment newrelic.DatastoreSegment
26+
}
27+
28+
var (
29+
segmentContextKey = contextKeyType(struct{}{})
30+
)
31+
32+
// NewHook creates a redis.Hook to instrument Redis calls. Add it to your
33+
// client, then ensure that all calls contain a context which includes the
34+
// transaction. The options are optional. Provide them to get instance metrics
35+
// broken out by host and port. The hook returned can be used with
36+
// redis.Client, redis.ClusterClient, and redis.Ring.
37+
func NewHook(opts *redis.Options) redis.Hook {
38+
h := hook{}
39+
h.segment.Product = newrelic.DatastoreRedis
40+
if opts != nil {
41+
// Per https://godoc.org/github.com/go-redis/redis#Options the
42+
// network should either be tcp or unix, and the default is tcp.
43+
if opts.Network == "unix" {
44+
h.segment.Host = "localhost"
45+
h.segment.PortPathOrID = opts.Addr
46+
} else if host, port, err := net.SplitHostPort(opts.Addr); err == nil {
47+
if "" == host {
48+
host = "localhost"
49+
}
50+
h.segment.Host = host
51+
h.segment.PortPathOrID = port
52+
}
53+
}
54+
return h
55+
}
56+
57+
func (h hook) before(ctx context.Context, operation string) (context.Context, error) {
58+
txn := newrelic.FromContext(ctx)
59+
if txn == nil {
60+
return ctx, nil
61+
}
62+
s := h.segment
63+
s.StartTime = txn.StartSegmentNow()
64+
s.Operation = operation
65+
ctx = context.WithValue(ctx, segmentContextKey, &s)
66+
return ctx, nil
67+
}
68+
69+
func (h hook) after(ctx context.Context) {
70+
if segment, ok := ctx.Value(segmentContextKey).(interface{ End() }); ok {
71+
segment.End()
72+
}
73+
}
74+
75+
func (h hook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) {
76+
return h.before(ctx, cmd.Name())
77+
}
78+
79+
func (h hook) AfterProcess(ctx context.Context, cmd redis.Cmder) error {
80+
h.after(ctx)
81+
return nil
82+
}
83+
84+
func pipelineOperation(cmds []redis.Cmder) string {
85+
operations := make([]string, 0, len(cmds))
86+
for _, cmd := range cmds {
87+
operations = append(operations, cmd.Name())
88+
}
89+
return "pipeline:" + strings.Join(operations, ",")
90+
}
91+
92+
func (h hook) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) {
93+
return h.before(ctx, pipelineOperation(cmds))
94+
}
95+
96+
func (h hook) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error {
97+
h.after(ctx)
98+
return nil
99+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2020 New Relic Corporation. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package nrredis_test
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
redis "github.com/go-redis/redis/v8"
11+
nrredis "github.com/newrelic/go-agent/v3/integrations/nrredis-v8"
12+
newrelic "github.com/newrelic/go-agent/v3/newrelic"
13+
)
14+
15+
func getTransaction() *newrelic.Transaction { return nil }
16+
17+
func Example_client() {
18+
opts := &redis.Options{Addr: "localhost:6379"}
19+
client := redis.NewClient(opts)
20+
21+
//
22+
// Step 1: Add a nrredis.NewHook() to your redis client.
23+
//
24+
client.AddHook(nrredis.NewHook(opts))
25+
26+
//
27+
// Step 2: Ensure that all client calls contain a context with includes
28+
// the transaction.
29+
//
30+
txn := getTransaction()
31+
ctx := newrelic.NewContext(context.Background(), txn)
32+
pong, err := client.WithContext(ctx).Ping(ctx).Result()
33+
fmt.Println(pong, err)
34+
}
35+
36+
func Example_clusterClient() {
37+
client := redis.NewClusterClient(&redis.ClusterOptions{
38+
Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"},
39+
})
40+
41+
//
42+
// Step 1: Add a nrredis.NewHook() to your redis cluster client.
43+
//
44+
client.AddHook(nrredis.NewHook(nil))
45+
46+
//
47+
// Step 2: Ensure that all client calls contain a context with includes
48+
// the transaction.
49+
//
50+
txn := getTransaction()
51+
ctx := newrelic.NewContext(context.Background(), txn)
52+
pong, err := client.WithContext(ctx).Ping(ctx).Result()
53+
fmt.Println(pong, err)
54+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Copyright 2020 New Relic Corporation. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package nrredis
5+
6+
import (
7+
"context"
8+
"net"
9+
"testing"
10+
11+
redis "github.com/go-redis/redis/v8"
12+
"github.com/newrelic/go-agent/v3/internal"
13+
"github.com/newrelic/go-agent/v3/internal/integrationsupport"
14+
newrelic "github.com/newrelic/go-agent/v3/newrelic"
15+
)
16+
17+
func emptyDialer(context.Context, string, string) (net.Conn, error) {
18+
return &net.TCPConn{}, nil
19+
}
20+
21+
func TestPing(t *testing.T) {
22+
opts := &redis.Options{
23+
Dialer: emptyDialer,
24+
Addr: "myhost:myport",
25+
}
26+
client := redis.NewClient(opts)
27+
28+
app := integrationsupport.NewTestApp(nil, nil)
29+
txn := app.StartTransaction("txnName")
30+
ctx := newrelic.NewContext(context.Background(), txn)
31+
32+
client.AddHook(NewHook(nil))
33+
client.WithContext(ctx).Ping(ctx)
34+
txn.End()
35+
36+
app.ExpectMetrics(t, []internal.WantMetric{
37+
{Name: "OtherTransaction/Go/txnName", Forced: nil},
38+
{Name: "OtherTransactionTotalTime/Go/txnName", Forced: nil},
39+
{Name: "OtherTransaction/all", Forced: nil},
40+
{Name: "OtherTransactionTotalTime", Forced: nil},
41+
{Name: "Datastore/all", Forced: nil},
42+
{Name: "Datastore/allOther", Forced: nil},
43+
{Name: "Datastore/Redis/all", Forced: nil},
44+
{Name: "Datastore/Redis/allOther", Forced: nil},
45+
{Name: "Datastore/operation/Redis/ping", Forced: nil},
46+
{Name: "Datastore/operation/Redis/ping", Scope: "OtherTransaction/Go/txnName", Forced: nil},
47+
})
48+
}
49+
50+
func TestPingWithOptionsAndAddress(t *testing.T) {
51+
opts := &redis.Options{
52+
Dialer: emptyDialer,
53+
Addr: "myhost:myport",
54+
}
55+
client := redis.NewClient(opts)
56+
57+
app := integrationsupport.NewTestApp(nil, nil)
58+
txn := app.StartTransaction("txnName")
59+
ctx := newrelic.NewContext(context.Background(), txn)
60+
61+
client.AddHook(NewHook(opts))
62+
client.WithContext(ctx).Ping(ctx)
63+
txn.End()
64+
65+
app.ExpectMetrics(t, []internal.WantMetric{
66+
{Name: "OtherTransaction/Go/txnName", Forced: nil},
67+
{Name: "OtherTransactionTotalTime/Go/txnName", Forced: nil},
68+
{Name: "OtherTransaction/all", Forced: nil},
69+
{Name: "OtherTransactionTotalTime", Forced: nil},
70+
{Name: "Datastore/all", Forced: nil},
71+
{Name: "Datastore/allOther", Forced: nil},
72+
{Name: "Datastore/Redis/all", Forced: nil},
73+
{Name: "Datastore/Redis/allOther", Forced: nil},
74+
{Name: "Datastore/instance/Redis/myhost/myport", Forced: nil},
75+
{Name: "Datastore/operation/Redis/ping", Forced: nil},
76+
{Name: "Datastore/operation/Redis/ping", Scope: "OtherTransaction/Go/txnName", Forced: nil},
77+
})
78+
}
79+
80+
func TestPipelineOperation(t *testing.T) {
81+
// As of Jan 16, 2020, it is impossible to test pipeline operations using
82+
// a &net.TCPConn{}, so we will have to make do with this.
83+
if op := pipelineOperation(nil); op != "pipeline:" {
84+
t.Error(op)
85+
}
86+
ctx := context.Background()
87+
cmds := []redis.Cmder{redis.NewCmd(ctx, "GET"), redis.NewCmd(ctx, "SET")}
88+
if op := pipelineOperation(cmds); op != "pipeline:get,set" {
89+
t.Error(op)
90+
}
91+
}
92+
93+
func TestNewHookAddress(t *testing.T) {
94+
testcases := []struct {
95+
network string
96+
address string
97+
expHost string
98+
expPort string
99+
}{
100+
// examples from net.Dial https://godoc.org/net#Dial
101+
{
102+
network: "tcp",
103+
address: "golang.org:http",
104+
expHost: "golang.org",
105+
expPort: "http",
106+
},
107+
{
108+
network: "", // tcp is assumed if missing
109+
address: "golang.org:http",
110+
expHost: "golang.org",
111+
expPort: "http",
112+
},
113+
{
114+
network: "tcp",
115+
address: "192.0.2.1:http",
116+
expHost: "192.0.2.1",
117+
expPort: "http",
118+
},
119+
{
120+
network: "tcp",
121+
address: "198.51.100.1:80",
122+
expHost: "198.51.100.1",
123+
expPort: "80",
124+
},
125+
{
126+
network: "tcp",
127+
address: ":80",
128+
expHost: "localhost",
129+
expPort: "80",
130+
},
131+
{
132+
network: "tcp",
133+
address: "0.0.0.0:80",
134+
expHost: "0.0.0.0",
135+
expPort: "80",
136+
},
137+
{
138+
network: "tcp",
139+
address: "[::]:80",
140+
expHost: "::",
141+
expPort: "80",
142+
},
143+
{
144+
network: "unix",
145+
address: "path/to/socket",
146+
expHost: "localhost",
147+
expPort: "path/to/socket",
148+
},
149+
}
150+
151+
for _, tc := range testcases {
152+
t.Run(tc.network+","+tc.address, func(t *testing.T) {
153+
hk := NewHook(&redis.Options{
154+
Network: tc.network,
155+
Addr: tc.address,
156+
}).(hook)
157+
158+
if hk.segment.Host != tc.expHost {
159+
t.Errorf("incorrect host: expect=%s actual=%s",
160+
tc.expHost, hk.segment.Host)
161+
}
162+
if hk.segment.PortPathOrID != tc.expPort {
163+
t.Errorf("incorrect port: expect=%s actual=%s",
164+
tc.expPort, hk.segment.PortPathOrID)
165+
}
166+
})
167+
}
168+
}

0 commit comments

Comments
 (0)