-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add aws API related metrics * Add unit test
- Loading branch information
1 parent
086bcb0
commit 9c7a642
Showing
6 changed files
with
336 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
package metrics | ||
|
||
import ( | ||
"github.com/aws/aws-sdk-go/aws/awserr" | ||
"github.com/aws/aws-sdk-go/aws/request" | ||
"github.com/prometheus/client_golang/prometheus" | ||
"strconv" | ||
"time" | ||
) | ||
|
||
const ( | ||
sdkHandlerCollectAPICallMetric = "collectAPICallMetric" | ||
sdkHandlerCollectAPIRequestMetric = "collectAPIRequestMetric" | ||
) | ||
|
||
type collector struct { | ||
instruments *instruments | ||
} | ||
|
||
func NewCollector(registerer prometheus.Registerer) (*collector, error) { | ||
instruments, err := newInstruments(registerer) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &collector{ | ||
instruments: instruments, | ||
}, nil | ||
} | ||
|
||
func (c *collector) InjectHandlers(handlers *request.Handlers) { | ||
handlers.CompleteAttempt.PushFrontNamed(request.NamedHandler{ | ||
Name: sdkHandlerCollectAPIRequestMetric, | ||
Fn: c.collectAPIRequestMetric, | ||
}) | ||
handlers.Complete.PushFrontNamed(request.NamedHandler{ | ||
Name: sdkHandlerCollectAPICallMetric, | ||
Fn: c.collectAPICallMetric, | ||
}) | ||
} | ||
|
||
func (c *collector) collectAPIRequestMetric(r *request.Request) { | ||
service := r.ClientInfo.ServiceID | ||
operation := r.Operation.Name | ||
statusCode := statusCodeForRequest(r) | ||
errorCode := errorCodeForRequest(r) | ||
duration := time.Since(r.AttemptTime) | ||
|
||
c.instruments.apiRequestsTotal.With(map[string]string{ | ||
labelService: service, | ||
labelOperation: operation, | ||
labelStatusCode: statusCode, | ||
labelErrorCode: errorCode, | ||
}).Inc() | ||
c.instruments.apiRequestDurationSecond.With(map[string]string{ | ||
labelService: service, | ||
labelOperation: operation, | ||
}).Observe(duration.Seconds()) | ||
} | ||
|
||
func (c *collector) collectAPICallMetric(r *request.Request) { | ||
service := r.ClientInfo.ServiceID | ||
operation := r.Operation.Name | ||
statusCode := statusCodeForRequest(r) | ||
errorCode := errorCodeForRequest(r) | ||
duration := time.Since(r.Time) | ||
|
||
c.instruments.apiCallsTotal.With(map[string]string{ | ||
labelService: service, | ||
labelOperation: operation, | ||
labelStatusCode: statusCode, | ||
labelErrorCode: errorCode, | ||
}).Inc() | ||
c.instruments.apiCallDurationSeconds.With(map[string]string{ | ||
labelService: service, | ||
labelOperation: operation, | ||
}).Observe(duration.Seconds()) | ||
c.instruments.apiCallRetries.With(map[string]string{ | ||
labelService: service, | ||
labelOperation: operation, | ||
}).Observe(float64(r.RetryCount)) | ||
} | ||
|
||
// statusCodeForRequest returns the http status code for request. | ||
// if there is no http response, returns "0". | ||
func statusCodeForRequest(r *request.Request) string { | ||
if r.HTTPResponse != nil { | ||
return strconv.Itoa(r.HTTPResponse.StatusCode) | ||
} | ||
return "0" | ||
} | ||
|
||
// errorCodeForRequest returns the error code for request. | ||
// if no error happened, returns "". | ||
func errorCodeForRequest(r *request.Request) string { | ||
if r.Error != nil { | ||
if awserr, ok := r.Error.(awserr.Error); ok { | ||
return awserr.Code() | ||
} | ||
return "internal" | ||
} | ||
return "" | ||
} | ||
|
||
// operationForRequest returns the operation for request. | ||
func operationForRequest(r *request.Request) string { | ||
if r.Operation != nil { | ||
return r.Operation.Name | ||
} | ||
return "?" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package metrics | ||
|
||
import ( | ||
"errors" | ||
"github.com/aws/aws-sdk-go/aws/awserr" | ||
"github.com/aws/aws-sdk-go/aws/request" | ||
"github.com/stretchr/testify/assert" | ||
"net/http" | ||
"testing" | ||
) | ||
|
||
func Test_statusCodeForRequest(t *testing.T) { | ||
type args struct { | ||
r *request.Request | ||
} | ||
tests := []struct { | ||
name string | ||
args args | ||
want string | ||
}{ | ||
{ | ||
name: "requests without http response", | ||
args: args{ | ||
r: &request.Request{}, | ||
}, | ||
want: "0", | ||
}, | ||
{ | ||
name: "requests with http response", | ||
args: args{ | ||
r: &request.Request{ | ||
HTTPResponse: &http.Response{ | ||
StatusCode: 200, | ||
}, | ||
}, | ||
}, | ||
want: "200", | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
got := statusCodeForRequest(tt.args.r) | ||
assert.Equal(t, tt.want, got) | ||
}) | ||
} | ||
} | ||
|
||
func Test_errorCodeForRequest(t *testing.T) { | ||
type args struct { | ||
r *request.Request | ||
} | ||
tests := []struct { | ||
name string | ||
args args | ||
want string | ||
}{ | ||
{ | ||
name: "requests without error", | ||
args: args{ | ||
r: &request.Request{}, | ||
}, | ||
want: "", | ||
}, | ||
{ | ||
name: "requests with internal error", | ||
args: args{ | ||
r: &request.Request{ | ||
Error: errors.New("oops, some internal error"), | ||
}, | ||
}, | ||
want: "internal", | ||
}, | ||
{ | ||
name: "requests with aws error", | ||
args: args{ | ||
r: &request.Request{ | ||
Error: awserr.New("NotFoundException", "", nil), | ||
}, | ||
}, | ||
want: "NotFoundException", | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
got := errorCodeForRequest(tt.args.r) | ||
assert.Equal(t, tt.want, got) | ||
}) | ||
} | ||
} | ||
|
||
func Test_operationForRequest(t *testing.T) { | ||
type args struct { | ||
r *request.Request | ||
} | ||
tests := []struct { | ||
name string | ||
args args | ||
want string | ||
}{ | ||
{ | ||
name: "requests without operation", | ||
args: args{ | ||
r: &request.Request{}, | ||
}, | ||
want: "?", | ||
}, | ||
{ | ||
name: "requests with operation", | ||
args: args{ | ||
r: &request.Request{ | ||
Operation: &request.Operation{ | ||
Name: "DescribeMesh", | ||
}, | ||
}, | ||
}, | ||
want: "DescribeMesh", | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
got := operationForRequest(tt.args.r) | ||
assert.Equal(t, tt.want, got) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package metrics | ||
|
||
import ( | ||
"github.com/prometheus/client_golang/prometheus" | ||
) | ||
|
||
const ( | ||
metricSubsystemAWS = "aws" | ||
|
||
metricAPICallsTotal = "api_calls_total" | ||
metricAPICallDurationSeconds = "api_call_duration_seconds" | ||
metricAPICallRetries = "api_call_retries" | ||
|
||
metricAPIRequestsTotal = "api_requests_total" | ||
metricAPIRequestDurationSeconds = "api_request_duration_seconds" | ||
) | ||
|
||
const ( | ||
labelService = "service" | ||
labelOperation = "operation" | ||
labelStatusCode = "status_code" | ||
labelErrorCode = "error_code" | ||
) | ||
|
||
type instruments struct { | ||
apiCallsTotal *prometheus.CounterVec | ||
apiCallDurationSeconds *prometheus.HistogramVec | ||
apiCallRetries *prometheus.HistogramVec | ||
apiRequestsTotal *prometheus.CounterVec | ||
apiRequestDurationSecond *prometheus.HistogramVec | ||
} | ||
|
||
// newInstruments allocates and register new metrics to registerer | ||
func newInstruments(registerer prometheus.Registerer) (*instruments, error) { | ||
apiCallsTotal := prometheus.NewCounterVec(prometheus.CounterOpts{ | ||
Subsystem: metricSubsystemAWS, | ||
Name: metricAPICallsTotal, | ||
Help: "Total number of SDK API calls from the customer's code to AWS services", | ||
}, []string{labelService, labelOperation, labelStatusCode, labelErrorCode}) | ||
apiCallDurationSeconds := prometheus.NewHistogramVec(prometheus.HistogramOpts{ | ||
Subsystem: metricSubsystemAWS, | ||
Name: metricAPICallDurationSeconds, | ||
Help: "Perceived latency from when your code makes an SDK call, includes retries", | ||
}, []string{labelService, labelOperation}) | ||
apiCallRetries := prometheus.NewHistogramVec(prometheus.HistogramOpts{ | ||
Subsystem: metricSubsystemAWS, | ||
Name: metricAPICallRetries, | ||
Help: "Number of times the SDK retried requests to AWS services for SDK API calls", | ||
Buckets: []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, | ||
}, []string{labelService, labelOperation}) | ||
|
||
apiRequestsTotal := prometheus.NewCounterVec(prometheus.CounterOpts{ | ||
Subsystem: metricSubsystemAWS, | ||
Name: metricAPIRequestsTotal, | ||
Help: "Total number of HTTP requests that the SDK made", | ||
}, []string{labelService, labelOperation, labelStatusCode, labelErrorCode}) | ||
apiRequestDurationSecond := prometheus.NewHistogramVec(prometheus.HistogramOpts{ | ||
Subsystem: metricSubsystemAWS, | ||
Name: metricAPIRequestDurationSeconds, | ||
Help: "Latency of an individual HTTP request to the service endpoint", | ||
}, []string{labelService, labelOperation}) | ||
|
||
if err := registerer.Register(apiCallsTotal); err != nil { | ||
return nil, err | ||
} | ||
if err := registerer.Register(apiCallDurationSeconds); err != nil { | ||
return nil, err | ||
} | ||
if err := registerer.Register(apiCallRetries); err != nil { | ||
return nil, err | ||
} | ||
if err := registerer.Register(apiRequestsTotal); err != nil { | ||
return nil, err | ||
} | ||
if err := registerer.Register(apiRequestDurationSecond); err != nil { | ||
return nil, err | ||
} | ||
return &instruments{ | ||
apiCallsTotal: apiCallsTotal, | ||
apiCallDurationSeconds: apiCallDurationSeconds, | ||
apiCallRetries: apiCallRetries, | ||
apiRequestsTotal: apiRequestsTotal, | ||
apiRequestDurationSecond: apiRequestDurationSecond, | ||
}, nil | ||
} |