
gobblin.metrics.MetricContext Maven / Gradle / Ivy
Show all versions of gobblin-metrics Show documentation
/*
* Copyright (C) 2014-2016 LinkedIn Corp. All rights reserved.
*
* 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.
*/
package gobblin.metrics;
import lombok.Getter;
import java.io.Closeable;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.codahale.metrics.Counter;
import com.codahale.metrics.MetricFilter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Metric;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.MetricSet;
import com.codahale.metrics.Timer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Closer;
import com.google.common.util.concurrent.MoreExecutors;
import gobblin.metrics.context.NameConflictException;
import gobblin.metrics.context.ReportableContext;
import gobblin.metrics.notification.EventNotification;
import gobblin.metrics.notification.Notification;
import gobblin.util.ExecutorsUtils;
/**
* This class models a {@link MetricSet} that optionally has a list of {@link Tag}s
* and a set of {@link com.codahale.metrics.ScheduledReporter}s associated with it. The
* {@link Tag}s associated with a {@link MetricContext} are used to construct the
* common metric name prefix of registered {@link com.codahale.metrics.Metric}s.
*
*
* {@link MetricContext}s can form a hierarchy and any {@link MetricContext} can create
* children {@link MetricContext}s. A child {@link MetricContext} inherit all the
* {@link Tag}s associated with its parent, in additional to the {@link Tag}s
* of itself. {@link Tag}s inherited from its parent will appear in front of those
* of itself when constructing the metric name prefix.
*
*
* @author Yinan Li
*/
public class MetricContext extends MetricRegistry implements ReportableContext, Closeable {
protected final Closer closer;
public static final String METRIC_CONTEXT_ID_TAG_NAME = "metricContextID";
public static final String METRIC_CONTEXT_NAME_TAG_NAME = "metricContextName";
@Getter
private final InnerMetricContext innerMetricContext;
private static final Logger LOG = LoggerFactory.getLogger(MetricContext.class);
public static final String GOBBLIN_METRICS_NOTIFICATIONS_TIMER_NAME = "gobblin.metrics.notifications.timer";
// Targets for notifications.
private final Map> notificationTargets;
private final ContextAwareTimer notificationTimer;
private Optional executorServiceOptional;
// This set exists so that metrics that have no hard references in code don't get GCed while the MetricContext
// is alive.
private final Set contextAwareMetricsSet;
protected MetricContext(String name, MetricContext parent, List> tags, boolean isRoot) throws NameConflictException {
Preconditions.checkArgument(!Strings.isNullOrEmpty(name));
this.closer = Closer.create();
try {
this.innerMetricContext = this.closer.register(new InnerMetricContext(this, name, parent, tags));
} catch(ExecutionException ee) {
throw Throwables.propagate(ee);
}
this.contextAwareMetricsSet = Sets.newConcurrentHashSet();
this.notificationTargets = Maps.newConcurrentMap();
this.executorServiceOptional = Optional.absent();
this.notificationTimer = new ContextAwareTimer(this, GOBBLIN_METRICS_NOTIFICATIONS_TIMER_NAME);
register(this.notificationTimer);
if (!isRoot) {
RootMetricContext.get().addMetricContext(this);
}
}
private synchronized ExecutorService getExecutorService() {
if(!this.executorServiceOptional.isPresent()) {
this.executorServiceOptional = Optional.of(MoreExecutors.getExitingExecutorService(
(ThreadPoolExecutor) Executors.newCachedThreadPool(ExecutorsUtils.newThreadFactory(Optional.of(LOG),
Optional.of("MetricContext-" + getName() + "-%d"))), 5,
TimeUnit.MINUTES));
}
return this.executorServiceOptional.get();
}
/**
* Get the name of this {@link MetricContext}.
*
* @return the name of this {@link MetricContext}
*/
public String getName() {
return this.innerMetricContext.getName();
}
/**
* Get the parent {@link MetricContext} of this {@link MetricContext} wrapped in an
* {@link com.google.common.base.Optional}, which may be absent if it has not parent
* {@link MetricContext}.
*
* @return the parent {@link MetricContext} of this {@link MetricContext} wrapped in an
* {@link com.google.common.base.Optional}
*/
public Optional getParent() {
return this.innerMetricContext.getParent();
}
/**
* Get a view of the child {@link gobblin.metrics.MetricContext}s as a {@link com.google.common.collect.ImmutableMap}.
* @return {@link com.google.common.collect.ImmutableMap} of
* child {@link gobblin.metrics.MetricContext}s keyed by their names.
*/
public Map getChildContextsAsMap() {
return this.innerMetricContext.getChildContextsAsMap();
}
/**
* See {@link com.codahale.metrics.MetricRegistry#getNames()}.
*
*
* This method will return fully-qualified metric names if the {@link MetricContext} is configured
* to report fully-qualified metric names.
*
*/
@Override
public SortedSet getNames() {
return this.innerMetricContext.getNames();
}
/**
* Submit {@link gobblin.metrics.GobblinTrackingEvent} to all notification listeners attached to this or any
* ancestor {@link gobblin.metrics.MetricContext}s. The argument for this method is mutated by the method, so it
* should not be reused by the caller.
*
* @param nonReusableEvent {@link GobblinTrackingEvent} to submit. This object will be mutated by the method,
* so it should not be reused by the caller.
*/
public void submitEvent(GobblinTrackingEvent nonReusableEvent) {
nonReusableEvent.setTimestamp(System.currentTimeMillis());
// Inject metric context tags into event metadata.
Map originalMetadata = nonReusableEvent.getMetadata();
Map tags = getTagMap();
Map newMetadata = Maps.newHashMap();
for(Map.Entry entry : tags.entrySet()) {
newMetadata.put(entry.getKey(), entry.getValue().toString());
}
newMetadata.putAll(originalMetadata);
nonReusableEvent.setMetadata(newMetadata);
EventNotification notification = new EventNotification(nonReusableEvent);
sendNotification(notification);
}
/**
* See {@link com.codahale.metrics.MetricRegistry#getMetrics()}.
*
*
* This method will return fully-qualified metric names if the {@link MetricContext} is configured
* to report fully-qualified metric names.
*
*/
@Override
public Map getMetrics() {
return this.innerMetricContext.getMetrics();
}
/**
* See {@link com.codahale.metrics.MetricRegistry#getGauges()}.
*
*
* This method will return fully-qualified metric names if the {@link MetricContext} is configured
* to report fully-qualified metric names.
*
*/
@Override
public SortedMap getGauges() {
return this.innerMetricContext.getGauges(MetricFilter.ALL);
}
/**
* See {@link com.codahale.metrics.MetricRegistry#getGauges(com.codahale.metrics.MetricFilter)}.
*
*
* This method will return fully-qualified metric names if the {@link MetricContext} is configured
* to report fully-qualified metric names.
*
*/
@Override
public SortedMap getGauges(MetricFilter filter) {
return this.innerMetricContext.getGauges(filter);
}
/**
* See {@link com.codahale.metrics.MetricRegistry#getCounters()}.
*
*
* This method will return fully-qualified metric names if the {@link MetricContext} is configured
* to report fully-qualified metric names.
*
*/
@Override
public SortedMap getCounters() {
return this.innerMetricContext.getCounters(MetricFilter.ALL);
}
/**
* See {@link com.codahale.metrics.MetricRegistry#getCounters(com.codahale.metrics.MetricFilter)}.
*
*
* This method will return fully-qualified metric names if the {@link MetricContext} is configured
* to report fully-qualified metric names.
*
*/
@Override
public SortedMap getCounters(MetricFilter filter) {
return this.innerMetricContext.getCounters(filter);
}
/**
* See {@link com.codahale.metrics.MetricRegistry#getHistograms()}.
*
*
* This method will return fully-qualified metric names if the {@link MetricContext} is configured
* to report fully-qualified metric names.
*
*/
@Override
public SortedMap getHistograms() {
return this.innerMetricContext.getHistograms(MetricFilter.ALL);
}
/**
* See {@link com.codahale.metrics.MetricRegistry#getHistograms(com.codahale.metrics.MetricFilter)}.
*
*
* This method will return fully-qualified metric names if the {@link MetricContext} is configured
* to report fully-qualified metric names.
*
*/
@Override
public SortedMap getHistograms(MetricFilter filter) {
return this.innerMetricContext.getHistograms(filter);
}
/**
* See {@link com.codahale.metrics.MetricRegistry#getMeters()}.
*
*
* This method will return fully-qualified metric names if the {@link MetricContext} is configured
* to report fully-qualified metric names.
*
*/
@Override
public SortedMap getMeters() {
return this.innerMetricContext.getMeters(MetricFilter.ALL);
}
/**
* See {@link com.codahale.metrics.MetricRegistry#getMeters(com.codahale.metrics.MetricFilter)}.
*
*
* This method will return fully-qualified metric names if the {@link MetricContext} is configured
* to report fully-qualified metric names.
*
*/
@Override
public SortedMap getMeters(MetricFilter filter) {
return this.innerMetricContext.getMeters(filter);
}
/**
* See {@link com.codahale.metrics.MetricRegistry#getTimers()}.
*
*
* This method will return fully-qualified metric names if the {@link MetricContext} is configured
* to report fully-qualified metric names.
*
*/
@Override
public SortedMap getTimers() {
return this.innerMetricContext.getTimers(MetricFilter.ALL);
}
/**
* See {@link com.codahale.metrics.MetricRegistry#getTimers(com.codahale.metrics.MetricFilter)}.
*
*
* This method will return fully-qualified metric names if the {@link MetricContext} is configured
* to report fully-qualified metric names.
*
*/
@Override
public SortedMap getTimers(MetricFilter filter) {
return this.innerMetricContext.getTimers(filter);
}
/**
* This is equivalent to {@link #contextAwareCounter(String)}.
*/
@Override
public Counter counter(String name) {
return contextAwareCounter(name);
}
/**
* This is equivalent to {@link #contextAwareMeter(String)}.
*/
@Override
public Meter meter(String name) {
return contextAwareMeter(name);
}
/**
* This is equivalent to {@link #contextAwareHistogram(String)}.
*/
@Override
public Histogram histogram(String name) {
return contextAwareHistogram(name);
}
/**
* This is equivalent to {@link #contextAwareTimer(String)}.
*/
@Override
public Timer timer(String name) {
return contextAwareTimer(name);
}
/**
* Register a given metric under a given name.
*
*
* This method does not support registering {@link com.codahale.metrics.MetricSet}s.
* See{@link #registerAll(com.codahale.metrics.MetricSet)}.
*
*
*
* This method will not register a metric with the same name in the parent context (if it exists).
*
*/
@Override
public synchronized T register(String name, T metric)
throws IllegalArgumentException {
if(!(metric instanceof ContextAwareMetric)) {
throw new UnsupportedOperationException("Can only register ContextAwareMetrics.");
}
return this.innerMetricContext.register(name, metric);
}
/**
* Register a {@link gobblin.metrics.ContextAwareMetric} under its own name.
*/
public T register(T metric) throws IllegalArgumentException {
return register(metric.getName(), metric);
}
/**
* Get a {@link ContextAwareCounter} with a given name.
*
* @param name name of the {@link ContextAwareCounter}
* @return the {@link ContextAwareCounter} with the given name
*/
public ContextAwareCounter contextAwareCounter(String name) {
return contextAwareCounter(name, ContextAwareMetricFactory.DEFAULT_CONTEXT_AWARE_COUNTER_FACTORY);
}
/**
* Get a {@link ContextAwareCounter} with a given name.
*
* @param name name of the {@link ContextAwareCounter}
* @param factory a {@link ContextAwareMetricFactory} for building {@link ContextAwareCounter}s
* @return the {@link ContextAwareCounter} with the given name
*/
public ContextAwareCounter contextAwareCounter(String name, ContextAwareMetricFactory factory) {
return this.innerMetricContext.getOrCreate(name, factory);
}
/**
* Get a {@link ContextAwareMeter} with a given name.
*
* @param name name of the {@link ContextAwareMeter}
* @return the {@link ContextAwareMeter} with the given name
*/
public ContextAwareMeter contextAwareMeter(String name) {
return contextAwareMeter(name, ContextAwareMetricFactory.DEFAULT_CONTEXT_AWARE_METER_FACTORY);
}
/**
* Get a {@link ContextAwareMeter} with a given name.
*
* @param name name of the {@link ContextAwareMeter}
* @param factory a {@link ContextAwareMetricFactory} for building {@link ContextAwareMeter}s
* @return the {@link ContextAwareMeter} with the given name
*/
public ContextAwareMeter contextAwareMeter(String name, ContextAwareMetricFactory factory) {
return this.innerMetricContext.getOrCreate(name, factory);
}
/**
* Get a {@link ContextAwareHistogram} with a given name.
*
* @param name name of the {@link ContextAwareHistogram}
* @return the {@link ContextAwareHistogram} with the given name
*/
public ContextAwareHistogram contextAwareHistogram(String name) {
return contextAwareHistogram(name, ContextAwareMetricFactory.DEFAULT_CONTEXT_AWARE_HISTOGRAM_FACTORY);
}
/**
* Get a {@link ContextAwareHistogram} with a given name.
*
* @param name name of the {@link ContextAwareHistogram}
* @param factory a {@link ContextAwareMetricFactory} for building {@link ContextAwareHistogram}s
* @return the {@link ContextAwareHistogram} with the given name
*/
public ContextAwareHistogram contextAwareHistogram(String name,
ContextAwareMetricFactory factory) {
return this.innerMetricContext.getOrCreate(name, factory);
}
/**
* Get a {@link ContextAwareTimer} with a given name.
*
* @param name name of the {@link ContextAwareTimer}
* @return the {@link ContextAwareTimer} with the given name
*/
public ContextAwareTimer contextAwareTimer(String name) {
return contextAwareTimer(name, ContextAwareMetricFactory.DEFAULT_CONTEXT_AWARE_TIMER_FACTORY);
}
/**
* Get a {@link ContextAwareTimer} with a given name.
*
* @param name name of the {@link ContextAwareTimer}
* @param factory a {@link ContextAwareMetricFactory} for building {@link ContextAwareTimer}s
* @return the {@link ContextAwareTimer} with the given name
*/
public ContextAwareTimer contextAwareTimer(String name, ContextAwareMetricFactory factory) {
return this.innerMetricContext.getOrCreate(name, factory);
}
/**
* Create a new {@link ContextAwareGauge} wrapping a given {@link com.codahale.metrics.Gauge}.
*
* @param name name of the {@link ContextAwareGauge}
* @param gauge the {@link com.codahale.metrics.Gauge} to be wrapped by the {@link ContextAwareGauge}
* @param the type of the {@link ContextAwareGauge}'s value
* @return a new {@link ContextAwareGauge}
*/
public ContextAwareGauge newContextAwareGauge(String name, Gauge gauge) {
return new ContextAwareGauge(this, name, gauge);
}
/**
* Remove a metric with a given name.
*
*
* This method will remove the metric with the given name from this {@link MetricContext}
* as well as metrics with the same name from every child {@link MetricContext}s.
*
*
* @param name name of the metric to be removed
* @return whether or not the metric has been removed
*/
@Override
public synchronized boolean remove(String name) {
return this.innerMetricContext.remove(name);
}
@Override
public void removeMatching(MetricFilter filter) {
this.innerMetricContext.removeMatching(filter);
}
public List> getTags() {
return this.innerMetricContext.getTags();
}
public Map getTagMap() {
return this.innerMetricContext.getTagMap();
}
@Override
public void close() throws IOException {
this.closer.close();
}
/**
* Get a new {@link MetricContext.Builder} for building child {@link MetricContext}s.
*
* @param name name of the child {@link MetricContext} to be built
* @return a new {@link MetricContext.Builder} for building child {@link MetricContext}s
*/
public Builder childBuilder(String name) {
return builder(name).hasParent(this);
}
/**
* Get a new {@link MetricContext.Builder}.
*
* @param name name of the {@link MetricContext} to be built
* @return a new {@link MetricContext.Builder}
*/
public static Builder builder(String name) {
return new Builder(name);
}
/**
* Add a target for {@link gobblin.metrics.notification.Notification}s.
* @param target A {@link com.google.common.base.Function} that will be run every time
* there is a new {@link gobblin.metrics.notification.Notification} in this context.
* @return a key for this notification target. Can be used to remove the notification target later.
*/
public UUID addNotificationTarget(Function target) {
UUID uuid = UUID.randomUUID();
if(this.notificationTargets.containsKey(uuid)) {
throw new RuntimeException("Failed to create notification target.");
}
this.notificationTargets.put(uuid, target);
return uuid;
}
/**
* Remove notification target identified by the given key.
* @param key key for the notification target to remove.
*/
public void removeNotificationTarget(UUID key) {
this.notificationTargets.remove(key);
}
/**
* Send a notification to all targets of this context and to the parent of this context.
* @param notification {@link gobblin.metrics.notification.Notification} to send.
*/
public void sendNotification(final Notification notification) {
ContextAwareTimer.Context timer = this.notificationTimer.time();
if(!this.notificationTargets.isEmpty()) {
for (final Map.Entry> entry : this.notificationTargets.entrySet()) {
try {
entry.getValue().apply(notification);
} catch (RuntimeException exception) {
LOG.warn("RuntimeException when running notification target. Skipping.", exception);
}
}
}
if(getParent().isPresent()) {
getParent().get().sendNotification(notification);
}
timer.stop();
}
void addChildContext(String childContextName, MetricContext childContext) throws NameConflictException,
ExecutionException {
this.innerMetricContext.addChildContext(childContextName, childContext);
}
void addToMetrics(ContextAwareMetric metric) {
this.contextAwareMetricsSet.add(metric);
}
void removeFromMetrics(ContextAwareMetric metric) {
this.contextAwareMetricsSet.remove(metric);
}
@VisibleForTesting
void clearNotificationTargets() {
this.notificationTargets.clear();
}
/**
* A builder class for {@link MetricContext}.
*/
public static class Builder {
private String name;
private MetricContext parent = null;
private final List> tags = Lists.newArrayList();
public Builder(String name) {
this.name = name;
}
/**
* Set the parent {@link MetricContext} of this {@link MetricContext} instance.
*
*
* This method is intentionally made private and is only called in {@link MetricContext#childBuilder(String)}
* so users will not mistakenly call this method twice if they use {@link MetricContext#childBuilder(String)}.
*
* @param parent the parent {@link MetricContext}
* @return {@code this}
*/
private Builder hasParent(MetricContext parent) {
this.parent = parent;
// Inherit parent context's tags
this.tags.addAll(parent.getTags());
return this;
}
/**
* Add a single {@link Tag}.
*
* @param tag the {@link Tag} to add
* @return {@code this}
*/
public Builder addTag(Tag> tag) {
this.tags.add(tag);
return this;
}
/**
* Add a collection of {@link Tag}s.
*
* @param tags the collection of {@link Tag}s to add
* @return {@code this}
*/
public Builder addTags(Collection> tags) {
this.tags.addAll(tags);
return this;
}
/**
* Builder a new {@link MetricContext}.
*
*
* See {@link Taggable#metricNamePrefix(boolean)} for the semantic of {@code includeTagKeys}.
*
*
*
* Note this builder may change the name of the built {@link MetricContext} if the parent context already has a child with
* that name. If this is unacceptable, use {@link #buildStrict} instead.
*
*
* @return the newly built {@link MetricContext}
*/
public MetricContext build() {
try {
return buildStrict();
} catch (NameConflictException nce) {
String uuid = UUID.randomUUID().toString();
LOG.warn("MetricContext with specified name already exists, appending UUID to the given name: " + uuid);
this.name = this.name + "_" + uuid;
try {
return buildStrict();
} catch (NameConflictException nce2) {
throw Throwables.propagate(nce2);
}
}
}
/**
* Builder a new {@link MetricContext}.
*
*
* See {@link Taggable#metricNamePrefix(boolean)} for the semantic of {@code includeTagKeys}.
*
*
* @return the newly built {@link MetricContext}
* @throws NameConflictException if the parent {@link MetricContext} already has a child with this name.
*/
public MetricContext buildStrict() throws NameConflictException {
if(this.parent == null) {
this.parent = RootMetricContext.get();
}
return new MetricContext(this.name, this.parent, this.tags, false);
}
}
}