Skip to content

Commit 9ae9a6d

Browse files
authored
feat: add instrument package for Segment (#226)
* Add instrument package for Segment * Add documentation for instrument package
1 parent ff99e14 commit 9ae9a6d

File tree

11 files changed

+322
-35
lines changed

11 files changed

+322
-35
lines changed

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ require (
3535
github.com/rs/cors v1.6.0
3636
github.com/satori/go.uuid v1.2.0
3737
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35
38+
github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 // indirect
3839
github.com/shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d
3940
github.com/sirupsen/logrus v1.6.0
4041
github.com/spf13/afero v1.2.2 // indirect
@@ -45,13 +46,15 @@ require (
4546
github.com/stretchr/testify v1.6.0
4647
github.com/tidwall/pretty v1.0.1 // indirect
4748
github.com/xdg/stringprep v1.0.0 // indirect
49+
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
4850
go.mongodb.org/mongo-driver v1.4.1
4951
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc
5052
gopkg.in/DataDog/dd-trace-go.v1 v1.26.0
5153
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
5254
gopkg.in/ghodss/yaml.v1 v1.0.0 // indirect
5355
gopkg.in/launchdarkly/go-sdk-common.v1 v1.0.0-20200401173443-991b2f427a01 // indirect
5456
gopkg.in/launchdarkly/go-server-sdk.v4 v4.0.0-20200729232655-2a44fb361895
57+
gopkg.in/segmentio/analytics-go.v3 v3.1.0
5558
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
5659
)
5760

go.sum

Lines changed: 6 additions & 35 deletions
Large diffs are not rendered by default.

instrument/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package instrument
2+
3+
type Config struct {
4+
// Write Key for the Segment source
5+
Key string `json:"key" yaml:"key"`
6+
// If this is false, instead of sending the event to Segment, emits verbose log to logger
7+
Enabled bool `json:"enabled" yaml:"enabled" default:"false"`
8+
}

instrument/doc.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Package instrument provides the tool to emit the events to the instrumentation
3+
destination. Currently it supports Segment or logger as a destination.
4+
5+
When enabling, make sure to follow the guideline specified in https://github.com/netlify/segment-events
6+
7+
In the config file, you can define the API key, as well as if it's enabled (use
8+
Segment) or not (use logger).
9+
INSTRUMENT_ENABLED=true
10+
INSTRUMENT_KEY=segment_api_key
11+
12+
To use, you can import this package:
13+
import "github.com/netlify/netlify-commons/instrument"
14+
15+
You will likely need to import the Segment's analytics package as well, to
16+
create new traits and properties.
17+
import "gopkg.in/segmentio/analytics-go.v3"
18+
19+
Then call the functions:
20+
instrument.Track("userid", "service:my_event", analytics.NewProperties().Set("color", "green"))
21+
22+
For testing, you can create your own mock instrument and use it:
23+
func TestSomething (t *testing.T) {
24+
old := instrument.GetGlobalClient()
25+
t.Cleanup(func(){ instrument.SetGlobalClient(old) })
26+
instrument.SetGlobalClient(myMockClient)
27+
}
28+
*/
29+
package instrument

instrument/global.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package instrument
2+
3+
import (
4+
"sync"
5+
6+
"github.com/sirupsen/logrus"
7+
"gopkg.in/segmentio/analytics-go.v3"
8+
)
9+
10+
var globalLock sync.Mutex
11+
var globalClient Client = MockClient{}
12+
13+
func SetGlobalClient(client Client) {
14+
if client == nil {
15+
return
16+
}
17+
globalLock.Lock()
18+
globalClient = client
19+
globalLock.Unlock()
20+
}
21+
22+
func GetGlobalClient() Client {
23+
globalLock.Lock()
24+
defer globalLock.Unlock()
25+
return globalClient
26+
}
27+
28+
// Init will initialize global client with a segment client
29+
func Init(conf Config, log logrus.FieldLogger) error {
30+
segmentClient, err := NewClient(&conf, log)
31+
if err != nil {
32+
return err
33+
}
34+
SetGlobalClient(segmentClient)
35+
return nil
36+
}
37+
38+
// Identify sends an identify type message to a queue to be sent to Segment.
39+
func Identify(userID string, traits analytics.Traits) error {
40+
return GetGlobalClient().Identify(userID, traits)
41+
}
42+
43+
// Track sends a track type message to a queue to be sent to Segment.
44+
func Track(userID string, event string, properties analytics.Properties) error {
45+
return GetGlobalClient().Track(userID, event, properties)
46+
}
47+
48+
// Page sends a page type message to a queue to be sent to Segment.
49+
func Page(userID string, name string, properties analytics.Properties) error {
50+
return GetGlobalClient().Page(userID, name, properties)
51+
}
52+
53+
// Group sends a group type message to a queue to be sent to Segment.
54+
func Group(userID string, groupID string, traits analytics.Traits) error {
55+
return GetGlobalClient().Group(userID, groupID, traits)
56+
}
57+
58+
// Alias sends an alias type message to a queue to be sent to Segment.
59+
func Alias(previousID string, userID string) error {
60+
return GetGlobalClient().Alias(previousID, userID)
61+
}

instrument/instrument.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package instrument
2+
3+
import (
4+
"io/ioutil"
5+
6+
"github.com/sirupsen/logrus"
7+
"gopkg.in/segmentio/analytics-go.v3"
8+
)
9+
10+
type Client interface {
11+
Identify(userID string, traits analytics.Traits) error
12+
Track(userID string, event string, properties analytics.Properties) error
13+
Page(userID string, name string, properties analytics.Properties) error
14+
Group(userID string, groupID string, traits analytics.Traits) error
15+
Alias(previousID string, userID string) error
16+
}
17+
18+
type segmentClient struct {
19+
analytics.Client
20+
log logrus.FieldLogger
21+
}
22+
23+
var _ Client = &segmentClient{}
24+
25+
func NewClient(cfg *Config, logger logrus.FieldLogger) (Client, error) {
26+
config := analytics.Config{}
27+
28+
if !cfg.Enabled {
29+
// use mockClient instead
30+
return &MockClient{logger}, nil
31+
}
32+
33+
configureLogger(&config, logger)
34+
35+
inner, err := analytics.NewWithConfig(cfg.Key, config)
36+
if err != nil {
37+
logger.WithError(err).Error("Unable to construct Segment client")
38+
}
39+
return &segmentClient{inner, logger}, err
40+
}
41+
42+
func (c segmentClient) Identify(userID string, traits analytics.Traits) error {
43+
return c.Client.Enqueue(analytics.Identify{
44+
UserId: userID,
45+
Traits: traits,
46+
})
47+
}
48+
49+
func (c segmentClient) Track(userID string, event string, properties analytics.Properties) error {
50+
return c.Client.Enqueue(analytics.Track{
51+
UserId: userID,
52+
Event: event,
53+
Properties: properties,
54+
})
55+
}
56+
57+
func (c segmentClient) Page(userID string, name string, properties analytics.Properties) error {
58+
return c.Client.Enqueue(analytics.Page{
59+
UserId: userID,
60+
Name: name,
61+
Properties: properties,
62+
})
63+
}
64+
65+
func (c segmentClient) Group(userID string, groupID string, traits analytics.Traits) error {
66+
return c.Client.Enqueue(analytics.Group{
67+
UserId: userID,
68+
GroupId: groupID,
69+
Traits: traits,
70+
})
71+
}
72+
73+
func (c segmentClient) Alias(previousID string, userID string) error {
74+
return c.Client.Enqueue(analytics.Alias{
75+
PreviousId: previousID,
76+
UserId: userID,
77+
})
78+
}
79+
80+
func configureLogger(conf *analytics.Config, log logrus.FieldLogger) {
81+
if log == nil {
82+
l := logrus.New()
83+
l.SetOutput(ioutil.Discard)
84+
log = l
85+
}
86+
log = log.WithField("component", "segment")
87+
conf.Logger = &wrapLog{log.Printf, log.Errorf}
88+
}
89+
90+
type wrapLog struct {
91+
printf func(format string, args ...interface{})
92+
errorf func(format string, args ...interface{})
93+
}
94+
95+
// Logf implements analytics.Logger interface
96+
func (l *wrapLog) Logf(format string, args ...interface{}) {
97+
l.printf(format, args...)
98+
}
99+
100+
// Errorf implements analytics.Logger interface
101+
func (l *wrapLog) Errorf(format string, args ...interface{}) {
102+
l.errorf(format, args...)
103+
}

instrument/instrument_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package instrument
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/netlify/netlify-commons/testutil"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
"gopkg.in/segmentio/analytics-go.v3"
11+
)
12+
13+
func TestLogOnlyClient(t *testing.T) {
14+
cfg := Config{
15+
Key: "ABCD",
16+
Enabled: false,
17+
}
18+
client, err := NewClient(&cfg, nil)
19+
require.NoError(t, err)
20+
21+
require.Equal(t, reflect.TypeOf(&MockClient{}).Kind(), reflect.TypeOf(client).Kind())
22+
}
23+
24+
func TestMockClient(t *testing.T) {
25+
log := testutil.TL(t)
26+
mock := MockClient{log}
27+
28+
require.NoError(t, mock.Identify("myuser", analytics.NewTraits().SetName("My User")))
29+
}
30+
31+
func TestLogging(t *testing.T) {
32+
cfg := Config{
33+
Key: "ABCD",
34+
}
35+
36+
log, hook := testutil.TestLogger(t)
37+
38+
client, err := NewClient(&cfg, log.WithField("component", "segment"))
39+
require.NoError(t, err)
40+
require.NoError(t, client.Identify("myuser", analytics.NewTraits().SetName("My User")))
41+
assert.NotEmpty(t, hook.LastEntry())
42+
}

instrument/mock.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package instrument
2+
3+
import (
4+
"github.com/sirupsen/logrus"
5+
"gopkg.in/segmentio/analytics-go.v3"
6+
)
7+
8+
type MockClient struct {
9+
Logger logrus.FieldLogger
10+
}
11+
12+
var _ Client = MockClient{}
13+
14+
func (c MockClient) Identify(userID string, traits analytics.Traits) error {
15+
c.Logger.WithFields(logrus.Fields{
16+
"user_id": userID,
17+
"traits": traits,
18+
}).Infof("Received Identity event")
19+
return nil
20+
}
21+
22+
func (c MockClient) Track(userID string, event string, properties analytics.Properties) error {
23+
c.Logger.WithFields(logrus.Fields{
24+
"user_id": userID,
25+
"event": event,
26+
"properties": properties,
27+
}).Infof("Received Track event")
28+
return nil
29+
}
30+
31+
func (c MockClient) Page(userID string, name string, properties analytics.Properties) error {
32+
c.Logger.WithFields(logrus.Fields{
33+
"user_id": userID,
34+
"name": name,
35+
"properties": properties,
36+
}).Infof("Received Page event")
37+
return nil
38+
}
39+
40+
func (c MockClient) Group(userID string, groupID string, traits analytics.Traits) error {
41+
c.Logger.WithFields(logrus.Fields{
42+
"user_id": userID,
43+
"group_id": groupID,
44+
"traits": traits,
45+
}).Infof("Received Group event")
46+
return nil
47+
}
48+
49+
func (c MockClient) Alias(previousID string, userID string) error {
50+
c.Logger.WithFields(logrus.Fields{
51+
"previous_id": previousID,
52+
"user_id": userID,
53+
}).Infof("Received Alias event")
54+
return nil
55+
}

nconf/args.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strings"
66

77
"github.com/netlify/netlify-commons/featureflag"
8+
"github.com/netlify/netlify-commons/instrument"
89
"github.com/netlify/netlify-commons/metriks"
910
"github.com/netlify/netlify-commons/tracing"
1011
"github.com/pkg/errors"
@@ -48,6 +49,10 @@ func (args *RootArgs) Setup(config interface{}, serviceName, version string) (lo
4849
return nil, errors.Wrap(err, "Failed to configure featureflags")
4950
}
5051

52+
if err := instrument.Init(rootConfig.Instrument, log); err != nil {
53+
return nil, errors.Wrap(err, "Failed to configure instrument")
54+
}
55+
5156
if err := sendDatadogEvents(rootConfig.Metrics, serviceName, version); err != nil {
5257
log.WithError(err).Error("Failed to send the startup events to datadog")
5358
}

nconf/args_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ func TestArgsLoadDefault(t *testing.T) {
9999
"request_timeout": "10s",
100100
"enabled": true,
101101
},
102+
"instrument": map[string]interface{}{
103+
"key": "greatkey",
104+
"enabled": true,
105+
},
102106
}
103107

104108
scenes := []struct {
@@ -161,6 +165,10 @@ func TestArgsLoadDefault(t *testing.T) {
161165
assert.Equal(t, true, cfg.FeatureFlag.Enabled)
162166
assert.Equal(t, false, cfg.FeatureFlag.DisableEvents)
163167
assert.Equal(t, "", cfg.FeatureFlag.RelayHost)
168+
169+
// instrument
170+
assert.Equal(t, "greatkey", cfg.Instrument.Key)
171+
assert.Equal(t, true, cfg.Instrument.Enabled)
164172
})
165173
}
166174
}

0 commit comments

Comments
 (0)