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:
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -41,7 +41,7 @@ tooltip:
|
||||
error:
|
||||
page-not-found: Эта страница не доступна.
|
||||
invalid-auth: Имя пользователя или пароль неверны.
|
||||
logs-skipped: Пропущено {total} записей
|
||||
logs-skipped: Показать {total} скрытых записей
|
||||
container-not-found: Контейнер не найден.
|
||||
events-stream:
|
||||
title: Неожиданная ошибка
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -41,7 +41,7 @@ tooltip:
|
||||
error:
|
||||
page-not-found: 此頁面不存在
|
||||
invalid-auth: 使用者名稱或密碼不正確
|
||||
logs-skipped: 已略過 {total} 筆記錄
|
||||
logs-skipped: 顯示 {total} 個隱藏項目
|
||||
container-not-found: 找不到容器
|
||||
events-stream:
|
||||
title: 意外錯誤
|
||||
|
||||
@@ -41,7 +41,7 @@ tooltip:
|
||||
error:
|
||||
page-not-found: 此页面不存在。
|
||||
invalid-auth: 用户名和密码无效。
|
||||
logs-skipped: 跳过 {total} 条记录
|
||||
logs-skipped: 显示 {total} 个隐藏条目
|
||||
container-not-found: 容器未找到。
|
||||
events-stream:
|
||||
title: 意外错误
|
||||
|
||||
Reference in New Issue
Block a user