feat: stat graphs
This commit is contained in:
parent
d5caddcb22
commit
3fcdeeb22e
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
# database
|
# database
|
||||||
/data/
|
/data/
|
||||||
|
/logs/
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
2023-11-12T01:48:57.462Z info: Not running database migrations, use drizzle-kit push to migrate
|
|
||||||
2023-11-12T01:48:57.463Z info: 🚀 Hostforge v1.0.0 ready!
|
|
||||||
2023-11-12T01:48:57.469Z info: Server listening on port 3000
|
|
||||||
2023-11-12T01:49:11.893Z warn: SIGTERM received, shutting down...
|
|
||||||
2023-11-12T01:49:17.625Z [32minfo[39m: Not running database migrations, use drizzle-kit push to migrate
|
|
||||||
2023-11-12T01:49:17.627Z [32minfo[39m: 🚀 Hostforge v1.0.0 ready!
|
|
||||||
2023-11-12T01:49:17.635Z [32minfo[39m: Server listening on port 3000
|
|
||||||
2023-11-12T01:50:31.777Z [33mwarn[39m: SIGTERM received, shutting down...
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
|
|
||||||
{"level":"info","message":"Server listening on port 3000"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
|
|
||||||
{"level":"info","message":"Server listening on port 3000"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
|
|
||||||
{"level":"info","message":"Server listening on port 3000"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
|
|
||||||
{"level":"info","message":"Server listening on port 3000"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
|
|
||||||
{"level":"info","message":"Server listening on port 3000"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
|
|
||||||
{"level":"info","message":"Server listening on port 3000"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
|
|
||||||
{"level":"info","message":"Server listening on port 3000"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
|
|
||||||
{"level":"info","message":"Server listening on port 3000"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
|
|
||||||
{"level":"info","message":"Server listening on port 3000"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
|
|
||||||
{"level":"info","message":"Server listening on port 3000"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
|
|
||||||
{"level":"info","message":"Server listening on port 3000"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
|
|
||||||
{"level":"info","message":"Server listening on port 3000"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge v1.0.0 ready!"}
|
|
||||||
{"level":"info","message":"Server listening on port 3000"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge"}
|
|
||||||
{"level":"info","message":"Server listening on localhost:3000"}
|
|
||||||
{"level":"info","message":"Version: 0.1.0"}
|
|
||||||
{"level":"info","message":"Environment: development"}
|
|
||||||
{"level":"info","message":"Build commit: unknown"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge"}
|
|
||||||
{"level":"info","message":" │ Server listening on localhost:3000"}
|
|
||||||
{"level":"info","message":" │ Version: 0.1.0"}
|
|
||||||
{"level":"info","message":" │ Environment: development"}
|
|
||||||
{"level":"info","message":" ╰ Build commit: unknown"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge"}
|
|
||||||
{"level":"info","message":"│ Server listening on localhost:3000"}
|
|
||||||
{"level":"info","message":"│ Version: 0.1.0"}
|
|
||||||
{"level":"info","message":"│ Environment: development"}
|
|
||||||
{"level":"info","message":"╰ Build commit: unknown"}
|
|
||||||
{"level":"warn","message":"SIGTERM received, shutting down..."}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge"}
|
|
||||||
{"level":"info","message":"│ Server listening on localhost:3000"}
|
|
||||||
{"level":"info","message":"│ Version: 0.1.0"}
|
|
||||||
{"level":"info","message":"│ Environment: development"}
|
|
||||||
{"level":"info","message":"╰ Build commit: unknown"}
|
|
||||||
{"level":"info","message":"Not running database migrations, use drizzle-kit push to migrate","module":"database"}
|
|
||||||
{"level":"info","message":"🚀 Hostforge"}
|
|
||||||
{"level":"info","message":"│ Server listening on 0.0.0.0:3000"}
|
|
||||||
{"level":"info","message":"│ Version: 0.1.0"}
|
|
||||||
{"level":"info","message":"│ Environment: development"}
|
|
||||||
{"level":"info","message":"╰ Build commit: unknown"}
|
|
|
@ -5,7 +5,7 @@ import { ResponsiveContainer, AreaChart, Area } from "recharts";
|
||||||
import styles from "./StatCard.module.css";
|
import styles from "./StatCard.module.css";
|
||||||
import { AnimatedNumber } from "~/components/AnimatedPercent";
|
import { AnimatedNumber } from "~/components/AnimatedPercent";
|
||||||
|
|
||||||
export function StatCard<T extends Record<string, number>>(props: {
|
export function StatCard<T extends Record<string, any>>(props: {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
icon: React.FC<{ className: string }>;
|
icon: React.FC<{ className: string }>;
|
||||||
|
@ -35,7 +35,7 @@ export function StatCard<T extends Record<string, number>>(props: {
|
||||||
<div className="absolute inset-0 z-0 h-full w-full">
|
<div className="absolute inset-0 z-0 h-full w-full">
|
||||||
<ResponsiveContainer
|
<ResponsiveContainer
|
||||||
width={"100%"}
|
width={"100%"}
|
||||||
height={"50%"}
|
height={"40%"}
|
||||||
className="absolute bottom-0 left-0"
|
className="absolute bottom-0 left-0"
|
||||||
>
|
>
|
||||||
<AreaChart
|
<AreaChart
|
||||||
|
|
|
@ -9,22 +9,15 @@ import {
|
||||||
FaHardDrive,
|
FaHardDrive,
|
||||||
FaEthernet,
|
FaEthernet,
|
||||||
} from "react-icons/fa6";
|
} from "react-icons/fa6";
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
const TEST_DATA = [
|
|
||||||
{ cpu: 0.05 },
|
|
||||||
{ cpu: 0.1 },
|
|
||||||
{ cpu: 0.08 },
|
|
||||||
{ cpu: 0.09 },
|
|
||||||
{ cpu: 0.2 },
|
|
||||||
{ cpu: 0.1 },
|
|
||||||
{ cpu: 0.12 },
|
|
||||||
{ cpu: 0.3 },
|
|
||||||
];
|
|
||||||
|
|
||||||
type StatData = RouterOutputs["system"]["currentStats"];
|
type StatData = RouterOutputs["system"]["currentStats"];
|
||||||
|
type HistoricalStatData = RouterOutputs["system"]["history"];
|
||||||
|
|
||||||
export function SystemStatistics(props: { initialData: StatData }) {
|
export function SystemStatistics(props: {
|
||||||
|
initialData: StatData;
|
||||||
|
historicalData: HistoricalStatData;
|
||||||
|
}) {
|
||||||
const [data, setData] = useState<StatData>(props.initialData);
|
const [data, setData] = useState<StatData>(props.initialData);
|
||||||
|
|
||||||
api.system.liveStats.useSubscription(undefined, {
|
api.system.liveStats.useSubscription(undefined, {
|
||||||
|
@ -33,6 +26,15 @@ export function SystemStatistics(props: { initialData: StatData }) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const historicalData = useMemo(
|
||||||
|
() =>
|
||||||
|
props.historicalData.map((data) => ({
|
||||||
|
...data,
|
||||||
|
network: data.networkTx + data.networkRx,
|
||||||
|
})),
|
||||||
|
[props.historicalData],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="m-8 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="m-8 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<StatCard
|
<StatCard
|
||||||
|
@ -41,8 +43,8 @@ export function SystemStatistics(props: { initialData: StatData }) {
|
||||||
unit="%"
|
unit="%"
|
||||||
subvalue={`of ${data.cpu.cores} CPUs`}
|
subvalue={`of ${data.cpu.cores} CPUs`}
|
||||||
icon={FaMicrochip}
|
icon={FaMicrochip}
|
||||||
data={TEST_DATA}
|
data={historicalData}
|
||||||
dataKey="cpu"
|
dataKey="cpuUsage"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StatCard
|
<StatCard
|
||||||
|
@ -53,8 +55,8 @@ export function SystemStatistics(props: { initialData: StatData }) {
|
||||||
2,
|
2,
|
||||||
)} GB`}
|
)} GB`}
|
||||||
icon={FaMemory}
|
icon={FaMemory}
|
||||||
data={TEST_DATA}
|
data={historicalData}
|
||||||
dataKey="cpu"
|
dataKey="memoryUsage"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StatCard
|
<StatCard
|
||||||
|
@ -65,8 +67,8 @@ export function SystemStatistics(props: { initialData: StatData }) {
|
||||||
2,
|
2,
|
||||||
)} / ${data.storage.total.toFixed(2)} GB`}
|
)} / ${data.storage.total.toFixed(2)} GB`}
|
||||||
icon={FaHardDrive}
|
icon={FaHardDrive}
|
||||||
data={TEST_DATA}
|
data={historicalData}
|
||||||
dataKey="cpu"
|
dataKey="diskUsage"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StatCard
|
<StatCard
|
||||||
|
@ -81,8 +83,8 @@ export function SystemStatistics(props: { initialData: StatData }) {
|
||||||
secondarySubvalue="RX / Mbps"
|
secondarySubvalue="RX / Mbps"
|
||||||
// misc
|
// misc
|
||||||
icon={FaEthernet}
|
icon={FaEthernet}
|
||||||
data={TEST_DATA}
|
data={historicalData}
|
||||||
dataKey="cpu"
|
dataKey="network"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,12 +4,18 @@ import { StatCard } from "./StatCard";
|
||||||
import { SystemStatistics } from "./SystemStatistics";
|
import { SystemStatistics } from "./SystemStatistics";
|
||||||
|
|
||||||
export default async function DashboardHome() {
|
export default async function DashboardHome() {
|
||||||
const initialStats = await api.system.currentStats.query();
|
const [initialStats, historicalData] = await Promise.all([
|
||||||
|
api.system.currentStats.query(),
|
||||||
|
api.system.history.query(),
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-[1500px]">
|
<div className="mx-auto max-w-[1500px]">
|
||||||
<Test />
|
<Test />
|
||||||
<SystemStatistics initialData={initialStats} />
|
<SystemStatistics
|
||||||
|
initialData={initialStats}
|
||||||
|
historicalData={historicalData}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,4 +17,10 @@ export const systemRouter = createTRPCRouter({
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
history: authenticatedProcedure.query(async ({ ctx }) => {
|
||||||
|
return await stats.getStatsInRange(
|
||||||
|
new Date(Date.now() - 1000 * 60 * 60 * 24),
|
||||||
|
);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,21 +2,30 @@ import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||||
import SQLite3 from "better-sqlite3";
|
import SQLite3 from "better-sqlite3";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
|
import logger from "../utils/logger";
|
||||||
|
|
||||||
const sqlite = new SQLite3(env.DATABASE_PATH);
|
const globalForDB = globalThis as unknown as {
|
||||||
|
db: ReturnType<typeof createDatabaseInstance>;
|
||||||
|
};
|
||||||
|
|
||||||
// enable WAL mode
|
function createDatabaseInstance() {
|
||||||
sqlite.pragma("journal_mode = WAL");
|
logger.child({ module: "database" }).debug("Creating database client.");
|
||||||
|
const sqlite = new SQLite3(env.DATABASE_PATH);
|
||||||
|
|
||||||
// load uuidv7 extension
|
// enable WAL mode
|
||||||
// built from https://github.com/craigpastro/sqlite-uuidv7
|
sqlite.pragma("journal_mode = WAL");
|
||||||
sqlite.loadExtension(
|
|
||||||
|
// load uuidv7 extension
|
||||||
|
// built from https://github.com/craigpastro/sqlite-uuidv7
|
||||||
|
sqlite.loadExtension(
|
||||||
env.SQLITE_UUIDV7_EXT_PATH ??
|
env.SQLITE_UUIDV7_EXT_PATH ??
|
||||||
join(
|
join(
|
||||||
// cannot use __dirname since this file will change locations when compiled
|
// cannot use __dirname since this file will change locations when compiled
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
"./exts/sqlite-uuidv7",
|
"./exts/sqlite-uuidv7",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
return drizzle(sqlite);
|
||||||
|
}
|
||||||
|
|
||||||
export const db = drizzle(sqlite);
|
export const db = (globalForDB.db ??= createDatabaseInstance());
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
integer,
|
integer,
|
||||||
sqliteTable,
|
sqliteTable,
|
||||||
index,
|
index,
|
||||||
|
real,
|
||||||
} from "drizzle-orm/sqlite-core";
|
} from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
// util
|
// util
|
||||||
|
@ -75,3 +76,20 @@ export const MFARequestSessionRelations = relations(
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System Statistics
|
||||||
|
* Historical data about the system's usage
|
||||||
|
*/
|
||||||
|
export const systemStats = sqliteTable("system_stats", {
|
||||||
|
timestamp: integer("id").primaryKey().default(now),
|
||||||
|
|
||||||
|
// percent as decimal * 10_000 to keep 2 decimal places
|
||||||
|
cpuUsage: integer("cpu_usage"),
|
||||||
|
|
||||||
|
// everything else is in megabytes
|
||||||
|
memoryUsage: integer("memory_usage").notNull(),
|
||||||
|
diskUsage: integer("disk_usage").notNull(),
|
||||||
|
networkTx: integer("network_tx").notNull(),
|
||||||
|
networkRx: integer("network_rx").notNull(),
|
||||||
|
});
|
||||||
|
|
|
@ -3,6 +3,9 @@ import TypedEmitter from "typed-emitter";
|
||||||
import osu from "node-os-utils";
|
import osu from "node-os-utils";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import baseLogger from "../../utils/logger";
|
import baseLogger from "../../utils/logger";
|
||||||
|
import { db } from "~/server/db";
|
||||||
|
import { systemStats } from "~/server/db/schema";
|
||||||
|
import { between, lte } from "drizzle-orm";
|
||||||
|
|
||||||
export type BasicServerStats = {
|
export type BasicServerStats = {
|
||||||
collectedAt: Date;
|
collectedAt: Date;
|
||||||
|
@ -86,7 +89,8 @@ type StatEvents = {
|
||||||
* Manages the stats for the current server.
|
* Manages the stats for the current server.
|
||||||
*/
|
*/
|
||||||
export class StatManager {
|
export class StatManager {
|
||||||
private logger = baseLogger.child({ module: "StatManager" });
|
static readonly DB_MAX_STORE_TIME = /* 1 week */ 7 * 24 * 60 * 60 * 1000;
|
||||||
|
private logger = baseLogger.child({ module: "stats" });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current stats for the server.
|
* The current stats for the server.
|
||||||
|
@ -126,16 +130,7 @@ export class StatManager {
|
||||||
private liveInterval: NodeJS.Timeout | null = null;
|
private liveInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.update();
|
this.update().then(() => this.updateDatabase());
|
||||||
|
|
||||||
// collect stats every hour
|
|
||||||
setInterval(
|
|
||||||
async () => {
|
|
||||||
await this.update();
|
|
||||||
await this.updateDatabase();
|
|
||||||
},
|
|
||||||
60 * 60 * 1000,
|
|
||||||
);
|
|
||||||
|
|
||||||
// whenever a new listener is added, start the live interval
|
// whenever a new listener is added, start the live interval
|
||||||
this.events.on("newListener", (event) => {
|
this.events.on("newListener", (event) => {
|
||||||
|
@ -157,6 +152,18 @@ export class StatManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.logger.info("Starting hourly stat collection.");
|
||||||
|
// collect stats every hour
|
||||||
|
setInterval(
|
||||||
|
async () => {
|
||||||
|
await this.update();
|
||||||
|
await this.updateDatabase();
|
||||||
|
},
|
||||||
|
60 * 60 * 1000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current stats for the server.
|
* Gets the current stats for the server.
|
||||||
*/
|
*/
|
||||||
|
@ -219,7 +226,46 @@ export class StatManager {
|
||||||
/**
|
/**
|
||||||
* Updates the database with the current stats.
|
* Updates the database with the current stats.
|
||||||
*/
|
*/
|
||||||
async updateDatabase() {}
|
async updateDatabase() {
|
||||||
|
const startTime = Date.now();
|
||||||
|
await db
|
||||||
|
.insert(systemStats)
|
||||||
|
.values({
|
||||||
|
timestamp: this.currentStats.collectedAt.getTime(),
|
||||||
|
cpuUsage: this.currentStats.cpu.usage,
|
||||||
|
diskUsage: this.currentStats.storage.used * 1024,
|
||||||
|
memoryUsage: this.currentStats.memory.used * 1024,
|
||||||
|
networkTx: this.currentStats.network.tx / 1024 / 1024,
|
||||||
|
networkRx: this.currentStats.network.rx / 1024 / 1024,
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Updated database with new stats. Took ${Date.now() - startTime}ms.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// delete old stats
|
||||||
|
const count = await db
|
||||||
|
.delete(systemStats)
|
||||||
|
.where(
|
||||||
|
lte(systemStats.timestamp, Date.now() - StatManager.DB_MAX_STORE_TIME),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
this.logger.debug(`Deleted ${count.changes} old stats from the database.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches stats from the database in the given time range.
|
||||||
|
*/
|
||||||
|
async getStatsInRange(start: Date, end = Date.now()) {
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(systemStats)
|
||||||
|
.where(between(systemStats.timestamp, start.getTime(), end));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const stats = new StatManager();
|
export const stats = (((globalThis as any).statsManager as
|
||||||
|
| StatManager
|
||||||
|
| undefined) ??= new StatManager());
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { db } from "./db";
|
||||||
import { mkdir, stat } from "fs/promises";
|
import { mkdir, stat } from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { version } from "../../package.json";
|
import { version } from "../../package.json";
|
||||||
|
import { stats } from "./modules/stats";
|
||||||
|
|
||||||
// check if database folder exists
|
// check if database folder exists
|
||||||
try {
|
try {
|
||||||
|
@ -34,14 +35,15 @@ if (env.NODE_ENV === "production") {
|
||||||
.info("Not running database migrations, use drizzle-kit push to migrate");
|
.info("Not running database migrations, use drizzle-kit push to migrate");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start statistics
|
||||||
|
stats.start();
|
||||||
|
|
||||||
// initialize the next app
|
// initialize the next app
|
||||||
const app = next({
|
const app = next({
|
||||||
dev: env.NODE_ENV !== "production",
|
dev: env.NODE_ENV !== "production",
|
||||||
hostname: env.HOSTNAME,
|
hostname: env.HOSTNAME,
|
||||||
port: env.PORT,
|
port: env.PORT,
|
||||||
// dir: path.join(__dirname, "../.."),
|
|
||||||
customServer: true,
|
customServer: true,
|
||||||
isNodeDebugging: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.prepare();
|
await app.prepare();
|
||||||
|
|
|
@ -19,6 +19,8 @@ const logger = createLogger({
|
||||||
)} ${message}`;
|
)} ${message}`;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
level: "debug",
|
||||||
}),
|
}),
|
||||||
new transports.File({ filename: "logs/error.log", level: "error" }),
|
new transports.File({ filename: "logs/error.log", level: "error" }),
|
||||||
new transports.File({ filename: "logs/combined.log" }),
|
new transports.File({ filename: "logs/combined.log" }),
|
||||||
|
|
Loading…
Reference in a new issue