You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@htrace.apache.org by cm...@apache.org on 2015/09/20 06:12:06 UTC

[2/5] incubator-htrace git commit: HTRACE-259. Rename htrace-core module to htrace-core4 to match the artifactId (stack via cmccabe)

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/3c20489f/htrace-core4/src/main/java/org/apache/htrace/core/TraceScope.java
----------------------------------------------------------------------
diff --git a/htrace-core4/src/main/java/org/apache/htrace/core/TraceScope.java b/htrace-core4/src/main/java/org/apache/htrace/core/TraceScope.java
new file mode 100644
index 0000000..05a053e
--- /dev/null
+++ b/htrace-core4/src/main/java/org/apache/htrace/core/TraceScope.java
@@ -0,0 +1,128 @@
+/*
+ * 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;
+
+/**
+ * Create a new TraceScope at major transitions. Hosts current tracing context.
+ */
+public class TraceScope implements Closeable {
+  /**
+   * The tracer to use for this scope.
+   */
+  final Tracer tracer;
+
+  /**
+   * The trace span for this scope, or null if the scope is closed.
+   *
+   * If the scope is closed, it must also be detached.
+   */
+  private final Span span;
+
+  /**
+   * The parent of this trace scope, or null if there is no parent.
+   */
+  private TraceScope parent;
+
+  /**
+   * True if this scope is detached.
+   */
+  boolean detached;
+
+  TraceScope(Tracer tracer, Span span, TraceScope parent) {
+    this.tracer = tracer;
+    this.span = span;
+    this.parent = parent;
+    this.detached = false;
+  }
+
+  /**
+   * Returns the span which this scope is managing.
+   */
+  public Span getSpan() {
+    return span;
+  }
+
+  /**
+   * Returns the span ID which this scope is managing.
+   */
+  public SpanId getSpanId() {
+    return span.getSpanId();
+  }
+
+  TraceScope getParent() {
+    return parent;
+  }
+
+  void setParent(TraceScope parent) {
+    this.parent = parent;
+  }
+
+  /**
+   * Detach this TraceScope from the current thread.
+   *
+   * It is OK to "leak" TraceScopes which have been detached.  They will not
+   * consume any resources other than a small amount of memory until they are
+   * garbage collected.  On the other hand, trace scopes which are still
+   * attached must never be leaked.
+   */
+  public void detach() {
+    if (detached) {
+      Tracer.throwClientError("Can't detach this TraceScope  because " +
+          "it is already detached.");
+    }
+    tracer.detachScope(this);
+    detached = true;
+    parent = null;
+  }
+
+  /**
+   * Attach this TraceScope to the current thread.
+   */
+  public void reattach() {
+    if (!detached) {
+      Tracer.throwClientError("Can't reattach this TraceScope  because " +
+          "it is not detached.");
+    }
+    tracer.reattachScope(this);
+    detached = false;
+  }
+
+  /**
+   * Close this TraceScope, ending the trace span it is managing.
+   */
+  @Override
+  public void close() {
+    tracer.closeScope(this);
+  }
+
+  public void addKVAnnotation(String key, String value) {
+    span.addKVAnnotation(key, value);
+  }
+
+  public void addTimelineAnnotation(String msg) {
+    span.addTimelineAnnotation(msg);
+  }
+
+  @Override
+  public String toString() {
+    return "TraceScope(tracerId=" + tracer.getTracerId() +
+        ", span=" + span.toJson() +
+        ", detached=" + detached + ")";
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/3c20489f/htrace-core4/src/main/java/org/apache/htrace/core/Tracer.java
----------------------------------------------------------------------
diff --git a/htrace-core4/src/main/java/org/apache/htrace/core/Tracer.java b/htrace-core4/src/main/java/org/apache/htrace/core/Tracer.java
new file mode 100644
index 0000000..39d972d
--- /dev/null
+++ b/htrace-core4/src/main/java/org/apache/htrace/core/Tracer.java
@@ -0,0 +1,673 @@
+/*
+ * 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.
+ * 
+ * <p>
+ */
+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
+     * @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;
+    }
+
+    public Builder conf(HTraceConfiguration conf) {
+      this.conf = conf;
+      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 SpanReceiver 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;
+    }
+
+    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.
+   */
+  public TraceScope newNullScope() {
+    ThreadContext context = threadContext.get();
+    context.pushScope();
+    return nullScope;
+  }
+
+  /**
+   * Wrap the callable in a TraceCallable, if tracing.
+   *
+   * @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, callable, description);
+  }
+
+  /**
+   * Wrap the runnable in a TraceRunnable, if tracing
+   *
+   * @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,
+                                                      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) {
+    final Tracer tracer = this;
+    InvocationHandler handler = new InvocationHandler() {
+      @Override
+      public Object invoke(Object obj, Method method, Object[] args)
+          throws Throwable {
+        TraceScope scope = tracer.newScope(method.getName());
+        try {
+          return method.invoke(instance, args);
+        } catch (Throwable ex) {
+          ex.printStackTrace();
+          throw ex;
+        } finally {
+          scope.close();
+        }
+      }
+    };
+    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.
+   */
+  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 SpanReceiver.
+   *
+   * @param sampler       The sampler to remove.
+   */
+  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 + ")";
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/3c20489f/htrace-core4/src/main/java/org/apache/htrace/core/TracerId.java
----------------------------------------------------------------------
diff --git a/htrace-core4/src/main/java/org/apache/htrace/core/TracerId.java b/htrace-core4/src/main/java/org/apache/htrace/core/TracerId.java
new file mode 100644
index 0000000..da482fe
--- /dev/null
+++ b/htrace-core4/src/main/java/org/apache/htrace/core/TracerId.java
@@ -0,0 +1,294 @@
+/*
+ * 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.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.management.ManagementFactory;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Enumeration;
+import java.util.Locale;
+import java.util.TreeSet;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * The HTrace tracer ID.<p/>
+ *
+ * HTrace tracer IDs are created from format strings.
+ * Format strings contain variables which the TracerId class will
+ * replace with the correct values at runtime.<p/>
+ *
+ * <ul>
+ * <li>%{tname}: the tracer name supplied when creating the Tracer.</li>
+ * <li>%{pname}: the process name obtained from the JVM.</li>
+ * <li>%{ip}: will be replaced with an ip address.</li>
+ * <li>%{pid}: the numerical process ID from the operating system.</li>
+ * </ul><p/>
+ *
+ * For example, the string "%{pname}/%{ip}" will be replaced with something
+ * like: DataNode/192.168.0.1, assuming that the process' name is DataNode
+ * and its IP address is 192.168.0.1.<p/>
+ *
+ *  ID strings can contain backslashes as escapes.
+ * For example, "\a" will map to "a".  "\%{ip}" will map to the literal
+ * string "%{ip}", not the IP address.  A backslash itself can be escaped by a
+ * preceding backslash.
+ */
+public final class TracerId {
+  private static final Log LOG = LogFactory.getLog(TracerId.class);
+
+  /**
+   * The configuration key to use for process id
+   */
+  public static final String TRACER_ID_KEY = "tracer.id";
+
+  /**
+   * The default tracer ID to use if no other ID is configured.
+   */
+  private static final String DEFAULT_TRACER_ID = "%{tname}/%{ip}";
+
+  private final String tracerName;
+
+  private final String tracerId;
+
+  public TracerId(HTraceConfiguration conf, String tracerName) {
+    this.tracerName = tracerName;
+    String fmt = conf.get(TRACER_ID_KEY, DEFAULT_TRACER_ID);
+    StringBuilder bld = new StringBuilder();
+    StringBuilder varBld = null;
+    boolean escaping = false;
+    int varSeen = 0;
+    for (int i = 0, len = fmt.length() ; i < len; i++) {
+      char c = fmt.charAt(i);
+      if (c == '\\') {
+        if (!escaping) {
+          escaping = true;
+          continue;
+        }
+      }
+      switch (varSeen) {
+        case 0:
+          if (c == '%') {
+            if (!escaping) {
+              varSeen = 1;
+              continue;
+            }
+          }
+          escaping = false;
+          varSeen = 0;
+          bld.append(c);
+          break;
+        case 1:
+          if (c == '{') {
+            if (!escaping) {
+              varSeen = 2;
+              varBld = new StringBuilder();
+              continue;
+            }
+          }
+          escaping = false;
+          varSeen = 0;
+          bld.append("%").append(c);
+          break;
+        default:
+          if (c == '}') {
+            if (!escaping) {
+              String var = varBld.toString();
+              bld.append(processShellVar(var));
+              varBld = null;
+              varSeen = 0;
+              continue;
+            }
+          }
+          escaping = false;
+          varBld.append(c);
+          varSeen++;
+          break;
+      }
+    }
+    if (varSeen > 0) {
+      LOG.warn("Unterminated process ID substitution variable at the end " +
+          "of format string " + fmt);
+    }
+    this.tracerId = bld.toString();
+    if (LOG.isTraceEnabled()) {
+      LOG.trace("ProcessID(fmt=" + fmt + "): computed process ID of \"" +
+          this.tracerId + "\"");
+    }
+  }
+
+  private String processShellVar(String var) {
+    if (var.equals("tname")) {
+      return tracerName;
+    } else if (var.equals("pname")) {
+      return getProcessName();
+    } else if (var.equals("ip")) {
+      return getBestIpString();
+    } else if (var.equals("pid")) {
+      return Long.valueOf(getOsPid()).toString();
+    } else {
+      LOG.warn("unknown ProcessID variable " + var);
+      return "";
+    }
+  }
+
+  static String getProcessName() {
+    String cmdLine = System.getProperty("sun.java.command");
+    if (cmdLine != null && !cmdLine.isEmpty()) {
+      String fullClassName = cmdLine.split("\\s+")[0];
+      String[] classParts = fullClassName.split("\\.");
+      cmdLine = classParts[classParts.length - 1];
+    }
+    return (cmdLine == null || cmdLine.isEmpty()) ? "Unknown" : cmdLine;
+  }
+
+  /**
+   * Get the best IP address that represents this node.<p/>
+   *
+   * This is complicated since nodes can have multiple network interfaces,
+   * and each network interface can have multiple IP addresses.  What we're
+   * looking for here is an IP address that will serve to identify this node
+   * to HTrace.  So we prefer site-local addresess (i.e. private ones on the
+   * LAN) to publicly routable interfaces.  If there are multiple addresses
+   * to choose from, we select the one which comes first in textual sort
+   * order.  This should ensure that we at least consistently call each node
+   * by a single name.
+   */
+  static String getBestIpString() {
+    Enumeration<NetworkInterface> ifaces;
+    try {
+      ifaces = NetworkInterface.getNetworkInterfaces();
+    } catch (SocketException e) {
+      LOG.error("Error getting network interfaces", e);
+      return "127.0.0.1";
+    }
+    TreeSet<String> siteLocalCandidates = new TreeSet<String>();
+    TreeSet<String> candidates = new TreeSet<String>();
+    while (ifaces.hasMoreElements()) {
+      NetworkInterface iface = ifaces.nextElement();
+      for (Enumeration<InetAddress> addrs =
+               iface.getInetAddresses(); addrs.hasMoreElements();) {
+        InetAddress addr = addrs.nextElement();
+        if (!addr.isLoopbackAddress()) {
+          if (addr.isSiteLocalAddress()) {
+            siteLocalCandidates.add(addr.getHostAddress());
+          } else {
+            candidates.add(addr.getHostAddress());
+          }
+        }
+      }
+    }
+    if (!siteLocalCandidates.isEmpty()) {
+      return siteLocalCandidates.first();
+    }
+    if (!candidates.isEmpty()) {
+      return candidates.first();
+    }
+    return "127.0.0.1";
+  }
+
+  /**
+   * Get the process id from the operating system.<p/>
+   *
+   * Unfortunately, there is no simple method to get the process id in Java.
+   * The approach we take here is to use the shell method (see
+   * {TracerId#getOsPidFromShellPpid}) unless we are on Windows, where the
+   * shell is not available.  On Windows, we use
+   * {TracerId#getOsPidFromManagementFactory}, which depends on some
+   * undocumented features of the JVM, but which doesn't require a shell.
+   */
+  static long getOsPid() {
+    if ((System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH)).
+        contains("windows")) {
+      return getOsPidFromManagementFactory();
+    } else {
+      return getOsPidFromShellPpid();
+    }
+  }
+
+  /**
+   * Get the process ID by executing a shell and printing the PPID (parent
+   * process ID).<p/>
+   *
+   * This method of getting the process ID doesn't depend on any undocumented
+   * features of the virtual machine, and should work on almost any UNIX
+   * operating system.
+   */
+  private static long getOsPidFromShellPpid() {
+    Process p = null;
+    StringBuilder sb = new StringBuilder();
+    try {
+      p = new ProcessBuilder("/usr/bin/env", "sh", "-c", "echo $PPID").
+        redirectErrorStream(true).start();
+      BufferedReader reader = new BufferedReader(
+          new InputStreamReader(p.getInputStream()));
+      String line = "";
+      while ((line = reader.readLine()) != null) {
+        sb.append(line.trim());
+      }
+      int exitVal = p.waitFor();
+      if (exitVal != 0) {
+        throw new IOException("Process exited with error code " +
+            Integer.valueOf(exitVal).toString());
+      }
+    } catch (InterruptedException e) {
+      LOG.error("Interrupted while getting operating system pid from " +
+          "the shell.", e);
+      return 0L;
+    } catch (IOException e) {
+      LOG.error("Error getting operating system pid from the shell.", e);
+      return 0L;
+    } finally {
+      if (p != null) {
+        p.destroy();
+      }
+    }
+    try {
+      return Long.parseLong(sb.toString());
+    } catch (NumberFormatException e) {
+      LOG.error("Error parsing operating system pid from the shell.", e);
+      return 0L;
+    }
+  }
+
+  /**
+   * Get the process ID by looking at the name of the managed bean for the
+   * runtime system of the Java virtual machine.<p/>
+   *
+   * Although this is undocumented, in the Oracle JVM this name is of the form
+   * [OS_PROCESS_ID]@[HOSTNAME].
+   */
+  private static long getOsPidFromManagementFactory() {
+    try {
+      return Long.parseLong(ManagementFactory.getRuntimeMXBean().
+          getName().split("@")[0]);
+    } catch (NumberFormatException e) {
+      LOG.error("Failed to get the operating system process ID from the name " +
+          "of the managed bean for the JVM.", e);
+      return 0L;
+    }
+  }
+
+  public String get() {
+    return tracerId;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/3c20489f/htrace-core4/src/main/java/org/apache/htrace/core/TracerPool.java
----------------------------------------------------------------------
diff --git a/htrace-core4/src/main/java/org/apache/htrace/core/TracerPool.java b/htrace-core4/src/main/java/org/apache/htrace/core/TracerPool.java
new file mode 100644
index 0000000..26e39f5
--- /dev/null
+++ b/htrace-core4/src/main/java/org/apache/htrace/core/TracerPool.java
@@ -0,0 +1,285 @@
+/*
+ * 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.util.Arrays;
+import java.util.HashSet;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * A pool of Tracer objects.
+ *
+ * There may be more than one {@link Tracer} running inside a single 'process'; for example,
+ * unit tests may spin up a DataNode, a NameNode, and HDFS clients all running in a single JVM
+ * instance, each with its own Tracer. TracerPool is where all Tracer instances register
+ * on creation so Tracers can coordinate around shared resources such as {@link SpanReceiver}
+ * instances. TracerPool takes care of properly cleaning up registered Tracer instances on shutdown.
+ */
+public class TracerPool {
+  private static final Log LOG = LogFactory.getLog(TracerPool.class);
+
+  /**
+   * The global pool of tracer objects.
+   *
+   * This is the pool that new tracers get put into by default.
+   */
+  static final TracerPool GLOBAL = new TracerPool("Global");
+
+  /**
+   * The shutdown hook which closes the Tracers in this pool when the process is
+   * shutting down.
+   */
+  private class SpanReceiverShutdownHook extends Thread {
+    SpanReceiverShutdownHook() {
+      setName("SpanReceiverShutdownHook");
+      setDaemon(false);
+    }
+
+    @Override
+    public void run() {
+      removeAndCloseAllSpanReceivers();
+    }
+  }
+
+  /**
+   * The name of this TracerPool.
+   */
+  private final String name;
+
+  /**
+   * The current span receivers which these tracers are using.
+   *
+   * Can be read locklessly.  Must be written under the lock.
+   * The array itself should never be modified.
+   */
+  private volatile SpanReceiver[] curReceivers;
+
+  /**
+   * The currently installed shutdown hook, or null if no hook has been
+   * installed.
+   */
+  private SpanReceiverShutdownHook shutdownHook;
+
+  /**
+   * The current Tracers.
+   */
+  private final HashSet<Tracer> curTracers;
+
+  /**
+   * Get the global tracer pool.
+   */
+  public static TracerPool getGlobalTracerPool() {
+    return GLOBAL;
+  }
+
+  public TracerPool(String name) {
+    this.name = name;
+    this.shutdownHook = null;
+    this.curTracers = new HashSet<Tracer>();
+    this.curReceivers = new SpanReceiver[0];
+  }
+
+  /**
+   * Return the name of this TracerPool.
+   */
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Returns an array of all the current span receivers.
+   *
+   * Note that if the current span receivers change, those changes will not be
+   * reflected in this array.  In other words, this array may be stale.
+   */
+  public SpanReceiver[] getReceivers() {
+    return curReceivers;
+  }
+
+  /**
+   * Add a new span receiver.
+   *
+   * @param receiver        The new receiver to add.
+   *
+   * @return                True if the new receiver was added; false if it
+   *                          already was there.
+   */
+  public synchronized boolean addReceiver(SpanReceiver receiver) {
+    SpanReceiver[] receivers = curReceivers;
+    for (int i = 0; i < receivers.length; i++) {
+      if (receivers[i] == receiver) {
+        LOG.trace(toString() + ": can't add receiver " + receiver.toString() +
+                  " since it is already in this pool.");
+        return false;
+      }
+    }
+    SpanReceiver[] newReceivers =
+        Arrays.copyOf(receivers, receivers.length + 1);
+    newReceivers[receivers.length] = receiver;
+    registerShutdownHookIfNeeded();
+    curReceivers = newReceivers;
+    LOG.trace(toString() + ": added receiver " + receiver.toString());
+    return true;
+  }
+
+  /**
+   * Register the shutdown hook if needed.
+   */
+  private synchronized void registerShutdownHookIfNeeded() {
+    if (shutdownHook != null) {
+      return;
+    }
+    shutdownHook = new SpanReceiverShutdownHook();
+    Runtime.getRuntime().addShutdownHook(shutdownHook);
+    LOG.trace(toString() + ": registered shutdown hook.");
+  }
+
+  /**
+   * Remove a span receiver.
+   *
+   * @param receiver        The receiver to remove.
+   *
+   * @return                True if the receiver was removed; false if it
+   *                          did not exist in this pool.
+   */
+  public synchronized boolean removeReceiver(SpanReceiver receiver) {
+    SpanReceiver[] receivers = curReceivers;
+    for (int i = 0; i < receivers.length; i++) {
+      if (receivers[i] == receiver) {
+        SpanReceiver[] newReceivers = new SpanReceiver[receivers.length - 1];
+        System.arraycopy(receivers, 0, newReceivers, 0, i);
+        System.arraycopy(receivers, i + 1, newReceivers, i,
+            receivers.length - i - 1);
+        curReceivers = newReceivers;
+        LOG.trace(toString() + ": removed receiver " + receiver.toString());
+        return true;
+      }
+    }
+    LOG.trace(toString() + ": can't remove receiver " + receiver.toString() +
+        " since it's not currently in this pool.");
+    return false;
+  }
+
+  /**
+   * Remove and close a span receiver.
+   *
+   * @param receiver        The receiver to remove.
+   *
+   * @return                True if the receiver was removed; false if it
+   *                          did not exist in this pool.
+   */
+  public boolean removeAndCloseReceiver(SpanReceiver receiver) {
+    if (!removeReceiver(receiver)) {
+      return false;
+    }
+    try {
+      LOG.trace(toString() + ": closing receiver " + receiver.toString());
+      receiver.close();
+    } catch (Throwable t) {
+      LOG.error(toString() + ": error closing " + receiver.toString(), t);
+    }
+    return true;
+  }
+
+  /**
+   * Remove and close all of the span receivers.
+   */
+  private synchronized void removeAndCloseAllSpanReceivers() {
+    SpanReceiver[] receivers = curReceivers;
+    curReceivers = new SpanReceiver[0];
+    for (SpanReceiver receiver : receivers) {
+      try {
+        LOG.trace(toString() + ": closing receiver " + receiver.toString());
+        receiver.close();
+      } catch (Throwable t) {
+        LOG.error(toString() + ": error closing " + receiver.toString(), t);
+      }
+    }
+  }
+
+  /**
+   * Given a SpanReceiver class name, return the existing instance of that span
+   * receiver, if possible; otherwise, invoke the callable to create a new
+   * instance.
+   *
+   * @param className       The span receiver class name.
+   * @param conf            The HTrace configuration.
+   * @param classLoader     The class loader to use.
+   *
+   * @return                The SpanReceiver.
+   */
+  public synchronized SpanReceiver loadReceiverType(String className,
+      HTraceConfiguration conf, ClassLoader classLoader) {
+    SpanReceiver[] receivers = curReceivers;
+    for (SpanReceiver receiver : receivers) {
+      if (receiver.getClass().getName().equals(className)) {
+        LOG.trace(toString() + ": returning a reference to receiver " +
+                  receiver.toString());
+        return receiver;
+      }
+    }
+    LOG.trace(toString() + ": creating a new SpanReceiver of type " +
+              className);
+    SpanReceiver receiver = new SpanReceiver.Builder(conf).
+        className(className).
+        classLoader(classLoader).
+        build();
+    addReceiver(receiver);
+    return receiver;
+  }
+
+  /**
+   * Returns an array of all the current Tracers.
+   *
+   * Note that if the current Tracers change, those changes will not be
+   * reflected in this array.  In other words, this array may be stale.
+   */
+  public synchronized Tracer[] getTracers() {
+    return curTracers.toArray(new Tracer[curTracers.size()]);
+  }
+
+  /**
+   * Add a new Tracer.
+   */
+  synchronized void addTracer(Tracer tracer) {
+    if (curTracers.add(tracer)) {
+      LOG.trace(toString() + ": adding tracer " + tracer.toString());
+    }
+  }
+
+  /**
+   * Remove a Tracer.
+   *
+   * If the Tracer removed was the last one, we will close all the SpanReceiver
+   * objects that we're managing.
+   */
+  synchronized void removeTracer(Tracer tracer) {
+    if (curTracers.remove(tracer)) {
+      LOG.trace(toString() + ": removing tracer " + tracer.toString());
+      if (curTracers.size() == 0) {
+        removeAndCloseAllSpanReceivers();
+      }
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "TracerPool(" + name + ")";
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/3c20489f/htrace-core4/src/test/java/org/apache/htrace/core/TestBadClient.java
----------------------------------------------------------------------
diff --git a/htrace-core4/src/test/java/org/apache/htrace/core/TestBadClient.java b/htrace-core4/src/test/java/org/apache/htrace/core/TestBadClient.java
new file mode 100644
index 0000000..87ae8e9
--- /dev/null
+++ b/htrace-core4/src/test/java/org/apache/htrace/core/TestBadClient.java
@@ -0,0 +1,205 @@
+/*
+ * 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.File;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.hamcrest.CoreMatchers.containsString;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestBadClient {
+  @After
+  public void clearBadState() {
+    // Clear the bad trace state so that we don't disrupt other unit tests
+    // that run in this JVM.
+    Tracer.threadLocalScope.set(null);
+  }
+
+  /**
+   * Test closing an outer scope when an inner one is still active.
+   */
+  @Test
+  public void TestClosingOuterScope() throws Exception {
+    Tracer tracer = new Tracer.Builder().
+        name("TestClosingOuterScopeTracer").
+        tracerPool(new TracerPool("TestClosingOuterScope")).
+        conf(HTraceConfiguration.
+            fromKeyValuePairs("sampler.classes", "AlwaysSampler")).build();
+    boolean gotException = false;
+    TraceScope outerScope = tracer.newScope("outer");
+    TraceScope innerScope = tracer.newScope("inner");
+    try {
+      outerScope.close();
+    } catch (RuntimeException e) {
+      assertThat(e.getMessage(),
+          containsString("it is not the current TraceScope"));
+      gotException = true;
+    }
+    assertTrue("Expected to get exception because of improper " +
+        "scope closure.", gotException);
+    innerScope.close();
+    tracer.close();
+  }
+
+  /**
+   * Test calling detach() two times on a scope object.
+   */
+  @Test
+  public void TestDoubleDetachIsCaught() throws Exception {
+    Tracer tracer = new Tracer.Builder().
+        name("TestDoubleDetach").
+        tracerPool(new TracerPool("TestDoubleDetachIsCaught")).
+        conf(HTraceConfiguration.
+            fromKeyValuePairs("sampler.classes", "AlwaysSampler")).build();
+    boolean gotException = false;
+    TraceScope myScope = tracer.newScope("myScope");
+    myScope.detach();
+    try {
+      myScope.detach();
+    } catch (RuntimeException e) {
+      assertThat(e.getMessage(),
+          containsString("it is already detached."));
+      gotException = true;
+    }
+    assertTrue("Expected to get exception because of double TraceScope " +
+        "detach.", gotException);
+    tracer.close();
+  }
+
+  /**
+   * Test calling detach() two times on a scope object.
+   */
+  @Test
+  public void TestDoubleDetachOnNullScope() throws Exception {
+    Tracer tracer = new Tracer.Builder().
+        name("TestDoubleDetachOnNullScope").
+        tracerPool(new TracerPool("TestDoubleDetachOnNullScope")).
+        conf(HTraceConfiguration.
+            fromKeyValuePairs("sampler.classes", "NeverSampler")).build();
+    boolean gotException = false;
+    TraceScope myScope = tracer.newScope("myScope");
+    myScope.detach();
+    try {
+      myScope.detach();
+    } catch (RuntimeException e) {
+      assertThat(e.getMessage(),
+          containsString("it is already detached."));
+      gotException = true;
+    }
+    assertTrue("Expected to get exception because of double TraceScope " +
+        "detach on NullScope.", gotException);
+    tracer.close();
+  }
+
+  /**
+   * Test calling reattach() two times on a scope object.
+   */
+  @Test
+  public void TestDoubleReattachIsCaught() throws Exception {
+    Tracer tracer = new Tracer.Builder().
+        name("TestDoubleReattach").
+        tracerPool(new TracerPool("TestDoubleReattachIsCaught")).
+        conf(HTraceConfiguration.
+            fromKeyValuePairs("sampler.classes", "AlwaysSampler")).build();
+    boolean gotException = false;
+    TraceScope myScope = tracer.newScope("myScope");
+    myScope.detach();
+    myScope.reattach();
+    try {
+      myScope.reattach();
+    } catch (RuntimeException e) {
+      assertThat(e.getMessage(),
+          containsString("it is not detached."));
+      gotException = true;
+    }
+    assertTrue("Expected to get exception because of double TraceScope " +
+        "reattach.", gotException);
+    tracer.close();
+  }
+
+  private static class ScopeHolder {
+    TraceScope scope;
+
+    void set(TraceScope scope) {
+      this.scope = scope;
+    }
+  }
+
+  /**
+   * Test correctly passing spans between threads using detach().
+   */
+  @Test
+  public void TestPassingSpanBetweenThreads() throws Exception {
+    final Tracer tracer = new Tracer.Builder().
+        name("TestPassingSpanBetweenThreads").
+        tracerPool(new TracerPool("TestPassingSpanBetweenThreads")).
+        conf(HTraceConfiguration.
+            fromKeyValuePairs("sampler.classes", "AlwaysSampler")).build();
+    POJOSpanReceiver receiver =
+        new POJOSpanReceiver(HTraceConfiguration.EMPTY);
+    tracer.getTracerPool().addReceiver(receiver);
+    final ScopeHolder scopeHolder = new ScopeHolder();
+    Thread th = new Thread(new Runnable() {
+      @Override
+      public void run() {
+        TraceScope workerScope = tracer.newScope("workerSpan");
+        workerScope.detach();
+        scopeHolder.set(workerScope);
+      }
+    });
+    th.start();
+    th.join();
+    TraceScope workerScope = scopeHolder.scope;
+    SpanId workerScopeId = workerScope.getSpan().getSpanId();
+
+    // Create new scope whose parent is the worker thread's span.
+    workerScope.reattach();
+    TraceScope nested = tracer.newScope("nested");
+    nested.close();
+    // Create another span which also descends from the worker thread's span.
+    TraceScope nested2 = tracer.newScope("nested2");
+    nested2.close();
+
+    // Close the worker thread's span.
+    workerScope.close();
+
+    // We can create another descendant, even though the worker thread's span
+    // has been stopped.
+    TraceScope lateChildScope = tracer.newScope("lateChild", workerScopeId);
+    lateChildScope.close();
+    tracer.close();
+
+    TraceGraph traceGraph = new TraceGraph(receiver.getSpans());
+    Collection<Span> rootSpans =
+        traceGraph.getSpansByParent().find(SpanId.INVALID);
+    Assert.assertEquals(1, rootSpans.size());
+    Assert.assertEquals(workerScopeId,
+        rootSpans.iterator().next().getSpanId());
+    Collection<Span> childSpans =
+        traceGraph.getSpansByParent().find(workerScopeId);
+    Assert.assertEquals(3, childSpans.size());
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/3c20489f/htrace-core4/src/test/java/org/apache/htrace/core/TestCountSampler.java
----------------------------------------------------------------------
diff --git a/htrace-core4/src/test/java/org/apache/htrace/core/TestCountSampler.java b/htrace-core4/src/test/java/org/apache/htrace/core/TestCountSampler.java
new file mode 100644
index 0000000..e26115d
--- /dev/null
+++ b/htrace-core4/src/test/java/org/apache/htrace/core/TestCountSampler.java
@@ -0,0 +1,41 @@
+/*
+ * 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 org.junit.Assert;
+import org.junit.Test;
+
+public class TestCountSampler {
+
+  @Test
+  public void testNext() {
+    CountSampler half = new CountSampler(HTraceConfiguration.
+        fromKeyValuePairs("sampler.frequency", "2"));
+    CountSampler hundred = new CountSampler(HTraceConfiguration.
+        fromKeyValuePairs("sampler.frequency", "100"));
+    int halfCount = 0;
+    int hundredCount = 0;
+    for (int i = 0; i < 200; i++) {
+      if (half.next())
+        halfCount++;
+      if (hundred.next())
+        hundredCount++;
+    }
+    Assert.assertEquals(2, hundredCount);
+    Assert.assertEquals(100, halfCount);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/3c20489f/htrace-core4/src/test/java/org/apache/htrace/core/TestHTrace.java
----------------------------------------------------------------------
diff --git a/htrace-core4/src/test/java/org/apache/htrace/core/TestHTrace.java b/htrace-core4/src/test/java/org/apache/htrace/core/TestHTrace.java
new file mode 100644
index 0000000..06ca189
--- /dev/null
+++ b/htrace-core4/src/test/java/org/apache/htrace/core/TestHTrace.java
@@ -0,0 +1,130 @@
+/*
+ * 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.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.htrace.core.TraceGraph.SpansByParent;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestHTrace {
+  @Test
+  public void TestTracerCreateAndClose() throws Exception {
+    Tracer tracer = new Tracer.Builder().
+        name("TestSimpleScope").
+        tracerPool(new TracerPool("TestTracerCreateAndClose")).
+        conf(HTraceConfiguration.fromKeyValuePairs(
+            "sampler.classes", "AlwaysSampler")).
+        build();
+    POJOSpanReceiver receiver =
+        new POJOSpanReceiver(HTraceConfiguration.EMPTY);
+    tracer.getTracerPool().addReceiver(receiver);
+    tracer.close();
+    Assert.assertTrue(receiver.getSpans().isEmpty());
+  }
+
+  @Test
+  public void TestSimpleScope() throws Exception {
+    Tracer tracer = new Tracer.Builder().
+        name("TestSimpleScope").
+        tracerPool(new TracerPool("TestSimpleScope")).
+        conf(HTraceConfiguration.fromKeyValuePairs(
+            "sampler.classes", "AlwaysSampler")).
+        build();
+    POJOSpanReceiver receiver =
+        new POJOSpanReceiver(HTraceConfiguration.EMPTY);
+    tracer.getTracerPool().addReceiver(receiver);
+    TraceScope scope = tracer.newScope("Foo");
+    scope.close();
+    tracer.close();
+    Assert.assertEquals(1, receiver.getSpans().size());
+    Span span = receiver.getSpans().iterator().next();
+    Assert.assertEquals(0, span.getParents().length);
+  }
+
+  @Test
+  public void TestCreateSpans() throws Exception {
+    Tracer tracer = new Tracer.Builder().
+        name("TestCreateSpans").
+        tracerPool(new TracerPool("TestCreateSpans")).
+        conf(HTraceConfiguration.fromKeyValuePairs(
+            "sampler.classes", "AlwaysSampler")).
+        build();
+    POJOSpanReceiver receiver =
+        new POJOSpanReceiver(HTraceConfiguration.EMPTY);
+    tracer.getTracerPool().addReceiver(receiver);
+    TraceCreator traceCreator = new TraceCreator(tracer);
+    traceCreator.createSampleRpcTrace();
+    traceCreator.createSimpleTrace();
+    traceCreator.createThreadedTrace();
+    tracer.close();
+    TraceGraph traceGraph = new TraceGraph(receiver.getSpans());
+    Collection<Span> roots = traceGraph.getSpansByParent().find(SpanId.INVALID);
+    Assert.assertTrue("Trace tree must have roots", !roots.isEmpty());
+    Assert.assertEquals(3, roots.size());
+
+    Map<String, Span> descriptionToRootSpan = new HashMap<String, Span>();
+    for (Span root : roots) {
+      descriptionToRootSpan.put(root.getDescription(), root);
+    }
+
+    Assert.assertTrue(descriptionToRootSpan.keySet().contains(
+        TraceCreator.RPC_TRACE_ROOT));
+    Assert.assertTrue(descriptionToRootSpan.keySet().contains(
+        TraceCreator.SIMPLE_TRACE_ROOT));
+    Assert.assertTrue(descriptionToRootSpan.keySet().contains(
+        TraceCreator.THREADED_TRACE_ROOT));
+
+    SpansByParent spansByParentId = traceGraph.getSpansByParent();
+
+    Span rpcTraceRoot = descriptionToRootSpan.get(TraceCreator.RPC_TRACE_ROOT);
+    Assert.assertEquals(1, spansByParentId.find(rpcTraceRoot.getSpanId()).size());
+
+    Span rpcTraceChild1 = spansByParentId.find(rpcTraceRoot.getSpanId())
+        .iterator().next();
+    Assert.assertEquals(1, spansByParentId.find(rpcTraceChild1.getSpanId()).size());
+
+    Span rpcTraceChild2 = spansByParentId.find(rpcTraceChild1.getSpanId())
+        .iterator().next();
+    Assert.assertEquals(1, spansByParentId.find(rpcTraceChild2.getSpanId()).size());
+
+    Span rpcTraceChild3 = spansByParentId.find(rpcTraceChild2.getSpanId())
+        .iterator().next();
+    Assert.assertEquals(0, spansByParentId.find(rpcTraceChild3.getSpanId()).size());
+  }
+
+  @Test(timeout=60000)
+  public void testRootSpansHaveNonZeroSpanId() throws Exception {
+    Tracer tracer = new Tracer.Builder().
+        name("testRootSpansHaveNonZeroSpanId").
+        tracerPool(new TracerPool("testRootSpansHaveNonZeroSpanId")).
+        conf(HTraceConfiguration.fromKeyValuePairs(
+            "sampler.classes", "AlwaysSampler")).build();
+    TraceScope scope = tracer.
+        newScope("myRootSpan", new SpanId(100L, 200L));
+    Assert.assertNotNull(scope);
+    Assert.assertEquals("myRootSpan", scope.getSpan().getDescription());
+    Assert.assertTrue(scope.getSpan().getSpanId().isValid());
+    Assert.assertEquals(100L, scope.getSpan().getSpanId().getHigh());
+    Assert.assertNotEquals(0L, scope.getSpan().getSpanId().getLow());
+    scope.close();
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/3c20489f/htrace-core4/src/test/java/org/apache/htrace/core/TestHTraceConfiguration.java
----------------------------------------------------------------------
diff --git a/htrace-core4/src/test/java/org/apache/htrace/core/TestHTraceConfiguration.java b/htrace-core4/src/test/java/org/apache/htrace/core/TestHTraceConfiguration.java
new file mode 100644
index 0000000..7ca897f
--- /dev/null
+++ b/htrace-core4/src/test/java/org/apache/htrace/core/TestHTraceConfiguration.java
@@ -0,0 +1,62 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+
+public class TestHTraceConfiguration {
+  @Test
+  public void testGetBoolean() throws Exception {
+
+    Map<String, String> m = new HashMap<String, String>();
+    m.put("testTrue", " True");
+    m.put("testFalse", "falsE ");
+    HTraceConfiguration configuration = HTraceConfiguration.fromMap(m);
+
+    // Tests for value being there
+    assertTrue(configuration.getBoolean("testTrue", false));
+    assertFalse(configuration.getBoolean("testFalse", true));
+
+    // Test for absent
+    assertTrue(configuration.getBoolean("absent", true));
+    assertFalse(configuration.getBoolean("absent", false));
+  }
+
+  @Test
+  public void testGetInt() throws Exception {
+    Map<String, String> m = new HashMap<String, String>();
+    m.put("a", "100");
+    m.put("b", "0");
+    m.put("c", "-100");
+    m.put("d", "5");
+
+    HTraceConfiguration configuration = HTraceConfiguration.fromMap(m);
+    assertEquals(100, configuration.getInt("a", -999));
+    assertEquals(0, configuration.getInt("b", -999));
+    assertEquals(-100, configuration.getInt("c", -999));
+    assertEquals(5, configuration.getInt("d", -999));
+    assertEquals(-999, configuration.getInt("absent", -999));
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/3c20489f/htrace-core4/src/test/java/org/apache/htrace/core/TestLocalFileSpanReceiver.java
----------------------------------------------------------------------
diff --git a/htrace-core4/src/test/java/org/apache/htrace/core/TestLocalFileSpanReceiver.java b/htrace-core4/src/test/java/org/apache/htrace/core/TestLocalFileSpanReceiver.java
new file mode 100644
index 0000000..9388707
--- /dev/null
+++ b/htrace-core4/src/test/java/org/apache/htrace/core/TestLocalFileSpanReceiver.java
@@ -0,0 +1,65 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Test;
+
+public class TestLocalFileSpanReceiver {
+  @Test
+  public void testUniqueLocalTraceFileName() {
+    String filename1 = LocalFileSpanReceiver.getUniqueLocalTraceFileName();
+    String filename2 = LocalFileSpanReceiver.getUniqueLocalTraceFileName();
+    boolean eq = filename1.equals(filename2);
+    if (System.getProperty("os.name").startsWith("Linux")) {
+      // ${java.io.tmpdir}/[pid]
+      assertTrue(eq);
+    } else {
+      // ${java.io.tmpdir}/[random UUID]
+      assertFalse(eq);
+    }
+  }
+
+  @Test
+  public void testWriteToLocalFile() throws IOException {
+    String traceFileName = LocalFileSpanReceiver.getUniqueLocalTraceFileName();
+    Tracer tracer = new Tracer.Builder().
+        name("testWriteToLocalFileTracer").
+        tracerPool(new TracerPool("testWriteToLocalFile")).
+        conf(HTraceConfiguration.fromKeyValuePairs(
+            "sampler.classes", "AlwaysSampler",
+            "span.receiver.classes", LocalFileSpanReceiver.class.getName(),
+            "local.file.span.receiver.path", traceFileName,
+            "tracer.id", "%{tname}")).
+        build();
+    TraceScope scope = tracer.newScope("testWriteToLocalFile");
+    scope.close();
+    tracer.close();
+
+    ObjectMapper mapper = new ObjectMapper();
+    MilliSpan span = mapper.readValue(new File(traceFileName), MilliSpan.class);
+    assertEquals("testWriteToLocalFile", span.getDescription());
+    assertEquals("testWriteToLocalFileTracer", span.getTracerId());
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/3c20489f/htrace-core4/src/test/java/org/apache/htrace/core/TestMilliSpan.java
----------------------------------------------------------------------
diff --git a/htrace-core4/src/test/java/org/apache/htrace/core/TestMilliSpan.java b/htrace-core4/src/test/java/org/apache/htrace/core/TestMilliSpan.java
new file mode 100644
index 0000000..7ce1fdb
--- /dev/null
+++ b/htrace-core4/src/test/java/org/apache/htrace/core/TestMilliSpan.java
@@ -0,0 +1,145 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ThreadLocalRandom;
+
+public class TestMilliSpan {
+  private void compareSpans(Span expected, Span got) throws Exception {
+    assertEquals(expected.getStartTimeMillis(), got.getStartTimeMillis());
+    assertEquals(expected.getStopTimeMillis(), got.getStopTimeMillis());
+    assertEquals(expected.getDescription(), got.getDescription());
+    assertEquals(expected.getSpanId(), got.getSpanId());
+    assertEquals(expected.getTracerId(), got.getTracerId());
+    assertTrue(Arrays.equals(expected.getParents(), got.getParents()));
+    Map<String, String> expectedT = expected.getKVAnnotations();
+    Map<String, String> gotT = got.getKVAnnotations();
+    if (expectedT == null) {
+      assertEquals(null, gotT);
+    } else {
+      assertEquals(expectedT.size(), gotT.size());
+      for (String key : expectedT.keySet()) {
+        assertEquals(expectedT.get(key), gotT.get(key));
+      }
+    }
+    List<TimelineAnnotation> expectedTimeline =
+        expected.getTimelineAnnotations();
+    List<TimelineAnnotation> gotTimeline =
+        got.getTimelineAnnotations();
+    if (expectedTimeline == null) {
+      assertEquals(null, gotTimeline);
+    } else {
+      assertEquals(expectedTimeline.size(), gotTimeline.size());
+      Iterator<TimelineAnnotation> iter = gotTimeline.iterator();
+      for (TimelineAnnotation expectedAnn : expectedTimeline) {
+        TimelineAnnotation gotAnn =  iter.next();
+        assertEquals(expectedAnn.getMessage(), gotAnn.getMessage());
+        assertEquals(expectedAnn.getTime(), gotAnn.getTime());
+      }
+    }
+  }
+
+  @Test
+  public void testJsonSerialization() throws Exception {
+    MilliSpan span = new MilliSpan.Builder().
+        description("foospan").
+        begin(123L).
+        end(456L).
+        parents(new SpanId[] { new SpanId(7L, 7L) }).
+        tracerId("b2404.halxg.com:8080").
+        spanId(new SpanId(7L, 8L)).
+        build();
+    String json = span.toJson();
+    MilliSpan dspan = MilliSpan.fromJson(json);
+    compareSpans(span, dspan);
+  }
+
+  @Test
+  public void testJsonSerializationWithNegativeLongValue() throws Exception {
+    MilliSpan span = new MilliSpan.Builder().
+        description("foospan").
+        begin(-1L).
+        end(-1L).
+        parents(new SpanId[] { new SpanId(-1L, -1L) }).
+        tracerId("b2404.halxg.com:8080").
+        spanId(new SpanId(-1L, -2L)).
+        build();
+    String json = span.toJson();
+    MilliSpan dspan = MilliSpan.fromJson(json);
+    compareSpans(span, dspan);
+  }
+
+  @Test
+  public void testJsonSerializationWithRandomLongValue() throws Exception {
+    SpanId parentId = SpanId.fromRandom();
+    MilliSpan span = new MilliSpan.Builder().
+        description("foospan").
+        begin(ThreadLocalRandom.current().nextLong()).
+        end(ThreadLocalRandom.current().nextLong()).
+        parents(new SpanId[] { parentId }).
+        tracerId("b2404.halxg.com:8080").
+        spanId(parentId.newChildId()).
+        build();
+    String json = span.toJson();
+    MilliSpan dspan = MilliSpan.fromJson(json);
+    compareSpans(span, dspan);
+  }
+
+  @Test
+  public void testJsonSerializationWithOptionalFields() throws Exception {
+    MilliSpan.Builder builder = new MilliSpan.Builder().
+        description("foospan").
+        begin(300).
+        end(400).
+        parents(new SpanId[] { }).
+        tracerId("b2408.halxg.com:8080").
+        spanId(new SpanId(111111111L, 111111111L));
+    Map<String, String> traceInfo = new HashMap<String, String>();
+    traceInfo.put("abc", "123");
+    traceInfo.put("def", "456");
+    builder.traceInfo(traceInfo);
+    List<TimelineAnnotation> timeline = new LinkedList<TimelineAnnotation>();
+    timeline.add(new TimelineAnnotation(310L, "something happened"));
+    timeline.add(new TimelineAnnotation(380L, "something else happened"));
+    timeline.add(new TimelineAnnotation(390L, "more things"));
+    builder.timeline(timeline);
+    MilliSpan span = builder.build();
+    String json = span.toJson();
+    MilliSpan dspan = MilliSpan.fromJson(json);
+    compareSpans(span, dspan);
+  }
+
+  @Test
+  public void testJsonSerializationWithFieldsNotSet() throws Exception {
+    MilliSpan span = new MilliSpan.Builder().build();
+    String json = span.toJson();
+    MilliSpan dspan = MilliSpan.fromJson(json);
+    compareSpans(span, dspan);
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/3c20489f/htrace-core4/src/test/java/org/apache/htrace/core/TestNullScope.java
----------------------------------------------------------------------
diff --git a/htrace-core4/src/test/java/org/apache/htrace/core/TestNullScope.java b/htrace-core4/src/test/java/org/apache/htrace/core/TestNullScope.java
new file mode 100644
index 0000000..c8ed7f1
--- /dev/null
+++ b/htrace-core4/src/test/java/org/apache/htrace/core/TestNullScope.java
@@ -0,0 +1,43 @@
+/*
+ * 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 org.junit.Assert;
+import org.junit.Test;
+
+public class TestNullScope {
+  private void verifyNullScope(TraceScope scope) {
+    Assert.assertTrue(null == scope.getSpan());
+    Assert.assertFalse(scope.detached);
+    scope.detach();
+    Assert.assertTrue(scope.detached);
+    scope.reattach();
+    Assert.assertFalse(scope.detached);
+    scope.close();
+  }
+
+  @Test
+  public void testNullScope() {
+    Tracer tracer = new Tracer.Builder().
+        name("testNullScope").
+        tracerPool(new TracerPool("testNullScope")).
+        conf(HTraceConfiguration.EMPTY).
+        build();
+    verifyNullScope(tracer.newScope("testNullScope"));
+    verifyNullScope(tracer.newNullScope());
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/3c20489f/htrace-core4/src/test/java/org/apache/htrace/core/TestSampler.java
----------------------------------------------------------------------
diff --git a/htrace-core4/src/test/java/org/apache/htrace/core/TestSampler.java b/htrace-core4/src/test/java/org/apache/htrace/core/TestSampler.java
new file mode 100644
index 0000000..2305d9f
--- /dev/null
+++ b/htrace-core4/src/test/java/org/apache/htrace/core/TestSampler.java
@@ -0,0 +1,100 @@
+/*
+ * 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.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestSampler {
+  private Sampler[] getSamplersFromConf(HTraceConfiguration conf) {
+    Tracer tracer = new Tracer.Builder().
+        name("MyTracer").
+        tracerPool(new TracerPool("getSamplersFromConf")).
+        conf(conf).
+        build();
+    Sampler[] samplers = tracer.getSamplers();
+    tracer.close();
+    return samplers;
+  }
+
+  private void checkArrayContains(List<Class<? extends Sampler>> expected,
+                                  Sampler[] samplers) {
+    for (Iterator<Class<? extends Sampler>> iter = expected.iterator();
+         iter.hasNext(); ) {
+      Class<? extends Sampler> samplerClass = iter.next();
+      boolean found = false;
+      for (int i = 0; i < samplers.length; i++) {
+        if (samplers[i] != null) {
+          if (samplers[i].getClass().equals(samplerClass)) {
+            samplers[i] = null;
+            found = true;
+            break;
+          }
+        }
+      }
+      Assert.assertTrue("Failed to find sampler class " +
+          samplerClass.getName(), found);
+    }
+    for (int i = 0; i < samplers.length; i++) {
+      if (samplers[i] != null) {
+        Assert.fail("Got extra sampler of type " +
+            samplers.getClass().getName());
+      }
+    }
+  }
+
+  private void checkArrayContains(Class<? extends Sampler> expected, Sampler[] samplers) {
+    LinkedList<Class<? extends Sampler>> expectedList =
+        new LinkedList<Class<? extends Sampler>>();
+    expectedList.add(expected);
+    checkArrayContains(expectedList, samplers);
+  }
+
+  @Test
+  public void testTracerBuilderCreatesCorrectSamplers() {
+    Sampler[] samplers = getSamplersFromConf(HTraceConfiguration.
+        fromKeyValuePairs("sampler.classes", "AlwaysSampler"));
+    checkArrayContains(AlwaysSampler.class, samplers);
+
+    samplers = getSamplersFromConf(HTraceConfiguration.
+        fromKeyValuePairs("sampler.classes", "NeverSampler"));
+    checkArrayContains(NeverSampler.class, samplers);
+
+    samplers = getSamplersFromConf(HTraceConfiguration.
+        fromKeyValuePairs("sampler.classes", "NonExistentSampler"));
+    Assert.assertEquals(0, samplers.length);
+
+    samplers = getSamplersFromConf(HTraceConfiguration.EMPTY);
+    Assert.assertEquals(0, samplers.length);
+  }
+
+  @Test
+  public void testAlwaysSampler() {
+    AlwaysSampler sampler = new AlwaysSampler(HTraceConfiguration.EMPTY);
+    Assert.assertTrue(sampler.next());
+  }
+
+  @Test
+  public void testNeverSampler() {
+    NeverSampler sampler = new NeverSampler(HTraceConfiguration.EMPTY);
+    Assert.assertTrue(!sampler.next());
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/3c20489f/htrace-core4/src/test/java/org/apache/htrace/core/TestSpanId.java
----------------------------------------------------------------------
diff --git a/htrace-core4/src/test/java/org/apache/htrace/core/TestSpanId.java b/htrace-core4/src/test/java/org/apache/htrace/core/TestSpanId.java
new file mode 100644
index 0000000..bb57368
--- /dev/null
+++ b/htrace-core4/src/test/java/org/apache/htrace/core/TestSpanId.java
@@ -0,0 +1,72 @@
+/*
+ * 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.util.Random;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestSpanId {
+  private void testRoundTrip(SpanId id) throws Exception {
+    String str = id.toString();
+    SpanId id2 = SpanId.fromString(str);
+    Assert.assertEquals(id, id2);
+  }
+
+  @Test
+  public void testToStringAndFromString() throws Exception {
+    testRoundTrip(SpanId.INVALID);
+    testRoundTrip(new SpanId(0x1234567812345678L, 0x1234567812345678L));
+    testRoundTrip(new SpanId(0xf234567812345678L, 0xf234567812345678L));
+    testRoundTrip(new SpanId(0xffffffffffffffffL, 0xffffffffffffffffL));
+    Random rand = new Random(12345);
+    for (int i = 0; i < 100; i++) {
+      testRoundTrip(new SpanId(rand.nextLong(), rand.nextLong()));
+    }
+  }
+
+  @Test
+  public void testValidAndInvalidIds() throws Exception {
+    Assert.assertFalse(SpanId.INVALID.isValid());
+    Assert.assertTrue(
+        new SpanId(0x1234567812345678L, 0x1234567812345678L).isValid());
+    Assert.assertTrue(
+        new SpanId(0xf234567812345678L, 0xf234567812345678L).isValid());
+  }
+
+  private void expectLessThan(SpanId a, SpanId b) throws Exception {
+    int cmp = a.compareTo(b);
+    Assert.assertTrue("Expected " + a + " to be less than " + b,
+        (cmp < 0));
+    int cmp2 = b.compareTo(a);
+    Assert.assertTrue("Expected " + b + " to be greater than " + a,
+        (cmp2 > 0));
+  }
+
+  @Test
+  public void testIdComparisons() throws Exception {
+    expectLessThan(new SpanId(0x0000000000000001L, 0x0000000000000001L),
+                   new SpanId(0x0000000000000001L, 0x0000000000000002L));
+    expectLessThan(new SpanId(0x0000000000000001L, 0x0000000000000001L),
+                   new SpanId(0x0000000000000002L, 0x0000000000000000L));
+    expectLessThan(SpanId.INVALID,
+                   new SpanId(0xffffffffffffffffL, 0xffffffffffffffffL));
+    expectLessThan(new SpanId(0x1234567812345678L, 0x1234567812345678L),
+                   new SpanId(0x1234567812345678L, 0xf234567812345678L));
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/3c20489f/htrace-core4/src/test/java/org/apache/htrace/core/TestSpanReceiverBuilder.java
----------------------------------------------------------------------
diff --git a/htrace-core4/src/test/java/org/apache/htrace/core/TestSpanReceiverBuilder.java b/htrace-core4/src/test/java/org/apache/htrace/core/TestSpanReceiverBuilder.java
new file mode 100644
index 0000000..b97d624
--- /dev/null
+++ b/htrace-core4/src/test/java/org/apache/htrace/core/TestSpanReceiverBuilder.java
@@ -0,0 +1,127 @@
+/*
+ * 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 org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+public class TestSpanReceiverBuilder {
+  private static final Log LOG =
+      LogFactory.getLog(TestSpanReceiverBuilder.class);
+
+  private List<SpanReceiver> createSpanReceivers(String classes) {
+    Tracer tracer = new Tracer.Builder().
+        name("MyTracer").
+        tracerPool(new TracerPool("createSpanReceivers")).
+        conf(HTraceConfiguration.fromKeyValuePairs(
+            "span.receiver.classes", classes)).
+        build();
+    SpanReceiver[] receivers = tracer.getTracerPool().getReceivers();
+    tracer.close();
+    LinkedList<SpanReceiver> receiverList = new LinkedList<SpanReceiver>();
+    for (SpanReceiver item: receivers) {
+      receiverList.add(item);
+    }
+    return receiverList;
+  }
+
+  @Test
+  public void TestCreateStandardSpanReceivers() {
+    List<SpanReceiver> receivers;
+    receivers = createSpanReceivers("");
+    Assert.assertTrue(receivers.isEmpty());
+    receivers = createSpanReceivers("POJOSpanReceiver");
+    Assert.assertTrue(receivers.get(0).getClass().getName().
+        equals("org.apache.htrace.core.POJOSpanReceiver"));
+    receivers = createSpanReceivers(
+               "org.apache.htrace.core.StandardOutSpanReceiver");
+    Assert.assertTrue(receivers.get(0).getClass().getName().
+        equals("org.apache.htrace.core.StandardOutSpanReceiver"));
+    receivers = createSpanReceivers(
+               "POJOSpanReceiver;StandardOutSpanReceiver");
+    Assert.assertEquals(2, receivers.size());
+    for (Iterator<SpanReceiver> iter = receivers.iterator(); iter.hasNext();) {
+      SpanReceiver receiver = iter.next();
+      if (receiver.getClass().getName().equals(
+          "org.apache.htrace.core.POJOSpanReceiver")) {
+        iter.remove();
+        break;
+      }
+    }
+    for (Iterator<SpanReceiver> iter = receivers.iterator(); iter.hasNext();) {
+      SpanReceiver receiver = iter.next();
+      if (receiver.getClass().getName().equals(
+          "org.apache.htrace.core.StandardOutSpanReceiver")) {
+        iter.remove();
+        break;
+      }
+    }
+    Assert.assertEquals(0, receivers.size());
+  }
+
+  public static class GoodSpanReceiver extends SpanReceiver {
+    public GoodSpanReceiver(HTraceConfiguration conf) {
+    }
+
+    @Override
+    public void receiveSpan(Span span) {
+    }
+
+    @Override
+    public void close() throws IOException {
+    }
+  }
+
+  public static class BadSpanReceiver extends SpanReceiver {
+    public BadSpanReceiver(HTraceConfiguration conf) {
+      throw new RuntimeException("Can't create BadSpanReceiver");
+    }
+
+    @Override
+    public void receiveSpan(Span span) {
+    }
+
+    @Override
+    public void close() throws IOException {
+    }
+  }
+
+  /**
+   * Test trying to create a SpanReceiver that experiences an error in the
+   * constructor.
+   */
+  @Test
+  public void testGetSpanReceiverWithConstructorError() throws Exception {
+    List<SpanReceiver> receivers;
+    receivers = createSpanReceivers(
+        GoodSpanReceiver.class.getName());
+    Assert.assertEquals(1, receivers.size());
+    Assert.assertTrue(receivers.get(0).getClass().getName().
+        contains("GoodSpanReceiver"));
+    receivers = createSpanReceivers(
+        BadSpanReceiver.class.getName());
+    Assert.assertEquals(0, receivers.size());
+  }
+}