feat(monitoring): add date range filtering and log cleanup scheduling

- Implement date range filtering for access logs and request statistics
- Add log cleanup scheduling with configurable cron expression
- Update UI components to support date range selection
- Refactor log processing and parsing to handle date filtering
- Add new database migration for log cleanup cron configuration
- Remove deprecated log rotation management logic
This commit is contained in:
Mauricio Siu
2025-03-08 14:20:27 -06:00
parent b64ddf1119
commit 673e0a6880
15 changed files with 5620 additions and 202 deletions
@@ -1,10 +1,10 @@
import { api } from "@/utils/api";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { api } from "@/utils/api";
import {
Area,
AreaChart,
@@ -14,6 +14,13 @@ import {
YAxis,
} from "recharts";
export interface RequestDistributionChartProps {
dateRange?: {
from: Date | undefined;
to: Date | undefined;
};
}
const chartConfig = {
views: {
label: "Page Views",
@@ -24,10 +31,22 @@ const chartConfig = {
},
} satisfies ChartConfig;
export const RequestDistributionChart = () => {
const { data: stats } = api.settings.readStats.useQuery(undefined, {
refetchInterval: 1333,
});
export const RequestDistributionChart = ({
dateRange,
}: RequestDistributionChartProps) => {
const { data: stats } = api.settings.readStats.useQuery(
{
dateRange: dateRange
? {
start: dateRange.from?.toISOString(),
end: dateRange.to?.toISOString(),
}
: undefined,
},
{
refetchInterval: 1333,
},
);
return (
<ResponsiveContainer width="100%" height={200}>
@@ -79,7 +79,15 @@ export const priorities = [
icon: Server,
},
];
export const RequestsTable = () => {
export interface RequestsTableProps {
dateRange?: {
from: Date | undefined;
to: Date | undefined;
};
}
export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
const [statusFilter, setStatusFilter] = useState<string[]>([]);
const [search, setSearch] = useState("");
const [selectedRow, setSelectedRow] = useState<LogEntry>();
@@ -98,6 +106,12 @@ export const RequestsTable = () => {
page: pagination,
search,
status: statusFilter,
dateRange: dateRange
? {
start: dateRange.from?.toISOString(),
end: dateRange.to?.toISOString(),
}
: undefined,
},
{
refetchInterval: 1333,
@@ -1,6 +1,7 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Card,
CardContent,
@@ -8,9 +9,29 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { type RouterOutputs, api } from "@/utils/api";
import { ArrowDownUp } from "lucide-react";
import { format } from "date-fns";
import {
ArrowDownUp,
AlertCircle,
InfoIcon,
Calendar as CalendarIcon,
} from "lucide-react";
import Link from "next/link";
import { useState, useEffect } from "react";
import { toast } from "sonner";
import { RequestDistributionChart } from "./request-distribution-chart";
import { RequestsTable } from "./requests-table";
@@ -20,17 +41,30 @@ export type LogEntry = NonNullable<
>[0];
export const ShowRequests = () => {
const { data: isLogRotateActive, refetch: refetchLogRotate } =
api.settings.getLogRotateStatus.useQuery();
const { mutateAsync: toggleLogRotate } =
api.settings.toggleLogRotate.useMutation();
const { data: isActive, refetch } =
api.settings.haveActivateRequests.useQuery();
const { mutateAsync: toggleRequests } =
api.settings.toggleRequests.useMutation();
const { data: logCleanupStatus } =
api.settings.getLogCleanupStatus.useQuery();
const { mutateAsync: updateLogCleanup } =
api.settings.updateLogCleanup.useMutation();
const [cronExpression, setCronExpression] = useState<string | null>(null);
const [dateRange, setDateRange] = useState<{
from: Date | undefined;
to: Date | undefined;
}>({
from: undefined,
to: undefined,
});
useEffect(() => {
if (logCleanupStatus) {
setCronExpression(logCleanupStatus.cronExpression || "0 0 * * *");
}
}, [logCleanupStatus]);
return (
<>
<div className="w-full">
@@ -57,7 +91,60 @@ export const ShowRequests = () => {
</AlertBlock>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
<div className="flex w-full gap-4 justify-end">
<div className="flex w-full gap-4 justify-end items-center">
<div className="flex-1 flex items-center gap-4">
<div className="flex items-center gap-2">
<Label htmlFor="cron" className="min-w-32">
Log Cleanup Schedule
</Label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<InfoIcon className="size-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-80">
At the scheduled time, the cleanup job will keep
only the last 1000 entries in the access log file
and signal Traefik to reopen its log files. The
default schedule is daily at midnight (0 0 * * *).
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex-1 flex gap-4">
<Input
id="cron"
placeholder="0 0 * * *"
value={cronExpression || ""}
onChange={(e) => setCronExpression(e.target.value)}
className="max-w-60"
required
/>
<Button
variant="outline"
onClick={async () => {
if (!cronExpression?.trim()) {
toast.error("Please enter a valid cron expression");
return;
}
try {
await updateLogCleanup({
cronExpression: cronExpression,
});
toast.success("Log cleanup schedule updated");
} catch (error) {
toast.error(
`Failed to update log cleanup schedule: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}}
>
Update Schedule
</Button>
</div>
</div>
<DialogAction
title={isActive ? "Deactivate Requests" : "Activate Requests"}
description="You will also need to restart Traefik to apply the changes"
@@ -77,53 +164,81 @@ export const ShowRequests = () => {
>
<Button>{isActive ? "Deactivate" : "Activate"}</Button>
</DialogAction>
<DialogAction
title={
isLogRotateActive
? "Activate Log Rotate"
: "Deactivate Log Rotate"
}
description={
isLogRotateActive
? "This will make the logs rotate on interval 1 day and maximum size of 100 MB and maximum 6 logs"
: "The log rotation will be disabled"
}
onClick={() => {
toggleLogRotate({
enable: !isLogRotateActive,
})
.then(() => {
toast.success(
`Log rotate ${isLogRotateActive ? "activated" : "deactivated"}`,
);
refetchLogRotate();
})
.catch((err) => {
toast.error(err.message);
});
}}
>
<Button variant="secondary">
{isLogRotateActive
? "Activate Log Rotate"
: "Deactivate Log Rotate"}
</Button>
</DialogAction>
</div>
<div>
{isActive ? (
<RequestDistributionChart />
) : (
<div className="flex items-center justify-center min-h-[25vh]">
<span className="text-muted-foreground py-6">
You need to activate requests
</span>
{isActive ? (
<>
<div className="flex justify-end mb-4 gap-2">
{(dateRange.from || dateRange.to) && (
<Button
variant="outline"
onClick={() =>
setDateRange({ from: undefined, to: undefined })
}
className="px-3"
>
Clear dates
</Button>
)}
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-[300px] justify-start text-left font-normal"
>
<CalendarIcon className="mr-2 h-4 w-4" />
{dateRange.from ? (
dateRange.to ? (
<>
{format(dateRange.from, "LLL dd, y")} -{" "}
{format(dateRange.to, "LLL dd, y")}
</>
) : (
format(dateRange.from, "LLL dd, y")
)
) : (
<span>Pick a date range</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end">
<Calendar
initialFocus
mode="range"
defaultMonth={dateRange.from}
selected={{
from: dateRange.from,
to: dateRange.to,
}}
onSelect={(range) => {
setDateRange({
from: range?.from,
to: range?.to,
});
}}
numberOfMonths={2}
/>
</PopoverContent>
</Popover>
</div>
)}
{isActive && <RequestsTable />}
</div>
<RequestDistributionChart dateRange={dateRange} />
<RequestsTable dateRange={dateRange} />
</>
) : (
<div className="flex flex-col items-center justify-center py-12 gap-4 text-muted-foreground">
<AlertCircle className="size-12 text-muted-foreground/50" />
<div className="text-center space-y-2">
<h3 className="text-lg font-medium">
Requests are not activated
</h3>
<p className="text-sm max-w-md">
Activate requests to see incoming traffic statistics and
monitor your application's usage. After activation, you'll
need to reload Traefik for the changes to take effect.
</p>
</div>
</div>
)}
</CardContent>
</div>
</Card>