diff --git a/carbon/carbon.go b/carbon/carbon.go new file mode 100644 index 0000000..06e9faf --- /dev/null +++ b/carbon/carbon.go @@ -0,0 +1,88 @@ +package carbon + +import ( + "bytes" + "strconv" +) + +var ( + nextPart = []byte(" ") + nextNode = []byte(".") + nextTag = []byte(";") + nextTagValue = []byte("=") + nextDatapoint = []byte("\n") +) + +// Metric is a Graphite metric builder +type Metric struct { + path *bytes.Buffer + tags *bytes.Buffer +} + +// NewMetric returns new Metric +func NewMetric(name string) *Metric { + metric := &Metric{ + path: &bytes.Buffer{}, + tags: &bytes.Buffer{}, + } + metric.path.WriteString(name) + return metric +} + +// AddNode adds a given node to the metric path +// For example NewMetric("first").AddNode("second") +// builds metric with path equal to "first.second" +func (m *Metric) AddNode(node string) *Metric { + m.path.Write(nextNode) + m.path.WriteString(node) + return m +} + +// AddTag puts given tag=value pair into metric tags +// For example NewMetric("first").AddNode("second").AddTag("tag1","tag1Value") +// builds metric with path "first.second" and tags ";tag1=tag1Value" +func (m *Metric) AddTag(name, value string) *Metric { + m.tags.Write(nextTag) + m.tags.WriteString(name) + m.tags.Write(nextTagValue) + m.tags.WriteString(value) + return m +} + +// Bytes returns byte representation of Graphite metric +func (m *Metric) Bytes() []byte { + if m.tags.Len() == 0 { + return m.path.Bytes() + } + return join(m.path.Bytes(), m.tags.Bytes()) +} + +// GetName returns full metric name +func (m *Metric) GetName() string { + return m.path.String() +} + +// NewDatapoint returns Graphite Plaintext record form of datapoint +// Datapoint is a value stored at a timestamp bucket +// If no value is recorded at a particular timestamp bucket in a series, +// the value will be None (null) +// Doc: https://graphite.readthedocs.io/en/latest/terminology.html +func NewDatapoint(metric *Metric, value float64, timestamp int64) []byte { + var ( + valueStr = strconv.FormatFloat(value, 'f', -1, 64) + timestampStr = strconv.FormatInt(timestamp, 10) + ) + return join(metric.Bytes(), nextPart, []byte(valueStr), nextPart, []byte(timestampStr), nextDatapoint) +} + +func join(s ...[]byte) []byte { + n := 0 + for _, v := range s { + n += len(v) + } + b, i := make([]byte, n), 0 + for _, v := range s { + i += copy(b[i:], v) + } + return b +} diff --git a/carbon/carbon_test.go b/carbon/carbon_test.go new file mode 100644 index 0000000..3800265 --- /dev/null +++ b/carbon/carbon_test.go @@ -0,0 +1,109 @@ +package carbon + +import ( + "bytes" + "testing" +) + +func Test_NewMetric(t *testing.T) { + testCases := []struct { + actual *Metric + expectedName string + expectedBytes []byte + expectedDatapoint []byte + }{ + { + actual: NewMetric("metric1"), + expectedName: "metric1", + expectedBytes: []byte("metric1"), + expectedDatapoint: []byte("metric1 3.14159265359 1552503600\n"), + }, + { + actual: NewMetric("metric1").AddNode("foo"), + expectedName: "metric1.foo", + expectedBytes: []byte("metric1.foo"), + expectedDatapoint: []byte("metric1.foo 3.14159265359 1552503600\n"), + }, + { + actual: NewMetric("metric1").AddNode("foo").AddNode("bar"), + expectedName: "metric1.foo.bar", + expectedBytes: []byte("metric1.foo.bar"), + expectedDatapoint: []byte("metric1.foo.bar 3.14159265359 1552503600\n"), + }, + { + actual: NewMetric("metric1").AddNode("foo").AddNode("bar").AddTag("cpu", "cpu1"), + expectedName: "metric1.foo.bar", + expectedBytes: []byte("metric1.foo.bar;cpu=cpu1"), + expectedDatapoint: []byte("metric1.foo.bar;cpu=cpu1 3.14159265359 1552503600\n"), + }, + { + actual: NewMetric("metric1").AddTag("cpu", "cpu1").AddNode("foo").AddNode("bar"), + expectedName: "metric1.foo.bar", + expectedBytes: []byte("metric1.foo.bar;cpu=cpu1"), + expectedDatapoint: []byte("metric1.foo.bar;cpu=cpu1 3.14159265359 1552503600\n"), + }, + { + actual: NewMetric("metric1").AddNode("foo").AddNode("bar").AddTag("cpu", "cpu1").AddTag("dc", "dc1"), + expectedName: "metric1.foo.bar", + expectedBytes: []byte("metric1.foo.bar;cpu=cpu1;dc=dc1"), + expectedDatapoint: []byte("metric1.foo.bar;cpu=cpu1;dc=dc1 3.14159265359 1552503600\n"), + }, + { + actual: NewMetric("metric1").AddTag("cpu", "cpu1").AddTag("dc", "dc1").AddNode("foo").AddNode("bar"), + expectedName: "metric1.foo.bar", + expectedBytes: []byte("metric1.foo.bar;cpu=cpu1;dc=dc1"), + expectedDatapoint: []byte("metric1.foo.bar;cpu=cpu1;dc=dc1 3.14159265359 1552503600\n"), + }, + } + + for _, testCase := range testCases { + const ( + value = 3.14159265359 + timestamp = 1552503600 + ) + var ( + actualName = testCase.actual.GetName() + actualBytes = testCase.actual.Bytes() + actualDatapoint = NewDatapoint(testCase.actual, value, timestamp) + ) + if actualName != testCase.expectedName { + t.Fatalf("expected name: %s\nactual name: %s", testCase.expectedName, actualName) + } + if bytes.Compare(actualBytes, testCase.expectedBytes) != 0 { + t.Fatalf("expected bytes: %s\nactual bytes: %s", testCase.expectedBytes, actualBytes) + } + if bytes.Compare(actualDatapoint, testCase.expectedDatapoint) != 0 { + t.Fatalf("expected datapoint: %s\nactual datapoint: %s", testCase.expectedDatapoint, actualDatapoint) + } + } +} + +func Benchmark_NewDatapointPreCreated(b *testing.B) { + metric := NewMetric("metric1"). + AddNode("foo"). + AddNode("bar"). + AddTag("cpu", "cpu1"). + AddTag("dc", "dc1"). + AddTag("env", "stage"). + AddTag("service", "exporter"). + AddTag("version", "v0.0.1"). + AddTag("git-commit", "0b1a09c") + for i := 0; i < b.N; i++ { + NewDatapoint(metric, 3.14159265359, 1552503600) + } +} + +func Benchmark_NewDatapointOnTheFly(b *testing.B) { + for i := 0; i < b.N; i++ { + metric := NewMetric("metric1"). + AddNode("foo"). + AddNode("bar"). + AddTag("cpu", "cpu1"). + AddTag("dc", "dc1"). + AddTag("env", "stage"). + AddTag("service", "exporter"). + AddTag("version", "v0.0.1"). + AddTag("git-commit", "0b1a09c") + NewDatapoint(metric, 3.14159265359, 1552503600) + } +}