From f9d0d4018302a97a204dbf41afa4195c03a7ed6f Mon Sep 17 00:00:00 2001 From: biersoeckli Date: Thu, 28 Nov 2024 14:29:58 +0000 Subject: [PATCH] added deployment logs persisted on filesystem --- src/app/api/build-logs/route.ts | 50 ++++++++++ .../[appId]/overview/build-logs-overlay.tsx | 9 +- .../app/[appId]/overview/deployments.tsx | 4 +- src/components/custom/build-logs-streamed.tsx | 95 +++++++++++++++++++ src/components/custom/logs-streamed.tsx | 3 +- src/server/services/app.service.ts | 38 +++++--- src/server/services/build.service.ts | 56 ++++++++++- .../services/deployment-logs.service.ts | 94 +++++++++++------- src/server/services/deployment.service.ts | 23 ++++- src/shared/model/build-job.ts | 1 + src/shared/model/deployment-info.model.ts | 1 + 11 files changed, 313 insertions(+), 61 deletions(-) create mode 100644 src/app/api/build-logs/route.ts create mode 100644 src/components/custom/build-logs-streamed.tsx diff --git a/src/app/api/build-logs/route.ts b/src/app/api/build-logs/route.ts new file mode 100644 index 0000000..6664923 --- /dev/null +++ b/src/app/api/build-logs/route.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; +import { simpleRoute } from "@/server/utils/action-wrapper.utils"; +import deploymentLogService from "@/server/services/deployment-logs.service"; + +// Prevents this route's response from being cached +export const dynamic = "force-dynamic"; + +const zodInputModel = z.object({ + deploymentId: z.string(), +}); + +export async function POST(request: Request) { + return simpleRoute(async () => { + const input = await request.json(); + + const inputInfo = zodInputModel.parse(input); + let { deploymentId } = inputInfo; + + let closeListenerFunc: (() => void) | undefined; + + const encoder = new TextEncoder(); + const customReadable = new ReadableStream({ + start(controller) { + const innerFunc = async () => { + console.log(`[CONNECT] Client joined build log stream for deployment ${deploymentId}`); + controller.enqueue(encoder.encode('Stream opened, loading build logs...\n')); + + closeListenerFunc = await deploymentLogService.getLogsStream(deploymentId, (chunk) => { + controller.enqueue(encoder.encode(chunk)); + }); + }; + innerFunc(); + }, + cancel() { + console.log(`[DISCONNECTED] Client disconnected build log stream for deployment ${deploymentId}`); + closeListenerFunc?.(); + }, + }) + + return new Response(customReadable, { + // Set the headers for Server-Sent Events (SSE) + headers: { + Connection: "keep-alive", + "Content-Encoding": "none", + "Cache-Control": "no-cache, no-transform", + "Content-Type": "text/event-stream; charset=utf-8", + }, + }) + }); +} diff --git a/src/app/project/app/[appId]/overview/build-logs-overlay.tsx b/src/app/project/app/[appId]/overview/build-logs-overlay.tsx index 29017f6..a7237b0 100644 --- a/src/app/project/app/[appId]/overview/build-logs-overlay.tsx +++ b/src/app/project/app/[appId]/overview/build-logs-overlay.tsx @@ -15,6 +15,7 @@ import { set } from "date-fns"; import { DeploymentInfoModel } from "@/shared/model/deployment-info.model"; import LogsStreamed from "../../../../../components/custom/logs-streamed"; import { formatDateTime } from "@/frontend/utils/format.utils"; +import BuildLogsStreamed from "@/components/custom/build-logs-streamed"; export function BuildLogsDialog({ deploymentInfo, @@ -34,14 +35,14 @@ export function BuildLogsDialog({ }}> - Build Logs + Deployment Logs - View the build logs for the selected deployment {formatDateTime(deploymentInfo.createdAt)}. + View the logs for the selected deployment {formatDateTime(deploymentInfo.createdAt)}.
- {!deploymentInfo.buildJobName && 'For this build is no log available'} - {deploymentInfo.buildJobName && } + {!deploymentInfo.deploymentId && 'For this build is no log available'} + {deploymentInfo.deploymentId && }
diff --git a/src/app/project/app/[appId]/overview/deployments.tsx b/src/app/project/app/[appId]/overview/deployments.tsx index 9163f96..64b5f86 100644 --- a/src/app/project/app/[appId]/overview/deployments.tsx +++ b/src/app/project/app/[appId]/overview/deployments.tsx @@ -77,7 +77,7 @@ export default function BuildsTab({ {item.status}], ["startTime", "Started At", true, (item) => formatDateTime(item.createdAt)], ['gitCommit', 'Git Commit', true, (item) => {item.gitCommit}], @@ -88,7 +88,7 @@ export default function BuildsTab({ return <>
- {item.buildJobName && } + {item.deploymentId && } {item.buildJobName && item.status === 'BUILDING' && }
diff --git a/src/components/custom/build-logs-streamed.tsx b/src/components/custom/build-logs-streamed.tsx new file mode 100644 index 0000000..b335981 --- /dev/null +++ b/src/components/custom/build-logs-streamed.tsx @@ -0,0 +1,95 @@ +import { useEffect, useRef, useState } from "react"; +import { Textarea } from "@/components/ui/textarea"; +import React from "react"; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card" + + +export default function BuildLogsStreamed({ + deploymentId, + fullHeight = false, +}: { + deploymentId?: string; + fullHeight?: boolean; +}) { + const [isConnected, setIsConnected] = useState(false); + const [logs, setLogs] = useState(''); + const textAreaRef = useRef(null); + + const initializeConnection = async (controller: AbortController) => { + // Initiate the first call to connect to SSE API + + setLogs('Loading...'); + + const signal = controller.signal; + const apiResponse = await fetch('/api/build-logs', { + method: "POST", + headers: { + "Content-Type": "text/event-stream", + }, + body: JSON.stringify({ deploymentId }), + signal: signal, + }); + + if (!apiResponse.ok) return; + if (!apiResponse.body) return; + setIsConnected(true); + + // To decode incoming data as a string + const reader = apiResponse.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + setLogs(''); + while (true) { + const { value, done } = await reader.read(); + if (done) { + setIsConnected(false); + break; + } + if (value) { + setLogs((prevLogs) => prevLogs + value); + } + } + } + + useEffect(() => { + if (!deploymentId) { + return; + } + const controller = new AbortController(); + initializeConnection(controller); + + return () => { + console.log('Disconnecting from logs'); + setLogs(''); + controller.abort(); + }; + }, [deploymentId]); + + useEffect(() => { + if (textAreaRef.current) { + // Scroll to the bottom every time logs change + textAreaRef.current.scrollTop = textAreaRef.current.scrollHeight; + } + }, [logs]); + + return <> +
+