├── .circleci └── config.yml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── cligraphite ├── cligraphite.go └── cligraphite_test.go ├── govent.go ├── govent_test.go └── graphite ├── graphite.go └── graphite_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | jobs: 4 | build: 5 | docker: 6 | - image: circleci/golang:1.11 7 | working_directory: /go/src/github.com/MediaMath/govent 8 | steps: 9 | - checkout 10 | - run: go get ./... 11 | - run: make test 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # License 2 | By contributing you agree that your contributions will be licensed under the govent project [license](LICENSE). 3 | 4 | # Code Style 5 | 6 | go vet, golint and go fmt should be run on all submitted code contributions. The standard copyright header should be included in all new source files. 7 | 8 | # Tests 9 | 10 | Submissions will be rejected if they cause existing tests to fail. New code contributions are expected to maintain a high standard of test coverage. 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 MediaMath 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of MediaMath nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test golint 2 | 3 | # Copyright 2015 MediaMath . All rights reserved. 4 | # Use of this source code is governed by a BSD-style 5 | # license that can be found in the LICENSE file. 6 | 7 | test: golint 8 | golint ./... 9 | go vet ./... 10 | go test $(TEST_VERBOSITY) ./... 11 | 12 | golint: 13 | go get github.com/golang/lint/golint 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [govent](https://github.com/MediaMath/govent) · [![CircleCI Status](https://circleci.com/gh/MediaMath/govent.svg?style=shield)](https://circleci.com/gh/MediaMath/govent) [![GitHub license](https://img.shields.io/badge/license-BSD3-blue.svg)](https://github.com/MediaMath/govent/blob/master/LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/MediaMath/govent/blob/master/CONTRIBUTING.md) 2 | 3 | ### govent - cli and library for sending events to the graphite events api 4 | 5 | #### Command line utility 6 | To install: 7 | 8 | ```bash 9 | $ go install github.com/MediaMath/govent 10 | ``` 11 | 12 | To use: 13 | 14 | ```bash 15 | $ export GRAPHITE_URL=https://example.com/events/ 16 | $ export GRAPHITE_USER=foo 17 | $ export GRAPHITE_PASSWORD=bar 18 | $ govent --tag go.write.me.an.event.build --what what.aint.no.country "my data is fo realz" 19 | ``` 20 | 21 | #### Go library 22 | 23 | To get: 24 | 25 | ```bash 26 | $ go get github.com/MediaMath/govent/graphite 27 | ``` 28 | 29 | To use: 30 | 31 | ```go 32 | import "github.com/MediaMath/govent/graphite" 33 | g := graphite.New("foo", "bar", "https://example.com/events/") 34 | g.Publish(graphite.NewEvent("what.aint.no.country", "my data is fo realz", "go.write.me.an.event.build")) 35 | ``` 36 | -------------------------------------------------------------------------------- /cligraphite/cligraphite.go: -------------------------------------------------------------------------------- 1 | package cligraphite 2 | 3 | //Copyright 2015 MediaMath . All rights reserved. 4 | //Use of this source code is governed by a BSD-style 5 | //license that can be found in the LICENSE file. 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/MediaMath/govent/graphite" 11 | "gopkg.in/urfave/cli.v1" 12 | ) 13 | 14 | //UserFlag has the username for http authenticated graphite apis 15 | var UserFlag = cli.StringFlag{ 16 | Name: "graphite_user", 17 | Usage: "Authenticate to graphite endpoint with this user.", 18 | EnvVar: "GRAPHITE_USER", 19 | } 20 | 21 | //PasswordFlag has the password for http authenticated graphite apis 22 | var PasswordFlag = cli.StringFlag{ 23 | Name: "graphite_password", 24 | Usage: "Authenticate to graphite endpoint with this password.", 25 | EnvVar: "GRAPHITE_PASSWORD", 26 | } 27 | 28 | //URLFlag is the graphite events endpoint. 29 | var URLFlag = cli.StringFlag{ 30 | Name: "graphite_url", 31 | Usage: "Graphite endpoint to send graphite events to.", 32 | EnvVar: "GRAPHITE_URL", 33 | } 34 | 35 | //Flags is the common graphite cli flags 36 | var Flags = []cli.Flag{UserFlag, PasswordFlag, URLFlag} 37 | 38 | //NewClientFromContext creates a graphite client from the cli context assuming it is using 39 | //the flags in this package 40 | func NewClientFromContext(ctx *cli.Context) (*graphite.Graphite, error) { 41 | url := ctx.String(URLFlag.Name) 42 | if url == "" { 43 | return nil, fmt.Errorf("%s is required", URLFlag.Name) 44 | } 45 | 46 | return graphite.New(ctx.String(UserFlag.Name), ctx.String(PasswordFlag.Name), url), nil 47 | } 48 | -------------------------------------------------------------------------------- /cligraphite/cligraphite_test.go: -------------------------------------------------------------------------------- 1 | package cligraphite 2 | 3 | //Copyright 2015 MediaMath . All rights reserved. 4 | //Use of this source code is governed by a BSD-style 5 | //license that can be found in the LICENSE file. 6 | 7 | import ( 8 | "flag" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | 13 | "github.com/MediaMath/govent/graphite" 14 | "gopkg.in/urfave/cli.v1" 15 | ) 16 | 17 | func TestURLRequired(t *testing.T) { 18 | set := flag.NewFlagSet("test", 0) 19 | _, err := NewClientFromContext(cli.NewContext(nil, set, nil)) 20 | 21 | if err == nil { 22 | t.Fatal(err) 23 | } 24 | 25 | set.String("graphite_url", "http://example.com", "") 26 | _, err = NewClientFromContext(cli.NewContext(nil, set, nil)) 27 | 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | } 32 | 33 | func TestClientFromContext(t *testing.T) { 34 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 35 | if r.Header.Get("Authorization") == "" { 36 | http.Error(w, "Auth Req", 401) 37 | } 38 | })) 39 | defer ts.Close() 40 | 41 | set := flag.NewFlagSet("test", 0) 42 | set.String("graphite_url", ts.URL, "") 43 | set.String("graphite_user", "foo", "") 44 | set.String("graphite_password", "bar", "") 45 | 46 | g, err := NewClientFromContext(cli.NewContext(nil, set, nil)) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | err = g.Publish(graphite.NewEvent("boo", "yeah", "boy")) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /govent.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //Copyright 2015 MediaMath . All rights reserved. 4 | //Use of this source code is governed by a BSD-style 5 | //license that can be found in the LICENSE file. 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | "os" 11 | 12 | "github.com/MediaMath/govent/cligraphite" 13 | "github.com/MediaMath/govent/graphite" 14 | "gopkg.in/urfave/cli.v1" 15 | ) 16 | 17 | var ( 18 | whatFlag = cli.StringFlag{ 19 | Name: "what", 20 | Usage: "The 'What' field in the event.", 21 | EnvVar: "GOVENT_WHAT", 22 | } 23 | 24 | tagsFlag = cli.StringSliceFlag{ 25 | Name: "tag", 26 | Usage: "The 'Tag' field in the event.", 27 | EnvVar: "GOVENT_TAGS", 28 | } 29 | ) 30 | 31 | func main() { 32 | app := cli.NewApp() 33 | app.Name = "govent" 34 | app.Usage = "send events to the graphite api" 35 | app.Flags = append(cligraphite.Flags, whatFlag, tagsFlag) 36 | 37 | app.Action = func(ctx *cli.Context) { 38 | event, err := eventFromCtx(ctx) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | client, err := cligraphite.NewClientFromContext(ctx) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | err = client.Publish(event) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | } 53 | 54 | app.Run(os.Args) 55 | } 56 | 57 | func eventFromCtx(ctx *cli.Context) (*graphite.Event, error) { 58 | tags := ctx.StringSlice(tagsFlag.Name) 59 | log.Printf("%v", tags) 60 | if len(tags) < 1 { 61 | return nil, fmt.Errorf("%s is required", tagsFlag.Name) 62 | } 63 | 64 | what := ctx.String(whatFlag.Name) 65 | if len(tags) != 1 && what == "" { 66 | return nil, fmt.Errorf("%s is required if multiple tags are used", whatFlag.Name) 67 | } else if what == "" { 68 | what = tags[0] 69 | } 70 | 71 | if len(ctx.Args()) != 1 { 72 | return nil, fmt.Errorf("Must provide data to post") 73 | } 74 | data := ctx.Args()[0] 75 | 76 | return graphite.NewEvent(what, data, tags...), nil 77 | } 78 | -------------------------------------------------------------------------------- /govent_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //Copyright 2015 MediaMath . All rights reserved. 4 | //Use of this source code is governed by a BSD-style 5 | //license that can be found in the LICENSE file. 6 | 7 | import ( 8 | "flag" 9 | "testing" 10 | 11 | "gopkg.in/urfave/cli.v1" 12 | ) 13 | 14 | func TestWhatRequired(t *testing.T) { 15 | set := flag.NewFlagSet("test", 0) 16 | s := cli.StringSlice([]string{"fo", "sho"}) 17 | set.Var(&s, "tag", "boom") 18 | ctx := cli.NewContext(nil, set, nil) 19 | set.Parse([]string{"data"}) 20 | 21 | _, err := eventFromCtx(ctx) 22 | if err == nil { 23 | t.Fatal("Should have failed") 24 | } 25 | 26 | set.String("what", "it", "is") 27 | 28 | _, err = eventFromCtx(ctx) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | } 33 | 34 | func TestDataRequired(t *testing.T) { 35 | set := flag.NewFlagSet("test", 0) 36 | s := cli.StringSlice([]string{"fo", "sho"}) 37 | set.Var(&s, "tag", "boom") 38 | set.String("what", "it", "is") 39 | 40 | ctx := cli.NewContext(nil, set, nil) 41 | 42 | _, err := eventFromCtx(ctx) 43 | if err == nil { 44 | t.Fatal("Should have failed") 45 | } 46 | 47 | set.Parse([]string{"data"}) 48 | 49 | _, err = eventFromCtx(ctx) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | } 54 | 55 | func TestEventUsesTagIfOnlyOneTagDefinedAndNoWhat(t *testing.T) { 56 | set := flag.NewFlagSet("test", 0) 57 | s := cli.StringSlice([]string{"fo"}) 58 | set.Var(&s, "tag", "boom") 59 | set.Parse([]string{"data"}) 60 | 61 | ctx := cli.NewContext(nil, set, nil) 62 | 63 | event, err := eventFromCtx(ctx) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | 68 | if event.What != "fo" && event.Tags[0] != "fo" && event.Data != "data" { 69 | t.Fatalf("%v", event) 70 | } 71 | 72 | } 73 | 74 | func TestEventFromCtx(t *testing.T) { 75 | set := flag.NewFlagSet("test", 0) 76 | s := cli.StringSlice([]string{"fo", "sho"}) 77 | set.Var(&s, "tag", "boom") 78 | set.String("what", "it", "is") 79 | set.Parse([]string{"data"}) 80 | 81 | ctx := cli.NewContext(nil, set, nil) 82 | 83 | event, err := eventFromCtx(ctx) 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | 88 | if event.What != "it" && event.Tags[0] != "fo" && event.Data != "data" { 89 | t.Fatalf("%v", event) 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /graphite/graphite.go: -------------------------------------------------------------------------------- 1 | package graphite 2 | 3 | //Copyright 2015 MediaMath . All rights reserved. 4 | //Use of this source code is governed by a BSD-style 5 | //license that can be found in the LICENSE file. 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "fmt" 11 | "io/ioutil" 12 | "net/http" 13 | "time" 14 | ) 15 | 16 | //Event to record in graphite 17 | type Event struct { 18 | What string `json:"what"` 19 | Tags []string `json:"tags"` 20 | Data string `json:"data"` 21 | When int64 `json:"when,omitempty"` 22 | } 23 | 24 | //At will set the When field with the appropriately formatted time 25 | func (e *Event) At(t time.Time) *Event { 26 | e.When = t.UTC().Unix() 27 | return e 28 | } 29 | 30 | //NewEvent creates an event with the provided data 31 | func NewEvent(what string, data string, tags ...string) *Event { 32 | return &Event{ 33 | What: what, 34 | Tags: tags, 35 | Data: data, 36 | } 37 | } 38 | 39 | //NewTaggedEvent creates an event with 1 tag and the what is the same as the tag 40 | func NewTaggedEvent(tag string, data string) *Event { 41 | return &Event{ 42 | What: tag, 43 | Tags: []string{tag}, 44 | Data: data, 45 | } 46 | } 47 | 48 | //New creates a new graphite client 49 | func New(username, password, addr string) *Graphite { 50 | return NewVerbose(username, password, addr, true) 51 | } 52 | 53 | //NewVerbose creates a new client with verbosity set 54 | func NewVerbose(username, password, addr string, verbose bool) *Graphite { 55 | return &Graphite{ 56 | Username: username, 57 | Password: password, 58 | Addr: addr, 59 | Client: &http.Client{Timeout: time.Duration(10) * time.Second}, 60 | Verbose: verbose, 61 | Prefix: "", 62 | } 63 | } 64 | 65 | //Graphite is a wrapper around the graphite events API 66 | type Graphite struct { 67 | Username string 68 | Password string 69 | Addr string 70 | Client HTTPClient 71 | Verbose bool 72 | Prefix string 73 | } 74 | 75 | //Publish sends the event to the graphite API 76 | func (g *Graphite) Publish(event *Event) error { 77 | if g.Prefix != "" { 78 | event.What = fmt.Sprintf("%v.%v", g.Prefix, event.What) 79 | 80 | var prefixed []string 81 | for _, tag := range event.Tags { 82 | prefixed = append(prefixed, fmt.Sprintf("%v.%v", g.Prefix, tag)) 83 | } 84 | event.Tags = prefixed 85 | } 86 | 87 | b, err := json.Marshal(event) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | req, err := http.NewRequest("POST", g.Addr, bytes.NewBuffer(b)) 93 | 94 | if err != nil { 95 | return err 96 | } 97 | 98 | if g.Username != "" && g.Password != "" { 99 | req.SetBasicAuth(g.Username, g.Password) 100 | } 101 | 102 | resp, err := g.Client.Do(req) 103 | 104 | if err != nil { 105 | return err 106 | } 107 | 108 | body, err := ioutil.ReadAll(resp.Body) 109 | resp.Body.Close() 110 | 111 | if err != nil { 112 | return err 113 | } 114 | 115 | if resp.StatusCode != 200 { 116 | if g.Verbose { 117 | return fmt.Errorf("%v:%v:%s:%s", g.Addr, resp.StatusCode, body, b) 118 | } 119 | 120 | return fmt.Errorf("%v:%v:%s", g.Addr, resp.StatusCode, b) 121 | } 122 | 123 | return nil 124 | } 125 | 126 | //HTTPClient is any client that can do a http request 127 | type HTTPClient interface { 128 | Do(request *http.Request) (*http.Response, error) 129 | } 130 | -------------------------------------------------------------------------------- /graphite/graphite_test.go: -------------------------------------------------------------------------------- 1 | package graphite 2 | 3 | //Copyright 2015 MediaMath . All rights reserved. 4 | //Use of this source code is governed by a BSD-style 5 | //license that can be found in the LICENSE file. 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "net/http/httptest" 14 | "os" 15 | "testing" 16 | "time" 17 | ) 18 | 19 | func TestIntegrate(t *testing.T) { 20 | username := os.Getenv("GRAPHITE_USER") 21 | password := os.Getenv("GRAPHITE_PASSWORD") 22 | url := os.Getenv("GRAPHITE_URL") 23 | 24 | if testing.Short() { 25 | t.Skipf("skipped because is an integration test") 26 | } 27 | 28 | if username == "" || password == "" || url == "" { 29 | t.Skipf("skipped because missing creds") 30 | } 31 | 32 | now := time.Now() 33 | event := NewTaggedEvent("com.mediamath.govent.test", "boom") 34 | event.At(now) 35 | 36 | graphite := New(username, password, url) 37 | 38 | err := graphite.Publish(event) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | graphite2 := &Graphite{ 44 | Username: username, 45 | Password: password, 46 | Verbose: false, 47 | Prefix: "com.mediamath.govent", 48 | Addr: url, 49 | Client: &http.Client{Timeout: time.Second * 10}, 50 | } 51 | 52 | event = NewTaggedEvent("test2", "boom") 53 | event.At(now) 54 | 55 | err = graphite2.Publish(event) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | } 60 | 61 | func TestGraphiteTagEvent(t *testing.T) { 62 | event := NewTaggedEvent("foo.bar", "data biz") 63 | if event.Data != "data biz" { 64 | t.Errorf("Data wrong: %v", event) 65 | } 66 | 67 | if event.Tags[0] != "foo.bar" { 68 | t.Errorf("Tags wrong: %v", event) 69 | } 70 | 71 | if event.What != "foo.bar" { 72 | t.Errorf("What wrong: %v", event) 73 | } 74 | } 75 | 76 | func TestGraphiteReturnsErrorOnNon200(t *testing.T) { 77 | 78 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 79 | http.Error(w, fmt.Sprintf("Die"), 589) 80 | })) 81 | defer ts.Close() 82 | 83 | graphite := New("", "", ts.URL) 84 | err := graphite.Publish(NewEvent("What", "Dat", "tag1", "tag2")) 85 | 86 | if err == nil { 87 | t.Fatal("Should have errored") 88 | } 89 | } 90 | 91 | func TestGraphiteSendsAuthWhenSet(t *testing.T) { 92 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 93 | if r.Header.Get("Authorization") == "" { 94 | http.Error(w, "Auth Req", 401) 95 | } 96 | })) 97 | defer ts.Close() 98 | 99 | graphite := New("", "", ts.URL) 100 | err := graphite.Publish(NewEvent("What", "Dat", "tag1", "tag2")) 101 | 102 | if err == nil { 103 | t.Fatal("Should have not authed") 104 | } 105 | 106 | graphite = New("foo", "bar", ts.URL) 107 | err = graphite.Publish(NewEvent("What", "Dat", "tag1", "tag2")) 108 | 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | } 113 | 114 | func TestGraphiteSendsEvents(t *testing.T) { 115 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 116 | body, err := ioutil.ReadAll(r.Body) 117 | r.Body.Close() 118 | if err != nil { 119 | http.Error(w, err.Error(), 527) 120 | } 121 | 122 | var event *Event 123 | if err == nil { 124 | err = json.Unmarshal(body, &event) 125 | if err != nil { 126 | http.Error(w, err.Error(), 528) 127 | return 128 | } 129 | } 130 | 131 | if err == nil { 132 | if event.What != "What" && event.Data != "Dat" && event.Tags[0] != "tag1" && event.Tags[0] != "tag2" { 133 | http.Error(w, fmt.Sprintf("%s", body), 529) 134 | return 135 | } 136 | } 137 | })) 138 | defer ts.Close() 139 | 140 | graphite := New("", "", ts.URL) 141 | err := graphite.Publish(NewEvent("What", "Dat", "tag1", "tag2")) 142 | 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | 147 | } 148 | --------------------------------------------------------------------------------