diff --git a/README.md b/README.md index 2cc0daa2..4a952147 100644 --- a/README.md +++ b/README.md @@ -864,11 +864,11 @@ $ ulimit -u # processes / threads Just pass a new number as the argument to change it. -## Prometheus Support +## Prometheus support -Vegeta has a built-in Prometheus Exporter that may be enabled during attacks so that you can point any Prometheus instance to Vegeta instances and get some metrics about http requests performance and about the Vegeta process itself. +Vegeta has a built-in Prometheus Exporter that may be enabled during attacks so that you can point any Prometheus instance to Vegeta attack processes and monitor attack metrics. -To enable the Prometheus Exporter on the command line, use the "prometheus-addr" flag. +To enable the Prometheus Exporter on the command line, set the "prometheus-addr" flag. A Prometheus HTTP endpoint will be available only during the lifespan of an attack and will be closed right after the attack is finished. @@ -877,53 +877,22 @@ The following metrics are exposed: * `request_bytes_in` - bytes count received from targeted servers by "url", "method" and "status" * `request_bytes_out` - bytes count sent to targeted server by "url", "method" and "status" * `request_seconds` - histogram with request latency and counters by "url", "method" and "status" +* `request_fail_count` - count of failed requests by "url", "method", "status" and "message" - + Check file [lib/prom/grafana.json](lib/prom/grafana.json) with the source of this sample dashboard in Grafana. -### Samples +### Limitations -If you want to query P90 quantiles, for example, use "histogram_quantile(0.90, sum(rate(request_seconds_bucket[1m])) by (le, status))" +1. Prometheus scrapes metrics from a running vegeta attack process and assigns timestamps to samples on its server. This means result timestamps aren't accurate (i.e. they're scraping time, not result time). +2. Configuring Prometheus to scrape vegeta needs to happen out-of-band. That's a hassle! +3. Since there's no coordination between a vegeta attack process and a Prometheus server, an attack process will finish before Prometheus has the chance to scrape the latest observations. -### Prometheus Exporter example -* Create a docker-compose.yml +Why aren't we using pushgateway instead? See [this comment](https://github.com/tsenart/vegeta/pull/534#issuecomment-1629943731). -``` -version: '3.5' -services: - vegeta: - image: tsenart/vegeta - ports: - - 8880:8880 - command: sh -c 'echo "GET https://www.yahoo.com" | vegeta attack -duration=30s -rate=5 -prometheus-addr=0.0.0.0:8880' - - prometheus: - image: flaviostutz/prometheus:2.19.2.0 - ports: - - 9090:9090 - environment: - - SCRAPE_INTERVAL=10s - - SCRAPE_TIMEOUT=10s - - STATIC_SCRAPE_TARGETS=vegeta@vegeta:8880 -``` - -* Run `docker-compose up -d` - -* Run `curl localhost:8880` to see plain Prometheus Exporter endpoint contents - -* Open Prometheus server instance with your browser at http://localhost:9090 - -* Go to "Graph" and execute query `rate(request_seconds_sum[1m])` and then select the "Graph" tab to see a graph with latency over time - -#### More resources - -* See https://prometheus.io/docs/prometheus/latest/querying/basics/ for query details - -* Use Grafana for creating stateful dashboards. Get a sample dashboard for Vegeta [here](grafana.json) - -* For more elaborated scenarios, see https://github.com/flaviostutz/promster so that you can automatically register new Vegeta Prometheus Exporter instances to Prometheus in elastic scenarios. +There's [an issue](https://github.com/tsenart/vegeta/issues/637) tracking the proper solution to all these limitations which is a remote write integration. ## License diff --git a/attack.go b/attack.go index 8362e03c..f83c2cba 100644 --- a/attack.go +++ b/attack.go @@ -183,20 +183,22 @@ func attack(opts *attackOpts) (err error) { return err } - // Start Prometheus Metrics and Server var pm *prom.Metrics if opts.promAddr != "" { + pm = prom.NewMetrics() + r := prometheus.NewRegistry() - pm, err = prom.NewMetrics(r) - if err != nil { - return err + if err := pm.Register(r); err != nil { + return fmt.Errorf("error registering prometheus metrics: %s", err) } - srv, err := prom.StartPromServer(opts.promAddr, r) - if err != nil { - return err + + srv := http.Server{ + Addr: opts.promAddr, + Handler: prom.NewHandler(r, time.Now().UTC()), } defer srv.Close() + go srv.ListenAndServe() } atk := vegeta.NewAttacker( @@ -245,9 +247,11 @@ func processAttack( if !ok { return nil } + if pm != nil { pm.Observe(r) } + if err := enc.Encode(r); err != nil { return err } diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 5ac11cdc..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,27 +0,0 @@ -version: '3.5' - -services: - - vegeta: - build: . - image: tsenart/vegeta - ports: - - 8880:8880 - command: sh -c 'echo "GET https://www.yahoo.com" | vegeta attack -duration=300s -rate=5 -prometheus-addr=0.0.0.0:8880 | vegeta report --type=text' - - prometheus: - image: flaviostutz/prometheus - ports: - - 9090:9090 - environment: - - SCRAPE_INTERVAL=10s - - SCRAPE_TIMEOUT=10s - - STATIC_SCRAPE_TARGETS=vegeta@vegeta:8880 - - grafana: - image: flaviostutz/grafana:5.2.4 - ports: - - 3000:3000 - environment: - - GF_SECURITY_ADMIN_PASSWORD=mypass - diff --git a/go.mod b/go.mod index f983596c..eee42379 100644 --- a/go.mod +++ b/go.mod @@ -23,13 +23,17 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd // indirect github.com/iancoleman/orderedmap v0.3.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.42.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect + github.com/prometheus/prometheus v0.45.0 // indirect golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect golang.org/x/mod v0.12.0 // indirect golang.org/x/sync v0.3.0 // indirect diff --git a/go.sum b/go.sum index 720e83b5..208cff90 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/dgryski/go-gk v0.0.0-20200319235926-a69029f61654/go.mod h1:qm+vckxRlD github.com/dgryski/go-lttb v0.0.0-20230207170358-f8fc36cdbff1 h1:dxwR3CStJdJamsIoMPCmxuIfBAPTgmzvFax+MvFav3M= github.com/dgryski/go-lttb v0.0.0-20230207170358-f8fc36cdbff1/go.mod h1:UwftcHUI/qTYvLAxrWmANuRckf8+08O3C3hwStvkhDU= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -25,6 +27,8 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd h1:PpuIBO5P3e9hpqBD0O/HjhShYuM6XE0i/lbE6J94kww= +github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= @@ -35,6 +39,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= @@ -44,16 +50,24 @@ github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/prometheus/prometheus v0.45.0 h1:O/uG+Nw4kNxx/jDPxmjsSDd+9Ohql6E7ZSY1x5x/0KI= +github.com/prometheus/prometheus v0.45.0/go.mod h1:jC5hyO8ItJBnDWGecbEucMyXjzxGv9cxsxsjS9u5s1w= github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 h1:Lt9DzQALzHoDwMBGJ6v8ObDPR0dzr2a6sXTB1Fq7IHs= github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= github.com/streadway/quantile v0.0.0-20220407130108-4246515d968d h1:X4+kt6zM/OVO6gbJdAfJR60MGPsqCzbtXNnjoGqdfAs= @@ -62,25 +76,50 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/tsenart/go-tsz v0.0.0-20180814235614-0bd30b3df1c3 h1:pcQGQzTwCg//7FgVywqge1sW9Yf8VMsMdG58MI5kd8s= github.com/tsenart/go-tsz v0.0.0-20180814235614-0bd30b3df1c3/go.mod h1:SWZznP1z5Ki7hDT2ioqiFKEse8K9tU2OUvaRI0NeGQo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca h1:PupagGYwj8+I4ubCxcmcBRk3VlUWtTg5huQpZR9flmE= gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= diff --git a/lib/prom/prom.go b/lib/prom/prom.go index 2872eb36..fb885f68 100644 --- a/lib/prom/prom.go +++ b/lib/prom/prom.go @@ -1,7 +1,7 @@ package prom import ( - "errors" + "fmt" "net/http" "strconv" "time" @@ -14,123 +14,69 @@ import ( // Metrics encapsulates Prometheus metrics of an attack. type Metrics struct { - RequestSecondsHistogram *prometheus.HistogramVec - RequestBytesInCounter *prometheus.CounterVec - RequestBytesOutCounter *prometheus.CounterVec - RequestFailCounter *prometheus.CounterVec - Registry prometheus.Registerer + requestLatencyHistogram *prometheus.HistogramVec + requestBytesInCounter *prometheus.CounterVec + requestBytesOutCounter *prometheus.CounterVec + requestFailCounter *prometheus.CounterVec } -// NewMetrics returns a new Metrics instance and registers all of them in the given Registry. -func NewMetrics(registry prometheus.Registerer) (*Metrics, error) { - if registry == nil { - registry = prometheus.DefaultRegisterer +// NewMetrics returns a new Metrics instance that must be +// registered in a Prometheus registry with Register. +func NewMetrics() *Metrics { + baseLabels := []string{"method", "url", "status"} + return &Metrics{ + requestLatencyHistogram: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: "request_seconds", + Help: "Request latency", + Buckets: prometheus.DefBuckets, + }, baseLabels), + requestBytesInCounter: prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "request_bytes_in", + Help: "Bytes received from servers as response to requests", + }, baseLabels), + requestBytesOutCounter: prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "request_bytes_out", + Help: "Bytes sent to servers during requests", + }, baseLabels), + requestFailCounter: prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "request_fail_count", + Help: "Count of failed requests", + }, append(baseLabels[:len(baseLabels):len(baseLabels)], "message")), } - - pm := &Metrics{Registry: registry} - - pm.RequestSecondsHistogram = prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Name: "request_seconds", - Help: "Request latency", - Buckets: prometheus.DefBuckets, - }, []string{ - "method", - "url", - "status", - }) - err := pm.Registry.Register(pm.RequestSecondsHistogram) - if err != nil { - return nil, err - } - - pm.RequestBytesInCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ - Name: "request_bytes_in", - Help: "Bytes received from servers as response to requests", - }, []string{ - "method", - "url", - "status", - }) - err = pm.Registry.Register(pm.RequestBytesInCounter) - if err != nil { - return nil, err - } - - pm.RequestBytesOutCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ - Name: "request_bytes_out", - Help: "Bytes sent to servers during requests", - }, []string{ - "method", - "url", - "status", - }) - err = pm.Registry.Register(pm.RequestBytesOutCounter) - if err != nil { - return nil, err - } - - pm.RequestFailCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ - Name: "request_fail_count", - Help: "Internal failures that prevented a hit to the target server", - }, []string{ - "method", - "url", - "code", - "message", - }) - err = pm.Registry.Register(pm.RequestFailCounter) - if err != nil { - return nil, err - } - - return pm, nil } -// Unregister all prometheus collectors -func (pm *Metrics) Unregister() error { - exists := pm.Registry.Unregister(pm.RequestSecondsHistogram) - if !exists { - return errors.New("'RequestSecondsHistogram' cannot be unregistered because it was not found") - } - - exists = pm.Registry.Unregister(pm.RequestBytesInCounter) - if !exists { - return errors.New("'RequestBytesInCounter' cannot be unregistered because it was not found") - } - - exists = pm.Registry.Unregister(pm.RequestBytesOutCounter) - if !exists { - return errors.New("'RequestBytesOutCounter' cannot be unregistered because it was not found") - } - - exists = pm.Registry.Unregister(pm.RequestFailCounter) - if !exists { - return errors.New("'RequestFailCounter' cannot be unregistered because it was not found") +// Register registers all Prometheus metrics in r. +func (pm *Metrics) Register(r prometheus.Registerer) error { + for _, c := range []prometheus.Collector{ + pm.requestLatencyHistogram, + pm.requestBytesInCounter, + pm.requestBytesOutCounter, + pm.requestFailCounter, + } { + if err := r.Register(c); err != nil { + return fmt.Errorf("failed to register metric %v: %w", c, err) + } } - return nil } -// Observe metrics with hit results +// Observe metrics given a vegeta.Result. func (pm *Metrics) Observe(res *vegeta.Result) { code := strconv.FormatUint(uint64(res.Code), 10) - pm.RequestBytesInCounter.WithLabelValues(res.Method, res.URL, code).Add(float64(res.BytesIn)) - pm.RequestBytesOutCounter.WithLabelValues(res.Method, res.URL, code).Add(float64(res.BytesOut)) - pm.RequestSecondsHistogram.WithLabelValues(res.Method, res.URL, code).Observe(float64(res.Latency) / float64(time.Second)) + pm.requestBytesInCounter.WithLabelValues(res.Method, res.URL, code).Add(float64(res.BytesIn)) + pm.requestBytesOutCounter.WithLabelValues(res.Method, res.URL, code).Add(float64(res.BytesOut)) + pm.requestLatencyHistogram.WithLabelValues(res.Method, res.URL, code).Observe(res.Latency.Seconds()) if res.Error != "" { - pm.RequestFailCounter.WithLabelValues(res.Method, res.URL, code, res.Error) + pm.requestFailCounter.WithLabelValues(res.Method, res.URL, code, res.Error) } } -// StartPromServer starts a new Prometheus server with metrics present in promRegistry -// launches a http server in a new goroutine and returns the http.Server instance -func StartPromServer(bindAddr string, promRegistry *prometheus.Registry) (*http.Server, error) { - srv := http.Server{ - Addr: bindAddr, - Handler: promhttp.HandlerFor(promRegistry, promhttp.HandlerOpts{}), - } - - go srv.ListenAndServe() - - return &srv, nil +// NewHandler returns a new http.Handler that exposes Prometheus +// metrics registed in r in the OpenMetrics format. +func NewHandler(r *prometheus.Registry, startTime time.Time) http.Handler { + return promhttp.HandlerFor(r, promhttp.HandlerOpts{ + Registry: r, + EnableOpenMetrics: true, + ProcessStartTime: startTime, + }) } diff --git a/lib/prom/prom_test.go b/lib/prom/prom_test.go index acb4332b..4c07a8cb 100644 --- a/lib/prom/prom_test.go +++ b/lib/prom/prom_test.go @@ -1,215 +1,90 @@ package prom import ( - "context" "io" "net/http" - "strings" + "net/http/httptest" "testing" "time" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/prometheus/model/textparse" vegeta "github.com/tsenart/vegeta/v12/lib" ) -func TestPromMetrics1(t *testing.T) { - pm, err := NewMetrics(nil) - if err != nil { - t.Errorf("Error creating metrics. err=%s", err) - } - - err = pm.Unregister() - if err != nil { - t.Errorf("Cannot unregister metrics. err=%s", err) - } -} - -func TestPromMetrics2(t *testing.T) { +func TestMetrics_Observe(t *testing.T) { reg := prometheus.NewRegistry() + pm := NewMetrics() - pm, err := NewMetrics(reg) - if err != nil { - t.Errorf("Error creating metrics. err=%s", err) + if err := pm.Register(reg); err != nil { + t.Fatal("error registering metrics", err) } - err = pm.Unregister() - if err != nil { - t.Errorf("Cannot unregister metrics. err=%s", err) - } + srv := httptest.NewServer(NewHandler(reg, time.Now().UTC())) + defer srv.Close() - // register again to check if registry was cleared correctly - pm, err = NewMetrics(reg) - if err != nil { - t.Errorf("Error creating metrics. err=%s", err) - } - - err = pm.Unregister() - if err != nil { - t.Errorf("Cannot unregister metrics. err=%s", err) - } - - // register again to check if registry was cleared correctly - pm, err = NewMetrics(reg) - if err != nil { - t.Errorf("Error creating metrics. err=%s", err) - } - - err = pm.Unregister() - if err != nil { - t.Errorf("Cannot unregister metrics. err=%s", err) - } - -} - -func TestPromServerBasic1(t *testing.T) { - r := prometheus.NewRegistry() - pm, err := NewMetrics(r) - if err != nil { - t.Errorf("Error creating metrics. err=%s", err) - } - - srv, err := StartPromServer("0.0.0.0:8880", r) - if err != nil { - t.Errorf("Error starting server. err=%s", err) - } - - err = srv.Shutdown(context.Background()) - if err != nil { - t.Errorf("Error shutting down server. err=%s", err) - } - pm.Unregister() -} - -func TestPromServerBasic2(t *testing.T) { - reg := prometheus.NewRegistry() - - pm, err := NewMetrics(reg) - if err != nil { - t.Errorf("Error creating metrics. err=%s", err) - } - - // start/stop 1 - srv, err := StartPromServer("0.0.0.0:8880", reg) - if err != nil { - t.Errorf("Error starting server. err=%s", err) - } - err = srv.Shutdown(context.Background()) - if err != nil { - t.Errorf("Error shutting down server. err=%s", err) - } - - // start/stop 2 - srv, err = StartPromServer("0.0.0.0:8880", reg) - if err != nil { - t.Errorf("Error starting server. err=%s", err) - } - err = srv.Shutdown(context.Background()) - if err != nil { - t.Errorf("Error shutting down server. err=%s", err) - } - - pm.Unregister() - - // start server again after reusing the same registry (sanity check) - _, err = NewMetrics(reg) - if err != nil { - t.Errorf("Error creating metrics. err=%s", err) - } - // start/stop 1 - srv, err = StartPromServer("0.0.0.0:8880", reg) - if err != nil { - t.Errorf("Error starting server. err=%s", err) - } - err = srv.Shutdown(context.Background()) - if err != nil { - t.Errorf("Error shutting down server. err=%s", err) - } - -} - -func TestPromServerObserve(t *testing.T) { - reg := prometheus.NewRegistry() - pm, err := NewMetrics(reg) - if err != nil { - if err != nil { - t.Errorf("Error launching Prometheus http server. err=%s", err) - } - } - - srv, err := StartPromServer("0.0.0.0:8880", reg) - if err != nil { - t.Errorf("Error starting server. err=%s", err) - } + // XXX: Result timestamps are ignored, since Prometheus aggregates metrics + // and only assigns timestamps to series in the server once it scrapes. + // To have accurate timestamps we'd have to implement a remote write integration. r := &vegeta.Result{ URL: "http://test.com/test1", Method: "GET", - Code: 200, - Error: "", + Code: 500, + Error: "Internal Server Error", Latency: 100 * time.Millisecond, BytesIn: 1000, BytesOut: 50, } - pm.Observe(r) - pm.Observe(r) - pm.Observe(r) + pm.Observe(r) - time.Sleep(3 * time.Second) - resp, err := http.Get("http://localhost:8880") + resp, err := http.Get(srv.URL) if err != nil { - t.Errorf("Error calling prometheus metrics. err=%s", err) + t.Fatalf("failed to get prometheus metrics. err=%s", err) } + if resp.StatusCode != 200 { - t.Errorf("Status code should be 200") + t.Fatalf("status code should be 200. code=%d", resp.StatusCode) } data, err := io.ReadAll(resp.Body) if err != nil { - t.Errorf("Error calling prometheus metrics. err=%s", err) - } - str := string(data) - if len(str) == 0 { - t.Errorf("Body not empty. body=%s", str) + t.Errorf("error reading response body: err=%v", err) } - if !strings.Contains(str, "request_seconds") { - t.Error("Metrics should contain request_seconds") - } - if !strings.Contains(str, "request_bytes_in") { - t.Error("Metrics should contain request_bytes_in") - } - if !strings.Contains(str, "request_bytes_out") { - t.Error("Metrics should contain request_bytes_out") - } - if strings.Contains(str, "request_fail_count") { - t.Error("Metrics should contain request_fail_count") - } - - r.Code = 500 - r.Error = "REQUEST FAILED" - pm.Observe(r) - resp, err = http.Get("http://localhost:8880") + p, err := textparse.New(data, resp.Header.Get("Content-Type"), true) if err != nil { - t.Errorf("Error calling prometheus metrics. err=%s", err) - } - if resp.StatusCode != 200 { - t.Errorf("Status code should be 200") + t.Fatalf("error creating prometheus metrics parser. err=%v", err) } - data, err = io.ReadAll(resp.Body) - if err != nil { - t.Errorf("Error calling prometheus metrics. err=%s", err) + want := map[string]struct{}{ + "request_seconds": struct{}{}, + "request_bytes_in": struct{}{}, + "request_bytes_out": struct{}{}, + "request_fail_count": struct{}{}, } - str = string(data) - if !strings.Contains(str, "request_fail_count") { - t.Error("Metrics should contain request_fail_count") + t.Log(string(data)) + + for len(want) > 0 { + _, err := p.Next() + if err != nil { + if err == io.EOF { + break + } + t.Fatalf("error parsing prometheus metrics. err=%v", err) + } + + name, _ := p.Help() + nameStr := string(name) + + if _, ok := want[nameStr]; ok { + delete(want, nameStr) + } } - err = srv.Shutdown(context.Background()) - if err != nil { - t.Errorf("Error shutting down server. err=%s", err) + if len(want) > 0 { + t.Errorf("missing metrics: %v", want) } - pm.Unregister() } diff --git a/prometheus-sample.png b/lib/prom/prometheus-sample.png similarity index 100% rename from prometheus-sample.png rename to lib/prom/prometheus-sample.png