/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 gobblin.metrics.reporter;
import java.io.IOException;
import java.util.Map;
import java.util.Properties;
import java.util.SortedMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.joda.time.Period;
import org.joda.time.format.PeriodFormatter;
import org.joda.time.format.PeriodFormatterBuilder;
import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricFilter;
import com.codahale.metrics.Timer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigValueFactory;
import lombok.extern.slf4j.Slf4j;
import gobblin.configuration.ConfigurationKeys;
import gobblin.metrics.InnerMetricContext;
import gobblin.metrics.context.ReportableContext;
import gobblin.metrics.metric.filter.MetricFilters;
import gobblin.metrics.metric.filter.MetricNameRegexFilter;
import gobblin.metrics.metric.filter.MetricTypeFilter;
import gobblin.util.ExecutorsUtils;
/**
* A {@link ContextAwareReporter} that reports on a schedule.
*/
@Slf4j
public abstract class ScheduledReporter extends ContextAwareReporter {
/**
* Interval at which metrics are reported. Format: hours, minutes, seconds. Examples: 1h, 1m, 10s, 1h30m, 2m30s, ...
*/
public static final String REPORTING_INTERVAL =
ConfigurationKeys.METRICS_CONFIGURATIONS_PREFIX + "reporting.interval";
public static final String DEFAULT_REPORTING_INTERVAL_PERIOD = "1M";
public static final PeriodFormatter PERIOD_FORMATTER = new PeriodFormatterBuilder().
appendHours().appendSuffix("H").
appendMinutes().appendSuffix("M").
appendSeconds().appendSuffix("S").toFormatter();
private static final String METRIC_FILTER_NAME_REGEX = "metric.filter.name.regex";
private static final String METRIC_FILTER_TYPE_LIST = "metric.filter.type.list";
@VisibleForTesting
static int parsePeriodToSeconds(String periodStr) {
try {
return Period.parse(periodStr.toUpperCase(), PERIOD_FORMATTER).toStandardSeconds().getSeconds();
} catch(ArithmeticException ae) {
throw new RuntimeException(String.format("Reporting interval is too long. Max: %d seconds.", Integer.MAX_VALUE));
}
}
public static void setReportingInterval(Properties props, long reportingInterval, TimeUnit reportingIntervalUnit) {
long seconds = TimeUnit.SECONDS.convert(reportingInterval, reportingIntervalUnit);
if (seconds > Integer.MAX_VALUE) {
throw new RuntimeException(String.format("Reporting interval is too long. Max: %d seconds.", Integer.MAX_VALUE));
}
props.setProperty(REPORTING_INTERVAL, Long.toString(seconds) + "S");
}
public static Config setReportingInterval(Config config, long reportingInterval, TimeUnit reportingIntervalUnit) {
long seconds = TimeUnit.SECONDS.convert(reportingInterval, reportingIntervalUnit);
if (seconds > Integer.MAX_VALUE) {
throw new RuntimeException(String.format("Reporting interval is too long. Max: %d seconds.", Integer.MAX_VALUE));
}
return config.withValue(REPORTING_INTERVAL, ConfigValueFactory.fromAnyRef(seconds + "S"));
}
private ScheduledExecutorService executor;
private MetricFilter metricFilter;
private Optional<ScheduledFuture> scheduledTask;
private int reportingPeriodSeconds;
public ScheduledReporter(String name, Config config) {
super(name, config);
ensureMetricFilterIsInitialized(config);
}
private synchronized void ensureMetricFilterIsInitialized(Config config) {
if (this.metricFilter == null) {
this.metricFilter = createMetricFilter(config);
}
}
private MetricFilter createMetricFilter(Config config) {
if (config.hasPath(METRIC_FILTER_NAME_REGEX) && config.hasPath(METRIC_FILTER_TYPE_LIST)) {
return MetricFilters.and(new MetricNameRegexFilter(config.getString(METRIC_FILTER_NAME_REGEX)),
new MetricTypeFilter(config.getString(METRIC_FILTER_TYPE_LIST)));
}
if (config.hasPath(METRIC_FILTER_NAME_REGEX)) {
return new MetricNameRegexFilter(config.getString(METRIC_FILTER_NAME_REGEX));
}
if (config.hasPath(METRIC_FILTER_TYPE_LIST)) {
return new MetricTypeFilter(config.getString(METRIC_FILTER_TYPE_LIST));
}
return MetricFilter.ALL;
}
@Override
public void startImpl() {
this.executor = Executors.newSingleThreadScheduledExecutor(
ExecutorsUtils.newDaemonThreadFactory(Optional.of(log), Optional.of("metrics-" + name + "-scheduler")));
this.reportingPeriodSeconds = parsePeriodToSeconds(
config.hasPath(REPORTING_INTERVAL) ? config.getString(REPORTING_INTERVAL) : DEFAULT_REPORTING_INTERVAL_PERIOD);
ensureMetricFilterIsInitialized(config);
this.scheduledTask = Optional.<ScheduledFuture>of(this.executor.scheduleAtFixedRate(new Runnable() {
@Override public void run() {
report();
}
}, 0, this.reportingPeriodSeconds, TimeUnit.SECONDS));
}
@Override
public void stopImpl() {
this.scheduledTask.get().cancel(false);
this.scheduledTask = Optional.absent();
ExecutorsUtils.shutdownExecutorService(this.executor, Optional.of(log), 10, TimeUnit.SECONDS);
// Report metrics one last time - this ensures any metrics values updated between intervals are reported
report(true);
}
@Override
public void close() throws IOException {
super.close();
}
@Override
protected void removedMetricContext(InnerMetricContext context) {
if (shouldReportInnerMetricContext(context)) {
report(context, true);
}
super.removedMetricContext(context);
}
/**
* Trigger emission of a report.
*/
public void report() {
report(false);
}
/***
* @param isFinal true if this is the final time report will be called for this reporter, false otherwise
* @see #report()
*/
protected void report(boolean isFinal) {
for (ReportableContext metricContext : getMetricContextsToReport()) {
report(metricContext, isFinal);
}
}
/**
* Report as {@link InnerMetricContext}.
*
* <p>
* This method is marked as final because it is not directly invoked from the framework, so this method should not
* be overloaded. Overload {@link #report(ReportableContext, boolean)} instead.
* </p>
*
* @param context {@link InnerMetricContext} to report.
* @see #report(ReportableContext, boolean)
*/
protected final void report(ReportableContext context) {
report(context, false);
}
/**
* @param context {@link InnerMetricContext} to report.
* @param isFinal true if this is the final time report will be called for the given context, false otherwise
* @see #report(ReportableContext)
*/
protected void report(ReportableContext context, boolean isFinal) {
report(context.getGauges(this.metricFilter), context.getCounters(this.metricFilter),
context.getHistograms(this.metricFilter), context.getMeters(this.metricFilter),
context.getTimers(this.metricFilter), context.getTagMap(), isFinal);
}
/**
* Report the input metrics. The input tags apply to all input metrics.
*
* <p>
* The default implementation of this method is to ignore the value of isFinal. Sub-classes that are interested in
* using the value of isFinal should override this method as well as
* {@link #report(SortedMap, SortedMap, SortedMap, SortedMap, SortedMap, Map)}. If they are not interested in the
* value of isFinal, they should just override
* {@link #report(SortedMap, SortedMap, SortedMap, SortedMap, SortedMap, Map)}.
* </p>
*
* @param isFinal true if this is the final time report will be called, false otherwise
*/
protected void report(SortedMap<String, Gauge> gauges, SortedMap<String, Counter> counters,
SortedMap<String, Histogram> histograms, SortedMap<String, Meter> meters, SortedMap<String, Timer> timers,
Map<String, Object> tags, boolean isFinal) {
report(gauges, counters, histograms, meters, timers, tags);
}
/**
* @see #report(SortedMap, SortedMap, SortedMap, SortedMap, SortedMap, Map)
*/
protected abstract void report(SortedMap<String, Gauge> gauges, SortedMap<String, Counter> counters,
SortedMap<String, Histogram> histograms, SortedMap<String, Meter> meters, SortedMap<String, Timer> timers,
Map<String, Object> tags);
}