1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-21 13:23:07 +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 onUpdated: typeof import('vue')['onUpdated']
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 reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
@@ -121,6 +121,7 @@ declare global {
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const showAllContainers: typeof import('./composables/settings')['showAllContainers']
const showStd: typeof import('./composables/settings')['showStd']
const showTimestamp: typeof import('./composables/settings')['showTimestamp']
const size: typeof import('./composables/settings')['size']
const smallerScrollbars: typeof import('./composables/settings')['smallerScrollbars']
@@ -428,7 +429,7 @@ declare module 'vue' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
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 reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
@@ -456,6 +457,7 @@ declare module 'vue' {
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
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 size: UnwrapRef<typeof import('./composables/settings')['size']>
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 onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
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 reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
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 shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
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 size: UnwrapRef<typeof import('./composables/settings')['size']>
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']
LogEventSource: typeof import('./components/LogViewer/LogEventSource.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']
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:lightChevronDoubleDown': typeof import('~icons/mdi-light/chevron-double-down')['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']
StatMonitor: typeof import('./components/LogViewer/StatMonitor.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']
}
}

View File

@@ -1,5 +1,8 @@
<template>
<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">
<log-date :date="logEntry.date"></log-date>
</div>

View File

@@ -5,7 +5,7 @@
</div>
<div class="column is-ellipsis">
{{ 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>
</template>

View File

@@ -1,6 +1,6 @@
<template>
<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-left">
<div class="level-item">
@@ -37,6 +37,34 @@
</div>
</div>
</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>
</template>
@@ -47,23 +75,14 @@ import { Container } from "@/models/Container";
const { showSearch } = useSearchFilter();
const { base } = config;
const { onClearClicked = (e: Event) => {} } = defineProps<{
onClearClicked: (e: Event) => void;
}>();
const clear = defineEmit();
const container = inject("container") as ComputedRef<Container>;
const streamConfig = inject("stream-config") as { stdout: boolean; stderr: boolean };
</script>
<style lang="scss" scoped>
#download.button,
#clear.button {
.icon {
height: 80%;
}
&:hover {
color: var(--primary-color);
border-color: var(--primary-color);
}
.level-left .level-item {
width: 2.2em;
}
</style>

View File

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

View File

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

View File

@@ -12,7 +12,8 @@ const emit = defineEmits<{
}>();
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 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>
<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">
<log-date :date="logEntry.date"></log-date>
</div>

View File

@@ -24,6 +24,7 @@ exports[`<LogEventSource /> > render html correctly > should render dates with 1
</div>
</div>
<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-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>
@@ -60,6 +61,7 @@ exports[`<LogEventSource /> > render html correctly > should render dates with 2
</div>
</div>
<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-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>
@@ -96,6 +98,7 @@ exports[`<LogEventSource /> > render html correctly > should render messages 1`]
</div>
</div>
<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-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>
@@ -132,6 +135,7 @@ exports[`<LogEventSource /> > render html correctly > should render messages wit
</div>
</div>
<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-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>
@@ -168,6 +172,7 @@ exports[`<LogEventSource /> > render html correctly > should render messages wit
</div>
</div>
<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-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>
@@ -204,6 +209,7 @@ exports[`<LogEventSource /> > render html correctly > should render messages wit
</div>
</div>
<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-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>
@@ -235,5 +241,6 @@ SimpleLogEntry {
"id": 1,
"level": 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);
}
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 buffer: LogEntry<string | JSONObject>[] = $ref([]);
const scrollingPaused = $ref(inject("scrollingPaused") as Ref<boolean>);
@@ -64,9 +69,20 @@ export function useLogStream(container: ComputedRef<Container>) {
lastEventId = "";
}
es = new EventSource(
`${config.base}/api/logs/stream?id=${container.value.id}&lastEventId=${lastEventId}&host=${sessionHost.value}`
);
const params = {
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?.close();
es = null;
@@ -93,9 +109,22 @@ export function useLogStream(container: ComputedRef<Container>) {
const last = messages[299].date;
const delta = to.getTime() - last.getTime();
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()}`)
).text();
const params = {
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) {
const newMessages = logs
.trim()
@@ -129,5 +158,7 @@ export function useLogStream(container: ComputedRef<Container>) {
{ immediate: true }
);
watch(streamConfig, () => connect());
return { ...$$({ messages }), loadOlderLogs };
}

View File

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

View File

@@ -1,3 +1,9 @@
import { Container } from "@/models/Container";
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 JSONObject = { [x: string]: JSONValue };
export type Position = "start" | "end" | "middle" | undefined;
export type Std = "stdout" | "stderr";
export interface LogEvent {
readonly m: string | JSONObject;
readonly ts: number;
readonly id: number;
readonly l: string;
readonly p: Position;
readonly s: number;
}
export abstract class LogEntry<T extends string | JSONObject> implements HasComponent {
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;
}
@@ -39,9 +47,10 @@ export class SimpleLogEntry extends LogEntry<string> {
id: number,
date: Date,
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 {
return SimpleLogItem;
@@ -56,9 +65,10 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
id: number,
date: Date,
public readonly level: string,
public readonly std: Std,
visibleKeys?: Ref<string[][]>
) {
super(message, id, date, level);
super(message, id, date, std, level);
if (visibleKeys) {
this.filteredMessage = computed(() => {
if (!visibleKeys.value.length) {
@@ -84,13 +94,13 @@ export class ComplexLogEntry extends LogEntry<JSONObject> {
}
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> {
constructor(message: string, date: Date, public readonly event: string) {
super(message, date.getTime(), date, "info");
super(message, date.getTime(), date, "stderr", "info");
}
getComponent(): Component {
return DockerEventLogItem;
@@ -107,7 +117,7 @@ export class SkippedLogsEntry extends LogEntry<string> {
public readonly firstSkipped: LogEntry<string | JSONObject>,
lastSkipped: LogEntry<string | JSONObject>
) {
super("", date.getTime(), date, "info");
super("", date.getTime(), date, "stderr", "info");
this._totalSkipped = totalSkipped;
this.lastSkipped = lastSkipped;
}
@@ -135,8 +145,15 @@ export class SkippedLogsEntry extends LogEntry<string> {
export function asLogEntry(event: LogEvent): LogEntry<string | JSONObject> {
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 {
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">
<o-switch v-model="showTimestamp"> {{ $t("settings.show-timesamps") }} </o-switch>
</div>
<div class="item">
<o-switch v-model="showStd"> {{ $t("settings.show-std") }} </o-switch>
</div>
<div class="item">
<o-switch v-model="softWrap"> {{ $t("settings.soft-wrap") }}</o-switch>
@@ -136,6 +139,7 @@ import {
lightTheme,
smallerScrollbars,
showTimestamp,
showStd,
hourStyle,
showAllContainers,
size,
@@ -155,7 +159,8 @@ async function fetchNextRelease() {
const response = await fetch("https://api.github.com/repos/amir20/dozzle/releases/latest");
if (response.ok) {
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;
}
} else {

View File

@@ -96,13 +96,6 @@ $light-toolbar-color: rgba($grey-darker, 0.7);
--text-light-color: #{$grey};
}
:root {
--green-color: #00b5ad;
--red-color: #f44336;
--purple-color: #9c27b0;
--orange-color: #ff9800;
}
[data-theme="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 {
overflow-x: 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) {
if (bytes === 0) return "0 Bytes";
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]);
}
export function persistentVisibleKeys(container: ComputedRef<Container>) {
return computed(() => useStorage(stripVersion(container.value.image) + ":" + container.value.command, []));
}
export function stripVersion(label: string) {
const [name, _] = label.split(":");
return name;

View File

@@ -27,6 +27,27 @@ type dockerClient struct {
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 {
ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error)
ContainerLogs(context.Context, string, types.ContainerLogsOptions) (io.ReadCloser, error)
@@ -40,9 +61,10 @@ type dockerProxy interface {
type Client interface {
ListContainers() ([]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)
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
Ping(context.Context) (types.Ping, error)
}
@@ -227,8 +249,8 @@ func (d *dockerClient) ContainerStats(ctx context.Context, id string, stats chan
return nil
}
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since string) (io.ReadCloser, error) {
log.WithField("id", id).WithField("since", since).Debug("streaming logs for container")
func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since string, stdType StdType) (io.ReadCloser, error) {
log.WithField("id", id).WithField("since", since).WithField("stdType", stdType).Debug("streaming logs for container")
if since != "" {
if millis, err := strconv.ParseInt(since, 10, 64); err == nil {
@@ -239,8 +261,8 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since strin
}
options := types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
ShowStdout: stdType&STDOUT != 0,
ShowStderr: stdType&STDERR != 0,
Follow: true,
Tail: "300",
Timestamps: true,
@@ -258,7 +280,7 @@ func (d *dockerClient) ContainerLogs(ctx context.Context, id string, since strin
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) {
@@ -290,11 +312,35 @@ func (d *dockerClient) Events(ctx context.Context) (<-chan ContainerEvent, <-cha
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{
ShowStdout: true,
ShowStderr: 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),
Until: to.Format(time.RFC3339),
}
@@ -312,7 +358,7 @@ func (d *dockerClient) ContainerLogsBetweenDates(ctx context.Context, id string,
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) {

View File

@@ -133,7 +133,7 @@ func Test_dockerClient_ContainerLogs_happy(t *testing.T) {
proxy.On("ContainerInspect", mock.Anything, id).Return(json, nil)
client := &dockerClient{proxy, filters.NewArgs()}
logReader, _ := client.ContainerLogs(context.Background(), id, "since")
logReader, _ := client.ContainerLogs(context.Background(), id, "since", STDALL)
actual, _ := io.ReadAll(logReader)
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)
client := &dockerClient{proxy, filters.NewArgs()}
logReader, _ := client.ContainerLogs(context.Background(), id, "")
logReader, _ := client.ContainerLogs(context.Background(), id, "", STDALL)
actual, _ := io.ReadAll(logReader)
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()}
reader, err := client.ContainerLogs(context.Background(), id, "")
reader, err := client.ContainerLogs(context.Background(), id, "", STDALL)
assert.Nil(t, reader, "reader should be nil")
assert.Error(t, err, "error should have been returned")

View File

@@ -93,8 +93,20 @@ func (g *eventGenerator) consume() {
if message != "" {
h := fnv.New32a()
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 {
logId := message[:index]

View File

@@ -12,7 +12,7 @@ import (
func TestNewEventIterator(t *testing.T) {
input := "example input"
reader := bufio.NewReader(strings.NewReader(input))
reader := bufio.NewReader(strings.NewReader("OUT" + input))
generator := NewEventIterator(reader)
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) {
input := "example input"
reader := bufio.NewReader(strings.NewReader(input))
reader := bufio.NewReader(strings.NewReader("OUT" + input))
generator := NewEventIterator(reader)
@@ -31,7 +31,7 @@ func TestEventGenerator_Next(t *testing.T) {
func TestEventGenerator_LastError(t *testing.T) {
input := "example input"
reader := bufio.NewReader(strings.NewReader(input))
reader := bufio.NewReader(strings.NewReader("OUT" + input))
generator := NewEventIterator(reader)
@@ -45,7 +45,7 @@ func TestEventGenerator_LastError(t *testing.T) {
func TestEventGenerator_Peek(t *testing.T) {
input := "example input"
reader := bufio.NewReader(strings.NewReader(input))
reader := bufio.NewReader(strings.NewReader("OUT" + input))
generator := NewEventIterator(reader)

View File

@@ -11,14 +11,16 @@ type logReader struct {
tty bool
lastHeader []byte
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{
reader,
tty,
make([]byte, 8),
bytes.Buffer{},
labelStd,
}
}
@@ -34,6 +36,16 @@ func (r *logReader) Read(p []byte) (n int, err error) {
if err != nil {
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:])
_, err = io.CopyN(&r.buffer, r.readerCloser, int64(count))
if err != nil {

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ toolbar:
clear: Limpiar
download: Descargar
search: Buscar
show: Mostrar {std}
label:
containers: Contenedores
total-containers: Contenedores Totales
@@ -49,3 +50,4 @@ settings:
update-available: >-
¡La nueva versión está disponible! Actualizar a la
<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
download: Descarregar
search: Pesquisa
show: Mostrar {std}
hide: Ocultar {std}
label:
containers: Contentores
total-containers: Contentores Totais
@@ -49,3 +51,4 @@ settings:
update-available: >-
Está disponível uma nova versão! Actualização para
<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: Очистить
download: Скачать
search: Поиск
show: Показать {std}
hide: Скрыть {std}
label:
containers: Контейнеры
total-containers: Всего Контейнеров
@@ -48,3 +50,4 @@ settings:
update-available: >-
Доступна новая версия! Обновить до <a :href="{href}" class="next-release"
target="_blank" rel="noreferrer noopener">{nextVersion}</a>.
show-std: Показывать метки stdout и stderr

View File

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

View File

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

View File

@@ -76,8 +76,8 @@ HTTP/1.1 200 OK
Connection: close
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":2908612274,"l":"info"}
{"m":"INFO Testing logs...","ts":1589396137772,"id":1122614848,"l":"info","s":1}
{"m":"INFO Testing logs...","ts":1589396137772,"id":1543246723,"l":"info","s":2}
/* snapshot: Test_handler_streamEvents_error */
HTTP/1.1 200 OK
@@ -143,6 +143,14 @@ X-Content-Type-Options: nosniff
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 */
HTTP/1.1 200 OK
Connection: close
@@ -152,7 +160,7 @@ Connection: keep-alive
Content-Type: text/event-stream
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
data: end of stream
@@ -178,7 +186,7 @@ Connection: keep-alive
Content-Type: text/event-stream
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
event: container-stopped

View File

@@ -28,7 +28,6 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
}
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-Type", "application/gzip")
@@ -38,7 +37,7 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
zw.Comment = "Logs generated by Dozzle"
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 {
http.Error(w, err.Error(), http.StatusInternalServerError)
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"))
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()
if err != nil {
@@ -83,6 +95,19 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
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)
if !ok {
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")
}
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 == io.EOF {
fmt.Fprintf(w, "event: container-stopped\ndata: end of stream\n\n")

View File

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

View File

@@ -5,7 +5,6 @@ import (
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/http/httptest"
@@ -112,7 +111,7 @@ func Test_createRoutes_username_password(t *testing.T) {
func Test_createRoutes_username_password_invalid(t *testing.T) {
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.")
rr := httptest.NewRecorder()
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) {
mockedClient := new(MockedClient)
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"})
// 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.")
session, _ := store.Get(req, sessionName)
session.Values[authorityKey] = time.Now().Unix()
@@ -192,7 +191,7 @@ func Test_createRoutes_username_password_valid_session(t *testing.T) {
cookies := recorder.Result().Cookies()
// 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.")
req.AddCookie(cookies[0])
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) {
mockedClient := new(MockedClient)
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"})
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.")
req.AddCookie(&http.Cookie{Name: "session", Value: "baddata"})
rr := httptest.NewRecorder()

View File

@@ -6,7 +6,6 @@ import (
"io"
"time"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
@@ -23,13 +22,15 @@ func Test_handler_streamLogs_happy(t *testing.T) {
req, err := http.NewRequest("GET", "/api/logs/stream", nil)
q := req.URL.Query()
q.Add("id", id)
q.Add("stdout", "true")
q.Add("stderr", "true")
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
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("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{
"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)
q := req.URL.Query()
q.Add("id", id)
q.Add("stdout", "true")
q.Add("stderr", "true")
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
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("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{
"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)
q := req.URL.Query()
q.Add("id", id)
q.Add("stdout", "true")
q.Add("stderr", "true")
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
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{
"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)
q := req.URL.Query()
q.Add("id", id)
q.Add("stdout", "true")
q.Add("stderr", "true")
req.URL.RawQuery = q.Encode()
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)
q := req.URL.Query()
q.Add("id", id)
q.Add("stdout", "true")
q.Add("stderr", "true")
req.URL.RawQuery = q.Encode()
require.NoError(t, err, "NewRequest should not return an error.")
mockedClient := new(MockedClient)
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{
"localhost": mockedClient,
@@ -232,11 +262,13 @@ func Test_handler_between_dates(t *testing.T) {
q.Add("from", from.Format(time.RFC3339))
q.Add("to", to.Format(time.RFC3339))
q.Add("id", "123456")
q.Add("stdout", "true")
q.Add("stderr", "true")
req.URL.RawQuery = q.Encode()
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"))
mockedClient.On("ContainerLogsBetweenDates", mock.Anything, "123456", from, to).Return(reader, nil)
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, docker.STDALL).Return(reader, nil)
clients := map[string]docker.Client{
"localhost": mockedClient,

View File

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