Skip to content

Commit

Permalink
feat: add instrument package for Segment (#226)
Browse files Browse the repository at this point in the history
* Add instrument package for Segment

* Add documentation for instrument package
  • Loading branch information
keiko713 authored Mar 26, 2021
1 parent ff99e14 commit 9ae9a6d
Show file tree
Hide file tree
Showing 11 changed files with 322 additions and 35 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ require (
github.com/rs/cors v1.6.0
github.com/satori/go.uuid v1.2.0
github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35
github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 // indirect
github.com/shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d
github.com/sirupsen/logrus v1.6.0
github.com/spf13/afero v1.2.2 // indirect
Expand All @@ -45,13 +46,15 @@ require (
github.com/stretchr/testify v1.6.0
github.com/tidwall/pretty v1.0.1 // indirect
github.com/xdg/stringprep v1.0.0 // indirect
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
go.mongodb.org/mongo-driver v1.4.1
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc
gopkg.in/DataDog/dd-trace-go.v1 v1.26.0
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/ghodss/yaml.v1 v1.0.0 // indirect
gopkg.in/launchdarkly/go-sdk-common.v1 v1.0.0-20200401173443-991b2f427a01 // indirect
gopkg.in/launchdarkly/go-server-sdk.v4 v4.0.0-20200729232655-2a44fb361895
gopkg.in/segmentio/analytics-go.v3 v3.1.0
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
)

Expand Down
41 changes: 6 additions & 35 deletions go.sum

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions instrument/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package instrument

type Config struct {
// Write Key for the Segment source
Key string `json:"key" yaml:"key"`
// If this is false, instead of sending the event to Segment, emits verbose log to logger
Enabled bool `json:"enabled" yaml:"enabled" default:"false"`
}
29 changes: 29 additions & 0 deletions instrument/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
Package instrument provides the tool to emit the events to the instrumentation
destination. Currently it supports Segment or logger as a destination.
When enabling, make sure to follow the guideline specified in https://github.com/netlify/segment-events
In the config file, you can define the API key, as well as if it's enabled (use
Segment) or not (use logger).
INSTRUMENT_ENABLED=true
INSTRUMENT_KEY=segment_api_key
To use, you can import this package:
import "github.com/netlify/netlify-commons/instrument"
You will likely need to import the Segment's analytics package as well, to
create new traits and properties.
import "gopkg.in/segmentio/analytics-go.v3"
Then call the functions:
instrument.Track("userid", "service:my_event", analytics.NewProperties().Set("color", "green"))
For testing, you can create your own mock instrument and use it:
func TestSomething (t *testing.T) {
old := instrument.GetGlobalClient()
t.Cleanup(func(){ instrument.SetGlobalClient(old) })
instrument.SetGlobalClient(myMockClient)
}
*/
package instrument
61 changes: 61 additions & 0 deletions instrument/global.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package instrument

import (
"sync"

"github.com/sirupsen/logrus"
"gopkg.in/segmentio/analytics-go.v3"
)

var globalLock sync.Mutex
var globalClient Client = MockClient{}

func SetGlobalClient(client Client) {
if client == nil {
return
}
globalLock.Lock()
globalClient = client
globalLock.Unlock()
}

func GetGlobalClient() Client {
globalLock.Lock()
defer globalLock.Unlock()
return globalClient
}

// Init will initialize global client with a segment client
func Init(conf Config, log logrus.FieldLogger) error {
segmentClient, err := NewClient(&conf, log)
if err != nil {
return err
}
SetGlobalClient(segmentClient)
return nil
}

// Identify sends an identify type message to a queue to be sent to Segment.
func Identify(userID string, traits analytics.Traits) error {
return GetGlobalClient().Identify(userID, traits)
}

// Track sends a track type message to a queue to be sent to Segment.
func Track(userID string, event string, properties analytics.Properties) error {
return GetGlobalClient().Track(userID, event, properties)
}

// Page sends a page type message to a queue to be sent to Segment.
func Page(userID string, name string, properties analytics.Properties) error {
return GetGlobalClient().Page(userID, name, properties)
}

// Group sends a group type message to a queue to be sent to Segment.
func Group(userID string, groupID string, traits analytics.Traits) error {
return GetGlobalClient().Group(userID, groupID, traits)
}

// Alias sends an alias type message to a queue to be sent to Segment.
func Alias(previousID string, userID string) error {
return GetGlobalClient().Alias(previousID, userID)
}
103 changes: 103 additions & 0 deletions instrument/instrument.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package instrument

import (
"io/ioutil"

"github.com/sirupsen/logrus"
"gopkg.in/segmentio/analytics-go.v3"
)

type Client interface {
Identify(userID string, traits analytics.Traits) error
Track(userID string, event string, properties analytics.Properties) error
Page(userID string, name string, properties analytics.Properties) error
Group(userID string, groupID string, traits analytics.Traits) error
Alias(previousID string, userID string) error
}

type segmentClient struct {
analytics.Client
log logrus.FieldLogger
}

var _ Client = &segmentClient{}

func NewClient(cfg *Config, logger logrus.FieldLogger) (Client, error) {
config := analytics.Config{}

if !cfg.Enabled {
// use mockClient instead
return &MockClient{logger}, nil
}

configureLogger(&config, logger)

inner, err := analytics.NewWithConfig(cfg.Key, config)
if err != nil {
logger.WithError(err).Error("Unable to construct Segment client")
}
return &segmentClient{inner, logger}, err
}

func (c segmentClient) Identify(userID string, traits analytics.Traits) error {
return c.Client.Enqueue(analytics.Identify{
UserId: userID,
Traits: traits,
})
}

func (c segmentClient) Track(userID string, event string, properties analytics.Properties) error {
return c.Client.Enqueue(analytics.Track{
UserId: userID,
Event: event,
Properties: properties,
})
}

func (c segmentClient) Page(userID string, name string, properties analytics.Properties) error {
return c.Client.Enqueue(analytics.Page{
UserId: userID,
Name: name,
Properties: properties,
})
}

func (c segmentClient) Group(userID string, groupID string, traits analytics.Traits) error {
return c.Client.Enqueue(analytics.Group{
UserId: userID,
GroupId: groupID,
Traits: traits,
})
}

func (c segmentClient) Alias(previousID string, userID string) error {
return c.Client.Enqueue(analytics.Alias{
PreviousId: previousID,
UserId: userID,
})
}

func configureLogger(conf *analytics.Config, log logrus.FieldLogger) {
if log == nil {
l := logrus.New()
l.SetOutput(ioutil.Discard)
log = l
}
log = log.WithField("component", "segment")
conf.Logger = &wrapLog{log.Printf, log.Errorf}
}

type wrapLog struct {
printf func(format string, args ...interface{})
errorf func(format string, args ...interface{})
}

// Logf implements analytics.Logger interface
func (l *wrapLog) Logf(format string, args ...interface{}) {
l.printf(format, args...)
}

// Errorf implements analytics.Logger interface
func (l *wrapLog) Errorf(format string, args ...interface{}) {
l.errorf(format, args...)
}
42 changes: 42 additions & 0 deletions instrument/instrument_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package instrument

import (
"reflect"
"testing"

"github.com/netlify/netlify-commons/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/segmentio/analytics-go.v3"
)

func TestLogOnlyClient(t *testing.T) {
cfg := Config{
Key: "ABCD",
Enabled: false,
}
client, err := NewClient(&cfg, nil)
require.NoError(t, err)

require.Equal(t, reflect.TypeOf(&MockClient{}).Kind(), reflect.TypeOf(client).Kind())
}

func TestMockClient(t *testing.T) {
log := testutil.TL(t)
mock := MockClient{log}

require.NoError(t, mock.Identify("myuser", analytics.NewTraits().SetName("My User")))
}

func TestLogging(t *testing.T) {
cfg := Config{
Key: "ABCD",
}

log, hook := testutil.TestLogger(t)

client, err := NewClient(&cfg, log.WithField("component", "segment"))
require.NoError(t, err)
require.NoError(t, client.Identify("myuser", analytics.NewTraits().SetName("My User")))
assert.NotEmpty(t, hook.LastEntry())
}
55 changes: 55 additions & 0 deletions instrument/mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package instrument

import (
"github.com/sirupsen/logrus"
"gopkg.in/segmentio/analytics-go.v3"
)

type MockClient struct {
Logger logrus.FieldLogger
}

var _ Client = MockClient{}

func (c MockClient) Identify(userID string, traits analytics.Traits) error {
c.Logger.WithFields(logrus.Fields{
"user_id": userID,
"traits": traits,
}).Infof("Received Identity event")
return nil
}

func (c MockClient) Track(userID string, event string, properties analytics.Properties) error {
c.Logger.WithFields(logrus.Fields{
"user_id": userID,
"event": event,
"properties": properties,
}).Infof("Received Track event")
return nil
}

func (c MockClient) Page(userID string, name string, properties analytics.Properties) error {
c.Logger.WithFields(logrus.Fields{
"user_id": userID,
"name": name,
"properties": properties,
}).Infof("Received Page event")
return nil
}

func (c MockClient) Group(userID string, groupID string, traits analytics.Traits) error {
c.Logger.WithFields(logrus.Fields{
"user_id": userID,
"group_id": groupID,
"traits": traits,
}).Infof("Received Group event")
return nil
}

func (c MockClient) Alias(previousID string, userID string) error {
c.Logger.WithFields(logrus.Fields{
"previous_id": previousID,
"user_id": userID,
}).Infof("Received Alias event")
return nil
}
5 changes: 5 additions & 0 deletions nconf/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strings"

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

if err := instrument.Init(rootConfig.Instrument, log); err != nil {
return nil, errors.Wrap(err, "Failed to configure instrument")
}

if err := sendDatadogEvents(rootConfig.Metrics, serviceName, version); err != nil {
log.WithError(err).Error("Failed to send the startup events to datadog")
}
Expand Down
8 changes: 8 additions & 0 deletions nconf/args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ func TestArgsLoadDefault(t *testing.T) {
"request_timeout": "10s",
"enabled": true,
},
"instrument": map[string]interface{}{
"key": "greatkey",
"enabled": true,
},
}

scenes := []struct {
Expand Down Expand Up @@ -161,6 +165,10 @@ func TestArgsLoadDefault(t *testing.T) {
assert.Equal(t, true, cfg.FeatureFlag.Enabled)
assert.Equal(t, false, cfg.FeatureFlag.DisableEvents)
assert.Equal(t, "", cfg.FeatureFlag.RelayHost)

// instrument
assert.Equal(t, "greatkey", cfg.Instrument.Key)
assert.Equal(t, true, cfg.Instrument.Enabled)
})
}
}
2 changes: 2 additions & 0 deletions nconf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig"
"github.com/netlify/netlify-commons/featureflag"
"github.com/netlify/netlify-commons/instrument"
"github.com/netlify/netlify-commons/metriks"
"github.com/netlify/netlify-commons/tracing"
"github.com/pkg/errors"
Expand All @@ -33,6 +34,7 @@ type RootConfig struct {
Metrics metriks.Config
Tracing tracing.Config
FeatureFlag featureflag.Config
Instrument instrument.Config
}

func DefaultConfig() RootConfig {
Expand Down

0 comments on commit 9ae9a6d

Please sign in to comment.