From 7bdef3fbe7d7fa886635f20d684c7145a724c30e Mon Sep 17 00:00:00 2001 From: Jonathan Thurman Date: Fri, 7 Aug 2020 16:31:25 -0700 Subject: [PATCH 1/4] feat(events): Add support for sending Events --- go.mod | 4 + telemetry/config.go | 24 ++++- telemetry/config_test.go | 48 ++++++---- telemetry/events.go | 82 ++++++++++++++++ telemetry/events_batch_test.go | 91 ++++++++++++++++++ telemetry/events_integration_test.go | 50 ++++++++++ telemetry/events_test.go | 136 +++++++++++++++++++++++++++ telemetry/harvester.go | 57 +++++++++-- telemetry/harvester_test.go | 17 ++++ telemetry/utilities.go | 13 ++- telemetry/utilities_test.go | 15 +++ 11 files changed, 508 insertions(+), 29 deletions(-) create mode 100644 telemetry/events.go create mode 100644 telemetry/events_batch_test.go create mode 100644 telemetry/events_integration_test.go create mode 100644 telemetry/events_test.go diff --git a/go.mod b/go.mod index 1865a89..0ce1589 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,5 @@ module github.com/newrelic/newrelic-telemetry-sdk-go + +go 1.13 + +require github.com/stretchr/testify v1.6.1 diff --git a/telemetry/config.go b/telemetry/config.go index a075687..d40922d 100644 --- a/telemetry/config.go +++ b/telemetry/config.go @@ -37,14 +37,16 @@ type Config struct { // AuditLogger receives structured log messages that include the // uncompressed data sent to New Relic. Use this to log all data sent. AuditLogger func(map[string]interface{}) - // MetricsURLOverride overrides the metrics endpoint if not not empty. + // MetricsURLOverride overrides the metrics endpoint if not empty. MetricsURLOverride string - // SpansURLOverride overrides the spans endpoint if not not empty. + // SpansURLOverride overrides the spans endpoint if not empty. // // To enable Infinite Tracing on the New Relic Edge, set this field to your // Trace Observer URL. See // https://docs.newrelic.com/docs/understand-dependencies/distributed-tracing/enable-configure/enable-distributed-tracing SpansURLOverride string + // EventsURLOverride overrides the events endpoint if not empty + EventsURLOverride string // Product is added to the User-Agent header. eg. "NewRelic-Go-OpenCensus" Product string // ProductVersion is added to the User-Agent header. eg. "0.1.0". @@ -113,13 +115,21 @@ func ConfigBasicAuditLogger(w io.Writer) func(*Config) { } // ConfigSpansURLOverride sets the Config's SpansURLOverride field which -// overrides the spans endpoint if not not empty. +// overrides the spans endpoint if not empty. func ConfigSpansURLOverride(url string) func(*Config) { return func(cfg *Config) { cfg.SpansURLOverride = url } } +// ConfigEventsURLOverride sets the Config's EventsURLOverride field which +// overrides the events endpoint if not empty. +func ConfigEventsURLOverride(url string) func(*Config) { + return func(cfg *Config) { + cfg.EventsURLOverride = url + } +} + // configTesting is the config function to be used when testing. It sets the // APIKey but disables the harvest goroutine. func configTesting(cfg *Config) { @@ -155,6 +165,7 @@ func (cfg *Config) logAudit(fields map[string]interface{}) { const ( defaultSpanURL = "https://trace-api.newrelic.com/trace/v1" defaultMetricURL = "https://metric-api.newrelic.com/metric/v1" + defaultEventURL = "https://insights-collector.newrelic.com/v1/accounts/events" ) func (cfg *Config) spanURL() string { @@ -171,6 +182,13 @@ func (cfg *Config) metricURL() string { return defaultMetricURL } +func (cfg *Config) eventURL() string { + if cfg.EventsURLOverride != "" { + return cfg.EventsURLOverride + } + return defaultEventURL +} + // userAgent creates the User-Agent header version according to the spec here: // https://github.com/newrelic/newrelic-telemetry-sdk-specs/blob/master/communication.md#user-agent func (cfg *Config) userAgent() string { diff --git a/telemetry/config_test.go b/telemetry/config_test.go index 95bb6b8..eea7ea5 100644 --- a/telemetry/config_test.go +++ b/telemetry/config_test.go @@ -7,6 +7,9 @@ import ( "bytes" "strings" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestConfigAPIKey(t *testing.T) { @@ -119,23 +122,36 @@ func TestConfigMetricURL(t *testing.T) { } } -func TestConfigSpanURL(t *testing.T) { +func TestConfigSpansURL(t *testing.T) { + t.Parallel() + + // We get the default h, err := NewHarvester(configTesting) - if nil == h || err != nil { - t.Fatal(h, err) - } - if u := h.config.spanURL(); u != defaultSpanURL { - t.Fatal(u) - } - h, err = NewHarvester(configTesting, func(cfg *Config) { - cfg.SpansURLOverride = "span-url-override" - }) - if nil == h || err != nil { - t.Fatal(h, err) - } - if u := h.config.spanURL(); u != "span-url-override" { - t.Fatal(u) - } + require.NoError(t, err) + require.NotNil(t, h) + assert.Equal(t, defaultSpanURL, h.config.spanURL()) + + // The override config option works + h, err = NewHarvester(configTesting, ConfigSpansURLOverride("span-url-override")) + require.NoError(t, err) + require.NotNil(t, h) + assert.Equal(t, "span-url-override", h.config.spanURL()) +} + +func TestConfigEventsURL(t *testing.T) { + t.Parallel() + + // We get the default + h, err := NewHarvester(configTesting) + require.NoError(t, err) + require.NotNil(t, h) + assert.Equal(t, defaultEventURL, h.config.eventURL()) + + // The override config option works + h, err = NewHarvester(configTesting, ConfigEventsURLOverride("event-url-override")) + require.NoError(t, err) + require.NotNil(t, h) + assert.Equal(t, "event-url-override", h.config.eventURL()) } func TestConfigUserAgent(t *testing.T) { diff --git a/telemetry/events.go b/telemetry/events.go new file mode 100644 index 0000000..0de0088 --- /dev/null +++ b/telemetry/events.go @@ -0,0 +1,82 @@ +package telemetry + +import ( + "bytes" + "encoding/json" + "time" + + "github.com/newrelic/newrelic-telemetry-sdk-go/internal" +) + +// Event is a unique set of data that happened at a specific point in time +type Event struct { + // Required Fields: + // + // EventType is the name of the event + EventType string + // Timestamp is when this event happened. If Timestamp is not set, it + // will be assigned to time.Now() in Harvester.RecordEvent. + Timestamp time.Time + + // Recommended Fields: + // + // Attributes is a map of user specified data on this event. The map + // values can be any of bool, number, or string. + Attributes map[string]interface{} + // AttributesJSON is a json.RawMessage of attributes for this metric. It + // will only be sent if Attributes is nil. + AttributesJSON json.RawMessage +} + +func (e *Event) writeJSON(buf *bytes.Buffer) { + w := internal.JSONFieldsWriter{Buf: buf} + buf.WriteByte('{') + + w.StringField("eventType", e.EventType) + w.IntField("timestamp", e.Timestamp.UnixNano()/(1000*1000)) + + internal.AddAttributes(&w, e.Attributes) + + buf.WriteByte('}') +} + +// eventBatch represents a single batch of events to report to New Relic. +type eventBatch struct { + Events []Event +} + +// split will split the eventBatch into 2 equally sized batches. +// If the number of events in the original is 0 or 1 then nil is returned. +func (batch *eventBatch) split() []requestsBuilder { + if len(batch.Events) < 2 { + return nil + } + + half := len(batch.Events) / 2 + b1 := *batch + b1.Events = batch.Events[:half] + b2 := *batch + b2.Events = batch.Events[half:] + + return []requestsBuilder{ + requestsBuilder(&b1), + requestsBuilder(&b2), + } +} + +func (batch *eventBatch) writeJSON(buf *bytes.Buffer) { + buf.WriteByte('[') + for idx, s := range batch.Events { + if idx > 0 { + buf.WriteByte(',') + } + s.writeJSON(buf) + } + buf.WriteByte(']') +} + +func (batch *eventBatch) makeBody() json.RawMessage { + buf := &bytes.Buffer{} + batch.writeJSON(buf) + return buf.Bytes() +} diff --git a/telemetry/events_batch_test.go b/telemetry/events_batch_test.go new file mode 100644 index 0000000..275bddb --- /dev/null +++ b/telemetry/events_batch_test.go @@ -0,0 +1,91 @@ +// Copyright 2019 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +build unit + +package telemetry + +import ( + "io/ioutil" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/newrelic/newrelic-telemetry-sdk-go/internal" +) + +func testEventBatchJSON(t testing.TB, batch *eventBatch, expect string) { + if th, ok := t.(interface{ Helper() }); ok { + th.Helper() + } + reqs, err := newRequests(batch, "apiKey", defaultEventURL, "userAgent") + require.NoError(t, err) + require.Equal(t, 1, len(reqs)) + + req := reqs[0] + assert.Equal(t, compactJSONString(expect), string(req.UncompressedBody)) + + body, err := ioutil.ReadAll(req.Request.Body) + req.Request.Body.Close() + require.NoError(t, err) + + assert.Equal(t, req.compressedBodyLength, len(body)) + + uncompressed, err := internal.Uncompress(body) + require.NoError(t, err) + assert.Equal(t, string(req.UncompressedBody), string(uncompressed)) +} + +func TestEventsPayloadSplit(t *testing.T) { + t.Parallel() + + // test len 0 + ev := &eventBatch{} + split := ev.split() + assert.Nil(t, split) + + // test len 1 + ev = &eventBatch{Events: []Event{{EventType: "a"}}} + split = ev.split() + assert.Nil(t, split) + + // test len 2 + ev = &eventBatch{Events: []Event{{EventType: "a"}, {EventType: "b"}}} + split = ev.split() + assert.Equal(t, 2, len(split)) + + testEventBatchJSON(t, split[0].(*eventBatch), `[{"eventType":"a","timestamp":-6795364578871}]`) + testEventBatchJSON(t, split[1].(*eventBatch), `[{"eventType":"b","timestamp":-6795364578871}]`) + + // test len 3 + ev = &eventBatch{Events: []Event{{EventType: "a"}, {EventType: "b"}, {EventType: "c"}}} + split = ev.split() + assert.Equal(t, 2, len(split)) + testEventBatchJSON(t, split[0].(*eventBatch), `[{"eventType":"a","timestamp":-6795364578871}]`) + testEventBatchJSON(t, split[1].(*eventBatch), `[{"eventType":"b","timestamp":-6795364578871},{"eventType":"c","timestamp":-6795364578871}]`) +} + +func TestEventsJSON(t *testing.T) { + t.Parallel() + + batch := &eventBatch{Events: []Event{ + {}, // Empty + { // with everything + EventType: "testEvent", + Timestamp: testTimestamp, + Attributes: map[string]interface{}{"zip": "zap"}, + }, + }} + + testEventBatchJSON(t, batch, `[ + { + "eventType":"", + "timestamp":-6795364578871 + }, + { + "eventType":"testEvent", + "timestamp":`+testTimeString+`, + "zip":"zap" + } + ]`) +} diff --git a/telemetry/events_integration_test.go b/telemetry/events_integration_test.go new file mode 100644 index 0000000..81ca9c0 --- /dev/null +++ b/telemetry/events_integration_test.go @@ -0,0 +1,50 @@ +// Copyright 2019 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +build integration + +package telemetry + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEvent(t *testing.T) { + t.Parallel() + + cfg := NewIntegrationTestConfig(t) + + h, err := NewHarvester(cfg) + assert.NoError(t, err) + assert.NotNil(t, h) + + err = h.RecordEvent(Event{ + EventType: "testEvent", + Attributes: map[string]interface{}{ + "zip": "zap", + }, + }) + assert.NoError(t, err) + + h.HarvestNow(context.Background()) +} + +func TestEventBatch(t *testing.T) { + t.Parallel() + + cfg := NewIntegrationTestConfig(t) + + h, err := NewHarvester(cfg) + assert.NoError(t, err) + assert.NotNil(t, h) + + // Batch up a few events + for x := 0; x < 10; x++ { + err = h.RecordEvent(Event{EventType: "testEvent", Attributes: map[string]interface{}{"zip": "zap", "count": x}}) + assert.NoError(t, err) + } + + h.HarvestNow(context.Background()) +} diff --git a/telemetry/events_test.go b/telemetry/events_test.go new file mode 100644 index 0000000..bb99412 --- /dev/null +++ b/telemetry/events_test.go @@ -0,0 +1,136 @@ +// Copyright 2019 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +build unit + +package telemetry + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func testHarvesterEvents(t testing.TB, h *Harvester, expect string) { + reqs := h.swapOutEvents() + if expect != "null" { + require.NotNil(t, reqs) + } + require.Equal(t, 1, len(reqs)) + require.Equal(t, defaultEventURL, reqs[0].Request.URL.String()) + + js := reqs[0].UncompressedBody + actual := string(js) + if th, ok := t.(interface{ Helper() }); ok { + th.Helper() + } + assert.Equal(t, compactJSONString(expect), actual) +} + +func TestEvent(t *testing.T) { + t.Parallel() + + tm := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) + h, err := NewHarvester(configTesting) + assert.NoError(t, err) + assert.NotNil(t, h) + + err = h.RecordEvent(Event{ + EventType: "testEvent", + Timestamp: tm, + Attributes: map[string]interface{}{ + "zip": "zap", + }, + }) + assert.NoError(t, err) + + expect := `[{ + "eventType":"testEvent", + "timestamp":1417136460000, + "zip":"zap" + }]` + + testHarvesterEvents(t, h, expect) +} + +func TestEventInvalidAttribute(t *testing.T) { + t.Parallel() + + tm := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) + h, err := NewHarvester(configTesting) + assert.NoError(t, err) + assert.NotNil(t, h) + + err = h.RecordEvent(Event{ + EventType: "testEvent", + Timestamp: tm, + Attributes: map[string]interface{}{ + "weird-things-get-turned-to-strings": struct{}{}, + "nil-gets-removed": nil, + }, + }) + assert.NoError(t, err) + + expect := `[{ + "eventType":"testEvent", + "timestamp":1417136460000, + "weird-things-get-turned-to-strings":"struct {}" + }]` + + testHarvesterEvents(t, h, expect) +} + +func TestRecordEventZeroTime(t *testing.T) { + t.Parallel() + + h, err := NewHarvester(configTesting) + assert.NoError(t, err) + assert.NotNil(t, h) + + err = h.RecordEvent(Event{ + EventType: "testEvent", + Attributes: map[string]interface{}{ + "zip": "zap", + "zop": 123, + }, + }) + + assert.NoError(t, err) +} +func TestRecordEventEmptyType(t *testing.T) { + t.Parallel() + + tm := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) + h, err := NewHarvester(configTesting) + assert.NoError(t, err) + assert.NotNil(t, h) + + err = h.RecordEvent(Event{ + Timestamp: tm, + Attributes: map[string]interface{}{ + "zip": "zap", + "zop": 123, + }, + }) + + assert.Error(t, err) + assert.Equal(t, errEventTypeUnset, err) +} + +func TestRecordEventNilHarvester(t *testing.T) { + t.Parallel() + + tm := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) + var h *Harvester + err := h.RecordEvent(Event{ + EventType: "testEvent", + Timestamp: tm, + Attributes: map[string]interface{}{ + "zip": "zap", + "zop": 123, + }, + }) + + assert.NoError(t, err) +} diff --git a/telemetry/harvester.go b/telemetry/harvester.go index 678cd57..a236394 100644 --- a/telemetry/harvester.go +++ b/telemetry/harvester.go @@ -31,6 +31,7 @@ type Harvester struct { rawMetrics []Metric aggregatedMetrics map[metricIdentity]*metric spans []Span + events []Event } const ( @@ -88,6 +89,7 @@ func NewHarvester(options ...func(*Config)) (*Harvester, error) { "harvest-period-seconds": h.config.HarvestPeriod.Seconds(), "metrics-url-override": h.config.MetricsURLOverride, "spans-url-override": h.config.SpansURLOverride, + "events-url-override": h.config.EventsURLOverride, "version": version, }) @@ -99,8 +101,9 @@ func NewHarvester(options ...func(*Config)) (*Harvester, error) { } var ( - errSpanIDUnset = errors.New("span id must be set") - errTraceIDUnset = errors.New("trace id must be set") + errSpanIDUnset = errors.New("span id must be set") + errTraceIDUnset = errors.New("trace id must be set") + errEventTypeUnset = errors.New("eventType must be set") ) // RecordSpan records the given span. @@ -145,6 +148,25 @@ func (h *Harvester) RecordMetric(m Metric) { h.rawMetrics = append(h.rawMetrics, m) } +// RecordEvent records the given event. +func (h *Harvester) RecordEvent(e Event) error { + if nil == h { + return nil + } + if "" == e.EventType { + return errEventTypeUnset + } + if e.Timestamp.IsZero() { + e.Timestamp = time.Now() + } + + h.lock.Lock() + defer h.lock.Unlock() + + h.events = append(h.events, e) + return nil +} + type response struct { statusCode int body []byte @@ -276,6 +298,29 @@ func (h *Harvester) swapOutSpans() []request { return reqs } +func (h *Harvester) swapOutEvents() []request { + h.lock.Lock() + events := h.events + h.events = nil + h.lock.Unlock() + + if nil == events { + return nil + } + batch := &eventBatch{ + Events: events, + } + reqs, err := newRequests(batch, h.config.APIKey, h.config.eventURL(), h.config.userAgent()) + if nil != err { + h.config.logError(map[string]interface{}{ + "err": err.Error(), + "message": "error creating requests for events", + }) + return nil + } + return reqs +} + func harvestRequest(req request, cfg *Config) { var attempts int for { @@ -343,6 +388,7 @@ func (h *Harvester) HarvestNow(ct context.Context) { var reqs []request reqs = append(reqs, h.swapOutMetrics(time.Now())...) reqs = append(reqs, h.swapOutSpans()...) + reqs = append(reqs, h.swapOutEvents()...) for _, req := range reqs { req.Request = req.Request.WithContext(ctx) @@ -363,13 +409,6 @@ func (h *Harvester) HarvestNow(ct context.Context) { } } -func minDuration(d1, d2 time.Duration) time.Duration { - if d1 < d2 { - return d1 - } - return d2 -} - func harvestRoutine(h *Harvester) { // Introduce a small jitter to ensure the backend isn't hammered if many // harvesters start at once. diff --git a/telemetry/harvester_test.go b/telemetry/harvester_test.go index e6865db..511e84d 100644 --- a/telemetry/harvester_test.go +++ b/telemetry/harvester_test.go @@ -18,6 +18,7 @@ import ( "time" "github.com/newrelic/newrelic-telemetry-sdk-go/internal" + "github.com/stretchr/testify/assert" ) // compactJSONString removes the whitespace from a JSON string. This function @@ -51,6 +52,22 @@ func TestNilHarvesterRecordSpan(t *testing.T) { }) } +func TestHarvesterRecordSpan(t *testing.T) { + t.Parallel() + + var h *Harvester + + err := h.RecordEvent(Event{ + EventType: "testEvent", + Timestamp: time.Now(), + Attributes: map[string]interface{}{ + "testName": "TestHarvesterRecordSpan", + }, + }) + + assert.NoError(t, err) +} + func TestHarvestErrorLogger(t *testing.T) { err := map[string]interface{}{} diff --git a/telemetry/utilities.go b/telemetry/utilities.go index 1ec2ede..d40097d 100644 --- a/telemetry/utilities.go +++ b/telemetry/utilities.go @@ -3,7 +3,10 @@ package telemetry -import "encoding/json" +import ( + "encoding/json" + "time" +) // jsonOrString returns its input as a jsonString if it is valid JSON, and as a // string otherwise. @@ -26,3 +29,11 @@ func (js jsonString) MarshalJSON() ([]byte, error) { } return []byte(js), nil } + +// minDuration returns the smaller of the two durations +func minDuration(d1, d2 time.Duration) time.Duration { + if d1 < d2 { + return d1 + } + return d2 +} diff --git a/telemetry/utilities_test.go b/telemetry/utilities_test.go index 55e866f..1f9413d 100644 --- a/telemetry/utilities_test.go +++ b/telemetry/utilities_test.go @@ -7,6 +7,9 @@ import ( "encoding/json" "fmt" "testing" + "time" + + "github.com/stretchr/testify/assert" ) func TestJSONString(t *testing.T) { @@ -53,3 +56,15 @@ func TestJSONOrString(t *testing.T) { t.Error(s) } } + +func TestMinDuration(t *testing.T) { + t.Parallel() + + t1 := time.Duration(1) + t2 := time.Duration(5) + + assert.Equal(t, t1, minDuration(t1, t2)) + assert.Equal(t, t1, minDuration(t1, t1)) + assert.Equal(t, t1, minDuration(t2, t1)) + assert.Equal(t, t2, minDuration(t2, t2)) +} From 6bb2159f421e4cadce501733868433460228bc04 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 18 Nov 2020 13:07:13 -0800 Subject: [PATCH 2/4] Refactor span events to use Event struct from events.go --- examples/server/main.go | 2 +- telemetry/spans.go | 15 +-------------- telemetry/spans_test.go | 6 ++---- 3 files changed, 4 insertions(+), 19 deletions(-) diff --git a/examples/server/main.go b/examples/server/main.go index 58efe16..61b3092 100644 --- a/examples/server/main.go +++ b/examples/server/main.go @@ -97,7 +97,7 @@ func wrapHandler(path string, handler func(http.ResponseWriter, *http.Request)) }, Events: []telemetry.Event{ telemetry.Event{ - Name: "exception", + EventType: "exception", Timestamp: before, Attributes: map[string]interface{}{ "exception.message": "Everything is fine!", diff --git a/telemetry/spans.go b/telemetry/spans.go index 5fac3a3..2b8c9be 100644 --- a/telemetry/spans.go +++ b/telemetry/spans.go @@ -87,7 +87,7 @@ func (s *Span) writeJSON(buf *bytes.Buffer) { } buf.WriteByte('{') aw := internal.JSONFieldsWriter{Buf: buf} - aw.StringField("name", e.Name) + aw.StringField("name", e.EventType) aw.IntField("timestamp", e.Timestamp.UnixNano()/(1000*1000)) aw.AddKey("attributes") buf.WriteByte('{') @@ -102,19 +102,6 @@ func (s *Span) writeJSON(buf *bytes.Buffer) { buf.WriteByte('}') } -// Event represents something that occurred during the execution of a span. -type Event struct { - // Name is the identifier of an Event. - Name string - // Timestamp is when the event occurred. It should be after the Timestamp of its - // containing Span, and before the containing spans Timestamp + Duration. - Timestamp time.Time - - // Attributes is a map of user specified tags on this event. The map values - // can be aany of bool, number, or string. - Attributes map[string]interface{} -} - // spanBatch represents a single batch of spans to report to New Relic. type spanBatch struct { // AttributesJSON is a json.RawMessage of attributes to apply to all diff --git a/telemetry/spans_test.go b/telemetry/spans_test.go index f803177..eee91a9 100644 --- a/telemetry/spans_test.go +++ b/telemetry/spans_test.go @@ -162,11 +162,10 @@ func TestSpanWithEvents(t *testing.T) { Attributes: map[string]interface{}{}, Events: []Event{ Event{ - Name: "exception", + EventType: "exception", Timestamp: tm, Attributes: map[string]interface{}{ "exception.message": "Everything is fine!", - "exception.type": "java.lang.EverythingIsFine", }, }, }, @@ -186,8 +185,7 @@ func TestSpanWithEvents(t *testing.T) { "name": "exception", "timestamp": 1417136460000, "attributes": { - "exception.message": "Everything is fine!", - "exception.type": "java.lang.EverythingIsFine" + "exception.message": "Everything is fine!" } } ] From 16d0ef23364fd10a08029e9ee2016f46afa9e7da Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 18 Nov 2020 13:24:49 -0800 Subject: [PATCH 3/4] Add example custom event to examples/server/main.go --- examples/server/main.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/examples/server/main.go b/examples/server/main.go index 61b3092..39050f1 100644 --- a/examples/server/main.go +++ b/examples/server/main.go @@ -106,6 +106,17 @@ func wrapHandler(path string, handler func(http.ResponseWriter, *http.Request)) }, }, }) + + h.RecordEvent(telemetry.Event{ + EventType: "CustomEvent", + Timestamp: before, + Attributes: map[string]interface{}{ + "path": path, + "http.method": req.Method, + "isWeb": true, + "isExample": true, + }, + }) } } @@ -132,7 +143,7 @@ func main() { rand.Seed(time.Now().UnixNano()) var err error h, err = telemetry.NewHarvester( - telemetry.ConfigAPIKey(mustGetEnv("NEW_RELIC_INSIGHTS_INSERT_API_KEY")), + telemetry.ConfigAPIKey(mustGetEnv("NEW_RELIC_INSERT_API_KEY")), telemetry.ConfigCommonAttributes(map[string]interface{}{ "app.name": "myServer", "host.name": "dev.server.com", @@ -141,8 +152,9 @@ func main() { telemetry.ConfigBasicErrorLogger(os.Stderr), telemetry.ConfigBasicDebugLogger(os.Stdout), func(cfg *telemetry.Config) { - cfg.MetricsURLOverride = os.Getenv("NEW_RELIC_METRICS_URL") - cfg.SpansURLOverride = os.Getenv("NEW_RELIC_SPANS_URL") + cfg.MetricsURLOverride = os.Getenv("NEW_RELIC_METRIC_URL") + cfg.SpansURLOverride = os.Getenv("NEW_RELIC_TRACE_URL") + cfg.EventsURLOverride = os.Getenv("NEW_RELIC_EVENT_URL") }, ) if nil != err { From c2401adf693fc4697ca3e56c107b4d8ae9f93033 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 18 Nov 2020 14:35:23 -0800 Subject: [PATCH 4/4] Remove testify dependency --- go.mod | 2 - telemetry/config_test.go | 39 +++++++++------- telemetry/events_batch_test.go | 61 ++++++++++++++++--------- telemetry/events_integration_test.go | 50 --------------------- telemetry/events_test.go | 66 ++++++++++++++++++---------- telemetry/harvester_test.go | 5 ++- telemetry/utilities_test.go | 16 ++++--- 7 files changed, 120 insertions(+), 119 deletions(-) delete mode 100644 telemetry/events_integration_test.go diff --git a/go.mod b/go.mod index 0ce1589..038f583 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module github.com/newrelic/newrelic-telemetry-sdk-go go 1.13 - -require github.com/stretchr/testify v1.6.1 diff --git a/telemetry/config_test.go b/telemetry/config_test.go index eea7ea5..92db3ac 100644 --- a/telemetry/config_test.go +++ b/telemetry/config_test.go @@ -7,9 +7,6 @@ import ( "bytes" "strings" "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestConfigAPIKey(t *testing.T) { @@ -127,15 +124,21 @@ func TestConfigSpansURL(t *testing.T) { // We get the default h, err := NewHarvester(configTesting) - require.NoError(t, err) - require.NotNil(t, h) - assert.Equal(t, defaultSpanURL, h.config.spanURL()) + if nil == h || err != nil { + t.Fatal(h, err) + } + if u := h.config.spanURL(); u != defaultSpanURL { + t.Fatal(u) + } // The override config option works h, err = NewHarvester(configTesting, ConfigSpansURLOverride("span-url-override")) - require.NoError(t, err) - require.NotNil(t, h) - assert.Equal(t, "span-url-override", h.config.spanURL()) + if nil == h || err != nil { + t.Fatal(h, err) + } + if u := h.config.spanURL(); u != "span-url-override" { + t.Fatal(u) + } } func TestConfigEventsURL(t *testing.T) { @@ -143,15 +146,21 @@ func TestConfigEventsURL(t *testing.T) { // We get the default h, err := NewHarvester(configTesting) - require.NoError(t, err) - require.NotNil(t, h) - assert.Equal(t, defaultEventURL, h.config.eventURL()) + if nil == h || err != nil { + t.Fatal(h, err) + } + if u := h.config.eventURL(); u != defaultEventURL { + t.Fatal(u) + } // The override config option works h, err = NewHarvester(configTesting, ConfigEventsURLOverride("event-url-override")) - require.NoError(t, err) - require.NotNil(t, h) - assert.Equal(t, "event-url-override", h.config.eventURL()) + if nil == h || err != nil { + t.Fatal(h, err) + } + if u := h.config.eventURL(); u != "event-url-override" { + t.Fatal(u) + } } func TestConfigUserAgent(t *testing.T) { diff --git a/telemetry/events_batch_test.go b/telemetry/events_batch_test.go index 275bddb..b3e533f 100644 --- a/telemetry/events_batch_test.go +++ b/telemetry/events_batch_test.go @@ -1,15 +1,12 @@ // Copyright 2019 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -// +build unit package telemetry import ( "io/ioutil" "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "time" "github.com/newrelic/newrelic-telemetry-sdk-go/internal" ) @@ -18,22 +15,36 @@ func testEventBatchJSON(t testing.TB, batch *eventBatch, expect string) { if th, ok := t.(interface{ Helper() }); ok { th.Helper() } - reqs, err := newRequests(batch, "apiKey", defaultEventURL, "userAgent") - require.NoError(t, err) - require.Equal(t, 1, len(reqs)) - + reqs, err := newRequests(batch, "apiKey", defaultSpanURL, "userAgent") + if nil != err { + t.Fatal(err) + } + if len(reqs) != 1 { + t.Fatal(reqs) + } req := reqs[0] - assert.Equal(t, compactJSONString(expect), string(req.UncompressedBody)) + actual := string(req.UncompressedBody) + compact := compactJSONString(expect) + if actual != compact { + t.Errorf("\nexpect=%s\nactual=%s\n", compact, actual) + } body, err := ioutil.ReadAll(req.Request.Body) req.Request.Body.Close() - require.NoError(t, err) - - assert.Equal(t, req.compressedBodyLength, len(body)) - + if err != nil { + t.Fatal("unable to read body", err) + } + if len(body) != req.compressedBodyLength { + t.Error("compressed body length mismatch", + len(body), req.compressedBodyLength) + } uncompressed, err := internal.Uncompress(body) - require.NoError(t, err) - assert.Equal(t, string(req.UncompressedBody), string(uncompressed)) + if err != nil { + t.Fatal("unable to uncompress body", err) + } + if string(uncompressed) != string(req.UncompressedBody) { + t.Error("request JSON mismatch", string(uncompressed), string(req.UncompressedBody)) + } } func TestEventsPayloadSplit(t *testing.T) { @@ -42,17 +53,23 @@ func TestEventsPayloadSplit(t *testing.T) { // test len 0 ev := &eventBatch{} split := ev.split() - assert.Nil(t, split) + if split != nil { + t.Error(split) + } // test len 1 ev = &eventBatch{Events: []Event{{EventType: "a"}}} split = ev.split() - assert.Nil(t, split) + if split != nil { + t.Error(split) + } // test len 2 ev = &eventBatch{Events: []Event{{EventType: "a"}, {EventType: "b"}}} split = ev.split() - assert.Equal(t, 2, len(split)) + if len(split) != 2 { + t.Error("split into incorrect number of slices", len(split)) + } testEventBatchJSON(t, split[0].(*eventBatch), `[{"eventType":"a","timestamp":-6795364578871}]`) testEventBatchJSON(t, split[1].(*eventBatch), `[{"eventType":"b","timestamp":-6795364578871}]`) @@ -60,7 +77,9 @@ func TestEventsPayloadSplit(t *testing.T) { // test len 3 ev = &eventBatch{Events: []Event{{EventType: "a"}, {EventType: "b"}, {EventType: "c"}}} split = ev.split() - assert.Equal(t, 2, len(split)) + if len(split) != 2 { + t.Error("split into incorrect number of slices", len(split)) + } testEventBatchJSON(t, split[0].(*eventBatch), `[{"eventType":"a","timestamp":-6795364578871}]`) testEventBatchJSON(t, split[1].(*eventBatch), `[{"eventType":"b","timestamp":-6795364578871},{"eventType":"c","timestamp":-6795364578871}]`) } @@ -72,7 +91,7 @@ func TestEventsJSON(t *testing.T) { {}, // Empty { // with everything EventType: "testEvent", - Timestamp: testTimestamp, + Timestamp: time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC), Attributes: map[string]interface{}{"zip": "zap"}, }, }} @@ -84,7 +103,7 @@ func TestEventsJSON(t *testing.T) { }, { "eventType":"testEvent", - "timestamp":`+testTimeString+`, + "timestamp":1417136460000, "zip":"zap" } ]`) diff --git a/telemetry/events_integration_test.go b/telemetry/events_integration_test.go deleted file mode 100644 index 81ca9c0..0000000 --- a/telemetry/events_integration_test.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2019 New Relic Corporation. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 -// +build integration - -package telemetry - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestEvent(t *testing.T) { - t.Parallel() - - cfg := NewIntegrationTestConfig(t) - - h, err := NewHarvester(cfg) - assert.NoError(t, err) - assert.NotNil(t, h) - - err = h.RecordEvent(Event{ - EventType: "testEvent", - Attributes: map[string]interface{}{ - "zip": "zap", - }, - }) - assert.NoError(t, err) - - h.HarvestNow(context.Background()) -} - -func TestEventBatch(t *testing.T) { - t.Parallel() - - cfg := NewIntegrationTestConfig(t) - - h, err := NewHarvester(cfg) - assert.NoError(t, err) - assert.NotNil(t, h) - - // Batch up a few events - for x := 0; x < 10; x++ { - err = h.RecordEvent(Event{EventType: "testEvent", Attributes: map[string]interface{}{"zip": "zap", "count": x}}) - assert.NoError(t, err) - } - - h.HarvestNow(context.Background()) -} diff --git a/telemetry/events_test.go b/telemetry/events_test.go index bb99412..c29a73e 100644 --- a/telemetry/events_test.go +++ b/telemetry/events_test.go @@ -1,31 +1,38 @@ // Copyright 2019 New Relic Corporation. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -// +build unit package telemetry import ( "testing" "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func testHarvesterEvents(t testing.TB, h *Harvester, expect string) { reqs := h.swapOutEvents() - if expect != "null" { - require.NotNil(t, reqs) + if nil == reqs { + if expect != "null" { + t.Error("nil spans", expect) + } + return + } + + if len(reqs) != 1 { + t.Fatal(reqs) + } + if u := reqs[0].Request.URL.String(); u != defaultEventURL { + t.Fatal(u) } - require.Equal(t, 1, len(reqs)) - require.Equal(t, defaultEventURL, reqs[0].Request.URL.String()) js := reqs[0].UncompressedBody actual := string(js) if th, ok := t.(interface{ Helper() }); ok { th.Helper() } - assert.Equal(t, compactJSONString(expect), actual) + compactExpect := compactJSONString(expect) + if compactExpect != actual { + t.Errorf("\nexpect=%s\nactual=%s\n", compactExpect, actual) + } } func TestEvent(t *testing.T) { @@ -33,8 +40,9 @@ func TestEvent(t *testing.T) { tm := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) h, err := NewHarvester(configTesting) - assert.NoError(t, err) - assert.NotNil(t, h) + if nil == h || err != nil { + t.Fatal(h, err) + } err = h.RecordEvent(Event{ EventType: "testEvent", @@ -43,7 +51,9 @@ func TestEvent(t *testing.T) { "zip": "zap", }, }) - assert.NoError(t, err) + if err != nil { + t.Fatal(err) + } expect := `[{ "eventType":"testEvent", @@ -59,8 +69,9 @@ func TestEventInvalidAttribute(t *testing.T) { tm := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) h, err := NewHarvester(configTesting) - assert.NoError(t, err) - assert.NotNil(t, h) + if nil == h || err != nil { + t.Fatal(h, err) + } err = h.RecordEvent(Event{ EventType: "testEvent", @@ -70,7 +81,9 @@ func TestEventInvalidAttribute(t *testing.T) { "nil-gets-removed": nil, }, }) - assert.NoError(t, err) + if err != nil { + t.Fatal(err) + } expect := `[{ "eventType":"testEvent", @@ -85,8 +98,9 @@ func TestRecordEventZeroTime(t *testing.T) { t.Parallel() h, err := NewHarvester(configTesting) - assert.NoError(t, err) - assert.NotNil(t, h) + if nil == h || err != nil { + t.Fatal(h, err) + } err = h.RecordEvent(Event{ EventType: "testEvent", @@ -96,15 +110,18 @@ func TestRecordEventZeroTime(t *testing.T) { }, }) - assert.NoError(t, err) + if err != nil { + t.Fatal(err) + } } func TestRecordEventEmptyType(t *testing.T) { t.Parallel() tm := time.Date(2014, time.November, 28, 1, 1, 0, 0, time.UTC) h, err := NewHarvester(configTesting) - assert.NoError(t, err) - assert.NotNil(t, h) + if nil == h || err != nil { + t.Fatal(h, err) + } err = h.RecordEvent(Event{ Timestamp: tm, @@ -114,8 +131,9 @@ func TestRecordEventEmptyType(t *testing.T) { }, }) - assert.Error(t, err) - assert.Equal(t, errEventTypeUnset, err) + if err != errEventTypeUnset { + t.Fatal(h, err) + } } func TestRecordEventNilHarvester(t *testing.T) { @@ -132,5 +150,7 @@ func TestRecordEventNilHarvester(t *testing.T) { }, }) - assert.NoError(t, err) + if err != nil { + t.Fatal(err) + } } diff --git a/telemetry/harvester_test.go b/telemetry/harvester_test.go index 511e84d..efb051d 100644 --- a/telemetry/harvester_test.go +++ b/telemetry/harvester_test.go @@ -18,7 +18,6 @@ import ( "time" "github.com/newrelic/newrelic-telemetry-sdk-go/internal" - "github.com/stretchr/testify/assert" ) // compactJSONString removes the whitespace from a JSON string. This function @@ -65,7 +64,9 @@ func TestHarvesterRecordSpan(t *testing.T) { }, }) - assert.NoError(t, err) + if err != nil { + t.Fatal(err) + } } func TestHarvestErrorLogger(t *testing.T) { diff --git a/telemetry/utilities_test.go b/telemetry/utilities_test.go index 1f9413d..15edaeb 100644 --- a/telemetry/utilities_test.go +++ b/telemetry/utilities_test.go @@ -8,8 +8,6 @@ import ( "fmt" "testing" "time" - - "github.com/stretchr/testify/assert" ) func TestJSONString(t *testing.T) { @@ -57,14 +55,20 @@ func TestJSONOrString(t *testing.T) { } } +func checkMinDuration(t *testing.T, expected time.Duration, actual time.Duration) { + if expected != actual { + t.Errorf("\nexpect=%s\nactual=%s\n", expected, actual) + } +} + func TestMinDuration(t *testing.T) { t.Parallel() t1 := time.Duration(1) t2 := time.Duration(5) - assert.Equal(t, t1, minDuration(t1, t2)) - assert.Equal(t, t1, minDuration(t1, t1)) - assert.Equal(t, t1, minDuration(t2, t1)) - assert.Equal(t, t2, minDuration(t2, t2)) + checkMinDuration(t, t1, minDuration(t1, t2)) + checkMinDuration(t, t1, minDuration(t1, t1)) + checkMinDuration(t, t1, minDuration(t2, t1)) + checkMinDuration(t, t2, minDuration(t2, t2)) }