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:
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user