package org.corfudb.util;
import com.codahale.metrics.*;
import com.codahale.metrics.jvm.FileDescriptorRatioGauge;
import com.codahale.metrics.jvm.GarbageCollectorMetricSet;
import com.codahale.metrics.jvm.MemoryUsageGaugeSet;
import com.codahale.metrics.jvm.ThreadStatesGaugeSet;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.FileInputStream;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
@Slf4j
public class MetricsUtils {
private static final FileDescriptorRatioGauge metricsJVMFdGauge = new FileDescriptorRatioGauge();
private static final MetricSet metricsJVMGC = new GarbageCollectorMetricSet();
private static final MetricSet metricsJVMMem = new MemoryUsageGaugeSet();
private static final MetricSet metricsJVMThread = new ThreadStatesGaugeSet();
private static Properties metricsProperties = new Properties();
@Getter
private static boolean metricsCollectionEnabled = true;
private static boolean metricsReportingEnabled = false;
private static String mpTrigger = "filter-trigger"; // internal use only
/**
* Load a metrics properties file.
* <p>
* The expected properties in this properties file are:
* <ul>
* <li> collection-enabled: Boolean for whether metrics collection is enabled.
* <li> reporting-enabled: Boolean for whether CSV output will be generated.
* <li> directory: String for the path to the CSV output subdirectory
* <li> nterval: Long for the reporting interval for CSV output
* </ul>
* <p>
* For each reporting interval, this function will be
* called to re-parse the properties file and to
* re-evaluate the value of 'collection-enabled' and
* 'reporting-enabled'. Changes to any other property
* in this file will be ignored.
*/
private static void loadPropertiesFile() {
String propPath;
if ((propPath = System.getenv("METRICS_PROPERTIES")) != null) {
try (FileInputStream is = new FileInputStream(propPath)) {
metricsProperties.load(is);
metricsCollectionEnabled = Boolean.valueOf((String) metricsProperties.get("collection-enabled"));
metricsReportingEnabled = Boolean.valueOf((String) metricsProperties.get("reporting-enabled"));
} catch (Exception e) {
log.error("Error processing METRICS_PROPERTIES {}: {}", propPath, e.toString());
}
}
}
/**
* Check if the metricsReportingSetup() function has been called
* on 'metrics' before now.
*
* @param metrics
* @return True if metricsReportingSetup() function has been called earlier
*/
public static boolean isMetricsReportingSetUp(MetricRegistry metrics) {
return metrics.getNames().contains(mpTrigger);
}
/**
* Start metrics reporting via the Dropwizard 'CsvReporter' file writer.
* Reporting can be turned on and off via the properties file described
* in loadPropertiesFile()'s docs. The report interval and report
* directory cannot be altered at runtime.
*
* @param metrics
*/
public static void metricsReportingSetup(MetricRegistry metrics) {
metrics.counter(mpTrigger);
loadPropertiesFile();
String outPath = (String) metricsProperties.get("directory");
if (outPath != null && !outPath.isEmpty()) {
Long interval = Long.valueOf((String) metricsProperties.get("interval"));
File statDir = new File(outPath);
statDir.mkdirs();
MetricFilter f = (name, metric) -> {
if (name.equals(mpTrigger)) {
loadPropertiesFile();
return false;
}
return metricsReportingEnabled;
};
CsvReporter reporter1 = CsvReporter.forRegistry(metrics)
.formatFor(Locale.US)
.convertRatesTo(TimeUnit.SECONDS)
.convertDurationsTo(TimeUnit.MILLISECONDS)
.filter(f)
.build(statDir);
reporter1.start(interval, TimeUnit.SECONDS);
}
}
public static void addCacheGauges(MetricRegistry metrics, String name, Cache cache) {
try {
metrics.register(name + "cache-size", (Gauge<Long>) () -> cache.estimatedSize());
metrics.register(name + "evictions", (Gauge<Long>) () -> cache.stats().evictionCount());
metrics.register(name + "hit-rate", (Gauge<Double>) () -> cache.stats().hitRate());
metrics.register(name + "hits", (Gauge<Long>) () -> cache.stats().hitCount());
metrics.register(name + "misses", (Gauge<Long>) () -> cache.stats().missCount());
} catch (IllegalArgumentException e) {
// Re-registering metrics during test runs, not a problem
}
}
public static void addJVMMetrics(MetricRegistry metrics, String pfx) {
try {
metrics.register(pfx + "jvm.gc", metricsJVMGC);
metrics.register(pfx + "jvm.memory", metricsJVMMem);
metrics.register(pfx + "jvm.thread", metricsJVMThread);
metrics.register(pfx + "jvm.file-descriptors-used", metricsJVMFdGauge);
} catch (IllegalArgumentException e) {
// Re-registering metrics during test runs, not a problem
}
}
public static Timer.Context getConditionalContext(Timer t) {
return getConditionalContext(metricsCollectionEnabled, t);
}
public static Timer.Context getConditionalContext(boolean enabled, Timer t) {
return enabled ? t.time() : null;
}
public static void stopConditionalContext(Timer.Context context) {
if (context != null) {
context.stop();
}
}
public static void incConditionalCounter(boolean enabled, Counter counter, long amount) {
if (enabled) {
counter.inc(amount);
}
}
}