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..00cf27653ff 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'; @@ -412,11 +412,35 @@ class Instance { streamLogfile(interval: number) { return logtail( - (opt) => this.axios.get(uri`actuator/logfile`, opt), + (opt) => + this.axios.get(uri`actuator/logfile`, { + ...opt, + suppressToast: (error: AxiosError) => error.response?.status === 416, + }), interval, ); } + 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}`, + }, + suppressToast: (error: AxiosError) => error.response?.status === 416, + }); + 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 +568,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 5e5d0c6c076..cf09f61f5bd 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 @@ -13,12 +13,65 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EMPTY, Observable, catchError, concatMap, of, timer } from './rxjs'; +import { + EMPTY, + Observable, + catchError, + concatMap, + of, + throwError, + timer, +} from './rxjs'; -export default (getFn, interval, initialSize = 300 * 1024) => { +export const DEFAULT_LOGFILE_CHUNK_SIZE = 300 * 1024; + +export enum StreamType { + Data = 'data', + Reset = 'reset', + Empty = 'empty', +} + +const parseInteger = (value, fallback) => { + const parsed = parseInt(value, 10); + return Number.isNaN(parsed) ? fallback : parsed; +}; + +export const getTotalBytesFrom416 = (response) => { + const contentRange = response.headers?.get?.('content-range'); + const match = contentRange?.match(/^bytes\s+\*\/(\d+)$/i); + + return match ? parseInteger(match[1], undefined) : undefined; +}; + +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; - let atTheEnd = false; + let streamUpdated = true; return timer(0, interval).pipe( concatMap(() => { @@ -28,63 +81,68 @@ export default (getFn, interval, initialSize = 300 * 1024) => { headers: { range, Accept: 'text/plain' }, }) .then((response) => { + streamUpdated = false; observer.next(response); observer.complete(); }) .catch((error) => observer.error(error)); }).pipe( - catchError((error) => of({ data: '', status: error.response.status })), + catchError((error) => { + if (error.response?.status !== 416) { + return throwError(() => error); + } + + return of({ + data: '', + status: error.response?.status, + headers: error.response?.headers, + }); + }), ); }), concatMap((response) => { - let initial = size === 0; - const contentLength = response.data.length; - - if (response.status === 200) { - if (!initial) { - throw 'Expected 206 - Partial Content on subsequent requests.'; + //resetting when log file is compressed + if (response.status === 416) { + const currentSize = getTotalBytesFrom416(response); + if (currentSize === size) { + return EMPTY; } - size = contentLength; - range = `bytes=${size - 1}-`; - } else if (response.status === 206) { - const contentRangeParts = response.headers['content-range'].split('/'); - size = parseInt(contentRangeParts[1]); - // The end value of the range is always one byte less than the size when at the end - atTheEnd = parseInt(contentRangeParts[0].split('-')[1]) == size - 1; - range = `bytes=${size - 1}-`; - } else if (response.status === 416) { size = 0; range = `bytes=-${initialSize}`; - initial = true; + streamUpdated = true; + return of({ type: StreamType.Reset }); + } + const { windowStart, windowEnd, totalBytes } = + getLogfileWindowMetadata(response); + let addendum = response.data; + let addendumWindowStart = windowStart; + if (response.status === 206 || response.status === 200) { + if (totalBytes > size) { + streamUpdated = true; + const overlap = Math.max(size - windowStart, 0); + addendum = addendum.substring(overlap); + addendumWindowStart = windowStart + overlap; + size = totalBytes; + range = `bytes=${Math.max(size - 1, 0)}-`; + } } else { throw 'Unexpected response status: ' + response.status; } - - let addendum = null; - let skipped = 0; - - if (initial) { - if (contentLength >= size) { - addendum = response.data; - } else { - // In case of a partial response find the first line break. - addendum = response.data.substring(response.data.indexOf('\n') + 1); - skipped = size - addendum.length; - } - } else if (response.data.length > 1) { - // Remove the first byte which has been part of the previous response. - addendum = response.data.substring(1); + if (size === 0) { + return of({ type: StreamType.Empty }); } - return addendum - ? of({ - totalBytes: size, - skipped, - // The log file always temporarily ends with a new line until the next one is written. - // Therefore, if we're at the end of it, we drop such a new line. - addendum: atTheEnd ? addendum.trimEnd() : addendum, - }) - : EMPTY; + if (streamUpdated) { + return of({ + type: StreamType.Data, + totalBytes: size, + addendum, + windowStart: addendumWindowStart, + windowEnd, + }); + } else { + return EMPTY; + } }), ); }; diff --git a/spring-boot-admin-server-ui/src/main/frontend/utils/rxjs.ts b/spring-boot-admin-server-ui/src/main/frontend/utils/rxjs.ts index a3b5ad9c564..3f47975c1f1 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/utils/rxjs.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/utils/rxjs.ts @@ -32,6 +32,7 @@ export { debounceTime, mergeWith, map, + retry, retryWhen, tap, filter, 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..866f21d6f34 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,15 @@ "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", + "stop_follow": "Live-Ansicht anhalten", + "reconnecting": "Verbindung wird wiederhergestellt...", + "compressed_reset": "Logdatei komprimiert, Seite wird zurückgesetzt." } } } 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..df189814d9d 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,15 @@ "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", + "stop_follow": "Stop following", + "reconnecting": "Reconnecting...", + "compressed_reset": "Logfile compressed, resetting page." } } } 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..d786c9d2bcd 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,15 @@ "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", + "stop_follow": "Detener seguimiento", + "reconnecting": "Reconectando...", + "compressed_reset": "Archivo de registro comprimido, restableciendo la página" } } } 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..80dd7ddc10c 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,16 @@ "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", + "stop_follow": "Arrêter le suivi", + "reconnecting": "Reconnexion...", + "compressed_reset": "Fichier journal compressé, réinitialisation de la page" } } } 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..0fc7d34216d 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,16 @@ "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", + "stop_follow": "Hætta að fylgja", + "reconnecting": "Endurtengist...", + "compressed_reset": "Atvikaskrá þjöppuð, síðan er endurstillt" } } } 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..45b4ca84490 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,16 @@ "instances": { "logfile": { "label": "로그파일", - "download": "다운로드" + "download": "다운로드", + "wrap_lines": "줄 바꿈", + "page_up": "페이지 위로", + "page_down": "페이지 아래로", + "previous_chunk": "이전 구간", + "next_chunk": "다음 구간", + "resume_follow": "실시간 추적 재개", + "stop_follow": "실시간 추적 중지", + "reconnecting": "다시 연결 중...", + "compressed_reset": "로그 파일이 압축되었습니다. 페이지를 재설정합니다" } } } 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..2fa1889364f 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,16 @@ "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", + "stop_follow": "Parar acompanhamento", + "reconnecting": "Reconectando...", + "compressed_reset": "Arquivo de log compactado, redefinindo a página" } } } 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..12ef10bdf61 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,16 @@ "instances": { "logfile": { "label": "Лог-файл", - "download": "Загрузить" + "download": "Загрузить", + "wrap_lines": "Перенос строк", + "page_up": "Страница вверх", + "page_down": "Страница вниз", + "previous_chunk": "Предыдущий фрагмент", + "next_chunk": "Следующий фрагмент", + "resume_follow": "Возобновить слежение", + "stop_follow": "Остановить слежение", + "reconnecting": "Повторное подключение...", + "compressed_reset": "Файл журнала сжат, страница сбрасывается" } } } 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..7dfd61ed92c 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,16 @@ "instances": { "logfile": { "label": "日志文件", - "download": "下载" + "download": "下载", + "wrap_lines": "自动换行", + "page_up": "向上翻页", + "page_down": "向下翻页", + "previous_chunk": "上一段", + "next_chunk": "下一段", + "resume_follow": "恢复跟踪", + "stop_follow": "停止跟踪", + "reconnecting": "正在重新连接...", + "compressed_reset": "日志文件已压缩,正在重置页面。" } } } 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..5d0603510e5 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,15 @@ "logfile": { "label": "日誌檔案", "download": "下載", - "wrap_lines": "自動換行" + "wrap_lines": "自動換行", + "page_up": "向上翻頁", + "page_down": "向下翻頁", + "previous_chunk": "上一段", + "next_chunk": "下一段", + "resume_follow": "恢復追蹤", + "stop_follow": "停止追蹤", + "reconnecting": "正在重新連線...", + "compressed_reset": "記錄檔已壓縮,正在重設頁面。" } } } 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 0e906e31e4e..95e25a24a9e 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 @@ -26,7 +26,17 @@
  -