Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a client object to libhoney for isolating traffic #40

Merged
merged 25 commits into from Feb 22, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
eeef19d
add a libhoney Client type to manage separate transmission queues
maplebed Feb 7, 2019
4240acb
fixing up tests
maplebed Feb 8, 2019
fdcdc93
fix tests
maplebed Feb 10, 2019
95f6ccb
adding tests for clients
maplebed Feb 10, 2019
7838da8
attempting and failing another implementation of Client
maplebed Feb 12, 2019
a500a26
change the Client from an interface to a struct, move transmission to…
maplebed Feb 13, 2019
b9ce6c1
updating and adding tests
maplebed Feb 15, 2019
dc684f5
more test fixing
maplebed Feb 15, 2019
647b92d
naming cleanup
maplebed Feb 15, 2019
1c5f1c2
Other things (such as the beeline) use the default WriterOutput, so w…
maplebed Feb 15, 2019
6ea6c07
little by little
maplebed Feb 15, 2019
b4aecc2
more of the published interface must remain the same
maplebed Feb 15, 2019
8b0dd45
globals are terrible. sync.Once broke unrelated tests
maplebed Feb 16, 2019
08c75ea
try out newer versions of go
maplebed Feb 16, 2019
00dbc3b
oh yaml
maplebed Feb 16, 2019
aa134f8
adding more race tests
maplebed Feb 17, 2019
7bb922b
this is actually done; marshaling protection is in the transmission now.
maplebed Feb 19, 2019
0fd79da
add in protection against modifying an event after sending, to give c…
maplebed Feb 19, 2019
e89d0aa
PR feedback
maplebed Feb 19, 2019
c2a186f
race race race
maplebed Feb 19, 2019
90121c0
re-add missing mock output to match old api surface
maplebed Feb 19, 2019
020d8ee
re-add missing mock output to match old api surface
maplebed Feb 19, 2019
f673dec
removing nil pointer receiver checks in Client because they don't exi…
maplebed Feb 21, 2019
c9399e9
PR feedback
maplebed Feb 21, 2019
f4991a9
PR feedback
maplebed Feb 21, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
146 changes: 146 additions & 0 deletions client.go
@@ -0,0 +1,146 @@
package libhoney

import (
"errors"
)

// TODO think about whether it would be useful for Client to be an interface instead
maplebed marked this conversation as resolved.
Show resolved Hide resolved

type Client interface {
Add(data interface{}) error
AddDynamicField(name string, fn func() interface{}) error
AddField(name string, val interface{})
Close()
Flush()
NewBuilder() *Builder
NewEvent() *Event
Responses() chan Response
}

type defaultClient struct {
conf Config
tx Output
logger Logger
responses chan Response
defaultBuilder *Builder
userAgentAddition string
}

func NewClient(conf Config) (Client, error) {
setConfigDefaults(&conf)

c := &defaultClient{
conf: conf,
tx: conf.Output,
logger: conf.Logger,
}

c.responses = make(chan Response, conf.PendingWorkCapacity*2)
if c.tx == nil {
// use default Honeycomb output
c.tx = &txDefaultClient{
maxBatchSize: conf.MaxBatchSize,
batchTimeout: conf.SendFrequency,
maxConcurrentBatches: conf.MaxConcurrentBatches,
pendingWorkCapacity: conf.PendingWorkCapacity,
blockOnSend: conf.BlockOnSend,
blockOnResponses: conf.BlockOnResponse,
transport: conf.Transport,
responses: c.responses,
logger: conf.Logger,
}
}
if err := c.tx.Start(); err != nil {
c.logger.Printf("transmission client failed to start: %s", err.Error())
return nil, err
}

c.defaultBuilder = &Builder{
WriteKey: conf.WriteKey,
Dataset: conf.Dataset,
SampleRate: conf.SampleRate,
APIHost: conf.APIHost,
dynFields: make([]dynamicField, 0, 0),
fieldHolder: fieldHolder{
data: make(map[string]interface{}),
},
client: c,
}

return c, nil
}

// Close waits for all in-flight messages to be sent. You should
// call Close() before app termination.
func (c *defaultClient) Close() {
c.logger.Printf("closing libhoney client")
if c.tx != nil {
c.tx.Stop()
}
close(c.responses)
}

// Flush closes and reopens the Output interface, ensuring events
// are sent without waiting on the batch to be sent asyncronously.
// Generally, it is more efficient to rely on asyncronous batches than to
// call Flush, but certain scenarios may require Flush if asynchronous sends
// are not guaranteed to run (i.e. running in AWS Lambda)
// Flush is not thread safe - use it only when you are sure that no other
// parts of your program are calling Send
func (c *defaultClient) Flush() {
c.logger.Printf("flushing libhoney client")
if c.tx != nil {
c.tx.Stop()
c.tx.Start()
}
}

// Responses returns the channel from which the caller can read the responses
// to sent events.
func (c *defaultClient) Responses() chan Response {
return c.responses
}

// AddDynamicField takes a field name and a function that will generate values
// for that metric. The function is called once every time a NewEvent() is
// created and added as a field (with name as the key) to the newly created
// event.
func (c *defaultClient) AddDynamicField(name string, fn func() interface{}) error {
return c.defaultBuilder.AddDynamicField(name, fn)
}

// AddField adds a Field to the global scope. This metric will be inherited by
// all builders and events.
func (c *defaultClient) AddField(name string, val interface{}) {
c.defaultBuilder.AddField(name, val)
}

// Add adds its data to the global scope. It adds all fields in a struct or all
// keys in a map as individual Fields. These metrics will be inherited by all
// builders and events.
func (c *defaultClient) Add(data interface{}) error {
return c.defaultBuilder.Add(data)
}

// NewEvent creates a new event prepopulated with any Fields present in the
// global scope.
func (c *defaultClient) NewEvent() *Event {
return c.defaultBuilder.NewEvent()
}

// NewBuilder creates a new event builder. The builder inherits any
// Dynamic or Static Fields present in the global scope.
func (c *defaultClient) NewBuilder() *Builder {
return c.defaultBuilder.Clone()
}

// sendResponse sends a dropped event response down the response channel
func (c *defaultClient) sendDroppedResponse(e *Event, message string) {
r := Response{
Err: errors.New(message),
Metadata: e.Metadata,
}
c.logger.Printf("got response code %d, error %s, and body %s",
r.StatusCode, r.Err, string(r.Body))
writeToResponse(c.responses, r, c.conf.BlockOnResponse)
}
70 changes: 70 additions & 0 deletions client_test.go
@@ -0,0 +1,70 @@
package libhoney

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestClientAdding(t *testing.T) {
b := &Builder{
dynFields: make([]dynamicField, 0, 0),
fieldHolder: fieldHolder{
data: make(map[string]interface{}),
},
}
c := &defaultClient{
defaultBuilder: b,
}

c.AddDynamicField("dynamo", func() interface{} { return nil })
assert.Equal(t, 1, len(b.dynFields), "after adding dynamic field, builder should have it")

c.AddField("pine", 34)
assert.Equal(t, 34, b.data["pine"], "after adding field, builder should have it")

c.Add(map[string]interface{}{"birch": 45})
assert.Equal(t, 45, b.data["birch"], "after adding complex field, builder should have it")

ev := c.NewEvent()
assert.Equal(t, 34, ev.data["pine"], "with default content, created events should be prepopulated")

b2 := c.NewBuilder()
assert.Equal(t, 34, b2.data["pine"], "with default content, cloned builders should be prepopulated")
}

func TestNewClient(t *testing.T) {
conf := Config{
WriteKey: "Oliver",
}
c, err := NewClient(conf)

assert.NoError(t, err, "new client should not error")
assert.Equal(t, "Oliver", c.(*defaultClient).conf.WriteKey, "initialized client should respect config")
assert.Equal(t, "Oliver", c.(*defaultClient).defaultBuilder.WriteKey, "initialized client should respect config")
}

func TestClientIsolated(t *testing.T) {
c1, _ := NewClient(Config{})
c2, _ := NewClient(Config{})

AddField("Mary", 83)
c1.AddField("Ursula", 88)
c2.AddField("Philip", 53)

ed := NewEvent()
assert.Equal(t, 83, ed.data["Mary"], "global libhoney should have global content")
assert.Equal(t, nil, ed.data["Ursula"], "global libhoney should not have client content")
assert.Equal(t, nil, ed.data["Philip"], "global libhoney should not have client content")

e1 := c1.NewEvent()
assert.Equal(t, 88, e1.data["Ursula"], "client events should have client-scoped date")
assert.Equal(t, nil, e1.data["Philip"], "client events should not have other client's content")
assert.Equal(t, nil, e1.data["Mary"], "client events should not have global content")

e2 := c2.NewEvent()
assert.Equal(t, 53, e2.data["Philip"], "client events should have client-scoped data")
assert.Equal(t, nil, e2.data["Ursula"], "client events should not have other client's content")
assert.Equal(t, nil, e2.data["Mary"], "client events should not have global content")

}