/* * Copyright 2015 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License 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.netflix.spectator.aws; import com.amazonaws.Request; import com.amazonaws.Response; import com.amazonaws.metrics.RequestMetricCollector; import com.amazonaws.util.AWSRequestMetrics; import com.amazonaws.util.AWSRequestMetrics.Field; import com.amazonaws.util.TimingInfo; import com.netflix.spectator.api.Id; import com.netflix.spectator.api.Registry; import com.netflix.spectator.impl.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.beans.Introspector; import java.net.URI; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.function.Function; /** * A {@link RequestMetricCollector} that captures request level metrics for AWS clients. */ public class SpectatorRequestMetricCollector extends RequestMetricCollector { private static final Logger LOGGER = LoggerFactory.getLogger(SpectatorRequestMetricCollector.class); private static final String UNKNOWN = "UNKNOWN"; private static final Field[] TIMERS = { Field.ClientExecuteTime, Field.CredentialsRequestTime, Field.HttpClientReceiveResponseTime, Field.HttpClientSendRequestTime, Field.HttpRequestTime, Field.RequestMarshallTime, Field.RequestSigningTime, Field.ResponseProcessingTime, Field.RetryPauseTime }; private static final Field[] COUNTERS = { Field.BytesProcessed, Field.HttpClientRetryCount, Field.RequestCount }; private static final TagField[] TAGS = { new TagField(Field.ServiceEndpoint, SpectatorRequestMetricCollector::getHost), new TagField(Field.ServiceName), new TagField(Field.StatusCode) }; private static final TagField[] ERRORS = { new TagField(Field.AWSErrorCode), new TagField(Field.Exception, e -> e.getClass().getSimpleName()) }; private final Registry registry; /** * Constructs a new instance. */ public SpectatorRequestMetricCollector(Registry registry) { super(); this.registry = Preconditions.checkNotNull(registry, "registry"); } @Override public void collectMetrics(Request<?> request, Response<?> response) { final AWSRequestMetrics metrics = request.getAWSRequestMetrics(); if (metrics.isEnabled()) { final Map<String, String> baseTags = getBaseTags(request); final TimingInfo timing = metrics.getTimingInfo(); for (Field counter : COUNTERS) { Optional.ofNullable(timing.getCounter(counter.name())) .filter(v -> v.longValue() > 0) .ifPresent(v -> registry.counter(metricId(counter, baseTags)).increment(v.longValue())); } for (Field timer : TIMERS) { Optional.ofNullable(timing.getLastSubMeasurement(timer.name())) .filter(TimingInfo::isEndTimeKnown) .ifPresent(t -> registry.timer(metricId(timer, baseTags)) .record(t.getEndTimeNano() - t.getStartTimeNano(), TimeUnit.NANOSECONDS)); } notEmpty(metrics.getProperty(Field.ThrottleException)).ifPresent(throttleExceptions -> { final Id throttling = metricId("throttling", baseTags); throttleExceptions.forEach(ex -> registry.counter(throttling.withTag("throttleException", ex.getClass().getSimpleName())) .increment()); }); } } private Id metricId(Field metric, Map<String, String> tags) { return metricId(metric.name(), tags); } private Id metricId(String metric, Map<String, String> tags) { return registry.createId(idName(metric), tags); } private Map<String, String> getBaseTags(Request<?> request) { final AWSRequestMetrics metrics = request.getAWSRequestMetrics(); final Map<String, String> baseTags = new HashMap<>(); for (TagField tag : TAGS) { baseTags.put(tag.getName(), tag.getValue(metrics).orElse(UNKNOWN)); } baseTags.put("requestType", request.getOriginalRequest().getClass().getSimpleName()); final boolean error = isError(metrics); if (error) { for (TagField tag : ERRORS) { baseTags.put(tag.getName(), tag.getValue(metrics).orElse(UNKNOWN)); } } baseTags.put("error", Boolean.toString(error)); return Collections.unmodifiableMap(baseTags); } /** * Produces the name of a metric from the name of the SDK measurement. * * @param name * Name of the SDK measurement, usually from the enum * {@link com.amazonaws.util.AWSRequestMetrics.Field}. * @return * Name to use in the metric id. */ //VisibleForTesting static String idName(String name) { return "aws.request." + Introspector.decapitalize(name); } private static Optional<List<Object>> notEmpty(List<Object> properties) { return Optional.ofNullable(properties).filter(v -> !v.isEmpty()); } /** * Extracts and transforms the first item from a list. * * @param properties * The list of properties to filter, may be null or empty * @param transform * The transform to apply to the extracted list item. The * transform is only applied if the list contains a non-null * item at index 0. * @param <R> * The transform return type * @return * The transformed value, or Optional.empty() if there is no * non-null item at index 0 of the list. */ //VisibleForTesting static <R> Optional<R> firstValue(List<Object> properties, Function<Object, R> transform) { return notEmpty(properties).map(v -> v.get(0)).map(transform::apply); } private static boolean isError(AWSRequestMetrics metrics) { for (TagField err : ERRORS) { if (err.getValue(metrics).isPresent()) { return true; } } return false; } private static String getHost(Object u) { try { return URI.create(u.toString()).getHost(); } catch (Exception e) { LOGGER.debug("failed to parse endpoint uri: " + u, e); return UNKNOWN; } } private static class TagField { private final Field field; private final String name; private final Function<Object, String> tagExtractor; public TagField(Field field) { this(field, Object::toString); } public TagField(Field field, Function<Object, String> tagExtractor) { this.field = field; this.tagExtractor = tagExtractor; this.name = Introspector.decapitalize(field.name()); } public String getName() { return name; } public Optional<String> getValue(AWSRequestMetrics metrics) { return firstValue(metrics.getProperty(field), tagExtractor); } } }