Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bbb23ba
Fixes #4342
vetri15 May 2, 2026
e3f7040
Merge remote-tracking branch 'origin/master'
vetri15 May 2, 2026
5c12767
made skipped bytes text to show at all times including 0 bytes
vetri15 May 3, 2026
794c137
Merge branch 'codecentric:master' into master
vetri15 May 6, 2026
2358e7f
Implemented logfile chunk navigation and recovery improvements.
vetri15 May 24, 2026
01fed9b
it now renders empty lines using br instead of pre tag
vetri15 May 24, 2026
eb21719
carried over css styles of the table , td , tr , br from PR(#5388)
vetri15 May 24, 2026
a4fefba
viewing first line of logfile now possible
vetri15 May 25, 2026
4c82b8f
manual mode now simply navigates chunks , previously it will scroll d…
vetri15 May 25, 2026
3677742
added a minimal guard , so the scroll to bottom won't spill over from…
vetri15 May 25, 2026
26c0248
Previously, while suppressing 416 errors, other errors were also supp…
vetri15 May 25, 2026
c3ee7a5
corrected tool tip to show proper text
vetri15 May 25, 2026
7d88aef
handled logfile compression in manualmetadatapolling
vetri15 May 25, 2026
fcbfdd2
manual navigation renders with broken style , corrected by forcing ta…
vetri15 May 26, 2026
d4b05e6
retry logic added to auto recover
vetri15 May 26, 2026
d668728
416 error console spam when requesting near the end of file rectified
vetri15 May 27, 2026
18f5ccb
added auto recovery on manual mode too
vetri15 May 27, 2026
8938ab8
Updated manual chunk navigation to fill the full chunk window near fi…
vetri15 May 28, 2026
3f4e6fe
buttons are now disabled when retrying
vetri15 May 29, 2026
e6e07ab
now manual navigation scrolls to bottom instead of top
vetri15 May 29, 2026
ae0684e
Merge branch 'master' into issue-4342-logile-view
vetri15 May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<LogfileRange> {
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' },
Expand Down Expand Up @@ -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'];
146 changes: 102 additions & 44 deletions spring-boot-admin-server-ui/src/main/frontend/utils/logtail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -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;
}
}),
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export {
debounceTime,
mergeWith,
map,
retry,
retryWhen,
tap,
filter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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": "로그 파일이 압축되었습니다. 페이지를 재설정합니다"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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": "Файл журнала сжат, страница сбрасывается"
}
}
}
Loading