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 2014/12/30 23:07:54 UTC

incubator-htrace git commit: HTRACE-31. more JSON serialization fixes (cmccabe)

Repository: incubator-htrace
Updated Branches:
  refs/heads/master bc63ac8cc -> 6b36b1aa6


HTRACE-31. more JSON serialization fixes (cmccabe)


Project: http://git-wip-us.apache.org/repos/asf/incubator-htrace/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-htrace/commit/6b36b1aa
Tree: http://git-wip-us.apache.org/repos/asf/incubator-htrace/tree/6b36b1aa
Diff: http://git-wip-us.apache.org/repos/asf/incubator-htrace/diff/6b36b1aa

Branch: refs/heads/master
Commit: 6b36b1aa677d1242ac850fdb05b6016842e881e3
Parents: bc63ac8
Author: Colin P. Mccabe <cm...@apache.org>
Authored: Tue Dec 30 14:06:38 2014 -0800
Committer: Colin P. Mccabe <cm...@apache.org>
Committed: Tue Dec 30 14:07:43 2014 -0800

----------------------------------------------------------------------
 htrace-core/pom.xml                             |  15 +-
 .../src/go/src/org/apache/htrace/common/span.go |  19 +-
 .../src/org/apache/htrace/common/span_test.go   |   4 +-
 .../src/go/src/org/apache/htrace/htrace/cmd.go  |   4 +-
 .../go/src/org/apache/htrace/htraced/rest.go    |   6 +-
 .../src/main/java/org/apache/htrace/Span.java   |  48 +++++
 .../java/org/apache/htrace/impl/MilliSpan.java  | 206 +++++++++++++++----
 .../org/apache/htrace/impl/TestMilliSpan.java   | 120 +++++++++++
 8 files changed, 368 insertions(+), 54 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/6b36b1aa/htrace-core/pom.xml
----------------------------------------------------------------------
diff --git a/htrace-core/pom.xml b/htrace-core/pom.xml
index 8ce720d..54c6c6c 100644
--- a/htrace-core/pom.xml
+++ b/htrace-core/pom.xml
@@ -61,8 +61,8 @@ language governing permissions and limitations under the License. -->
                   <shadedPattern>org.apache.htrace.commons.logging</shadedPattern>
                 </relocation>
                 <relocation>
-                  <pattern>org.mortbay</pattern>
-                  <shadedPattern>org.apache.htrace.mortbay</shadedPattern>
+                  <pattern>com.fasterxml.jackson.core</pattern>
+                  <shadedPattern>org.apache.htrace.fasterxml.jackson.core</shadedPattern>
                 </relocation>
               </relocations>
             </configuration>
@@ -137,9 +137,14 @@ language governing permissions and limitations under the License. -->
       <artifactId>commons-logging</artifactId>
     </dependency>
     <dependency>
-      <groupId>org.mortbay.jetty</groupId>
-      <artifactId>jetty-util</artifactId>
-      <version>6.1.26</version>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-core</artifactId>
+      <version>2.4.0</version>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <version>2.4.0</version>
     </dependency>
   </dependencies>
 </project>

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/6b36b1aa/htrace-core/src/go/src/org/apache/htrace/common/span.go
----------------------------------------------------------------------
diff --git a/htrace-core/src/go/src/org/apache/htrace/common/span.go b/htrace-core/src/go/src/org/apache/htrace/common/span.go
index 81fe0e8..540ba12 100644
--- a/htrace-core/src/go/src/org/apache/htrace/common/span.go
+++ b/htrace-core/src/go/src/org/apache/htrace/common/span.go
@@ -38,14 +38,14 @@ import (
 type TraceInfoMap map[string][]byte
 
 type TimelineAnnotation struct {
-	Time int64  `json:"time,string"`
-	Msg  string `json:"msg"`
+	Time int64  `json:"t"`
+	Msg  string `json:"m"`
 }
 
 type SpanId int64
 
 func (id SpanId) String() string {
-	return fmt.Sprintf("%08x", id)
+	return fmt.Sprintf("%016x", id)
 }
 
 func (id SpanId) Val() int64 {
@@ -56,13 +56,18 @@ func (id SpanId) MarshalJSON() ([]byte, error) {
 	return []byte(`"` + fmt.Sprintf("%016x", uint64(id)) + `"`), nil
 }
 
-func (id SpanId) UnMarshalJSON() ([]byte, error) {
-	return []byte(`"` + strconv.FormatUint(uint64(id), 16) + `"`), nil
+func (id *SpanId) UnMarshalJSON(b []byte) error {
+	v, err := strconv.ParseUint(string(b), 16, 64)
+	if err != nil {
+		return err
+	}
+	*id = SpanId(v)
+	return nil
 }
 
 type SpanData struct {
-	Begin               int64                `json:"b,string"`
-	End                 int64                `json:"e,string"`
+	Begin               int64                `json:"b"`
+	End                 int64                `json:"e"`
 	Description         string               `json:"d"`
 	TraceId             SpanId               `json:"i"`
 	Parents             []SpanId             `json:"p"`

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/6b36b1aa/htrace-core/src/go/src/org/apache/htrace/common/span_test.go
----------------------------------------------------------------------
diff --git a/htrace-core/src/go/src/org/apache/htrace/common/span_test.go b/htrace-core/src/go/src/org/apache/htrace/common/span_test.go
index 1d098fc..f218b3a 100644
--- a/htrace-core/src/go/src/org/apache/htrace/common/span_test.go
+++ b/htrace-core/src/go/src/org/apache/htrace/common/span_test.go
@@ -35,7 +35,7 @@ func TestSpanToJson(t *testing.T) {
 			ProcessId:   "testProcessId",
 		}}
 	ExpectStrEqual(t,
-		`{"s":"2000000000000000","b":"123","e":"456","d":"getFileDescriptors","i":"00000000000003e7","p":[],"r":"testProcessId"}`,
+		`{"s":"2000000000000000","b":123,"e":456,"d":"getFileDescriptors","i":"00000000000003e7","p":[],"r":"testProcessId"}`,
 		string(span.ToJson()))
 }
 
@@ -61,6 +61,6 @@ func TestAnnotatedSpanToJson(t *testing.T) {
 			},
 		}}
 	ExpectStrEqual(t,
-		`{"s":"121f2e036d442000","b":"1234","e":"4567","d":"getFileDescriptors2","i":"00000000000003e7","p":[],"r":"testAnnotatedProcessId","t":[{"time":"7777","msg":"contactedServer"},{"time":"8888","msg":"passedFd"}]}`,
+		`{"s":"121f2e036d442000","b":1234,"e":4567,"d":"getFileDescriptors2","i":"00000000000003e7","p":[],"r":"testAnnotatedProcessId","t":[{"t":7777,"m":"contactedServer"},{"t":8888,"m":"passedFd"}]}`,
 		string(span.ToJson()))
 }

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/6b36b1aa/htrace-core/src/go/src/org/apache/htrace/htrace/cmd.go
----------------------------------------------------------------------
diff --git a/htrace-core/src/go/src/org/apache/htrace/htrace/cmd.go b/htrace-core/src/go/src/org/apache/htrace/htrace/cmd.go
index d4bb253..9633ba3 100644
--- a/htrace-core/src/go/src/org/apache/htrace/htrace/cmd.go
+++ b/htrace-core/src/go/src/org/apache/htrace/htrace/cmd.go
@@ -90,7 +90,7 @@ func printServerInfo(restAddr string) int {
 
 // Print information about a trace span.
 func doFindSpan(restAddr string, sid int64) int {
-	buf, err := makeRestRequest(restAddr, fmt.Sprintf("findSid?sid=%d", sid))
+	buf, err := makeRestRequest(restAddr, fmt.Sprintf("findSid?sid=%016x", sid))
 	if err != nil {
 		fmt.Printf("%s\n", err.Error())
 		return 1
@@ -113,7 +113,7 @@ func doFindSpan(restAddr string, sid int64) int {
 
 // Find information about the children of a span.
 func doFindChildren(restAddr string, sid int64, lim int) int {
-	buf, err := makeRestRequest(restAddr, fmt.Sprintf("findChildren?sid=%d&lim=%d", sid, lim))
+	buf, err := makeRestRequest(restAddr, fmt.Sprintf("findChildren?sid=%016x&lim=%d", sid, lim))
 	if err != nil {
 		fmt.Printf("%s\n", err.Error())
 		return 1

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/6b36b1aa/htrace-core/src/go/src/org/apache/htrace/htraced/rest.go
----------------------------------------------------------------------
diff --git a/htrace-core/src/go/src/org/apache/htrace/htraced/rest.go b/htrace-core/src/go/src/org/apache/htrace/htraced/rest.go
index f0bb2c1..5b97313 100644
--- a/htrace-core/src/go/src/org/apache/htrace/htraced/rest.go
+++ b/htrace-core/src/go/src/org/apache/htrace/htraced/rest.go
@@ -55,13 +55,13 @@ func (hand *dataStoreHandler) getReqField64(fieldName string, w http.ResponseWri
 		w.Write([]byte("No " + fieldName + " specified."))
 		return -1, false
 	}
-	val, err := strconv.ParseInt(str, 10, 64)
+	val, err := strconv.ParseUint(str, 16, 64)
 	if err != nil {
 		w.WriteHeader(http.StatusBadRequest)
 		w.Write([]byte("Error parsing " + fieldName + ": " + err.Error()))
 		return -1, false
 	}
-	return val, true
+	return int64(val), true
 }
 
 func (hand *dataStoreHandler) getReqField32(fieldName string, w http.ResponseWriter,
@@ -72,7 +72,7 @@ func (hand *dataStoreHandler) getReqField32(fieldName string, w http.ResponseWri
 		w.Write([]byte("No " + fieldName + " specified."))
 		return -1, false
 	}
-	val, err := strconv.ParseInt(str, 10, 32)
+	val, err := strconv.ParseUint(str, 16, 32)
 	if err != nil {
 		w.WriteHeader(http.StatusBadRequest)
 		w.Write([]byte("Error parsing " + fieldName + ": " + err.Error()))

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/6b36b1aa/htrace-core/src/main/java/org/apache/htrace/Span.java
----------------------------------------------------------------------
diff --git a/htrace-core/src/main/java/org/apache/htrace/Span.java b/htrace-core/src/main/java/org/apache/htrace/Span.java
index 50cec7c..b8af10d 100644
--- a/htrace-core/src/main/java/org/apache/htrace/Span.java
+++ b/htrace-core/src/main/java/org/apache/htrace/Span.java
@@ -16,6 +16,12 @@
  */
 package org.apache.htrace;
 
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
+import java.io.IOException;
 import java.util.List;
 import java.util.Map;
 
@@ -27,6 +33,7 @@ import java.util.Map;
  * Spans form a tree structure with the parent relationship. The first span in a
  * trace has no parent span.
  */
+@JsonSerialize(using = Span.SpanSerializer.class)
 public interface Span {
   public static final long ROOT_SPAN_ID = 0x74ace;
 
@@ -118,4 +125,45 @@ public interface Span {
    * Serialize to Json
    */
   String toJson();
+
+  public static class SpanSerializer extends JsonSerializer<Span> {
+    @Override
+    public void serialize(Span span, JsonGenerator jgen, SerializerProvider provider)
+        throws IOException {
+      jgen.writeStartObject();
+      jgen.writeStringField("i", String.format("%016x", span.getTraceId()));
+      jgen.writeStringField("s", String.format("%016x", span.getSpanId()));
+      jgen.writeNumberField("b", span.getStartTimeMillis());
+      jgen.writeNumberField("e", span.getStopTimeMillis());
+      jgen.writeStringField("d", span.getDescription());
+      jgen.writeStringField("r", span.getProcessId());
+      jgen.writeArrayFieldStart("p");
+      if (span.getParentId() != ROOT_SPAN_ID) {
+        jgen.writeString(String.format("%016x", span.getParentId()));
+      }
+      jgen.writeEndArray();
+      Map<byte[], byte[]> traceInfoMap = span.getKVAnnotations();
+      if (!traceInfoMap.isEmpty()) {
+        jgen.writeObjectFieldStart("n");
+        for (Map.Entry<byte[], byte[]> e : traceInfoMap.entrySet()) {
+          jgen.writeStringField(new String(e.getKey(), "UTF-8"),
+              new String(e.getValue(), "UTF-8"));
+        }
+        jgen.writeEndObject();
+      }
+      List<TimelineAnnotation> timelineAnnotations =
+          span.getTimelineAnnotations();
+      if (!timelineAnnotations.isEmpty()) {
+        jgen.writeArrayFieldStart("t");
+        for (TimelineAnnotation tl : timelineAnnotations) {
+          jgen.writeStartObject();
+          jgen.writeNumberField("t", tl.getTime());
+          jgen.writeStringField("m", tl.getMessage());
+          jgen.writeEndObject();
+        }
+        jgen.writeEndArray();
+      }
+      jgen.writeEndObject();
+    }
+  }
 }

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/6b36b1aa/htrace-core/src/main/java/org/apache/htrace/impl/MilliSpan.java
----------------------------------------------------------------------
diff --git a/htrace-core/src/main/java/org/apache/htrace/impl/MilliSpan.java b/htrace-core/src/main/java/org/apache/htrace/impl/MilliSpan.java
index b58839b..3932b79 100644
--- a/htrace-core/src/main/java/org/apache/htrace/impl/MilliSpan.java
+++ b/htrace-core/src/main/java/org/apache/htrace/impl/MilliSpan.java
@@ -16,15 +16,24 @@
  */
 package org.apache.htrace.impl;
 
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import org.apache.htrace.Span;
 import org.apache.htrace.TimelineAnnotation;
 import org.apache.htrace.Tracer;
-import org.mortbay.util.ajax.JSON;
+import com.fasterxml.jackson.databind.ObjectMapper;
 
+import java.io.IOException;
+import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.LinkedHashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
@@ -33,12 +42,13 @@ import java.util.Random;
  * A Span implementation that stores its information in milliseconds since the
  * epoch.
  */
+@JsonDeserialize(using = MilliSpan.MilliSpanDeserializer.class)
 public class MilliSpan implements Span {
 
   private static Random rand = new Random();
 
-  private long start;
-  private long stop;
+  private long begin;
+  private long end;
   private final String description;
   private final long traceId;
   private final long parents[];
@@ -52,6 +62,94 @@ public class MilliSpan implements Span {
     return new MilliSpan(description, traceId, spanId, rand.nextLong(), processId);
   }
 
+  /**
+   * The public interface for constructing a MilliSpan.
+   */
+  public static class Builder {
+    private long begin;
+    private long end;
+    private String description;
+    private long traceId;
+    private long parents[];
+    private long spanId;
+    private Map<byte[], byte[]> traceInfo = null;
+    private String processId;
+    private List<TimelineAnnotation> timeline = null;
+
+    public Builder() {
+    }
+
+    public Builder begin(long begin) {
+      this.begin = begin;
+      return this;
+    }
+
+    public Builder end(long end) {
+      this.end = end;
+      return this;
+    }
+
+    public Builder description(String description) {
+      this.description = description;
+      return this;
+    }
+
+    public Builder traceId(long traceId) {
+      this.traceId = traceId;
+      return this;
+    }
+
+    public Builder parents(long parents[]) {
+      this.parents = parents;
+      return this;
+    }
+
+    public Builder parents(List<Long> parentList) {
+      long[] parents = new long[parentList.size()];
+      for (int i = 0; i < parentList.size(); i++) {
+        parents[i] = parentList.get(i).longValue();
+      }
+      this.parents = parents;
+      return this;
+    }
+
+    public Builder spanId(long spanId) {
+      this.spanId = spanId;
+      return this;
+    }
+
+    public Builder traceInfo(Map<byte[], byte[]> traceInfo) {
+      this.traceInfo = traceInfo.isEmpty() ? null : traceInfo;
+      return this;
+    }
+
+    public Builder processId(String processId) {
+      this.processId = processId;
+      return this;
+    }
+
+    public Builder timeline(List<TimelineAnnotation> timeline) {
+      this.timeline = timeline.isEmpty() ? null : timeline;
+      return this;
+    }
+
+    public MilliSpan build() {
+      return new MilliSpan(this);
+    }
+  }
+
+  private MilliSpan(Builder builder) {
+    this.begin = builder.begin;
+    this.end = builder.end;
+    this.description = builder.description;
+    this.traceId = builder.traceId;
+    this.parents = builder.parents;
+    this.spanId = builder.spanId;
+    this.traceInfo = builder.traceInfo;
+    this.processId = builder.processId;
+    this.timeline = builder.timeline;
+  }
+
   public MilliSpan(String description, long traceId, long parentSpanId, long spanId, String processId) {
     this.description = description;
     this.traceId = traceId;
@@ -61,18 +159,18 @@ public class MilliSpan implements Span {
       this.parents = new long[] { parentSpanId };
     } 
     this.spanId = spanId;
-    this.start = System.currentTimeMillis();
-    this.stop = 0;
+    this.begin = System.currentTimeMillis();
+    this.end = 0;
     this.processId = processId;
   }
 
   @Override
   public synchronized void stop() {
-    if (stop == 0) {
-      if (start == 0)
+    if (end == 0) {
+      if (begin == 0)
         throw new IllegalStateException("Span for " + description
             + " has not been started");
-      stop = System.currentTimeMillis();
+      end = System.currentTimeMillis();
       Tracer.getInstance().deliver(this);
     }
   }
@@ -83,16 +181,16 @@ public class MilliSpan implements Span {
 
   @Override
   public synchronized boolean isRunning() {
-    return start != 0 && stop == 0;
+    return begin != 0 && end == 0;
   }
 
   @Override
   public synchronized long getAccumulatedMillis() {
-    if (start == 0)
+    if (begin == 0)
       return 0;
-    if (stop > 0)
-      return stop - start;
-    return currentTimeMillis() - start;
+    if (end > 0)
+      return end - begin;
+    return currentTimeMillis() - begin;
   }
 
   @Override
@@ -127,12 +225,12 @@ public class MilliSpan implements Span {
 
   @Override
   public long getStartTimeMillis() {
-    return start;
+    return begin;
   }
 
   @Override
   public long getStopTimeMillis() {
-    return stop;
+    return end;
   }
 
   @Override
@@ -172,24 +270,62 @@ public class MilliSpan implements Span {
 
   @Override
   public String toJson() {
-    Map<String, Object> values = new LinkedHashMap<String, Object>();
-    values.put("i", String.format("%016x", traceId));
-    values.put("s", String.format("%016x", spanId));
-    String parentStrs[] = new String[parents.length];
-    for (int parentIdx = 0; parentIdx < parents.length; parentIdx++) {
-      parentStrs[parentIdx] = String.format("%016x", parents[parentIdx]);
-    }
-    values.put("p", parentStrs);
-    values.put("r", processId);
-    values.put("b", Long.toString(start));
-    values.put("e", Long.toString(stop));
-    values.put("d", description);
-    if (timeline != null) {
-      values.put("t", timeline);
-    }
-    if (traceInfo != null){
-      values.put("n", traceInfo);
-    }
-    return JSON.toString(values);
+    StringWriter writer = new StringWriter();
+    ObjectMapper mapper = new ObjectMapper();
+    try {
+      mapper.writeValue(writer, this);
+    } catch (IOException e) {
+      // An IOException should not be possible when writing to a string.
+      throw new RuntimeException(e);
+    }
+    return writer.toString();
+  }
+
+  public static class MilliSpanDeserializer
+        extends JsonDeserializer<MilliSpan> {
+    @Override
+    public MilliSpan deserialize(JsonParser jp, DeserializationContext ctxt)
+          throws IOException, JsonProcessingException {
+      JsonNode root = jp.getCodec().readTree(jp);
+      Builder builder = new Builder();
+      builder.begin(root.get("b").asLong()).
+              end(root.get("e").asLong()).
+              description(root.get("d").asText()).
+              traceId(Long.parseLong(root.get("i").asText(), 16)).
+              spanId(Long.parseLong(root.get("s").asText(), 16)).
+              processId(root.get("r").asText());
+      JsonNode parentsNode = root.get("p");
+      LinkedList<Long> parents = new LinkedList<Long>();
+      for (Iterator<JsonNode> iter = parentsNode.elements();
+           iter.hasNext(); ) {
+        JsonNode parentIdNode = iter.next();
+        parents.add(Long.parseLong(parentIdNode.asText(), 16));
+      }
+      builder.parents(parents);
+      JsonNode traceInfoNode = root.get("n");
+      if (traceInfoNode != null) {
+        HashMap<byte[], byte[]> traceInfo = new HashMap<byte[], byte[]>();
+        for (Iterator<String> iter = traceInfoNode.fieldNames();
+             iter.hasNext(); ) {
+          String field = iter.next();
+          traceInfo.put(field.getBytes("UTF-8"),
+              traceInfoNode.get(field).asText().getBytes("UTF-8"));
+        }
+        builder.traceInfo(traceInfo);
+      }
+      JsonNode timelineNode = root.get("t");
+      if (timelineNode != null) {
+        LinkedList<TimelineAnnotation> timeline =
+            new LinkedList<TimelineAnnotation>();
+        for (Iterator<JsonNode> iter = timelineNode.elements();
+             iter.hasNext(); ) {
+          JsonNode ann = iter.next();
+          timeline.add(new TimelineAnnotation(ann.get("t").asLong(),
+              ann.get("m").asText()));
+        }
+        builder.timeline(timeline);
+      }
+      return builder.build();
+    }
   }
 }

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/6b36b1aa/htrace-core/src/test/java/org/apache/htrace/impl/TestMilliSpan.java
----------------------------------------------------------------------
diff --git a/htrace-core/src/test/java/org/apache/htrace/impl/TestMilliSpan.java b/htrace-core/src/test/java/org/apache/htrace/impl/TestMilliSpan.java
new file mode 100644
index 0000000..908e74e
--- /dev/null
+++ b/htrace-core/src/test/java/org/apache/htrace/impl/TestMilliSpan.java
@@ -0,0 +1,120 @@
+/*
+ * 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.impl;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.htrace.Span;
+import org.apache.htrace.TimelineAnnotation;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+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.getTraceId(), got.getTraceId());
+    assertEquals(expected.getSpanId(), got.getSpanId());
+    assertEquals(expected.getProcessId(), got.getProcessId());
+    assertEquals(expected.getParentId(), got.getParentId());
+    Map<byte[], byte[]> expectedT = expected.getKVAnnotations();
+    Map<byte[], byte[]> gotT = got.getKVAnnotations();
+    if (expectedT == null) {
+      assertEquals(null, gotT);
+    } else {
+      assertEquals(expectedT.size(), gotT.size());
+      Map<String, String> expectedTMap = new HashMap<String, String>();
+      for (byte[] key : expectedT.keySet()) {
+        expectedTMap.put(new String(key, "UTF-8"),
+            new String(expectedT.get(key), "UTF-8"));
+      }
+      Map<String, String> gotTMap = new HashMap<String, String>();
+      for (byte[] key : gotT.keySet()) {
+        gotTMap.put(new String(key, "UTF-8"),
+            new String(gotT.get(key), "UTF-8"));
+      }
+      for (String key : expectedTMap.keySet()) {
+        assertEquals(expectedTMap.get(key), gotTMap.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 long[] { 7L }).
+        processId("b2404.halxg.com:8080").
+        spanId(989L).
+        traceId(444).build();
+    String json = span.toJson();
+    ObjectMapper mapper = new ObjectMapper();
+    MilliSpan dspan = mapper.readValue(json, MilliSpan.class);
+    compareSpans(span, dspan);
+  }
+
+  @Test
+  public void testJsonSerializationWithOptionalFields() throws Exception {
+    MilliSpan.Builder builder = new MilliSpan.Builder().
+        description("foospan").
+        begin(300).
+        end(400).
+        parents(new long[] { }).
+        processId("b2408.halxg.com:8080").
+        spanId(111111111L).
+        traceId(4443);
+    Map<byte[], byte[]> traceInfo = new HashMap<byte[], byte[]>();
+    traceInfo.put("abc".getBytes("UTF-8"), "123".getBytes("UTF-8"));
+    traceInfo.put("def".getBytes("UTF-8"), "456".getBytes("UTF-8"));
+    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();
+    ObjectMapper mapper = new ObjectMapper();
+    MilliSpan dspan = mapper.readValue(json, MilliSpan.class);
+    compareSpans(span, dspan);
+  }
+}