feat: add support for viewing docker logs in swarm mode

This commit is contained in:
faytranevozter 2024-12-18 16:40:33 +07:00
parent 852895c382
commit 6211a19805
10 changed files with 280 additions and 12 deletions

View File

@ -35,7 +35,7 @@ interface Props {
}
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
const { data, isLoading } = api.docker.getServiceContainersByAppName.useQuery(
{
appName,
serverId,
@ -81,7 +81,8 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
key={container.containerId}
value={container.containerId}
>
{container.name} ({container.containerId}) {container.state}
{container.name} ({container.containerId}@{container.node}){" "}
{container.state}
</SelectItem>
))}
<SelectLabel>Containers ({data?.length})</SelectLabel>
@ -91,6 +92,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
<DockerLogs
serverId={serverId || ""}
containerId={containerId || "select-a-container"}
runType="swarm"
/>
</CardContent>
</Card>

View File

@ -0,0 +1,100 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Loader, Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
export const DockerLogs = dynamic(
() =>
import("@/components/dashboard/docker/logs/docker-logs-id").then(
(e) => e.DockerLogsId,
),
{
ssr: false,
},
);
interface Props {
appName: string;
serverId?: string;
}
export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
const { data, isLoading } = api.docker.getStackContainersByAppName.useQuery(
{
appName,
serverId,
},
{
enabled: !!appName,
},
);
const [containerId, setContainerId] = useState<string | undefined>();
useEffect(() => {
if (data && data?.length > 0) {
setContainerId(data[0]?.containerId);
}
}, [data]);
return (
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Logs</CardTitle>
<CardDescription>
Watch the logs of the application in real time
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Label>Select a container to view logs</Label>
<Select onValueChange={setContainerId} value={containerId}>
<SelectTrigger>
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SelectValue placeholder="Select a container" />
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
{data?.map((container) => (
<SelectItem
key={container.containerId}
value={container.containerId}
>
{container.name} ({container.containerId}@{container.node}){" "}
{container.state}
</SelectItem>
))}
<SelectLabel>Containers ({data?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<DockerLogs
serverId={serverId || ""}
containerId={containerId || "select-a-container"}
runType="swarm"
/>
</CardContent>
</Card>
);
};

View File

@ -97,6 +97,7 @@ export const ShowDockerLogsCompose = ({
<DockerLogs
serverId={serverId || ""}
containerId={containerId || "select-a-container"}
runType="native"
/>
</CardContent>
</Card>

View File

@ -12,6 +12,7 @@ import { type LogLine, getLogType, parseLogs } from "./utils";
interface Props {
containerId: string;
serverId?: string | null;
runType: "swarm" | "native";
}
export const priorities = [
@ -37,7 +38,11 @@ export const priorities = [
},
];
export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
export const DockerLogsId: React.FC<Props> = ({
containerId,
serverId,
runType,
}) => {
const { data } = api.docker.getConfig.useQuery(
{
containerId,
@ -104,6 +109,7 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
tail: lines.toString(),
since,
search,
runType,
});
if (serverId) {

View File

@ -46,7 +46,11 @@ export const ShowDockerModalLogs = ({
<DialogDescription>View the logs for {containerId}</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 pt-2.5">
<DockerLogsId containerId={containerId || ""} serverId={serverId} />
<DockerLogsId
containerId={containerId || ""}
serverId={serverId}
runType="native"
/>
</div>
</DialogContent>
</Dialog>

View File

@ -91,7 +91,11 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
</SelectGroup>
</SelectContent>
</Select>
<DockerLogsId containerId={containerId || ""} serverId={serverId} />
<DockerLogsId
containerId={containerId || ""}
serverId={serverId}
runType="native"
/>
</div>
</DialogContent>
</Dialog>

View File

@ -6,6 +6,7 @@ import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show-
import { ShowEnvironmentCompose } from "@/components/dashboard/compose/enviroment/show";
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
import { ShowDockerLogsStack } from "@/components/dashboard/compose/logs/show-stack";
import { ShowMonitoringCompose } from "@/components/dashboard/compose/monitoring/show";
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
import { ProjectLayout } from "@/components/layouts/project-layout";
@ -251,11 +252,18 @@ const Service = (
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogsCompose
serverId={data?.serverId || ""}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>
{data?.composeType === "docker-compose" ? (
<ShowDockerLogsCompose
serverId={data?.serverId || ""}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>
) : (
<ShowDockerLogsStack
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
)}
</div>
</TabsContent>

View File

@ -4,6 +4,8 @@ import {
getContainers,
getContainersByAppLabel,
getContainersByAppNameMatch,
getServiceContainersByAppName,
getStackContainersByAppName,
} from "@dokploy/server";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
@ -68,4 +70,26 @@ export const dockerRouter = createTRPCRouter({
.query(async ({ input }) => {
return await getContainersByAppLabel(input.appName, input.serverId);
}),
getStackContainersByAppName: protectedProcedure
.input(
z.object({
appName: z.string().min(1),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return await getStackContainersByAppName(input.appName, input.serverId);
}),
getServiceContainersByAppName: protectedProcedure
.input(
z.object({
appName: z.string().min(1),
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return await getServiceContainersByAppName(input.appName, input.serverId);
}),
});

View File

@ -34,6 +34,7 @@ export const setupDockerContainerLogsWebSocketServer = (
const search = url.searchParams.get("search");
const since = url.searchParams.get("since");
const serverId = url.searchParams.get("serverId");
const runType = url.searchParams.get("runType");
const { user, session } = await validateWebSocketRequest(req);
if (!containerId) {
@ -53,7 +54,7 @@ export const setupDockerContainerLogsWebSocketServer = (
const client = new Client();
client
.once("ready", () => {
const baseCommand = `docker container logs --timestamps --tail ${tail} ${
const baseCommand = `docker ${runType==="swarm"?"service":"container"} logs --timestamps --tail ${tail} ${
since === "all" ? "" : `--since ${since}`
} --follow ${containerId}`;
const escapedSearch = search ? search.replace(/'/g, "'\\''") : "";
@ -97,7 +98,7 @@ export const setupDockerContainerLogsWebSocketServer = (
});
} else {
const shell = getShell();
const baseCommand = `docker container logs --timestamps --tail ${tail} ${
const baseCommand = `docker ${runType==="swarm"?"service":"container"} logs --timestamps --tail ${tail} ${
since === "all" ? "" : `--since ${since}`
} --follow ${containerId}`;
const command = search

View File

@ -157,6 +157,124 @@ export const getContainersByAppNameMatch = async (
return [];
};
export const getStackContainersByAppName = async (
appName: string,
serverId?: string,
) => {
try {
let result: string[] = [];
const command = `docker stack ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`;
if (serverId) {
const { stdout, stderr } = await execAsyncRemote(serverId, command);
if (stderr) {
return [];
}
if (!stdout) return [];
result = stdout.trim().split("\n");
} else {
const { stdout, stderr } = await execAsync(command);
if (stderr) {
return [];
}
if (!stdout) return [];
result = stdout.trim().split("\n");
}
const containers = result.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const state = parts[2]
? parts[2].replace("State: ", "").trim()
: "No state";
const node = parts[3]
? parts[3].replace("Node: ", "").trim()
: "No specific node";
return {
containerId,
name,
state,
node,
};
});
return containers || [];
} catch (error) {}
return [];
};
export const getServiceContainersByAppName = async (
appName: string,
serverId?: string,
) => {
try {
let result: string[] = [];
const command = `docker service ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`;
if (serverId) {
const { stdout, stderr } = await execAsyncRemote(serverId, command);
if (stderr) {
return [];
}
if (!stdout) return [];
result = stdout.trim().split("\n");
} else {
const { stdout, stderr } = await execAsync(command);
if (stderr) {
return [];
}
if (!stdout) return [];
result = stdout.trim().split("\n");
}
const containers = result.map((line) => {
const parts = line.split(" | ");
const containerId = parts[0]
? parts[0].replace("CONTAINER ID : ", "").trim()
: "No container id";
const name = parts[1]
? parts[1].replace("Name: ", "").trim()
: "No container name";
const state = parts[2]
? parts[2].replace("State: ", "").trim()
: "No state";
const node = parts[3]
? parts[3].replace("Node: ", "").trim()
: "No specific node";
return {
containerId,
name,
state,
node,
};
});
return containers || [];
} catch (error) {}
return [];
};
export const getContainersByAppLabel = async (
appName: string,
serverId?: string,