diff --git a/app/api_topologies_test.go b/app/api_topologies_test.go index ea76bb1b98..0c9b67da4e 100644 --- a/app/api_topologies_test.go +++ b/app/api_topologies_test.go @@ -15,7 +15,7 @@ func TestAPITopology(t *testing.T) { if err := json.Unmarshal(body, &topos); err != nil { t.Fatalf("JSON parse error: %s", err) } - equals(t, 2, len(topos)) + equals(t, 3, len(topos)) for _, topo := range topos { is200(t, ts, topo.URL) if topo.GroupedURL != "" { diff --git a/app/router.go b/app/router.go index 02427c1a0d..0b9c51a2a5 100644 --- a/app/router.go +++ b/app/router.go @@ -47,5 +47,6 @@ var topologyRegistry = map[string]struct { hasGrouped bool }{ "applications": {"Applications", selectProcess, report.ProcessPID, true}, + "containers": {"Containers", selectProcess, report.ProcessContainer, true}, "hosts": {"Hosts", selectNetwork, report.NetworkHostname, false}, } diff --git a/app/scope_test.go b/app/scope_test.go index a9d9caa943..68d350ae18 100644 --- a/app/scope_test.go +++ b/app/scope_test.go @@ -69,7 +69,6 @@ func checkRequest(t *testing.T, ts *httptest.Server, method, path string, body [ // getRawJSON GETs a file, checks it is JSON, and returns the non-parsed body func getRawJSON(t *testing.T, ts *httptest.Server, path string) []byte { - res, body := checkGet(t, ts, path) if res.StatusCode != 200 { diff --git a/probe/docker_process_mapper.go b/probe/docker_process_mapper.go index ed57526c41..941616efb2 100644 --- a/probe/docker_process_mapper.go +++ b/probe/docker_process_mapper.go @@ -3,6 +3,7 @@ package main import ( "fmt" "log" + "strings" "sync" "time" @@ -135,7 +136,7 @@ func (m *dockerMapper) idMapper() processMapper { func (m *dockerMapper) nameMapper() processMapper { return &dockerProcessMapper{m, "docker_name", func(c *docker.Container) string { - return c.Name + return strings.TrimPrefix(c.Name, "/") }} } diff --git a/report/mapping_functions.go b/report/mapping_functions.go index 2a206db09e..1b20108f5d 100644 --- a/report/mapping_functions.go +++ b/report/mapping_functions.go @@ -48,6 +48,37 @@ func ProcessPID(_ string, m NodeMetadata, grouped bool) (MappedNode, bool) { }, show } +// ProcessContainer maps Process topology nodes to the containers they run in. +// We consider container and image IDs to be globally unique, and so don't +// scope them further by e.g. host. If no container metadata is found, nodes +// are grouped into the Uncontained node. If grouped is true, nodes with the +// same container image ID are merged together. +func ProcessContainer(_ string, m NodeMetadata, grouped bool) (MappedNode, bool) { + var ( + containerID = m["docker_id"] + containerName = m["docker_name"] + imageID = m["docker_image_id"] + imageName = m["docker_image_name"] + domain = m["domain"] + ) + + var id, major, minor, rank string + if containerID == "" { + id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained" + } else if grouped { + id, major, minor, rank = imageID, imageName, "", imageID + } else { + id, major, minor, rank = containerID, containerName, domain, imageID + } + + return MappedNode{ + ID: id, + Major: major, + Minor: minor, + Rank: rank, + }, true +} + // NetworkHostname takes a node NodeMetadata from a Network topology, and // returns a representation based on the hostname. Major label is the // hostname, the minor label is the domain, if any. diff --git a/report/mapping_test.go b/report/mapping_test.go index bb3ed957e9..7f3ca11718 100644 --- a/report/mapping_test.go +++ b/report/mapping_test.go @@ -51,6 +51,38 @@ func TestUngroupedMapping(t *testing.T) { wantMinor: "hosta (42)", wantRank: "42", }, + { + f: ProcessContainer, + id: "foo-id", + meta: NodeMetadata{ + "pid": "42", + "name": "curl", + "domain": "hosta", + }, + wantOK: true, + wantID: "uncontained", + wantMajor: "Uncontained", + wantMinor: "", + wantRank: "uncontained", + }, + { + f: ProcessContainer, + id: "bar-id", + meta: NodeMetadata{ + "pid": "42", + "name": "curl", + "domain": "hosta", + "docker_id": "d321fe0", + "docker_name": "walking_sparrow", + "docker_image_id": "1101fff", + "docker_image_name": "org/app:latest", + }, + wantOK: true, + wantID: "d321fe0", + wantMajor: "walking_sparrow", + wantMinor: "hosta", + wantRank: "1101fff", + }, } { identity := fmt.Sprintf("(%d %s %v)", i, c.id, c.meta)