/* * 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; 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. * * <p> * {@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. * </p> * * @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<UUID, Function<Notification, Void>> notificationTargets; private final ContextAwareTimer notificationTimer; private Optional<ExecutorService> 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<ContextAwareMetric> contextAwareMetricsSet; protected MetricContext(String name, MetricContext parent, List<Tag<?>> 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<MetricContext> 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<String, MetricContext> getChildContextsAsMap() { return this.innerMetricContext.getChildContextsAsMap(); } /** * See {@link com.codahale.metrics.MetricRegistry#getNames()}. * * <p> * This method will return fully-qualified metric names if the {@link MetricContext} is configured * to report fully-qualified metric names. * </p> */ @Override public SortedSet<String> 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<String, String> originalMetadata = nonReusableEvent.getMetadata(); Map<String, Object> tags = getTagMap(); Map<String, String> newMetadata = Maps.newHashMap(); for(Map.Entry<String, Object> 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()}. * * <p> * This method will return fully-qualified metric names if the {@link MetricContext} is configured * to report fully-qualified metric names. * </p> */ @Override public Map<String, Metric> getMetrics() { return this.innerMetricContext.getMetrics(); } /** * See {@link com.codahale.metrics.MetricRegistry#getGauges()}. * * <p> * This method will return fully-qualified metric names if the {@link MetricContext} is configured * to report fully-qualified metric names. * </p> */ @Override public SortedMap<String, Gauge> getGauges() { return this.innerMetricContext.getGauges(MetricFilter.ALL); } /** * See {@link com.codahale.metrics.MetricRegistry#getGauges(com.codahale.metrics.MetricFilter)}. * * <p> * This method will return fully-qualified metric names if the {@link MetricContext} is configured * to report fully-qualified metric names. * </p> */ @Override public SortedMap<String, Gauge> getGauges(MetricFilter filter) { return this.innerMetricContext.getGauges(filter); } /** * See {@link com.codahale.metrics.MetricRegistry#getCounters()}. * * <p> * This method will return fully-qualified metric names if the {@link MetricContext} is configured * to report fully-qualified metric names. * </p> */ @Override public SortedMap<String, Counter> getCounters() { return this.innerMetricContext.getCounters(MetricFilter.ALL); } /** * See {@link com.codahale.metrics.MetricRegistry#getCounters(com.codahale.metrics.MetricFilter)}. * * <p> * This method will return fully-qualified metric names if the {@link MetricContext} is configured * to report fully-qualified metric names. * </p> */ @Override public SortedMap<String, Counter> getCounters(MetricFilter filter) { return this.innerMetricContext.getCounters(filter); } /** * See {@link com.codahale.metrics.MetricRegistry#getHistograms()}. * * <p> * This method will return fully-qualified metric names if the {@link MetricContext} is configured * to report fully-qualified metric names. * </p> */ @Override public SortedMap<String, Histogram> getHistograms() { return this.innerMetricContext.getHistograms(MetricFilter.ALL); } /** * See {@link com.codahale.metrics.MetricRegistry#getHistograms(com.codahale.metrics.MetricFilter)}. * * <p> * This method will return fully-qualified metric names if the {@link MetricContext} is configured * to report fully-qualified metric names. * </p> */ @Override public SortedMap<String, Histogram> getHistograms(MetricFilter filter) { return this.innerMetricContext.getHistograms(filter); } /** * See {@link com.codahale.metrics.MetricRegistry#getMeters()}. * * <p> * This method will return fully-qualified metric names if the {@link MetricContext} is configured * to report fully-qualified metric names. * </p> */ @Override public SortedMap<String, Meter> getMeters() { return this.innerMetricContext.getMeters(MetricFilter.ALL); } /** * See {@link com.codahale.metrics.MetricRegistry#getMeters(com.codahale.metrics.MetricFilter)}. * * <p> * This method will return fully-qualified metric names if the {@link MetricContext} is configured * to report fully-qualified metric names. * </p> */ @Override public SortedMap<String, Meter> getMeters(MetricFilter filter) { return this.innerMetricContext.getMeters(filter); } /** * See {@link com.codahale.metrics.MetricRegistry#getTimers()}. * * <p> * This method will return fully-qualified metric names if the {@link MetricContext} is configured * to report fully-qualified metric names. * </p> */ @Override public SortedMap<String, Timer> getTimers() { return this.innerMetricContext.getTimers(MetricFilter.ALL); } /** * See {@link com.codahale.metrics.MetricRegistry#getTimers(com.codahale.metrics.MetricFilter)}. * * <p> * This method will return fully-qualified metric names if the {@link MetricContext} is configured * to report fully-qualified metric names. * </p> */ @Override public SortedMap<String, Timer> 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 ContextAwareTimer timer(String name) { return contextAwareTimer(name); } /** * Register a given metric under a given name. * * <p> * This method does not support registering {@link com.codahale.metrics.MetricSet}s. * See{@link #registerAll(com.codahale.metrics.MetricSet)}. * </p> * * <p> * This method will not register a metric with the same name in the parent context (if it exists). * </p> */ @Override public synchronized <T extends Metric> 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 extends ContextAwareMetric> 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<ContextAwareCounter> 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<ContextAwareMeter> 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<ContextAwareHistogram> 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<ContextAwareTimer> 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 <T> the type of the {@link ContextAwareGauge}'s value * @return a new {@link ContextAwareGauge} */ public <T> ContextAwareGauge<T> newContextAwareGauge(String name, Gauge<T> gauge) { return new ContextAwareGauge<T>(this, name, gauge); } /** * Remove a metric with a given name. * * <p> * 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. * </p> * * @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<Tag<?>> getTags() { return this.innerMetricContext.getTags(); } public Map<String, Object> 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<Notification, Void> 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<UUID, Function<Notification, Void>> 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<Tag<?>> tags = Lists.newArrayList(); public Builder(String name) { this.name = name; } /** * Set the parent {@link MetricContext} of this {@link MetricContext} instance. * * <p> * 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)}. * </p> * @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<Tag<?>> tags) { this.tags.addAll(tags); return this; } /** * Builder a new {@link MetricContext}. * * <p> * See {@link Taggable#metricNamePrefix(boolean)} for the semantic of {@code includeTagKeys}. * </p> * * <p> * 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. * </p> * * @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}. * * <p> * See {@link Taggable#metricNamePrefix(boolean)} for the semantic of {@code includeTagKeys}. * </p> * * @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); } } }