git-vendor-name: datadog-go git-vendor-dir: vendor/github.com/datadog/datadog-go git-vendor-repository: https://github.com/datadog/datadog-go git-vendor-ref: masterpull/1/head
| @ -0,0 +1,8 @@ | |||||
| language: go | |||||
| go: | |||||
| - 1.4 | |||||
| - 1.5 | |||||
| script: | |||||
| - go test -v ./... | |||||
| @ -0,0 +1,19 @@ | |||||
| Copyright (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. | |||||
| @ -0,0 +1,32 @@ | |||||
| # Overview | |||||
| Packages in `datadog-go` provide Go clients for various APIs at [DataDog](http://datadoghq.com). | |||||
| ## Statsd | |||||
| [](https://godoc.org/github.com/DataDog/datadog-go/statsd) | |||||
| [](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. | |||||
| @ -0,0 +1,45 @@ | |||||
| ## 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") | |||||
| err = c.Gauge("request.duration", 1.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). | |||||
| @ -0,0 +1,448 @@ | |||||
| // 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" | |||||
| "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)) | |||||
| } | |||||
| // do not append to c.Tags directly, because it's shared | |||||
| // across all invocations of this function | |||||
| tagCopy := make([]string, len(c.Tags), len(c.Tags)+len(tags)) | |||||
| copy(tagCopy, c.Tags) | |||||
| tags = append(tagCopy, tags...) | |||||
| if len(tags) > 0 { | |||||
| buf.WriteString("|#") | |||||
| buf.WriteString(tags[0]) | |||||
| for _, tag := range tags[1:] { | |||||
| buf.WriteString(",") | |||||
| buf.WriteString(tag) | |||||
| } | |||||
| } | |||||
| 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.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 { | |||||
| // if this client is buffered, then we'll just append this | |||||
| c.Lock() | |||||
| defer c.Unlock() | |||||
| if c.bufferLength > 0 { | |||||
| // return an error if message is bigger than OptimalPayloadSize | |||||
| if len(msg) > MaxUDPPayloadSize { | |||||
| return errors.New("message size exceeds MaxUDPPayloadSize") | |||||
| } | |||||
| 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) | |||||
| } | |||||
| // 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) | |||||
| } | |||||
| // 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 { | |||||
| 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) | |||||
| } | |||||
| // 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)) | |||||
| } | |||||
| if len(tags)+len(e.Tags) > 0 { | |||||
| all := make([]string, 0, len(tags)+len(e.Tags)) | |||||
| all = append(all, tags...) | |||||
| all = append(all, e.Tags...) | |||||
| buffer.WriteString("|#") | |||||
| buffer.WriteString(all[0]) | |||||
| for _, tag := range all[1:] { | |||||
| buffer.WriteString(",") | |||||
| buffer.WriteString(tag) | |||||
| } | |||||
| } | |||||
| return buffer.String(), nil | |||||
| } | |||||
| func (e Event) escapedText() string { | |||||
| return strings.Replace(e.Text, "\n", "\\n", -1) | |||||
| } | |||||
| @ -0,0 +1,454 @@ | |||||
| // Copyright 2013 Ooyala, Inc. | |||||
| package statsd | |||||
| import ( | |||||
| "fmt" | |||||
| "io" | |||||
| "net" | |||||
| "reflect" | |||||
| "strings" | |||||
| "testing" | |||||
| ) | |||||
| 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"}, | |||||
| } | |||||
| 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 := 5 | |||||
| client := &Client{ | |||||
| conn: conn, | |||||
| commands: make([]string, 0, bufferLength), | |||||
| bufferLength: bufferLength, | |||||
| } | |||||
| 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.Set("ss", "ss", nil, 1) | |||||
| if len(client.commands) != 4 { | |||||
| t.Errorf("Expected client to have buffered 4 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.cc:1|c|#dd:2`, | |||||
| `foo.gg:10.000000|g|#dd:2`, | |||||
| `foo.hh:1.000000|h|#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) { | |||||
| c := Client{bufferLength: 1} | |||||
| err := c.sendMsg(strings.Repeat("x", OptimalPayloadSize)) | |||||
| if err != nil { | |||||
| t.Errorf("Expected no error to be returned if message size is smaller or equal to MaxPayloadSize, got: %s", err.Error()) | |||||
| } | |||||
| err = c.sendMsg(strings.Repeat("x", OptimalPayloadSize+1)) | |||||
| if err == nil { | |||||
| t.Error("Expected error to be returned if message size is bigger that MaxPayloadSize") | |||||
| } | |||||
| } | |||||
| 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) }) | |||||
| } | |||||
| 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`, | |||||
| }, | |||||
| } | |||||
| 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.") | |||||
| } | |||||
| } | |||||
| // 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) | |||||
| } | |||||
| } | |||||