Skip to content

Commit 8057bff

Browse files
authored
feat: supports copying logs to clipboard (#4451)
1 parent f30e8e7 commit 8057bff

File tree

22 files changed

+144
-13
lines changed

22 files changed

+144
-13
lines changed

assets/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ declare module 'vue' {
102102
'Mdi:cloud': typeof import('~icons/mdi/cloud')['default']
103103
'Mdi:cloudOutline': typeof import('~icons/mdi/cloud-outline')['default']
104104
'Mdi:cog': typeof import('~icons/mdi/cog')['default']
105+
'Mdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
105106
'Mdi:docker': typeof import('~icons/mdi/docker')['default']
106107
'Mdi:gauge': typeof import('~icons/mdi/gauge')['default']
107108
'Mdi:github': typeof import('~icons/mdi/github')['default']

assets/components/ContainerViewer/ContainerActionsToolbar.vue

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@
110110
{{ isFiltered ? $t("toolbar.download-filtered") : $t("toolbar.download") }}
111111
</a>
112112
</li>
113+
<li v-if="isSupported">
114+
<a @click="copyLogs()">
115+
<mdi:content-copy />
116+
{{ isFiltered ? $t("toolbar.copy-filtered-logs") : $t("toolbar.copy-logs") }}
117+
</a>
118+
</li>
113119
<li>
114120
<a @click="copyPermalink()">
115121
<material-symbols:link />
@@ -189,7 +195,7 @@ const { actionStates, start, stop, restart } = useContainerActions(toRef(() => c
189195
const router = useRouter();
190196
const { copy, copied, isSupported } = useClipboard();
191197
const { t } = useI18n();
192-
const { showToast } = useToast();
198+
const { showToast, removeToast } = useToast();
193199
194200
async function copyPermalink() {
195201
const url = router.resolve({
@@ -225,6 +231,68 @@ async function copyPermalink() {
225231
}
226232
}
227233
234+
async function copyLogs() {
235+
const params = new URLSearchParams();
236+
if (streamConfig.value.stdout) params.append("stdout", "1");
237+
if (streamConfig.value.stderr) params.append("stderr", "1");
238+
params.append("everything", "1");
239+
240+
const { debouncedSearchFilter } = useSearchFilter();
241+
if (debouncedSearchFilter.value) {
242+
params.append("filter", debouncedSearchFilter.value);
243+
}
244+
245+
const selectedLevels = Array.from(levels.value);
246+
if (selectedLevels.length > 0 && selectedLevels.length < allLevels.length) {
247+
selectedLevels.forEach((level) => params.append("levels", level));
248+
}
249+
250+
const url = withBase(`/api/hosts/${container.host}/containers/${container.id}/logs?${params.toString()}`);
251+
252+
const toastId = "copy-logs";
253+
showToast(
254+
{
255+
id: toastId,
256+
title: t("toolbar.copying-logs"),
257+
message: "",
258+
type: "info",
259+
},
260+
{ once: true },
261+
);
262+
263+
const blobPromise = fetch(url, { headers: { Accept: "text/plain" } })
264+
.then((response) => {
265+
if (!response.ok) throw new Error(response.statusText);
266+
return response.blob();
267+
})
268+
.then((blob) => {
269+
removeToast(toastId);
270+
showToast(
271+
{
272+
title: t("toasts.copied.title"),
273+
message: t("toasts.copied.message"),
274+
type: "info",
275+
},
276+
{ expire: 2000 },
277+
);
278+
return blob;
279+
})
280+
.catch((err) => {
281+
removeToast(toastId);
282+
showToast(
283+
{
284+
title: "Error",
285+
message: err.message,
286+
type: "error",
287+
},
288+
{ expire: 5000 },
289+
);
290+
throw err;
291+
});
292+
293+
await navigator.clipboard.write([new ClipboardItem({ "text/plain": blobPromise })]);
294+
}
295+
228296
onKeyStroke("f", (e) => {
229297
if (hasComplexLogs.value) {
230298
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {

internal/web/__snapshots__/web.snapshot

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ Content-Type: text/html
8484
{"t":"single","m":"INFO Testing stdout logs...","rm":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout","c":"123456"}
8585
{"t":"single","m":"INFO Testing stderr logs...","rm":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr","c":"123456"}
8686

87-
8887
/* snapshot: Test_handler_between_dates_with_everything_complex */
8988
{"t":"complex","m":{"msg":"a complex log message"},"rm":"{\"msg\":\"a complex log message\"}","ts":1589396197772,"id":62280847,"l":"unknown","s":"stdout","c":"123456"}
9089

@@ -93,7 +92,6 @@ Content-Type: text/html
9392
{"t":"single","m":"INFO Testing stdout logs...","rm":"INFO Testing stdout logs...","ts":1589396137772,"id":466600245,"l":"info","s":"stdout","c":"123456"}
9493
{"t":"single","m":"INFO Testing stderr logs...","rm":"INFO Testing stderr logs...","ts":1589396197772,"id":1101501603,"l":"info","s":"stderr","c":"123456"}
9594

96-
9795
/* snapshot: Test_handler_download_logs */
9896
INFO Testing logs...
9997

@@ -173,9 +171,6 @@ data: {"t":"single","m":"INFO Testing logs...\n","ts":0,"id":3835490584,"l":"inf
173171
event: container-event
174172
data: {"name":"container-stopped","host":"localhost","actorId":"123456","time":"<removed>"}
175173

176-
177-
178-
179174
/* snapshot: Test_handler_streamLogs_happy_container_stopped */
180175
:ping
181176

internal/web/logs.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"encoding/json"
77
"errors"
8+
"fmt"
89
"math"
910
"regexp"
1011
"sort"
@@ -59,7 +60,12 @@ func (h *handler) resolveLabels(r *http.Request) container.ContainerLabels {
5960
}
6061

6162
func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request) {
62-
w.Header().Set("Content-Type", "application/x-jsonl; charset=UTF-8")
63+
plainText := strings.Contains(r.Header.Get("Accept"), "text/plain")
64+
if plainText {
65+
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
66+
} else {
67+
w.Header().Set("Content-Type", "application/x-jsonl; charset=UTF-8")
68+
}
6369

6470
from, _ := time.Parse(time.RFC3339Nano, r.URL.Query().Get("from"))
6571
to, _ := time.Parse(time.RFC3339Nano, r.URL.Query().Get("to"))
@@ -150,13 +156,14 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
150156
startId = uint32(num)
151157
}
152158

153-
encoder := json.NewEncoder(w)
159+
var writer io.Writer = w
154160
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
155161
w.Header().Set("Content-Encoding", "gzip")
156-
writer := gzip.NewWriter(w)
157-
defer writer.Close()
158-
encoder = json.NewEncoder(writer)
162+
gzWriter := gzip.NewWriter(w)
163+
defer gzWriter.Close()
164+
writer = gzWriter
159165
}
166+
encoder := json.NewEncoder(writer)
160167

161168
startIdFound := startId == 0
162169
for {
@@ -178,7 +185,17 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
178185
if _, ok := event.Message.(string); onlyComplex && ok {
179186
continue
180187
}
181-
if err := encoder.Encode(event); err != nil {
188+
if regex != nil && !support_web.Search(regex, event) {
189+
continue
190+
}
191+
if len(levels) > 0 {
192+
if _, ok := levels[event.Level]; !ok {
193+
continue
194+
}
195+
}
196+
if plainText {
197+
fmt.Fprintf(writer, "%s\n", event.RawMessage)
198+
} else if err := encoder.Encode(event); err != nil {
182199
log.Error().Err(err).Msg("error encoding log event")
183200
}
184201
continue

internal/web/logs_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,6 @@ func Test_handler_between_dates_with_everything_complex(t *testing.T) {
330330
q.Add("stdout", "true")
331331
q.Add("stderr", "true")
332332
q.Add("everything", "true")
333-
q.Add("levels", "info")
334333

335334
req.URL.RawQuery = q.Encode()
336335

locales/da.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ toolbar:
1313
shell: Shell
1414
attach: Tilknyt
1515
copy-permalink: Kopiér permanent link
16+
copy-logs: Kopiér logs
17+
copy-filtered-logs: Kopiér filtrerede logs
18+
copying-logs: Kopierer logs...
1619
action:
1720
copy-log: Kopier log
1821
copy-link: Kopier permalink

locales/de.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ toolbar:
1313
shell: Shell
1414
attach: Anhängen
1515
copy-permalink: Permanentlink kopieren
16+
copy-logs: Logs kopieren
17+
copy-filtered-logs: Gefilterte Logs kopieren
18+
copying-logs: Logs werden kopiert...
1619
action:
1720
copy-log: Log kopieren
1821
copy-link: Permalink kopieren

locales/en.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ toolbar:
1313
shell: Shell
1414
attach: Attach
1515
copy-permalink: Copy permanent link
16+
copy-logs: Copy logs
17+
copy-filtered-logs: Copy Filtered Logs
18+
copying-logs: Copying logs...
1619
action:
1720
copy-log: Copy log
1821
copy-link: Copy permalink

locales/es.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ toolbar:
1313
shell: Shell
1414
attach: Conectar
1515
copy-permalink: Copiar enlace permanente
16+
copy-logs: Copiar registros
17+
copy-filtered-logs: Copiar registros filtrados
18+
copying-logs: Copiando registros...
1619
action:
1720
copy-log: Copiar registro
1821
copy-link: Copiar enlace permanente

locales/fr.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ toolbar:
1313
shell: Shell
1414
attach: Attacher
1515
copy-permalink: Copier le lien permanent
16+
copy-logs: Copier les journaux
17+
copy-filtered-logs: Copier les journaux filtrés
18+
copying-logs: Copie des journaux...
1619
action:
1720
copy-log: Copier le journal
1821
copy-link: Copier le permalien

0 commit comments

Comments
 (0)