Skip to content

Commit

Permalink
Merge pull request #68 from weaveworks/enhanced-details
Browse files Browse the repository at this point in the history
Enhance the details pane
  • Loading branch information
peterbourgon committed May 22, 2015
2 parents a045fb6 + d42b03f commit 65cfe63
Show file tree
Hide file tree
Showing 26 changed files with 530 additions and 757 deletions.
9 changes: 6 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ SCOPE_UI_BUILD_IMAGE=$(DOCKERHUB_USER)/scope-ui-build

all: $(SCOPE_EXPORT)

$(SCOPE_EXPORT): $(APP_EXE) $(PROBE_EXE) docker/*
$(SCOPE_EXPORT): $(APP_EXE) $(PROBE_EXE) docker/*
cp $(APP_EXE) $(PROBE_EXE) docker/
$(SUDO) docker build -t $(SCOPE_IMAGE) docker/
$(SUDO) docker save $(SCOPE_IMAGE):latest | $(SUDO) $(DOCKER_SQUASH) -t $(SCOPE_IMAGE) | tee $@ | $(SUDO) docker load
Expand Down Expand Up @@ -45,8 +45,11 @@ clean:
rm -rf $(SCOPE_EXPORT) $(SCOPE_UI_BUILD_EXPORT) client/dist

deps:
go get github.com/jwilder/docker-squash \
go get \
github.com/jwilder/docker-squash \
github.com/golang/lint/golint \
github.com/fzipp/gocyclo \
github.com/mattn/goveralls \
github.com/mjibson/esc
github.com/mjibson/esc \
github.com/davecgh/go-spew/spew \
github.com/pmezard/go-difflib/difflib
77 changes: 12 additions & 65 deletions app/api_topology.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package main

import (
"fmt"
"net/http"
"strconv"
"time"

"github.com/gorilla/mux"
Expand Down Expand Up @@ -54,12 +52,9 @@ func makeTopologyHandlers(
) {
// Full topology.
get.HandleFunc(base, func(w http.ResponseWriter, r *http.Request) {
rpt := rep.Report()
rendered := topo(rpt).RenderBy(mapping, grouped)
t := APITopology{
Nodes: rendered,
}
respondWith(w, http.StatusOK, t)
respondWith(w, http.StatusOK, APITopology{
Nodes: topo(rep.Report()).RenderBy(mapping, grouped),
})
})

// Websocket for the full topology. This route overlaps with the next.
Expand Down Expand Up @@ -88,8 +83,9 @@ func makeTopologyHandlers(
http.NotFound(w, r)
return
}
of := func(nodeID string) (Origin, bool) { return getOrigin(rep, nodeID) }
respondWith(w, http.StatusOK, APINode{Node: makeDetailed(node, of)})
originHostFunc := func(id string) (OriginHost, bool) { return getOriginHost(rpt.HostMetadatas, id) }
originNodeFunc := func(id string) (OriginNode, bool) { return getOriginNode(topo(rpt), id) }
respondWith(w, http.StatusOK, APINode{Node: makeDetailed(node, originHostFunc, originNodeFunc)})
})

// Individual edges.
Expand All @@ -105,48 +101,6 @@ func makeTopologyHandlers(
})
}

// TODO(pb): temporary hack
func makeDetailed(n report.RenderableNode, of func(string) (Origin, bool)) report.DetailedNode {
// A RenderableNode may be the result of merge operation(s), and so may
// have multiple origins.
origins := []report.Table{}
for _, originID := range n.Origin {
origin, ok := of(originID)
if !ok {
origin = unknownOrigin
}
origins = append(origins, report.Table{
Title: "Origin",
Numeric: false,
Rows: []report.Row{
{"Hostname", origin.Hostname, ""},
{"Load", fmt.Sprintf("%.2f %.2f %.2f", origin.LoadOne, origin.LoadFive, origin.LoadFifteen), ""},
{"OS", origin.OS, ""},
//{"Addresses", strings.Join(origin.Addresses, ", "), ""},
{"ID", originID, ""},
},
})
}

tables := []report.Table{}
tables = append(tables, report.Table{
Title: "Connections",
Numeric: true,
Rows: []report.Row{
{"TCP (max)", strconv.FormatInt(int64(n.Metadata[report.KeyMaxConnCountTCP]), 10), ""},
},
})
tables = append(tables, origins...)

return report.DetailedNode{
ID: n.ID,
LabelMajor: n.LabelMajor,
LabelMinor: n.LabelMinor,
Pseudo: n.Pseudo,
Tables: tables,
}
}

var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
Expand All @@ -169,16 +123,18 @@ func handleWebsocket(

quit := make(chan struct{})
go func(c *websocket.Conn) {
// Discard all the browser sends us.
for {
for { // just discard everything the browser sends
if _, _, err := c.NextReader(); err != nil {
close(quit)
break
}
}
}(conn)

var previousTopo map[string]report.RenderableNode
var (
previousTopo map[string]report.RenderableNode
tick = time.Tick(loop)
)
for {
newTopo := topo(rep.Report()).RenderBy(mapping, grouped)
diff := report.TopoDiff(previousTopo, newTopo)
Expand All @@ -192,16 +148,7 @@ func handleWebsocket(
select {
case <-quit:
return
case <-time.After(loop):
case <-tick:
}
}
}

var unknownOrigin = Origin{
Hostname: "unknown",
OS: "unknown",
Addresses: []string{},
LoadOne: 0.0,
LoadFive: 0.0,
LoadFifteen: 0.0,
}
46 changes: 20 additions & 26 deletions app/api_topology_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ import (
func TestAPITopologyApplications(t *testing.T) {
ts := httptest.NewServer(Router(StaticReport{}))
defer ts.Close()

is404(t, ts, "/api/topology/applications/foobar")

{
body := getRawJSON(t, ts, "/api/topology/applications")
var topo APITopology
if err := json.Unmarshal(body, &topo); err != nil {
t.Fatalf("JSON parse error: %s", err)
t.Fatal(err)
}
equals(t, 4, len(topo.Nodes))
node, ok := topo.Nodes["pid:node-a.local:23128"]
Expand All @@ -30,25 +28,25 @@ func TestAPITopologyApplications(t *testing.T) {
}
equals(t, 1, len(node.Adjacency))
equals(t, report.NewIDList("pid:node-b.local:215"), node.Adjacency)
equals(t, report.NewIDList("hostA"), node.Origin)
equals(t, report.NewIDList("hostA"), node.OriginHosts)
equals(t, "curl", node.LabelMajor)
equals(t, "node-a.local (23128)", node.LabelMinor)
equals(t, "23128", node.Rank)
equals(t, false, node.Pseudo)
}

{
// Node detail
body := getRawJSON(t, ts, "/api/topology/applications/pid:node-a.local:23128")
var node APINode
if err := json.Unmarshal(body, &node); err != nil {
t.Fatalf("JSON parse error: %s", err)
t.Fatal(err)
}
// TODO(pb): replace
equals(t, "pid:node-a.local:23128", node.Node.ID)
equals(t, "curl", node.Node.LabelMajor)
equals(t, "node-a.local (23128)", node.Node.LabelMinor)
equals(t, false, node.Node.Pseudo)
// Let's not unit-test the specific content of the detail tables
}

{
// Edge detail
body := getRawJSON(t, ts, "/api/topology/applications/pid:node-a.local:23128/pid:node-b.local:215")
var edge APIEdge
if err := json.Unmarshal(body, &edge); err != nil {
Expand All @@ -68,41 +66,38 @@ func TestAPITopologyApplications(t *testing.T) {
func TestAPITopologyHosts(t *testing.T) {
ts := httptest.NewServer(Router(StaticReport{}))
defer ts.Close()

is404(t, ts, "/api/topology/hosts/foobar")

{
body := getRawJSON(t, ts, "/api/topology/hosts")
var topo APITopology
if err := json.Unmarshal(body, &topo); err != nil {
t.Fatalf("JSON parse error: %s", err)
t.Fatal(err)
}

equals(t, 3, len(topo.Nodes))
node, ok := topo.Nodes["host:host-b"]
if !ok {
t.Errorf("missing host:host-b node")
}
equals(t, report.NewIDList("host:host-a"), node.Adjacency)
equals(t, report.NewIDList("hostB"), node.Origin)
equals(t, report.NewIDList("hostB"), node.OriginHosts)
equals(t, "host-b", node.LabelMajor)
equals(t, "", node.LabelMinor)
equals(t, "host-b", node.Rank)
equals(t, false, node.Pseudo)
}

{
// Node detail
body := getRawJSON(t, ts, "/api/topology/hosts/host:host-b")
var node APINode
if err := json.Unmarshal(body, &node); err != nil {
t.Fatalf("JSON parse error: %s", err)
t.Fatal(err)
}
// TODO(pb): replace
equals(t, "host:host-b", node.Node.ID)
equals(t, "host-b", node.Node.LabelMajor)
equals(t, "", node.Node.LabelMinor)
equals(t, false, node.Node.Pseudo)
// Let's not unit-test the specific content of the detail tables
}

{
// Edge detail
body := getRawJSON(t, ts, "/api/topology/hosts/host:host-b/host:host-a")
var edge APIEdge
if err := json.Unmarshal(body, &edge); err != nil {
Expand All @@ -123,24 +118,23 @@ func TestAPITopologyHosts(t *testing.T) {
func TestAPITopologyWebsocket(t *testing.T) {
ts := httptest.NewServer(Router(StaticReport{}))
defer ts.Close()

url := "/api/topology/applications/ws"

// Not a websocket request:
// Not a websocket request
res, _ := checkGet(t, ts, url)
if have := res.StatusCode; have != 400 {
t.Fatalf("Expected status %d, got %d.", 400, have)
}

// Proper websocket request:
// Proper websocket request
ts.URL = "ws" + ts.URL[len("http"):]
dialer := &websocket.Dialer{}
ws, res, err := dialer.Dial(ts.URL+url, nil)
ok(t, err)
defer ws.Close()

if have := res.StatusCode; have != 101 {
t.Fatalf("Expected status %d, got %d.", 101, have)
if want, have := 101, res.StatusCode; want != have {
t.Fatalf("want %d, have %d", want, have)
}

_, p, err := ws.ReadMessage()
Expand Down
97 changes: 97 additions & 0 deletions app/detail_pane.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package main

import (
"fmt"
"reflect"
"strconv"

"github.com/weaveworks/scope/report"
)

func makeDetailed(
n report.RenderableNode,
originHostLookup func(string) (OriginHost, bool),
originNodeLookup func(string) (OriginNode, bool),
) report.DetailedNode {
tables := []report.Table{{
Title: "Connections",
Numeric: true,
Rows: []report.Row{
// TODO omit these rows if there's no data?
{"TCP connections", strconv.FormatInt(int64(n.Metadata[report.KeyMaxConnCountTCP]), 10), ""},
{"Bytes ingress", strconv.FormatInt(int64(n.Metadata[report.KeyBytesIngress]), 10), ""},
{"Bytes egress", strconv.FormatInt(int64(n.Metadata[report.KeyBytesEgress]), 10), ""},
},
}}

// Note that a RenderableNode may be the result of merge operation(s), and
// so may have multiple origin hosts and nodes.

outer:
for _, id := range n.OriginNodes {
// Origin node IDs in e.g. the process topology are actually network
// n-tuples. (The process topology is actually more like a network
// n-tuple topology.) So we can have multiple IDs mapping to the same
// process. There are several ways to dedupe that, but here we take
// the lazy way and do simple equivalence of the resulting table.
node, ok := originNodeLookup(id)
if !ok {
node = unknownOriginNode(id)
}
for _, table := range tables {
if reflect.DeepEqual(table, node.Table) {
continue outer
}
}
tables = append(tables, node.Table)
}

for _, id := range n.OriginHosts {
host, ok := originHostLookup(id)
if !ok {
host = unknownOriginHost(id)
}
tables = append(tables, report.Table{
Title: "Origin Host",
Numeric: false,
Rows: []report.Row{
{"Hostname", host.Hostname, ""},
{"Load", fmt.Sprintf("%.2f %.2f %.2f", host.LoadOne, host.LoadFive, host.LoadFifteen), ""},
{"OS", host.OS, ""},
//{"Addresses", strings.Join(host.Addresses, ", "), ""},
{"ID", id, ""},
},
})
}

return report.DetailedNode{
ID: n.ID,
LabelMajor: n.LabelMajor,
LabelMinor: n.LabelMinor,
Pseudo: n.Pseudo,
Tables: tables,
}
}

func unknownOriginHost(id string) OriginHost {
return OriginHost{
Hostname: fmt.Sprintf("[%s]", id),
OS: "unknown",
Addresses: []string{},
LoadOne: 0.0,
LoadFive: 0.0,
LoadFifteen: 0.0,
}
}

func unknownOriginNode(id string) OriginNode {
return OriginNode{
Table: report.Table{
Title: "Origin Node",
Numeric: false,
Rows: []report.Row{
{"ID", id, ""},
},
},
}
}
Loading

0 comments on commit 65cfe63

Please sign in to comment.