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, ¬if) + 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>
