diff --git a/assets/components.d.ts b/assets/components.d.ts index f41a378f..3661a3cf 100644 --- a/assets/components.d.ts +++ b/assets/components.d.ts @@ -1,10 +1,10 @@ /* eslint-disable */ -/* prettier-ignore */ // @ts-nocheck // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 export {} +/* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { BarChart: typeof import('./components/BarChart.vue')['default'] diff --git a/internal/analytics/types.go b/internal/analytics/types.go index 001d6e9d..d5df1290 100644 --- a/internal/analytics/types.go +++ b/internal/analytics/types.go @@ -13,4 +13,6 @@ type BeaconEvent struct { RunningContainers int `json:"runningContainers"` HasActions bool `json:"hasActions"` IsSwarmMode bool `json:"isSwarmMode"` + ServerVersion string `json:"serverVersion"` + ServerID string `json:"serverID"` } diff --git a/internal/docker/client.go b/internal/docker/client.go index 37c6e28c..167630bc 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -19,6 +19,7 @@ import ( "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" log "github.com/sirupsen/logrus" @@ -56,6 +57,7 @@ type DockerCLI interface { ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error + Info(ctx context.Context) (system.Info, error) } type Client interface { @@ -69,17 +71,33 @@ type Client interface { Host() *Host ContainerActions(action string, containerID string) error IsSwarmMode() bool + SystemInfo() system.Info } type httpClient struct { - cli DockerCLI - filters filters.Args - host *Host - SwarmMode bool + cli DockerCLI + filters filters.Args + host *Host + info system.Info } -func NewClient(cli DockerCLI, filters filters.Args, host *Host, swarm bool) Client { - return &httpClient{cli, filters, host, swarm} +func NewClient(cli DockerCLI, filters filters.Args, host *Host) Client { + client := &httpClient{ + cli: cli, + filters: filters, + host: host, + } + + var err error + client.info, err = cli.Info(context.Background()) + if err != nil { + log.Errorf("unable to get docker info: %v", err) + } + + host.NCPU = client.info.NCPU + host.MemTotal = client.info.MemTotal + + return client } // NewClientWithFilters creates a new instance of Client with docker filters @@ -99,10 +117,7 @@ func NewClientWithFilters(f map[string][]string) (Client, error) { return nil, err } - info, _ := cli.Info(context.Background()) - swarm := info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive - - return NewClient(cli, filterArgs, &Host{Name: "localhost", ID: "localhost"}, swarm), nil + return NewClient(cli, filterArgs, &Host{Name: "localhost", ID: "localhost"}), nil } func NewClientWithTlsAndFilter(f map[string][]string, host Host) (Client, error) { @@ -138,10 +153,7 @@ func NewClientWithTlsAndFilter(f map[string][]string, host Host) (Client, error) return nil, err } - info, _ := cli.Info(context.Background()) - swarm := info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive - - return NewClient(cli, filterArgs, &host, swarm), nil + return NewClient(cli, filterArgs, &host), nil } func (d *httpClient) FindContainer(id string) (Container, error) { @@ -355,7 +367,11 @@ func (d *httpClient) Host() *Host { } func (d *httpClient) IsSwarmMode() bool { - return d.SwarmMode + return d.info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive +} + +func (d *httpClient) SystemInfo() system.Info { + return d.info } var PARENTHESIS_RE = regexp.MustCompile(`\(([a-zA-Z]+)\)`) diff --git a/internal/docker/client_test.go b/internal/docker/client_test.go index 774d8b2c..dbf1e1ba 100644 --- a/internal/docker/client_test.go +++ b/internal/docker/client_test.go @@ -12,6 +12,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/system" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -89,7 +90,7 @@ func (m *mockedProxy) ContainerRestart(ctx context.Context, containerID string, func Test_dockerClient_ListContainers_null(t *testing.T) { proxy := new(mockedProxy) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, nil) - client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}} list, err := client.ListContainers() assert.Empty(t, list, "list should be empty") @@ -101,7 +102,7 @@ func Test_dockerClient_ListContainers_null(t *testing.T) { func Test_dockerClient_ListContainers_error(t *testing.T) { proxy := new(mockedProxy) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(nil, errors.New("test")) - client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}} list, err := client.ListContainers() assert.Nil(t, list, "list should be nil") @@ -124,7 +125,7 @@ func Test_dockerClient_ListContainers_happy(t *testing.T) { proxy := new(mockedProxy) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil) - client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}} list, err := client.ListContainers() require.NoError(t, err, "error should not return an error.") @@ -151,7 +152,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) { options := container.LogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "300", Timestamps: true, Since: "since"} proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil) - client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}} logReader, _ := client.ContainerLogs(context.Background(), id, "since", STDALL) actual, _ := io.ReadAll(logReader) @@ -165,7 +166,7 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) { proxy.On("ContainerLogs", mock.Anything, id, mock.Anything).Return(nil, errors.New("test")) - client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}} reader, err := client.ContainerLogs(context.Background(), id, "", STDALL) @@ -192,7 +193,7 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) { json := types.ContainerJSON{Config: &container.Config{Tty: false}} proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil) - client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}} container, err := client.FindContainer("abcdefghijkl") require.NoError(t, err, "error should not be thrown") @@ -215,7 +216,7 @@ func Test_dockerClient_FindContainer_error(t *testing.T) { proxy := new(mockedProxy) proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil) - client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}} _, err := client.FindContainer("not_valid") require.Error(t, err, "error should be thrown") @@ -236,7 +237,7 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) { } proxy := new(mockedProxy) - client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}} json := types.ContainerJSON{Config: &container.Config{Tty: false}} proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil) proxy.On("ContainerInspect", mock.Anything, "abcdefghijkl").Return(json, nil) @@ -272,7 +273,7 @@ func Test_dockerClient_ContainerActions_error(t *testing.T) { } proxy := new(mockedProxy) - client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, false} + client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}} proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil) proxy.On("ContainerStart", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("test")) diff --git a/internal/docker/host.go b/internal/docker/host.go index 7cbb9d77..90bbdad7 100644 --- a/internal/docker/host.go +++ b/internal/docker/host.go @@ -17,6 +17,8 @@ type Host struct { CACertPath string `json:"-"` KeyPath string `json:"-"` ValidCerts bool `json:"-"` + NCPU int `json:"nCPU"` + MemTotal int64 `json:"memTotal"` } func (h *Host) String() string { diff --git a/internal/web/events.go b/internal/web/events.go index bf9bd843..146c77ce 100644 --- a/internal/web/events.go +++ b/internal/web/events.go @@ -27,22 +27,9 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - b := analytics.BeaconEvent{ - Name: "events", - Version: h.config.Version, - Browser: r.Header.Get("User-Agent"), - AuthProvider: string(h.config.Authorization.Provider), - HasHostname: h.config.Hostname != "", - HasCustomBase: h.config.Base != "/", - HasCustomAddress: h.config.Addr != ":8080", - Clients: len(h.clients), - HasActions: h.config.EnableActions, - } - allContainers := make([]docker.Container, 0) events := make(chan docker.ContainerEvent) stats := make(chan docker.ContainerStat) - hasSwarm := false for _, store := range h.stores { if containers, err := store.List(); err == nil { @@ -57,9 +44,6 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) { store.SubscribeStats(ctx, stats) store.Subscribe(ctx, events) - if store.Client().IsSwarmMode() { - hasSwarm = true - } } defer func() { @@ -71,18 +55,10 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) { if err := sendContainersJSON(allContainers, w); err != nil { log.Errorf("error writing containers to event stream: %v", err) } - b.RunningContainers = len(allContainers) - b.IsSwarmMode = hasSwarm f.Flush() - if !h.config.NoAnalytics { - go func() { - if err := analytics.SendBeacon(b); err != nil { - log.Debugf("error sending beacon: %v", err) - } - }() - } + go sendBeaconEvent(h, r, len(allContainers)) for { select { @@ -141,6 +117,44 @@ func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) { } } +func sendBeaconEvent(h *handler, r *http.Request, runningContainers int) { + b := analytics.BeaconEvent{ + Name: "events", + Version: h.config.Version, + Browser: r.Header.Get("User-Agent"), + AuthProvider: string(h.config.Authorization.Provider), + HasHostname: h.config.Hostname != "", + HasCustomBase: h.config.Base != "/", + HasCustomAddress: h.config.Addr != ":8080", + Clients: len(h.clients), + HasActions: h.config.EnableActions, + RunningContainers: runningContainers, + } + + for _, store := range h.stores { + if store.Client().IsSwarmMode() { + b.IsSwarmMode = true + break + } + } + + if client, ok := h.clients["localhost"]; ok { + b.ServerID = client.SystemInfo().ID + } else { + for _, client := range h.clients { + b.ServerID = client.SystemInfo().ID + break + } + } + + if !h.config.NoAnalytics { + if err := analytics.SendBeacon(b); err != nil { + log.Debugf("error sending beacon: %v", err) + } + } + +} + func sendContainersJSON(containers []docker.Container, w http.ResponseWriter) error { if _, err := fmt.Fprint(w, "event: containers-changed\ndata: "); err != nil { return err diff --git a/internal/web/routes_test.go b/internal/web/routes_test.go index 980fe497..de724924 100644 --- a/internal/web/routes_test.go +++ b/internal/web/routes_test.go @@ -8,6 +8,7 @@ import ( "io/fs" "github.com/amir20/dozzle/internal/docker" + "github.com/docker/docker/api/types/system" "github.com/go-chi/chi/v5" "github.com/stretchr/testify/mock" @@ -63,6 +64,10 @@ func (m *MockedClient) IsSwarmMode() bool { return false } +func (m *MockedClient) SystemInfo() system.Info { + return system.Info{ID: "123"} +} + func createHandler(client docker.Client, content fs.FS, config Config) *chi.Mux { if client == nil { client = new(MockedClient) diff --git a/main.go b/main.go index ef814deb..f3c2dda8 100644 --- a/main.go +++ b/main.go @@ -83,7 +83,7 @@ func main() { } srv := createServer(args, clients) - go doStartEvent(args) + go doStartEvent(args, clients) go func() { log.Infof("Accepting connections on %s", srv.Addr) if err := srv.ListenAndServe(); err != http.ErrServerClosed { @@ -104,7 +104,7 @@ func main() { log.Debug("shutdown complete") } -func doStartEvent(arg args) { +func doStartEvent(arg args, clients map[string]docker.Client) { if arg.NoAnalytics { log.Debug("Analytics disabled.") return @@ -115,6 +115,17 @@ func doStartEvent(arg args) { Version: version, } + if client, ok := clients["localhost"]; ok { + event.ServerID = client.SystemInfo().ID + event.ServerVersion = client.SystemInfo().ServerVersion + } else { + for _, client := range clients { + event.ServerID = client.SystemInfo().ID + event.ServerVersion = client.SystemInfo().ServerVersion + break + } + } + if err := analytics.SendBeacon(event); err != nil { log.Debug(err) } diff --git a/main_test.go b/main_test.go index 3df1214e..ea8b65dc 100644 --- a/main_test.go +++ b/main_test.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/system" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -23,13 +24,17 @@ func (f *fakeCLI) ContainerList(context.Context, container.ListOptions) ([]types return args.Get(0).([]types.Container), args.Error(1) } +func (f *fakeCLI) Info(context.Context) (system.Info, error) { + return system.Info{}, nil +} + func Test_valid_localhost(t *testing.T) { client := new(fakeCLI) client.On("ContainerList").Return([]types.Container{}, nil) fakeClientFactory := func(filter map[string][]string) (docker.Client, error) { return docker.NewClient(client, filters.NewArgs(), &docker.Host{ ID: "localhost", - }, false), nil + }), nil } args := args{} @@ -46,7 +51,7 @@ func Test_invalid_localhost(t *testing.T) { fakeClientFactory := func(filter map[string][]string) (docker.Client, error) { return docker.NewClient(client, filters.NewArgs(), &docker.Host{ ID: "localhost", - }, false), nil + }), nil } args := args{} @@ -63,7 +68,7 @@ func Test_valid_remote(t *testing.T) { fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) { return docker.NewClient(local, filters.NewArgs(), &docker.Host{ ID: "localhost", - }, false), nil + }), nil } remote := new(fakeCLI) @@ -71,7 +76,7 @@ func Test_valid_remote(t *testing.T) { fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) { return docker.NewClient(remote, filters.NewArgs(), &docker.Host{ ID: "test", - }, false), nil + }), nil } args := args{ @@ -93,7 +98,7 @@ func Test_valid_remote_and_local(t *testing.T) { fakeLocalClientFactory := func(filter map[string][]string) (docker.Client, error) { return docker.NewClient(local, filters.NewArgs(), &docker.Host{ ID: "localhost", - }, false), nil + }), nil } remote := new(fakeCLI) @@ -101,7 +106,7 @@ func Test_valid_remote_and_local(t *testing.T) { fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) { return docker.NewClient(remote, filters.NewArgs(), &docker.Host{ ID: "test", - }, false), nil + }), nil } args := args{ RemoteHost: []string{"tcp://test:2375"}, @@ -123,13 +128,13 @@ func Test_no_clients(t *testing.T) { return docker.NewClient(local, filters.NewArgs(), &docker.Host{ ID: "localhost", - }, false), nil + }), nil } fakeRemoteClientFactory := func(filter map[string][]string, host docker.Host) (docker.Client, error) { client := new(fakeCLI) return docker.NewClient(client, filters.NewArgs(), &docker.Host{ ID: "test", - }, false), nil + }), nil } args := args{}