diff --git a/frontend/src/lib/components/LogSnippetViewer.svelte b/frontend/src/lib/components/LogSnippetViewer.svelte index 8aa1ad9dec..ce913cb6c2 100644 --- a/frontend/src/lib/components/LogSnippetViewer.svelte +++ b/frontend/src/lib/components/LogSnippetViewer.svelte @@ -36,9 +36,10 @@ let html = highlightSnippet(untrack(() => content)) - - -{@html html} - - +{@html html} diff --git a/frontend/src/lib/components/ServiceLogsInner.svelte b/frontend/src/lib/components/ServiceLogsInner.svelte index 16bdd7d2ad..85c6cf7fb9 100644 --- a/frontend/src/lib/components/ServiceLogsInner.svelte +++ b/frontend/src/lib/components/ServiceLogsInner.svelte @@ -1,7 +1,4 @@ - + {#snippet actions()} copyToClipboard(content)} - color="light" - size="xs" - startIcon={{ - icon: ClipboardCopy - }} + onClick={() => copyToClipboard(content)} + variant="default" + unifiedSize="md" + startIcon={{ icon: ClipboardCopy }} > Copy to clipboard {/snippet} - - ...{#each content.split('\n') as line, index}{@html line}{/each}... - + {#each content.split('\n') as line, index}{@html line}{/each} - - {#snippet left_pane()} - - - { - minTs = undefined - maxTs = undefined - allLogs = undefined - const ts = timeframe.computeMinMax() - getAllLogs(ts.minTs ?? undefined, ts.maxTs ?? undefined) - }} - /> - - 0' }} - on:change={() => { - allLogs = undefined - getAllLogs(minTs, maxTs) - }} - /> - { - if (e.detail) { - getAllLogs(maxTs, undefined) - } else { - timeout && clearTimeout(timeout) - } - }} - options={{ right: 'auto-refresh' }} - /> - {#if searchError} - - - {searchError} - + + + {@render title?.()} + + + + {#if searchTerm !== '' && queryParseErrors && queryParseErrors.length > 0} + + + + {#snippet text()} + + Some search terms were ignored because they could not be parsed: + + {#each queryParseErrors ?? [] as msg} + {msg} + {/each} + + + {/snippet} + {/if} - {#if allLogs == undefined} - - {:else if Object.keys(allLogs).length == 0} - - No logs - Search only covers a recent time window, configurable in instance settings under - Indexer. - - {:else if minTs && maxTs} - {@const minTsN = new Date(minTs).getTime()} - {@const maxTsN = new Date(maxTs).getTime()} - {@const diff = maxTsN - minTsN} - {#if searchTerm === ''} - - + + { + minTs = undefined + maxTs = undefined + allLogs = undefined + const ts = timeframe.computeMinMax() + getAllLogs(ts.minTs ?? undefined, ts.maxTs ?? undefined) + }} + /> + + { + allLogs = undefined + getAllLogs(minTs, maxTs) + }} + /> + { + if (e.detail) { + getAllLogs(maxTs, undefined) + } else { + timeout && clearTimeout(timeout) + } + }} + options={{ + right: 'Auto-refresh', + rightTooltip: 'Fetch new log files every 5 seconds. Paused while searching.' + }} + /> + + - {new Date(minTs).toLocaleTimeString([], { - day: '2-digit', - month: '2-digit', - hour: '2-digit', - minute: '2-digit' - })}{new Date(maxTs).toLocaleTimeString([], { - day: '2-digit', - month: '2-digit', - hour: '2-digit', - minute: '2-digit' - })} - - {/if} - - { - selected = undefined - }} - placeholder="Select a service" - /> + {#if searchError} + + {searchError} + + {/if} + + + + {#snippet left_pane()} + + {#if allLogs == undefined} + + {#each new Array(6) as _} + + {/each} + + {:else if Object.keys(allLogs).length == 0} + + + + {:else} + + selected, + (v) => (v ? selectHost(v.mode, v.workerGroup, v.hostname) : (selected = v)) + } + items={getSelectItems(allLogs, countsPerHost)} + onClear={() => { + selected = undefined + }} + size="sm" + placeholder="Find a host" + /> + + {@const minTsN = minTs ? new Date(minTs).getTime() : 0} + {@const diff = maxTs && minTs ? new Date(maxTs).getTime() - minTsN || 1 : 1} + + {#if searchTerm === '' && minTs && maxTs} + + + + + {formatAxisTime(minTs, maxTs)} + {formatAxisTime(maxTs, minTs)} + + + {/if} + {#each sortedEntries(allLogsOrQueryResults(allLogs, countsPerHost)) as [mode, o1]} + + {modeLabel(mode, true)} + {#each sortedEntries(o1) as [wg, o2]} + {@const hosts = visibleHosts(mode, wg, o2)} + {#if hosts.length > 0} + + {#if wg} + {wg} + {/if} + {#each hosts as [hn, files]} + {@const hostKey = `${mode},${wg},${hn}`} + {@const hostMax = Math.max( + 1, + ...files.map((f) => f.ok_lines + f.err_lines) + )} + selectHost(mode, wg, hn)} + > + {truncateRev(hn, 14)} + {#if loadingLogCounts} + + {:else if countsPerHost} + + {countsPerHost[hostKey]?.doc_count ?? 0} matches + + {:else} + + {#each files as file} + {@const total = file.ok_lines + file.err_lines} + {@const height = + total > 0 ? Math.max(10, (100 * total) / hostMax) : 0} + {@const errShare = total > 0 ? file.err_lines / total : 0} + + + + + {/each} + + {/if} + + {/each} + + {/if} + {/each} + + {/each} + {#if !loadingLogCounts && sumOtherDocCount != 0} + + {sumOtherDocCount} more matches are not attributed to any of these hosts. + + {/if} + + {/if} - {#each Object.entries(allLogsOrQueryResults(allLogs, countsPerHost)) as [mode, o1]} - - {mode}s - {#each Object.entries(o1) as [wg, o2]} - - {#if wg && wg != ''} - {wg} + {/snippet} + {#snippet right_pane()} + + {#if selected} + + + + + {modeLabel(selected.mode, false)} + + {selected.hostname} + + {#if selected.workerGroup} + + Worker group {selected.workerGroup} + {/if} - - {#each Object.entries(o2).filter(([hn, files]) => { - if (selected && selected.mode === mode && selected.workerGroup === wg && selected.hostname === hn) { - return true - } - const hostKey = `${mode},${wg},${hn}` - if (countsPerHost && (countsPerHost[hostKey] == undefined || countsPerHost[hostKey].doc_count === 0)) { - return false - } - return true - }) as [hn, files]} - {@const hostKey = `${mode},${wg},${hn}`} - - - { - selected = { mode, workerGroup: wg, hostname: hn } - upToIsLatest = true - upTo = getLatestUpTo(selected) - scrollToBottom() + + {#if searchTerm == ''} + + Last 5 log files up to + shiftUpTo(-5)} + /> + + + { + upToIsLatest = false + jumpToEnd() }} + /> + + shiftUpTo(5)} + /> + { + upTo = new Date().toISOString() + upToIsLatest = true + jumpToEnd() + }} + > + Latest + + + {:else if logs != undefined && !loadingLogs} + + {(logs.hits ?? []).length} matches on this host + + {/if} + + {#if searchTerm == ''} + + Logs appear about a minute after they are written, once their file is compacted. + + {/if} + + + {#if loadingLogs} + + + + {:else if logs != undefined} + + + {#each logs.hits ?? [] as hit, i (`${i}:${hit.file_path}:${hit.line_no}`)} + seeLogContext(hit.line_no, hit.file_path, hit.host, true)} + /> + {/each} + {#if (logs.hits ?? []).length === 0} + + + + {/if} + {#if (logs.hits ?? []).length === 1000} + + Only the 1000 most recent matches are shown. Narrow the search or the time + range to see older ones. + + {/if} + + {:else} + + {#each getLogs(selected, upTo) as file (file.file_path)} + {@const entry = logsContent[fileKey(selected.hostname, file.file_path)]} + {truncateRev(hn, countsPerHost || loadingLogs ? 40 : 8)} - {#if loadingLogCounts} - - {:else if countsPerHost} - - {countsPerHost[hostKey]?.doc_count ?? 0} matches + {formatTime(file.ts)} + + {#if entry == undefined} + + {:else if entry.error} + + {#if entry.error?.startsWith('Not Found')} + + Servers and workers need a shared log volume, or the EE object storage + for logs set in the instance settings, for their log files to be + readable from here. + + {:else} + + {entry.error} + + {/if} + {:else if entry.content} + {:else} - - {#each files as file} - {@const okHeight = 100.0 * ((file.ok_lines * 1.0) / (max_lines ?? 1))} - {@const errHeight = 100.0 * ((file.err_lines * 1.0) / (max_lines ?? 1))} - - - {/each} - + This log file is empty. {/if} {/each} - - - {/each} - - {/each} - {#if !loadingLogCounts && sumOtherDocCount != 0} - - Note: {sumOtherDocCount} additional matches weren't grouped into any of the above hosts. - - {/if} - {/if} - - {/snippet} - {#snippet right_pane()} - - {#if selected} - {#if !loadingLogs && logs == undefined} - - 1 min delay: logs are compacted before being available - - {/if} - - {#if loadingLogs} - - - - - - {:else if logs != undefined} - - - {#each logs.hits ?? [] as hit, i (`${i}:${hit.file_path}:${hit.line_no}`)} - seeLogContext(hit.line_no, hit.file_path, hit.host, true)} - /> - {/each} - {#if (logs.hits ?? []).length === 0} - No logs - {/if} - {#if (logs.hits ?? []).length === 1000} - - Older matches were truncated from this search, try refining your filters to get - more precise results. - - {/if} - - - {:else} - {#each getLogs(selected, upTo) as file} - - {new Date(file.ts).toLocaleTimeString([], { - day: '2-digit', - month: '2-digit', - hour: '2-digit', - minute: '2-digit' - })} - {#if logsContent[file.file_path] == undefined} - - {:else if logsContent[file.file_path]} - {#if logsContent[file.file_path].error} - {#if logsContent[file.file_path].error?.startsWith('Not Found')} - Log file is missing. Log files require a shared log volume to be mounted - across servers and workers or to use the EE S3/object storage integration - for logs. To avoid mounting a shared volume, set the EE object store logs in - the instance settings - {:else} - {logsContent[file.file_path].error} - {/if} - {:else if logsContent[file.file_path].content} - - - - {:else} - No logs - {/if} {/if} - {/each} + {#if showScrollToEnd && logs == undefined} + + scrollToEnd(true)} + > + {#if hasNewLogs}New logs{/if} + + {#if hasNewLogs} + + + + + {/if} + + {/if} + + + {hasNewLogs ? 'New logs available below' : ''} + + + {:else} + + + {/if} - {#if searchTerm == ''} - - Last 5 log files up to: - - {#if upTo} - { - if (upTo) { - upToIsLatest = false - upTo = new Date(new Date(upTo).getTime() - 5 * 60 * 1000).toISOString() - } - }}>{'<'} 5m - {:else} - - {/if} + {/snippet} + + + - - - {#if upTo} - { - if (upTo) { - upToIsLatest = false - upTo = new Date(new Date(upTo).getTime() + 5 * 60 * 1000).toISOString() - } - }}>5m {'>'} - {:else} - - {/if} - - - { - upTo = new Date().toISOString() - upToIsLatest = true - }}>now - - - {/if} - {:else} - Select a host to see its logs - {/if} - {/snippet} - + diff --git a/frontend/src/routes/(root)/(logged)/service_logs/+page.svelte b/frontend/src/routes/(root)/(logged)/service_logs/+page.svelte index cab2a14790..5b8e56b5a2 100644 --- a/frontend/src/routes/(root)/(logged)/service_logs/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/service_logs/+page.svelte @@ -1,72 +1,34 @@ - - - - - Service logs - Explore and search Windmill service logs from within Windmill! - - +{#snippet title()} + + Service logs + + Logs written by the servers, workers and indexers of this instance, grouped by host. + +{/snippet} + {#if $devopsRole == false} - - Service logs are only available to superadmins (or devops) - - {:else} - - - - - {#if searchTerm !== '' && queryParseErrors && queryParseErrors.length > 0} - - - {#snippet text()} - Some of your search terms have been ignored because one or more parse errors: - {#if queryParseErrors} - - {#each queryParseErrors as msg} - - {msg} - {/each} - - {/if} - {/snippet} - - {/if} + + {@render title()} + + Ask one of them to look at the logs, or to grant you the devops role. + - - + {:else} + {/if} - -
-{@html html} -
...{#each content.split('\n') as line, index}{@html line}{/each}...
{#each content.split('\n') as line, index}{@html line}{/each}
This log file is empty.