From 8dac197f6026551695eeedace1215bc5237f593b Mon Sep 17 00:00:00 2001 From: Amir Raminfar Date: Tue, 5 May 2026 16:11:32 -0700 Subject: [PATCH] feat(cloud-proto): add SearchLogs unary RPC (#4672) Co-authored-by: Claude Opus 4.7 (1M context) --- assets/auto-imports.d.ts | 11 + assets/components.d.ts | 9 + assets/components/CloudSearchInline.vue | 35 ++ assets/components/CloudSettingsCard.vue | 4 +- assets/components/FuzzySearchModal.spec.ts | 2 +- assets/components/FuzzySearchModal.vue | 235 ++++++++++--- assets/components/LogViewer/EventSource.vue | 5 +- assets/components/LogViewer/LogDetails.vue | 42 +-- assets/components/LogViewer/LogItem.vue | 9 +- assets/components/LogViewer/LogList.vue | 19 + assets/components/PageWithLinks.vue | 5 +- assets/components/SidePanel.vue | 11 - assets/components/common/JsonFormatted.vue | 37 ++ assets/components/common/JsonText.vue | 31 ++ assets/components/common/JsonValue.vue | 106 ++++++ assets/components/common/KeyShortcut.vue | 2 +- assets/composable/cloudConfig.ts | 5 + assets/composable/cloudLogSearch.ts | 155 +++++++++ assets/composable/fuzzySearch.ts | 16 + assets/layouts/default.vue | 14 +- assets/main.css | 3 + assets/pages/cloud/search.vue | 232 +++++++++++++ assets/typed-router.d.ts | 13 + e2e/default.spec.ts | 2 +- .../dark-homepage-1-Mobile-Chrome-linux.png | Bin 20692 -> 20768 bytes .../dark-homepage-1-chromium-linux.png | Bin 20675 -> 19433 bytes ...default-homepage-1-Mobile-Chrome-linux.png | Bin 20023 -> 19019 bytes .../default-homepage-1-chromium-linux.png | Bin 20264 -> 18981 bytes internal/cloud/client.go | 8 + internal/cloud/log_streamer.go | 1 + internal/cloud/search.go | 123 +++++++ internal/container/event_generator.go | 14 +- internal/container/event_generator_test.go | 16 +- internal/web/cloud.go | 20 +- internal/web/cloud_search.go | 99 ++++++ internal/web/routes.go | 21 +- locales/da.yml | 28 ++ locales/de.yml | 28 ++ locales/en.yml | 28 ++ locales/es.yml | 28 ++ locales/fr.yml | 28 ++ locales/id.yml | 28 ++ locales/it.yml | 28 ++ locales/ko.yml | 28 ++ locales/nl.yml | 28 ++ locales/pl.yml | 28 ++ locales/pr.yml | 28 ++ locales/pt.yml | 28 ++ locales/ru.yml | 28 ++ locales/sl.yml | 28 ++ locales/tr.yml | 28 ++ locales/zh-tw.yml | 28 ++ locales/zh.yml | 28 ++ main.go | 11 +- proto/cloud/cloud.pb.go | 324 +++++++++++++++++- proto/cloud/cloud_grpc.pb.go | 49 ++- protos/cloud.proto | 51 +++ 57 files changed, 2048 insertions(+), 168 deletions(-) create mode 100644 assets/components/CloudSearchInline.vue create mode 100644 assets/components/common/JsonFormatted.vue create mode 100644 assets/components/common/JsonText.vue create mode 100644 assets/components/common/JsonValue.vue create mode 100644 assets/composable/cloudLogSearch.ts create mode 100644 assets/composable/fuzzySearch.ts create mode 100644 assets/pages/cloud/search.vue create mode 100644 internal/cloud/search.go create mode 100644 internal/web/cloud_search.go diff --git a/assets/auto-imports.d.ts b/assets/auto-imports.d.ts index 3b2d67ee..7247bfa5 100644 --- a/assets/auto-imports.d.ts +++ b/assets/auto-imports.d.ts @@ -54,6 +54,7 @@ declare global { const drawerContext: typeof import('./composable/drawer').drawerContext const eagerComputed: typeof import('@vueuse/core').eagerComputed const effectScope: typeof import('vue').effectScope + const escapeHtml: typeof import('./utils/index').escapeHtml const extendRef: typeof import('@vueuse/core').extendRef const flattenJSON: typeof import('./utils/index').flattenJSON const flattenJSONToMap: typeof import('./utils/index').flattenJSONToMap @@ -68,6 +69,7 @@ declare global { const groupContainers: typeof import('./stores/settings').groupContainers const h: typeof import('vue').h const hashCode: typeof import('./utils/index').hashCode + const highlightSubstringInHtml: typeof import('./utils/index').highlightSubstringInHtml const hourStyle: typeof import('./stores/settings').hourStyle const ignorableWatch: typeof import('@vueuse/core').ignorableWatch const inject: typeof import('vue').inject @@ -158,6 +160,7 @@ declare global { const stripVersion: typeof import('./utils/index').stripVersion const syncRef: typeof import('@vueuse/core').syncRef const syncRefs: typeof import('@vueuse/core').syncRefs + const syntaxHighlightJson: typeof import('./utils/index').syntaxHighlightJson const templateRef: typeof import('@vueuse/core').templateRef const throttledRef: typeof import('@vueuse/core').throttledRef const throttledWatch: typeof import('@vueuse/core').throttledWatch @@ -168,6 +171,7 @@ declare global { const toRelativeTime: typeof import('./utils/index').toRelativeTime const toValue: typeof import('vue').toValue const triggerRef: typeof import('vue').triggerRef + const tryFormatJson: typeof import('./utils/index').tryFormatJson const tryOnBeforeMount: typeof import('@vueuse/core').tryOnBeforeMount const tryOnBeforeUnmount: typeof import('@vueuse/core').tryOnBeforeUnmount const tryOnMounted: typeof import('@vueuse/core').tryOnMounted @@ -206,6 +210,7 @@ declare global { const useClipboardItems: typeof import('@vueuse/core').useClipboardItems const useCloned: typeof import('@vueuse/core').useCloned const useCloudConfig: typeof import('./composable/cloudConfig').useCloudConfig + const useCloudLogSearch: typeof import('./composable/cloudLogSearch').useCloudLogSearch const useColorMode: typeof import('@vueuse/core').useColorMode const useConfirmDialog: typeof import('@vueuse/core').useConfirmDialog const useContainerActions: typeof import('./composable/containerActions').useContainerActions @@ -254,6 +259,7 @@ declare global { const useFocusWithin: typeof import('@vueuse/core').useFocusWithin const useFps: typeof import('@vueuse/core').useFps const useFullscreen: typeof import('@vueuse/core').useFullscreen + const useFuzzySearch: typeof import('./composable/fuzzySearch').useFuzzySearch const useGamepad: typeof import('@vueuse/core').useGamepad const useGeolocation: typeof import('@vueuse/core').useGeolocation const useGroupedStream: typeof import('./composable/eventStreams').useGroupedStream @@ -407,6 +413,9 @@ declare global { export type { AlertFormOptions, ContainerResult } from './composable/alertForm' import('./composable/alertForm') // @ts-ignore + export type { CloudLogHit } from './composable/cloudLogSearch' + import('./composable/cloudLogSearch') + // @ts-ignore export type { DrawerWidth } from './composable/drawer' import('./composable/drawer') // @ts-ignore @@ -637,6 +646,7 @@ declare module 'vue' { readonly useClipboardItems: UnwrapRef readonly useCloned: UnwrapRef readonly useCloudConfig: UnwrapRef + readonly useCloudLogSearch: UnwrapRef readonly useColorMode: UnwrapRef readonly useConfirmDialog: UnwrapRef readonly useContainerActions: UnwrapRef @@ -685,6 +695,7 @@ declare module 'vue' { readonly useFocusWithin: UnwrapRef readonly useFps: UnwrapRef readonly useFullscreen: UnwrapRef + readonly useFuzzySearch: UnwrapRef readonly useGamepad: UnwrapRef readonly useGeolocation: UnwrapRef readonly useGroupedStream: UnwrapRef diff --git a/assets/components.d.ts b/assets/components.d.ts index 651360e2..535358dd 100644 --- a/assets/components.d.ts +++ b/assets/components.d.ts @@ -37,6 +37,7 @@ declare module 'vue' { 'Cil:xCircle': typeof import('~icons/cil/x-circle')['default'] CloudDestinationForm: typeof import('./components/Notification/CloudDestinationForm.vue')['default'] CloudPopover: typeof import('./components/CloudPopover.vue')['default'] + CloudSearchInline: typeof import('./components/CloudSearchInline.vue')['default'] CloudSettingsCard: typeof import('./components/CloudSettingsCard.vue')['default'] ComplexLogItem: typeof import('./components/LogViewer/ComplexLogItem.vue')['default'] ContainerActionsToolbar: typeof import('./components/ContainerViewer/ContainerActionsToolbar.vue')['default'] @@ -70,6 +71,9 @@ declare module 'vue' { 'Ic:sharpKeyboardReturn': typeof import('~icons/ic/sharp-keyboard-return')['default'] IndeterminateBar: typeof import('./components/common/IndeterminateBar.vue')['default'] 'Ion:ellipsisVertical': typeof import('~icons/ion/ellipsis-vertical')['default'] + JsonFormatted: typeof import('./components/common/JsonFormatted.vue')['default'] + JsonText: typeof import('./components/common/JsonText.vue')['default'] + JsonValue: typeof import('./components/common/JsonValue.vue')['default'] K8sMenu: typeof import('./components/K8sMenu.vue')['default'] KeyShortcut: typeof import('./components/common/KeyShortcut.vue')['default'] LabeledInput: typeof import('./components/common/LabeledInput.vue')['default'] @@ -97,8 +101,10 @@ declare module 'vue' { 'Mdi:account': typeof import('~icons/mdi/account')['default'] 'Mdi:alert': typeof import('~icons/mdi/alert')['default'] 'Mdi:alertCircle': typeof import('~icons/mdi/alert-circle')['default'] + 'Mdi:alertCircleOutline': typeof import('~icons/mdi/alert-circle-outline')['default'] 'Mdi:alertOutline': typeof import('~icons/mdi/alert-outline')['default'] 'Mdi:announcement': typeof import('~icons/mdi/announcement')['default'] + 'Mdi:archiveOutline': typeof import('~icons/mdi/archive-outline')['default'] 'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default'] 'Mdi:beer': typeof import('~icons/mdi/beer')['default'] 'Mdi:bell': typeof import('~icons/mdi/bell')['default'] @@ -113,11 +119,14 @@ declare module 'vue' { 'Mdi:chevronRight': typeof import('~icons/mdi/chevron-right')['default'] 'Mdi:close': typeof import('~icons/mdi/close')['default'] 'Mdi:cloud': typeof import('~icons/mdi/cloud')['default'] + 'Mdi:cloudCheckOutline': typeof import('~icons/mdi/cloud-check-outline')['default'] 'Mdi:cloudOffOutline': typeof import('~icons/mdi/cloud-off-outline')['default'] 'Mdi:cloudOutline': typeof import('~icons/mdi/cloud-outline')['default'] + 'Mdi:cloudSearchOutline': typeof import('~icons/mdi/cloud-search-outline')['default'] 'Mdi:cog': typeof import('~icons/mdi/cog')['default'] 'Mdi:contentCopy': typeof import('~icons/mdi/content-copy')['default'] 'Mdi:docker': typeof import('~icons/mdi/docker')['default'] + 'Mdi:flash': typeof import('~icons/mdi/flash')['default'] 'Mdi:gauge': typeof import('~icons/mdi/gauge')['default'] 'Mdi:github': typeof import('~icons/mdi/github')['default'] 'Mdi:hamburgerMenu': typeof import('~icons/mdi/hamburger-menu')['default'] diff --git a/assets/components/CloudSearchInline.vue b/assets/components/CloudSearchInline.vue new file mode 100644 index 00000000..2de11860 --- /dev/null +++ b/assets/components/CloudSearchInline.vue @@ -0,0 +1,35 @@ + + + diff --git a/assets/components/CloudSettingsCard.vue b/assets/components/CloudSettingsCard.vue index 1396ddf4..e2de36e4 100644 --- a/assets/components/CloudSettingsCard.vue +++ b/assets/components/CloudSettingsCard.vue @@ -140,7 +140,7 @@ const { cloudStatus, cloudStatusError, isLoadingCloudStatus, - fetchCloudConfig, + initialLoad, fetchCloudStatus, clearCloudState, } = useCloudConfig(); @@ -199,7 +199,7 @@ async function doUnlink() { } onMounted(async () => { - await fetchCloudConfig(); + await initialLoad; if (cloudConfig.value?.linked) { fetchCloudStatus(); } diff --git a/assets/components/FuzzySearchModal.spec.ts b/assets/components/FuzzySearchModal.spec.ts index d56846fd..ed9e4945 100644 --- a/assets/components/FuzzySearchModal.spec.ts +++ b/assets/components/FuzzySearchModal.spec.ts @@ -109,7 +109,7 @@ describe("", () => { await wrapper.find("input").setValue("foo"); expect(wrapper.findAll("li").length).toBe(1); expect(wrapper.find("ul [data-name]").html()).toMatchInlineSnapshot( - `"foo bar"`, + `"foo bar"`, ); }); diff --git a/assets/components/FuzzySearchModal.vue b/assets/components/FuzzySearchModal.vue index d3b0e617..c2779c96 100644 --- a/assets/components/FuzzySearchModal.vue +++ b/assets/components/FuzzySearchModal.vue @@ -1,70 +1,159 @@ @@ -73,15 +162,23 @@ import { ContainerState } from "@/types/Container"; import { useFuse } from "@vueuse/integrations/useFuse"; import { type FuseResult } from "fuse.js"; +import { useCloudConfig } from "@/composable/cloudConfig"; +import { useCloudLogSearch } from "@/composable/cloudLogSearch"; const close = defineEmit(); -const query = ref(""); +const router = useRouter(); +const route = useRoute(); + +// Prefill with the current /cloud/search query so the user can refine +// without retyping. Empty everywhere else. Null-safe for unit tests +// that mount the component without a router context. +const initialQuery = route?.path === "/cloud/search" && typeof route.query?.q === "string" ? route.query.q : ""; +const query = ref(initialQuery); const input = ref(); const listItems = ref(); const selectedIndex = ref(0); -const router = useRouter(); const containerStore = useContainerStore(); const pinnedStore = usePinnedLogsStore(); const { visibleContainers } = storeToRefs(containerStore); @@ -89,12 +186,27 @@ const { visibleContainers } = storeToRefs(containerStore); const swarmStore = useSwarmStore(); const { stacks, services } = storeToRefs(swarmStore); +const { cloudConfig } = useCloudConfig(); +// Mounted only so the "Search logs for X" CTA can read `available`. We +// don't render the hits inside the popup. The composable's debounced +// watch short-circuits on empty query, so opening the modal alone does +// not fire a request. +const cloudSearch = useCloudLogSearch(query); + +const logSearchVisible = computed(() => query.value.trim().length > 0); + +const { t } = useI18n(); +const placeholderCopy = computed(() => + cloudSearch.available.value ? t("cloud-search.modal-placeholder-cloud") : t("cloud-search.modal-placeholder-plain"), +); + onMounted(async () => { const dialog = input.value?.closest("dialog"); if (dialog) { const animations = dialog.getAnimations(); await Promise.all(animations.map((animation) => animation.finished)); input.value?.focus(); + if (initialQuery) input.value?.select(); } }); @@ -193,6 +305,25 @@ function selected(item: Item) { close(); } +function onEnter() { + // Plain Enter prefers a container match if one is selected. With no + // container matches (cloud-only query like "OOM"), fall back to log search + // so the user isn't stuck on a popup that does nothing. + if (data.value.length > 0) { + selected(data.value[selectedIndex.value].item); + } else if (cloudSearch.available.value && logSearchVisible.value) { + runLogSearch(); + } +} + +function runLogSearch() { + if (!cloudSearch.available.value) return; + const q = query.value.trim(); + if (!q) return; + router.push({ path: "/cloud/search", query: { q } }); + close(); +} + function addColumn(container: { id: string }) { pinnedStore.pinContainer(container); close(); diff --git a/assets/components/LogViewer/EventSource.vue b/assets/components/LogViewer/EventSource.vue index 421e6ee3..9161c1c2 100644 --- a/assets/components/LogViewer/EventSource.vue +++ b/assets/components/LogViewer/EventSource.vue @@ -41,10 +41,11 @@ defineExpose({ clear: () => (messages.value = []), }); -if (historical.value && route.query.logId) { +if (historical.value && typeof route.query.logId === "string") { + const targetId = route.query.logId; watchOnce(messages, async () => { await nextTick(); - document.getElementById(route.query.logId as string)?.scrollIntoView({ behavior: "instant", block: "center" }); + document.getElementById(targetId)?.scrollIntoView({ behavior: "instant", block: "center" }); }); } diff --git a/assets/components/LogViewer/LogDetails.vue b/assets/components/LogViewer/LogDetails.vue index 11a5610b..72026981 100644 --- a/assets/components/LogViewer/LogDetails.vue +++ b/assets/components/LogViewer/LogDetails.vue @@ -37,7 +37,7 @@
-

+        
       
@@ -142,28 +142,6 @@ const toggleAllFields = computed({ }, }); -function syntaxHighlight(json: string) { - json = JSON.stringify(JSON.parse(json.replace(/&/g, "&").replace(//g, ">")), null, 2); - return json.replace( - /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|\b\d+\b)/g, - function (match: string) { - var cls = "json-number"; - if (match.startsWith('"')) { - if (match.endsWith(":")) { - cls = "json-key"; - } else { - cls = "json-string"; - } - } else if (/true|false/.test(match)) { - cls = "json-boolean"; - } else if (/null/.test(match)) { - cls = "json-null"; - } - return `${match}`; - }, - ); -} - useSortable(list, fields); diff --git a/assets/components/LogViewer/LogItem.vue b/assets/components/LogViewer/LogItem.vue index 15051fb0..5879a542 100644 --- a/assets/components/LogViewer/LogItem.vue +++ b/assets/components/LogViewer/LogItem.vue @@ -14,12 +14,7 @@ > {{ container.name }} - + @@ -37,6 +32,4 @@ const { hosts } = useHosts(); const container = currentContainer(toRef(() => logEntry.containerID)); const host = computed(() => hosts.value[container.value.host]); - -const route = useRoute(); diff --git a/assets/components/LogViewer/LogList.vue b/assets/components/LogViewer/LogList.vue index f10c9437..a9f982ae 100644 --- a/assets/components/LogViewer/LogList.vue +++ b/assets/components/LogViewer/LogList.vue @@ -7,6 +7,7 @@ :id="item.id.toString()" :data-time="item.date.getTime()" class="group/entry" + :class="{ 'log-permalink-target': permalinkLogId === item.id.toString() }" > @@ -24,6 +25,9 @@ const { messages } = defineProps<{ const { containers } = useLoggingContext(); +const route = useRoute(); +const permalinkLogId = computed(() => (typeof route.query.logId === "string" ? route.query.logId : "")); + const list = ref([]); let previousDate = new Date(); @@ -71,6 +75,11 @@ ul { &:last-child { scroll-margin-block-end: 5rem; } + + &.log-permalink-target { + @apply bg-secondary/15 border-secondary -ml-1 border-l-4 pl-3; + animation: log-permalink-pulse 1.4s ease-out; + } } &.small { @@ -113,4 +122,14 @@ ul { transform: scale(1.05); } } + +@keyframes log-permalink-pulse { + 0% { + background-color: var(--color-secondary); + } + 100% { + /* Settle to the resting bg-secondary/15 declared on the .li above. */ + background-color: color-mix(in oklab, var(--color-secondary) 15%, transparent); + } +} diff --git a/assets/components/PageWithLinks.vue b/assets/components/PageWithLinks.vue index 20236836..65f9050f 100644 --- a/assets/components/PageWithLinks.vue +++ b/assets/components/PageWithLinks.vue @@ -1,7 +1,8 @@ diff --git a/assets/components/common/JsonFormatted.vue b/assets/components/common/JsonFormatted.vue new file mode 100644 index 00000000..1298c018 --- /dev/null +++ b/assets/components/common/JsonFormatted.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/assets/components/common/JsonText.vue b/assets/components/common/JsonText.vue new file mode 100644 index 00000000..cee2a6b9 --- /dev/null +++ b/assets/components/common/JsonText.vue @@ -0,0 +1,31 @@ + + + diff --git a/assets/components/common/JsonValue.vue b/assets/components/common/JsonValue.vue new file mode 100644 index 00000000..64dc3c99 --- /dev/null +++ b/assets/components/common/JsonValue.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/assets/components/common/KeyShortcut.vue b/assets/components/common/KeyShortcut.vue index d412b4df..b29491e8 100644 --- a/assets/components/common/KeyShortcut.vue +++ b/assets/components/common/KeyShortcut.vue @@ -7,7 +7,7 @@ - {{ char }} + {{ char }} diff --git a/assets/composable/cloudConfig.ts b/assets/composable/cloudConfig.ts index 7e774548..747c4560 100644 --- a/assets/composable/cloudConfig.ts +++ b/assets/composable/cloudConfig.ts @@ -19,6 +19,10 @@ async function fetchCloudConfig() { } } +// Loaded once at module import (i.e. app boot). Every consumer reads the +// shared `cloudConfig` ref — no per-component fetch. +const initialLoad = fetchCloudConfig(); + async function fetchCloudStatus() { if (!cloudConfig.value?.linked) return; isLoadingCloudStatus.value = true; @@ -49,6 +53,7 @@ export function useCloudConfig() { cloudStatus, cloudStatusError, isLoadingCloudStatus, + initialLoad, fetchCloudConfig, fetchCloudStatus, clearCloudState, diff --git a/assets/composable/cloudLogSearch.ts b/assets/composable/cloudLogSearch.ts new file mode 100644 index 00000000..7dab9e91 --- /dev/null +++ b/assets/composable/cloudLogSearch.ts @@ -0,0 +1,155 @@ +import { useCloudConfig } from "@/composable/cloudConfig"; + +export interface CloudLogHit { + ts: number; + hostId: string; + containerId: string; + containerName: string; + message: string; + stream: string; + level: string; + // Dozzle's deterministic FNV-32a id for the raw log line — used to deep-link + // to the exact line in the local log viewer. Optional: pre-indexing logs + // (or older Dozzle clients) won't have it. + logId?: number; +} + +interface CloudLogSearchResponse { + hits: CloudLogHit[]; + hasMore: boolean; + // Cursor for the next older page. Pass back as `before=` in the URL. + // Omitted when there's nothing more to load. + nextBefore?: number; +} + +const debounceMs = 250; + +/** + * useCloudLogSearch performs Cloud-side log search via the Dozzle backend's + * /api/cloud/search/logs endpoint. Identity is derived server-side from the + * authenticated gRPC connection; this composable passes only the query. + * + * Behavior: + * - debounced 250ms; whitespace-only short-circuits to [] + * - aborts any in-flight request on each new keystroke (AbortController) + * - `available` is computed: cloud linked AND streamLogs enabled + * - when `available` is false, results stay [] regardless of query + * + * Status mapping: + * 200 -> hits populated (may be empty) + * 204 -> streaming disabled server-side (defense-in-depth) + * 503 -> cloud not configured + * 504 -> timeout (500ms upstream) + * any other 4xx/5xx -> error set, results cleared + */ +export function useCloudLogSearch(query: Ref) { + const { cloudConfig } = useCloudConfig(); + + const results = ref([]); + const loading = ref(false); + const loadingMore = ref(false); + const error = ref(null); + const hasMore = ref(false); + // Cursor (timestamp_ns) of the last hit on the current page; 0 = at the + // newest page. Cleared on every new query. + const nextBefore = ref(0); + + const available = computed(() => !!cloudConfig.value?.linked && !!cloudConfig.value?.streamLogs); + + // Two parallel fetch lifecycles — keystroke search (cancels on next + // keystroke / unmount) and pagination loadMore (cancels on unmount or + // when a new query lands and supersedes pagination state). Tracking + // them separately avoids the keystroke aborter cancelling an in-flight + // pagination request and vice versa. + let abortController: AbortController | null = null; + let loadMoreAborter: AbortController | null = null; + + function clearResults() { + results.value = []; + error.value = null; + loading.value = false; + loadingMore.value = false; + hasMore.value = false; + nextBefore.value = 0; + } + + async function fetchPage(q: string, before: number, signal: AbortSignal): Promise { + let url = withBase(`/api/cloud/search/logs?q=${encodeURIComponent(q)}&limit=20`); + if (before > 0) url += `&before=${before}`; + const res = await fetch(url, { signal }); + if (res.status === 204) return { hits: [], hasMore: false }; + if (!res.ok) throw new Error(`cloud search failed: ${res.status}`); + return (await res.json()) as CloudLogSearchResponse; + } + + async function runSearch(q: string) { + if (abortController) abortController.abort(); + // A fresh query supersedes any in-flight pagination — that page is + // for the previous query and would be appended onto the wrong result + // set if it landed late. + loadMoreAborter?.abort(); + abortController = new AbortController(); + loading.value = true; + error.value = null; + nextBefore.value = 0; + + try { + const body = await fetchPage(q, 0, abortController.signal); + if (!body) return; + results.value = body.hits ?? []; + hasMore.value = !!body.hasMore; + nextBefore.value = body.nextBefore ?? 0; + } catch (e) { + if ((e as DOMException)?.name !== "AbortError") { + error.value = e as Error; + results.value = []; + hasMore.value = false; + } + } finally { + loading.value = false; + } + } + + // loadMore appends the next older page. Safe to call repeatedly — guarded + // by hasMore + a separate loading flag so the input-debounced search and + // the user-triggered pagination don't trip each other. + async function loadMore() { + if (loadingMore.value || !hasMore.value || nextBefore.value <= 0) return; + const q = query.value.trim(); + if (!q) return; + loadingMore.value = true; + loadMoreAborter?.abort(); + loadMoreAborter = new AbortController(); + try { + const body = await fetchPage(q, nextBefore.value, loadMoreAborter.signal); + if (!body) return; + results.value = [...results.value, ...(body.hits ?? [])]; + hasMore.value = !!body.hasMore; + nextBefore.value = body.nextBefore ?? 0; + } catch (e) { + if ((e as DOMException)?.name !== "AbortError") error.value = e as Error; + } finally { + loadingMore.value = false; + } + } + + watchDebounced( + [query, available], + ([q, isAvailable]) => { + const trimmed = q.trim(); + if (!isAvailable || trimmed === "") { + clearResults(); + return; + } + runSearch(trimmed); + }, + { debounce: debounceMs, immediate: true }, + ); + + onScopeDispose(() => { + abortController?.abort(); + loadMoreAborter?.abort(); + }); + + return { results, loading, loadingMore, error, available, hasMore, loadMore }; +} diff --git a/assets/composable/fuzzySearch.ts b/assets/composable/fuzzySearch.ts new file mode 100644 index 00000000..4fc7ca99 --- /dev/null +++ b/assets/composable/fuzzySearch.ts @@ -0,0 +1,16 @@ +// Shared open state for the global Cmd+K fuzzy-search modal. +// Lives outside the layout so any surface (home page hero, mobile menu, +// sidebar trigger) can open the same modal without prop-drilling. +const open = ref(false); + +export function useFuzzySearch() { + return { + open, + openSearch: () => { + open.value = true; + }, + closeSearch: () => { + open.value = false; + }, + }; +} diff --git a/assets/layouts/default.vue b/assets/layouts/default.vue index 83db5328..1b066e52 100644 --- a/assets/layouts/default.vue +++ b/assets/layouts/default.vue @@ -3,7 +3,7 @@ - + @@ -34,9 +34,9 @@ - + @@ -63,8 +63,10 @@ const { pinnedLogs } = storeToRefs(pinnedLogsStore); const drawer = useTemplateRef>("drawer") as Ref>; const { component: drawerComponent, properties: drawerProperties, width: drawerWidth } = createDrawer(drawer); +import { useFuzzySearch } from "@/composable/fuzzySearch"; + const modal = ref(); -const open = ref(false); +const { open, openSearch: showFuzzySearch, closeSearch } = useFuzzySearch(); const searchParams = new URLSearchParams(window.location.search); const forceMenuHidden = ref(searchParams.has("hideMenu")); @@ -83,10 +85,6 @@ onKeyStroke("k", (e) => { } }); -function showFuzzySearch() { - open.value = true; -} - function onResized({ panes }: { panes: { size: number }[] }) { if (panes.length == 2) { menuWidth.value = Math.min(panes[0].size, 50); diff --git a/assets/main.css b/assets/main.css index 2db1ce55..72da8ab8 100644 --- a/assets/main.css +++ b/assets/main.css @@ -214,3 +214,6 @@ body { .status-pill-warning { @apply text-warning border-warning/30 bg-warning/10; } +.status-pill-error { + @apply text-error border-error/30 bg-error/10; +} diff --git a/assets/pages/cloud/search.vue b/assets/pages/cloud/search.vue new file mode 100644 index 00000000..8f5bcb2e --- /dev/null +++ b/assets/pages/cloud/search.vue @@ -0,0 +1,232 @@ + + + diff --git a/assets/typed-router.d.ts b/assets/typed-router.d.ts index a93c008d..27c52cb9 100644 --- a/assets/typed-router.d.ts +++ b/assets/typed-router.d.ts @@ -44,6 +44,13 @@ declare module 'vue-router/auto-routes' { { all: ParamValue }, | never >, + '/cloud/search': RouteRecordInfo< + '/cloud/search', + '/cloud/search', + Record, + Record, + | never + >, '/container/[id]': RouteRecordInfo< '/container/[id]', '/container/:id', @@ -167,6 +174,12 @@ declare module 'vue-router/auto-routes' { views: | never } + 'assets/pages/cloud/search.vue': { + routes: + | '/cloud/search' + views: + | never + } 'assets/pages/container/[id].vue': { routes: | '/container/[id]' diff --git a/e2e/default.spec.ts b/e2e/default.spec.ts index 7b355734..40f33e27 100644 --- a/e2e/default.spec.ts +++ b/e2e/default.spec.ts @@ -19,7 +19,7 @@ test("click on settings button", async ({ page }) => { test("shortcut for fuzzy search", async ({ page }) => { await page.locator("body").press("Control+k"); - await expect(page.locator(".modal").getByPlaceholder("Search containers (⌘ + k, ⌃k)")).toBeVisible(); + await expect(page.locator(".modal").getByPlaceholder("Search containers…")).toBeVisible(); }); test("route by name", async ({ page }) => { diff --git a/e2e/visual.spec.ts-snapshots/dark-homepage-1-Mobile-Chrome-linux.png b/e2e/visual.spec.ts-snapshots/dark-homepage-1-Mobile-Chrome-linux.png index c27523b1e3a2b1b774ae3ff26e5056cb46552551..8163f4623554dfe0222f5fb81f5c52c284f6d040 100644 GIT binary patch literal 20768 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 diff --git a/e2e/visual.spec.ts-snapshots/dark-homepage-1-chromium-linux.png b/e2e/visual.spec.ts-snapshots/dark-homepage-1-chromium-linux.png index caff1ef23868dadd5cfd8171ccc40a5d701445f0..e5bd3880cd72af86262b400e256211efd023db28 100644 GIT binary patch literal 19433 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≤sK<-iM{ zO+as|7q7vYn|-EDm`_4GDXYBix^3<7{n`m$jc|;1GdWt9lf8TwdBYlaLKx$sbMP~J zUc~#yCY}RUxj<+euhF*(Cw=Ivi^5-YO7SZtaE`etM#VNOpXn+>&}2Y6HYEr0Z=9dz0@9lgY$?iUhfxY&;LCnmDa z(r=GyX}+X2^ONkywGC;Me{^@~UTN^L?jR}W7f8O?w>}Rmjm)^x%JbIeK1AFYXq(;{ zCR#RiJqbP4yHZNNF82=%e91DhNfvjwgQM>|Hp#f}txHm&ayy)7sM-|#YHyGVT^851I+AVFz zRO)TiIA+!ZqVO-3RT?VWUnPrg|8P{&HlVBH*H|~xR}RwcQ`(M>Y&M`yY2 z>Y|fc{AU7Zi>?%v3$xJ& zJY<3*TL&12fJQI9etio+)NmpEo#)~mqpGRJ0>25moI33ORCv8+Sh14&@h*~ zypCnkh86F!Oa7jB{kVUp1N!0EF2eVB0tZ&8na$(uP{QoOWBidSA9qLEs5q3n!Nmz5 zcili_GmR71JBpz%m_5y2_1-UpUv23}6*vx0rfHT=pX_&VEscY2Cnu-kQsB4kl|n@->TDFZpjhq&&~; z?&(7J#;I13g*>2FEUy=X3SkGXgRnXEUiE7>Bf0*5xzGm z$f6|WHsPJ?p=zAjgk(Io9^}+7-8SnY7W`Q?aHl(nZ#D1B=EgUv_-IVyT z!|*&+a|wq_!0R?VGSb#7eP(7R)zIsxep1j(3d8sLc6fZ;A}nSCgX?K6_+8gMqr$5}2yJpv8ucrI!twjWsPze1HcQL;lxsWn_ z09$$4LQkPk`tDFUdNLb1g6={#?=9t`dVn>`_uyeyZr-i{`gUGVRpS?sy)ckzD|CzX zSr;;E$bH>)G4pFzdV2bBxRA9WM4N9N#R}z?acZ)9$OD4y&9BQ$M)Ed-LR#oCB<^T5F4>W9my~RAeh8 zf}-T-QE}whYf(!y)s80XUUcYJ&rx?neq>U&PiwKa#=S)37HZS6cwYbQ=;*lQv9Yn~ z3qvB}`Sv#)h3v9Mui3HQYO-rDMX$+WbuipgBF}NTcYq4dz_GEQh?H)@K-~hyf>A+3 ztFnw^PTgHe3$-$GbB}J_pz!%`K&QiC4Q^}c+$LG$RdqoI1cO*P;gn%Sqn#(k&JIFv zGhVPp`+zN#UZX~%r+)gq@467d?0jcWn-n@2Y3$L~XbfMpQK;uxA0-t@4w9}JR<`xdvgjDJl1zPNuc>El%AMm`eLSP=D?>`( zcoWf1|GK11o;-K%?eJjVcpJBZQ~50S+_t=Zu==(8ZAj>S`wBu3Wd)g2|>ziK$>ze@@Y|?ME%{uQ5H6}tbeP7=1T3Y1f=7-7S zZ3+_`M=}0ku0xJSlXJr8GMpj`iGAb6V=f(y}gpDr}O1lpv9~#e0nuK zy(r1GgIxbEZ+EiIb~Fj+KR-#rysR|U)aspg2Q3-KNQ=e75QCr)dKWA$EQaf%_j^Z@ z>70A>&fNYiO0GovpCHa&WrNJl0p0&@L-`;H$8z>kw zgKKg?m|r8R{;I23QPFhoP4ogxB8Y*nl|HJohofjNA z71hUy(FZP1*bLr-RiHr1|=cW*apzqh+Z|M#2Rab4})9Imf98?bhB{Mepn zjf;jpR|5&^-48__9X`FmUn*osJ4Kfd4Q=7e4eK=wT2 z~IiYRGU( zgC!BSASgnxqCz+)!?M)hLcPFrCmkKnlRe?S>8q71X<=}&ywT~s`{U?7&m)lK=3vsO z))Z3FZVZRd?Ub&%0~HT%G-Gn1Qcus<#KpJs{rM!-z%AWTNkRg$_iw6^U#r;=BEP(> zY@|5_%C8#5t1d*A_p$!BFQp(&>F{eh&~ORt3#*nHf79>FQ}p)c2F5l z=NjIWyiU0GwCKzg(_CVNDWm)Jf`?pwoLF*{;Po)N1{^P)Q&Zj9_NX}fb6#ole`bE_ z$;I|>4Hig5Rkfe<$`fx5_4gBt9o?v@sS$afvIJAhci*qrRd+pu&JljFvsq1u+f|tj z#m4bp|3TndLmok-1|g;-GqxQT;C<~tLE|p?zCUJj-Wvv?0vjVMzeQ$z%;m?PpTs$jsk-~lMBaCDFiXofG8s?Sy5*T{fwHa8#GLOcW(YWeEElBhQ#+OeXV*w zJ?ahXtH6_HhtQbzMn{>h&u8WW;rq$b(0ZApRHxKf1cv(+7C0)0n&|q1O+M{C^yQW! z$r^6v)YR0%Txo&?8&YBV$V5bki;uUnO4EDTbzzWEs4fs+H#%@;-=6O>xt(`NHh-y= zD7F>*b$^_QVVYfo^G0>ny|%l}S8QkpHDyj75oMj7PPVh7hMz zW@fupNOez!a4B`8j#2Tk{_iPcjif0y|K3`L|*3u;n za=y^Zt5EFl3NDGjaHK)4KZ_h4T{ur+MX1Yx&NHangSnGB37$#Gsrh|1L=+c@j&`z8 zDRwt|Snl_FB^3rv!#9Pu4D2M73vuRiUB2G(c`T4r+KKFTCIsRl4Xw9G*WDj)an4g6 z`O(y$!;HpdlozmYDp&?j*=^*R9ONel3F`Dh+N)ygBVU4lVz~A(c2kc`YgQViH0x!I zpf6jac)2IIB`=_`4(npOm zel+P>2oprHRn4ESQpzjyuhAlbsWj>~%q8S@+Umi?V>bT1`SI?KNxy`o6V7h8ja<5M z`{lMoD&Zm{&r=L1!p%W0z`S0rlkXgf7Afn1$j|-l7-QJ(y#hi*v&|ePqu6n&(!9i4 z;QJMAe%(lJKo0_wTm(u%N7p>xm@l8TjSSd(J~J(YWpmDHPFG$$$;?Sun+8)9%KXp! zVN8tqa1lP$JtgD@9Z`lP5XiUNQC?ntj!Kz?<7#DOMAldI0>&CEA{BeqVz>Xt-e@Y3eXaC!7AYzW2lBhuoBcZBaB(Sh?w+jX0oJik0#<-9>iK)- z*D$i;23dx3O*z`+NZhkCZ?w>R&gxbDdgo?*tY5by>kk6?4Bm5wIxn*^LR?7eKFIM_Rx~kPuCpNW ze`T>tuw=kT#Eq^<4woiQ6nSyYL}vmW75~oH?UXvsEELCUv^FDt9){(6=wE956?#Qx z|GZoyIGJdCRb_C2!l4w0&%m#uI}rM$h@Hkzy+P?}1D#7uLH9j;R_WWWkzuX2J`Lg@ z*bX@!uiTL5NP7m9QM-@=Trsz-Z~c+h4TE=IZs|`r5a32F4o~cM8cPlDzT{TNv-H$Y z@WM5s!!@lCL;pKMeI#FDD4*+LDkOnh%5J-I?gX=8*wI{FMEi)r~$)oKkvl2b2^cP3P2nCLMWuEMs z3h91-nEnnf9MAQx=+!GjTL2*&l`lnSCaGp(gBxxsoLg z)_$cXo43u)qLZAjoG|=Tt1gF#@4#&RHw?BzgI6M6Tj^&smu7`lIMMO$)uB?PGIWh< zE<)_?aqH{viCUAiHCxNsm$mH>CghaN{WiNuyW2P~qoUR+?ES;ce_rd}uW~vcK{>B; zT3*{+ffB4$;OHPfh*}x>R78-k->#hO${LD=kCREw*wBrPHT38ZZkq8U z^>TL*vDx}V*66C}oG&^egCZ_MPd%^cfaSkRdP*}@px)@7z)!7qo>g>SwWRf>MADAh zqX{dDL-DhSwN$b>GWfLP6=q=(?Q(4ZlFGt}d{kH*Fw z1{Yp~_xgTMdohfK5AuWwmbc(MZ#$K_QdFzS*%+o-8hVLL4m50TN9L~Hjk$|nNyj53 zXZV_l#wg%YTpG7Yd*0>8?Cmtf$|hw>-6hU2Y)mFOLePXa;0}HdTqYL7aNRUVz-xH zWX*Do=yp8X=JPshRm~-t}I| zw?3!E0dW2&4pY&TT7v{!zv+|?(iDfyO=YGk%wzq4((G)|!b`7=!rr@0(ExVT;A_PQ zO-twJvO)xq!U~tU&ap7(?9}@}Une3Bou&vfZ7F8=s%2JtD~4Y}Pj45}9J_TxU-Z-> z4=$L5@&+OSZ^N`z{G(GAAj3a`C;MSD2^XfOyLVK)IG^V!vc=rU zoBmn}Y=z`j>afU8s_TF*9T`*GENMbqp_(as$xGDq)EIdL0u@xJ6KFx0$_(2SnRJk1 z4O1Lc^uaJM{scs3n$2gZorVi+5)mI{Vn_Gttq|RV^gXeTX=x~x9zOQ&$qn_gysRN? zkr?b=(|bW|@8#89wrxGpA=7P=d>>igC(5+4fYSx7bMX0x58me>Y~bzCuD2ZjJDk{; zX@)%`g=R8cAQ4|t6oQq;8oqLP-}Pm^!@JMr*9cnl87`{sV;30ab*oBJTCN-dcV)yqO>^*;?Krjj1g9|q}evFA-C&Rac6Zc z&pk)618y9%tS^%DbJ{Lqv%{!NE;c0#nozu{H&GCDe0g;=?h&KXBQb|k}MboK8LL$vnz4kAY$luC?Z<;+0qP) zAY(3@Jqs8S7|Q?;%ttzi4_WxACv4HQgO}Hi#@)}8X*7vb*X#RM$bwC4TnfHdgJdIz@Ru81k}!8G9+JG`Tl1lE ze~d-wYX;d%2n(sHE=kDU8NDK|MTSQQ%gP|Sp1MVAyII1f+V1e?wrX|``N?PBAVsFp zO*+bS@PYBR&d~emZyq003#>H5MR_(&^}4LsI;rLo(`jNT{E?TtjA-u;x@Q`bjz@ z>_Yb&`z5*ooMh3kB+L>@sWYSE2`X~qelZvqc{B6(H`j$23m1nv&d(RF1uQc(eEL={ zwAEGDdm0%8bu<*i+6Ss=W6#z<_TA_PFhG4w5uAmi)|KC$DbVO1WGD!0XXtn>NS2Ei zp2Q_2Pg#h^@+c@XAgXB>9wlz&K?%l9u!bTPt}ua-ff@3`)!^lg}sQJ$Oe8#>WVooW)vYP+@g*n-FWy@Lb=`&Pbn zcuy7sU)zgLtu1`xlPp!bmK`(dNwK5Cn?&gJ%LxB!$r@F0#2!~YN|+7jwCxtqr7dQr zST?0S+)-mCmq$=)B}p*jEw_Qu?ERJ##ZV7FqU7i)KBGrd5tsV7ic!ABuUcW)>xaN} z^9cFH7j3SjSLj-$J_**hzoVo_5jB~PcOVyI!J?_Dd)zpfCaw}J;5JS0Hk8@F1eq(x z`KrQQXr^0b@=Os7wp&K4r)T~rT4Ne>Rfn`GN<+Y}$v*BXF(&2d#38A51-CM>_c$KE zGG}UCsH&vfJ0&9lgW|DMW*Qpal3}yo>&;mt3qb zHM1ASb#cthv%vHn_U0;A0f3VW_KWKX&miAKMH;JqP+|)hv?`1|6~@P5E>ywVScNoH z{i!L@)K`-~t0?@@$EX~hgzf60<)3@Ma_KnYeAdXZ3ORiccV8C5v6F&oqY-db&ad_> z(=pIF$@wn?Y>8D0KPb%h=INliP8sV)WjvP*`6`uUhAs)EzlS&ghEbY05DqnbD-eRT z9|;xJ?{3K z!<#~;_Zcr^y7l`(j(~T@cz|A$q=WhKIYRzh20k-J7m#jq^vi^Niz$ppE zA8K6A#H%!o&%NsXvh!g>j3Y5EjobM5kMT^dK0?e6l*_p?H37m&;21AY>nBAsu6BpW zls!8)LM5GKK2|6^p<=a-zLZkj6Lb#2O&ycf1?^kiL1bmE!9oYBG z1qf3eFv0wRMQCk&o9FB8S6l$U=i%XT;nT_6lz=ih0%Vp{Cm2uD*Uc)MjA`wUgfICo z4I)gKW7gF3VqBPzAt51GS66Amfnrrz%qCzfK^~h-U5`5e9kBv3$s5w2ekI|`#M7%5 z)M`%FV$BSeJ*%DtsG@!}OiV(1NovSHkt1Kkiq|w@_Sm8EQmAX4~Kp;3w`ro`^ z@Hm0EaGna?d`De;b;$mx<7hCS!6S}Us38fDDDUJ+oIhNKF*eoW+;X6d&K9xReS2d}wOeVhvG(k|8pO->x&AUjAfREgm(A-c%62xdW#qe`ZdLQmfOu}-ddGy) z-mD*`Bt+}r&%}geXThQ{iMYz?N+aFz&J;cbJOnwqaDKa~6k?kj^6%qy!!Hi(w@BLz zy$6L{pXW9F`~=Bj>0Lar0^!;^;CkTx|MTzvaXS9L^woI7e0ok!P62^++s0$9LlTdl z7RzUD7whfK#l@ind*bF|`uh5gj*b!%5;{8R1=P`7B+;S^|G@&p638>h7rK03{^|I3 zzi|~QaWIm5VS{tC(Xf<{kOx8TOVwozu)C-g@b`X0U6>x)p)K{g-kNqxIdu5j}3KwVYXNS zkKd!_9a-Qf;EDbMCSfDX72^CjuqV-BwaJs8a6$I=Wbx~G28T6&{XUyXZ!n2iXw8)?ad|jDYQ*Dw0Wb?6{JQeq00Q z(4C{%5(IGd{5$;>xYpVIULUm#Jtex^ZOffj9hx~h43xxkVWEOYStZCBI4zYZ84}>|&=skI;s1`CZGKYxj&z#1?*?jIh;cn+^ zIJ*klLCgfaE|*U}kdTn{tS;VukK51yohhIuO*JW)=7t!I!Fw2G>K6B~E9m}IN&nOE zmfom~pB)++3Cehi>N=VD2QbXc8enj?XXKbHdTNuP2|Ygfexw0Wmgmb^vzfKUSQ-le zC~3t5tkORE(Uh{B2XX%R0{e$ z*X0kxm*tyruFb>m%mL?So!GXA%0saPi3tfCUMCE0V^bqwM+dxarjM0MsAy<+dQ%d; zfsk;@6&(O8hd~_}7`WLV4UeFxdt@8h`N|=$3@r6ZTqb6<968df8~_7 zVOXSZ0>EXA3=_W#%#wwR=?D!5{TAc|5=KJRyuW|Vmx7d^f$KYT^5c4xiKPZS3@}_e zhK7V0{^J~rTNl;c?_Q_DL<`4Y8k(A#8X5*3+aUn_4i|*J`lPeF;e8R~gz$WpLk#^5 zZ3c(D<-F|#InkKJ77p{(at4^Z%vZp?H~2P8@e_kVupgsyOtc|L;8|=bi035eknRIO zsaIHf+8315F@e_~zfuip>FMns(M^#Sa2%16dV7I!3!}>{-;FU}D}2)2i%j*?6nV@! zkW@!FPa%_YZ-0NMM&kNe!56@vN-mg5MGiz^A>naWSd4cT#`j&j1~Q2B5*74jvzS4lE=OX}W%x1B zDsw-qeV-&z6pnhjIXK~gNgh94u2(T69?UcPc;)w8!0<1CP+!iw` z+NpGa6WrpqOXtW@wX8T-mKbW8d-nt8k9!6X4*s-s*K#;a@Q4hPsc0U&sG_))u{9#D zZ)8ERoVFqzDF=DVP>VNwz|AW@k|I%CszIlM< zna_fQWP#5u{Huo%1zj>h2zeG?Od|s6o5QQom!;j7C)C}Q%3`Ah>8F8cYZ|F%+DZqA ze!?ZQ><wC&yiL<4cYu59Vtzmu z0jw%c|n-n-XX z`jU1Ti?il*(U=Al1ps*p=*w!u&I|%5-PUk7CESx7Zpn)8HkgSG<buaOU)FOD8QR%pTwYI1{VyhFayn(*+SJu? zeK+My6%1y-W~8QK5fLGUr7-AWKCQzbL1&B~;|WY%kFzfLB;Di4+M+&Vb&$Q_abB3Z zhIP?s2J3t7C7;$z+Xo~?n=e9^gb;#;U%tXzTue{<_Si7@z4~rdeLJiu@AQ{FP0|U6 z4k30<;@g9FjJ50|yyef!8W+$Yk^@YC1exL{eeBf{gd?~`+SE{H&P-&-#j4dM>2 z*yb^7ILn?yY}M!n#%Ya7pYAkC{z8BUv2Xju{p>p<54{K5_@66;(+EVWTF?aV!}lze zv|SvbUZI*WlTpT#u&7IAo78tjLluF1aSxt+AC>??d$oy!r zsV^>x*KUO`pw@U!N=ix=7UzSB>{I5cQ}kj9sbK-W(03Z1^!$}Sw*&kGp@}%J>$5*b=O?T4e2%4WA)Yf6KK<&LHCEiLjc@7h$z$pj zn)SN)YK+YOS7vjC)t*BL@!ye3o!;KsmW_0X_p!Vu_&+19V<{MGM!hY&&-HUp7CGnZ z6OjOc#TBrf33=UGyJ1`3cX8S;3PEyM2jn_BDi*f$nGMdr;;RQvL)DMl#NVW24W#KmkhBCkR3f z4V~}jWvdW@LwGm8M_t_wD~qUJLJ6_nevhhpigO_>T*VTZJbAE)hFeUtPWIPwUv{a0 zpG|hr@#^2F3aO)+K{itzA2sh)oxKHl7xGsuMsxUfgYk{jxW*k+pn==*b@{K|ArX6! zz0R$_AdON*k!>ab;}DQ_CSco7PHV6&8g|5mMazAR*45PV8j${5w(p_q63$%w)b1nq zF^4A?tK+WYn+=a?p+R4kDp}Z!exKjR5{AqE`F=k9Ee?@|?Vtt*8_;6Zkx^Joj+Lx1 zcxb*Kqw|xmeESmz85T%z4dAA?NE3m}%*Z;06@jbe)ta=I#A=IzmrA20a$xg@io9%u zPQ_O zSgaV`dW{2;XqEdI^6s-k8X~{XjoQ%d=&NrV1VnES;(4(z)6pt|H!ZVr;}+iQjXd^V z+h2J069^rf=Sq_}a+yj!ak5e7Tp|^5+HzI10;fw-|4Mb`(~Iez9|Q>*;r$Hf3}OcK z?xPZicL*niMdZXH&_esB_Ej&ZwF+yyIHaV6WJ5S^7~74ZXHVg`s?MH&@oskGgzEYv z?KpR9`Q}=v7yr(x+~$kA1 z<@5cAPq2Wo_xJpRzuOab6Rx>}xC+p_ZTzhU87Dhz`l(96XyJ4U7323KU_+H!+|>e$ z0ybG^F(IZlH|Hci+I*;)53Y!};=4|q6#=Ftf`O8laO{h-28%i6{&{LV*}rFID0iy`ylJPBCrwM+}tQo&azGN ze^0C8^>A@c!k7*hF4cmCg}prj;)URx$cTvH;o<26_v5)EKpMVn1tI15zk6m;T*@02 z0}T{F{TtSr4pivWkBp2ID`tZxl>l$0r1W*C(X#$2Mb9yS_p#l2nv{O?*mX%|LbVj;Yj{gdUI)Gwih##_HNkhmf7Oo$$Zrh| zjW8bvAR*kE3M29hw|V3ObO{QXZEM4w{{m+iQQ`e?4$XI$uVt>6pML@_ zA#Q7XJb+~sjUIZGk%3zpB#CneJ`Gt6!*~d$2Ep`C*VcM$J~qj39p{>bJ%&CwoX-pW z3PhcqEFX_bjY{S?g%RaY#sjP=M?I+bA8=K-z)pl|60IO!8IskfJhe~6F;=6Qs!8XW_JWygA-88#_1(|Z6zD^0!_ z%gDzWs4L|^b$`6Sw7H&8%BL{{H7j5oxb*}=hT(?`KcB3%)=yiDe_WtEQeFe)$&B1w zqOS4&9&UOt600}Bni28391aorcUVssTwYimJ-+!xgWCf=sGua^B>%q8?rO9TppP7& ztmWzH37nAw*K}G{uao}oeY*WX^(dk{*YDY8HY$ctz-u59!$B(Vw}Us5y*5YIjn(us z*b$E%Kvsa^Rb5%>;DXbSDo$)2;8fs`_HJzaiV8*&%AnT-kMwWH7JS?a3P(2bxq`G4 z1OQ4ry&J$wYMX*@GP;)yb&&l|uG(-G`xU*T8$avn4g)+!u-f{ z{57a$Py_J;acs)~*7FL03PK@2e!fSg?=|v%KY@1Su@JmyQCWF;`@@EJtp>JP_jV|r zXf7!WOFYLD9Z|=^ncFVYZ7)s!hhs40D92Jz<3B{{5$?LvXUcccdO(=P<`nx@W*p=V zInA+Ca+i7aTwNs@iywg|ki=Ruj8v-b@NjW^#R+@;(HX}n7;OXjzQkEUbP*^X{bwj6PWeO_ z>-%ATw;UpzWY2z}KpeL&Ap|0cHmf6|97@c{K2T5;%@OTn^?&!tDyxC!Lz!o`U>8br2)HU>rjuOmYbo0&taA?wvqLLD_wwf)aNl`lZ zX(tVB!fgnva6uz8s7qZ9l}i-3Jc_j@T_m(9KQyJ1X^>n@&rn0C!Q=d7|5M z!Nv4uaGtgMI`lM+)G?MZ43u(zR^(3r@84Kp{vl9(7fbLxE-S%?4lBorOW~Q2Ygnpo zJElF=&)VeHr6$g3M8*G1gNx}tV z-p7`fmH>UG;lI>hjvH7M(Z<~oDpt90IW$p&aKi=BBbIgb2N&4986Ii7w(i9@Kfc|u zeZ;ojv|=uSGh_dVu3iQ2wb;o|w#y3*`Cd3_3PuCo$XqZA{K)x!Ib~Kd7=>(J3@I=S z3|tHD#M_I6UJ?lSF_Ru!EDJu{jGtC9U^?44$Bvk<`62K1teDZ%hnmJHK`F!=Di<2P z{d2?!yZGc{XZX0yB8-E^ilcrrARUG=rYKj%vI^Ol2vdfF9LCrC8qGN2!xI1*GqZw$ z=0Ycr1E8Vu%HkI2N666A6snQE@qclx99!FpVBL!Uqm}~$0iVgx7Y|`tfI2Knm@?8f zje5UAH3AEGFG)m&VJGfRfpE`C4N*LTO7z3wG+FNjC2?>bPBbQF9qJ5q#>0PUg52}S zHULFfR3gkX){@x>>F5Dmw{pqAHJ;V)*G~UM&ZF67r%ZNPIRt6G3nEY0oN6XLmr~sZ zqBt0zwk#TY$3Vh?;d^JLT8HHGwde@_`kgP)VC`}WUrF`oRWqn7fb6FnOFVnD&Ld4t z9p-+vGN20qV(DP|Ywg9-A!ln#D|;G>;l-Uy#6R7CmL@=j@ZI2s>xOIel*xB$C%J*K z(%+Xj%WzWgg&u?%9hmG?^#a>h{be5*Z*~=O?LTD zru+6iHvsjM?g|mSGig7iaK9CKClMhBN)M9%H(cF0L_YkRsD$Vro+A?-VQFa@x^J9j z|MyQk5E9};f|=YB2A$=Hab`LIAtN&NYm_A*3ehO#fB&EWQW~iMVPG?#u*&r7f874IL;6@Cp7RJF~jDOP9|^A0@2)o4RXn_U9Ea<(;j!trZSvDOkRUa+fm7NcLNvhtLu$jSMi{wC&^ygyC)AEN724n5|EKO1aLBAXFru3glr4GH4X7Re z^V>fErvP-oNR3zpy+l4eAuDUF1&75{yxx(*t-=`_QdU;h)E^c3a3_k%F&P0HSniG;-$Tv%E+|uG`-#6@^^Bi=3kN{1PogFycZySygN65 zay<*XKj8X+>XOH4?T}`*p@8f09B|fQvTA5)bzhIrJ%dp@|19DBx}mSEoVWRT$+8}} z`N-hTaXKb>63CU-+gyQHU>Jr-@P1eApDtg#zY+FaErq9Gaz%9;?o_M@kkIG5lQ0+v zq<;xg0=RGn5a;^==P(Svmw4-8LDc0#Dseak0QLP2^TUvv-7YpNC&j0OjsSVe^2#v5 zbAT^`Y&nsoqp8{C^YpC)f|B4g=Y9y$fgKD-&6G5&EHOJfW*4_D$m5VQZF+hdC;;T^ zy^Pd#0M$_-jXps_5w#r8xC3(3kj>=RIW;8$o^Ryj~7E;k+J2zF&@RjzyK1p{(kW{w=N(MfA$Hc22r@ZF9jpl7VIT^E3(L--|nA|kBY?&eU~j3gBjycoIw&I_2r!}akvniXpV^}F>! z7^`Dqne9(T-e-;U*b>TR!GXx2+<%I38G6>*oMgEZ5c1B>&f5DiTpLR*&L6zht^2s_ z{Xo$>M{?5?5cM#kfMnt+COI(^naVI5;sh2kzft3MX(F>Ux?M=IANoB(exi`u`oJ|Jk0(e?XA`OvmN_{@?#}J?Q_;(up7OsQd>D z_}?us{hv9)|9e#bTdlJH`=86Qq!V_qTAOQ(`lyVU9&c~5ex*%)KQ92)`ED#6qp@jZF)=^Ix4ae z^DCRW!s1w)_qqPRb7L;;^STF4)RDa~J6poUK@}s2C0eY^lr`71QZch%k(1|=Tqq+v z^{)Sr6xbAvRmiuSW5z($TU%NVki$f7mNNUR>dVT0`{pUZ!y}FlFHStoRE{yBn@j6q zUcHrou8}OC(6>Fzv_b<)0ZM!_3H>lT#Nm-fjcH@@_j(MPVVgANprJE!%pg$BPDXE0 zON--rn=*$2K0FN>*}5gGjJz_3&J~*qD-QXq6{@<*eA=R3WR!xET< zSFEJ?)3z)&36$Xp)T5Vuz^+c2sJEa%yqi-yx`mhH)rQDUtzI9xDbBYVtiA_|5t zoL|rfPB6*Y%>2e3Ye0nQWp%_lwsz|g5nqx9!kxt9l$do9N^{HY=pZ#hXhEcUXJc`? zE7aWjg!87i`Rb;A14m6O1y{JRqT(=flOP>7I}ol}vm7p)t^%b>Qm02Rgjxk<{pN`U z=ECQ9&SDe6BeNYOV%rVvmVa7 zV$R;Kp1N7q;>#$5Azp9HtcMN>ev_e6XDg41K5%fI2@fTSY!c2`f_KNNdqD~A6$Fuc z>i%RNG+e+ue)f&Ee%hE#IKz7Sd$ zT_b6R@XIeGF@hqNk|+pL#NR&{2@-n_zrdS&K-Blx*MUGj zw$n<&)=Ls$@5PgF#^I1bVNNq?NLR4<6>uqbkGw5iMswtRW!bHS!VChXO?f>$`edMF zhbC;9g<^yMogrsnp=9~Gs@c66w_OLLwGP~R3BXbdAh^lr0fefisHmuj$Y;B9`7Caf z$`GLL5UOP0GA)s+|7|#eD#yA}7l@fg5Rm{ID=jCtT5pBKhYRCFwL;9=9fcELNzS6o zmI;4v-9Aoj0}53^jO(M52~8?W8i|ChUWD@|tq$OIKabu;5wu!saR!HiHy%F#if(p* zqvU(d0-_PXY}j#s0HTv0@Ww!n@d)GzhQKdF0B(@xvJ(V)xbtF20&};lKt=7($cU*F z5|jorE=A#fC0d$_3CQRk&)&KslfAGfI#I$kV;flBe*!LAtK#rubbfMOGT?*CKCxyLiTH*kFBwzZ8dmylcJI^iYc zNTrE!O7bYVb0&@=w(2OJ$!W%1BXlbFZ5?vyv^bBRESH&OQnAN&Ogxxt9QVguN}TVW zf1baefA`1t_4>VDyL~?I_vg!KZq_`?PF&3X4*WS5pZ{h8NL$F34l#Rkaricv=7PR} z{^ZMX!k$nh0{2FgMF%v3FU7)8p{3TDrhv-TsA)1W05<#hJRK?U1=|2Hc$_r{BBf1u+en;ad2Gf*`w0aqFv)o3*t$JyFsnV36K! zabIm?8F1BfS>DCOaxw3Ed%0ZT%~zys4C&|3QV`Z1$%g$hR^q37l$3GU)D$-OUm{On=1KAWNSO*N1<2b3es{ZkA0HBGLt`y+D8QdNriS@VcsUi{r zY-di}Smk#@R6z%XY8nHKapg9+L%^zR6b!6u&XRBl7WrlxO#A>E!*IAJn&hXl>P-2d6*7<4{1^W*8Mb_7a98X;8 z3KX?Se(cJ`0N+(qgh)iI9L`pSQQxP#<)A4GkRqKnbhkb}`E9OtC3s+9qxZ)gp=|f7 z{A+xNE62s)F3}NGolApQn9PkK_wNxJ@pu>b}?a-|wqTdrV#I zu7SP45uHf6ZU!#oB)^qdTZtioKEhGquqXYh8oQJRgH31q?^!anO$5_a`94N-jx@RPCg&~n(LXSrJtP1N7B}} zZIn-!XXd8>Jop)!o&ecD#669!_r-QJgF?uWJnfm})Ut(;t`gTj3%ujtjEJpT13kTe z6SWmQiyi1RKH?6(Y)Sly_>!n%IM|e?rqA?7}HiN-U6b;J$ z6DStEcuqAgknG&LRJ&+wDP`VS?;l}5F3AY}L6Xn{MH_yJOnrb)s?{>*Q<2+ChZ^@>#h diff --git a/e2e/visual.spec.ts-snapshots/default-homepage-1-Mobile-Chrome-linux.png b/e2e/visual.spec.ts-snapshots/default-homepage-1-Mobile-Chrome-linux.png index c9bba9f676f530e9b0f20dd0cfed53eb442cac18..11a23dc4297f9f793c7d6e83cf34a41291a1d32b 100644 GIT binary patch literal 19019 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

t#>aqm+`=Ib8Wpza62y<4H4;a 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

{#3RlwnEyu5!PgHTh0rkDs~Opb^ZRV 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 0 { - log.Debug().Int("count", len(orphanBuffer)).Str("container", g.containerID).Msg("skipped orphaned continuation lines") + // If the chain broke because `current` is far in time from the + // last buffered line, the buffered lines weren't continuations + // of anything — they're real isolated entries. Emit them as + // singles so first-of-window lines aren't silently dropped + // (e.g. postgres "checkpoint starting: time" — only entry in + // a 5-min window followed by a 0.4s-later "complete" line). + timeGap := lastTimestamp != 0 && current.Timestamp > 0 && + math.Abs(float64(lastTimestamp-current.Timestamp)) >= maxGroupTimeDelta + if timeGap { + g.emitAsSingles(orphanBuffer) + } else { + log.Debug().Int("count", len(orphanBuffer)).Str("container", g.containerID).Msg("skipped orphaned continuation lines") + } } return current } diff --git a/internal/container/event_generator_test.go b/internal/container/event_generator_test.go index 1a247f70..a4c5f799 100644 --- a/internal/container/event_generator_test.go +++ b/internal/container/event_generator_test.go @@ -360,8 +360,11 @@ func TestEventGenerator_OrphanNotSkipped_AllLevellessLines(t *testing.T) { } func TestEventGenerator_OrphanNotSkipped_TimestampGapBreaksOrphanDetection(t *testing.T) { - // Lines far apart in time — first is buffered as orphan candidate, but the - // gap breaks the chain so it's treated as non-orphan. + // Lines far apart in time — first is buffered as orphan candidate but the + // gap breaks the chain. Both must be emitted: a single isolated levelless + // line is not an orphan continuation, and dropping it loses real user + // content (e.g. postgres "checkpoint starting: time" is the first event + // of every 5-min historical window). containerStart := time.Date(2020, 5, 13, 10, 0, 0, 0, time.UTC) messages := []string{ "2020-05-13T18:55:37.000Z some log without level", @@ -375,12 +378,15 @@ func TestEventGenerator_OrphanNotSkipped_TimestampGapBreaksOrphanDetection(t *te g := NewEventGenerator(context.Background(), reader, Container{Tty: false, StartedAt: containerStart}) - // First line is buffered as orphan candidate, but the second has a timestamp - // gap so it's not an orphan — first is skipped, second is emitted. event1 := <-g.Events require.NotNil(t, event1) assert.Equal(t, LogTypeSingle, event1.Type) - assert.Equal(t, "another log without level", event1.Message) + assert.Equal(t, "some log without level", event1.Message) + + event2 := <-g.Events + require.NotNil(t, event2) + assert.Equal(t, LogTypeSingle, event2.Type) + assert.Equal(t, "another log without level", event2.Message) } func TestEventGenerator_OrphanNotSkipped_NoTimestamp(t *testing.T) { diff --git a/internal/web/cloud.go b/internal/web/cloud.go index 264e1015..750c36ea 100644 --- a/internal/web/cloud.go +++ b/internal/web/cloud.go @@ -96,11 +96,11 @@ func (h *handler) cloudCallback(w http.ResponseWriter, r *http.Request) { // Drop any existing connection so a relink with a new key takes effect // immediately instead of riding out the old stream. - if h.config.OnCloudUpdate != nil { - h.config.OnCloudUpdate() + if h.config.Cloud.OnUpdate != nil { + h.config.Cloud.OnUpdate() } - if h.config.OnCloudSetup != nil { - h.config.OnCloudSetup() + if h.config.Cloud.OnSetup != nil { + h.config.Cloud.OnSetup() } base := h.config.Base @@ -174,8 +174,8 @@ func (h *handler) cloudStatus(w http.ResponseWriter, r *http.Request) { } `json:"plan"` } if json.Unmarshal(body, &statusResp) == nil && statusResp.Plan.Name == "pro" { - if h.config.OnCloudSetup != nil { - h.config.OnCloudSetup() + if h.config.Cloud.OnSetup != nil { + h.config.Cloud.OnSetup() } } @@ -234,8 +234,8 @@ func (h *handler) updateCloudConfig(w http.ResponseWriter, r *http.Request) { h.hostService.SetCloudStreamLogs(*req.StreamLogs) // Drop the active cloud connection so the new flag is picked up on // the next dial — a streamer may need to start or stop. - if h.config.OnCloudUpdate != nil { - h.config.OnCloudUpdate() + if h.config.Cloud.OnUpdate != nil { + h.config.Cloud.OnUpdate() } } @@ -247,8 +247,8 @@ func (h *handler) deleteCloudConfig(w http.ResponseWriter, r *http.Request) { // Drop the active cloud connection so the server stops seeing this // instance immediately, instead of riding out the existing stream with // a now-deleted key. - if h.config.OnCloudUpdate != nil { - h.config.OnCloudUpdate() + if h.config.Cloud.OnUpdate != nil { + h.config.Cloud.OnUpdate() } w.WriteHeader(http.StatusNoContent) } diff --git a/internal/web/cloud_search.go b/internal/web/cloud_search.go new file mode 100644 index 00000000..35725edc --- /dev/null +++ b/internal/web/cloud_search.go @@ -0,0 +1,99 @@ +package web + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "strconv" + "time" + + "github.com/amir20/dozzle/internal/cloud" + "github.com/rs/zerolog/log" +) + +// cloudSearchTimeout caps the round-trip to Doligence Cloud. Search is on +// the keystroke path; we'd rather show "no results" than block typing. +const cloudSearchTimeout = 500 * time.Millisecond + +// cloudSearchLogs proxies a search query to Doligence Cloud over the existing +// authenticated gRPC connection. Identity is derived server-side from the +// API key — this handler passes neither user nor instance ids. +// +// Status mapping: +// 200 — hits returned (may be empty) +// 204 — streamLogs is disabled; nothing to search +// 503 — cloud not configured (no API key) or no SearchLogs func wired +// 504 — cloud round-trip exceeded the search timeout +// 502 — any other cloud-side error +func (h *handler) cloudSearchLogs(w http.ResponseWriter, r *http.Request) { + if h.config.Cloud.SearchLogs == nil { + writeError(w, http.StatusServiceUnavailable, "cloud not configured") + return + } + + cc := h.hostService.CloudConfig() + if cc == nil || !cc.StreamLogsEnabled() { + // Defense in depth — the UI already gates on streamLogs, but a stale + // flag client-side mustn't trigger spurious cloud queries. + w.WriteHeader(http.StatusNoContent) + return + } + + q := r.URL.Query().Get("q") + if q == "" { + writeError(w, http.StatusBadRequest, "missing q") + return + } + // Defense in depth: the UI input is short (debounced typing) but a + // malicious client could POST any size. Reject anything past 512 + // chars rather than fan it out to Cloud's gRPC backend. + if len(q) > 512 { + writeError(w, http.StatusBadRequest, "q too long") + return + } + // Cloud caps server-side at 50; mirror it here so a misbehaving client + // can't tie up the keystroke path with an oversized request. ParseInt + // with bitSize=32 guarantees the value fits in int32, so the cast is + // provably safe (out-of-range parses return an error and fall through). + limit := int32(20) + if v := r.URL.Query().Get("limit"); v != "" { + if n, err := strconv.ParseInt(v, 10, 32); err == nil && n > 0 { + if n > 50 { + n = 50 + } + limit = int32(n) + } + } + hostID := r.URL.Query().Get("hostId") + containerID := r.URL.Query().Get("containerId") + // Pagination cursor — pass-through to Cloud. 0 (the default) means + // "newest"; subsequent pages send back the prior response's nextBefore. + var before int64 + if v := r.URL.Query().Get("before"); v != "" { + if n, err := strconv.ParseInt(v, 10, 64); err == nil && n > 0 { + before = n + } + } + + ctx, cancel := context.WithTimeout(r.Context(), cloudSearchTimeout) + defer cancel() + + result, err := h.config.Cloud.SearchLogs(ctx, q, limit, hostID, containerID, before) + if err != nil { + if errors.Is(err, cloud.ErrNotConfigured) { + writeError(w, http.StatusServiceUnavailable, "cloud not configured") + return + } + if errors.Is(err, context.DeadlineExceeded) { + writeError(w, http.StatusGatewayTimeout, "cloud search timed out") + return + } + log.Warn().Err(err).Msg("cloud search failed") + writeError(w, http.StatusBadGateway, "cloud search failed") + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(result) +} diff --git a/internal/web/routes.go b/internal/web/routes.go index 0ae0ec32..6df21173 100644 --- a/internal/web/routes.go +++ b/internal/web/routes.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/amir20/dozzle/internal/auth" + "github.com/amir20/dozzle/internal/cloud" "github.com/amir20/dozzle/internal/container" "github.com/amir20/dozzle/internal/notification" "github.com/amir20/dozzle/internal/notification/dispatcher" @@ -50,8 +51,23 @@ type Config struct { DisableAvatars bool ReleaseCheckMode ReleaseCheckMode Labels container.ContainerLabels - OnCloudSetup func() - OnCloudUpdate func() + Cloud CloudHooks +} + +// CloudHooks bundles cloud-side callbacks the web layer invokes. Grouping +// them keeps Config and createServer signatures stable as more cloud RPCs +// land. Nil-valued fields mean "feature unavailable" — handlers should +// degrade gracefully (e.g. SearchLogs nil → 503). +type CloudHooks struct { + // OnSetup signals that cloud configuration has been (re)written and + // the client should reconnect / re-authenticate. + OnSetup func() + // OnUpdate is fired when cloud-affecting settings change (e.g. + // streamLogs toggle) so the existing connection can pick them up. + OnUpdate func() + // SearchLogs proxies a substring/word-filter query to Doligence Cloud + // over the authenticated gRPC connection. Nil when cloud is not wired. + SearchLogs func(ctx context.Context, query string, limit int32, hostID, containerID string, before int64) (*cloud.SearchLogResult, error) } type Authorization struct { @@ -192,6 +208,7 @@ func createRouter(h *handler) *chi.Mux { // Cloud API r.Get("/cloud/status", h.cloudStatus) + r.Get("/cloud/search/logs", h.cloudSearchLogs) r.Get("/cloud/config", h.cloudConfig) r.Patch("/cloud/config", h.updateCloudConfig) r.Delete("/cloud/config", h.deleteCloudConfig) diff --git a/locales/da.yml b/locales/da.yml index 32cdb77f..23ee36f1 100644 --- a/locales/da.yml +++ b/locales/da.yml @@ -339,3 +339,31 @@ cloud: create-alert: Opret din første advarsel default-alert-name: Container exited with error later: "Det gør jeg senere" +cloud-search: + containers-section: "Containere" + search-logs-for: "Søg logs efter \"{query}\"" + across-containers: "indekseret på tværs af alle dine containere" + connect-to-enable: "Tilslut Dozzle Cloud for at søge i logs" + enable-streaming-to-search: "Aktiver log-streaming til Cloud" + open-container: "åbn container" + search-logs-shortcut: "søg logs" + cloud-connected: "Cloud tilsluttet" + results-page-title: "Logsøgning" + no-results: "Ingen matchende loglinjer." + search-failed: "Cloud-søgning mislykkedes." + search-empty-prompt: "Skriv en forespørgsel for at søge i logs." + searching: "Søger…" + hits-count: "{n} resultater" + window-suffix: "i de seneste 14 dage" + hero-title-cloud: "Søg containere og logs" + hero-title-plain: "Søg containere" + hero-pill-indexed: "Cloud-indeks" + modal-placeholder-cloud: "Søg containere og logs…" + modal-placeholder-plain: "Søg containere…" + col-time: "Tid" + col-level: "Niveau" + col-container: "Container" + col-message: "Besked" + container-removed: "Containeren er slettet" + cta-settings: "Cloud-indstillinger" + container-removed-pill: "slettet" diff --git a/locales/de.yml b/locales/de.yml index 1b2dde4b..8a15e0fa 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -339,3 +339,31 @@ cloud: create-alert: Erste Benachrichtigung erstellen default-alert-name: Container exited with error later: "Das mache ich später" +cloud-search: + containers-section: "Container" + search-logs-for: "Logs durchsuchen nach \"{query}\"" + across-containers: "indexiert über alle deine Container" + connect-to-enable: "Dozzle Cloud verbinden, um Logs zu durchsuchen" + enable-streaming-to-search: "Log-Streaming zur Cloud aktivieren" + open-container: "Container öffnen" + search-logs-shortcut: "Logs durchsuchen" + cloud-connected: "Cloud verbunden" + results-page-title: "Log-Suche" + no-results: "Keine passenden Logzeilen." + search-failed: "Cloud-Suche fehlgeschlagen." + search-empty-prompt: "Suchanfrage eingeben, um Logs zu durchsuchen." + searching: "Suche läuft…" + hits-count: "{n} Treffer" + window-suffix: "in den letzten 14 Tagen" + hero-title-cloud: "Container und Logs durchsuchen" + hero-title-plain: "Container durchsuchen" + hero-pill-indexed: "Cloud-Index" + modal-placeholder-cloud: "Container und Logs durchsuchen…" + modal-placeholder-plain: "Container durchsuchen…" + col-time: "Zeit" + col-level: "Level" + col-container: "Container" + col-message: "Nachricht" + container-removed: "Container wurde gelöscht" + cta-settings: "Cloud-Einstellungen" + container-removed-pill: "gelöscht" diff --git a/locales/en.yml b/locales/en.yml index 5d5ff737..bc603f11 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -113,6 +113,34 @@ button: placeholder: search-containers: Search containers (⌘ + k, ⌃k) search: Search +cloud-search: + containers-section: Containers + search-logs-for: Search logs for "{query}" + across-containers: indexed across all your containers + connect-to-enable: Connect Dozzle Cloud to search logs + enable-streaming-to-search: Enable Stream Logs to Cloud to search + open-container: open container + search-logs-shortcut: search logs + cloud-connected: Cloud connected + results-page-title: Logs search + no-results: No matching log lines. + search-failed: Cloud search failed. + search-empty-prompt: Type a query to search logs across your containers. + searching: Searching… + hits-count: "{n} hits" + window-suffix: in the last 14 days + hero-title-cloud: Search containers and logs + hero-title-plain: Search containers + hero-pill-indexed: Cloud index + modal-placeholder-cloud: Search containers and logs… + modal-placeholder-plain: Search containers… + col-time: Time + col-level: Level + col-container: Container + col-message: Message + container-removed: Container has been deleted + cta-settings: "Cloud settings" + container-removed-pill: removed settings: help-support: > Please support Dozzle by donating or sponsoring us on GitHub. Your contributions help us improve Dozzle for diff --git a/locales/es.yml b/locales/es.yml index 96d73e28..9444898a 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -106,6 +106,34 @@ button: placeholder: search-containers: Buscar contenedores (⌘ + K, CTRL + K) search: Buscar +cloud-search: + containers-section: "Contenedores" + search-logs-for: "Buscar logs por \"{query}\"" + across-containers: "indexados en todos tus contenedores" + connect-to-enable: "Conecta Dozzle Cloud para buscar logs" + enable-streaming-to-search: "Activa el streaming de logs a Cloud" + open-container: "abrir contenedor" + search-logs-shortcut: "buscar logs" + cloud-connected: "Cloud conectado" + results-page-title: "Búsqueda de logs" + no-results: "No hay líneas de log coincidentes." + search-failed: "La búsqueda en Cloud falló." + search-empty-prompt: "Escribe una consulta para buscar en los logs." + searching: "Buscando…" + hits-count: "{n} resultados" + window-suffix: "en los últimos 14 días" + hero-title-cloud: "Buscar contenedores y logs" + hero-title-plain: "Buscar contenedores" + hero-pill-indexed: "Índice Cloud" + modal-placeholder-cloud: "Buscar contenedores y logs…" + modal-placeholder-plain: "Buscar contenedores…" + col-time: "Hora" + col-level: "Nivel" + col-container: "Contenedor" + col-message: "Mensaje" + container-removed: "El contenedor ha sido eliminado" + cta-settings: "Configuración de Cloud" + container-removed-pill: "eliminado" settings: help-support: > Por favor, apoya a Dozzle donando o patrocinándonos en GitHub. Tus contribuciones nos ayudan a mejorar Dozzle para todos. ¡Gracias! 🙏🏼 diff --git a/locales/fr.yml b/locales/fr.yml index 5e9bc1a7..959e1a32 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -339,3 +339,31 @@ cloud: create-alert: Créer votre première alerte default-alert-name: Container exited with error later: "Je ferai ça plus tard" +cloud-search: + containers-section: "Conteneurs" + search-logs-for: "Rechercher des logs pour \"{query}\"" + across-containers: "indexés sur tous vos conteneurs" + connect-to-enable: "Connecter Dozzle Cloud pour rechercher des logs" + enable-streaming-to-search: "Activer le streaming des logs vers le Cloud" + open-container: "ouvrir le conteneur" + search-logs-shortcut: "rechercher des logs" + cloud-connected: "Cloud connecté" + results-page-title: "Recherche de logs" + no-results: "Aucune ligne de log ne correspond." + search-failed: "La recherche Cloud a échoué." + search-empty-prompt: "Tapez une requête pour rechercher dans les logs." + searching: "Recherche…" + hits-count: "{n} résultats" + window-suffix: "au cours des 14 derniers jours" + hero-title-cloud: "Rechercher conteneurs et logs" + hero-title-plain: "Rechercher des conteneurs" + hero-pill-indexed: "Index Cloud" + modal-placeholder-cloud: "Rechercher conteneurs et logs…" + modal-placeholder-plain: "Rechercher des conteneurs…" + col-time: "Heure" + col-level: "Niveau" + col-container: "Conteneur" + col-message: "Message" + container-removed: "Le conteneur a été supprimé" + cta-settings: "Paramètres Cloud" + container-removed-pill: "supprimé" diff --git a/locales/id.yml b/locales/id.yml index ac93ea97..39835f42 100644 --- a/locales/id.yml +++ b/locales/id.yml @@ -351,3 +351,31 @@ cloud: create-alert: Buat peringatan pertama Anda default-alert-name: Container exited with error later: "Nanti saja" +cloud-search: + containers-section: "Kontainer" + search-logs-for: "Cari log untuk \"{query}\"" + across-containers: "diindeks di semua kontainer Anda" + connect-to-enable: "Hubungkan Dozzle Cloud untuk mencari log" + enable-streaming-to-search: "Aktifkan streaming log ke Cloud" + open-container: "buka kontainer" + search-logs-shortcut: "cari log" + cloud-connected: "Cloud terhubung" + results-page-title: "Pencarian log" + no-results: "Tidak ada baris log yang cocok." + search-failed: "Pencarian Cloud gagal." + search-empty-prompt: "Ketik kueri untuk mencari di log." + searching: "Mencari…" + hits-count: "{n} hasil" + window-suffix: "dalam 14 hari terakhir" + hero-title-cloud: "Cari kontainer dan log" + hero-title-plain: "Cari kontainer" + hero-pill-indexed: "Indeks Cloud" + modal-placeholder-cloud: "Cari kontainer dan log…" + modal-placeholder-plain: "Cari kontainer…" + col-time: "Waktu" + col-level: "Level" + col-container: "Kontainer" + col-message: "Pesan" + container-removed: "Kontainer telah dihapus" + cta-settings: "Pengaturan Cloud" + container-removed-pill: "dihapus" diff --git a/locales/it.yml b/locales/it.yml index be9506c4..a3dbeb6f 100644 --- a/locales/it.yml +++ b/locales/it.yml @@ -339,3 +339,31 @@ cloud: create-alert: Crea il tuo primo avviso default-alert-name: Container exited with error later: "Lo farò dopo" +cloud-search: + containers-section: "Container" + search-logs-for: "Cerca log per \"{query}\"" + across-containers: "indicizzati su tutti i tuoi container" + connect-to-enable: "Connetti Dozzle Cloud per cercare nei log" + enable-streaming-to-search: "Abilita lo streaming dei log su Cloud" + open-container: "apri container" + search-logs-shortcut: "cerca log" + cloud-connected: "Cloud connesso" + results-page-title: "Ricerca log" + no-results: "Nessuna riga di log corrispondente." + search-failed: "Ricerca Cloud fallita." + search-empty-prompt: "Digita una query per cercare nei log." + searching: "Ricerca…" + hits-count: "{n} risultati" + window-suffix: "negli ultimi 14 giorni" + hero-title-cloud: "Cerca container e log" + hero-title-plain: "Cerca container" + hero-pill-indexed: "Indice Cloud" + modal-placeholder-cloud: "Cerca container e log…" + modal-placeholder-plain: "Cerca container…" + col-time: "Ora" + col-level: "Livello" + col-container: "Container" + col-message: "Messaggio" + container-removed: "Il container è stato eliminato" + cta-settings: "Impostazioni Cloud" + container-removed-pill: "eliminato" diff --git a/locales/ko.yml b/locales/ko.yml index 59206abd..c17d47be 100644 --- a/locales/ko.yml +++ b/locales/ko.yml @@ -342,3 +342,31 @@ cloud: create-alert: 첫 번째 알림 만들기 default-alert-name: Container exited with error later: "나중에 할게요" +cloud-search: + containers-section: "컨테이너" + search-logs-for: "\"{query}\" 로그 검색" + across-containers: "모든 컨테이너에서 색인됨" + connect-to-enable: "로그를 검색하려면 Dozzle Cloud에 연결하세요" + enable-streaming-to-search: "Cloud로 로그 스트리밍 활성화" + open-container: "컨테이너 열기" + search-logs-shortcut: "로그 검색" + cloud-connected: "Cloud 연결됨" + results-page-title: "로그 검색" + no-results: "일치하는 로그 라인이 없습니다." + search-failed: "Cloud 검색 실패." + search-empty-prompt: "로그를 검색하려면 쿼리를 입력하세요." + searching: "검색 중…" + hits-count: "{n}개 결과" + window-suffix: "최근 14일 동안" + hero-title-cloud: "컨테이너 및 로그 검색" + hero-title-plain: "컨테이너 검색" + hero-pill-indexed: "Cloud 인덱스" + modal-placeholder-cloud: "컨테이너 및 로그 검색…" + modal-placeholder-plain: "컨테이너 검색…" + col-time: "시간" + col-level: "레벨" + col-container: "컨테이너" + col-message: "메시지" + container-removed: "컨테이너가 삭제되었습니다" + cta-settings: "Cloud 설정" + container-removed-pill: "삭제됨" diff --git a/locales/nl.yml b/locales/nl.yml index c17a2641..6904aeeb 100644 --- a/locales/nl.yml +++ b/locales/nl.yml @@ -340,3 +340,31 @@ cloud: create-alert: Maak je eerste melding default-alert-name: Container exited with error later: "Dat doe ik later" +cloud-search: + containers-section: "Containers" + search-logs-for: "Logs zoeken op \"{query}\"" + across-containers: "geïndexeerd over al je containers" + connect-to-enable: "Dozzle Cloud verbinden om logs te zoeken" + enable-streaming-to-search: "Schakel logstreaming naar Cloud in" + open-container: "container openen" + search-logs-shortcut: "logs zoeken" + cloud-connected: "Cloud verbonden" + results-page-title: "Logs doorzoeken" + no-results: "Geen overeenkomende logregels." + search-failed: "Cloud-zoekopdracht mislukt." + search-empty-prompt: "Typ een zoekopdracht om logs te doorzoeken." + searching: "Zoeken…" + hits-count: "{n} treffers" + window-suffix: "in de afgelopen 14 dagen" + hero-title-cloud: "Containers en logs zoeken" + hero-title-plain: "Containers zoeken" + hero-pill-indexed: "Cloud-index" + modal-placeholder-cloud: "Containers en logs zoeken…" + modal-placeholder-plain: "Containers zoeken…" + col-time: "Tijd" + col-level: "Niveau" + col-container: "Container" + col-message: "Bericht" + container-removed: "Container is verwijderd" + cta-settings: "Cloud-instellingen" + container-removed-pill: "verwijderd" diff --git a/locales/pl.yml b/locales/pl.yml index 9a8e7bee..58d0ba96 100644 --- a/locales/pl.yml +++ b/locales/pl.yml @@ -346,3 +346,31 @@ cloud: create-alert: Utwórz pierwszy alert default-alert-name: Container exited with error later: "Zrobię to później" +cloud-search: + containers-section: "Kontenery" + search-logs-for: "Przeszukaj logi pod kątem \"{query}\"" + across-containers: "zindeksowane we wszystkich Twoich kontenerach" + connect-to-enable: "Połącz Dozzle Cloud, aby przeszukiwać logi" + enable-streaming-to-search: "Włącz przesyłanie logów do Cloud" + open-container: "otwórz kontener" + search-logs-shortcut: "przeszukaj logi" + cloud-connected: "Cloud połączony" + results-page-title: "Wyszukiwanie logów" + no-results: "Brak pasujących linii logu." + search-failed: "Wyszukiwanie Cloud nie powiodło się." + search-empty-prompt: "Wpisz zapytanie, aby przeszukać logi." + searching: "Wyszukiwanie…" + hits-count: "{n} wyników" + window-suffix: "w ciągu ostatnich 14 dni" + hero-title-cloud: "Szukaj kontenerów i logów" + hero-title-plain: "Szukaj kontenerów" + hero-pill-indexed: "Indeks Cloud" + modal-placeholder-cloud: "Szukaj kontenerów i logów…" + modal-placeholder-plain: "Szukaj kontenerów…" + col-time: "Czas" + col-level: "Poziom" + col-container: "Kontener" + col-message: "Wiadomość" + container-removed: "Kontener został usunięty" + cta-settings: "Ustawienia Cloud" + container-removed-pill: "usunięty" diff --git a/locales/pr.yml b/locales/pr.yml index 94a195f4..d214e6cc 100644 --- a/locales/pr.yml +++ b/locales/pr.yml @@ -348,3 +348,31 @@ cloud: create-alert: Create Yer First Alert default-alert-name: Container exited with error later: "I'll do this later, arrr" +cloud-search: + containers-section: "Contentores" + search-logs-for: "Pesquisar logs por \"{query}\"" + across-containers: "indexados em todos os seus contentores" + connect-to-enable: "Ligar o Dozzle Cloud para pesquisar logs" + enable-streaming-to-search: "Ativar streaming de logs para a Cloud" + open-container: "abrir contentor" + search-logs-shortcut: "pesquisar logs" + cloud-connected: "Cloud ligada" + results-page-title: "Pesquisa de logs" + no-results: "Nenhuma linha de log corresponde." + search-failed: "Falha na pesquisa Cloud." + search-empty-prompt: "Escreva uma consulta para pesquisar nos logs." + searching: "A pesquisar…" + hits-count: "{n} resultados" + window-suffix: "nos últimos 14 dias" + hero-title-cloud: "Pesquisar contentores e logs" + hero-title-plain: "Pesquisar contentores" + hero-pill-indexed: "Índice Cloud" + modal-placeholder-cloud: "Pesquisar contentores e logs…" + modal-placeholder-plain: "Pesquisar contentores…" + col-time: "Hora" + col-level: "Nível" + col-container: "Contentor" + col-message: "Mensagem" + container-removed: "O contentor foi eliminado" + cta-settings: "Definições da Cloud" + container-removed-pill: "removido" diff --git a/locales/pt.yml b/locales/pt.yml index 2d82f7c4..925a9845 100644 --- a/locales/pt.yml +++ b/locales/pt.yml @@ -338,3 +338,31 @@ cloud: create-alert: Crie seu primeiro alerta default-alert-name: Container exited with error later: "Farei isso depois" +cloud-search: + containers-section: "Containers" + search-logs-for: "Buscar logs por \"{query}\"" + across-containers: "indexados em todos os seus containers" + connect-to-enable: "Conectar Dozzle Cloud para buscar logs" + enable-streaming-to-search: "Ativar streaming de logs para a Cloud" + open-container: "abrir container" + search-logs-shortcut: "buscar logs" + cloud-connected: "Cloud conectado" + results-page-title: "Busca de logs" + no-results: "Nenhuma linha de log correspondente." + search-failed: "Falha na busca da Cloud." + search-empty-prompt: "Digite uma consulta para buscar nos logs." + searching: "Buscando…" + hits-count: "{n} resultados" + window-suffix: "nos últimos 14 dias" + hero-title-cloud: "Buscar containers e logs" + hero-title-plain: "Buscar containers" + hero-pill-indexed: "Índice Cloud" + modal-placeholder-cloud: "Buscar containers e logs…" + modal-placeholder-plain: "Buscar containers…" + col-time: "Hora" + col-level: "Nível" + col-container: "Container" + col-message: "Mensagem" + container-removed: "O container foi excluído" + cta-settings: "Configurações da Cloud" + container-removed-pill: "removido" diff --git a/locales/ru.yml b/locales/ru.yml index 1b7281f6..c1ed9d1c 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -339,3 +339,31 @@ cloud: create-alert: Создайте первое оповещение default-alert-name: Container exited with error later: "Сделаю это позже" +cloud-search: + containers-section: "Контейнеры" + search-logs-for: "Поиск логов по \"{query}\"" + across-containers: "проиндексированных по всем вашим контейнерам" + connect-to-enable: "Подключите Dozzle Cloud, чтобы искать в логах" + enable-streaming-to-search: "Включите стриминг логов в Cloud" + open-container: "открыть контейнер" + search-logs-shortcut: "поиск логов" + cloud-connected: "Cloud подключён" + results-page-title: "Поиск по логам" + no-results: "Совпадений в логах не найдено." + search-failed: "Поиск в Cloud не удался." + search-empty-prompt: "Введите запрос для поиска по логам." + searching: "Поиск…" + hits-count: "{n} результатов" + window-suffix: "за последние 14 дней" + hero-title-cloud: "Поиск по контейнерам и логам" + hero-title-plain: "Поиск по контейнерам" + hero-pill-indexed: "Индекс Cloud" + modal-placeholder-cloud: "Поиск по контейнерам и логам…" + modal-placeholder-plain: "Поиск по контейнерам…" + col-time: "Время" + col-level: "Уровень" + col-container: "Контейнер" + col-message: "Сообщение" + container-removed: "Контейнер был удалён" + cta-settings: "Настройки Cloud" + container-removed-pill: "удалён" diff --git a/locales/sl.yml b/locales/sl.yml index 7d5cf4bc..c8f92c17 100644 --- a/locales/sl.yml +++ b/locales/sl.yml @@ -344,3 +344,31 @@ cloud: create-alert: Ustvarite prvo opozorilo default-alert-name: Container exited with error later: "To bom naredil pozneje" +cloud-search: + containers-section: "Vsebniki" + search-logs-for: "Iskanje dnevnikov za \"{query}\"" + across-containers: "indeksirano po vseh vaših vsebnikih" + connect-to-enable: "Povežite Dozzle Cloud za iskanje po dnevnikih" + enable-streaming-to-search: "Omogoči pretakanje dnevnikov v Cloud" + open-container: "odpri vsebnik" + search-logs-shortcut: "išči po dnevnikih" + cloud-connected: "Cloud povezan" + results-page-title: "Iskanje dnevnikov" + no-results: "Ni ujemajočih se vrstic dnevnika." + search-failed: "Iskanje v Cloudu ni uspelo." + search-empty-prompt: "Vpišite poizvedbo za iskanje po dnevnikih." + searching: "Iskanje…" + hits-count: "{n} zadetkov" + window-suffix: "v zadnjih 14 dneh" + hero-title-cloud: "Iskanje vsebnikov in dnevnikov" + hero-title-plain: "Iskanje vsebnikov" + hero-pill-indexed: "Cloud indeks" + modal-placeholder-cloud: "Iskanje vsebnikov in dnevnikov…" + modal-placeholder-plain: "Iskanje vsebnikov…" + col-time: "Čas" + col-level: "Raven" + col-container: "Vsebnik" + col-message: "Sporočilo" + container-removed: "Vsebnik je bil izbrisan" + cta-settings: "Nastavitve Cloud" + container-removed-pill: "izbrisano" diff --git a/locales/tr.yml b/locales/tr.yml index 8919de3e..3ffd8dd6 100644 --- a/locales/tr.yml +++ b/locales/tr.yml @@ -339,3 +339,31 @@ cloud: create-alert: İlk uyarınızı oluşturun default-alert-name: Container exited with error later: "Bunu daha sonra yapacağım" +cloud-search: + containers-section: "Konteynerler" + search-logs-for: "\"{query}\" için logları ara" + across-containers: "tüm konteynerleriniz arasında indekslenmiş" + connect-to-enable: "Logları aramak için Dozzle Cloud'a bağlanın" + enable-streaming-to-search: "Log akışını Cloud'a etkinleştirin" + open-container: "konteyneri aç" + search-logs-shortcut: "logları ara" + cloud-connected: "Cloud bağlı" + results-page-title: "Log araması" + no-results: "Eşleşen log satırı yok." + search-failed: "Cloud araması başarısız oldu." + search-empty-prompt: "Loglarda arama yapmak için bir sorgu yazın." + searching: "Aranıyor…" + hits-count: "{n} sonuç" + window-suffix: "son 14 gün içinde" + hero-title-cloud: "Konteynerleri ve logları ara" + hero-title-plain: "Konteynerleri ara" + hero-pill-indexed: "Cloud indeksi" + modal-placeholder-cloud: "Konteynerleri ve logları ara…" + modal-placeholder-plain: "Konteynerleri ara…" + col-time: "Zaman" + col-level: "Seviye" + col-container: "Konteyner" + col-message: "Mesaj" + container-removed: "Konteyner silindi" + cta-settings: "Cloud ayarları" + container-removed-pill: "silindi" diff --git a/locales/zh-tw.yml b/locales/zh-tw.yml index 77bb5c55..e678dbbf 100644 --- a/locales/zh-tw.yml +++ b/locales/zh-tw.yml @@ -342,3 +342,31 @@ cloud: create-alert: 建立您的第一個警示 default-alert-name: Container exited with error later: "稍後再說" +cloud-search: + containers-section: "容器" + search-logs-for: "搜尋日誌 \"{query}\"" + across-containers: "已在所有容器中建立索引" + connect-to-enable: "連接 Dozzle Cloud 以搜尋日誌" + enable-streaming-to-search: "啟用日誌串流到 Cloud" + open-container: "打開容器" + search-logs-shortcut: "搜尋日誌" + cloud-connected: "Cloud 已連接" + results-page-title: "日誌搜尋" + no-results: "沒有匹配的日誌行。" + search-failed: "Cloud 搜尋失敗。" + search-empty-prompt: "輸入查詢以搜尋日誌。" + searching: "搜尋中…" + hits-count: "{n} 條結果" + window-suffix: "在過去 14 天內" + hero-title-cloud: "搜尋容器和日誌" + hero-title-plain: "搜尋容器" + hero-pill-indexed: "Cloud 索引" + modal-placeholder-cloud: "搜尋容器和日誌…" + modal-placeholder-plain: "搜尋容器…" + col-time: "時間" + col-level: "級別" + col-container: "容器" + col-message: "訊息" + container-removed: "容器已被刪除" + cta-settings: "Cloud 設定" + container-removed-pill: "已刪除" diff --git a/locales/zh.yml b/locales/zh.yml index fa287c6f..1ce7cd57 100644 --- a/locales/zh.yml +++ b/locales/zh.yml @@ -339,3 +339,31 @@ cloud: create-alert: 创建您的第一个警报 default-alert-name: Container exited with error later: "稍后再说" +cloud-search: + containers-section: "容器" + search-logs-for: "搜索日志 \"{query}\"" + across-containers: "已在所有容器中建立索引" + connect-to-enable: "连接 Dozzle Cloud 以搜索日志" + enable-streaming-to-search: "启用日志流式传输到 Cloud" + open-container: "打开容器" + search-logs-shortcut: "搜索日志" + cloud-connected: "Cloud 已连接" + results-page-title: "日志搜索" + no-results: "没有匹配的日志行。" + search-failed: "Cloud 搜索失败。" + search-empty-prompt: "输入查询以搜索日志。" + searching: "搜索中…" + hits-count: "{n} 条结果" + window-suffix: "在过去 14 天内" + hero-title-cloud: "搜索容器和日志" + hero-title-plain: "搜索容器" + hero-pill-indexed: "Cloud 索引" + modal-placeholder-cloud: "搜索容器和日志…" + modal-placeholder-plain: "搜索容器…" + col-time: "时间" + col-level: "级别" + col-container: "容器" + col-message: "消息" + container-removed: "容器已被删除" + cta-settings: "Cloud 设置" + container-removed-pill: "已删除" diff --git a/main.go b/main.go index 0bae82f1..10652126 100644 --- a/main.go +++ b/main.go @@ -186,7 +186,11 @@ func main() { cloudClient.Notify() } - srv := createServer(args, hostService, cloudClient.Notify, cloudClient.Reconnect) + srv := createServer(args, hostService, web.CloudHooks{ + OnSetup: cloudClient.Notify, + OnUpdate: cloudClient.Reconnect, + SearchLogs: cloudClient.SearchLogs, + }) go func() { log.Info().Msgf("Accepting connections on %s", args.Addr) @@ -214,7 +218,7 @@ func fileExists(filename string) bool { return err == nil } -func createServer(args cli.Args, hostService web.HostService, onCloudSetup func(), onCloudUpdate func()) *http.Server { +func createServer(args cli.Args, hostService web.HostService, cloudHooks web.CloudHooks) *http.Server { _, dev := os.LookupEnv("DEV") var releaseCheckMode web.ReleaseCheckMode = web.Automatic @@ -293,8 +297,7 @@ func createServer(args cli.Args, hostService web.HostService, onCloudSetup func( DisableAvatars: args.DisableAvatars, ReleaseCheckMode: releaseCheckMode, Labels: args.Filter, - OnCloudSetup: onCloudSetup, - OnCloudUpdate: onCloudUpdate, + Cloud: cloudHooks, } assets, err := fs.Sub(content, "dist") diff --git a/proto/cloud/cloud.pb.go b/proto/cloud/cloud.pb.go index ee286f45..0a8ac828 100644 --- a/proto/cloud/cloud.pb.go +++ b/proto/cloud/cloud.pb.go @@ -353,6 +353,12 @@ type LogBatchEntry struct { Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` Stream string `protobuf:"bytes,6,opt,name=stream,proto3" json:"stream,omitempty"` // "stdout" or "stderr" Level string `protobuf:"bytes,7,opt,name=level,proto3" json:"level,omitempty"` // "info", "warn", "error", etc. (best-effort) + // Deterministic FNV-32a hash of the raw log line, the same id Dozzle + // stamps on LogEvent.Id. Cloud indexes this as a non-stream field so + // search results can produce a stable permanent link + // (/container/:id/time/:datetime?logId=...) that lands the user on + // exactly the matching line in the local log viewer. + LogId uint32 `protobuf:"varint,8,opt,name=log_id,json=logId,proto3" json:"log_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -436,6 +442,13 @@ func (x *LogBatchEntry) GetLevel() string { return "" } +func (x *LogBatchEntry) GetLogId() uint32 { + if x != nil { + return x.LogId + } + return 0 +} + type ListToolsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -1910,6 +1923,256 @@ func (x *NotificationResult) GetMessage() string { return "" } +// Cloud log search. +type SearchLogsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Substring/word-filter query. Empty -> empty result. Whitespace-only + // is rejected client-side; server treats as empty. + Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + // Result cap. Default 20, server-capped at 50. + Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + // Pagination cursor: return only hits with timestamp_ns < this value. + // 0 = newest. Reserved for future use. + BeforeTsNs int64 `protobuf:"varint,3,opt,name=before_ts_ns,json=beforeTsNs,proto3" json:"before_ts_ns,omitempty"` + // Optional filter — narrow to a specific Docker host inside the instance. + // Empty = all hosts under this instance. + HostId string `protobuf:"bytes,4,opt,name=host_id,json=hostId,proto3" json:"host_id,omitempty"` + // Optional filter — narrow to a specific container. + ContainerId string `protobuf:"bytes,5,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SearchLogsRequest) Reset() { + *x = SearchLogsRequest{} + mi := &file_cloud_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SearchLogsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchLogsRequest) ProtoMessage() {} + +func (x *SearchLogsRequest) ProtoReflect() protoreflect.Message { + mi := &file_cloud_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchLogsRequest.ProtoReflect.Descriptor instead. +func (*SearchLogsRequest) Descriptor() ([]byte, []int) { + return file_cloud_proto_rawDescGZIP(), []int{22} +} + +func (x *SearchLogsRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *SearchLogsRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *SearchLogsRequest) GetBeforeTsNs() int64 { + if x != nil { + return x.BeforeTsNs + } + return 0 +} + +func (x *SearchLogsRequest) GetHostId() string { + if x != nil { + return x.HostId + } + return "" +} + +func (x *SearchLogsRequest) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +type SearchLogsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Hits []*SearchLogHit `protobuf:"bytes,1,rep,name=hits,proto3" json:"hits,omitempty"` + HasMore bool `protobuf:"varint,2,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` + // For pagination: pass back as before_ts_ns to fetch the next page. + NextBeforeTsNs int64 `protobuf:"varint,3,opt,name=next_before_ts_ns,json=nextBeforeTsNs,proto3" json:"next_before_ts_ns,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SearchLogsResponse) Reset() { + *x = SearchLogsResponse{} + mi := &file_cloud_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SearchLogsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchLogsResponse) ProtoMessage() {} + +func (x *SearchLogsResponse) ProtoReflect() protoreflect.Message { + mi := &file_cloud_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchLogsResponse.ProtoReflect.Descriptor instead. +func (*SearchLogsResponse) Descriptor() ([]byte, []int) { + return file_cloud_proto_rawDescGZIP(), []int{23} +} + +func (x *SearchLogsResponse) GetHits() []*SearchLogHit { + if x != nil { + return x.Hits + } + return nil +} + +func (x *SearchLogsResponse) GetHasMore() bool { + if x != nil { + return x.HasMore + } + return false +} + +func (x *SearchLogsResponse) GetNextBeforeTsNs() int64 { + if x != nil { + return x.NextBeforeTsNs + } + return 0 +} + +type SearchLogHit struct { + state protoimpl.MessageState `protogen:"open.v1"` + TimestampNs int64 `protobuf:"varint,1,opt,name=timestamp_ns,json=timestampNs,proto3" json:"timestamp_ns,omitempty"` + HostId string `protobuf:"bytes,2,opt,name=host_id,json=hostId,proto3" json:"host_id,omitempty"` + ContainerId string `protobuf:"bytes,3,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"` + ContainerName string `protobuf:"bytes,4,opt,name=container_name,json=containerName,proto3" json:"container_name,omitempty"` + // Full log line as indexed. + Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` + Stream string `protobuf:"bytes,6,opt,name=stream,proto3" json:"stream,omitempty"` + Level string `protobuf:"bytes,7,opt,name=level,proto3" json:"level,omitempty"` + // FNV-32a hash of the raw log line — same id Dozzle assigns LogEvent.Id + // and exposes via "Copy permalink". Lets the search-result row deep-link + // straight to the matching line. + LogId uint32 `protobuf:"varint,8,opt,name=log_id,json=logId,proto3" json:"log_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SearchLogHit) Reset() { + *x = SearchLogHit{} + mi := &file_cloud_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SearchLogHit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchLogHit) ProtoMessage() {} + +func (x *SearchLogHit) ProtoReflect() protoreflect.Message { + mi := &file_cloud_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchLogHit.ProtoReflect.Descriptor instead. +func (*SearchLogHit) Descriptor() ([]byte, []int) { + return file_cloud_proto_rawDescGZIP(), []int{24} +} + +func (x *SearchLogHit) GetTimestampNs() int64 { + if x != nil { + return x.TimestampNs + } + return 0 +} + +func (x *SearchLogHit) GetHostId() string { + if x != nil { + return x.HostId + } + return "" +} + +func (x *SearchLogHit) GetContainerId() string { + if x != nil { + return x.ContainerId + } + return "" +} + +func (x *SearchLogHit) GetContainerName() string { + if x != nil { + return x.ContainerName + } + return "" +} + +func (x *SearchLogHit) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *SearchLogHit) GetStream() string { + if x != nil { + return x.Stream + } + return "" +} + +func (x *SearchLogHit) GetLevel() string { + if x != nil { + return x.Level + } + return "" +} + +func (x *SearchLogHit) GetLogId() uint32 { + if x != nil { + return x.LogId + } + return 0 +} + var File_cloud_proto protoreflect.FileDescriptor const file_cloud_proto_rawDesc = "" + @@ -1932,7 +2195,7 @@ const file_cloud_proto_rawDesc = "" + "\tlog_batch\x18\x04 \x01(\v2\x0f.cloud.LogBatchH\x00R\blogBatchB\x06\n" + "\x04type\":\n" + "\bLogBatch\x12.\n" + - "\aentries\x18\x01 \x03(\v2\x14.cloud.LogBatchEntryR\aentries\"\xdd\x01\n" + + "\aentries\x18\x01 \x03(\v2\x14.cloud.LogBatchEntryR\aentries\"\xf4\x01\n" + "\rLogBatchEntry\x12\x17\n" + "\ahost_id\x18\x01 \x01(\tR\x06hostId\x12!\n" + "\fcontainer_id\x18\x02 \x01(\tR\vcontainerId\x12%\n" + @@ -1940,7 +2203,8 @@ const file_cloud_proto_rawDesc = "" + "\ftimestamp_ns\x18\x04 \x01(\x03R\vtimestampNs\x12\x18\n" + "\amessage\x18\x05 \x01(\tR\amessage\x12\x16\n" + "\x06stream\x18\x06 \x01(\tR\x06stream\x12\x14\n" + - "\x05level\x18\a \x01(\tR\x05level\"\x12\n" + + "\x05level\x18\a \x01(\tR\x05level\x12\x15\n" + + "\x06log_id\x18\b \x01(\rR\x05logId\"\x12\n" + "\x10ListToolsRequest\"Z\n" + "\x11ListToolsResponse\x12+\n" + "\x05tools\x18\x01 \x03(\v2\x15.cloud.ToolDefinitionR\x05tools\x12\x18\n" + @@ -2068,15 +2332,37 @@ const file_cloud_proto_rawDesc = "" + "\amessage\x18\x03 \x01(\tR\amessage\"H\n" + "\x12NotificationResult\x12\x18\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessage*o\n" + + "\amessage\x18\x02 \x01(\tR\amessage\"\x9d\x01\n" + + "\x11SearchLogsRequest\x12\x14\n" + + "\x05query\x18\x01 \x01(\tR\x05query\x12\x14\n" + + "\x05limit\x18\x02 \x01(\x05R\x05limit\x12 \n" + + "\fbefore_ts_ns\x18\x03 \x01(\x03R\n" + + "beforeTsNs\x12\x17\n" + + "\ahost_id\x18\x04 \x01(\tR\x06hostId\x12!\n" + + "\fcontainer_id\x18\x05 \x01(\tR\vcontainerId\"\x83\x01\n" + + "\x12SearchLogsResponse\x12'\n" + + "\x04hits\x18\x01 \x03(\v2\x13.cloud.SearchLogHitR\x04hits\x12\x19\n" + + "\bhas_more\x18\x02 \x01(\bR\ahasMore\x12)\n" + + "\x11next_before_ts_ns\x18\x03 \x01(\x03R\x0enextBeforeTsNs\"\xf3\x01\n" + + "\fSearchLogHit\x12!\n" + + "\ftimestamp_ns\x18\x01 \x01(\x03R\vtimestampNs\x12\x17\n" + + "\ahost_id\x18\x02 \x01(\tR\x06hostId\x12!\n" + + "\fcontainer_id\x18\x03 \x01(\tR\vcontainerId\x12%\n" + + "\x0econtainer_name\x18\x04 \x01(\tR\rcontainerName\x12\x18\n" + + "\amessage\x18\x05 \x01(\tR\amessage\x12\x16\n" + + "\x06stream\x18\x06 \x01(\tR\x06stream\x12\x14\n" + + "\x05level\x18\a \x01(\tR\x05level\x12\x15\n" + + "\x06log_id\x18\b \x01(\rR\x05logId*o\n" + "\tToolScope\x12\x1a\n" + "\x16TOOL_SCOPE_UNSPECIFIED\x10\x00\x12\x17\n" + "\x13TOOL_SCOPE_INSTANCE\x10\x01\x12\x13\n" + "\x0fTOOL_SCOPE_HOST\x10\x02\x12\x18\n" + - "\x14TOOL_SCOPE_CONTAINER\x10\x032M\n" + + "\x14TOOL_SCOPE_CONTAINER\x10\x032\x90\x01\n" + "\x10CloudToolService\x129\n" + "\n" + - "ToolStream\x12\x13.cloud.ToolResponse\x1a\x12.cloud.ToolRequest(\x010\x01B&Z$github.com/amir20/dozzle/proto/cloudb\x06proto3" + "ToolStream\x12\x13.cloud.ToolResponse\x1a\x12.cloud.ToolRequest(\x010\x01\x12A\n" + + "\n" + + "SearchLogs\x12\x18.cloud.SearchLogsRequest\x1a\x19.cloud.SearchLogsResponseB&Z$github.com/amir20/dozzle/proto/cloudb\x06proto3" var ( file_cloud_proto_rawDescOnce sync.Once @@ -2091,7 +2377,7 @@ func file_cloud_proto_rawDescGZIP() []byte { } var file_cloud_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_cloud_proto_msgTypes = make([]protoimpl.MessageInfo, 23) +var file_cloud_proto_msgTypes = make([]protoimpl.MessageInfo, 26) var file_cloud_proto_goTypes = []any{ (ToolScope)(0), // 0: cloud.ToolScope (*ToolRequest)(nil), // 1: cloud.ToolRequest @@ -2116,7 +2402,10 @@ var file_cloud_proto_goTypes = []any{ (*ActionResult)(nil), // 20: cloud.ActionResult (*DeployResult)(nil), // 21: cloud.DeployResult (*NotificationResult)(nil), // 22: cloud.NotificationResult - nil, // 23: cloud.InspectContainerResult.LabelsEntry + (*SearchLogsRequest)(nil), // 23: cloud.SearchLogsRequest + (*SearchLogsResponse)(nil), // 24: cloud.SearchLogsResponse + (*SearchLogHit)(nil), // 25: cloud.SearchLogHit + nil, // 26: cloud.InspectContainerResult.LabelsEntry } var file_cloud_proto_depIdxs = []int32{ 5, // 0: cloud.ToolRequest.list_tools:type_name -> cloud.ListToolsRequest @@ -2140,14 +2429,17 @@ var file_cloud_proto_depIdxs = []int32{ 13, // 18: cloud.ListContainersResult.containers:type_name -> cloud.ContainerInfo 15, // 19: cloud.ContainerStatsResult.stats:type_name -> cloud.ContainerStatEntry 17, // 20: cloud.FetchLogsResult.entries:type_name -> cloud.LogEntry - 23, // 21: cloud.InspectContainerResult.labels:type_name -> cloud.InspectContainerResult.LabelsEntry - 2, // 22: cloud.CloudToolService.ToolStream:input_type -> cloud.ToolResponse - 1, // 23: cloud.CloudToolService.ToolStream:output_type -> cloud.ToolRequest - 23, // [23:24] is the sub-list for method output_type - 22, // [22:23] is the sub-list for method input_type - 22, // [22:22] is the sub-list for extension type_name - 22, // [22:22] is the sub-list for extension extendee - 0, // [0:22] is the sub-list for field type_name + 26, // 21: cloud.InspectContainerResult.labels:type_name -> cloud.InspectContainerResult.LabelsEntry + 25, // 22: cloud.SearchLogsResponse.hits:type_name -> cloud.SearchLogHit + 2, // 23: cloud.CloudToolService.ToolStream:input_type -> cloud.ToolResponse + 23, // 24: cloud.CloudToolService.SearchLogs:input_type -> cloud.SearchLogsRequest + 1, // 25: cloud.CloudToolService.ToolStream:output_type -> cloud.ToolRequest + 24, // 26: cloud.CloudToolService.SearchLogs:output_type -> cloud.SearchLogsResponse + 25, // [25:27] is the sub-list for method output_type + 23, // [23:25] is the sub-list for method input_type + 23, // [23:23] is the sub-list for extension type_name + 23, // [23:23] is the sub-list for extension extendee + 0, // [0:23] is the sub-list for field type_name } func init() { file_cloud_proto_init() } @@ -2181,7 +2473,7 @@ func file_cloud_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_cloud_proto_rawDesc), len(file_cloud_proto_rawDesc)), NumEnums: 1, - NumMessages: 23, + NumMessages: 26, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/cloud/cloud_grpc.pb.go b/proto/cloud/cloud_grpc.pb.go index 31c464ea..ceb9dc99 100644 --- a/proto/cloud/cloud_grpc.pb.go +++ b/proto/cloud/cloud_grpc.pb.go @@ -20,6 +20,7 @@ const _ = grpc.SupportPackageIsVersion9 const ( CloudToolService_ToolStream_FullMethodName = "/cloud.CloudToolService/ToolStream" + CloudToolService_SearchLogs_FullMethodName = "/cloud.CloudToolService/SearchLogs" ) // CloudToolServiceClient is the client API for CloudToolService service. @@ -28,6 +29,11 @@ const ( type CloudToolServiceClient interface { // Dozzle sends ToolResponse, cloud sends ToolRequest ToolStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ToolResponse, ToolRequest], error) + // Dozzle-initiated unary call: search log lines this instance has streamed + // to Cloud (gated by streamLogs opt-in). Cloud scopes the query server-side + // to the (user_id, api_key_id) derived from the authenticated connection; + // Dozzle does NOT pass any identity fields in the request. + SearchLogs(ctx context.Context, in *SearchLogsRequest, opts ...grpc.CallOption) (*SearchLogsResponse, error) } type cloudToolServiceClient struct { @@ -51,12 +57,27 @@ func (c *cloudToolServiceClient) ToolStream(ctx context.Context, opts ...grpc.Ca // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type CloudToolService_ToolStreamClient = grpc.BidiStreamingClient[ToolResponse, ToolRequest] +func (c *cloudToolServiceClient) SearchLogs(ctx context.Context, in *SearchLogsRequest, opts ...grpc.CallOption) (*SearchLogsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SearchLogsResponse) + err := c.cc.Invoke(ctx, CloudToolService_SearchLogs_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // CloudToolServiceServer is the server API for CloudToolService service. // All implementations must embed UnimplementedCloudToolServiceServer // for forward compatibility. type CloudToolServiceServer interface { // Dozzle sends ToolResponse, cloud sends ToolRequest ToolStream(grpc.BidiStreamingServer[ToolResponse, ToolRequest]) error + // Dozzle-initiated unary call: search log lines this instance has streamed + // to Cloud (gated by streamLogs opt-in). Cloud scopes the query server-side + // to the (user_id, api_key_id) derived from the authenticated connection; + // Dozzle does NOT pass any identity fields in the request. + SearchLogs(context.Context, *SearchLogsRequest) (*SearchLogsResponse, error) mustEmbedUnimplementedCloudToolServiceServer() } @@ -70,6 +91,9 @@ type UnimplementedCloudToolServiceServer struct{} func (UnimplementedCloudToolServiceServer) ToolStream(grpc.BidiStreamingServer[ToolResponse, ToolRequest]) error { return status.Error(codes.Unimplemented, "method ToolStream not implemented") } +func (UnimplementedCloudToolServiceServer) SearchLogs(context.Context, *SearchLogsRequest) (*SearchLogsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SearchLogs not implemented") +} func (UnimplementedCloudToolServiceServer) mustEmbedUnimplementedCloudToolServiceServer() {} func (UnimplementedCloudToolServiceServer) testEmbeddedByValue() {} @@ -98,13 +122,36 @@ func _CloudToolService_ToolStream_Handler(srv interface{}, stream grpc.ServerStr // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. type CloudToolService_ToolStreamServer = grpc.BidiStreamingServer[ToolResponse, ToolRequest] +func _CloudToolService_SearchLogs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SearchLogsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CloudToolServiceServer).SearchLogs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CloudToolService_SearchLogs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CloudToolServiceServer).SearchLogs(ctx, req.(*SearchLogsRequest)) + } + return interceptor(ctx, in, info, handler) +} + // CloudToolService_ServiceDesc is the grpc.ServiceDesc for CloudToolService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var CloudToolService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "cloud.CloudToolService", HandlerType: (*CloudToolServiceServer)(nil), - Methods: []grpc.MethodDesc{}, + Methods: []grpc.MethodDesc{ + { + MethodName: "SearchLogs", + Handler: _CloudToolService_SearchLogs_Handler, + }, + }, Streams: []grpc.StreamDesc{ { StreamName: "ToolStream", diff --git a/protos/cloud.proto b/protos/cloud.proto index d961d64c..2197e779 100644 --- a/protos/cloud.proto +++ b/protos/cloud.proto @@ -7,6 +7,12 @@ option go_package = "github.com/amir20/dozzle/proto/cloud"; service CloudToolService { // Dozzle sends ToolResponse, cloud sends ToolRequest rpc ToolStream(stream ToolResponse) returns (stream ToolRequest); + + // Dozzle-initiated unary call: search log lines this instance has streamed + // to Cloud (gated by streamLogs opt-in). Cloud scopes the query server-side + // to the (user_id, api_key_id) derived from the authenticated connection; + // Dozzle does NOT pass any identity fields in the request. + rpc SearchLogs(SearchLogsRequest) returns (SearchLogsResponse); } message ToolRequest { @@ -45,6 +51,12 @@ message LogBatchEntry { string message = 5; string stream = 6; // "stdout" or "stderr" string level = 7; // "info", "warn", "error", etc. (best-effort) + // Deterministic FNV-32a hash of the raw log line, the same id Dozzle + // stamps on LogEvent.Id. Cloud indexes this as a non-stream field so + // search results can produce a stable permanent link + // (/container/:id/time/:datetime?logId=...) that lands the user on + // exactly the matching line in the local log viewer. + uint32 log_id = 8; } message ListToolsRequest {} @@ -230,3 +242,42 @@ message NotificationResult { bool success = 1; string message = 2; } + +// Cloud log search. +message SearchLogsRequest { + // Substring/word-filter query. Empty -> empty result. Whitespace-only + // is rejected client-side; server treats as empty. + string query = 1; + // Result cap. Default 20, server-capped at 50. + int32 limit = 2; + // Pagination cursor: return only hits with timestamp_ns < this value. + // 0 = newest. Reserved for future use. + int64 before_ts_ns = 3; + // Optional filter — narrow to a specific Docker host inside the instance. + // Empty = all hosts under this instance. + string host_id = 4; + // Optional filter — narrow to a specific container. + string container_id = 5; +} + +message SearchLogsResponse { + repeated SearchLogHit hits = 1; + bool has_more = 2; + // For pagination: pass back as before_ts_ns to fetch the next page. + int64 next_before_ts_ns = 3; +} + +message SearchLogHit { + int64 timestamp_ns = 1; + string host_id = 2; + string container_id = 3; + string container_name = 4; + // Full log line as indexed. + string message = 5; + string stream = 6; + string level = 7; + // FNV-32a hash of the raw log line — same id Dozzle assigns LogEvent.Id + // and exposes via "Copy permalink". Lets the search-result row deep-link + // straight to the matching line. + uint32 log_id = 8; +}