-
-
- {
- setLines(Number(e.target.value) || 1);
- }}
- />
-
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
+ const wsUrl = `${protocol}//${
+ window.location.host
+ }/docker-container-logs?containerId=${containerId}&tail=${lines}&since=${since}&search=${search}${
+ serverId ? `&serverId=${serverId}` : ""
+ }`;
+ console.log("Connecting to WebSocket:", wsUrl);
+ const ws = new WebSocket(wsUrl);
-
-
- );
+ ws.onopen = () => {
+ console.log("WebSocket connected");
+ };
+
+ ws.onmessage = (e) => {
+ // console.log("Received message:", e.data);
+ setRawLogs((prev) => prev + e.data);
+ };
+
+ ws.onerror = (error) => {
+ console.error("WebSocket error:", error);
+ };
+
+ ws.onclose = (e) => {
+ console.log("WebSocket closed:", e.reason);
+ setRawLogs(
+ (prev) =>
+ prev +
+ `Connection closed!\nReason: ${
+ e.reason || "WebSocket was closed try to refresh"
+ }\n`
+ );
+ };
+
+ return () => {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.close();
+ }
+ };
+ }, [containerId, serverId, lines, search, since]);
+
+ const handleDownload = () => {
+ const logContent = filteredLogs
+ .map(
+ ({ timestamp, message }: { timestamp: Date | null; message: string }) =>
+ `${timestamp?.toISOString() || "No timestamp"} ${message}`
+ )
+ .join("\n");
+
+ const blob = new Blob([logContent], { type: "text/plain" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `dokploy-logs-${new Date().toISOString()}.txt`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ };
+
+ const handleFilter = (logs: LogLine[]) => {
+ return logs.filter((log) => {
+ const logType = getLogType(log.message).type;
+
+ const matchesType = typeFilter === "all" || logType === typeFilter;
+
+ return matchesType;
+ });
+ };
+
+ useEffect(() => {
+ setRawLogs("");
+ setFilteredLogs([]);
+ }, [containerId]);
+
+ useEffect(() => {
+ const logs = parseLogs(rawLogs);
+ const filtered = handleFilter(logs);
+ setFilteredLogs(filtered);
+ }, [rawLogs, search, lines, since, typeFilter]);
+
+ useEffect(() => {
+ scrollToBottom();
+
+ if (autoScroll && scrollRef.current) {
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+ }
+ }, [filteredLogs, autoScroll]);
+
+ return (
+
diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx
new file mode 100644
index 00000000..36499df9
--- /dev/null
+++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx
@@ -0,0 +1,73 @@
+import { Badge } from "@/components/ui/badge";
+import { cn } from "@/lib/utils";
+import { getLogType, type LogLine } from "./utils";
+import React from "react";
+
+interface LogLineProps {
+ log: LogLine;
+ searchTerm?: string;
+}
+
+export function TerminalLine({ log, searchTerm }: LogLineProps) {
+ const { timestamp, message } = log;
+ const { type, variant, color } = getLogType(message);
+
+ const formattedTime = timestamp
+ ? timestamp.toLocaleString("en-GB", {
+ day: "2-digit",
+ month: "short",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: false,
+ })
+ : "--- No time found ---";
+
+ const highlightMessage = (text: string, term: string) => {
+ if (!term) return text;
+
+ const parts = text.split(new RegExp(`(${term})`, "gi"));
+ return parts.map((part, index) =>
+ part.toLowerCase() === term.toLowerCase() ? (
+
+ {part}
+
+ ) : (
+ part
+ )
+ );
+ };
+
+ return (
+
+ {" "}
+
+ {/* Icon to expand the log item maybe implement a colapsible later */}
+ {/*
*/}
+
+
+ {formattedTime}
+
+
+ {type}
+
+
+
+ {searchTerm ? highlightMessage(message, searchTerm) : message}
+
+
+ );
+}
diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts
new file mode 100644
index 00000000..1c010681
--- /dev/null
+++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts
@@ -0,0 +1,132 @@
+export type LogType = "error" | "warning" | "success" | "info";
+export type LogVariant = "red" | "yellow" | "green" | "blue";
+
+export interface LogLine {
+ timestamp: Date | null;
+ message: string;
+}
+
+interface LogStyle {
+ type: LogType;
+ variant: LogVariant;
+ color: string;
+}
+
+const LOG_STYLES: Record
= {
+ error: {
+ type: "error",
+ variant: "red",
+ color: "bg-red-500/40",
+ },
+ warning: {
+ type: "warning",
+ variant: "yellow",
+ color: "bg-yellow-500/40",
+ },
+ success: {
+ type: "success",
+ variant: "green",
+ color: "bg-green-500/40",
+ },
+ info: {
+ type: "info",
+ variant: "blue",
+ color: "bg-blue-600/40",
+ },
+} as const;
+
+export function parseLogs(logString: string): LogLine[] {
+ // Regex to match the log line format
+ // Exemple of return :
+ // 1 2024-12-10T10:00:00.000Z The server is running on port 8080
+ // Should return :
+ // { timestamp: new Date("2024-12-10T10:00:00.000Z"),
+ // message: "The server is running on port 8080" }
+ const logRegex =
+ /^(?:(\d+)\s+)?(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC)?\s*(.*)$/;
+
+ return logString
+ .split("\n")
+ .map((line) => line.trim())
+ .filter((line) => line !== "")
+ .map((line) => {
+ const match = line.match(logRegex);
+ if (!match) return null;
+
+ const [, , timestamp, message] = match;
+
+ if (!message?.trim()) return null;
+
+ // Delete other timestamps and keep only the one from --timestamps
+ const cleanedMessage = message
+ ?.replace(
+ /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC/g,
+ ""
+ )
+ .trim();
+
+ return {
+ timestamp: timestamp ? new Date(timestamp.replace(" UTC", "Z")) : null,
+ message: cleanedMessage,
+ };
+ })
+ .filter((log) => log !== null);
+}
+
+// Detect log type based on message content
+export const getLogType = (message: string): LogStyle => {
+ const lowerMessage = message.toLowerCase();
+
+ if (
+ /(?:^|\s)(?:error|err):?\s/i.test(lowerMessage) ||
+ /\b(?:exception|failed|failure)\b/i.test(lowerMessage) ||
+ /(?:stack\s?trace):\s*$/i.test(lowerMessage) ||
+ /^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(lowerMessage) ||
+ /\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(lowerMessage) ||
+ /Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(lowerMessage) ||
+ /\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(lowerMessage) ||
+ /\[(?:error|err|fatal)\]/i.test(lowerMessage) ||
+ /\b(?:crash|critical|fatal)\b/i.test(lowerMessage) ||
+ /\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(lowerMessage)
+ ) {
+ return LOG_STYLES.error;
+ }
+
+ if (
+ /(?:^|\s)(?:warning|warn):?\s/i.test(lowerMessage) ||
+ /\[(?:warn(?:ing)?|attention)\]/i.test(lowerMessage) ||
+ /(?:deprecated|obsolete)\s+(?:since|in|as\s+of)/i.test(lowerMessage) ||
+ /\b(?:caution|attention|notice):\s/i.test(lowerMessage) ||
+ /(?:might|may|could)\s+(?:not|cause|lead\s+to)/i.test(lowerMessage) ||
+ /(?:!+\s*(?:warning|caution|attention)\s*!+)/i.test(lowerMessage) ||
+ /\b(?:deprecated|obsolete)\b/i.test(lowerMessage) ||
+ /\b(?:unstable|experimental)\b/i.test(lowerMessage)
+ ) {
+ return LOG_STYLES.warning;
+ }
+
+ if (
+ /(?:successfully|complete[d]?)\s+(?:initialized|started|completed|done)/i.test(
+ lowerMessage
+ ) ||
+ /\[(?:success|ok|done)\]/i.test(lowerMessage) ||
+ /(?:listening|running)\s+(?:on|at)\s+(?:port\s+)?\d+/i.test(lowerMessage) ||
+ /(?:connected|established|ready)\s+(?:to|for|on)/i.test(lowerMessage) ||
+ /\b(?:loaded|mounted|initialized)\s+successfully\b/i.test(lowerMessage) ||
+ /✓|√|\[ok\]|done!/i.test(lowerMessage) ||
+ /\b(?:success(?:ful)?|completed|ready)\b/i.test(lowerMessage) ||
+ /\b(?:started|running|active)\b/i.test(lowerMessage)
+ ) {
+ return LOG_STYLES.success;
+ }
+
+ if (
+ /(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) ||
+ /\[(info|log|debug|trace|server|db|api)\]/i.test(lowerMessage) ||
+ /\b(?:version|config|start|import|load)\b:?/i.test(lowerMessage)
+ ) {
+ return LOG_STYLES.info;
+ }
+
+ return LOG_STYLES.info;
+};
diff --git a/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx b/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx
index 2693f79c..f15e475c 100644
--- a/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx
+++ b/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx
@@ -92,7 +92,6 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
diff --git a/apps/dokploy/components/ui/badge.tsx b/apps/dokploy/components/ui/badge.tsx
index f38976c0..fd190b8a 100644
--- a/apps/dokploy/components/ui/badge.tsx
+++ b/apps/dokploy/components/ui/badge.tsx
@@ -14,6 +14,18 @@ const badgeVariants = cva(
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ red:
+ "border-transparent select-none items-center whitespace-nowrap font-medium bg-red-500/15 text-destructive text-xs h-4 px-1 py-1 rounded-md",
+ yellow:
+ "border-transparent select-none items-center whitespace-nowrap font-medium bg-yellow-500/15 text-yellow-500 text-xs h-4 px-1 py-1 rounded-md",
+ orange:
+ "border-transparent select-none items-center whitespace-nowrap font-medium bg-orange-500/15 text-orange-500 text-xs h-4 px-1 py-1 rounded-md",
+ green:
+ "border-transparent select-none items-center whitespace-nowrap font-medium bg-emerald-500/15 text-emerald-500 text-xs h-4 px-1 py-1 rounded-md",
+ blue:
+ "border-transparent select-none items-center whitespace-nowrap font-medium bg-blue-500/15 text-blue-500 text-xs h-4 px-1 py-1 rounded-md",
+ blank:
+ "border-transparent select-none items-center whitespace-nowrap font-medium dark:bg-white/15 bg-black/15 text-foreground text-xs h-4 px-1 py-1 rounded-md",
outline: "text-foreground",
},
},
diff --git a/apps/dokploy/server/wss/docker-container-logs.ts b/apps/dokploy/server/wss/docker-container-logs.ts
index 1493f698..882b2b4e 100644
--- a/apps/dokploy/server/wss/docker-container-logs.ts
+++ b/apps/dokploy/server/wss/docker-container-logs.ts
@@ -31,6 +31,8 @@ export const setupDockerContainerLogsWebSocketServer = (
const url = new URL(req.url || "", `http://${req.headers.host}`);
const containerId = url.searchParams.get("containerId");
const tail = url.searchParams.get("tail");
+ const search = url.searchParams.get("search");
+ const since = url.searchParams.get("since")
const serverId = url.searchParams.get("serverId");
const { user, session } = await validateWebSocketRequest(req);
@@ -52,7 +54,7 @@ export const setupDockerContainerLogsWebSocketServer = (
client
.once("ready", () => {
const command = `
- bash -c "docker container logs --tail ${tail} --follow ${containerId}"
+ bash -c "docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -i '${search}'"
`;
client.exec(command, (err, stream) => {
if (err) {
@@ -95,7 +97,7 @@ export const setupDockerContainerLogsWebSocketServer = (
shell,
[
"-c",
- `docker container logs --tail ${tail} --follow ${containerId}`,
+ `docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -i '${search}'`,
],
{
name: "xterm-256color",
diff --git a/apps/dokploy/styles/globals.css b/apps/dokploy/styles/globals.css
index f7e2a71f..a116d66b 100644
--- a/apps/dokploy/styles/globals.css
+++ b/apps/dokploy/styles/globals.css
@@ -173,3 +173,29 @@
padding-top: 1rem !important;
}
}
+
+/* Docker Logs Scrollbar */
+@layer utilities {
+ .custom-logs-scrollbar {
+ scrollbar-width: thin;
+ scrollbar-color: hsl(var(--muted-foreground)) transparent;
+ }
+
+ .custom-logs-scrollbar::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ .custom-logs-scrollbar::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ .custom-logs-scrollbar::-webkit-scrollbar-thumb {
+ background-color: hsl(var(--muted-foreground) / 0.3);
+ border-radius: 20px;
+ }
+
+ .custom-logs-scrollbar::-webkit-scrollbar-thumb:hover {
+ background-color: hsl(var(--muted-foreground) / 0.5);
+ }
+}
\ No newline at end of file
diff --git a/packages/server/src/wss/docker-container-logs.ts b/packages/server/src/wss/docker-container-logs.ts
index 75292018..ab3234b7 100644
--- a/packages/server/src/wss/docker-container-logs.ts
+++ b/packages/server/src/wss/docker-container-logs.ts
@@ -32,6 +32,8 @@ export const setupDockerContainerLogsWebSocketServer = (
const url = new URL(req.url || "", `http://${req.headers.host}`);
const containerId = url.searchParams.get("containerId");
const tail = url.searchParams.get("tail");
+ const since = url.searchParams.get("since");
+ const search = url.searchParams.get("search");
const serverId = url.searchParams.get("serverId");
const { user, session } = await validateWebSocketRequest(req);
@@ -54,7 +56,7 @@ export const setupDockerContainerLogsWebSocketServer = (
client
.once("ready", () => {
const command = `
- bash -c "docker container logs --tail ${tail} --follow ${containerId}"
+ bash -c "docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -i '${search}'"
`;
client.exec(command, (err, stream) => {
if (err) {
@@ -89,7 +91,7 @@ export const setupDockerContainerLogsWebSocketServer = (
shell,
[
"-c",
- `docker container logs --tail ${tail} --follow ${containerId}`,
+ `docker container logs --timestamps --tail ${tail} ${since === "all" ? "" : `--since ${since}`} --follow ${containerId} | grep -i '${search}'`,
],
{
name: "xterm-256color",