mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 21:33:18 +01:00
Support for JSON logs (#1759)
* WIP for using json all the time * Updates to render * adds a new component for json * Updates styles * Adds nesting * Adds field list * Adds expanding * Adds new composable for event source * Creates an add button * Removes unused code * Adds and removes fields with defaults * Fixes jumping when adding new fields * Returns JSON correctly * Fixes little bugs * Fixes js tests * Adds vscode * Fixes json buffer error * Fixes extra line * Fixes tests * Fixes tests and adds support for search * Refactors visible payload keys to a composable * Fixes typescript errors and refactors * Fixes visible keys by ComputedRef<Ref> * Fixes search bugs * Updates tests * Fixes go tests * Fixes scroll view * Fixes vue tsc errors * Fixes EOF error * Fixes build error * Uses application/ld+json * Fixes arrays and records * Marks for json too
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ static
|
|||||||
dozzle
|
dozzle
|
||||||
coverage
|
coverage
|
||||||
.pnpm-debug.log
|
.pnpm-debug.log
|
||||||
|
.vscode
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ function showFuzzySearch() {
|
|||||||
active: true,
|
active: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function onResized(e) {
|
function onResized(e: any) {
|
||||||
if (e.length == 2) {
|
if (e.length == 2) {
|
||||||
menuWidth.value = e[0].size;
|
menuWidth.value = e[0].size;
|
||||||
}
|
}
|
||||||
|
|||||||
2
assets/components.d.ts
vendored
2
assets/components.d.ts
vendored
@@ -13,8 +13,10 @@ declare module '@vue/runtime-core' {
|
|||||||
ContainerStat: typeof import('./components/ContainerStat.vue')['default']
|
ContainerStat: typeof import('./components/ContainerStat.vue')['default']
|
||||||
ContainerTitle: typeof import('./components/ContainerTitle.vue')['default']
|
ContainerTitle: typeof import('./components/ContainerTitle.vue')['default']
|
||||||
DropdownMenu: typeof import('./components/DropdownMenu.vue')['default']
|
DropdownMenu: typeof import('./components/DropdownMenu.vue')['default']
|
||||||
|
FieldList: typeof import('./components/FieldList.vue')['default']
|
||||||
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
|
FuzzySearchModal: typeof import('./components/FuzzySearchModal.vue')['default']
|
||||||
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
|
InfiniteLoader: typeof import('./components/InfiniteLoader.vue')['default']
|
||||||
|
JSONPayload: typeof import('./components/JSONPayload.vue')['default']
|
||||||
LogActionsToolbar: typeof import('./components/LogActionsToolbar.vue')['default']
|
LogActionsToolbar: typeof import('./components/LogActionsToolbar.vue')['default']
|
||||||
LogContainer: typeof import('./components/LogContainer.vue')['default']
|
LogContainer: typeof import('./components/LogContainer.vue')['default']
|
||||||
LogEventSource: typeof import('./components/LogEventSource.vue')['default']
|
LogEventSource: typeof import('./components/LogEventSource.vue')['default']
|
||||||
|
|||||||
@@ -1,34 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="is-size-7 is-uppercase columns is-marginless is-mobile">
|
<div class="is-size-7 is-uppercase columns is-marginless is-mobile" v-if="container.stat">
|
||||||
<div class="column is-narrow has-text-weight-bold">
|
<div class="column is-narrow has-text-weight-bold">
|
||||||
{{ state }}
|
{{ container.state }}
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow" v-if="stat.memoryUsage !== null">
|
<div class="column is-narrow" v-if="container.stat.memoryUsage !== null">
|
||||||
<span class="has-text-weight-light has-spacer">mem</span>
|
<span class="has-text-weight-light has-spacer">mem</span>
|
||||||
<span class="has-text-weight-bold">
|
<span class="has-text-weight-bold">
|
||||||
{{ formatBytes(stat.memoryUsage) }}
|
{{ formatBytes(container.stat.memoryUsage) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-narrow" v-if="stat.cpu !== null">
|
<div class="column is-narrow" v-if="container.stat.cpu !== null">
|
||||||
<span class="has-text-weight-light has-spacer">load</span>
|
<span class="has-text-weight-light has-spacer">load</span>
|
||||||
<span class="has-text-weight-bold"> {{ stat.cpu }}% </span>
|
<span class="has-text-weight-bold"> {{ container.stat.cpu }}% </span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ContainerStat } from "@/types/Container";
|
import { Container } from "@/types/Container";
|
||||||
import { PropType } from "vue";
|
import { ComputedRef, inject } from "vue";
|
||||||
import { formatBytes } from "@/utils";
|
import { formatBytes } from "@/utils";
|
||||||
|
|
||||||
defineProps({
|
const container = inject("container") as ComputedRef<Container>;
|
||||||
stat: {
|
|
||||||
type: Object as PropType<ContainerStat>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
state: String,
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -9,13 +9,8 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Container } from "@/types/Container";
|
import { Container } from "@/types/Container";
|
||||||
import { PropType } from "vue";
|
import { inject, ComputedRef } from "vue";
|
||||||
defineProps({
|
const container = inject("container") as ComputedRef<Container>;
|
||||||
container: {
|
|
||||||
type: Object as PropType<Container>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|||||||
84
assets/components/FieldList.vue
Normal file
84
assets/components/FieldList.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<ul v-if="expanded" ref="root">
|
||||||
|
<li v-for="(value, name) in fields">
|
||||||
|
<template v-if="isObject(value)">
|
||||||
|
<span class="has-text-grey">{{ name }}=</span>
|
||||||
|
<field-list
|
||||||
|
:fields="value"
|
||||||
|
:parent-key="parentKey.concat(name)"
|
||||||
|
:visible-keys="visibleKeys"
|
||||||
|
expanded
|
||||||
|
></field-list>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="Array.isArray(value)">
|
||||||
|
<a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }} </a>
|
||||||
|
<span class="has-text-grey">{{ name }}=</span>[
|
||||||
|
<span class="has-text-weight-bold" v-for="(item, index) in value">
|
||||||
|
{{ item }}
|
||||||
|
<span v-if="index !== value.length - 1">,</span>
|
||||||
|
</span>
|
||||||
|
]
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a @click="toggleField(name)"> {{ hasField(name) ? "remove" : "add" }} </a>
|
||||||
|
<span class="has-text-grey">{{ name }}=</span><span class="has-text-weight-bold">{{ value }}</span>
|
||||||
|
</template>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { arrayEquals, isObject } from "@/utils";
|
||||||
|
import { nextTick, PropType, ref, toRaw } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
fields: {
|
||||||
|
type: Object as PropType<Record<string, any>>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
expanded: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
parentKey: {
|
||||||
|
type: Array as PropType<string[]>,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
visibleKeys: {
|
||||||
|
type: Array as PropType<string[][]>,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const root = ref<HTMLElement>();
|
||||||
|
|
||||||
|
async function toggleField(field: string) {
|
||||||
|
const index = fieldIndex(field);
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
props.visibleKeys.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
props.visibleKeys.push(props.parentKey.concat(field));
|
||||||
|
}
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
root.value?.scrollIntoView({
|
||||||
|
block: "center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasField(field: string) {
|
||||||
|
return fieldIndex(field) > -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fieldIndex(field: string) {
|
||||||
|
const path = props.parentKey.concat(field);
|
||||||
|
return props.visibleKeys.findIndex((keys) => arrayEquals(toRaw(keys), toRaw(path)));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
ul {
|
||||||
|
margin-left: 2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -22,7 +22,7 @@ const root = ref<HTMLElement>();
|
|||||||
const observer = new IntersectionObserver(async (entries) => {
|
const observer = new IntersectionObserver(async (entries) => {
|
||||||
if (entries[0].intersectionRatio <= 0) return;
|
if (entries[0].intersectionRatio <= 0) return;
|
||||||
if (props.onLoadMore && props.enabled) {
|
if (props.onLoadMore && props.enabled) {
|
||||||
const scrollingParent = root.value.closest("[data-scrolling]") || document.documentElement;
|
const scrollingParent = root.value?.closest("[data-scrolling]") || document.documentElement;
|
||||||
const previousHeight = scrollingParent.scrollHeight;
|
const previousHeight = scrollingParent.scrollHeight;
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
await props.onLoadMore();
|
await props.onLoadMore();
|
||||||
@@ -32,7 +32,7 @@ const observer = new IntersectionObserver(async (entries) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => observer.observe(root.value));
|
onMounted(() => observer.observe(root.value!));
|
||||||
onUnmounted(() => observer.disconnect());
|
onUnmounted(() => observer.disconnect());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
55
assets/components/JSONPayload.vue
Normal file
55
assets/components/JSONPayload.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<ul class="fields" @click="expanded = !expanded">
|
||||||
|
<li v-for="(value, name) in logEntry.payload">
|
||||||
|
<template v-if="value">
|
||||||
|
<span class="has-text-grey">{{ name }}=</span>
|
||||||
|
<span class="has-text-weight-bold" v-html="markSearch(value)"></span>
|
||||||
|
</template>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<field-list :fields="logEntry.unfilteredPayload" :expanded="expanded" :visible-keys="visibleKeys"></field-list>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useSearchFilter } from "@/composables/search";
|
||||||
|
import { VisibleLogEntry } from "@/types/VisibleLogEntry";
|
||||||
|
|
||||||
|
import { PropType, ref } from "vue";
|
||||||
|
|
||||||
|
const { markSearch } = useSearchFilter();
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
logEntry: {
|
||||||
|
type: Object as PropType<VisibleLogEntry>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
visibleKeys: {
|
||||||
|
type: Array as PropType<string[][]>,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const expanded = ref(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.fields {
|
||||||
|
display: inline-block;
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
&::after {
|
||||||
|
content: "expand json";
|
||||||
|
color: var(--secondary-color);
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -41,11 +41,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted, PropType } from "vue";
|
import { inject, onMounted, onUnmounted, PropType, ComputedRef } from "vue";
|
||||||
import hotkeys from "hotkeys-js";
|
import hotkeys from "hotkeys-js";
|
||||||
import config from "@/stores/config";
|
import config from "@/stores/config";
|
||||||
import { Container } from "@/types/Container";
|
|
||||||
import { useSearchFilter } from "@/composables/search";
|
import { useSearchFilter } from "@/composables/search";
|
||||||
|
import { Container } from "@/types/Container";
|
||||||
|
|
||||||
const { showSearch } = useSearchFilter();
|
const { showSearch } = useSearchFilter();
|
||||||
|
|
||||||
@@ -56,10 +56,6 @@ const props = defineProps({
|
|||||||
type: Function as PropType<(e: Event) => void>,
|
type: Function as PropType<(e: Event) => void>,
|
||||||
default: (e: Event) => {},
|
default: (e: Event) => {},
|
||||||
},
|
},
|
||||||
container: {
|
|
||||||
type: Object as () => Container,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const onHotkey = (event: Event) => {
|
const onHotkey = (event: Event) => {
|
||||||
@@ -67,6 +63,8 @@ const onHotkey = (event: Event) => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const container = inject("container") as ComputedRef<Container>;
|
||||||
|
|
||||||
onMounted(() => hotkeys("shift+command+l, shift+ctrl+l", onHotkey));
|
onMounted(() => hotkeys("shift+command+l, shift+ctrl+l", onHotkey));
|
||||||
onUnmounted(() => hotkeys.unbind("shift+command+l, shift+ctrl+l", onHotkey));
|
onUnmounted(() => hotkeys.unbind("shift+command+l, shift+ctrl+l", onHotkey));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
<template #header v-if="showTitle">
|
<template #header v-if="showTitle">
|
||||||
<div class="mr-0 columns is-vcentered is-marginless is-hidden-mobile">
|
<div class="mr-0 columns is-vcentered is-marginless is-hidden-mobile">
|
||||||
<div class="column is-clipped is-paddingless">
|
<div class="column is-clipped is-paddingless">
|
||||||
<container-title :container="container" @close="$emit('close')" />
|
<container-title @close="$emit('close')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-narrow is-paddingless">
|
<div class="column is-narrow is-paddingless">
|
||||||
<container-stat :stat="container.stat" :state="container.state" v-if="container.stat" />
|
<container-stat v-if="container.stat" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mr-2 column is-narrow is-paddingless">
|
<div class="mr-2 column is-narrow is-paddingless">
|
||||||
<log-actions-toolbar :container="container" :onClearClicked="onClearClicked" />
|
<log-actions-toolbar :onClearClicked="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>
|
||||||
@@ -18,13 +18,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ setLoading }">
|
<template #default="{ setLoading }">
|
||||||
<log-viewer-with-source ref="viewer" :id="id" @loading-more="setLoading($event)" />
|
<log-viewer-with-source ref="viewer" @loading-more="setLoading($event)" />
|
||||||
</template>
|
</template>
|
||||||
</scrollable-view>
|
</scrollable-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, toRefs } from "vue";
|
import { provide, ref, toRefs } from "vue";
|
||||||
import LogViewerWithSource from "./LogViewerWithSource.vue";
|
import LogViewerWithSource from "./LogViewerWithSource.vue";
|
||||||
import { useContainerStore } from "@/stores/container";
|
import { useContainerStore } from "@/stores/container";
|
||||||
|
|
||||||
@@ -54,6 +54,8 @@ const store = useContainerStore();
|
|||||||
|
|
||||||
const container = store.currentContainer(id);
|
const container = store.currentContainer(id);
|
||||||
|
|
||||||
|
provide("container", container);
|
||||||
|
|
||||||
const viewer = ref<InstanceType<typeof LogViewerWithSource>>();
|
const viewer = ref<InstanceType<typeof LogViewerWithSource>>();
|
||||||
|
|
||||||
function onClearClicked() {
|
function onClearClicked() {
|
||||||
|
|||||||
@@ -18,17 +18,6 @@ vi.mock("lodash.debounce", () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/stores/container", () => ({
|
|
||||||
__esModule: true,
|
|
||||||
useContainerStore() {
|
|
||||||
return {
|
|
||||||
currentContainer(id: Ref<string>) {
|
|
||||||
return computed(() => ({ id: id.value }));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/stores/config", () => ({
|
vi.mock("@/stores/config", () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: { base: "" },
|
default: { base: "" },
|
||||||
@@ -78,13 +67,16 @@ describe("<LogEventSource />", () => {
|
|||||||
components: {
|
components: {
|
||||||
LogViewer,
|
LogViewer,
|
||||||
},
|
},
|
||||||
|
provide: {
|
||||||
|
container: computed(() => ({ id: "abc", image: "test:v123" })),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
slots: {
|
slots: {
|
||||||
default: `
|
default: `
|
||||||
<template #scoped="params"><log-viewer :messages="params.messages"></log-viewer></template>
|
<template #scoped="params"><log-viewer :messages="params.messages"></log-viewer></template>
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
props: { id: "abc" },
|
props: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,41 +103,11 @@ describe("<LogEventSource />", () => {
|
|||||||
const wrapper = createLogEventSource();
|
const wrapper = createLogEventSource();
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||||
data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
|
data: `{"ts":1560336942.459, "m":"This is a message.", "id":1}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [message, _] = wrapper.vm.messages;
|
const [message, _] = wrapper.vm.messages;
|
||||||
const { key, ...messageWithoutKey } = message;
|
expect(message).toMatchSnapshot();
|
||||||
|
|
||||||
expect(key).toBe("2019-06-12T10:55:42.459034602Z");
|
|
||||||
expect(messageWithoutKey).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should parse messages with loki's timestamp format", async () => {
|
|
||||||
const wrapper = createLogEventSource();
|
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({ data: `2020-04-27T12:35:43.272974324+02:00 xxxxx` });
|
|
||||||
|
|
||||||
const [message, _] = wrapper.vm.messages;
|
|
||||||
const { key, ...messageWithoutKey } = message;
|
|
||||||
|
|
||||||
expect(key).toBe("2020-04-27T12:35:43.272974324+02:00");
|
|
||||||
expect(messageWithoutKey).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should pass messages to slot", async () => {
|
|
||||||
const wrapper = createLogEventSource();
|
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
|
||||||
data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
|
|
||||||
});
|
|
||||||
const [message, _] = wrapper.getComponent(LogViewer).vm.messages;
|
|
||||||
|
|
||||||
const { key, ...messageWithoutKey } = message;
|
|
||||||
|
|
||||||
expect(key).toBe("2019-06-12T10:55:42.459034602Z");
|
|
||||||
|
|
||||||
expect(messageWithoutKey).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("render html correctly", () => {
|
describe("render html correctly", () => {
|
||||||
@@ -169,7 +131,7 @@ describe("<LogEventSource />", () => {
|
|||||||
const wrapper = createLogEventSource();
|
const wrapper = createLogEventSource();
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||||
data: `2019-06-12T10:55:42.459034602Z "This is a message."`,
|
data: `{"ts":1560336942.459, "m":"This is a message.", "id":1}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
@@ -180,7 +142,7 @@ describe("<LogEventSource />", () => {
|
|||||||
const wrapper = createLogEventSource();
|
const wrapper = createLogEventSource();
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||||
data: `2019-06-12T10:55:42.459034602Z \x1b[30mblack\x1b[37mwhite`,
|
data: '{"ts":1560336942.459,"m":"\\u001b[30mblack\\u001b[37mwhite", "id":1}',
|
||||||
});
|
});
|
||||||
|
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
@@ -191,7 +153,7 @@ describe("<LogEventSource />", () => {
|
|||||||
const wrapper = createLogEventSource();
|
const wrapper = createLogEventSource();
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||||
data: `2019-06-12T10:55:42.459034602Z <test>foo bar</test>`,
|
data: `{"ts":1560336942.459, "m":"<test>foo bar</test>", "id":1}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
@@ -202,7 +164,7 @@ describe("<LogEventSource />", () => {
|
|||||||
const wrapper = createLogEventSource({ hourStyle: "12" });
|
const wrapper = createLogEventSource({ hourStyle: "12" });
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||||
data: `2019-06-12T23:55:42.459034602Z <test>foo bar</test>`,
|
data: `{"ts":1560336942.459, "m":"<test>foo bar</test>", "id":1}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
@@ -213,7 +175,7 @@ describe("<LogEventSource />", () => {
|
|||||||
const wrapper = createLogEventSource({ hourStyle: "24" });
|
const wrapper = createLogEventSource({ hourStyle: "24" });
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||||
data: `2019-06-12T23:55:42.459034602Z <test>foo bar</test>`,
|
data: `{"ts":1560336942.459, "m":"<test>foo bar</test>", "id":1}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
@@ -224,10 +186,10 @@ describe("<LogEventSource />", () => {
|
|||||||
const wrapper = createLogEventSource({ searchFilter: "test" });
|
const wrapper = createLogEventSource({ searchFilter: "test" });
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
sources["/api/logs/stream?id=abc&lastEventId="].emitOpen();
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||||
data: `2019-06-11T10:55:42.459034602Z Foo bar`,
|
data: `{"ts":1560336942.459, "m":"<test>foo bar</test>", "id":1}`,
|
||||||
});
|
});
|
||||||
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
sources["/api/logs/stream?id=abc&lastEventId="].emitMessage({
|
||||||
data: `2019-06-12T10:55:42.459034602Z This is a test <hi></hi>`,
|
data: `{"ts":1560336942.459, "m":"<test>test bar</test>", "id":1}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
|
|||||||
@@ -1,133 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<infinite-loader :onLoadMore="loadOlderLogs" :enabled="messages.length > 100"></infinite-loader>
|
<infinite-loader :onLoadMore="fetchMore" :enabled="messages.length > 100"></infinite-loader>
|
||||||
<slot :messages="messages"></slot>
|
<slot :messages="messages"></slot>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, ref, watch, onUnmounted } from "vue";
|
import { useEventSource } from "@/composables/eventsource";
|
||||||
import debounce from "lodash.debounce";
|
import { Container } from "@/types/Container";
|
||||||
|
import { inject, ComputedRef } from "vue";
|
||||||
|
|
||||||
import { LogEntry } from "@/types/LogEntry";
|
|
||||||
import InfiniteLoader from "./InfiniteLoader.vue";
|
|
||||||
import config from "@/stores/config";
|
|
||||||
import { useContainerStore } from "@/stores/container";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { id } = toRefs(props);
|
|
||||||
const emit = defineEmits(["loading-more"]);
|
const emit = defineEmits(["loading-more"]);
|
||||||
const store = useContainerStore();
|
const container = inject("container") as ComputedRef<Container>;
|
||||||
const container = store.currentContainer(id);
|
const { connect, messages, loadOlderLogs } = useEventSource(container);
|
||||||
|
|
||||||
const messages = ref<LogEntry[]>([]);
|
const beforeLoading = () => emit("loading-more", true);
|
||||||
const buffer = ref<LogEntry[]>([]);
|
const afterLoading = () => emit("loading-more", false);
|
||||||
|
|
||||||
function flushNow() {
|
|
||||||
messages.value.push(...buffer.value);
|
|
||||||
buffer.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const flushBuffer = debounce(flushNow, 250, { maxWait: 1000 });
|
|
||||||
|
|
||||||
let es: EventSource | null = null;
|
|
||||||
let lastEventId = "";
|
|
||||||
|
|
||||||
function connect({ clear } = { clear: true }) {
|
|
||||||
es?.close();
|
|
||||||
|
|
||||||
if (clear) {
|
|
||||||
flushBuffer.cancel();
|
|
||||||
messages.value = [];
|
|
||||||
buffer.value = [];
|
|
||||||
lastEventId = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
es = new EventSource(`${config.base}/api/logs/stream?id=${props.id}&lastEventId=${lastEventId}`);
|
|
||||||
es.addEventListener("container-stopped", () => {
|
|
||||||
es?.close();
|
|
||||||
es = null;
|
|
||||||
buffer.value.push({
|
|
||||||
event: "container-stopped",
|
|
||||||
message: "Container stopped",
|
|
||||||
date: new Date(),
|
|
||||||
key: new Date().toString(),
|
|
||||||
});
|
|
||||||
flushBuffer();
|
|
||||||
flushBuffer.flush();
|
|
||||||
});
|
|
||||||
es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
|
|
||||||
es.onmessage = (e) => {
|
|
||||||
lastEventId = e.lastEventId;
|
|
||||||
if (e.data) {
|
|
||||||
buffer.value.push(parseMessage(e.data));
|
|
||||||
flushBuffer();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadOlderLogs() {
|
|
||||||
if (messages.value.length < 300) return;
|
|
||||||
|
|
||||||
emit("loading-more", true);
|
|
||||||
const to = messages.value[0].date;
|
|
||||||
const last = messages.value[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=${props.id}&from=${from.toISOString()}&to=${to.toISOString()}`)
|
|
||||||
).text();
|
|
||||||
if (logs) {
|
|
||||||
const newMessages = logs
|
|
||||||
.trim()
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => parseMessage(line));
|
|
||||||
messages.value.unshift(...newMessages);
|
|
||||||
}
|
|
||||||
emit("loading-more", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMessage(data: String): LogEntry {
|
|
||||||
let i = data.indexOf(" ");
|
|
||||||
if (i == -1) {
|
|
||||||
i = data.length;
|
|
||||||
}
|
|
||||||
const key = data.substring(0, i);
|
|
||||||
const date = new Date(key);
|
|
||||||
const message = data.substring(i + 1);
|
|
||||||
return { key, date, message };
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => container.value.state,
|
|
||||||
(newValue, oldValue) => {
|
|
||||||
console.log("LogEventSource: container changed", newValue, oldValue);
|
|
||||||
if (newValue == "running" && newValue != oldValue) {
|
|
||||||
buffer.value.push({
|
|
||||||
event: "container-started",
|
|
||||||
message: "Container started",
|
|
||||||
date: new Date(),
|
|
||||||
key: new Date().toString(),
|
|
||||||
});
|
|
||||||
connect({ clear: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (es) {
|
|
||||||
es.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
connect();
|
|
||||||
watch(id, () => connect());
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
clear: () => (messages.value = []),
|
clear: () => (messages.value = []),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fetchMore = () => loadOlderLogs({ beforeLoading, afterLoading });
|
||||||
|
|
||||||
|
connect();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
<ul class="events" ref="events" :class="{ 'disable-wrap': !softWrap, [size]: true }">
|
<ul class="events" ref="events" :class="{ 'disable-wrap': !softWrap, [size]: true }">
|
||||||
<li
|
<li
|
||||||
v-for="(item, index) in filtered"
|
v-for="(item, index) in filtered"
|
||||||
:key="item.key"
|
:key="item.id"
|
||||||
:data-key="item.key"
|
:data-key="item.id"
|
||||||
:data-event="item.event"
|
:data-event="item.event"
|
||||||
:class="{ selected: item.selected }"
|
:class="{ selected: toRaw(item) === toRaw(lastSelectedItem) }"
|
||||||
>
|
>
|
||||||
<div class="line-options" v-show="isSearching()">
|
<div class="line-options" v-show="isSearching()">
|
||||||
<dropdown-menu :class="{ 'is-last': index === filtered.length - 1 }" class="is-top minimal">
|
<dropdown-menu :class="{ 'is-last': index === filtered.length - 1 }" class="is-top minimal">
|
||||||
<a class="dropdown-item" @click="handleJumpLineSelected($event, item)" :href="`#${item.key}`">
|
<a class="dropdown-item" @click="handleJumpLineSelected($event, item)" :href="`#${item.id}`">
|
||||||
<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">
|
||||||
@@ -25,20 +25,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="line">
|
<div class="line">
|
||||||
<span class="date" v-if="showTimestamp"> <relative-time :date="item.date"></relative-time></span>
|
<span class="date" v-if="showTimestamp"> <relative-time :date="item.date"></relative-time></span>
|
||||||
<span class="text" v-html="colorize(item.message)"></span>
|
<JSONPayload :log-entry="item" :visible-keys="visibleKeys.value" v-if="item.hasPayload()"></JSONPayload>
|
||||||
|
<span class="text" v-html="colorize(item.message)" v-else-if="item.message"></span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PropType, ref, toRefs, watch } from "vue";
|
import { ComputedRef, inject, PropType, ref, toRefs, watch, toRaw } from "vue";
|
||||||
import { useRouteHash } from "@vueuse/router";
|
import { useRouteHash } from "@vueuse/router";
|
||||||
import { size, showTimestamp, softWrap } from "@/composables/settings";
|
import { size, showTimestamp, softWrap } from "@/composables/settings";
|
||||||
import RelativeTime from "./RelativeTime.vue";
|
import { VisibleLogEntry } from "@/types/VisibleLogEntry";
|
||||||
import AnsiConvertor from "ansi-to-html";
|
|
||||||
import { LogEntry } from "@/types/LogEntry";
|
import { LogEntry } from "@/types/LogEntry";
|
||||||
import { useSearchFilter } from "@/composables/search";
|
import { useSearchFilter } from "@/composables/search";
|
||||||
|
import { useVisibleFilter } from "@/composables/visible";
|
||||||
|
import { Container } from "@/types/Container";
|
||||||
|
import { persistentVisibleKeys } from "@/utils";
|
||||||
|
|
||||||
|
import RelativeTime from "./RelativeTime.vue";
|
||||||
|
import AnsiConvertor from "ansi-to-html";
|
||||||
|
import JSONPayload from "./JSONPayload.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
messages: {
|
messages: {
|
||||||
@@ -48,18 +55,22 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
|
const ansiConvertor = new AnsiConvertor({ escapeXML: true });
|
||||||
const { filteredMessages, resetSearch, markSearch, isSearching } = useSearchFilter();
|
|
||||||
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
|
const colorize = (value: string) => markSearch(ansiConvertor.toHtml(value));
|
||||||
|
|
||||||
const { messages } = toRefs(props);
|
const { messages } = toRefs(props);
|
||||||
const filtered = filteredMessages(messages);
|
let visibleKeys = persistentVisibleKeys(inject("container") as ComputedRef<Container>);
|
||||||
|
|
||||||
|
const { filteredPayload } = useVisibleFilter(visibleKeys);
|
||||||
|
const { filteredMessages, resetSearch, markSearch, isSearching } = useSearchFilter();
|
||||||
|
|
||||||
|
const visible = filteredPayload(messages);
|
||||||
|
const filtered = filteredMessages(visible);
|
||||||
|
|
||||||
const events = ref<HTMLElement>();
|
const events = ref<HTMLElement>();
|
||||||
let lastSelectedItem: LogEntry | undefined = undefined;
|
let lastSelectedItem = ref<VisibleLogEntry>();
|
||||||
function handleJumpLineSelected(e: Event, item: LogEntry) {
|
|
||||||
if (lastSelectedItem) {
|
function handleJumpLineSelected(e: Event, item: VisibleLogEntry) {
|
||||||
lastSelectedItem.selected = false;
|
lastSelectedItem.value = item;
|
||||||
}
|
|
||||||
lastSelectedItem = item;
|
|
||||||
item.selected = true;
|
|
||||||
resetSearch();
|
resetSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +95,13 @@ watch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
&::before {
|
||||||
|
content: " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& > li {
|
& > li {
|
||||||
display: flex;
|
display: flex;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
@@ -167,13 +185,6 @@ watch(
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
&::before {
|
|
||||||
content: " ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(mark) {
|
:deep(mark) {
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-color: var(--secondary-color);
|
background-color: var(--secondary-color);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<log-event-source ref="source" :id="id" #default="{ messages }" @loading-more="emit('loading-more', $event)">
|
<log-event-source ref="source" #default="{ messages }" @loading-more="emit('loading-more', $event)">
|
||||||
<log-viewer :messages="messages"></log-viewer>
|
<log-viewer :messages="messages"></log-viewer>
|
||||||
</log-event-source>
|
</log-event-source>
|
||||||
</template>
|
</template>
|
||||||
@@ -7,12 +7,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import LogViewer from "./LogViewer.vue";
|
import LogViewer from "./LogViewer.vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
defineProps({
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(["loading-more"]);
|
const emit = defineEmits(["loading-more"]);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<div class="is-scrollbar-notification">
|
<div class="is-scrollbar-notification">
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<button class="button" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom('instant')" v-show="paused">
|
<button class="button" :class="hasMore ? 'has-more' : ''" @click="scrollToBottom()" v-show="paused">
|
||||||
<mdi-light-chevron-double-down />
|
<mdi-light-chevron-double-down />
|
||||||
</button>
|
</button>
|
||||||
</transition>
|
</transition>
|
||||||
@@ -24,61 +24,51 @@
|
|||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
export default {
|
import { onMounted, ref } from "vue";
|
||||||
props: {
|
|
||||||
|
defineProps({
|
||||||
scrollable: {
|
scrollable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
|
|
||||||
name: "ScrollableView",
|
const paused = ref(false);
|
||||||
data() {
|
const hasMore = ref(false);
|
||||||
return {
|
const loading = ref(false);
|
||||||
paused: false,
|
const scrollObserver = ref<HTMLElement>();
|
||||||
hasMore: false,
|
const scrollableContent = ref<HTMLElement>();
|
||||||
loading: false,
|
|
||||||
mutationObserver: null,
|
const mutationObserver = new MutationObserver((e) => {
|
||||||
intersectionObserver: null,
|
if (!paused.value) {
|
||||||
};
|
scrollToBottom();
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const { scrollableContent } = this.$refs;
|
|
||||||
this.mutationObserver = new MutationObserver((e) => {
|
|
||||||
if (!this.paused) {
|
|
||||||
this.scrollToBottom("instant");
|
|
||||||
} else {
|
} else {
|
||||||
const record = e[e.length - 1];
|
const record = e[e.length - 1];
|
||||||
if (
|
if (record.target.children[record.target.children.length - 1] == record.addedNodes[record.addedNodes.length - 1]) {
|
||||||
record.target.children[record.target.children.length - 1] == record.addedNodes[record.addedNodes.length - 1]
|
hasMore.value = true;
|
||||||
) {
|
|
||||||
this.hasMore = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.mutationObserver.observe(scrollableContent, { childList: true, subtree: true });
|
|
||||||
|
|
||||||
this.intersectionObserver = new IntersectionObserver(
|
const intersectionObserver = new IntersectionObserver((entries) => (paused.value = entries[0].intersectionRatio == 0), {
|
||||||
(entries) => (this.paused = entries[0].intersectionRatio == 0),
|
threshholds: [0, 1],
|
||||||
{ threshholds: [0, 1], rootMargin: "80px 0px" }
|
rootMargin: "80px 0px",
|
||||||
);
|
});
|
||||||
this.intersectionObserver.observe(this.$refs.scrollObserver);
|
|
||||||
},
|
onMounted(() => {
|
||||||
beforeUnmount() {
|
mutationObserver.observe(scrollableContent.value!, { childList: true, subtree: true });
|
||||||
this.mutationObserver.disconnect();
|
intersectionObserver.observe(scrollObserver.value!);
|
||||||
this.intersectionObserver.disconnect();
|
});
|
||||||
},
|
|
||||||
methods: {
|
function scrollToBottom(behavior: "auto" | "smooth" = "auto") {
|
||||||
scrollToBottom(behavior = "instant") {
|
scrollObserver.value?.scrollIntoView({ behavior });
|
||||||
this.$refs.scrollObserver.scrollIntoView({ behavior });
|
hasMore.value = false;
|
||||||
this.hasMore = false;
|
}
|
||||||
},
|
|
||||||
setLoading(loading) {
|
function setLoading(value: boolean) {
|
||||||
this.loading = loading;
|
loading.value = value;
|
||||||
},
|
}
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
section {
|
section {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
exports[`<LogEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
|
exports[`<LogEventSource /> > render html correctly > should render dates with 12 hour style 1`] = `
|
||||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
||||||
<li data-key=\\"2019-06-12T23:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
||||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T23:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
|
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
||||||
@@ -23,19 +23,19 @@ exports[`<LogEventSource /> > render html correctly > should render dates with 1
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T23:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 11:55:42 PM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\"><test>foo bar</test></span></div>
|
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\"><test>foo bar</test></span></div>
|
||||||
</li>
|
</li>
|
||||||
</ul>"
|
</ul>"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<LogEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
|
exports[`<LogEventSource /> > render html correctly > should render dates with 24 hour style 1`] = `
|
||||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
||||||
<li data-key=\\"2019-06-12T23:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
||||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T23:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
|
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
||||||
@@ -51,19 +51,19 @@ exports[`<LogEventSource /> > render html correctly > should render dates with 2
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T23:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 23:55:42</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\"><test>foo bar</test></span></div>
|
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\"><test>foo bar</test></span></div>
|
||||||
</li>
|
</li>
|
||||||
</ul>"
|
</ul>"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<LogEventSource /> > render html correctly > should render messages 1`] = `
|
exports[`<LogEventSource /> > render html correctly > should render messages 1`] = `
|
||||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
||||||
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
||||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
|
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
||||||
@@ -79,19 +79,19 @@ exports[`<LogEventSource /> > render html correctly > should render messages 1`]
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\">\\"This is a message.\\"</span></div>
|
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\">This is a message.</span></div>
|
||||||
</li>
|
</li>
|
||||||
</ul>"
|
</ul>"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<LogEventSource /> > render html correctly > should render messages with color 1`] = `
|
exports[`<LogEventSource /> > render html correctly > should render messages with color 1`] = `
|
||||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
||||||
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
||||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
|
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
||||||
@@ -114,12 +114,12 @@ exports[`<LogEventSource /> > render html correctly > should render messages wit
|
|||||||
|
|
||||||
exports[`<LogEventSource /> > render html correctly > should render messages with filter 1`] = `
|
exports[`<LogEventSource /> > render html correctly > should render messages with filter 1`] = `
|
||||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
||||||
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
||||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
<div class=\\"dropdown is-hoverable is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
|
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
||||||
@@ -135,19 +135,42 @@ exports[`<LogEventSource /> > render html correctly > should render messages wit
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\">This is a <mark>test</mark> <hi></hi></span></div>
|
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\"><<mark>test</mark>>foo bar</test></span></div>
|
||||||
|
</li>
|
||||||
|
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||||
|
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
||||||
|
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
||||||
|
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||||
|
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||||
|
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
|
||||||
|
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
||||||
|
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
||||||
|
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
||||||
|
<path fill=\\"currentColor\\" d=\\"M334.627 16H48v480h424V153.373ZM440 464H80V48h241.373L440 166.627Z\\"></path>
|
||||||
|
<path fill=\\"currentColor\\" d=\\"M239.861 152a95.861 95.861 0 1 0 53.624 175.284l68.03 68.029l22.627-22.626l-67.5-67.5A95.816 95.816 0 0 0 239.861 152ZM176 247.861a63.862 63.862 0 1 1 63.861 63.861A63.933 63.933 0 0 1 176 247.861Z\\"></path>
|
||||||
|
</svg></div>
|
||||||
|
</div>
|
||||||
|
<div class=\\"level-right\\" data-v-cce5b553=\\"\\">
|
||||||
|
<div class=\\"level-item\\" data-v-cce5b553=\\"\\">Jump to Context</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class=\\"line\\" data-v-cce5b553=\\"\\"><span class=\\"date\\" data-v-cce5b553=\\"\\"><time datetime=\\"2019-06-12T10:55:42.459Z\\" data-v-cce5b553=\\"\\">today at 10:55:42 AM</time></span><span class=\\"text\\" data-v-cce5b553=\\"\\"><<mark>test</mark>>test bar</test></span></div>
|
||||||
</li>
|
</li>
|
||||||
</ul>"
|
</ul>"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
|
exports[`<LogEventSource /> > render html correctly > should render messages with html entities 1`] = `
|
||||||
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
"<ul class=\\"events medium\\" data-v-cce5b553=\\"\\">
|
||||||
<li data-key=\\"2019-06-12T10:55:42.459034602Z\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
<li data-key=\\"1\\" class=\\"\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
<div class=\\"line-options\\" data-v-cce5b553=\\"\\" style=\\"display: none;\\">
|
||||||
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
<div class=\\"dropdown is-hoverable is-last is-top minimal\\" data-v-539164cb=\\"\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
<div class=\\"dropdown-trigger\\" data-v-539164cb=\\"\\"><button class=\\"button\\" aria-haspopup=\\"true\\" aria-controls=\\"dropdown-menu\\" data-v-539164cb=\\"\\"><span class=\\"icon\\" data-v-539164cb=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 24 24\\" width=\\"1.2em\\" height=\\"1.2em\\" data-v-539164cb=\\"\\"><path fill=\\"currentColor\\" d=\\"M12 16a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2m0-6a2 2 0 0 1 2 2a2 2 0 0 1-2 2a2 2 0 0 1-2-2a2 2 0 0 1 2-2Z\\"></path></svg></span></button></div>
|
||||||
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
<div class=\\"dropdown-menu\\" id=\\"dropdown-menu\\" role=\\"menu\\" data-v-539164cb=\\"\\">
|
||||||
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#2019-06-12T10:55:42.459034602Z\\" data-v-cce5b553=\\"\\">
|
<div class=\\"dropdown-content\\" data-v-539164cb=\\"\\"><a class=\\"dropdown-item\\" href=\\"#1\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level is-justify-content-start\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level-left\\" data-v-cce5b553=\\"\\">
|
||||||
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
<div class=\\"level-item\\" data-v-cce5b553=\\"\\"><svg preserveAspectRatio=\\"xMidYMid meet\\" viewBox=\\"0 0 512 512\\" width=\\"1.2em\\" height=\\"1.2em\\" class=\\"mr-4\\" data-v-cce5b553=\\"\\">
|
||||||
@@ -182,20 +205,8 @@ exports[`<LogEventSource /> > renders correctly 1`] = `
|
|||||||
exports[`<LogEventSource /> > should parse messages 1`] = `
|
exports[`<LogEventSource /> > should parse messages 1`] = `
|
||||||
{
|
{
|
||||||
"date": 2019-06-12T10:55:42.459Z,
|
"date": 2019-06-12T10:55:42.459Z,
|
||||||
"message": "\\"This is a message.\\"",
|
"id": 1,
|
||||||
}
|
"message": "This is a message.",
|
||||||
`;
|
"payload": undefined,
|
||||||
|
|
||||||
exports[`<LogEventSource /> > should parse messages with loki's timestamp format 1`] = `
|
|
||||||
{
|
|
||||||
"date": 2020-04-27T10:35:43.272Z,
|
|
||||||
"message": "xxxxx",
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<LogEventSource /> > should pass messages to slot 1`] = `
|
|
||||||
{
|
|
||||||
"date": 2019-06-12T10:55:42.459Z,
|
|
||||||
"message": "\\"This is a message.\\"",
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
111
assets/composables/eventsource.ts
Normal file
111
assets/composables/eventsource.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { ref, watch, onUnmounted, ComputedRef } from "vue";
|
||||||
|
import debounce from "lodash.debounce";
|
||||||
|
|
||||||
|
import { LogEntry, LogEvent } from "@/types/LogEntry";
|
||||||
|
|
||||||
|
import config from "@/stores/config";
|
||||||
|
import { Container } from "@/types/Container";
|
||||||
|
|
||||||
|
function parseMessage(data: string): LogEntry {
|
||||||
|
const e = JSON.parse(data) as LogEvent;
|
||||||
|
|
||||||
|
const id = e.id;
|
||||||
|
const date = new Date(e.ts * 1000);
|
||||||
|
return { id, date, message: e.m, payload: e.d };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEventSource(container: ComputedRef<Container>) {
|
||||||
|
const messages = ref<LogEntry[]>([]);
|
||||||
|
const buffer = ref<LogEntry[]>([]);
|
||||||
|
|
||||||
|
function flushNow() {
|
||||||
|
messages.value.push(...buffer.value);
|
||||||
|
buffer.value = [];
|
||||||
|
}
|
||||||
|
const flushBuffer = debounce(flushNow, 250, { maxWait: 1000 });
|
||||||
|
let es: EventSource | null = null;
|
||||||
|
let lastEventId = "";
|
||||||
|
|
||||||
|
function connect({ clear } = { clear: true }) {
|
||||||
|
es?.close();
|
||||||
|
|
||||||
|
if (clear) {
|
||||||
|
flushBuffer.cancel();
|
||||||
|
messages.value = [];
|
||||||
|
buffer.value = [];
|
||||||
|
lastEventId = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
es = new EventSource(`${config.base}/api/logs/stream?id=${container.value.id}&lastEventId=${lastEventId}`);
|
||||||
|
es.addEventListener("container-stopped", () => {
|
||||||
|
es?.close();
|
||||||
|
es = null;
|
||||||
|
buffer.value.push({
|
||||||
|
event: "container-stopped",
|
||||||
|
message: "Container stopped",
|
||||||
|
date: new Date(),
|
||||||
|
id: new Date().getTime(),
|
||||||
|
});
|
||||||
|
flushBuffer();
|
||||||
|
flushBuffer.flush();
|
||||||
|
});
|
||||||
|
es.addEventListener("error", (e) => console.error("EventSource failed: " + JSON.stringify(e)));
|
||||||
|
es.onmessage = (e) => {
|
||||||
|
lastEventId = e.lastEventId;
|
||||||
|
if (e.data) {
|
||||||
|
buffer.value.push(parseMessage(e.data));
|
||||||
|
flushBuffer();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadOlderLogs({ beforeLoading, afterLoading } = { beforeLoading: () => {}, afterLoading: () => {} }) {
|
||||||
|
if (messages.value.length < 300) return;
|
||||||
|
|
||||||
|
beforeLoading();
|
||||||
|
const to = messages.value[0].date;
|
||||||
|
const last = messages.value[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();
|
||||||
|
if (logs) {
|
||||||
|
const newMessages = logs
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => parseMessage(line));
|
||||||
|
messages.value.unshift(...newMessages);
|
||||||
|
}
|
||||||
|
afterLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => container.value.state,
|
||||||
|
(newValue, oldValue) => {
|
||||||
|
console.log("LogEventSource: container changed", newValue, oldValue);
|
||||||
|
if (newValue == "running" && newValue != oldValue) {
|
||||||
|
buffer.value.push({
|
||||||
|
event: "container-started",
|
||||||
|
message: "Container started",
|
||||||
|
date: new Date(),
|
||||||
|
id: new Date().getTime(),
|
||||||
|
});
|
||||||
|
connect({ clear: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (es) {
|
||||||
|
es.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => container.value.id,
|
||||||
|
() => connect()
|
||||||
|
);
|
||||||
|
|
||||||
|
return { connect, messages, loadOlderLogs };
|
||||||
|
}
|
||||||
@@ -3,7 +3,20 @@ import { ref, computed, Ref } from "vue";
|
|||||||
const searchFilter = ref<string>("");
|
const searchFilter = ref<string>("");
|
||||||
const showSearch = ref(false);
|
const showSearch = ref(false);
|
||||||
|
|
||||||
import type { LogEntry } from "@/types/LogEntry";
|
import { VisibleLogEntry } from "@/types/VisibleLogEntry";
|
||||||
|
|
||||||
|
function matchRecord(record: Record<string, any>, regex: RegExp): boolean {
|
||||||
|
for (const key in record) {
|
||||||
|
const value = record[key];
|
||||||
|
if (typeof value === "string" && regex.test(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value) && matchRecord(value, regex)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function useSearchFilter() {
|
export function useSearchFilter() {
|
||||||
const regex = computed(() => {
|
const regex = computed(() => {
|
||||||
@@ -11,11 +24,17 @@ export function useSearchFilter() {
|
|||||||
return isSmartCase ? new RegExp(searchFilter.value, "i") : new RegExp(searchFilter.value);
|
return isSmartCase ? new RegExp(searchFilter.value, "i") : new RegExp(searchFilter.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
function filteredMessages(messages: Ref<LogEntry[]>) {
|
function filteredMessages(messages: Ref<VisibleLogEntry[]>) {
|
||||||
return computed(() => {
|
return computed(() => {
|
||||||
if (searchFilter && searchFilter.value) {
|
if (searchFilter.value) {
|
||||||
try {
|
try {
|
||||||
return messages.value.filter((d) => d.message.match(regex.value));
|
return messages.value.filter((d) => {
|
||||||
|
if (d.hasPayload()) {
|
||||||
|
return matchRecord(d.payload, regex.value);
|
||||||
|
} else {
|
||||||
|
return regex.value.test(d.message ?? "");
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof SyntaxError) {
|
if (e instanceof SyntaxError) {
|
||||||
console.info(`Ignoring SyntaxError from search.`, e);
|
console.info(`Ignoring SyntaxError from search.`, e);
|
||||||
@@ -29,12 +48,18 @@ export function useSearchFilter() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function markSearch(log: string) {
|
function markSearch(log: string): string;
|
||||||
if (searchFilter && searchFilter.value) {
|
function markSearch(log: string[]): string[];
|
||||||
return log.replace(regex.value, `<mark>$&</mark>`);
|
function markSearch(log: string | string[]) {
|
||||||
}
|
if (!searchFilter.value) {
|
||||||
return log;
|
return log;
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(log)) {
|
||||||
|
return log.map((d) => markSearch(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
return log.toString().replace(regex.value, (match) => `<mark>${match}</mark>`);
|
||||||
|
}
|
||||||
|
|
||||||
function resetSearch() {
|
function resetSearch() {
|
||||||
searchFilter.value = "";
|
searchFilter.value = "";
|
||||||
|
|||||||
13
assets/composables/visible.ts
Normal file
13
assets/composables/visible.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { LogEntry } from "@/types/LogEntry";
|
||||||
|
import { VisibleLogEntry } from "@/types/VisibleLogEntry";
|
||||||
|
import { computed, ComputedRef, Ref } from "vue";
|
||||||
|
|
||||||
|
export function useVisibleFilter(visibleKeys: ComputedRef<Ref<string[][]>>) {
|
||||||
|
function filteredPayload(messages: Ref<LogEntry[]>) {
|
||||||
|
return computed(() => {
|
||||||
|
return messages.value.map((d) => new VisibleLogEntry(d, visibleKeys.value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { filteredPayload };
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ $menu-item-hover-color: var(--menu-item-hover-color);
|
|||||||
|
|
||||||
$text-strong: var(--text-strong-color);
|
$text-strong: var(--text-strong-color);
|
||||||
$text: var(--text-color);
|
$text: var(--text-color);
|
||||||
|
$text-light: var(--text-light-color);
|
||||||
|
|
||||||
$panel-heading-background-color: var(--panel-heading-background-color);
|
$panel-heading-background-color: var(--panel-heading-background-color);
|
||||||
$panel-heading-color: var(--panel-heading-color);
|
$panel-heading-color: var(--panel-heading-color);
|
||||||
@@ -38,8 +39,7 @@ $light-toolbar-color: rgba($grey-darker, 0.7);
|
|||||||
@import "@oruga-ui/theme-bulma/dist/scss/components/skeleton.scss";
|
@import "@oruga-ui/theme-bulma/dist/scss/components/skeleton.scss";
|
||||||
@import "splitpanes/dist/splitpanes.css";
|
@import "splitpanes/dist/splitpanes.css";
|
||||||
|
|
||||||
html,
|
@mixin dark {
|
||||||
[data-theme="dark"] {
|
|
||||||
--scheme-main: #{$black};
|
--scheme-main: #{$black};
|
||||||
--scheme-main-bis: #{$black-bis};
|
--scheme-main-bis: #{$black-bis};
|
||||||
--scheme-main-ter: #{$black-ter};
|
--scheme-main-ter: #{$black-ter};
|
||||||
@@ -64,93 +64,57 @@ html,
|
|||||||
|
|
||||||
--text-strong-color: #{$grey-lightest};
|
--text-strong-color: #{$grey-lightest};
|
||||||
--text-color: #{$grey-lighter};
|
--text-color: #{$grey-lighter};
|
||||||
|
--text-light-color: #{$grey};
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
--scheme-main: #{$white};
|
||||||
|
--scheme-main-bis: #{$white-bis};
|
||||||
|
--scheme-main-ter: #{$white-ter};
|
||||||
|
|
||||||
|
--border-color: #{$grey-lighter};
|
||||||
|
--border-hover-color: var(--secondary-color);
|
||||||
|
--logo-color: #{$grey-darker};
|
||||||
|
|
||||||
|
--primary-color: #{$turquoise};
|
||||||
|
--secondary-color: #d8f0ca;
|
||||||
|
|
||||||
|
--body-background-color: #{$white-bis};
|
||||||
|
--action-toolbar-background-color: #{$light-toolbar-color};
|
||||||
|
--body-color: #{$grey-darker};
|
||||||
|
|
||||||
|
--menu-item-color: #{$grey-dark};
|
||||||
|
--menu-item-hover-background-color: #eee8e7;
|
||||||
|
--menu-item-hover-color: #{black-ter};
|
||||||
|
|
||||||
|
--panel-heading-background-color: var(--secondary-color);
|
||||||
|
--panel-heading-color: var(--text-strong-color);
|
||||||
|
|
||||||
|
--text-strong-color: #{$grey-dark};
|
||||||
|
--text-color: #{$grey-darker};
|
||||||
|
--text-light-color: #{$grey};
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
@include dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] {
|
||||||
|
@include light;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
html {
|
html {
|
||||||
--scheme-main: #{$black};
|
@include dark;
|
||||||
--scheme-main-bis: #{$black-bis};
|
|
||||||
--scheme-main-ter: #{$black-ter};
|
|
||||||
|
|
||||||
--border-color: #{$grey-darker};
|
|
||||||
--border-hover-color: var(--secondary-color);
|
|
||||||
--logo-color: var(--secondary-color);
|
|
||||||
|
|
||||||
--primary-color: #{$turquoise};
|
|
||||||
--secondary-color: #{$yellow};
|
|
||||||
|
|
||||||
--body-background-color: #{$black-bis};
|
|
||||||
--action-toolbar-background-color: #{$dark-toolbar-color};
|
|
||||||
|
|
||||||
--menu-item-active-background-color: var(--primary-color);
|
|
||||||
--menu-item-color: hsl(0, 6%, 87%);
|
|
||||||
--menu-item-hover-background-color: #{$white-ter};
|
|
||||||
--menu-item-hover-color: #{$black-ter};
|
|
||||||
|
|
||||||
--panel-heading-background-color: var(--secondary-color);
|
|
||||||
--panel-heading-color: var(--scheme-main-bis);
|
|
||||||
|
|
||||||
--text-strong-color: #{$grey-lightest};
|
|
||||||
--text-color: #{$grey-lighter};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
html {
|
html {
|
||||||
--scheme-main: #{$white};
|
@include light;
|
||||||
--scheme-main-bis: #{$white-bis};
|
|
||||||
--scheme-main-ter: #{$white-ter};
|
|
||||||
|
|
||||||
--border-color: #{$grey-lighter};
|
|
||||||
--border-hover-color: var(--secondary-color);
|
|
||||||
--logo-color: #{$grey-darker};
|
|
||||||
|
|
||||||
--primary-color: #{$turquoise};
|
|
||||||
--secondary-color: #d8f0ca;
|
|
||||||
|
|
||||||
--body-background-color: #{$white-bis};
|
|
||||||
--action-toolbar-background-color: #{$light-toolbar-color};
|
|
||||||
--body-color: #{$grey-darker};
|
|
||||||
|
|
||||||
--menu-item-color: #{$grey-dark};
|
|
||||||
--menu-item-hover-background-color: #eee8e7;
|
|
||||||
--menu-item-hover-color: #{black-ter};
|
|
||||||
|
|
||||||
--panel-heading-background-color: var(--secondary-color);
|
|
||||||
--panel-heading-color: var(--text-strong-color);
|
|
||||||
|
|
||||||
--text-strong-color: #{$grey-dark};
|
|
||||||
--text-color: #{$grey-darker};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="light"] {
|
|
||||||
--scheme-main: #{$white};
|
|
||||||
--scheme-main-bis: #{$white-bis};
|
|
||||||
--scheme-main-ter: #{$white-ter};
|
|
||||||
|
|
||||||
--border-color: #{$grey-lighter};
|
|
||||||
--border-hover-color: var(--secondary-color);
|
|
||||||
--logo-color: #{$grey-darker};
|
|
||||||
|
|
||||||
--primary-color: #{$turquoise};
|
|
||||||
--secondary-color: #d8f0ca;
|
|
||||||
|
|
||||||
--body-background-color: #{$white-bis};
|
|
||||||
--action-toolbar-background-color: #{$light-toolbar-color};
|
|
||||||
--body-color: #{$grey-darker};
|
|
||||||
|
|
||||||
--menu-item-color: #{$grey-dark};
|
|
||||||
--menu-item-hover-background-color: #eee8e7;
|
|
||||||
--menu-item-hover-color: #{black-ter};
|
|
||||||
|
|
||||||
--panel-heading-background-color: var(--secondary-color);
|
|
||||||
--panel-heading-color: var(--text-strong-color);
|
|
||||||
|
|
||||||
--text-strong-color: #{$grey-dark};
|
|
||||||
--text-color: #{$grey-darker};
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
overflow-x: unset;
|
overflow-x: unset;
|
||||||
overflow-y: unset;
|
overflow-y: unset;
|
||||||
|
|||||||
1
assets/types/Container.d.ts
vendored
1
assets/types/Container.d.ts
vendored
@@ -4,6 +4,7 @@ export interface Container {
|
|||||||
readonly image: string;
|
readonly image: string;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly status: string;
|
readonly status: string;
|
||||||
|
readonly command: string;
|
||||||
state: "created" | "running" | "exited" | "dead" | "paused" | "restarting";
|
state: "created" | "running" | "exited" | "dead" | "paused" | "restarting";
|
||||||
stat?: ContainerStat;
|
stat?: ContainerStat;
|
||||||
}
|
}
|
||||||
|
|||||||
14
assets/types/LogEntry.d.ts
vendored
14
assets/types/LogEntry.d.ts
vendored
@@ -1,7 +1,15 @@
|
|||||||
export interface LogEntry {
|
export interface LogEntry {
|
||||||
date: Date;
|
readonly date: Date;
|
||||||
message: string;
|
readonly message?: string;
|
||||||
key: string;
|
readonly payload?: Record<string, any>;
|
||||||
|
readonly id: number;
|
||||||
event?: string;
|
event?: string;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LogEvent {
|
||||||
|
readonly m?: string;
|
||||||
|
readonly ts: number;
|
||||||
|
readonly d?: Record<string, any>;
|
||||||
|
readonly id: number;
|
||||||
|
}
|
||||||
|
|||||||
51
assets/types/VisibleLogEntry.ts
Normal file
51
assets/types/VisibleLogEntry.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { computed, ComputedRef, Ref } from "vue";
|
||||||
|
import { LogEntry } from "./LogEntry";
|
||||||
|
import { flattenJSON, getDeep } from "@/utils";
|
||||||
|
|
||||||
|
export class VisibleLogEntry implements LogEntry {
|
||||||
|
private readonly entry: LogEntry;
|
||||||
|
filteredPayload: undefined | ComputedRef<Record<string, any>>;
|
||||||
|
|
||||||
|
constructor(entry: LogEntry, visibleKeys: Ref<string[][]>) {
|
||||||
|
this.entry = entry;
|
||||||
|
this.filteredPayload = undefined;
|
||||||
|
if (this.entry.payload) {
|
||||||
|
const payload = this.entry.payload;
|
||||||
|
this.filteredPayload = computed(() => {
|
||||||
|
if (!visibleKeys.value.length) {
|
||||||
|
return flattenJSON(payload);
|
||||||
|
} else {
|
||||||
|
return visibleKeys.value.reduce((acc, attr) => ({ ...acc, [attr.join(".")]: getDeep(payload, attr) }), {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasPayload(): this is { payload: Record<string, any> } {
|
||||||
|
return this.entry.payload !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get unfilteredPayload(): Record<string, any> | undefined {
|
||||||
|
return this.entry.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get payload(): Record<string, any> | undefined {
|
||||||
|
return this.filteredPayload?.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get date(): Date {
|
||||||
|
return this.entry.date;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get message(): string | undefined {
|
||||||
|
return this.entry.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get id(): number {
|
||||||
|
return this.entry.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get event(): string | undefined {
|
||||||
|
return this.entry.event;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { Container } from "@/types/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;
|
||||||
@@ -6,3 +10,38 @@ export function formatBytes(bytes: number, decimals = 2) {
|
|||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDeep(obj: Record<string, any>, path: string[]) {
|
||||||
|
return path.reduce((acc, key) => acc?.[key], obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isObject(value: any): value is Record<string, any> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenJSON(obj: Record<string, any>, path: string[] = []) {
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
Object.keys(obj).forEach((key) => {
|
||||||
|
const value = obj[key];
|
||||||
|
const newPath = path.concat(key);
|
||||||
|
if (isObject(value)) {
|
||||||
|
Object.assign(result, flattenJSON(value, newPath));
|
||||||
|
} else {
|
||||||
|
result[newPath.join(".")] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,3 +26,10 @@ type ContainerEvent struct {
|
|||||||
ActorID string `json:"actorId"`
|
ActorID string `json:"actorId"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LogEvent struct {
|
||||||
|
Message string `json:"m,omitempty"`
|
||||||
|
Timestamp int64 `json:"ts"`
|
||||||
|
Data map[string]interface{} `json:"d,omitempty"`
|
||||||
|
Id uint32 `json:"id,omitempty"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,7 +68,8 @@
|
|||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"release-it": "^15.3.0",
|
"release-it": "^15.3.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"vitest": "^0.22.0"
|
"vitest": "^0.22.0",
|
||||||
|
"vue-tsc": "^0.40.1"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,vue,css}": [
|
"*.{js,vue,css}": [
|
||||||
|
|||||||
49
pnpm-lock.yaml
generated
49
pnpm-lock.yaml
generated
@@ -46,6 +46,7 @@ specifiers:
|
|||||||
vitest: ^0.22.0
|
vitest: ^0.22.0
|
||||||
vue: ^3.2.37
|
vue: ^3.2.37
|
||||||
vue-router: ^4.1.3
|
vue-router: ^4.1.3
|
||||||
|
vue-tsc: ^0.40.1
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify-json/carbon': 1.1.7
|
'@iconify-json/carbon': 1.1.7
|
||||||
@@ -95,6 +96,7 @@ devDependencies:
|
|||||||
release-it: 15.3.0
|
release-it: 15.3.0
|
||||||
ts-node: 10.9.1_7se4izhtmlozvpvgaax5rnuwve
|
ts-node: 10.9.1_7se4izhtmlozvpvgaax5rnuwve
|
||||||
vitest: 0.22.0_jsdom@20.0.0+sass@1.54.4
|
vitest: 0.22.0_jsdom@20.0.0+sass@1.54.4
|
||||||
|
vue-tsc: 0.40.1_typescript@4.7.4
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -620,6 +622,42 @@ packages:
|
|||||||
vue: 3.2.37
|
vue: 3.2.37
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@volar/code-gen/0.40.1:
|
||||||
|
resolution: {integrity: sha512-mN1jn08wRKLoUj+KThltyWfsiEGt6Um1yT6S7bkruwV76yiLlzIR4WZgWng254byGMozJ00qgkZmBhraD5b48A==}
|
||||||
|
dependencies:
|
||||||
|
'@volar/source-map': 0.40.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@volar/source-map/0.40.1:
|
||||||
|
resolution: {integrity: sha512-ORYg5W+R4iT2k/k2U4ASkKvDxabIzKtP+lXZ1CcqFIbTF81GOooAv5tJZImf8ifhUV9p8bgGaitFj/VnNzkdYg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@volar/typescript-faster/0.40.1:
|
||||||
|
resolution: {integrity: sha512-UiX8OzVRJtpudGfTY2KgB5m78DIA8oVbwI4QN5i4Ot8oURQPOviH7MahikHeeXidbh3iOy/u4vceMb+mfdizpQ==}
|
||||||
|
dependencies:
|
||||||
|
semver: 7.3.7
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@volar/vue-language-core/0.40.1:
|
||||||
|
resolution: {integrity: sha512-RBU2nQkj+asKZ/ht3sU3hTau+dGuTjJrQS3nNSw4+vnwUJnN/WogO/MmgKdrvVf3pUdLiucIog1E/Us1C8Y5wg==}
|
||||||
|
dependencies:
|
||||||
|
'@volar/code-gen': 0.40.1
|
||||||
|
'@volar/source-map': 0.40.1
|
||||||
|
'@vue/compiler-core': 3.2.37
|
||||||
|
'@vue/compiler-dom': 3.2.37
|
||||||
|
'@vue/compiler-sfc': 3.2.37
|
||||||
|
'@vue/reactivity': 3.2.37
|
||||||
|
'@vue/shared': 3.2.37
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@volar/vue-typescript/0.40.1:
|
||||||
|
resolution: {integrity: sha512-58nW/Xwy7VBkeIPmbyEmi/j1Ta2HxGl/5aFiEEpWxoas7vI1AM+txz8+MhWho4ZMw0w0eCqPtGgugD2rr+/v7w==}
|
||||||
|
dependencies:
|
||||||
|
'@volar/code-gen': 0.40.1
|
||||||
|
'@volar/typescript-faster': 0.40.1
|
||||||
|
'@volar/vue-language-core': 0.40.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@vue/compiler-core/3.2.37:
|
/@vue/compiler-core/3.2.37:
|
||||||
resolution: {integrity: sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==}
|
resolution: {integrity: sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4688,6 +4726,17 @@ packages:
|
|||||||
vue: 3.2.37
|
vue: 3.2.37
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/vue-tsc/0.40.1_typescript@4.7.4:
|
||||||
|
resolution: {integrity: sha512-Z+3rlp/6TrtKvLuaFYwBn03zrdinMR6lBb3mWBJtDA+KwlRu+I4eMoqC1qT9D7i/29u0Bw58dH7ErjMpNLN9bQ==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '*'
|
||||||
|
dependencies:
|
||||||
|
'@volar/vue-language-core': 0.40.1
|
||||||
|
'@volar/vue-typescript': 0.40.1
|
||||||
|
typescript: 4.7.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
/vue/3.2.37:
|
/vue/3.2.37:
|
||||||
resolution: {integrity: sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==}
|
resolution: {integrity: sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["assets/*"]
|
"@/*": ["assets/*"]
|
||||||
}
|
},
|
||||||
|
"jsx": "preserve",
|
||||||
},
|
},
|
||||||
"include": ["assets/**/*.ts", "assets/**/*.d.ts", "assets/**/*.vue"],
|
"include": ["assets/**/*.ts", "assets/**/*.d.ts", "assets/**/*.vue"],
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ Connection: keep-alive
|
|||||||
Content-Type: text/event-stream
|
Content-Type: text/event-stream
|
||||||
X-Accel-Buffering: no
|
X-Accel-Buffering: no
|
||||||
|
|
||||||
data: INFO Testing logs...
|
data: {"m":"INFO Testing logs...","ts":0,"id":4256192898}
|
||||||
|
|
||||||
event: container-stopped
|
event: container-stopped
|
||||||
data: end of stream
|
data: end of stream
|
||||||
@@ -170,8 +170,8 @@ Connection: keep-alive
|
|||||||
Content-Type: text/event-stream
|
Content-Type: text/event-stream
|
||||||
X-Accel-Buffering: no
|
X-Accel-Buffering: no
|
||||||
|
|
||||||
data: 2020-05-13T18:55:37.772853839Z INFO Testing logs...
|
data: {"m":"INFO Testing logs...","ts":1589396137,"id":1469707724}
|
||||||
id: 2020-05-13T18:55:37.772853839Z
|
id: 1589396137
|
||||||
|
|
||||||
event: container-stopped
|
event: container-stopped
|
||||||
data: end of stream
|
data: end of stream
|
||||||
101
web/logs.go
101
web/logs.go
@@ -4,6 +4,8 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"hash/fnv"
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -13,29 +15,12 @@ import (
|
|||||||
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/amir20/dozzle/docker"
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
|
|
||||||
|
|
||||||
from, _ := time.Parse(time.RFC3339, r.URL.Query().Get("from"))
|
|
||||||
to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to"))
|
|
||||||
id := r.URL.Query().Get("id")
|
|
||||||
|
|
||||||
reader, err := h.client.ContainerLogsBetweenDates(r.Context(), id, from, to)
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
io.Copy(w, reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
id := r.URL.Query().Get("id")
|
id := r.URL.Query().Get("id")
|
||||||
container, err := h.client.FindContainer(id)
|
container, err := h.client.FindContainer(id)
|
||||||
@@ -63,6 +48,68 @@ func (h *handler) downloadLogs(w http.ResponseWriter, r *http.Request) {
|
|||||||
io.Copy(zw, reader)
|
io.Copy(zw, reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func logEventIterator(reader *bufio.Reader) func() (docker.LogEvent, error) {
|
||||||
|
return func() (docker.LogEvent, error) {
|
||||||
|
message, readerError := reader.ReadString('\n')
|
||||||
|
h := fnv.New32a()
|
||||||
|
h.Write([]byte(message))
|
||||||
|
logEvent := docker.LogEvent{Id: h.Sum32()}
|
||||||
|
|
||||||
|
if index := strings.IndexAny(message, " "); index != -1 {
|
||||||
|
logId := message[:index]
|
||||||
|
if timestamp, err := time.Parse(time.RFC3339Nano, logId); err == nil {
|
||||||
|
logEvent.Timestamp = timestamp.Unix()
|
||||||
|
message = strings.TrimSuffix(message[index+1:], "\n")
|
||||||
|
if strings.HasPrefix(message, "{") && strings.HasSuffix(message, "}") {
|
||||||
|
var data map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(message), &data); err != nil {
|
||||||
|
log.Errorf("json unmarshal error while streaming %v", err.Error())
|
||||||
|
}
|
||||||
|
logEvent.Data = data
|
||||||
|
} else {
|
||||||
|
logEvent.Message = message
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logEvent.Message = message
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logEvent.Message = message
|
||||||
|
}
|
||||||
|
|
||||||
|
return logEvent, readerError
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/ld+json; charset=UTF-8")
|
||||||
|
|
||||||
|
from, _ := time.Parse(time.RFC3339, r.URL.Query().Get("from"))
|
||||||
|
to, _ := time.Parse(time.RFC3339, r.URL.Query().Get("to"))
|
||||||
|
id := r.URL.Query().Get("id")
|
||||||
|
|
||||||
|
reader, err := h.client.ContainerLogsBetweenDates(r.Context(), id, from, to)
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buffered := bufio.NewReader(reader)
|
||||||
|
eventIterator := logEventIterator(buffered)
|
||||||
|
|
||||||
|
for {
|
||||||
|
logEvent, readerError := eventIterator()
|
||||||
|
if readerError != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err := json.NewEncoder(w).Encode(logEvent); err != nil {
|
||||||
|
log.Errorf("json encoding error while streaming %v", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
id := r.URL.Query().Get("id")
|
id := r.URL.Query().Get("id")
|
||||||
if id == "" {
|
if id == "" {
|
||||||
@@ -122,15 +169,19 @@ func (h *handler) streamLogs(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
buffered := bufio.NewReader(reader)
|
buffered := bufio.NewReader(reader)
|
||||||
var readerError error
|
var readerError error
|
||||||
var message string
|
eventIterator := logEventIterator(buffered)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
message, readerError = buffered.ReadString('\n')
|
|
||||||
fmt.Fprintf(w, "data: %s\n", strings.TrimRight(message, "\n"))
|
var logEvent docker.LogEvent
|
||||||
if index := strings.IndexAny(message, " "); index != -1 {
|
logEvent, readerError = eventIterator()
|
||||||
id := message[:index]
|
if buf, err := json.Marshal(logEvent); err != nil {
|
||||||
if _, err := time.Parse(time.RFC3339Nano, id); err == nil {
|
log.Errorf("json encoding error while streaming %v", err.Error())
|
||||||
fmt.Fprintf(w, "id: %s\n", id)
|
} else {
|
||||||
|
fmt.Fprintf(w, "data: %s\n", buf)
|
||||||
}
|
}
|
||||||
|
if logEvent.Timestamp > 0 {
|
||||||
|
fmt.Fprintf(w, "id: %d\n", logEvent.Timestamp)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "\n")
|
fmt.Fprintf(w, "\n")
|
||||||
f.Flush()
|
f.Flush()
|
||||||
|
|||||||
Reference in New Issue
Block a user