mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: add log rotation manager
This commit is contained in:
@@ -64,6 +64,8 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { columns } from "./columns";
|
import { columns } from "./columns";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export type LogEntry = NonNullable<
|
export type LogEntry = NonNullable<
|
||||||
RouterOutputs["settings"]["readMonitoringConfig"]["data"]
|
RouterOutputs["settings"]["readMonitoringConfig"]["data"]
|
||||||
@@ -79,7 +81,15 @@ const chartConfig = {
|
|||||||
},
|
},
|
||||||
} satisfies ChartConfig;
|
} satisfies ChartConfig;
|
||||||
export const ShowRequests = () => {
|
export const ShowRequests = () => {
|
||||||
const { data } = api.settings.readMonitoringConfig.useQuery();
|
const { data } = api.settings.readMonitoringConfig.useQuery(undefined, {
|
||||||
|
refetchInterval: 1000,
|
||||||
|
});
|
||||||
|
const { data: isLogRotateActive, refetch: refetchLogRotate } =
|
||||||
|
api.settings.getLogRotateStatus.useQuery();
|
||||||
|
|
||||||
|
const { mutateAsync } = api.settings.activateLogRotate.useMutation();
|
||||||
|
const { mutateAsync: deactivateLogRotate } =
|
||||||
|
api.settings.deactivateLogRotate.useMutation();
|
||||||
|
|
||||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||||
@@ -112,9 +122,44 @@ export const ShowRequests = () => {
|
|||||||
<Card className="bg-transparent mt-10">
|
<Card className="bg-transparent mt-10">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Request Distribution</CardTitle>
|
<CardTitle>Request Distribution</CardTitle>
|
||||||
<CardDescription>
|
<div className="flex justify-between gap-2">
|
||||||
Showing web and API requests over time
|
<CardDescription>
|
||||||
</CardDescription>
|
<span>Showing web and API requests over time</span>
|
||||||
|
</CardDescription>
|
||||||
|
{!isLogRotateActive && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
mutateAsync()
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Log rotate activated");
|
||||||
|
refetchLogRotate();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.message);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Activate Log Rotate
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLogRotateActive && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
deactivateLogRotate()
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Log rotate deactivated");
|
||||||
|
refetchLogRotate();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.message);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Deactivate Log Rotate
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0033_sweet_black_bird.sql
Normal file
1
apps/dokploy/drizzle/0033_sweet_black_bird.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "admin" ADD COLUMN "enableLogRotation" boolean DEFAULT false NOT NULL;
|
||||||
3078
apps/dokploy/drizzle/meta/0033_snapshot.json
Normal file
3078
apps/dokploy/drizzle/meta/0033_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -232,6 +232,13 @@
|
|||||||
"when": 1723705257806,
|
"when": 1723705257806,
|
||||||
"tag": "0032_flashy_shadow_king",
|
"tag": "0032_flashy_shadow_king",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 33,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1724555040199,
|
||||||
|
"tag": "0033_sweet_black_bird",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
"test": "vitest --config __test__/vitest.config.ts"
|
"test": "vitest --config __test__/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"rotating-file-stream": "3.2.3",
|
||||||
"@aws-sdk/client-s3": "3.515.0",
|
"@aws-sdk/client-s3": "3.515.0",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-yaml": "^6.1.1",
|
"@codemirror/lang-yaml": "^6.1.1",
|
||||||
@@ -180,8 +181,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"commitlint": {
|
"commitlint": {
|
||||||
"extends": [
|
"extends": ["@commitlint/config-conventional"]
|
||||||
"@commitlint/config-conventional"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MAIN_TRAEFIK_PATH, MONITORING_PATH, docker } from "@/server/constants";
|
import { MAIN_TRAEFIK_PATH, MONITORING_PATH } from "@/server/constants";
|
||||||
import {
|
import {
|
||||||
apiAssignDomain,
|
apiAssignDomain,
|
||||||
apiEnableDashboard,
|
apiEnableDashboard,
|
||||||
@@ -50,6 +50,8 @@ import {
|
|||||||
} from "../services/settings";
|
} from "../services/settings";
|
||||||
import { canAccessToTraefikFiles } from "../services/user";
|
import { canAccessToTraefikFiles } from "../services/user";
|
||||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
import { parseRawConfig, processLogs } from "@/server/utils/access-log/utils";
|
||||||
|
import { logRotationManager } from "@/server/utils/access-log/handler";
|
||||||
|
|
||||||
export const settingsRouter = createTRPCRouter({
|
export const settingsRouter = createTRPCRouter({
|
||||||
reloadServer: adminProcedure.mutation(async () => {
|
reloadServer: adminProcedure.mutation(async () => {
|
||||||
@@ -353,105 +355,18 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
const parsedConfig = parseRawConfig(rawConfig as string);
|
const parsedConfig = parseRawConfig(rawConfig as string);
|
||||||
const processedLogs = processLogs(rawConfig as string);
|
const processedLogs = processLogs(rawConfig as string);
|
||||||
return {
|
return {
|
||||||
data: parsedConfig,
|
data: parsedConfig || [],
|
||||||
hourlyData: processedLogs,
|
hourlyData: processedLogs || [],
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
activateLogRotate: adminProcedure.mutation(async () => {
|
||||||
|
await logRotationManager.activate();
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
deactivateLogRotate: adminProcedure.mutation(async () => {
|
||||||
|
return await logRotationManager.deactivate();
|
||||||
|
}),
|
||||||
|
getLogRotateStatus: adminProcedure.query(async () => {
|
||||||
|
return await logRotationManager.getStatus();
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const admins = pgTable("admin", {
|
|||||||
letsEncryptEmail: text("letsEncryptEmail"),
|
letsEncryptEmail: text("letsEncryptEmail"),
|
||||||
sshPrivateKey: text("sshPrivateKey"),
|
sshPrivateKey: text("sshPrivateKey"),
|
||||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||||
|
enableLogRotation: boolean("enableLogRotation").notNull().default(false),
|
||||||
authId: text("authId")
|
authId: text("authId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => auth.id, { onDelete: "cascade" }),
|
.references(() => auth.id, { onDelete: "cascade" }),
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ void app.prepare().then(async () => {
|
|||||||
setupDeploymentLogsWebSocketServer(server);
|
setupDeploymentLogsWebSocketServer(server);
|
||||||
setupDockerContainerLogsWebSocketServer(server);
|
setupDockerContainerLogsWebSocketServer(server);
|
||||||
setupDockerContainerTerminalWebSocketServer(server);
|
setupDockerContainerTerminalWebSocketServer(server);
|
||||||
|
// setupTraefikLogsWebSocketServer(server);
|
||||||
setupTerminalWebSocketServer(server);
|
setupTerminalWebSocketServer(server);
|
||||||
setupDockerStatsMonitoringSocketServer(server);
|
setupDockerStatsMonitoringSocketServer(server);
|
||||||
|
|
||||||
|
|||||||
114
apps/dokploy/server/utils/access-log/handler.ts
Normal file
114
apps/dokploy/server/utils/access-log/handler.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { DYNAMIC_TRAEFIK_PATH } from "@/server/constants";
|
||||||
|
import { createStream, type RotatingFileStream } from "rotating-file-stream";
|
||||||
|
import { execAsync } from "../process/execAsync";
|
||||||
|
import { findAdmin, updateAdmin } from "@/server/api/services/admin";
|
||||||
|
|
||||||
|
class LogRotationManager {
|
||||||
|
private static instance: LogRotationManager;
|
||||||
|
private stream: RotatingFileStream | null = null;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.initialize().catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): LogRotationManager {
|
||||||
|
if (!LogRotationManager.instance) {
|
||||||
|
LogRotationManager.instance = new LogRotationManager();
|
||||||
|
}
|
||||||
|
return LogRotationManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initialize(): Promise<void> {
|
||||||
|
const isActive = await this.getStateFromDB();
|
||||||
|
if (isActive) {
|
||||||
|
await this.activateStream();
|
||||||
|
}
|
||||||
|
console.log(`Log rotation initialized. Active: ${isActive}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getStateFromDB(): Promise<boolean> {
|
||||||
|
const setting = await findAdmin();
|
||||||
|
return setting?.enableLogRotation ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setStateInDB(active: boolean): Promise<void> {
|
||||||
|
const admin = await findAdmin();
|
||||||
|
await updateAdmin(admin.authId, {
|
||||||
|
enableLogRotation: active,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async activateStream(): Promise<void> {
|
||||||
|
if (this.stream) {
|
||||||
|
await this.deactivateStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stream = createStream("access.log", {
|
||||||
|
size: "100M",
|
||||||
|
interval: "1d",
|
||||||
|
path: DYNAMIC_TRAEFIK_PATH,
|
||||||
|
rotate: 6,
|
||||||
|
compress: "gzip",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.stream.on("rotation", this.handleRotation.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deactivateStream(): Promise<void> {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
if (this.stream) {
|
||||||
|
this.stream.end(() => {
|
||||||
|
this.stream = null;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async activate(): Promise<boolean> {
|
||||||
|
const currentState = await this.getStateFromDB();
|
||||||
|
if (currentState) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.setStateInDB(true);
|
||||||
|
await this.activateStream();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deactivate(): Promise<boolean> {
|
||||||
|
console.log("Deactivating log rotation...");
|
||||||
|
const currentState = await this.getStateFromDB();
|
||||||
|
if (!currentState) {
|
||||||
|
console.log("Log rotation is already inactive in DB");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.setStateInDB(false);
|
||||||
|
await this.deactivateStream();
|
||||||
|
console.log("Log rotation deactivated successfully");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleRotation() {
|
||||||
|
try {
|
||||||
|
const status = await this.getStatus();
|
||||||
|
if (!status) {
|
||||||
|
await this.deactivateStream();
|
||||||
|
}
|
||||||
|
await execAsync(
|
||||||
|
"docker kill -s USR1 $(docker ps -q --filter name=traefik)",
|
||||||
|
);
|
||||||
|
console.log("USR1 Signal send to Traefik");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error to send USR1 Signal to Traefik:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public async getStatus(): Promise<boolean> {
|
||||||
|
const dbState = await this.getStateFromDB();
|
||||||
|
return dbState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const logRotationManager = LogRotationManager.getInstance();
|
||||||
48
apps/dokploy/server/utils/access-log/types.ts
Normal file
48
apps/dokploy/server/utils/access-log/types.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
export 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;
|
||||||
|
}
|
||||||
52
apps/dokploy/server/utils/access-log/utils.ts
Normal file
52
apps/dokploy/server/utils/access-log/utils.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
interface LogEntry {
|
||||||
|
StartUTC: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HourlyData {
|
||||||
|
hour: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processLogs(logString: string): HourlyData[] {
|
||||||
|
const hourlyData: Record<string, number> = {};
|
||||||
|
|
||||||
|
if (!logString || logString?.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRawConfig(rawConfig: string): LogEntry[] {
|
||||||
|
try {
|
||||||
|
if (!rawConfig || rawConfig?.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const jsonLines = rawConfig
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => line.trim() !== "");
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -342,6 +342,9 @@ importers:
|
|||||||
recharts:
|
recharts:
|
||||||
specifier: ^2.12.7
|
specifier: ^2.12.7
|
||||||
version: 2.12.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 2.12.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
|
rotating-file-stream:
|
||||||
|
specifier: 3.2.3
|
||||||
|
version: 3.2.3
|
||||||
slugify:
|
slugify:
|
||||||
specifier: ^1.6.6
|
specifier: ^1.6.6
|
||||||
version: 1.6.6
|
version: 1.6.6
|
||||||
@@ -7488,6 +7491,10 @@ packages:
|
|||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
rotating-file-stream@3.2.3:
|
||||||
|
resolution: {integrity: sha512-cfmm3tqdnbuYw2FBmRTPBDaohYEbMJ3211T35o6eZdr4d7v69+ZeK1Av84Br7FLj2dlzyeZSbN6qTuXXE6dawQ==}
|
||||||
|
engines: {node: '>=14.0'}
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|
||||||
@@ -16947,6 +16954,8 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc': 4.19.1
|
'@rollup/rollup-win32-x64-msvc': 4.19.1
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
rotating-file-stream@3.2.3: {}
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
|
|||||||
Reference in New Issue
Block a user