From 6e89bdef9f11a26338f7ce04833c35c83753dbf0 Mon Sep 17 00:00:00 2001 From: Victor Marmol Date: Mon, 3 Nov 2014 17:03:43 -0800 Subject: [PATCH] Add /docker/ UI endpoint. Fixes #294 --- cadvisor.go | 16 ++---- container/docker/factory.go | 22 +++++++- container/docker/handler.go | 21 +------ pages/containers.go | 100 +++++++++------------------------- pages/containers_html.go | 10 ++-- pages/docker.go | 106 ++++++++++++++++++++++++++++++++++++ pages/pages.go | 82 ++++++++++++++++++++++++++++ 7 files changed, 248 insertions(+), 109 deletions(-) create mode 100644 pages/docker.go create mode 100644 pages/pages.go diff --git a/cadvisor.go b/cadvisor.go index bf499ea8..dec93e61 100644 --- a/cadvisor.go +++ b/cadvisor.go @@ -63,12 +63,12 @@ func main() { // Register the raw driver. if err := raw.Register(containerManager); err != nil { - glog.Fatalf("raw registration failed: %v.", err) + glog.Fatalf("Raw registration failed: %v.", err) } // Basic health handler. if err := healthz.RegisterHandler(); err != nil { - glog.Fatalf("failed to register healthz handler: %s", err) + glog.Fatalf("Failed to register healthz handler: %s", err) } // Handler for static content. @@ -81,19 +81,15 @@ func main() { // Register API handler. if err := api.RegisterHandlers(containerManager); err != nil { - glog.Fatalf("failed to register API handlers: %s", err) + glog.Fatalf("Failed to register API handlers: %s", err) } // Redirect / to containers page. http.Handle("/", http.RedirectHandler(pages.ContainersPage, http.StatusTemporaryRedirect)) - // Register the handler for the containers page. - http.HandleFunc(pages.ContainersPage, func(w http.ResponseWriter, r *http.Request) { - err := pages.ServerContainersPage(containerManager, w, r.URL) - if err != nil { - fmt.Fprintf(w, "%s", err) - } - }) + if err := pages.RegisterHandlers(containerManager); err != nil { + glog.Fatalf("Failed to register pages handlers: %s", err) + } // Start the manager. if err := containerManager.Start(); err != nil { diff --git a/container/docker/factory.go b/container/docker/factory.go index 65a3a7a4..e46ad850 100644 --- a/container/docker/factory.go +++ b/container/docker/factory.go @@ -82,6 +82,26 @@ func IsDockerContainerName(name string) bool { } } +// Returns the Docker ID from the full container name. +func ContainerNameToDockerId(name string) string { + id := path.Base(name) + + // Turn systemd cgroup name into Docker ID. + if useSystemd { + const systemdDockerPrefix = "docker-" + if strings.HasPrefix(id, systemdDockerPrefix) { + id = id[len(systemdDockerPrefix):] + } + + const systemdScopeSuffix = ".scope" + if strings.HasSuffix(id, systemdScopeSuffix) { + id = id[:len(id)-len(systemdScopeSuffix)] + } + } + + return id +} + // Returns a list of possible full container names for the specified Docker container name. func FullDockerContainerNames(dockerName string) []string { names := make([]string, 0, 2) @@ -107,7 +127,7 @@ func (self *dockerFactory) CanHandle(name string) (bool, error) { } // Check if the container is known to docker and it is active. - id := containerNameToDockerId(name) + id := ContainerNameToDockerId(name) // We assume that if Inspect fails then the container is not known to docker. ctnr, err := self.client.InspectContainer(id) diff --git a/container/docker/handler.go b/container/docker/handler.go index 968b40e0..e3d8ee4e 100644 --- a/container/docker/handler.go +++ b/container/docker/handler.go @@ -85,7 +85,7 @@ func newDockerContainerHandler( if handler.isDockerRoot() { return handler, nil } - id := containerNameToDockerId(name) + id := ContainerNameToDockerId(name) handler.id = id ctnr, err := client.InspectContainer(id) // We assume that if Inspect fails then the container is not known to docker. @@ -96,25 +96,6 @@ func newDockerContainerHandler( return handler, nil } -func containerNameToDockerId(name string) string { - id := path.Base(name) - - // Turn systemd cgroup name into Docker ID. - if useSystemd { - const systemdDockerPrefix = "docker-" - if strings.HasPrefix(id, systemdDockerPrefix) { - id = id[len(systemdDockerPrefix):] - } - - const systemdScopeSuffix = ".scope" - if strings.HasSuffix(id, systemdScopeSuffix) { - id = id[:len(id)-len(systemdScopeSuffix)] - } - } - - return id -} - func (self *dockerContainerHandler) ContainerReference() (info.ContainerReference, error) { return info.ContainerReference{ Name: self.name, diff --git a/pages/containers.go b/pages/containers.go index d6ceb04c..4b6d707b 100644 --- a/pages/containers.go +++ b/pages/containers.go @@ -88,7 +88,6 @@ func (b ByteSize) Unit() string { } var funcMap = template.FuncMap{ - "containerLink": containerLink, "printMask": printMask, "printCores": printCores, "printShares": printShares, @@ -102,52 +101,6 @@ var funcMap = template.FuncMap{ "getFsUsagePercent": getFsUsagePercent, } -// TODO(vmarmol): Consider housekeeping Spec too so we can show changes through time. We probably don't need it ever second though. - -var pageTemplate *template.Template - -type pageData struct { - ContainerName string - ParentContainers []info.ContainerReference - Subcontainers []info.ContainerReference - Spec info.ContainerSpec - Stats []*info.ContainerStats - MachineInfo *info.MachineInfo - ResourcesAvailable bool - CpuAvailable bool - MemoryAvailable bool - NetworkAvailable bool - FsAvailable bool -} - -func init() { - pageTemplate = template.New("containersTemplate").Funcs(funcMap) - _, err := pageTemplate.Parse(containersHtmlTemplate) - if err != nil { - glog.Fatalf("Failed to parse template: %s", err) - } -} - -// TODO(vmarmol): Escape this correctly. -func containerLink(container info.ContainerReference, basenameOnly bool, cssClasses string) interface{} { - var displayName string - containerName := container.Name - if len(container.Aliases) > 0 { - displayName = container.Aliases[0] - } else if basenameOnly { - displayName = path.Base(string(container.Name)) - } else { - displayName = string(container.Name) - } - if container.Name == "root" { - containerName = "/" - } else if strings.Contains(container.Name, " ") { - // If it has a space, it is an a.k.a, so keep the base-name - containerName = container.Name[:strings.Index(container.Name, " ")] - } - return template.HTML(fmt.Sprintf("%s", cssClasses, ContainersPage[:len(ContainersPage)-1], containerName, displayName)) -} - func printMask(mask string, numCores int) interface{} { masks := make([]string, numCores) activeCores := getActiveCores(mask) @@ -266,7 +219,7 @@ func getFsUsagePercent(limit, used uint64) uint64 { return uint64((float64(used) / float64(limit)) * 100) } -func ServerContainersPage(m manager.Manager, w http.ResponseWriter, u *url.URL) error { +func serveContainersPage(m manager.Manager, w http.ResponseWriter, u *url.URL) error { start := time.Now() // The container name is the path after the handler @@ -278,8 +231,9 @@ func ServerContainersPage(m manager.Manager, w http.ResponseWriter, u *url.URL) } cont, err := m.GetContainerInfo(containerName, &reqParams) if err != nil { - return fmt.Errorf("Failed to get container \"%s\" with error: %s", containerName, err) + return fmt.Errorf("Failed to get container %q with error: %v", containerName, err) } + displayName := getContainerDisplayName(cont.ContainerReference) // Get the MachineInfo machineInfo, err := m.GetMachineInfo() @@ -288,41 +242,41 @@ func ServerContainersPage(m manager.Manager, w http.ResponseWriter, u *url.URL) } // Make a list of the parent containers and their links - var parentContainers []info.ContainerReference - parentContainers = append(parentContainers, info.ContainerReference{Name: "root"}) - parentName := "" - for _, part := range strings.Split(string(cont.Name), "/") { - if part == "" { + pathParts := strings.Split(string(cont.Name), "/") + parentContainers := make([]link, 0, len(pathParts)) + parentContainers = append(parentContainers, link{ + Text: "root", + Link: ContainersPage, + }) + for i := 1; i < len(pathParts); i++ { + // Skip empty parts. + if pathParts[i] == "" { continue } - parentName += "/" + part - parentContainers = append(parentContainers, info.ContainerReference{Name: parentName}) + parentContainers = append(parentContainers, link{ + Text: pathParts[i], + Link: path.Join(ContainersPage, path.Join(pathParts[1:i+1]...)), + }) } - // Pick the shortest name of the container as the display name. - displayName := cont.Name - for _, alias := range cont.Aliases { - if len(displayName) >= len(alias) { - displayName = alias - } - } - - // Replace the last part of the parent containers with the displayName. - if displayName != cont.Name { - parentContainers[len(parentContainers)-1] = info.ContainerReference{ - Name: fmt.Sprintf("%s (%s)", displayName, path.Base(cont.Name)), - } + // Build the links for the subcontainers. + subcontainerLinks := make([]link, 0, len(cont.Subcontainers)) + for _, sub := range cont.Subcontainers { + subcontainerLinks = append(subcontainerLinks, link{ + Text: getContainerDisplayName(sub), + Link: path.Join(ContainersPage, sub.Name), + }) } data := &pageData{ - ContainerName: displayName, - // TODO(vmarmol): Only use strings for this. + DisplayName: displayName, + ContainerName: cont.Name, ParentContainers: parentContainers, - Subcontainers: cont.Subcontainers, + Subcontainers: subcontainerLinks, Spec: cont.Spec, Stats: cont.Stats, MachineInfo: machineInfo, - ResourcesAvailable: cont.Spec.HasCpu || cont.Spec.HasMemory || cont.Spec.HasNetwork, + ResourcesAvailable: cont.Spec.HasCpu || cont.Spec.HasMemory || cont.Spec.HasNetwork || cont.Spec.HasFilesystem, CpuAvailable: cont.Spec.HasCpu, MemoryAvailable: cont.Spec.HasMemory, NetworkAvailable: cont.Spec.HasNetwork, diff --git a/pages/containers_html.go b/pages/containers_html.go index d3d0979d..6ca2406c 100644 --- a/pages/containers_html.go +++ b/pages/containers_html.go @@ -17,7 +17,7 @@ package pages const containersHtmlTemplate = ` - cAdvisor - Container {{.ContainerName}} + cAdvisor - {{.DisplayName}} @@ -39,11 +39,11 @@ const containersHtmlTemplate = `
@@ -54,7 +54,7 @@ const containersHtmlTemplate = `
{{range $subcontainer := .Subcontainers}} - {{containerLink $subcontainer false "list-group-item"}} + {{$subcontainer.Text}} {{end}}
@@ -165,7 +165,7 @@ const containersHtmlTemplate = ` -
+
{{printSize .Limit}} {{printUnit .Limit}} ({{getFsUsagePercent .Limit .Usage}}%)
{{end}} diff --git a/pages/docker.go b/pages/docker.go new file mode 100644 index 00000000..116adc3b --- /dev/null +++ b/pages/docker.go @@ -0,0 +1,106 @@ +package pages + +import ( + "fmt" + "net/http" + "net/url" + "path" + "time" + + "github.com/golang/glog" + "github.com/google/cadvisor/container/docker" + "github.com/google/cadvisor/info" + "github.com/google/cadvisor/manager" +) + +const DockerPage = "/docker/" + +func serveDockerPage(m manager.Manager, w http.ResponseWriter, u *url.URL) error { + start := time.Now() + + // The container name is the path after the handler + containerName := u.Path[len(DockerPage)-1:] + + var data *pageData + if containerName == "/" { + // Get the containers. + reqParams := info.ContainerInfoRequest{ + NumStats: 0, + } + conts, err := m.DockerContainersInfo("/", &reqParams) + if err != nil { + return fmt.Errorf("Failed to get container %q with error: %v", containerName, err) + } + subcontainers := make([]link, 0, len(conts)) + for _, cont := range conts { + subcontainers = append(subcontainers, link{ + Text: getContainerDisplayName(cont.ContainerReference), + Link: path.Join("/docker", docker.ContainerNameToDockerId(cont.ContainerReference.Name)), + }) + } + + dockerContainersText := "Docker Containers" + data = &pageData{ + DisplayName: dockerContainersText, + ParentContainers: []link{ + link{ + Text: dockerContainersText, + Link: DockerPage, + }}, + Subcontainers: subcontainers, + } + } else { + // Get the container. + reqParams := info.ContainerInfoRequest{ + NumStats: 60, + } + conts, err := m.DockerContainersInfo(containerName, &reqParams) + if err != nil { + return fmt.Errorf("failed to get container %q with error: %v", containerName, err) + } + if len(conts) != 1 { + return fmt.Errorf("received unexpected container for %q: %v", conts) + } + cont := conts[0] + displayName := getContainerDisplayName(cont.ContainerReference) + + // Make a list of the parent containers and their links + var parentContainers []link + parentContainers = append(parentContainers, link{ + Text: "Docker containers", + Link: DockerPage, + }) + parentContainers = append(parentContainers, link{ + Text: displayName, + Link: path.Join(DockerPage, docker.ContainerNameToDockerId(cont.Name)), + }) + + // Get the MachineInfo + machineInfo, err := m.GetMachineInfo() + if err != nil { + return err + } + + data = &pageData{ + DisplayName: displayName, + ContainerName: cont.Name, + ParentContainers: parentContainers, + Spec: cont.Spec, + Stats: cont.Stats, + MachineInfo: machineInfo, + ResourcesAvailable: cont.Spec.HasCpu || cont.Spec.HasMemory || cont.Spec.HasNetwork, + CpuAvailable: cont.Spec.HasCpu, + MemoryAvailable: cont.Spec.HasMemory, + NetworkAvailable: cont.Spec.HasNetwork, + FsAvailable: cont.Spec.HasFilesystem, + } + } + + err := pageTemplate.Execute(w, data) + if err != nil { + glog.Errorf("Failed to apply template: %s", err) + } + + glog.V(1).Infof("Request took %s", time.Since(start)) + return nil +} diff --git a/pages/pages.go b/pages/pages.go new file mode 100644 index 00000000..bc5fc289 --- /dev/null +++ b/pages/pages.go @@ -0,0 +1,82 @@ +package pages + +import ( + "fmt" + "html/template" + "net/http" + + "github.com/golang/glog" + "github.com/google/cadvisor/info" + "github.com/google/cadvisor/manager" +) + +var pageTemplate *template.Template + +type link struct { + // Text to show in the link. + Text string + + // Web address to link to. + Link string +} + +type pageData struct { + DisplayName string + ContainerName string + ParentContainers []link + Subcontainers []link + Spec info.ContainerSpec + Stats []*info.ContainerStats + MachineInfo *info.MachineInfo + ResourcesAvailable bool + CpuAvailable bool + MemoryAvailable bool + NetworkAvailable bool + FsAvailable bool +} + +func init() { + pageTemplate = template.New("containersTemplate").Funcs(funcMap) + _, err := pageTemplate.Parse(containersHtmlTemplate) + if err != nil { + glog.Fatalf("Failed to parse template: %s", err) + } +} + +// Register http handlers for pages. +func RegisterHandlers(containerManager manager.Manager) error { + // Register the handler for the containers page. + http.HandleFunc(ContainersPage, func(w http.ResponseWriter, r *http.Request) { + err := serveContainersPage(containerManager, w, r.URL) + if err != nil { + fmt.Fprintf(w, "%s", err) + } + }) + + // Register the handler for the docker page. + http.HandleFunc(DockerPage, func(w http.ResponseWriter, r *http.Request) { + err := serveDockerPage(containerManager, w, r.URL) + if err != nil { + fmt.Fprintf(w, "%s", err) + } + }) + + return nil +} + +func getContainerDisplayName(cont info.ContainerReference) string { + // Pick the shortest name of the container as the display name. + displayName := cont.Name + for _, alias := range cont.Aliases { + if len(displayName) >= len(alias) { + displayName = alias + } + } + + // Add the full container name to the display name. + if displayName != cont.Name { + displayName = fmt.Sprintf("%s (%s)", displayName, cont.Name) + } + + return displayName +}