Skip to content

Commit 61f41e3

Browse files
committed
Add business metrics implementation guidelines
1 parent 78e9dc0 commit 61f41e3

File tree

2 files changed

+165
-1
lines changed

2 files changed

+165
-1
lines changed

docs/guidelines/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,7 @@ Guidelines for when and how to use Optional in the SDK, including restrictions o
3535
Patterns for preferring static factory methods over constructors, including naming conventions for factory methods and the benefits of this approach for immutable objects and API design.
3636

3737
### [Client Configuration](ClientConfiguration.md)
38-
Structural requirements for configuration objects including immutability patterns, builder interfaces, field naming conventions, and proper handling of collection types in configuration APIs.
38+
Structural requirements for configuration objects including immutability patterns, builder interfaces, field naming conventions, and proper handling of collection types in configuration APIs.
39+
40+
### [Business Metrics Guidelines](business-metrics-guidelines.md)
41+
Guidelines for implementing business metrics in the AWS SDK for Java v2. Covers feature-centric placement principles, performance considerations, functional testing approaches, and a few examples of where we added business metrics for various features.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Business Metrics Implementation Guidelines
2+
3+
## Table of Contents
4+
- [Overview](#overview)
5+
- [Core Principles](#core-principles)
6+
- [Implementation Patterns](#implementation-patterns)
7+
- [Performance Considerations](#performance-considerations)
8+
- [Testing Requirements](#testing-requirements)
9+
- [Examples and References](#examples-and-references)
10+
11+
## Overview
12+
13+
Business metrics are short identifiers added to the User-Agent header for telemetry tracking. They help AWS understand feature usage patterns across the SDK. This document provides guidelines for implementing business metrics in the AWS SDK for Java v2, based on team architectural decisions and performance considerations.
14+
15+
**Key Concepts:**
16+
- **Business Metrics**: Short string identifiers (e.g., "S", "A", "B") that represent feature usage
17+
- **User-Agent Header**: HTTP header where business metrics are included for telemetry
18+
19+
## Core Principles
20+
21+
### Feature-Centric Placement
22+
23+
**MUST** add business metrics where the feature is resolved, at the point where it is finalized that the feature is being used. Consider cases where features can be overridden - the decision is to add business metrics at the place where the feature is finalized.
24+
25+
**Rationale:** Based on team discussion, this approach was chosen over centralized placement in `ApplyUserAgentStage` because:
26+
- **Better separation of concerns**: `ApplyUserAgentStage` remains ignorant of internal feature implementation details
27+
- **Easier maintenance**: Feature refactoring doesn't require updating multiple places
28+
- **Reduced coupling**: Avoids tight coupling between stages and feature implementations
29+
30+
31+
## Implementation Patterns
32+
33+
For GZIP compression, we know that the request is compressed in `CompressRequestStage`, so we add the business metric there. For checksums, we know that checksum is resolved in `HttpChecksumStage`, so we add the business metric there.
34+
35+
```java
36+
// Example from CompressRequestStage
37+
private void updateContentEncodingHeader(SdkHttpFullRequest.Builder input,
38+
Compressor compressor,
39+
ExecutionAttributes executionAttributes) {
40+
// Record business metric when compression is actually applied
41+
executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS)
42+
.addMetric(BusinessMetricFeatureId.GZIP_REQUEST_COMPRESSION.value());
43+
44+
if (input.firstMatchingHeader(COMPRESSION_HEADER).isPresent()) {
45+
input.appendHeader(COMPRESSION_HEADER, compressor.compressorType());
46+
} else {
47+
input.putHeader(COMPRESSION_HEADER, compressor.compressorType());
48+
}
49+
}
50+
51+
// Example from HttpChecksumStage - showing where business metrics are recorded
52+
@Override
53+
public SdkHttpFullRequest.Builder execute(SdkHttpFullRequest.Builder request,
54+
RequestExecutionContext context) throws Exception {
55+
// ... feature resolution logic ...
56+
57+
SdkHttpFullRequest.Builder result = processChecksum(request, context);
58+
59+
// Record business metrics after feature is finalized
60+
recordChecksumBusinessMetrics(context.executionAttributes());
61+
62+
return result;
63+
}
64+
```
65+
66+
## Performance Considerations
67+
68+
### Avoid Request Mutation for Business Metrics
69+
70+
**SHOULD NOT** use request mutation (`.toBuilder().build()`) for adding business metrics as it creates unnecessary object copies and performance overhead.
71+
72+
**Avoid This Pattern** (Used in waiter/paginator implementations):
73+
```java
74+
// Creates new objects (performance overhead)
75+
Consumer<AwsRequestOverrideConfiguration.Builder> userAgentApplier =
76+
b -> b.addApiName(ApiName.builder().name("sdk-metrics").version("B").build());
77+
78+
AwsRequestOverrideConfiguration overrideConfiguration =
79+
request.overrideConfiguration().map(c -> c.toBuilder().applyMutation(userAgentApplier).build())
80+
.orElse(AwsRequestOverrideConfiguration.builder().applyMutation(userAgentApplier).build());
81+
82+
return (T) request.toBuilder().overrideConfiguration(overrideConfiguration).build();
83+
```
84+
85+
**Prefer This ExecutionAttributes Pattern**:
86+
```java
87+
// Direct business metrics collection (no object creation)
88+
private void recordFeatureBusinessMetric(ExecutionAttributes executionAttributes) {
89+
BusinessMetricCollection businessMetrics =
90+
executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS);
91+
92+
if (businessMetrics != null) {
93+
businessMetrics.addMetric(BusinessMetricFeatureId.FEATURE_ID.value());
94+
}
95+
}
96+
```
97+
98+
## Testing Requirements
99+
100+
### Functional Testing with Mock HTTP Clients
101+
102+
**MUST** use functional testing with mock HTTP clients instead of interceptor-based testing.
103+
104+
**Why Mock HTTP Clients:**
105+
- **Reliability**: Tests are not affected by interceptor ordering changes or SDK internal modifications
106+
- **End-to-end verification**: Tests verify the complete flow from feature usage to User-Agent header inclusion
107+
- **Simplicity**: Direct access to the final HTTP request without interceptor setup
108+
- **Maintainability**: Tests remain stable even when internal pipeline stages are refactored
109+
110+
**Testing Pattern:**
111+
1. Create a mock HTTP client and stub the response
112+
2. Build the SDK client with the mock HTTP client
113+
3. Execute the operation that should trigger the business metric
114+
4. Extract the User-Agent header from the captured request
115+
5. Verify the business metric is present using pattern matching
116+
117+
```java
118+
@Test
119+
void testBusinessMetric_withMockHttpClient() {
120+
MockSyncHttpClient mockHttpClient = new MockSyncHttpClient();
121+
mockHttpClient.stubNextResponse(HttpExecuteResponse.builder()
122+
.response(SdkHttpResponse.builder()
123+
.statusCode(200)
124+
.build())
125+
.build());
126+
127+
// Create client with mock HTTP client and make request
128+
S3Client client = S3Client.builder()
129+
.httpClient(mockHttpClient)
130+
.build();
131+
132+
client.listBuckets();
133+
134+
// Extract User-Agent from the last request
135+
SdkHttpRequest lastRequest = mockHttpClient.getLastRequest();
136+
String userAgent = lastRequest.firstMatchingHeader("User-Agent").orElse("");
137+
138+
// Verify business metric is present
139+
assertThat(userAgent).matches(METRIC_SEARCH_PATTERN.apply("A"));
140+
}
141+
```
142+
143+
**For Async Clients:**
144+
Use `MockAsyncHttpClient` with the same pattern for testing async operations.
145+
146+
### Reference Test Files
147+
- `test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/UserAgentProviderTest.java`
148+
- `test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/rpcv2cbor/RpcV2CborUserAgentTest.java`
149+
150+
151+
## Examples and References
152+
153+
Here are some example implementations:
154+
155+
### Key Files and Classes
156+
- **BusinessMetricFeatureId**: `core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java`
157+
- **BusinessMetricsUtils**: `core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/BusinessMetricsUtils.java`
158+
- **ApplyUserAgentStage**: `core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStage.java`
159+
- **HttpChecksumStage**: `core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStage.java`
160+
- **CompressRequestStage**: `core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/CompressRequestStage.java`
161+
- **AuthSchemeInterceptorSpec**: `codegen/src/main/java/software/amazon/awssdk/codegen/poet/auth/scheme/AuthSchemeInterceptorSpec.java`

0 commit comments

Comments
 (0)