1
0
mirror of https://github.com/amir20/dozzle.git synced 2026-01-02 11:07:26 +01:00

feat: adds a button to fetch logs when skipping happens (#3920)

This commit is contained in:
Amir Raminfar
2025-05-28 08:47:01 -07:00
committed by GitHub
parent cfe7285bf7
commit f6f1b5207e
19 changed files with 87 additions and 58 deletions

View File

@@ -22,7 +22,9 @@ const { entity, streamSource } = $defineProps<{
entity: T;
}>();
const { messages, loadOlderLogs, isLoadingMore, opened, loading, error, eventSourceURL } = streamSource($$(entity));
const { messages, loadOlderLogs, isLoadingMore, opened, loading, error, eventSourceURL } = streamSource(
toRef(() => entity),
);
const { loadingMore } = useLoggingContext();
const color = computed(() => {
if (error.value) return "error";

View File

@@ -2,16 +2,16 @@
<div class="my-4 flex-1 text-center">
<div class="relative">
<ZigZag class="absolute inset-0 mt-2" />
<span class="bg-base-200 relative px-4 py-2 font-bold whitespace-pre-wrap">
<button class="btn btn-primary btn-xs relative whitespace-pre-wrap" @click="logEntry.loadSkippedEntries()">
{{ $t("error.logs-skipped", { total: logEntry.totalSkipped }) }}
</span>
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { SkippedLogsEntry } from "@/models/LogEntry";
defineProps<{
const { logEntry } = defineProps<{
logEntry: SkippedLogsEntry;
}>();
</script>

View File

@@ -71,10 +71,9 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
} else {
const firstItem = buffer.value.at(0) as LogEntry<string | JSONObject>;
const lastItem = buffer.value.at(-1) as LogEntry<string | JSONObject>;
messages.value = [
...messages.value,
new SkippedLogsEntry(new Date(), buffer.value.length, firstItem, lastItem),
new SkippedLogsEntry(new Date(), buffer.value.length, firstItem, lastItem, loadSkippedLogs),
];
}
buffer.value = [];
@@ -177,44 +176,67 @@ function useLogStream(url: Ref<string>, loadMoreUrl?: Ref<string>) {
const isLoadingMore = ref(false);
async function loadOlderLogs() {
if (!loadMoreUrl) return;
if (isLoadingMore.value) return;
async function loadBetween(from: Date, to: Date, lastSeenId: number, minimum: number = 0) {
const abortController = new AbortController();
const signal = abortController.signal;
if (isLoadingMore.value) throw new Error("Already loading");
try {
isLoadingMore.value = true;
const urlWithMoreParams = computed(() => {
const loadMoreParams = new URLSearchParams(params.value);
loadMoreParams.append("from", from.toISOString());
loadMoreParams.append("to", to.toISOString());
if (minimum > 0) {
loadMoreParams.append("minimum", String(minimum));
}
loadMoreParams.append("lastSeenId", String(lastSeenId));
return withBase(`${loadMoreUrl!.value}?${loadMoreParams.toString()}`);
});
const stopWatcher = watchOnce(urlWithMoreParams, () => abortController.abort("stream changed"));
const logs = await (await fetch(urlWithMoreParams.value, { signal })).text();
stopWatcher();
return {
logs: logs
.trim()
.split("\n")
.map((line) => parseMessage(line)),
signal,
};
} finally {
isLoadingMore.value = false;
}
}
async function loadOlderLogs() {
if (!loadMoreUrl) throw new Error("No loadMoreUrl");
const to = messages.value[0].date;
const lastSeenId = messages.value[0].id;
const last = messages.value[Math.min(messages.value.length - 1, 300)].date;
const delta = to.getTime() - last.getTime();
const from = new Date(to.getTime() + delta);
const abortController = new AbortController();
const signal = abortController.signal;
isLoadingMore.value = true;
try {
const urlWithMoreParams = computed(() => {
const loadMoreParams = new URLSearchParams(params.value);
loadMoreParams.append("from", from.toISOString());
loadMoreParams.append("to", to.toISOString());
loadMoreParams.append("minimum", "100");
loadMoreParams.append("lastSeenId", String(lastSeenId));
return withBase(`${loadMoreUrl.value}?${loadMoreParams.toString()}`);
});
const stopWatcher = watchOnce(urlWithMoreParams, () => abortController.abort("stream changed"));
const logs = await (await fetch(urlWithMoreParams.value, { signal })).text();
stopWatcher();
const { logs, signal } = await loadBetween(from, to, lastSeenId, 100);
if (logs && signal.aborted === false) {
const newMessages = logs
.trim()
.split("\n")
.map((line) => parseMessage(line));
messages.value = [...newMessages, ...messages.value];
messages.value = [...logs, ...messages.value];
}
} catch (e) {
console.error("Error loading older logs", e);
} finally {
isLoadingMore.value = false;
} catch (error) {
console.error(error);
}
}
async function loadSkippedLogs(entry: SkippedLogsEntry) {
if (!loadMoreUrl) throw new Error("No loadMoreUrl");
const from = entry.firstSkipped.date;
const to = entry.lastSkippedLog.date;
const lastSeenId = entry.lastSkippedLog.id;
try {
const { logs, signal } = await loadBetween(from, to, lastSeenId);
if (logs && signal.aborted === false) {
messages.value = messages.value.slice(logs.length).flatMap((log) => (log === entry ? logs : [log]));
}
} catch (error) {
console.error(error);
}
}

View File

@@ -148,7 +148,7 @@ export class ContainerEventLogEntry extends LogEntry<string> {
}
export class SkippedLogsEntry extends LogEntry<string> {
private _totalSkipped = 0;
private _totalSkipped = ref(0);
private lastSkipped: LogEntry<string | JSONObject>;
constructor(
@@ -156,9 +156,10 @@ export class SkippedLogsEntry extends LogEntry<string> {
totalSkipped: number,
public readonly firstSkipped: LogEntry<string | JSONObject>,
lastSkipped: LogEntry<string | JSONObject>,
private readonly loader: (i: SkippedLogsEntry) => Promise<void>,
) {
super("", "", date.getTime(), date, "stderr", "info");
this._totalSkipped = totalSkipped;
this._totalSkipped.value = totalSkipped;
this.lastSkipped = lastSkipped;
}
getComponent(): Component {
@@ -166,20 +167,24 @@ export class SkippedLogsEntry extends LogEntry<string> {
}
public get message(): string {
return `Skipped ${this.totalSkipped} entries`;
return `Skipped ${this._totalSkipped.value} entries`;
}
public addSkippedEntries(totalSkipped: number, lastItem: LogEntry<string | JSONObject>) {
this._totalSkipped += totalSkipped;
this._totalSkipped.value += totalSkipped;
this.lastSkipped = lastItem;
}
public get totalSkipped(): number {
return this._totalSkipped;
public get lastSkippedLog(): LogEntry<string | JSONObject> {
return this.lastSkipped;
}
public get lastSkippedItem(): LogEntry<string | JSONObject> {
return this.lastSkipped;
public async loadSkippedEntries(): Promise<void> {
await this.loader(this);
}
public get totalSkipped(): number {
return unref(this._totalSkipped);
}
}

View File

@@ -41,7 +41,7 @@ tooltip:
error:
page-not-found: Denne side eksisterer ikke
invalid-auth: Brugernavn eller kodeord er ikke gyldig
logs-skipped: Sprang over {total} indtastninger
logs-skipped: Vis {total} skjulte indtastninger
container-not-found: Container ikke fundet
events-stream:
title: Uventet fejl

View File

@@ -41,7 +41,7 @@ tooltip:
error:
page-not-found: Diese Seite existiert nicht.
invalid-auth: Benutzername und Passwort sind ungültig.
logs-skipped: \{total} Einträge übersprungen
logs-skipped: Zeige {total} versteckte Einträge
container-not-found: Container nicht gefunden.
events-stream:
title: Unerwarteter Fehler

View File

@@ -41,7 +41,7 @@ tooltip:
error:
page-not-found: This page does not exist
invalid-auth: Username or password are not valid
logs-skipped: Skipped {total} entries
logs-skipped: Show {total} hidden entries
container-not-found: Container not found
events-stream:
title: Unexpected Error

View File

@@ -41,7 +41,7 @@ tooltip:
error:
page-not-found: Esta página no existe.
invalid-auth: El nombre de usuario y la contraseña no son válidos.
logs-skipped: Omitidas {total} entrada/s
logs-skipped: Mostrar {total} entradas ocultas
container-not-found: Contenedor no encontrado.
events-stream:
title: Error inesperado

View File

@@ -41,7 +41,7 @@ tooltip:
error:
page-not-found: Cette page n'existe pas
invalid-auth: Nom d'utilisateur ou mot de passe non valides
logs-skipped: Entrées {total} ignorées
logs-skipped: Afficher {total} entrées cachées
container-not-found: Conteneur non trouvé
events-stream:
title: Erreur inattendue

View File

@@ -44,7 +44,7 @@ tooltip:
error:
page-not-found: Halaman ini tidak ada
invalid-auth: Nama pengguna atau kata sandi tidak valid
logs-skipped: Melewati {total} entri
logs-skipped: Tampilkan {total} entri tersembunyi
container-not-found: Kontainer tidak ditemukan
events-stream:
title: Kesalahan Tak Terduga

View File

@@ -41,7 +41,7 @@ tooltip:
error:
page-not-found: Questa pagina non esiste
invalid-auth: Username o password non valide
logs-skipped: Saltato {total} registrazioni
logs-skipped: Mostra {total} voci nascoste
container-not-found: Container non trovato
events-stream:
title: Errore inaspettato

View File

@@ -41,7 +41,7 @@ tooltip:
error:
page-not-found: Deze pagina bestaat niet
invalid-auth: Gebruikersnaam of wachtwoord is ongeldig
logs-skipped: \{total} items overgeslagen
logs-skipped: Toon {total} verborgen items
container-not-found: Container niet gevonden
events-stream:
title: Onverwachte fout

View File

@@ -45,7 +45,7 @@ tooltip:
error:
page-not-found: Ta strona nie istnieje
invalid-auth: Nazwa użytkownika lub hasło są niepoprawne
logs-skipped: Pominiętych wpisów {total}
logs-skipped: Pokaż {total} ukrytych wpisów
container-not-found: Kontener nie został znaleziony
events-stream:
title: Niespodziewany błąd

View File

@@ -40,7 +40,7 @@ tooltip:
error:
page-not-found: Esta página não existe.
invalid-auth: O nome de usuário e a senha não são válidos.
logs-skipped: Saltado {total} entradas
logs-skipped: Mostrar {total} entradas ocultas
container-not-found: Contentor não encontrado.
events-stream:
title: Erro inesperado

View File

@@ -41,7 +41,7 @@ tooltip:
error:
page-not-found: Esta página não existe
invalid-auth: Usuário ou senha inválidos
logs-skipped: \{total} entradas ignoradas
logs-skipped: Mostrar {total} entradas ocultas
container-not-found: Container não encontrado
events-stream:
title: Erro Inesperado

View File

@@ -41,7 +41,7 @@ tooltip:
error:
page-not-found: Эта страница не доступна.
invalid-auth: Имя пользователя или пароль неверны.
logs-skipped: Пропущено {total} записей
logs-skipped: Показать {total} скрытых записей
container-not-found: Контейнер не найден.
events-stream:
title: Неожиданная ошибка

View File

@@ -47,7 +47,7 @@ tooltip:
error:
page-not-found: Bu sayfa bulunamadı
invalid-auth: Kullanıcı adı veya şifre geçersiz
logs-skipped: "{total} giriş atlanıldı"
logs-skipped: "{total} gizli girişi göster"
container-not-found: Konteyner bulunamadı
events-stream:
title: Beklenmeyen Hata

View File

@@ -41,7 +41,7 @@ tooltip:
error:
page-not-found: 此頁面不存在
invalid-auth: 使用者名稱或密碼不正確
logs-skipped: 已略過 {total} 筆記錄
logs-skipped: 顯示 {total} 個隱藏項目
container-not-found: 找不到容器
events-stream:
title: 意外錯誤

View File

@@ -41,7 +41,7 @@ tooltip:
error:
page-not-found: 此页面不存在。
invalid-auth: 用户名和密码无效。
logs-skipped: 跳过 {total} 条记录
logs-skipped: 显示 {total} 个隐藏条目
container-not-found: 容器未找到。
events-stream:
title: 意外错误