55
66package 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
77140public 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
87151public 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
97162public 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
113179public 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
133201public 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
0 commit comments