implemented Logs

This commit is contained in:
biersoeckli
2024-11-02 11:10:49 +00:00
parent a45b854c32
commit 8faeb78c1b
6 changed files with 85 additions and 115 deletions

View File

@@ -10,7 +10,7 @@ import DomainsList from "./domains/domains";
import StorageList from "./storage/storages";
import { AppExtendedModel } from "@/model/app-extended.model";
import { BuildJobModel } from "@/model/build-job";
import BuildsTab from "./overview/builds-tab";
import BuildsTab from "./overview/builds";
import Logs from "./overview/logs";
export default function AppTabs({
@@ -35,10 +35,9 @@ export default function AppTabs({
<TabsTrigger value="domains">Domains</TabsTrigger>
<TabsTrigger value="storage">Storage</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<TabsContent value="overview" className="grid grid-cols-1 3xl:grid-cols-2 gap-4">
<Logs app={app} />
<BuildsTab app={app} />
</TabsContent>
<TabsContent value="general" className="space-y-4">
<GeneralAppSource app={app} />

View File

@@ -16,7 +16,7 @@ import { Toast } from "@/lib/toast.utils";
import { DeploymentInfoModel } from "@/model/deployment-info.model";
import DeploymentStatusBadge from "./deployment-status-badge";
import { io } from "socket.io-client";
import { podLogsSocket } from "@/socket";
import { podLogsSocket } from "@/lib/sockets";
export default function BuildsTab({
app

View File

@@ -1,70 +1,67 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { AppExtendedModel } from "@/model/app-extended.model";
import { useEffect, useState } from "react";
import { podLogsSocket } from "@/socket";
import { podLogsSocket } from "@/lib/sockets";
import { Textarea } from "@/components/ui/textarea";
export default function LogsStreamed({
app,
namespace,
podName,
}: {
app: AppExtendedModel;
namespace: string;
podName: string;
}) {
const [isConnected, setIsConnected] = useState(false);
const [transport, setTransport] = useState("N/A");
const [logs, setLogs] = useState<string>('');
function onConnect() {
setIsConnected(true);
setTransport(podLogsSocket.io.engine.transport.name);
podLogsSocket.io.engine.on("upgrade", (transport) => {
setTransport(transport.name);
});
}
function onDisconnect() {
setIsConnected(false);
setTransport("N/A");
}
const myListener = (e: string) => {
setLogs(e);
}
useEffect(() => {
const logEventName = `${app.projectId}_${app.id}_${podName}`;
function onConnect() {
setIsConnected(true);
setTransport(podLogsSocket.io.engine.transport.name);
podLogsSocket.io.engine.on("upgrade", (transport) => {
setTransport(transport.name);
});
}
function onDisconnect() {
setIsConnected(false);
setTransport("N/A");
if (!podName) {
return;
}
const logEventName = `${namespace}_${podName}`;
console.log('Connecting to logs ' + logEventName);
if (podLogsSocket.connected) {
onConnect();
}
podLogsSocket.emit('joinPodLog', { appId: app.id, podName });
const myListener = (e: string) => {
setLogs(e);
}
podLogsSocket.emit('joinPodLog', { namespace, podName });
podLogsSocket.on("connect", onConnect);
podLogsSocket.on("disconnect", onDisconnect);
podLogsSocket.on(logEventName, myListener);
return () => {
if (!podName) {
return;
}
console.log('Disconnecting from logs ' + logEventName);
podLogsSocket.emit('leavePodLog', { namespace, podName });
setLogs('');
podLogsSocket.off("connect", onConnect);
podLogsSocket.off("disconnect", onDisconnect);
podLogsSocket.off(logEventName, myListener);
podLogsSocket.disconnect();
};
}, [app, podName]);
if (app.sourceType === 'container') {
return <></>;
}
}, [namespace, podName]);
return <>
<Textarea value={logs} readOnly className="h-[400px] bg-slate-900 text-white" />
<div className="text-sm pl-1">Status: {isConnected ? 'Connected' : 'Disconnected'}</div>
</>;
}

View File

@@ -1,12 +1,13 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { AppExtendedModel } from "@/model/app-extended.model";
import { useEffect, useState } from "react";
import { podLogsSocket } from "@/socket";
import { podLogsSocket } from "@/lib/sockets";
import LogsStreamed from "./logs-streamed";
import { getPodsForApp } from "./actions";
import { PodsInfoModel } from "@/model/pods-info.model";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import FullLoadingSpinner from "@/components/ui/full-loading-spinnter";
import { toast } from "sonner";
export default function Logs({
app
@@ -15,56 +16,61 @@ export default function Logs({
}) {
const [selectedPod, setSelectedPod] = useState<string | undefined>(undefined);
const [appPods, setAppPods] = useState<PodsInfoModel[] | undefined>(undefined);
const [error, setError] = useState<string | undefined>(undefined);
const updateBuilds = async () => {
setError(undefined);
try {
const response = await getPodsForApp(app.id);
if (response.status === 'success' && response.data) {
setAppPods(response.data);
if (!selectedPod && response.data.length > 0) {
setSelectedPod(response.data[0].podName);
}
} else {
console.error(response);
setError(response.message ?? 'An unknown error occurred.');
toast.error(response.message ?? 'An unknown error occurred while loading pods.');
}
} catch (ex) {
console.error(ex);
setError('An unknown error occurred.');
toast.error('An unknown error occurred while loading pods.');
}
}
useEffect(() => {
if (app.sourceType === 'container') {
return;
}
updateBuilds();
updateBuilds()
const intervalId = setInterval(updateBuilds, 10000);
return () => clearInterval(intervalId);
}, [app]);
useEffect(() => {
if (appPods && selectedPod && !appPods.find(p => p.podName === selectedPod)) {
// current selected pod is not in the list anymore
setSelectedPod(undefined);
if (appPods.length > 0) {
setSelectedPod(appPods[0].podName);
}
} else if (!selectedPod && appPods && appPods.length > 0) {
// no pod selected yet, initialize with first pod
setSelectedPod(appPods[0].podName);
}
}, [appPods]);
return <>
<Card>
<CardHeader>
<CardTitle>Logs</CardTitle>
<CardDescription>App Logs.</CardDescription>
<CardDescription>Read logs from all running Containers.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{!appPods && <FullLoadingSpinner />}
{appPods && appPods.length === 0 && <div>No running pods found for this app.</div>}
{appPods && <Select defaultValue={appPods[0].podName} onValueChange={(val) => setSelectedPod(val)}>
<SelectTrigger className="w-[180px]">
{selectedPod && appPods && <Select className="w-full" value={selectedPod} onValueChange={(val) => setSelectedPod(val)}>
<SelectTrigger >
<SelectValue placeholder="Pod wählen" />
</SelectTrigger>
<SelectContent>
{appPods.map(pod => <SelectItem key={pod.podName} value={pod.podName}>{pod.podName}</SelectItem>)}
</SelectContent>
</Select>}
{selectedPod && <LogsStreamed app={app} podName={selectedPod} />}
{app.projectId && selectedPod && <LogsStreamed namespace={app.projectId} podName={selectedPod} />}
</CardContent>
</Card >
</>;

View File

@@ -1,8 +1,3 @@
import { revalidateTag, unstable_cache } from "next/cache";
import dataAccess from "../adapter/db.client";
import { Tags } from "../utils/cache-tag-generator.utils";
import { App, Prisma, Project } from "@prisma/client";
import { StringUtils } from "../utils/string.utils";
import deploymentService from "./deployment.service";
import k3s from "../adapter/kubernetes-api.adapter";
import { DefaultEventsMap, Socket } from "socket.io";
@@ -11,84 +6,57 @@ import { PodsInfoModel } from "@/model/pods-info.model";
class LogStreamService {
activeStreams = new Map<string, { logStream: stream.PassThrough, clients: number, k3sStreamRequest: any }>();
async streamLogs(socket: Socket<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>) {
console.log('Client connected:', socket.id);
console.log('[CONNECT] Client connected:', socket.id);
let logStream: stream.PassThrough;
let k3sStreamRequest: any;
let streamKey: string;
socket.on('joinPodLog', async (podInfo) => {
const { appId, podName } = podInfo;
if (!appId || !podName) {
const { namespace, podName } = podInfo;
if (!namespace || !podName) {
return;
}
const app = await dataAccess.client.app.findFirstOrThrow({
where: {
id: appId
}
});
const pod = await deploymentService.getPodByName(namespace, podName);
const pod = await deploymentService.getPodByName(app.projectId, podName);
streamKey = `${namespace}_${pod.podName}`;
const streamKey = `${app.projectId}_${app.id}_${pod.podName}`;
const existingActiveStream = this.activeStreams.get(streamKey);
if (!existingActiveStream) {
// create stream if not existing
const retVal = await this.createLogStreamForPod(socket, streamKey, app, pod);
this.activeStreams.set(streamKey, retVal);
}
// Client dem Raum hinzufügen und Anzahl der Clients für diesen Pod erhöhen
socket.join(streamKey);
this.activeStreams.get(streamKey)!.clients += 1;
// create stream if not existing
const retVal = await this.createLogStreamForPod(socket, streamKey, namespace, pod);
logStream = retVal.logStream;
k3sStreamRequest = retVal.k3sStreamRequest;
console.log(`Client ${socket.id} joined log stream for ${streamKey}`);
console.log(`[CONNECTED] Client ${socket.id} joined log stream for ${streamKey}`);
});
socket.on('disconnecting', () => {
socket.on('leavePodLog', () => {
// Über alle Räume iterieren, die dieser Socket abonniert hat
for (const streamKey of Array.from(socket.rooms)) {
const existingActiveStream = this.activeStreams.get(streamKey);
if (existingActiveStream) {
// Anzahl der Clients für diesen Stream verringern
existingActiveStream.clients -= 1;
console.log(`Client ${socket.id} left log stream for ${streamKey}`);
logStream?.end();
k3sStreamRequest?.abort();
console.log(`[LEAVE] Client ${socket.id} left log stream for ${streamKey}`);
});
// Falls keine Clients mehr übrig sind, den Stream beenden
if (existingActiveStream.clients === 0) {
this.deleteLogStream(existingActiveStream, streamKey);
}
}
}
socket.on('disconnect', () => {
// Über alle Räume iterieren, die dieser Socket abonniert hat
logStream?.end();
k3sStreamRequest?.abort();
console.log(`[DISCONNECTED] Client ${socket.id} disconnected log stream for ${streamKey}`);
});
}
private deleteLogStream(existingActiveStream: { logStream: stream.PassThrough; clients: number; k3sStreamRequest: any; }, streamKey: string) {
existingActiveStream.logStream.end();
existingActiveStream.k3sStreamRequest.abort();
this.activeStreams.delete(streamKey);
console.log(`Stopped log stream for ${streamKey} as no clients are listening.`);
}
private async createLogStreamForPod(socket: Socket<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>, streamKey: string, app: App, pod: PodsInfoModel) {
private async createLogStreamForPod(socket: Socket<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>, streamKey: string, namespace: string, pod: PodsInfoModel) {
const logStream = new stream.PassThrough();
logStream.on('data', (chunk) => {
socket.emit(streamKey, chunk.toString());
});
logStream.on('data', (chunk) => {
socket.to(streamKey).emit(`${streamKey}`, chunk.toString());
});
let k3sStreamRequest = await k3s.log.log(app.projectId, pod.podName, pod.containerName, logStream, {
let k3sStreamRequest = await k3s.log.log(namespace, pod.podName, pod.containerName, logStream, {
follow: true,
pretty: false,
tailLines: 100,
}); /*.catch((err) => {
console.error(`Error streaming logs for ${streamKey}:`, err);
logStream.end();
});*/
const retVal = { logStream, clients: 0, k3sStreamRequest };
return retVal;
});
return { logStream, k3sStreamRequest };
}
}