From e6dcdb89fb4915cd3927dc713c994872c30797d8 Mon Sep 17 00:00:00 2001 From: Shaun Ford Date: Tue, 5 Sep 2017 10:05:08 -0700 Subject: [PATCH] Allow Metrics to be sent to SignalFx (#65) * Add a feature to allow metrics reporting to SignalFx * Make signalfx props optional * Report JVM metrics --- gradle.properties | 2 +- gradle/dependencies.gradle | 1 + gradle/develop.gradle | 2 +- .../config/MetricsConfigurationHelper.java | 210 ++++++++++++++++++ .../cerberus/server/config/CmsConfig.java | 7 +- .../server/config/guice/CmsGuiceModule.java | 81 +------ .../config/guice/MetricsGuiceModule.java | 204 +++++++++++++++++ src/main/resources/cms-local.conf | 2 + src/main/resources/cms.conf | 9 + 9 files changed, 446 insertions(+), 72 deletions(-) create mode 100644 src/main/java/com/nike/cerberus/config/MetricsConfigurationHelper.java create mode 100644 src/main/java/com/nike/cerberus/server/config/guice/MetricsGuiceModule.java diff --git a/gradle.properties b/gradle.properties index 3906fad51..04bb84a8b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,6 +14,6 @@ # limitations under the License. # -version=0.28.0 +version=0.29.0 groupId=com.nike.cerberus artifactId=cms diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 012e6cf24..c41b93e8e 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -36,6 +36,7 @@ dependencies { "com.nike.riposte:riposte-guice-typesafe-config:$riposteVersion", "com.nike.riposte:riposte-async-http-client:$riposteVersion", "com.nike.riposte:riposte-metrics-codahale:$riposteVersion", + "com.nike.riposte:riposte-metrics-codahale-signalfx:$riposteVersion", "javax:javaee-api:7.0", "org.codehaus.groovy:groovy-all:$groovyVersion", // For logback groovy config processing diff --git a/gradle/develop.gradle b/gradle/develop.gradle index 19dfbb1f6..bf251c393 100644 --- a/gradle/develop.gradle +++ b/gradle/develop.gradle @@ -18,7 +18,7 @@ import org.apache.tools.ant.taskdefs.condition.Os import groovyx.net.http.RESTClient import static groovyx.net.http.ContentType.* -def dashboardRelease = 'v1.3.0' +def dashboardRelease = 'v1.6.0' def vaultVersion = "0.7.3" def reverseProxyPort = 9001 diff --git a/src/main/java/com/nike/cerberus/config/MetricsConfigurationHelper.java b/src/main/java/com/nike/cerberus/config/MetricsConfigurationHelper.java new file mode 100644 index 000000000..4e6aab8b9 --- /dev/null +++ b/src/main/java/com/nike/cerberus/config/MetricsConfigurationHelper.java @@ -0,0 +1,210 @@ +package com.nike.cerberus.config; + +import com.amazonaws.util.EC2MetadataUtils; +import com.codahale.metrics.Histogram; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.SlidingTimeWindowReservoir; +import com.codahale.metrics.Timer; +import com.google.inject.Inject; +import com.nike.internal.util.Pair; +import com.nike.riposte.metrics.codahale.CodahaleMetricsCollector; +import com.nike.riposte.metrics.codahale.CodahaleMetricsListener; +import com.nike.riposte.metrics.codahale.EndpointMetricsHandler; +import com.nike.riposte.metrics.codahale.contrib.SignalFxReporterFactory; +import com.nike.riposte.metrics.codahale.impl.SignalFxEndpointMetricsHandler; +import com.nike.riposte.metrics.codahale.impl.SignalFxEndpointMetricsHandler.MetricDimensionConfigurator; +import com.nike.riposte.server.http.HttpProcessingState; +import com.nike.riposte.server.http.RequestInfo; +import com.nike.riposte.server.http.ResponseInfo; +import com.signalfx.codahale.reporter.SignalFxReporter; +import com.signalfx.codahale.reporter.SignalFxReporter.Builder; +import com.signalfx.codahale.reporter.SignalFxReporter.MetricDetails; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import static com.nike.riposte.metrics.codahale.impl.SignalFxEndpointMetricsHandler.DEFAULT_REQUEST_LATENCY_TIMER_DIMENSION_CONFIGURATOR; + +/** + * A set of methods used to help instantiate the necessary metrics objects in the Guice module + */ +public class MetricsConfigurationHelper { + + private static final String EC2_HOSTNAME_PREFIX = "ip-"; + + private final String signalFxApiKey; + + private final String signalFxAppEnvDim; + + /** + * This 'holder' class allows optional injection of SignalFx-specific properties that are only necessary when + * SignalFx metrics reporting is enabled. + * + * The 'optional=true' parameter to Guice @Inject cannot be used in combination with the @Provides annotation + * or with constructor injection. + * + * https://github.com/google/guice/wiki/FrequentlyAskedQuestions + */ + static class SignalFxOptionalPropertyHolder { + @Inject(optional=true) + @com.google.inject.name.Named("metrics.signalfx.api_key") + String signalFxApiKey = "signalfx api key injection failed"; // set default value so key is never null + + @Inject(optional=true) + @com.google.inject.name.Named("metrics.signalfx.dimension.app_env") + String signalFxAppEnvDim = "empty"; // set default value so 'env' dimension is never null + } + + @Inject + public MetricsConfigurationHelper(SignalFxOptionalPropertyHolder signalFxPropertyHolder) { + signalFxApiKey = signalFxPropertyHolder.signalFxApiKey; + signalFxAppEnvDim = signalFxPropertyHolder.signalFxAppEnvDim; + } + + /** + * Generates the SignalFx metrics reporter + * @param serviceVersionDim The version number for this service/app + * @param customReporterConfigurator Allows custom SignalFx dimensions or configuration to be sent to SignalFx + * @param metricDetailsToReport The set of metric details that should be reported to SignalFx + * @return The SignalFxReporterFactory + */ + public SignalFxReporterFactory generateSignalFxReporterFactory( + String serviceVersionDim, + String appNameDim, + Function customReporterConfigurator, + Set metricDetailsToReport + ) { + Set finalMetricDetailsToReport = (metricDetailsToReport == null) + ? DEFAULT_METRIC_DETAILS_TO_SEND_TO_SIGNALFX + : metricDetailsToReport; + + String host = EC2MetadataUtils.getLocalHostName(); + String ec2Hostname = EC2_HOSTNAME_PREFIX + EC2MetadataUtils.getPrivateIpAddress(); + + Function defaultReporterConfigurator = + (builder) -> builder + .addUniqueDimension("host", host) + .addUniqueDimension("ec2_hostname", ec2Hostname) + .addUniqueDimension("app", appNameDim) + .addUniqueDimension("env", signalFxAppEnvDim) + .addUniqueDimension("framework", "riposte") + .addUniqueDimension("app_version", serviceVersionDim) + .setDetailsToAdd(finalMetricDetailsToReport); + + if (customReporterConfigurator == null) + customReporterConfigurator = Function.identity(); + + return new SignalFxReporterFactory( + signalFxApiKey, + defaultReporterConfigurator.andThen(customReporterConfigurator), + // Report metrics at a 10 second interval + Pair.of(10L, TimeUnit.SECONDS) + ); + } + + public CodahaleMetricsListener generateCodahaleMetricsListenerWithSignalFxSupport( + SignalFxReporterFactory signalFxReporterFactory, + CodahaleMetricsCollector metricsCollector, + MetricDimensionConfigurator customRequestTimerDimensionConfigurator, + ExtraRequestLogicHandler extraRequestLogicHandler + ) { + MetricRegistry metricRegistry = metricsCollector.getMetricRegistry(); + + // Use the identity function if customRequestTimerDimensionConfigurator is null. + if (customRequestTimerDimensionConfigurator == null) + customRequestTimerDimensionConfigurator = METRIC_DIMENSION_CONFIGURATOR_IDENTITY; + + // Create the SignalFxEndpointMetricsHandler with the customRequestTimerDimensionConfigurator and + // extraRequestLogicHandler specifics. + EndpointMetricsHandler endpointMetricsHandler = new SignalFxEndpointMetricsHandler( + signalFxReporterFactory.getReporter(metricRegistry).getMetricMetadata(), + metricRegistry, + // Use a rolling window reservoir with the same window as the reporting frequency, + // to prevent the dashboards from producing false or misleading data. + new SignalFxEndpointMetricsHandler.RollingWindowTimerBuilder(signalFxReporterFactory.getInterval(), + signalFxReporterFactory.getTimeUnit()), + // Do the default request latency timer dimensions, chained with customRequestTimerDimensionConfigurator + // for any custom logic desired. + DEFAULT_REQUEST_LATENCY_TIMER_DIMENSION_CONFIGURATOR + .chainedWith(customRequestTimerDimensionConfigurator) + ) { + @Override + public void handleRequest(RequestInfo requestInfo, ResponseInfo responseInfo, + HttpProcessingState httpState, + int responseHttpStatusCode, int responseHttpStatusCodeXXValue, + long requestElapsedTimeMillis) { + + // Do the normal endpoint stuff. + super.handleRequest(requestInfo, responseInfo, httpState, responseHttpStatusCode, + responseHttpStatusCodeXXValue, + requestElapsedTimeMillis); + + // Do any extra logic (if desired). + if (extraRequestLogicHandler != null) { + extraRequestLogicHandler.handleExtraRequestLogic( + requestInfo, responseInfo, httpState, responseHttpStatusCode, responseHttpStatusCodeXXValue, + requestElapsedTimeMillis + ); + } + } + }; + + return CodahaleMetricsListener + .newBuilder(metricsCollector) + .withEndpointMetricsHandler(endpointMetricsHandler) + // The metric names should be basic with no prefix - SignalFx dimensions do the job normally covered + // by metric name prefixes. + .withServerStatsMetricNamingStrategy(CodahaleMetricsListener.MetricNamingStrategy.defaultNoPrefixImpl()) + .withServerConfigMetricNamingStrategy(CodahaleMetricsListener.MetricNamingStrategy.defaultNoPrefixImpl()) + // Histograms should use a rolling window reservoir with the same window as the reporting frequency, + // otherwise the dashboards will produce false or misleading data. + .withRequestAndResponseSizeHistogramSupplier( + () -> new Histogram( + new SlidingTimeWindowReservoir(signalFxReporterFactory.getInterval(), + signalFxReporterFactory.getTimeUnit()) + ) + ) + .build(); + } + + /** + * The default set of metric details to send to SignalFx when reporting metrics. By reducing these to only the + * common ones necessary and letting SignalFx calculate aggregates for us where possible (e.g. calculating rates + * just from the count metric rather than us sending the pre-aggregated codahale 1min/5min/15min metric details) + * of data sent to SignalFx is significantly decreased and therefore saves a lot of money. + */ + public static final Set DEFAULT_METRIC_DETAILS_TO_SEND_TO_SIGNALFX = Collections.unmodifiableSet( + EnumSet.of( + SignalFxReporter.MetricDetails.COUNT, + SignalFxReporter.MetricDetails.MIN, + SignalFxReporter.MetricDetails.MEAN, + SignalFxReporter.MetricDetails.MAX, + SignalFxReporter.MetricDetails.PERCENT_95, + SignalFxReporter.MetricDetails.PERCENT_99 + ) + ); + + /** + * A {@link SignalFxEndpointMetricsHandler.MetricDimensionConfigurator} that is the identity function - nothing will + * be done when it's called and it will simply return the provided rawBuilder. + */ + public static final SignalFxEndpointMetricsHandler.MetricDimensionConfigurator + METRIC_DIMENSION_CONFIGURATOR_IDENTITY = + (rawBuilder, a, b, c, d, e, f, g, h, i, j) -> rawBuilder; + + /** + * A functional interface for doing any extra logic you want on a per-request basis (e.g. extra metrics not covered + * by the default request timer). + */ + @FunctionalInterface + public interface ExtraRequestLogicHandler { + + void handleExtraRequestLogic(RequestInfo requestInfo, ResponseInfo responseInfo, + HttpProcessingState httpState, + int responseHttpStatusCode, int responseHttpStatusCodeXXValue, + long requestElapsedTimeMillis); + } +} 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 86d7fb0ff..2e3896e69 100644 --- a/src/main/java/com/nike/cerberus/server/config/CmsConfig.java +++ b/src/main/java/com/nike/cerberus/server/config/CmsConfig.java @@ -39,6 +39,8 @@ import com.google.inject.Injector; import com.google.inject.Module; import com.typesafe.config.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Arrays; @@ -58,6 +60,8 @@ */ public class CmsConfig implements ServerConfig { + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + /* We use a GuiceProvidedServerConfigValues to generate most of the values we need to return for ServerConfig's methods. Some values will be provided by config files (using a PropertiesRegistrationGuiceModule), others from CmsGuiceModule, @@ -90,7 +94,8 @@ protected CmsConfig(Config appConfig, PropertiesRegistrationGuiceModule properti new CmsMyBatisModule(), new BackstopperRiposteConfigGuiceModule(), new CmsFlywayModule(), - new OneLoginGuiceModule() + new OneLoginGuiceModule(), + new MetricsGuiceModule() )); // 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/CmsGuiceModule.java b/src/main/java/com/nike/cerberus/server/config/guice/CmsGuiceModule.java index bc5759daa..b7d40a22a 100644 --- a/src/main/java/com/nike/cerberus/server/config/guice/CmsGuiceModule.java +++ b/src/main/java/com/nike/cerberus/server/config/guice/CmsGuiceModule.java @@ -18,15 +18,18 @@ package com.nike.cerberus.server.config.guice; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.inject.AbstractModule; +import com.google.inject.Provides; import com.google.inject.name.Names; import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; +import com.nike.cerberus.auth.connector.AuthConnector; import com.nike.cerberus.config.CmsEnvPropertiesLoader; import com.nike.cerberus.endpoints.HealthCheckEndpoint; import com.nike.cerberus.endpoints.admin.CleanUpInactiveOrOrphanedRecords; import com.nike.cerberus.endpoints.admin.GetSDBMetadata; import com.nike.cerberus.endpoints.admin.PutSDBMetadata; -import com.nike.cerberus.endpoints.authentication.AuthenticateIamRole; import com.nike.cerberus.endpoints.authentication.AuthenticateIamPrincipal; +import com.nike.cerberus.endpoints.authentication.AuthenticateIamRole; import com.nike.cerberus.endpoints.authentication.AuthenticateUser; import com.nike.cerberus.endpoints.authentication.MfaCheck; import com.nike.cerberus.endpoints.authentication.RefreshUserToken; @@ -46,37 +49,28 @@ import com.nike.cerberus.endpoints.sdb.UpdateSafeDepositBoxV1; import com.nike.cerberus.endpoints.sdb.UpdateSafeDepositBoxV2; import com.nike.cerberus.error.DefaultApiErrorsImpl; -import com.nike.cerberus.auth.connector.AuthConnector; import com.nike.cerberus.security.CmsRequestSecurityValidator; import com.nike.cerberus.util.UuidSupplier; import com.nike.cerberus.vault.CmsVaultCredentialsProvider; import com.nike.cerberus.vault.CmsVaultUrlResolver; +import com.nike.riposte.client.asynchttp.ning.AsyncHttpClientHelper; +import com.nike.riposte.server.config.AppInfo; +import com.nike.riposte.server.http.Endpoint; +import com.nike.riposte.util.AwsUtil; import com.nike.vault.client.ClientVersion; import com.nike.vault.client.UrlResolver; import com.nike.vault.client.VaultAdminClient; import com.nike.vault.client.VaultClientFactory; import com.nike.vault.client.auth.VaultCredentialsProvider; -import com.nike.riposte.client.asynchttp.ning.AsyncHttpClientHelper; -import com.nike.riposte.metrics.codahale.CodahaleMetricsCollector; -import com.nike.riposte.metrics.codahale.CodahaleMetricsEngine; -import com.nike.riposte.metrics.codahale.CodahaleMetricsListener; -import com.nike.riposte.metrics.codahale.ReporterFactory; -import com.nike.riposte.metrics.codahale.contrib.DefaultGraphiteReporterFactory; -import com.nike.riposte.metrics.codahale.contrib.DefaultJMXReporterFactory; -import com.nike.riposte.metrics.codahale.contrib.DefaultSLF4jReporterFactory; -import com.nike.riposte.server.config.AppInfo; -import com.nike.riposte.server.http.Endpoint; -import com.nike.riposte.util.AwsUtil; - -import com.google.inject.AbstractModule; -import com.google.inject.Provides; import com.typesafe.config.Config; - import com.typesafe.config.ConfigValueFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; +import javax.inject.Named; +import javax.inject.Singleton; +import javax.validation.Validation; +import javax.validation.Validator; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; @@ -85,11 +79,6 @@ import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; -import javax.inject.Named; -import javax.inject.Singleton; -import javax.validation.Validation; -import javax.validation.Validator; - public class CmsGuiceModule extends AbstractModule { private static final String KMS_KEY_ID_KEY = "CONFIG_KEY_ID"; @@ -218,52 +207,6 @@ public AsyncHttpClientHelper asyncHttpClientHelper() { return new AsyncHttpClientHelper(); } - @Provides - @Singleton - public CodahaleMetricsListener metricsListener( - @Named("metrics.slf4j.reporting.enabled") boolean slf4jReportingEnabled, - @Named("metrics.jmx.reporting.enabled") boolean jmxReportingEnabled, - @Named("graphite.url") String graphiteUrl, - @Named("graphite.port") int graphitePort, - @Named("graphite.reporting.enabled") boolean graphiteEnabled, - @Named("appInfoFuture") CompletableFuture appInfoFuture, - CodahaleMetricsCollector metricsCollector) { - List reporters = new ArrayList<>(); - - if (slf4jReportingEnabled) - reporters.add(new DefaultSLF4jReporterFactory()); - - if (jmxReportingEnabled) - reporters.add(new DefaultJMXReporterFactory()); - - if (graphiteEnabled) { - AppInfo appInfo = appInfoFuture.join(); - String graphitePrefix = appInfo.appId() + "." + appInfo.dataCenter() + "." + appInfo.environment() - + "." + appInfo.instanceId(); - reporters.add(new DefaultGraphiteReporterFactory(graphitePrefix, graphiteUrl, graphitePort)); - } - - if (reporters.isEmpty()) { - logger.info("No metrics reporters enabled - disabling metrics entirely."); - return null; - } - - String metricReporterTypes = reporters.stream() - .map(rf -> rf.getClass().getSimpleName()) - .collect(Collectors.joining(",", "[", "]")); - logger.info("Metrics enabled. metric_reporter_types={}", metricReporterTypes); - - CodahaleMetricsEngine metricsEngine = new CodahaleMetricsEngine(metricsCollector, reporters); - metricsEngine.start(); - return new CodahaleMetricsListener(metricsCollector); - } - - @Provides - @Singleton - public CodahaleMetricsCollector codahaleMetricsCollector() { - return new CodahaleMetricsCollector(); - } - @Singleton @Provides public UuidSupplier uuidSupplier() { diff --git a/src/main/java/com/nike/cerberus/server/config/guice/MetricsGuiceModule.java b/src/main/java/com/nike/cerberus/server/config/guice/MetricsGuiceModule.java new file mode 100644 index 000000000..302008602 --- /dev/null +++ b/src/main/java/com/nike/cerberus/server/config/guice/MetricsGuiceModule.java @@ -0,0 +1,204 @@ +/* + * 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.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.nike.cerberus.config.MetricsConfigurationHelper; +import com.nike.riposte.metrics.codahale.CodahaleMetricsCollector; +import com.nike.riposte.metrics.codahale.CodahaleMetricsEngine; +import com.nike.riposte.metrics.codahale.CodahaleMetricsListener; +import com.nike.riposte.metrics.codahale.ReporterFactory; +import com.nike.riposte.metrics.codahale.SignalFxAwareCodahaleMetricsCollector; +import com.nike.riposte.metrics.codahale.contrib.DefaultGraphiteReporterFactory; +import com.nike.riposte.metrics.codahale.contrib.DefaultJMXReporterFactory; +import com.nike.riposte.metrics.codahale.contrib.DefaultSLF4jReporterFactory; +import com.nike.riposte.metrics.codahale.contrib.SignalFxReporterFactory; +import com.nike.riposte.server.config.AppInfo; +import com.signalfx.codahale.metrics.MetricBuilder; +import com.signalfx.codahale.reporter.MetricMetadataImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import javax.inject.Named; +import javax.inject.Singleton; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import static com.nike.cerberus.config.MetricsConfigurationHelper.DEFAULT_METRIC_DETAILS_TO_SEND_TO_SIGNALFX; + +/** + * Guice Module to instantiate metrics objects + */ +public class MetricsGuiceModule extends AbstractModule { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + protected void configure() { + } + + /** + * Note: The CodahaleMetricsEngine is not required in this provider, but is passed here to ensure the engine + * is created and started + */ + @Provides + @Singleton + public CodahaleMetricsListener metricsListener(@Nullable CodahaleMetricsCollector metricsCollector, + @Nullable CodahaleMetricsEngine engine, + @Nullable SignalFxReporterFactory signalFxReporterFactory, + MetricsConfigurationHelper metricsConfigurationHelper) { + if (metricsCollector == null) + return null; + + if (signalFxReporterFactory == null) { + // No SignalFx - just use the default. + return new CodahaleMetricsListener(metricsCollector); + } + else { + // SignalFx is being used - make sure we use a CodahaleMetricsListener that sets everything up + // properly for SignalFx. + return metricsConfigurationHelper.generateCodahaleMetricsListenerWithSignalFxSupport( + signalFxReporterFactory, metricsCollector, null, null + ); + } + } + + @Provides + @Singleton + public CodahaleMetricsEngine codahaleMetricsEngine(@Nullable CodahaleMetricsCollector cmc, + @Nullable List reporters, + @Named("metrics.reportJvmMetrics") boolean reportJvmMetrics) { + if (cmc == null) + return null; + + if (reporters == null) + reporters = Collections.emptyList(); + + CodahaleMetricsEngine engine = new CodahaleMetricsEngine(cmc, reporters); + if (reportJvmMetrics) + engine.reportJvmMetrics(); + engine.start(); + return engine; + } + + @Provides + @Singleton + public CodahaleMetricsCollector codahaleMetricsCollector( + @Nullable List reporters, @Nullable SignalFxReporterFactory signalFxReporterFactory + ) { + if (reporters == null) { + return null; + } + + // Try to return a SignalFx-based collector if possible. + if (signalFxReporterFactory != null) { + return new SignalFxAwareCodahaleMetricsCollector(signalFxReporterFactory); + } + + // No SignalFx support available - just use a default collector. + return new CodahaleMetricsCollector(); + } + + @Provides + @Singleton + public SignalFxAwareCodahaleMetricsCollector sfxAwareCodahaleMetricsCollector( + @Nullable CodahaleMetricsCollector configuredAppCollector + ) { + // If metrics are completely disabled then return null. + if (configuredAppCollector == null) + return null; + + return configuredAppCollector instanceof SignalFxAwareCodahaleMetricsCollector + ? (SignalFxAwareCodahaleMetricsCollector)configuredAppCollector + // Metrics are enabled, but SignalFx is not. Create a dummy SignalFxAwareCodahaleMetricsCollector + // using the real MetricRegistry for the app, a dummy MetricMetadata, and default metric builders. + // This will let users use the API even if SignalFx is disabled (i.e. local environment), and + // different metric objects will be used based on the dimensions. + : new SignalFxAwareCodahaleMetricsCollector( + configuredAppCollector.getMetricRegistry(), new MetricMetadataImpl(), + MetricBuilder.TIMERS, MetricBuilder.HISTOGRAMS + ); + } + + @Provides + @Singleton + public List metricsReporters( + @Named("metrics.slf4j.reporting.enabled") boolean slf4jReportingEnabled, + @Named("metrics.jmx.reporting.enabled") boolean jmxReportingEnabled, + @Named("metrics.signalfx.reporting.enabled") boolean signalFxEnabled, + @Named("graphite.url") String graphiteUrl, + @Named("graphite.port") int graphitePort, + @Named("graphite.reporting.enabled") boolean graphiteEnabled, + @Named("appInfoFuture") CompletableFuture appInfoFuture, + @Nullable SignalFxReporterFactory signalFxReporterFactory + ) { + List reporters = new ArrayList<>(); + + if (slf4jReportingEnabled) + reporters.add(new DefaultSLF4jReporterFactory()); + + if (jmxReportingEnabled) + reporters.add(new DefaultJMXReporterFactory()); + + if (signalFxEnabled && signalFxReporterFactory != null) + reporters.add(signalFxReporterFactory); + + if (graphiteEnabled) { + AppInfo appInfo = appInfoFuture.join(); + String graphitePrefix = appInfo.appId() + "." + appInfo.dataCenter() + "." + appInfo.environment() + + "." + appInfo.instanceId(); + reporters.add(new DefaultGraphiteReporterFactory(graphitePrefix, graphiteUrl, graphitePort)); + } + + if (reporters.isEmpty()) { + logger.info("No metrics reporters enabled - disabling metrics entirely."); + return null; + } + + String metricReporterTypes = reporters.stream() + .map(rf -> rf.getClass().getSimpleName()) + .collect(Collectors.joining(",", "[", "]")); + logger.info("Metrics reporters enabled. metric_reporter_types={}", metricReporterTypes); + + return reporters; + } + + @Provides + @Singleton + public SignalFxReporterFactory signalFxReporterFactory( + @Named("metrics.signalfx.reporting.enabled") boolean signalFxEnabled, + @Named("service.version") String serviceVersion, + @Named("cms.app.name") String appName, + MetricsConfigurationHelper metricsConfigurationHelper + ) { + if (signalFxEnabled) { + return metricsConfigurationHelper.generateSignalFxReporterFactory( + serviceVersion, + appName, + null, + DEFAULT_METRIC_DETAILS_TO_SEND_TO_SIGNALFX); + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/resources/cms-local.conf b/src/main/resources/cms-local.conf index f655c80c0..eec230258 100644 --- a/src/main/resources/cms-local.conf +++ b/src/main/resources/cms-local.conf @@ -10,4 +10,6 @@ cms.env.load.disable=true mybatis.environment.id=local +metrics.signalfx.dimension.app_env=local + include "cms-local-overrides.conf" \ No newline at end of file diff --git a/src/main/resources/cms.conf b/src/main/resources/cms.conf index 9730991a3..f1d86223a 100644 --- a/src/main/resources/cms.conf +++ b/src/main/resources/cms.conf @@ -45,6 +45,12 @@ metrics.slf4j.reporting.enabled=false # Whether or not to report metrics periodically to JMX. metrics.jmx.reporting.enabled=true +# Whether or not to report metrics periodically to SignalFx. +metrics.signalfx.reporting.enabled=false + +# Whether or not to report jvm metrics +metrics.reportJvmMetrics=true + # Whether or not embedded cassandra should start up. If set to disable cassandra then the cassandra endpoint will not function. Disabling cassandra is necessary everywhere # except local box testing since it is difficult to get working in an AWS/AMI environment and is not worth the time to figure out for this example project. disableCassandra=true @@ -74,3 +80,6 @@ c3p0.preferredTestQuery=SELECT 1 # Vault Admin client vault.maxRequestsPerHost=200 + +# Application name +cms.app.name=cms