Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4dfc0e3e59 | |||
| 4ebd0c3624 | |||
| 10baad1d4a | |||
| cceab9bd29 | |||
| bea1876c7c | |||
| ae7eee9bca | |||
| d98b84e24a | |||
| 18c87256cc | |||
| e96631930e | |||
| 76b1229341 | |||
| 440f9394d6 | |||
| ec792318e6 | |||
| 16db90b660 | |||
| eb22752b9a | |||
| 642ab45ef7 | |||
| 12fadb9348 | |||
| 1cf178e37d | |||
| 061cd0c445 | |||
| e8f796a7d6 | |||
| 78f94f1f16 | |||
| cc980361fd | |||
| 736ad8ed5b | |||
| d0dbd2051e |
@@ -35,7 +35,7 @@ pre_cmd = []
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = true
|
||||
stop_on_error = false
|
||||
stop_on_error = true
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
|
||||
@@ -43,7 +43,7 @@ int:
|
||||
docker compose up --build --force-recreate --exit-code-from playwright
|
||||
|
||||
shared_key.pem:
|
||||
@openssl genpkey -algorithm RSA -out shared_key.pem -pkeyopt rsa_keygen_bits:2048
|
||||
@openssl genpkey -algorithm Ed25519 -out shared_key.pem
|
||||
|
||||
shared_cert.pem: shared_key.pem
|
||||
@openssl req -new -key shared_key.pem -out shared_request.csr -subj "/C=US/ST=California/L=San Francisco/O=Dozzle"
|
||||
|
||||
@@ -373,7 +373,7 @@ declare global {
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
// @ts-ignore
|
||||
export type { DrawerWidth } from './composable/drawer'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="dropdown dropdown-open w-full">
|
||||
<div class="dropdown dropdown-open w-full shadow-md">
|
||||
<div class="input input-xl input-primary flex w-full items-center">
|
||||
<mdi:magnify class="flex size-8" />
|
||||
<input
|
||||
|
||||
@@ -11,18 +11,26 @@
|
||||
<textarea
|
||||
v-model="query"
|
||||
class="textarea textarea-primary w-full font-mono text-lg"
|
||||
:class="{ 'textarea-error': error }"
|
||||
:class="{ 'textarea-error!': error }"
|
||||
:disabled="state === 'downloading'"
|
||||
></textarea>
|
||||
<div class="label max-h-48 overflow-y-auto pr-2">
|
||||
<span class="label-text-alt text-error" v-if="error">{{ error }}</span>
|
||||
<span class="label-text-alt" v-else>
|
||||
Total {{ results.numRows }} records
|
||||
<template v-if="results.numRows > pageLimit"> . Showing first {{ page.numRows }}. </template>
|
||||
<div class="mt-2">
|
||||
<span class="text-error" v-if="error">{{ error }}</span>
|
||||
<span v-else-if="state === 'initializing'">{{ $t("analytics.creating_table") }}</span>
|
||||
<span v-else-if="state === 'downloading'">{{
|
||||
$t("analytics.downloading", { size: formatBytes(bytes, { decimals: 1 }) })
|
||||
}}</span>
|
||||
<span v-else-if="evaluating">{{ $t("analytics.evaluating_query") }}</span>
|
||||
<span v-else>
|
||||
{{ $t("analytics.total_records", { count: results.numRows.toLocaleString() }) }}
|
||||
<template v-if="results.numRows > pageLimit">{{
|
||||
$t("analytics.showing_first", { count: page.numRows.toLocaleString() })
|
||||
}}</template>
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</section>
|
||||
<SQLTable :table="page" :loading="evaluating || !isReady" />
|
||||
<SQLTable :table="page" :loading="evaluating || state !== 'ready'" />
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -30,12 +38,15 @@
|
||||
<script setup lang="ts">
|
||||
import { Container } from "@/models/Container";
|
||||
import { type Table } from "@apache-arrow/esnext-esm";
|
||||
|
||||
const { container } = defineProps<{ container: Container }>();
|
||||
const query = ref("SELECT * FROM logs LIMIT 100");
|
||||
const error = ref<string | null>(null);
|
||||
const debouncedQuery = debouncedRef(query, 500);
|
||||
const evaluating = ref(false);
|
||||
const pageLimit = 1000;
|
||||
const state = ref<"downloading" | "ready" | "initializing">("downloading");
|
||||
const bytes = ref(0);
|
||||
|
||||
const url = withBase(
|
||||
`/api/hosts/${container.host}/containers/${container.id}/logs?stdout=1&stderr=1&everything&jsonOnly`,
|
||||
@@ -51,27 +62,49 @@ if (!response.ok) {
|
||||
const { db, conn } = await useDuckDB();
|
||||
const empty = await conn.query<Record<string, any>>(`SELECT 1 LIMIT 0`);
|
||||
|
||||
const { isReady } = useAsyncState(
|
||||
async () => {
|
||||
await db.registerFileBuffer("logs.json", new Uint8Array(await response.arrayBuffer()));
|
||||
onMounted(async () => {
|
||||
try {
|
||||
state.value = "downloading";
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) throw new Error("No reader available from stream");
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
bytes.value = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
bytes.value += value.length;
|
||||
}
|
||||
|
||||
const arrayBuffer = new Uint8Array(bytes.value);
|
||||
let position = 0;
|
||||
for (const chunk of chunks) {
|
||||
arrayBuffer.set(chunk, position);
|
||||
position += chunk.length;
|
||||
}
|
||||
|
||||
await db.registerFileBuffer("logs.json", arrayBuffer);
|
||||
|
||||
state.value = "initializing";
|
||||
await conn.query(
|
||||
`CREATE TABLE logs AS SELECT unnest(m) FROM read_json('logs.json', ignore_errors = true, format = 'newline_delimited')`,
|
||||
);
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
onError: (e) => {
|
||||
console.error(e);
|
||||
if (e instanceof Error) {
|
||||
error.value = e.message;
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
state.value = "ready";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof Error) {
|
||||
error.value = e.message;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const results = computedAsync(
|
||||
async () => {
|
||||
if (isReady.value) {
|
||||
if (state.value === "ready") {
|
||||
return await conn.query<Record<string, any>>(debouncedQuery.value);
|
||||
} else {
|
||||
return empty;
|
||||
@@ -89,7 +122,10 @@ const results = computedAsync(
|
||||
},
|
||||
);
|
||||
|
||||
whenever(evaluating, () => (error.value = null));
|
||||
whenever(evaluating, () => {
|
||||
error.value = null;
|
||||
state.value = "ready";
|
||||
});
|
||||
const page = computed(() =>
|
||||
results.value.numRows > pageLimit ? results.value.slice(0, pageLimit) : results.value,
|
||||
) as unknown as ComputedRef<Table<Record<string, any>>>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="toast toast-end whitespace-normal max-md:m-0 max-md:w-full">
|
||||
<div class="toast toast-end whitespace-normal max-md:end-auto max-md:m-0 max-md:max-w-full">
|
||||
<div
|
||||
class="alert max-w-xl max-md:rounded-none"
|
||||
class="alert max-w-xl shadow-sm max-md:rounded-none"
|
||||
v-for="{ toast, options: { timed } } in toasts"
|
||||
:key="toast.id"
|
||||
:class="{
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
<label
|
||||
class="btn btn-circle swap bg-base-content/10 swap-rotate border-base-content/20 hover:border-primary fixed bottom-4 -left-12 w-16 transition-all hover:-left-4"
|
||||
class="btn btn-circle swap bg-base-content/10 swap-rotate border-base-content/20 hover:border-primary fixed bottom-4 -left-12 w-16 shadow-sm transition-all hover:-left-4"
|
||||
:class="{ '-left-6!': collapseNav }"
|
||||
v-if="!isMobile && !forceMenuHidden"
|
||||
>
|
||||
@@ -34,7 +34,7 @@
|
||||
<mdi:chevron-left class="swap-off" />
|
||||
</label>
|
||||
</div>
|
||||
<dialog ref="modal" class="modal items-start bg-white/15 backdrop-blur-xs transition-none!" @close="open = false">
|
||||
<dialog ref="modal" class="modal bg-base-300/50! items-start backdrop-blur-md transition-none!" @close="open = false">
|
||||
<div class="modal-box max-w-2xl bg-transparent pt-20 shadow-none">
|
||||
<FuzzySearchModal @close="open = false" v-if="open" />
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,7 @@ services:
|
||||
|
||||
playwright:
|
||||
container_name: playwright
|
||||
image: mcr.microsoft.com/playwright:v1.51.1-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- .:/app
|
||||
|
||||
@@ -180,7 +180,7 @@ This will mount the `cert.pem` and `key.pem` files to the agent. The agent will
|
||||
To generate certificates, you can use the following command:
|
||||
|
||||
```sh
|
||||
$ openssl genpkey -algorithm RSA -out key.pem -pkeyopt rsa_keygen_bits:2048
|
||||
$ openssl genpkey -algorithm Ed25519 -out key.pem
|
||||
$ openssl req -new -key key.pem -out request.csr -subj "/C=US/ST=California/L=San Francisco/O=My Company"
|
||||
$ openssl x509 -req -in request.csr -signkey key.pem -out cert.pem -days 365
|
||||
```
|
||||
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
@@ -4,7 +4,7 @@ require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/alexflint/go-arg v1.5.1
|
||||
github.com/beme/abide v0.0.0-20190723115211-635a09831760
|
||||
github.com/docker/docker v28.0.4+incompatible
|
||||
github.com/docker/docker v28.1.1+incompatible
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
@@ -24,6 +24,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.3
|
||||
github.com/andybalholm/brotli v1.1.1
|
||||
github.com/go-chi/chi/v5 v5.2.1
|
||||
github.com/go-chi/jwtauth/v5 v5.3.3
|
||||
github.com/go-faker/faker/v4 v4.6.0
|
||||
@@ -32,7 +33,7 @@ require (
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/samber/lo v1.49.1
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8
|
||||
github.com/yuin/goldmark v1.7.8
|
||||
github.com/yuin/goldmark v1.7.10
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/sync v0.13.0
|
||||
google.golang.org/grpc v1.71.1
|
||||
@@ -80,6 +81,7 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
|
||||
@@ -2,14 +2,14 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7O
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
|
||||
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y=
|
||||
github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
|
||||
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
@@ -34,8 +34,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok=
|
||||
github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I=
|
||||
github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -130,6 +130,10 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk=
|
||||
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -191,11 +195,13 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI=
|
||||
github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
|
||||
@@ -246,8 +252,6 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
|
||||
|
||||
@@ -2,26 +2,27 @@ package support_web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type SSEWriter struct {
|
||||
w io.Writer
|
||||
f http.Flusher
|
||||
w http.ResponseWriter
|
||||
}
|
||||
|
||||
type HasId interface {
|
||||
MessageId() int64
|
||||
}
|
||||
|
||||
func NewSSEWriter(ctx context.Context, w http.ResponseWriter) (*SSEWriter, error) {
|
||||
f, ok := w.(http.Flusher)
|
||||
|
||||
if !ok {
|
||||
func NewSSEWriter(ctx context.Context, w http.ResponseWriter, r *http.Request) (*SSEWriter, error) {
|
||||
if _, ok := w.(http.Flusher); !ok {
|
||||
return nil, http.ErrNotSupported
|
||||
}
|
||||
|
||||
@@ -31,9 +32,15 @@ func NewSSEWriter(ctx context.Context, w http.ResponseWriter) (*SSEWriter, error
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
var writer io.Writer = w
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
writer = gzip.NewWriter(w)
|
||||
}
|
||||
|
||||
sse := &SSEWriter{
|
||||
f: f,
|
||||
w: w,
|
||||
w: writer,
|
||||
f: w.(http.Flusher),
|
||||
}
|
||||
|
||||
return sse, nil
|
||||
@@ -50,6 +57,13 @@ func (s *SSEWriter) Write(data []byte) (int, error) {
|
||||
return written, err
|
||||
}
|
||||
|
||||
if f, ok := s.w.(*gzip.Writer); ok {
|
||||
err := f.Flush()
|
||||
if err != nil {
|
||||
return written, err
|
||||
}
|
||||
}
|
||||
|
||||
s.f.Flush()
|
||||
|
||||
return written, nil
|
||||
@@ -60,6 +74,12 @@ func (s *SSEWriter) Ping() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SSEWriter) Close() {
|
||||
if closer, ok := s.w.(io.Closer); ok && s.w != nil {
|
||||
closer.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSEWriter) Message(data any) error {
|
||||
encoded, err := json.Marshal(data)
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
)
|
||||
|
||||
type brotliWriter struct {
|
||||
io.Writer
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func (w brotliWriter) Write(b []byte) (int, error) {
|
||||
return w.Writer.Write(b)
|
||||
}
|
||||
|
||||
func Brotli(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if client supports Brotli
|
||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "br") {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Encoding", "br")
|
||||
w.Header().Add("Vary", "Accept-Encoding")
|
||||
|
||||
brWriter := brotli.NewWriter(w)
|
||||
defer brWriter.Close()
|
||||
|
||||
bw := brotliWriter{Writer: brWriter, ResponseWriter: w}
|
||||
next.ServeHTTP(bw, r)
|
||||
})
|
||||
}
|
||||
@@ -13,7 +13,9 @@ import (
|
||||
)
|
||||
|
||||
func (h *handler) streamEvents(w http.ResponseWriter, r *http.Request) {
|
||||
sseWriter, err := support_web.NewSSEWriter(r.Context(), w)
|
||||
sseWriter, err := support_web.NewSSEWriter(r.Context(), w, r)
|
||||
defer sseWriter.Close()
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error creating sse writer")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
@@ -3,10 +3,7 @@ package web
|
||||
import (
|
||||
"html/template"
|
||||
"io"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
@@ -24,14 +21,7 @@ func (h *handler) index(w http.ResponseWriter, req *http.Request) {
|
||||
_, err := h.content.Open(path)
|
||||
if err == nil && req.URL.Path != "" && req.URL.Path != "/" {
|
||||
w.Header().Set("Cache-Control", "max-age=31536000, immutable")
|
||||
// if brotli is enabled, then just send over the compressed file
|
||||
if file, err := h.content.Open(path + ".br"); strings.Contains(req.Header.Get("Accept-Encoding"), "br") && err == nil {
|
||||
w.Header().Set("Content-Encoding", "br")
|
||||
w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(path)))
|
||||
io.Copy(w, file)
|
||||
} else {
|
||||
fileServer.ServeHTTP(w, req)
|
||||
}
|
||||
fileServer.ServeHTTP(w, req)
|
||||
} else {
|
||||
h.executeTemplate(w, req)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"errors"
|
||||
"regexp"
|
||||
@@ -112,6 +113,12 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
writer := gzip.NewWriter(w)
|
||||
defer writer.Close()
|
||||
encoder = json.NewEncoder(writer)
|
||||
}
|
||||
|
||||
for {
|
||||
if buffer.Len() > minimum {
|
||||
@@ -238,7 +245,8 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
sseWriter, err := support_web.NewSSEWriter(r.Context(), w)
|
||||
sseWriter, err := support_web.NewSSEWriter(r.Context(), w, r)
|
||||
defer sseWriter.Close()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error creating sse writer")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
@@ -140,7 +140,7 @@ func createRouter(h *handler) *chi.Mux {
|
||||
r.Get("/healthcheck", h.healthcheck)
|
||||
|
||||
defaultHandler := http.StripPrefix(strings.Replace(base+"/", "//", "/", 1), http.HandlerFunc(h.index))
|
||||
r.Get("/*", func(w http.ResponseWriter, req *http.Request) {
|
||||
r.With(Brotli).Get("/*", func(w http.ResponseWriter, req *http.Request) {
|
||||
defaultHandler.ServeHTTP(w, req)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -114,3 +114,9 @@ toasts:
|
||||
copied:
|
||||
title: Kopieret
|
||||
message: Log kopieret til clipboard
|
||||
analytics:
|
||||
creating_table: Opretter midlertidig tabel...
|
||||
downloading: Henter containerlogfiler... ({size})
|
||||
evaluating_query: Evaluerer forespørgsel...
|
||||
total_records: I alt {count} poster.
|
||||
showing_first: Viser de første {count}.
|
||||
|
||||
@@ -114,3 +114,9 @@ toasts:
|
||||
copied:
|
||||
title: Kopiert
|
||||
message: Log in Zwischenablage kopiert
|
||||
analytics:
|
||||
creating_table: Temporäre Tabelle wird erstellt...
|
||||
downloading: Container-Logs werden abgerufen... ({size})
|
||||
evaluating_query: Abfrage wird ausgewertet...
|
||||
total_records: Insgesamt {count} Datensätze.
|
||||
showing_first: Zeige die ersten {count}.
|
||||
|
||||
@@ -119,3 +119,9 @@ toasts:
|
||||
copied:
|
||||
title: Copied
|
||||
message: Log copied to clipboard
|
||||
analytics:
|
||||
creating_table: Creating temporary table...
|
||||
downloading: Fetching container logs... ({size})
|
||||
evaluating_query: Evaluating query...
|
||||
total_records: Total {count} records.
|
||||
showing_first: Showing first {count}.
|
||||
|
||||
@@ -114,3 +114,9 @@ toasts:
|
||||
copied:
|
||||
title: Copiado
|
||||
message: Registro copiado al portapapeles
|
||||
analytics:
|
||||
creating_table: Creando tabla temporal...
|
||||
downloading: Obteniendo registros de contenedores... ({size})
|
||||
evaluating_query: Evaluando consulta...
|
||||
total_records: Total {count} registros.
|
||||
showing_first: Mostrando los primeros {count}.
|
||||
|
||||
@@ -114,3 +114,9 @@ toasts:
|
||||
copied:
|
||||
title: Copié
|
||||
message: Journal copié dans le presse-papiers
|
||||
analytics:
|
||||
creating_table: Création d'une table temporaire...
|
||||
downloading: Récupération des journaux des conteneurs... ({size})
|
||||
evaluating_query: Évaluation de la requête...
|
||||
total_records: Total de {count} enregistrements.
|
||||
showing_first: Affichage des {count} premiers.
|
||||
|
||||
@@ -114,3 +114,9 @@ toasts:
|
||||
copied:
|
||||
title: Copiato
|
||||
message: Log copiato nella clipboard
|
||||
analytics:
|
||||
creating_table: Creazione tabella temporanea...
|
||||
downloading: Recupero dei log dei container... ({size})
|
||||
evaluating_query: Valutazione della query...
|
||||
total_records: Totale {count} record.
|
||||
showing_first: Mostrati i primi {count}.
|
||||
|
||||
@@ -114,3 +114,9 @@ toasts:
|
||||
copied:
|
||||
title: Skopiowany
|
||||
message: Log skopiowany do schowka
|
||||
analytics:
|
||||
creating_table: Tworzenie tymczasowej tabeli...
|
||||
downloading: Pobieranie logów kontenerów... ({size})
|
||||
evaluating_query: Przetwarzanie zapytania...
|
||||
total_records: Razem {count} rekordów.
|
||||
showing_first: Pokazywanie pierwszych {count}.
|
||||
|
||||
@@ -109,3 +109,10 @@ settings:
|
||||
start-line: Esta é uma mensagem de erro multi-linha
|
||||
middle-line: com uma segunda linha
|
||||
end-line: e finalmente uma terceira linha.
|
||||
|
||||
analytics:
|
||||
creating_table: A criar tabela temporária...
|
||||
downloading: A obter registos dos contentores... ({size})
|
||||
evaluating_query: A avaliar consulta...
|
||||
total_records: Total de {count} registos.
|
||||
showing_first: A mostrar os primeiros {count}.
|
||||
|
||||
@@ -114,3 +114,9 @@ toasts:
|
||||
copied:
|
||||
title: Copiado
|
||||
message: Log copiado para a área de transferência
|
||||
analytics:
|
||||
creating_table: Criando tabela temporária...
|
||||
downloading: Buscando logs dos containers... ({size})
|
||||
evaluating_query: Avaliando consulta...
|
||||
total_records: Total de {count} registros.
|
||||
showing_first: Mostrando os primeiros {count}.
|
||||
|
||||
@@ -114,3 +114,9 @@ toasts:
|
||||
copied:
|
||||
title: Copied
|
||||
message: Log copied to clipboard
|
||||
analytics:
|
||||
creating_table: Создание временной таблицы...
|
||||
downloading: Получение логов контейнеров... ({size})
|
||||
evaluating_query: Выполнение запроса...
|
||||
total_records: Всего {count} записей.
|
||||
showing_first: Показаны первые {count}.
|
||||
|
||||
@@ -107,3 +107,9 @@ toasts:
|
||||
copied:
|
||||
title: Kopyalandı
|
||||
message: Günlük panoya kopyalandı
|
||||
analytics:
|
||||
creating_table: Geçici tablo oluşturuluyor...
|
||||
downloading: Konteyner günlükleri alınıyor... ({size})
|
||||
evaluating_query: Sorgu değerlendiriliyor...
|
||||
total_records: Toplam {count} kayıt.
|
||||
showing_first: İlk {count} gösteriliyor.
|
||||
|
||||
@@ -116,4 +116,10 @@ log_actions:
|
||||
toasts:
|
||||
copied:
|
||||
title: 已複製
|
||||
message: 日誌已複製到剪貼簿
|
||||
message: 日誌已複製到剪貼簿
|
||||
analytics:
|
||||
creating_table: 正在建立臨時資料表...
|
||||
downloading: 正在取得容器日誌... ({size})
|
||||
evaluating_query: 正在評估查詢...
|
||||
total_records: 共計 {count} 筆記錄。
|
||||
showing_first: 顯示前 {count} 筆。
|
||||
|
||||
@@ -114,3 +114,9 @@ toasts:
|
||||
copied:
|
||||
title: 已复制
|
||||
message: 日志已复制到剪贴板
|
||||
analytics:
|
||||
creating_table: 正在创建临时表...
|
||||
downloading: 正在获取容器日志... ({size})
|
||||
evaluating_query: 正在评估查询...
|
||||
total_records: 总共 {count} 条记录。
|
||||
showing_first: 显示前 {count} 条。
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dozzle",
|
||||
"version": "8.12.8",
|
||||
"version": "8.12.10",
|
||||
"description": "Realtime log viewer for docker containers.",
|
||||
"homepage": "https://github.com/amir20/dozzle#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/amir20/dozzle/issues"
|
||||
},
|
||||
"packageManager": "pnpm@10.8.0",
|
||||
"packageManager": "pnpm@10.8.1",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -33,15 +33,15 @@
|
||||
"@iconify-json/carbon": "^1.2.8",
|
||||
"@iconify-json/cil": "^1.2.2",
|
||||
"@iconify-json/ic": "^1.2.2",
|
||||
"@iconify-json/material-symbols": "^1.2.19",
|
||||
"@iconify-json/material-symbols": "^1.2.20",
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@iconify-json/mdi-light": "^1.2.2",
|
||||
"@iconify-json/octicon": "^1.2.5",
|
||||
"@iconify-json/ph": "^1.2.2",
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.5",
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.7",
|
||||
"@oddbird/css-anchor-positioning": "^0.5.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "4.1.3",
|
||||
"@tailwindcss/vite": "4.1.4",
|
||||
"@vueuse/components": "^13.1.0",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"@vueuse/integrations": "^13.1.0",
|
||||
@@ -55,7 +55,7 @@
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-shape": "^3.2.0",
|
||||
"d3-transition": "^3.0.1",
|
||||
"daisyui": "5.0.19",
|
||||
"daisyui": "5.0.27",
|
||||
"date-fns": "^4.1.0",
|
||||
"entities": "^6.0.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
@@ -64,14 +64,13 @@
|
||||
"sortablejs": "^1.15.6",
|
||||
"splitpanes": "^4.0.3",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tailwindcss": "4.1.3",
|
||||
"tailwindcss": "4.1.4",
|
||||
"unplugin-auto-import": "^19.1.2",
|
||||
"unplugin-icons": "^22.1.0",
|
||||
"unplugin-vue-components": "^28.4.1",
|
||||
"unplugin-vue-components": "^28.5.0",
|
||||
"unplugin-vue-macros": "^2.14.5",
|
||||
"unplugin-vue-router": "^0.12.0",
|
||||
"vite": "6.2.6",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vite": "6.3.2",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vitepress": "1.6.3",
|
||||
@@ -81,10 +80,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apache-arrow/esnext-esm": "^19.0.1",
|
||||
"@iconify-json/material-symbols-light": "^1.2.19",
|
||||
"@iconify-json/material-symbols-light": "^1.2.20",
|
||||
"@iconify-json/ri": "^1.2.5",
|
||||
"@pinia/testing": "^1.0.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@types/d3-array": "^3.2.1",
|
||||
"@types/d3-ease": "^3.0.2",
|
||||
"@types/d3-scale": "^4.0.9",
|
||||
|
||||
@@ -9,7 +9,6 @@ import IconsResolver from "unplugin-icons/resolver";
|
||||
import VueRouter from "unplugin-vue-router/vite";
|
||||
import Layouts from "vite-plugin-vue-layouts";
|
||||
import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";
|
||||
import { compression } from "vite-plugin-compression2";
|
||||
import { VueRouterAutoImports } from "unplugin-vue-router";
|
||||
import svgLoader from "vite-svg-loader";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
@@ -71,7 +70,6 @@ export default defineConfig(() => ({
|
||||
strictMessage: false,
|
||||
include: [path.resolve(__dirname, "locales/**")],
|
||||
}),
|
||||
compression({ algorithm: "brotliCompress", exclude: [/\.(html)$/] }),
|
||||
svgLoader({}),
|
||||
tailwindcss(),
|
||||
],
|
||||
|
||||