Skip to content

Commit ef242e0

Browse files
authored
Add business metrics implementation guidelines (#6527)
* Add business metrics implementation guidelines * Address PR feedback * Adding few more guidelines
1 parent 0e55835 commit ef242e0

File tree

2 files changed

+175
-1
lines changed

2 files changed

+175
-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: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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+
- [Versioning and Backward Compatibility](#versioning-and-backward-compatibility)
9+
- [Testing Requirements](#testing-requirements)
10+
- [Examples and References](#examples-and-references)
11+
12+
## Overview
13+
14+
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.
15+
16+
**Key Concepts:**
17+
- **Business Metrics**: Short string identifiers (e.g., "S", "A", "B") that represent feature usage
18+
- **User-Agent Header**: HTTP header where business metrics are included for telemetry
19+
20+
## Core Principles
21+
22+
### Feature-Centric Placement
23+
24+
**MUST** add business metrics when we finalize/know for sure that the feature is being used. To account for cases where features can be overridden, add business metrics at the point where feature usage is confirmed and finalized.
25+
26+
**Rationale:** Based on team discussion, this approach was chosen over centralized placement in `ApplyUserAgentStage` because:
27+
- **Better separation of concerns**: `ApplyUserAgentStage` remains ignorant of internal feature implementation details
28+
- **Easier maintenance**: Feature refactoring doesn't require updating multiple places
29+
- **Reduced coupling**: Avoids tight coupling between stages and feature implementations
30+
31+
32+
## Implementation Patterns
33+
34+
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.
35+
36+
```java
37+
// Example from CompressRequestStage
38+
private void updateContentEncodingHeader(SdkHttpFullRequest.Builder input,
39+
Compressor compressor,
40+
ExecutionAttributes executionAttributes) {
41+
// Record business metric when compression is actually applied
42+
executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS)
43+
.addMetric(BusinessMetricFeatureId.GZIP_REQUEST_COMPRESSION.value());
44+
45+
if (input.firstMatchingHeader(COMPRESSION_HEADER).isPresent()) {
46+
input.appendHeader(COMPRESSION_HEADER, compressor.compressorType());
47+
} else {
48+
input.putHeader(COMPRESSION_HEADER, compressor.compressorType());
49+
}
50+
}
51+
52+
// Example from HttpChecksumStage - showing where business metrics are recorded
53+
@Override
54+
public SdkHttpFullRequest.Builder execute(SdkHttpFullRequest.Builder request,
55+
RequestExecutionContext context) throws Exception {
56+
// ... feature resolution logic ...
57+
58+
SdkHttpFullRequest.Builder result = processChecksum(request, context);
59+
60+
// Record business metrics after feature is finalized
61+
recordChecksumBusinessMetrics(context.executionAttributes());
62+
63+
return result;
64+
}
65+
```
66+
67+
## Performance Considerations
68+
69+
### Avoid Request Mutation for Business Metrics
70+
71+
**SHOULD NOT** use request mutation (`.toBuilder().build()`) for adding business metrics as it creates unnecessary object copies and performance overhead.
72+
73+
**Avoid This Pattern** (Used in waiter/paginator implementations):
74+
```java
75+
// Creates new objects (performance overhead)
76+
Consumer<AwsRequestOverrideConfiguration.Builder> userAgentApplier =
77+
b -> b.addApiName(ApiName.builder().name("sdk-metrics").version("B").build());
78+
79+
AwsRequestOverrideConfiguration overrideConfiguration =
80+
request.overrideConfiguration().map(c -> c.toBuilder().applyMutation(userAgentApplier).build())
81+
.orElse(AwsRequestOverrideConfiguration.builder().applyMutation(userAgentApplier).build());
82+
83+
return (T) request.toBuilder().overrideConfiguration(overrideConfiguration).build();
84+
```
85+
86+
**Prefer This ExecutionAttributes Pattern**:
87+
```java
88+
// Direct business metrics collection (no object creation)
89+
private void recordFeatureBusinessMetric(ExecutionAttributes executionAttributes) {
90+
BusinessMetricCollection businessMetrics =
91+
executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS);
92+
93+
if (businessMetrics != null) {
94+
businessMetrics.addMetric(BusinessMetricFeatureId.FEATURE_ID.value());
95+
}
96+
}
97+
```
98+
99+
In cases of high-level features (for example, Transfer Manager, Batch Manager, Cross-Region operations) that are resolved before ExecutionContext is built and we don't have access to ExecutionAttributes, prefer using `AwsExecutionContextBuilder.resolveUserAgentBusinessMetrics()` if the feature can be detected from client configuration or execution parameters (for example, retry mode from client config or RPC v2 CBOR protocol from execution parameters). If there is no option then request mutation is acceptable.
100+
101+
## Versioning and Backward Compatibility
102+
103+
### Business Metrics Changes Are Backward Compatible
104+
105+
In general, changes to existing business metrics can be treated as backward compatible since business metrics don't affect SDK functionality or customer code behavior, so customer applications remain unaffected. Business metrics are purely observational telemetry, so changes like modifying an existing business metric are safe changes. If we are making changes to existing business metrics, then discuss with the team and do a minor version bump if needed so that teams can identify the new metric from that version.
106+
107+
108+
## Testing Requirements
109+
110+
### Functional Testing with Mock HTTP Clients
111+
112+
**MUST** use functional testing with mock HTTP clients instead of interceptor-based testing.
113+
114+
**Why Mock HTTP Clients:**
115+
- **Reliability**: Tests are not affected by interceptor ordering changes or SDK internal modifications
116+
- **End-to-end verification**: Tests verify the complete flow from feature usage to User-Agent header inclusion
117+
- **Simplicity**: Direct access to the final HTTP request without interceptor setup
118+
- **Maintainability**: Tests remain stable even when internal pipeline stages are refactored
119+
120+
**Testing Pattern:**
121+
1. Create a mock HTTP client and stub the response
122+
2. Build the SDK client with the mock HTTP client
123+
3. Execute the operation that should trigger the business metric
124+
4. Extract the User-Agent header from the captured request
125+
5. Verify the business metric is present using pattern matching
126+
127+
```java
128+
@Test
129+
void testBusinessMetric_withMockHttpClient() {
130+
MockSyncHttpClient mockHttpClient = new MockSyncHttpClient();
131+
mockHttpClient.stubNextResponse(HttpExecuteResponse.builder()
132+
.response(SdkHttpResponse.builder()
133+
.statusCode(200)
134+
.build())
135+
.build());
136+
137+
// Create client with mock HTTP client and make request
138+
S3Client client = S3Client.builder()
139+
.httpClient(mockHttpClient)
140+
.build();
141+
142+
client.listBuckets();
143+
144+
// Extract User-Agent from the last request
145+
SdkHttpRequest lastRequest = mockHttpClient.getLastRequest();
146+
String userAgent = lastRequest.firstMatchingHeader("User-Agent").orElse("");
147+
148+
// Verify business metric is present
149+
assertThat(userAgent).matches(METRIC_SEARCH_PATTERN.apply("A"));
150+
}
151+
```
152+
153+
**For Async Clients:**
154+
Use `MockAsyncHttpClient` with the same pattern for testing async operations.
155+
156+
### Reference Test Files
157+
- `test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/UserAgentProviderTest.java`
158+
- `test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/rpcv2cbor/RpcV2CborUserAgentTest.java`
159+
160+
161+
## Examples and References
162+
163+
Here are some example implementations:
164+
165+
### Key Files and Classes
166+
- **BusinessMetricFeatureId**: `core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java`
167+
- **BusinessMetricsUtils**: `core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/BusinessMetricsUtils.java`
168+
- **ApplyUserAgentStage**: `core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStage.java`
169+
- **HttpChecksumStage**: `core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStage.java`
170+
- **CompressRequestStage**: `core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/CompressRequestStage.java`
171+
- **AuthSchemeInterceptorSpec**: `codegen/src/main/java/software/amazon/awssdk/codegen/poet/auth/scheme/AuthSchemeInterceptorSpec.java`

0 commit comments

Comments
 (0)