Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance the details pane #68

Merged
merged 4 commits into from
May 22, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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