Skip to content

Commit

Permalink
feat: UserAgent v2.1 (#1690)
Browse files Browse the repository at this point in the history
* Add BusinessMetrics struct, Context extension for businessMetrics computed property, utility function for grabbing flags from config / context and saving them into context.businessMetrics.

* Move AWSUserAgentMetadata construction from within generated UserAgentMiddlewrae initialization code, to the runtime UserAgentMiddleware code. This delays user agent struct initialization to right before the request is sent, and is done because we need access to the last-minute values in the context right before the request is sent, in order to grab all features used in request flow and put them in the business metrics section of the user agent.

* Replace config metadata and feature metadata with business metrics. Refactor fromConfig() initializer wrapper to fromConfigAndContext(), now that business metrics needs values saved in context as well.

* Remove config metadata and feature metadata code and tests.

* Add UserAgetnValuesFromConfig class to act as container for subset of relevant values from config. Add unit tests for BusinessMetrics.

* Update codegen tests with new UserAgentMiddleware init params

* Refine comment for progress tracking in future

---------

Co-authored-by: Sichan Yoo <[email protected]>
  • Loading branch information
sichanyoo and Sichan Yoo authored Aug 26, 2024
1 parent ea5922d commit ec7cee4
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 139 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,34 @@ public struct UserAgentMiddleware<OperationStackInput, OperationStackOutput> {
private let X_AMZ_USER_AGENT: String = "x-amz-user-agent"
private let USER_AGENT: String = "User-Agent"

let metadata: AWSUserAgentMetadata

public init(metadata: AWSUserAgentMetadata) {
self.metadata = metadata
}

private func addHeader(builder: HTTPRequestBuilder) {
builder.withHeader(name: USER_AGENT, value: metadata.userAgent)
let serviceID: String
let version: String
let config: DefaultClientConfiguration & AWSDefaultClientConfiguration

public init(
serviceID: String,
version: String,
config: DefaultClientConfiguration & AWSDefaultClientConfiguration
) {
self.serviceID = serviceID
self.version = version
self.config = config
}
}

extension UserAgentMiddleware: HttpInterceptor {
public typealias InputType = OperationStackInput
public typealias OutputType = OperationStackOutput

public func modifyBeforeRetryLoop(context: some MutableRequest<Self.InputType, HTTPRequest>) async throws {
public func modifyBeforeTransmit(context: some MutableRequest<Self.InputType, HTTPRequest>) async throws {
let awsUserAgentString = AWSUserAgentMetadata.fromConfigAndContext(
serviceID: serviceID,
version: version,
config: UserAgentValuesFromConfig(config: config),
context: context.getAttributes()
).userAgent
let builder = context.getRequest().toBuilder()
addHeader(builder: builder)
builder.withHeader(name: USER_AGENT, value: awsUserAgentString)
context.updateRequest(updated: builder.build())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import ClientRuntime
import class Smithy.Context

public struct AWSUserAgentMetadata {
let sdkMetadata: SDKMetadata
Expand All @@ -15,9 +16,8 @@ public struct AWSUserAgentMetadata {
let osMetadata: OSMetadata
let languageMetadata: LanguageMetadata
let executionEnvMetadata: ExecutionEnvMetadata?
let configMetadata: [ConfigMetadata]
let businessMetrics: BusinessMetrics?
let appIDMetadata: AppIDMetadata?
let featureMetadata: [FeatureMetadata]
let frameworkMetadata: [FrameworkMetadata]

/// ABNF for the user agent:
Expand All @@ -29,9 +29,8 @@ public struct AWSUserAgentMetadata {
/// language-metadata RWS
/// [env-metadata RWS]
/// ; ordering is not strictly required in the following section
/// *(config-metadata RWS)
/// [business-metrics]
/// [appId]
/// *(feat-metadata RWS)
/// *(framework-metadata RWS)
var userAgent: String {
return [
Expand All @@ -42,9 +41,8 @@ public struct AWSUserAgentMetadata {
[osMetadata.description],
[languageMetadata.description],
[executionEnvMetadata?.description],
configMetadata.map(\.description) as [String?],
[businessMetrics?.description],
[appIDMetadata?.description],
featureMetadata.map(\.description) as [String?],
frameworkMetadata.map(\.description) as [String?]
].flatMap { $0 }.compactMap { $0 }.joined(separator: " ")
}
Expand All @@ -56,9 +54,8 @@ public struct AWSUserAgentMetadata {
osMetadata: OSMetadata,
languageMetadata: LanguageMetadata,
executionEnvMetadata: ExecutionEnvMetadata? = nil,
configMetadata: [ConfigMetadata] = [],
businessMetrics: BusinessMetrics? = nil,
appIDMetadata: AppIDMetadata? = nil,
featureMetadata: [FeatureMetadata] = [],
frameworkMetadata: [FrameworkMetadata] = []
) {
self.sdkMetadata = sdkMetadata
Expand All @@ -67,16 +64,16 @@ public struct AWSUserAgentMetadata {
self.osMetadata = osMetadata
self.languageMetadata = languageMetadata
self.executionEnvMetadata = executionEnvMetadata
self.configMetadata = configMetadata
self.businessMetrics = businessMetrics
self.appIDMetadata = appIDMetadata
self.featureMetadata = featureMetadata
self.frameworkMetadata = frameworkMetadata
}

public static func fromConfig(
public static func fromConfigAndContext(
serviceID: String,
version: String,
config: DefaultClientConfiguration & AWSDefaultClientConfiguration
config: UserAgentValuesFromConfig,
context: Context
) -> AWSUserAgentMetadata {
let apiMetadata = APIMetadata(serviceID: serviceID, version: version)
let sdkMetadata = SDKMetadata(version: apiMetadata.version)
Expand All @@ -85,7 +82,7 @@ public struct AWSUserAgentMetadata {
let osVersion = PlatformOperationSystemVersion.operatingSystemVersion()
let osMetadata = OSMetadata(family: currentOS, version: osVersion)
let languageMetadata = LanguageMetadata(version: swiftVersion)
let configMetadata = [ConfigMetadata(type: .retry(config.awsRetryMode))]
let businessMetrics = BusinessMetrics(config: config, context: context)
let appIDMetadata = AppIDMetadata(name: config.appID)
let frameworkMetadata = [FrameworkMetadata]()
return AWSUserAgentMetadata(
Expand All @@ -95,10 +92,27 @@ public struct AWSUserAgentMetadata {
osMetadata: osMetadata,
languageMetadata: languageMetadata,
executionEnvMetadata: ExecutionEnvMetadata.detectExecEnv(),
configMetadata: configMetadata,
businessMetrics: businessMetrics,
appIDMetadata: appIDMetadata,
featureMetadata: [], // Feature metadata will be supplied when features are implemented
frameworkMetadata: frameworkMetadata
)
}
}

public class UserAgentValuesFromConfig {
var appID: String?
var endpoint: String?
var awsRetryMode: AWSRetryMode

public init(appID: String?, endpoint: String?, awsRetryMode: AWSRetryMode) {
self.endpoint = endpoint
self.awsRetryMode = awsRetryMode
self.appID = appID
}

public init(config: DefaultClientConfiguration & AWSDefaultClientConfiguration) {
self.appID = config.appID
self.endpoint = config.endpoint
self.awsRetryMode = config.awsRetryMode
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import ClientRuntime
import class Smithy.Context
import struct Smithy.AttributeKey

struct BusinessMetrics {
// Mapping of human readable feature ID to the corresponding metric value
let features: [String: String]

init(
config: UserAgentValuesFromConfig,
context: Context
) {
setFlagsIntoContext(config: config, context: context)
self.features = context.businessMetrics
}
}

extension BusinessMetrics: CustomStringConvertible {
var description: String {
var commaSeparatedMetricValues = features.values.sorted().joined(separator: ",")
// Cut last metric value from string until the
// comma-separated list of metric values are at or below 1024 bytes in size
if commaSeparatedMetricValues.lengthOfBytes(using: .ascii) > 1024 {
while commaSeparatedMetricValues.lengthOfBytes(using: .ascii) > 1024 {
commaSeparatedMetricValues = commaSeparatedMetricValues.substringBeforeLast(",")
}
}
return "m/\(commaSeparatedMetricValues)"
}
}

private extension String {
func substringBeforeLast(_ separator: String) -> String {
if let range = self.range(of: separator, options: .backwards) {
return String(self[..<range.lowerBound])
} else {
return self
}
}
}

public extension Context {
var businessMetrics: Dictionary<String, String> {
get { attributes.get(key: businessMetricsKey) ?? [:] }
set(newPair) {
var combined = businessMetrics
combined.merge(newPair) { (current, new) in new }

Check warning on line 54 in Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/UserAgent/BusinessMetrics.swift

View workflow job for this annotation

GitHub Actions / swiftlint

Unused parameter in a closure should be replaced with _ (unused_closure_parameter)
attributes.set(key: businessMetricsKey, value: combined)
}
}
}

public let businessMetricsKey = AttributeKey<Dictionary<String, String>>(name: "BusinessMetrics")

/* List of readable "feature ID" to "metric value"; last updated on 08/19/2024
[Feature ID] [Metric Value] [Flag Supported]
"RESOURCE_MODEL" : "A" :
"WAITER" : "B" :
"PAGINATOR" : "C" :
"RETRY_MODE_LEGACY" : "D" : Y
"RETRY_MODE_STANDARD" : "E" : Y
"RETRY_MODE_ADAPTIVE" : "F" : Y
"S3_TRANSFER" : "G" :
"S3_CRYPTO_V1N" : "H" :
"S3_CRYPTO_V2" : "I" :
"S3_EXPRESS_BUCKET" : "J" :
"S3_ACCESS_GRANTS" : "K" :
"GZIP_REQUEST_COMPRESSION" : "L" :
"PROTOCOL_RPC_V2_CBOR" : "M" :
"ENDPOINT_OVERRIDE" : "N" : Y
"ACCOUNT_ID_ENDPOINT" : "O" :
"ACCOUNT_ID_MODE_PREFERRED" : "P" :
"ACCOUNT_ID_MODE_DISABLED" : "Q" :
"ACCOUNT_ID_MODE_REQUIRED" : "R" :
"SIGV4A_SIGNING" : "S" : Y
"RESOLVED_ACCOUNT_ID" : "T" :
*/
fileprivate func setFlagsIntoContext(

Check warning on line 85 in Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/UserAgent/BusinessMetrics.swift

View workflow job for this annotation

GitHub Actions / swiftlint

Prefer `private` over `fileprivate` declarations (private_over_fileprivate)
config: UserAgentValuesFromConfig,
context: Context
) {
// Handle D, E, F
switch config.awsRetryMode {
case .legacy:
context.businessMetrics = ["RETRY_MODE_LEGACY": "D"]
case .standard:
context.businessMetrics = ["RETRY_MODE_STANDARD": "E"]
case .adaptive:
context.businessMetrics = ["RETRY_MODE_ADAPTIVE": "F"]
}
// Handle N
if let endpoint = config.endpoint, !endpoint.isEmpty {
context.businessMetrics = ["ENDPOINT_OVERRIDE": "N"]
}
// Handle S
if context.selectedAuthScheme?.schemeID == "aws.auth#sigv4a" {
context.businessMetrics = ["SIGV4A_SIGNING": "S"]
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import XCTest
import ClientRuntime
@testable import AWSClientRuntime
import SmithyRetriesAPI
import SmithyHTTPAuthAPI
import SmithyIdentity
import SmithyRetriesAPI
import Smithy

class BusinessMetricsTests: XCTestCase {
var context: Context!

override func setUp() async throws {
context = Context(attributes: Attributes())
}

func test_business_metrics_section_truncation() {
context.businessMetrics = ["SHORT_FILLER": "A"]
let longMetricValue = String(repeating: "F", count: 1025)
context.businessMetrics = ["LONG_FILLER": longMetricValue]
let userAgent = AWSUserAgentMetadata.fromConfigAndContext(
serviceID: "test",
version: "1.0",
config: UserAgentValuesFromConfig(appID: nil, endpoint: nil, awsRetryMode: .standard),
context: context
)
// Assert values in context match with values assigned to user agent
XCTAssertEqual(userAgent.businessMetrics?.features, context.businessMetrics)
// Assert string gets truncated successfully
let expectedTruncatedString = "m/A,E"
XCTAssertEqual(userAgent.businessMetrics?.description, expectedTruncatedString)
}

func test_multiple_flags_in_context() {
context.businessMetrics = ["FIRST": "A"]
context.businessMetrics = ["SECOND": "B"]
context.setSelectedAuthScheme(SelectedAuthScheme( // S
schemeID: "aws.auth#sigv4a",
identity: nil,
signingProperties: nil,
signer: nil
))
let userAgent = AWSUserAgentMetadata.fromConfigAndContext(
serviceID: "test",
version: "1.0",
config: UserAgentValuesFromConfig(appID: nil, endpoint: "test-endpoint", awsRetryMode: .adaptive),
context: context
)
// F comes from retry mode being adaptive & N comes from endpoint override
let expectedString = "m/A,B,F,N,S"
XCTAssertEqual(userAgent.businessMetrics?.description, expectedString)
}
}
Loading

0 comments on commit ec7cee4

Please sign in to comment.