mirror of
https://github.com/amir20/dozzle.git
synced 2026-01-04 12:05:07 +01:00
perf: fixes an issue with too many containers (#3144)
This commit is contained in:
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -176,37 +175,15 @@ func NewRemoteClient(f map[string][]string, host Host) (Client, error) {
|
||||
return NewClient(cli, filterArgs, host), nil
|
||||
}
|
||||
|
||||
// Finds a container by id, skipping the filters
|
||||
func (d *httpClient) FindContainer(id string) (Container, error) {
|
||||
log.Debugf("finding container with id: %s", id)
|
||||
var container Container
|
||||
containers, err := d.ListContainers()
|
||||
if err != nil {
|
||||
return container, err
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, c := range containers {
|
||||
if c.ID == id {
|
||||
container = c
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return container, fmt.Errorf("unable to find container with id: %s", id)
|
||||
}
|
||||
|
||||
if json, err := d.cli.ContainerInspect(context.Background(), container.ID); err == nil {
|
||||
container.Tty = json.Config.Tty
|
||||
if startedAt, err := time.Parse(time.RFC3339Nano, json.State.StartedAt); err == nil {
|
||||
utc := startedAt.UTC()
|
||||
container.StartedAt = &utc
|
||||
}
|
||||
if json, err := d.cli.ContainerInspect(context.Background(), id); err == nil {
|
||||
return newContainerFromJSON(json, d.host.ID), nil
|
||||
} else {
|
||||
return container, err
|
||||
return Container{}, err
|
||||
}
|
||||
|
||||
return container, nil
|
||||
}
|
||||
|
||||
func (d *httpClient) ContainerActions(action ContainerAction, containerID string) error {
|
||||
@@ -223,6 +200,7 @@ func (d *httpClient) ContainerActions(action ContainerAction, containerID string
|
||||
}
|
||||
|
||||
func (d *httpClient) ListContainers() ([]Container, error) {
|
||||
log.Debugf("listing containers with filters: %v", d.filters)
|
||||
containerListOptions := container.ListOptions{
|
||||
Filters: d.filters,
|
||||
All: true,
|
||||
@@ -234,33 +212,7 @@ func (d *httpClient) ListContainers() ([]Container, error) {
|
||||
|
||||
var containers = make([]Container, 0, len(list))
|
||||
for _, c := range list {
|
||||
name := "no name"
|
||||
if len(c.Names) > 0 {
|
||||
name = strings.TrimPrefix(c.Names[0], "/")
|
||||
}
|
||||
|
||||
group := ""
|
||||
if c.Labels["dev.dozzle.group"] != "" {
|
||||
group = c.Labels["dev.dozzle.group"]
|
||||
}
|
||||
|
||||
container := Container{
|
||||
ID: c.ID[:12],
|
||||
Names: c.Names,
|
||||
Name: name,
|
||||
Image: c.Image,
|
||||
ImageID: c.ImageID,
|
||||
Command: c.Command,
|
||||
Created: time.Unix(c.Created, 0),
|
||||
State: c.State,
|
||||
Status: c.Status,
|
||||
Host: d.host.ID,
|
||||
Health: findBetweenParentheses(c.Status),
|
||||
Labels: c.Labels,
|
||||
Stats: utils.NewRingBuffer[ContainerStat](300), // 300 seconds of stats
|
||||
Group: group,
|
||||
}
|
||||
containers = append(containers, container)
|
||||
containers = append(containers, newContainer(c, d.host.ID))
|
||||
}
|
||||
|
||||
sort.Slice(containers, func(i, j int) bool {
|
||||
@@ -398,11 +350,69 @@ func (d *httpClient) SystemInfo() system.Info {
|
||||
return d.info
|
||||
}
|
||||
|
||||
var PARENTHESIS_RE = regexp.MustCompile(`\(([a-zA-Z]+)\)`)
|
||||
|
||||
func findBetweenParentheses(s string) string {
|
||||
if results := PARENTHESIS_RE.FindStringSubmatch(s); results != nil {
|
||||
return results[1]
|
||||
func newContainer(c types.Container, host string) Container {
|
||||
name := "no name"
|
||||
if len(c.Names) > 0 {
|
||||
name = strings.TrimPrefix(c.Names[0], "/")
|
||||
}
|
||||
|
||||
group := ""
|
||||
if c.Labels["dev.dozzle.group"] != "" {
|
||||
group = c.Labels["dev.dozzle.group"]
|
||||
}
|
||||
return Container{
|
||||
ID: c.ID[:12],
|
||||
Name: name,
|
||||
Image: c.Image,
|
||||
ImageID: c.ImageID,
|
||||
Command: c.Command,
|
||||
Created: time.Unix(c.Created, 0),
|
||||
State: c.State,
|
||||
Host: host,
|
||||
Labels: c.Labels,
|
||||
Stats: utils.NewRingBuffer[ContainerStat](300), // 300 seconds of stats
|
||||
Group: group,
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func newContainerFromJSON(c types.ContainerJSON, host string) Container {
|
||||
name := "no name"
|
||||
if len(c.Name) > 0 {
|
||||
name = strings.TrimPrefix(c.Name, "/")
|
||||
}
|
||||
|
||||
group := ""
|
||||
if c.Config.Labels["dev.dozzle.group"] != "" {
|
||||
group = c.Config.Labels["dev.dozzle.group"]
|
||||
}
|
||||
|
||||
container := Container{
|
||||
ID: c.ID[:12],
|
||||
Name: name,
|
||||
Image: c.Image,
|
||||
ImageID: c.Image,
|
||||
Command: strings.Join(c.Config.Entrypoint, " ") + " " + strings.Join(c.Config.Cmd, " "),
|
||||
State: c.State.Status,
|
||||
Host: host,
|
||||
Labels: c.Config.Labels,
|
||||
Stats: utils.NewRingBuffer[ContainerStat](300), // 300 seconds of stats
|
||||
Group: group,
|
||||
Tty: c.Config.Tty,
|
||||
}
|
||||
|
||||
if startedAt, err := time.Parse(time.RFC3339Nano, c.State.StartedAt); err == nil {
|
||||
utc := startedAt.UTC()
|
||||
container.StartedAt = &utc
|
||||
}
|
||||
|
||||
if createdAt, err := time.Parse(time.RFC3339Nano, c.Created); err == nil {
|
||||
utc := createdAt.UTC()
|
||||
container.Created = utc
|
||||
}
|
||||
|
||||
if c.State.Health != nil {
|
||||
container.Health = strings.ToLower(c.State.Health.Status)
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
@@ -183,22 +183,10 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_dockerClient_FindContainer_happy(t *testing.T) {
|
||||
containers := []types.Container{
|
||||
{
|
||||
ID: "abcdefghijklmnopqrst",
|
||||
Names: []string{"/z_test_container"},
|
||||
},
|
||||
{
|
||||
ID: "1234567890_abcxyzdef",
|
||||
Names: []string{"/a_test_container"},
|
||||
},
|
||||
}
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
|
||||
state := &types.ContainerState{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
|
||||
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{State: state}, Config: &container.Config{Tty: false}}
|
||||
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ID: "abcdefghijklmnopqrst", State: state}, Config: &container.Config{Tty: false}}
|
||||
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
|
||||
|
||||
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
|
||||
@@ -210,20 +198,10 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
|
||||
|
||||
proxy.AssertExpectations(t)
|
||||
}
|
||||
func Test_dockerClient_FindContainer_error(t *testing.T) {
|
||||
containers := []types.Container{
|
||||
{
|
||||
ID: "abcdefghijklmnopqrst",
|
||||
Names: []string{"/z_test_container"},
|
||||
},
|
||||
{
|
||||
ID: "1234567890_abcxyzdef",
|
||||
Names: []string{"/a_test_container"},
|
||||
},
|
||||
}
|
||||
|
||||
func Test_dockerClient_FindContainer_error(t *testing.T) {
|
||||
proxy := new(mockedProxy)
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
proxy.On("ContainerInspect", mock.Anything, "not_valid").Return(types.ContainerJSON{}, errors.New("not found"))
|
||||
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
_, err := client.FindContainer("not_valid")
|
||||
@@ -233,24 +211,12 @@ func Test_dockerClient_FindContainer_error(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_dockerClient_ContainerActions_happy(t *testing.T) {
|
||||
containers := []types.Container{
|
||||
{
|
||||
ID: "abcdefghijklmnopqrst",
|
||||
Names: []string{"/z_test_container"},
|
||||
},
|
||||
{
|
||||
ID: "1234567890_abcxyzdef",
|
||||
Names: []string{"/a_test_container"},
|
||||
},
|
||||
}
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
state := &types.ContainerState{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
|
||||
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{State: state}, Config: &container.Config{Tty: false}}
|
||||
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{ID: "abcdefghijkl", State: state}, Config: &container.Config{Tty: false}}
|
||||
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil)
|
||||
proxy.On("ContainerStart", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
|
||||
proxy.On("ContainerStop", mock.Anything, "abcdefghijkl", mock.Anything).Return(nil)
|
||||
@@ -272,21 +238,10 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_dockerClient_ContainerActions_error(t *testing.T) {
|
||||
containers := []types.Container{
|
||||
{
|
||||
ID: "abcdefghijklmnopqrst",
|
||||
Names: []string{"/z_test_container"},
|
||||
},
|
||||
{
|
||||
ID: "1234567890_abcxyzdef",
|
||||
Names: []string{"/a_test_container"},
|
||||
},
|
||||
}
|
||||
|
||||
proxy := new(mockedProxy)
|
||||
client := &httpClient{proxy, filters.NewArgs(), Host{ID: "localhost"}, system.Info{}}
|
||||
|
||||
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
|
||||
proxy.On("ContainerInspect", mock.Anything, "random-id").Return(types.ContainerJSON{}, errors.New("not found"))
|
||||
proxy.On("ContainerStart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
|
||||
proxy.On("ContainerStop", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
|
||||
proxy.On("ContainerRestart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test"))
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"github.com/samber/lo"
|
||||
lop "github.com/samber/lo/parallel"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -59,10 +60,23 @@ func (s *ContainerStore) checkConnectivity() error {
|
||||
return err
|
||||
} else {
|
||||
s.containers.Clear()
|
||||
lop.ForEach(containers, func(c Container, _ int) {
|
||||
container, _ := s.client.FindContainer(c.ID)
|
||||
s.containers.Store(c.ID, &container)
|
||||
|
||||
for _, c := range containers {
|
||||
s.containers.Store(c.ID, &c)
|
||||
}
|
||||
|
||||
running := lo.Filter(containers, func(item Container, index int) bool {
|
||||
return item.State == "running"
|
||||
})
|
||||
|
||||
chunks := lo.Chunk(running, 100)
|
||||
|
||||
for _, chunk := range chunks {
|
||||
lop.ForEach(chunk, func(c Container, _ int) {
|
||||
container, _ := s.client.FindContainer(c.ID)
|
||||
s.containers.Store(c.ID, &container)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,19 +99,14 @@ func (s *ContainerStore) ListContainers() ([]Container, error) {
|
||||
}
|
||||
|
||||
func (s *ContainerStore) FindContainer(id string) (Container, error) {
|
||||
list, err := s.ListContainers()
|
||||
if err != nil {
|
||||
return Container{}, err
|
||||
}
|
||||
container, ok := s.containers.Load(id)
|
||||
|
||||
for _, c := range list {
|
||||
if c.ID == id {
|
||||
return c, nil
|
||||
}
|
||||
if ok {
|
||||
return *container, nil
|
||||
} else {
|
||||
log.Warnf("container %s not found in store", id)
|
||||
return Container{}, ErrContainerNotFound
|
||||
}
|
||||
|
||||
log.Warnf("container %s not found in store", id)
|
||||
return Container{}, ErrContainerNotFound
|
||||
}
|
||||
|
||||
func (s *ContainerStore) Client() Client {
|
||||
@@ -150,15 +159,24 @@ func (s *ContainerStore) init() {
|
||||
switch event.Name {
|
||||
case "start":
|
||||
if container, err := s.client.FindContainer(event.ActorID); err == nil {
|
||||
log.Debugf("container %s started", container.ID)
|
||||
s.containers.Store(container.ID, &container)
|
||||
s.newContainerSubscribers.Range(func(c context.Context, containers chan<- Container) bool {
|
||||
select {
|
||||
case containers <- container:
|
||||
case <-c.Done():
|
||||
}
|
||||
return true
|
||||
list, _ := s.client.ListContainers()
|
||||
|
||||
// make sure the container is in the list of containers when using filter
|
||||
valid := lo.ContainsBy(list, func(item Container) bool {
|
||||
return item.ID == container.ID
|
||||
})
|
||||
|
||||
if valid {
|
||||
log.Debugf("container %s started", container.ID)
|
||||
s.containers.Store(container.ID, &container)
|
||||
s.newContainerSubscribers.Range(func(c context.Context, containers chan<- Container) bool {
|
||||
select {
|
||||
case containers <- container:
|
||||
case <-c.Done():
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
case "destroy":
|
||||
log.Debugf("container %s destroyed", event.ActorID)
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
// Container represents an internal representation of docker containers
|
||||
type Container struct {
|
||||
ID string `json:"id"`
|
||||
Names []string `json:"names"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
ImageID string `json:"imageId"`
|
||||
@@ -19,7 +18,6 @@ type Container struct {
|
||||
Created time.Time `json:"created"`
|
||||
StartedAt *time.Time `json:"startedAt,omitempty"`
|
||||
State string `json:"state"`
|
||||
Status string `json:"status"`
|
||||
Health string `json:"health,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Tty bool `json:"-"`
|
||||
|
||||
Reference in New Issue
Block a user