-
Notifications
You must be signed in to change notification settings - Fork 301
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #251 from ilmimris/v3/integrations/nrredis-v8
Added support for `v8` of go-redis/redis
- Loading branch information
Showing
6 changed files
with
393 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# v3/integrations/nrredis-v8 [](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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |