1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-21 13:23:07 +01:00

feat: show start event for containers (#3014)

This commit is contained in:
Amir Raminfar
2024-06-06 08:26:03 -07:00
committed by GitHub
parent 5169c211f3
commit 220218d44d
12 changed files with 88 additions and 55 deletions

View File

@@ -25,6 +25,7 @@ declare module 'vue' {
'Cil:xCircle': typeof import('~icons/cil/x-circle')['default']
ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default']
ContainerActionsToolbar: typeof import('./components/ContainerViewer/ContainerActionsToolbar.vue')['default']
ContainerEventLogItem: typeof import('./components/LogViewer/ContainerEventLogItem.vue')['default']
ContainerHealth: typeof import('./components/ContainerViewer/ContainerHealth.vue')['default']
ContainerLog: typeof import('./components/ContainerViewer/ContainerLog.vue')['default']
ContainerName: typeof import('./components/LogViewer/ContainerName.vue')['default']

View File

@@ -28,13 +28,13 @@
</div>
</template>
<script lang="ts" setup>
import { DockerEventLogEntry } from "@/models/LogEntry";
import { ContainerEventLogEntry } from "@/models/LogEntry";
const router = useRouter();
const { showToast } = useToast();
const { t } = useI18n();
const { logEntry } = defineProps<{
logEntry: DockerEventLogEntry;
logEntry: ContainerEventLogEntry;
showContainerName?: boolean;
}>();

View File

@@ -6,7 +6,7 @@ import {
type JSONObject,
LogEntry,
asLogEntry,
DockerEventLogEntry,
ContainerEventLogEntry,
SkippedLogsEntry,
} from "@/models/LogEntry";
import { Service, Stack } from "@/models/Stack";
@@ -162,9 +162,15 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
es = new EventSource(url.value);
es.addEventListener("container-stopped", (e) => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string };
buffer.push(new DockerEventLogEntry("Container stopped", event.actorId, new Date(), "container-stopped"));
es.addEventListener("container-event", (e) => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string; name: string };
const containerEvent = new ContainerEventLogEntry(
event.name == "container-started" ? "Container started" : "Container stopped",
event.actorId,
new Date(),
event.name as "container-stopped" | "container-started",
);
buffer.push(containerEvent);
flushBuffer();
flushBuffer.flush();

View File

@@ -2,7 +2,7 @@ import { Component, ComputedRef, Ref } from "vue";
import { flattenJSON, getDeep } from "@/utils";
import ComplexLogItem from "@/components/LogViewer/ComplexLogItem.vue";
import SimpleLogItem from "@/components/LogViewer/SimpleLogItem.vue";
import DockerEventLogItem from "@/components/LogViewer/DockerEventLogItem.vue";
import ContainerEventLogItem from "@/components/LogViewer/ContainerEventLogItem.vue";
import SkippedEntriesLogItem from "@/components/LogViewer/SkippedEntriesLogItem.vue";
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
@@ -107,7 +107,7 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
}
}
export class DockerEventLogEntry extends LogEntry<string> {
export class ContainerEventLogEntry extends LogEntry<string> {
constructor(
message: string,
containerID: string,
@@ -117,7 +117,7 @@ export class DockerEventLogEntry extends LogEntry<string> {
super(message, containerID, date.getTime(), date, "stderr", "info");
}
getComponent(): Component {
return DockerEventLogItem;
return ContainerEventLogItem;
}
}

View File

@@ -124,7 +124,7 @@ export const useContainerStore = defineStore("container", () => {
...newContainers.map((c) => {
return new Container(
c.id,
new Date(c.created * 1000),
new Date(c.created),
c.image,
c.name,
c.command,

View File

@@ -63,7 +63,7 @@ type DockerCLI interface {
type Client interface {
ListContainers() ([]Container, error)
FindContainer(string) (Container, error)
ContainerLogs(context.Context, string, string, StdType) (io.ReadCloser, error)
ContainerLogs(context.Context, string, *time.Time, StdType) (io.ReadCloser, error)
Events(context.Context, chan<- ContainerEvent) error
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, StdType) (io.ReadCloser, error)
ContainerStats(context.Context, string, chan<- ContainerStat) error
@@ -177,6 +177,10 @@ func (d *httpClient) FindContainer(id string) (Container, error) {
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
}
} else {
return container, err
}
@@ -226,7 +230,7 @@ func (d *httpClient) ListContainers() ([]Container, error) {
Image: c.Image,
ImageID: c.ImageID,
Command: c.Command,
Created: c.Created,
Created: time.Unix(c.Created, 0),
State: c.State,
Status: c.Status,
Host: d.host.ID,
@@ -295,15 +299,12 @@ func (d *httpClient) ContainerStats(ctx context.Context, id string, stats chan<-
}
}
func (d *httpClient) ContainerLogs(ctx context.Context, id string, since string, stdType StdType) (io.ReadCloser, error) {
func (d *httpClient) ContainerLogs(ctx context.Context, id string, since *time.Time, stdType StdType) (io.ReadCloser, error) {
log.WithField("id", id).WithField("since", since).WithField("stdType", stdType).Debug("streaming logs for container")
if since != "" {
if millis, err := strconv.ParseInt(since, 10, 64); err == nil {
since = time.UnixMicro(millis).Add(time.Millisecond).Format(time.RFC3339Nano)
} else {
log.WithError(err).Debug("unable to parse since")
}
sinceQuery := ""
if since != nil {
sinceQuery = since.Add(time.Millisecond).Format(time.RFC3339Nano)
}
options := container.LogsOptions{
@@ -312,7 +313,7 @@ func (d *httpClient) ContainerLogs(ctx context.Context, id string, since string,
Follow: true,
Tail: strconv.Itoa(100),
Timestamps: true,
Since: since,
Since: sinceQuery,
}
reader, err := d.cli.ContainerLogs(ctx, id, options)

View File

@@ -6,6 +6,7 @@ import (
"encoding/binary"
"errors"
"io"
"time"
"testing"
@@ -149,11 +150,18 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
b = append(b, []byte(expected)...)
reader := io.NopCloser(bytes.NewReader(b))
options := container.LogsOptions{ShowStdout: true, ShowStderr: true, Follow: true, Tail: "100", Timestamps: true, Since: "since"}
since := time.Date(2021, time.January, 1, 0, 0, 0, 0, time.UTC)
options := container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
Tail: "100",
Timestamps: true,
Since: "2021-01-01T00:00:00.001Z"}
proxy.On("ContainerLogs", mock.Anything, id, options).Return(reader, nil)
client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}}
logReader, _ := client.ContainerLogs(context.Background(), id, "since", STDALL)
logReader, _ := client.ContainerLogs(context.Background(), id, &since, STDALL)
actual, _ := io.ReadAll(logReader)
assert.Equal(t, string(b), string(actual), "message doesn't match expected")
@@ -168,7 +176,7 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}}
reader, err := client.ContainerLogs(context.Background(), id, "", STDALL)
reader, err := client.ContainerLogs(context.Background(), id, nil, STDALL)
assert.Nil(t, reader, "reader should be nil")
assert.Error(t, err, "error should have been returned")
@@ -190,7 +198,8 @@ func Test_dockerClient_FindContainer_happy(t *testing.T) {
proxy := new(mockedProxy)
proxy.On("ContainerList", mock.Anything, mock.Anything).Return(containers, nil)
json := types.ContainerJSON{Config: &container.Config{Tty: false}}
state := &types.ContainerState{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{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{}}
@@ -238,7 +247,10 @@ func Test_dockerClient_ContainerActions_happy(t *testing.T) {
proxy := new(mockedProxy)
client := &httpClient{proxy, filters.NewArgs(), &Host{ID: "localhost"}, system.Info{}}
json := types.ContainerJSON{Config: &container.Config{Tty: false}}
state := &types.ContainerState{Status: "running", StartedAt: time.Now().Format(time.RFC3339Nano)}
json := types.ContainerJSON{ContainerJSONBase: &types.ContainerJSONBase{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)

View File

@@ -2,6 +2,7 @@ package docker
import (
"math"
"time"
"github.com/amir20/dozzle/internal/utils"
)
@@ -14,7 +15,8 @@ type Container struct {
Image string `json:"image"`
ImageID string `json:"imageId"`
Command string `json:"command"`
Created int64 `json:"created"`
Created time.Time `json:"created"`
StartedAt *time.Time `json:"startedAt,omitempty"`
State string `json:"state"`
Status string `json:"status"`
Health string `json:"health,omitempty"`

View File

@@ -132,7 +132,7 @@ data: []
event: containers-changed
data: [{"id":"1234","names":null,"name":"test","image":"test","imageId":"","command":"","created":0,"state":"","status":"","stats":[]}]
data: [{"id":"1234","names":null,"name":"test","image":"test","imageId":"","command":"","created":"0001-01-01T00:00:00Z","state":"","status":"","stats":[]}]
event: container-start
@@ -178,7 +178,7 @@ X-Accel-Buffering: no
data: {"m":"INFO Testing logs...","ts":0,"id":4256192898,"l":"info","s":"stdout","c":"123456"}
event: container-stopped
event: container-event
data: {"actorId":"123456","name":"container-stopped","host":"localhost"}
/* snapshot: Test_handler_streamLogs_happy_container_stopped */
@@ -204,5 +204,5 @@ X-Accel-Buffering: no
data: {"m":"INFO Testing logs...","ts":1589396137772,"id":1469707724,"l":"info","s":"stdout","c":"123456"}
id: 1589396137772
event: container-stopped
event: container-event
data: {"actorId":"123456","name":"container-stopped","host":"localhost"}

View File

@@ -343,11 +343,13 @@ func streamLogsForContainers(w http.ResponseWriter, r *http.Request, clients map
w.Header().Set("X-Accel-Buffering", "no")
logs := make(chan *docker.LogEvent)
events := make(chan *docker.ContainerEvent)
events := make(chan *docker.ContainerEvent, 1)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
started := time.Now()
loop:
for {
select {
@@ -366,8 +368,11 @@ loop:
fmt.Fprintf(w, ":ping \n\n")
f.Flush()
case container := <-containers:
if container.StartedAt != nil && container.StartedAt.After(started) {
events <- &docker.ContainerEvent{ActorID: container.ID, Name: "container-started", Host: container.Host}
}
go func(container docker.Container) {
reader, err := clients[container.Host].ContainerLogs(r.Context(), container.ID, "", stdTypes)
reader, err := clients[container.Host].ContainerLogs(r.Context(), container.ID, container.StartedAt, stdTypes)
if err != nil {
return
}
@@ -395,7 +400,7 @@ loop:
if buf, err := json.Marshal(event); err != nil {
log.Errorf("json encoding error while streaming %v", err.Error())
} else {
fmt.Fprintf(w, "event: container-stopped\ndata: %s\n\n", buf)
fmt.Fprintf(w, "event: container-event\ndata: %s\n\n", buf)
f.Flush()
}

View File

@@ -36,8 +36,10 @@ func Test_handler_streamLogs_happy(t *testing.T) {
data := makeMessage("INFO Testing logs...", docker.STDOUT)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Tty: false, Host: "localhost"}, nil)
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "", docker.STDALL).Return(io.NopCloser(bytes.NewReader(data)), nil).
now := time.Now()
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Tty: false, Host: "localhost", StartedAt: &now}, nil)
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, &now, docker.STDALL).Return(io.NopCloser(bytes.NewReader(data)), nil).
Run(func(args mock.Arguments) {
go func() {
time.Sleep(50 * time.Millisecond)
@@ -67,8 +69,10 @@ func Test_handler_streamLogs_happy_with_id(t *testing.T) {
data := makeMessage("2020-05-13T18:55:37.772853839Z INFO Testing logs...", docker.STDOUT)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Host: "localhost"}, nil)
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "", docker.STDALL).Return(io.NopCloser(bytes.NewReader(data)), nil).
started := time.Date(2020, time.May, 13, 18, 55, 37, 772853839, time.UTC)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Host: "localhost", StartedAt: &started}, nil)
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, &started, docker.STDALL).Return(io.NopCloser(bytes.NewReader(data)), nil).
Run(func(args mock.Arguments) {
go func() {
time.Sleep(50 * time.Millisecond)
@@ -94,9 +98,10 @@ func Test_handler_streamLogs_happy_container_stopped(t *testing.T) {
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
started := time.Date(2020, time.May, 13, 18, 55, 37, 772853839, time.UTC)
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Host: "localhost"}, nil)
mockedClient.On("ContainerLogs", mock.Anything, id, "", docker.STDALL).Return(io.NopCloser(strings.NewReader("")), io.EOF).
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Host: "localhost", StartedAt: &started}, nil)
mockedClient.On("ContainerLogs", mock.Anything, id, &started, docker.STDALL).Return(io.NopCloser(strings.NewReader("")), io.EOF).
Run(func(args mock.Arguments) {
go func() {
time.Sleep(50 * time.Millisecond)
@@ -150,9 +155,10 @@ func Test_handler_streamLogs_error_reading(t *testing.T) {
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
started := time.Date(2020, time.May, 13, 18, 55, 37, 772853839, time.UTC)
mockedClient := new(MockedClient)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Host: "localhost"}, nil)
mockedClient.On("ContainerLogs", mock.Anything, id, "", docker.STDALL).Return(io.NopCloser(strings.NewReader("")), errors.New("test error")).
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id, Host: "localhost", StartedAt: &started}, nil)
mockedClient.On("ContainerLogs", mock.Anything, id, &started, docker.STDALL).Return(io.NopCloser(strings.NewReader("")), errors.New("test error")).
Run(func(args mock.Arguments) {
go func() {
time.Sleep(50 * time.Millisecond)

View File

@@ -36,7 +36,7 @@ func (m *MockedClient) ListContainers() ([]docker.Container, error) {
return args.Get(0).([]docker.Container), args.Error(1)
}
func (m *MockedClient) ContainerLogs(ctx context.Context, id string, since string, stdType docker.StdType) (io.ReadCloser, error) {
func (m *MockedClient) ContainerLogs(ctx context.Context, id string, since *time.Time, stdType docker.StdType) (io.ReadCloser, error) {
args := m.Called(ctx, id, since, stdType)
return args.Get(0).(io.ReadCloser), args.Error(1)
}