-
Notifications
You must be signed in to change notification settings - Fork 28
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
feat: business metrics #1096
Conversation
This comment has been minimized.
This comment has been minimized.
1 similar comment
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
||
declareSection(EndpointBusinessMetrics) | ||
|
There was a problem hiding this comment.
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.
renderingEndpointUrl = true | ||
renderExpression(rule.endpoint.url) | ||
renderingEndpointUrl = false |
There was a problem hiding this comment.
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)
}
}
}
}
}
}
…
}
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
// 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)") | ||
} |
There was a problem hiding this comment.
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.
val request = algorithm.compressRequest(context.protocolRequest) | ||
context.executionContext.emitBusinessMetric(BusinessMetrics.GZIP_REQUEST_COMPRESSION) | ||
request |
There was a problem hiding this comment.
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)
}
/** | ||
* 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") |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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") |
There was a problem hiding this comment.
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 secondedit: I see why this was duplicated in the name, seems good to keep itAccountId
* All the valid business metrics along with their identifiers | ||
*/ | ||
@InternalApi | ||
public enum class BusinessMetrics(public val identifier: String) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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() {
...
}
"private fun expectEqualEndpoints(expected: #T, actual: #T) {", | ||
"}", | ||
RuntimeTypes.SmithyClient.Endpoints.Endpoint, | ||
RuntimeTypes.SmithyClient.Endpoints.Endpoint, |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
when (strategy::class) { | ||
StandardRetryStrategy::class -> modified.context.emitBusinessMetric(BusinessMetrics.RETRY_MODE_STANDARD) | ||
AdaptiveRetryStrategy::class -> modified.context.emitBusinessMetric(BusinessMetrics.RETRY_MODE_ADAPTIVE) | ||
} |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
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") |
There was a problem hiding this comment.
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
renderingEndpointUrl = true | ||
renderExpression(rule.endpoint.url) | ||
renderingEndpointUrl = false |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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")
There was a problem hiding this comment.
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.
This comment has been minimized.
1 similar comment
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
1 similar comment
Affected ArtifactsChanged in size
|
Issue #
N/A
Description of changes
This change tracks business metrics in the execution context of an operation. To do so:
DefaultEndpointProviderGenerator
was modified to include endpoint business metrics in the endpoint attributesDefaultEndpointProviderTestGenerator
to be tolerant of business metrics attributesEndpointResolverAdapterGenerator
to add endpoint business metrics to the execution context if business metrics exist in the endpoint attributesBy submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.