pax_global_header00006660000000000000000000000064132575304030014514gustar00rootroot0000000000000052 comment=e67964b4021ad3a334e748e8811eb3cd6becbc6e datadog-go-2.1.0/000077500000000000000000000000001325753040300135225ustar00rootroot00000000000000datadog-go-2.1.0/.travis.yml000066400000000000000000000001351325753040300156320ustar00rootroot00000000000000language: go go: - 1.5 - 1.6 - 1.7 - 1.8 - 1.9 script: - go test -race -v ./... datadog-go-2.1.0/CHANGELOG.md000066400000000000000000000102221325753040300153300ustar00rootroot00000000000000## Changes # 2.1.0 / 2018-03-30 ### Notes * [IMPROVEMENT] Protect client methods from nil client. See [#52][], thanks [@ods94065][]. # 2.0.0 / 2018-01-29 ### Details Version `2.0.0` contains breaking changes and beta features, please refer to the _Notes_ section below for details. ### Notes * [BREAKING] `statsdWriter` now implements io.Writer interface. See [#46][]. * [BUGFIX] Flush buffer on close. See [#47][]. * [BETA] Add support for global distributions. See [#45][]. * [FEATURE] Add support for Unix Domain Sockets. See [#37][]. * [FEATURE] Export `eventAlertType` and `eventPriority`. See [#42][], thanks [@thomas91310][]. * [FEATURE] Export `Flush` method. See [#40][], thanks [@colega][]. * [BUGFIX] Prevent panics when closing the `udsWriter`. See [#43][], thanks [@jacek-adamek][]. * [IMPROVEMENT] Fix issues reported by Golint. See [#39][], thanks [@tariq1890][]. * [IMPROVEMENT] Improve message building speed by using less `fmt.Sprintf`s. See [#32][], thanks [@corsc][]. # 1.1.0 / 2017-04-27 ### 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 [#32]: https://github.com/DataDog/datadog-go/issues/32 [#37]: https://github.com/DataDog/datadog-go/issues/37 [#39]: https://github.com/DataDog/datadog-go/issues/39 [#40]: https://github.com/DataDog/datadog-go/issues/40 [#42]: https://github.com/DataDog/datadog-go/issues/42 [#43]: https://github.com/DataDog/datadog-go/issues/43 [#45]: https://github.com/DataDog/datadog-go/issues/45 [#46]: https://github.com/DataDog/datadog-go/issues/46 [#47]: https://github.com/DataDog/datadog-go/issues/47 [#52]: https://github.com/DataDog/datadog-go/issues/52 [@Jasrags]: https://github.com/Jasrags [@abtris]: https://github.com/abtris [@aviau]: https://github.com/aviau [@colega]: https://github.com/colega [@corsc]: https://github.com/corsc [@diasjorge]: https://github.com/diasjorge [@gphat]: https://github.com/gphat [@jacek-adamek]: https://github.com/jacek-adamek [@jovanbrakus]: https://github.com/jovanbrakus [@kcollasarundell]: https://github.com/kcollasarundell [@ods94065]: https://github.com/ods94065 [@seiffert]: https://github.com/seiffert [@sjung-stripe]: https://github.com/sjung-stripe [@sschepens]: https://github.com/sschepens [@tariq1890]: https://github.com/tariq1890 [@theckman]: https://github.com/theckman [@thomas91310]: https://github.com/thomas91310 [@tummychow]: https://github.com/tummychow [@victortrac]: https://github.com/victortrac [@w-vi]: https://github.com/w-vidatadog-go-2.1.0/LICENSE.txt000066400000000000000000000020401325753040300153410ustar00rootroot00000000000000Copyright (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-2.1.0/README.md000066400000000000000000000022341325753040300150020ustar00rootroot00000000000000[![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, "region: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-2.1.0/statsd/000077500000000000000000000000001325753040300150245ustar00rootroot00000000000000datadog-go-2.1.0/statsd/README.md000066400000000000000000000042521325753040300163060ustar00rootroot00000000000000## 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. ## Unix Domain Sockets Client DogStatsD version 6 accepts packets through a Unix Socket datagram connection. You can use this protocol by giving a `unix:///path/to/dsd.socket` addr argument to the `New` or `NewBufferingClient`. With this protocol, writes can become blocking if the server's receiving buffer is full. Our default behaviour is to timeout and drop the packet after 1 ms. You can set a custom timeout duration via the `SetWriteTimeout` method. The default mode is to pass write errors from the socket to the caller. This includes write errors the library will automatically recover from (DogStatsD server not ready yet or is restarting). You can drop these errors and emulate the UDP behaviour by setting the `SkipErrors` property to `true`. Please note that packets will be dropped in both modes. ## 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-2.1.0/statsd/statsd.go000066400000000000000000000436751325753040300166740ustar00rootroot00000000000000// 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" "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 /* UnixAddressPrefix holds the prefix to use to enable Unix Domain Socket traffic instead of UDP. */ const UnixAddressPrefix = "unix://" /* Stat suffixes */ var ( gaugeSuffix = []byte("|g") countSuffix = []byte("|c") histogramSuffix = []byte("|h") distributionSuffix = []byte("|d") decrSuffix = []byte("-1|c") incrSuffix = []byte("1|c") setSuffix = []byte("|s") timingSuffix = []byte("|ms") ) // A statsdWriter offers a standard interface regardless of the underlying // protocol. For now UDS and UPD writers are available. type statsdWriter interface { Write(data []byte) (n int, err error) SetWriteTimeout(time.Duration) error Close() error } // A Client is a handle for sending messages to dogstatsd. It is safe to // use one Client from multiple goroutines simultaneously. type Client struct { // Writer handles the underlying networking protocol writer statsdWriter // Namespace to prepend to all statsd calls Namespace string // Tags are global tags to be added to every statsd call Tags []string // skipErrors turns off error passing and allows UDS to emulate UDP behaviour SkipErrors bool // BufferLength is the length of the buffer in commands. bufferLength int flushTime time.Duration commands []string buffer bytes.Buffer stop chan struct{} sync.Mutex } // New returns a pointer to a new Client given an addr in the format "hostname:port" or // "unix:///path/to/socket". func New(addr string) (*Client, error) { if strings.HasPrefix(addr, UnixAddressPrefix) { w, err := newUdsWriter(addr[len(UnixAddressPrefix)-1:]) if err != nil { return nil, err } return NewWithWriter(w) } w, err := newUDPWriter(addr) if err != nil { return nil, err } return NewWithWriter(w) } // NewWithWriter creates a new Client with given writer. Writer is a // io.WriteCloser + SetWriteTimeout(time.Duration) error func NewWithWriter(w statsdWriter) (*Client, error) { client := &Client{writer: w, SkipErrors: false} 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 client.stop = make(chan struct{}, 1) 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 string, value interface{}, suffix []byte, tags []string, rate float64) string { var buf bytes.Buffer if c.Namespace != "" { buf.WriteString(c.Namespace) } buf.WriteString(name) buf.WriteString(":") switch val := value.(type) { case float64: buf.Write(strconv.AppendFloat([]byte{}, val, 'f', 6, 64)) case int64: buf.Write(strconv.AppendInt([]byte{}, val, 10)) case string: buf.WriteString(val) default: // do nothing } buf.Write(suffix) if rate < 1 { buf.WriteString(`|@`) buf.WriteString(strconv.FormatFloat(rate, 'f', -1, 64)) } writeTagString(&buf, c.Tags, tags) return buf.String() } // SetWriteTimeout allows the user to set a custom UDS write timeout. Not supported for UDP. func (c *Client) SetWriteTimeout(d time.Duration) error { if c == nil { return nil } return c.writer.SetWriteTimeout(d) } func (c *Client) watch() { ticker := time.NewTicker(c.flushTime) for { select { case <-ticker.C: c.Lock() if len(c.commands) > 0 { // FIXME: eating error here c.flushLocked() } c.Unlock() case <-c.stop: ticker.Stop() return } } } 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.flushLocked(); 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 forces a flush of the pending commands in the buffer func (c *Client) Flush() error { if c == nil { return nil } c.Lock() defer c.Unlock() return c.flushLocked() } // flush the commands in the buffer. Lock must be held by caller. func (c *Client) flushLocked() error { frames, flushable := c.joinMaxSize(c.commands, "\n", OptimalPayloadSize) var err error cmdsFlushed := 0 for i, data := range frames { _, e := c.writer.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.writer.Write([]byte(msg)) if c.SkipErrors { return nil } return err } // send handles sampling and sends the message over UDP. It also adds global namespace prefixes and tags. func (c *Client) send(name string, value interface{}, suffix []byte, tags []string, rate float64) error { if c == nil { return nil } if rate < 1 && rand.Float64() > rate { return nil } data := c.format(name, value, suffix, 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 { return c.send(name, value, gaugeSuffix, tags, rate) } // Count tracks how many times something happened per second. func (c *Client) Count(name string, value int64, tags []string, rate float64) error { return c.send(name, value, countSuffix, tags, rate) } // Histogram tracks the statistical distribution of a set of values on each host. func (c *Client) Histogram(name string, value float64, tags []string, rate float64) error { return c.send(name, value, histogramSuffix, tags, rate) } // Distribution tracks the statistical distribution of a set of values across your infrastructure. func (c *Client) Distribution(name string, value float64, tags []string, rate float64) error { return c.send(name, value, distributionSuffix, tags, rate) } // Decr is just Count of -1 func (c *Client) Decr(name string, tags []string, rate float64) error { return c.send(name, nil, decrSuffix, tags, rate) } // Incr is just Count of 1 func (c *Client) Incr(name string, tags []string, rate float64) error { return c.send(name, nil, incrSuffix, 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 { return c.send(name, value, setSuffix, 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 { return c.send(name, value, timingSuffix, 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 { if c == nil { return nil } 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 } select { case c.stop <- struct{}{}: default: } // if this client is buffered, flush before closing the writer if c.bufferLength > 0 { if err := c.Flush(); err != nil { return err } } return c.writer.Close() } // Events support // EventAlertType and EventAlertPriority became exported types after this issue was submitted: https://github.com/DataDog/datadog-go/issues/41 // The reason why they got exported is so that client code can directly use the types. // EventAlertType is the alert type for events 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" ) // EventPriority is the event priority for events 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 } // ServiceCheckStatus 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-2.1.0/statsd/statsd_benchmark_test.go000066400000000000000000000027261325753040300217350ustar00rootroot00000000000000package statsd import ( "fmt" "strconv" "testing" ) var statBytes []byte var stat string // Results: // BenchmarkStatBuildGauge_Sprintf-8 500 45699958 ns/op // BenchmarkStatBuildGauge_Concat-8 1000 23452863 ns/op // BenchmarkStatBuildGauge_BytesAppend-8 1000 21705121 ns/op func BenchmarkStatBuildGauge_Sprintf(b *testing.B) { for n := 0; n < b.N; n++ { for x := 0; x < 100000; x++ { stat = fmt.Sprintf("%f|g", 3.14159) } } } func BenchmarkStatBuildGauge_Concat(b *testing.B) { for n := 0; n < b.N; n++ { for x := 0; x < 100000; x++ { stat = strconv.FormatFloat(3.14159, 'f', -1, 64) + "|g" } } } func BenchmarkStatBuildGauge_BytesAppend(b *testing.B) { suffix := []byte("|g") for n := 0; n < b.N; n++ { for x := 0; x < 100000; x++ { statBytes = []byte{} statBytes = append(strconv.AppendFloat(statBytes, 3.14159, 'f', -1, 64), suffix...) } } } func BenchmarkStatBuildCount_Sprintf(b *testing.B) { for n := 0; n < b.N; n++ { for x := 0; x < 100000; x++ { stat = fmt.Sprintf("%d|c", 314) } } } func BenchmarkStatBuildCount_Concat(b *testing.B) { for n := 0; n < b.N; n++ { for x := 0; x < 100000; x++ { stat = strconv.FormatInt(314, 10) + "|c" } } } func BenchmarkStatBuildCount_BytesAppend(b *testing.B) { suffix := []byte("|c") for n := 0; n < b.N; n++ { for x := 0; x < 100000; x++ { statBytes = []byte{} statBytes = append(strconv.AppendInt(statBytes, 314, 10), suffix...) } } } datadog-go-2.1.0/statsd/statsd_test.go000066400000000000000000000631121325753040300177170ustar00rootroot00000000000000// Copyright 2013 Ooyala, Inc. package statsd import ( "bytes" "fmt" "io" "io/ioutil" "net" "os" "path/filepath" "reflect" "strconv" "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, "Distribution", "test.distribution", 2.3, []string{"tagA"}, 1.0, "test.distribution:2.300000|d|#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 TestClientUDP(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) } clientTest(t, server, client) } type statsdWriterWrapper struct { io.WriteCloser } func (statsdWriterWrapper) SetWriteTimeout(time.Duration) error { return nil } func TestClientWithConn(t *testing.T) { server, conn, err := os.Pipe() if err != nil { t.Fatal(err) } client, err := NewWithWriter(statsdWriterWrapper{conn}) if err != nil { t.Fatal(err) } clientTest(t, server, client) } func clientTest(t *testing.T, server io.Reader, client *Client) { 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 TestClientUDS(t *testing.T) { dir, err := ioutil.TempDir("", "socket") if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) // clean up addr := filepath.Join(dir, "dsd.socket") udsAddr, err := net.ResolveUnixAddr("unixgram", addr) if err != nil { t.Fatal(err) } server, err := net.ListenUnixgram("unixgram", udsAddr) if err != nil { t.Fatal(err) } defer server.Close() addrParts := []string{UnixAddressPrefix, addr} client, err := New(strings.Join(addrParts, "")) 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 TestClientUDSClose(t *testing.T) { dir, err := ioutil.TempDir("", "socket") if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) // clean up addr := filepath.Join(dir, "dsd.socket") addrParts := []string{UnixAddressPrefix, addr} client, err := New(strings.Join(addrParts, "")) if err != nil { t.Fatal(err) } assertNotPanics(t, func() { client.Close() }) } 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() bufferLength := 9 client, err := NewBuffered(addr, bufferLength) if err != nil { t.Fatal(err) } 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.Distribution("dd", 1, nil, 1) client.Timing("tt", dur, nil, 1) client.Set("ss", "ss", nil, 1) if len(client.commands) != (bufferLength - 1) { t.Errorf("Expected client to have buffered %d commands, but found %d\n", (bufferLength - 1), len(client.commands)) } client.Set("ss", "xx", nil, 1) client.Lock() err = client.flushLocked() client.Unlock() 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.dd:1.000000|d|#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)) } client.Lock() err = client.flushLocked() client.Unlock() 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 TestBufferedClientBackground(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() bufferLength := 5 client, err := NewBuffered(addr, bufferLength) if err != nil { t.Fatal(err) } defer client.Close() client.Namespace = "foo." client.Tags = []string{"dd:2"} client.Count("cc", 1, nil, 1) client.Gauge("gg", 10, nil, 1) client.Histogram("hh", 1, nil, 1) client.Distribution("dd", 1, nil, 1) client.Set("ss", "ss", nil, 1) client.Set("ss", "xx", nil, 1) time.Sleep(client.flushTime * 2) client.Lock() if len(client.commands) != 0 { t.Errorf("Watch goroutine should have flushed commands, but found %d\n", len(client.commands)) } client.Unlock() } func TestBufferedClientFlush(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() bufferLength := 5 client, err := NewBuffered(addr, bufferLength) if err != nil { t.Fatal(err) } defer client.Close() client.Namespace = "foo." client.Tags = []string{"dd:2"} client.Count("cc", 1, nil, 1) client.Gauge("gg", 10, nil, 1) client.Histogram("hh", 1, nil, 1) client.Distribution("dd", 1, nil, 1) client.Set("ss", "ss", nil, 1) client.Set("ss", "xx", nil, 1) client.Flush() client.Lock() if len(client.commands) != 0 { t.Errorf("Flush should have flushed commands, but found %d\n", len(client.commands)) } client.Unlock() } 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 TestSendMsgUDP(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) } 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") } message := "test message" err = client.sendMsg(message) 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 != len(message) { t.Fatalf("Failed to read full message from buffer. Got size `%d` expected `%d`", n, MaxUDPPayloadSize) } if string(buffer[:n]) != message { t.Fatalf("The received message did not match what we expect.") } client, err = NewBuffered(addr, 1) if err != nil { t.Fatal(err) } 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(message) 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.flushLocked() 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 != len(message) { t.Fatalf("Failed to read full message from buffer. Got size `%d` expected `%d`", n, MaxUDPPayloadSize) } if string(buffer[:n]) != message { t.Fatalf("The received message did not match what we expect.") } } func TestSendUDSErrors(t *testing.T) { dir, err := ioutil.TempDir("", "socket") if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) // clean up message := "test message" addr := filepath.Join(dir, "dsd.socket") udsAddr, err := net.ResolveUnixAddr("unixgram", addr) if err != nil { t.Fatal(err) } addrParts := []string{UnixAddressPrefix, addr} client, err := New(strings.Join(addrParts, "")) if err != nil { t.Fatal(err) } // Server not listening yet err = client.sendMsg(message) if err == nil || !strings.HasSuffix(err.Error(), "no such file or directory") { t.Errorf("Expected error \"no such file or directory\", got: %s", err.Error()) } // Start server and send packet server, err := net.ListenUnixgram("unixgram", udsAddr) if err != nil { t.Fatal(err) } err = client.sendMsg(message) if err != nil { t.Errorf("Expected no error to be returned when server is listening, got: %s", err.Error()) } bytes := make([]byte, 1024) n, err := server.Read(bytes) if err != nil { t.Fatal(err) } if string(bytes[:n]) != message { t.Errorf("Expected: %s. Actual: %s", string(message), string(bytes)) } // close server and send packet server.Close() os.Remove(addr) err = client.sendMsg(message) if err == nil { t.Error("Expected an error, got nil") } // Restart server and send packet server, err = net.ListenUnixgram("unixgram", udsAddr) if err != nil { t.Fatal(err) } time.Sleep(100 * time.Millisecond) defer server.Close() err = client.sendMsg(message) if err != nil { t.Errorf("Expected no error to be returned when server is listening, got: %s", err.Error()) } bytes = make([]byte, 1024) n, err = server.Read(bytes) if err != nil { t.Fatal(err) } if string(bytes[:n]) != message { t.Errorf("Expected: %s. Actual: %s", string(message), string(bytes)) } } func TestSendUDSIgnoreErrors(t *testing.T) { client, err := New("unix:///invalid") if err != nil { t.Fatal(err) } // Default mode throws error err = client.sendMsg("message") if err == nil || !strings.HasSuffix(err.Error(), "no such file or directory") { t.Errorf("Expected error \"connect: no such file or directory\", got: %s", err.Error()) } // Skip errors client.SkipErrors = true err = client.sendMsg("message") if err != nil { t.Errorf("Expected no error to be returned when in skip errors mode, got: %s", err.Error()) } } func TestNilSafe(t *testing.T) { var c *Client assertNotPanics(t, func() { c.SetWriteTimeout(0) }) assertNotPanics(t, func() { c.Flush() }) 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.Distribution("", 0, nil, 1) }) assertNotPanics(t, func() { c.Gauge("", 0, nil, 1) }) assertNotPanics(t, func() { c.Set("", "", nil, 1) }) assertNotPanics(t, func() { c.send("", "", []byte(""), nil, 1) }) assertNotPanics(t, func() { c.Event(NewEvent("", "")) }) assertNotPanics(t, func() { c.SimpleEvent("", "") }) assertNotPanics(t, func() { c.ServiceCheck(NewServiceCheck("", Ok)) }) assertNotPanics(t, func() { c.SimpleServiceCheck("", Ok) }) } 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.") } } func TestFlushOnClose(t *testing.T) { client, err := NewBuffered("localhost:1201", 64) if err != nil { t.Fatal(err) } // stop the flushing mechanism so we can test the buffer without interferences client.stop <- struct{}{} message := "test message" err = client.sendMsg(message) if err != nil { t.Fatal(err) } if len(client.commands) != 1 { t.Errorf("Commands buffer should contain 1 item, got %d", len(client.commands)) } err = client.Close() if err != nil { t.Fatal(err) } if len(client.commands) != 0 { t.Errorf("Commands buffer should be empty, got %d", len(client.commands)) } } // These benchmarks show that using different format options: // v1: sprintf-ing together a bunch of intermediate strings is 4-5x faster // v2: some use of buffer // v3: removing sprintf from stat generation and pushing stat building into format func BenchmarkFormatV3(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, gaugeSuffix, []string{"foo"}, 1) c.format("system.cpu.load", 0.1, gaugeSuffix, nil, 0.9) } } func BenchmarkFormatV1(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.formatV1("system.cpu.idle", 10, []string{"foo"}, 1) c.formatV1("system.cpu.load", 0.1, nil, 0.9) } } // V1 formatting function, added to client for tests func (c *Client) formatV1(name string, value float64, tags []string, rate float64) string { valueAsString := fmt.Sprintf("%f|g", value) if rate < 1 { valueAsString = fmt.Sprintf("%s|@%f", valueAsString, rate) } if c.Namespace != "" { name = fmt.Sprintf("%s%s", c.Namespace, name) } tags = append(c.Tags, tags...) if len(tags) > 0 { valueAsString = fmt.Sprintf("%s|#%s", valueAsString, strings.Join(tags, ",")) } return fmt.Sprintf("%s:%s", name, valueAsString) } func BenchmarkFormatV2(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.formatV2("system.cpu.idle", 10, []string{"foo"}, 1) c.formatV2("system.cpu.load", 0.1, nil, 0.9) } } // V2 formatting function, added to client for tests func (c *Client) formatV2(name string, value float64, tags []string, rate float64) string { var buf bytes.Buffer if c.Namespace != "" { buf.WriteString(c.Namespace) } buf.WriteString(name) buf.WriteString(":") buf.WriteString(fmt.Sprintf("%f|g", value)) if rate < 1 { buf.WriteString(`|@`) buf.WriteString(strconv.FormatFloat(rate, 'f', -1, 64)) } writeTagString(&buf, c.Tags, tags) return buf.String() } datadog-go-2.1.0/statsd/udp.go000066400000000000000000000016561325753040300161530ustar00rootroot00000000000000package statsd import ( "errors" "net" "time" ) // udpWriter is an internal class wrapping around management of UDP connection type udpWriter struct { conn net.Conn } // New returns a pointer to a new udpWriter given an addr in the format "hostname:port". func newUDPWriter(addr string) (*udpWriter, 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 } writer := &udpWriter{conn: conn} return writer, nil } // SetWriteTimeout is not needed for UDP, returns error func (w *udpWriter) SetWriteTimeout(d time.Duration) error { return errors.New("SetWriteTimeout: not supported for UDP connections") } // Write data to the UDP connection with no error handling func (w *udpWriter) Write(data []byte) (int, error) { return w.conn.Write(data) } func (w *udpWriter) Close() error { return w.conn.Close() } datadog-go-2.1.0/statsd/uds.go000066400000000000000000000033641325753040300161540ustar00rootroot00000000000000package statsd import ( "net" "time" ) /* UDSTimeout holds the default timeout for UDS socket writes, as they can get blocking when the receiving buffer is full. */ const defaultUDSTimeout = 1 * time.Millisecond // udsWriter is an internal class wrapping around management of UDS connection type udsWriter struct { // Address to send metrics to, needed to allow reconnection on error addr net.Addr // Established connection object, or nil if not connected yet conn net.Conn // write timeout writeTimeout time.Duration } // New returns a pointer to a new udsWriter given a socket file path as addr. func newUdsWriter(addr string) (*udsWriter, error) { udsAddr, err := net.ResolveUnixAddr("unixgram", addr) if err != nil { return nil, err } // Defer connection to first Write writer := &udsWriter{addr: udsAddr, conn: nil, writeTimeout: defaultUDSTimeout} return writer, nil } // SetWriteTimeout allows the user to set a custom write timeout func (w *udsWriter) SetWriteTimeout(d time.Duration) error { w.writeTimeout = d return nil } // Write data to the UDS connection with write timeout and minimal error handling: // create the connection if nil, and destroy it if the statsd server has disconnected func (w *udsWriter) Write(data []byte) (int, error) { // Try connecting (first packet or connection lost) if w.conn == nil { conn, err := net.Dial(w.addr.Network(), w.addr.String()) if err != nil { return 0, err } w.conn = conn } w.conn.SetWriteDeadline(time.Now().Add(w.writeTimeout)) n, e := w.conn.Write(data) if e != nil { // Statsd server disconnected, retry connecting at next packet w.conn = nil return 0, e } return n, e } func (w *udsWriter) Close() error { if w.conn != nil { return w.conn.Close() } return nil }