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/04/23 01:08:13 UTC

[19/41] incubator-htrace git commit: HTRACE-154. Move go and web to htrace-htraced (abe via cmccabe)

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/go/src/org/apache/htrace/htraced/hrpc.go
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/go/src/org/apache/htrace/htraced/hrpc.go b/htrace-htraced/src/go/src/org/apache/htrace/htraced/hrpc.go
new file mode 100644
index 0000000..9696cbc
--- /dev/null
+++ b/htrace-htraced/src/go/src/org/apache/htrace/htraced/hrpc.go
@@ -0,0 +1,197 @@
+/*
+ * 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 main
+
+import (
+	"bufio"
+	"encoding/binary"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/rpc"
+	"org/apache/htrace/common"
+	"org/apache/htrace/conf"
+)
+
+// Handles HRPC calls
+type HrpcHandler struct {
+	lg    *common.Logger
+	store *dataStore
+}
+
+// The HRPC server
+type HrpcServer struct {
+	*rpc.Server
+	hand     *HrpcHandler
+	listener net.Listener
+}
+
+// Codec which encodes HRPC data via JSON
+type HrpcServerCodec struct {
+	rwc    io.ReadWriteCloser
+	length uint32
+}
+
+func (cdc *HrpcServerCodec) ReadRequestHeader(req *rpc.Request) error {
+	hdr := common.HrpcRequestHeader{}
+	err := binary.Read(cdc.rwc, binary.BigEndian, &hdr)
+	if err != nil {
+		return errors.New(fmt.Sprintf("Error reading header bytes: %s", err.Error()))
+	}
+	if hdr.Magic != common.HRPC_MAGIC {
+		return errors.New(fmt.Sprintf("Invalid request header: expected "+
+			"magic number of 0x%04x, but got 0x%04x", common.HRPC_MAGIC, hdr.Magic))
+	}
+	if hdr.Length > common.MAX_HRPC_BODY_LENGTH {
+		return errors.New(fmt.Sprintf("Length prefix was too long.  Maximum "+
+			"length is %d, but we got %d.", common.MAX_HRPC_BODY_LENGTH, hdr.Length))
+	}
+	req.ServiceMethod = common.HrpcMethodIdToMethodName(hdr.MethodId)
+	if req.ServiceMethod == "" {
+		return errors.New(fmt.Sprintf("Unknown MethodID code 0x%04x",
+			hdr.MethodId))
+	}
+	req.Seq = hdr.Seq
+	cdc.length = hdr.Length
+	return nil
+}
+
+func (cdc *HrpcServerCodec) ReadRequestBody(body interface{}) error {
+	dec := json.NewDecoder(io.LimitReader(cdc.rwc, int64(cdc.length)))
+	err := dec.Decode(body)
+	if err != nil {
+		return errors.New(fmt.Sprintf("Failed to read request body: %s",
+			err.Error()))
+	}
+	return nil
+}
+
+var EMPTY []byte = make([]byte, 0)
+
+func (cdc *HrpcServerCodec) WriteResponse(resp *rpc.Response, msg interface{}) error {
+	var err error
+	buf := EMPTY
+	if msg != nil {
+		buf, err = json.Marshal(msg)
+		if err != nil {
+			return errors.New(fmt.Sprintf("Failed to marshal response message: %s",
+				err.Error()))
+		}
+	}
+	hdr := common.HrpcResponseHeader{}
+	hdr.MethodId = common.HrpcMethodNameToId(resp.ServiceMethod)
+	hdr.Seq = resp.Seq
+	hdr.ErrLength = uint32(len(resp.Error))
+	hdr.Length = uint32(len(buf))
+	writer := bufio.NewWriterSize(cdc.rwc, 256)
+	err = binary.Write(writer, binary.BigEndian, &hdr)
+	if err != nil {
+		return errors.New(fmt.Sprintf("Failed to write response header: %s",
+			err.Error()))
+	}
+	if hdr.ErrLength > 0 {
+		_, err = io.WriteString(writer, resp.Error)
+		if err != nil {
+			return errors.New(fmt.Sprintf("Failed to write error string: %s",
+				err.Error()))
+		}
+	}
+	if hdr.Length > 0 {
+		var length int
+		length, err = writer.Write(buf)
+		if err != nil {
+			return errors.New(fmt.Sprintf("Failed to write response "+
+				"message: %s", err.Error()))
+		}
+		if uint32(length) != hdr.Length {
+			return errors.New(fmt.Sprintf("Failed to write all of response "+
+				"message: %s", err.Error()))
+		}
+	}
+	err = writer.Flush()
+	if err != nil {
+		return errors.New(fmt.Sprintf("Failed to write the response bytes: "+
+			"%s", err.Error()))
+	}
+	return nil
+}
+
+func (cdc *HrpcServerCodec) Close() error {
+	return cdc.rwc.Close()
+}
+
+func (hand *HrpcHandler) WriteSpans(req *common.WriteSpansReq,
+	resp *common.WriteSpansResp) (err error) {
+	hand.lg.Debugf("hrpc writeSpansHandler: received %d span(s).  "+
+		"defaultPid = %s\n", len(req.Spans), req.DefaultPid)
+	for i := range req.Spans {
+		span := req.Spans[i]
+		if span.ProcessId == "" {
+			span.ProcessId = req.DefaultPid
+		}
+		hand.lg.Tracef("writing span %d: %s\n", i, span.ToJson())
+		hand.store.WriteSpan(span)
+	}
+	return nil
+}
+
+func CreateHrpcServer(cnf *conf.Config, store *dataStore) (*HrpcServer, error) {
+	lg := common.NewLogger("hrpc", cnf)
+	hsv := &HrpcServer{
+		Server: rpc.NewServer(),
+		hand: &HrpcHandler{
+			lg:    lg,
+			store: store,
+		},
+	}
+	var err error
+	hsv.listener, err = net.Listen("tcp", cnf.Get(conf.HTRACE_HRPC_ADDRESS))
+	if err != nil {
+		return nil, err
+	}
+	hsv.Server.Register(hsv.hand)
+	go hsv.run()
+	lg.Infof("Started HRPC server on %s...\n", hsv.listener.Addr().String())
+	return hsv, nil
+}
+
+func (hsv *HrpcServer) run() {
+	lg := hsv.hand.lg
+	for {
+		conn, err := hsv.listener.Accept()
+		if err != nil {
+			lg.Errorf("HRPC Accept error: %s\n", err.Error())
+			continue
+		}
+		go hsv.ServeCodec(&HrpcServerCodec{
+			rwc: conn,
+		})
+	}
+}
+
+func (hsv *HrpcServer) Addr() net.Addr {
+	return hsv.listener.Addr()
+}
+
+func (hsv *HrpcServer) Close() {
+	hsv.listener.Close()
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/go/src/org/apache/htrace/htraced/htraced.go
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/go/src/org/apache/htrace/htraced/htraced.go b/htrace-htraced/src/go/src/org/apache/htrace/htraced/htraced.go
new file mode 100644
index 0000000..64da457
--- /dev/null
+++ b/htrace-htraced/src/go/src/org/apache/htrace/htraced/htraced.go
@@ -0,0 +1,140 @@
+/*
+ * 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 main
+
+import (
+	"encoding/json"
+	"fmt"
+	"net"
+	"org/apache/htrace/common"
+	"org/apache/htrace/conf"
+	"os"
+	"strings"
+	"time"
+)
+
+var RELEASE_VERSION string
+var GIT_VERSION string
+
+const USAGE = `htraced: the HTrace server daemon.
+
+htraced receives trace spans sent from HTrace clients.  It exposes a REST
+interface which others can query.  It also runs a web server with a graphical
+user interface.  htraced stores its span data in levelDB files on the local
+disks.
+
+Usage:
+--help: this help message
+
+-Dk=v: set configuration key 'k' to value 'v'
+For example -Dweb.address=127.0.0.1:8080 sets the web address to localhost,
+port 8080.
+
+-Dk: set configuration key 'k' to 'true'
+
+Normally, configuration options should be set in the ` + conf.CONFIG_FILE_NAME + `
+configuration file.  We find this file by searching the paths in the 
+` + conf.HTRACED_CONF_DIR + `. The command-line options are just an alternate way
+of setting configuration when launching the daemon.
+`
+
+func main() {
+	for idx := range os.Args {
+		arg := os.Args[idx]
+		if strings.HasPrefix(arg, "--h") || strings.HasPrefix(arg, "-h") {
+			fmt.Fprintf(os.Stderr, USAGE)
+			os.Exit(0)
+		}
+	}
+	cnf := common.LoadApplicationConfig()
+	common.InstallSignalHandlers(cnf)
+	lg := common.NewLogger("main", cnf)
+	defer lg.Close()
+	store, err := CreateDataStore(cnf, nil)
+	if err != nil {
+		lg.Errorf("Error creating datastore: %s\n", err.Error())
+		os.Exit(1)
+	}
+	var rsv *RestServer
+	rsv, err = CreateRestServer(cnf, store)
+	if err != nil {
+		lg.Errorf("Error creating REST server: %s\n", err.Error())
+		os.Exit(1)
+	}
+	var hsv *HrpcServer
+	if cnf.Get(conf.HTRACE_HRPC_ADDRESS) != "" {
+		hsv, err = CreateHrpcServer(cnf, store)
+		if err != nil {
+			lg.Errorf("Error creating HRPC server: %s\n", err.Error())
+			os.Exit(1)
+		}
+	} else {
+		lg.Infof("Not starting HRPC server because no value was given for %s.\n",
+			conf.HTRACE_HRPC_ADDRESS)
+	}
+	naddr := cnf.Get(conf.HTRACE_STARTUP_NOTIFICATION_ADDRESS)
+	if naddr != "" {
+		notif := StartupNotification{
+			HttpAddr:  rsv.Addr().String(),
+			ProcessId: os.Getpid(),
+		}
+		if hsv != nil {
+			notif.HrpcAddr = hsv.Addr().String()
+		}
+		err = sendStartupNotification(naddr, &notif)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send startup notification: "+
+				"%s\n", err.Error())
+			os.Exit(1)
+		}
+	}
+	for {
+		time.Sleep(time.Duration(10) * time.Hour)
+	}
+}
+
+// A startup notification message that we optionally send on startup.
+// Used by unit tests.
+type StartupNotification struct {
+	HttpAddr  string
+	HrpcAddr  string
+	ProcessId int
+}
+
+func sendStartupNotification(naddr string, notif *StartupNotification) error {
+	conn, err := net.Dial("tcp", naddr)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		if conn != nil {
+			conn.Close()
+		}
+	}()
+	var buf []byte
+	buf, err = json.Marshal(notif)
+	if err != nil {
+		return err
+	}
+	_, err = conn.Write(buf)
+	conn.Close()
+	conn = nil
+	return nil
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/go/src/org/apache/htrace/htraced/mini_htraced.go
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/go/src/org/apache/htrace/htraced/mini_htraced.go b/htrace-htraced/src/go/src/org/apache/htrace/htraced/mini_htraced.go
new file mode 100644
index 0000000..a54f2cb
--- /dev/null
+++ b/htrace-htraced/src/go/src/org/apache/htrace/htraced/mini_htraced.go
@@ -0,0 +1,165 @@
+/*
+ * 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 main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"org/apache/htrace/common"
+	"org/apache/htrace/conf"
+	"os"
+	"strings"
+)
+
+//
+// MiniHTraceD is used in unit tests to set up a daemon with certain settings.
+// It takes care of things like creating and cleaning up temporary directories.
+//
+
+// The default number of managed data directories to use.
+const DEFAULT_NUM_DATA_DIRS = 2
+
+// Builds a MiniHTraced object.
+type MiniHTracedBuilder struct {
+	// The name of the MiniHTraced to build.  This shows up in the test directory name and some
+	// other places.
+	Name string
+
+	// The configuration values to use for the MiniHTraced.
+	// If ths is nil, we use the default configuration for everything.
+	Cnf map[string]string
+
+	// The DataDirs to use.  Empty entries will turn into random names.
+	DataDirs []string
+
+	// If true, we will keep the data dirs around after MiniHTraced#Close
+	KeepDataDirsOnClose bool
+
+	// If non-null, the WrittenSpans channel to use when creating the DataStore.
+	WrittenSpans chan *common.Span
+}
+
+type MiniHTraced struct {
+	Name                string
+	Cnf                 *conf.Config
+	DataDirs            []string
+	Store               *dataStore
+	Rsv                 *RestServer
+	Hsv                 *HrpcServer
+	Lg                  *common.Logger
+	KeepDataDirsOnClose bool
+}
+
+func (bld *MiniHTracedBuilder) Build() (*MiniHTraced, error) {
+	var err error
+	var store *dataStore
+	var rsv *RestServer
+	var hsv *HrpcServer
+	if bld.Name == "" {
+		bld.Name = "HTraceTest"
+	}
+	if bld.Cnf == nil {
+		bld.Cnf = make(map[string]string)
+	}
+	if bld.DataDirs == nil {
+		bld.DataDirs = make([]string, 2)
+	}
+	for idx := range bld.DataDirs {
+		if bld.DataDirs[idx] == "" {
+			bld.DataDirs[idx], err = ioutil.TempDir(os.TempDir(),
+				fmt.Sprintf("%s%d", bld.Name, idx+1))
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
+	bld.Cnf[conf.HTRACE_DATA_STORE_DIRECTORIES] =
+		strings.Join(bld.DataDirs, conf.PATH_LIST_SEP)
+	bld.Cnf[conf.HTRACE_WEB_ADDRESS] = ":0"  // use a random port for the REST server
+	bld.Cnf[conf.HTRACE_HRPC_ADDRESS] = ":0" // use a random port for the HRPC server
+	bld.Cnf[conf.HTRACE_LOG_LEVEL] = "TRACE"
+	cnfBld := conf.Builder{Values: bld.Cnf, Defaults: conf.DEFAULTS}
+	cnf, err := cnfBld.Build()
+	if err != nil {
+		return nil, err
+	}
+	lg := common.NewLogger("mini.htraced", cnf)
+	defer func() {
+		if err != nil {
+			if store != nil {
+				store.Close()
+			}
+			for idx := range bld.DataDirs {
+				if bld.DataDirs[idx] != "" {
+					os.RemoveAll(bld.DataDirs[idx])
+				}
+			}
+			if rsv != nil {
+				rsv.Close()
+			}
+			lg.Infof("Failed to create MiniHTraced %s: %s\n", bld.Name, err.Error())
+			lg.Close()
+		}
+	}()
+	store, err = CreateDataStore(cnf, bld.WrittenSpans)
+	if err != nil {
+		return nil, err
+	}
+	rsv, err = CreateRestServer(cnf, store)
+	if err != nil {
+		return nil, err
+	}
+	hsv, err = CreateHrpcServer(cnf, store)
+	if err != nil {
+		return nil, err
+	}
+
+	lg.Infof("Created MiniHTraced %s\n", bld.Name)
+	return &MiniHTraced{
+		Name:                bld.Name,
+		Cnf:                 cnf,
+		DataDirs:            bld.DataDirs,
+		Store:               store,
+		Rsv:                 rsv,
+		Hsv:                 hsv,
+		Lg:                  lg,
+		KeepDataDirsOnClose: bld.KeepDataDirsOnClose,
+	}, nil
+}
+
+// Return a Config object that clients can use to connect to this MiniHTraceD.
+func (ht *MiniHTraced) ClientConf() *conf.Config {
+	return ht.Cnf.Clone(conf.HTRACE_WEB_ADDRESS, ht.Rsv.Addr().String(),
+		conf.HTRACE_HRPC_ADDRESS, ht.Hsv.Addr().String())
+}
+
+func (ht *MiniHTraced) Close() {
+	ht.Lg.Infof("Closing MiniHTraced %s\n", ht.Name)
+	ht.Rsv.Close()
+	ht.Store.Close()
+	if !ht.KeepDataDirsOnClose {
+		for idx := range ht.DataDirs {
+			ht.Lg.Infof("Removing %s...\n", ht.DataDirs[idx])
+			os.RemoveAll(ht.DataDirs[idx])
+		}
+	}
+	ht.Lg.Infof("Finished closing MiniHTraced %s\n", ht.Name)
+	ht.Lg.Close()
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/go/src/org/apache/htrace/htraced/rest.go
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/go/src/org/apache/htrace/htraced/rest.go b/htrace-htraced/src/go/src/org/apache/htrace/htraced/rest.go
new file mode 100644
index 0000000..1449802
--- /dev/null
+++ b/htrace-htraced/src/go/src/org/apache/htrace/htraced/rest.go
@@ -0,0 +1,304 @@
+/*
+ * 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 main
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"github.com/gorilla/mux"
+	"io"
+	"net"
+	"net/http"
+	"org/apache/htrace/common"
+	"org/apache/htrace/conf"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+)
+
+// Set the response headers.
+func setResponseHeaders(hdr http.Header) {
+	hdr.Set("Content-Type", "application/json")
+}
+
+// Write a JSON error response.
+func writeError(lg *common.Logger, w http.ResponseWriter, errCode int,
+	errStr string) {
+	str := strings.Replace(errStr, `"`, `'`, -1)
+	lg.Info(str + "\n")
+	w.WriteHeader(errCode)
+	w.Write([]byte(`{ "error" : "` + str + `"}`))
+}
+
+type serverInfoHandler struct {
+	lg *common.Logger
+}
+
+func (hand *serverInfoHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	setResponseHeaders(w.Header())
+	version := common.ServerInfo{ReleaseVersion: RELEASE_VERSION,
+		GitVersion: GIT_VERSION}
+	buf, err := json.Marshal(&version)
+	if err != nil {
+		writeError(hand.lg, w, http.StatusInternalServerError,
+			fmt.Sprintf("error marshalling ServerInfo: %s\n", err.Error()))
+		return
+	}
+	hand.lg.Debugf("Returned serverInfo %s\n", string(buf))
+	w.Write(buf)
+}
+
+type dataStoreHandler struct {
+	lg    *common.Logger
+	store *dataStore
+}
+
+func (hand *dataStoreHandler) parseSid(w http.ResponseWriter,
+	str string) (common.SpanId, bool) {
+	val, err := strconv.ParseUint(str, 16, 64)
+	if err != nil {
+		writeError(hand.lg, w, http.StatusBadRequest,
+			fmt.Sprintf("Failed to parse span ID %s: %s", str, err.Error()))
+		w.Write([]byte("Error parsing : " + err.Error()))
+		return 0, false
+	}
+	return common.SpanId(val), true
+}
+
+func (hand *dataStoreHandler) getReqField32(fieldName string, w http.ResponseWriter,
+	req *http.Request) (int32, bool) {
+	str := req.FormValue(fieldName)
+	if str == "" {
+		writeError(hand.lg, w, http.StatusBadRequest, fmt.Sprintf("No %s specified.", fieldName))
+		return -1, false
+	}
+	val, err := strconv.ParseUint(str, 16, 32)
+	if err != nil {
+		writeError(hand.lg, w, http.StatusBadRequest,
+			fmt.Sprintf("Error parsing %s: %s.", fieldName, err.Error()))
+		return -1, false
+	}
+	return int32(val), true
+}
+
+type findSidHandler struct {
+	dataStoreHandler
+}
+
+func (hand *findSidHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	setResponseHeaders(w.Header())
+	req.ParseForm()
+	vars := mux.Vars(req)
+	stringSid := vars["id"]
+	sid, ok := hand.parseSid(w, stringSid)
+	if !ok {
+		return
+	}
+	hand.lg.Debugf("findSidHandler(sid=%s)\n", sid.String())
+	span := hand.store.FindSpan(sid)
+	if span == nil {
+		writeError(hand.lg, w, http.StatusNoContent,
+			fmt.Sprintf("No such span as %s\n", sid.String()))
+		return
+	}
+	w.Write(span.ToJson())
+}
+
+type findChildrenHandler struct {
+	dataStoreHandler
+}
+
+func (hand *findChildrenHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	setResponseHeaders(w.Header())
+	req.ParseForm()
+	vars := mux.Vars(req)
+	stringSid := vars["id"]
+	sid, ok := hand.parseSid(w, stringSid)
+	if !ok {
+		return
+	}
+	var lim int32
+	lim, ok = hand.getReqField32("lim", w, req)
+	if !ok {
+		return
+	}
+	hand.lg.Debugf("findChildrenHandler(sid=%s, lim=%d)\n", sid.String(), lim)
+	children := hand.store.FindChildren(sid, lim)
+	jbytes, err := json.Marshal(children)
+	if err != nil {
+		writeError(hand.lg, w, http.StatusInternalServerError,
+			fmt.Sprintf("Error marshalling children: %s", err.Error()))
+		return
+	}
+	w.Write(jbytes)
+}
+
+type writeSpansHandler struct {
+	dataStoreHandler
+}
+
+func (hand *writeSpansHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	setResponseHeaders(w.Header())
+	dec := json.NewDecoder(req.Body)
+	spans := make([]*common.Span, 0, 32)
+	defaultPid := req.Header.Get("htrace-pid")
+	for {
+		var span common.Span
+		err := dec.Decode(&span)
+		if err != nil {
+			if err != io.EOF {
+				writeError(hand.lg, w, http.StatusBadRequest,
+					fmt.Sprintf("Error parsing spans: %s", err.Error()))
+				return
+			}
+			break
+		}
+		if span.ProcessId == "" {
+			span.ProcessId = defaultPid
+		}
+		spans = append(spans, &span)
+	}
+	hand.lg.Debugf("writeSpansHandler: received %d span(s).  defaultPid = %s\n",
+		len(spans), defaultPid)
+	for spanIdx := range spans {
+		hand.lg.Debugf("writing span %s\n", spans[spanIdx].ToJson())
+		hand.store.WriteSpan(spans[spanIdx])
+	}
+}
+
+type queryHandler struct {
+	lg *common.Logger
+	dataStoreHandler
+}
+
+func (hand *queryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	setResponseHeaders(w.Header())
+	queryString := req.FormValue("query")
+	if queryString == "" {
+		writeError(hand.lg, w, http.StatusBadRequest, "No query provided.\n")
+		return
+	}
+	var query common.Query
+	reader := bytes.NewBufferString(queryString)
+	dec := json.NewDecoder(reader)
+	err := dec.Decode(&query)
+	if err != nil {
+		writeError(hand.lg, w, http.StatusBadRequest,
+			fmt.Sprintf("Error parsing query: %s", err.Error()))
+		return
+	}
+	var results []*common.Span
+	results, err = hand.store.HandleQuery(&query)
+	if err != nil {
+		writeError(hand.lg, w, http.StatusInternalServerError,
+			fmt.Sprintf("Internal error processing query %s: %s",
+				query.String(), err.Error()))
+		return
+	}
+	var jbytes []byte
+	jbytes, err = json.Marshal(results)
+	if err != nil {
+		writeError(hand.lg, w, http.StatusInternalServerError,
+			fmt.Sprintf("Error marshalling results: %s", err.Error()))
+		return
+	}
+	w.Write(jbytes)
+}
+
+type logErrorHandler struct {
+	lg *common.Logger
+}
+
+func (hand *logErrorHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	hand.lg.Errorf("Got unknown request %s\n", req.RequestURI)
+	writeError(hand.lg, w, http.StatusBadRequest, "Unknown request.")
+}
+
+type RestServer struct {
+	listener net.Listener
+	lg       *common.Logger
+}
+
+func CreateRestServer(cnf *conf.Config, store *dataStore) (*RestServer, error) {
+	var err error
+	rsv := &RestServer{}
+	rsv.listener, err = net.Listen("tcp", cnf.Get(conf.HTRACE_WEB_ADDRESS))
+	if err != nil {
+		return nil, err
+	}
+	var success bool
+	defer func() {
+		if !success {
+			rsv.Close()
+		}
+	}()
+	rsv.lg = common.NewLogger("rest", cnf)
+
+	r := mux.NewRouter().StrictSlash(false)
+
+	r.Handle("/server/info", &serverInfoHandler{lg: rsv.lg}).Methods("GET")
+
+	writeSpansH := &writeSpansHandler{dataStoreHandler: dataStoreHandler{
+		store: store, lg: rsv.lg}}
+	r.Handle("/writeSpans", writeSpansH).Methods("POST")
+
+	queryH := &queryHandler{lg: rsv.lg, dataStoreHandler: dataStoreHandler{store: store}}
+	r.Handle("/query", queryH).Methods("GET")
+
+	span := r.PathPrefix("/span").Subrouter()
+	findSidH := &findSidHandler{dataStoreHandler: dataStoreHandler{store: store, lg: rsv.lg}}
+	span.Handle("/{id}", findSidH).Methods("GET")
+
+	findChildrenH := &findChildrenHandler{dataStoreHandler: dataStoreHandler{store: store,
+		lg: rsv.lg}}
+	span.Handle("/{id}/children", findChildrenH).Methods("GET")
+
+	// Default Handler. This will serve requests for static requests.
+	webdir := os.Getenv("HTRACED_WEB_DIR")
+	if webdir == "" {
+		webdir, err = filepath.Abs(filepath.Join(filepath.Dir(os.Args[0]), "..", "..", "web"))
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	rsv.lg.Infof("Serving static files from %s\n.", webdir)
+	r.PathPrefix("/").Handler(http.FileServer(http.Dir(webdir))).Methods("GET")
+
+	// Log an error message for unknown non-GET requests.
+	r.PathPrefix("/").Handler(&logErrorHandler{lg: rsv.lg})
+
+	go http.Serve(rsv.listener, r)
+
+	rsv.lg.Infof("Started REST server on %s...\n", rsv.listener.Addr().String())
+	success = true
+	return rsv, nil
+}
+
+func (rsv *RestServer) Addr() net.Addr {
+	return rsv.listener.Addr()
+}
+
+func (rsv *RestServer) Close() {
+	rsv.listener.Close()
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/go/src/org/apache/htrace/test/random.go
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/go/src/org/apache/htrace/test/random.go b/htrace-htraced/src/go/src/org/apache/htrace/test/random.go
new file mode 100644
index 0000000..d10e2f9
--- /dev/null
+++ b/htrace-htraced/src/go/src/org/apache/htrace/test/random.go
@@ -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 test
+
+import (
+	"fmt"
+	"math/rand"
+	"org/apache/htrace/common"
+)
+
+func NonZeroRand64(rnd *rand.Rand) int64 {
+	for {
+		r := rnd.Int63()
+		if r == 0 {
+			continue
+		}
+		if rnd.Intn(1) != 0 {
+			return -r
+		}
+		return r
+	}
+}
+
+func NonZeroRand32(rnd *rand.Rand) int32 {
+	for {
+		r := rnd.Int31()
+		if r == 0 {
+			continue
+		}
+		if rnd.Intn(1) != 0 {
+			return -r
+		}
+		return r
+	}
+}
+
+// Create a random span.
+func NewRandomSpan(rnd *rand.Rand, potentialParents []*common.Span) *common.Span {
+	parents := []common.SpanId{}
+	if potentialParents != nil {
+		parentIdx := rnd.Intn(len(potentialParents) + 1)
+		if parentIdx < len(potentialParents) {
+			parents = []common.SpanId{potentialParents[parentIdx].Id}
+		}
+	}
+	return &common.Span{Id: common.SpanId(NonZeroRand64(rnd)),
+		SpanData: common.SpanData{
+			Begin:       NonZeroRand64(rnd),
+			End:         NonZeroRand64(rnd),
+			Description: "getFileDescriptors",
+			TraceId:     common.SpanId(NonZeroRand64(rnd)),
+			Parents:     parents,
+			ProcessId:   fmt.Sprintf("process%d", NonZeroRand32(rnd)),
+		}}
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/go/src/org/apache/htrace/test/util.go
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/go/src/org/apache/htrace/test/util.go b/htrace-htraced/src/go/src/org/apache/htrace/test/util.go
new file mode 100644
index 0000000..cc058e0
--- /dev/null
+++ b/htrace-htraced/src/go/src/org/apache/htrace/test/util.go
@@ -0,0 +1,33 @@
+/*
+ * 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 test
+
+import (
+	"org/apache/htrace/common"
+)
+
+func SpanId(str string) common.SpanId {
+	var spanId common.SpanId
+	err := spanId.FromString(str)
+	if err != nil {
+		panic(err.Error())
+	}
+	return spanId
+}

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/test/java/org/apache/htrace/util/HTracedProcess.java
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/test/java/org/apache/htrace/util/HTracedProcess.java b/htrace-htraced/src/test/java/org/apache/htrace/util/HTracedProcess.java
index 9623a8f..5fa5d95 100644
--- a/htrace-htraced/src/test/java/org/apache/htrace/util/HTracedProcess.java
+++ b/htrace-htraced/src/test/java/org/apache/htrace/util/HTracedProcess.java
@@ -166,7 +166,7 @@ public class HTracedProcess extends Process {
    * @return Path to the htraced binary.
    */
   public static File getPathToHTraceBinaryFromTopLevel(final File topLevel) {
-    return new File(new File(new File(new File(new File(topLevel, "htrace-core"), "src"), "go"),
+    return new File(new File(new File(new File(new File(topLevel, "htrace-htraced"), "src"), "go"),
       "build"), "htraced");
   }
 }

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/web/app/app.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/app.js b/htrace-htraced/src/web/app/app.js
new file mode 100644
index 0000000..0bc7100
--- /dev/null
+++ b/htrace-htraced/src/web/app/app.js
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+window.app = new Marionette.Application();

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/web/app/models/span.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/models/span.js b/htrace-htraced/src/web/app/models/span.js
new file mode 100644
index 0000000..b8dc114
--- /dev/null
+++ b/htrace-htraced/src/web/app/models/span.js
@@ -0,0 +1,144 @@
+/*
+ * 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.
+ */
+
+// Span model
+app.Span = Backbone.Model.extend({
+  "defaults": {
+    "spanId": null,
+    "traceId": null,
+    "processId": null,
+    "parents": null,
+    "description": null,
+    "beginTime": 0,
+    "stopTime": 0
+  },
+
+  shorthand: {
+    "s": "spanId",
+    "b": "beginTime",
+    "e": "stopTime",
+    "d": "description",
+    "r": "processId",
+    "p": "parents",
+    "i": "traceId"
+  },
+
+  parse: function(response, options) {
+    var attrs = {};
+    var $this = this;
+    $.each(response, function(key, value) {
+      attrs[(key in $this.shorthand) ? $this.shorthand[key] : key] = value;
+    });
+    return attrs;
+  },
+
+  duration: function() {
+    return this.get('stopTime') - this.get('beginTime');
+  }
+});
+
+app.Spans = Backbone.PageableCollection.extend({
+  model: app.Span,
+  mode: "infinite",
+  url: "/query",
+  state: {
+    pageSize: 10,
+    lastSpanId: null,
+    finished: false,
+    predicates: []
+  },
+  queryParams: {
+    totalPages: null,
+    totalRecords: null,
+    firstPage: null,
+    lastPage: null,
+    currentPage: null,
+    pageSize: null,
+    sortKey: null,
+    order: null,
+    directions: null,
+
+    /**
+     * Query parameter for htraced.
+     */
+    query: function() {
+      var predicates = this.state.predicates.slice(0);
+      var lastSpanId = this.state.lastSpanId;
+
+      /**
+       * Use last pulled span ID to paginate.
+       * The htraced API works such that order is defined by the first predicate.
+       * Adding a predicate to the end of the predicates list won't change the order.
+       * Providing the predicate on spanid will filter all previous spanids.
+       */
+      if (lastSpanId) {
+        predicates.push({
+          "op": "gt",
+          "field": "spanid",
+          "val": lastSpanId
+        });
+      }
+
+      return JSON.stringify({
+        lim: this.state.pageSize + 1,
+        pred: predicates
+      });
+    }
+  },
+
+  initialize: function() {
+    this.on("reset", function(collection, response, options) {
+      if (response.length == 0) {
+        delete this.links[this.state.currentPage];
+        this.getPreviousPage();
+      }
+    }, this);
+  },
+
+  parseLinks: function(resp, xhr) {
+    this.state.finished = resp.length <= this.state.pageSize;
+
+    if (this.state.finished) {
+      this.state.lastSpanId = null;
+    } else {
+      this.state.lastSpanId = resp[this.state.pageSize - 1].s;
+    }
+
+    if (this.state.finished) {
+      return {};
+    }
+
+    return {
+      "next": "/query?query=" + this.queryParams.query.call(this)
+    };
+  },
+
+  parseRecords: function(resp) {
+    return resp.slice(0, 10);
+  },
+
+  setPredicates: function(predicates) {
+    if (!$.isArray(predicates)) {
+      console.error("predicates should be an array");
+      return;
+    }
+
+    this.state.predicates = predicates;
+  }
+});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/web/app/setup.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/setup.js b/htrace-htraced/src/web/app/setup.js
new file mode 100644
index 0000000..beb06db
--- /dev/null
+++ b/htrace-htraced/src/web/app/setup.js
@@ -0,0 +1,192 @@
+/*
+ * 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.
+ */
+
+var BaseView = Backbone.Marionette.LayoutView.extend({
+  "el": "body",
+  "regions": {
+    "header": "#header",
+    "app": "#app"
+  }
+});
+
+var Router = Backbone.Marionette.AppRouter.extend({
+  "routes": {
+    "": "init",
+    "!/search(?:query)": "search",
+    "!/spans/:id": "span",
+    "!/swimlane/:id": "swimlane",
+    "!/swimlane/:id:?:lim": "swimlane"
+  },
+
+  "initialize": function() {
+    // Collection
+    this.spansCollection = new app.Spans();
+  },
+
+  "init": function() {
+    Backbone.history.navigate("!/search", {"trigger": true});
+  },
+
+  "search": function(query) {
+    app.root.app.show(new app.SearchView());
+
+    var predicates;
+
+    this.spansCollection.switchMode("infinite", {
+      fetch: false,
+      resetState: true
+    });
+
+    if (query) {
+      predicates = _(query.split(";"))
+      .map(function(predicate) {
+        return _(predicate.split('&'))
+          .reduce(function(mem, op) {
+            var op = op.split('=');
+            mem[op[0]] = op[1];
+            return mem;
+          }, {});
+      });
+      this.spansCollection.fullCollection.reset();
+      this.spansCollection.setPredicates(predicates);
+    }
+    else {
+      this.spansCollection.fullCollection.reset();
+      this.spansCollection.setPredicates([{"op":"cn","field":"description","val":""}]);
+    }
+    this.spansCollection.fetch();
+
+    app.root.app.currentView.controls.show(
+      new app.SearchControlsView({
+        "collection": this.spansCollection,
+        "predicates": predicates
+      }));
+    app.root.app.currentView.main.show(
+      new Backgrid.Grid({
+        "collection": this.spansCollection,
+        "columns": [{
+          "label": "Begin",
+          "cell": Backgrid.Cell.extend({
+            className: "begin-cell",
+            formatter: {
+              fromRaw: function(rawData, model) {
+                var beginMs = model.get("beginTime")
+                return moment(beginMs).format('YYYY/MM/DD HH:mm:ss,SSS');
+              },
+              toRaw: function(formattedData, model) {
+                return formattedData // data entry not supported for this cell
+              }
+            }
+          }),
+          "editable": false,
+          "sortable": false
+        }, {
+          "name": "spanId",
+          "label": "ID",
+          "cell": "string",
+          "editable": false,
+          "sortable": false
+        }, {
+          "name": "processId",
+          "label": "processId",
+          "cell": "string",
+          "editable": false,
+          "sortable": false
+        }, {
+          "label": "Duration",
+          "cell": Backgrid.Cell.extend({
+            className: "duration-cell",
+            formatter: {
+              fromRaw: function(rawData, model) {
+                return model.duration() + " ms"
+              },
+              toRaw: function(formattedData, model) {
+                return formattedData // data entry not supported for this cell
+              }
+            }
+          }),
+          "editable": false,
+          "sortable": false
+        }, {
+          "name": "description",
+          "label": "Description",
+          "cell": "string",
+          "editable": false,
+          "sortable": false
+        }],
+        "row": Backgrid.Row.extend({
+          "events": {
+            "click": "details"
+          },
+          "details": function() {
+            Backbone.history.navigate("!/spans/" + this.model.get("spanId"), {"trigger": true});
+          }
+        })
+      }));
+    app.root.app.currentView.pagination.show(
+      new Backgrid.Extension.Paginator({
+        collection: this.spansCollection,
+      }));
+  },
+
+  "span": function(id) {
+    var span = this.spansCollection.findWhere({
+      "spanId": id
+    });
+
+    if (!span) {
+      Backbone.history.navigate("!/search", {"trigger": true});
+      return;
+    }
+
+    var graphView = new app.GraphView({
+      "collection": this.spansCollection,
+      "id": "span-graph"
+    });
+
+    graphView.on("update:span", function(d) {
+      app.root.app.currentView.span.show(
+        new app.SpanDetailsView({
+          "model": d.span
+        }));
+    });
+
+    app.root.app.show(new app.DetailsView());
+    app.root.app.currentView.content.show(graphView);
+    app.root.app.currentView.content.currentView.setSpanId(id);
+  },
+
+  "swimlane": function(id, lim) {
+    var top = new app.SwimlaneView();
+    app.root.app.show(top);
+    top.swimlane.show(new app.SwimlaneGraphView({
+      "spanId": id,
+      "lim": lim
+    }));
+  }
+});
+
+app.on("start", function(options) {
+  app.root = new BaseView();
+  app.routes = new Router();
+
+  Backbone.history.start();
+});
+
+app.start();

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/web/app/views/details/details.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/views/details/details.js b/htrace-htraced/src/web/app/views/details/details.js
new file mode 100644
index 0000000..2f79e1b
--- /dev/null
+++ b/htrace-htraced/src/web/app/views/details/details.js
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+app.DetailsView = Backbone.Marionette.LayoutView.extend({
+  "template": "#details-layout-template",
+  "regions": {
+    "span": "div[role='complementary']",
+    "content": "div[role='main']"
+  }
+});
+
+app.SpanDetailsView = Backbone.Marionette.ItemView.extend({
+  "className": "span",
+  "template": "#span-details-template",
+
+  "serializeData": function() {
+    var context = {
+      "span": this.model.toJSON()
+    };
+    context["span"]["duration"] = this.model.duration();
+    return context;
+  },
+  
+  "events": {
+    "click": "swimlane"
+  },
+  "swimlane": function() {
+    Backbone.history.navigate("!/swimlane/" + this.model.get("spanId"),
+                              {"trigger": true});
+  }
+});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/web/app/views/graph/graph.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/views/graph/graph.js b/htrace-htraced/src/web/app/views/graph/graph.js
new file mode 100644
index 0000000..7b4f89e
--- /dev/null
+++ b/htrace-htraced/src/web/app/views/graph/graph.js
@@ -0,0 +1,262 @@
+/*
+ * 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.
+ */
+
+app.GraphView = Backbone.View.extend({
+  initialize: function(options) {
+    options = options || {};
+
+    if (!options.id) {
+      console.error("GraphView requires argument 'id' to uniquely identify this graph.");
+      return;
+    }
+
+    _.bindAll(this, "render");
+    this.collection.bind('change', this.render);
+
+    var links = this.links = [];
+    var linkTable = this.linkTable = {};
+    var nodes = this.nodes = [];
+    var nodeTable = this.nodeTable = {};
+    var force = this.force
+        = d3.layout.force().size([$(window).width(), $(window).height() * 3/4])
+                           .linkDistance($(window).height() / 5)
+                           .charge(-120)
+                           .gravity(0)
+                           ;
+    force.nodes(nodes)
+         .links(links);
+
+    force.on("tick", function(e) {
+      var root = d3.select("#" + options.id);
+      
+      if (!root.node()) {
+        return;
+      }
+
+      var selectedDatum = root.select(".selected").datum();
+
+      // center selected node
+      root.select("svg").attr("width", $(root.node()).width());
+      selectedDatum.x = root.select("svg").attr("width") / 2;
+      selectedDatum.y = root.select("svg").attr("height") / 2;
+
+      // Push sources up and targets down to form a weak tree.
+      var k = 10 * e.alpha;
+      force.links().forEach(function(d, i) {
+        d.source.y -= k;
+        d.target.y += k;
+      });
+
+      var nodes = root.selectAll(".node").data(force.nodes());
+      nodes.select("circle")
+        .attr("cx", function(d) { return d.x; })
+        .attr("cy", function(d) { return d.y; });
+      nodes.select("text")
+        .attr("x", function(d) { return d.x - this.getComputedTextLength() / 2; })
+        .attr("y", function(d) { return d.y; });
+      root.selectAll(".link").data(force.links())
+        .attr("d", function(d) {
+          var start = {},
+              end = {},
+              angle = Math.atan2((d.target.x - d.source.x), (d.target.y - d.source.y));
+          start.x = d.source.x + d.source.r * Math.sin(angle);
+          end.x = d.target.x - d.source.r * Math.sin(angle);
+          start.y = d.source.y + d.source.r * Math.cos(angle);
+          end.y = d.target.y - d.source.r * Math.cos(angle);
+          return "M" + start.x + " " + start.y
+              + " L" + end.x + " " + end.y;
+        });
+    });
+  },
+
+  updateLinksAndNodes: function() {
+    if (!this.spanId) {
+      return;
+    }
+
+    var $this = this, collection = this.collection;
+
+    var selectedSpan = this.collection.findWhere({
+      "spanId": this.spanId
+    });
+
+    var findChildren = function(span) {
+      var spanId = span.get("spanId");
+      var spans = collection.filter(function(model) {
+        return _(model.get("parents")).contains(spanId);
+      });
+      return _(spans).reject(function(span) {
+        return span == null;
+      });
+    };
+    var findParents = function(span) {
+      var spans = _(span.get("parents")).map(function(parentSpanId) {
+        return collection.findWhere({
+          "spanId": parentSpanId
+        });
+      });
+      return _(spans).reject(function(span) {
+        return span == null;
+      });
+    };
+    var spanToNode = function(span, level) {
+      var table = $this.nodeTable;
+      if (!(span.get("spanId") in table)) {
+        table[span.get("spanId")] = {
+          "name": span.get("spanId"),
+          "span": span,
+          "level": level,
+          "group": 0,
+          "x": parseInt($this.svg.attr('width')) / 2,
+          "y": 250 + level * 50
+        };
+        $this.nodes.push(table[span.get("spanId")]);
+      }
+
+      return table[span.get("spanId")];
+    };
+    var createLink = function(source, target) {
+      var table = $this.linkTable;
+      var name = source.span.get("spanId") + "-" + target.span.get("spanId");
+      if (!(name in table)) {
+        table[name] = {
+          "source": source,
+          "target": target
+        };
+        $this.links.push(table[name]);
+      }
+
+      return table[name];
+    };
+
+    var parents = [], children = [];
+    var selectedSpanNode = spanToNode(selectedSpan, 1);
+
+    Array.prototype.push.apply(parents, findParents(selectedSpan));
+    _(parents).each(function(span) {
+      Array.prototype.push.apply(parents, findParents(span));
+      createLink(spanToNode(span, 0), selectedSpanNode)
+    });
+
+    Array.prototype.push.apply(children, findChildren(selectedSpan));
+    _(children).each(function(span) {
+      Array.prototype.push.apply(children, findChildren(span));
+      createLink(selectedSpanNode, spanToNode(span, 2))
+    });
+  },
+
+  renderLinks: function(selection) {
+    var path = selection.enter().append("path")
+        .classed("link", true)
+        .style("marker-end",  "url(#suit)");
+    selection.exit().remove();
+    return selection;
+  },
+
+  renderNodes: function(selection) {
+    var $this = this;
+    var g = selection.enter().append("g").attr("class", "node");
+    var circle = g.append("circle")
+      .attr("r", function(d) {
+        if (!d.radius) {
+          d.r = Math.log(d.span.duration());
+         
+          if (d.r > app.GraphView.MAX_NODE_SIZE) {
+            d.r = app.GraphView.MAX_NODE_SIZE;
+          }
+
+          if (d.r < app.GraphView.MIN_NODE_SIZE) {
+            d.r = app.GraphView.MIN_NODE_SIZE;
+          }
+        }
+
+        return d.r;
+      });
+    var text = g.append("text").text(function(d) {
+      return d.span.get("description");
+    });
+
+    selection.exit().remove();
+
+    circle.on("click", function(d) {
+      $this.setSpanId(d.name);
+    });
+
+    selection.classed("selected", null);
+    selection.filter(function(d) {
+      return d.span.get("spanId") == $this.spanId;
+    }).classed("selected", true);
+    
+    return selection;
+  },
+
+  setSpanId: function(spanId) {
+    var $this = this;
+    this.spanId = spanId;
+
+    this.updateLinksAndNodes();
+
+    this.renderNodes(
+      this.svg.selectAll(".node")
+        .data(this.force.nodes(), function(d) {
+          return d.name;
+        }));
+
+    this.renderLinks(
+      this.svg.selectAll(".link")
+        .data(this.force.links(), function(d) {
+          return d.source.name + "-" + d.target.name;
+        }));
+
+    this.force.start();
+
+    Backbone.history.navigate("!/spans/" + spanId);
+    this.trigger("update:span", {"span": this.collection.findWhere({
+      "spanId": spanId
+    })});
+  },
+
+  render: function() {
+    this.svg = d3.select(this.$el[0]).append("svg");
+    this.svg.attr("height", 500)
+       .attr("width", $(window).width())
+       .attr("id", this.id);
+
+    // Arrows
+    this.svg.append("defs").selectAll("marker")
+      .data(["suit", "licensing", "resolved"])
+    .enter().append("marker")
+      .attr("id", function(d) { return d; })
+      .attr("viewBox", "0 -5 10 10")
+      .attr("refX", 25)
+      .attr("refY", 0)
+      .attr("markerWidth", 6)
+      .attr("markerHeight", 6)
+      .attr("orient", "auto")
+    .append("path")
+      .attr("d", "M0,-5L10,0L0,5 L10,0 L0, -5")
+      .style("stroke", "#4679BD")
+      .style("opacity", "0.6");
+
+    return this;
+  }
+});
+
+app.GraphView.MAX_NODE_SIZE = 150;
+app.GraphView.MIN_NODE_SIZE = 50;

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/web/app/views/search/field.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/views/search/field.js b/htrace-htraced/src/web/app/views/search/field.js
new file mode 100644
index 0000000..c9f048a
--- /dev/null
+++ b/htrace-htraced/src/web/app/views/search/field.js
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+
+app.SearchFieldView = Backbone.View.extend({
+  'className': 'search-field',
+
+  'template': _.template($("#search-field-template").html()),
+
+  'events': {
+    'change .field': 'showSearchField',
+     'click .remove-field': 'destroyField'
+  },
+
+  'initialize': function(options) {
+    this.options = options;
+    this.field = options.field;
+  },
+
+  'render': function() {
+    this.$el.html(this.template({ field: this.field }));
+    this.showSearchField();
+    if (this.options.value) this.setValue();
+    return this;
+  },
+
+  'showSearchField': function() {
+    // this.$el.find('.value').hide();
+    // this.$el.find('.op').hide();
+    // this.$el.find('label').hide();
+    this.$el.find('.search-field').hide();
+    switch (this.field) {
+      case 'begin':
+      case 'end':
+        this.$el.find('.op').show();
+        this.$el.find('.start-end-date-time').show();
+        this.$el.find('label.start-end-date-time').text(this.field === 'begin' ? 'Begin' : 'End');
+        rome(this.$el.find('#start-end-date-time')[0]);
+        break;
+      case 'duration':
+        this.op = 'ge'
+        this.$el.find('.duration').show();
+        break;
+      case 'description':
+        this.op = 'cn'
+        this.$el.find('.description').show();
+        break;
+      default:
+        break;
+    }
+  },
+
+  'destroyField': function(e) {
+    this.undelegateEvents();
+
+    $(this.el).removeData().unbind();
+
+    this.remove();
+    Backbone.View.prototype.remove.call(this);
+    this.options.manager.trigger('removeSearchField', [this.cid]);
+  },
+
+  'addPredicate': function() {
+    this.options.predicates.push(
+      {
+        'op': this.op ? this.op : this.$('.op:visible').val(),
+        'field': this.field,
+        'val': this.getValue()
+      }
+    );
+  },
+
+  'getPredicate': function() {
+    return {
+      'op': this.op ? this.op : this.$('.op:visible').val(),
+      'field': this.field,
+      'val': this.getValue()
+    };
+  },
+
+  'getValue': function() {
+    switch (this.field) {
+      case 'begin':
+      case 'end':
+        var now = new moment();
+        var datetime = new moment(this.$('input.start-end-date-time:visible').val()).unix();
+        return datetime.toString();
+      case 'duration':
+        return this.$("input.duration:visible").val().toString();
+      case 'description':
+        return this.$('input.description').val();
+      default:
+        return '';
+    }
+  },
+
+  'setValue': function() {
+    switch (this.field) {
+      case 'begin':
+      case 'end':
+        this.$('select.op').val(this.options.op);
+        this.$('input.start-end-date-time').val(moment.unix(this.options.value).format('YYYY-MM-DD HH:mm'));
+      case 'duration':
+        this.$("input.duration").val(this.options.value);
+      case 'description':
+        this.$('input.description').val(this.options.value);
+    }
+  }
+});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/web/app/views/search/search.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/views/search/search.js b/htrace-htraced/src/web/app/views/search/search.js
new file mode 100644
index 0000000..b9acee5
--- /dev/null
+++ b/htrace-htraced/src/web/app/views/search/search.js
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+app.SearchView = Backbone.Marionette.LayoutView.extend({
+  "template": "#search-layout-template",
+  "regions": {
+    "controls": "div[role='form']",
+    "main": "div[role='main']",
+    "pagination": "div[role='complementary']"
+  }
+});
+
+app.SearchControlsView = Backbone.Marionette.View.extend({
+  "template": _.template($("#search-controls-template").html()),
+  "events": {
+    "click a.add-field": "addSearchField",
+    "click button.search": "search",
+  },
+
+  "initialize": function(options) {
+    this.options = options;
+    this.predicates = [];
+    this.searchFields = [];
+    this.searchFields.push(new app.SearchFieldView({
+      predicates: this.predicates,
+      manager: this,
+      field: 'description'
+    }));
+    this.on('removeSearchField', this.removeSearchField, this);
+  },
+
+  "render": function() {
+    this.$el.html(this.template());
+    this.$el.find('.search-fields').append(this.searchFields[0].render().$el);
+
+    _(this.options.predicates).each(function(pred) {
+      if (pred.field === 'description') {
+        this.$el.find('input.description').val(pred.val);
+      } else {
+        this.addSearchField(pred);
+      }
+    }.bind(this));
+
+    return this;
+  },
+
+  "addSearchField": function(e) {
+    var target = e.target ? $(e.target) : e;
+    if (e.target) $('button.field').text(target.text());
+    var searchOptions = {
+      predicates: this.predicates,
+      manager: this,
+      field: target.data ? target.data('field') : target.field,
+    };
+    if (!e.target) _.extend(searchOptions, { value: target.val, op: target.op})
+
+    var newSearchField = new app.SearchFieldView(searchOptions);
+    this.$el.find('.search-fields').append(newSearchField.render().$el);
+    this.searchFields.push(newSearchField);
+  },
+
+  "removeSearchField": function(cid) {
+    var removedFieldIndex = _(this.searchFields).indexOf(_(this.searchFields).findWhere({cid: cid}));
+    this.searchFields.splice(removedFieldIndex, 1);
+  },
+
+  "search": function(e) {
+    this.predicates = _(this.searchFields).map(function(field) {
+      return field.getPredicate();
+    }).filter(function(predicate) {
+      return predicate.val;
+    });
+
+    this.searchParams = _(this.predicates).map(function(predicate) {
+      return $.param(predicate);
+    }).join(';');
+    Backbone.history.navigate('!/search?' + this.searchParams, { trigger: false });
+
+    this.collection.switchMode("infinite", {
+      fetch: false,
+      resetState: true
+    });
+
+    this.collection.fullCollection.reset();
+    this.collection.setPredicates(this.predicates);
+    this.collection.fetch();
+    return false;
+  }
+});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/web/app/views/swimlane/swimlane.js
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/app/views/swimlane/swimlane.js b/htrace-htraced/src/web/app/views/swimlane/swimlane.js
new file mode 100644
index 0000000..99f0b88
--- /dev/null
+++ b/htrace-htraced/src/web/app/views/swimlane/swimlane.js
@@ -0,0 +1,178 @@
+/**
+ * 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.
+ */
+
+app.SwimlaneView = Backbone.Marionette.LayoutView.extend({
+  "template": "#swimlane-layout-template",
+  "regions": {
+    "swimlane": "div[role='main']",
+  }
+});
+
+app.SwimlaneGraphView = Backbone.Marionette.View.extend({
+  className: "swimlane",
+
+  initialize: function() {
+    this.spans = this.getSpans(0, [], 
+                               this.getJsonSync("/span/" + this.options.spanId),
+                               this.options.lim || "lim=100",
+                               this.getJsonSync);
+  },
+  
+  onShow: function() {
+    this.appendSVG(this.spans);
+  },
+
+  getSpans: function getSpans(depth, spans, span, lim, getJSON) {
+    span.depth = depth;
+    spans.push(span);
+    var children = [];
+    getJSON("/span/" + span.s + "/children?" + lim).forEach(function(childId) {
+      children.push(getJSON("/span/" + childId));
+    });
+    children.sort(function(x, y) {
+      return x.b < y.b ? -1 : x.b > y.b ? 1 : 0;
+    });
+    children.forEach(function(child) {
+      spans = getSpans(depth + 1, spans, child, lim, getJSON);
+    });
+    return spans;
+  },
+
+  getJsonSync: function getJsonSync(url) {
+    return $.ajax({
+      type: "GET",
+      url: url,
+      async: false,
+      dataType: "json"
+    }).responseJSON;
+  },
+
+  appendSVG: function appendSVG(spans) {
+    const height_span = 20;
+    const width_span = 700;
+    const size_tl = 6;
+    const margin = {top: 50, bottom: 50, left: 20, right: 1000, process: 300};
+
+    var height_screen = spans.length * height_span;
+    var dmax = d3.max(spans, function(s) { return s.depth; });
+    var tmin = d3.min(spans, function(s) { return s.b; });
+    var tmax = d3.max(spans, function(s) { return s.e; });
+    var xscale = d3.time.scale()
+      .domain([new Date(tmin), new Date(tmax)]).range([0, width_span]);
+
+    var svg = d3.select("div[role='main']").append("svg")
+      .attr("id", "svg-swimlane")
+      .attr("width", width_span + margin.process + margin.left + margin.right)
+      .attr("height", height_screen + margin.top + margin.bottom)
+      .append("g")
+      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+    var bars = svg.append("g")
+      .attr("id", "bars")
+      .attr("width", width_span)
+      .attr("height", height_screen)
+      .attr("transform", "translate(" + (10 * dmax + margin.process) + ", 0)");
+
+    var axis = d3.svg.axis()
+      .scale(xscale)
+      .orient("top")
+      .tickValues(xscale.domain())
+      .tickFormat(d3.time.format("%x %X.%L"))
+      .tickSize(6, 3);
+
+    bars.append("g").attr("class", "axis").call(axis);
+    
+    var span_g = bars.selectAll("g.span")
+      .data(spans)
+      .enter()
+      .append("g")
+      .attr("transform", function(s, i) {
+        return "translate(0, " + (i * height_span + 5) + ")";
+      })
+      .classed("timeline", function(d) { return d.t; });
+
+    span_g.append("text")
+      .text(function(s) { return s.r; })
+      .style("alignment-baseline", "hanging")
+      .style("font-size", "14px")
+      .attr("transform", function(s) {
+        return "translate(" + (s.depth * 10 - margin.process - 10 * dmax) + ", 0)";
+      });
+
+    var rect_g = span_g.append("g")
+      .attr("transform", function(s) {
+        return "translate(" + xscale(new Date(s.b)) + ", 0)";
+      });
+
+    rect_g.append("rect")
+      .attr("height", height_span - 1)
+      .attr("width", function (s) {
+        return (width_span * (s.e - s.b)) / (tmax - tmin) + 1;
+      })
+      .style("fill", "lightblue")
+      .attr("class", "span")
+
+    rect_g.append("text")
+      .text(function(s){ return s.d; })
+      .style("alignment-baseline", "hanging")
+      .style("font-size", "14px");
+
+    rect_g.append("text")
+      .text(function(s){ return s.e - s.b; })
+      .style("alignment-baseline", "baseline")
+      .style("text-anchor", "end")
+      .style("font-size", "10px")
+      .attr("transform", function(s, i) { return "translate(0, 10)"; });
+
+    bars.selectAll("g.timeline").selectAll("rect.timeline")
+      .data(function(s) { return s.t; })
+      .enter()
+      .append("rect")
+      .style("fill", "red")
+      .attr("class", "timeline")
+      .attr("height", size_tl)
+      .attr("width", size_tl)
+      .attr("transform", function(t) {
+        return "translate(" + xscale(t.t) + "," + (height_span - 1 - size_tl) + ")";
+      });
+
+    var popup = d3.select("div[role='main']").append("div")
+      .attr("class", "popup")
+      .style("opacity", 0);
+
+    bars.selectAll("g.timeline")
+      .on("mouseover", function(d) {
+        popup.transition().duration(300).style("opacity", .95);
+        var text = "<table>";
+        d.t.forEach(function (t) {
+          text += "<tr><td>" + (t.t - tmin) + "</td>";
+          text += "<td> : " + t.m + "</td></tr>";
+        });
+        text += "</table>"
+        popup.html(text)
+          .style("left", (document.body.scrollLeft + 50) + "px")
+          .style("top", (document.body.scrollTop + 70) + "px")
+          .style("width", "700px")
+          .style("background", "orange")
+          .style("position", "absolute");
+      })
+      .on("mouseout", function(d) {
+        popup.transition().duration(300).style("opacity", 0);
+      });
+  }
+});

http://git-wip-us.apache.org/repos/asf/incubator-htrace/blob/39e89ea0/htrace-htraced/src/web/index.html
----------------------------------------------------------------------
diff --git a/htrace-htraced/src/web/index.html b/htrace-htraced/src/web/index.html
new file mode 100644
index 0000000..d403860
--- /dev/null
+++ b/htrace-htraced/src/web/index.html
@@ -0,0 +1,196 @@
+<!doctype html>
+<!--
+   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.
+-->
+<html lang="en-US">
+  <head>
+    <meta charset="utf-8">
+    <title>HTrace</title>
+
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+
+    <!-- TODO: Add Favicon -->
+    <link rel="icon" href="//favicon.ico" type="image/x-icon" sizes="16x16">
+    <link href="lib/bootstrap-3.3.1/css/bootstrap.css" rel="stylesheet">
+    <link href="lib/css/backgrid-0.3.5.min.css" rel="stylesheet">
+    <link href="lib/css/backgrid-paginator-0.3.5.min.css" rel="stylesheet">
+    <link href="lib/rome-2.1.0/rome.min.css" rel="stylesheet">
+    <link href="lib/css/main.css" rel="stylesheet">
+
+    <!-- TODO: Remove shiv -->
+    <!--[if lt IE 9]>
+      <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.6.2/html5shiv.js"></script>
+    <![endif]-->
+  </head>
+  <body>
+    <header id="header" role="banner">
+      <nav class="navbar navbar-default navbar-static-top" role="navigation">
+        <div class="container-fluid">
+          <a class="navbar-brand" href="#">HTrace</a>
+        </div>
+      </nav>
+    </header>
+
+    <div id="app" class="container-fluid" role="application"></div>
+
+    <footer></footer>
+
+    <script id="search-layout-template" type="text/html">
+    <div class="container-fluid" id="list" role="application">
+      <div class="row">
+        <div class="col-md-4" role="form"></div>
+        <div class="col-md-8">
+          <div class="row">
+            <div class="col-md-12" role="main"></div>
+          </div>
+          <div class="row">
+            <div class="col-md-12" role="complementary"></div>
+          </div>
+        </div>
+      </div>
+    </div>
+    </script>
+
+    <script id="search-controls-template" type="text/html">
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <h3 class="panel-title">Controls</h3>
+      </div>
+      <div class="panel-body">
+        <form class="form-horizontal">
+          <div class="search-fields"></div>
+          <div class="form-group">
+            <div class="col-sm-12">
+              <div class="btn-group">
+                <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
+                  Add Field <span class="caret"></span>
+                </button>
+                <ul class="dropdown-menu" role="menu">
+                  <li><a href="javascript:void(0)" class="add-field" data-field="begin">Start Date/Time</a></li>
+                  <li><a href="javascript:void(0)" class="add-field" data-field="end">End Date/Time</a></li>
+                  <li><a href="javascript:void(0)" class="add-field" data-field="duration">Duration</a></li>
+                </ul>
+              </div>
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="col-sm-12">
+              <button type="button" class="search btn btn-default">Search</button>
+            </div>
+          </div>
+        </form>
+      </div>
+    </div>
+    </script>
+
+    <script id='search-field-template' type='text/html'>
+      <div class='form-group search-field start-end-date-time'>
+        <label for="start-end-date-time" class="start-end-date-time control-label col-sm-3">Date</label>
+        <div class="col-sm-3">
+          <select class='op form-control'>
+            <option selected value='ge'>After</option>
+            <option value='le'>Before</option>
+          </select>
+        </div>
+        <div class='col-sm-5'>
+          <input placeholder="Date/Time" id="start-end-date-time" class="start-end-date-time date form-control value" />
+        </div>
+        <button class="btn btn-link remove-field" type="button">x</button>
+      </div>
+      <div class='form-group search-field duration'>
+        <label for="duration" class="duration control-label col-sm-3">Duration</label>
+        <div class='col-sm-8'>
+          <input type="text" class="duration form-control value" placeholder="Duration" />
+        </div>
+        <button class="btn btn-link remove-field" type="button">x</button>
+      </div>
+      <div class='form-group search-field description'>
+        <label for="description" class="description control-label col-sm-3">Description</label>
+        <div class='col-sm-8'>
+        <input type="search" id="description" class="description value form-control" placeholder="Search description" />
+        </div>
+      </div>
+    </script>
+
+    <script id="details-layout-template" type="text/html">
+    <div class="container-fluid" id="list" role="application">
+      <div class="row">
+        <div class="col-md-12" role="main"></div>
+      </div>
+
+      <hr>
+
+      <div class="row">
+        <div class="col-md-12" role="complementary"></div>
+      </div>
+    </div>
+    </script>
+
+    <script id="span-details-template" type="text/html">
+    <table class="table table-condensed">
+      <thead>
+        <tr>
+          <th>Description</th>
+          <th>Span ID</th>
+          <th>Process ID</th>
+          <th>Start time</th>
+          <th>End time</th>
+          <th>Duration</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td><%- span.description %></td>
+          <td><%- span.spanId %></td>
+          <td><%- span.processId %></td>
+          <td><%- span.beginTime %></td>
+          <td><%- span.stopTime %></td>
+          <td><%- span.duration %></td>
+        </tr>
+      </tbody>
+    </table>
+    </script>
+
+    <script id="swimlane-layout-template" type="text/html">
+    <div class="container-fluid" id="list" role="application">
+      <div class="row">
+        <div class="col-md-12" role="main"></div>
+      </div>
+    </div>
+    </script>
+
+    <script src="lib/js/jquery-2.1.3.min.js" type="text/javascript"></script>
+    <script src="lib/js/d3-3.5.5.js" type="text/javascript"></script>
+    <script src="lib/bootstrap-3.3.1/js/bootstrap.min.js" type="text/javascript"></script>
+    <script src="lib/js/underscore-1.7.0.js" type="text/javascript"></script>
+    <script src="lib/js/backbone-1.1.2.js" type="text/javascript"></script>
+    <script src="lib/js/backbone.marionette-2.4.1.min.js" type="text/javascript"></script>
+    <script src="lib/js/backbone.paginator-2.0.2.js" type="text/javascript"></script>
+    <script src="lib/js/backgrid-0.3.5.js" type="text/javascript"></script>
+    <script src="lib/js/backgrid-paginator-0.3.5.js" type="text/javascript"></script>
+    <script src="lib/js/moment-2.9.0.min.js" type="text/javascript"></script>
+    <script src="lib/rome-2.1.0/rome.standalone.min.js" type="text/javascript"></script>
+
+    <script src="app/app.js" type="text/javascript"></script>
+    <script src="app/models/span.js" type="text/javascript"></script>
+    <script src="app/views/graph/graph.js" type="text/javascript"></script>
+    <script src="app/views/search/field.js" type="text/javascript"></script>
+    <script src="app/views/search/search.js" type="text/javascript"></script>
+    <script src="app/views/details/details.js" type="text/javascript"></script>
+    <script src="app/views/swimlane/swimlane.js" type="text/javascript"></script>
+    <script src="app/setup.js" type="text/javascript"></script>
+  </body>
+</html>