Skip to content

Commit 3dcddcf

Browse files
authored
feat: extend user agent metadata with framework, feature, and config (#372)
1 parent 96fdd7f commit 3dcddcf

File tree

11 files changed

+410
-72
lines changed

11 files changed

+410
-72
lines changed

aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/imds/ImdsClient.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public class ImdsClient private constructor(builder: Builder) : InstanceMetadata
9797
resolver = ImdsEndpointResolver(platformProvider, endpointConfiguration)
9898
},
9999
UserAgent.create {
100-
metadata = AwsUserAgentMetadata.fromEnvironment(ApiMetadata(SERVICE, "unknown"))
100+
staticMetadata = AwsUserAgentMetadata.fromEnvironment(ApiMetadata(SERVICE, "unknown"))
101101
},
102102
TokenMiddleware.create {
103103
httpClient = this@ImdsClient.httpClient

aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/AwsUserAgentMetadata.kt

Lines changed: 133 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,20 @@
55

66
package aws.sdk.kotlin.runtime.http
77

8-
import aws.smithy.kotlin.runtime.util.OsFamily
9-
import aws.smithy.kotlin.runtime.util.Platform
8+
import aws.sdk.kotlin.runtime.InternalSdkApi
9+
import aws.sdk.kotlin.runtime.http.operation.ConfigMetadata
10+
import aws.sdk.kotlin.runtime.http.operation.CustomUserAgentMetadata
11+
import aws.sdk.kotlin.runtime.http.operation.FeatureMetadata
12+
import aws.smithy.kotlin.runtime.util.*
13+
import kotlin.jvm.JvmInline
1014

11-
private const val AWS_EXECUTION_ENV: String = "AWS_EXECUTION_ENV"
15+
internal const val AWS_EXECUTION_ENV = "AWS_EXECUTION_ENV"
16+
internal const val AWS_APP_ID_ENV = "AWS_SDK_UA_APP_ID"
17+
18+
// non-standard environment variables/properties
19+
internal const val AWS_APP_ID_PROP = "aws.userAgentAppId"
20+
internal const val FRAMEWORK_METADATA_ENV = "AWS_FRAMEWORK_METADATA"
21+
internal const val FRAMEWORK_METADATA_PROP = "aws.frameworkMetadata"
1222

1323
/**
1424
* Metadata used to populate the `User-Agent` and `x-amz-user-agent` headers
@@ -18,62 +28,115 @@ public data class AwsUserAgentMetadata(
1828
val apiMetadata: ApiMetadata,
1929
val osMetadata: OsMetadata,
2030
val languageMetadata: LanguageMetadata,
21-
val execEnvMetadata: ExecutionEnvMetadata? = null
31+
val execEnvMetadata: ExecutionEnvMetadata? = null,
32+
val frameworkMetadata: FrameworkMetadata? = null,
33+
val appId: String? = null,
34+
val customMetadata: CustomUserAgentMetadata? = null
2235
) {
2336

2437
public companion object {
2538
/**
2639
* Load user agent configuration data from the current environment
2740
*/
28-
public fun fromEnvironment(apiMeta: ApiMetadata): AwsUserAgentMetadata {
29-
val sdkMeta = SdkMetadata("kotlin", apiMeta.version)
30-
val osInfo = Platform.osInfo()
31-
val osMetadata = OsMetadata(osInfo.family, osInfo.version)
32-
val langMeta = platformLanguageMetadata()
33-
return AwsUserAgentMetadata(sdkMeta, apiMeta, osMetadata, langMeta, detectExecEnv())
34-
}
41+
public fun fromEnvironment(
42+
apiMeta: ApiMetadata,
43+
): AwsUserAgentMetadata = loadAwsUserAgentMetadataFromEnvironment(Platform, apiMeta)
3544
}
3645

3746
/**
3847
* New-style user agent header value for `x-amz-user-agent`
3948
*/
40-
val xAmzUserAgent: String = buildString {
41-
/*
42-
ABNF for the user agent:
43-
ua-string =
44-
sdk-metadata RWS
45-
[api-metadata RWS]
46-
os-metadata RWS
47-
language-metadata RWS
48-
[env-metadata RWS]
49-
*(feat-metadata RWS)
50-
*(config-metadata RWS)
51-
*(framework-metadata RWS)
52-
[appId]
53-
*/
54-
append("$sdkMetadata ")
55-
append("$apiMetadata ")
56-
append("$osMetadata ")
57-
append("$languageMetadata ")
58-
execEnvMetadata?.let { append("$it") }
59-
60-
// TODO - feature metadata
61-
// TODO - config metadata
62-
// TODO - framework metadata (e.g. Amplify would be a good candidate for this data)
63-
// TODO - appId
64-
}.trimEnd()
49+
val xAmzUserAgent: String
50+
get() {
51+
/*
52+
ABNF for the user agent:
53+
ua-string =
54+
[internal-metadata RWS]
55+
sdk-metadata RWS
56+
[api-metadata RWS]
57+
os-metadata RWS
58+
language-metadata RWS
59+
[env-metadata RWS]
60+
*(feat-metadata RWS)
61+
*(config-metadata RWS)
62+
*(framework-metadata RWS)
63+
[appId]
64+
*/
65+
val ua = mutableListOf<String>()
66+
67+
val isInternal = customMetadata?.extras?.remove("internal")
68+
if (isInternal != null) {
69+
ua.add("md/internal")
70+
}
71+
72+
ua.add("$sdkMetadata")
73+
ua.add("$apiMetadata")
74+
ua.add("$osMetadata")
75+
ua.add("$languageMetadata")
76+
execEnvMetadata?.let { ua.add("$it") }
77+
78+
val features = customMetadata?.typedExtras?.filterIsInstance<FeatureMetadata>()
79+
features?.forEach { ua.add("$it") }
80+
81+
val config = customMetadata?.typedExtras?.filterIsInstance<ConfigMetadata>()
82+
config?.forEach { ua.add("$it") }
83+
84+
frameworkMetadata?.let { ua.add("$it") }
85+
appId?.let { ua.add("app/$it") }
86+
87+
customMetadata?.extras?.let {
88+
val wrapper = AdditionalMetadata(it)
89+
ua.add("$wrapper")
90+
}
91+
92+
return ua.joinToString(separator = " ")
93+
}
6594

6695
/**
6796
* Legacy user agent header value for `UserAgent`
6897
*/
69-
val userAgent: String = "$sdkMetadata"
98+
val userAgent: String
99+
get() = "$sdkMetadata"
100+
}
101+
102+
internal fun loadAwsUserAgentMetadataFromEnvironment(platform: PlatformProvider, apiMeta: ApiMetadata): AwsUserAgentMetadata {
103+
val sdkMeta = SdkMetadata("kotlin", apiMeta.version)
104+
val osInfo = platform.osInfo()
105+
val osMetadata = OsMetadata(osInfo.family, osInfo.version)
106+
val langMeta = platformLanguageMetadata()
107+
val appId = platform.getProperty(AWS_APP_ID_PROP) ?: platform.getenv(AWS_APP_ID_ENV)
108+
109+
val frameworkMetadata = FrameworkMetadata.fromEnvironment(platform)
110+
return AwsUserAgentMetadata(
111+
sdkMeta,
112+
apiMeta,
113+
osMetadata,
114+
langMeta,
115+
detectExecEnv(platform),
116+
frameworkMetadata = frameworkMetadata,
117+
appId = appId,
118+
)
119+
}
120+
121+
/**
122+
* Wrapper around additional metadata kv-pairs that handles formatting
123+
*/
124+
@JvmInline
125+
internal value class AdditionalMetadata(private val extras: Map<String, String>) {
126+
override fun toString(): String = extras.entries.joinToString(separator = " ") { entry ->
127+
when (entry.value.lowercase()) {
128+
"true" -> "md/${entry.key}"
129+
else -> "md/${entry.key.encodeUaToken()}/${entry.value.encodeUaToken()}"
130+
}
131+
}
70132
}
71133

72134
/**
73135
* SDK metadata
74136
* @property name The SDK (language) name
75137
* @property version The SDK version
76138
*/
139+
@InternalSdkApi
77140
public data class SdkMetadata(val name: String, val version: String) {
78141
override fun toString(): String = "aws-sdk-$name/$version"
79142
}
@@ -84,6 +147,7 @@ public data class SdkMetadata(val name: String, val version: String) {
84147
* @property version The version of the client (note this may be the same as [SdkMetadata.version] for SDK's
85148
* that don't independently version clients from one another.
86149
*/
150+
@InternalSdkApi
87151
public data class ApiMetadata(val serviceId: String, val version: String) {
88152
override fun toString(): String {
89153
val formattedServiceId = serviceId.replace(" ", "-").lowercase()
@@ -94,6 +158,7 @@ public data class ApiMetadata(val serviceId: String, val version: String) {
94158
/**
95159
* Operating system metadata
96160
*/
161+
@InternalSdkApi
97162
public data class OsMetadata(val family: OsFamily, val version: String? = null) {
98163
override fun toString(): String {
99164
// os-family = windows / linux / macos / android / ios / other
@@ -110,15 +175,17 @@ public data class OsMetadata(val family: OsFamily, val version: String? = null)
110175
* @property version The kotlin version in use
111176
* @property extras Additional key value pairs appropriate for the language/runtime (e.g.`jvmVm=OpenJdk`, etc)
112177
*/
178+
@InternalSdkApi
113179
public data class LanguageMetadata(
114180
val version: String = KotlinVersion.CURRENT.toString(),
115181
// additional metadata key/value pairs
116182
val extras: Map<String, String> = emptyMap()
117183
) {
118184
override fun toString(): String = buildString {
119185
append("lang/kotlin/$version")
120-
extras.entries.forEach { (key, value) ->
121-
append(" md/$key/${value.encodeUaToken()}")
186+
if (extras.isNotEmpty()) {
187+
val wrapper = AdditionalMetadata(extras)
188+
append(" $wrapper")
122189
}
123190
}
124191
}
@@ -130,12 +197,37 @@ internal expect fun platformLanguageMetadata(): LanguageMetadata
130197
* Execution environment metadata
131198
* @property name The execution environment name (e.g. "lambda")
132199
*/
200+
@InternalSdkApi
133201
public data class ExecutionEnvMetadata(val name: String) {
134202
override fun toString(): String = "exec-env/${name.encodeUaToken()}"
135203
}
136204

137-
private fun detectExecEnv(): ExecutionEnvMetadata? =
138-
Platform.getenv(AWS_EXECUTION_ENV)?.let {
205+
/**
206+
* Framework metadata (e.g. name = "amplify" version = "1.2.3")
207+
* @property name The framework name
208+
* @property version The framework version
209+
*/
210+
@InternalSdkApi
211+
public data class FrameworkMetadata(
212+
val name: String,
213+
val version: String,
214+
) {
215+
internal companion object {
216+
internal fun fromEnvironment(provider: PlatformEnvironProvider): FrameworkMetadata? {
217+
val kvPair = provider.getProperty(FRAMEWORK_METADATA_PROP) ?: provider.getenv(FRAMEWORK_METADATA_ENV)
218+
return kvPair?.let {
219+
val kv = kvPair.split(':', limit = 2)
220+
check(kv.size == 2) { "Invalid value for FRAMEWORK_METADATA: $kvPair; must be of the form `name:version`" }
221+
FrameworkMetadata(kv[0], kv[1])
222+
}
223+
}
224+
}
225+
226+
override fun toString(): String = "lib/$name/$version"
227+
}
228+
229+
private fun detectExecEnv(platform: PlatformEnvironProvider): ExecutionEnvMetadata? =
230+
platform.getenv(AWS_EXECUTION_ENV)?.let {
139231
ExecutionEnvMetadata(it)
140232
}
141233

aws-runtime/aws-http/common/src/aws/sdk/kotlin/runtime/http/middleware/UserAgent.kt

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ package aws.sdk.kotlin.runtime.http.middleware
77

88
import aws.sdk.kotlin.runtime.InternalSdkApi
99
import aws.sdk.kotlin.runtime.http.AwsUserAgentMetadata
10+
import aws.sdk.kotlin.runtime.http.operation.CustomUserAgentMetadata
1011
import aws.smithy.kotlin.runtime.http.Feature
1112
import aws.smithy.kotlin.runtime.http.FeatureKey
1213
import aws.smithy.kotlin.runtime.http.HttpClientFeatureFactory
1314
import aws.smithy.kotlin.runtime.http.operation.SdkHttpOperation
15+
import aws.smithy.kotlin.runtime.io.middleware.Phase
1416

1517
internal const val X_AMZ_USER_AGENT: String = "x-amz-user-agent"
1618
internal const val USER_AGENT: String = "User-Agent"
@@ -19,10 +21,15 @@ internal const val USER_AGENT: String = "User-Agent"
1921
* Http middleware that sets the User-Agent and x-amz-user-agent headers
2022
*/
2123
@InternalSdkApi
22-
public class UserAgent(private val awsUserAgentMetadata: AwsUserAgentMetadata) : Feature {
24+
public class UserAgent(
25+
private val staticMetadata: AwsUserAgentMetadata
26+
) : Feature {
2327

2428
public class Config {
25-
public var metadata: AwsUserAgentMetadata? = null
29+
/**
30+
* Metadata that doesn't change per/request (e.g. sdk and environment related metadata)
31+
*/
32+
public var staticMetadata: AwsUserAgentMetadata? = null
2633
}
2734

2835
public companion object Feature :
@@ -31,19 +38,26 @@ public class UserAgent(private val awsUserAgentMetadata: AwsUserAgentMetadata) :
3138

3239
override fun create(block: Config.() -> Unit): UserAgent {
3340
val config = Config().apply(block)
34-
val metadata = requireNotNull(config.metadata) { "metadata is required" }
41+
val metadata = requireNotNull(config.staticMetadata) { "staticMetadata is required" }
3542
return UserAgent(metadata)
3643
}
3744
}
3845

3946
override fun <I, O> install(operation: SdkHttpOperation<I, O>) {
40-
operation.execution.mutate.intercept { req, next ->
47+
operation.execution.mutate.intercept(Phase.Order.After) { req, next ->
48+
49+
// pull dynamic values out of the context
50+
val customMetadata = req.context.getOrNull(CustomUserAgentMetadata.ContextKey)
51+
52+
// resolve the metadata for the request which is a combination of the static and per/operation metadata
53+
val requestMetadata = staticMetadata.copy(customMetadata = customMetadata)
54+
4155
// NOTE: Due to legacy issues with processing the user agent, the original content for
4256
// x-amz-user-agent and User-Agent is swapped here. See top note in the
4357
// sdk-user-agent-header SEP and https://github.com/awslabs/smithy-kotlin/issues/373
4458
// for further details.
45-
req.subject.headers[USER_AGENT] = awsUserAgentMetadata.xAmzUserAgent
46-
req.subject.headers[X_AMZ_USER_AGENT] = awsUserAgentMetadata.userAgent
59+
req.subject.headers[USER_AGENT] = requestMetadata.xAmzUserAgent
60+
req.subject.headers[X_AMZ_USER_AGENT] = requestMetadata.userAgent
4761
next.call(req)
4862
}
4963
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
6+
package aws.sdk.kotlin.runtime.http.operation
7+
8+
import aws.sdk.kotlin.runtime.InternalSdkApi
9+
import aws.smithy.kotlin.runtime.client.ExecutionContext
10+
import aws.smithy.kotlin.runtime.util.AttributeKey
11+
12+
/**
13+
* Operation context element for adding additional metadata to the `User-Agent` header string.
14+
*
15+
* Access via extension property [ExecutionContext.customUserAgentMetadata]
16+
*/
17+
public class CustomUserAgentMetadata {
18+
internal val extras: MutableMap<String, String> = mutableMapOf()
19+
internal val typedExtras: MutableList<TypedUserAgentMetadata> = mutableListOf()
20+
21+
internal companion object {
22+
public val ContextKey: AttributeKey<CustomUserAgentMetadata> = AttributeKey("CustomUserAgentMetadata")
23+
}
24+
25+
/**
26+
* Add additional key-value pairs of metadata to the request. These will show up as `md/key/value` when sent.
27+
*/
28+
public fun add(key: String, value: String) {
29+
extras[key] = value
30+
}
31+
32+
@InternalSdkApi
33+
public fun add(metadata: TypedUserAgentMetadata) {
34+
typedExtras.add(metadata)
35+
}
36+
}
37+
38+
/**
39+
* Get the [CustomUserAgentMetadata] instance to append additional context to the generated `User-Agent` string.
40+
*/
41+
public val ExecutionContext.customUserAgentMetadata: CustomUserAgentMetadata
42+
get() = computeIfAbsent(CustomUserAgentMetadata.ContextKey) { CustomUserAgentMetadata() }
43+
44+
/**
45+
* Marker interface for addition of classified metadata types (e.g. [ConfigMetadata] or [FeatureMetadata]).
46+
*/
47+
@InternalSdkApi
48+
public sealed interface TypedUserAgentMetadata
49+
50+
/**
51+
* Feature metadata
52+
* @property name The name of the feature
53+
* @property version Optional version of the feature (if independently versioned)
54+
*/
55+
@InternalSdkApi
56+
public data class FeatureMetadata(val name: String, val version: String? = null) : TypedUserAgentMetadata {
57+
override fun toString(): String = if (version != null) "ft/$name/$version" else "ft/$name"
58+
}
59+
60+
/**
61+
* Configuration metadata
62+
* @property name The configuration property name (e.g. "retry-mode")
63+
* @property value The property value (e.g. "standard")
64+
*/
65+
@InternalSdkApi
66+
public data class ConfigMetadata(val name: String, val value: String) : TypedUserAgentMetadata {
67+
override fun toString(): String = when (value.lowercase()) {
68+
"true" -> "cfg/$name"
69+
else -> "cfg/$name/$value"
70+
}
71+
}

0 commit comments

Comments
 (0)