/* * Copyright 2010-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ package com.amazonaws.metrics.internal.cloudwatch; import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.Request; import com.amazonaws.Response; import com.amazonaws.metrics.MetricType; import com.amazonaws.metrics.RequestMetricCollector; import com.amazonaws.metrics.internal.cloudwatch.spi.AWSMetricTransformerFactory; import com.amazonaws.metrics.internal.cloudwatch.spi.Dimensions; import com.amazonaws.services.cloudwatch.model.Dimension; import com.amazonaws.services.cloudwatch.model.MetricDatum; import com.amazonaws.services.cloudwatch.model.StandardUnit; import com.amazonaws.util.AWSRequestMetrics; import com.amazonaws.util.AWSRequestMetrics.Field; import com.amazonaws.util.TimingInfo; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.amazonaws.annotation.ThreadSafe; import java.util.ArrayList; import java.util.Collections; import java.util.List; import static com.amazonaws.metrics.internal.cloudwatch.spi.MetricData.newMetricDatum; import static com.amazonaws.metrics.internal.cloudwatch.spi.RequestMetricTransformer.Utils.endTimestamp; /** * Used to transform the predefined metrics of the AWS SDK into instances of * {@link MetricDatum}. * * See <a href= * "http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/publishingMetrics.html" * >http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/ * publishingMetrics.html</a> * * @see AWSRequestMetrics * @see RequestMetricCollector */ @ThreadSafe public class PredefinedMetricTransformer { private static final Log log = LogFactory.getLog(PredefinedMetricTransformer.class); static final boolean INCLUDE_REQUEST_TYPE = true; static final boolean EXCLUDE_REQUEST_TYPE = !INCLUDE_REQUEST_TYPE; /** * Returns a non-null list of metric datum for the metrics collected for the * given request/response. * * @param metricType the request metric type */ public List<MetricDatum> toMetricData(MetricType metricType, Request<?> request, Response<?> response) { if (metricType instanceof Field) { // Predefined metrics across all AWS http clients Field predefined = (Field) metricType; switch(predefined) { case HttpClientRetryCount: case HttpClientPoolAvailableCount: case HttpClientPoolLeasedCount: case HttpClientPoolPendingCount: return metricOfCount(predefined, request, response); case RequestCount: // intentionally fall through to reuse the same routine as RetryCount case RetryCount: return metricOfRequestOrRetryCount(predefined, request, response); case ThrottledRetryCount: // drop through case RetryCapacityConsumed: return counterMetricOf(predefined, request, response, EXCLUDE_REQUEST_TYPE); case ResponseProcessingTime: // drop through case RequestSigningTime: // drop through return latencyMetricOf(predefined, request, response, EXCLUDE_REQUEST_TYPE); case ClientExecuteTime: return latencyOfClientExecuteTime(request, response); case HttpClientSendRequestTime: case HttpClientReceiveResponseTime: case HttpRequestTime: case HttpSocketReadTime: return latencyMetricOf(predefined, request, response, INCLUDE_REQUEST_TYPE); case Exception: case ThrottleException: return counterMetricOf(predefined, request, response, INCLUDE_REQUEST_TYPE); default: break; } } // Predefined metrics for specific service clients for (AWSMetricTransformerFactory aws: AWSMetricTransformerFactory.values()) { if (metricType.name().startsWith(aws.name())) { List<MetricDatum> metricData = aws.getRequestMetricTransformer() .toMetricData(metricType, request, response); if (metricData != null) return metricData; break; } } if (log.isDebugEnabled()) { AmazonWebServiceRequest origReq = request == null ? null : request .getOriginalRequest(); String reqClassName = origReq == null ? null : origReq.getClass().getName(); log.debug("No request metric transformer can be found for metric type " + metricType.name() + " for " + reqClassName); } return Collections.emptyList(); } /** * Returns a list with a single metric datum for the specified retry or * request count predefined metric; or an empty list if there is none. * * @param metricType * must be either {@link Field#RequestCount} or * {@link Field#RetryCount}; or else GIGO. */ protected List<MetricDatum> metricOfRequestOrRetryCount( Field metricType, Request<?> req, Object resp) { AWSRequestMetrics m = req.getAWSRequestMetrics(); TimingInfo ti = m.getTimingInfo(); // Always retrieve the request count even for retry which is equivalent // to the number of requests minus one. Number counter = ti.getCounter(Field.RequestCount.name()); if (counter == null) { // this is possible if one of the request handlers screwed up return Collections.emptyList(); } int requestCount = counter.intValue(); if (requestCount < 1) { LogFactory.getLog(getClass()).debug( "request count must be at least one"); return Collections.emptyList(); } final double count = metricType == Field.RequestCount ? requestCount : requestCount-1 // retryCount = requestCount - 1 ; if (count < 1) { return Collections.emptyList(); } else { return Collections.singletonList(new MetricDatum() .withMetricName(req.getServiceName()) .withDimensions(new Dimension() .withName(Dimensions.MetricType.name()) .withValue(metricType.name())) .withUnit(StandardUnit.Count) .withValue(Double.valueOf(count)) .withTimestamp(endTimestamp(ti))) ; } } protected List<MetricDatum> metricOfCount( Field metricType, Request<?> req, Object resp) { AWSRequestMetrics m = req.getAWSRequestMetrics(); TimingInfo ti = m.getTimingInfo(); Number counter = ti.getCounter(metricType.name()); if (counter == null) { return Collections.emptyList(); } final double count = counter.doubleValue(); if (count < 1) { return Collections.emptyList(); } else { return Collections.singletonList(new MetricDatum() .withMetricName(req.getServiceName()) .withDimensions(new Dimension() .withName(Dimensions.MetricType.name()) .withValue(metricType.name())) .withUnit(StandardUnit.Count) .withValue(Double.valueOf(count)) .withTimestamp(endTimestamp(ti))) ; } } /** * Returns all the latency metric data recorded for the specified metric * event type; or an empty list if there is none. The number of metric datum * in the returned list should be exactly one when there is no retries, or * more than one when there are retries. * * @param includesRequestType * true iff the "request" dimension is to be included; */ protected List<MetricDatum> latencyMetricOf(MetricType metricType, Request<?> req, Object response, boolean includesRequestType) { AWSRequestMetrics m = req.getAWSRequestMetrics(); TimingInfo root = m.getTimingInfo(); final String metricName = metricType.name(); List<TimingInfo> subMeasures = root.getAllSubMeasurements(metricName); if (subMeasures != null) { List<MetricDatum> result = new ArrayList<MetricDatum>(subMeasures.size()); for (TimingInfo sub : subMeasures) { if (sub.isEndTimeKnown()) { // being defensive List<Dimension> dims = new ArrayList<Dimension>(); dims.add(new Dimension() .withName(Dimensions.MetricType.name()) .withValue(metricName)); // Either a non request type specific datum is created per // sub-measurement, or a request type specific one is // created but not both if (includesRequestType) { dims.add(new Dimension() .withName(Dimensions.RequestType.name()) .withValue(requestType(req))); } MetricDatum datum = new MetricDatum() .withMetricName(req.getServiceName()) .withDimensions(dims) .withUnit(StandardUnit.Milliseconds) .withValue(sub.getTimeTakenMillisIfKnown()); result.add(datum); } } return result; } return Collections.emptyList(); } /** * Returns a request type specific metrics for * {@link Field#ClientExecuteTime} which is special in the sense that it * makes a more accurate measurement by taking the {@link TimingInfo} at the * root into account. */ protected List<MetricDatum> latencyOfClientExecuteTime(Request<?> req, Object response) { AWSRequestMetrics m = req.getAWSRequestMetrics(); TimingInfo root = m.getTimingInfo(); final String metricName = Field.ClientExecuteTime.name(); if (root.isEndTimeKnown()) { // being defensive List<Dimension> dims = new ArrayList<Dimension>(); dims.add(new Dimension() .withName(Dimensions.MetricType.name()) .withValue(metricName)); // request type specific dims.add(new Dimension() .withName(Dimensions.RequestType.name()) .withValue(requestType(req))); MetricDatum datum = new MetricDatum() .withMetricName(req.getServiceName()) .withDimensions(dims) .withUnit(StandardUnit.Milliseconds) .withValue(root.getTimeTakenMillisIfKnown()); return Collections.singletonList(datum); } return Collections.emptyList(); } /** * Returns the name of the type of request. */ private String requestType(Request<?> req) { return req.getOriginalRequest().getClass().getSimpleName(); } /** * Returns a list of metric datum recorded for the specified counter metric * type; or an empty list if there is none. * * @param includesRequestType * true iff an additional metric datum is to be created that * includes the "request" dimension */ protected List<MetricDatum> counterMetricOf(MetricType type, Request<?> req, Object resp, boolean includesRequestType) { AWSRequestMetrics m = req.getAWSRequestMetrics(); TimingInfo ti = m.getTimingInfo(); final String metricName = type.name(); Number counter = ti.getCounter(metricName); if (counter == null) { return Collections.emptyList(); } int count = counter.intValue(); if (count < 1) { LogFactory.getLog(getClass()).debug("Count must be at least one"); return Collections.emptyList(); } final List<MetricDatum> result = new ArrayList<MetricDatum>(); final Dimension metricDimension = new Dimension() .withName(Dimensions.MetricType.name()) .withValue(metricName); // non-request type specific metric datum final MetricDatum first = new MetricDatum() .withMetricName(req.getServiceName()) .withDimensions(metricDimension) .withUnit(StandardUnit.Count) .withValue(Double.valueOf(count)) .withTimestamp(endTimestamp(ti)); result.add(first); if (includesRequestType) { // additional request type specific metric datum Dimension requestDimension = new Dimension() .withName(Dimensions.RequestType.name()) .withValue(requestType(req)); final MetricDatum second = newMetricDatum(first, metricDimension, requestDimension); result.add(second); } return result; } }