1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-25 23:03:47 +01:00

feat: enables container filter to be configured at multiple places

This commit is contained in:
Amir Raminfar
2024-12-13 11:59:39 -08:00
parent a62cef7e25
commit 74b5adad00
20 changed files with 468 additions and 350 deletions

View File

@@ -15,7 +15,6 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client"
@@ -59,7 +58,7 @@ type DockerCLI interface {
}
type Client interface {
ListContainers(context.Context) ([]Container, error)
ListContainers(context.Context, ContainerFilter) ([]Container, error)
FindContainer(context.Context, string) (Container, error)
ContainerLogs(context.Context, string, time.Time, StdType) (io.ReadCloser, error)
ContainerEvents(context.Context, chan<- ContainerEvent) error
@@ -73,13 +72,12 @@ type Client interface {
}
type httpClient struct {
cli DockerCLI
filters filters.Args
host Host
info system.Info
cli DockerCLI
host Host
info system.Info
}
func NewClient(cli DockerCLI, filters filters.Args, host Host) Client {
func NewClient(cli DockerCLI, host Host) Client {
info, err := cli.Info(context.Background())
if err != nil {
log.Error().Err(err).Msg("Failed to get docker info")
@@ -90,24 +88,14 @@ func NewClient(cli DockerCLI, filters filters.Args, host Host) Client {
host.DockerVersion = info.ServerVersion
return &httpClient{
cli: cli,
filters: filters,
host: host,
info: info,
cli: cli,
host: host,
info: info,
}
}
// NewClientWithFilters creates a new instance of Client with docker filters
func NewLocalClient(f map[string][]string, hostname string) (Client, error) {
filterArgs := filters.NewArgs()
for key, values := range f {
for _, value := range values {
filterArgs.Add(key, value)
}
}
log.Debug().Interface("filterArgs", filterArgs).Msg("Creating local client")
func NewLocalClient(hostname string) (Client, error) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
@@ -137,19 +125,10 @@ func NewLocalClient(f map[string][]string, hostname string) (Client, error) {
host.Name = hostname
}
return NewClient(cli, filterArgs, host), nil
return NewClient(cli, host), nil
}
func NewRemoteClient(f map[string][]string, host Host) (Client, error) {
filterArgs := filters.NewArgs()
for key, values := range f {
for _, value := range values {
filterArgs.Add(key, value)
}
}
log.Debug().Interface("filterArgs", filterArgs).Msg("Creating remote client")
func NewRemoteClient(host Host) (Client, error) {
if host.URL.Scheme != "tcp" {
return nil, fmt.Errorf("invalid scheme: %s", host.URL.Scheme)
}
@@ -175,7 +154,7 @@ func NewRemoteClient(f map[string][]string, host Host) (Client, error) {
host.Type = "remote"
return NewClient(cli, filterArgs, host), nil
return NewClient(cli, host), nil
}
// Finds a container by id, skipping the filters
@@ -202,10 +181,10 @@ func (d *httpClient) ContainerActions(ctx context.Context, action ContainerActio
}
}
func (d *httpClient) ListContainers(ctx context.Context) ([]Container, error) {
log.Debug().Interface("filter", d.filters).Str("host", d.host.Name).Msg("Listing containers")
func (d *httpClient) ListContainers(ctx context.Context, filter ContainerFilter) ([]Container, error) {
log.Debug().Interface("filter", filter).Str("host", d.host.Name).Msg("Listing containers")
containerListOptions := container.ListOptions{
Filters: d.filters,
Filters: filter.asArgs(),
All: true,
}
list, err := d.cli.ContainerList(ctx, containerListOptions)

View File

@@ -23,18 +23,20 @@ type ContainerStore struct {
connected atomic.Bool
events chan ContainerEvent
ctx context.Context
filter ContainerFilter
}
func NewContainerStore(ctx context.Context, client Client) *ContainerStore {
func NewContainerStore(ctx context.Context, client Client, filter ContainerFilter) *ContainerStore {
s := &ContainerStore{
containers: xsync.NewMapOf[string, *Container](),
client: client,
subscribers: xsync.NewMapOf[context.Context, chan<- ContainerEvent](),
newContainerSubscribers: xsync.NewMapOf[context.Context, chan<- Container](),
statsCollector: NewStatsCollector(client),
statsCollector: NewStatsCollector(client, filter),
wg: sync.WaitGroup{},
events: make(chan ContainerEvent),
ctx: ctx,
filter: filter,
}
s.wg.Add(1)
@@ -62,7 +64,7 @@ func (s *ContainerStore) checkConnectivity() error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) // 3s is enough to fetch all containers
defer cancel()
if containers, err := s.client.ListContainers(ctx); err != nil {
if containers, err := s.client.ListContainers(ctx, s.filter); err != nil {
return err
} else {
s.containers.Clear()
@@ -103,15 +105,27 @@ func (s *ContainerStore) checkConnectivity() error {
return nil
}
func (s *ContainerStore) ListContainers() ([]Container, error) {
func (s *ContainerStore) ListContainers(filter ContainerFilter) ([]Container, error) {
s.wg.Wait()
if err := s.checkConnectivity(); err != nil {
return nil, err
}
validContainers, err := s.client.ListContainers(s.ctx, filter)
if err != nil {
return nil, err
}
validIDMap := lo.KeyBy(validContainers, func(item Container) string {
return item.ID
})
containers := make([]Container, 0)
s.containers.Range(func(_ string, c *Container) bool {
containers = append(containers, *c)
if _, ok := validIDMap[c.ID]; ok {
containers = append(containers, *c)
}
return true
})
@@ -181,7 +195,7 @@ func (s *ContainerStore) init() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
if container, err := s.client.FindContainer(ctx, event.ActorID); err == nil {
list, _ := s.client.ListContainers(ctx)
list, _ := s.client.ListContainers(ctx, s.filter)
// make sure the container is in the list of containers when using filter
valid := lo.ContainsBy(list, func(item Container) bool {

View File

@@ -21,16 +21,18 @@ type StatsCollector struct {
timer *time.Timer
mu sync.Mutex
totalStarted atomic.Int32
filter ContainerFilter
}
var timeToStop = 6 * time.Hour
func NewStatsCollector(client Client) *StatsCollector {
func NewStatsCollector(client Client, filter ContainerFilter) *StatsCollector {
return &StatsCollector{
stream: make(chan ContainerStat),
subscribers: xsync.NewMapOf[context.Context, chan<- ContainerStat](),
client: client,
cancelers: xsync.NewMapOf[string, context.CancelFunc](),
filter: filter,
}
}
@@ -98,7 +100,7 @@ func (sc *StatsCollector) Start(parentCtx context.Context) bool {
sc.mu.Unlock()
timeoutCtx, cancel := context.WithTimeout(parentCtx, 3*time.Second) // 3 seconds to list containers is hard limit
if containers, err := sc.client.ListContainers(timeoutCtx); err == nil {
if containers, err := sc.client.ListContainers(timeoutCtx, sc.filter); err == nil {
for _, c := range containers {
if c.State == "running" {
go streamStats(ctx, sc, c.ID)

View File

@@ -3,9 +3,11 @@ package docker
import (
"fmt"
"math"
"strings"
"time"
"github.com/amir20/dozzle/internal/utils"
"github.com/docker/docker/api/types/filters"
)
// Container represents an internal representation of docker containers
@@ -41,6 +43,34 @@ type ContainerEvent struct {
ActorAttributes map[string]string `json:"actorAttributes,omitempty"`
}
type ContainerFilter map[string][]string
func NewContainerFilter(values map[string]string) (ContainerFilter, error) {
containerFilter := make(ContainerFilter)
for _, filter := range values {
pos := strings.Index(filter, "=")
if pos == -1 {
return nil, fmt.Errorf("invalid filter: %s. each filter should be of the form key=value", filter)
}
key := filter[:pos]
val := filter[pos+1:]
containerFilter[key] = append(containerFilter[key], val)
}
return containerFilter, nil
}
func (f ContainerFilter) asArgs() filters.Args {
filterArgs := filters.NewArgs()
for key, values := range f {
for _, value := range values {
filterArgs.Add(key, value)
}
}
return filterArgs
}
type LogPosition string
const (