Fix log view (#1874)

closes #1857
closes #1520
closes #1879 
- fixes unicode log lines as reported in matrix

---------

Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
Anbraten 2023-07-01 18:55:00 +02:00 committed by GitHub
parent 3714e9c218
commit 3d435a9cb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 56 additions and 15 deletions

View file

@ -186,8 +186,8 @@ func LogStreamSSE(c *gin.Context) {
} }
if step.State != model.StatusRunning { if step.State != model.StatusRunning {
log.Debug().Msg("stream not found.") log.Debug().Msg("step not running (anymore).")
logWriteStringErr(io.WriteString(rw, "event: error\ndata: stream not found\n\n")) logWriteStringErr(io.WriteString(rw, "event: error\ndata: step not running (anymore)\n\n"))
return return
} }

View file

@ -61,12 +61,12 @@ func (l *log) Open(_ context.Context, stepID int64) error {
return nil return nil
} }
func (l *log) Write(_ context.Context, stepID int64, logEntry *model.LogEntry) error { func (l *log) Write(ctx context.Context, stepID int64, logEntry *model.LogEntry) error {
l.Lock() l.Lock()
s, ok := l.streams[stepID] s, ok := l.streams[stepID]
l.Unlock() l.Unlock()
if !ok { if !ok {
return ErrNotFound return l.Open(ctx, stepID)
} }
s.Lock() s.Lock()
s.list = append(s.list, logEntry) s.list = append(s.list, logEntry)

View file

@ -34,13 +34,41 @@
<div <div
v-show="hasLogs && loadedLogs" v-show="hasLogs && loadedLogs"
ref="consoleElement" ref="consoleElement"
class="w-full max-w-full grid grid-cols-[min-content,1fr,min-content] auto-rows-min flex-grow p-2 gap-x-2 overflow-x-hidden overflow-y-auto" class="w-full max-w-full grid grid-cols-[min-content,1fr,min-content] auto-rows-min flex-grow p-2 overflow-x-hidden overflow-y-auto"
> >
<div v-for="line in log" :id="`L${line.index}`" :key="line.index" class="contents font-mono"> <div v-for="line in log" :key="line.index" class="contents font-mono">
<span class="text-gray-500 whitespace-nowrap select-none text-right">{{ line.index + 1 }}</span> <a
<!-- eslint-disable-next-line vue/no-v-html --> :id="`L${line.index}`"
<span class="align-top text-color whitespace-pre-wrap break-words" v-html="line.text" /> :href="`#L${line.index}`"
<span class="text-gray-500 whitespace-nowrap select-none text-right">{{ formatTime(line.time) }}</span> class="text-gray-500 whitespace-nowrap select-none text-right pl-1 pr-2"
:class="{
'bg-opacity-40 dark:bg-opacity-50 bg-red-600 dark:bg-red-800': line.type === 'error',
'bg-opacity-40 dark:bg-opacity-50 bg-yellow-600 dark:bg-yellow-800': line.type === 'warning',
'bg-opacity-20 bg-blue-600': $route.hash === `#L${line.index}`,
underline: $route.hash === `#L${line.index}`,
}"
>{{ line.index + 1 }}</a
>
<!-- eslint-disable vue/no-v-html -->
<span
class="align-top text-color whitespace-pre-wrap break-words"
:class="{
'bg-opacity-40 dark:bg-opacity-50 bg-red-600 dark:bg-red-800': line.type === 'error',
'bg-opacity-40 dark:bg-opacity-50 bg-yellow-600 dark:bg-yellow-800': line.type === 'warning',
'bg-opacity-20 bg-blue-600': $route.hash === `#L${line.index}`,
}"
v-html="line.text"
/>
<!-- eslint-enable vue/no-v-html -->
<span
class="text-gray-500 whitespace-nowrap select-none text-right pr-1"
:class="{
'bg-opacity-40 dark:bg-opacity-50 bg-red-600 dark:bg-red-800': line.type === 'error',
'bg-opacity-40 dark:bg-opacity-50 bg-yellow-600 dark:bg-yellow-800': line.type === 'warning',
'bg-opacity-20 bg-blue-600': $route.hash === `#L${line.index}`,
}"
>{{ formatTime(line.time) }}</span
>
</div> </div>
</div> </div>
@ -84,6 +112,7 @@ type LogLine = {
index: number; index: number;
text: string; text: string;
time?: number; time?: number;
type: 'error' | 'warning' | null;
}; };
const props = defineProps<{ const props = defineProps<{
@ -128,14 +157,26 @@ function formatTime(time?: number): string {
return time === undefined ? '' : `${time}s`; return time === undefined ? '' : `${time}s`;
} }
function writeLog(line: LogLine) { function writeLog(line: Partial<LogLine>) {
logBuffer.value.push({ logBuffer.value.push({
index: line.index ?? 0, index: line.index ?? 0,
text: ansiUp.value.ansi_to_html(line.text), text: ansiUp.value.ansi_to_html(line.text ?? ''),
time: line.time ?? 0, time: line.time ?? 0,
type: null, // TODO: implement way to detect errors and warnings
}); });
} }
// SOURCE: https://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
function b64DecodeUnicode(str: string) {
return decodeURIComponent(
window
.atob(str)
.split('')
.map((c) => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`)
.join(''),
);
}
function scrollDown() { function scrollDown() {
nextTick(() => { nextTick(() => {
if (!consoleElement.value) { if (!consoleElement.value) {
@ -198,7 +239,7 @@ async function download() {
downloadInProgress.value = false; downloadInProgress.value = false;
} }
const fileURL = window.URL.createObjectURL( const fileURL = window.URL.createObjectURL(
new Blob([logs.map((line) => atob(line.data)).join('')], { new Blob([logs.map((line) => b64DecodeUnicode(line.data)).join('')], {
type: 'text/plain', type: 'text/plain',
}), }),
); );
@ -240,13 +281,13 @@ async function loadLogs() {
if (isStepFinished(step.value)) { if (isStepFinished(step.value)) {
const logs = await apiClient.getLogs(repo.value.id, pipeline.value.number, step.value.id); const logs = await apiClient.getLogs(repo.value.id, pipeline.value.number, step.value.id);
logs?.forEach((line) => writeLog({ index: line.line, text: atob(line.data), time: line.time })); logs?.forEach((line) => writeLog({ index: line.line, text: b64DecodeUnicode(line.data), time: line.time }));
flushLogs(false); flushLogs(false);
} }
if (isStepRunning(step.value)) { if (isStepRunning(step.value)) {
stream.value = apiClient.streamLogs(repo.value.id, pipeline.value.number, step.value.id, (line) => { stream.value = apiClient.streamLogs(repo.value.id, pipeline.value.number, step.value.id, (line) => {
writeLog({ index: line.line, text: atob(line.data), time: line.time }); writeLog({ index: line.line, text: b64DecodeUnicode(line.data), time: line.time });
flushLogs(true); flushLogs(true);
}); });
} }