Compare commits
98 Commits
0d0c3aca71
...
renovate/w
Author | SHA1 | Date | |
---|---|---|---|
75879eb5a6 | |||
c7853d4153 | |||
1481bd1d00 | |||
1bf3c86a4c | |||
6727e1177c | |||
47f3f8b1e0 | |||
27703ff009 | |||
f788f574c0 | |||
6c19559964 | |||
f83ff0a7a6 | |||
ee69ce3c8d | |||
8f47e210d5 | |||
8c1055b468 | |||
42558f3500 | |||
0a3e8f28ba | |||
419b2b0286 | |||
59816d7410 | |||
cdb7dbf18f | |||
4696d512eb | |||
3793071c2d | |||
43e9492a47 | |||
745e4e6fe5 | |||
3274e3bc2d | |||
c463cdff0a | |||
4c7a46e493 | |||
01df7d1e13 | |||
dc06a3877d | |||
c816304a71 | |||
32a50e9de4 | |||
2bfe562269 | |||
c381d82e2a | |||
d12287a13a | |||
f193dacd14 | |||
6a9ccad31c | |||
cf49d249bd | |||
85b5b7cbce | |||
71239fa078 | |||
66f067990b | |||
9701ecdcd7 | |||
adf46b9cb8 | |||
9f40efa75a | |||
6984c35710 | |||
f395b66d77 | |||
627fe5f38f | |||
ba4807f33f | |||
3f791ad52a | |||
bfe26c2250 | |||
b1451f9e88 | |||
d8fafec1ba | |||
666036ef2e | |||
7f457497e3 | |||
7ea96c5cc3 | |||
e208b99192 | |||
58696a9566 | |||
908e9e1618 | |||
3496865c2e | |||
21c3677d0e | |||
466f96b649 | |||
4b2a428646 | |||
20acbfd7f4 | |||
d2d1597b72 | |||
d468e2b070 | |||
4a81455f96 | |||
6221a58062 | |||
98cb26e310 | |||
ab53e24978 | |||
3f399bec10 | |||
cf72d4886a | |||
3bed998640 | |||
4b7b43c036 | |||
d15f1613f0 | |||
1bd47d2b60 | |||
a8bc162d8b | |||
cf66e8c488 | |||
2b8af4050b | |||
3568e195b9 | |||
2d0f4ddca1 | |||
9ffa0c549c | |||
4543309fd1 | |||
2780f4f5f6 | |||
169d53f2cf | |||
74ccee0381 | |||
6880cd30ce | |||
d9d6d2b3bb | |||
3b33e7f9ae | |||
0f62409e88 | |||
ff11240334 | |||
358bb6272c | |||
9540a03366 | |||
0781d74067 | |||
88472c81e9 | |||
d841900cfb | |||
404fc5b267 | |||
5fd535a19c | |||
899aac977f | |||
d76c5a9131 | |||
31176e4025 | |||
7369ad4d7b |
28
.gitea/workflows/ci.yml
Normal file
28
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
name: "Deploy CI"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths-ignore: # Files in here will not trigger a build
|
||||||
|
- "README.md"
|
||||||
|
- "LICENSE"
|
||||||
|
- "grafana-dashboard.json"
|
||||||
|
- "storage-tracker.sh"
|
||||||
|
- "useful-stuff.md"
|
||||||
|
- ".gitea/workflows/publish.yml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Cloning repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Push to dokku
|
||||||
|
uses: dokku/github-action@master
|
||||||
|
with:
|
||||||
|
git_remote_url: "ssh://dokku@51.158.63.74:22/mc-tracker"
|
||||||
|
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
|
@ -4,7 +4,13 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "master"
|
- "master"
|
||||||
- "influx-only"
|
paths-ignore: # Files in here will not trigger a build
|
||||||
|
- "README.md"
|
||||||
|
- "LICENSE"
|
||||||
|
- "grafana-dashboard.json"
|
||||||
|
- "storage-tracker.sh"
|
||||||
|
- "useful-stuff.md"
|
||||||
|
- ".gitea/workflows/ci.yml"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
@ -14,7 +20,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Restore Docker Cache
|
- name: Restore Docker Cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
id: docker-cache
|
id: docker-cache
|
||||||
with:
|
with:
|
||||||
path: /usr/bin/docker
|
path: /usr/bin/docker
|
||||||
@ -37,26 +43,11 @@ jobs:
|
|||||||
username: ${{ secrets.REPO_USERNAME }}
|
username: ${{ secrets.REPO_USERNAME }}
|
||||||
password: ${{ secrets.REPO_TOKEN }}
|
password: ${{ secrets.REPO_TOKEN }}
|
||||||
|
|
||||||
- name: Restore Docker Build Cache
|
|
||||||
uses: actions/cache@v3
|
|
||||||
id: build-cache
|
|
||||||
with:
|
|
||||||
path: /tmp/.buildx-cache
|
|
||||||
key: ${{ runner.os }}-buildx
|
|
||||||
|
|
||||||
- name: Build and Push (Latest)
|
- name: Build and Push (Latest)
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
context: .
|
context: .
|
||||||
tags: fascinated/mc-tracker:latest
|
tags: fascinated/mc-tracker:latest
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
|
|
||||||
- name: Save Docker Build Cache
|
|
||||||
if: steps.build-cache.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
|
||||||
mkdir -p /tmp/.buildx-cache
|
|
||||||
cp -r /tmp/.buildx-cache/. /tmp/.buildx-cache-new
|
|
||||||
rm -rf /tmp/.buildx-cache
|
|
||||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
|
@ -4,8 +4,9 @@ ENV NODE_ENV=production
|
|||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
COPY ["package.json", "pnpm-lock.yaml", "./"]
|
COPY ["package.json", "pnpm-lock.yaml", "./"]
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN pnpm install --production --silent
|
RUN pnpm install --production --silent
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
CMD pnpm run build && pnpm run start
|
CMD pnpm run build && pnpm run start
|
||||||
|
@ -1,2 +1,9 @@
|
|||||||
# backend
|
# Mc Tracker
|
||||||
|
|
||||||
|
You can view the live instance of this project [here](https://mc-tracker.fascinated.cc).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Track Minecraft server's player count over time
|
||||||
|
- See player count peaks (all time)
|
||||||
|
- Minecraft Java and Bedrock support
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
{
|
{
|
||||||
"influx": {
|
"influx": {
|
||||||
"url": "http://localhost:8086",
|
"url": "http://localhost:8086",
|
||||||
"token": "my-token",
|
"token": "setme",
|
||||||
"org": "my-org",
|
"org": "setme",
|
||||||
"bucket": "my-bucket"
|
"bucket": "mc-tracker"
|
||||||
},
|
},
|
||||||
"scanner": {
|
"pinger": {
|
||||||
"updateCron": "*/1 * * * *",
|
"pingCron": "*/1 * * * *",
|
||||||
|
"dnsInvalidationCron": "0 */6 * * *",
|
||||||
"timeout": 2000
|
"timeout": 2000
|
||||||
},
|
|
||||||
"backup": {
|
|
||||||
"cron": "0 0 * * *"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"name": "HiveMC",
|
||||||
|
"ip": "geo.hivebedrock.network",
|
||||||
|
"type": "PE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cubecraft - Bedrock",
|
||||||
|
"ip": "play.cubecraft.net",
|
||||||
|
"type": "PE"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "WildNetwork",
|
"name": "WildNetwork",
|
||||||
"ip": "wildnetwork.net",
|
"ip": "play.wildnetwork.net",
|
||||||
"type": "PC"
|
"type": "PC"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -40,15 +50,10 @@
|
|||||||
"type": "PC"
|
"type": "PC"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Grand Theft Minecraft",
|
"name": "Grand Theft Minecart",
|
||||||
"ip": "gtm.network",
|
"ip": "gtm.network",
|
||||||
"type": "PC"
|
"type": "PC"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "HiveMC",
|
|
||||||
"ip": "geo.hivebedrock.network",
|
|
||||||
"type": "PE"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Purple Prison",
|
"name": "Purple Prison",
|
||||||
"ip": "MCSL.PURPLE.WTF",
|
"ip": "MCSL.PURPLE.WTF",
|
||||||
@ -58,5 +63,70 @@
|
|||||||
"name": "MinecraftOnline",
|
"name": "MinecraftOnline",
|
||||||
"ip": "minecraftonline.com",
|
"ip": "minecraftonline.com",
|
||||||
"type": "PC"
|
"type": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hoplite",
|
||||||
|
"ip": "hoplite.gg",
|
||||||
|
"type": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Minemen Club",
|
||||||
|
"ip": "minemen.club",
|
||||||
|
"type": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "OPLegends",
|
||||||
|
"ip": "play.oplegends.com",
|
||||||
|
"type": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Minecadia",
|
||||||
|
"ip": "play.minecadia.com",
|
||||||
|
"type": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Manacube",
|
||||||
|
"ip": "play.manacube.com",
|
||||||
|
"type": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "InvadedLands",
|
||||||
|
"ip": "invadedlands.net",
|
||||||
|
"type": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Complex Gaming",
|
||||||
|
"ip": "hub.mc-complex.com",
|
||||||
|
"type": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Monumenta",
|
||||||
|
"ip": "server.playmonumenta.com",
|
||||||
|
"type": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Shotbow",
|
||||||
|
"ip": "play.shotbow.net",
|
||||||
|
"type": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stray",
|
||||||
|
"ip": "stray.gg",
|
||||||
|
"type": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Aetheria Anarchy",
|
||||||
|
"ip": "aetheria.cc",
|
||||||
|
"type": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VexedMC",
|
||||||
|
"ip": "play.vexedmc.com",
|
||||||
|
"type": "PC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AzureMC",
|
||||||
|
"ip": "play.azuremc.org",
|
||||||
|
"type": "PC"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
10
data/websites.json
Normal file
10
data/websites.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Minecraft Textures",
|
||||||
|
"url": "https://textures.minecraft.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Minecraft Session Server",
|
||||||
|
"url": "https://session.minecraft.net"
|
||||||
|
}
|
||||||
|
]
|
@ -7,6 +7,18 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./data:/usr/src/app/data
|
- ./data:/usr/src/app/data
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
environment:
|
||||||
|
- GF_ANALYTICS_REPORTING_ENABLED=false
|
||||||
|
- GF_SERVER_DOMAIN=mc-tracker.fascinated.cc
|
||||||
|
- GF_AUTH_ANONYMOUS_ENABLED=true
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
volumes:
|
||||||
|
- /home/grafana/data:/var/lib/grafana
|
||||||
|
restart: always
|
||||||
|
|
||||||
influxdb:
|
influxdb:
|
||||||
image: influxdb:latest
|
image: influxdb:latest
|
||||||
container_name: influxdb
|
container_name: influxdb
|
||||||
|
1282
grafana-dashboard.json
Normal file
1282
grafana-dashboard.json
Normal file
File diff suppressed because it is too large
Load Diff
4084
package-lock.json
generated
4084
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,7 +14,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@influxdata/influxdb-client": "^1.33.2",
|
"@influxdata/influxdb-client": "^1.33.2",
|
||||||
"@types/mcping-js": "^1.5.4",
|
"@types/mcping-js": "^1.5.4",
|
||||||
|
"@types/node": "^20.10.6",
|
||||||
"@types/node-cron": "^3.0.11",
|
"@types/node-cron": "^3.0.11",
|
||||||
|
"axios": "^1.6.4",
|
||||||
"dns": "^0.2.2",
|
"dns": "^0.2.2",
|
||||||
"mcpe-ping-fixed": "^0.0.3",
|
"mcpe-ping-fixed": "^0.0.3",
|
||||||
"mcping-js": "^1.5.0",
|
"mcping-js": "^1.5.0",
|
||||||
@ -22,7 +24,6 @@
|
|||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsup": "^8.0.1",
|
"tsup": "^8.0.1",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0"
|
||||||
"@types/node": "^20.10.6"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
806
pnpm-lock.yaml
generated
806
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,3 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"extends": ["config:recommended", ":dependencyDashboard"]
|
||||||
"extends": [
|
}
|
||||||
"local>Fascinated/renovate-config"
|
|
||||||
]
|
|
||||||
}
|
|
16
src/index.ts
16
src/index.ts
@ -1,20 +1,14 @@
|
|||||||
import Influx from "./influx/influx";
|
import Influx from "./influx/influx";
|
||||||
import Scanner from "./scanner/scanner";
|
|
||||||
import ServerManager from "./server/serverManager";
|
import ServerManager from "./server/serverManager";
|
||||||
|
import WebsiteManager from "./website/websiteManager";
|
||||||
/**
|
import {logger} from "./utils/logger";
|
||||||
* The server manager instance.
|
|
||||||
*/
|
|
||||||
export const serverManager = new ServerManager();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The influx database instance.
|
* The influx database instance.
|
||||||
*/
|
*/
|
||||||
export const influx = new Influx();
|
export const influx = new Influx();
|
||||||
|
|
||||||
(async () => {
|
new ServerManager();
|
||||||
await serverManager.init();
|
new WebsiteManager();
|
||||||
|
|
||||||
// The scanner is responsible for scanning all servers
|
logger.info("Done loading!");
|
||||||
new Scanner();
|
|
||||||
})();
|
|
@ -14,13 +14,13 @@ export default class Influx {
|
|||||||
url: Config.influx.url,
|
url: Config.influx.url,
|
||||||
token: Config.influx.token,
|
token: Config.influx.token,
|
||||||
});
|
});
|
||||||
logger.info("InfluxDB initialized");
|
|
||||||
|
|
||||||
this.writeApi = this.influx.getWriteApi(
|
this.writeApi = this.influx.getWriteApi(
|
||||||
Config.influx.org,
|
Config.influx.org,
|
||||||
Config.influx.bucket,
|
Config.influx.bucket,
|
||||||
"ms"
|
"ms"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
logger.info("InfluxDB initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,7 +28,7 @@ export default class Influx {
|
|||||||
*
|
*
|
||||||
* @param point the point to write
|
* @param point the point to write
|
||||||
*/
|
*/
|
||||||
public async writePoint(point: Point): Promise<void> {
|
public writePoint(point: Point) {
|
||||||
this.writeApi.writePoint(point);
|
this.writeApi.writePoint(point);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import cron from "node-cron";
|
|
||||||
|
|
||||||
import { serverManager } from "..";
|
|
||||||
import Config from "../../data/config.json";
|
|
||||||
import { logger } from "../utils/logger";
|
|
||||||
|
|
||||||
export default class Scanner {
|
|
||||||
constructor() {
|
|
||||||
cron.schedule(Config.scanner.updateCron, () => {
|
|
||||||
this.scanServers();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a server scan to ping all servers.
|
|
||||||
*/
|
|
||||||
private async scanServers(): Promise<void> {
|
|
||||||
logger.info(`Scanning servers ${serverManager.getServers().length}`);
|
|
||||||
|
|
||||||
// ping all servers in parallel
|
|
||||||
await Promise.all(
|
|
||||||
serverManager.getServers().map((server) => server.pingServer())
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info("Finished scanning servers");
|
|
||||||
}
|
|
||||||
}
|
|
@ -15,10 +15,6 @@ import { logger } from "../utils/logger";
|
|||||||
*/
|
*/
|
||||||
export type ServerType = "PC" | "PE";
|
export type ServerType = "PC" | "PE";
|
||||||
|
|
||||||
export enum ServerStatus {
|
|
||||||
OFFLINE = "Unable to reach host",
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServerOptions = {
|
type ServerOptions = {
|
||||||
name: string;
|
name: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
@ -35,27 +31,22 @@ export default class Server {
|
|||||||
/**
|
/**
|
||||||
* The name of the server.
|
* The name of the server.
|
||||||
*/
|
*/
|
||||||
private name: string;
|
private readonly name: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The IP address of the server.
|
* The IP address of the server.
|
||||||
*/
|
*/
|
||||||
private ip: string;
|
private readonly ip: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The port of the server.
|
* The port of the server.
|
||||||
*/
|
*/
|
||||||
private port: number | undefined;
|
private readonly port: number | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of server.
|
* The type of server.
|
||||||
*/
|
*/
|
||||||
private type: ServerType;
|
private readonly type: ServerType;
|
||||||
|
|
||||||
/**
|
|
||||||
* The favicon of the server.
|
|
||||||
*/
|
|
||||||
private favicon: string | undefined;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The resolved server information from
|
* The resolved server information from
|
||||||
@ -78,6 +69,7 @@ export default class Server {
|
|||||||
* @returns the ping response or undefined if the server is offline
|
* @returns the ping response or undefined if the server is offline
|
||||||
*/
|
*/
|
||||||
public async pingServer(): Promise<Ping | undefined> {
|
public async pingServer(): Promise<Ping | undefined> {
|
||||||
|
const before = Date.now();
|
||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
@ -101,10 +93,14 @@ export default class Server {
|
|||||||
new Point("playerCount")
|
new Point("playerCount")
|
||||||
.tag("name", this.getName())
|
.tag("name", this.getName())
|
||||||
.intField("playerCount", response.playerCount)
|
.intField("playerCount", response.playerCount)
|
||||||
|
.intField("latency", Date.now() - before)
|
||||||
.timestamp(response.timestamp)
|
.timestamp(response.timestamp)
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`Failed to write ping to influxdb`, err);
|
logger.warn(
|
||||||
|
`Failed to write point to Influx for ${this.getName()} - ${this.getIP()}`,
|
||||||
|
err
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
@ -147,13 +143,13 @@ export default class Server {
|
|||||||
|
|
||||||
const serverPing = new javaPing.MinecraftServer(ip, port);
|
const serverPing = new javaPing.MinecraftServer(ip, port);
|
||||||
|
|
||||||
|
// todo: do something to get the latest protocol? (is this even needed??)
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
serverPing.ping(Config.scanner.timeout, 765, (err, res) => {
|
serverPing.ping(Config.pinger.timeout, 765, (err, res) => {
|
||||||
if (err || res == undefined) {
|
if (err || res == undefined) {
|
||||||
return reject(err);
|
return reject(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.favicon = res.favicon; // Set the favicon
|
|
||||||
resolve({
|
resolve({
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
ip: ip,
|
ip: ip,
|
||||||
@ -189,6 +185,15 @@ export default class Server {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates the DNS cache for the server.
|
||||||
|
*/
|
||||||
|
public invalidateDns() {
|
||||||
|
this.dnsInfo = {
|
||||||
|
hasResolved: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name of the server.
|
* Returns the name of the server.
|
||||||
*
|
*
|
||||||
@ -224,13 +229,4 @@ export default class Server {
|
|||||||
public getType(): ServerType {
|
public getType(): ServerType {
|
||||||
return this.type;
|
return this.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the favicon of the server.
|
|
||||||
*
|
|
||||||
* @returns the favicon
|
|
||||||
*/
|
|
||||||
public getFavicon(): string | undefined {
|
|
||||||
return this.favicon;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
|
import cron from "node-cron";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
import Server, { ServerType } from "./server";
|
import Server, { ServerType } from "./server";
|
||||||
|
|
||||||
|
import Config from "../../data/config.json";
|
||||||
import Servers from "../../data/servers.json";
|
import Servers from "../../data/servers.json";
|
||||||
import { logger } from "../utils/logger";
|
|
||||||
|
|
||||||
export default class ServerManager {
|
export default class ServerManager {
|
||||||
private servers: Server[] = [];
|
private servers: Server[] = [];
|
||||||
|
|
||||||
constructor() {}
|
constructor() {
|
||||||
|
logger.info("Loading servers...");
|
||||||
/**
|
|
||||||
* Loads the servers from the config file.
|
|
||||||
*/
|
|
||||||
async init() {
|
|
||||||
logger.info("Loading servers");
|
|
||||||
for (const configServer of Servers) {
|
for (const configServer of Servers) {
|
||||||
const server = new Server({
|
const server = new Server({
|
||||||
ip: configServer.ip,
|
ip: configServer.ip,
|
||||||
@ -21,15 +18,29 @@ export default class ServerManager {
|
|||||||
});
|
});
|
||||||
this.servers.push(server);
|
this.servers.push(server);
|
||||||
}
|
}
|
||||||
logger.info(`Loaded ${this.servers.length} servers`);
|
logger.info(`Loaded ${this.servers.length} servers!`);
|
||||||
|
|
||||||
|
cron.schedule(Config.pinger.pingCron, async () => {
|
||||||
|
await this.pingServers();
|
||||||
|
});
|
||||||
|
|
||||||
|
cron.schedule(Config.pinger.dnsInvalidationCron, () => {
|
||||||
|
logger.info("Invalidating DNS cache for all servers");
|
||||||
|
for (const server of this.servers) {
|
||||||
|
server.invalidateDns();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the servers.
|
* Ping all servers to update their status.
|
||||||
*
|
|
||||||
* @returns the servers
|
|
||||||
*/
|
*/
|
||||||
public getServers(): Server[] {
|
private async pingServers(): Promise<void> {
|
||||||
return this.servers;
|
logger.info(`Pinging servers ${this.servers.length}`);
|
||||||
|
|
||||||
|
// ping all servers in parallel
|
||||||
|
await Promise.all(this.servers.map((server) => server.pingServer()));
|
||||||
|
|
||||||
|
logger.info("Finished pinging servers!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
import fs from "fs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a directory at the given path.
|
|
||||||
*
|
|
||||||
* @param path the path to the file
|
|
||||||
* @param recursive whether to create the directory tree if it doesn't exist (defaults to true)
|
|
||||||
* @returns a promise that resolves when the file is created
|
|
||||||
*/
|
|
||||||
export async function createDirectory(
|
|
||||||
path: string,
|
|
||||||
recursive?: boolean
|
|
||||||
): Promise<void> {
|
|
||||||
if (recursive == undefined) {
|
|
||||||
recursive = true; // Set to true by default
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
fs.mkdir(path, { recursive: recursive }, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a file exists at the given path.
|
|
||||||
*
|
|
||||||
* @param path the path to the file
|
|
||||||
* @returns a promise that returns true if the file exists, false otherwise
|
|
||||||
*/
|
|
||||||
export async function exists(path: string): Promise<boolean> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
fs.exists(path, (exists) => {
|
|
||||||
resolve(exists);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@ -9,7 +9,7 @@ interface LogInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customFormat = format.combine(
|
const customFormat = format.combine(
|
||||||
timestamp({ format: "YY-MM-DD HH:MM:SS" }),
|
timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||||
printf((info: LogInfo) => {
|
printf((info: LogInfo) => {
|
||||||
return `[${info.timestamp}] ${info.level}: ${info.message}`;
|
return `[${info.timestamp}] ${info.level}: ${info.message}`;
|
||||||
})
|
})
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
/**
|
|
||||||
* Gets the current date as YYYY-MM-DD.
|
|
||||||
*
|
|
||||||
* @returns the date
|
|
||||||
*/
|
|
||||||
export function getFormattedDate() {
|
|
||||||
return new Date().toISOString().slice(0, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a timestamp as YYYY-MM-DD.
|
|
||||||
*
|
|
||||||
* @param timestamp the timestamp
|
|
||||||
* @returns the formatted timestamp
|
|
||||||
*/
|
|
||||||
export function formatTimestamp(timestamp: number) {
|
|
||||||
return new Date(timestamp).toISOString().slice(0, 10);
|
|
||||||
}
|
|
64
src/website/website.ts
Normal file
64
src/website/website.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Point } from "@influxdata/influxdb-client";
|
||||||
|
import axios from "axios";
|
||||||
|
import { influx } from "..";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
|
import Config from "../../data/config.json";
|
||||||
|
|
||||||
|
type WebsiteOptions = {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Website {
|
||||||
|
/**
|
||||||
|
* The name of the website.
|
||||||
|
*/
|
||||||
|
private readonly name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The url of the website.
|
||||||
|
*/
|
||||||
|
private readonly url: string;
|
||||||
|
|
||||||
|
constructor({ name, url }: WebsiteOptions) {
|
||||||
|
this.name = name;
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pings a website and gets the response.
|
||||||
|
*
|
||||||
|
* @returns the response
|
||||||
|
*/
|
||||||
|
public async pingWebsite(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const before = Date.now();
|
||||||
|
const response = await axios.get(this.url, {
|
||||||
|
validateStatus: () => true, // Don't throw a error on non-200 status codes
|
||||||
|
timeout: Config.pinger.timeout,
|
||||||
|
});
|
||||||
|
if (response.status === 500) {
|
||||||
|
throw new Error("Server returned 500 status code");
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseTime = Date.now() - before;
|
||||||
|
|
||||||
|
influx.writePoint(
|
||||||
|
new Point("websiteStatus")
|
||||||
|
.tag("name", this.name)
|
||||||
|
.booleanField("online", true)
|
||||||
|
.intField("responseTime", responseTime)
|
||||||
|
.timestamp(Date.now())
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`Failed to ping ${this.name}:`, err);
|
||||||
|
influx.writePoint(
|
||||||
|
new Point("websiteStatus")
|
||||||
|
.tag("name", this.name)
|
||||||
|
.booleanField("online", false)
|
||||||
|
.timestamp(Date.now())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
src/website/websiteManager.ts
Normal file
38
src/website/websiteManager.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import cron from "node-cron";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
|
import Config from "../../data/config.json";
|
||||||
|
import Websites from "../../data/websites.json";
|
||||||
|
import Website from "./website";
|
||||||
|
|
||||||
|
export default class WebsiteManager {
|
||||||
|
private websites: Website[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
logger.info("Loading websites...");
|
||||||
|
for (const configWebsite of Websites) {
|
||||||
|
const website = new Website({
|
||||||
|
name: configWebsite.name,
|
||||||
|
url: configWebsite.url,
|
||||||
|
});
|
||||||
|
this.websites.push(website);
|
||||||
|
}
|
||||||
|
logger.info(`Loaded ${this.websites.length} websites!`);
|
||||||
|
|
||||||
|
cron.schedule(Config.pinger.pingCron, async () => {
|
||||||
|
await this.pingWebsites();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ping all websites to update their status.
|
||||||
|
*/
|
||||||
|
private async pingWebsites(): Promise<void> {
|
||||||
|
logger.info(`Pinging websites ${this.websites.length}`);
|
||||||
|
|
||||||
|
// ping all websites in parallel
|
||||||
|
await Promise.all(this.websites.map((website) => website.pingWebsite()));
|
||||||
|
|
||||||
|
logger.info("Finished pinging websites!");
|
||||||
|
}
|
||||||
|
}
|
12
storage-tracker.sh
Normal file
12
storage-tracker.sh
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
usage_engine=$(du -s /home/tracker/influx/db/engine/data/setme/ | cut -f1)
|
||||||
|
usage_wal=$(du -s /home/tracker/influx/db/engine/wal/setme/ | cut -f1)
|
||||||
|
|
||||||
|
# Calculate the sum of usage_engine and usage_wal
|
||||||
|
total_usage=$((usage_engine + usage_wal))
|
||||||
|
|
||||||
|
docker exec influxdb influx write \
|
||||||
|
--org homelab \
|
||||||
|
--bucket influx_metrics \
|
||||||
|
--token setme \
|
||||||
|
--precision s \
|
||||||
|
"storage_usage,db=mc-tracker value=$total_usage"
|
7
useful-stuff.md
Normal file
7
useful-stuff.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Useful stuff
|
||||||
|
|
||||||
|
## Deleteing a specific server from influx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
influx delete --bucket mc-tracker --start 2024-01-01T00:00:00Z --stop 2024-01-05T00:00:00Z --org homelab --token nou --predicate '_measurement="playerCount" AND "name"="Grand Theft Minecraft"'
|
||||||
|
```
|
Reference in New Issue
Block a user