Skip to content

Commit

Permalink
Merge pull request #187 from newrelic/adn/default_attributes_for_events
Browse files Browse the repository at this point in the history
Enable custom attributes for events
  • Loading branch information
varas authored Nov 29, 2019
2 parents a0d0415 + ed8ea85 commit 23e2c90
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 107 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- JMX no-auth connection
- JMX constructor option to provice custom nrjmx tool executable
- JMX full connection URL
- Allow events that contain attributes to be decorated by the entity
customAttributes.

### Changed

Expand Down
46 changes: 46 additions & 0 deletions data/attribute/attribute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package attribute

import "fmt"

const (
// nsAttributeSeparator is the metric attribute key-value separator applied to generate the metric ns.
nsAttributeSeparator = "=="
)

// Attribute represents a metric attribute key-value pair.
type Attribute struct {
Key string
Value string
}

// Attributes list of attributes
type Attributes []Attribute

// Required for Go < v.18, as these do not include sort.Slice

// Len ...
func (a Attributes) Len() int { return len(a) }

// Swap ...
func (a Attributes) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// Less ...
func (a Attributes) Less(i, j int) bool {
if a[i].Key == a[j].Key {
return a[i].Value < a[j].Value
}
return a[i].Key < a[j].Key
}

// Namespace generates the string value of an attribute used to namespace a metric.
func (a *Attribute) Namespace() string {
return fmt.Sprintf("%s%s%s", a.Key, nsAttributeSeparator, a.Value)
}

// Attr creates an attribute aimed to namespace a metric-set.
func Attr(key string, value string) Attribute {
return Attribute{
Key: key,
Value: value,
}
}
18 changes: 16 additions & 2 deletions data/event/event.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package event

import "github.com/newrelic/infra-integrations-sdk/data/attribute"

const (
// NotificationEventCategory category for notification events.
NotificationEventCategory = "notifications"
Expand Down Expand Up @@ -28,8 +30,9 @@ type Event struct {
// New creates a new event.
func New(summary, category string) *Event {
return &Event{
Summary: summary,
Category: category,
Summary: summary,
Category: category,
Attributes: make(map[string]interface{}),
}
}

Expand All @@ -44,3 +47,14 @@ func NewWithAttributes(summary, category string, attributes map[string]interface
e.Attributes = attributes
return e
}

func (e *Event) setAttribute(key string, val interface{}) {
e.Attributes[key] = val
}

// AddCustomAttributes add customAttributes to the Event
func AddCustomAttributes(e *Event, customAttributes []attribute.Attribute) {
for _, attr := range customAttributes {
e.setAttribute(attr.Key, attr.Value)
}
}
19 changes: 18 additions & 1 deletion data/event/event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package event
import (
"testing"

"github.com/newrelic/infra-integrations-sdk/data/attribute"
"github.com/stretchr/testify/assert"
)

Expand All @@ -18,7 +19,7 @@ func TestNewNotification(t *testing.T) {
assert.Equal(t, n.Summary, "summary")
}

func TestNewAttributes(t *testing.T) {
func TestNewEventsWithAttributes(t *testing.T) {
e := NewWithAttributes(
"summary",
"category",
Expand All @@ -29,3 +30,19 @@ func TestNewAttributes(t *testing.T) {
assert.Equal(t, e.Category, "category")
assert.Equal(t, e.Attributes["attrKey"], "attrVal")
}

func TestEventsAddCustomAttributes(t *testing.T) {
e := &Event{
Summary: "summary",
Category: "category",
Attributes: map[string]interface{}{"attrKey": "attrVal"},
}

a := attribute.Attributes{attribute.Attr("clusterName", "my-cluster")}

AddCustomAttributes(e, a)

assert.Equal(t, e.Summary, "summary")
assert.Equal(t, e.Category, "category")
assert.Equal(t, e.Attributes, map[string]interface{}{"attrKey": "attrVal", "clusterName": "my-cluster"})
}
49 changes: 5 additions & 44 deletions data/metric/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,14 @@ import (
"sort"
"strconv"

"github.com/newrelic/infra-integrations-sdk/data/attribute"
"github.com/newrelic/infra-integrations-sdk/persist"
"github.com/pkg/errors"
)

// Attribute represents a metric attribute key-value pair.
type Attribute struct {
Key string
Value string
}

const (
// nsSeparator is the metric namespace separator
nsSeparator = "::"
// nsAttributeSeparator is the metric attribute key-value separator applied to generate the metric ns.
nsAttributeSeparator = "=="
)

// Errors
Expand All @@ -37,12 +30,12 @@ var (
type Set struct {
storer persist.Storer
Metrics map[string]interface{}
nsAttributes []Attribute
nsAttributes []attribute.Attribute
}

// NewSet creates new metrics set, optionally related to a list of attributes. These attributes makes the metric-set unique.
// If related attributes are used, then new attributes are added.
func NewSet(eventType string, storer persist.Storer, attributes ...Attribute) (s *Set) {
func NewSet(eventType string, storer persist.Storer, attributes ...attribute.Attribute) (s *Set) {
s = &Set{
Metrics: make(map[string]interface{}),
storer: storer,
Expand All @@ -59,20 +52,12 @@ func NewSet(eventType string, storer persist.Storer, attributes ...Attribute) (s
}

// AddCustomAttributes add customAttributes to MetricSet
func AddCustomAttributes(metricSet *Set, customAttributes []Attribute) {
func AddCustomAttributes(metricSet *Set, customAttributes []attribute.Attribute) {
for _, attr := range customAttributes {
metricSet.setSetAttribute(attr.Key, attr.Value)
}
}

// Attr creates an attribute aimed to namespace a metric-set.
func Attr(key string, value string) Attribute {
return Attribute{
Key: key,
Value: value,
}
}

// SetMetric adds a metric to the Set object or updates the metric value if the metric already exists.
// It calculates elapsed difference for RATE and DELTA types.
func (ms *Set) SetMetric(name string, value interface{}, sourceType SourceType) (err error) {
Expand Down Expand Up @@ -183,7 +168,7 @@ func (ms *Set) namespace(metricName string) string {
separator := ""

attrs := ms.nsAttributes
sort.Sort(Attributes(attrs))
sort.Sort(attribute.Attributes(attrs))

for _, attr := range attrs {
ns = fmt.Sprintf("%s%s%s", ns, separator, attr.Namespace())
Expand All @@ -193,11 +178,6 @@ func (ms *Set) namespace(metricName string) string {
return fmt.Sprintf("%s%s%s", ns, separator, metricName)
}

// Namespace generates the string value of an attribute used to namespace a metric.
func (a *Attribute) Namespace() string {
return fmt.Sprintf("%s%s%s", a.Key, nsAttributeSeparator, a.Value)
}

// MarshalJSON adapts the internal structure of the metrics Set to the payload that is compliant with the protocol
func (ms *Set) MarshalJSON() ([]byte, error) {
return json.Marshal(ms.Metrics)
Expand All @@ -207,22 +187,3 @@ func (ms *Set) MarshalJSON() ([]byte, error) {
func (ms *Set) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &ms.Metrics)
}

// Required for Go < v.18, as these do not include sort.Slice

// Attributes list of attributes
type Attributes []Attribute

// Len ...
func (a Attributes) Len() int { return len(a) }

// Swap ...
func (a Attributes) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// Less ...
func (a Attributes) Less(i, j int) bool {
if a[i].Key == a[j].Key {
return a[i].Value < a[j].Value
}
return a[i].Key < a[j].Key
}
37 changes: 19 additions & 18 deletions data/metric/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"
"time"

"github.com/newrelic/infra-integrations-sdk/data/attribute"
"github.com/newrelic/infra-integrations-sdk/log"
"github.com/newrelic/infra-integrations-sdk/persist"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -72,7 +73,7 @@ func TestSet_SetMetricCachesRateAndDeltas(t *testing.T) {
for _, sourceType := range []SourceType{DELTA, RATE, PRATE, PDELTA} {
persist.SetNow(growingTime)

ms := NewSet("some-event-type", storer, Attr("k", "v"))
ms := NewSet("some-event-type", storer, attribute.Attr("k", "v"))

for _, tt := range rateAndDeltaTests {
// user should not store different types under the same key
Expand Down Expand Up @@ -115,7 +116,7 @@ func TestSet_SetMetricsRatesAndDeltas(t *testing.T) {

persist.SetNow(growingTime)

ms := NewSet("some-event-type", persist.NewInMemoryStore(), Attr("k", "v"))
ms := NewSet("some-event-type", persist.NewInMemoryStore(), attribute.Attr("k", "v"))

assert.NoError(t, ms.SetMetric("d", tc.firstValue, tc.sourceType))
assert.NoError(t, ms.SetMetric("d", tc.secondValue, tc.sourceType))
Expand All @@ -131,7 +132,7 @@ func TestSet_SetMetricPositivesThrowsOnNegativeValues(t *testing.T) {
ms := NewSet(
"some-event-type",
persist.NewInMemoryStore(),
Attr("k", "v"),
attribute.Attr("k", "v"),
)
assert.NoError(t, ms.SetMetric("d", 5, sourceType))
assert.Error(
Expand Down Expand Up @@ -175,7 +176,7 @@ func TestSet_SetMetric_IncorrectMetricType(t *testing.T) {
}

func TestSet_MarshalJSON(t *testing.T) {
ms := NewSet("some-event-type", persist.NewInMemoryStore(), Attr("k", "v"))
ms := NewSet("some-event-type", persist.NewInMemoryStore(), attribute.Attr("k", "v"))

ms.SetMetric("foo", 1, RATE)
ms.SetMetric("bar", 1, DELTA)
Expand All @@ -192,7 +193,7 @@ func TestSet_MarshalJSON(t *testing.T) {
}

func TestSet_UnmarshalJSON(t *testing.T) {
ms := NewSet("some-event-type", persist.NewInMemoryStore(), Attr("k", "v"))
ms := NewSet("some-event-type", persist.NewInMemoryStore(), attribute.Attr("k", "v"))

err := ms.UnmarshalJSON([]byte(`{"foo":0,"bar":1.5,"quux":"bar"}`))

Expand All @@ -210,7 +211,7 @@ func TestNewSet_FileStore_StoresBetweenRuns(t *testing.T) {
s, err := persist.NewFileStore(storeFile, log.Discard, 1*time.Hour)
assert.NoError(t, err)

set1 := NewSet("type", s, Attr("k", "v"))
set1 := NewSet("type", s, attribute.Attr("k", "v"))

assert.NoError(t, set1.SetMetric("foo", 1, DELTA))

Expand All @@ -219,7 +220,7 @@ func TestNewSet_FileStore_StoresBetweenRuns(t *testing.T) {
s2, err := persist.NewFileStore(storeFile, log.Discard, 1*time.Hour)
assert.NoError(t, err)

set2 := NewSet("type", s2, Attr("k", "v"))
set2 := NewSet("type", s2, attribute.Attr("k", "v"))

assert.NoError(t, set2.SetMetric("foo", 3, DELTA))

Expand All @@ -238,8 +239,8 @@ func TestNewSet_Attr_AddsAttributes(t *testing.T) {
set := NewSet(
"type",
storeWrite,
Attr("pod", "pod-a"),
Attr("node", "node-a"),
attribute.Attr("pod", "pod-a"),
attribute.Attr("node", "node-a"),
)

assert.Equal(t, "pod-a", set.Metrics["pod"])
Expand All @@ -255,9 +256,9 @@ func TestNewSet_Attr_SolvesCacheCollision(t *testing.T) {
storeWrite, err := persist.NewFileStore(storeFile, log.Discard, 1*time.Hour)
assert.NoError(t, err)

ms1 := NewSet("type", storeWrite, Attr("pod", "pod-a"))
ms2 := NewSet("type", storeWrite, Attr("pod", "pod-a"))
ms3 := NewSet("type", storeWrite, Attr("pod", "pod-b"))
ms1 := NewSet("type", storeWrite, attribute.Attr("pod", "pod-a"))
ms2 := NewSet("type", storeWrite, attribute.Attr("pod", "pod-a"))
ms3 := NewSet("type", storeWrite, attribute.Attr("pod", "pod-b"))

assert.NoError(t, ms1.SetMetric("field", 1, DELTA))
assert.NoError(t, ms2.SetMetric("field", 2, DELTA))
Expand All @@ -269,7 +270,7 @@ func TestNewSet_Attr_SolvesCacheCollision(t *testing.T) {
storeRead, err := persist.NewFileStore(storeFile, log.Discard, 1*time.Hour)
assert.NoError(t, err)

msRead := NewSet("type", storeRead, Attr("pod", "pod-a"))
msRead := NewSet("type", storeRead, attribute.Attr("pod", "pod-a"))

// write is required to make data available for read
assert.NoError(t, msRead.SetMetric("field", 10, DELTA))
Expand All @@ -278,16 +279,16 @@ func TestNewSet_Attr_SolvesCacheCollision(t *testing.T) {
}

func TestSet_namespace(t *testing.T) {
s := NewSet("type", persist.NewInMemoryStore(), Attr("k", "v"))
s := NewSet("type", persist.NewInMemoryStore(), attribute.Attr("k", "v"))

assert.Equal(t, fmt.Sprintf("k==v::foo"), s.namespace("foo"))

// several attributed are supported
s = NewSet(
"type",
persist.NewInMemoryStore(),
Attr("k1", "v1"),
Attr("k2", "v2"),
attribute.Attr("k1", "v1"),
attribute.Attr("k2", "v2"),
)

assert.Equal(t, fmt.Sprintf("k1==v1::k2==v2::foo"), s.namespace("foo"))
Expand All @@ -296,8 +297,8 @@ func TestSet_namespace(t *testing.T) {
s = NewSet(
"type",
persist.NewInMemoryStore(),
Attr("k2", "v2"),
Attr("k1", "v1"),
attribute.Attr("k2", "v2"),
attribute.Attr("k1", "v1"),
)

assert.Equal(t, fmt.Sprintf("k1==v1::k2==v2::foo"), s.namespace("foo"))
Expand Down
3 changes: 2 additions & 1 deletion data/metric/set_marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package metric
import (
"testing"

"github.com/newrelic/infra-integrations-sdk/data/attribute"
"github.com/newrelic/infra-integrations-sdk/persist"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -132,7 +133,7 @@ func TestSet_MarshalMetricsMissingOrInvalidTags(t *testing.T) {
}

func newTestSet() *Set {
return NewSet("some-event-type", persist.NewInMemoryStore(), Attr("k", "v"))
return NewSet("some-event-type", persist.NewInMemoryStore(), attribute.Attr("k", "v"))
}

func assertEqualsMetrics(t *testing.T, expected map[string]interface{}, got map[string]interface{}) {
Expand Down
Loading

0 comments on commit 23e2c90

Please sign in to comment.