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

feat: adds support for different std out and err streams (#2229)

* feat: adds support for different std out and err streams

* feat: adds std to json

* fixes tests

* fixes deprecated code

* fixes download

* adds defineEmit as an option

* chore: updates modules

* adds ui elements

* fixes tests

* updates languages
This commit is contained in:
Amir Raminfar
2023-05-31 15:19:40 -07:00
committed by GitHub
parent 84b8e24ca3
commit 5f92e84d9d
41 changed files with 1061 additions and 383 deletions

View File

@@ -93,7 +93,7 @@ declare global {
const onUnmounted: typeof import('vue')['onUnmounted'] const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated'] const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const persistentVisibleKeys: typeof import('./utils/index')['persistentVisibleKeys'] const persistentVisibleKeys: typeof import('./composables/storage')['persistentVisibleKeys']
const provide: typeof import('vue')['provide'] const provide: typeof import('vue')['provide']
const reactify: typeof import('@vueuse/core')['reactify'] const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
@@ -121,6 +121,7 @@ declare global {
const shallowReadonly: typeof import('vue')['shallowReadonly'] const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef'] const shallowRef: typeof import('vue')['shallowRef']
const showAllContainers: typeof import('./composables/settings')['showAllContainers'] const showAllContainers: typeof import('./composables/settings')['showAllContainers']
const showStd: typeof import('./composables/settings')['showStd']
const showTimestamp: typeof import('./composables/settings')['showTimestamp'] const showTimestamp: typeof import('./composables/settings')['showTimestamp']
const size: typeof import('./composables/settings')['size'] const size: typeof import('./composables/settings')['size']
const smallerScrollbars: typeof import('./composables/settings')['smallerScrollbars'] const smallerScrollbars: typeof import('./composables/settings')['smallerScrollbars']
@@ -428,7 +429,7 @@ declare module 'vue' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']> readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']> readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']> readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./utils/index')['persistentVisibleKeys']> readonly persistentVisibleKeys: UnwrapRef<typeof import('./composables/storage')['persistentVisibleKeys']>
readonly provide: UnwrapRef<typeof import('vue')['provide']> readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']> readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']> readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
@@ -456,6 +457,7 @@ declare module 'vue' {
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']> readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']> readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']> readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']>
readonly showStd: UnwrapRef<typeof import('./composables/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']> readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./composables/settings')['size']> readonly size: UnwrapRef<typeof import('./composables/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']> readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']>
@@ -757,7 +759,7 @@ declare module '@vue/runtime-core' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']> readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']> readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']> readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistentVisibleKeys: UnwrapRef<typeof import('./utils/index')['persistentVisibleKeys']> readonly persistentVisibleKeys: UnwrapRef<typeof import('./composables/storage')['persistentVisibleKeys']>
readonly provide: UnwrapRef<typeof import('vue')['provide']> readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']> readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']> readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
@@ -785,6 +787,7 @@ declare module '@vue/runtime-core' {
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']> readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']> readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']> readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']>
readonly showStd: UnwrapRef<typeof import('./composables/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']> readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./composables/settings')['size']> readonly size: UnwrapRef<typeof import('./composables/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']> readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']>

View File

@@ -32,8 +32,10 @@ declare module '@vue/runtime-core' {
LogDate: typeof import('./components/LogViewer/LogDate.vue')['default'] LogDate: typeof import('./components/LogViewer/LogDate.vue')['default']
LogEventSource: typeof import('./components/LogViewer/LogEventSource.vue')['default'] LogEventSource: typeof import('./components/LogViewer/LogEventSource.vue')['default']
LogLevel: typeof import('./components/LogViewer/LogLevel.vue')['default'] LogLevel: typeof import('./components/LogViewer/LogLevel.vue')['default']
LogStd: typeof import('./components/LogViewer/LogStd.vue')['default']
LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default'] LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default']
LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default'] LogViewerWithSource: typeof import('./components/LogViewer/LogViewerWithSource.vue')['default']
'Mdi:check': typeof import('~icons/mdi/check')['default']
'Mdi:dotsVertical': typeof import('~icons/mdi/dots-vertical')['default'] 'Mdi:dotsVertical': typeof import('~icons/mdi/dots-vertical')['default']
'Mdi:lightChevronDoubleDown': typeof import('~icons/mdi-light/chevron-double-down')['default'] 'Mdi:lightChevronDoubleDown': typeof import('~icons/mdi-light/chevron-double-down')['default']
'Mdi:lightChevronLeft': typeof import('~icons/mdi-light/chevron-left')['default'] 'Mdi:lightChevronLeft': typeof import('~icons/mdi-light/chevron-left')['default']
@@ -56,6 +58,7 @@ declare module '@vue/runtime-core' {
SkippedEntriesLogItem: typeof import('./components/LogViewer/SkippedEntriesLogItem.vue')['default'] SkippedEntriesLogItem: typeof import('./components/LogViewer/SkippedEntriesLogItem.vue')['default']
StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default'] StatMonitor: typeof import('./components/LogViewer/StatMonitor.vue')['default']
StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default'] StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default']
Tag: typeof import('./components/Tag.vue')['default']
ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default'] ZigZag: typeof import('./components/LogViewer/ZigZag.vue')['default']
} }
} }

View File

@@ -1,5 +1,8 @@
<template> <template>
<div class="columns is-1 is-variable is-mobile"> <div class="columns is-1 is-variable is-mobile">
<div class="column is-narrow" v-if="showStd">
<log-std :std="logEntry.std"></log-std>
</div>
<div class="column is-narrow" v-if="showTimestamp"> <div class="column is-narrow" v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date> <log-date :date="logEntry.date"></log-date>
</div> </div>

View File

@@ -5,7 +5,7 @@
</div> </div>
<div class="column is-ellipsis"> <div class="column is-ellipsis">
{{ container.name }}<span v-if="container.isSwarm">{{ container.swarmId }}</span> {{ container.name }}<span v-if="container.isSwarm">{{ container.swarmId }}</span>
<span class="tag is-dark is-hidden-mobile">{{ container.image.replace(/@sha.*/, "") }}</span> <tag class="is-hidden-mobile">{{ container.image.replace(/@sha.*/, "") }}</tag>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,6 +1,6 @@
<template> <template>
<dropdown-menu class="is-right"> <dropdown-menu class="is-right">
<a class="dropdown-item" @click="onClearClicked"> <a class="dropdown-item" @click="clear()">
<div class="level is-justify-content-start"> <div class="level is-justify-content-start">
<div class="level-left"> <div class="level-left">
<div class="level-item"> <div class="level-item">
@@ -37,6 +37,34 @@
</div> </div>
</div> </div>
</a> </a>
<hr class="dropdown-divider" />
<a class="dropdown-item" @click="streamConfig.stdout = !streamConfig.stdout">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<mdi:check class="mr-4 is-blue" v-if="streamConfig.stdout" />
</div>
</div>
<div class="level-right">
{{ $t(streamConfig.stdout ? "toolbar.hide" : "toolbar.show", { std: "STDOUT" }) }}
</div>
</div>
</a>
<a class="dropdown-item" @click="streamConfig.stderr = !streamConfig.stderr">
<div class="level is-justify-content-start">
<div class="level-left">
<div class="level-item">
<mdi:check class="mr-4 is-red" v-if="streamConfig.stderr" />
</div>
</div>
<div class="level-right">
<div class="level-item">
{{ $t(streamConfig.stderr ? "toolbar.hide" : "toolbar.show", { std: "STDERR" }) }}
</div>
</div>
</div>
</a>
</dropdown-menu> </dropdown-menu>
</template> </template>
@@ -47,23 +75,14 @@ import { Container } from "@/models/Container";
const { showSearch } = useSearchFilter(); const { showSearch } = useSearchFilter();
const { base } = config; const { base } = config;
const { onClearClicked = (e: Event) => {} } = defineProps<{ const clear = defineEmit();
onClearClicked: (e: Event) => void;
}>();
const container = inject("container") as ComputedRef<Container>; const container = inject("container") as ComputedRef<Container>;
const streamConfig = inject("stream-config") as { stdout: boolean; stderr: boolean };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
#download.button, .level-left .level-item {
#clear.button { width: 2.2em;
.icon {
height: 80%;
}
&:hover {
color: var(--primary-color);
border-color: var(--primary-color);
}
} }
</style> </style>

View File

@@ -10,7 +10,7 @@
</div> </div>
<div class="mr-2 column is-narrow is-paddingless is-hidden-mobile"> <div class="mr-2 column is-narrow is-paddingless is-hidden-mobile">
<log-actions-toolbar :onClearClicked="onClearClicked" /> <log-actions-toolbar @clear="onClearClicked()" />
</div> </div>
<div class="mr-2 column is-narrow is-paddingless" v-if="closable"> <div class="mr-2 column is-narrow is-paddingless" v-if="closable">
<button class="delete is-medium" @click="emit('close')"></button> <button class="delete is-medium" @click="emit('close')"></button>
@@ -45,8 +45,10 @@ const emit = defineEmits<{
const store = useContainerStore(); const store = useContainerStore();
const container = store.currentContainer($$(id)); const container = store.currentContainer($$(id));
const config = reactive({ stdout: true, stderr: true });
provide("container", container); provide("container", container);
provide("stream-config", config);
const viewer = ref<InstanceType<typeof LogViewerWithSource>>(); const viewer = ref<InstanceType<typeof LogViewerWithSource>>();

View File

@@ -72,6 +72,7 @@ describe("<LogEventSource />", () => {
}, },
provide: { provide: {
container: computed(() => ({ id: "abc", image: "test:v123" })), container: computed(() => ({ id: "abc", image: "test:v123" })),
"stream-config": reactive({ stdout: true, stderr: true }),
scrollingPaused: computed(() => false), scrollingPaused: computed(() => false),
}, },
}, },
@@ -84,6 +85,8 @@ describe("<LogEventSource />", () => {
}); });
} }
const sourceUrl = "/api/logs/stream?id=abc&lastEventId=&host=localhost&stdout=1&stderr=1";
test("renders correctly", async () => { test("renders correctly", async () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
expect(wrapper.html()).toMatchSnapshot(); expect(wrapper.html()).toMatchSnapshot();
@@ -91,22 +94,22 @@ describe("<LogEventSource />", () => {
test("should connect to EventSource", async () => { test("should connect to EventSource", async () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); sources[sourceUrl].emitOpen();
expect(sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].readyState).toBe(1); expect(sources[sourceUrl].readyState).toBe(1);
wrapper.unmount(); wrapper.unmount();
}); });
test("should close EventSource", async () => { test("should close EventSource", async () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); sources[sourceUrl].emitOpen();
wrapper.unmount(); wrapper.unmount();
expect(sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].readyState).toBe(2); expect(sources[sourceUrl].readyState).toBe(2);
}); });
test("should parse messages", async () => { test("should parse messages", async () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); sources[sourceUrl].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`, data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
}); });
@@ -121,8 +124,8 @@ describe("<LogEventSource />", () => {
describe("render html correctly", () => { describe("render html correctly", () => {
test("should render messages", async () => { test("should render messages", async () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); sources[sourceUrl].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`, data: `{"ts":1560336942459, "m":"This is a message.", "id":1}`,
}); });
@@ -134,8 +137,8 @@ describe("<LogEventSource />", () => {
test("should render messages with color", async () => { test("should render messages with color", async () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); sources[sourceUrl].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ sources[sourceUrl].emitMessage({
data: '{"ts":1560336942459,"m":"\\u001b[30mblack\\u001b[37mwhite", "id":1}', data: '{"ts":1560336942459,"m":"\\u001b[30mblack\\u001b[37mwhite", "id":1}',
}); });
@@ -147,8 +150,8 @@ describe("<LogEventSource />", () => {
test("should render messages with html entities", async () => { test("should render messages with html entities", async () => {
const wrapper = createLogEventSource(); const wrapper = createLogEventSource();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); sources[sourceUrl].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`, data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
}); });
@@ -160,8 +163,8 @@ describe("<LogEventSource />", () => {
test("should render dates with 12 hour style", async () => { test("should render dates with 12 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "12" }); const wrapper = createLogEventSource({ hourStyle: "12" });
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); sources[sourceUrl].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`, data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
}); });
@@ -173,8 +176,8 @@ describe("<LogEventSource />", () => {
test("should render dates with 24 hour style", async () => { test("should render dates with 24 hour style", async () => {
const wrapper = createLogEventSource({ hourStyle: "24" }); const wrapper = createLogEventSource({ hourStyle: "24" });
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); sources[sourceUrl].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`, data: `{"ts":1560336942459, "m":"<test>foo bar</test>", "id":1}`,
}); });
@@ -186,11 +189,11 @@ describe("<LogEventSource />", () => {
test("should render messages with filter", async () => { test("should render messages with filter", async () => {
const wrapper = createLogEventSource({ searchFilter: "test" }); const wrapper = createLogEventSource({ searchFilter: "test" });
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitOpen(); sources[sourceUrl].emitOpen();
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"foo bar", "id":1}`, data: `{"ts":1560336942459, "m":"foo bar", "id":1}`,
}); });
sources["/api/logs/stream?id=abc&lastEventId=&host=localhost"].emitMessage({ sources[sourceUrl].emitMessage({
data: `{"ts":1560336942459, "m":"test bar", "id":2}`, data: `{"ts":1560336942459, "m":"test bar", "id":2}`,
}); });

View File

@@ -12,7 +12,8 @@ const emit = defineEmits<{
}>(); }>();
const container = inject("container") as ComputedRef<Container>; const container = inject("container") as ComputedRef<Container>;
const { messages, loadOlderLogs } = useLogStream(container); const config = inject("stream-config") as { stdout: boolean; stderr: boolean };
const { messages, loadOlderLogs } = useLogStream(container, config);
const beforeLoading = () => emit("loading-more", true); const beforeLoading = () => emit("loading-more", true);
const afterLoading = () => emit("loading-more", false); const afterLoading = () => emit("loading-more", false);

View File

@@ -0,0 +1,25 @@
<template>
<tag size="small" :std="std">
{{ std }}
</tag>
</template>
<script lang="ts" setup>
import { Std } from "@/models/LogEntry";
defineProps<{
std: Std;
}>();
</script>
<style lang="scss" scoped>
.tag {
&[std="stdout"] {
color: var(--blue-color);
}
&[std="stderr"] {
color: var(--red-color);
}
}
</style>

View File

@@ -1,5 +1,8 @@
<template> <template>
<div class="columns is-1 is-variable is-mobile"> <div class="columns is-1 is-variable is-mobile">
<div class="column is-narrow" v-if="showStd">
<log-std :std="logEntry.std"></log-std>
</div>
<div class="column is-narrow" v-if="showTimestamp"> <div class="column is-narrow" v-if="showTimestamp">
<log-date :date="logEntry.date"></log-date> <log-date :date="logEntry.date"></log-date>
</div> </div>

View File

@@ -24,6 +24,7 @@ exports[`<LogEventSource /> > render html correctly > should render dates with 1
</div> </div>
</div> </div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\"> <div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\"> <div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div> <div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div> </div>
@@ -60,6 +61,7 @@ exports[`<LogEventSource /> > render html correctly > should render dates with 2
</div> </div>
</div> </div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\"> <div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\"> <div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42</time></div> <div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42</time></div>
</div> </div>
@@ -96,6 +98,7 @@ exports[`<LogEventSource /> > render html correctly > should render messages 1`]
</div> </div>
</div> </div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\"> <div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\"> <div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div> <div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div> </div>
@@ -132,6 +135,7 @@ exports[`<LogEventSource /> > render html correctly > should render messages wit
</div> </div>
</div> </div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\"> <div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\"> <div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div> <div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div> </div>
@@ -168,6 +172,7 @@ exports[`<LogEventSource /> > render html correctly > should render messages wit
</div> </div>
</div> </div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\"> <div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\"> <div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div> <div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div> </div>
@@ -204,6 +209,7 @@ exports[`<LogEventSource /> > render html correctly > should render messages wit
</div> </div>
</div> </div>
<div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\"> <div data-v-a49e52d4=\\"\\" data-v-2e92daca=\\"\\" class=\\"columns is-1 is-variable is-mobile\\" visible-keys=\\"\\">
<!--v-if-->
<div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\"> <div data-v-a49e52d4=\\"\\" class=\\"column is-narrow\\">
<div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div> <div data-v-de513450=\\"\\" data-v-a49e52d4=\\"\\" class=\\"date\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" class=\\"is-hidden-mobile\\">06/12/2019</time> <time datetime=\\"2019-06-12T10:55:42.459Z\\">10:55:42 AM</time></div>
</div> </div>
@@ -235,5 +241,6 @@ SimpleLogEntry {
"id": 1, "id": 1,
"level": undefined, "level": undefined,
"position": undefined, "position": undefined,
"std": "stderr",
} }
`; `;

19
assets/components/Tag.vue Normal file
View File

@@ -0,0 +1,19 @@
<template>
<div class="tag" :size="size">
<slot></slot>
</div>
</template>
<script lang="ts" setup>
const { size = undefined } = defineProps<{ size?: "small" | undefined }>();
</script>
<style scoped lang="scss">
.tag {
background-color: var(--scheme-main-ter);
border: 1px solid var(--border-color);
&[size="small"] {
font-size: 0.61rem;
}
}
</style>

View File

@@ -21,7 +21,12 @@ function parseMessage(data: string): LogEntry<string | JSONObject> {
return asLogEntry(e); return asLogEntry(e);
} }
export function useLogStream(container: ComputedRef<Container>) { type LogStreamConfig = {
stdout: boolean;
stderr: boolean;
};
export function useLogStream(container: ComputedRef<Container>, streamConfig: LogStreamConfig) {
let messages: LogEntry<string | JSONObject>[] = $ref([]); let messages: LogEntry<string | JSONObject>[] = $ref([]);
let buffer: LogEntry<string | JSONObject>[] = $ref([]); let buffer: LogEntry<string | JSONObject>[] = $ref([]);
const scrollingPaused = $ref(inject("scrollingPaused") as Ref<boolean>); const scrollingPaused = $ref(inject("scrollingPaused") as Ref<boolean>);
@@ -64,9 +69,20 @@ export function useLogStream(container: ComputedRef<Container>) {
lastEventId = ""; lastEventId = "";
} }
es = new EventSource( const params = {
`${config.base}/api/logs/stream?id=${container.value.id}&lastEventId=${lastEventId}&host=${sessionHost.value}` id: container.value.id,
); lastEventId,
host: sessionHost.value,
} as { id: string; lastEventId: string; host: string; stdout?: string; stderr?: string };
if (streamConfig.stdout) {
params.stdout = "1";
}
if (streamConfig.stderr) {
params.stderr = "1";
}
es = new EventSource(`${config.base}/api/logs/stream?${new URLSearchParams(params).toString()}`);
es.addEventListener("container-stopped", () => { es.addEventListener("container-stopped", () => {
es?.close(); es?.close();
es = null; es = null;
@@ -93,9 +109,22 @@ export function useLogStream(container: ComputedRef<Container>) {
const last = messages[299].date; const last = messages[299].date;
const delta = to.getTime() - last.getTime(); const delta = to.getTime() - last.getTime();
const from = new Date(to.getTime() + delta); const from = new Date(to.getTime() + delta);
const logs = await (
await fetch(`${config.base}/api/logs?id=${container.value.id}&from=${from.toISOString()}&to=${to.toISOString()}`) const params = {
).text(); id: container.value.id,
from: from.toISOString(),
to: to.toISOString(),
host: sessionHost.value,
} as { id: string; from: string; to: string; host: string; stdout?: string; stderr?: string };
if (streamConfig.stdout) {
params.stdout = "1";
}
if (streamConfig.stderr) {
params.stderr = "1";
}
const logs = await (await fetch(`${config.base}/api/logs?${new URLSearchParams(params).toString()}`)).text();
if (logs) { if (logs) {
const newMessages = logs const newMessages = logs
.trim() .trim()
@@ -129,5 +158,7 @@ export function useLogStream(container: ComputedRef<Container>) {
{ immediate: true } { immediate: true }
); );
watch(streamConfig, () => connect());
return { ...$$({ messages }), loadOlderLogs }; return { ...$$({ messages }), loadOlderLogs };
} }

View File

@@ -6,6 +6,7 @@ export const DEFAULT_SETTINGS: {
menuWidth: number; menuWidth: number;
smallerScrollbars: boolean; smallerScrollbars: boolean;
showTimestamp: boolean; showTimestamp: boolean;
showStd: boolean;
showAllContainers: boolean; showAllContainers: boolean;
lightTheme: "auto" | "dark" | "light"; lightTheme: "auto" | "dark" | "light";
hourStyle: "auto" | "24" | "12"; hourStyle: "auto" | "24" | "12";
@@ -17,6 +18,7 @@ export const DEFAULT_SETTINGS: {
menuWidth: 15, menuWidth: 15,
smallerScrollbars: false, smallerScrollbars: false,
showTimestamp: true, showTimestamp: true,
showStd: false,
showAllContainers: false, showAllContainers: false,
lightTheme: "auto", lightTheme: "auto",
hourStyle: "auto", hourStyle: "auto",
@@ -51,6 +53,11 @@ const showTimestamp = computed({
set: (value) => (settings.value.showTimestamp = value), set: (value) => (settings.value.showTimestamp = value),
}); });
const showStd = computed({
get: () => settings.value.showStd,
set: (value) => (settings.value.showStd = value),
});
const showAllContainers = computed({ const showAllContainers = computed({
get: () => settings.value.showAllContainers, get: () => settings.value.showAllContainers,
set: (value) => (settings.value.showAllContainers = value), set: (value) => (settings.value.showAllContainers = value),
@@ -83,6 +90,7 @@ export {
lightTheme, lightTheme,
showAllContainers, showAllContainers,
showTimestamp, showTimestamp,
showStd,
smallerScrollbars, smallerScrollbars,
menuWidth, menuWidth,
size, size,

View File

@@ -1,3 +1,9 @@
import { Container } from "@/models/Container";
const sessionHost = useSessionStorage("host", "localhost"); const sessionHost = useSessionStorage("host", "localhost");
export { sessionHost }; function persistentVisibleKeys(container: ComputedRef<Container>) {
return computed(() => useStorage(stripVersion(container.value.image) + ":" + container.value.command, []));
}
export { sessionHost, persistentVisibleKeys };

View File

@@ -12,17 +12,25 @@ export interface HasComponent {
export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>; export type JSONValue = string | number | boolean | JSONObject | Array<JSONValue>;
export type JSONObject = { [x: string]: JSONValue }; export type JSONObject = { [x: string]: JSONValue };
export type Position = "start" | "end" | "middle" | undefined; export type Position = "start" | "end" | "middle" | undefined;
export type Std = "stdout" | "stderr";
export interface LogEvent { export interface LogEvent {
readonly m: string | JSONObject; readonly m: string | JSONObject;
readonly ts: number; readonly ts: number;
readonly id: number; readonly id: number;
readonly l: string; readonly l: string;
readonly p: Position; readonly p: Position;
readonly s: number;
} }
export abstract class LogEntry<T extends string | JSONObject> implements HasComponent { export abstract class LogEntry<T extends string | JSONObject> implements HasComponent {
protected readonly _message: T; protected readonly _message: T;
constructor(message: T, public readonly id: number, public readonly date: Date, public readonly level?: string) { constructor(
message: T,
public readonly id: number,
public readonly date: Date,
public readonly std: Std,
public readonly level?: string
) {
this._message = message; this._message = message;
} }
@@ -39,9 +47,10 @@ export class SimpleLogEntry extends LogEntry<string> {
id: number, id: number,
date: Date, date: Date,
public readonly level: string, public readonly level: string,
public readonly position: Position public readonly position: Position,
public readonly std: Std
) { ) {
super(message, id, date, level); super(message, id, date, std, level);
} }
getComponent(): Component { getComponent(): Component {
return SimpleLogItem; return SimpleLogItem;
@@ -56,9 +65,10 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
id: number, id: number,
date: Date, date: Date,
public readonly level: string, public readonly level: string,
public readonly std: Std,
visibleKeys?: Ref<string[][]> visibleKeys?: Ref<string[][]>
) { ) {
super(message, id, date, level); super(message, id, date, std, level);
if (visibleKeys) { if (visibleKeys) {
this.filteredMessage = computed(() => { this.filteredMessage = computed(() => {
if (!visibleKeys.value.length) { if (!visibleKeys.value.length) {
@@ -84,13 +94,13 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
} }
static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<string[][]>): ComplexLogEntry { static fromLogEvent(event: ComplexLogEntry, visibleKeys: Ref<string[][]>): ComplexLogEntry {
return new ComplexLogEntry(event._message, event.id, event.date, event.level, visibleKeys); return new ComplexLogEntry(event._message, event.id, event.date, event.level, event.std, visibleKeys);
} }
} }
export class DockerEventLogEntry extends LogEntry<string> { export class DockerEventLogEntry extends LogEntry<string> {
constructor(message: string, date: Date, public readonly event: string) { constructor(message: string, date: Date, public readonly event: string) {
super(message, date.getTime(), date, "info"); super(message, date.getTime(), date, "stderr", "info");
} }
getComponent(): Component { getComponent(): Component {
return DockerEventLogItem; return DockerEventLogItem;
@@ -107,7 +117,7 @@ export class SkippedLogsEntry extends LogEntry<string> {
public readonly firstSkipped: LogEntry<string | JSONObject>, public readonly firstSkipped: LogEntry<string | JSONObject>,
lastSkipped: LogEntry<string | JSONObject> lastSkipped: LogEntry<string | JSONObject>
) { ) {
super("", date.getTime(), date, "info"); super("", date.getTime(), date, "stderr", "info");
this._totalSkipped = totalSkipped; this._totalSkipped = totalSkipped;
this.lastSkipped = lastSkipped; this.lastSkipped = lastSkipped;
} }
@@ -135,8 +145,15 @@ export class SkippedLogsEntry extends LogEntry<string> {
export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> { export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
if (typeof event.m === "string") { if (typeof event.m === "string") {
return new SimpleLogEntry(event.m, event.id, new Date(event.ts), event.l, event.p); return new SimpleLogEntry(
event.m,
event.id,
new Date(event.ts),
event.l,
event.p,
event.s === 1 ? "stdout" : "stderr"
);
} else { } else {
return new ComplexLogEntry(event.m, event.id, new Date(event.ts), event.l); return new ComplexLogEntry(event.m, event.id, new Date(event.ts), event.l, event.s === 1 ? "stdout" : "stderr");
} }
} }

View File

@@ -25,6 +25,9 @@
<div class="item"> <div class="item">
<o-switch v-model="showTimestamp"> {{ $t("settings.show-timesamps") }} </o-switch> <o-switch v-model="showTimestamp"> {{ $t("settings.show-timesamps") }} </o-switch>
</div> </div>
<div class="item">
<o-switch v-model="showStd"> {{ $t("settings.show-std") }} </o-switch>
</div>
<div class="item"> <div class="item">
<o-switch v-model="softWrap"> {{ $t("settings.soft-wrap") }}</o-switch> <o-switch v-model="softWrap"> {{ $t("settings.soft-wrap") }}</o-switch>
@@ -136,6 +139,7 @@ import {
lightTheme, lightTheme,
smallerScrollbars, smallerScrollbars,
showTimestamp, showTimestamp,
showStd,
hourStyle, hourStyle,
showAllContainers, showAllContainers,
size, size,
@@ -155,7 +159,8 @@ async function fetchNextRelease() {
const response = await fetch("https://api.github.com/repos/amir20/dozzle/releases/latest"); const response = await fetch("https://api.github.com/repos/amir20/dozzle/releases/latest");
if (response.ok) { if (response.ok) {
const release = await response.json(); const release = await response.json();
hasUpdate = release.tag_name.slice(1).localeCompare(currentVersion, undefined, { numeric: true, sensitivity: 'base' }) > 0; hasUpdate =
release.tag_name.slice(1).localeCompare(currentVersion, undefined, { numeric: true, sensitivity: "base" }) > 0;
nextRelease = release; nextRelease = release;
} }
} else { } else {

View File

@@ -96,13 +96,6 @@ $light-toolbar-color: rgba($grey-darker, 0.7);
--text-light-color: #{$grey}; --text-light-color: #{$grey};
} }
:root {
--green-color: #00b5ad;
--red-color: #f44336;
--purple-color: #9c27b0;
--orange-color: #ff9800;
}
[data-theme="dark"] { [data-theme="dark"] {
@include dark; @include dark;
} }
@@ -123,6 +116,34 @@ $light-toolbar-color: rgba($grey-darker, 0.7);
} }
} }
:root {
--green-color: #00b5ad;
--red-color: #f44336;
--purple-color: #9c27b0;
--orange-color: #ff9800;
--blue-color: #2196f3;
}
.is-red {
color: var(--red-color);
}
.is-green {
color: var(--green-color);
}
.is-purple {
color: var(--purple-color);
}
.is-orange {
color: var(--orange-color);
}
.is-blue {
color: var(--blue-color);
}
html { html {
overflow-x: unset; overflow-x: unset;
overflow-y: unset; overflow-y: unset;

View File

@@ -1,7 +1,3 @@
import { Container } from "@/models/Container";
import { useStorage } from "@vueuse/core";
import { computed, ComputedRef } from "vue";
export function formatBytes(bytes: number, decimals = 2) { export function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) return "0 Bytes"; if (bytes === 0) return "0 Bytes";
const k = 1024; const k = 1024;
@@ -37,10 +33,6 @@ export function arrayEquals(a: string[], b: string[]): boolean {
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((val, index) => val === b[index]); return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((val, index) => val === b[index]);
} }
export function persistentVisibleKeys(container: ComputedRef<Container>) {
return computed(() => useStorage(stripVersion(container.value.image) + ":" + container.value.command, []));
}
export function stripVersion(label: string) { export function stripVersion(label: string) {
const [name, _] = label.split(":"); const [name, _] = label.split(":");
return name; return name;

View File

@@ -27,6 +27,27 @@ type dockerClient struct {
filters filters.Args filters filters.Args
} }
type StdType int
const (
STDOUT StdType = 1 << iota
STDERR
)
const STDALL = STDOUT | STDERR
func (s StdType) String() string {
switch s {
case STDOUT:
return "out"
case STDERR:
return "err"
case STDALL:
return "all"
default:
return ""
}
}
type dockerProxy interface { type dockerProxy interface {
ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error) ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error)
ContainerLogs(context.Context, string, types.ContainerLogsOptions) (io.ReadCloser, error) ContainerLogs(context.Context, string, types.ContainerLogsOptions) (io.ReadCloser, error)
@@ -40,9 +61,10 @@ type dockerProxy interface {
type Client interface { type Client interface {
ListContainers() ([]Container, error) ListContainers() ([]Container, error)
FindContainer(string) (Container, error) FindContainer(string) (Container, error)
ContainerLogs(context.Context, string, string) (io.ReadCloser, error) ContainerLogs(context.Context, string, string, StdType) (io.ReadCloser, error)
ContainerLogReader(context.Context, string) (io.ReadCloser, error)
Events(context.Context) (<-chan ContainerEvent, <-chan error) Events(context.Context) (<-chan ContainerEvent, <-chan error)
ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time) (io.ReadCloser, error) ContainerLogsBetweenDates(context.Context, string, time.Time, time.Time, StdType) (io.ReadCloser, error)
ContainerStats(context.Context, string, chan<- ContainerStat) error ContainerStats(context.Context, string, chan<- ContainerStat) error
Ping(context.Context) (types.Ping, error) Ping(context.Context) (types.Ping, error)
} }
@@ -227,8 +249,8 @@ func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan
return nil return nil
} }
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since string) (io.ReadCloser, error) { func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since string, stdType StdType) (io.ReadCloser, error) {
log.WithField("id", id).WithField("since", since).Debug("streaming logs for container") log.WithField("id", id).WithField("since", since).WithField("stdType", stdType).Debug("streaming logs for container")
if since != "" { if since != "" {
if millis, err := strconv.ParseInt(since, 10, 64); err == nil { if millis, err := strconv.ParseInt(since, 10, 64); err == nil {
@@ -239,8 +261,8 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since strin
} }
options := types.ContainerLogsOptions{ options := types.ContainerLogsOptions{
ShowStdout: true, ShowStdout: stdType&STDOUT != 0,
ShowStderr: true, ShowStderr: stdType&STDERR != 0,
Follow: true, Follow: true,
Tail: "300", Tail: "300",
Timestamps: true, Timestamps: true,
@@ -258,7 +280,7 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since strin
return nil, err return nil, err
} }
return newLogReader(reader, containerJSON.Config.Tty), nil return newLogReader(reader, containerJSON.Config.Tty, true), nil
} }
func (d *dockerClient) Events(ctx context.Context) (<-chan ContainerEvent, <-chan error) { func (d *dockerClient) Events(ctx context.Context) (<-chan ContainerEvent, <-chan error) {
@@ -290,11 +312,35 @@ func (d *dockerClient) Events(ctx context.Context) (<-chan ContainerEvent, <-cha
return messages, errors return messages, errors
} }
func (d *dockerClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time) (io.ReadCloser, error) { func (d *dockerClient) ContainerLogReader(ctx context.Context, id string) (io.ReadCloser, error) {
options := types.ContainerLogsOptions{ options := types.ContainerLogsOptions{
ShowStdout: true, ShowStdout: true,
ShowStderr: true, ShowStderr: true,
Timestamps: true, Timestamps: true,
Since: time.Unix(0, 0).Format(time.RFC3339),
Until: time.Now().Format(time.RFC3339),
}
reader, err := d.cli.ContainerLogs(ctx, id, options)
if err != nil {
return nil, err
}
containerJSON, err := d.cli.ContainerInspect(ctx, id)
if err != nil {
return nil, err
}
return newLogReader(reader, containerJSON.Config.Tty, false), nil
}
func (d *dockerClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType StdType) (io.ReadCloser, error) {
options := types.ContainerLogsOptions{
ShowStdout: stdType&STDOUT != 0,
ShowStderr: stdType&STDERR != 0,
Timestamps: true,
Since: from.Format(time.RFC3339), Since: from.Format(time.RFC3339),
Until: to.Format(time.RFC3339), Until: to.Format(time.RFC3339),
} }
@@ -312,7 +358,7 @@ func (d *dockerClient) ContainerLogsBetweenDates(ctx context.Context, id string,
return nil, err return nil, err
} }
return newLogReader(reader, containerJSON.Config.Tty), nil return newLogReader(reader, containerJSON.Config.Tty, true), nil
} }
func (d *dockerClient) Ping(ctx context.Context) (types.Ping, error) { func (d *dockerClient) Ping(ctx context.Context) (types.Ping, error) {

View File

@@ -133,7 +133,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil) proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
client := &dockerClient{proxy, filters.NewArgs()} client := &dockerClient{proxy, filters.NewArgs()}
logReader, _ := client.ContainerLogs(context.Background(), id, "since") logReader, _ := client.ContainerLogs(context.Background(), id, "since", STDALL)
actual, _ := io.ReadAll(logReader) actual, _ := io.ReadAll(logReader)
assert.Equal(t, expected, string(actual), "message doesn't match expected") assert.Equal(t, expected, string(actual), "message doesn't match expected")
@@ -154,7 +154,7 @@ func Test_dockerClient_ContainerLogs_happy_with_tty(t *testing.T) {
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil) proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
client := &dockerClient{proxy, filters.NewArgs()} client := &dockerClient{proxy, filters.NewArgs()}
logReader, _ := client.ContainerLogs(context.Background(), id, "") logReader, _ := client.ContainerLogs(context.Background(), id, "", STDALL)
actual, _ := io.ReadAll(logReader) actual, _ := io.ReadAll(logReader)
assert.Equal(t, expected, string(actual), "message doesn't match expected") assert.Equal(t, expected, string(actual), "message doesn't match expected")
@@ -170,7 +170,7 @@ func Test_dockerClient_ContainerLogs_error(t *testing.T) {
client := &dockerClient{proxy, filters.NewArgs()} client := &dockerClient{proxy, filters.NewArgs()}
reader, err := client.ContainerLogs(context.Background(), id, "") reader, err := client.ContainerLogs(context.Background(), id, "", STDALL)
assert.Nil(t, reader, "reader should be nil") assert.Nil(t, reader, "reader should be nil")
assert.Error(t, err, "error should have been returned") assert.Error(t, err, "error should have been returned")

View File

@@ -93,8 +93,20 @@ func (g *eventGenerator) consume() {
if message != "" { if message != "" {
h := fnv.New32a() h := fnv.New32a()
h.Write([]byte(message)) h.Write([]byte(message))
std := message[:3]
var stdType StdType
switch std {
case "OUT":
stdType = STDOUT
case "ERR":
stdType = STDERR
default:
log.Panicf("unknown std type [%s] with message [%s]", std, message)
}
logEvent := &LogEvent{Id: h.Sum32(), Message: message} message = message[3:]
logEvent := &LogEvent{Id: h.Sum32(), Message: message, StdType: stdType}
if index := strings.IndexAny(message, " "); index != -1 { if index := strings.IndexAny(message, " "); index != -1 {
logId := message[:index] logId := message[:index]

View File

@@ -12,7 +12,7 @@ import (
func TestNewEventIterator(t *testing.T) { func TestNewEventIterator(t *testing.T) {
input := "example input" input := "example input"
reader := bufio.NewReader(strings.NewReader(input)) reader := bufio.NewReader(strings.NewReader("OUT" + input))
generator := NewEventIterator(reader) generator := NewEventIterator(reader)
require.NotNil(t, generator, "Expected generator to not be nil, but got nil") require.NotNil(t, generator, "Expected generator to not be nil, but got nil")
@@ -20,7 +20,7 @@ func TestNewEventIterator(t *testing.T) {
func TestEventGenerator_Next(t *testing.T) { func TestEventGenerator_Next(t *testing.T) {
input := "example input" input := "example input"
reader := bufio.NewReader(strings.NewReader(input)) reader := bufio.NewReader(strings.NewReader("OUT" + input))
generator := NewEventIterator(reader) generator := NewEventIterator(reader)
@@ -31,7 +31,7 @@ func TestEventGenerator_Next(t *testing.T) {
func TestEventGenerator_LastError(t *testing.T) { func TestEventGenerator_LastError(t *testing.T) {
input := "example input" input := "example input"
reader := bufio.NewReader(strings.NewReader(input)) reader := bufio.NewReader(strings.NewReader("OUT" + input))
generator := NewEventIterator(reader) generator := NewEventIterator(reader)
@@ -45,7 +45,7 @@ func TestEventGenerator_LastError(t *testing.T) {
func TestEventGenerator_Peek(t *testing.T) { func TestEventGenerator_Peek(t *testing.T) {
input := "example input" input := "example input"
reader := bufio.NewReader(strings.NewReader(input)) reader := bufio.NewReader(strings.NewReader("OUT" + input))
generator := NewEventIterator(reader) generator := NewEventIterator(reader)

View File

@@ -11,14 +11,16 @@ type logReader struct {
tty bool tty bool
lastHeader []byte lastHeader []byte
buffer bytes.Buffer buffer bytes.Buffer
label bool
} }
func newLogReader(reader io.ReadCloser, tty bool) io.ReadCloser { func newLogReader(reader io.ReadCloser, tty bool, labelStd bool) io.ReadCloser {
return &logReader{ return &logReader{
reader, reader,
tty, tty,
make([]byte, 8), make([]byte, 8),
bytes.Buffer{}, bytes.Buffer{},
labelStd,
} }
} }
@@ -34,6 +36,16 @@ func (r *logReader) Read(p []byte) (n int, err error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
if r.label {
std := r.lastHeader[0] // https://github.com/rancher/docker/blob/master/pkg/stdcopy/stdcopy.go#L94
if std == 1 {
r.buffer.WriteString("OUT")
}
if std == 2 {
r.buffer.WriteString("ERR")
}
}
count := binary.BigEndian.Uint32(r.lastHeader[4:]) count := binary.BigEndian.Uint32(r.lastHeader[4:])
_, err = io.CopyN(&r.buffer, r.readerCloser, int64(count)) _, err = io.CopyN(&r.buffer, r.readerCloser, int64(count))
if err != nil { if err != nil {

View File

@@ -1,6 +1,8 @@
package docker package docker
import "math" import (
"math"
)
// Container represents an internal representation of docker containers // Container represents an internal representation of docker containers
type Container struct { type Container struct {
@@ -44,6 +46,7 @@ type LogEvent struct {
Id uint32 `json:"id,omitempty"` Id uint32 `json:"id,omitempty"`
Level string `json:"l,omitempty"` Level string `json:"l,omitempty"`
Position LogPosition `json:"p,omitempty"` Position LogPosition `json:"p,omitempty"`
StdType StdType `json:"s,omitempty"`
} }
func (l *LogEvent) HasLevel() bool { func (l *LogEvent) HasLevel() bool {

View File

@@ -3,11 +3,11 @@
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@netlify/functions": "^1.6.0", "@netlify/functions": "^1.6.0",
"@unocss/preset-typography": "^0.52.3", "@unocss/preset-typography": "^0.52.7",
"@unocss/reset": "^0.52.3", "@unocss/reset": "^0.52.7",
"@unocss/transformer-directives": "^0.52.3", "@unocss/transformer-directives": "^0.52.7",
"dozzle": "workspace:*", "dozzle": "workspace:*",
"sitemap": "^7.1.1", "sitemap": "^7.1.1",
"unocss": "^0.52.3" "unocss": "^0.52.7"
} }
} }

248
docs/pnpm-lock.yaml generated
View File

@@ -1,18 +1,22 @@
lockfileVersion: '6.0' lockfileVersion: '6.1'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
devDependencies: devDependencies:
'@netlify/functions': '@netlify/functions':
specifier: ^1.6.0 specifier: ^1.6.0
version: 1.6.0 version: 1.6.0
'@unocss/preset-typography': '@unocss/preset-typography':
specifier: ^0.52.3 specifier: ^0.52.7
version: 0.52.3 version: 0.52.7
'@unocss/reset': '@unocss/reset':
specifier: ^0.52.3 specifier: ^0.52.7
version: 0.52.3 version: 0.52.7
'@unocss/transformer-directives': '@unocss/transformer-directives':
specifier: ^0.52.3 specifier: ^0.52.7
version: 0.52.3 version: 0.52.7
dozzle: dozzle:
specifier: workspace:* specifier: workspace:*
version: link:.. version: link:..
@@ -20,8 +24,8 @@ devDependencies:
specifier: ^7.1.1 specifier: ^7.1.1
version: 7.1.1 version: 7.1.1
unocss: unocss:
specifier: ^0.52.3 specifier: ^0.52.7
version: 0.52.3(postcss@8.4.23)(vite@4.3.8) version: 0.52.7(postcss@8.4.24)(vite@4.3.9)
packages: packages:
@@ -40,8 +44,8 @@ packages:
find-up: 5.0.0 find-up: 5.0.0
dev: true dev: true
/@antfu/utils@0.7.2: /@antfu/utils@0.7.4:
resolution: {integrity: sha512-vy9fM3pIxZmX07dL+VX1aZe7ynZ+YyB0jY+jE6r3hOK6GNY2t6W8rzpFC4tgpbXUYABkFQwgJq2XYXlxbXAI0g==} resolution: {integrity: sha512-qe8Nmh9rYI/HIspLSTwtbMFPj6dISG6+dJnOguTlPNXtCvS2uezdxscVBb7/3DrmNbQK49TDqpkSQ1chbRGdpQ==}
dev: true dev: true
/@esbuild/android-arm64@0.17.19: /@esbuild/android-arm64@0.17.19:
@@ -250,7 +254,7 @@ packages:
resolution: {integrity: sha512-6MvDI+I6QMvXn5rK9KQGdpEE4mmLTcuQdLZEiX5N+uZB+vc4Yw9K1OtnOgkl8mp4d9X0UrILREyZgF1NUwUt+Q==} resolution: {integrity: sha512-6MvDI+I6QMvXn5rK9KQGdpEE4mmLTcuQdLZEiX5N+uZB+vc4Yw9K1OtnOgkl8mp4d9X0UrILREyZgF1NUwUt+Q==}
dependencies: dependencies:
'@antfu/install-pkg': 0.1.1 '@antfu/install-pkg': 0.1.1
'@antfu/utils': 0.7.2 '@antfu/utils': 0.7.4
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
debug: 4.3.4 debug: 4.3.4
kolorist: 1.8.0 kolorist: 1.8.0
@@ -357,27 +361,27 @@ packages:
'@types/node': 18.14.2 '@types/node': 18.14.2
dev: true dev: true
/@unocss/astro@0.52.3(vite@4.3.8): /@unocss/astro@0.52.7(vite@4.3.9):
resolution: {integrity: sha512-S9Rb1TROB0Q1c4qgLBwLWqccaYq+Q+ZJaUvpgNjvDeKdam1pcGCELJos0HIK5oxOXpALSVmlMkGEh7OOZzDhCQ==} resolution: {integrity: sha512-jGm3sVB6AU3A1vXJskCdG2kUw1aRdg2fV60nILCBiRmj7SIlbMTXEHrz864AaleGVnxTiV7oGL4P1DfDJ3tQSA==}
dependencies: dependencies:
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
'@unocss/reset': 0.52.3 '@unocss/reset': 0.52.7
'@unocss/vite': 0.52.3(vite@4.3.8) '@unocss/vite': 0.52.7(vite@4.3.9)
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
- vite - vite
dev: true dev: true
/@unocss/cli@0.52.3: /@unocss/cli@0.52.7:
resolution: {integrity: sha512-bVR9cwltNvYi35gWR7XYdtrgwU+saYxeBRWt7vlargaIPmQ0s9EgfcHYC7mlD82SZPnRj1KQhyFVTFtyrQCiVg==} resolution: {integrity: sha512-WC82yIMH6RH8W/0Gb26WEjNf/E8Rb1m6qywhtpuzwEYWmA8z6+uDvIaoXu8lhSpVeggQwjdzOXFe0++GRTcQ3Q==}
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
dependencies: dependencies:
'@ampproject/remapping': 2.2.1 '@ampproject/remapping': 2.2.1
'@rollup/pluginutils': 5.0.2 '@rollup/pluginutils': 5.0.2
'@unocss/config': 0.52.3 '@unocss/config': 0.52.7
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
'@unocss/preset-uno': 0.52.3 '@unocss/preset-uno': 0.52.7
cac: 6.7.14 cac: 6.7.14
chokidar: 3.5.3 chokidar: 3.5.3
colorette: 2.0.20 colorette: 2.0.20
@@ -390,158 +394,158 @@ packages:
- rollup - rollup
dev: true dev: true
/@unocss/config@0.52.3: /@unocss/config@0.52.7:
resolution: {integrity: sha512-T/OLuf8twR6/b6zcRgdL3iVmz8jEv2CSy08kUQlpjVDJhV2MZcdlTNi+pQcLK1NTRkHiBVodZwTFPNje2eUIxA==} resolution: {integrity: sha512-VKj4VnJR88EK0ikJnQbfslZfMCqdGu6LhnErs3x0HjQPVQU1oFsB1IM4ySGLaGhM4WcfZf05gzMzIav3kFyopg==}
engines: {node: '>=14'} engines: {node: '>=14'}
dependencies: dependencies:
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
unconfig: 0.3.9 unconfig: 0.3.9
dev: true dev: true
/@unocss/core@0.52.3: /@unocss/core@0.52.7:
resolution: {integrity: sha512-AdpksuSj1+jAjF7Ek1Ubtt+pE/bi4EmVqz/sx7PTgp9RUyBX1457kDlSWJPFOvEEkKL8VLtwXB46hD2oPAp36Q==} resolution: {integrity: sha512-dZonrlfu33SkUMsZXlsyYSM79tr2nLer/hBEU2ZaemRik2KchxIUNlZV6kX1f1k3m+gEtVQOyx1MImpgLS8PWg==}
dev: true dev: true
/@unocss/extractor-arbitrary-variants@0.52.3: /@unocss/extractor-arbitrary-variants@0.52.7:
resolution: {integrity: sha512-dEDQ9mfwlS/aC420iRO6wUT1p0z2WBH5nupTdVgrU9Wjtff+NmLaas78skN+GPE5FCPXgKTJJsaDX6+etc/hrw==} resolution: {integrity: sha512-nJ4iE7nIRpoOIQfD8S58yG4qJd6AhVPEfEOf7ksX1u8xLf71rrBIojwraRXvv7aPqNdZiWvXdh/znpA/QC5b9w==}
dependencies: dependencies:
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
dev: true dev: true
/@unocss/inspector@0.52.3: /@unocss/inspector@0.52.7:
resolution: {integrity: sha512-VXbglsSzwpXGo51IAnmQWsjqrROMz+DbGujMW8xksmDqUcJArV1KgLRpZHaeyhs5o2D6UTstgpSpqWgvlcvLNA==} resolution: {integrity: sha512-XuxoCerVpIw9XR1iO8PEPrCj+KLwEGLAziHedObnXkS5ANbHdd+eWXIPpsG8DbICdLGUDnalL7wfxB19X1S9AQ==}
dependencies: dependencies:
gzip-size: 6.0.0 gzip-size: 6.0.0
sirv: 2.0.3 sirv: 2.0.3
dev: true dev: true
/@unocss/postcss@0.52.3(postcss@8.4.23): /@unocss/postcss@0.52.7(postcss@8.4.24):
resolution: {integrity: sha512-n3SdpSsn0MpWB9Pf6JjzR7U2rsA6jkD5QPJttIL9yxrK9i4KXTwGNio/4iM2Rs4x+qAzLtNjIBJ1xdxtIFA3kA==} resolution: {integrity: sha512-0yG7K8ie9gky7Y/oD29Jzpe4l92IgRPB2Fo9a7g2f4dGlKOuih5S+NsH3EO4WODrawntISyxVXMHsIydze2vAw==}
engines: {node: '>=14'} engines: {node: '>=14'}
peerDependencies: peerDependencies:
postcss: ^8.4.21 postcss: ^8.4.21
dependencies: dependencies:
'@unocss/config': 0.52.3 '@unocss/config': 0.52.7
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
css-tree: 2.3.1 css-tree: 2.3.1
fast-glob: 3.2.12 fast-glob: 3.2.12
magic-string: 0.30.0 magic-string: 0.30.0
postcss: 8.4.23 postcss: 8.4.24
dev: true dev: true
/@unocss/preset-attributify@0.52.3: /@unocss/preset-attributify@0.52.7:
resolution: {integrity: sha512-2+1i1iMnTv+Mh+KHmNm7kDtAfTD/rJn134PjIgTJq06WmS62RF9lDsj7ng0NA09vXLHQKtwXGeRk7Ca3P7/Jwg==} resolution: {integrity: sha512-rq3ntPbuwGTZO7ebQhsuaZjKCmkDPBNP7sX+lXhaOsIsIGM4JGmLTBNSZ03YUx6QVgYVbjO1MKv734AHNYG4/Q==}
dependencies: dependencies:
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
dev: true dev: true
/@unocss/preset-icons@0.52.3: /@unocss/preset-icons@0.52.7:
resolution: {integrity: sha512-OBy9AeLE8li8R2ActigLBC/GEq3SrcCA4SVUVvz4pM17RoXhxSyg6sxa97UgcJ0QTbJQL6YzgS9lB857Bv0fjA==} resolution: {integrity: sha512-4M8V7dhNxA+XGRqz+mlmEtqHOnyXYuqFpc+3biqjhlJb4zirNgJ9ujEty0OWwrKhC8QKfxifVlTtHInfjQQkDA==}
dependencies: dependencies:
'@iconify/utils': 2.1.5 '@iconify/utils': 2.1.5
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
ofetch: 1.0.1 ofetch: 1.0.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true dev: true
/@unocss/preset-mini@0.52.3: /@unocss/preset-mini@0.52.7:
resolution: {integrity: sha512-9KJMlO3YF6UZRgua3js7pTh8lImMFLbtTpGWrrRNojJH2MvsmQNd4OlWLDobs3jUJG+4tlYiSH175Y3bdEHVXQ==} resolution: {integrity: sha512-c5VRzPwyAmIBWwz2ufEboYwHGiheG+V9SCmJJLHlu/gcW5KndFsxoeJPE6nOfXVmbx4AGq/rkzV35ZXtH8Iecw==}
dependencies: dependencies:
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
'@unocss/extractor-arbitrary-variants': 0.52.3 '@unocss/extractor-arbitrary-variants': 0.52.7
dev: true dev: true
/@unocss/preset-tagify@0.52.3: /@unocss/preset-tagify@0.52.7:
resolution: {integrity: sha512-zdBHZRYRAbtRQu7kzg18lMa8ZxtmAt93eUjQa8qEv180roL3+ycx2G05wkLn+dRx9n3Nn/wEL++FN/y5Fu/3Zg==} resolution: {integrity: sha512-Zoard/LvUT03buLkDAnFAsgUUDfqIrVXADQFqRN7uDkf5lXocqjp56IzHng1Py2EJY4RpqHx+Mixn0fBH45E4g==}
dependencies: dependencies:
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
dev: true dev: true
/@unocss/preset-typography@0.52.3: /@unocss/preset-typography@0.52.7:
resolution: {integrity: sha512-BfgBrLDjIS7Mbjie8eZWRh8VDLAT3o5EoW9OLbOpJfeyy2wfgtj2e10TK7xk8sNqaxSud5wTovQJi0tr4+Fc7w==} resolution: {integrity: sha512-mx7NQm6ZEo1UTQX9ZIzhZePjIBb2PEw7VDg6rWAPzdMRYQ1PnetjVbGFK5IafKmgVD1PP43UUwqDo8P0bD/aOg==}
dependencies: dependencies:
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
'@unocss/preset-mini': 0.52.3 '@unocss/preset-mini': 0.52.7
dev: true dev: true
/@unocss/preset-uno@0.52.3: /@unocss/preset-uno@0.52.7:
resolution: {integrity: sha512-6rNjthD517yUBST3efxE5dsiErYf198RNh6fV8Fxhw0JwI+X1B9e5lzhviuyXbJj+qvJTpZFYcebyVxlzyT1lQ==} resolution: {integrity: sha512-J5royXxvaPvwRplZ2zwEcB1jJETp3dTA3sIezf9ydSNr4px3h6Ul6TxFDuJpBUWlx/cxP7aRWM0p9+e2ivdRkA==}
dependencies: dependencies:
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
'@unocss/preset-mini': 0.52.3 '@unocss/preset-mini': 0.52.7
'@unocss/preset-wind': 0.52.3 '@unocss/preset-wind': 0.52.7
dev: true dev: true
/@unocss/preset-web-fonts@0.52.3: /@unocss/preset-web-fonts@0.52.7:
resolution: {integrity: sha512-beILgZF707CjzoBy7AYAgdoX+oX6ZHUfSFEqVbenkargZv2w4M3Tgae/mJxwaQfHB8lMyq2IRTnn1fOj8J814g==} resolution: {integrity: sha512-KnWpYPqRVqD1wu8pJMQVy+sMgrJKSqr5R0C1xMMT4u4TZk4fc2YWXox6UNw5WWWzdc1KzJ/k36wSPnq+jSjfDA==}
dependencies: dependencies:
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
ofetch: 1.0.1 ofetch: 1.0.1
dev: true dev: true
/@unocss/preset-wind@0.52.3: /@unocss/preset-wind@0.52.7:
resolution: {integrity: sha512-YBfn1goa509Xxet2+mJimUkVO9t1rsTcqv5ytDpA9kUMNMdR8hrHh6hyM6WPB5Pg8/B7yQ739iZ6dkfbr/UFgQ==} resolution: {integrity: sha512-IT36cDftK7B+zDUElL4qdZZEj6iwknIpetXwuVvW/X8ljS/ocY/qfyjSX7C8k163FLAw7nTARFjW3xL066NsLw==}
dependencies: dependencies:
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
'@unocss/preset-mini': 0.52.3 '@unocss/preset-mini': 0.52.7
dev: true dev: true
/@unocss/reset@0.52.3: /@unocss/reset@0.52.7:
resolution: {integrity: sha512-2vp4egIZC+d48IwX9e4jv8x04aPdKy0mP5VZSE+n4wczlh2ctLE5b9z6hnv0mM9BwHgA1nIX/7iNkdd+2pkJ6g==} resolution: {integrity: sha512-TJW2BaGGQoh0OSDd22Ti8bZ/Ds3YMGT8aBxNPkcyhesH4fCJeWK+rwsAc5g8CS/wp9OdLS8P4Jy9k2Yg/GfrVQ==}
dev: true dev: true
/@unocss/scope@0.52.3: /@unocss/scope@0.52.7:
resolution: {integrity: sha512-TYpb7ICvIK4KNsj2Uq8Fa4RBeABG+7zoauo9RK9c9NoVUiDJhm/lCba1Q6V7ArEAsEKldG4JA4F08k9Hr0rcRQ==} resolution: {integrity: sha512-J8QMwfbm+lCt3Lpt52NllnXbuICvH8+Njl/L65wN9TfE6gHk0StA5nrEOlOB79R1aOhnRaoqG4MkAvFXK/1dcQ==}
dev: true dev: true
/@unocss/transformer-attributify-jsx-babel@0.52.3: /@unocss/transformer-attributify-jsx-babel@0.52.7:
resolution: {integrity: sha512-KO0c+uCGstKulHAlTtoWb7RS8uq/MkjADhxtvGsyj73vQT6CiicZ4dgzPvN+XP9cEs02H0Hl5OJ1171dbvtKgw==} resolution: {integrity: sha512-6O2wSmALwaY0gmo/6quIEEiB6mpE3HFRJU2FmDQny5PVBrDhKps72h1zeNkDA8wjxz8XizNBhPbH/Uzc1lnAVg==}
dependencies: dependencies:
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
dev: true dev: true
/@unocss/transformer-attributify-jsx@0.52.3: /@unocss/transformer-attributify-jsx@0.52.7:
resolution: {integrity: sha512-1qYNY3qGLBu2Fsoq2j1LGVyATkIe1BtLogK7o+Zpk3tAGR3GvJl8HTzirIaI1FaBfYScsPEFS4uFtLawNVvSww==} resolution: {integrity: sha512-5Wz4KCUB+ZnXKwvtyASoN0yH61GPMRyNfLP3tz/uel9H2lyfgIPSKFthPVY8dsUCEixT7oGiIvQCLqk6f3po3A==}
dependencies: dependencies:
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
dev: true dev: true
/@unocss/transformer-compile-class@0.52.3: /@unocss/transformer-compile-class@0.52.7:
resolution: {integrity: sha512-dQKxPuCWOahLJueu6mup+nJFas3pqosj4/jiJEok9uFFXbeq2Y9z3XxI1MWGTI/JSPtD6yLxH6Vwe0eOk2OJOw==} resolution: {integrity: sha512-4gHqzeLq+9Ehl+yxYtGNUWrYACxnNfeiHBXfix7VmRHsBWIRol0/81Shqplxm9JRhkQcbXzadogynOav5LQcBg==}
dependencies: dependencies:
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
dev: true dev: true
/@unocss/transformer-directives@0.52.3: /@unocss/transformer-directives@0.52.7:
resolution: {integrity: sha512-19ECVhIOzllR8iTA9oTupsMdVs9F1+5ooLmfeRtvl9hJP+3YhSP0nPHau5x172rbx2lrt4MsomjWBlcQV+twUw==} resolution: {integrity: sha512-v68nQjeU/8I8aOIQC6prIk5GJi8SpkaFsdh9p1UPSkJPL3rYv0bBLIkYrwBcmaqKUOvzL5joN0Cueolq/+GtUw==}
dependencies: dependencies:
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
css-tree: 2.3.1 css-tree: 2.3.1
dev: true dev: true
/@unocss/transformer-variant-group@0.52.3: /@unocss/transformer-variant-group@0.52.7:
resolution: {integrity: sha512-tr4ZfwvBGQBXkjiM+Jroe7T9AlryFzt5F1pkvqdx3cDy9BeQpzC6+ZrLjH1xPLDv1wposHXbURLfMe/9dXka7w==} resolution: {integrity: sha512-pGqTfT1hax3F+yjs6n6r5loSIP/Dsm/NuEA5nwazTu4gmubiIBi11UjoK/pE/cFg9Z3yp6n9Lspo71yALJbpVg==}
dependencies: dependencies:
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
dev: true dev: true
/@unocss/vite@0.52.3(vite@4.3.8): /@unocss/vite@0.52.7(vite@4.3.9):
resolution: {integrity: sha512-N/e2zbRGrn8mmllVAiCeCoB3AQ96+l1XTTTN5mvOTj2VMzfsaYE4z28X4jUQ35JppfppfDKwESaDD+b/DZyJqA==} resolution: {integrity: sha512-Hn1u6/uPP2q0s5gfwA7KQFtclviEUrEKnEa3l1kFJA3S/tHXYjwQkzbDQObQzolVAXyzIhf1cQ8e1tEMyHm1qg==}
peerDependencies: peerDependencies:
vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0
dependencies: dependencies:
'@ampproject/remapping': 2.2.1 '@ampproject/remapping': 2.2.1
'@rollup/pluginutils': 5.0.2 '@rollup/pluginutils': 5.0.2
'@unocss/config': 0.52.3 '@unocss/config': 0.52.7
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
'@unocss/inspector': 0.52.3 '@unocss/inspector': 0.52.7
'@unocss/scope': 0.52.3 '@unocss/scope': 0.52.7
'@unocss/transformer-directives': 0.52.3 '@unocss/transformer-directives': 0.52.7
chokidar: 3.5.3 chokidar: 3.5.3
fast-glob: 3.2.12 fast-glob: 3.2.12
magic-string: 0.30.0 magic-string: 0.30.0
vite: 4.3.8 vite: 4.3.9
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
dev: true dev: true
@@ -930,8 +934,8 @@ packages:
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
dev: true dev: true
/postcss@8.4.23: /postcss@8.4.24:
resolution: {integrity: sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==} resolution: {integrity: sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
dependencies: dependencies:
nanoid: 3.3.6 nanoid: 3.3.6
@@ -1038,40 +1042,40 @@ packages:
/unconfig@0.3.9: /unconfig@0.3.9:
resolution: {integrity: sha512-8yhetFd48M641mxrkWA+C/lZU4N0rCOdlo3dFsyFPnBHBjMJfjT/3eAZBRT2RxCRqeBMAKBVgikejdS6yeBjMw==} resolution: {integrity: sha512-8yhetFd48M641mxrkWA+C/lZU4N0rCOdlo3dFsyFPnBHBjMJfjT/3eAZBRT2RxCRqeBMAKBVgikejdS6yeBjMw==}
dependencies: dependencies:
'@antfu/utils': 0.7.2 '@antfu/utils': 0.7.4
defu: 6.1.2 defu: 6.1.2
jiti: 1.18.2 jiti: 1.18.2
dev: true dev: true
/unocss@0.52.3(postcss@8.4.23)(vite@4.3.8): /unocss@0.52.7(postcss@8.4.24)(vite@4.3.9):
resolution: {integrity: sha512-BgL3kbxwt839t0ojo/j+i8xU4qu+fyV34SJOMQuFhLu6xkPNepvr6uPeipzNDajR7EZP3Q+jXJT9AWLKLLg1jw==} resolution: {integrity: sha512-c35lqmzWqnQH0hW2IE1owac2qfGOvNAhrIrLV2+pNmc2MDWq8WMjIEuWo8G+OS5JqFQY3ZBlE61q2x/tHPlujQ==}
engines: {node: '>=14'} engines: {node: '>=14'}
peerDependencies: peerDependencies:
'@unocss/webpack': 0.52.3 '@unocss/webpack': 0.52.7
peerDependenciesMeta: peerDependenciesMeta:
'@unocss/webpack': '@unocss/webpack':
optional: true optional: true
dependencies: dependencies:
'@unocss/astro': 0.52.3(vite@4.3.8) '@unocss/astro': 0.52.7(vite@4.3.9)
'@unocss/cli': 0.52.3 '@unocss/cli': 0.52.7
'@unocss/core': 0.52.3 '@unocss/core': 0.52.7
'@unocss/extractor-arbitrary-variants': 0.52.3 '@unocss/extractor-arbitrary-variants': 0.52.7
'@unocss/postcss': 0.52.3(postcss@8.4.23) '@unocss/postcss': 0.52.7(postcss@8.4.24)
'@unocss/preset-attributify': 0.52.3 '@unocss/preset-attributify': 0.52.7
'@unocss/preset-icons': 0.52.3 '@unocss/preset-icons': 0.52.7
'@unocss/preset-mini': 0.52.3 '@unocss/preset-mini': 0.52.7
'@unocss/preset-tagify': 0.52.3 '@unocss/preset-tagify': 0.52.7
'@unocss/preset-typography': 0.52.3 '@unocss/preset-typography': 0.52.7
'@unocss/preset-uno': 0.52.3 '@unocss/preset-uno': 0.52.7
'@unocss/preset-web-fonts': 0.52.3 '@unocss/preset-web-fonts': 0.52.7
'@unocss/preset-wind': 0.52.3 '@unocss/preset-wind': 0.52.7
'@unocss/reset': 0.52.3 '@unocss/reset': 0.52.7
'@unocss/transformer-attributify-jsx': 0.52.3 '@unocss/transformer-attributify-jsx': 0.52.7
'@unocss/transformer-attributify-jsx-babel': 0.52.3 '@unocss/transformer-attributify-jsx-babel': 0.52.7
'@unocss/transformer-compile-class': 0.52.3 '@unocss/transformer-compile-class': 0.52.7
'@unocss/transformer-directives': 0.52.3 '@unocss/transformer-directives': 0.52.7
'@unocss/transformer-variant-group': 0.52.3 '@unocss/transformer-variant-group': 0.52.7
'@unocss/vite': 0.52.3(vite@4.3.8) '@unocss/vite': 0.52.7(vite@4.3.9)
transitivePeerDependencies: transitivePeerDependencies:
- postcss - postcss
- rollup - rollup
@@ -1079,8 +1083,8 @@ packages:
- vite - vite
dev: true dev: true
/vite@4.3.8: /vite@4.3.9:
resolution: {integrity: sha512-uYB8PwN7hbMrf4j1xzGDk/lqjsZvCDbt/JC5dyfxc19Pg8kRm14LinK/uq+HSLNswZEoKmweGdtpbnxRtrAXiQ==} resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -1105,7 +1109,7 @@ packages:
optional: true optional: true
dependencies: dependencies:
esbuild: 0.17.19 esbuild: 0.17.19
postcss: 8.4.23 postcss: 8.4.24
rollup: 3.23.0 rollup: 3.23.0
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2

View File

@@ -2,6 +2,8 @@ toolbar:
clear: Clear clear: Clear
download: Download download: Download
search: Search search: Search
show: Show {std}
hide: Hide {std}
label: label:
containers: Containers containers: Containers
total-containers: Total Containers total-containers: Total Containers
@@ -49,3 +51,4 @@ settings:
update-available: >- update-available: >-
New version is available! Update to <a :href="{href}" class="next-release" New version is available! Update to <a :href="{href}" class="next-release"
target="_blank" rel="noreferrer noopener">{nextVersion}</a>. target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Show stdout and stderr labels

View File

@@ -2,6 +2,7 @@ toolbar:
clear: Limpiar clear: Limpiar
download: Descargar download: Descargar
search: Buscar search: Buscar
show: Mostrar {std}
label: label:
containers: Contenedores containers: Contenedores
total-containers: Contenedores Totales total-containers: Contenedores Totales
@@ -49,3 +50,4 @@ settings:
update-available: >- update-available: >-
¡La nueva versión está disponible! Actualizar a la ¡La nueva versión está disponible! Actualizar a la
<a :href="{href}" class="next-release" target="_blank" rel="noreferrer noopener">{nextVersion}</a>. <a :href="{href}" class="next-release" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Mostrar etiquetas de salida estándar y salida de error estándar

View File

@@ -2,6 +2,8 @@ toolbar:
clear: Limpar clear: Limpar
download: Descarregar download: Descarregar
search: Pesquisa search: Pesquisa
show: Mostrar {std}
hide: Ocultar {std}
label: label:
containers: Contentores containers: Contentores
total-containers: Contentores Totais total-containers: Contentores Totais
@@ -49,3 +51,4 @@ settings:
update-available: >- update-available: >-
Está disponível uma nova versão! Actualização para Está disponível uma nova versão! Actualização para
<a :href="{href}" class="next-release" target="_blank" rel="noreferrer noopener">{nextVersion}</a>. <a :href="{href}" class="next-release" target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Mostrar etiquetas de saída padrão e saída de erro padrão

View File

@@ -2,6 +2,8 @@ toolbar:
clear: Очистить clear: Очистить
download: Скачать download: Скачать
search: Поиск search: Поиск
show: Показать {std}
hide: Скрыть {std}
label: label:
containers: Контейнеры containers: Контейнеры
total-containers: Всего Контейнеров total-containers: Всего Контейнеров
@@ -48,3 +50,4 @@ settings:
update-available: >- update-available: >-
Доступна новая версия! Обновить до <a :href="{href}" class="next-release" Доступна новая версия! Обновить до <a :href="{href}" class="next-release"
target="_blank" rel="noreferrer noopener">{nextVersion}</a>. target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Показывать метки stdout и stderr

View File

@@ -26,7 +26,7 @@
"docs:preview": "vitepress preview docs" "docs:preview": "vitepress preview docs"
}, },
"dependencies": { "dependencies": {
"@iconify-json/carbon": "^1.1.16", "@iconify-json/carbon": "^1.1.17",
"@iconify-json/cil": "^1.1.4", "@iconify-json/cil": "^1.1.4",
"@iconify-json/mdi": "^1.1.52", "@iconify-json/mdi": "^1.1.52",
"@iconify-json/mdi-light": "^1.1.6", "@iconify-json/mdi-light": "^1.1.6",
@@ -39,7 +39,7 @@
"@vueuse/router": "^10.1.2", "@vueuse/router": "^10.1.2",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",
"bulma": "^0.9.4", "bulma": "^0.9.4",
"d3-array": "^3.2.3", "d3-array": "^3.2.4",
"d3-ease": "^3.0.1", "d3-ease": "^3.0.1",
"d3-scale": "^4.0.2", "d3-scale": "^4.0.2",
"d3-selection": "^3.0.0", "d3-selection": "^3.0.0",
@@ -53,30 +53,29 @@
"splitpanes": "^3.1.5", "splitpanes": "^3.1.5",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-i18n": "^9.2.2", "vue-i18n": "^9.2.2",
"vue-router": "^4.2.1" "vue-router": "^4.2.2"
}, },
"devDependencies": { "devDependencies": {
"@pinia/testing": "^0.1.2", "@pinia/testing": "^0.1.2",
"@playwright/test": "^1.34.2", "@playwright/test": "^1.34.3",
"@types/d3-array": "^3.0.4", "@types/d3-array": "^3.0.5",
"@types/d3-ease": "^3.0.0", "@types/d3-ease": "^3.0.0",
"@types/d3-scale": "^4.0.3", "@types/d3-scale": "^4.0.3",
"@types/d3-selection": "^3.0.5", "@types/d3-selection": "^3.0.5",
"@types/d3-shape": "^3.1.1", "@types/d3-shape": "^3.1.1",
"@types/d3-transition": "^3.0.3", "@types/d3-transition": "^3.0.3",
"@types/lodash.debounce": "^4.0.7", "@types/lodash.debounce": "^4.0.7",
"@types/node": "^20.2.3", "@types/node": "^20.2.5",
"@types/semver": "^7.5.0", "@types/semver": "^7.5.0",
"@vitejs/plugin-basic-ssl": "^1.0.1", "@vitejs/plugin-basic-ssl": "^1.0.1",
"@vitejs/plugin-vue": "4.2.3", "@vitejs/plugin-vue": "4.2.3",
"@vue-macros/reactivity-transform": "^0.3.8",
"@vue/compiler-sfc": "^3.3.4", "@vue/compiler-sfc": "^3.3.4",
"@vue/test-utils": "^2.3.2", "@vue/test-utils": "^2.3.2",
"bumpp": "^9.1.0", "bumpp": "^9.1.0",
"c8": "^7.13.0", "c8": "^7.14.0",
"eventsourcemock": "^2.0.0", "eventsourcemock": "^2.0.0",
"jest-serializer-vue": "^3.1.0", "jest-serializer-vue": "^3.1.0",
"jsdom": "^22.0.0", "jsdom": "^22.1.0",
"lint-staged": "^13.2.2", "lint-staged": "^13.2.2",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.8.8", "prettier": "^2.8.8",
@@ -84,14 +83,15 @@
"simple-git-hooks": "^2.8.1", "simple-git-hooks": "^2.8.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"unplugin-auto-import": "^0.16.2", "unplugin-auto-import": "^0.16.4",
"unplugin-icons": "^0.16.1", "unplugin-icons": "^0.16.1",
"unplugin-vue-components": "^0.25.0", "unplugin-vue-components": "^0.25.0",
"unplugin-vue-macros": "^2.2.1",
"vite": "4.3.9", "vite": "4.3.9",
"vite-plugin-pages": "^0.30.1", "vite-plugin-pages": "^0.30.1",
"vite-plugin-vue-layouts": "^0.8.0", "vite-plugin-vue-layouts": "^0.8.0",
"vitepress": "1.0.0-beta.1", "vitepress": "1.0.0-beta.1",
"vitest": "^0.31.1", "vitest": "^0.31.3",
"vue-tsc": "^1.6.5" "vue-tsc": "^1.6.5"
}, },
"lint-staged": { "lint-staged": {

586
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
"vue/ref-macros", "vue/ref-macros",
"vite-plugin-pages/client", "vite-plugin-pages/client",
"vite-plugin-vue-layouts/client", "vite-plugin-vue-layouts/client",
"@vue-macros/reactivity-transform/macros-global" "unplugin-vue-macros/macros-global"
] ]
}, },
"include": ["assets/**/*.ts", "assets/**/*.d.ts", "assets/**/*.vue"], "include": ["assets/**/*.ts", "assets/**/*.d.ts", "assets/**/*.vue"],

View File

@@ -1,7 +1,7 @@
import path from "path"; import path from "path";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue"; import Vue from "@vitejs/plugin-vue";
import ReactivityTransform from "@vue-macros/reactivity-transform/vite"; import VueMacros from "unplugin-vue-macros/vite";
import Icons from "unplugin-icons/vite"; import Icons from "unplugin-icons/vite";
import Components from "unplugin-vue-components/vite"; import Components from "unplugin-vue-components/vite";
import AutoImport from "unplugin-auto-import/vite"; import AutoImport from "unplugin-auto-import/vite";
@@ -26,14 +26,17 @@ export default defineConfig(() => ({
}, },
}, },
plugins: [ plugins: [
ReactivityTransform(), VueMacros({
vue({ plugins: {
vue: Vue({
template: { template: {
compilerOptions: { compilerOptions: {
whitespace: "preserve", whitespace: "preserve",
}, },
}, },
}), }),
},
}),
Icons({ Icons({
autoInstall: true, autoInstall: true,
}), }),

View File

@@ -76,8 +76,8 @@ HTTP/1.1 200 OK
Connection: close Connection: close
Content-Type: application/ld+json; charset=UTF-8 Content-Type: application/ld+json; charset=UTF-8
{"m":"INFO Testing logs...","ts":1589396137772,"id":2908612274,"l":"info"} {"m":"INFO Testing logs...","ts":1589396137772,"id":1122614848,"l":"info","s":1}
{"m":"INFO Testing logs...","ts":1589396137772,"id":2908612274,"l":"info"} {"m":"INFO Testing logs...","ts":1589396137772,"id":1543246723,"l":"info","s":2}
/* snapshot: Test_handler_streamEvents_error */ /* snapshot: Test_handler_streamEvents_error */
HTTP/1.1 200 OK HTTP/1.1 200 OK
@@ -143,6 +143,14 @@ X-Content-Type-Options: nosniff
test error test error
/* snapshot: Test_handler_streamLogs_error_std */
HTTP/1.1 400 Bad Request
Connection: close
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
stdout or stderr is required
/* snapshot: Test_handler_streamLogs_happy */ /* snapshot: Test_handler_streamLogs_happy */
HTTP/1.1 200 OK HTTP/1.1 200 OK
Connection: close Connection: close
@@ -152,7 +160,7 @@ Connection: keep-alive
Content-Type: text/event-stream Content-Type: text/event-stream
X-Accel-Buffering: no X-Accel-Buffering: no
data: {"m":"INFO Testing logs...","ts":0,"id":4256192898,"l":"info"} data: {"m":"INFO Testing logs...","ts":0,"id":852638900,"l":"info","s":1}
event: container-stopped event: container-stopped
data: end of stream data: end of stream
@@ -178,7 +186,7 @@ Connection: keep-alive
Content-Type: text/event-stream Content-Type: text/event-stream
X-Accel-Buffering: no X-Accel-Buffering: no
data: {"m":"INFO Testing logs...","ts":1589396137772,"id":1469707724,"l":"info"} data: {"m":"INFO Testing logs...","ts":1589396137772,"id":3373215946,"l":"info","s":1}
id: 1589396137772 id: 1589396137772
event: container-stopped event: container-stopped

View File

@@ -28,7 +28,6 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
} }
now := time.Now() now := time.Now()
from := time.Unix(container.Created, 0)
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s-%s.log.gz", container.Name, now.Format("2006-01-02T15-04-05"))) w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s-%s.log.gz", container.Name, now.Format("2006-01-02T15-04-05")))
w.Header().Set("Content-Type", "application/gzip") w.Header().Set("Content-Type", "application/gzip")
@@ -38,7 +37,7 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
zw.Comment = "Logs generated by Dozzle" zw.Comment = "Logs generated by Dozzle"
zw.ModTime = now zw.ModTime = now
reader, err := h.clientFromRequest(r).ContainerLogsBetweenDates(r.Context(), container.ID, from, now) reader, err := h.clientFromRequest(r).ContainerLogReader(r.Context(), container.ID)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@@ -53,7 +52,20 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to")) to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to"))
id := r.URL.Query().Get("id") id := r.URL.Query().Get("id")
reader, err := h.clientFromRequest(r).ContainerLogsBetweenDates(r.Context(), id, from, to) var stdTypes docker.StdType
if r.URL.Query().Has("stdout") {
stdTypes |= docker.STDOUT
}
if r.URL.Query().Has("stderr") {
stdTypes |= docker.STDERR
}
if stdTypes == 0 {
http.Error(w, "stdout or stderr is required", http.StatusBadRequest)
return
}
reader, err := h.clientFromRequest(r).ContainerLogsBetweenDates(r.Context(), id, from, to, stdTypes)
defer reader.Close() defer reader.Close()
if err != nil { if err != nil {
@@ -83,6 +95,19 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
return return
} }
var stdTypes docker.StdType
if r.URL.Query().Has("stdout") {
stdTypes |= docker.STDOUT
}
if r.URL.Query().Has("stderr") {
stdTypes |= docker.STDERR
}
if stdTypes == 0 {
http.Error(w, "stdout or stderr is required", http.StatusBadRequest)
return
}
f, ok := w.(http.Flusher) f, ok := w.(http.Flusher)
if !ok { if !ok {
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError) http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
@@ -106,7 +131,7 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
lastEventId = r.URL.Query().Get("lastEventId") lastEventId = r.URL.Query().Get("lastEventId")
} }
reader, err := h.clientFromRequest(r).ContainerLogs(r.Context(), container.ID, lastEventId) reader, err := h.clientFromRequest(r).ContainerLogs(r.Context(), container.ID, lastEventId, stdTypes)
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
fmt.Fprintf(w, "event: container-stopped\ndata: end of stream\n\n") fmt.Fprintf(w, "event: container-stopped\ndata: end of stream\n\n")

View File

@@ -3,8 +3,9 @@ package web
import ( import (
"fmt" "fmt"
"html/template" "html/template"
"io"
"io/fs" "io/fs"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path" "path"
@@ -118,7 +119,7 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)
} }
bytes, err := ioutil.ReadAll(file) bytes, err := io.ReadAll(file)
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)
} }

View File

@@ -5,7 +5,6 @@ import (
"io" "io"
"io/ioutil"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@@ -112,7 +111,7 @@ func Test_createRoutes_username_password(t *testing.T) {
func Test_createRoutes_username_password_invalid(t *testing.T) { func Test_createRoutes_username_password_invalid(t *testing.T) {
handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"}) handler := createHandler(nil, nil, Config{Base: "/", Username: "amir", Password: "password"})
req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil) req, err := http.NewRequest("GET", "/api/logs/stream?id=123&stdout=1&stderr=1", nil)
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req) handler.ServeHTTP(rr, req)
@@ -179,11 +178,11 @@ func Test_createRoutes_username_password_login_failed(t *testing.T) {
func Test_createRoutes_username_password_valid_session(t *testing.T) { func Test_createRoutes_username_password_valid_session(t *testing.T) {
mockedClient := new(MockedClient) mockedClient := new(MockedClient)
mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil) mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil)
mockedClient.On("ContainerLogs", mock.Anything, "123", "").Return(ioutil.NopCloser(strings.NewReader("test data")), io.EOF) mockedClient.On("ContainerLogs", mock.Anything, "123", "", docker.STDALL).Return(io.NopCloser(strings.NewReader("test data")), io.EOF)
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"}) handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"})
// Get cookie first // Get cookie first
req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil) req, err := http.NewRequest("GET", "/api/logs/stream?id=123&stdout=1&stderr=1", nil)
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")
session, _ := store.Get(req, sessionName) session, _ := store.Get(req, sessionName)
session.Values[authorityKey] = time.Now().Unix() session.Values[authorityKey] = time.Now().Unix()
@@ -192,7 +191,7 @@ func Test_createRoutes_username_password_valid_session(t *testing.T) {
cookies := recorder.Result().Cookies() cookies := recorder.Result().Cookies()
// Test with cookie // Test with cookie
req, err = http.NewRequest("GET", "/api/logs/stream?id=123", nil) req, err = http.NewRequest("GET", "/api/logs/stream?id=123&stdout=1&stderr=1", nil)
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")
req.AddCookie(cookies[0]) req.AddCookie(cookies[0])
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
@@ -203,9 +202,9 @@ func Test_createRoutes_username_password_valid_session(t *testing.T) {
func Test_createRoutes_username_password_invalid_session(t *testing.T) { func Test_createRoutes_username_password_invalid_session(t *testing.T) {
mockedClient := new(MockedClient) mockedClient := new(MockedClient)
mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil) mockedClient.On("FindContainer", "123").Return(docker.Container{ID: "123"}, nil)
mockedClient.On("ContainerLogs", mock.Anything, "since").Return(ioutil.NopCloser(strings.NewReader("test data")), io.EOF) mockedClient.On("ContainerLogs", mock.Anything, "since", docker.STDALL).Return(io.NopCloser(strings.NewReader("test data")), io.EOF)
handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"}) handler := createHandler(mockedClient, nil, Config{Base: "/", Username: "amir", Password: "password"})
req, err := http.NewRequest("GET", "/api/logs/stream?id=123", nil) req, err := http.NewRequest("GET", "/api/logs/stream?id=123&stdout=1&stderr=1", nil)
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")
req.AddCookie(&http.Cookie{Name: "session", Value: "baddata"}) req.AddCookie(&http.Cookie{Name: "session", Value: "baddata"})
rr := httptest.NewRecorder() rr := httptest.NewRecorder()

View File

@@ -6,7 +6,6 @@ import (
"io" "io"
"time" "time"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
@@ -23,13 +22,15 @@ func Test_handler_streamLogs_happy(t *testing.T) {
req, err := http.NewRequest("GET", "/api/logs/stream", nil) req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query() q := req.URL.Query()
q.Add("id", id) q.Add("id", id)
q.Add("stdout", "true")
q.Add("stderr", "true")
req.URL.RawQuery = q.Encode() req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient) mockedClient := new(MockedClient)
reader := ioutil.NopCloser(strings.NewReader("INFO Testing logs...")) reader := io.NopCloser(strings.NewReader("OUTINFO Testing logs..."))
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "").Return(reader, nil) mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "", docker.STDALL).Return(reader, nil)
clients := map[string]docker.Client{ clients := map[string]docker.Client{
"localhost": mockedClient, "localhost": mockedClient,
@@ -47,13 +48,15 @@ func Test_handler_streamLogs_happy_with_id(t *testing.T) {
req, err := http.NewRequest("GET", "/api/logs/stream", nil) req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query() q := req.URL.Query()
q.Add("id", id) q.Add("id", id)
q.Add("stdout", "true")
q.Add("stderr", "true")
req.URL.RawQuery = q.Encode() req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient) mockedClient := new(MockedClient)
reader := ioutil.NopCloser(strings.NewReader("2020-05-13T18:55:37.772853839Z INFO Testing logs...")) reader := io.NopCloser(strings.NewReader("OUT2020-05-13T18:55:37.772853839Z INFO Testing logs..."))
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "").Return(reader, nil) mockedClient.On("ContainerLogs", mock.Anything, mock.Anything, "", docker.STDALL).Return(reader, nil)
clients := map[string]docker.Client{ clients := map[string]docker.Client{
"localhost": mockedClient, "localhost": mockedClient,
@@ -71,12 +74,14 @@ func Test_handler_streamLogs_happy_container_stopped(t *testing.T) {
req, err := http.NewRequest("GET", "/api/logs/stream", nil) req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query() q := req.URL.Query()
q.Add("id", id) q.Add("id", id)
q.Add("stdout", "true")
q.Add("stderr", "true")
req.URL.RawQuery = q.Encode() req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient) mockedClient := new(MockedClient)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
mockedClient.On("ContainerLogs", mock.Anything, id, "").Return(ioutil.NopCloser(strings.NewReader("")), io.EOF) mockedClient.On("ContainerLogs", mock.Anything, id, "", docker.STDALL).Return(io.NopCloser(strings.NewReader("")), io.EOF)
clients := map[string]docker.Client{ clients := map[string]docker.Client{
"localhost": mockedClient, "localhost": mockedClient,
@@ -94,6 +99,8 @@ func Test_handler_streamLogs_error_finding_container(t *testing.T) {
req, err := http.NewRequest("GET", "/api/logs/stream", nil) req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query() q := req.URL.Query()
q.Add("id", id) q.Add("id", id)
q.Add("stdout", "true")
q.Add("stderr", "true")
req.URL.RawQuery = q.Encode() req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")
@@ -116,12 +123,35 @@ func Test_handler_streamLogs_error_reading(t *testing.T) {
req, err := http.NewRequest("GET", "/api/logs/stream", nil) req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query() q := req.URL.Query()
q.Add("id", id) q.Add("id", id)
q.Add("stdout", "true")
q.Add("stderr", "true")
req.URL.RawQuery = q.Encode() req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.") require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient) mockedClient := new(MockedClient)
mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil) mockedClient.On("FindContainer", id).Return(docker.Container{ID: id}, nil)
mockedClient.On("ContainerLogs", mock.Anything, id, "").Return(ioutil.NopCloser(strings.NewReader("")), errors.New("test error")) mockedClient.On("ContainerLogs", mock.Anything, id, "", docker.STDALL).Return(io.NopCloser(strings.NewReader("")), errors.New("test error"))
clients := map[string]docker.Client{
"localhost": mockedClient,
}
h := handler{clients: clients, config: &Config{}}
handler := http.HandlerFunc(h.streamLogs)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
abide.AssertHTTPResponse(t, t.Name(), rr.Result())
mockedClient.AssertExpectations(t)
}
func Test_handler_streamLogs_error_std(t *testing.T) {
id := "123456"
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query()
q.Add("id", id)
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
clients := map[string]docker.Client{ clients := map[string]docker.Client{
"localhost": mockedClient, "localhost": mockedClient,
@@ -232,11 +262,13 @@ func Test_handler_between_dates(t *testing.T) {
q.Add("from", from.Format(time.RFC3339)) q.Add("from", from.Format(time.RFC3339))
q.Add("to", to.Format(time.RFC3339)) q.Add("to", to.Format(time.RFC3339))
q.Add("id", "123456") q.Add("id", "123456")
q.Add("stdout", "true")
q.Add("stderr", "true")
req.URL.RawQuery = q.Encode() req.URL.RawQuery = q.Encode()
mockedClient := new(MockedClient) mockedClient := new(MockedClient)
reader := ioutil.NopCloser(strings.NewReader("2020-05-13T18:55:37.772853839Z INFO Testing logs...\n2020-05-13T18:55:37.772853839Z INFO Testing logs...\n")) reader := io.NopCloser(strings.NewReader("OUT2020-05-13T18:55:37.772853839Z INFO Testing logs...\nERR2020-05-13T18:55:37.772853839Z INFO Testing logs...\n"))
mockedClient.On("ContainerLogsBetweenDates", mock.Anything, "123456", from, to).Return(reader, nil) mockedClient.On("ContainerLogsBetweenDates", mock.Anything, "123456", from, to, docker.STDALL).Return(reader, nil)
clients := map[string]docker.Client{ clients := map[string]docker.Client{
"localhost": mockedClient, "localhost": mockedClient,

View File

@@ -31,8 +31,8 @@ func (m *MockedClient) ListContainers() ([]docker.Container, error) {
return args.Get(0).([]docker.Container), args.Error(1) return args.Get(0).([]docker.Container), args.Error(1)
} }
func (m *MockedClient) ContainerLogs(ctx context.Context, id string, since string) (io.ReadCloser, error) { func (m *MockedClient) ContainerLogs(ctx context.Context, id string, since string, stdType docker.StdType) (io.ReadCloser, error) {
args := m.Called(ctx, id, since) args := m.Called(ctx, id, since, stdType)
return args.Get(0).(io.ReadCloser), args.Error(1) return args.Get(0).(io.ReadCloser), args.Error(1)
} }
@@ -54,8 +54,8 @@ func (m *MockedClient) ContainerStats(context.Context, string, chan<- docker.Con
return nil return nil
} }
func (m *MockedClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time) (io.ReadCloser, error) { func (m *MockedClient) ContainerLogsBetweenDates(ctx context.Context, id string, from time.Time, to time.Time, stdType docker.StdType) (io.ReadCloser, error) {
args := m.Called(ctx, id, from, to) args := m.Called(ctx, id, from, to, stdType)
return args.Get(0).(io.ReadCloser), args.Error(1) return args.Get(0).(io.ReadCloser), args.Error(1)
} }