From bbb23ba91204adcbea23323e7f7b44bb9f279130 Mon Sep 17 00:00:00 2001 From: vetri15 <64657217+vetri15@users.noreply.github.com> Date: Sun, 3 May 2026 01:14:43 +0530 Subject: [PATCH 1/2] Fixes #4342 Add byte-range logfile chunk navigation for skipped content, with a compact follow/page up/page down toolbar that switches between live tailing and manual browsing. Also parse logfile window metadata, show skipped bytes for manual chunks, and add localized navigation labels. --- .../src/main/frontend/services/instance.ts | 29 +- .../src/main/frontend/utils/logtail.ts | 47 ++- .../views/instances/logfile/i18n.de.json | 7 +- .../views/instances/logfile/i18n.en.json | 7 +- .../views/instances/logfile/i18n.es.json | 8 +- .../views/instances/logfile/i18n.fr.json | 8 +- .../views/instances/logfile/i18n.is.json | 8 +- .../views/instances/logfile/i18n.ko.json | 8 +- .../views/instances/logfile/i18n.pt-BR.json | 8 +- .../views/instances/logfile/i18n.ru.json | 8 +- .../views/instances/logfile/i18n.zh-CN.json | 8 +- .../views/instances/logfile/i18n.zh-TW.json | 7 +- .../views/instances/logfile/index.vue | 298 ++++++++++++++---- 13 files changed, 377 insertions(+), 74 deletions(-) diff --git a/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts b/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts index edb7159475b..2efa23d1c30 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts @@ -23,7 +23,7 @@ import axios, { registerErrorToastInterceptor, } from '../utils/axios'; import waitForPolyfill from '../utils/eventsource-polyfill'; -import logtail from '../utils/logtail'; +import logtail, { getLogfileWindowMetadata } from '../utils/logtail'; import uri from '../utils/uri'; import { useSbaConfig } from '@/sba-config'; @@ -417,6 +417,25 @@ class Instance { ); } + async fetchLogfileRange(start: number, end: number): Promise { + const response = await this.axios.get(uri`actuator/logfile`, { + responseType: 'text', + headers: { + Accept: 'text/plain', + Range: `bytes=${start}-${end}`, + }, + }); + const metadata = getLogfileWindowMetadata(response); + + return { + data: response.data, + totalBytes: metadata.totalBytes, + windowStart: metadata.windowStart, + windowEnd: metadata.windowEnd, + status: response.status, + }; + } + async listMBeans() { return this.axios.get(uri`actuator/jolokia/list`, { headers: { Accept: 'application/json' }, @@ -544,6 +563,14 @@ type Endpoint = { url: string; }; +export type LogfileRange = { + data: string; + totalBytes: number; + windowStart: number; + windowEnd: number; + status: number; +}; + export const DOWN_STATES = ['OUT_OF_SERVICE', 'DOWN', 'OFFLINE', 'RESTRICTED']; export const UP_STATES = ['UP']; export const UNKNOWN_STATES = ['UNKNOWN']; diff --git a/spring-boot-admin-server-ui/src/main/frontend/utils/logtail.ts b/spring-boot-admin-server-ui/src/main/frontend/utils/logtail.ts index f0469aaa215..cb1619c2d8c 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/utils/logtail.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/utils/logtail.ts @@ -15,7 +15,39 @@ */ import { EMPTY, Observable, catchError, concatMap, of, timer } from './rxjs'; -export default (getFn, interval, initialSize = 300 * 1024) => { +export const DEFAULT_LOGFILE_CHUNK_SIZE = 300 * 1024; + +const parseInteger = (value, fallback) => { + const parsed = parseInt(value, 10); + return Number.isNaN(parsed) ? fallback : parsed; +}; + +export const getLogfileWindowMetadata = (response) => { + const contentLength = response.data.length; + const contentRange = response.headers.get('content-range'); + const rangeMatch = contentRange?.match(/^bytes\s+(\d+)-(\d+)\/(\d+|\*)$/i); + + if (rangeMatch) { + return { + windowStart: parseInteger(rangeMatch[1], 0), + windowEnd: parseInteger(rangeMatch[2], Math.max(contentLength - 1, 0)), + totalBytes: parseInteger(rangeMatch[3], contentLength), + }; + } + + const totalBytes = parseInteger( + response.headers.get('content-length'), + contentLength, + ); + + return { + windowStart: 0, + windowEnd: Math.max(contentLength - 1, 0), + totalBytes, + }; +}; + +export default (getFn, interval, initialSize = DEFAULT_LOGFILE_CHUNK_SIZE) => { let range = `bytes=-${initialSize}`; let size = 0; @@ -38,16 +70,21 @@ export default (getFn, interval, initialSize = 300 * 1024) => { concatMap((response) => { let initial = size === 0; const contentLength = response.data.length; + let windowStart = 0; + let windowEnd = Math.max(contentLength - 1, 0); if (response.status === 200) { if (!initial) { throw 'Expected 206 - Partial Content on subsequent requests.'; } size = contentLength; - range = `bytes=${size - 1}-`; + range = `bytes=${Math.max(size - 1, 0)}-`; } else if (response.status === 206) { - size = parseInt(response.headers['content-range'].split('/')[1]); - range = `bytes=${size - 1}-`; + const metadata = getLogfileWindowMetadata(response); + size = metadata.totalBytes; + windowStart = metadata.windowStart; + windowEnd = metadata.windowEnd; + range = `bytes=${Math.max(size - 1, 0)}-`; } else if (response.status === 416) { size = 0; range = `bytes=-${initialSize}`; @@ -77,6 +114,8 @@ export default (getFn, interval, initialSize = 300 * 1024) => { totalBytes: size, skipped, addendum, + windowStart, + windowEnd, }) : EMPTY; }), diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.de.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.de.json index 1666c0e9df8..f5fb3252dca 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.de.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.de.json @@ -3,7 +3,12 @@ "logfile": { "label": "Log", "download": "Herunterladen", - "wrap_lines": "Zeilen umbrechen" + "wrap_lines": "Zeilen umbrechen", + "page_up": "Seite nach oben", + "page_down": "Seite nach unten", + "previous_chunk": "Vorheriger Abschnitt", + "next_chunk": "Nächster Abschnitt", + "resume_follow": "Live-Ansicht fortsetzen" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.en.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.en.json index 8e0e2b909f9..8fe73010447 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.en.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.en.json @@ -3,7 +3,12 @@ "logfile": { "label": "Logfile", "download": "Download", - "wrap_lines": "Wrap lines" + "wrap_lines": "Wrap lines", + "page_up": "Page up", + "page_down": "Page down", + "previous_chunk": "Previous chunk", + "next_chunk": "Next chunk", + "resume_follow": "Resume follow" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.es.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.es.json index 724c7d54e6b..950ceceff92 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.es.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.es.json @@ -3,8 +3,12 @@ "logfile": { "label": "Archivo de log", "download": "Descargar", - - "wrap_lines": "Ajustar líneas" + "wrap_lines": "Ajustar líneas", + "page_up": "Subir página", + "page_down": "Bajar página", + "previous_chunk": "Fragmento anterior", + "next_chunk": "Fragmento siguiente", + "resume_follow": "Reanudar seguimiento" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.fr.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.fr.json index 8a5c72e802e..40b774559db 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.fr.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.fr.json @@ -2,7 +2,13 @@ "instances": { "logfile": { "label": "Fichier de log", - "download": "Téléchargement" + "download": "Téléchargement", + "wrap_lines": "Retour à la ligne", + "page_up": "Page vers le haut", + "page_down": "Page vers le bas", + "previous_chunk": "Segment précédent", + "next_chunk": "Segment suivant", + "resume_follow": "Reprendre le suivi" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.is.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.is.json index 0c0dfcbbee7..8fc53182239 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.is.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.is.json @@ -2,7 +2,13 @@ "instances": { "logfile": { "label": "Annálaskrá", - "download": "Niðurhal" + "download": "Niðurhal", + "wrap_lines": "Línuskipting", + "page_up": "Síða upp", + "page_down": "Síða niður", + "previous_chunk": "Fyrri hluti", + "next_chunk": "Næsti hluti", + "resume_follow": "Fylgja aftur" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.ko.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.ko.json index cd8994f94cc..0df3ad8655f 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.ko.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.ko.json @@ -2,7 +2,13 @@ "instances": { "logfile": { "label": "로그파일", - "download": "다운로드" + "download": "다운로드", + "wrap_lines": "줄 바꿈", + "page_up": "페이지 위로", + "page_down": "페이지 아래로", + "previous_chunk": "이전 구간", + "next_chunk": "다음 구간", + "resume_follow": "실시간 추적 재개" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.pt-BR.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.pt-BR.json index b11ff6016da..4042d925c9c 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.pt-BR.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.pt-BR.json @@ -2,7 +2,13 @@ "instances": { "logfile": { "label": "Arquivo de Log", - "download": "Download" + "download": "Download", + "wrap_lines": "Quebrar linhas", + "page_up": "Subir página", + "page_down": "Descer página", + "previous_chunk": "Trecho anterior", + "next_chunk": "Próximo trecho", + "resume_follow": "Retomar acompanhamento" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.ru.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.ru.json index 7cf7a720722..ab1fb909521 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.ru.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.ru.json @@ -2,7 +2,13 @@ "instances": { "logfile": { "label": "Лог-файл", - "download": "Загрузить" + "download": "Загрузить", + "wrap_lines": "Перенос строк", + "page_up": "Страница вверх", + "page_down": "Страница вниз", + "previous_chunk": "Предыдущий фрагмент", + "next_chunk": "Следующий фрагмент", + "resume_follow": "Возобновить слежение" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.zh-CN.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.zh-CN.json index ecb7c1ee088..a20bed683f6 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.zh-CN.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.zh-CN.json @@ -2,7 +2,13 @@ "instances": { "logfile": { "label": "日志文件", - "download": "下载" + "download": "下载", + "wrap_lines": "自动换行", + "page_up": "向上翻页", + "page_down": "向下翻页", + "previous_chunk": "上一段", + "next_chunk": "下一段", + "resume_follow": "恢复跟踪" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.zh-TW.json b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.zh-TW.json index 3d2fa9a1d77..15e8f21c7fe 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.zh-TW.json +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/i18n.zh-TW.json @@ -3,7 +3,12 @@ "logfile": { "label": "日誌檔案", "download": "下載", - "wrap_lines": "自動換行" + "wrap_lines": "自動換行", + "page_up": "向上翻頁", + "page_down": "向下翻頁", + "previous_chunk": "上一段", + "next_chunk": "下一段", + "resume_follow": "恢復追蹤" } } } diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/index.vue b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/index.vue index 3064d11e958..4c310ca9321 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/index.vue +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/logfile/index.vue @@ -48,37 +48,54 @@
- - - - + + - - - - + + + + +
@@ -95,12 +112,28 @@ :class="{ 'wrap-lines': wrapLines }" class="log-viewer overflow-x-auto text-sm -mx-6 -my-20 pt-14" > - +
+ + + + + +
+
+