pax_global_header00006660000000000000000000000064131004213560014505gustar00rootroot0000000000000052 comment=0ddda6bee21174ef6c4873647cb0d6ec9cba996f datadog-go-1.1.0/000077500000000000000000000000001310042135600135125ustar00rootroot00000000000000datadog-go-1.1.0/.travis.yml000066400000000000000000000000771310042135600156270ustar00rootroot00000000000000language: go go: - 1.4 - 1.5 script: - go test -v ./... datadog-go-1.1.0/CHANGELOG.md000066400000000000000000000046221310042135600153270ustar00rootroot00000000000000Changes ======= # 1.1.0 / Unreleased ### Notes * [FEATURE] Export serviceCheckStatus allowing interfaces to statsd.Client. See [#19][] (Thanks [@Jasrags][]) * [FEATURE] Client.sendMsg(). Check payload length for all messages. See [#25][] (Thanks [@theckman][]) * [BUGFIX] Remove new lines from tags. See [#21][] (Thanks [@sjung-stripe][]) * [BUGFIX] Do not panic on Client.Event when `nil`. See [#28][] * [DOCUMENTATION] Update `decr` documentation to match implementation. See [#30][] (Thanks [@kcollasarundell][]) # 1.0.0 / 2016-08-22 ### Details We hadn't been properly versioning this project. We will begin to do so with this `1.0.0` release. We had some contributions in the past and would like to thank the contributors [@aviau][], [@sschepens][], [@jovanbrakus][], [@abtris][], [@tummychow][], [@gphat][], [@diasjorge][], [@victortrac][], [@seiffert][] and [@w-vi][], in no particular order, for their work. Below, for reference, the latest improvements made in 07/2016 - 08/2016 ### Notes * [FEATURE] Implemented support for service checks. See [#17][] and [#5][]. (Thanks [@jovanbrakus][] and [@diasjorge][]). * [FEATURE] Add Incr, Decr, Timing and more docs.. See [#15][]. (Thanks [@gphat][]) * [BUGFIX] Do not append to shared slice. See [#16][]. (Thanks [@tummychow][]) [#5]: https://github.com/DataDog/datadog-go/issues/5 [#15]: https://github.com/DataDog/datadog-go/issues/15 [#16]: https://github.com/DataDog/datadog-go/issues/16 [#17]: https://github.com/DataDog/datadog-go/issues/17 [#19]: https://github.com/DataDog/datadog-go/issues/19 [#21]: https://github.com/DataDog/datadog-go/issues/21 [#25]: https://github.com/DataDog/datadog-go/issues/25 [#28]: https://github.com/DataDog/datadog-go/issues/28 [#30]: https://github.com/DataDog/datadog-go/issues/30 [@Jasrags]: https://github.com/Jasrags [@abtris]: https://github.com/abtris [@aviau]: https://github.com/aviau [@diasjorge]: https://github.com/diasjorge [@gphat]: https://github.com/gphat [@jovanbrakus]: https://github.com/jovanbrakus [@kcollasarundell]: https://github.com/kcollasarundell [@seiffert]: https://github.com/seiffert [@sjung-stripe]: https://github.com/sjung-stripe [@sschepens]: https://github.com/sschepens [@theckman]: https://github.com/theckman [@tummychow]: https://github.com/tummychow [@victortrac]: https://github.com/victortrac [@w-vi]: https://github.com/w-vidatadog-go-1.1.0/LICENSE.txt000066400000000000000000000020401310042135600153310ustar00rootroot00000000000000Copyright (c) 2015 Datadog, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. datadog-go-1.1.0/README.md000066400000000000000000000022251310042135600147720ustar00rootroot00000000000000[![Build Status](https://travis-ci.org/DataDog/datadog-go.svg?branch=master)](https://travis-ci.org/DataDog/datadog-go) # Overview Packages in `datadog-go` provide Go clients for various APIs at [DataDog](http://datadoghq.com). ## Statsd [![Godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/DataDog/datadog-go/statsd) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](http://opensource.org/licenses/MIT) The [statsd](https://github.com/DataDog/datadog-go/tree/master/statsd) package provides a client for [dogstatsd](http://docs.datadoghq.com/guides/dogstatsd/): ```go import "github.com/DataDog/datadog-go/statsd" func main() { c, err := statsd.New("127.0.0.1:8125") if err != nil { log.Fatal(err) } // prefix every metric with the app name c.Namespace = "flubber." // send the EC2 availability zone as a tag with every metric c.Tags = append(c.Tags, "us-east-1a") err = c.Gauge("request.duration", 1.2, nil, 1) // ... } ``` ## License All code distributed under the [MIT License](http://opensource.org/licenses/MIT) unless otherwise specified. datadog-go-1.1.0/statsd/000077500000000000000000000000001310042135600150145ustar00rootroot00000000000000datadog-go-1.1.0/statsd/README.md000066400000000000000000000025731310042135600163020ustar00rootroot00000000000000## Overview Package `statsd` provides a Go [dogstatsd](http://docs.datadoghq.com/guides/dogstatsd/) client. Dogstatsd extends Statsd, adding tags and histograms. ## Get the code $ go get github.com/DataDog/datadog-go/statsd ## Usage ```go // Create the client c, err := statsd.New("127.0.0.1:8125") if err != nil { log.Fatal(err) } // Prefix every metric with the app name c.Namespace = "flubber." // Send the EC2 availability zone as a tag with every metric c.Tags = append(c.Tags, "us-east-1a") // Do some metrics! err = c.Gauge("request.queue_depth", 12, nil, 1) err = c.Timing("request.duration", duration, nil, 1) // Uses a time.Duration! err = c.TimeInMilliseconds("request", 12, nil, 1) err = c.Incr("request.count_total", nil, 1) err = c.Decr("request.count_total", nil, 1) err = c.Count("request.count_total", 2, nil, 1) ``` ## Buffering Client DogStatsD accepts packets with multiple statsd payloads in them. Using the BufferingClient via `NewBufferingClient` will buffer up commands and send them when the buffer is reached or after 100msec. ## Development Run the tests with: $ go test ## Documentation Please see: http://godoc.org/github.com/DataDog/datadog-go/statsd ## License go-dogstatsd is released under the [MIT license](http://www.opensource.org/licenses/mit-license.php). ## Credits Original code by [ooyala](https://github.com/ooyala/go-dogstatsd). datadog-go-1.1.0/statsd/statsd.go000066400000000000000000000363431310042135600166560ustar00rootroot00000000000000// Copyright 2013 Ooyala, Inc. /* Package statsd provides a Go dogstatsd client. Dogstatsd extends the popular statsd, adding tags and histograms and pushing upstream to Datadog. Refer to http://docs.datadoghq.com/guides/dogstatsd/ for information about DogStatsD. Example Usage: // Create the client c, err := statsd.New("127.0.0.1:8125") if err != nil { log.Fatal(err) } // Prefix every metric with the app name c.Namespace = "flubber." // Send the EC2 availability zone as a tag with every metric c.Tags = append(c.Tags, "us-east-1a") err = c.Gauge("request.duration", 1.2, nil, 1) statsd is based on go-statsd-client. */ package statsd import ( "bytes" "errors" "fmt" "io" "math/rand" "net" "strconv" "strings" "sync" "time" ) /* OptimalPayloadSize defines the optimal payload size for a UDP datagram, 1432 bytes is optimal for regular networks with an MTU of 1500 so datagrams don't get fragmented. It's generally recommended not to fragment UDP datagrams as losing a single fragment will cause the entire datagram to be lost. This can be increased if your network has a greater MTU or you don't mind UDP datagrams getting fragmented. The practical limit is MaxUDPPayloadSize */ const OptimalPayloadSize = 1432 /* MaxUDPPayloadSize defines the maximum payload size for a UDP datagram. Its value comes from the calculation: 65535 bytes Max UDP datagram size - 8byte UDP header - 60byte max IP headers any number greater than that will see frames being cut out. */ const MaxUDPPayloadSize = 65467 // A Client is a handle for sending udp messages to dogstatsd. It is safe to // use one Client from multiple goroutines simultaneously. type Client struct { conn net.Conn // Namespace to prepend to all statsd calls Namespace string // Tags are global tags to be added to every statsd call Tags []string // BufferLength is the length of the buffer in commands. bufferLength int flushTime time.Duration commands []string buffer bytes.Buffer stop bool sync.Mutex } // New returns a pointer to a new Client given an addr in the format "hostname:port". func New(addr string) (*Client, error) { udpAddr, err := net.ResolveUDPAddr("udp", addr) if err != nil { return nil, err } conn, err := net.DialUDP("udp", nil, udpAddr) if err != nil { return nil, err } client := &Client{conn: conn} return client, nil } // NewBuffered returns a Client that buffers its output and sends it in chunks. // Buflen is the length of the buffer in number of commands. func NewBuffered(addr string, buflen int) (*Client, error) { client, err := New(addr) if err != nil { return nil, err } client.bufferLength = buflen client.commands = make([]string, 0, buflen) client.flushTime = time.Millisecond * 100 go client.watch() return client, nil } // format a message from its name, value, tags and rate. Also adds global // namespace and tags. func (c *Client) format(name, value string, tags []string, rate float64) string { var buf bytes.Buffer if c.Namespace != "" { buf.WriteString(c.Namespace) } buf.WriteString(name) buf.WriteString(":") buf.WriteString(value) if rate < 1 { buf.WriteString(`|@`) buf.WriteString(strconv.FormatFloat(rate, 'f', -1, 64)) } writeTagString(&buf, c.Tags, tags) return buf.String() } func (c *Client) watch() { for _ = range time.Tick(c.flushTime) { if c.stop { return } c.Lock() if len(c.commands) > 0 { // FIXME: eating error here c.flush() } c.Unlock() } } func (c *Client) append(cmd string) error { c.Lock() defer c.Unlock() c.commands = append(c.commands, cmd) // if we should flush, lets do it if len(c.commands) == c.bufferLength { if err := c.flush(); err != nil { return err } } return nil } func (c *Client) joinMaxSize(cmds []string, sep string, maxSize int) ([][]byte, []int) { c.buffer.Reset() //clear buffer var frames [][]byte var ncmds []int sepBytes := []byte(sep) sepLen := len(sep) elem := 0 for _, cmd := range cmds { needed := len(cmd) if elem != 0 { needed = needed + sepLen } if c.buffer.Len()+needed <= maxSize { if elem != 0 { c.buffer.Write(sepBytes) } c.buffer.WriteString(cmd) elem++ } else { frames = append(frames, copyAndResetBuffer(&c.buffer)) ncmds = append(ncmds, elem) // if cmd is bigger than maxSize it will get flushed on next loop c.buffer.WriteString(cmd) elem = 1 } } //add whatever is left! if there's actually something if c.buffer.Len() > 0 { frames = append(frames, copyAndResetBuffer(&c.buffer)) ncmds = append(ncmds, elem) } return frames, ncmds } func copyAndResetBuffer(buf *bytes.Buffer) []byte { tmpBuf := make([]byte, buf.Len()) copy(tmpBuf, buf.Bytes()) buf.Reset() return tmpBuf } // flush the commands in the buffer. Lock must be held by caller. func (c *Client) flush() error { frames, flushable := c.joinMaxSize(c.commands, "\n", OptimalPayloadSize) var err error cmdsFlushed := 0 for i, data := range frames { _, e := c.conn.Write(data) if e != nil { err = e break } cmdsFlushed += flushable[i] } // clear the slice with a slice op, doesn't realloc if cmdsFlushed == len(c.commands) { c.commands = c.commands[:0] } else { //this case will cause a future realloc... // drop problematic command though (sorry). c.commands = c.commands[cmdsFlushed+1:] } return err } func (c *Client) sendMsg(msg string) error { // return an error if message is bigger than MaxUDPPayloadSize if len(msg) > MaxUDPPayloadSize { return errors.New("message size exceeds MaxUDPPayloadSize") } // if this client is buffered, then we'll just append this if c.bufferLength > 0 { return c.append(msg) } _, err := c.conn.Write([]byte(msg)) return err } // send handles sampling and sends the message over UDP. It also adds global namespace prefixes and tags. func (c *Client) send(name, value string, tags []string, rate float64) error { if c == nil { return nil } if rate < 1 && rand.Float64() > rate { return nil } data := c.format(name, value, tags, rate) return c.sendMsg(data) } // Gauge measures the value of a metric at a particular time. func (c *Client) Gauge(name string, value float64, tags []string, rate float64) error { stat := fmt.Sprintf("%f|g", value) return c.send(name, stat, tags, rate) } // Count tracks how many times something happened per second. func (c *Client) Count(name string, value int64, tags []string, rate float64) error { stat := fmt.Sprintf("%d|c", value) return c.send(name, stat, tags, rate) } // Histogram tracks the statistical distribution of a set of values. func (c *Client) Histogram(name string, value float64, tags []string, rate float64) error { stat := fmt.Sprintf("%f|h", value) return c.send(name, stat, tags, rate) } // Decr is just Count of -1 func (c *Client) Decr(name string, tags []string, rate float64) error { return c.send(name, "-1|c", tags, rate) } // Incr is just Count of 1 func (c *Client) Incr(name string, tags []string, rate float64) error { return c.send(name, "1|c", tags, rate) } // Set counts the number of unique elements in a group. func (c *Client) Set(name string, value string, tags []string, rate float64) error { stat := fmt.Sprintf("%s|s", value) return c.send(name, stat, tags, rate) } // Timing sends timing information, it is an alias for TimeInMilliseconds func (c *Client) Timing(name string, value time.Duration, tags []string, rate float64) error { return c.TimeInMilliseconds(name, value.Seconds()*1000, tags, rate) } // TimeInMilliseconds sends timing information in milliseconds. // It is flushed by statsd with percentiles, mean and other info (https://github.com/etsy/statsd/blob/master/docs/metric_types.md#timing) func (c *Client) TimeInMilliseconds(name string, value float64, tags []string, rate float64) error { stat := fmt.Sprintf("%f|ms", value) return c.send(name, stat, tags, rate) } // Event sends the provided Event. func (c *Client) Event(e *Event) error { if c == nil { return nil } stat, err := e.Encode(c.Tags...) if err != nil { return err } return c.sendMsg(stat) } // SimpleEvent sends an event with the provided title and text. func (c *Client) SimpleEvent(title, text string) error { e := NewEvent(title, text) return c.Event(e) } // ServiceCheck sends the provided ServiceCheck. func (c *Client) ServiceCheck(sc *ServiceCheck) error { stat, err := sc.Encode(c.Tags...) if err != nil { return err } return c.sendMsg(stat) } // SimpleServiceCheck sends an serviceCheck with the provided name and status. func (c *Client) SimpleServiceCheck(name string, status ServiceCheckStatus) error { sc := NewServiceCheck(name, status) return c.ServiceCheck(sc) } // Close the client connection. func (c *Client) Close() error { if c == nil { return nil } c.stop = true return c.conn.Close() } // Events support type eventAlertType string const ( // Info is the "info" AlertType for events Info eventAlertType = "info" // Error is the "error" AlertType for events Error eventAlertType = "error" // Warning is the "warning" AlertType for events Warning eventAlertType = "warning" // Success is the "success" AlertType for events Success eventAlertType = "success" ) type eventPriority string const ( // Normal is the "normal" Priority for events Normal eventPriority = "normal" // Low is the "low" Priority for events Low eventPriority = "low" ) // An Event is an object that can be posted to your DataDog event stream. type Event struct { // Title of the event. Required. Title string // Text is the description of the event. Required. Text string // Timestamp is a timestamp for the event. If not provided, the dogstatsd // server will set this to the current time. Timestamp time.Time // Hostname for the event. Hostname string // AggregationKey groups this event with others of the same key. AggregationKey string // Priority of the event. Can be statsd.Low or statsd.Normal. Priority eventPriority // SourceTypeName is a source type for the event. SourceTypeName string // AlertType can be statsd.Info, statsd.Error, statsd.Warning, or statsd.Success. // If absent, the default value applied by the dogstatsd server is Info. AlertType eventAlertType // Tags for the event. Tags []string } // NewEvent creates a new event with the given title and text. Error checking // against these values is done at send-time, or upon running e.Check. func NewEvent(title, text string) *Event { return &Event{ Title: title, Text: text, } } // Check verifies that an event is valid. func (e Event) Check() error { if len(e.Title) == 0 { return fmt.Errorf("statsd.Event title is required") } if len(e.Text) == 0 { return fmt.Errorf("statsd.Event text is required") } return nil } // Encode returns the dogstatsd wire protocol representation for an event. // Tags may be passed which will be added to the encoded output but not to // the Event's list of tags, eg. for default tags. func (e Event) Encode(tags ...string) (string, error) { err := e.Check() if err != nil { return "", err } text := e.escapedText() var buffer bytes.Buffer buffer.WriteString("_e{") buffer.WriteString(strconv.FormatInt(int64(len(e.Title)), 10)) buffer.WriteRune(',') buffer.WriteString(strconv.FormatInt(int64(len(text)), 10)) buffer.WriteString("}:") buffer.WriteString(e.Title) buffer.WriteRune('|') buffer.WriteString(text) if !e.Timestamp.IsZero() { buffer.WriteString("|d:") buffer.WriteString(strconv.FormatInt(int64(e.Timestamp.Unix()), 10)) } if len(e.Hostname) != 0 { buffer.WriteString("|h:") buffer.WriteString(e.Hostname) } if len(e.AggregationKey) != 0 { buffer.WriteString("|k:") buffer.WriteString(e.AggregationKey) } if len(e.Priority) != 0 { buffer.WriteString("|p:") buffer.WriteString(string(e.Priority)) } if len(e.SourceTypeName) != 0 { buffer.WriteString("|s:") buffer.WriteString(e.SourceTypeName) } if len(e.AlertType) != 0 { buffer.WriteString("|t:") buffer.WriteString(string(e.AlertType)) } writeTagString(&buffer, tags, e.Tags) return buffer.String(), nil } // ServiceCheck support type ServiceCheckStatus byte const ( // Ok is the "ok" ServiceCheck status Ok ServiceCheckStatus = 0 // Warn is the "warning" ServiceCheck status Warn ServiceCheckStatus = 1 // Critical is the "critical" ServiceCheck status Critical ServiceCheckStatus = 2 // Unknown is the "unknown" ServiceCheck status Unknown ServiceCheckStatus = 3 ) // An ServiceCheck is an object that contains status of DataDog service check. type ServiceCheck struct { // Name of the service check. Required. Name string // Status of service check. Required. Status ServiceCheckStatus // Timestamp is a timestamp for the serviceCheck. If not provided, the dogstatsd // server will set this to the current time. Timestamp time.Time // Hostname for the serviceCheck. Hostname string // A message describing the current state of the serviceCheck. Message string // Tags for the serviceCheck. Tags []string } // NewServiceCheck creates a new serviceCheck with the given name and status. Error checking // against these values is done at send-time, or upon running sc.Check. func NewServiceCheck(name string, status ServiceCheckStatus) *ServiceCheck { return &ServiceCheck{ Name: name, Status: status, } } // Check verifies that an event is valid. func (sc ServiceCheck) Check() error { if len(sc.Name) == 0 { return fmt.Errorf("statsd.ServiceCheck name is required") } if byte(sc.Status) < 0 || byte(sc.Status) > 3 { return fmt.Errorf("statsd.ServiceCheck status has invalid value") } return nil } // Encode returns the dogstatsd wire protocol representation for an serviceCheck. // Tags may be passed which will be added to the encoded output but not to // the Event's list of tags, eg. for default tags. func (sc ServiceCheck) Encode(tags ...string) (string, error) { err := sc.Check() if err != nil { return "", err } message := sc.escapedMessage() var buffer bytes.Buffer buffer.WriteString("_sc|") buffer.WriteString(sc.Name) buffer.WriteRune('|') buffer.WriteString(strconv.FormatInt(int64(sc.Status), 10)) if !sc.Timestamp.IsZero() { buffer.WriteString("|d:") buffer.WriteString(strconv.FormatInt(int64(sc.Timestamp.Unix()), 10)) } if len(sc.Hostname) != 0 { buffer.WriteString("|h:") buffer.WriteString(sc.Hostname) } writeTagString(&buffer, tags, sc.Tags) if len(message) != 0 { buffer.WriteString("|m:") buffer.WriteString(message) } return buffer.String(), nil } func (e Event) escapedText() string { return strings.Replace(e.Text, "\n", "\\n", -1) } func (sc ServiceCheck) escapedMessage() string { msg := strings.Replace(sc.Message, "\n", "\\n", -1) return strings.Replace(msg, "m:", `m\:`, -1) } func removeNewlines(str string) string { return strings.Replace(str, "\n", "", -1) } func writeTagString(w io.Writer, tagList1, tagList2 []string) { // the tag lists may be shared with other callers, so we cannot modify // them in any way (which means we cannot append to them either) // therefore we must make an entirely separate copy just for this call totalLen := len(tagList1) + len(tagList2) if totalLen == 0 { return } tags := make([]string, 0, totalLen) tags = append(tags, tagList1...) tags = append(tags, tagList2...) io.WriteString(w, "|#") io.WriteString(w, removeNewlines(tags[0])) for _, tag := range tags[1:] { io.WriteString(w, ",") io.WriteString(w, removeNewlines(tag)) } } datadog-go-1.1.0/statsd/statsd_test.go000066400000000000000000000432061310042135600177110ustar00rootroot00000000000000// Copyright 2013 Ooyala, Inc. package statsd import ( "fmt" "io" "net" "reflect" "strings" "testing" "time" ) var dogstatsdTests = []struct { GlobalNamespace string GlobalTags []string Method string Metric string Value interface{} Tags []string Rate float64 Expected string }{ {"", nil, "Gauge", "test.gauge", 1.0, nil, 1.0, "test.gauge:1.000000|g"}, {"", nil, "Gauge", "test.gauge", 1.0, nil, 0.999999, "test.gauge:1.000000|g|@0.999999"}, {"", nil, "Gauge", "test.gauge", 1.0, []string{"tagA"}, 1.0, "test.gauge:1.000000|g|#tagA"}, {"", nil, "Gauge", "test.gauge", 1.0, []string{"tagA", "tagB"}, 1.0, "test.gauge:1.000000|g|#tagA,tagB"}, {"", nil, "Gauge", "test.gauge", 1.0, []string{"tagA"}, 0.999999, "test.gauge:1.000000|g|@0.999999|#tagA"}, {"", nil, "Count", "test.count", int64(1), []string{"tagA"}, 1.0, "test.count:1|c|#tagA"}, {"", nil, "Count", "test.count", int64(-1), []string{"tagA"}, 1.0, "test.count:-1|c|#tagA"}, {"", nil, "Histogram", "test.histogram", 2.3, []string{"tagA"}, 1.0, "test.histogram:2.300000|h|#tagA"}, {"", nil, "Set", "test.set", "uuid", []string{"tagA"}, 1.0, "test.set:uuid|s|#tagA"}, {"flubber.", nil, "Set", "test.set", "uuid", []string{"tagA"}, 1.0, "flubber.test.set:uuid|s|#tagA"}, {"", []string{"tagC"}, "Set", "test.set", "uuid", []string{"tagA"}, 1.0, "test.set:uuid|s|#tagC,tagA"}, {"", nil, "Count", "test.count", int64(1), []string{"hello\nworld"}, 1.0, "test.count:1|c|#helloworld"}, } func assertNotPanics(t *testing.T, f func()) { defer func() { if r := recover(); r != nil { t.Fatal(r) } }() f() } func TestClient(t *testing.T) { addr := "localhost:1201" udpAddr, err := net.ResolveUDPAddr("udp", addr) if err != nil { t.Fatal(err) } server, err := net.ListenUDP("udp", udpAddr) if err != nil { t.Fatal(err) } defer server.Close() client, err := New(addr) if err != nil { t.Fatal(err) } for _, tt := range dogstatsdTests { client.Namespace = tt.GlobalNamespace client.Tags = tt.GlobalTags method := reflect.ValueOf(client).MethodByName(tt.Method) e := method.Call([]reflect.Value{ reflect.ValueOf(tt.Metric), reflect.ValueOf(tt.Value), reflect.ValueOf(tt.Tags), reflect.ValueOf(tt.Rate)})[0] errInter := e.Interface() if errInter != nil { t.Fatal(errInter.(error)) } bytes := make([]byte, 1024) n, err := server.Read(bytes) if err != nil { t.Fatal(err) } message := bytes[:n] if string(message) != tt.Expected { t.Errorf("Expected: %s. Actual: %s", tt.Expected, string(message)) } } } func TestBufferedClient(t *testing.T) { addr := "localhost:1201" udpAddr, err := net.ResolveUDPAddr("udp", addr) if err != nil { t.Fatal(err) } server, err := net.ListenUDP("udp", udpAddr) if err != nil { t.Fatal(err) } defer server.Close() conn, err := net.DialUDP("udp", nil, udpAddr) if err != nil { t.Fatal(err) } bufferLength := 8 client := &Client{ conn: conn, commands: make([]string, 0, bufferLength), bufferLength: bufferLength, } client.Namespace = "foo." client.Tags = []string{"dd:2"} dur, _ := time.ParseDuration("123us") client.Incr("ic", nil, 1) client.Decr("dc", nil, 1) client.Count("cc", 1, nil, 1) client.Gauge("gg", 10, nil, 1) client.Histogram("hh", 1, nil, 1) client.Timing("tt", dur, nil, 1) client.Set("ss", "ss", nil, 1) if len(client.commands) != 7 { t.Errorf("Expected client to have buffered 7 commands, but found %d\n", len(client.commands)) } client.Set("ss", "xx", nil, 1) err = client.flush() if err != nil { t.Errorf("Error sending: %s", err) } if len(client.commands) != 0 { t.Errorf("Expecting send to flush commands, but found %d\n", len(client.commands)) } buffer := make([]byte, 4096) n, err := io.ReadAtLeast(server, buffer, 1) result := string(buffer[:n]) if err != nil { t.Error(err) } expected := []string{ `foo.ic:1|c|#dd:2`, `foo.dc:-1|c|#dd:2`, `foo.cc:1|c|#dd:2`, `foo.gg:10.000000|g|#dd:2`, `foo.hh:1.000000|h|#dd:2`, `foo.tt:0.123000|ms|#dd:2`, `foo.ss:ss|s|#dd:2`, `foo.ss:xx|s|#dd:2`, } for i, res := range strings.Split(result, "\n") { if res != expected[i] { t.Errorf("Got `%s`, expected `%s`", res, expected[i]) } } client.Event(&Event{Title: "title1", Text: "text1", Priority: Normal, AlertType: Success, Tags: []string{"tagg"}}) client.SimpleEvent("event1", "text1") if len(client.commands) != 2 { t.Errorf("Expected to find %d commands, but found %d\n", 2, len(client.commands)) } err = client.flush() if err != nil { t.Errorf("Error sending: %s", err) } if len(client.commands) != 0 { t.Errorf("Expecting send to flush commands, but found %d\n", len(client.commands)) } buffer = make([]byte, 1024) n, err = io.ReadAtLeast(server, buffer, 1) result = string(buffer[:n]) if err != nil { t.Error(err) } if n == 0 { t.Errorf("Read 0 bytes but expected more.") } expected = []string{ `_e{6,5}:title1|text1|p:normal|t:success|#dd:2,tagg`, `_e{6,5}:event1|text1|#dd:2`, } for i, res := range strings.Split(result, "\n") { if res != expected[i] { t.Errorf("Got `%s`, expected `%s`", res, expected[i]) } } } func TestJoinMaxSize(t *testing.T) { c := Client{} elements := []string{"abc", "abcd", "ab", "xyz", "foobaz", "x", "wwxxyyzz"} res, n := c.joinMaxSize(elements, " ", 8) if len(res) != len(n) && len(res) != 4 { t.Errorf("Was expecting 4 frames to flush but got: %v - %v", n, res) } if n[0] != 2 { t.Errorf("Was expecting 2 elements in first frame but got: %v", n[0]) } if string(res[0]) != "abc abcd" { t.Errorf("Join should have returned \"abc abcd\" in frame, but found: %s", res[0]) } if n[1] != 2 { t.Errorf("Was expecting 2 elements in second frame but got: %v - %v", n[1], n) } if string(res[1]) != "ab xyz" { t.Errorf("Join should have returned \"ab xyz\" in frame, but found: %s", res[1]) } if n[2] != 2 { t.Errorf("Was expecting 2 elements in third frame but got: %v - %v", n[2], n) } if string(res[2]) != "foobaz x" { t.Errorf("Join should have returned \"foobaz x\" in frame, but found: %s", res[2]) } if n[3] != 1 { t.Errorf("Was expecting 1 element in fourth frame but got: %v - %v", n[3], n) } if string(res[3]) != "wwxxyyzz" { t.Errorf("Join should have returned \"wwxxyyzz\" in frame, but found: %s", res[3]) } res, n = c.joinMaxSize(elements, " ", 11) if len(res) != len(n) && len(res) != 3 { t.Errorf("Was expecting 3 frames to flush but got: %v - %v", n, res) } if n[0] != 3 { t.Errorf("Was expecting 3 elements in first frame but got: %v", n[0]) } if string(res[0]) != "abc abcd ab" { t.Errorf("Join should have returned \"abc abcd ab\" in frame, but got: %s", res[0]) } if n[1] != 2 { t.Errorf("Was expecting 2 elements in second frame but got: %v", n[1]) } if string(res[1]) != "xyz foobaz" { t.Errorf("Join should have returned \"xyz foobaz\" in frame, but got: %s", res[1]) } if n[2] != 2 { t.Errorf("Was expecting 2 elements in third frame but got: %v", n[2]) } if string(res[2]) != "x wwxxyyzz" { t.Errorf("Join should have returned \"x wwxxyyzz\" in frame, but got: %s", res[2]) } res, n = c.joinMaxSize(elements, " ", 8) if len(res) != len(n) && len(res) != 7 { t.Errorf("Was expecting 7 frames to flush but got: %v - %v", n, res) } if n[0] != 1 { t.Errorf("Separator is long, expected a single element in frame but got: %d - %v", n[0], res) } if string(res[0]) != "abc" { t.Errorf("Join should have returned \"abc\" in first frame, but got: %s", res) } if n[1] != 1 { t.Errorf("Separator is long, expected a single element in frame but got: %d - %v", n[1], res) } if string(res[1]) != "abcd" { t.Errorf("Join should have returned \"abcd\" in second frame, but got: %s", res[1]) } if n[2] != 1 { t.Errorf("Separator is long, expected a single element in third frame but got: %d - %v", n[2], res) } if string(res[2]) != "ab" { t.Errorf("Join should have returned \"ab\" in third frame, but got: %s", res[2]) } if n[3] != 1 { t.Errorf("Separator is long, expected a single element in fourth frame but got: %d - %v", n[3], res) } if string(res[3]) != "xyz" { t.Errorf("Join should have returned \"xyz\" in fourth frame, but got: %s", res[3]) } if n[4] != 1 { t.Errorf("Separator is long, expected a single element in fifth frame but got: %d - %v", n[4], res) } if string(res[4]) != "foobaz" { t.Errorf("Join should have returned \"foobaz\" in fifth frame, but got: %s", res[4]) } if n[5] != 1 { t.Errorf("Separator is long, expected a single element in sixth frame but got: %d - %v", n[5], res) } if string(res[5]) != "x" { t.Errorf("Join should have returned \"x\" in sixth frame, but got: %s", res[5]) } if n[6] != 1 { t.Errorf("Separator is long, expected a single element in seventh frame but got: %d - %v", n[6], res) } if string(res[6]) != "wwxxyyzz" { t.Errorf("Join should have returned \"wwxxyyzz\" in seventh frame, but got: %s", res[6]) } res, n = c.joinMaxSize(elements[4:], " ", 6) if len(res) != len(n) && len(res) != 3 { t.Errorf("Was expecting 3 frames to flush but got: %v - %v", n, res) } if n[0] != 1 { t.Errorf("Element should just fit in frame - expected single element in frame: %d - %v", n[0], res) } if string(res[0]) != "foobaz" { t.Errorf("Join should have returned \"foobaz\" in first frame, but got: %s", res[0]) } if n[1] != 1 { t.Errorf("Single element expected in frame, but got. %d - %v", n[1], res) } if string(res[1]) != "x" { t.Errorf("Join should' have returned \"x\" in second frame, but got: %s", res[1]) } if n[2] != 1 { t.Errorf("Even though element is greater then max size we still try to send it. %d - %v", n[2], res) } if string(res[2]) != "wwxxyyzz" { t.Errorf("Join should have returned \"wwxxyyzz\" in third frame, but got: %s", res[2]) } } func TestSendMsg(t *testing.T) { addr := "localhost:1201" udpAddr, err := net.ResolveUDPAddr("udp", addr) if err != nil { t.Fatal(err) } server, err := net.ListenUDP("udp", udpAddr) if err != nil { t.Fatal(err) } defer server.Close() conn, err := net.DialUDP("udp", nil, udpAddr) if err != nil { t.Fatal(err) } defer conn.Close() client := &Client{ conn: conn, bufferLength: 0, } err = client.sendMsg(strings.Repeat("x", MaxUDPPayloadSize+1)) if err == nil { t.Error("Expected error to be returned if message size is bigger than MaxUDPPayloadSize") } longMsg := strings.Repeat("x", MaxUDPPayloadSize) err = client.sendMsg(longMsg) if err != nil { t.Errorf("Expected no error to be returned if message size is smaller or equal to MaxUDPPayloadSize, got: %s", err.Error()) } buffer := make([]byte, MaxUDPPayloadSize+1) n, err := io.ReadAtLeast(server, buffer, 1) if err != nil { t.Fatalf("Expected no error to be returned reading the buffer, got: %s", err.Error()) } if n != MaxUDPPayloadSize { t.Fatalf("Failed to read full message from buffer. Got size `%d` expected `%d`", n, MaxUDPPayloadSize) } if string(buffer[:n]) != longMsg { t.Fatalf("The received message did not match what we expect.") } client = &Client{ conn: conn, commands: make([]string, 0, 1), bufferLength: 1, } err = client.sendMsg(strings.Repeat("x", MaxUDPPayloadSize+1)) if err == nil { t.Error("Expected error to be returned if message size is bigger than MaxUDPPayloadSize") } err = client.sendMsg(longMsg) if err != nil { t.Errorf("Expected no error to be returned if message size is smaller or equal to MaxUDPPayloadSize, got: %s", err.Error()) } client.Lock() err = client.flush() client.Unlock() if err != nil { t.Fatalf("Expected no error to be returned flushing the client, got: %s", err.Error()) } buffer = make([]byte, MaxUDPPayloadSize+1) n, err = io.ReadAtLeast(server, buffer, 1) if err != nil { t.Fatalf("Expected no error to be returned reading the buffer, got: %s", err.Error()) } if n != MaxUDPPayloadSize { t.Fatalf("Failed to read full message from buffer. Got size `%d` expected `%d`", n, MaxUDPPayloadSize) } if string(buffer[:n]) != longMsg { t.Fatalf("The received message did not match what we expect.") } } func TestNilSafe(t *testing.T) { var c *Client assertNotPanics(t, func() { c.Close() }) assertNotPanics(t, func() { c.Count("", 0, nil, 1) }) assertNotPanics(t, func() { c.Histogram("", 0, nil, 1) }) assertNotPanics(t, func() { c.Gauge("", 0, nil, 1) }) assertNotPanics(t, func() { c.Set("", "", nil, 1) }) assertNotPanics(t, func() { c.send("", "", nil, 1) }) assertNotPanics(t, func() { c.SimpleEvent("", "") }) } func TestEvents(t *testing.T) { matrix := []struct { event *Event encoded string }{ { NewEvent("Hello", "Something happened to my event"), `_e{5,30}:Hello|Something happened to my event`, }, { &Event{Title: "hi", Text: "okay", AggregationKey: "foo"}, `_e{2,4}:hi|okay|k:foo`, }, { &Event{Title: "hi", Text: "okay", AggregationKey: "foo", AlertType: Info}, `_e{2,4}:hi|okay|k:foo|t:info`, }, { &Event{Title: "hi", Text: "w/e", AlertType: Error, Priority: Normal}, `_e{2,3}:hi|w/e|p:normal|t:error`, }, { &Event{Title: "hi", Text: "uh", Tags: []string{"host:foo", "app:bar"}}, `_e{2,2}:hi|uh|#host:foo,app:bar`, }, { &Event{Title: "hi", Text: "line1\nline2", Tags: []string{"hello\nworld"}}, `_e{2,12}:hi|line1\nline2|#helloworld`, }, } for _, m := range matrix { r, err := m.event.Encode() if err != nil { t.Errorf("Error encoding: %s\n", err) continue } if r != m.encoded { t.Errorf("Expected `%s`, got `%s`\n", m.encoded, r) } } e := NewEvent("", "hi") if _, err := e.Encode(); err == nil { t.Errorf("Expected error on empty Title.") } e = NewEvent("hi", "") if _, err := e.Encode(); err == nil { t.Errorf("Expected error on empty Text.") } e = NewEvent("hello", "world") s, err := e.Encode("tag1", "tag2") if err != nil { t.Error(err) } expected := "_e{5,5}:hello|world|#tag1,tag2" if s != expected { t.Errorf("Expected %s, got %s", expected, s) } if len(e.Tags) != 0 { t.Errorf("Modified event in place illegally.") } } func TestServiceChecks(t *testing.T) { matrix := []struct { serviceCheck *ServiceCheck encoded string }{ { NewServiceCheck("DataCatService", Ok), `_sc|DataCatService|0`, }, { NewServiceCheck("DataCatService", Warn), `_sc|DataCatService|1`, }, { NewServiceCheck("DataCatService", Critical), `_sc|DataCatService|2`, }, { NewServiceCheck("DataCatService", Unknown), `_sc|DataCatService|3`, }, { &ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat"}, `_sc|DataCatService|0|h:DataStation.Cat`, }, { &ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat", Message: "Here goes valuable message"}, `_sc|DataCatService|0|h:DataStation.Cat|m:Here goes valuable message`, }, { &ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat", Message: "Here are some cyrillic chars: к л м н о п р с т у ф х ц ч ш"}, `_sc|DataCatService|0|h:DataStation.Cat|m:Here are some cyrillic chars: к л м н о п р с т у ф х ц ч ш`, }, { &ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat", Message: "Here goes valuable message", Tags: []string{"host:foo", "app:bar"}}, `_sc|DataCatService|0|h:DataStation.Cat|#host:foo,app:bar|m:Here goes valuable message`, }, { &ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat", Message: "Here goes \n that should be escaped", Tags: []string{"host:foo", "app:b\nar"}}, `_sc|DataCatService|0|h:DataStation.Cat|#host:foo,app:bar|m:Here goes \n that should be escaped`, }, { &ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat", Message: "Here goes m: that should be escaped", Tags: []string{"host:foo", "app:bar"}}, `_sc|DataCatService|0|h:DataStation.Cat|#host:foo,app:bar|m:Here goes m\: that should be escaped`, }, } for _, m := range matrix { r, err := m.serviceCheck.Encode() if err != nil { t.Errorf("Error encoding: %s\n", err) continue } if r != m.encoded { t.Errorf("Expected `%s`, got `%s`\n", m.encoded, r) } } sc := NewServiceCheck("", Ok) if _, err := sc.Encode(); err == nil { t.Errorf("Expected error on empty Name.") } sc = NewServiceCheck("sc", ServiceCheckStatus(5)) if _, err := sc.Encode(); err == nil { t.Errorf("Expected error on invalid status value.") } sc = NewServiceCheck("hello", Warn) s, err := sc.Encode("tag1", "tag2") if err != nil { t.Error(err) } expected := "_sc|hello|1|#tag1,tag2" if s != expected { t.Errorf("Expected %s, got %s", expected, s) } if len(sc.Tags) != 0 { t.Errorf("Modified serviceCheck in place illegally.") } } // These benchmarks show that using a buffer instead of sprintf-ing together // a bunch of intermediate strings is 4-5x faster func BenchmarkFormatNew(b *testing.B) { b.StopTimer() c := &Client{} c.Namespace = "foo.bar." c.Tags = []string{"app:foo", "host:bar"} b.StartTimer() for i := 0; i < b.N; i++ { c.format("system.cpu.idle", "10", []string{"foo"}, 1) c.format("system.cpu.load", "0.1", nil, 0.9) } } // Old formatting function, added to client for tests func (c *Client) formatOld(name, value string, tags []string, rate float64) string { if rate < 1 { value = fmt.Sprintf("%s|@%f", value, rate) } if c.Namespace != "" { name = fmt.Sprintf("%s%s", c.Namespace, name) } tags = append(c.Tags, tags...) if len(tags) > 0 { value = fmt.Sprintf("%s|#%s", value, strings.Join(tags, ",")) } return fmt.Sprintf("%s:%s", name, value) } func BenchmarkFormatOld(b *testing.B) { b.StopTimer() c := &Client{} c.Namespace = "foo.bar." c.Tags = []string{"app:foo", "host:bar"} b.StartTimer() for i := 0; i < b.N; i++ { c.formatOld("system.cpu.idle", "10", []string{"foo"}, 1) c.formatOld("system.cpu.load", "0.1", nil, 0.9) } }