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

added general tags that should be attached to all metrics #3

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
### Go template
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so

# Folders
_obj
_test

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out

*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*

_testmain.go

*.exe
*.test
*.prof


### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

*.iml

# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml

# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/dataSources.local.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml

# Gradle:
.idea/gradle.xml
.idea/libraries

# Mongo Explorer plugin:
.idea/mongoSettings.xml

## File-based project format:
*.iws

## Plugin-specific files:

# IntelliJ
/out/
/.idea
*.iml

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
language: go

go:
- 1.11

script:
- go get github.com/go-check/check
- env GO111MODULE=on go test -v -tags .

sudo: false
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[![Build Status](https://travis-ci.org/eSailors/go-datadog.svg?branch=master)](https://travis-ci.org/eSailors/go-datadog)
# Go Datadog
Simple [Go](http://golang.org/) interface to the [Datadog
API](http://docs.datadoghq.com/api/).
Expand All @@ -11,7 +12,7 @@ along the lines of the following:

```go
import(
"github.com/vistarmedia/datadog"
"github.com/esailors/go-datadog"
"os"
"time"
)
Expand Down
118 changes: 81 additions & 37 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
package datadog

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httputil"

"bytes"
"github.com/rcrowley/go-metrics"
"io"
"net/http"
"io/ioutil"
)

const (
Expand All @@ -23,7 +26,7 @@ type Client struct {
}

type seriesMessage struct {
Series []*Series `json:"series,omitempty"`
Series []json.RawMessage `json:"series,omitempty"`
}

type Series struct {
Expand Down Expand Up @@ -64,62 +67,103 @@ func (c *Client) EventUrl() string {
}

func (c *Client) PostEvent(event *Event) (err error) {
bs, err := json.Marshal(event)
if err != nil {
return err
}

resp, err := http.Post(c.EventUrl(), CONTENT_TYPE, bytes.NewBuffer(bs))
body, err := json.Marshal(event)
if err != nil {
return err
}

defer resp.Body.Close()
if !(resp.StatusCode == 200 || resp.StatusCode == 202) {
return fmt.Errorf("Bad Datadog response: '%s'", resp.Status)
}
return
return c.doRequest(c.EventUrl(), body)
}

const messageChunkSize = 2 * 1024 * 1024

// Posts an array of series data to the Datadog API. The API expects an object,
// not an array, so it will be wrapped in a `seriesMessage` with a single
// `series` field.
func (c *Client) PostSeries(series []*Series) (err error) {
body, err := c.seriesReader(series)
if err != nil {
return err
//
// If the slice contains too many series, the message will be split into
// multiple chunks of around 2mb each.
//
func (c *Client) PostSeries(series []Series, reg metrics.Registry) error {
var approxTotalSize int
var encodedSeries []json.RawMessage

for _, serie := range series {
// encode series to json
jsonSerie, err := json.Marshal(serie)
if err != nil {
return err
}

encoded := json.RawMessage(jsonSerie)

// count bytes of this message
approxTotalSize += len(encoded)
encodedSeries = append(encodedSeries, encoded)

if approxTotalSize > messageChunkSize {
if err := c.sendEncodedSeries(encodedSeries); err != nil {
return err
}

// reset and start to collect the next chunk
encodedSeries = encodedSeries[:0]
approxTotalSize = 0
}
}
resp, err := http.Post(c.SeriesUrl(), CONTENT_TYPE, body)
if err != nil {
return err
}
defer resp.Body.Close()
if !(resp.StatusCode == 200 || resp.StatusCode == 202) {
return fmt.Errorf("Bad Datadog response: '%s'", resp.Status)

if len(encodedSeries) > 0 {
return c.sendEncodedSeries(encodedSeries)
}
return

return nil
}

// Serializes an array of `Series` to JSON. The array will be wrapped in a
// `seriesMessage`, changing the serialized type from an array to an object with
// a single `series` field.
func (c *Client) seriesReader(series []*Series) (io.Reader, error) {
msg := &seriesMessage{series}
bs, err := json.Marshal(msg)
func (c *Client) sendEncodedSeries(series []json.RawMessage) error {
body, err := json.Marshal(seriesMessage{series})
if err != nil {
return nil, err
return err
}
return bytes.NewBuffer(bs), nil

return c.doRequest(c.SeriesUrl(), body)
}

// Create a `MetricsReporter` for the given metrics reporter. The returned
// reporter will not be started.
func (c *Client) Reporter(reg metrics.Registry) *MetricsReporter {
return Reporter(c, reg)
func (c *Client) Reporter(reg metrics.Registry, tags []string) *MetricsReporter {
return Reporter(c, reg, tags)
}

// Create a `MetricsReporter` configured to use metric's default registry. This
// reporter will not be started.
func (c *Client) DefaultReporter() *MetricsReporter {
return Reporter(c, metrics.DefaultRegistry)
return Reporter(c, metrics.DefaultRegistry, nil)
}

func (c *Client) doRequest(url string, body []byte) (err error) {
req, err := http.NewRequest("POST", c.SeriesUrl(), bytes.NewReader(body))
if err != nil {
return fmt.Errorf("building request: %s", err)
}

req.ContentLength = int64(len(body))
req.Header.Set("Content-Type", "application/json")

// now execute the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}

// for keep-alive we ensure that the response-body is read.
defer io.Copy(ioutil.Discard, resp.Body)
defer resp.Body.Close()

if resp.StatusCode/100 != 2 {
dumpReq, _ := httputil.DumpRequest(req, false)
dumpRes, _ := httputil.DumpResponse(resp, true)
return fmt.Errorf("bad datadog request and response:\n%s\n%s", string(dumpReq), string(dumpRes))
}

return nil
}
29 changes: 1 addition & 28 deletions client_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package datadog

import (
"io/ioutil"
. "launchpad.net/gocheck"
"strings"
. "github.com/go-check/check"
"testing"
)

Expand All @@ -24,33 +22,8 @@ func (s *ClientSuite) TestSeriesEndpoint(c *C) {
"https://app.datadoghq.com/api/v1/series?api_key=secret")
}

func (s *ClientSuite) TestSingleSeriesReader(c *C) {
series := &Series{
Metric: "foo.bar.baz",
Points: [][2]interface{}{[2]interface{}{1346340794, 66.6}},
Type: "gauge",
Host: "hostname",
Tags: []string{"one", "two", "three"},
}

reader, err := client.seriesReader([]*Series{series})
c.Check(err, IsNil)

b, err := ioutil.ReadAll(reader)
c.Check(err, IsNil)

body := string(b)
c.Check(strings.Index(body, `"metric":"foo.bar.baz"`), Not(Equals), -1)
c.Check(strings.Index(body, `"points":[[1346340794,66.6]]`), Not(Equals), -1)
c.Check(strings.Index(body, `"type":"gauge"`), Not(Equals), -1)
c.Check(strings.Index(body, `"host":"hostname"`), Not(Equals), -1)
c.Check(strings.Index(body, `"tags":["one","two","three"]`), Not(Equals), -1)
}


func (s *ClientSuite) TestEventsEndpoint(c *C) {
client.ApiKey = "secret"
c.Check(client.EventUrl(), Equals,
"https://app.datadoghq.com/api/v1/events?api_key=secret")
}

7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/eSailors/go-datadog

require (
github.com/go-check/check v0.0.0-20180628173108-788fd7840127
github.com/kr/pretty v0.1.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165
)
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 h1:nkcn14uNmFEuGCb2mBZbBb24RdNRL08b/wb+xBOYpuk=
github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
29 changes: 29 additions & 0 deletions metric_name.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package datadog

import (
"errors"
"strings"
)

func NewCheckedMetricName(prefix string, name string, tags string) (string, error) {
if len(name) == 0 {
return "", errors.New("Metric name cannot be empty.")
}

return NewMetricName(prefix, name, tags), nil
}

func NewMetricName(prefix string, name string, tags string) string {
var buffer strings.Builder
if len(prefix) > 0 {
buffer.WriteString(prefix)
buffer.WriteString(".")
}
buffer.WriteString(name)
if len(tags) > 0 {
buffer.WriteString("[")
buffer.WriteString(tags)
buffer.WriteString("]")
}
return buffer.String()
}
32 changes: 32 additions & 0 deletions metric_name_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package datadog

import (
"testing"
)

func TestName(t *testing.T) {
var testData = []struct {
prefix string
name string
tags string
wanted string
}{
{"", "meter", "", "meter"},
{"prefix", "meter", "", "prefix.meter"},
{"prefix", "meter", "tag", "prefix.meter[tag]"},
{"", "meter", "tag", "meter[tag]"},
{"", "meter", "tag:a,tag:b", "meter[tag:a,tag:b]"},
}
for _, metricNameParts := range testData {
name, _ := NewCheckedMetricName(metricNameParts.prefix, metricNameParts.name, metricNameParts.tags)
if want, have := metricNameParts.wanted, name; want != have {
t.Errorf("%s [wanted] != %s [have]", want, have)
}
}
}

func TestMissingName(t *testing.T) {
if _, err := NewCheckedMetricName("", "", ""); err == nil {
t.Error("An empty name must return an error.")
}
}
Loading