Skip to content

Commit

Permalink
feat: handles container exits better and uses the real time when cont…
Browse files Browse the repository at this point in the history
…ainer has exited (#3504)
  • Loading branch information
amir20 authored Jan 3, 2025
1 parent cc620ad commit 46fb09e
Show file tree
Hide file tree
Showing 16 changed files with 191 additions and 90 deletions.
18 changes: 14 additions & 4 deletions assets/components/ContainerPopup.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
<template>
<div>
<span class="font-light capitalize"> RUNNING </span>
<span class="font-light capitalize"> STATE </span>
<span class="font-semibold uppercase"> {{ container.state }} </span>
</div>
<div v-if="container.startedAt.getFullYear() > 0">
<span class="font-light capitalize"> STARTED </span>
<span class="font-semibold">
<DistanceTime :date="container.created" strict :suffix="false" />
<DistanceTime :date="container.startedAt" strict />
</span>
</div>
<div>
<div v-if="container.state != 'running' && container.finishedAt.getFullYear() > 0">
<span class="font-light capitalize"> Finished </span>
<span class="font-semibold">
<DistanceTime :date="container.finishedAt" strict />
</span>
</div>
<div v-if="container.state == 'running'">
<span class="font-light capitalize"> Load </span>
<span class="font-semibold"> {{ container.stat.cpu.toFixed(2) }}% </span>
</div>
<div>
<div v-if="container.state == 'running'">
<span class="font-light capitalize"> MEM </span>
<span class="font-semibold"> {{ formatBytes(container.stat.memoryUsage) }} </span>
</div>
Expand Down
42 changes: 39 additions & 3 deletions assets/components/FuzzySearchModal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,45 @@ function createFuzzySearchModal() {
initialState: {
container: {
containers: [
new Container("123", new Date(), "image", "test", "command", "host", {}, "running", []),
new Container("345", new Date(), "image", "foo bar", "command", "host", {}, "running", []),
new Container("567", new Date(), "image", "baz", "command", "host", {}, "running", []),
new Container(
"123",
new Date(),
new Date(),
new Date(),
"image",
"test",
"command",
"host",
{},
"running",
[],
),
new Container(
"345",
new Date(),
new Date(),
new Date(),
"image",
"foo bar",
"command",
"host",
{},
"running",
[],
),
new Container(
"567",
new Date(),
new Date(),
new Date(),
"image",
"baz",
"command",
"host",
{},
"running",
[],
),
],
},
},
Expand Down
2 changes: 1 addition & 1 deletion assets/components/HostMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
</span>
</router-link>
<template #content>
<ContainerPopup :container="item as Container" />
<ContainerPopup :container="item" />
</template>
</Popup>
</li>
Expand Down
14 changes: 13 additions & 1 deletion assets/components/LogViewer/EventSource.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,19 @@ describe("<ContainerEventSource />", () => {
},
props: {
streamSource: useContainerStream,
entity: new Container("abc", new Date(), "image", "name", "command", "localhost", {}, "created", []),
entity: new Container(
"abc",
new Date(), // created
new Date(), // started
new Date(), // finished
"image",
"name",
"command",
"localhost",
{},
"created",
[],
),
},
});
}
Expand Down
3 changes: 2 additions & 1 deletion assets/composable/eventStreams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,12 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
const event = JSON.parse((e as MessageEvent).data) as {
actorId: string;
name: "container-stopped" | "container-started";
time: string;
};
const containerEvent = new ContainerEventLogEntry(
event.name == "container-started" ? "Container started" : "Container stopped",
event.actorId,
new Date(),
new Date(event.time),
event.name,
);

Expand Down
2 changes: 2 additions & 0 deletions assets/models/Container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export class Container {
constructor(
public readonly id: string,
public readonly created: Date,
public startedAt: Date,
public finishedAt: Date,
public readonly image: string,
name: string,
public readonly command: string,
Expand Down
17 changes: 16 additions & 1 deletion assets/stores/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,13 @@ export const useContainerStore = defineStore("container", () => {
}
});
es.addEventListener("container-event", (e) => {
const event = JSON.parse((e as MessageEvent).data) as { actorId: string; name: string };
const event = JSON.parse((e as MessageEvent).data) as { actorId: string; name: string; time: string };
const container = allContainersById.value[event.actorId];
if (container) {
switch (event.name) {
case "die":
container.state = "exited";
container.finishedAt = new Date(event.time);
break;
case "destroy":
container.state = "deleted";
Expand All @@ -75,6 +76,18 @@ export const useContainerStore = defineStore("container", () => {
}
});

es.addEventListener("container-updated", (e) => {
const container = JSON.parse((e as MessageEvent).data) as ContainerJson;
const existing = allContainersById.value[container.id];
if (existing) {
existing.name = container.name;
existing.state = container.state;
existing.health = container.health;
existing.startedAt = new Date(container.startedAt);
existing.finishedAt = new Date(container.finishedAt);
}
});

es.addEventListener("update-host", (e) => {
const host = JSON.parse((e as MessageEvent).data) as Host;
updateHost(host);
Expand Down Expand Up @@ -133,6 +146,8 @@ export const useContainerStore = defineStore("container", () => {
return new Container(
c.id,
new Date(c.created),
new Date(c.startedAt),
new Date(c.finishedAt),
c.image,
c.name,
c.command,
Expand Down
4 changes: 3 additions & 1 deletion assets/types/Container.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ export interface ContainerStat {

export type ContainerJson = {
readonly id: string;
readonly created: number;
readonly created: string;
readonly startedAt: string;
readonly finishedAt: string;
readonly image: string;
readonly name: string;
readonly command: string;
Expand Down
8 changes: 6 additions & 2 deletions internal/docker/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,12 +385,16 @@ func newContainerFromJSON(c types.ContainerJSON, host string) Container {
Tty: c.Config.Tty,
}

if createdAt, err := time.Parse(time.RFC3339Nano, c.Created); err == nil {
container.Created = createdAt.UTC()
}

if startedAt, err := time.Parse(time.RFC3339Nano, c.State.StartedAt); err == nil {
container.StartedAt = startedAt.UTC()
}

if createdAt, err := time.Parse(time.RFC3339Nano, c.Created); err == nil {
container.Created = createdAt.UTC()
if stoppedAt, err := time.Parse(time.RFC3339Nano, c.State.FinishedAt); err == nil {
container.FinishedAt = stoppedAt.UTC()
}

if c.State.Health != nil {
Expand Down
28 changes: 28 additions & 0 deletions internal/docker/container_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,34 @@ func (s *ContainerStore) FindContainer(id string, filter ContainerFilter) (Conta
}

if container, ok := s.containers.Load(id); ok {
if container.StartedAt.IsZero() {
log.Debug().Str("id", id).Msg("container doesn't have detailed information, fetching it")
if newContainer, ok := s.containers.Compute(id, func(c *Container, loaded bool) (*Container, bool) {
if loaded {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if newContainer, err := s.client.FindContainer(ctx, id); err == nil {
return &newContainer, false
}
}
return c, false
}); ok {
event := ContainerEvent{
Name: "update",
Host: s.client.Host().ID,
ActorID: id,
}
s.subscribers.Range(func(c context.Context, events chan<- ContainerEvent) bool {
select {
case events <- event:
case <-c.Done():
s.subscribers.Delete(c)
}
return true
})
return *newContainer, nil
}
}
return *container, nil
} else {
log.Warn().Str("id", id).Msg("container not found")
Expand Down
28 changes: 15 additions & 13 deletions internal/docker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,20 @@ import (

// Container represents an internal representation of docker containers
type Container struct {
ID string `json:"id"`
Name string `json:"name"`
Image string `json:"image"`
Command string `json:"command"`
Created time.Time `json:"created"`
StartedAt time.Time `json:"startedAt,omitempty"`
State string `json:"state"`
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"`
Name string `json:"name"`
Image string `json:"image"`
Command string `json:"command"`
Created time.Time `json:"created"`
StartedAt time.Time `json:"startedAt"`
FinishedAt time.Time `json:"finishedAt"`
State string `json:"state"`
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
Expand All @@ -41,6 +42,7 @@ type ContainerEvent struct {
Host string `json:"host"`
ActorID string `json:"actorId"`
ActorAttributes map[string]string `json:"actorAttributes,omitempty"`
Time time.Time `json:"time"`
}

type ContainerFilter map[string][]string
Expand Down
Loading

0 comments on commit 46fb09e

Please sign in to comment.