Skip to content

Commit

Permalink
Merge pull request #17 from rogerwelin/cloudwatch-metrics
Browse files Browse the repository at this point in the history
Cloudwatch metrics
  • Loading branch information
rogerwelin committed May 10, 2020
2 parents 42720fe + 9dc6da7 commit c1ed989
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
@@ -1,6 +1,6 @@
language: go
go:
- 1.13.x
- 1.14.x
os:
- linux
install: true
Expand Down
16 changes: 15 additions & 1 deletion README.md
Expand Up @@ -27,6 +27,7 @@ Toc
* [File Slurp Mode](#file-slurp-mode)
* [Exporting Metrics to File](#exporting-metrics-to-file)
* [Exporting Metrics to Prometheus](#exporting-metrics-to-prometheus)
* [Exporting Metrics to Cloudwatch](#exporting-metrics-to-cloudwatch)
* [Load Test with POST Data](#load-test-with-post-data)
* [Specifying a Duration](#specifying-a-duration-for-the-load-test)
* [Adding HTTP Headers](#adding-http-headers)
Expand All @@ -41,7 +42,7 @@ Features

- **2 Load Testing modes**: one standard and one spread mode where URL Paths can be specified from a file (ideal if you want to hit several underlying microservices)
- **CI Friendly**: Well-suited to be part of a CI pipeline step
- **Flexible metrics**: Prometheus metrics (pushing metrics to Prometheus PushGateway), JSON file
- **Flexible metrics**: Cloudwatch metrics, Prometheus metrics (pushing metrics to Prometheus PushGateway), JSON file
- **Configurable**: Able to pass in arbitrary HTTP headers, able to configure the HTTP client
- **Supports GET, POST & PUT** - POST and PUT data can be defined in a file
- **Cross Platform**: One single pre-built binary for Linux, Mac OSX and Windows
Expand Down Expand Up @@ -160,6 +161,19 @@ Starting Load Test with 100000 requests using 125 concurrent users

```

### Exporting Metrics to Cloudwatch
**Cassowary** can export metrics to AWS Cloudwatch just by adding the *--cloudwatch* flag without a value. Take note that you will need to tell Cassoway which AWS Region you want to use. The easiest way is using an environment variable as shown below:

```bash
$ export AWS_REGION=eu-north-1 && ./cassowary run -u http://localhost:8000 -c 125 -n 100000 --cloudwatch

Starting Load Test with 100000 requests using 125 concurrent users

[ omitted for brevity ]

```


### Load Test with POST Data
Example hitting a POST endpoint where POST json data is defined in a file:

Expand Down
33 changes: 33 additions & 0 deletions cmd/cassowary/cli.go
Expand Up @@ -7,6 +7,8 @@ import (
"os"
"strconv"

"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/fatih/color"
"github.com/rogerwelin/cassowary/pkg/client"
"github.com/urfave/cli"
Expand Down Expand Up @@ -64,6 +66,27 @@ func runLoadTest(c *client.Cassowary) error {
if c.ExportMetrics {
return outPutJSON(c.ExportMetricsFile, metrics)
}

if c.PromExport {
err := c.PushPrometheusMetrics(metrics)
if err != nil {
return err
}
}

if c.Cloudwatch {
session, err := session.NewSession()
if err != nil {
return err
}

svc := cloudwatch.New(session)
_, err = c.PutCloudwatchMetrics(svc, metrics)
if err != nil {
return err
}
}

return nil
}

Expand Down Expand Up @@ -137,6 +160,7 @@ func validateCLI(c *cli.Context) error {
Duration: duration,
PromExport: prometheusEnabled,
PromURL: c.String("prompushgwurl"),
Cloudwatch: c.Bool("cloudwatch"),
ExportMetrics: c.Bool("json-metrics"),
ExportMetricsFile: c.String("json-metrics-file"),
DisableKeepAlive: c.Bool("disable-keep-alive"),
Expand Down Expand Up @@ -184,6 +208,7 @@ func validateCLIFile(c *cli.Context) error {
RequestHeader: header,
PromExport: prometheusEnabled,
PromURL: c.String("prompushgwurl"),
Cloudwatch: c.Bool("cloudwatch"),
ExportMetrics: c.Bool("json-metrics"),
ExportMetricsFile: c.String("json-metrics-file"),
DisableKeepAlive: c.Bool("diable-keep-alive"),
Expand Down Expand Up @@ -237,6 +262,10 @@ func runCLI(args []string) {
Name: "p, prompushgwurl",
Usage: "specify prometheus push gateway url to send metrics (optional)",
},
cli.BoolFlag{
Name: "C, cloudwatch",
Usage: "enable to send metrics to AWS Cloudwatch",
},
cli.StringFlag{
Name: "H, header",
Usage: "add arbitrary header, eg. 'Host: www.example.com'",
Expand Down Expand Up @@ -288,6 +317,10 @@ func runCLI(args []string) {
Name: "p, prompushgwurl",
Usage: "specify prometheus push gateway url to send metrics (optional)",
},
cli.BoolFlag{
Name: "C, cloudwatch",
Usage: "enable to send metrics to AWS Cloudwatch",
},
cli.StringFlag{
Name: "H, header",
Usage: "add arbitrary header, eg. 'Host: www.example.com'",
Expand Down
3 changes: 2 additions & 1 deletion go.mod
@@ -1,8 +1,9 @@
module github.com/rogerwelin/cassowary

go 1.13
go 1.14

require (
github.com/aws/aws-sdk-go v1.30.24
github.com/fatih/color v1.7.0
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Expand Up @@ -3,6 +3,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aws/aws-sdk-go v1.30.24 h1:y3JPD51VuEmVqN3BEDVm4amGpDma2cKJcDPuAU1OR58=
github.com/aws/aws-sdk-go v1.30.24/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
Expand All @@ -19,6 +21,7 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
Expand All @@ -28,6 +31,8 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
Expand All @@ -47,6 +52,7 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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 v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
Expand Down Expand Up @@ -76,12 +82,14 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
154 changes: 154 additions & 0 deletions pkg/client/cloudwatch.go
@@ -0,0 +1,154 @@
package client

import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
)

// PutCloudwatchMetrics exports metrics to AWS Cloudwatch
func (c *Cassowary) PutCloudwatchMetrics(svc cloudwatchiface.CloudWatchAPI, metrics ResultMetrics) (*cloudwatch.PutMetricDataOutput, error) {
resp, err := svc.PutMetricData(&cloudwatch.PutMetricDataInput{
Namespace: aws.String("Cassowary/Metrics"),
MetricData: []*cloudwatch.MetricDatum{
&cloudwatch.MetricDatum{
MetricName: aws.String("tcp_connect_mean"),
Unit: aws.String("Milliseconds"),
Value: aws.Float64(metrics.TCPStats.TCPMean),
Dimensions: []*cloudwatch.Dimension{
&cloudwatch.Dimension{
Name: aws.String("Site"),
Value: aws.String(c.BaseURL),
},
},
},
&cloudwatch.MetricDatum{
MetricName: aws.String("tcp_connect_median"),
Unit: aws.String("Milliseconds"),
Value: aws.Float64(metrics.TCPStats.TCPMedian),
Dimensions: []*cloudwatch.Dimension{
&cloudwatch.Dimension{
Name: aws.String("Site"),
Value: aws.String(c.BaseURL),
},
},
},
&cloudwatch.MetricDatum{
MetricName: aws.String("tcp_connect_95p"),
Unit: aws.String("Milliseconds"),
Value: aws.Float64(metrics.TCPStats.TCP95p),
Dimensions: []*cloudwatch.Dimension{
&cloudwatch.Dimension{
Name: aws.String("Site"),
Value: aws.String(c.BaseURL),
},
},
},
&cloudwatch.MetricDatum{
MetricName: aws.String("server_processing_mean"),
Unit: aws.String("Milliseconds"),
Value: aws.Float64(metrics.ProcessingStats.ServerProcessingMean),
Dimensions: []*cloudwatch.Dimension{
&cloudwatch.Dimension{
Name: aws.String("Site"),
Value: aws.String(c.BaseURL),
},
},
},
&cloudwatch.MetricDatum{
MetricName: aws.String("server_processing_median"),
Unit: aws.String("Milliseconds"),
Value: aws.Float64(metrics.ProcessingStats.ServerProcessingMedian),
Dimensions: []*cloudwatch.Dimension{
&cloudwatch.Dimension{
Name: aws.String("Site"),
Value: aws.String(c.BaseURL),
},
},
},
&cloudwatch.MetricDatum{
MetricName: aws.String("server_processing_95p"),
Unit: aws.String("Milliseconds"),
Value: aws.Float64(metrics.ProcessingStats.ServerProcessing95p),
Dimensions: []*cloudwatch.Dimension{
&cloudwatch.Dimension{
Name: aws.String("Site"),
Value: aws.String(c.BaseURL),
},
},
},
&cloudwatch.MetricDatum{
MetricName: aws.String("content_transfer_mean"),
Unit: aws.String("Milliseconds"),
Value: aws.Float64(metrics.ContentStats.ContentTransferMean),
Dimensions: []*cloudwatch.Dimension{
&cloudwatch.Dimension{
Name: aws.String("Site"),
Value: aws.String(c.BaseURL),
},
},
},
&cloudwatch.MetricDatum{
MetricName: aws.String("content_transfer_median"),
Unit: aws.String("Milliseconds"),
Value: aws.Float64(metrics.ContentStats.ContentTransferMedian),
Dimensions: []*cloudwatch.Dimension{
&cloudwatch.Dimension{
Name: aws.String("Site"),
Value: aws.String(c.BaseURL),
},
},
},
&cloudwatch.MetricDatum{
MetricName: aws.String("content_transfer_95p"),
Unit: aws.String("Milliseconds"),
Value: aws.Float64(metrics.ContentStats.ContentTransfer95p),
Dimensions: []*cloudwatch.Dimension{
&cloudwatch.Dimension{
Name: aws.String("Site"),
Value: aws.String(c.BaseURL),
},
},
},
&cloudwatch.MetricDatum{
MetricName: aws.String("total_requests"),
Unit: aws.String("Count"),
Value: aws.Float64(float64(metrics.TotalRequests)),
Dimensions: []*cloudwatch.Dimension{
&cloudwatch.Dimension{
Name: aws.String("Site"),
Value: aws.String(c.BaseURL),
},
},
},
&cloudwatch.MetricDatum{
MetricName: aws.String("failed_requests"),
Unit: aws.String("Count"),
Value: aws.Float64(float64(metrics.FailedRequests)),
Dimensions: []*cloudwatch.Dimension{
&cloudwatch.Dimension{
Name: aws.String("Site"),
Value: aws.String(c.BaseURL),
},
},
},
&cloudwatch.MetricDatum{
MetricName: aws.String("requests_per_second"),
Unit: aws.String("Count/Second"),
Value: aws.Float64(metrics.RequestsPerSecond),
Dimensions: []*cloudwatch.Dimension{
&cloudwatch.Dimension{
Name: aws.String("Site"),
Value: aws.String(c.BaseURL),
},
},
},
},
})

if err != nil {
return nil, err
}

return resp, nil
}
43 changes: 43 additions & 0 deletions pkg/client/cloudwatch_test.go
@@ -0,0 +1,43 @@
package client

import (
"testing"

"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatch/cloudwatchiface"
)

type mockCloudWatchClient struct {
cloudwatchiface.CloudWatchAPI
}

func (m *mockCloudWatchClient) PutMetricData(input *cloudwatch.PutMetricDataInput) (*cloudwatch.PutMetricDataOutput, error) {
// mock response/functionality
return nil, nil
}

func TestPutCloudwatchMetrics(t *testing.T) {
c := Cassowary{}
metrics := ResultMetrics{
FailedRequests: 1,
TotalRequests: 100,
RequestsPerSecond: 100.10,
TCPStats: tcpStats{
TCPMean: 10.0,
TCPMedian: 10.0,
TCP95p: 10.0,
},
ProcessingStats: serverProcessingStats{
ServerProcessingMean: 1.0,
ServerProcessingMedian: 1.0,
ServerProcessing95p: 1.0,
},
}
mockSvc := &mockCloudWatchClient{}
_, err := c.PutCloudwatchMetrics(mockSvc, metrics)

if err != nil {
t.Errorf("Wanted ok but got error: %v", err)
}

}
1 change: 1 addition & 0 deletions pkg/client/types.go
Expand Up @@ -17,6 +17,7 @@ type Cassowary struct {
ExportMetrics bool
ExportMetricsFile string
PromExport bool
Cloudwatch bool
PromURL string
RequestHeader []string
URLPaths []string
Expand Down

0 comments on commit c1ed989

Please sign in to comment.