1
0
mirror of https://github.com/amir20/dozzle.git synced 2026-01-03 19:45:01 +01:00

feat: enable merging logs for all containers in a host (#3492)

This commit is contained in:
Amir Raminfar
2024-12-31 08:55:48 -08:00
committed by GitHub
parent 08e5deba7e
commit cc76c00933
17 changed files with 129 additions and 15 deletions

View File

@@ -234,6 +234,7 @@ declare global {
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useGroupedStream: typeof import('./composable/eventStreams')['useGroupedStream']
const useHead: typeof import('@vueuse/head')['useHead']
const useHostStream: typeof import('./composable/eventStreams')['useHostStream']
const useHosts: typeof import('./stores/hosts')['useHosts']
const useI18n: typeof import('vue-i18n')['useI18n']
const useId: typeof import('vue')['useId']
@@ -621,6 +622,7 @@ declare module 'vue' {
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useGroupedStream: UnwrapRef<typeof import('./composable/eventStreams')['useGroupedStream']>
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']>
readonly useHostStream: UnwrapRef<typeof import('./composable/eventStreams')['useHostStream']>
readonly useHosts: UnwrapRef<typeof import('./stores/hosts')['useHosts']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useId: UnwrapRef<typeof import('vue')['useId']>

View File

@@ -39,6 +39,7 @@ declare module 'vue' {
GroupedLog: typeof import('./components/GroupedViewer/GroupedLog.vue')['default']
HostIcon: typeof import('./components/common/HostIcon.vue')['default']
HostList: typeof import('./components/HostList.vue')['default']
HostLog: typeof import('./components/HostViewer/HostLog.vue')['default']
HostMenu: typeof import('./components/HostMenu.vue')['default']
'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default']
IndeterminateBar: typeof import('./components/common/IndeterminateBar.vue')['default']
@@ -91,6 +92,7 @@ declare module 'vue' {
'Ph:dotsThreeVerticalBold': typeof import('~icons/ph/dots-three-vertical-bold')['default']
'Ph:fileSql': typeof import('~icons/ph/file-sql')['default']
'Ph:globeSimple': typeof import('~icons/ph/globe-simple')['default']
'Ph:listBulletsFill': typeof import('~icons/ph/list-bullets-fill')['default']
'Ph:memory': typeof import('~icons/ph/memory')['default']
'Ph:stack': typeof import('~icons/ph/stack')['default']
'Ph:stackSimple': typeof import('~icons/ph/stack-simple')['default']

View File

@@ -6,7 +6,18 @@
<a @click.prevent="setHost(null)" class="link-primary">{{ $t("label.hosts") }}</a>
</li>
<li v-if="sessionHost && hosts[sessionHost]" class="cursor-default">
{{ hosts[sessionHost].name }}
<router-link
:to="{
name: '/host/[id]',
params: { id: hosts[sessionHost].id },
}"
class="btn btn-outline btn-primary btn-xs"
active-class="btn-active"
:title="$t('tooltip.merge-hosts')"
>
<ph:arrows-merge />
{{ hosts[sessionHost].name }}
</router-link>
</li>
</ul>
</div>

View File

@@ -0,0 +1,43 @@
<template>
<ScrollableView :scrollable="scrollable" v-if="host">
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="flex flex-1 gap-1.5 truncate md:gap-2">
<ph:computer-tower />
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ host.name }}</div>
</div>
<Tag class="mobile-hidden font-mono" size="small">
{{ $t("label.container", containers.length) }}
</Tag>
</div>
<MultiContainerStat class="ml-auto" :containers="containers" />
<MultiContainerActionToolbar class="mobile-hidden" @clear="viewer?.clear()" />
</div>
</template>
<template #default>
<ViewerWithSource
ref="viewer"
:stream-source="useHostStream"
:entity="host"
:visible-keys="new Map<string[], boolean>()"
/>
</template>
</ScrollableView>
</template>
<script lang="ts" setup>
import ViewerWithSource from "@/components/LogViewer/ViewerWithSource.vue";
import { ComponentExposed } from "vue-component-type-helpers";
const { id, scrollable = false } = defineProps<{
id: string;
scrollable?: boolean;
}>();
const store = useContainerStore();
const { containersByHost } = storeToRefs(store);
const { hosts } = useHosts();
const host = computed(() => hosts.value[id]);
const containers = computed(() => containersByHost.value?.[id].filter((c) => c.state === "running") ?? []);
const viewer = useTemplateRef<ComponentExposed<typeof ViewerWithSource>>("viewer");
provideLoggingContext(containers, { showContainerName: true, showHostname: false });
</script>

View File

@@ -3,6 +3,7 @@
<template #header>
<div class="mx-2 flex items-center gap-2 md:ml-4">
<div class="flex flex-1 gap-1.5 truncate @container md:gap-2">
<ph:stack-simple />
<div class="inline-flex font-mono text-sm">
<div class="font-semibold">{{ service.name }}</div>
</div>

View File

@@ -20,7 +20,6 @@
</template>
<script lang="ts" setup>
import { onBeforeRouteLeave } from "vue-router";
const containerStore = useContainerStore();
const { ready } = storeToRefs(containerStore);
const route = useRoute();
@@ -29,18 +28,16 @@ const { services, customGroups } = storeToRefs(swarmStore);
const showSwarm = useSessionStorage<boolean>("DOZZLE_SWARM_MODE", false);
if (route.meta.swarmMode) {
showSwarm.value = true;
} else if (route.meta.containerMode) {
showSwarm.value = false;
}
onBeforeRouteLeave((to) => {
if (to.meta.swarmMode) {
showSwarm.value = true;
} else if (to.meta.containerMode) {
showSwarm.value = false;
}
});
watch(
route,
() => {
if (route.meta.swarmMode) {
showSwarm.value = true;
} else if (route.meta.containerMode) {
showSwarm.value = false;
}
},
{ immediate: true },
);
</script>
<style scoped lang="postcss"></style>

View File

@@ -26,6 +26,10 @@ export function useContainerStream(container: Ref<Container>): LogStreamSource {
return useLogStream(url, loadMoreUrl);
}
export function useHostStream(host: Ref<Host>): LogStreamSource {
return useLogStream(computed(() => `/api/hosts/${host.value.id}/logs/stream`));
}
export function useStackStream(stack: Ref<Stack>): LogStreamSource {
return useLogStream(computed(() => `/api/stacks/${stack.value.name}/logs/stream`));
}

View File

@@ -0,0 +1,30 @@
<template>
<Search />
<HostLog :id="route.params.id" :scrollable="pinnedLogs.length > 0" />
</template>
<script lang="ts" setup>
const route = useRoute("/host/[id]");
const containerStore = useContainerStore();
const { ready } = storeToRefs(containerStore);
const pinnedLogsStore = usePinnedLogsStore();
const { pinnedLogs } = storeToRefs(pinnedLogsStore);
const { hosts } = useHosts();
const host = computed(() => hosts.value[route.params.id]);
watchEffect(() => {
if (ready.value) {
if (host.value?.name) {
setTitle(host.value.name);
} else {
setTitle("Not Found");
}
}
});
</script>
<route lang="yaml">
meta:
containerMode: true
</route>

View File

@@ -161,9 +161,23 @@ export const useContainerStore = defineStore("container", () => {
const findContainerById = (id: string) => allContainersById.value[id];
const containersByHost = computed(() =>
containers.value.reduce(
(acc, container) => {
if (!acc[container.host]) {
acc[container.host] = [];
}
acc[container.host].push(container);
return acc;
},
{} as Record<string, Container[]>,
),
);
return {
containers,
allContainersById,
containersByHost,
visibleContainers,
currentContainer,
findContainerById,

View File

@@ -22,6 +22,7 @@ declare module 'vue-router/auto-routes' {
'/[...all]': RouteRecordInfo<'/[...all]', '/:all(.*)', { all: ParamValue<true> }, { all: ParamValue<false> }>,
'/container/[id]': RouteRecordInfo<'/container/[id]', '/container/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
'/group/[name]': RouteRecordInfo<'/group/[name]', '/group/:name', { name: ParamValue<true> }, { name: ParamValue<false> }>,
'/host/[id]': RouteRecordInfo<'/host/[id]', '/host/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
'/login': RouteRecordInfo<'/login', '/login', Record<never, never>, Record<never, never>>,
'/merged/[ids]': RouteRecordInfo<'/merged/[ids]', '/merged/:ids', { ids: ParamValue<true> }, { ids: ParamValue<false> }>,
'/service/[name]': RouteRecordInfo<'/service/[name]', '/service/:name', { name: ParamValue<true> }, { name: ParamValue<false> }>,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -220,6 +220,13 @@ func (h *handler) streamStackLogs(w http.ResponseWriter, r *http.Request) {
})
}
func (h *handler) streamHostLogs(w http.ResponseWriter, r *http.Request) {
host := hostKey(r)
h.streamLogsForContainers(w, r, func(container *docker.Container) bool {
return container.State == "running" && container.Host == host
})
}
func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request, containerFilter ContainerFilter) {
var stdTypes docker.StdType
if r.URL.Query().Has("stdout") {

View File

@@ -94,6 +94,7 @@ func createRouter(h *handler) *chi.Mux {
r.Use(auth.RequireAuthentication)
}
r.Get("/hosts/{host}/containers/{id}/logs/stream", h.streamContainerLogs)
r.Get("/hosts/{host}/logs/stream", h.streamHostLogs)
r.Get("/hosts/{host}/containers/{id}/logs", h.fetchLogsBetweenDates)
r.Get("/hosts/{host}/logs/mergedStream/{ids}", h.streamLogsMerged)
r.Get("/containers/{hostIds}/download", h.downloadLogs) // formatted as host:container,host:container

View File

@@ -34,6 +34,7 @@ tooltip:
pin-column: Pin as column
merge-services: Merge all services into one view
merge-containers: Merge all containers into one view
merge-hosts: Merge all containers on this host into one view
error:
page-not-found: This page does not exist
invalid-auth: Username or password are not valid