feat: add requests

This commit is contained in:
Mauricio Siu 2024-08-20 00:15:08 -06:00
parent afbe42a577
commit ac1637eaf8
10 changed files with 1070 additions and 11 deletions

View File

@ -0,0 +1,105 @@
import type { ColumnDef } from "@tanstack/react-table";
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Badge } from "@/components/ui/badge";
import type { LogEntry } from "./show-requests";
export const columns: ColumnDef<LogEntry>[] = [
{
accessorKey: "level",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Level
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => {
return <div>{row.original.level}</div>;
},
},
{
accessorKey: "RequestPath",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Message
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => {
const log = row.original;
return (
<div className=" flex flex-col gap-2">
<div>
{log.RequestMethod} {log.RequestPath}
</div>
<div className="flex flex-row gap-3 w-full">
<Badge
variant={log.OriginStatus <= 200 ? "default" : "destructive"}
>
Status: {log.OriginStatus}
</Badge>
<Badge variant={"secondary"}>
Exec Time: {convertMicroseconds(log.Duration)}
</Badge>
<Badge variant={"secondary"}>IP: {log.ClientAddr}</Badge>
</div>
</div>
);
},
},
{
id: "actions",
enableHiding: false,
cell: ({ row }) => {
const container = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
{/* <ShowDockerModalLogs containerId={container.containerId}>
View Logs
</ShowDockerModalLogs>
<ShowContainerConfig containerId={container.containerId} />
<DockerTerminalModal containerId={container.containerId}>
Terminal
</DockerTerminalModal> */}
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
function convertMicroseconds(microseconds: number): string {
if (microseconds < 1000) {
return `${microseconds} µs`;
}
if (microseconds < 1000000) {
return `${(microseconds / 1000).toFixed(2)} ms`;
}
return `${(microseconds / 1000000).toFixed(2)} s`;
}

View File

@ -0,0 +1,300 @@
import { api, type RouterOutputs } from "@/utils/api";
import {
type ColumnFiltersState,
type SortingState,
type VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import * as React from "react";
import {
Area,
AreaChart,
Bar,
BarChart,
CartesianGrid,
ResponsiveContainer,
XAxis,
YAxis,
} from "recharts";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ChevronDown, MoreHorizontal, TrendingUp } from "lucide-react";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { columns } from "./columns";
import { Input } from "@/components/ui/input";
export type LogEntry = NonNullable<
RouterOutputs["settings"]["readMonitoringConfig"]["data"]
>[0];
const chartConfig = {
views: {
label: "Page Views",
},
count: {
label: "Count",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;
export const ShowRequests = () => {
const { data } = api.settings.readMonitoringConfig.useQuery();
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const table = useReactTable({
data: data?.data ?? [],
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
});
return (
<>
<Card className="bg-transparent mt-10">
<CardHeader>
<CardTitle>Request Distribution</CardTitle>
<CardDescription>
Showing web and API requests over time
</CardDescription>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={200}>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={data?.hourlyData || []}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="hour"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) =>
new Date(value).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})
}
/>
<YAxis tickLine={false} axisLine={false} tickMargin={8} />
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="line" />}
labelFormatter={(value) =>
new Date(value).toLocaleString([], {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})
}
/>
<Area
dataKey="count"
type="natural"
fill="hsl(var(--chart-1))"
fillOpacity={0.4}
stroke="hsl(var(--chart-1))"
/>
</AreaChart>
</ChartContainer>
</ResponsiveContainer>
</CardContent>
</Card>
<div className="flex flex-col gap-6 w-full">
<div className="mt-6 grid gap-4 pb-20 w-full">
<div className="flex flex-col gap-4 w-full overflow-auto">
<div className="flex items-center gap-2 max-sm:flex-wrap">
<Input
placeholder="Filter by name..."
value={
(table
.getColumn("RequestPath")
?.getFilterValue() as string) ?? ""
}
onChange={(event) =>
table
.getColumn("RequestPath")
?.setFilterValue(event.target.value)
}
className="md:max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="sm:ml-auto max-sm:w-full"
>
Columns <ChevronDown className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
{/* {isLoading ? (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
</div>
) : (
<>No results.</>
)} */}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<div className="space-x-2 flex flex-wrap">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
</div>
</div>
</>
);
};

View File

@ -19,6 +19,7 @@ export type TabState =
| "monitoring"
| "settings"
| "traefik"
| "requests"
| "docker";
const tabMap: Record<TabState, TabInfo> = {
@ -49,6 +50,14 @@ const tabMap: Record<TabState, TabInfo> = {
return Boolean(rol === "admin" || user?.canAccessToDocker);
},
},
requests: {
label: "Requests",
description: "Manage your requests",
index: "/dashboard/requests",
// isShow: ({ rol, user }) => {
// return Boolean(rol === "admin" || user?.canAccessToDocker);
// },
},
settings: {
label: "Settings",
description: "Manage your settings",

View File

@ -0,0 +1,363 @@
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

View File

@ -114,7 +114,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.49.3",
"recharts": "^2.12.3",
"recharts": "^2.12.7",
"slugify": "^1.6.6",
"sonner": "^1.4.0",
"superjson": "^2.2.1",
@ -180,6 +180,8 @@
]
},
"commitlint": {
"extends": ["@commitlint/config-conventional"]
"extends": [
"@commitlint/config-conventional"
]
}
}

View File

@ -0,0 +1,153 @@
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { validateRequest } from "@/server/auth/auth";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import * as React from "react";
import { ShowRequests } from "@/components/dashboard/requests/show-requests";
export default function Requests() {
return (
<>
{/* <Card className="bg-transparent mt-10">
<CardHeader>
<CardTitle>Request Distribution</CardTitle>
<CardDescription>
Showing web and API requests over time
</CardDescription>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={200}>
<ChartContainer config={chartConfig}>
<AreaChart
accessibilityLayer
data={data?.hourlyData || []}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="hour"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) =>
new Date(value).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})
}
/>
<YAxis tickLine={false} axisLine={false} tickMargin={8} />
<ChartTooltip
cursor={false}
content={<ChartTooltipContent indicator="line" />}
labelFormatter={(value) =>
new Date(value).toLocaleString([], {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})
}
/>
<Area
dataKey="count"
type="natural"
fill="hsl(var(--chart-1))"
fillOpacity={0.4}
stroke="hsl(var(--chart-1))"
/>
</AreaChart>
</ChartContainer>
</ResponsiveContainer>
</CardContent>
</Card> */}
{/* <div className="flex flex-col gap-6">
<Table>
<TableCaption>See all users</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Level</TableHead>
<TableHead className="text-center">Message</TableHead>
<TableHead className="text-center">Created</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.data?.map((log, index) => {
return (
<TableRow
key={`${log.time}-${index}`}
onClick={() => console.log(log)}
>
<TableCell className="text-center">
<Badge variant={"secondary"}>{log.level}</Badge>
</TableCell>
<TableCell className=" flex flex-col gap-2">
<div>
{log.RequestMethod} {log.RequestPath}
</div>
<div className="flex flex-row gap-3 w-full">
<Badge variant={"secondary"}>
Status: {log.OriginStatus}
</Badge>
<Badge variant={"secondary"}>
Exec Time: {log.Duration} ms
</Badge>
<Badge variant={"secondary"}>{log.ClientAddr}</Badge>
</div>
</TableCell>
<TableCell className="text-center">
{new Date(log.time).toLocaleString([], {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
</TableCell>
<TableCell className="text-right flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div> */}
<ShowRequests />
</>
);
}
Requests.getLayout = (page: ReactElement) => {
return <DashboardLayout tab={"requests"}>{page}</DashboardLayout>;
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user } = await validateRequest(ctx.req, ctx.res);
if (!user) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
return {
props: {},
};
}

View File

@ -26,6 +26,7 @@ import { spawnAsync } from "@/server/utils/process/spawnAsync";
import {
readConfig,
readConfigInPath,
readMonitoringConfig,
writeConfig,
writeTraefikConfigInPath,
} from "@/server/utils/traefik/application";
@ -346,4 +347,111 @@ export const settingsRouter = createTRPCRouter({
return false;
}),
readMonitoringConfig: adminProcedure.query(() => {
const rawConfig = readMonitoringConfig();
const parsedConfig = parseRawConfig(rawConfig as string);
const processedLogs = processLogs(rawConfig as string);
return {
data: parsedConfig,
hourlyData: processedLogs,
};
}),
});
interface LogEntry {
StartUTC: string;
// Otros campos relevantes...
}
interface HourlyData {
hour: string;
count: number;
}
function processLogs(logString: string): HourlyData[] {
const hourlyData: Record<string, number> = {};
const logEntries = logString.trim().split("\n");
for (const entry of logEntries) {
try {
const log: LogEntry = JSON.parse(entry);
const date = new Date(log.StartUTC);
const hourKey = `${date.toISOString().slice(0, 13)}:00:00Z`; // Agrupa por hora
hourlyData[hourKey] = (hourlyData[hourKey] || 0) + 1;
} catch (error) {
console.error("Error parsing log entry:", error);
}
}
return Object.entries(hourlyData)
.map(([hour, count]) => ({ hour, count }))
.sort((a, b) => new Date(a.hour).getTime() - new Date(b.hour).getTime());
}
interface LogEntry {
ClientAddr: string;
ClientHost: string;
ClientPort: string;
ClientUsername: string;
DownstreamContentSize: number;
DownstreamStatus: number;
Duration: number;
OriginContentSize: number;
OriginDuration: number;
OriginStatus: number;
Overhead: number;
RequestAddr: string;
RequestContentSize: number;
RequestCount: number;
RequestHost: string;
RequestMethod: string;
RequestPath: string;
RequestPort: string;
RequestProtocol: string;
RequestScheme: string;
RetryAttempts: number;
RouterName: string;
ServiceAddr: string;
ServiceName: string;
ServiceURL: {
Scheme: string;
Opaque: string;
User: null;
Host: string;
Path: string;
RawPath: string;
ForceQuery: boolean;
RawQuery: string;
Fragment: string;
RawFragment: string;
};
StartLocal: string;
StartUTC: string;
downstream_Content_Type: string;
entryPointName: string;
level: string;
msg: string;
origin_Content_Type: string;
request_Content_Type: string;
request_User_Agent: string;
time: string;
}
function parseRawConfig(rawConfig: string): LogEntry[] {
try {
// Dividir el string en líneas y filtrar las líneas vacías
const jsonLines = rawConfig
.split("\n")
.filter((line) => line.trim() !== "");
// Parsear cada línea como un objeto JSON
const parsedConfig = jsonLines.map((line) => JSON.parse(line) as LogEntry);
return parsedConfig;
} catch (error) {
console.error("Error parsing rawConfig:", error);
throw new Error("Failed to parse rawConfig");
}
}

View File

@ -76,6 +76,15 @@ export const readConfig = (appName: string) => {
return null;
};
export const readMonitoringConfig = () => {
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log");
if (fs.existsSync(configPath)) {
const yamlStr = fs.readFileSync(configPath, "utf8");
return yamlStr;
}
return null;
};
export const readConfigInPath = (pathFile: string) => {
const configPath = path.join(pathFile);
if (fs.existsSync(configPath)) {

View File

@ -34,6 +34,12 @@
--radius: 0.5rem;
--overlay: rgba(0, 0, 0, 0.2);
--chart-1: 173 58% 39%;
--chart-2: 12 76% 61%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
@ -65,7 +71,13 @@
--input: 240 4% 10%;
--ring: 240 4.9% 83.9%;
--overlay: rgba(0, 0, 0, 0.5);
--overlay: rgba(0, 0, 0, 0.5);
--chart-1: 220 70% 50%;
--chart-5: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-2: 340 75% 55%;
}
}
@ -125,7 +137,6 @@
@apply min-h-[25rem];
}
@keyframes heartbeat {
0% {
transform: scale(1);
@ -157,9 +168,8 @@
background-color: white;
}
.swagger-ui .info{
.swagger-ui .info {
margin: 0px !important;
padding-top: 1rem !important;
}
}
}

View File

@ -340,7 +340,7 @@ importers:
specifier: ^7.49.3
version: 7.52.1(react@18.2.0)
recharts:
specifier: ^2.12.3
specifier: ^2.12.7
version: 2.12.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
slugify:
specifier: ^1.6.6
@ -14040,7 +14040,7 @@ snapshots:
eslint: 8.45.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1)(eslint@8.45.0)
eslint-plugin-jsx-a11y: 6.9.0(eslint@8.45.0)
eslint-plugin-react: 7.35.0(eslint@8.45.0)
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.45.0)
@ -14064,7 +14064,7 @@ snapshots:
enhanced-resolve: 5.17.1
eslint: 8.45.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1)(eslint@8.45.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.5
is-core-module: 2.15.0
@ -14086,7 +14086,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.45.0))(eslint@8.45.0))(eslint@8.45.0):
eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.1.6))(eslint-import-resolver-typescript@3.6.1)(eslint@8.45.0):
dependencies:
array-includes: 3.1.8
array.prototype.findlastindex: 1.2.5