blob: f78e0a08b79b430dcda2d77995397c20a7a3ddd4 [file] [log] [blame]
/*
* 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 org.apache.htrace.core;
import java.io.Closeable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Use a Tracer instance inside a 'process' to collect and distribute its trace Spans.
* Example processes are an HDFS DataNode or an HBase RegionServer. A Tracer instance is your
* one-stop shop for all things tracing.
*/
public class Tracer implements Closeable {
private static final Log LOG = LogFactory.getLog(Tracer.class);
public final static String SPAN_RECEIVER_CLASSES_KEY = "span.receiver.classes";
public final static String SAMPLER_CLASSES_KEY = "sampler.classes";
public static class Builder {
private String name;
private HTraceConfiguration conf = HTraceConfiguration.EMPTY;
private ClassLoader classLoader =
Builder.class.getClassLoader();
private TracerPool tracerPool = TracerPool.GLOBAL;
/**
* @deprecated Since 4.0.0. Use Constructor that takes a <code>name</code> argument instead
*/
@Deprecated
public Builder() {
}
public Builder(final String name) {
name(name);
}
/**
* @param name The name of the Tracer to create.
* @return this
* @deprecated Since 4.0.0. Use Constructor that takes a <code>name</code> argument instead.
*/
@Deprecated
public Builder name(String name) {
this.name = name;
return this;
}
/**
* @param conf The configuration to set.
* @return this
*/
public Builder conf(HTraceConfiguration conf) {
this.conf = conf;
return this;
}
/**
* @param tracerPool The pool to set.
* @return this
*/
public Builder tracerPool(TracerPool tracerPool) {
this.tracerPool = tracerPool;
return this;
}
private void loadSamplers(List<Sampler> samplers) {
String classNamesStr = conf.get(SAMPLER_CLASSES_KEY, "");
List<String> classNames = getClassNamesFromConf(classNamesStr);
StringBuilder bld = new StringBuilder();
String prefix = "";
for (String className : classNames) {
try {
Sampler sampler = new Sampler.Builder(conf).
className(className).
classLoader(classLoader).
build();
samplers.add(sampler);
bld.append(prefix).append(className);
prefix = ", ";
} catch (Throwable e) {
LOG.error("Failed to create Sampler of type " + className, e);
}
}
String resultString = bld.toString();
if (resultString.isEmpty()) {
resultString = "no samplers";
}
LOG.debug(SAMPLER_CLASSES_KEY + " = " + classNamesStr +
"; loaded " + resultString);
}
private void loadSpanReceivers() {
String classNamesStr = conf.get(SPAN_RECEIVER_CLASSES_KEY, "");
List<String> classNames = getClassNamesFromConf(classNamesStr);
StringBuilder bld = new StringBuilder();
String prefix = "";
for (String className : classNames) {
try {
tracerPool.loadReceiverType(className, conf, classLoader);
bld.append(prefix).append(className);
prefix = ", ";
} catch (Throwable e) {
LOG.error("Failed to create SpanReceiver of type " + className, e);
}
}
String resultString = bld.toString();
if (resultString.isEmpty()) {
resultString = "no span receivers";
}
LOG.debug(SPAN_RECEIVER_CLASSES_KEY + " = " + classNamesStr +
"; loaded " + resultString);
}
/**
* Get a list of class names from the HTrace configuration.
* Entries which are empty will be removed. Entries which lack a package will
* be given the default package.
*
* @param classNamesStr A semicolon-separated string containing a list
* of class names.
* @return A list of class names.
*/
private List<String> getClassNamesFromConf(String classNamesStr) {
String classNames[] = classNamesStr.split(";");
LinkedList<String> cleanedClassNames = new LinkedList<String>();
for (String className : classNames) {
String cleanedClassName = className.trim();
if (!cleanedClassName.isEmpty()) {
cleanedClassNames.add(cleanedClassName);
}
}
return cleanedClassNames;
}
/**
* @return The new Tracer object.
*/
public Tracer build() {
if (name == null) {
throw new RuntimeException("You must specify a name for this Tracer.");
}
LinkedList<Sampler> samplers = new LinkedList<Sampler>();
loadSamplers(samplers);
String tracerId = new TracerId(conf, name).get();
Tracer tracer = new Tracer(tracerId, tracerPool,
samplers.toArray(new Sampler[samplers.size()]));
tracerPool.addTracer(tracer);
loadSpanReceivers();
if (LOG.isTraceEnabled()) {
LOG.trace("Created " + tracer + " for " + name);
}
return tracer;
}
}
/**
* The thread-specific context for this Tracer.
*
* This tracks the current number of trace scopes in a particular thread
* created by this tracer. We use this to apply our samplers only for the
* "top-level" spans.
*
* Note that we can't put the TraceScope objects themselves in this context,
* since we need to be able to use TraceScopes created by other Tracers, and
* this context is per-Tracer.
*/
private static class ThreadContext {
private long depth;
ThreadContext() {
this.depth = 0;
}
boolean isTopLevel() {
return (depth == 0);
}
void pushScope() {
depth++;
}
TraceScope pushNewScope(Tracer tracer, Span span, TraceScope parentScope) {
TraceScope scope = new TraceScope(tracer, span, parentScope);
threadLocalScope.set(scope);
depth++;
return scope;
}
void popScope() {
if (depth <= 0) {
throwClientError("There were more trace scopes closed than " +
"were opened.");
}
depth--;
}
};
/**
* A subclass of ThreadLocal that starts off with a non-null initial value in
* each thread.
*/
private static class ThreadLocalContext extends ThreadLocal<ThreadContext> {
@Override
protected ThreadContext initialValue() {
return new ThreadContext();
}
};
/**
* The current trace scope. This is global, so it is shared amongst all
* libraries using HTrace.
*/
final static ThreadLocal<TraceScope> threadLocalScope =
new ThreadLocal<TraceScope>();
/**
* An empty array of SpanId objects. Can be used rather than constructing a
* new object whenever we need an empty array.
*/
private static final SpanId EMPTY_PARENT_ARRAY[] = new SpanId[0];
/**
* The tracerId.
*/
private final String tracerId;
/**
* The TracerPool which this Tracer belongs to.
*
* This gets set to null after the Tracer is closed in order to catch some
* use-after-close errors. Note that we do not synchronize access on this
* field, since it only changes when the Tracer is closed, and the Tracer
* should not be used after that.
*/
private TracerPool tracerPool;
/**
* The current thread-local context for this particualr Tracer.
*/
private final ThreadLocalContext threadContext;
/**
* The NullScope instance for this Tracer.
*/
private final NullScope nullScope;
/**
* The currently active Samplers.
*
* Arrays are immutable once set. You must take the Tracer lock in order to
* set this to a new array. If this is null, the Tracer is closed.
*/
private volatile Sampler[] curSamplers;
/**
* Log a client error, and throw an exception.
*
* @param str The message to use in the log and the exception.
*/
static void throwClientError(String str) {
LOG.error(str);
throw new RuntimeException(str);
}
/**
* @return If the current thread is tracing, this function returns the Tracer that is
* being used; otherwise, it returns null.
*/
public static Tracer curThreadTracer() {
TraceScope traceScope = threadLocalScope.get();
if (traceScope == null) {
return null;
}
return traceScope.tracer;
}
Tracer(String tracerId, TracerPool tracerPool, Sampler[] curSamplers) {
this.tracerId = tracerId;
this.tracerPool = tracerPool;
this.threadContext = new ThreadLocalContext();
this.nullScope = new NullScope(this);
this.curSamplers = curSamplers;
}
public String getTracerId() {
return tracerId;
}
private TraceScope newScopeImpl(ThreadContext context, String description) {
Span span = new MilliSpan.Builder().
tracerId(tracerId).
begin(System.currentTimeMillis()).
description(description).
parents(EMPTY_PARENT_ARRAY).
spanId(SpanId.fromRandom()).
build();
return context.pushNewScope(this, span, null);
}
private TraceScope newScopeImpl(ThreadContext context, String description,
TraceScope parentScope) {
SpanId parentId = parentScope.getSpan().getSpanId();
Span span = new MilliSpan.Builder().
tracerId(tracerId).
begin(System.currentTimeMillis()).
description(description).
parents(new SpanId[] { parentId }).
spanId(parentId.newChildId()).
build();
return context.pushNewScope(this, span, parentScope);
}
private TraceScope newScopeImpl(ThreadContext context, String description,
SpanId parentId) {
Span span = new MilliSpan.Builder().
tracerId(tracerId).
begin(System.currentTimeMillis()).
description(description).
parents(new SpanId[] { parentId }).
spanId(parentId.newChildId()).
build();
return context.pushNewScope(this, span, null);
}
private TraceScope newScopeImpl(ThreadContext context, String description,
TraceScope parentScope, SpanId secondParentId) {
SpanId parentId = parentScope.getSpan().getSpanId();
Span span = new MilliSpan.Builder().
tracerId(tracerId).
begin(System.currentTimeMillis()).
description(description).
parents(new SpanId[] { parentId, secondParentId }).
spanId(parentId.newChildId()).
build();
return context.pushNewScope(this, span, parentScope);
}
/**
* Create a new trace scope.
*
* If there are no scopes above the current scope, we will apply our
* configured samplers. Otherwise, we will create a trace Span only if this thread
* is already tracing, or if the passed parentID was valid.
*
* @param description The description of the new span to create.
* @param parentId If this is a valid span ID, it will be added to
* the parents of the new span we create.
* @return The new trace scope.
*/
public TraceScope newScope(String description, SpanId parentId) {
TraceScope parentScope = threadLocalScope.get();
ThreadContext context = threadContext.get();
if (parentScope != null) {
if (parentId.isValid() &&
(!parentId.equals(parentScope.getSpan().getSpanId()))) {
return newScopeImpl(context, description, parentScope, parentId);
} else {
return newScopeImpl(context, description, parentScope);
}
} else if (parentId.isValid()) {
return newScopeImpl(context, description, parentId);
}
if (!context.isTopLevel()) {
context.pushScope();
return nullScope;
}
if (!sample()) {
context.pushScope();
return nullScope;
}
return newScopeImpl(context, description);
}
/**
* Create a new trace scope.
*
* If there are no scopes above the current scope, we will apply our
* configured samplers. Otherwise, we will create a trace Span only if this thread
* is already tracing.
* @param description The description of the new span to create.
* @return The new trace scope.
*/
public TraceScope newScope(String description) {
TraceScope parentScope = threadLocalScope.get();
ThreadContext context = threadContext.get();
if (parentScope != null) {
return newScopeImpl(context, description, parentScope);
}
if (!context.isTopLevel()) {
context.pushScope();
return nullScope;
}
if (!sample()) {
context.pushScope();
return nullScope;
}
return newScopeImpl(context, description);
}
/**
* Return a null trace scope.
*
* @return The null trace scope.
*/
public TraceScope newNullScope() {
ThreadContext context = threadContext.get();
context.pushScope();
return nullScope;
}
/**
* Wrap the callable in a TraceCallable, if tracing.
*
* @param <V> The subclass of callable.
* @param callable The callable to wrap.
* @param description A description of the callable, or null if there
* is no description.
* @return The callable provided, wrapped if tracing, 'callable' if not.
*/
public <V> Callable<V> wrap(Callable<V> callable, String description) {
TraceScope parentScope = threadLocalScope.get();
if (parentScope == null) {
return callable;
}
return new TraceCallable<V>(this, parentScope.getSpanId(), callable, description);
}
/**
* Wrap the runnable in a TraceRunnable, if tracing
*
* @param runnable The runnable to wrap.
* @param description A description of the runnable, or null if there is
* no description.
* @return The runnable provided, wrapped if tracing, 'runnable' if not.
*/
public Runnable wrap(Runnable runnable, String description) {
TraceScope parentScope = threadLocalScope.get();
if (parentScope == null) {
return runnable;
}
return new TraceRunnable(this, parentScope, runnable, description);
}
public TraceExecutorService newTraceExecutorService(ExecutorService impl) {
return newTraceExecutorService(impl, null);
}
public TraceExecutorService newTraceExecutorService(ExecutorService impl,
String scopeName) {
return new TraceExecutorService(this, scopeName, impl);
}
public TracerPool getTracerPool() {
if (tracerPool == null) {
throwClientError(toString() + " is closed.");
}
return tracerPool;
}
/**
* Returns an object that will trace all calls to itself.
*/
@SuppressWarnings("unchecked")
<T, V> T createProxy(final T instance) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object obj, Method method, Object[] args)
throws Throwable {
try (TraceScope scope = Tracer.this.newScope(method.getName());) {
return method.invoke(instance, args);
} catch (Throwable ex) {
ex.printStackTrace();
throw ex;
}
}
};
return (T) Proxy.newProxyInstance(instance.getClass().getClassLoader(),
instance.getClass().getInterfaces(), handler);
}
/**
* Return true if we should create a new top-level span.
*
* We will create the span if any configured sampler returns true.
*/
private boolean sample() {
Sampler[] samplers = curSamplers;
for (Sampler sampler : samplers) {
if (sampler.next()) {
return true;
}
}
return false;
}
/**
* Returns an array of all the current Samplers.
*
* Note that if the current Samplers change, those changes will not be
* reflected in this array. In other words, this array may be stale.
*
* @return The current samplers.
*/
public Sampler[] getSamplers() {
return curSamplers;
}
/**
* Add a new Sampler.
*
* @param sampler The new sampler to add.
* You cannot add a particular Sampler object more
* than once. You may add multiple Sampler objects
* of the same type, although this is not recommended.
*
* @return True if the sampler was added; false if it already had
* been added earlier.
*/
public synchronized boolean addSampler(Sampler sampler) {
if (tracerPool == null) {
throwClientError(toString() + " is closed.");
}
Sampler[] samplers = curSamplers;
for (int i = 0; i < samplers.length; i++) {
if (samplers[i] == sampler) {
return false;
}
}
Sampler[] newSamplers =
Arrays.copyOf(samplers, samplers.length + 1);
newSamplers[samplers.length] = sampler;
curSamplers = newSamplers;
return true;
}
/**
* Remove a Sampler.
*
* @param sampler The sampler to remove.
* @return True only if the sampler was removed.
*/
public synchronized boolean removeSampler(Sampler sampler) {
if (tracerPool == null) {
throwClientError(toString() + " is closed.");
}
Sampler[] samplers = curSamplers;
for (int i = 0; i < samplers.length; i++) {
if (samplers[i] == sampler) {
Sampler[] newSamplers = new Sampler[samplers.length - 1];
System.arraycopy(samplers, 0, newSamplers, 0, i);
System.arraycopy(samplers, i + 1, newSamplers, i,
samplers.length - i - 1);
curSamplers = newSamplers;
return true;
}
}
return false;
}
void detachScope(TraceScope scope) {
TraceScope curScope = threadLocalScope.get();
if (curScope != scope) {
throwClientError("Can't detach TraceScope for " +
scope.getSpan().toJson() + " because it is not the current " +
"TraceScope in thread " + Thread.currentThread().getName());
}
ThreadContext context = threadContext.get();
context.popScope();
threadLocalScope.set(scope.getParent());
}
void reattachScope(TraceScope scope) {
TraceScope parent = threadLocalScope.get();
Tracer.threadLocalScope.set(scope);
ThreadContext context = threadContext.get();
context.pushScope();
scope.setParent(parent);
}
void closeScope(TraceScope scope) {
TraceScope curScope = threadLocalScope.get();
if (curScope != scope) {
throwClientError("Can't close TraceScope for " +
scope.getSpan().toJson() + " because it is not the current " +
"TraceScope in thread " + Thread.currentThread().getName());
}
if (tracerPool == null) {
throwClientError(toString() + " is closed.");
}
SpanReceiver[] receivers = tracerPool.getReceivers();
if (receivers == null) {
throwClientError(toString() + " is closed.");
}
ThreadContext context = threadContext.get();
context.popScope();
threadLocalScope.set(scope.getParent());
scope.setParent(null);
Span span = scope.getSpan();
span.stop();
for (SpanReceiver receiver : receivers) {
receiver.receiveSpan(span);
}
}
void popNullScope() {
TraceScope curScope = threadLocalScope.get();
if (curScope != null) {
throwClientError("Attempted to close an empty scope, but it was not " +
"the current thread scope in thread " +
Thread.currentThread().getName());
}
ThreadContext context = threadContext.get();
context.popScope();
}
public static Span getCurrentSpan() {
TraceScope curScope = threadLocalScope.get();
if (curScope == null) {
return null;
} else {
return curScope.getSpan();
}
}
public static SpanId getCurrentSpanId() {
TraceScope curScope = threadLocalScope.get();
if (curScope == null) {
return SpanId.INVALID;
} else {
return curScope.getSpan().getSpanId();
}
}
@Override
public synchronized void close() {
if (tracerPool == null) {
return;
}
curSamplers = new Sampler[0];
tracerPool.removeTracer(this);
}
/**
* Get the hash code of a Tracer object.
*
* This hash code is based on object identity.
* This is used in TracerPool to create a hash table of Tracers.
*/
@Override
public int hashCode() {
return System.identityHashCode(this);
}
/**
* Compare two tracer objects.
*
* Tracer objects are always compared by object equality.
* This is used in TracerPool to create a hash table of Tracers.
*/
@Override
public boolean equals(Object other) {
return (this == other);
}
@Override
public String toString() {
return "Tracer(" + tracerId + ")";
}
}