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) | |||
| } | |||
| } | |||