From 950c83056c8ae17f7293d75fb0ab56fb11ed9d1d Mon Sep 17 00:00:00 2001 From: Shaun Ford Date: Mon, 2 Oct 2017 17:14:45 -0700 Subject: [PATCH] Send API Exception Metrics to SignalFx (#72) * Optionally send API error metrics to SignalFx --- .../SfxAwareApiExceptionHandlerUtils.java | 110 ++++++++++++++++++ .../cerberus/server/config/CmsConfig.java | 4 +- ...CerberusBackstopperRiposteGuiceModule.java | 72 ++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/nike/cerberus/error/SfxAwareApiExceptionHandlerUtils.java create mode 100644 src/main/java/com/nike/cerberus/server/config/guice/CerberusBackstopperRiposteGuiceModule.java diff --git a/src/main/java/com/nike/cerberus/error/SfxAwareApiExceptionHandlerUtils.java b/src/main/java/com/nike/cerberus/error/SfxAwareApiExceptionHandlerUtils.java new file mode 100644 index 000000000..fb2ecc4cc --- /dev/null +++ b/src/main/java/com/nike/cerberus/error/SfxAwareApiExceptionHandlerUtils.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2017 Nike, 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.nike.cerberus.error; + +import com.codahale.metrics.Counter; +import com.codahale.metrics.MetricRegistry; +import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.handler.ApiExceptionHandlerUtils; +import com.nike.backstopper.handler.RequestInfoForLogging; +import com.nike.internal.util.Pair; +import com.signalfx.codahale.metrics.MetricBuilder; +import com.signalfx.codahale.reporter.MetricMetadata; +import com.signalfx.codahale.reporter.SignalFxReporter; + +import java.util.Collection; +import java.util.List; + +/** + * A SignalFx-aware ApiExceptionHandlerUtils that increments an api_errors {@link Counter} + * metric with the following dimensions (based on the error info that gets logged): + * response_code, contributing_errors, and exception_class. + */ +public class SfxAwareApiExceptionHandlerUtils extends ApiExceptionHandlerUtils { + + /** + * The name of the API errors metric sent to SignalFx. + */ + public static final String API_ERRORS_METRIC_NAME = "api_errors"; + /** + * The name/key of the HTTP response code dimension applied to the API errors metric. + */ + public static final String RESPONSE_CODE_DIM_KEY = "response_code"; + /** + * The name/key of the contributing errors dimension applied to the API errors metric. + */ + public static final String CONTRIBUTING_ERRORS_DIM_KEY = "contributing_errors"; + /** + * The name/key of the exception class dimension applied to the API errors metric. + */ + public static final String EXCEPTION_CLASS_DIM_KEY = "exception_class"; + + protected final MetricRegistry metricRegistry; + protected final MetricMetadata sfxMetricMetadata; + + /** + * Creates a new instance. + * + * @param metricRegistry The {@link MetricRegistry} that is used to create metrics for reporting to SignalFx. + * Cannot be null. + * @param sfxMetricMetadata The SignalFx reporter's {@link MetricMetadata} for building dimensioned metrics - + * this can be retrieved by calling {@link SignalFxReporter#getMetricMetadata()} on your SignalFx reporter. + * Cannot be null. + */ + public SfxAwareApiExceptionHandlerUtils(MetricRegistry metricRegistry, + MetricMetadata sfxMetricMetadata) { + if (metricRegistry == null) + throw new IllegalArgumentException("metricRegistry cannot be null"); + + if (sfxMetricMetadata == null) + throw new IllegalArgumentException("sfxMetricMetadata cannot be null"); + + this.metricRegistry = metricRegistry; + this.sfxMetricMetadata = sfxMetricMetadata; + } + + @Override + public String buildErrorMessageForLogs(StringBuilder sb, RequestInfoForLogging request, + Collection contributingErrors, Integer httpStatusCode, + Throwable cause, + List> extraDetailsForLogging) { + try { + // Do the normal logging thing. + return super.buildErrorMessageForLogs( + sb, request, contributingErrors, httpStatusCode, cause, extraDetailsForLogging + ); + } + finally { + // Update SignalFx metrics around API Errors. + String contributingErrorsString = contributingErrors == null + ? "[NONE]" + : concatenateErrorCollection(contributingErrors); + + Counter apiErrorsCounterMetric = sfxMetricMetadata + .forBuilder(MetricBuilder.COUNTERS) + .withMetricName(API_ERRORS_METRIC_NAME) + .withDimension(RESPONSE_CODE_DIM_KEY, String.valueOf(httpStatusCode)) + .withDimension(CONTRIBUTING_ERRORS_DIM_KEY, contributingErrorsString) + .withDimension(EXCEPTION_CLASS_DIM_KEY, cause.getClass().getName()) + .createOrGet(metricRegistry); + + apiErrorsCounterMetric.inc(); + } + } +} + diff --git a/src/main/java/com/nike/cerberus/server/config/CmsConfig.java b/src/main/java/com/nike/cerberus/server/config/CmsConfig.java index e1b945d1e..ae546df3b 100644 --- a/src/main/java/com/nike/cerberus/server/config/CmsConfig.java +++ b/src/main/java/com/nike/cerberus/server/config/CmsConfig.java @@ -20,6 +20,7 @@ import com.netflix.config.ConfigurationManager; import com.netflix.config.DynamicPropertyFactory; import com.nike.backstopper.handler.riposte.config.guice.BackstopperRiposteConfigGuiceModule; +import com.nike.cerberus.server.config.guice.CerberusBackstopperRiposteGuiceModule; import com.nike.cerberus.server.config.guice.CmsFlywayModule; import com.nike.cerberus.server.config.guice.CmsGuiceModule; import com.nike.cerberus.server.config.guice.CmsMyBatisModule; @@ -106,7 +107,8 @@ protected CmsConfig(Config appConfig, PropertiesRegistrationGuiceModule properti new BackstopperRiposteConfigGuiceModule(), new CmsFlywayModule(), new OneLoginGuiceModule(), - new MetricsGuiceModule() + new MetricsGuiceModule(), + new CerberusBackstopperRiposteGuiceModule() )); // bind the CMS Guice module last allowing the S3 props file to override any given application property diff --git a/src/main/java/com/nike/cerberus/server/config/guice/CerberusBackstopperRiposteGuiceModule.java b/src/main/java/com/nike/cerberus/server/config/guice/CerberusBackstopperRiposteGuiceModule.java new file mode 100644 index 000000000..8be45c0c5 --- /dev/null +++ b/src/main/java/com/nike/cerberus/server/config/guice/CerberusBackstopperRiposteGuiceModule.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2017 Nike, 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.nike.cerberus.server.config.guice; + +import com.codahale.metrics.MetricRegistry; +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.nike.backstopper.handler.ApiExceptionHandlerUtils; +import com.nike.cerberus.error.SfxAwareApiExceptionHandlerUtils; +import com.nike.riposte.metrics.codahale.CodahaleMetricsCollector; +import com.nike.riposte.metrics.codahale.contrib.SignalFxReporterFactory; +import com.signalfx.codahale.reporter.MetricMetadata; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import javax.inject.Singleton; + +public class CerberusBackstopperRiposteGuiceModule extends AbstractModule { + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + protected void configure() { + } + + /** + * @param metricsCollector The {@link CodahaleMetricsCollector} being used for the app. Can be null - if null is + * passed in then {@link ApiExceptionHandlerUtils} will be returned. + * @param sfxReporterFactory The {@link SignalFxReporterFactory} being used for the app. Can be null - if null is + * passed in then {@link ApiExceptionHandlerUtils} will be returned. + * @return A {@link SfxAwareApiExceptionHandlerUtils} if the given args are not null, or {@link + * ApiExceptionHandlerUtils} if either arg is null. + */ + @Provides + @Singleton + public ApiExceptionHandlerUtils sfxAwareApiExceptionHandlerUtils(@Nullable CodahaleMetricsCollector metricsCollector, + @Nullable SignalFxReporterFactory sfxReporterFactory) { + + MetricRegistry metricRegistry = metricsCollector == null ? null : metricsCollector.getMetricRegistry(); + MetricMetadata sfxMetricMetadata = sfxReporterFactory == null || metricRegistry == null + ? null + : sfxReporterFactory.getReporter(metricRegistry).getMetricMetadata(); + + if (metricRegistry == null || sfxMetricMetadata == null) { + logger.warn("Unable to do SignalFx metric gathering around API Errors - the CodahaleMetricsCollector " + + "and/or SignalFxReporterFactory were null. Defaulting to ApiExceptionHandlerUtils. " + + "metrics_collector_is_null={}, sfx_reporter_factory_is_null={}", + metricsCollector == null, + sfxReporterFactory == null); + return new ApiExceptionHandlerUtils(); + } + + // We have all the bits we need to do metrics reporting, so return a SfxAwareApiExceptionHandlerUtils + // that will do it. + return new SfxAwareApiExceptionHandlerUtils(metricRegistry, sfxMetricMetadata); + } +}