Skip to content
This repository has been archived by the owner on Jan 12, 2024. It is now read-only.

Commit

Permalink
Allow Metrics to be sent to SignalFx (#65)
Browse files Browse the repository at this point in the history
* Add a feature to allow metrics reporting to SignalFx
* Make signalfx props optional
* Report JVM metrics
  • Loading branch information
sdford authored Sep 5, 2017
1 parent 780d675 commit e6dcdb8
Show file tree
Hide file tree
Showing 9 changed files with 446 additions and 72 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
# limitations under the License.
#

version=0.28.0
version=0.29.0
groupId=com.nike.cerberus
artifactId=cms
1 change: 1 addition & 0 deletions gradle/dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion gradle/develop.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
210 changes: 210 additions & 0 deletions src/main/java/com/nike/cerberus/config/MetricsConfigurationHelper.java
Original file line number Diff line number Diff line change
@@ -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<Builder, Builder> customReporterConfigurator,
Set<MetricDetails> metricDetailsToReport
) {
Set<SignalFxReporter.MetricDetails> finalMetricDetailsToReport = (metricDetailsToReport == null)
? DEFAULT_METRIC_DETAILS_TO_SEND_TO_SIGNALFX
: metricDetailsToReport;

String host = EC2MetadataUtils.getLocalHostName();
String ec2Hostname = EC2_HOSTNAME_PREFIX + EC2MetadataUtils.getPrivateIpAddress();

Function<SignalFxReporter.Builder, SignalFxReporter.Builder> 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<Timer> 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<SignalFxReporter.MetricDetails> 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<Timer>
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);
}
}
7 changes: 6 additions & 1 deletion src/main/java/com/nike/cerberus/server/config/CmsConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -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<AppInfo> appInfoFuture,
CodahaleMetricsCollector metricsCollector) {
List<ReporterFactory> 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() {
Expand Down
Loading

0 comments on commit e6dcdb8

Please sign in to comment.