Skip to content

Commit

Permalink
feat: business metrics (#1096)
Browse files Browse the repository at this point in the history
  • Loading branch information
0marperez authored Jun 17, 2024
1 parent 5e0301c commit 14c6038
Show file tree
Hide file tree
Showing 11 changed files with 308 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ object RuntimeTypes {
val ClientException = symbol("ClientException")
val SdkDsl = symbol("SdkDsl")

object BusinessMetrics : RuntimeTypePackage(KotlinDependency.CORE, "businessmetrics") {
val AccountIdBasedEndpointAccountId = symbol("AccountIdBasedEndpointAccountId")
val ServiceEndpointOverride = symbol("ServiceEndpointOverride")
val emitBusinessMetric = symbol("emitBusinessMetric")
val SmithyBusinessMetric = symbol("SmithyBusinessMetric")
}

object Collections : RuntimeTypePackage(KotlinDependency.CORE, "collections") {
val Attributes = symbol("Attributes")
val attributesOf = symbol("attributesOf")
Expand All @@ -110,6 +117,8 @@ object RuntimeTypes {
val putIfAbsent = symbol("putIfAbsent")
val putIfAbsentNotNull = symbol("putIfAbsentNotNull")
val ReadThroughCache = symbol("ReadThroughCache")
val toMutableAttributes = symbol("toMutableAttributes")
val emptyAttributes = symbol("emptyAttributes")
}

object Content : RuntimeTypePackage(KotlinDependency.CORE, "content") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ fun interface ExpressionRenderer {
fun renderExpression(expr: Expression)
}

/**
* Will be toggled to true if it is determined an endpoint is account ID based then to false again
*/
private var hasAccountIdBasedEndpoint = false

/**
* Will be toggled to true if determined an endpoint comes from a service endpoint override then to false again
*/
private var hasServiceEndpointOverride = false

/**
* Will be toggled to true when rendering an endpoint URL then to false again
*/
private var renderingEndpointUrl = false

/**
* Renders the default endpoint provider based on the provided rule set.
*/
Expand Down Expand Up @@ -170,9 +185,13 @@ class DefaultEndpointProviderGenerator(
withConditions(rule.conditions) {
writer.withBlock("return #T(", ")", RuntimeTypes.SmithyClient.Endpoints.Endpoint) {
writeInline("#T.parse(", RuntimeTypes.Core.Net.Url.Url)
renderingEndpointUrl = true
renderExpression(rule.endpoint.url)
renderingEndpointUrl = false
write("),")

val needAdditionalEndpointProperties = hasAccountIdBasedEndpoint || hasServiceEndpointOverride

if (rule.endpoint.headers.isNotEmpty()) {
withBlock("headers = #T {", "},", RuntimeTypes.Http.Headers) {
rule.endpoint.headers.entries.forEach { (k, v) ->
Expand All @@ -185,7 +204,7 @@ class DefaultEndpointProviderGenerator(
}
}

if (rule.endpoint.properties.isNotEmpty()) {
if (rule.endpoint.properties.isNotEmpty() || needAdditionalEndpointProperties) {
withBlock("attributes = #T {", "},", RuntimeTypes.Core.Collections.attributesOf) {
rule.endpoint.properties.entries.forEach { (k, v) ->
val kStr = k.toString()
Expand All @@ -204,6 +223,15 @@ class DefaultEndpointProviderGenerator(
renderExpression(v)
ensureNewline()
}

if (hasAccountIdBasedEndpoint) {
writer.write("#T to params.accountId", RuntimeTypes.Core.BusinessMetrics.AccountIdBasedEndpointAccountId)
hasAccountIdBasedEndpoint = false
}
if (hasServiceEndpointOverride) {
writer.write("#T to true", RuntimeTypes.Core.BusinessMetrics.ServiceEndpointOverride)
hasServiceEndpointOverride = false
}
}
}
}
Expand Down Expand Up @@ -235,10 +263,18 @@ class ExpressionGenerator(
}

override fun visitRef(reference: Reference) {
if (isParamRef(reference)) {
val referenceName = reference.name.defaultName()
val isParamReference = isParamRef(reference)

if (isParamReference) {
writer.writeInline("params.")
}
writer.writeInline(reference.name.defaultName())
writer.writeInline(referenceName)

if (renderingEndpointUrl) {
if (isParamReference && referenceName == "accountId") hasAccountIdBasedEndpoint = true
if (isParamReference && referenceName == "endpoint") hasServiceEndpointOverride = true
}
}

override fun visitGetAttr(getAttr: GetAttr) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,44 @@ class DefaultEndpointProviderTestGenerator(
fun render() {
writer.addImport("*", namespace = "kotlin.test")
writer.withBlock("public class #L {", "}", CLASS_NAME) {
writer.write("")
writer.withBlock(
"private fun expectEqualEndpoints(expected: #1T, actual: #1T) {",
"}",
RuntimeTypes.SmithyClient.Endpoints.Endpoint,
) {
// Remove ONLY business metrics endpoint attributes
writer.withBlock(
"if (actual.attributes.contains(#T) || actual.attributes.contains(#T)) {",
"} else { assertEquals(expected, actual) }",
RuntimeTypes.Core.BusinessMetrics.ServiceEndpointOverride,
RuntimeTypes.Core.BusinessMetrics.AccountIdBasedEndpointAccountId,
) {
writer.write(
"val newActualAttributes = actual.attributes.#T()",
RuntimeTypes.Core.Collections.toMutableAttributes,
)
writer.write(
"if (actual.attributes.contains(#1T)) newActualAttributes.remove(#1T)",
RuntimeTypes.Core.BusinessMetrics.ServiceEndpointOverride,
)
writer.write(
"if (actual.attributes.contains(#1T)) newActualAttributes.remove(#1T)",
RuntimeTypes.Core.BusinessMetrics.AccountIdBasedEndpointAccountId,
)
writer.write(
"val newActualAttributesOrEmpty = if (newActualAttributes.isEmpty) #T() else newActualAttributes",
RuntimeTypes.Core.Collections.emptyAttributes,
)
writer.write(
"val newActual = #T(actual.uri, actual.headers, newActualAttributesOrEmpty)",
RuntimeTypes.SmithyClient.Endpoints.Endpoint,
)
writer.write("assertEquals(expected, newActual)")
}
}
writer.write("")

cases.forEachIndexed { index, it ->
renderTestCase(index, it)
write("")
Expand Down Expand Up @@ -148,6 +186,6 @@ class DefaultEndpointProviderTestGenerator(
}

writer.write("val actual = #T().resolveEndpoint(params)", providerSymbol)
writer.write("assertEquals(expected, actual)")
writer.write("expectEqualEndpoints(expected, actual)")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import software.amazon.smithy.codegen.core.CodegenException
import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.kotlin.codegen.KotlinSettings
import software.amazon.smithy.kotlin.codegen.core.*
import software.amazon.smithy.kotlin.codegen.integration.SectionId
import software.amazon.smithy.kotlin.codegen.model.*
import software.amazon.smithy.kotlin.codegen.model.knowledge.EndpointParameterIndex
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator
Expand All @@ -17,6 +18,8 @@ import software.amazon.smithy.rulesengine.language.syntax.parameters.ParameterTy
import software.amazon.smithy.rulesengine.traits.ClientContextParamsTrait
import software.amazon.smithy.utils.StringUtils

object EndpointBusinessMetrics : SectionId

/**
* Generates resolver adapter for going from generic HTTP operation endpoint resolver to the generated
* type specific provider generated based on the rules.
Expand Down Expand Up @@ -153,6 +156,9 @@ class EndpointResolverAdapterGenerator(
) {
write("val params = resolveEndpointParams(config, request)")
write("val endpoint = config.endpointProvider.resolveEndpoint(params)")

declareSection(EndpointBusinessMetrics)

renderPostResolution()
write("return endpoint")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

package aws.smithy.kotlin.runtime.http.interceptors

import aws.smithy.kotlin.runtime.businessmetrics.*
import aws.smithy.kotlin.runtime.client.*
import aws.smithy.kotlin.runtime.collections.get
import aws.smithy.kotlin.runtime.http.HttpCall
import aws.smithy.kotlin.runtime.http.operation.OperationTypeInfo
import aws.smithy.kotlin.runtime.http.request.HttpRequest
Expand Down Expand Up @@ -188,7 +190,9 @@ internal class InterceptorExecutor<I, O>(

suspend fun modifyBeforeRetryLoop(request: HttpRequest): HttpRequest =
modifyHttpRequestHook(request) { interceptor, context ->
interceptor.modifyBeforeRetryLoop(context)
interceptor.modifyBeforeRetryLoop(context).also { modifiedRequest ->
updateBusinessMetrics(request, modifiedRequest, context)
}
}

fun readBeforeAttempt(request: HttpRequest): Result<Unit> {
Expand All @@ -203,7 +207,9 @@ internal class InterceptorExecutor<I, O>(

suspend fun modifyBeforeSigning(request: HttpRequest): HttpRequest =
modifyHttpRequestHook(request) { interceptor, context ->
interceptor.modifyBeforeSigning(context)
interceptor.modifyBeforeSigning(context).also { modifiedRequest ->
updateBusinessMetrics(request, modifiedRequest, context)
}
}

fun readBeforeSigning(request: HttpRequest) = readHttpHook(request) { interceptor, context ->
Expand All @@ -216,7 +222,9 @@ internal class InterceptorExecutor<I, O>(

suspend fun modifyBeforeTransmit(request: HttpRequest): HttpRequest =
modifyHttpRequestHook(request) { interceptor, context ->
interceptor.modifyBeforeTransmit(context)
interceptor.modifyBeforeTransmit(context).also { modifiedRequest ->
updateBusinessMetrics(request, modifiedRequest, context)
}
}

fun readBeforeTransmit(request: HttpRequest) = readHttpHook(request) { interceptor, context ->
Expand Down Expand Up @@ -328,4 +336,22 @@ internal class InterceptorExecutor<I, O>(
},
)
}

private fun updateBusinessMetrics(
request: HttpRequest,
modifiedRequest: HttpRequest,
context: HttpProtocolRequestInterceptorContext<Any>,
) {
if (modifiedRequest.url != request.url) {
if (
context.executionContext.containsBusinessMetric(SmithyBusinessMetric.ACCOUNT_ID_BASED_ENDPOINT) &&
!modifiedRequest.url.toString().contains(context.executionContext.attributes[AccountIdBasedEndpointAccountId])
) {
context.executionContext.removeBusinessMetric(SmithyBusinessMetric.ACCOUNT_ID_BASED_ENDPOINT)
}
if (context.executionContext.containsBusinessMetric(SmithyBusinessMetric.SERVICE_ENDPOINT_OVERRIDE)) {
context.executionContext.removeBusinessMetric(SmithyBusinessMetric.SERVICE_ENDPOINT_OVERRIDE)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
package aws.smithy.kotlin.runtime.http.interceptors

import aws.smithy.kotlin.runtime.InternalApi
import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric
import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric
import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext
import aws.smithy.kotlin.runtime.compression.CompressionAlgorithm
import aws.smithy.kotlin.runtime.http.HttpBody
Expand Down Expand Up @@ -42,7 +44,9 @@ public class RequestCompressionInterceptor(
}

return if (algorithm != null && (context.protocolRequest.body.isStreaming || payloadSizeBytes?.let { it >= compressionThresholdBytes } == true)) {
algorithm.compressRequest(context.protocolRequest)
algorithm.compressRequest(context.protocolRequest).also {
context.executionContext.emitBusinessMetric(SmithyBusinessMetric.GZIP_REQUEST_COMPRESSION)
}
} else {
val logger = coroutineContext.logger<RequestCompressionInterceptor>()
val skipCause = if (algorithm == null) "no modeled compression algorithms are supported by the client" else "request size threshold ($compressionThresholdBytes) was not met"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

package aws.smithy.kotlin.runtime.http.middleware

import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric
import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric
import aws.smithy.kotlin.runtime.http.interceptors.InterceptorExecutor
import aws.smithy.kotlin.runtime.http.operation.*
import aws.smithy.kotlin.runtime.http.operation.deepCopy
Expand All @@ -13,7 +15,9 @@ import aws.smithy.kotlin.runtime.http.request.immutableView
import aws.smithy.kotlin.runtime.http.request.toBuilder
import aws.smithy.kotlin.runtime.io.Handler
import aws.smithy.kotlin.runtime.io.middleware.Middleware
import aws.smithy.kotlin.runtime.retries.AdaptiveRetryStrategy
import aws.smithy.kotlin.runtime.retries.RetryStrategy
import aws.smithy.kotlin.runtime.retries.StandardRetryStrategy
import aws.smithy.kotlin.runtime.retries.policy.RetryDirective
import aws.smithy.kotlin.runtime.retries.policy.RetryPolicy
import aws.smithy.kotlin.runtime.retries.toResult
Expand Down Expand Up @@ -45,6 +49,11 @@ internal class RetryMiddleware<I, O>(

val outcome = strategy.retry(wrappedPolicy) {
withSpan<RetryMiddleware<*, *>, _>("Attempt-$attempt") {
when (strategy::class) {
StandardRetryStrategy::class -> modified.context.emitBusinessMetric(SmithyBusinessMetric.RETRY_MODE_STANDARD)
AdaptiveRetryStrategy::class -> modified.context.emitBusinessMetric(SmithyBusinessMetric.RETRY_MODE_ADAPTIVE)
}

if (attempt > 1) {
coroutineContext.debug<RetryMiddleware<*, *>> { "retrying request, attempt $attempt" }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,18 @@
package aws.smithy.kotlin.runtime.http.operation

import aws.smithy.kotlin.runtime.InternalApi
import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric
import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric
import aws.smithy.kotlin.runtime.client.LogMode
import aws.smithy.kotlin.runtime.client.endpoints.authOptions
import aws.smithy.kotlin.runtime.client.logMode
import aws.smithy.kotlin.runtime.collections.attributesOf
import aws.smithy.kotlin.runtime.collections.emptyAttributes
import aws.smithy.kotlin.runtime.collections.merge
import aws.smithy.kotlin.runtime.http.HttpCall
import aws.smithy.kotlin.runtime.http.HttpHandler
import aws.smithy.kotlin.runtime.http.*
import aws.smithy.kotlin.runtime.http.auth.SignHttpRequest
import aws.smithy.kotlin.runtime.http.complete
import aws.smithy.kotlin.runtime.http.interceptors.InterceptorExecutor
import aws.smithy.kotlin.runtime.http.middleware.RetryMiddleware
import aws.smithy.kotlin.runtime.http.readAll
import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder
import aws.smithy.kotlin.runtime.http.request.dumpRequest
import aws.smithy.kotlin.runtime.http.request.immutableView
Expand Down Expand Up @@ -313,6 +312,10 @@ internal class AuthHandler<Input, Output>(
authScheme.signer.sign(signingRequest)
}

if (authScheme.schemeId.id == "aws.auth#sigv4a") {
request.context.emitBusinessMetric(SmithyBusinessMetric.SIGV4A_SIGNING)
}

interceptors.readAfterSigning(modified.subject.immutableView())
return inner.call(modified)
}
Expand Down
28 changes: 28 additions & 0 deletions runtime/runtime-core/api/runtime-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,34 @@ public final class aws/smithy/kotlin/runtime/ServiceException$ErrorType : java/l
public static fun values ()[Laws/smithy/kotlin/runtime/ServiceException$ErrorType;
}

public abstract interface class aws/smithy/kotlin/runtime/businessmetrics/BusinessMetric {
public abstract fun getIdentifier ()Ljava/lang/String;
}

public final class aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtilsKt {
public static final fun containsBusinessMetric (Laws/smithy/kotlin/runtime/operation/ExecutionContext;Laws/smithy/kotlin/runtime/businessmetrics/BusinessMetric;)Z
public static final fun emitBusinessMetric (Laws/smithy/kotlin/runtime/operation/ExecutionContext;Laws/smithy/kotlin/runtime/businessmetrics/BusinessMetric;)V
public static final fun getAccountIdBasedEndpointAccountId ()Laws/smithy/kotlin/runtime/collections/AttributeKey;
public static final fun getBusinessMetrics ()Laws/smithy/kotlin/runtime/collections/AttributeKey;
public static final fun getServiceEndpointOverride ()Laws/smithy/kotlin/runtime/collections/AttributeKey;
public static final fun removeBusinessMetric (Laws/smithy/kotlin/runtime/operation/ExecutionContext;Laws/smithy/kotlin/runtime/businessmetrics/BusinessMetric;)V
}

public final class aws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric : java/lang/Enum, aws/smithy/kotlin/runtime/businessmetrics/BusinessMetric {
public static final field ACCOUNT_ID_BASED_ENDPOINT Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric;
public static final field GZIP_REQUEST_COMPRESSION Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric;
public static final field PAGINATOR Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric;
public static final field RETRY_MODE_ADAPTIVE Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric;
public static final field RETRY_MODE_STANDARD Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric;
public static final field SERVICE_ENDPOINT_OVERRIDE Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric;
public static final field SIGV4A_SIGNING Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric;
public static final field WAITER Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public fun getIdentifier ()Ljava/lang/String;
public static fun valueOf (Ljava/lang/String;)Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric;
public static fun values ()[Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric;
}

public final class aws/smithy/kotlin/runtime/collections/AttributeKey {
public fun <init> (Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
Expand Down
Loading

0 comments on commit 14c6038

Please sign in to comment.