Skip to content

Commit

Permalink
Implement RED metrics recording (#245)
Browse files Browse the repository at this point in the history
- Implement RED metrics recording, using swift-metrics, include AWS service and operation

Co-authored-by: Joe Smith <yasumoto7@gmail.com>
  • Loading branch information
adam-fowler and Yasumoto committed May 13, 2020
1 parent 9132a69 commit 4130949
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 6 deletions.
2 changes: 2 additions & 0 deletions Package.swift
Expand Up @@ -21,6 +21,7 @@ let package = Package(
.library(name: "AWSSDKSwiftCore", targets: ["AWSSDKSwiftCore"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-metrics.git", "1.0.0" ..< "3.0.0"),
.package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from:"2.13.1")),
.package(url: "https://github.com/apple/swift-nio-ssl.git", .upToNextMajor(from:"2.4.1")),
.package(url: "https://github.com/apple/swift-nio-transport-services.git", .upToNextMajor(from:"1.0.0")),
Expand All @@ -32,6 +33,7 @@ let package = Package(
dependencies: [
"AsyncHTTPClient",
"AWSSignerV4",
"Metrics",
"NIO",
"NIOHTTP1",
"NIOSSL",
Expand Down
36 changes: 31 additions & 5 deletions Sources/AWSSDKSwiftCore/AWSClient.swift
Expand Up @@ -13,14 +13,15 @@
//===----------------------------------------------------------------------===//

import AsyncHTTPClient
import Dispatch
import Metrics
import NIO
import NIOHTTP1
import NIOTransportServices
import class Foundation.ProcessInfo
import class Foundation.JSONSerialization
import class Foundation.JSONDecoder
import struct Foundation.Data
import struct Foundation.Date
import struct Foundation.URL
import struct Foundation.URLComponents
import struct Foundation.URLQueryItem
Expand Down Expand Up @@ -251,7 +252,7 @@ extension AWSClient {
/// Empty Future that completes when response is received
public func send<Input: AWSEncodableShape>(operation operationName: String, path: String, httpMethod: String, input: Input, on eventLoop: EventLoop? = nil) -> EventLoopFuture<Void> {
let eventLoop = eventLoop ?? eventLoopGroup.next()
return credentialProvider.getCredential(on: eventLoop).flatMapThrowing { credential in
let future: EventLoopFuture<Void> = credentialProvider.getCredential(on: eventLoop).flatMapThrowing { credential in
let signer = AWSSigner(credentials: credential, name: self.serviceConfig.signingName, region: self.serviceConfig.region.rawValue)
let awsRequest = try self.createAWSRequest(
operation: operationName,
Expand All @@ -264,6 +265,7 @@ extension AWSClient {
}.map { _ in
return
}
return recordMetrics(future, service: serviceConfig.service, operation: operationName)
}

/// send an empty request and return a future with an empty response
Expand All @@ -275,7 +277,7 @@ extension AWSClient {
/// Empty Future that completes when response is received
public func send(operation operationName: String, path: String, httpMethod: String, on eventLoop: EventLoop? = nil) -> EventLoopFuture<Void> {
let eventLoop = eventLoop ?? eventLoopGroup.next()
return credentialProvider.getCredential(on: eventLoop).flatMapThrowing { credential in
let future: EventLoopFuture<Void> = credentialProvider.getCredential(on: eventLoop).flatMapThrowing { credential in
let signer = AWSSigner(credentials: credential, name: self.serviceConfig.signingName, region: self.serviceConfig.region.rawValue)
let awsRequest = try self.createAWSRequest(
operation: operationName,
Expand All @@ -287,6 +289,7 @@ extension AWSClient {
}.map { _ in
return
}
return recordMetrics(future, service: serviceConfig.service, operation: operationName)
}

/// send an empty request and return a future with the output object generated from the response
Expand All @@ -298,7 +301,7 @@ extension AWSClient {
/// Future containing output object that completes when response is received
public func send<Output: AWSDecodableShape>(operation operationName: String, path: String, httpMethod: String, on eventLoop: EventLoop? = nil) -> EventLoopFuture<Output> {
let eventLoop = eventLoop ?? eventLoopGroup.next()
return credentialProvider.getCredential(on: eventLoop).flatMapThrowing { credential in
let future: EventLoopFuture<Output> = credentialProvider.getCredential(on: eventLoop).flatMapThrowing { credential in
let signer = AWSSigner(credentials: credential, name: self.serviceConfig.signingName, region: self.serviceConfig.region.rawValue)
let awsRequest = try self.createAWSRequest(
operation: operationName,
Expand All @@ -310,6 +313,7 @@ extension AWSClient {
}.flatMapThrowing { response in
return try self.validate(operation: operationName, response: response)
}
return recordMetrics(future, service: serviceConfig.service, operation: operationName)
}

/// send a request with an input object and return a future with the output object generated from the response
Expand All @@ -322,7 +326,7 @@ extension AWSClient {
/// Future containing output object that completes when response is received
public func send<Output: AWSDecodableShape, Input: AWSEncodableShape>(operation operationName: String, path: String, httpMethod: String, input: Input, on eventLoop: EventLoop? = nil) -> EventLoopFuture<Output> {
let eventLoop = eventLoop ?? eventLoopGroup.next()
return credentialProvider.getCredential(on: eventLoop).flatMapThrowing { credential in
let future: EventLoopFuture<Output> = credentialProvider.getCredential(on: eventLoop).flatMapThrowing { credential in
let signer = AWSSigner(credentials: credential, name: self.serviceConfig.signingName, region: self.serviceConfig.region.rawValue)
let awsRequest = try self.createAWSRequest(
operation: operationName,
Expand All @@ -335,6 +339,7 @@ extension AWSClient {
}.flatMapThrowing { response in
return try self.validate(operation: operationName, response: response)
}
return recordMetrics(future, service: serviceConfig.service, operation: operationName)
}

/// generate a signed URL
Expand Down Expand Up @@ -788,6 +793,27 @@ extension AWSClient.ClientError: CustomStringConvertible {
}
}

extension AWSClient {
func recordMetrics<Output>(_ future: EventLoopFuture<Output>, service: String, operation: String) -> EventLoopFuture<Output> {
let dimensions: [(String, String)] = [("service", service), ("operation", operation)]
let startTime = DispatchTime.now().uptimeNanoseconds

Counter(label: "aws_requests_total", dimensions: dimensions).increment()

return future.map { response in
Metrics.Timer(
label: "aws_request_duration",
dimensions: dimensions,
preferredDisplayUnit: .seconds
).recordNanoseconds(DispatchTime.now().uptimeNanoseconds - startTime)
return response
}.flatMapErrorThrowing { error in
Counter(label: "aws_request_errors", dimensions: dimensions).increment()
throw error
}
}
}

protocol QueryEncodableArray {
var queryEncoded: [String] { get }
}
Expand Down
6 changes: 5 additions & 1 deletion Sources/AWSSDKSwiftCore/ServiceConfig.swift
Expand Up @@ -16,10 +16,13 @@ import class Foundation.ProcessInfo

/// Configuration class defining an AWS service
public struct ServiceConfig {

/// Region where service is running
public let region: Region
/// The destination service of the request. Added as a header value, along with the operation name
public let amzTarget: String?
/// Name of service
public let service: String
/// Name used to sign requests
public let signingName: String
/// Protocol used by service json/xml/query
Expand All @@ -32,7 +35,7 @@ public struct ServiceConfig {
public let possibleErrorTypes: [AWSErrorType.Type]
/// Middleware code specific to the service used to edit requests before they sent and responses before they are decoded
public let middlewares: [AWSServiceMiddleware]

/// Create a ServiceConfig object
///
/// - Parameters:
Expand Down Expand Up @@ -75,6 +78,7 @@ public struct ServiceConfig {
self.region = .useast1
}

self.service = service
self.apiVersion = apiVersion
self.signingName = signingName ?? service
self.amzTarget = amzTarget
Expand Down

0 comments on commit 4130949

Please sign in to comment.