/*
* 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.instrumented;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import org.apache.commons.lang3.StringUtils;
import com.codahale.metrics.Meter;
import com.codahale.metrics.Timer;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.io.Closer;
import gobblin.Constructs;
import gobblin.configuration.ConfigurationKeys;
import gobblin.configuration.State;
import gobblin.converter.Converter;
import gobblin.fork.ForkOperator;
import gobblin.metrics.GobblinMetrics;
import gobblin.metrics.GobblinMetricsRegistry;
import gobblin.metrics.MetricContext;
import gobblin.metrics.Tag;
import gobblin.qualitychecker.row.RowLevelPolicy;
import gobblin.source.extractor.Extractor;
import gobblin.util.DecoratorUtils;
import gobblin.writer.DataWriter;
/**
* Provides simple instrumentation for gobblin-core components.
*
* <p>
* Creates {@link gobblin.metrics.MetricContext}. Tries to read the name of the parent context
* from key "metrics.context.name" at state, and tries to get the parent context by name from
* the {@link gobblin.metrics.MetricContext} registry (the parent context must be registered).
* </p>
*
* <p>
* Automatically adds two tags to the inner context:
* <ul>
* <li> component: attempts to determine which component type within gobblin-api generated this instance. </li>
* <li> class: the specific class of the object that generated this instance of Instrumented </li>
* </ul>
* </p>
*
*/
public class Instrumented implements Instrumentable, Closeable {
public static final String METRIC_CONTEXT_NAME_KEY = ConfigurationKeys.METRIC_CONTEXT_NAME_KEY;
public static final Random RAND = new Random();
private final boolean instrumentationEnabled;
protected MetricContext metricContext;
protected final Closer closer;
/**
* Gets metric context with no additional tags.
* See {@link #getMetricContext(State, Class, List)}
*/
public static MetricContext getMetricContext(State state, Class<?> klazz) {
return getMetricContext(state, klazz, new ArrayList<Tag<?>>());
}
/**
* Get a {@link gobblin.metrics.MetricContext} to be used by an object needing instrumentation.
*
* <p>
* This method will read the property "metrics.context.name" from the input State, and will attempt
* to find a MetricContext with that name in the global instance of {@link gobblin.metrics.GobblinMetricsRegistry}.
* If it succeeds, the generated MetricContext will be a child of the retrieved Context, otherwise it will
* be a parent-less context.
* </p>
* <p>
* The method will automatically add two tags to the context:
* <ul>
* <li> construct will contain the name of the {@link gobblin.Constructs} that klazz represents. </li>
* <li> class will contain the canonical name of the input class. </li>
* </ul>
* </p>
*
* @param state {@link gobblin.configuration.State} used to find the parent MetricContext.
* @param klazz Class of the object needing instrumentation.
* @param tags Additional tags to add to the returned context.
* @return A {@link gobblin.metrics.MetricContext} with the appropriate tags and parent.
*/
public static MetricContext getMetricContext(State state, Class<?> klazz, List<Tag<?>> tags) {
int randomId = RAND.nextInt(Integer.MAX_VALUE);
List<Tag<?>> generatedTags = Lists.newArrayList();
Constructs construct = null;
if (Converter.class.isAssignableFrom(klazz)) {
construct = Constructs.CONVERTER;
} else if (ForkOperator.class.isAssignableFrom(klazz)) {
construct = Constructs.FORK_OPERATOR;
} else if (RowLevelPolicy.class.isAssignableFrom(klazz)) {
construct = Constructs.ROW_QUALITY_CHECKER;
} else if (Extractor.class.isAssignableFrom(klazz)) {
construct = Constructs.EXTRACTOR;
} else if (DataWriter.class.isAssignableFrom(klazz)) {
construct = Constructs.WRITER;
}
if (construct != null) {
generatedTags.add(new Tag<>(GobblinMetricsKeys.CONSTRUCT_META, construct.toString()));
}
if (!klazz.isAnonymousClass()) {
generatedTags.add(new Tag<>(GobblinMetricsKeys.CLASS_META, klazz.getCanonicalName()));
}
Optional<GobblinMetrics> gobblinMetrics = state.contains(METRIC_CONTEXT_NAME_KEY)
? GobblinMetricsRegistry.getInstance().get(state.getProp(METRIC_CONTEXT_NAME_KEY))
: Optional.<GobblinMetrics> absent();
MetricContext.Builder builder = gobblinMetrics.isPresent()
? gobblinMetrics.get().getMetricContext().childBuilder(klazz.getCanonicalName() + "." + randomId)
: MetricContext.builder(klazz.getCanonicalName() + "." + randomId);
return builder.addTags(generatedTags).addTags(tags).build();
}
/**
* Generates a new {@link gobblin.metrics.MetricContext} with the parent and tags taken from the reference context.
* Allows replacing {@link gobblin.metrics.Tag} with new input tags.
* This method will not copy any {@link gobblin.metrics.Metric} contained in the reference {@link gobblin.metrics.MetricContext}.
*
* @param context Reference {@link gobblin.metrics.MetricContext}.
* @param newTags New {@link gobblin.metrics.Tag} to apply to context. Repeated keys will override old tags.
* @param name Name of the new {@link gobblin.metrics.MetricContext}.
* If absent or empty, will modify old name by adding a random integer at the end.
* @return Generated {@link gobblin.metrics.MetricContext}.
*/
public static MetricContext newContextFromReferenceContext(MetricContext context, List<Tag<?>> newTags,
Optional<String> name) {
String newName = name.orNull();
if (Strings.isNullOrEmpty(newName)) {
UUID uuid = UUID.randomUUID();
String randomIdPrefix = "uuid:";
String oldName = context.getName();
List<String> splitName = Strings.isNullOrEmpty(oldName) ? Lists.<String> newArrayList()
: Lists.newArrayList(Splitter.on(".").splitToList(oldName));
if (splitName.size() > 0 && StringUtils.startsWith(Iterables.getLast(splitName), randomIdPrefix)) {
splitName.set(splitName.size() - 1, String.format("%s%s", randomIdPrefix, uuid.toString()));
} else {
splitName.add(String.format("%s%s", randomIdPrefix, uuid.toString()));
}
newName = Joiner.on(".").join(splitName);
}
MetricContext.Builder builder = context.getParent().isPresent() ? context.getParent().get().childBuilder(newName)
: MetricContext.builder(newName);
return builder.addTags(context.getTags()).addTags(newTags).build();
}
/**
* Determines whether an object or, if it is a {@link gobblin.util.Decorator}, any object on its lineage,
* is of class {@link gobblin.instrumented.Instrumentable}.
* @param obj Object to analyze.
* @return Whether the lineage is instrumented.
*/
public static boolean isLineageInstrumented(Object obj) {
List<Object> lineage = DecoratorUtils.getDecoratorLineage(obj);
for (Object node : lineage) {
if (node instanceof Instrumentable) {
return true;
}
}
return false;
}
/**
* Returns a {@link com.codahale.metrics.Timer.Context} only if {@link gobblin.metrics.MetricContext} is defined.
* @param context an Optional<{@link gobblin.metrics.MetricContext}$gt;
* @param name name of the timer.
* @return an Optional<{@link com.codahale.metrics.Timer.Context}$gt;
*/
public static Optional<Timer.Context> timerContext(Optional<MetricContext> context, final String name) {
return context.transform(new Function<MetricContext, Timer.Context>() {
@Override
public Timer.Context apply(@Nonnull MetricContext input) {
return input.timer(name).time();
}
});
}
/**
* Ends a {@link com.codahale.metrics.Timer.Context} only if it exists.
* @param timer an Optional<{@link com.codahale.metrics.Timer.Context}$gt;
*/
public static void endTimer(Optional<Timer.Context> timer) {
timer.transform(new Function<Timer.Context, Timer.Context>() {
@Override
public Timer.Context apply(@Nonnull Timer.Context input) {
input.close();
return input;
}
});
}
/**
* Updates a timer only if it is defined.
* @param timer an Optional<{@link com.codahale.metrics.Timer}>
* @param duration
* @param unit
*/
public static void updateTimer(Optional<Timer> timer, final long duration, final TimeUnit unit) {
timer.transform(new Function<Timer, Timer>() {
@Override
public Timer apply(@Nonnull Timer input) {
input.update(duration, unit);
return input;
}
});
}
/**
* Marks a meter only if it is defined.
* @param meter an Optional<{@link com.codahale.metrics.Meter}>
*/
public static void markMeter(Optional<Meter> meter) {
markMeter(meter, 1);
}
/**
* Marks a meter only if it is defined.
* @param meter an Optional<{@link com.codahale.metrics.Meter}>
* @param value value to mark
*/
public static void markMeter(Optional<Meter> meter, final long value) {
meter.transform(new Function<Meter, Meter>() {
@Override
public Meter apply(@Nonnull Meter input) {
input.mark(value);
return input;
}
});
}
/**
* Sets the key {@link #METRIC_CONTEXT_NAME_KEY} to the specified name, in the given {@link State}.
*/
public static void setMetricContextName(State state, String name) {
state.setProp(Instrumented.METRIC_CONTEXT_NAME_KEY, name);
}
public Instrumented(State state, Class<?> klazz) {
this(state, klazz, ImmutableList.<Tag<?>> of());
}
public Instrumented(State state, Class<?> klazz, List<Tag<?>> tags) {
this.closer = Closer.create();
this.instrumentationEnabled = GobblinMetrics.isEnabled(state);
this.metricContext = this.closer.register(getMetricContext(state, klazz, tags));
}
/** Default with no additional tags */
@Override
public List<Tag<?>> generateTags(State state) {
return Lists.newArrayList();
}
@Override
public boolean isInstrumentationEnabled() {
return this.instrumentationEnabled;
}
@Override
public MetricContext getMetricContext() {
return this.metricContext;
}
@Override
public void switchMetricContext(List<Tag<?>> tags) {
this.metricContext = this.closer
.register(Instrumented.newContextFromReferenceContext(this.metricContext, tags, Optional.<String> absent()));
}
@Override
public void switchMetricContext(MetricContext context) {
this.metricContext = context;
}
@Override
public void close() throws IOException {
this.closer.close();
}
}