1717
1818import static software .amazon .awssdk .utils .FunctionalUtils .runAndLogError ;
1919
20+ import java .time .Duration ;
2021import java .util .Optional ;
2122import java .util .concurrent .CompletableFuture ;
2223import java .util .function .Function ;
5051import software .amazon .awssdk .http .SdkHttpFullRequest ;
5152import software .amazon .awssdk .http .SdkHttpFullResponse ;
5253import software .amazon .awssdk .metrics .MetricCollector ;
54+ import software .amazon .awssdk .metrics .NoOpMetricCollector ;
5355import software .amazon .awssdk .utils .CompletableFutureUtils ;
5456import software .amazon .awssdk .utils .Logger ;
5557
@@ -70,7 +72,7 @@ protected BaseAsyncClientHandler(SdkClientConfiguration clientConfiguration,
7072 public <InputT extends SdkRequest , OutputT extends SdkResponse > CompletableFuture <OutputT > execute (
7173 ClientExecutionParams <InputT , OutputT > executionParams ) {
7274
73- return measureApiCallSuccess (executionParams , () -> {
75+ return measureApiCall (executionParams , () -> {
7476 // Running beforeExecution interceptors and modifyRequest interceptors.
7577 ExecutionContext executionContext = invokeInterceptorsAndCreateExecutionContext (executionParams );
7678
@@ -86,7 +88,7 @@ public <InputT extends SdkRequest, OutputT extends SdkResponse, ReturnT> Complet
8688 ClientExecutionParams <InputT , OutputT > executionParams ,
8789 AsyncResponseTransformer <OutputT , ReturnT > asyncResponseTransformer ) {
8890
89- return measureApiCallSuccess (executionParams , () -> {
91+ return measureApiCall (executionParams , () -> {
9092 if (executionParams .getCombinedResponseHandler () != null ) {
9193 // There is no support for catching errors in a body for streaming responses. Our codegen must never
9294 // attempt to do this.
@@ -232,7 +234,10 @@ private <InputT extends SdkRequest, OutputT extends SdkResponse, ReturnT> Comple
232234 new AsyncAfterTransmissionInterceptorCallingResponseHandler <>(asyncResponseHandler ,
233235 executionContext ));
234236
237+ // Captured because the requestBody branch above may reassign 'marshalled', leaving it not effectively final.
238+ SdkHttpFullRequest requestForMetrics = marshalled ;
235239 CompletableFuture <ReturnT > exceptionTranslatedFuture = invokeFuture .handle ((resp , err ) -> {
240+ reportServiceEndpointMetric (executionContext , requestForMetrics );
236241 if (err != null ) {
237242 throw ThrowableUtils .failure (err );
238243 }
@@ -288,27 +293,49 @@ private <InputT extends SdkRequest, OutputT> CompletableFuture<OutputT> invoke(
288293 .execute (responseHandler );
289294 }
290295
291- private <T > CompletableFuture <T > measureApiCallSuccess (ClientExecutionParams <?, ?> executionParams ,
292- Supplier <CompletableFuture <T >> apiCall ) {
296+ /**
297+ * Measure {@link CoreMetric#API_CALL_DURATION} and report {@link CoreMetric#API_CALL_SUCCESSFUL} for the whole API
298+ * call.
299+ *
300+ * <p>The window deliberately encloses everything the SDK does for the call, marshalling included, and closes when
301+ * the returned future completes. Measuring inside the request pipeline is not an option: the pipeline's input is the
302+ * already-marshalled request, so no arrangement of pipeline stages can enclose marshalling. Measuring here also
303+ * keeps the window identical to the synchronous client's.
304+ */
305+ private <T > CompletableFuture <T > measureApiCall (ClientExecutionParams <?, ?> executionParams ,
306+ Supplier <CompletableFuture <T >> apiCall ) {
307+ MetricCollector metricCollector = executionParams .getMetricCollector ();
308+ if (metricCollector == null || metricCollector instanceof NoOpMetricCollector ) {
309+ // Nothing will consume these metrics, so don't pay for the clock reads or the extra future. A null collector
310+ // is treated the same as NoOp: when the params carry none, the collector that AwsExecutionContextBuilder
311+ // substitutes into the ExecutionContext is never handed to a publisher, so anything reported to it is
312+ // discarded.
313+ try {
314+ return apiCall .get ();
315+ } catch (Exception e ) {
316+ return CompletableFutureUtils .failedFuture (e );
317+ }
318+ }
319+
320+ long callStart = System .nanoTime ();
293321 try {
294322 CompletableFuture <T > apiCallResult = apiCall .get ();
295323 CompletableFuture <T > outputFuture =
296- apiCallResult .whenComplete ((r , t ) -> reportApiCallSuccess ( executionParams , t == null ));
324+ apiCallResult .whenComplete ((r , t ) -> reportApiCallMetrics ( metricCollector , callStart , t == null ));
297325
298326 // Preserve cancellations on the output future, by passing cancellations of the output future to the api call future.
299327 CompletableFutureUtils .forwardExceptionTo (outputFuture , apiCallResult );
300328
301329 return outputFuture ;
302330 } catch (Exception e ) {
303- reportApiCallSuccess ( executionParams , false );
331+ reportApiCallMetrics ( metricCollector , callStart , false );
304332 return CompletableFutureUtils .failedFuture (e );
305333 }
306334 }
307335
308- private void reportApiCallSuccess (ClientExecutionParams <?, ?> executionParams , boolean value ) {
309- MetricCollector metricCollector = executionParams .getMetricCollector ();
310- if (metricCollector != null ) {
311- metricCollector .reportMetric (CoreMetric .API_CALL_SUCCESSFUL , value );
312- }
336+ private void reportApiCallMetrics (MetricCollector metricCollector , long callStartNanoTime , boolean successful ) {
337+ long durationNanos = System .nanoTime () - callStartNanoTime ;
338+ metricCollector .reportMetric (CoreMetric .API_CALL_SUCCESSFUL , successful );
339+ metricCollector .reportMetric (CoreMetric .API_CALL_DURATION , Duration .ofNanos (durationNanos ));
313340 }
314341}
0 commit comments