├── scripts └── test.sh ├── phony.go ├── terraform ├── cluster │ ├── output.tf │ ├── gcp │ │ ├── output.tf │ │ ├── variables.tf │ │ ├── network.tf │ │ ├── main.tf │ │ └── security.tf │ ├── aws │ │ ├── outputs.tf │ │ ├── variables.tf │ │ ├── alb.tf │ │ ├── security.tf │ │ ├── main.tf │ │ └── network.tf │ └── main.tf └── application │ └── main.tf ├── server ├── Dockerfile ├── Makefile └── main.go ├── .gitignore ├── vendor └── github.com │ └── DataDog │ └── datadog-go │ ├── .travis.yml │ ├── statsd │ ├── udp.go │ ├── statsd_benchmark_test.go │ ├── uds.go │ ├── README.md │ ├── statsd.go │ └── statsd_test.go │ ├── LICENSE.txt │ ├── README.md │ └── CHANGELOG.md ├── Gopkg.lock ├── Gopkg.toml ├── nomad_job_files ├── binary.hcl ├── app.hcl ├── app_with_vault.hcl └── system.hcl └── README.md /scripts/test.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /phony.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | } 5 | -------------------------------------------------------------------------------- /terraform/cluster/output.tf: -------------------------------------------------------------------------------- 1 | output "aws_alb" { 2 | value = "${module.aws.alb}" 3 | } 4 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | COPY ./test-server . 4 | 5 | ENTRYPOINT ["./test-server"] 6 | -------------------------------------------------------------------------------- /terraform/cluster/gcp/output.tf: -------------------------------------------------------------------------------- 1 | output "network" { 2 | value = "${google_compute_network.nomad.name}" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | terraform.tfstate 3 | .terraform 4 | .terraform.tfstate.lock.info 5 | terraform.tfstate.backup 6 | test-server 7 | flow.md 8 | -------------------------------------------------------------------------------- /vendor/github.com/DataDog/datadog-go/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.5 5 | - 1.6 6 | - 1.7 7 | - 1.8 8 | - 1.9 9 | 10 | script: 11 | - go test -race -v ./... 12 | -------------------------------------------------------------------------------- /terraform/application/main.tf: -------------------------------------------------------------------------------- 1 | provider "nomad" { 2 | address = "http://localhost:5646" 3 | region = "gcp" 4 | } 5 | 6 | resource "nomad_job" "application" { 7 | jobspec = "${file("../../nomad_job_files/binary.hcl")}" 8 | } 9 | -------------------------------------------------------------------------------- /server/Makefile: -------------------------------------------------------------------------------- 1 | build_linux: 2 | CGO_ENABLED=0 GOOS=linux go build -o test-server . 3 | 4 | build_docker: build_linux 5 | docker build -t nicholasjackson/test-server:latest . 6 | 7 | push_docker: 8 | docker push nicholasjackson/test-server:latest 9 | -------------------------------------------------------------------------------- /terraform/cluster/gcp/variables.tf: -------------------------------------------------------------------------------- 1 | variable "namespace" { 2 | default = "nomadgcp" 3 | } 4 | 5 | variable "region" { 6 | default = "us-central1" 7 | } 8 | 9 | variable "zone" { 10 | default = "us-central1-a" 11 | } 12 | 13 | variable "vpc_cidr_block" { 14 | default = "10.128.0.0/20" 15 | } 16 | -------------------------------------------------------------------------------- /terraform/cluster/aws/outputs.tf: -------------------------------------------------------------------------------- 1 | output "security_group_id" { 2 | value = "${aws_security_group.nomad.id}" 3 | } 4 | 5 | output "vpc_id" { 6 | value = "${aws_vpc.default.id}" 7 | } 8 | 9 | output "route_table_id" { 10 | value = "${aws_vpc.default.main_route_table_id}" 11 | } 12 | 13 | output "alb" { 14 | value = "${aws_alb.nomad.dns_name}" 15 | } 16 | -------------------------------------------------------------------------------- /terraform/cluster/gcp/network.tf: -------------------------------------------------------------------------------- 1 | resource "google_compute_network" "nomad" { 2 | name = "${var.namespace}-nomad" 3 | auto_create_subnetworks = "true" 4 | } 5 | 6 | /* 7 | resource "google_compute_subnetwork" "nomad" { 8 | name = "${var.namespace}-nomad-subnetwork" 9 | ip_cidr_range = "${var.cidr_block}" 10 | network = "${google_compute_network.nomad.self_link}" 11 | region = "${var.region}" 12 | } 13 | */ 14 | 15 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/DataDog/datadog-go" 6 | packages = ["statsd"] 7 | revision = "9487d3a9d3be5bf3cf60b86ae810b97926964515" 8 | version = "2.0.0" 9 | 10 | [solve-meta] 11 | analyzer-name = "dep" 12 | analyzer-version = 1 13 | inputs-digest = "1a6b47c9cd76e3937f7e7768f1badb593ccd8858d128eb15c1b3503f0cf7a53a" 14 | solver-name = "gps-cdcl" 15 | solver-version = 1 16 | -------------------------------------------------------------------------------- /terraform/cluster/aws/variables.tf: -------------------------------------------------------------------------------- 1 | variable "namespace" { 2 | default = "nomad-multi-cloud" 3 | } 4 | 5 | variable "ssh_key" { 6 | default = "~/.ssh/id_rsa.pub" 7 | description = "SSH public key to add to instances" 8 | } 9 | 10 | variable "vpc_cidr_block" { 11 | default = "10.0.0.0/16" 12 | } 13 | 14 | variable "azs" { 15 | default = ["eu-west-1a", "eu-west-1b", "eu-west-1c"] 16 | } 17 | 18 | variable "private_subnets" { 19 | default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] 20 | } 21 | 22 | variable "instance_type" { 23 | default = "t2.micro" 24 | } 25 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "github.com/DataDog/datadog-go" 26 | version = "2.0.0" 27 | -------------------------------------------------------------------------------- /terraform/cluster/gcp/main.tf: -------------------------------------------------------------------------------- 1 | module "gcp" { 2 | source = "nicholasjackson/hashicorp-suite/gcp" 3 | version = "0.2.0" 4 | 5 | namespace = "${var.namespace}" 6 | zone = "${var.zone}" 7 | 8 | min_servers = "3" 9 | max_servers = "5" 10 | min_agents = "5" 11 | max_agents = "8" 12 | 13 | vpc_id = "${google_compute_network.nomad.name}" 14 | key_name = "~/.ssh/id_rsa.pub" 15 | 16 | /* 17 | client_target_groups = ["${aws_alb_target_group.proxy.arn}"] 18 | server_target_groups = ["${aws_alb_target_group.nomad.arn}"] 19 | */ 20 | 21 | consul_enabled = true 22 | consul_version = "1.0.6" 23 | consul_join_tag_key = "autojoin" 24 | consul_join_tag_value = "${var.namespace}" 25 | nomad_enabled = true 26 | nomad_version = "0.7.1" 27 | } 28 | -------------------------------------------------------------------------------- /terraform/cluster/aws/alb.tf: -------------------------------------------------------------------------------- 1 | resource "aws_alb" "nomad" { 2 | name = "${var.namespace}-nomad" 3 | internal = false 4 | security_groups = ["${aws_security_group.nomad.id}"] 5 | subnets = ["${aws_subnet.default.*.id}"] 6 | } 7 | 8 | resource "aws_alb_target_group" "nomad" { 9 | name = "${var.namespace}-nomad" 10 | port = 80 11 | protocol = "HTTP" 12 | vpc_id = "${aws_vpc.default.id}" 13 | 14 | health_check { 15 | path = "/" 16 | matcher = "200,202" 17 | } 18 | } 19 | 20 | resource "aws_alb_listener" "nomad" { 21 | load_balancer_arn = "${aws_alb.nomad.arn}" 22 | port = "80" 23 | protocol = "HTTP" 24 | 25 | default_action { 26 | target_group_arn = "${aws_alb_target_group.nomad.arn}" 27 | type = "forward" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /terraform/cluster/gcp/security.tf: -------------------------------------------------------------------------------- 1 | resource "google_compute_firewall" "internal" { 2 | name = "internal" 3 | network = "${google_compute_network.nomad.name}" 4 | source_ranges = ["${var.vpc_cidr_block}"] 5 | 6 | allow { 7 | protocol = "icmp" 8 | } 9 | 10 | allow { 11 | protocol = "tcp" 12 | ports = ["0-65535"] 13 | } 14 | 15 | allow { 16 | protocol = "udp" 17 | ports = ["0-65535"] 18 | } 19 | } 20 | 21 | resource "google_compute_firewall" "external" { 22 | name = "external" 23 | network = "${google_compute_network.nomad.name}" 24 | source_ranges = ["0.0.0.0/0"] 25 | 26 | allow { 27 | protocol = "icmp" 28 | } 29 | 30 | allow { 31 | protocol = "tcp" 32 | ports = ["22"] 33 | } 34 | 35 | allow { 36 | protocol = "udp" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /terraform/cluster/aws/security.tf: -------------------------------------------------------------------------------- 1 | resource "aws_key_pair" "nomad" { 2 | key_name = "${var.namespace}-nomad" 3 | public_key = "${file("${var.ssh_key}")}" 4 | } 5 | 6 | resource "aws_security_group" "nomad" { 7 | name = "${var.namespace}.nomad" 8 | description = "Allow SSH Externally, everything on internal VPC" 9 | vpc_id = "${aws_vpc.default.id}" 10 | 11 | ingress { 12 | from_port = 22 13 | to_port = 22 14 | protocol = "tcp" 15 | cidr_blocks = ["0.0.0.0/0"] 16 | } 17 | 18 | ingress { 19 | from_port = 80 20 | to_port = 80 21 | protocol = "tcp" 22 | cidr_blocks = ["0.0.0.0/0"] 23 | } 24 | 25 | ingress { 26 | from_port = 0 27 | to_port = 0 28 | protocol = "-1" 29 | cidr_blocks = ["${var.vpc_cidr_block}"] 30 | } 31 | 32 | egress { 33 | from_port = 0 34 | to_port = 0 35 | protocol = "-1" 36 | cidr_blocks = ["0.0.0.0/0"] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /terraform/cluster/main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" {} 2 | 3 | provider "google" { 4 | region = "us-central1" 5 | project = "nomad-multi-cloud" 6 | } 7 | 8 | # Create AWS resources including a Nomad cluster and network 9 | module "aws" { 10 | source = "./aws" 11 | 12 | vpc_cidr_block = "10.0.0.0/16" 13 | } 14 | 15 | # Create GCP resources including a Nomad cluster and network 16 | module "gcp" { 17 | source = "./gcp" 18 | 19 | vpc_cidr_block = "10.128.0.0/20" 20 | } 21 | 22 | # Create a VPN connection between GCP and AWS 23 | module "vpn" { 24 | source = "nicholasjackson/gcp-vpn/aws" 25 | version = "0.1.0" 26 | 27 | aws_cidr = "10.0.0.0/16" 28 | gcp_cidr = "10.128.0.0/20" 29 | 30 | aws_region = "eu-west-1" 31 | gcp_region = "us-central1" 32 | 33 | gcp_network = "${module.gcp.network}" 34 | 35 | aws_vpc = "${module.aws.vpc_id}" 36 | aws_sg = "${module.aws.security_group_id}" 37 | aws_route_table_id = "${module.aws.route_table_id}" 38 | } 39 | -------------------------------------------------------------------------------- /terraform/cluster/aws/main.tf: -------------------------------------------------------------------------------- 1 | module "suite" { 2 | source = "/Users/nicj/Developer/terraform/terraform-aws-hashicorp-suite" 3 | 4 | //source = "nicholasjackson/hashicorp-suite/aws" 5 | //version = "0.2.1" 6 | 7 | namespace = "${var.namespace}" 8 | min_servers = "1" 9 | max_servers = "3" 10 | min_agents = "3" 11 | max_agents = "5" 12 | subnets = ["${aws_subnet.default.*.id}"] 13 | vpc_id = "${aws_vpc.default.id}" 14 | key_name = "${aws_key_pair.nomad.id}" 15 | security_group = "${aws_security_group.nomad.id}" 16 | client_target_groups = ["${aws_alb_target_group.nomad.arn}"] 17 | consul_enabled = true 18 | consul_version = "1.0.6" 19 | consul_join_tag_key = "autojoin" 20 | consul_join_tag_value = "${var.namespace}" 21 | nomad_enabled = true 22 | nomad_version = "0.8.0-rc1" 23 | //nomad_version = "0.7.1" 24 | vault_enabled = true 25 | vault_version = "0.9.6" 26 | } 27 | -------------------------------------------------------------------------------- /vendor/github.com/DataDog/datadog-go/statsd/udp.go: -------------------------------------------------------------------------------- 1 | package statsd 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "time" 7 | ) 8 | 9 | // udpWriter is an internal class wrapping around management of UDP connection 10 | type udpWriter struct { 11 | conn net.Conn 12 | } 13 | 14 | // New returns a pointer to a new udpWriter given an addr in the format "hostname:port". 15 | func newUDPWriter(addr string) (*udpWriter, error) { 16 | udpAddr, err := net.ResolveUDPAddr("udp", addr) 17 | if err != nil { 18 | return nil, err 19 | } 20 | conn, err := net.DialUDP("udp", nil, udpAddr) 21 | if err != nil { 22 | return nil, err 23 | } 24 | writer := &udpWriter{conn: conn} 25 | return writer, nil 26 | } 27 | 28 | // SetWriteTimeout is not needed for UDP, returns error 29 | func (w *udpWriter) SetWriteTimeout(d time.Duration) error { 30 | return errors.New("SetWriteTimeout: not supported for UDP connections") 31 | } 32 | 33 | // Write data to the UDP connection with no error handling 34 | func (w *udpWriter) Write(data []byte) (int, error) { 35 | return w.conn.Write(data) 36 | } 37 | 38 | func (w *udpWriter) Close() error { 39 | return w.conn.Close() 40 | } 41 | -------------------------------------------------------------------------------- /vendor/github.com/DataDog/datadog-go/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Datadog, Inc 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /terraform/cluster/aws/network.tf: -------------------------------------------------------------------------------- 1 | # Create a VPC to launch our instances into 2 | resource "aws_vpc" "default" { 3 | cidr_block = "${var.vpc_cidr_block}" 4 | enable_dns_hostnames = true 5 | 6 | tags { 7 | "Name" = "${var.namespace}" 8 | } 9 | } 10 | 11 | # Create an internet gateway to give our subnet access to the outside world 12 | resource "aws_internet_gateway" "default" { 13 | vpc_id = "${aws_vpc.default.id}" 14 | 15 | tags { 16 | "Name" = "${var.namespace}" 17 | } 18 | } 19 | 20 | # Grant the VPC internet access on its main route table 21 | resource "aws_route" "internet_access" { 22 | route_table_id = "${aws_vpc.default.main_route_table_id}" 23 | destination_cidr_block = "0.0.0.0/0" 24 | gateway_id = "${aws_internet_gateway.default.id}" 25 | } 26 | 27 | # Create a subnet to launch our instances into 28 | resource "aws_subnet" "default" { 29 | count = "${length(var.private_subnets)}" 30 | vpc_id = "${aws_vpc.default.id}" 31 | availability_zone = "${var.azs[count.index]}" 32 | cidr_block = "${var.private_subnets[count.index]}" 33 | map_public_ip_on_launch = true 34 | 35 | tags { 36 | "Name" = "${var.namespace}" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /nomad_job_files/binary.hcl: -------------------------------------------------------------------------------- 1 | job "test-app-binary" { 2 | datacenters = ["dc1"] 3 | 4 | type = "service" 5 | 6 | group "test-app" { 7 | count = 2 8 | 9 | task "test-app" { 10 | driver = "exec" 11 | 12 | artifact { 13 | source = "https://github.com/nicholasjackson/terraform-nomad-multicloud/releases/download/v0.1/test-server" 14 | destination = "local" 15 | } 16 | 17 | config { 18 | command = "local/test-server" 19 | 20 | args = [ 21 | "-statsd_addr", "${NOMAD_IP_http}:8125", 22 | "-port", "${NOMAD_PORT_http}", 23 | "-version", "v1.1", 24 | "-alloc_id","${NOMAD_ALLOC_ID}" 25 | ] 26 | } 27 | 28 | resources { 29 | cpu = 100 # 100 MHz 30 | memory = 64 # 128MB 31 | 32 | network { 33 | mbits = 1 34 | 35 | port "http" {} 36 | } 37 | } 38 | 39 | service { 40 | port = "http" 41 | name = "test-app" 42 | tags = ["microservice", "urlprefix-/"] 43 | 44 | check { 45 | type = "http" 46 | port = "http" 47 | interval = "10s" 48 | timeout = "2s" 49 | path = "/health" 50 | } 51 | } 52 | 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /vendor/github.com/DataDog/datadog-go/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/DataDog/datadog-go.svg?branch=master)](https://travis-ci.org/DataDog/datadog-go) 2 | # Overview 3 | 4 | Packages in `datadog-go` provide Go clients for various APIs at [DataDog](http://datadoghq.com). 5 | 6 | ## Statsd 7 | 8 | [![Godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/DataDog/datadog-go/statsd) 9 | [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](http://opensource.org/licenses/MIT) 10 | 11 | The [statsd](https://github.com/DataDog/datadog-go/tree/master/statsd) package provides a client for 12 | [dogstatsd](http://docs.datadoghq.com/guides/dogstatsd/): 13 | 14 | ```go 15 | import "github.com/DataDog/datadog-go/statsd" 16 | 17 | func main() { 18 | c, err := statsd.New("127.0.0.1:8125") 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | // prefix every metric with the app name 23 | c.Namespace = "flubber." 24 | // send the EC2 availability zone as a tag with every metric 25 | c.Tags = append(c.Tags, "us-east-1a") 26 | err = c.Gauge("request.duration", 1.2, nil, 1) 27 | // ... 28 | } 29 | ``` 30 | 31 | ## License 32 | 33 | All code distributed under the [MIT License](http://opensource.org/licenses/MIT) unless otherwise specified. 34 | -------------------------------------------------------------------------------- /nomad_job_files/app.hcl: -------------------------------------------------------------------------------- 1 | job "test-app" { 2 | datacenters = ["dc1"] 3 | 4 | type = "service" 5 | 6 | # All tasks in this job must run on linux. 7 | constraint { 8 | attribute = "${attr.kernel.name}" 9 | value = "linux" 10 | } 11 | 12 | group "test-app" { 13 | count = 5 14 | 15 | update { 16 | max_parallel = 1 17 | min_healthy_time = "10s" 18 | healthy_deadline = "2m" 19 | canary = 1 20 | } 21 | 22 | task "test-app" { 23 | driver = "docker" 24 | 25 | config { 26 | image = "nicholasjackson/test-server:latest" 27 | 28 | args = [ 29 | "-statsd_addr", "${NOMAD_IP_http}:8125", 30 | "-version", "v1.3", 31 | "-alloc_id","${NOMAD_ALLOC_ID}", 32 | ] 33 | 34 | port_map { 35 | http = 8080 36 | } 37 | } 38 | 39 | resources { 40 | cpu = 100 # 100 MHz 41 | memory = 64 # 128MB 42 | 43 | network { 44 | mbits = 1 45 | 46 | port "http" {} 47 | } 48 | } 49 | 50 | service { 51 | port = "http" 52 | name = "test-app" 53 | tags = ["microservice", "urlprefix-/"] 54 | 55 | check { 56 | type = "http" 57 | port = "http" 58 | interval = "10s" 59 | timeout = "2s" 60 | path = "/health" 61 | } 62 | } 63 | 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /vendor/github.com/DataDog/datadog-go/statsd/statsd_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package statsd 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | var statBytes []byte 10 | var stat string 11 | 12 | // Results: 13 | // BenchmarkStatBuildGauge_Sprintf-8 500 45699958 ns/op 14 | // BenchmarkStatBuildGauge_Concat-8 1000 23452863 ns/op 15 | // BenchmarkStatBuildGauge_BytesAppend-8 1000 21705121 ns/op 16 | func BenchmarkStatBuildGauge_Sprintf(b *testing.B) { 17 | for n := 0; n < b.N; n++ { 18 | for x := 0; x < 100000; x++ { 19 | stat = fmt.Sprintf("%f|g", 3.14159) 20 | } 21 | } 22 | } 23 | 24 | func BenchmarkStatBuildGauge_Concat(b *testing.B) { 25 | for n := 0; n < b.N; n++ { 26 | for x := 0; x < 100000; x++ { 27 | stat = strconv.FormatFloat(3.14159, 'f', -1, 64) + "|g" 28 | } 29 | } 30 | } 31 | 32 | func BenchmarkStatBuildGauge_BytesAppend(b *testing.B) { 33 | suffix := []byte("|g") 34 | 35 | for n := 0; n < b.N; n++ { 36 | for x := 0; x < 100000; x++ { 37 | statBytes = []byte{} 38 | statBytes = append(strconv.AppendFloat(statBytes, 3.14159, 'f', -1, 64), suffix...) 39 | } 40 | } 41 | } 42 | 43 | func BenchmarkStatBuildCount_Sprintf(b *testing.B) { 44 | for n := 0; n < b.N; n++ { 45 | for x := 0; x < 100000; x++ { 46 | stat = fmt.Sprintf("%d|c", 314) 47 | } 48 | } 49 | } 50 | 51 | func BenchmarkStatBuildCount_Concat(b *testing.B) { 52 | for n := 0; n < b.N; n++ { 53 | for x := 0; x < 100000; x++ { 54 | stat = strconv.FormatInt(314, 10) + "|c" 55 | } 56 | } 57 | } 58 | 59 | func BenchmarkStatBuildCount_BytesAppend(b *testing.B) { 60 | suffix := []byte("|c") 61 | 62 | for n := 0; n < b.N; n++ { 63 | for x := 0; x < 100000; x++ { 64 | statBytes = []byte{} 65 | statBytes = append(strconv.AppendInt(statBytes, 314, 10), suffix...) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /nomad_job_files/app_with_vault.hcl: -------------------------------------------------------------------------------- 1 | job "test-app" { 2 | datacenters = ["dc1"] 3 | 4 | type = "service" 5 | 6 | group "test-app" { 7 | count = 5 8 | 9 | task "test-app" { 10 | driver = "docker" 11 | 12 | config { 13 | image = "nicholasjackson/test-server:latest" 14 | 15 | args = [ 16 | "-statsd_addr", "${NOMAD_IP_http}:8125", 17 | "-version", "v1.1", 18 | "-alloc_id","${NOMAD_ALLOC_ID}", 19 | "-tls_key","secrets/key.pem", 20 | "-tls_cert","secrets/cert.pem" 21 | ] 22 | 23 | port_map { 24 | http = 8080 25 | } 26 | } 27 | 28 | vault { 29 | policies = ["my-policy"] 30 | } 31 | 32 | template { 33 | destination = "secrets/cert.pem" 34 | data = < 51 | $ nomad server-join 52 | ``` 53 | 54 | The federation process will work from either AWS or GCP however only needs to be performed once. 55 | -------------------------------------------------------------------------------- /vendor/github.com/DataDog/datadog-go/statsd/README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | Package `statsd` provides a Go [dogstatsd](http://docs.datadoghq.com/guides/dogstatsd/) client. Dogstatsd extends Statsd, adding tags 4 | and histograms. 5 | 6 | ## Get the code 7 | 8 | $ go get github.com/DataDog/datadog-go/statsd 9 | 10 | ## Usage 11 | 12 | ```go 13 | // Create the client 14 | c, err := statsd.New("127.0.0.1:8125") 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | // Prefix every metric with the app name 19 | c.Namespace = "flubber." 20 | // Send the EC2 availability zone as a tag with every metric 21 | c.Tags = append(c.Tags, "us-east-1a") 22 | 23 | // Do some metrics! 24 | err = c.Gauge("request.queue_depth", 12, nil, 1) 25 | err = c.Timing("request.duration", duration, nil, 1) // Uses a time.Duration! 26 | err = c.TimeInMilliseconds("request", 12, nil, 1) 27 | err = c.Incr("request.count_total", nil, 1) 28 | err = c.Decr("request.count_total", nil, 1) 29 | err = c.Count("request.count_total", 2, nil, 1) 30 | ``` 31 | 32 | ## Buffering Client 33 | 34 | 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. 35 | 36 | ## Unix Domain Sockets Client 37 | 38 | DogStatsD version 6 accepts packets through a Unix Socket datagram connection. You can use this protocol by giving a 39 | `unix:///path/to/dsd.socket` addr argument to the `New` or `NewBufferingClient`. 40 | 41 | With this protocol, writes can become blocking if the server's receiving buffer is full. Our default behaviour is to 42 | timeout and drop the packet after 1 ms. You can set a custom timeout duration via the `SetWriteTimeout` method. 43 | 44 | The default mode is to pass write errors from the socket to the caller. This includes write errors the library will 45 | automatically recover from (DogStatsD server not ready yet or is restarting). You can drop these errors and emulate 46 | the UDP behaviour by setting the `SkipErrors` property to `true`. Please note that packets will be dropped in both modes. 47 | 48 | ## Development 49 | 50 | Run the tests with: 51 | 52 | $ go test 53 | 54 | ## Documentation 55 | 56 | Please see: http://godoc.org/github.com/DataDog/datadog-go/statsd 57 | 58 | ## License 59 | 60 | go-dogstatsd is released under the [MIT license](http://www.opensource.org/licenses/mit-license.php). 61 | 62 | ## Credits 63 | 64 | Original code by [ooyala](https://github.com/ooyala/go-dogstatsd). 65 | -------------------------------------------------------------------------------- /vendor/github.com/DataDog/datadog-go/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changes 2 | 3 | # 2.0.0 / 2018-01-29 4 | 5 | ### Details 6 | 7 | Version `2.0.0` contains breaking changes and beta features, please refer to the 8 | _Notes_ section below for details. 9 | 10 | ### Notes 11 | 12 | * [BREAKING] `statsdWriter` now implements io.Writer interface. See [#46][]. 13 | * [BUGFIX] Flush buffer on close. See [#47][]. 14 | * [BETA] Add support for global distributions. See [#45][]. 15 | * [FEATURE] Add support for Unix Domain Sockets. See [#37][]. 16 | * [FEATURE] Export `eventAlertType` and `eventPriority`. See [#42][], thanks [@thomas91310][]. 17 | * [FEATURE] Export `Flush` method. See [#40][], thanks [@colega][]. 18 | * [BUGFIX] Prevent panics when closing the `udsWriter`. See [#43][], thanks [@jacek-adamek][]. 19 | * [IMPROVEMENT] Fix issues reported by Golint. See [#39][], thanks [@tariq1890][]. 20 | * [IMPROVEMENT] Improve message building speed by using less `fmt.Sprintf`s. See [#32][], thanks [@corsc][]. 21 | 22 | # 1.1.0 / 2017-04-27 23 | 24 | ### Notes 25 | 26 | * [FEATURE] Export serviceCheckStatus allowing interfaces to statsd.Client. See [#19][] (Thanks [@Jasrags][]) 27 | * [FEATURE] Client.sendMsg(). Check payload length for all messages. See [#25][] (Thanks [@theckman][]) 28 | * [BUGFIX] Remove new lines from tags. See [#21][] (Thanks [@sjung-stripe][]) 29 | * [BUGFIX] Do not panic on Client.Event when `nil`. See [#28][] 30 | * [DOCUMENTATION] Update `decr` documentation to match implementation. See [#30][] (Thanks [@kcollasarundell][]) 31 | 32 | # 1.0.0 / 2016-08-22 33 | 34 | ### Details 35 | 36 | We hadn't been properly versioning this project. We will begin to do so with this 37 | `1.0.0` release. We had some contributions in the past and would like to thank the 38 | contributors [@aviau][], [@sschepens][], [@jovanbrakus][], [@abtris][], [@tummychow][], [@gphat][], [@diasjorge][], 39 | [@victortrac][], [@seiffert][] and [@w-vi][], in no particular order, for their work. 40 | 41 | Below, for reference, the latest improvements made in 07/2016 - 08/2016 42 | 43 | ### Notes 44 | 45 | * [FEATURE] Implemented support for service checks. See [#17][] and [#5][]. (Thanks [@jovanbrakus][] and [@diasjorge][]). 46 | * [FEATURE] Add Incr, Decr, Timing and more docs.. See [#15][]. (Thanks [@gphat][]) 47 | * [BUGFIX] Do not append to shared slice. See [#16][]. (Thanks [@tummychow][]) 48 | 49 | 50 | [#5]: https://github.com/DataDog/datadog-go/issues/5 51 | [#15]: https://github.com/DataDog/datadog-go/issues/15 52 | [#16]: https://github.com/DataDog/datadog-go/issues/16 53 | [#17]: https://github.com/DataDog/datadog-go/issues/17 54 | [#19]: https://github.com/DataDog/datadog-go/issues/19 55 | [#21]: https://github.com/DataDog/datadog-go/issues/21 56 | [#25]: https://github.com/DataDog/datadog-go/issues/25 57 | [#28]: https://github.com/DataDog/datadog-go/issues/28 58 | [#30]: https://github.com/DataDog/datadog-go/issues/30 59 | [#32]: https://github.com/DataDog/datadog-go/issues/32 60 | [#37]: https://github.com/DataDog/datadog-go/issues/37 61 | [#39]: https://github.com/DataDog/datadog-go/issues/39 62 | [#40]: https://github.com/DataDog/datadog-go/issues/40 63 | [#42]: https://github.com/DataDog/datadog-go/issues/42 64 | [#43]: https://github.com/DataDog/datadog-go/issues/43 65 | [#45]: https://github.com/DataDog/datadog-go/issues/45 66 | [#46]: https://github.com/DataDog/datadog-go/issues/46 67 | [#47]: https://github.com/DataDog/datadog-go/issues/47 68 | [@Jasrags]: https://github.com/Jasrags 69 | [@abtris]: https://github.com/abtris 70 | [@aviau]: https://github.com/aviau 71 | [@colega]: https://github.com/colega 72 | [@corsc]: https://github.com/corsc 73 | [@diasjorge]: https://github.com/diasjorge 74 | [@gphat]: https://github.com/gphat 75 | [@jacek-adamek]: https://github.com/jacek-adamek 76 | [@jovanbrakus]: https://github.com/jovanbrakus 77 | [@kcollasarundell]: https://github.com/kcollasarundell 78 | [@seiffert]: https://github.com/seiffert 79 | [@sjung-stripe]: https://github.com/sjung-stripe 80 | [@sschepens]: https://github.com/sschepens 81 | [@tariq1890]: https://github.com/tariq1890 82 | [@theckman]: https://github.com/theckman 83 | [@thomas91310]: https://github.com/thomas91310 84 | [@tummychow]: https://github.com/tummychow 85 | [@victortrac]: https://github.com/victortrac 86 | [@w-vi]: https://github.com/w-vi -------------------------------------------------------------------------------- /vendor/github.com/DataDog/datadog-go/statsd/statsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Ooyala, Inc. 2 | 3 | /* 4 | Package statsd provides a Go dogstatsd client. Dogstatsd extends the popular statsd, 5 | adding tags and histograms and pushing upstream to Datadog. 6 | 7 | Refer to http://docs.datadoghq.com/guides/dogstatsd/ for information about DogStatsD. 8 | 9 | Example Usage: 10 | 11 | // Create the client 12 | c, err := statsd.New("127.0.0.1:8125") 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | // Prefix every metric with the app name 17 | c.Namespace = "flubber." 18 | // Send the EC2 availability zone as a tag with every metric 19 | c.Tags = append(c.Tags, "us-east-1a") 20 | err = c.Gauge("request.duration", 1.2, nil, 1) 21 | 22 | statsd is based on go-statsd-client. 23 | */ 24 | package statsd 25 | 26 | import ( 27 | "bytes" 28 | "errors" 29 | "fmt" 30 | "io" 31 | "math/rand" 32 | "strconv" 33 | "strings" 34 | "sync" 35 | "time" 36 | ) 37 | 38 | /* 39 | OptimalPayloadSize defines the optimal payload size for a UDP datagram, 1432 bytes 40 | is optimal for regular networks with an MTU of 1500 so datagrams don't get 41 | fragmented. It's generally recommended not to fragment UDP datagrams as losing 42 | a single fragment will cause the entire datagram to be lost. 43 | 44 | This can be increased if your network has a greater MTU or you don't mind UDP 45 | datagrams getting fragmented. The practical limit is MaxUDPPayloadSize 46 | */ 47 | const OptimalPayloadSize = 1432 48 | 49 | /* 50 | MaxUDPPayloadSize defines the maximum payload size for a UDP datagram. 51 | Its value comes from the calculation: 65535 bytes Max UDP datagram size - 52 | 8byte UDP header - 60byte max IP headers 53 | any number greater than that will see frames being cut out. 54 | */ 55 | const MaxUDPPayloadSize = 65467 56 | 57 | /* 58 | UnixAddressPrefix holds the prefix to use to enable Unix Domain Socket 59 | traffic instead of UDP. 60 | */ 61 | const UnixAddressPrefix = "unix://" 62 | 63 | /* 64 | Stat suffixes 65 | */ 66 | var ( 67 | gaugeSuffix = []byte("|g") 68 | countSuffix = []byte("|c") 69 | histogramSuffix = []byte("|h") 70 | distributionSuffix = []byte("|d") 71 | decrSuffix = []byte("-1|c") 72 | incrSuffix = []byte("1|c") 73 | setSuffix = []byte("|s") 74 | timingSuffix = []byte("|ms") 75 | ) 76 | 77 | // A statsdWriter offers a standard interface regardless of the underlying 78 | // protocol. For now UDS and UPD writers are available. 79 | type statsdWriter interface { 80 | Write(data []byte) (n int, err error) 81 | SetWriteTimeout(time.Duration) error 82 | Close() error 83 | } 84 | 85 | // A Client is a handle for sending messages to dogstatsd. It is safe to 86 | // use one Client from multiple goroutines simultaneously. 87 | type Client struct { 88 | // Writer handles the underlying networking protocol 89 | writer statsdWriter 90 | // Namespace to prepend to all statsd calls 91 | Namespace string 92 | // Tags are global tags to be added to every statsd call 93 | Tags []string 94 | // skipErrors turns off error passing and allows UDS to emulate UDP behaviour 95 | SkipErrors bool 96 | // BufferLength is the length of the buffer in commands. 97 | bufferLength int 98 | flushTime time.Duration 99 | commands []string 100 | buffer bytes.Buffer 101 | stop chan struct{} 102 | sync.Mutex 103 | } 104 | 105 | // New returns a pointer to a new Client given an addr in the format "hostname:port" or 106 | // "unix:///path/to/socket". 107 | func New(addr string) (*Client, error) { 108 | if strings.HasPrefix(addr, UnixAddressPrefix) { 109 | w, err := newUdsWriter(addr[len(UnixAddressPrefix)-1:]) 110 | if err != nil { 111 | return nil, err 112 | } 113 | return NewWithWriter(w) 114 | } 115 | w, err := newUDPWriter(addr) 116 | if err != nil { 117 | return nil, err 118 | } 119 | return NewWithWriter(w) 120 | } 121 | 122 | // NewWithWriter creates a new Client with given writer. Writer is a 123 | // io.WriteCloser + SetWriteTimeout(time.Duration) error 124 | func NewWithWriter(w statsdWriter) (*Client, error) { 125 | client := &Client{writer: w, SkipErrors: false} 126 | return client, nil 127 | } 128 | 129 | // NewBuffered returns a Client that buffers its output and sends it in chunks. 130 | // Buflen is the length of the buffer in number of commands. 131 | func NewBuffered(addr string, buflen int) (*Client, error) { 132 | client, err := New(addr) 133 | if err != nil { 134 | return nil, err 135 | } 136 | client.bufferLength = buflen 137 | client.commands = make([]string, 0, buflen) 138 | client.flushTime = time.Millisecond * 100 139 | client.stop = make(chan struct{}, 1) 140 | go client.watch() 141 | return client, nil 142 | } 143 | 144 | // format a message from its name, value, tags and rate. Also adds global 145 | // namespace and tags. 146 | func (c *Client) format(name string, value interface{}, suffix []byte, tags []string, rate float64) string { 147 | var buf bytes.Buffer 148 | if c.Namespace != "" { 149 | buf.WriteString(c.Namespace) 150 | } 151 | buf.WriteString(name) 152 | buf.WriteString(":") 153 | 154 | switch val := value.(type) { 155 | case float64: 156 | buf.Write(strconv.AppendFloat([]byte{}, val, 'f', 6, 64)) 157 | 158 | case int64: 159 | buf.Write(strconv.AppendInt([]byte{}, val, 10)) 160 | 161 | case string: 162 | buf.WriteString(val) 163 | 164 | default: 165 | // do nothing 166 | } 167 | buf.Write(suffix) 168 | 169 | if rate < 1 { 170 | buf.WriteString(`|@`) 171 | buf.WriteString(strconv.FormatFloat(rate, 'f', -1, 64)) 172 | } 173 | 174 | writeTagString(&buf, c.Tags, tags) 175 | 176 | return buf.String() 177 | } 178 | 179 | // SetWriteTimeout allows the user to set a custom UDS write timeout. Not supported for UDP. 180 | func (c *Client) SetWriteTimeout(d time.Duration) error { 181 | return c.writer.SetWriteTimeout(d) 182 | } 183 | 184 | func (c *Client) watch() { 185 | ticker := time.NewTicker(c.flushTime) 186 | 187 | for { 188 | select { 189 | case <-ticker.C: 190 | c.Lock() 191 | if len(c.commands) > 0 { 192 | // FIXME: eating error here 193 | c.flushLocked() 194 | } 195 | c.Unlock() 196 | case <-c.stop: 197 | ticker.Stop() 198 | return 199 | } 200 | } 201 | } 202 | 203 | func (c *Client) append(cmd string) error { 204 | c.Lock() 205 | defer c.Unlock() 206 | c.commands = append(c.commands, cmd) 207 | // if we should flush, lets do it 208 | if len(c.commands) == c.bufferLength { 209 | if err := c.flushLocked(); err != nil { 210 | return err 211 | } 212 | } 213 | return nil 214 | } 215 | 216 | func (c *Client) joinMaxSize(cmds []string, sep string, maxSize int) ([][]byte, []int) { 217 | c.buffer.Reset() //clear buffer 218 | 219 | var frames [][]byte 220 | var ncmds []int 221 | sepBytes := []byte(sep) 222 | sepLen := len(sep) 223 | 224 | elem := 0 225 | for _, cmd := range cmds { 226 | needed := len(cmd) 227 | 228 | if elem != 0 { 229 | needed = needed + sepLen 230 | } 231 | 232 | if c.buffer.Len()+needed <= maxSize { 233 | if elem != 0 { 234 | c.buffer.Write(sepBytes) 235 | } 236 | c.buffer.WriteString(cmd) 237 | elem++ 238 | } else { 239 | frames = append(frames, copyAndResetBuffer(&c.buffer)) 240 | ncmds = append(ncmds, elem) 241 | // if cmd is bigger than maxSize it will get flushed on next loop 242 | c.buffer.WriteString(cmd) 243 | elem = 1 244 | } 245 | } 246 | 247 | //add whatever is left! if there's actually something 248 | if c.buffer.Len() > 0 { 249 | frames = append(frames, copyAndResetBuffer(&c.buffer)) 250 | ncmds = append(ncmds, elem) 251 | } 252 | 253 | return frames, ncmds 254 | } 255 | 256 | func copyAndResetBuffer(buf *bytes.Buffer) []byte { 257 | tmpBuf := make([]byte, buf.Len()) 258 | copy(tmpBuf, buf.Bytes()) 259 | buf.Reset() 260 | return tmpBuf 261 | } 262 | 263 | // Flush forces a flush of the pending commands in the buffer 264 | func (c *Client) Flush() error { 265 | c.Lock() 266 | defer c.Unlock() 267 | return c.flushLocked() 268 | } 269 | 270 | // flush the commands in the buffer. Lock must be held by caller. 271 | func (c *Client) flushLocked() error { 272 | frames, flushable := c.joinMaxSize(c.commands, "\n", OptimalPayloadSize) 273 | var err error 274 | cmdsFlushed := 0 275 | for i, data := range frames { 276 | _, e := c.writer.Write(data) 277 | if e != nil { 278 | err = e 279 | break 280 | } 281 | cmdsFlushed += flushable[i] 282 | } 283 | 284 | // clear the slice with a slice op, doesn't realloc 285 | if cmdsFlushed == len(c.commands) { 286 | c.commands = c.commands[:0] 287 | } else { 288 | //this case will cause a future realloc... 289 | // drop problematic command though (sorry). 290 | c.commands = c.commands[cmdsFlushed+1:] 291 | } 292 | return err 293 | } 294 | 295 | func (c *Client) sendMsg(msg string) error { 296 | // return an error if message is bigger than MaxUDPPayloadSize 297 | if len(msg) > MaxUDPPayloadSize { 298 | return errors.New("message size exceeds MaxUDPPayloadSize") 299 | } 300 | 301 | // if this client is buffered, then we'll just append this 302 | if c.bufferLength > 0 { 303 | return c.append(msg) 304 | } 305 | 306 | _, err := c.writer.Write([]byte(msg)) 307 | 308 | if c.SkipErrors { 309 | return nil 310 | } 311 | return err 312 | } 313 | 314 | // send handles sampling and sends the message over UDP. It also adds global namespace prefixes and tags. 315 | func (c *Client) send(name string, value interface{}, suffix []byte, tags []string, rate float64) error { 316 | if c == nil { 317 | return nil 318 | } 319 | if rate < 1 && rand.Float64() > rate { 320 | return nil 321 | } 322 | data := c.format(name, value, suffix, tags, rate) 323 | return c.sendMsg(data) 324 | } 325 | 326 | // Gauge measures the value of a metric at a particular time. 327 | func (c *Client) Gauge(name string, value float64, tags []string, rate float64) error { 328 | return c.send(name, value, gaugeSuffix, tags, rate) 329 | } 330 | 331 | // Count tracks how many times something happened per second. 332 | func (c *Client) Count(name string, value int64, tags []string, rate float64) error { 333 | return c.send(name, value, countSuffix, tags, rate) 334 | } 335 | 336 | // Histogram tracks the statistical distribution of a set of values on each host. 337 | func (c *Client) Histogram(name string, value float64, tags []string, rate float64) error { 338 | return c.send(name, value, histogramSuffix, tags, rate) 339 | } 340 | 341 | // Distribution tracks the statistical distribution of a set of values across your infrastructure. 342 | func (c *Client) Distribution(name string, value float64, tags []string, rate float64) error { 343 | return c.send(name, value, distributionSuffix, tags, rate) 344 | } 345 | 346 | // Decr is just Count of -1 347 | func (c *Client) Decr(name string, tags []string, rate float64) error { 348 | return c.send(name, nil, decrSuffix, tags, rate) 349 | } 350 | 351 | // Incr is just Count of 1 352 | func (c *Client) Incr(name string, tags []string, rate float64) error { 353 | return c.send(name, nil, incrSuffix, tags, rate) 354 | } 355 | 356 | // Set counts the number of unique elements in a group. 357 | func (c *Client) Set(name string, value string, tags []string, rate float64) error { 358 | return c.send(name, value, setSuffix, tags, rate) 359 | } 360 | 361 | // Timing sends timing information, it is an alias for TimeInMilliseconds 362 | func (c *Client) Timing(name string, value time.Duration, tags []string, rate float64) error { 363 | return c.TimeInMilliseconds(name, value.Seconds()*1000, tags, rate) 364 | } 365 | 366 | // TimeInMilliseconds sends timing information in milliseconds. 367 | // It is flushed by statsd with percentiles, mean and other info (https://github.com/etsy/statsd/blob/master/docs/metric_types.md#timing) 368 | func (c *Client) TimeInMilliseconds(name string, value float64, tags []string, rate float64) error { 369 | return c.send(name, value, timingSuffix, tags, rate) 370 | } 371 | 372 | // Event sends the provided Event. 373 | func (c *Client) Event(e *Event) error { 374 | if c == nil { 375 | return nil 376 | } 377 | stat, err := e.Encode(c.Tags...) 378 | if err != nil { 379 | return err 380 | } 381 | return c.sendMsg(stat) 382 | } 383 | 384 | // SimpleEvent sends an event with the provided title and text. 385 | func (c *Client) SimpleEvent(title, text string) error { 386 | e := NewEvent(title, text) 387 | return c.Event(e) 388 | } 389 | 390 | // ServiceCheck sends the provided ServiceCheck. 391 | func (c *Client) ServiceCheck(sc *ServiceCheck) error { 392 | stat, err := sc.Encode(c.Tags...) 393 | if err != nil { 394 | return err 395 | } 396 | return c.sendMsg(stat) 397 | } 398 | 399 | // SimpleServiceCheck sends an serviceCheck with the provided name and status. 400 | func (c *Client) SimpleServiceCheck(name string, status ServiceCheckStatus) error { 401 | sc := NewServiceCheck(name, status) 402 | return c.ServiceCheck(sc) 403 | } 404 | 405 | // Close the client connection. 406 | func (c *Client) Close() error { 407 | if c == nil { 408 | return nil 409 | } 410 | select { 411 | case c.stop <- struct{}{}: 412 | default: 413 | } 414 | 415 | // if this client is buffered, flush before closing the writer 416 | if c.bufferLength > 0 { 417 | if err := c.Flush(); err != nil { 418 | return err 419 | } 420 | } 421 | 422 | return c.writer.Close() 423 | } 424 | 425 | // Events support 426 | // EventAlertType and EventAlertPriority became exported types after this issue was submitted: https://github.com/DataDog/datadog-go/issues/41 427 | // The reason why they got exported is so that client code can directly use the types. 428 | 429 | // EventAlertType is the alert type for events 430 | type EventAlertType string 431 | 432 | const ( 433 | // Info is the "info" AlertType for events 434 | Info EventAlertType = "info" 435 | // Error is the "error" AlertType for events 436 | Error EventAlertType = "error" 437 | // Warning is the "warning" AlertType for events 438 | Warning EventAlertType = "warning" 439 | // Success is the "success" AlertType for events 440 | Success EventAlertType = "success" 441 | ) 442 | 443 | // EventPriority is the event priority for events 444 | type EventPriority string 445 | 446 | const ( 447 | // Normal is the "normal" Priority for events 448 | Normal EventPriority = "normal" 449 | // Low is the "low" Priority for events 450 | Low EventPriority = "low" 451 | ) 452 | 453 | // An Event is an object that can be posted to your DataDog event stream. 454 | type Event struct { 455 | // Title of the event. Required. 456 | Title string 457 | // Text is the description of the event. Required. 458 | Text string 459 | // Timestamp is a timestamp for the event. If not provided, the dogstatsd 460 | // server will set this to the current time. 461 | Timestamp time.Time 462 | // Hostname for the event. 463 | Hostname string 464 | // AggregationKey groups this event with others of the same key. 465 | AggregationKey string 466 | // Priority of the event. Can be statsd.Low or statsd.Normal. 467 | Priority EventPriority 468 | // SourceTypeName is a source type for the event. 469 | SourceTypeName string 470 | // AlertType can be statsd.Info, statsd.Error, statsd.Warning, or statsd.Success. 471 | // If absent, the default value applied by the dogstatsd server is Info. 472 | AlertType EventAlertType 473 | // Tags for the event. 474 | Tags []string 475 | } 476 | 477 | // NewEvent creates a new event with the given title and text. Error checking 478 | // against these values is done at send-time, or upon running e.Check. 479 | func NewEvent(title, text string) *Event { 480 | return &Event{ 481 | Title: title, 482 | Text: text, 483 | } 484 | } 485 | 486 | // Check verifies that an event is valid. 487 | func (e Event) Check() error { 488 | if len(e.Title) == 0 { 489 | return fmt.Errorf("statsd.Event title is required") 490 | } 491 | if len(e.Text) == 0 { 492 | return fmt.Errorf("statsd.Event text is required") 493 | } 494 | return nil 495 | } 496 | 497 | // Encode returns the dogstatsd wire protocol representation for an event. 498 | // Tags may be passed which will be added to the encoded output but not to 499 | // the Event's list of tags, eg. for default tags. 500 | func (e Event) Encode(tags ...string) (string, error) { 501 | err := e.Check() 502 | if err != nil { 503 | return "", err 504 | } 505 | text := e.escapedText() 506 | 507 | var buffer bytes.Buffer 508 | buffer.WriteString("_e{") 509 | buffer.WriteString(strconv.FormatInt(int64(len(e.Title)), 10)) 510 | buffer.WriteRune(',') 511 | buffer.WriteString(strconv.FormatInt(int64(len(text)), 10)) 512 | buffer.WriteString("}:") 513 | buffer.WriteString(e.Title) 514 | buffer.WriteRune('|') 515 | buffer.WriteString(text) 516 | 517 | if !e.Timestamp.IsZero() { 518 | buffer.WriteString("|d:") 519 | buffer.WriteString(strconv.FormatInt(int64(e.Timestamp.Unix()), 10)) 520 | } 521 | 522 | if len(e.Hostname) != 0 { 523 | buffer.WriteString("|h:") 524 | buffer.WriteString(e.Hostname) 525 | } 526 | 527 | if len(e.AggregationKey) != 0 { 528 | buffer.WriteString("|k:") 529 | buffer.WriteString(e.AggregationKey) 530 | 531 | } 532 | 533 | if len(e.Priority) != 0 { 534 | buffer.WriteString("|p:") 535 | buffer.WriteString(string(e.Priority)) 536 | } 537 | 538 | if len(e.SourceTypeName) != 0 { 539 | buffer.WriteString("|s:") 540 | buffer.WriteString(e.SourceTypeName) 541 | } 542 | 543 | if len(e.AlertType) != 0 { 544 | buffer.WriteString("|t:") 545 | buffer.WriteString(string(e.AlertType)) 546 | } 547 | 548 | writeTagString(&buffer, tags, e.Tags) 549 | 550 | return buffer.String(), nil 551 | } 552 | 553 | // ServiceCheckStatus support 554 | type ServiceCheckStatus byte 555 | 556 | const ( 557 | // Ok is the "ok" ServiceCheck status 558 | Ok ServiceCheckStatus = 0 559 | // Warn is the "warning" ServiceCheck status 560 | Warn ServiceCheckStatus = 1 561 | // Critical is the "critical" ServiceCheck status 562 | Critical ServiceCheckStatus = 2 563 | // Unknown is the "unknown" ServiceCheck status 564 | Unknown ServiceCheckStatus = 3 565 | ) 566 | 567 | // An ServiceCheck is an object that contains status of DataDog service check. 568 | type ServiceCheck struct { 569 | // Name of the service check. Required. 570 | Name string 571 | // Status of service check. Required. 572 | Status ServiceCheckStatus 573 | // Timestamp is a timestamp for the serviceCheck. If not provided, the dogstatsd 574 | // server will set this to the current time. 575 | Timestamp time.Time 576 | // Hostname for the serviceCheck. 577 | Hostname string 578 | // A message describing the current state of the serviceCheck. 579 | Message string 580 | // Tags for the serviceCheck. 581 | Tags []string 582 | } 583 | 584 | // NewServiceCheck creates a new serviceCheck with the given name and status. Error checking 585 | // against these values is done at send-time, or upon running sc.Check. 586 | func NewServiceCheck(name string, status ServiceCheckStatus) *ServiceCheck { 587 | return &ServiceCheck{ 588 | Name: name, 589 | Status: status, 590 | } 591 | } 592 | 593 | // Check verifies that an event is valid. 594 | func (sc ServiceCheck) Check() error { 595 | if len(sc.Name) == 0 { 596 | return fmt.Errorf("statsd.ServiceCheck name is required") 597 | } 598 | if byte(sc.Status) < 0 || byte(sc.Status) > 3 { 599 | return fmt.Errorf("statsd.ServiceCheck status has invalid value") 600 | } 601 | return nil 602 | } 603 | 604 | // Encode returns the dogstatsd wire protocol representation for an serviceCheck. 605 | // Tags may be passed which will be added to the encoded output but not to 606 | // the Event's list of tags, eg. for default tags. 607 | func (sc ServiceCheck) Encode(tags ...string) (string, error) { 608 | err := sc.Check() 609 | if err != nil { 610 | return "", err 611 | } 612 | message := sc.escapedMessage() 613 | 614 | var buffer bytes.Buffer 615 | buffer.WriteString("_sc|") 616 | buffer.WriteString(sc.Name) 617 | buffer.WriteRune('|') 618 | buffer.WriteString(strconv.FormatInt(int64(sc.Status), 10)) 619 | 620 | if !sc.Timestamp.IsZero() { 621 | buffer.WriteString("|d:") 622 | buffer.WriteString(strconv.FormatInt(int64(sc.Timestamp.Unix()), 10)) 623 | } 624 | 625 | if len(sc.Hostname) != 0 { 626 | buffer.WriteString("|h:") 627 | buffer.WriteString(sc.Hostname) 628 | } 629 | 630 | writeTagString(&buffer, tags, sc.Tags) 631 | 632 | if len(message) != 0 { 633 | buffer.WriteString("|m:") 634 | buffer.WriteString(message) 635 | } 636 | 637 | return buffer.String(), nil 638 | } 639 | 640 | func (e Event) escapedText() string { 641 | return strings.Replace(e.Text, "\n", "\\n", -1) 642 | } 643 | 644 | func (sc ServiceCheck) escapedMessage() string { 645 | msg := strings.Replace(sc.Message, "\n", "\\n", -1) 646 | return strings.Replace(msg, "m:", `m\:`, -1) 647 | } 648 | 649 | func removeNewlines(str string) string { 650 | return strings.Replace(str, "\n", "", -1) 651 | } 652 | 653 | func writeTagString(w io.Writer, tagList1, tagList2 []string) { 654 | // the tag lists may be shared with other callers, so we cannot modify 655 | // them in any way (which means we cannot append to them either) 656 | // therefore we must make an entirely separate copy just for this call 657 | totalLen := len(tagList1) + len(tagList2) 658 | if totalLen == 0 { 659 | return 660 | } 661 | tags := make([]string, 0, totalLen) 662 | tags = append(tags, tagList1...) 663 | tags = append(tags, tagList2...) 664 | 665 | io.WriteString(w, "|#") 666 | io.WriteString(w, removeNewlines(tags[0])) 667 | for _, tag := range tags[1:] { 668 | io.WriteString(w, ",") 669 | io.WriteString(w, removeNewlines(tag)) 670 | } 671 | } 672 | -------------------------------------------------------------------------------- /vendor/github.com/DataDog/datadog-go/statsd/statsd_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Ooyala, Inc. 2 | 3 | package statsd 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net" 11 | "os" 12 | "path/filepath" 13 | "reflect" 14 | "strconv" 15 | "strings" 16 | "testing" 17 | "time" 18 | ) 19 | 20 | var dogstatsdTests = []struct { 21 | GlobalNamespace string 22 | GlobalTags []string 23 | Method string 24 | Metric string 25 | Value interface{} 26 | Tags []string 27 | Rate float64 28 | Expected string 29 | }{ 30 | {"", nil, "Gauge", "test.gauge", 1.0, nil, 1.0, "test.gauge:1.000000|g"}, 31 | {"", nil, "Gauge", "test.gauge", 1.0, nil, 0.999999, "test.gauge:1.000000|g|@0.999999"}, 32 | {"", nil, "Gauge", "test.gauge", 1.0, []string{"tagA"}, 1.0, "test.gauge:1.000000|g|#tagA"}, 33 | {"", nil, "Gauge", "test.gauge", 1.0, []string{"tagA", "tagB"}, 1.0, "test.gauge:1.000000|g|#tagA,tagB"}, 34 | {"", nil, "Gauge", "test.gauge", 1.0, []string{"tagA"}, 0.999999, "test.gauge:1.000000|g|@0.999999|#tagA"}, 35 | {"", nil, "Count", "test.count", int64(1), []string{"tagA"}, 1.0, "test.count:1|c|#tagA"}, 36 | {"", nil, "Count", "test.count", int64(-1), []string{"tagA"}, 1.0, "test.count:-1|c|#tagA"}, 37 | {"", nil, "Histogram", "test.histogram", 2.3, []string{"tagA"}, 1.0, "test.histogram:2.300000|h|#tagA"}, 38 | {"", nil, "Distribution", "test.distribution", 2.3, []string{"tagA"}, 1.0, "test.distribution:2.300000|d|#tagA"}, 39 | {"", nil, "Set", "test.set", "uuid", []string{"tagA"}, 1.0, "test.set:uuid|s|#tagA"}, 40 | {"flubber.", nil, "Set", "test.set", "uuid", []string{"tagA"}, 1.0, "flubber.test.set:uuid|s|#tagA"}, 41 | {"", []string{"tagC"}, "Set", "test.set", "uuid", []string{"tagA"}, 1.0, "test.set:uuid|s|#tagC,tagA"}, 42 | {"", nil, "Count", "test.count", int64(1), []string{"hello\nworld"}, 1.0, "test.count:1|c|#helloworld"}, 43 | } 44 | 45 | func assertNotPanics(t *testing.T, f func()) { 46 | defer func() { 47 | if r := recover(); r != nil { 48 | t.Fatal(r) 49 | } 50 | }() 51 | f() 52 | } 53 | 54 | func TestClientUDP(t *testing.T) { 55 | addr := "localhost:1201" 56 | udpAddr, err := net.ResolveUDPAddr("udp", addr) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | server, err := net.ListenUDP("udp", udpAddr) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | defer server.Close() 66 | 67 | client, err := New(addr) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | clientTest(t, server, client) 73 | } 74 | 75 | type statsdWriterWrapper struct { 76 | io.WriteCloser 77 | } 78 | 79 | func (statsdWriterWrapper) SetWriteTimeout(time.Duration) error { 80 | return nil 81 | } 82 | 83 | func TestClientWithConn(t *testing.T) { 84 | server, conn, err := os.Pipe() 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | 89 | client, err := NewWithWriter(statsdWriterWrapper{conn}) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | 94 | clientTest(t, server, client) 95 | } 96 | 97 | func clientTest(t *testing.T, server io.Reader, client *Client) { 98 | for _, tt := range dogstatsdTests { 99 | client.Namespace = tt.GlobalNamespace 100 | client.Tags = tt.GlobalTags 101 | method := reflect.ValueOf(client).MethodByName(tt.Method) 102 | e := method.Call([]reflect.Value{ 103 | reflect.ValueOf(tt.Metric), 104 | reflect.ValueOf(tt.Value), 105 | reflect.ValueOf(tt.Tags), 106 | reflect.ValueOf(tt.Rate)})[0] 107 | errInter := e.Interface() 108 | if errInter != nil { 109 | t.Fatal(errInter.(error)) 110 | } 111 | 112 | bytes := make([]byte, 1024) 113 | n, err := server.Read(bytes) 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | message := bytes[:n] 118 | if string(message) != tt.Expected { 119 | t.Errorf("Expected: %s. Actual: %s", tt.Expected, string(message)) 120 | } 121 | } 122 | } 123 | 124 | func TestClientUDS(t *testing.T) { 125 | dir, err := ioutil.TempDir("", "socket") 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | defer os.RemoveAll(dir) // clean up 130 | 131 | addr := filepath.Join(dir, "dsd.socket") 132 | 133 | udsAddr, err := net.ResolveUnixAddr("unixgram", addr) 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | 138 | server, err := net.ListenUnixgram("unixgram", udsAddr) 139 | if err != nil { 140 | t.Fatal(err) 141 | } 142 | defer server.Close() 143 | 144 | addrParts := []string{UnixAddressPrefix, addr} 145 | client, err := New(strings.Join(addrParts, "")) 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | 150 | for _, tt := range dogstatsdTests { 151 | client.Namespace = tt.GlobalNamespace 152 | client.Tags = tt.GlobalTags 153 | method := reflect.ValueOf(client).MethodByName(tt.Method) 154 | e := method.Call([]reflect.Value{ 155 | reflect.ValueOf(tt.Metric), 156 | reflect.ValueOf(tt.Value), 157 | reflect.ValueOf(tt.Tags), 158 | reflect.ValueOf(tt.Rate)})[0] 159 | errInter := e.Interface() 160 | if errInter != nil { 161 | t.Fatal(errInter.(error)) 162 | } 163 | 164 | bytes := make([]byte, 1024) 165 | n, err := server.Read(bytes) 166 | if err != nil { 167 | t.Fatal(err) 168 | } 169 | message := bytes[:n] 170 | if string(message) != tt.Expected { 171 | t.Errorf("Expected: %s. Actual: %s", tt.Expected, string(message)) 172 | } 173 | } 174 | } 175 | 176 | func TestClientUDSClose(t *testing.T) { 177 | dir, err := ioutil.TempDir("", "socket") 178 | if err != nil { 179 | t.Fatal(err) 180 | } 181 | defer os.RemoveAll(dir) // clean up 182 | 183 | addr := filepath.Join(dir, "dsd.socket") 184 | 185 | addrParts := []string{UnixAddressPrefix, addr} 186 | client, err := New(strings.Join(addrParts, "")) 187 | if err != nil { 188 | t.Fatal(err) 189 | } 190 | 191 | assertNotPanics(t, func() { client.Close() }) 192 | } 193 | 194 | func TestBufferedClient(t *testing.T) { 195 | addr := "localhost:1201" 196 | udpAddr, err := net.ResolveUDPAddr("udp", addr) 197 | if err != nil { 198 | t.Fatal(err) 199 | } 200 | 201 | server, err := net.ListenUDP("udp", udpAddr) 202 | if err != nil { 203 | t.Fatal(err) 204 | } 205 | defer server.Close() 206 | 207 | bufferLength := 9 208 | client, err := NewBuffered(addr, bufferLength) 209 | if err != nil { 210 | t.Fatal(err) 211 | } 212 | 213 | client.Namespace = "foo." 214 | client.Tags = []string{"dd:2"} 215 | 216 | dur, _ := time.ParseDuration("123us") 217 | 218 | client.Incr("ic", nil, 1) 219 | client.Decr("dc", nil, 1) 220 | client.Count("cc", 1, nil, 1) 221 | client.Gauge("gg", 10, nil, 1) 222 | client.Histogram("hh", 1, nil, 1) 223 | client.Distribution("dd", 1, nil, 1) 224 | client.Timing("tt", dur, nil, 1) 225 | client.Set("ss", "ss", nil, 1) 226 | 227 | if len(client.commands) != (bufferLength - 1) { 228 | t.Errorf("Expected client to have buffered %d commands, but found %d\n", (bufferLength - 1), len(client.commands)) 229 | } 230 | 231 | client.Set("ss", "xx", nil, 1) 232 | client.Lock() 233 | err = client.flushLocked() 234 | client.Unlock() 235 | if err != nil { 236 | t.Errorf("Error sending: %s", err) 237 | } 238 | 239 | if len(client.commands) != 0 { 240 | t.Errorf("Expecting send to flush commands, but found %d\n", len(client.commands)) 241 | } 242 | 243 | buffer := make([]byte, 4096) 244 | n, err := io.ReadAtLeast(server, buffer, 1) 245 | result := string(buffer[:n]) 246 | 247 | if err != nil { 248 | t.Error(err) 249 | } 250 | 251 | expected := []string{ 252 | `foo.ic:1|c|#dd:2`, 253 | `foo.dc:-1|c|#dd:2`, 254 | `foo.cc:1|c|#dd:2`, 255 | `foo.gg:10.000000|g|#dd:2`, 256 | `foo.hh:1.000000|h|#dd:2`, 257 | `foo.dd:1.000000|d|#dd:2`, 258 | `foo.tt:0.123000|ms|#dd:2`, 259 | `foo.ss:ss|s|#dd:2`, 260 | `foo.ss:xx|s|#dd:2`, 261 | } 262 | 263 | for i, res := range strings.Split(result, "\n") { 264 | if res != expected[i] { 265 | t.Errorf("Got `%s`, expected `%s`", res, expected[i]) 266 | } 267 | } 268 | 269 | client.Event(&Event{Title: "title1", Text: "text1", Priority: Normal, AlertType: Success, Tags: []string{"tagg"}}) 270 | client.SimpleEvent("event1", "text1") 271 | 272 | if len(client.commands) != 2 { 273 | t.Errorf("Expected to find %d commands, but found %d\n", 2, len(client.commands)) 274 | } 275 | 276 | client.Lock() 277 | err = client.flushLocked() 278 | client.Unlock() 279 | 280 | if err != nil { 281 | t.Errorf("Error sending: %s", err) 282 | } 283 | 284 | if len(client.commands) != 0 { 285 | t.Errorf("Expecting send to flush commands, but found %d\n", len(client.commands)) 286 | } 287 | 288 | buffer = make([]byte, 1024) 289 | n, err = io.ReadAtLeast(server, buffer, 1) 290 | result = string(buffer[:n]) 291 | 292 | if err != nil { 293 | t.Error(err) 294 | } 295 | 296 | if n == 0 { 297 | t.Errorf("Read 0 bytes but expected more.") 298 | } 299 | 300 | expected = []string{ 301 | `_e{6,5}:title1|text1|p:normal|t:success|#dd:2,tagg`, 302 | `_e{6,5}:event1|text1|#dd:2`, 303 | } 304 | 305 | for i, res := range strings.Split(result, "\n") { 306 | if res != expected[i] { 307 | t.Errorf("Got `%s`, expected `%s`", res, expected[i]) 308 | } 309 | } 310 | 311 | } 312 | 313 | func TestBufferedClientBackground(t *testing.T) { 314 | addr := "localhost:1201" 315 | udpAddr, err := net.ResolveUDPAddr("udp", addr) 316 | if err != nil { 317 | t.Fatal(err) 318 | } 319 | 320 | server, err := net.ListenUDP("udp", udpAddr) 321 | if err != nil { 322 | t.Fatal(err) 323 | } 324 | defer server.Close() 325 | 326 | bufferLength := 5 327 | client, err := NewBuffered(addr, bufferLength) 328 | if err != nil { 329 | t.Fatal(err) 330 | } 331 | defer client.Close() 332 | 333 | client.Namespace = "foo." 334 | client.Tags = []string{"dd:2"} 335 | 336 | client.Count("cc", 1, nil, 1) 337 | client.Gauge("gg", 10, nil, 1) 338 | client.Histogram("hh", 1, nil, 1) 339 | client.Distribution("dd", 1, nil, 1) 340 | client.Set("ss", "ss", nil, 1) 341 | client.Set("ss", "xx", nil, 1) 342 | 343 | time.Sleep(client.flushTime * 2) 344 | client.Lock() 345 | if len(client.commands) != 0 { 346 | t.Errorf("Watch goroutine should have flushed commands, but found %d\n", len(client.commands)) 347 | } 348 | client.Unlock() 349 | } 350 | 351 | func TestBufferedClientFlush(t *testing.T) { 352 | addr := "localhost:1201" 353 | udpAddr, err := net.ResolveUDPAddr("udp", addr) 354 | if err != nil { 355 | t.Fatal(err) 356 | } 357 | 358 | server, err := net.ListenUDP("udp", udpAddr) 359 | if err != nil { 360 | t.Fatal(err) 361 | } 362 | defer server.Close() 363 | 364 | bufferLength := 5 365 | client, err := NewBuffered(addr, bufferLength) 366 | if err != nil { 367 | t.Fatal(err) 368 | } 369 | defer client.Close() 370 | 371 | client.Namespace = "foo." 372 | client.Tags = []string{"dd:2"} 373 | 374 | client.Count("cc", 1, nil, 1) 375 | client.Gauge("gg", 10, nil, 1) 376 | client.Histogram("hh", 1, nil, 1) 377 | client.Distribution("dd", 1, nil, 1) 378 | client.Set("ss", "ss", nil, 1) 379 | client.Set("ss", "xx", nil, 1) 380 | 381 | client.Flush() 382 | 383 | client.Lock() 384 | if len(client.commands) != 0 { 385 | t.Errorf("Flush should have flushed commands, but found %d\n", len(client.commands)) 386 | } 387 | client.Unlock() 388 | } 389 | 390 | func TestJoinMaxSize(t *testing.T) { 391 | c := Client{} 392 | elements := []string{"abc", "abcd", "ab", "xyz", "foobaz", "x", "wwxxyyzz"} 393 | res, n := c.joinMaxSize(elements, " ", 8) 394 | 395 | if len(res) != len(n) && len(res) != 4 { 396 | t.Errorf("Was expecting 4 frames to flush but got: %v - %v", n, res) 397 | } 398 | if n[0] != 2 { 399 | t.Errorf("Was expecting 2 elements in first frame but got: %v", n[0]) 400 | } 401 | if string(res[0]) != "abc abcd" { 402 | t.Errorf("Join should have returned \"abc abcd\" in frame, but found: %s", res[0]) 403 | } 404 | if n[1] != 2 { 405 | t.Errorf("Was expecting 2 elements in second frame but got: %v - %v", n[1], n) 406 | } 407 | if string(res[1]) != "ab xyz" { 408 | t.Errorf("Join should have returned \"ab xyz\" in frame, but found: %s", res[1]) 409 | } 410 | if n[2] != 2 { 411 | t.Errorf("Was expecting 2 elements in third frame but got: %v - %v", n[2], n) 412 | } 413 | if string(res[2]) != "foobaz x" { 414 | t.Errorf("Join should have returned \"foobaz x\" in frame, but found: %s", res[2]) 415 | } 416 | if n[3] != 1 { 417 | t.Errorf("Was expecting 1 element in fourth frame but got: %v - %v", n[3], n) 418 | } 419 | if string(res[3]) != "wwxxyyzz" { 420 | t.Errorf("Join should have returned \"wwxxyyzz\" in frame, but found: %s", res[3]) 421 | } 422 | 423 | res, n = c.joinMaxSize(elements, " ", 11) 424 | 425 | if len(res) != len(n) && len(res) != 3 { 426 | t.Errorf("Was expecting 3 frames to flush but got: %v - %v", n, res) 427 | } 428 | if n[0] != 3 { 429 | t.Errorf("Was expecting 3 elements in first frame but got: %v", n[0]) 430 | } 431 | if string(res[0]) != "abc abcd ab" { 432 | t.Errorf("Join should have returned \"abc abcd ab\" in frame, but got: %s", res[0]) 433 | } 434 | if n[1] != 2 { 435 | t.Errorf("Was expecting 2 elements in second frame but got: %v", n[1]) 436 | } 437 | if string(res[1]) != "xyz foobaz" { 438 | t.Errorf("Join should have returned \"xyz foobaz\" in frame, but got: %s", res[1]) 439 | } 440 | if n[2] != 2 { 441 | t.Errorf("Was expecting 2 elements in third frame but got: %v", n[2]) 442 | } 443 | if string(res[2]) != "x wwxxyyzz" { 444 | t.Errorf("Join should have returned \"x wwxxyyzz\" in frame, but got: %s", res[2]) 445 | } 446 | 447 | res, n = c.joinMaxSize(elements, " ", 8) 448 | 449 | if len(res) != len(n) && len(res) != 7 { 450 | t.Errorf("Was expecting 7 frames to flush but got: %v - %v", n, res) 451 | } 452 | if n[0] != 1 { 453 | t.Errorf("Separator is long, expected a single element in frame but got: %d - %v", n[0], res) 454 | } 455 | if string(res[0]) != "abc" { 456 | t.Errorf("Join should have returned \"abc\" in first frame, but got: %s", res) 457 | } 458 | if n[1] != 1 { 459 | t.Errorf("Separator is long, expected a single element in frame but got: %d - %v", n[1], res) 460 | } 461 | if string(res[1]) != "abcd" { 462 | t.Errorf("Join should have returned \"abcd\" in second frame, but got: %s", res[1]) 463 | } 464 | if n[2] != 1 { 465 | t.Errorf("Separator is long, expected a single element in third frame but got: %d - %v", n[2], res) 466 | } 467 | if string(res[2]) != "ab" { 468 | t.Errorf("Join should have returned \"ab\" in third frame, but got: %s", res[2]) 469 | } 470 | if n[3] != 1 { 471 | t.Errorf("Separator is long, expected a single element in fourth frame but got: %d - %v", n[3], res) 472 | } 473 | if string(res[3]) != "xyz" { 474 | t.Errorf("Join should have returned \"xyz\" in fourth frame, but got: %s", res[3]) 475 | } 476 | if n[4] != 1 { 477 | t.Errorf("Separator is long, expected a single element in fifth frame but got: %d - %v", n[4], res) 478 | } 479 | if string(res[4]) != "foobaz" { 480 | t.Errorf("Join should have returned \"foobaz\" in fifth frame, but got: %s", res[4]) 481 | } 482 | if n[5] != 1 { 483 | t.Errorf("Separator is long, expected a single element in sixth frame but got: %d - %v", n[5], res) 484 | } 485 | if string(res[5]) != "x" { 486 | t.Errorf("Join should have returned \"x\" in sixth frame, but got: %s", res[5]) 487 | } 488 | if n[6] != 1 { 489 | t.Errorf("Separator is long, expected a single element in seventh frame but got: %d - %v", n[6], res) 490 | } 491 | if string(res[6]) != "wwxxyyzz" { 492 | t.Errorf("Join should have returned \"wwxxyyzz\" in seventh frame, but got: %s", res[6]) 493 | } 494 | 495 | res, n = c.joinMaxSize(elements[4:], " ", 6) 496 | if len(res) != len(n) && len(res) != 3 { 497 | t.Errorf("Was expecting 3 frames to flush but got: %v - %v", n, res) 498 | 499 | } 500 | if n[0] != 1 { 501 | t.Errorf("Element should just fit in frame - expected single element in frame: %d - %v", n[0], res) 502 | } 503 | if string(res[0]) != "foobaz" { 504 | t.Errorf("Join should have returned \"foobaz\" in first frame, but got: %s", res[0]) 505 | } 506 | if n[1] != 1 { 507 | t.Errorf("Single element expected in frame, but got. %d - %v", n[1], res) 508 | } 509 | if string(res[1]) != "x" { 510 | t.Errorf("Join should' have returned \"x\" in second frame, but got: %s", res[1]) 511 | } 512 | if n[2] != 1 { 513 | t.Errorf("Even though element is greater then max size we still try to send it. %d - %v", n[2], res) 514 | } 515 | if string(res[2]) != "wwxxyyzz" { 516 | t.Errorf("Join should have returned \"wwxxyyzz\" in third frame, but got: %s", res[2]) 517 | } 518 | } 519 | 520 | func TestSendMsgUDP(t *testing.T) { 521 | addr := "localhost:1201" 522 | udpAddr, err := net.ResolveUDPAddr("udp", addr) 523 | if err != nil { 524 | t.Fatal(err) 525 | } 526 | 527 | server, err := net.ListenUDP("udp", udpAddr) 528 | if err != nil { 529 | t.Fatal(err) 530 | } 531 | defer server.Close() 532 | 533 | client, err := New(addr) 534 | if err != nil { 535 | t.Fatal(err) 536 | } 537 | 538 | err = client.sendMsg(strings.Repeat("x", MaxUDPPayloadSize+1)) 539 | if err == nil { 540 | t.Error("Expected error to be returned if message size is bigger than MaxUDPPayloadSize") 541 | } 542 | 543 | message := "test message" 544 | 545 | err = client.sendMsg(message) 546 | if err != nil { 547 | t.Errorf("Expected no error to be returned if message size is smaller or equal to MaxUDPPayloadSize, got: %s", err.Error()) 548 | } 549 | 550 | buffer := make([]byte, MaxUDPPayloadSize+1) 551 | n, err := io.ReadAtLeast(server, buffer, 1) 552 | 553 | if err != nil { 554 | t.Fatalf("Expected no error to be returned reading the buffer, got: %s", err.Error()) 555 | } 556 | 557 | if n != len(message) { 558 | t.Fatalf("Failed to read full message from buffer. Got size `%d` expected `%d`", n, MaxUDPPayloadSize) 559 | } 560 | 561 | if string(buffer[:n]) != message { 562 | t.Fatalf("The received message did not match what we expect.") 563 | } 564 | 565 | client, err = NewBuffered(addr, 1) 566 | if err != nil { 567 | t.Fatal(err) 568 | } 569 | 570 | err = client.sendMsg(strings.Repeat("x", MaxUDPPayloadSize+1)) 571 | if err == nil { 572 | t.Error("Expected error to be returned if message size is bigger than MaxUDPPayloadSize") 573 | } 574 | 575 | err = client.sendMsg(message) 576 | if err != nil { 577 | t.Errorf("Expected no error to be returned if message size is smaller or equal to MaxUDPPayloadSize, got: %s", err.Error()) 578 | } 579 | 580 | client.Lock() 581 | err = client.flushLocked() 582 | client.Unlock() 583 | 584 | if err != nil { 585 | t.Fatalf("Expected no error to be returned flushing the client, got: %s", err.Error()) 586 | } 587 | 588 | buffer = make([]byte, MaxUDPPayloadSize+1) 589 | n, err = io.ReadAtLeast(server, buffer, 1) 590 | 591 | if err != nil { 592 | t.Fatalf("Expected no error to be returned reading the buffer, got: %s", err.Error()) 593 | } 594 | 595 | if n != len(message) { 596 | t.Fatalf("Failed to read full message from buffer. Got size `%d` expected `%d`", n, MaxUDPPayloadSize) 597 | } 598 | 599 | if string(buffer[:n]) != message { 600 | t.Fatalf("The received message did not match what we expect.") 601 | } 602 | } 603 | 604 | func TestSendUDSErrors(t *testing.T) { 605 | dir, err := ioutil.TempDir("", "socket") 606 | if err != nil { 607 | t.Fatal(err) 608 | } 609 | defer os.RemoveAll(dir) // clean up 610 | 611 | message := "test message" 612 | 613 | addr := filepath.Join(dir, "dsd.socket") 614 | udsAddr, err := net.ResolveUnixAddr("unixgram", addr) 615 | if err != nil { 616 | t.Fatal(err) 617 | } 618 | 619 | addrParts := []string{UnixAddressPrefix, addr} 620 | client, err := New(strings.Join(addrParts, "")) 621 | if err != nil { 622 | t.Fatal(err) 623 | } 624 | 625 | // Server not listening yet 626 | err = client.sendMsg(message) 627 | if err == nil || !strings.HasSuffix(err.Error(), "no such file or directory") { 628 | t.Errorf("Expected error \"no such file or directory\", got: %s", err.Error()) 629 | } 630 | 631 | // Start server and send packet 632 | server, err := net.ListenUnixgram("unixgram", udsAddr) 633 | if err != nil { 634 | t.Fatal(err) 635 | } 636 | err = client.sendMsg(message) 637 | if err != nil { 638 | t.Errorf("Expected no error to be returned when server is listening, got: %s", err.Error()) 639 | } 640 | bytes := make([]byte, 1024) 641 | n, err := server.Read(bytes) 642 | if err != nil { 643 | t.Fatal(err) 644 | } 645 | if string(bytes[:n]) != message { 646 | t.Errorf("Expected: %s. Actual: %s", string(message), string(bytes)) 647 | } 648 | 649 | // close server and send packet 650 | server.Close() 651 | os.Remove(addr) 652 | err = client.sendMsg(message) 653 | if err == nil { 654 | t.Error("Expected an error, got nil") 655 | } 656 | 657 | // Restart server and send packet 658 | server, err = net.ListenUnixgram("unixgram", udsAddr) 659 | if err != nil { 660 | t.Fatal(err) 661 | } 662 | time.Sleep(100 * time.Millisecond) 663 | defer server.Close() 664 | err = client.sendMsg(message) 665 | if err != nil { 666 | t.Errorf("Expected no error to be returned when server is listening, got: %s", err.Error()) 667 | } 668 | 669 | bytes = make([]byte, 1024) 670 | n, err = server.Read(bytes) 671 | if err != nil { 672 | t.Fatal(err) 673 | } 674 | if string(bytes[:n]) != message { 675 | t.Errorf("Expected: %s. Actual: %s", string(message), string(bytes)) 676 | } 677 | } 678 | 679 | func TestSendUDSIgnoreErrors(t *testing.T) { 680 | client, err := New("unix:///invalid") 681 | if err != nil { 682 | t.Fatal(err) 683 | } 684 | 685 | // Default mode throws error 686 | err = client.sendMsg("message") 687 | if err == nil || !strings.HasSuffix(err.Error(), "no such file or directory") { 688 | t.Errorf("Expected error \"connect: no such file or directory\", got: %s", err.Error()) 689 | } 690 | 691 | // Skip errors 692 | client.SkipErrors = true 693 | err = client.sendMsg("message") 694 | if err != nil { 695 | t.Errorf("Expected no error to be returned when in skip errors mode, got: %s", err.Error()) 696 | } 697 | } 698 | 699 | func TestNilSafe(t *testing.T) { 700 | var c *Client 701 | assertNotPanics(t, func() { c.Close() }) 702 | assertNotPanics(t, func() { c.Count("", 0, nil, 1) }) 703 | assertNotPanics(t, func() { c.Histogram("", 0, nil, 1) }) 704 | assertNotPanics(t, func() { c.Distribution("", 0, nil, 1) }) 705 | assertNotPanics(t, func() { c.Gauge("", 0, nil, 1) }) 706 | assertNotPanics(t, func() { c.Set("", "", nil, 1) }) 707 | assertNotPanics(t, func() { 708 | c.send("", "", []byte(""), nil, 1) 709 | }) 710 | assertNotPanics(t, func() { c.SimpleEvent("", "") }) 711 | } 712 | 713 | func TestEvents(t *testing.T) { 714 | matrix := []struct { 715 | event *Event 716 | encoded string 717 | }{ 718 | { 719 | NewEvent("Hello", "Something happened to my event"), 720 | `_e{5,30}:Hello|Something happened to my event`, 721 | }, { 722 | &Event{Title: "hi", Text: "okay", AggregationKey: "foo"}, 723 | `_e{2,4}:hi|okay|k:foo`, 724 | }, { 725 | &Event{Title: "hi", Text: "okay", AggregationKey: "foo", AlertType: Info}, 726 | `_e{2,4}:hi|okay|k:foo|t:info`, 727 | }, { 728 | &Event{Title: "hi", Text: "w/e", AlertType: Error, Priority: Normal}, 729 | `_e{2,3}:hi|w/e|p:normal|t:error`, 730 | }, { 731 | &Event{Title: "hi", Text: "uh", Tags: []string{"host:foo", "app:bar"}}, 732 | `_e{2,2}:hi|uh|#host:foo,app:bar`, 733 | }, { 734 | &Event{Title: "hi", Text: "line1\nline2", Tags: []string{"hello\nworld"}}, 735 | `_e{2,12}:hi|line1\nline2|#helloworld`, 736 | }, 737 | } 738 | 739 | for _, m := range matrix { 740 | r, err := m.event.Encode() 741 | if err != nil { 742 | t.Errorf("Error encoding: %s\n", err) 743 | continue 744 | } 745 | if r != m.encoded { 746 | t.Errorf("Expected `%s`, got `%s`\n", m.encoded, r) 747 | } 748 | } 749 | 750 | e := NewEvent("", "hi") 751 | if _, err := e.Encode(); err == nil { 752 | t.Errorf("Expected error on empty Title.") 753 | } 754 | 755 | e = NewEvent("hi", "") 756 | if _, err := e.Encode(); err == nil { 757 | t.Errorf("Expected error on empty Text.") 758 | } 759 | 760 | e = NewEvent("hello", "world") 761 | s, err := e.Encode("tag1", "tag2") 762 | if err != nil { 763 | t.Error(err) 764 | } 765 | expected := "_e{5,5}:hello|world|#tag1,tag2" 766 | if s != expected { 767 | t.Errorf("Expected %s, got %s", expected, s) 768 | } 769 | if len(e.Tags) != 0 { 770 | t.Errorf("Modified event in place illegally.") 771 | } 772 | } 773 | 774 | func TestServiceChecks(t *testing.T) { 775 | matrix := []struct { 776 | serviceCheck *ServiceCheck 777 | encoded string 778 | }{ 779 | { 780 | NewServiceCheck("DataCatService", Ok), 781 | `_sc|DataCatService|0`, 782 | }, { 783 | NewServiceCheck("DataCatService", Warn), 784 | `_sc|DataCatService|1`, 785 | }, { 786 | NewServiceCheck("DataCatService", Critical), 787 | `_sc|DataCatService|2`, 788 | }, { 789 | NewServiceCheck("DataCatService", Unknown), 790 | `_sc|DataCatService|3`, 791 | }, { 792 | &ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat"}, 793 | `_sc|DataCatService|0|h:DataStation.Cat`, 794 | }, { 795 | &ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat", Message: "Here goes valuable message"}, 796 | `_sc|DataCatService|0|h:DataStation.Cat|m:Here goes valuable message`, 797 | }, { 798 | &ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat", Message: "Here are some cyrillic chars: к л м н о п р с т у ф х ц ч ш"}, 799 | `_sc|DataCatService|0|h:DataStation.Cat|m:Here are some cyrillic chars: к л м н о п р с т у ф х ц ч ш`, 800 | }, { 801 | &ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat", Message: "Here goes valuable message", Tags: []string{"host:foo", "app:bar"}}, 802 | `_sc|DataCatService|0|h:DataStation.Cat|#host:foo,app:bar|m:Here goes valuable message`, 803 | }, { 804 | &ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat", Message: "Here goes \n that should be escaped", Tags: []string{"host:foo", "app:b\nar"}}, 805 | `_sc|DataCatService|0|h:DataStation.Cat|#host:foo,app:bar|m:Here goes \n that should be escaped`, 806 | }, { 807 | &ServiceCheck{Name: "DataCatService", Status: Ok, Hostname: "DataStation.Cat", Message: "Here goes m: that should be escaped", Tags: []string{"host:foo", "app:bar"}}, 808 | `_sc|DataCatService|0|h:DataStation.Cat|#host:foo,app:bar|m:Here goes m\: that should be escaped`, 809 | }, 810 | } 811 | 812 | for _, m := range matrix { 813 | r, err := m.serviceCheck.Encode() 814 | if err != nil { 815 | t.Errorf("Error encoding: %s\n", err) 816 | continue 817 | } 818 | if r != m.encoded { 819 | t.Errorf("Expected `%s`, got `%s`\n", m.encoded, r) 820 | } 821 | } 822 | 823 | sc := NewServiceCheck("", Ok) 824 | if _, err := sc.Encode(); err == nil { 825 | t.Errorf("Expected error on empty Name.") 826 | } 827 | 828 | sc = NewServiceCheck("sc", ServiceCheckStatus(5)) 829 | if _, err := sc.Encode(); err == nil { 830 | t.Errorf("Expected error on invalid status value.") 831 | } 832 | 833 | sc = NewServiceCheck("hello", Warn) 834 | s, err := sc.Encode("tag1", "tag2") 835 | if err != nil { 836 | t.Error(err) 837 | } 838 | expected := "_sc|hello|1|#tag1,tag2" 839 | if s != expected { 840 | t.Errorf("Expected %s, got %s", expected, s) 841 | } 842 | if len(sc.Tags) != 0 { 843 | t.Errorf("Modified serviceCheck in place illegally.") 844 | } 845 | } 846 | 847 | func TestFlushOnClose(t *testing.T) { 848 | client, err := NewBuffered("localhost:1201", 64) 849 | if err != nil { 850 | t.Fatal(err) 851 | } 852 | // stop the flushing mechanism so we can test the buffer without interferences 853 | client.stop <- struct{}{} 854 | 855 | message := "test message" 856 | 857 | err = client.sendMsg(message) 858 | if err != nil { 859 | t.Fatal(err) 860 | } 861 | 862 | if len(client.commands) != 1 { 863 | t.Errorf("Commands buffer should contain 1 item, got %d", len(client.commands)) 864 | } 865 | 866 | err = client.Close() 867 | if err != nil { 868 | t.Fatal(err) 869 | } 870 | 871 | if len(client.commands) != 0 { 872 | t.Errorf("Commands buffer should be empty, got %d", len(client.commands)) 873 | } 874 | } 875 | 876 | // These benchmarks show that using different format options: 877 | // v1: sprintf-ing together a bunch of intermediate strings is 4-5x faster 878 | // v2: some use of buffer 879 | // v3: removing sprintf from stat generation and pushing stat building into format 880 | func BenchmarkFormatV3(b *testing.B) { 881 | b.StopTimer() 882 | c := &Client{} 883 | c.Namespace = "foo.bar." 884 | c.Tags = []string{"app:foo", "host:bar"} 885 | b.StartTimer() 886 | for i := 0; i < b.N; i++ { 887 | c.format("system.cpu.idle", 10, gaugeSuffix, []string{"foo"}, 1) 888 | c.format("system.cpu.load", 0.1, gaugeSuffix, nil, 0.9) 889 | } 890 | } 891 | 892 | func BenchmarkFormatV1(b *testing.B) { 893 | b.StopTimer() 894 | c := &Client{} 895 | c.Namespace = "foo.bar." 896 | c.Tags = []string{"app:foo", "host:bar"} 897 | b.StartTimer() 898 | for i := 0; i < b.N; i++ { 899 | c.formatV1("system.cpu.idle", 10, []string{"foo"}, 1) 900 | c.formatV1("system.cpu.load", 0.1, nil, 0.9) 901 | } 902 | } 903 | 904 | // V1 formatting function, added to client for tests 905 | func (c *Client) formatV1(name string, value float64, tags []string, rate float64) string { 906 | valueAsString := fmt.Sprintf("%f|g", value) 907 | if rate < 1 { 908 | valueAsString = fmt.Sprintf("%s|@%f", valueAsString, rate) 909 | } 910 | if c.Namespace != "" { 911 | name = fmt.Sprintf("%s%s", c.Namespace, name) 912 | } 913 | 914 | tags = append(c.Tags, tags...) 915 | if len(tags) > 0 { 916 | valueAsString = fmt.Sprintf("%s|#%s", valueAsString, strings.Join(tags, ",")) 917 | } 918 | 919 | return fmt.Sprintf("%s:%s", name, valueAsString) 920 | 921 | } 922 | 923 | func BenchmarkFormatV2(b *testing.B) { 924 | b.StopTimer() 925 | c := &Client{} 926 | c.Namespace = "foo.bar." 927 | c.Tags = []string{"app:foo", "host:bar"} 928 | b.StartTimer() 929 | for i := 0; i < b.N; i++ { 930 | c.formatV2("system.cpu.idle", 10, []string{"foo"}, 1) 931 | c.formatV2("system.cpu.load", 0.1, nil, 0.9) 932 | } 933 | } 934 | 935 | // V2 formatting function, added to client for tests 936 | func (c *Client) formatV2(name string, value float64, tags []string, rate float64) string { 937 | var buf bytes.Buffer 938 | if c.Namespace != "" { 939 | buf.WriteString(c.Namespace) 940 | } 941 | buf.WriteString(name) 942 | buf.WriteString(":") 943 | buf.WriteString(fmt.Sprintf("%f|g", value)) 944 | if rate < 1 { 945 | buf.WriteString(`|@`) 946 | buf.WriteString(strconv.FormatFloat(rate, 'f', -1, 64)) 947 | } 948 | 949 | writeTagString(&buf, c.Tags, tags) 950 | 951 | return buf.String() 952 | } 953 | --------------------------------------------------------------------------------