Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: business metrics #1096

Merged
merged 10 commits into from
Jun 17, 2024
Merged

feat: business metrics #1096

merged 10 commits into from
Jun 17, 2024

Conversation

0marperez
Copy link
Contributor

@0marperez 0marperez commented May 23, 2024

Issue #

N/A

Description of changes

This change tracks business metrics in the execution context of an operation. To do so:

  • The DefaultEndpointProviderGenerator was modified to include endpoint business metrics in the endpoint attributes
  • This change required modifying the DefaultEndpointProviderTestGenerator to be tolerant of business metrics attributes
  • A section was declared in EndpointResolverAdapterGenerator to add endpoint business metrics to the execution context if business metrics exist in the endpoint attributes
  • Business metrics are now added to the execution context directly in various locations
  • Required runtime types to emit business metrics were added

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@0marperez 0marperez added the no-changelog Indicates that a changelog entry isn't required for a pull request. Use sparingly. label May 23, 2024

This comment has been minimized.

1 similar comment

This comment has been minimized.

This comment has been minimized.

@0marperez 0marperez marked this pull request as ready for review May 24, 2024 20:49
@0marperez 0marperez requested a review from a team as a code owner May 24, 2024 20:49
Comment on lines +159 to +161

declareSection(EndpointBusinessMetrics)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: This is the point at which we know an endpoint was resolved but not the point where we could definitively say the endpoint was used. (A subsequent interceptor may replace the endpoint.) We should find a way to plumb this information closer to where the endpoint is actually used.

Comment on lines +188 to +190
renderingEndpointUrl = true
renderExpression(rule.endpoint.url)
renderingEndpointUrl = false
Copy link
Contributor

@ianbotsf ianbotsf May 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: Doing this kind of mutable state tracking via class-level fields is difficult to follow and error-prone. The visitor pattern allows using a return type to get information from within the tree but it looks like we currently use ExpressionVisitor<Unit> and skip returning anything.

My suggestion would be to refactor this to keep a context result in the visitor:

data class EndpointInfo(val params: MutableSet<String>) {
    companion object {
        val Empty = EndpointInfo(params = mutableSetOf())
    }

    operator fun plus(other: EndpointInfo) = EndpointInfo(
        params = this.params + other.params,
    )
}

class ExpressionGenerator(…): ExpressionVisitor<EndpointInfo>, … {
    …

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

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

        return if (isParamReference) {
            EndpointInfo(params = mutableSetOf(referenceName))
        } else {
            EndpointInfo.Empty
        }
    }

    …
}

Then you can modify the expression renderer to return/use the information:

fun interface ExpressionRenderer {
    fun renderExpression(expr: Expression): EndpointInfo
}

class DefaultEndpointProviderGenerator(…): ExpressionRenderer {
    …

    override fun renderExpression(expr: Expression): EndpointInfo = expr.accept(expressionGenerator)

    private fun renderEndpointRule(rule: EndpointRule) {
        withConditions(rule.conditions) {
            writer.withBlock("return #T(", ")", RuntimeTypes.SmithyClient.Endpoints.Endpoint) {
                writeInline("#T.parse(", RuntimeTypes.Core.Net.Url.Url)
                val endpointInfo = renderExpression(rule.endpoint.url)
                write("),")

                val hasAccountIdBasedEndpoint = "accountId" in endpointInfo.params
                val hasServiceEndpointOverride = "endpoint" in endpointInfo.params
                val needAdditionalEndpointProperties = hasAccountIdBasedEndpoint || hasServiceEndpointOverride

                if (rule.endpoint.headers.isNotEmpty()) { … }

                if (rule.endpoint.properties.isNotEmpty() || needAdditionalEndpointProperties) {
                    withBlock("attributes = #T {", "},", RuntimeTypes.Core.Collections.attributesOf) {
                        rule.endpoint.properties.entries.forEach { (k, v) -> … }

                        if (hasAccountIdBasedEndpoint) {
                            writer.write("#T to true", RuntimeTypes.Core.BusinessMetrics.accountIdBasedEndPoint)
                        }
                        if (hasServiceEndpointOverride) {
                            writer.write("#T to true", RuntimeTypes.Core.BusinessMetrics.serviceEndpointOverride)
                        }
                    }
                }
            }
        }
    }

    …
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0marperez Were you able to take a look at this feedback, do you think it's possible to refactor? I also agree that the boolean toggling stands out and seems error-prone for future implementations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran into some difficulties while trying to implement this and decided to leave it as is since the comment is marked as style. I think it's possible so I'll give it another go

Comment on lines 71 to 97
// Make exceptions ONLY for business metrics attributes
writer.withBlock(
"if (actual.attributes.contains(#T) || actual.attributes.contains(#T)) {",
"} else { assertEquals(expected, actual) }",
RuntimeTypes.Core.BusinessMetrics.serviceEndpointOverride,
RuntimeTypes.Core.BusinessMetrics.accountIdBasedEndPoint,
) {
writer.write(
"val updatedAttributes = expected.attributes.#T()",
RuntimeTypes.Core.Collections.toMutableAttributes,
)
writer.write(
"if (actual.attributes.contains(#T)) updatedAttributes[#T] = true",
RuntimeTypes.Core.BusinessMetrics.serviceEndpointOverride,
RuntimeTypes.Core.BusinessMetrics.serviceEndpointOverride,
)
writer.write(
"if (actual.attributes.contains(#T)) updatedAttributes[#T] = true",
RuntimeTypes.Core.BusinessMetrics.accountIdBasedEndPoint,
RuntimeTypes.Core.BusinessMetrics.accountIdBasedEndPoint,
)
writer.write(
"val newExpected = #T(expected.uri, expected.headers, updatedAttributes)",
RuntimeTypes.SmithyClient.Endpoints.Endpoint,
)
writer.write("assertEquals(newExpected, actual)")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: This appears to be setting the expected attributes based on the actual attributes, which is backwards. If the actual attributes erroneously omitted, for example, account ID then this test wouldn't catch it.

Comment on lines 47 to 49
val request = algorithm.compressRequest(context.protocolRequest)
context.executionContext.emitBusinessMetric(BusinessMetrics.GZIP_REQUEST_COMPRESSION)
request
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: You can avoid this kind of temporary-storage-for-control-flow pattern by using also:

algorithm.compressRequest(context.protocolRequest).also {
    context.executionContext.emitBusinessMetric(BusinessMetrics.GZIP_REQUEST_COMPRESSION)
}

Comment on lines 18 to 28
/**
* If an endpoint is account ID based
*/
@InternalApi
public val accountIdBasedEndPoint: AttributeKey<Boolean> = AttributeKey("aws.smithy.kotlin#AccountIdBasedEndpoint")

/**
* If an endpoint is service endpoint override based
*/
@InternalApi
public val serviceEndpointOverride: AttributeKey<Boolean> = AttributeKey("aws.smithy.kotlin#ServiceEndpointOverride")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: I'm not sure I see the value of dedicated attribute keys for these. At the point we have access to the these attributes, we could be calling emitBusinessMetrics already, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean in the DefaultEndpointProviderGenerator? If so I don't think we can, we only have access to the endpoint attributes but not to the execution context attributes

This comment has been minimized.

* The account ID in an account ID based endpoint
*/
@InternalApi
public val accountIdBasedEndPointAccountId: AttributeKey<String> = AttributeKey("aws.smithy.kotlin#AccountIdBasedEndpointAccountId")
Copy link
Contributor

@lauzadis lauzadis Jun 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming: AccountIdBasedEndpointAccountId

  • No camel casing
  • EndPoint -> Endpoint
    - Remove second AccountId edit: I see why this was duplicated in the name, seems good to keep it

* All the valid business metrics along with their identifiers
*/
@InternalApi
public enum class BusinessMetrics(public val identifier: String) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Are these enum values correct? I see something different in the internal spec. "J" is S3_EXPRESS_BUCKET, "L" is GZIP_REQUEST_COMPRESSION, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, they're outdated now. Thanks

* Keeps track of all business metrics along an operations execution
*/
@InternalApi
public val businessMetrics: AttributeKey<MutableSet<String>> = AttributeKey("aws.sdk.kotlin#BusinessMetrics")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming: AttributeKeys are not usually camel-cased, BusinessMetrics

* If an endpoint is service endpoint override based
*/
@InternalApi
public val serviceEndpointOverride: AttributeKey<Boolean> = AttributeKey("aws.smithy.kotlin#ServiceEndpointOverride")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming: ServiceEndpointOverride

* All the valid business metrics along with their identifiers
*/
@InternalApi
public enum class BusinessMetrics(public val identifier: String) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming: BusinessMetric: Enum class names are usually not pluralized,, since an instance of this enum will only ever represent a single metric

@@ -185,7 +204,7 @@ class DefaultEndpointProviderGenerator(
}
}

if (rule.endpoint.properties.isNotEmpty()) {
if (rule.endpoint.properties.isNotEmpty() || needAdditionalEndpointProperties) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: rule.endpoint.properties are mutable, so instead of creating this new boolean / tracking extra state, you can just add the accountIdBasedEndpoint and serviceEndpointOverride parameters to the endpoint params before entering this if statement.

Something like this:

rule.endpoint.properties[Identifier.of(RuntimeTypes.Core.BusinessMetrics.accountIdBasedEndPointAccountId.name)] = Literal.of("params.accountId")
rule.endpoint.properties[Identifier.of(RuntimeTypes.Core.BusinessMetrics.serviceEndpointOverride.name)] = Literal.booleanLiteral(true)

if (rule.endpoint.properties.isNotEmpty() { 
    ...
} 

Comment on lines 66 to 69
"private fun expectEqualEndpoints(expected: #T, actual: #T) {",
"}",
RuntimeTypes.SmithyClient.Endpoints.Endpoint,
RuntimeTypes.SmithyClient.Endpoints.Endpoint,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simplification: You can remove the duplicated RuntimeTypes.SmithyClient.Endpoints.Endpoint by replacing #T with #1T

Same for all other lines where you duplicate the #T

RuntimeTypes.SmithyClient.Endpoints.Endpoint,
RuntimeTypes.SmithyClient.Endpoints.Endpoint,
) {
// Remove ONLY business metrics endpoint attributes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Was this modification required to get endpoint tests passing? It would be nice if we didn't have to customize the endpoint tests based on business metrics

Copy link
Contributor Author

@0marperez 0marperez Jun 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the endpoint tests in the model(s) don't include business metrics in the expected. To fix this the model(s) would have to be updated

Comment on lines 54 to 57
when (strategy::class) {
StandardRetryStrategy::class -> modified.context.emitBusinessMetric(BusinessMetrics.RETRY_MODE_STANDARD)
AdaptiveRetryStrategy::class -> modified.context.emitBusinessMetric(BusinessMetrics.RETRY_MODE_ADAPTIVE)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correctness: this is only emitting metrics when a request is retried (first attempt failed), but it seems like the SEP wants us to emit the metrics any time the strategy is used (even on the first attempt).

@@ -328,4 +336,22 @@ internal class InterceptorExecutor<I, O>(
},
)
}

private fun updateBusinessMetrics(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Instead of calling this function after almost every interceptor stage, can it just be added as a new interceptor? It can keep an internal var request : HttpRequest that is updated at each stage, and then you can use the readBeforeX interceptor hooks to compare the modifiedRequest

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on this suggestion? I think it would be cleaner, InterceptorExecutor shouldn't really be responsible for emitting / updating business metrics IMO

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if it would work, the interceptor would only run once but there may be other interceptors that run afterwards or before that change the url and it wouldn't be picked up

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently we're only checking if the URL changed after the modifyX interceptors are executed. We could instead add a new interceptor right after in the readY phase to check if the URL had changed.

It's not a one-way decision so I'm fine to leave it as it is and refactor later if needed.

* All the valid business metrics along with their identifiers
*/
@InternalApi
public enum class BusinessMetrics(public val identifier: String) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these metrics are AWS-specific and don't belong in smithy-kotlin. We spent a lot of time making sure S3 Express was not mentioned in smithy-kotlin.

Is there any way the AWS-specific enums can be relocated to aws-sdk-kotlin?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that too but it seems like that would require smithy kotlin have a dependency on the sdk. I think we want to avoid that

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, smithy-kotlin can not depend on aws-sdk-kotlin at all, that's a circular dependency.

Fixing this will certainly involve some rearchitecture. You will probably need to make an AwsBusinessMetrics class which implements BusinessMetrics so it can be used interchangeably in all the functions / utilities you wrote. That would also probably mean moving away from enum class, we can discuss offline

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

object BusinessMetrics : RuntimeTypePackage(KotlinDependency.CORE, "businessmetrics") {
val AccountIdBasedEndpointAccountId = symbol("AccountIdBasedEndpointAccountId")
val ServiceEndpointOverride = symbol("ServiceEndpointOverride")
val emitBusinessMetrics = symbol("emitBusinessMetric")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming mismatch: emitBusinessMetrics vs. emitBusinessMetric

Comment on lines +188 to +190
renderingEndpointUrl = true
renderExpression(rule.endpoint.url)
renderingEndpointUrl = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@0marperez Were you able to take a look at this feedback, do you think it's possible to refactor? I also agree that the boolean toggling stands out and seems error-prone for future implementations

@@ -328,4 +336,22 @@ internal class InterceptorExecutor<I, O>(
},
)
}

private fun updateBusinessMetrics(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on this suggestion? I think it would be cleaner, InterceptorExecutor shouldn't really be responsible for emitting / updating business metrics IMO

* Generic business metrics
*/
@InternalApi
public enum class SmithyBusinessMetric(public override val identifier: String) : BusinessMetric {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We support some features that aren't listed here. Should we add TODOs to add them in the future?

  • WAITER("B")
  • PAGINATOR("C")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, those features were added to the spec after we started the implementation. I think it's a good idea

This comment has been minimized.

1 similar comment

This comment has been minimized.

This comment has been minimized.

1 similar comment
Copy link

Affected Artifacts

Changed in size
Artifact Pull Request (bytes) Latest Release (bytes) Delta (bytes) Delta (percentage)
http-client-jvm.jar 332,851 331,000 1,851 0.56%
runtime-core-jvm.jar 883,226 878,947 4,279 0.49%

@0marperez 0marperez merged commit 14c6038 into main Jun 17, 2024
15 checks passed
@0marperez 0marperez deleted the business-metrics branch June 17, 2024 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
no-changelog Indicates that a changelog entry isn't required for a pull request. Use sparingly.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants