mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-03 02:32:21 +00:00
Merge branch 'Dokploy:canary' into fix/chaining-canary
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
import { getLogType } from "@/components/dashboard/docker/logs/utils";
|
||||||
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
test("classifies real failures as error", () => {
|
||||||
|
expect(getLogType("Error: connection refused at db:5432").type).toBe("error");
|
||||||
|
expect(getLogType("[ERROR] something went wrong").type).toBe("error");
|
||||||
|
expect(getLogType("Deployment failed").type).toBe("error");
|
||||||
|
expect(
|
||||||
|
getLogType(
|
||||||
|
'NOTICE [Job "sync-1m" (e1305c5b54b1)] Finished in "326ms", failed: true, skipped: false, error: exit code 1',
|
||||||
|
).type,
|
||||||
|
).toBe("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not classify explicit non-error key/values as error (#4538)", () => {
|
||||||
|
// ofelia job-completion summary for a successful run
|
||||||
|
expect(
|
||||||
|
getLogType(
|
||||||
|
'NOTICE [Job "sync-1m" (e1305c5b54b1)] Finished in "326.16795ms", failed: false, skipped: false, error: none',
|
||||||
|
).type,
|
||||||
|
).not.toBe("error");
|
||||||
|
|
||||||
|
expect(getLogType("request done, error: null").type).not.toBe("error");
|
||||||
|
expect(getLogType("checks passed, failures=0").type).not.toBe("error");
|
||||||
|
expect(getLogType('shutdown clean, error=""').type).not.toBe("error");
|
||||||
|
expect(getLogType('job done failed=false error=""').type).not.toBe("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps errors whose value merely starts with a no-error word", () => {
|
||||||
|
expect(getLogType("connect failed: no route to host").type).toBe("error");
|
||||||
|
expect(getLogType("connection failed: no such host").type).toBe("error");
|
||||||
|
expect(
|
||||||
|
getLogType("error: none of the configured nodes are available").type,
|
||||||
|
).toBe("error");
|
||||||
|
expect(getLogType("error: nil pointer dereference").type).toBe("error");
|
||||||
|
expect(getLogType("request failed: 0 bytes received").type).toBe("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps statusCode-based classification", () => {
|
||||||
|
expect(getLogType('{"statusCode": "500"}').type).toBe("error");
|
||||||
|
expect(getLogType('{"statusCode": "204"}').type).toBe("success");
|
||||||
|
});
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { getLogType } from "@/components/dashboard/docker/logs/utils";
|
||||||
|
|
||||||
|
describe("getLogType", () => {
|
||||||
|
describe("explicit level declared by structured loggers", () => {
|
||||||
|
test("JSON string levels (pino, winston, zap)", () => {
|
||||||
|
expect(getLogType('{"level":"trace","msg":"x"}').type).toBe("debug");
|
||||||
|
expect(getLogType('{"level":"debug","msg":"x"}').type).toBe("debug");
|
||||||
|
expect(getLogType('{"level":"info","msg":"x"}').type).toBe("info");
|
||||||
|
expect(getLogType('{"level":"warn","msg":"x"}').type).toBe("warning");
|
||||||
|
expect(getLogType('{"level":"warning","msg":"x"}').type).toBe("warning");
|
||||||
|
expect(getLogType('{"level":"error","msg":"x"}').type).toBe("error");
|
||||||
|
expect(getLogType('{"level":"fatal","msg":"x"}').type).toBe("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("JSON numeric levels (pino, bunyan)", () => {
|
||||||
|
expect(getLogType('{"level":20,"msg":"x"}').type).toBe("debug");
|
||||||
|
expect(getLogType('{"level":30,"msg":"x"}').type).toBe("info");
|
||||||
|
expect(getLogType('{"level":40,"msg":"x"}').type).toBe("warning");
|
||||||
|
expect(getLogType('{"level":50,"msg":"x"}').type).toBe("error");
|
||||||
|
expect(getLogType('{"level":60,"msg":"x"}').type).toBe("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("syslog/GELF numeric levels", () => {
|
||||||
|
expect(getLogType('{"level":3,"msg":"x"}').type).toBe("error");
|
||||||
|
expect(getLogType('{"level":4,"msg":"x"}').type).toBe("warning");
|
||||||
|
expect(getLogType('{"level":6,"msg":"x"}').type).toBe("info");
|
||||||
|
expect(getLogType('{"level":7,"msg":"x"}').type).toBe("debug");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("GCP-style severity and ECS log.level", () => {
|
||||||
|
expect(getLogType('{"severity":"ERROR","message":"x"}').type).toBe(
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
expect(getLogType('{"severity":"WARNING","message":"x"}').type).toBe(
|
||||||
|
"warning",
|
||||||
|
);
|
||||||
|
expect(getLogType('{"log.level":"error","message":"x"}').type).toBe(
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logfmt levels", () => {
|
||||||
|
expect(
|
||||||
|
getLogType('ts=2026-06-12T10:00:00Z level=error msg="boom"').type,
|
||||||
|
).toBe("error");
|
||||||
|
expect(
|
||||||
|
getLogType('ts=2026-06-12T10:00:00Z level=info msg="ok"').type,
|
||||||
|
).toBe("info");
|
||||||
|
expect(getLogType("level=warn msg=careful").type).toBe("warning");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("declared level wins over keywords in the message (#4589, #1996)", () => {
|
||||||
|
// "version"/"GET" in this pino line would otherwise match the debug keywords
|
||||||
|
const pinoError =
|
||||||
|
'{"level":"error","version":"72b4450","method":"GET","path":"/api/campaigns","err":{"type":"ForbiddenError","stack":"ForbiddenError: at requireRole (/app/src/plugins/campaign.plugin.ts:166:15)"},"msg":"Forbidden"}';
|
||||||
|
expect(getLogType(pinoError).type).toBe("error");
|
||||||
|
|
||||||
|
// info line containing error-like keywords (#4589)
|
||||||
|
expect(
|
||||||
|
getLogType(
|
||||||
|
'{"level":"info","msg":"Failed to open mempool file. Continuing anyway."}',
|
||||||
|
).type,
|
||||||
|
).toBe("info");
|
||||||
|
|
||||||
|
// successful job summary with "failed: false, error: none" (#4538)
|
||||||
|
expect(
|
||||||
|
getLogType(
|
||||||
|
'level=info msg="Finished job, failed: false, skipped: false, error: none"',
|
||||||
|
).type,
|
||||||
|
).toBe("info");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("declared level wins over statusCode", () => {
|
||||||
|
expect(getLogType('{"level":"info","statusCode":500}').type).toBe("info");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unknown level names fall back to keyword detection", () => {
|
||||||
|
expect(
|
||||||
|
getLogType('{"level":"verbose","msg":"connection failed"}').type,
|
||||||
|
).toBe("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("env-var-like text is not treated as logfmt level", () => {
|
||||||
|
expect(getLogType("LOG_LEVEL=error NODE_ENV=production").type).not.toBe(
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fallback detection for unstructured logs (unchanged)", () => {
|
||||||
|
test("statusCode classification", () => {
|
||||||
|
expect(getLogType('{"statusCode":500,"msg":"x"}').type).toBe("error");
|
||||||
|
expect(getLogType('{"statusCode":404,"msg":"x"}').type).toBe("warning");
|
||||||
|
expect(getLogType('{"statusCode":200,"msg":"x"}').type).toBe("success");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keyword classification", () => {
|
||||||
|
expect(getLogType("error: something broke").type).toBe("error");
|
||||||
|
expect(getLogType("warning: disk almost full").type).toBe("warning");
|
||||||
|
expect(getLogType("Server listening on port 8080").type).toBe("success");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("defaults to info", () => {
|
||||||
|
expect(getLogType("hello world").type).toBe("info");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -88,6 +88,7 @@ const mySchema = z.discriminatedUnion("buildType", [
|
|||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal(BuildType.nixpacks),
|
buildType: z.literal(BuildType.nixpacks),
|
||||||
publishDirectory: z.string().optional(),
|
publishDirectory: z.string().optional(),
|
||||||
|
isStaticSpa: z.boolean().default(false),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal(BuildType.railpack),
|
buildType: z.literal(BuildType.railpack),
|
||||||
@@ -138,6 +139,7 @@ const resetData = (data: ApplicationData): AddTemplate => {
|
|||||||
return {
|
return {
|
||||||
buildType: BuildType.nixpacks,
|
buildType: BuildType.nixpacks,
|
||||||
publishDirectory: data.publishDirectory || undefined,
|
publishDirectory: data.publishDirectory || undefined,
|
||||||
|
isStaticSpa: data.isStaticSpa ?? false,
|
||||||
};
|
};
|
||||||
case BuildType.paketo_buildpacks:
|
case BuildType.paketo_buildpacks:
|
||||||
return {
|
return {
|
||||||
@@ -179,6 +181,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
|
|
||||||
const buildType = form.watch("buildType");
|
const buildType = form.watch("buildType");
|
||||||
const railpackVersion = form.watch("railpackVersion");
|
const railpackVersion = form.watch("railpackVersion");
|
||||||
|
const publishDirectory = form.watch("publishDirectory");
|
||||||
const [isManualRailpackVersion, setIsManualRailpackVersion] = useState(false);
|
const [isManualRailpackVersion, setIsManualRailpackVersion] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -224,7 +227,10 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
? data.herokuVersion
|
? data.herokuVersion
|
||||||
: null,
|
: null,
|
||||||
isStaticSpa:
|
isStaticSpa:
|
||||||
data.buildType === BuildType.static ? data.isStaticSpa : null,
|
data.buildType === BuildType.static ||
|
||||||
|
data.buildType === BuildType.nixpacks
|
||||||
|
? data.isStaticSpa
|
||||||
|
: null,
|
||||||
railpackVersion:
|
railpackVersion:
|
||||||
data.buildType === BuildType.railpack
|
data.buildType === BuildType.railpack
|
||||||
? data.railpackVersion || "0.15.4"
|
? data.railpackVersion || "0.15.4"
|
||||||
@@ -419,6 +425,30 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{buildType === BuildType.nixpacks && publishDirectory && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isStaticSpa"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex items-center gap-x-2 p-2">
|
||||||
|
<Checkbox
|
||||||
|
id="checkboxIsStaticSpaNixpacks"
|
||||||
|
value={String(field.value)}
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
<FormLabel htmlFor="checkboxIsStaticSpaNixpacks">
|
||||||
|
Single Page Application (SPA)
|
||||||
|
</FormLabel>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{buildType === BuildType.static && (
|
{buildType === BuildType.static && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -72,8 +72,68 @@ export function parseLogs(logString: string): LogLine[] {
|
|||||||
.filter((log) => log !== null);
|
.filter((log) => log !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LEVEL_NAME_TO_TYPE: Record<string, LogType> = {
|
||||||
|
trace: "debug",
|
||||||
|
debug: "debug",
|
||||||
|
info: "info",
|
||||||
|
information: "info",
|
||||||
|
notice: "info",
|
||||||
|
warn: "warning",
|
||||||
|
warning: "warning",
|
||||||
|
error: "error",
|
||||||
|
err: "error",
|
||||||
|
fatal: "error",
|
||||||
|
critical: "error",
|
||||||
|
panic: "error",
|
||||||
|
alert: "error",
|
||||||
|
emergency: "error",
|
||||||
|
};
|
||||||
|
|
||||||
|
const numericLevelToType = (level: number): LogType => {
|
||||||
|
// pino/bunyan scale: 10=trace 20=debug 30=info 40=warn 50=error 60=fatal
|
||||||
|
if (level >= 50) return "error";
|
||||||
|
if (level >= 40) return "warning";
|
||||||
|
if (level >= 30) return "info";
|
||||||
|
if (level >= 10) return "debug";
|
||||||
|
// syslog/GELF scale: 0=emergency ... 7=debug
|
||||||
|
if (level <= 3) return "error";
|
||||||
|
if (level === 4) return "warning";
|
||||||
|
if (level <= 6) return "info";
|
||||||
|
return "debug";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract the log level explicitly declared by structured loggers
|
||||||
|
// (pino, bunyan, winston, zap, slog, logfmt, GCP severity)
|
||||||
|
const getExplicitLevelType = (message: string): LogType | null => {
|
||||||
|
// JSON string levels: {"level":"error"} / {"severity":"ERROR"} / {"log.level":"warn"}
|
||||||
|
const jsonStringMatch = message.match(
|
||||||
|
/"(?:level|severity|log\.level|loglevel)"\s*:\s*"([a-z]+)"/i,
|
||||||
|
);
|
||||||
|
if (jsonStringMatch?.[1]) {
|
||||||
|
return LEVEL_NAME_TO_TYPE[jsonStringMatch[1].toLowerCase()] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON numeric levels: {"level":50}
|
||||||
|
const jsonNumericMatch = message.match(/"level"\s*:\s*(\d{1,2})\b/);
|
||||||
|
if (jsonNumericMatch?.[1]) {
|
||||||
|
return numericLevelToType(Number(jsonNumericMatch[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// logfmt: level=error
|
||||||
|
const logfmtMatch = message.match(/(?:^|\s)(?:level|severity)=([a-z]+)\b/i);
|
||||||
|
if (logfmtMatch?.[1]) {
|
||||||
|
return LEVEL_NAME_TO_TYPE[logfmtMatch[1].toLowerCase()] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
// Detect log type based on message content
|
// Detect log type based on message content
|
||||||
export const getLogType = (message: string): LogStyle => {
|
export const getLogType = (message: string): LogStyle => {
|
||||||
|
// A level explicitly declared by the logger wins over any inference
|
||||||
|
const explicitType = getExplicitLevelType(message);
|
||||||
|
if (explicitType) return LOG_STYLES[explicitType];
|
||||||
|
|
||||||
// Detect HTTP statusCode
|
// Detect HTTP statusCode
|
||||||
const statusMatch = message.match(/"statusCode"\s*:\s*"?(\d{3})"?/);
|
const statusMatch = message.match(/"statusCode"\s*:\s*"?(\d{3})"?/);
|
||||||
|
|
||||||
@@ -97,17 +157,27 @@ export const getLogType = (message: string): LogStyle => {
|
|||||||
return LOG_STYLES.info;
|
return LOG_STYLES.info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Key/value pairs that explicitly report a non-error (e.g. "error: none",
|
||||||
|
// "failed: false") must not trigger the error keyword patterns below
|
||||||
|
const nonErrorColonPairs =
|
||||||
|
/\b(?:error|err|errors|failed|failure|failures)\s*:\s*(?:none|null|nil|false|0|no|-|""|'')(?=[,;.)\]]|$)/gi;
|
||||||
|
const nonErrorLogfmtPairs =
|
||||||
|
/\b(?:error|err|errors|failed|failure|failures)\s*=\s*(?:none|null|nil|false|0|no|-|""|'')(?=[\s,;.)\]]|$)/gi;
|
||||||
|
const errorScope = lowerMessage
|
||||||
|
.replace(nonErrorColonPairs, "")
|
||||||
|
.replace(nonErrorLogfmtPairs, "");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
/(?:^|\s)(?:error|err):?\s/i.test(lowerMessage) ||
|
/(?:^|\s)(?:error|err):?\s/i.test(errorScope) ||
|
||||||
/\b(?:exception|failed|failure)\b/i.test(lowerMessage) ||
|
/\b(?:exception|failed|failure)\b/i.test(errorScope) ||
|
||||||
/(?:stack\s?trace):\s*$/i.test(lowerMessage) ||
|
/(?:stack\s?trace):\s*$/i.test(errorScope) ||
|
||||||
/^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(lowerMessage) ||
|
/^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(errorScope) ||
|
||||||
/\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(lowerMessage) ||
|
/\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(errorScope) ||
|
||||||
/Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(lowerMessage) ||
|
/Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(errorScope) ||
|
||||||
/\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(lowerMessage) ||
|
/\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(errorScope) ||
|
||||||
/\[(?:error|err|fatal)\]/i.test(lowerMessage) ||
|
/\[(?:error|err|fatal)\]/i.test(errorScope) ||
|
||||||
/\b(?:crash|critical|fatal)\b/i.test(lowerMessage) ||
|
/\b(?:crash|critical|fatal)\b/i.test(errorScope) ||
|
||||||
/\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(lowerMessage)
|
/\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(errorScope)
|
||||||
) {
|
) {
|
||||||
return LOG_STYLES.error;
|
return LOG_STYLES.error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { formatMb, toMb } from "@dokploy/server/monitoring/units";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts";
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts";
|
||||||
import {
|
import {
|
||||||
@@ -29,8 +30,8 @@ export const DockerBlockChart = ({ accumulativeData }: Props) => {
|
|||||||
const transformedData = accumulativeData.map((item, index) => ({
|
const transformedData = accumulativeData.map((item, index) => ({
|
||||||
time: item.time,
|
time: item.time,
|
||||||
name: `Point ${index + 1}`,
|
name: `Point ${index + 1}`,
|
||||||
readMb: item.value.readMb,
|
readMb: toMb(item.value.readMb),
|
||||||
writeMb: item.value.writeMb,
|
writeMb: toMb(item.value.writeMb),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -77,7 +78,7 @@ export const DockerBlockChart = ({ accumulativeData }: Props) => {
|
|||||||
}}
|
}}
|
||||||
formatter={(value, name) => {
|
formatter={(value, name) => {
|
||||||
const label = name === "readMb" ? "Read" : "Write";
|
const label = name === "readMb" ? "Read" : "Write";
|
||||||
return [`${value} MB`, label];
|
return [formatMb(Number(value)), label];
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -1,3 +1,4 @@
|
|||||||
|
import { formatMb, toMb } from "@dokploy/server/monitoring/units";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts";
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts";
|
||||||
import {
|
import {
|
||||||
@@ -29,8 +30,8 @@ export const DockerNetworkChart = ({ accumulativeData }: Props) => {
|
|||||||
const transformedData = accumulativeData.map((item, index) => ({
|
const transformedData = accumulativeData.map((item, index) => ({
|
||||||
time: item.time,
|
time: item.time,
|
||||||
name: `Point ${index + 1}`,
|
name: `Point ${index + 1}`,
|
||||||
inMB: item.value.inputMb,
|
inMB: toMb(item.value.inputMb),
|
||||||
outMB: item.value.outputMb,
|
outMB: toMb(item.value.outputMb),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -73,7 +74,7 @@ export const DockerNetworkChart = ({ accumulativeData }: Props) => {
|
|||||||
}}
|
}}
|
||||||
formatter={(value, name) => {
|
formatter={(value, name) => {
|
||||||
const label = name === "inMB" ? "In" : "Out";
|
const label = name === "inMB" ? "In" : "Out";
|
||||||
return [`${value} MB`, label];
|
return [formatMb(Number(value)), label];
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -1,3 +1,4 @@
|
|||||||
|
import { formatMb } from "@dokploy/server/monitoring/units";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
@@ -305,7 +306,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{`Read: ${currentData.block.value.readMb} / Write: ${currentData.block.value.writeMb} `}
|
{`Read: ${formatMb(currentData.block.value.readMb)} / Write: ${formatMb(currentData.block.value.writeMb)}`}
|
||||||
</span>
|
</span>
|
||||||
<DockerBlockChart accumulativeData={accumulativeData.block} />
|
<DockerBlockChart accumulativeData={accumulativeData.block} />
|
||||||
</div>
|
</div>
|
||||||
@@ -318,7 +319,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{`In MB: ${currentData.network.value.inputMb} / Out MB: ${currentData.network.value.outputMb} `}
|
{`In: ${formatMb(currentData.network.value.inputMb)} / Out: ${formatMb(currentData.network.value.outputMb)}`}
|
||||||
</span>
|
</span>
|
||||||
<DockerNetworkChart accumulativeData={accumulativeData.network} />
|
<DockerNetworkChart accumulativeData={accumulativeData.network} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const ShowRequests = () => {
|
|||||||
const [dateRange, setDateRange] = useState<{
|
const [dateRange, setDateRange] = useState<{
|
||||||
from: Date | undefined;
|
from: Date | undefined;
|
||||||
to: Date | undefined;
|
to: Date | undefined;
|
||||||
}>(getDefaultDateRange());
|
}>(() => getDefaultDateRange());
|
||||||
|
|
||||||
// Check if logs exist to determine if traefik has been reloaded
|
// Check if logs exist to determine if traefik has been reloaded
|
||||||
// Only fetch when active to minimize network calls
|
// Only fetch when active to minimize network calls
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ export const TerminalModal = ({
|
|||||||
serverId,
|
serverId,
|
||||||
asButton = false,
|
asButton = false,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [terminalKey, setTerminalKey] = useState<string>(getTerminalKey());
|
const [terminalKey, setTerminalKey] = useState<string>(() =>
|
||||||
|
getTerminalKey(),
|
||||||
|
);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const isLocalServer = serverId === "local";
|
const isLocalServer = serverId === "local";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
const IO_UNIT_FACTORS: Record<string, number> = {
|
||||||
|
B: 1 / 1e6,
|
||||||
|
kB: 1e-3,
|
||||||
|
KB: 1e-3,
|
||||||
|
MB: 1,
|
||||||
|
GB: 1e3,
|
||||||
|
TB: 1e6,
|
||||||
|
KiB: 1024 / 1e6,
|
||||||
|
MiB: 1024 ** 2 / 1e6,
|
||||||
|
GiB: 1024 ** 3 / 1e6,
|
||||||
|
TiB: 1024 ** 4 / 1e6,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toMb = (value: number | string | undefined): number => {
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (!value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const parsed = Number.parseFloat(value);
|
||||||
|
if (Number.isNaN(parsed)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const unit = value.replace(/[0-9.\s]/g, "");
|
||||||
|
return parsed * (IO_UNIT_FACTORS[unit] ?? 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseIoToMb = (raw: string | undefined): number =>
|
||||||
|
Number(toMb(raw).toFixed(6));
|
||||||
|
|
||||||
|
export const formatMb = (value: number | string | undefined): string => {
|
||||||
|
const mb = toMb(value);
|
||||||
|
if (mb >= 1000) {
|
||||||
|
return `${(mb / 1000).toFixed(2)} GB`;
|
||||||
|
}
|
||||||
|
if (mb < 0.01 && mb > 0) {
|
||||||
|
return `${(mb * 1000).toFixed(2)} kB`;
|
||||||
|
}
|
||||||
|
return `${mb.toFixed(2)} MB`;
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { promises } from "node:fs";
|
import { promises } from "node:fs";
|
||||||
import { OSUtils } from "node-os-utils";
|
import { OSUtils } from "node-os-utils";
|
||||||
import { paths } from "../constants";
|
import { paths } from "../constants";
|
||||||
|
import { parseIoToMb } from "./units";
|
||||||
|
|
||||||
export interface Container {
|
export interface Container {
|
||||||
BlockIO: string;
|
BlockIO: string;
|
||||||
@@ -28,13 +29,13 @@ export const recordAdvancedStats = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
await updateStatsFile(appName, "block", {
|
await updateStatsFile(appName, "block", {
|
||||||
readMb: stats.BlockIO.split(" ")[0],
|
readMb: parseIoToMb(stats.BlockIO.split(" ")[0]),
|
||||||
writeMb: stats.BlockIO.split(" ")[2],
|
writeMb: parseIoToMb(stats.BlockIO.split(" ")[2]),
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateStatsFile(appName, "network", {
|
await updateStatsFile(appName, "network", {
|
||||||
inputMb: stats.NetIO.split(" ")[0],
|
inputMb: parseIoToMb(stats.NetIO.split(" ")[0]),
|
||||||
outputMb: stats.NetIO.split(" ")[2],
|
outputMb: parseIoToMb(stats.NetIO.split(" ")[2]),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (appName === "dokploy") {
|
if (appName === "dokploy") {
|
||||||
|
|||||||
@@ -482,7 +482,7 @@ const installUtilities = () => `
|
|||||||
|
|
||||||
case "$OS_TYPE" in
|
case "$OS_TYPE" in
|
||||||
arch)
|
arch)
|
||||||
$SUDO_CMD pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true
|
$SUDO_CMD pacman -Sy --noconfirm --needed unzip curl wget git git-lfs jq openssl >/dev/null || true
|
||||||
;;
|
;;
|
||||||
alpine)
|
alpine)
|
||||||
$SUDO_CMD sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
$SUDO_CMD sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||||
|
|||||||
@@ -220,6 +220,17 @@ export const addDomainToCompose = async (
|
|||||||
labels.unshift("traefik.swarm.network=dokploy-network");
|
labels.unshift("traefik.swarm.network=dokploy-network");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const isolatedNetwork = compose.suffix || compose.appName;
|
||||||
|
if (compose.composeType === "docker-compose") {
|
||||||
|
if (!labels.includes(`traefik.docker.network=${isolatedNetwork}`)) {
|
||||||
|
labels.unshift(`traefik.docker.network=${isolatedNetwork}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!labels.includes(`traefik.swarm.network=${isolatedNetwork}`)) {
|
||||||
|
labels.unshift(`traefik.swarm.network=${isolatedNetwork}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export const runCommand = async (scheduleId: string) => {
|
|||||||
const { SCHEDULES_PATH } = paths(true);
|
const { SCHEDULES_PATH } = paths(true);
|
||||||
const fullPath = path.join(SCHEDULES_PATH, appName || "");
|
const fullPath = path.join(SCHEDULES_PATH, appName || "");
|
||||||
const command = `
|
const command = `
|
||||||
set -e
|
set -euo pipefail
|
||||||
echo "Running script" >> ${deployment.logPath};
|
echo "Running script" >> ${deployment.logPath};
|
||||||
bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
|
bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
|
||||||
echo "❌ Command failed" >> ${deployment.logPath};
|
echo "❌ Command failed" >> ${deployment.logPath};
|
||||||
|
|||||||
Reference in New Issue
Block a user