diff --git a/render/detailed_node.go b/render/detailed_node.go index 2d7e9f34b..66e42d135 100644 --- a/render/detailed_node.go +++ b/render/detailed_node.go @@ -64,23 +64,27 @@ func (r sortableRows) Less(i, j int) bool { } } -type tables []Table +type sortableTables []Table -func (t tables) Len() int { return len(t) } -func (t tables) Swap(i, j int) { t[i], t[j] = t[j], t[i] } -func (t tables) Less(i, j int) bool { return t[i].Rank > t[j].Rank } +func (t sortableTables) Len() int { return len(t) } +func (t sortableTables) Swap(i, j int) { t[i], t[j] = t[j], t[i] } +func (t sortableTables) Less(i, j int) bool { return t[i].Rank > t[j].Rank } // MakeDetailedNode transforms a renderable node to a detailed node. It uses // aggregate metadata, plus the set of origin node IDs, to produce tables. func MakeDetailedNode(r report.Report, n RenderableNode) DetailedNode { - tables := tables{} + tables := sortableTables{} + + // Figure out if multiple hosts/containers are referenced by the renderableNode + multiContainer, multiHost := getRenderingContext(r, n) + // RenderableNode may be the result of merge operation(s), and so may have // multiple origins. The ultimate goal here is to generate tables to view // in the UI, so we skip the intermediate representations, but we could // add them later. connections := []Row{} for _, id := range n.Origins { - if table, ok := OriginTable(r, id); ok { + if table, ok := OriginTable(r, id, multiHost, multiContainer); ok { tables = append(tables, table) } else if _, ok := r.Endpoint.NodeMetadatas[id]; ok { connections = append(connections, connectionDetailsRows(r.Endpoint, id)...) @@ -89,7 +93,9 @@ func MakeDetailedNode(r report.Report, n RenderableNode) DetailedNode { } } - addConnectionsTable(&tables, connections, r, n) + if table, ok := connectionsTable(connections, r, n); ok { + tables = append(tables, table) + } // Sort tables by rank sort.Sort(tables) @@ -103,7 +109,29 @@ func MakeDetailedNode(r report.Report, n RenderableNode) DetailedNode { } } -func addConnectionsTable(tables *tables, connections []Row, r report.Report, n RenderableNode) { +func getRenderingContext(r report.Report, n RenderableNode) (multiContainer bool, multiHost bool) { + originHosts := make(map[string]struct{}) + originContainers := make(map[string]struct{}) + for _, id := range n.Origins { + for _, topology := range r.Topologies() { + if nmd, ok := topology.NodeMetadatas[id]; ok { + originHosts[report.ExtractHostID(nmd)] = struct{}{} + if id, ok := nmd.Metadata[docker.ContainerID]; ok { + originContainers[id] = struct{}{} + } + } + // Return early if possible + multiHost = len(originHosts) > 1 + multiContainer = len(originContainers) > 1 + if multiHost && multiContainer { + return + } + } + } + return +} + +func connectionsTable(connections []Row, r report.Report, n RenderableNode) (Table, bool) { sec := r.Window.Seconds() rate := func(u *uint64) (float64, bool) { if u == nil { @@ -149,18 +177,19 @@ func addConnectionsTable(tables *tables, connections []Row, r report.Report, n R rows = append(rows, connections...) } if len(rows) > 0 { - *tables = append(*tables, Table{"Connections", true, connectionsRank, rows}) + return Table{"Connections", true, connectionsRank, rows}, true } + return Table{}, false } // OriginTable produces a table (to be consumed directly by the UI) based on // an origin ID, which is (optimistically) a node ID in one of our topologies. -func OriginTable(r report.Report, originID string) (Table, bool) { +func OriginTable(r report.Report, originID string, addHostTags bool, addContainerTags bool) (Table, bool) { if nmd, ok := r.Process.NodeMetadatas[originID]; ok { - return processOriginTable(nmd) + return processOriginTable(nmd, addHostTags, addContainerTags) } if nmd, ok := r.Container.NodeMetadatas[originID]; ok { - return containerOriginTable(nmd) + return containerOriginTable(nmd, addHostTags) } if nmd, ok := r.ContainerImage.NodeMetadatas[originID]; ok { return containerImageOriginTable(nmd) @@ -224,11 +253,9 @@ func connectionDetailsRows(topology report.Topology, originID string) []Row { return rows } -func processOriginTable(nmd report.NodeMetadata) (Table, bool) { +func processOriginTable(nmd report.NodeMetadata, addHostTag bool, addContainerTag bool) (Table, bool) { rows := []Row{} for _, tuple := range []struct{ key, human string }{ - {process.Comm, "Name"}, - {process.PID, "PID"}, {process.PPID, "Parent PID"}, {process.Cmdline, "Command"}, {process.Threads, "# Threads"}, @@ -238,19 +265,37 @@ func processOriginTable(nmd report.NodeMetadata) (Table, bool) { } } + if containerID, ok := nmd.Metadata[docker.ContainerID]; ok && addContainerTag { + rows = append([]Row{{Key: "Container ID", ValueMajor: containerID}}, rows...) + } + + if addHostTag { + rows = append([]Row{{Key: "Host", ValueMajor: report.ExtractHostID(nmd)}}, rows...) + } + + var ( + title = "Process" + name, commFound = nmd.Metadata[process.Comm] + pid, pidFound = nmd.Metadata[process.PID] + ) + if commFound { + title += ` "` + name + `"` + } + if pidFound { + title += " (" + pid + ")" + } return Table{ - Title: "Origin Process", + Title: title, Numeric: false, Rows: rows, Rank: processRank, - }, len(rows) > 0 + }, len(rows) > 0 || commFound || pidFound } -func containerOriginTable(nmd report.NodeMetadata) (Table, bool) { +func containerOriginTable(nmd report.NodeMetadata, addHostTag bool) (Table, bool) { rows := []Row{} for _, tuple := range []struct{ key, human string }{ {docker.ContainerID, "ID"}, - {docker.ContainerName, "Name"}, {docker.ImageID, "Image ID"}, {docker.ContainerPorts, "Ports"}, {docker.ContainerCreated, "Created"}, @@ -268,37 +313,54 @@ func containerOriginTable(nmd report.NodeMetadata) (Table, bool) { rows = append(rows, Row{Key: "Memory Usage (MB):", ValueMajor: memoryStr, ValueMinor: ""}) } } + if addHostTag { + rows = append([]Row{{Key: "Host", ValueMajor: report.ExtractHostID(nmd)}}, rows...) + } + + var ( + title = "Container" + name, nameFound = nmd.Metadata[docker.ContainerName] + ) + if nameFound { + title += ` "` + name + `"` + } return Table{ - Title: "Origin Container", + Title: title, Numeric: false, Rows: rows, Rank: containerRank, - }, len(rows) > 0 + }, len(rows) > 0 || nameFound } func containerImageOriginTable(nmd report.NodeMetadata) (Table, bool) { rows := []Row{} for _, tuple := range []struct{ key, human string }{ {docker.ImageID, "Image ID"}, - {docker.ImageName, "Image name"}, } { if val, ok := nmd.Metadata[tuple.key]; ok { rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""}) } } + title := "Container Image" + var ( + nameFound bool + name string + ) + if name, nameFound = nmd.Metadata[docker.ImageName]; nameFound { + title += ` "` + name + `"` + } return Table{ - Title: "Origin Container Image", + Title: title, Numeric: false, Rows: rows, Rank: containerImageRank, - }, len(rows) > 0 + }, len(rows) > 0 || nameFound } func hostOriginTable(nmd report.NodeMetadata) (Table, bool) { rows := []Row{} for _, tuple := range []struct{ key, human string }{ - {host.HostName, "Host name"}, {host.Load, "Load"}, {host.OS, "Operating system"}, {host.KernelVersion, "Kernel version"}, @@ -309,10 +371,18 @@ func hostOriginTable(nmd report.NodeMetadata) (Table, bool) { } } + title := "Host" + var ( + name string + foundName bool + ) + if name, foundName = nmd.Metadata[host.HostName]; foundName { + title += ` "` + name + `"` + } return Table{ - Title: "Origin Host", + Title: title, Numeric: false, Rows: rows, Rank: hostRank, - }, len(rows) > 0 + }, len(rows) > 0 || foundName } diff --git a/render/detailed_node_test.go b/render/detailed_node_test.go index 11742d62e..af34a06b7 100644 --- a/render/detailed_node_test.go +++ b/render/detailed_node_test.go @@ -10,31 +10,26 @@ import ( ) func TestOriginTable(t *testing.T) { - if _, ok := render.OriginTable(test.Report, "not-found"); ok { + if _, ok := render.OriginTable(test.Report, "not-found", false, false); ok { t.Errorf("unknown origin ID gave unexpected success") } - for originID, want := range map[string]render.Table{ - test.ServerProcessNodeID: { - Title: "Origin Process", - Numeric: false, - Rank: 2, - Rows: []render.Row{ - {"Name", "apache", "", false}, - {"PID", test.ServerPID, "", false}, - }, - }, + for originID, want := range map[string]render.Table{test.ServerProcessNodeID: { + Title: fmt.Sprintf(`Process "apache" (%s)`, test.ServerPID), + Numeric: false, + Rank: 2, + Rows: []render.Row{}, + }, test.ServerHostNodeID: { - Title: "Origin Host", + Title: fmt.Sprintf("Host %q", test.ServerHostName), Numeric: false, Rank: 1, Rows: []render.Row{ - {"Host name", test.ServerHostName, "", false}, {"Load", "0.01 0.01 0.01", "", false}, {"Operating system", "Linux", "", false}, }, }, } { - have, ok := render.OriginTable(test.Report, originID) + have, ok := render.OriginTable(test.Report, originID, false, false) if !ok { t.Errorf("%q: not OK", originID) continue @@ -43,6 +38,39 @@ func TestOriginTable(t *testing.T) { t.Errorf("%q: %s", originID, test.Diff(want, have)) } } + + // Test host/container tags + for originID, want := range map[string]render.Table{ + test.ServerProcessNodeID: { + Title: fmt.Sprintf(`Process "apache" (%s)`, test.ServerPID), + Numeric: false, + Rank: 2, + Rows: []render.Row{ + {"Host", test.ServerHostID, "", false}, + {"Container ID", test.ServerContainerID, "", false}, + }, + }, + test.ServerContainerNodeID: { + Title: `Container "server"`, + Numeric: false, + Rank: 3, + Rows: []render.Row{ + {"Host", test.ServerHostID, "", false}, + {"ID", test.ServerContainerID, "", false}, + {"Image ID", test.ServerContainerImageID, "", false}, + }, + }, + } { + have, ok := render.OriginTable(test.Report, originID, true, true) + if !ok { + t.Errorf("%q: not OK", originID) + continue + } + if !reflect.DeepEqual(want, have) { + t.Errorf("%q: %s", originID, test.Diff(want, have)) + } + } + } func TestMakeDetailedHostNode(t *testing.T) { @@ -55,15 +83,10 @@ func TestMakeDetailedHostNode(t *testing.T) { Pseudo: false, Tables: []render.Table{ { - Title: "Origin Host", + Title: fmt.Sprintf("Host %q", test.ClientHostName), Numeric: false, Rank: 1, Rows: []render.Row{ - { - Key: "Host name", - ValueMajor: "client.hostname.com", - ValueMinor: "", - }, { Key: "Load", ValueMajor: "0.01 0.01 0.01", @@ -117,30 +140,25 @@ func TestMakeDetailedContainerNode(t *testing.T) { Pseudo: false, Tables: []render.Table{ { - Title: "Origin Container", + Title: `Container "server"`, Numeric: false, Rank: 3, Rows: []render.Row{ {"ID", test.ServerContainerID, "", false}, - {"Name", "server", "", false}, {"Image ID", test.ServerContainerImageID, "", false}, }, }, { - Title: "Origin Process", + Title: fmt.Sprintf(`Process "apache" (%s)`, test.ServerPID), Numeric: false, Rank: 2, - Rows: []render.Row{ - {"Name", "apache", "", false}, - {"PID", test.ServerPID, "", false}, - }, + Rows: []render.Row{}, }, { - Title: "Origin Host", + Title: fmt.Sprintf("Host %q", test.ServerHostName), Numeric: false, Rank: 1, Rows: []render.Row{ - {"Host name", test.ServerHostName, "", false}, {"Load", "0.01 0.01 0.01", "", false}, {"Operating system", "Linux", "", false}, },