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:
1
assets/components.d.ts
vendored
1
assets/components.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -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;
|
||||
}>();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,27 +2,29 @@ package docker
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/utils"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
Command string `json:"command"`
|
||||
Created int64 `json:"created"`
|
||||
State string `json:"state"`
|
||||
Status string `json:"status"`
|
||||
Health string `json:"health,omitempty"`
|
||||
Host string `json:"host,omitempty"`
|
||||
Tty bool `json:"-"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Names []string `json:"names"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
ImageID string `json:"imageId"`
|
||||
Command string `json:"command"`
|
||||
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:"-"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Stats *utils.RingBuffer[ContainerStat] `json:"stats,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
}
|
||||
|
||||
// ContainerStat represent stats instant for a container
|
||||
|
||||
@@ -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"}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user