feat(logs): add Database logs and Build logs to the Server Logs dashboard page

This commit is contained in:
2026-06-14 14:58:02 -07:00
parent f8cc4b32b0
commit e0354d969e
3 changed files with 250 additions and 19 deletions

View File

@@ -23,9 +23,28 @@ export default function LogsPage() {
const previews = anatomy?.hosting.previews ?? []; const previews = anatomy?.hosting.previews ?? [];
const databases = anatomy?.infrastructure.databases ?? [];
const deployments: { uuid: string; name: string }[] = [];
live.forEach((app) => {
if (app.inFlightBuild) {
deployments.push({
uuid: app.inFlightBuild.uuid,
name: `Build (Active): ${app.name}`,
});
}
if (app.lastBuild) {
deployments.push({
uuid: app.lastBuild.uuid,
name: `Build (Latest): ${app.name}`,
});
}
});
const [activeItem, setActiveItem] = useState<{ const [activeItem, setActiveItem] = useState<{
type: "app" | "preview"; type: "app" | "preview" | "database" | "deployment";
id: string; id: string;
name: string;
} | null>(null); } | null>(null);
const [logs, setLogs] = useState<string | null>(null); const [logs, setLogs] = useState<string | null>(null);
const [logsLoading, setLogsLoading] = useState(false); const [logsLoading, setLogsLoading] = useState(false);
@@ -34,20 +53,46 @@ export default function LogsPage() {
useEffect(() => { useEffect(() => {
if (activeItem) return; if (activeItem) return;
if (live.length > 0) { if (live.length > 0) {
setActiveItem({ type: "app", id: live[0].uuid }); setActiveItem({ type: "app", id: live[0].uuid, name: live[0].name });
} else if (previews.length > 0) { } else if (previews.length > 0) {
setActiveItem({ type: "preview", id: previews[0].id }); setActiveItem({
type: "preview",
id: previews[0].id,
name: previews[0].name || `Port ${previews[0].port}`,
});
} else if (databases.length > 0) {
setActiveItem({
type: "database",
id: databases[0].uuid,
name: databases[0].name,
});
} else if (deployments.length > 0) {
setActiveItem({
type: "deployment",
id: deployments[0].uuid,
name: deployments[0].name,
});
} }
}, [live, previews, activeItem]); }, [live, previews, databases, deployments, activeItem]);
const fetchLogs = async (item: { type: "app" | "preview"; id: string }) => { const fetchLogs = async (item: {
type: "app" | "preview" | "database" | "deployment";
id: string;
name: string;
}) => {
setLogsLoading(true); setLogsLoading(true);
try { try {
const action = item.type === "app" ? "apps.logs" : "dev_server.logs"; let action = "apps.logs";
const payloadParams = let payloadParams: Record<string, any> = { uuid: item.id, lines: 100 };
item.type === "app"
? { uuid: item.id, lines: 100 } if (item.type === "preview") {
: { id: item.id, lines: 100, projectId }; action = "dev_server.logs";
payloadParams = { id: item.id, lines: 100, projectId };
} else if (item.type === "database") {
action = "databases.logs";
} else if (item.type === "deployment") {
action = "deployments.logs";
}
const r = await fetch(`/api/mcp`, { const r = await fetch(`/api/mcp`, {
method: "POST", method: "POST",
@@ -134,7 +179,10 @@ export default function LogsPage() {
> >
<Loader2 size={15} className="animate-spin" /> Loading <Loader2 size={15} className="animate-spin" /> Loading
</div> </div>
) : live.length === 0 && previews.length === 0 ? ( ) : live.length === 0 &&
previews.length === 0 &&
databases.length === 0 &&
deployments.length === 0 ? (
<div style={{ padding: 24 }}> <div style={{ padding: 24 }}>
<EmptyState <EmptyState
icon={<Activity size={22} />} icon={<Activity size={22} />}
@@ -189,7 +237,13 @@ export default function LogsPage() {
{live.map((app) => ( {live.map((app) => (
<button <button
key={app.uuid} key={app.uuid}
onClick={() => setActiveItem({ type: "app", id: app.uuid })} onClick={() =>
setActiveItem({
type: "app",
id: app.uuid,
name: app.name,
})
}
style={{ style={{
textAlign: "left", textAlign: "left",
padding: "8px 12px", padding: "8px 12px",
@@ -240,7 +294,11 @@ export default function LogsPage() {
<button <button
key={preview.id} key={preview.id}
onClick={() => onClick={() =>
setActiveItem({ type: "preview", id: preview.id }) setActiveItem({
type: "preview",
id: preview.id,
name: preview.name || `Port ${preview.port}`,
})
} }
style={{ style={{
textAlign: "left", textAlign: "left",
@@ -263,6 +321,117 @@ export default function LogsPage() {
</button> </button>
))} ))}
</div> </div>
{databases.length > 0 && (
<div style={{ padding: "24px 20px 8px" }}>
<h3
style={{
fontSize: "0.7rem",
fontWeight: 600,
color: THEME.muted,
textTransform: "uppercase",
letterSpacing: "0.06em",
margin: 0,
}}
>
Databases
</h3>
</div>
)}
<div
style={{
display: "flex",
flexDirection: "column",
gap: 4,
padding: "0 20px",
}}
>
{databases.map((db) => (
<button
key={db.uuid}
onClick={() =>
setActiveItem({
type: "database",
id: db.uuid,
name: db.name,
})
}
style={{
textAlign: "left",
padding: "8px 12px",
background:
activeItem?.id === db.uuid
? THEME.subtleBg
: "transparent",
border: "1px solid transparent",
borderRadius: THEME.radiusSm,
cursor: "pointer",
fontSize: "0.875rem",
fontWeight: activeItem?.id === db.uuid ? 500 : 400,
color: activeItem?.id === db.uuid ? THEME.ink : THEME.mid,
transition: "all 0.1s ease",
}}
>
{db.name}
</button>
))}
</div>
{deployments.length > 0 && (
<div style={{ padding: "24px 20px 8px" }}>
<h3
style={{
fontSize: "0.7rem",
fontWeight: 600,
color: THEME.muted,
textTransform: "uppercase",
letterSpacing: "0.06em",
margin: 0,
}}
>
Build Logs
</h3>
</div>
)}
<div
style={{
display: "flex",
flexDirection: "column",
gap: 4,
padding: "0 20px",
}}
>
{deployments.map((dep) => (
<button
key={dep.uuid}
onClick={() =>
setActiveItem({
type: "deployment",
id: dep.uuid,
name: dep.name,
})
}
style={{
textAlign: "left",
padding: "8px 12px",
background:
activeItem?.id === dep.uuid
? THEME.subtleBg
: "transparent",
border: "1px solid transparent",
borderRadius: THEME.radiusSm,
cursor: "pointer",
fontSize: "0.875rem",
fontWeight: activeItem?.id === dep.uuid ? 500 : 400,
color:
activeItem?.id === dep.uuid ? THEME.ink : THEME.mid,
transition: "all 0.1s ease",
}}
>
{dep.name}
</button>
))}
</div>
</div> </div>
{/* Log Viewer Column */} {/* Log Viewer Column */}
@@ -294,11 +463,7 @@ export default function LogsPage() {
color: THEME.mid, color: THEME.mid,
}} }}
> >
{activeItem?.type === "app" {activeItem?.name ?? "Logs"}
? (live.find((a) => a.uuid === activeItem.id)?.name ??
"Logs")
: previews.find((p) => p.id === activeItem?.id)?.name ||
"Dev Server Logs"}
</span> </span>
<SecondaryButton <SecondaryButton
icon={ icon={

View File

@@ -41,6 +41,7 @@ import {
getWorkspaceGcsHmacCredentials, getWorkspaceGcsHmacCredentials,
} from "@/lib/workspace-gcs"; } from "@/lib/workspace-gcs";
import { VIBN_GCS_LOCATION } from "@/lib/gcp/storage"; import { VIBN_GCS_LOCATION } from "@/lib/gcp/storage";
import { coolifyFetch } from "@/lib/coolify";
import { getApplicationRuntimeLogs } from "@/lib/coolify-logs"; import { getApplicationRuntimeLogs } from "@/lib/coolify-logs";
import { callVibnChat } from "@/lib/ai/vibn-chat-model"; import { callVibnChat } from "@/lib/ai/vibn-chat-model";
import { execInCoolifyApp } from "@/lib/coolify-exec"; import { execInCoolifyApp } from "@/lib/coolify-exec";
@@ -375,6 +376,10 @@ export async function POST(request: Request) {
return await toolDatabasesUpdate(principal, params); return await toolDatabasesUpdate(principal, params);
case "databases.delete": case "databases.delete":
return await toolDatabasesDelete(principal, params); return await toolDatabasesDelete(principal, params);
case "databases.logs":
return await toolDatabasesLogs(principal, params);
case "deployments.logs":
return await toolDeploymentsLogs(principal, params);
case "auth.list": case "auth.list":
return await toolAuthList(principal); return await toolAuthList(principal);
@@ -5270,6 +5275,67 @@ async function toolFsGrep(principal: Principal, params: Record<string, any>) {
}); });
} }
async function toolDatabasesLogs(
principal: Principal,
params: Record<string, any>,
) {
const projectUuid = requireCoolifyProject(principal);
if (projectUuid instanceof NextResponse) return projectUuid;
const ownedUuids = await getOwnedCoolifyProjectUuids(principal.workspace);
const uuid = String(params.uuid ?? "").trim();
if (!uuid)
return NextResponse.json(
{ error: 'Param "uuid" is required' },
{ status: 400 },
);
await getDatabaseInWorkspace(uuid, ownedUuids);
const linesRaw = Number(params.lines ?? 200);
const lines = Number.isFinite(linesRaw) ? linesRaw : 200;
try {
const cmd = `cid=$(docker ps -a --filter name=${uuid} --format '{{.Names}}' | head -1); if [ -z "$cid" ]; then echo "NO_CONTAINER"; exit 0; fi; docker logs --tail ${lines} "$cid" 2>&1`;
const res = await runOnCoolifyHost(cmd, { timeoutMs: 15_000 });
if (res.code !== 0) {
throw new Error(`docker logs exited ${res.code}: ${res.stderr.trim()}`);
}
const raw =
res.stdout.trim() === "NO_CONTAINER"
? "No running container found for this database."
: res.stdout;
return NextResponse.json({ result: raw });
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
async function toolDeploymentsLogs(
principal: Principal,
params: Record<string, any>,
) {
// Validate workspace
const projectUuid = requireCoolifyProject(principal);
if (projectUuid instanceof NextResponse) return projectUuid;
const uuid = String(params.uuid ?? "").trim();
if (!uuid)
return NextResponse.json(
{ error: 'Param "uuid" is required' },
{ status: 400 },
);
try {
const res = await coolifyFetch(`/deployments/${uuid}/logs`);
return NextResponse.json({
result: res.logs || "No logs available for this deployment.",
});
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
// ── dev_server.* ───────────────────────────────────────────────────── // ── dev_server.* ─────────────────────────────────────────────────────
async function toolDevServerStart( async function toolDevServerStart(

View File

@@ -162,7 +162,7 @@ export interface CoolifyDeployment {
commit?: string; commit?: string;
} }
async function coolifyFetch(path: string, options: RequestInit = {}) { export async function coolifyFetch(path: string, options: RequestInit = {}) {
const url = `${COOLIFY_URL}/api/v1${path}`; const url = `${COOLIFY_URL}/api/v1${path}`;
const res = await fetch(url, { const res = await fetch(url, {
...options, ...options,