mirror of
https://github.com/amir20/dozzle.git
synced 2026-06-23 04:10:12 +00:00
chore: cleanups across mounts + multi-container stats (#4716)
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Deploy VitePress site to Pages / build (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
Push container / Push branches and PRs (push) Has been cancelled
Test / Typecheck (push) Has been cancelled
Test / JavaScript Tests (push) Has been cancelled
Test / Go Tests (push) Has been cancelled
Test / Go Staticcheck (push) Has been cancelled
Test / Integration Tests (push) Has been cancelled
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
tabindex="0"
|
||||
class="dropdown-content rounded-box bg-base-200 border-base-content/20 z-50 mt-1 w-72 border p-2 text-xs shadow-sm"
|
||||
>
|
||||
<div class="text-base-content/60 mb-1.5 px-1 text-[11px] tracking-wide uppercase">Volumes</div>
|
||||
<div class="text-base-content/60 mb-1.5 px-1 text-[11px] tracking-wide uppercase">{{ t("tooltip.volumes") }}</div>
|
||||
<ul class="space-y-1.5">
|
||||
<li
|
||||
v-for="m in sortedMounts"
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
<div class="text-base-content/60 flex justify-between tabular-nums">
|
||||
<span v-if="m.available">{{ formatBytes(m.used) }} / {{ formatBytes(m.total) }}</span>
|
||||
<span v-else>not reachable from this host</span>
|
||||
<span v-else>{{ t("tooltip.volume-unreachable") }}</span>
|
||||
<RelativeTime v-if="m.lastChecked" :date="m.lastChecked" class="text-[10.5px]" />
|
||||
</div>
|
||||
</li>
|
||||
@@ -52,6 +52,8 @@ const CRITICAL = 0.95;
|
||||
|
||||
const { container } = defineProps<{ container: Container; openUp?: boolean }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface DerivedMount {
|
||||
destination: string;
|
||||
total: number;
|
||||
@@ -107,6 +109,6 @@ function formatPct(pct: number) {
|
||||
|
||||
const title = computed(() => {
|
||||
if (!worst.value) return "";
|
||||
return `${worst.value.destination} is ${formatPct(worst.value.pct)} full`;
|
||||
return t("tooltip.volume-full", { destination: worst.value.destination, pct: formatPct(worst.value.pct) });
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -34,9 +34,11 @@ const { networkRx, networkTx, diskRead, diskWrite } = defineProps<{
|
||||
diskWrite: number;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const tooltip = computed(
|
||||
() =>
|
||||
`Network: ↑ ${formatBytes(networkTx)}/s · ↓ ${formatBytes(networkRx)}/s\n` +
|
||||
`Disk: ↑ ${formatBytes(diskWrite)}/s · ↓ ${formatBytes(diskRead)}/s`,
|
||||
t("tooltip.network-io", { tx: formatBytes(networkTx), rx: formatBytes(networkRx) }) +
|
||||
"\n" +
|
||||
t("tooltip.disk-io", { write: formatBytes(diskWrite), read: formatBytes(diskRead) }),
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
:icon="PhCpu"
|
||||
card-class="bg-primary/10 md:min-w-56"
|
||||
icon-class="text-primary"
|
||||
:title="`CPU ${totalStat.cpu.toFixed(2)}% / ${roundCPU(limits.cpu)} cores`"
|
||||
:title="t('tooltip.cpu-usage', { cpu: totalStat.cpu.toFixed(2), cores: roundCPU(limits.cpu) })"
|
||||
>
|
||||
<template #value="{ hoveredValue }">
|
||||
<span class="tabular-nums">
|
||||
@@ -28,7 +28,9 @@
|
||||
:icon="PhMemory"
|
||||
card-class="bg-secondary/10 md:min-w-56"
|
||||
icon-class="text-secondary"
|
||||
:title="`Memory ${formatBytes(totalStat.memoryUsage)} / ${formatBytes(limits.memory)}`"
|
||||
:title="
|
||||
t('tooltip.memory-usage', { used: formatBytes(totalStat.memoryUsage), total: formatBytes(limits.memory) })
|
||||
"
|
||||
>
|
||||
<template #value="{ hoveredValue }">
|
||||
<span class="tabular-nums">
|
||||
@@ -46,8 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Stat } from "@/models/Container";
|
||||
import { Container } from "@/models/Container";
|
||||
import { Container, Stat, emptyStat } from "@/models/Container";
|
||||
import StatCard from "@/components/LogViewer/StatCard.vue";
|
||||
import IOCard from "@/components/LogViewer/IOCard.vue";
|
||||
import Sparkline from "@/components/LogViewer/Sparkline.vue";
|
||||
@@ -60,15 +61,9 @@ const { containers } = defineProps<{
|
||||
containers: Container[];
|
||||
}>();
|
||||
|
||||
const totalStat = ref<Stat>({
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
memoryUsage: 0,
|
||||
networkRxTotal: 0,
|
||||
networkTxTotal: 0,
|
||||
diskReadTotal: 0,
|
||||
diskWriteTotal: 0,
|
||||
});
|
||||
const { t } = useI18n();
|
||||
|
||||
const totalStat = ref<Stat>(emptyStat());
|
||||
const { history, reset } = useSimpleRefHistory(totalStat, { capacity: 300 });
|
||||
const { hosts } = useHosts();
|
||||
const networkRate = ref({ rx: 0, tx: 0 });
|
||||
@@ -89,33 +84,22 @@ watch(
|
||||
() => {
|
||||
const initial: Stat[] = [];
|
||||
for (let i = 1; i <= 300; i++) {
|
||||
const stat = containers.reduce(
|
||||
(acc, container) => {
|
||||
const item = container.statsHistory.at(-i);
|
||||
if (!item) {
|
||||
return acc;
|
||||
}
|
||||
const cores = toContainerCores(container);
|
||||
return {
|
||||
cpu: acc.cpu + item.cpu / cores,
|
||||
memory: acc.memory + item.memory,
|
||||
memoryUsage: acc.memoryUsage + item.memoryUsage,
|
||||
networkRxTotal: acc.networkRxTotal + item.networkRxTotal,
|
||||
networkTxTotal: acc.networkTxTotal + item.networkTxTotal,
|
||||
diskReadTotal: acc.diskReadTotal + item.diskReadTotal,
|
||||
diskWriteTotal: acc.diskWriteTotal + item.diskWriteTotal,
|
||||
};
|
||||
},
|
||||
{
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
memoryUsage: 0,
|
||||
networkRxTotal: 0,
|
||||
networkTxTotal: 0,
|
||||
diskReadTotal: 0,
|
||||
diskWriteTotal: 0,
|
||||
},
|
||||
);
|
||||
const stat = containers.reduce((acc, container) => {
|
||||
const item = container.statsHistory.at(-i);
|
||||
if (!item) {
|
||||
return acc;
|
||||
}
|
||||
const cores = toContainerCores(container);
|
||||
return {
|
||||
cpu: acc.cpu + item.cpu / cores,
|
||||
memory: acc.memory + item.memory,
|
||||
memoryUsage: acc.memoryUsage + item.memoryUsage,
|
||||
networkRxTotal: acc.networkRxTotal + item.networkRxTotal,
|
||||
networkTxTotal: acc.networkTxTotal + item.networkTxTotal,
|
||||
diskReadTotal: acc.diskReadTotal + item.diskReadTotal,
|
||||
diskWriteTotal: acc.diskWriteTotal + item.diskWriteTotal,
|
||||
};
|
||||
}, emptyStat());
|
||||
initial.push(stat);
|
||||
}
|
||||
totalStat.value = initial[0];
|
||||
@@ -164,21 +148,18 @@ const limits = computed(() => {
|
||||
|
||||
useIntervalFn(() => {
|
||||
const previousStat = totalStat.value;
|
||||
totalStat.value = containers.reduce(
|
||||
(acc, container) => {
|
||||
const cores = toContainerCores(container);
|
||||
return {
|
||||
cpu: acc.cpu + container.stat.cpu / cores,
|
||||
memory: acc.memory + container.stat.memory,
|
||||
memoryUsage: acc.memoryUsage + container.stat.memoryUsage,
|
||||
networkRxTotal: acc.networkRxTotal + container.stat.networkRxTotal,
|
||||
networkTxTotal: acc.networkTxTotal + container.stat.networkTxTotal,
|
||||
diskReadTotal: acc.diskReadTotal + container.stat.diskReadTotal,
|
||||
diskWriteTotal: acc.diskWriteTotal + container.stat.diskWriteTotal,
|
||||
};
|
||||
},
|
||||
{ cpu: 0, memory: 0, memoryUsage: 0, networkRxTotal: 0, networkTxTotal: 0, diskReadTotal: 0, diskWriteTotal: 0 },
|
||||
);
|
||||
totalStat.value = containers.reduce((acc, container) => {
|
||||
const cores = toContainerCores(container);
|
||||
return {
|
||||
cpu: acc.cpu + container.stat.cpu / cores,
|
||||
memory: acc.memory + container.stat.memory,
|
||||
memoryUsage: acc.memoryUsage + container.stat.memoryUsage,
|
||||
networkRxTotal: acc.networkRxTotal + container.stat.networkRxTotal,
|
||||
networkTxTotal: acc.networkTxTotal + container.stat.networkTxTotal,
|
||||
diskReadTotal: acc.diskReadTotal + container.stat.diskReadTotal,
|
||||
diskWriteTotal: acc.diskWriteTotal + container.stat.diskWriteTotal,
|
||||
};
|
||||
}, emptyStat());
|
||||
|
||||
networkRate.value = {
|
||||
rx: Math.max(0, totalStat.value.networkRxTotal - previousStat.networkRxTotal),
|
||||
|
||||
@@ -230,12 +230,17 @@ async function skipFeedback() {
|
||||
}
|
||||
}
|
||||
|
||||
let abortController: AbortController | null = null;
|
||||
|
||||
async function createDefaultAlerts() {
|
||||
if (creating.value) return;
|
||||
creating.value = true;
|
||||
abortController?.abort();
|
||||
abortController = new AbortController();
|
||||
const signal = abortController.signal;
|
||||
const chosen = signals.value.filter((s) => selectedSignals.value.includes(s.key));
|
||||
try {
|
||||
const dispatchersRes = await fetch(withBase("/api/notifications/dispatchers"));
|
||||
const dispatchersRes = await fetch(withBase("/api/notifications/dispatchers"), { signal });
|
||||
if (!dispatchersRes.ok) throw new Error("dispatchers fetch failed");
|
||||
const dispatchers: Array<{ id: number; type: string }> = await dispatchersRes.json();
|
||||
const cloud = dispatchers.find((d) => d.type === "cloud");
|
||||
@@ -246,22 +251,23 @@ async function createDefaultAlerts() {
|
||||
// fallback toast path. Acceptable for a welcome modal; the user can edit
|
||||
// or delete rules from /notifications.
|
||||
await Promise.all(
|
||||
chosen.map((signal) =>
|
||||
chosen.map((s) =>
|
||||
fetch(withBase("/api/notifications/rules"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
signal,
|
||||
body: JSON.stringify({
|
||||
name: signal.ruleName,
|
||||
name: s.ruleName,
|
||||
enabled: true,
|
||||
dispatcherId: cloud.id,
|
||||
logExpression: "",
|
||||
containerExpression: "true",
|
||||
eventExpression: signal.kind === "event" ? signal.expression : "",
|
||||
metricExpression: signal.kind === "metric" ? signal.expression : "",
|
||||
eventExpression: s.kind === "event" ? s.expression : "",
|
||||
metricExpression: s.kind === "metric" ? s.expression : "",
|
||||
// Metric alerts: don't re-fire more than once an hour per container,
|
||||
// and require the threshold to hold for the default sample window.
|
||||
cooldown: signal.kind === "metric" ? 3600 : 0,
|
||||
sampleWindow: signal.kind === "metric" ? 60 : 0,
|
||||
cooldown: s.kind === "metric" ? 3600 : 0,
|
||||
sampleWindow: s.kind === "metric" ? 60 : 0,
|
||||
}),
|
||||
}).then((res) => {
|
||||
if (!res.ok) throw new Error("rule POST failed");
|
||||
@@ -271,7 +277,8 @@ async function createDefaultAlerts() {
|
||||
|
||||
close();
|
||||
router.push({ path: "/notifications" });
|
||||
} catch {
|
||||
} catch (err) {
|
||||
if ((err as Error)?.name === "AbortError") return;
|
||||
close();
|
||||
showToast(
|
||||
{
|
||||
@@ -299,6 +306,10 @@ function close() {
|
||||
modal.value?.close();
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
abortController?.abort();
|
||||
});
|
||||
|
||||
function onClose() {
|
||||
if (step.value === "step1" && !feedbackSent) {
|
||||
feedbackSent = true;
|
||||
|
||||
@@ -10,6 +10,16 @@ import { Ref } from "vue";
|
||||
|
||||
export type Stat = Omit<ContainerStat, "id">;
|
||||
|
||||
export const emptyStat = (): Stat => ({
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
memoryUsage: 0,
|
||||
networkRxTotal: 0,
|
||||
networkTxTotal: 0,
|
||||
diskReadTotal: 0,
|
||||
diskWriteTotal: 0,
|
||||
});
|
||||
|
||||
const hosts = computed(() =>
|
||||
config.hosts.reduce(
|
||||
(acc, item) => {
|
||||
@@ -65,15 +75,7 @@ export class Container {
|
||||
) {
|
||||
this.mounts = mounts;
|
||||
this.mountStats = mountStats;
|
||||
const defaultStat = {
|
||||
cpu: 0,
|
||||
memory: 0,
|
||||
memoryUsage: 0,
|
||||
networkRxTotal: 0,
|
||||
networkTxTotal: 0,
|
||||
diskReadTotal: 0,
|
||||
diskWriteTotal: 0,
|
||||
} as Stat;
|
||||
const defaultStat = emptyStat();
|
||||
this._stat = ref(stats.at(-1) || defaultStat);
|
||||
const recentStats = stats.slice(-300);
|
||||
const padding = Array(300 - recentStats.length).fill(defaultStat);
|
||||
|
||||
@@ -98,10 +98,10 @@ type Container struct {
|
||||
FullyLoaded bool `protobuf:"varint,19,opt,name=fullyLoaded,proto3" json:"fullyLoaded,omitempty"`
|
||||
Env []string `protobuf:"bytes,20,rep,name=env,proto3" json:"env,omitempty"`
|
||||
Ports []string `protobuf:"bytes,21,rep,name=ports,proto3" json:"ports,omitempty"`
|
||||
Mounts []*Mount `protobuf:"bytes,22,rep,name=mounts,proto3" json:"mounts,omitempty"`
|
||||
RestartPolicy string `protobuf:"bytes,23,opt,name=restartPolicy,proto3" json:"restartPolicy,omitempty"`
|
||||
NetworkMode string `protobuf:"bytes,24,opt,name=networkMode,proto3" json:"networkMode,omitempty"`
|
||||
MountStats []*MountStat `protobuf:"bytes,25,rep,name=mountStats,proto3" json:"mountStats,omitempty"`
|
||||
Mounts []*Mount `protobuf:"bytes,26,rep,name=mounts,proto3" json:"mounts,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -283,13 +283,6 @@ func (x *Container) GetPorts() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Container) GetMounts() []*Mount {
|
||||
if x != nil {
|
||||
return x.Mounts
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Container) GetRestartPolicy() string {
|
||||
if x != nil {
|
||||
return x.RestartPolicy
|
||||
@@ -311,6 +304,13 @@ func (x *Container) GetMountStats() []*MountStat {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Container) GetMounts() []*Mount {
|
||||
if x != nil {
|
||||
return x.Mounts
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ContainerStat struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
@@ -1387,7 +1387,7 @@ var File_types_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_types_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\vtypes.proto\x12\bprotobuf\x1a\x19google/protobuf/any.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xf0\x06\n" +
|
||||
"\vtypes.proto\x12\bprotobuf\x1a\x19google/protobuf/any.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xf6\x06\n" +
|
||||
"\tContainer\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" +
|
||||
@@ -1410,16 +1410,16 @@ const file_types_proto_rawDesc = "" +
|
||||
"\bcpuLimit\x18\x12 \x01(\x01R\bcpuLimit\x12 \n" +
|
||||
"\vfullyLoaded\x18\x13 \x01(\bR\vfullyLoaded\x12\x10\n" +
|
||||
"\x03env\x18\x14 \x03(\tR\x03env\x12\x14\n" +
|
||||
"\x05ports\x18\x15 \x03(\tR\x05ports\x12'\n" +
|
||||
"\x06mounts\x18\x16 \x03(\v2\x0f.protobuf.MountR\x06mounts\x12$\n" +
|
||||
"\x05ports\x18\x15 \x03(\tR\x05ports\x12$\n" +
|
||||
"\rrestartPolicy\x18\x17 \x01(\tR\rrestartPolicy\x12 \n" +
|
||||
"\vnetworkMode\x18\x18 \x01(\tR\vnetworkMode\x123\n" +
|
||||
"\n" +
|
||||
"mountStats\x18\x19 \x03(\v2\x13.protobuf.MountStatR\n" +
|
||||
"mountStats\x1a9\n" +
|
||||
"mountStats\x12'\n" +
|
||||
"\x06mounts\x18\x1a \x03(\v2\x0f.protobuf.MountR\x06mounts\x1a9\n" +
|
||||
"\vLabelsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xa5\x02\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01J\x04\b\x16\x10\x17\"\xa5\x02\n" +
|
||||
"\rContainerStat\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x1e\n" +
|
||||
"\n" +
|
||||
@@ -1572,8 +1572,8 @@ var file_types_proto_depIdxs = []int32{
|
||||
16, // 2: protobuf.Container.labels:type_name -> protobuf.Container.LabelsEntry
|
||||
2, // 3: protobuf.Container.stats:type_name -> protobuf.ContainerStat
|
||||
20, // 4: protobuf.Container.finished:type_name -> google.protobuf.Timestamp
|
||||
3, // 5: protobuf.Container.mounts:type_name -> protobuf.Mount
|
||||
4, // 6: protobuf.Container.mountStats:type_name -> protobuf.MountStat
|
||||
4, // 5: protobuf.Container.mountStats:type_name -> protobuf.MountStat
|
||||
3, // 6: protobuf.Container.mounts:type_name -> protobuf.Mount
|
||||
20, // 7: protobuf.MountStat.lastChecked:type_name -> google.protobuf.Timestamp
|
||||
21, // 8: protobuf.LogEvent.message:type_name -> google.protobuf.Any
|
||||
20, // 9: protobuf.LogEvent.timestamp:type_name -> google.protobuf.Timestamp
|
||||
|
||||
@@ -2,6 +2,7 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/puzpuzpuz/xsync/v4"
|
||||
@@ -20,9 +21,12 @@ const (
|
||||
volumeQueueSize = 64
|
||||
)
|
||||
|
||||
// volumeTracker is shared between the stat-producing path (observe) and the
|
||||
// refresh worker. Both fields are accessed concurrently and must use atomics.
|
||||
// lastCheckNanos stores time.Time as UnixNano; zero means "never checked".
|
||||
type volumeTracker struct {
|
||||
lastWriteTotal uint64
|
||||
lastCheck time.Time
|
||||
lastWriteTotal atomic.Uint64
|
||||
lastCheckNanos atomic.Int64
|
||||
}
|
||||
|
||||
type volumeMonitor struct {
|
||||
@@ -57,17 +61,20 @@ func (v *volumeMonitor) observe(c *Container, stat ContainerStat) {
|
||||
t, _ := v.trackers.LoadOrCompute(c.ID, func() (*volumeTracker, bool) {
|
||||
// Initialize with the current write total so the first refresh is
|
||||
// driven by the idle timer, not a phantom delta.
|
||||
return &volumeTracker{lastWriteTotal: stat.DiskWriteTotal}, false
|
||||
tr := &volumeTracker{}
|
||||
tr.lastWriteTotal.Store(stat.DiskWriteTotal)
|
||||
return tr, false
|
||||
})
|
||||
|
||||
delta := stat.DiskWriteTotal - t.lastWriteTotal
|
||||
if stat.DiskWriteTotal < t.lastWriteTotal {
|
||||
last := t.lastWriteTotal.Load()
|
||||
delta := stat.DiskWriteTotal - last
|
||||
if stat.DiskWriteTotal < last {
|
||||
// Counter reset (container restarted with same ID? unlikely but defend).
|
||||
delta = stat.DiskWriteTotal
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
idle := t.lastCheck.IsZero() || now.Sub(t.lastCheck) >= idleRefreshInterval
|
||||
lastNanos := t.lastCheckNanos.Load()
|
||||
idle := lastNanos == 0 || time.Since(time.Unix(0, lastNanos)) >= idleRefreshInterval
|
||||
if !idle && delta < writeThresholdBytes {
|
||||
return
|
||||
}
|
||||
@@ -141,7 +148,17 @@ func (v *volumeMonitor) refresh(id string) {
|
||||
if data := c.Stats.Data(); len(data) > 0 {
|
||||
latestWrite = data[len(data)-1].DiskWriteTotal
|
||||
}
|
||||
v.trackers.Store(id, &volumeTracker{lastWriteTotal: latestWrite, lastCheck: time.Now()})
|
||||
// Update in place under the per-key shard lock so concurrent observe()
|
||||
// calls see consistent counters.
|
||||
v.trackers.Compute(id, func(existing *volumeTracker, loaded bool) (*volumeTracker, xsync.ComputeOp) {
|
||||
tr := existing
|
||||
if !loaded || tr == nil {
|
||||
tr = &volumeTracker{}
|
||||
}
|
||||
tr.lastWriteTotal.Store(latestWrite)
|
||||
tr.lastCheckNanos.Store(time.Now().UnixNano())
|
||||
return tr, xsync.UpdateOp
|
||||
})
|
||||
|
||||
v.store.applyMountStats(id, stats)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,13 @@ tooltip:
|
||||
search: Søg containere (⌘ + k, ⌃k)
|
||||
pin-column: Fastgør som kolonne
|
||||
merge-all: Sammenlæg alt i én stream
|
||||
network-io: "Netværk: ↑ {tx}/s · ↓ {rx}/s"
|
||||
disk-io: "Disk: ↑ {write}/s · ↓ {read}/s"
|
||||
cpu-usage: "CPU {cpu}% / {cores} kerner"
|
||||
memory-usage: "Hukommelse {used} / {total}"
|
||||
volumes: Volumener
|
||||
volume-unreachable: ikke tilgængelig fra denne host
|
||||
volume-full: "{destination} er {pct} fuld"
|
||||
error:
|
||||
page-not-found: Denne side eksisterer ikke
|
||||
host-group-not-found: 'Ingen værter fundet i gruppen "{name}"'
|
||||
|
||||
@@ -62,6 +62,13 @@ tooltip:
|
||||
search: Suche Container (⌘ + k, ⌃k)
|
||||
pin-column: Als Spalte anheften
|
||||
merge-all: Alles in einen Stream zusammenführen
|
||||
network-io: "Netzwerk: ↑ {tx}/s · ↓ {rx}/s"
|
||||
disk-io: "Festplatte: ↑ {write}/s · ↓ {read}/s"
|
||||
cpu-usage: "CPU {cpu}% / {cores} Kerne"
|
||||
memory-usage: "Speicher {used} / {total}"
|
||||
volumes: Volumes
|
||||
volume-unreachable: von diesem Host nicht erreichbar
|
||||
volume-full: "{destination} ist zu {pct} voll"
|
||||
error:
|
||||
page-not-found: Diese Seite existiert nicht.
|
||||
host-group-not-found: 'Keine Hosts in der Gruppe "{name}" gefunden'
|
||||
|
||||
@@ -64,6 +64,13 @@ tooltip:
|
||||
search: Search containers (⌘ + k, ⌃k)
|
||||
pin-column: Pin as column
|
||||
merge-all: Merge all into one stream
|
||||
network-io: "Network: ↑ {tx}/s · ↓ {rx}/s"
|
||||
disk-io: "Disk: ↑ {write}/s · ↓ {read}/s"
|
||||
cpu-usage: "CPU {cpu}% / {cores} cores"
|
||||
memory-usage: "Memory {used} / {total}"
|
||||
volumes: Volumes
|
||||
volume-unreachable: not reachable from this host
|
||||
volume-full: "{destination} is {pct} full"
|
||||
error:
|
||||
page-not-found: This page does not exist
|
||||
host-group-not-found: 'No hosts found in group "{name}"'
|
||||
|
||||
@@ -62,6 +62,13 @@ tooltip:
|
||||
search: Buscar contenedores (⌘ + K, CTRL + K)
|
||||
pin-column: Anclar como columna
|
||||
merge-all: Fusionar todo en un flujo
|
||||
network-io: "Red: ↑ {tx}/s · ↓ {rx}/s"
|
||||
disk-io: "Disco: ↑ {write}/s · ↓ {read}/s"
|
||||
cpu-usage: "CPU {cpu}% / {cores} núcleos"
|
||||
memory-usage: "Memoria {used} / {total}"
|
||||
volumes: Volúmenes
|
||||
volume-unreachable: no accesible desde este host
|
||||
volume-full: "{destination} está al {pct}"
|
||||
error:
|
||||
page-not-found: Esta página no existe.
|
||||
host-group-not-found: 'No se encontraron hosts en el grupo "{name}"'
|
||||
|
||||
@@ -62,6 +62,13 @@ tooltip:
|
||||
search: Recherche de conteneurs (⌘ + k, ⌃k)
|
||||
pin-column: Epinglé en colonne
|
||||
merge-all: Fusionner tout dans un flux
|
||||
network-io: "Réseau : ↑ {tx}/s · ↓ {rx}/s"
|
||||
disk-io: "Disque : ↑ {write}/s · ↓ {read}/s"
|
||||
cpu-usage: "CPU {cpu}% / {cores} cœurs"
|
||||
memory-usage: "Mémoire {used} / {total}"
|
||||
volumes: Volumes
|
||||
volume-unreachable: inaccessible depuis cet hôte
|
||||
volume-full: "{destination} est plein à {pct}"
|
||||
error:
|
||||
page-not-found: Cette page n'existe pas
|
||||
host-group-not-found: 'Aucun hôte trouvé dans le groupe « {name} »'
|
||||
|
||||
@@ -64,6 +64,13 @@ tooltip:
|
||||
search: Cari kontainer (⌘ + k, ⌃k)
|
||||
pin-column: Sematkan sebagai kolom
|
||||
merge-all: Gabungkan semua ke dalam satu aliran
|
||||
network-io: "Jaringan: ↑ {tx}/s · ↓ {rx}/s"
|
||||
disk-io: "Disk: ↑ {write}/s · ↓ {read}/s"
|
||||
cpu-usage: "CPU {cpu}% / {cores} core"
|
||||
memory-usage: "Memori {used} / {total}"
|
||||
volumes: Volume
|
||||
volume-unreachable: tidak dapat dijangkau dari host ini
|
||||
volume-full: "{destination} terisi {pct}"
|
||||
|
||||
error:
|
||||
page-not-found: Halaman ini tidak ada
|
||||
|
||||
@@ -62,6 +62,13 @@ tooltip:
|
||||
search: Ricerca container (⌘ + k, ⌃k)
|
||||
pin-column: Blocca come colonna
|
||||
merge-all: Unisci tutto in un flusso
|
||||
network-io: "Rete: ↑ {tx}/s · ↓ {rx}/s"
|
||||
disk-io: "Disco: ↑ {write}/s · ↓ {read}/s"
|
||||
cpu-usage: "CPU {cpu}% / {cores} core"
|
||||
memory-usage: "Memoria {used} / {total}"
|
||||
volumes: Volumi
|
||||
volume-unreachable: non raggiungibile da questo host
|
||||
volume-full: "{destination} è pieno al {pct}"
|
||||
error:
|
||||
page-not-found: Questa pagina non esiste
|
||||
host-group-not-found: 'Nessun host trovato nel gruppo "{name}"'
|
||||
|
||||
@@ -61,6 +61,13 @@ tooltip:
|
||||
search: 컨테이너 검색 (⌘ + k, ⌃k)
|
||||
pin-column: 열로 고정
|
||||
merge-all: 모두 하나의 스트림으로 병합
|
||||
network-io: "네트워크: ↑ {tx}/s · ↓ {rx}/s"
|
||||
disk-io: "디스크: ↑ {write}/s · ↓ {read}/s"
|
||||
cpu-usage: "CPU {cpu}% / {cores} 코어"
|
||||
memory-usage: "메모리 {used} / {total}"
|
||||
volumes: 볼륨
|
||||
volume-unreachable: 이 호스트에서 접근할 수 없음
|
||||
volume-full: "{destination}이(가) {pct} 사용 중"
|
||||
error:
|
||||
page-not-found: 페이지를 찾을 수 없습니다
|
||||
host-group-not-found: '"{name}" 그룹에 호스트가 없습니다'
|
||||
|
||||
@@ -62,6 +62,13 @@ tooltip:
|
||||
search: Zoek containers (⌘ + k, ⌃k)
|
||||
pin-column: Vastzetten als kolom
|
||||
merge-all: Voeg alles samen in één stream
|
||||
network-io: "Netwerk: ↑ {tx}/s · ↓ {rx}/s"
|
||||
disk-io: "Schijf: ↑ {write}/s · ↓ {read}/s"
|
||||
cpu-usage: "CPU {cpu}% / {cores} kernen"
|
||||
memory-usage: "Geheugen {used} / {total}"
|
||||
volumes: Volumes
|
||||
volume-unreachable: niet bereikbaar vanaf deze host
|
||||
volume-full: "{destination} is {pct} vol"
|
||||
error:
|
||||
page-not-found: Deze pagina bestaat niet
|
||||
host-group-not-found: 'Geen hosts gevonden in groep "{name}"'
|
||||
|
||||
@@ -66,6 +66,13 @@ tooltip:
|
||||
search: Przeszukaj kontenery (⌘ + k, ⌃k)
|
||||
pin-column: Przypnij jako kolumna
|
||||
merge-all: Scal wszystko w jeden strumień
|
||||
network-io: "Sieć: ↑ {tx}/s · ↓ {rx}/s"
|
||||
disk-io: "Dysk: ↑ {write}/s · ↓ {read}/s"
|
||||
cpu-usage: "CPU {cpu}% / {cores} rdzeni"
|
||||
memory-usage: "Pamięć {used} / {total}"
|
||||
volumes: Wolumeny
|
||||
volume-unreachable: nieosiągalne z tego hosta
|
||||
volume-full: "{destination} zapełniony w {pct}"
|
||||
error:
|
||||
page-not-found: Ta strona nie istnieje
|
||||
host-group-not-found: 'Nie znaleziono hostów w grupie „{name}”'
|
||||
|
||||
@@ -61,6 +61,13 @@ tooltip:
|
||||
search: Pesquisar containers (⌘ + k, ⌃k)
|
||||
pin-column: Fixar como coluna
|
||||
merge-all: Unir tudo em um fluxo
|
||||
network-io: "Rede: ↑ {tx}/s · ↓ {rx}/s"
|
||||
disk-io: "Disco: ↑ {write}/s · ↓ {read}/s"
|
||||
cpu-usage: "CPU {cpu}% / {cores} núcleos"
|
||||
memory-usage: "Memória {used} / {total}"
|
||||
volumes: Volumes
|
||||
volume-unreachable: inacessível a partir deste host
|
||||
volume-full: "{destination} está {pct} cheio"
|
||||
error:
|
||||
page-not-found: Esta página não existe
|
||||
host-group-not-found: 'Nenhum host encontrado no grupo "{name}"'
|
||||
|
||||
@@ -62,6 +62,13 @@ tooltip:
|
||||
search: Поиск контейнеров (⌘ + k, ⌃k)
|
||||
pin-column: Закрепить столбец
|
||||
merge-all: Объединить все в один поток
|
||||
network-io: "Сеть: ↑ {tx}/s · ↓ {rx}/s"
|
||||
disk-io: "Диск: ↑ {write}/s · ↓ {read}/s"
|
||||
cpu-usage: "ЦП {cpu}% / {cores} ядер"
|
||||
memory-usage: "Память {used} / {total}"
|
||||
volumes: Тома
|
||||
volume-unreachable: недоступно с этого хоста
|
||||
volume-full: "{destination} заполнено на {pct}"
|
||||
error:
|
||||
page-not-found: Эта страница не доступна.
|
||||
host-group-not-found: 'Хосты в группе «{name}» не найдены'
|
||||
|
||||
@@ -61,6 +61,13 @@ tooltip:
|
||||
search: Iskanje zabojnikov (⌘ + k, ⌃k)
|
||||
pin-column: Pripni kot stolpec
|
||||
merge-all: Združi vse v en tok
|
||||
network-io: "Omrežje: ↑ {tx}/s · ↓ {rx}/s"
|
||||
disk-io: "Disk: ↑ {write}/s · ↓ {read}/s"
|
||||
cpu-usage: "CPE {cpu}% / {cores} jeder"
|
||||
memory-usage: "Pomnilnik {used} / {total}"
|
||||
volumes: Nosilci
|
||||
volume-unreachable: nedosegljivo iz tega gostitelja
|
||||
volume-full: "{destination} je {pct} poln"
|
||||
error:
|
||||
page-not-found: Ta stran ne obstaja
|
||||
host-group-not-found: 'V skupini "{name}" ni najdenih gostiteljev'
|
||||
|
||||
@@ -67,6 +67,13 @@ tooltip:
|
||||
search: Konteynerlerde ara (⌘ + k, ⌃k)
|
||||
pin-column: Sütun olarak sabitle
|
||||
merge-all: Tümünü tek bir akışta birleştir
|
||||
network-io: "Ağ: ↑ {tx}/sn · ↓ {rx}/sn"
|
||||
disk-io: "Disk: ↑ {write}/sn · ↓ {read}/sn"
|
||||
cpu-usage: "CPU {cpu}% / {cores} çekirdek"
|
||||
memory-usage: "Bellek {used} / {total}"
|
||||
volumes: Birimler
|
||||
volume-unreachable: bu sunucudan erişilemiyor
|
||||
volume-full: "{destination} {pct} dolu"
|
||||
error:
|
||||
page-not-found: Bu sayfa bulunamadı
|
||||
host-group-not-found: '"{name}" grubunda ana bilgisayar bulunamadı'
|
||||
|
||||
@@ -62,6 +62,13 @@ tooltip:
|
||||
search: 搜尋容器 (⌘ + k, ⌃k)
|
||||
pin-column: 釘選為欄位
|
||||
merge-all: 將所有內容合併到一個流中
|
||||
network-io: "網路:↑ {tx}/s · ↓ {rx}/s"
|
||||
disk-io: "磁碟:↑ {write}/s · ↓ {read}/s"
|
||||
cpu-usage: "CPU {cpu}% / {cores} 核心"
|
||||
memory-usage: "記憶體 {used} / {total}"
|
||||
volumes: 卷
|
||||
volume-unreachable: 無法從此主機存取
|
||||
volume-full: "{destination} 已使用 {pct}"
|
||||
error:
|
||||
page-not-found: 此頁面不存在
|
||||
host-group-not-found: '在群組「{name}」中找不到主機'
|
||||
|
||||
@@ -62,6 +62,13 @@ tooltip:
|
||||
search: 搜索 (⌘ + k, ⌃k)
|
||||
pin-column: 固定为列
|
||||
merge-all: 将所有内容合并到一个流中
|
||||
network-io: "网络:↑ {tx}/s · ↓ {rx}/s"
|
||||
disk-io: "磁盘:↑ {write}/s · ↓ {read}/s"
|
||||
cpu-usage: "CPU {cpu}% / {cores} 核"
|
||||
memory-usage: "内存 {used} / {total}"
|
||||
volumes: 卷
|
||||
volume-unreachable: 无法从此主机访问
|
||||
volume-full: "{destination} 已使用 {pct}"
|
||||
error:
|
||||
page-not-found: 此页面不存在。
|
||||
host-group-not-found: '在组 "{name}" 中未找到主机'
|
||||
|
||||
+5
-1
@@ -29,10 +29,14 @@ message Container {
|
||||
bool fullyLoaded = 19;
|
||||
repeated string env = 20;
|
||||
repeated string ports = 21;
|
||||
repeated Mount mounts = 22;
|
||||
// Field 22 was previously `repeated string mounts` (raw paths). Renumbered
|
||||
// to 26 with a richer Mount message; old tag reserved to avoid wire collisions
|
||||
// with pre-v10.6 agents.
|
||||
reserved 22;
|
||||
string restartPolicy = 23;
|
||||
string networkMode = 24;
|
||||
repeated MountStat mountStats = 25;
|
||||
repeated Mount mounts = 26;
|
||||
}
|
||||
|
||||
message ContainerStat {
|
||||
|
||||
Reference in New Issue
Block a user