Compare commits
123 Commits
9e17262804
...
renovate/n
Author | SHA1 | Date | |
---|---|---|---|
1c0e3d5eb4 | |||
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 | |||
0d0c3aca71 | |||
f71e423d6c | |||
103f96d5bd | |||
2cee3d9c7f | |||
b0f3d8532f | |||
4095d70722 | |||
8826a2530a | |||
53ae4dbd89 | |||
56a75560e8 | |||
9c6966f180 | |||
8383445898 | |||
29e3696be3 | |||
8f344d246f | |||
d4bba5426e | |||
dba6aa8bef | |||
2ff05f9255 | |||
fb6dbc3b5d | |||
663f997ee7 | |||
00deb93193 | |||
fd4ef15fae | |||
9453ef59d3 | |||
d2a2bdacd5 | |||
4f3a9ea9dd | |||
e603f65fdc | |||
4ed5d2af84 |
24
.dockerignore
Normal file
24
.dockerignore
Normal file
@ -0,0 +1,24 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
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 }}
|
53
.gitea/workflows/publish.yml
Normal file
53
.gitea/workflows/publish.yml
Normal file
@ -0,0 +1,53 @@
|
||||
name: Publish Docker Image
|
||||
|
||||
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/ci.yml"
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore Docker Cache
|
||||
uses: actions/cache@v4
|
||||
id: docker-cache
|
||||
with:
|
||||
path: /usr/bin/docker
|
||||
key: ${{ runner.os }}-docker
|
||||
|
||||
- name: Install Docker (if not cached)
|
||||
if: steps.docker-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
wget -q -O /tmp/docker.tgz https://download.docker.com/linux/static/stable/x86_64/docker-20.10.23.tgz \
|
||||
&& tar --extract --file /tmp/docker.tgz --directory /usr/bin --strip-components 1 --no-same-owner docker/docker \
|
||||
&& rm -rf /tmp/* &&
|
||||
echo "Done"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Repo
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.REPO_USERNAME }}
|
||||
password: ${{ secrets.REPO_TOKEN }}
|
||||
|
||||
- name: Build and Push (Latest)
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
tags: fascinated/mc-tracker:latest
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -228,3 +228,5 @@ data/db.sqlite
|
||||
data/db.sqlite-shm
|
||||
data/db.sqlite-wal
|
||||
data/database-backups
|
||||
|
||||
data/config.json
|
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Docker Node.js Launch",
|
||||
"type": "docker",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "docker-run: debug",
|
||||
"platform": "node"
|
||||
}
|
||||
]
|
||||
}
|
35
.vscode/tasks.json
vendored
Normal file
35
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "docker-build",
|
||||
"label": "docker-build",
|
||||
"platform": "node",
|
||||
"dockerBuild": {
|
||||
"dockerfile": "${workspaceFolder}/Dockerfile",
|
||||
"context": "${workspaceFolder}",
|
||||
"pull": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "docker-run",
|
||||
"label": "docker-run: release",
|
||||
"dependsOn": ["docker-build"],
|
||||
"platform": "node"
|
||||
},
|
||||
{
|
||||
"type": "docker-run",
|
||||
"label": "docker-run: debug",
|
||||
"dependsOn": ["docker-build"],
|
||||
"dockerRun": {
|
||||
"env": {
|
||||
"DEBUG": "*",
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
},
|
||||
"node": {
|
||||
"enableDebugging": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@ -0,0 +1,12 @@
|
||||
FROM fascinated/docker-images:node-pnpm-latest AS base
|
||||
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY ["package.json", "pnpm-lock.yaml", "./"]
|
||||
|
||||
RUN pnpm install --production --silent
|
||||
|
||||
COPY . .
|
||||
|
||||
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,12 +0,0 @@
|
||||
{
|
||||
"websocket": {
|
||||
"port": 3000
|
||||
},
|
||||
"scanner": {
|
||||
"updateCron": "*/1 * * * *",
|
||||
"timeout": 2000
|
||||
},
|
||||
"backup": {
|
||||
"cron": "0 0 * * *"
|
||||
}
|
||||
}
|
13
data/config.json-example
Normal file
13
data/config.json-example
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"influx": {
|
||||
"url": "http://localhost:8086",
|
||||
"token": "setme",
|
||||
"org": "setme",
|
||||
"bucket": "mc-tracker"
|
||||
},
|
||||
"pinger": {
|
||||
"pingCron": "*/1 * * * *",
|
||||
"dnsInvalidationCron": "0 */6 * * *",
|
||||
"timeout": 2000
|
||||
}
|
||||
}
|
@ -1,74 +1,132 @@
|
||||
[
|
||||
{
|
||||
"name": "WildPrison",
|
||||
"ip": "wildprison.net",
|
||||
"type": "PC",
|
||||
"id": 0
|
||||
"name": "HiveMC",
|
||||
"ip": "geo.hivebedrock.network",
|
||||
"type": "PE"
|
||||
},
|
||||
{
|
||||
"name": "Cubecraft - Bedrock",
|
||||
"ip": "play.cubecraft.net",
|
||||
"type": "PE"
|
||||
},
|
||||
{
|
||||
"name": "WildNetwork",
|
||||
"ip": "play.wildnetwork.net",
|
||||
"type": "PC"
|
||||
},
|
||||
{
|
||||
"name": "Hypixel",
|
||||
"ip": "mc.hypixel.net",
|
||||
"type": "PC",
|
||||
"id": 1
|
||||
"type": "PC"
|
||||
},
|
||||
{
|
||||
"name": "CubeCraft",
|
||||
"name": "Cubecraft",
|
||||
"ip": "play.cubecraft.net",
|
||||
"type": "PC",
|
||||
"id": 2
|
||||
"type": "PC"
|
||||
},
|
||||
{
|
||||
"name": "Mineplex",
|
||||
"ip": "mineplex.com",
|
||||
"type": "PC",
|
||||
"id": 3
|
||||
"type": "PC"
|
||||
},
|
||||
{
|
||||
"name": "2b2t",
|
||||
"ip": "2b2t.org",
|
||||
"type": "PC",
|
||||
"id": 4
|
||||
"type": "PC"
|
||||
},
|
||||
{
|
||||
"name": "AkumaMC",
|
||||
"ip": "akumamc.net",
|
||||
"type": "PC",
|
||||
"id": 5
|
||||
"type": "PC"
|
||||
},
|
||||
{
|
||||
"name": "Wynncraft",
|
||||
"ip": "play.wynncraft.com",
|
||||
"type": "PC",
|
||||
"id": 6
|
||||
"type": "PC"
|
||||
},
|
||||
{
|
||||
"name": "Minehut",
|
||||
"ip": "minehut.com",
|
||||
"type": "PC",
|
||||
"id": 7
|
||||
"type": "PC"
|
||||
},
|
||||
{
|
||||
"name": "Grand Theft Minecraft",
|
||||
"name": "Grand Theft Minecart",
|
||||
"ip": "gtm.network",
|
||||
"type": "PC",
|
||||
"id": 8
|
||||
},
|
||||
{
|
||||
"name": "HiveMC",
|
||||
"ip": "geo.hivebedrock.network",
|
||||
"type": "PE",
|
||||
"id": 9
|
||||
"type": "PC"
|
||||
},
|
||||
{
|
||||
"name": "Purple Prison",
|
||||
"ip": "MCSL.PURPLE.WTF",
|
||||
"type": "PC",
|
||||
"id": 10
|
||||
"type": "PC"
|
||||
},
|
||||
{
|
||||
"name": "MinecraftOnline",
|
||||
"ip": "minecraftonline.com",
|
||||
"type": "PC",
|
||||
"id": 11
|
||||
"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"
|
||||
}
|
||||
]
|
34
docker-compose.yml
Normal file
34
docker-compose.yml
Normal file
@ -0,0 +1,34 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
tracker:
|
||||
restart: always
|
||||
image: fascinated/mc-tracker:latest
|
||||
volumes:
|
||||
- ./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:
|
||||
image: influxdb:latest
|
||||
container_name: influxdb
|
||||
networks:
|
||||
- default
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
restart: always
|
||||
ports:
|
||||
- "8086:8086"
|
||||
volumes:
|
||||
- ./influx/config:/etc/influxdb2
|
||||
- ./influx/db:/var/lib/influxdb2
|
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
package.json
14
package.json
@ -4,26 +4,26 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "nodemon --exec ts-node src/index.ts"
|
||||
"dev": "nodemon --exec ts-node src/index.ts",
|
||||
"build": "tsup src/index.ts --format cjs",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@influxdata/influxdb-client": "^1.33.2",
|
||||
"@types/mcping-js": "^1.5.4",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"axios": "^1.6.4",
|
||||
"dns": "^0.2.2",
|
||||
"mcpe-ping-fixed": "^0.0.3",
|
||||
"mcping-js": "^1.5.0",
|
||||
"node-cron": "^3.0.3",
|
||||
"socket.io": "^4.7.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.3.3",
|
||||
"winston": "^3.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.6"
|
||||
}
|
||||
}
|
||||
|
1440
pnpm-lock.yaml
generated
1440
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
3
renovate.json
Normal file
3
renovate.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["config:recommended", ":dependencyDashboard"]
|
||||
}
|
@ -1,158 +0,0 @@
|
||||
import SQLiteDatabase from "better-sqlite3";
|
||||
import cron from "node-cron";
|
||||
import Server from "../server/server";
|
||||
import { logger } from "../utils/logger";
|
||||
import { getFormattedDate } from "../utils/timeUtils";
|
||||
|
||||
import Config from "../../data/config.json";
|
||||
import { Ping } from "../types/ping";
|
||||
import { createDirectory } from "../utils/fsUtils";
|
||||
|
||||
const DATA_DIR = "data";
|
||||
const BACKUP_DIR = `${DATA_DIR}/database-backups`;
|
||||
|
||||
const PINGS_TABLE = "pings";
|
||||
const RECORD_TABLE = "record";
|
||||
|
||||
/**
|
||||
* SQL Queries
|
||||
*/
|
||||
const CREATE_PINGS_TABLE = `
|
||||
CREATE TABLE IF NOT EXISTS pings (
|
||||
id INTEGER NOT NULL,
|
||||
timestamp BIGINT NOT NULL,
|
||||
ip TINYTEXT NOT NULL,
|
||||
playerCount MEDIUMINT NOT NULL
|
||||
);
|
||||
`;
|
||||
const CREATE_RECORD_TABLE = `
|
||||
CREATE TABLE IF NOT EXISTS record (
|
||||
id INTEGER PRIMARY KEY,
|
||||
timestamp BIGINT NOT NULL,
|
||||
ip TINYTEXT NOT NULL,
|
||||
playerCount MEDIUMINT NOT NULL
|
||||
);
|
||||
`;
|
||||
|
||||
const CREATE_PINGS_INDEX = `CREATE INDEX IF NOT EXISTS ip_index ON pings (id, ip, playerCount)`;
|
||||
const CREATE_TIMESTAMP_INDEX = `CREATE INDEX IF NOT EXISTS timestamp_index on PINGS (id, timestamp)`;
|
||||
|
||||
const INSERT_PING = `
|
||||
INSERT INTO ${PINGS_TABLE} (id, timestamp, ip, playerCount)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`;
|
||||
const INSERT_RECORD = `
|
||||
INSERT INTO ${RECORD_TABLE} (id, timestamp, ip, playerCount)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
timestamp = excluded.timestamp,
|
||||
playerCount = excluded.playerCount,
|
||||
ip = excluded.ip
|
||||
`;
|
||||
|
||||
const SELECT_PINGS = `
|
||||
SELECT * FROM ${PINGS_TABLE} WHERE id = ? AND timestamp >= ? AND timestamp <= ?
|
||||
`;
|
||||
const SELECT_RECORD = `
|
||||
SELECT playerCount, timestamp FROM ${RECORD_TABLE} WHERE {} = ?
|
||||
`;
|
||||
const SELECT_RECORD_BY_ID = SELECT_RECORD.replace("{}", "id");
|
||||
const SELECT_RECORD_BY_IP = SELECT_RECORD.replace("{}", "ip");
|
||||
|
||||
export default class Database {
|
||||
private db: SQLiteDatabase.Database;
|
||||
|
||||
constructor() {
|
||||
this.db = new SQLiteDatabase(`${DATA_DIR}/db.sqlite`);
|
||||
this.db.pragma("journal_mode = WAL"); // Enable WAL mode for better performance
|
||||
|
||||
logger.info("Ensuring tables exist");
|
||||
this.db.exec(CREATE_PINGS_TABLE); // Ensure the pings table exists
|
||||
this.db.exec(CREATE_RECORD_TABLE); // Ensure the record table exists
|
||||
|
||||
logger.info("Ensuring indexes exist");
|
||||
this.db.exec(CREATE_PINGS_INDEX); // Ensure the pings index exists
|
||||
this.db.exec(CREATE_TIMESTAMP_INDEX); // Ensure the timestamp index exists
|
||||
|
||||
cron.schedule(Config.backup.cron, () => {
|
||||
this.createBackup();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the pings for a server.
|
||||
*
|
||||
* @param id the server ID
|
||||
* @param startTime the start time
|
||||
* @param endTime the end time
|
||||
* @returns the pings for the server
|
||||
*/
|
||||
public getPings(id: number, startTime: number, endTime: number): Ping[] | [] {
|
||||
return this.db.prepare(SELECT_PINGS).all(id, startTime, endTime) as
|
||||
| Ping[]
|
||||
| [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the record player count for a server.
|
||||
*
|
||||
* @param value the server ID or IP
|
||||
* @returns the record for the server
|
||||
*/
|
||||
public getRecord(value: any): Ping | undefined {
|
||||
if (typeof value === "number") {
|
||||
return this.db.prepare(SELECT_RECORD_BY_ID).get(value) as
|
||||
| Ping
|
||||
| undefined;
|
||||
}
|
||||
return this.db.prepare(SELECT_RECORD_BY_IP).get(value) as Ping | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a full backup of the database.
|
||||
*/
|
||||
public async createBackup() {
|
||||
logger.info("Creating database backup");
|
||||
createDirectory(BACKUP_DIR);
|
||||
await this.db.backup(`${BACKUP_DIR}/${getFormattedDate()}.sqlite`);
|
||||
logger.info("Finished creating database backup");
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a ping into the database.
|
||||
*
|
||||
* @param timestamp the timestamp of the ping
|
||||
* @param ip the IP address of the server
|
||||
* @param playerCount the number of players online
|
||||
*/
|
||||
public insertPing(server: Server, response: Ping) {
|
||||
const { timestamp, playerCount } = response;
|
||||
const id = server.getID();
|
||||
const ip = server.getIP();
|
||||
|
||||
const statement = this.db.prepare(INSERT_PING);
|
||||
statement.run(id, timestamp, ip, playerCount); // Insert the ping into the database
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a record into the database.
|
||||
*
|
||||
* @param server the server to insert
|
||||
* @param response the response to insert
|
||||
* @returns true if the a new record was set, false otherwise
|
||||
*/
|
||||
public insertRecord(server: Server, response: Ping): boolean {
|
||||
const { timestamp, playerCount } = response;
|
||||
const id = server.getID();
|
||||
const ip = server.getIP();
|
||||
|
||||
const oldRecord = this.getRecord(id);
|
||||
if (oldRecord && oldRecord.playerCount >= playerCount) {
|
||||
return false; // Don't update the record if the player count is lower
|
||||
}
|
||||
|
||||
const statement = this.db.prepare(INSERT_RECORD);
|
||||
statement.run(id, timestamp, ip, playerCount); // Insert the record into the database
|
||||
return true;
|
||||
}
|
||||
}
|
45
src/index.ts
45
src/index.ts
@ -1,43 +1,14 @@
|
||||
import Database from "./database/database";
|
||||
import Scanner from "./scanner/scanner";
|
||||
import Influx from "./influx/influx";
|
||||
import ServerManager from "./server/serverManager";
|
||||
import WebsocketServer from "./websocket/websocket";
|
||||
|
||||
import Config from "../data/config.json";
|
||||
import WebsiteManager from "./website/websiteManager";
|
||||
import {logger} from "./utils/logger";
|
||||
|
||||
/**
|
||||
* The database instance.
|
||||
* The influx database instance.
|
||||
*/
|
||||
export const database = new Database();
|
||||
export const influx = new Influx();
|
||||
|
||||
/**
|
||||
* The server manager instance.
|
||||
*/
|
||||
export const serverManager = new ServerManager();
|
||||
new ServerManager();
|
||||
new WebsiteManager();
|
||||
|
||||
/**
|
||||
* The websocket server instance.
|
||||
*/
|
||||
export const websocketServer = new WebsocketServer(Config.websocket.port);
|
||||
|
||||
(async () => {
|
||||
await serverManager.init();
|
||||
|
||||
// The scanner is responsible for scanning all servers
|
||||
new Scanner();
|
||||
})();
|
||||
|
||||
// The websocket server is responsible for
|
||||
// sending data to the client in real time
|
||||
|
||||
// serverManager.getServers().forEach((server) => {
|
||||
// const record = database.getRecord(server.getID());
|
||||
// if (!record) {
|
||||
// return;
|
||||
// }
|
||||
// console.log(
|
||||
// `Record for "${server.getName()}": ${record.playerCount} (${formatTimestamp(
|
||||
// record.timestamp
|
||||
// )})`
|
||||
// );
|
||||
// });
|
||||
logger.info("Done loading!");
|
34
src/influx/influx.ts
Normal file
34
src/influx/influx.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { InfluxDB, Point, WriteApi } from "@influxdata/influxdb-client";
|
||||
|
||||
import Config from "../../data/config.json";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export default class Influx {
|
||||
private influx: InfluxDB;
|
||||
private writeApi: WriteApi;
|
||||
|
||||
constructor() {
|
||||
logger.info("Loading influx database");
|
||||
|
||||
this.influx = new InfluxDB({
|
||||
url: Config.influx.url,
|
||||
token: Config.influx.token,
|
||||
});
|
||||
this.writeApi = this.influx.getWriteApi(
|
||||
Config.influx.org,
|
||||
Config.influx.bucket,
|
||||
"ms"
|
||||
);
|
||||
|
||||
logger.info("InfluxDB initialized");
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a point to the database.
|
||||
*
|
||||
* @param point the point to write
|
||||
*/
|
||||
public writePoint(point: Point) {
|
||||
this.writeApi.writePoint(point);
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
import cron from "node-cron";
|
||||
import { database, serverManager, websocketServer } from "..";
|
||||
import Server, { ServerStatus } from "../server/server";
|
||||
|
||||
import Config from "../../data/config.json";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export default class Scanner {
|
||||
constructor() {
|
||||
logger.info("Loading scanner database");
|
||||
|
||||
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) => this.scanServer(server))
|
||||
);
|
||||
|
||||
logger.info("Finished scanning servers");
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans a server and inserts the ping into the database.
|
||||
*
|
||||
* @param server the server to scan
|
||||
* @returns a promise that resolves when the server has been scanned
|
||||
*/
|
||||
async scanServer(server: Server): Promise<void> {
|
||||
//logger.info(`Scanning server ${server.getIP()} - ${server.getType()}`);
|
||||
let response;
|
||||
let online = false;
|
||||
|
||||
try {
|
||||
response = await server.pingServer();
|
||||
if (response == undefined) {
|
||||
return; // Server is offline
|
||||
}
|
||||
online = true;
|
||||
} catch (err) {
|
||||
logger.info(`Failed to ping ${server.getIP()}`, err);
|
||||
websocketServer.sendServerError(server, ServerStatus.OFFLINE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!online || !response) {
|
||||
return; // Server is offline
|
||||
}
|
||||
|
||||
database.insertPing(server, response);
|
||||
const isNewRecord = database.insertRecord(server, response);
|
||||
|
||||
// todo: send all server pings at once
|
||||
websocketServer.sendNewPing(server, response, isNewRecord);
|
||||
}
|
||||
}
|
@ -2,8 +2,11 @@ import javaPing from "mcping-js";
|
||||
import { ResolvedServer, resolveDns } from "../utils/dnsResolver";
|
||||
const bedrockPing = require("mcpe-ping-fixed"); // Doesn't have typescript definitions
|
||||
|
||||
import { Point } from "@influxdata/influxdb-client";
|
||||
import { influx } from "..";
|
||||
import Config from "../../data/config.json";
|
||||
import { Ping } from "../types/ping";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* The type of server.
|
||||
@ -12,12 +15,7 @@ import { Ping } from "../types/ping";
|
||||
*/
|
||||
export type ServerType = "PC" | "PE";
|
||||
|
||||
export enum ServerStatus {
|
||||
OFFLINE = "Unable to reach host",
|
||||
}
|
||||
|
||||
type ServerOptions = {
|
||||
id: number;
|
||||
name: string;
|
||||
ip: string;
|
||||
port?: number;
|
||||
@ -30,35 +28,25 @@ type DnsInfo = {
|
||||
};
|
||||
|
||||
export default class Server {
|
||||
/**
|
||||
* The ID of the server.
|
||||
*/
|
||||
private id: number;
|
||||
|
||||
/**
|
||||
* The name of the server.
|
||||
*/
|
||||
private name: string;
|
||||
private readonly name: string;
|
||||
|
||||
/**
|
||||
* The IP address of the server.
|
||||
*/
|
||||
private ip: string;
|
||||
private readonly ip: string;
|
||||
|
||||
/**
|
||||
* The port of the server.
|
||||
*/
|
||||
private port: number | undefined;
|
||||
private readonly port: number | undefined;
|
||||
|
||||
/**
|
||||
* The type of server.
|
||||
*/
|
||||
private type: ServerType;
|
||||
|
||||
/**
|
||||
* The favicon of the server.
|
||||
*/
|
||||
private favicon: string | undefined;
|
||||
private readonly type: ServerType;
|
||||
|
||||
/**
|
||||
* The resolved server information from
|
||||
@ -68,8 +56,7 @@ export default class Server {
|
||||
hasResolved: false,
|
||||
};
|
||||
|
||||
constructor({ id, name, ip, port, type }: ServerOptions) {
|
||||
this.id = id;
|
||||
constructor({ name, ip, port, type }: ServerOptions) {
|
||||
this.name = name;
|
||||
this.ip = ip;
|
||||
this.port = port;
|
||||
@ -79,23 +66,47 @@ export default class Server {
|
||||
/**
|
||||
* Pings a server and gets the response.
|
||||
*
|
||||
* @param server the server to ping
|
||||
* @param insertPing whether to insert the ping into the database
|
||||
* @returns the ping response or undefined if the server is offline
|
||||
*/
|
||||
public pingServer(): Promise<Ping | undefined> {
|
||||
public async pingServer(): Promise<Ping | undefined> {
|
||||
const before = Date.now();
|
||||
try {
|
||||
let response;
|
||||
|
||||
switch (this.getType()) {
|
||||
case "PC": {
|
||||
return this.pingPCServer();
|
||||
response = await this.pingPCServer();
|
||||
break;
|
||||
}
|
||||
case "PE": {
|
||||
return this.pingPEServer();
|
||||
response = await this.pingPEServer();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Unknown server type ${this.getType()} for ${this.getName()}`
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
try {
|
||||
influx.writePoint(
|
||||
new Point("playerCount")
|
||||
.tag("name", this.getName())
|
||||
.intField("playerCount", response.playerCount)
|
||||
.intField("latency", Date.now() - before)
|
||||
.timestamp(response.timestamp)
|
||||
);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Failed to write point to Influx for ${this.getName()} - ${this.getIP()}`,
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve(response);
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to ping ${this.getIP()}`, err);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,15 +143,14 @@ export default class Server {
|
||||
|
||||
const serverPing = new javaPing.MinecraftServer(ip, port);
|
||||
|
||||
// todo: do something to get the latest protocol? (is this even needed??)
|
||||
return new Promise((resolve, reject) => {
|
||||
serverPing.ping(Config.scanner.timeout, 700, (err, res) => {
|
||||
serverPing.ping(Config.pinger.timeout, 765, (err, res) => {
|
||||
if (err || res == undefined) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
this.favicon = res.favicon; // Set the favicon
|
||||
resolve({
|
||||
id: this.getID(),
|
||||
timestamp: Date.now(),
|
||||
ip: ip,
|
||||
playerCount: res.players.online,
|
||||
@ -166,7 +176,6 @@ export default class Server {
|
||||
}
|
||||
|
||||
resolve({
|
||||
id: this.getID(),
|
||||
timestamp: Date.now(),
|
||||
ip: this.getIP(),
|
||||
playerCount: res.currentPlayers,
|
||||
@ -177,12 +186,12 @@ export default class Server {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the server.
|
||||
*
|
||||
* @returns the ID
|
||||
* Invalidates the DNS cache for the server.
|
||||
*/
|
||||
public getID(): number {
|
||||
return this.id;
|
||||
public invalidateDns() {
|
||||
this.dnsInfo = {
|
||||
hasResolved: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -220,13 +229,4 @@ export default class Server {
|
||||
public getType(): ServerType {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the favicon of the server.
|
||||
*
|
||||
* @returns the favicon
|
||||
*/
|
||||
public getFavicon(): string | undefined {
|
||||
return this.favicon;
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +1,46 @@
|
||||
import cron from "node-cron";
|
||||
import { logger } from "../utils/logger";
|
||||
import Server, { ServerType } from "./server";
|
||||
|
||||
import Config from "../../data/config.json";
|
||||
import Servers from "../../data/servers.json";
|
||||
|
||||
export default class ServerManager {
|
||||
private servers: Server[] = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Loads the servers from the config file.
|
||||
*/
|
||||
async init() {
|
||||
constructor() {
|
||||
logger.info("Loading servers...");
|
||||
for (const configServer of Servers) {
|
||||
const server = new Server({
|
||||
id: configServer.id,
|
||||
ip: configServer.ip,
|
||||
name: configServer.name,
|
||||
type: configServer.type as ServerType,
|
||||
});
|
||||
try {
|
||||
await server.pingServer();
|
||||
} catch (err) {}
|
||||
this.servers.push(server);
|
||||
}
|
||||
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.
|
||||
*
|
||||
* @returns the servers
|
||||
* Ping all servers to update their status.
|
||||
*/
|
||||
public getServers(): Server[] {
|
||||
return this.servers;
|
||||
private async pingServers(): Promise<void> {
|
||||
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,5 +1,4 @@
|
||||
export type Ping = {
|
||||
id: number;
|
||||
timestamp: number;
|
||||
ip: string;
|
||||
playerCount: number;
|
||||
|
@ -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(
|
||||
timestamp({ format: "YY-MM-DD HH:MM:SS" }),
|
||||
timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
printf((info: LogInfo) => {
|
||||
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!");
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
import { Socket, Server as SocketServer } from "socket.io";
|
||||
import { serverManager } from "..";
|
||||
import Server, { ServerStatus } from "../server/server";
|
||||
import { Ping } from "../types/ping";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export default class WebsocketServer {
|
||||
private server: SocketServer;
|
||||
|
||||
constructor(port: number) {
|
||||
logger.info(`Starting websocket server on port ${port}`);
|
||||
this.server = new SocketServer(port);
|
||||
|
||||
this.server.on("connection", (socket) => {
|
||||
logger.debug("ws: Client connected");
|
||||
this.sendServerList(socket);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the server list to the given socket.
|
||||
*
|
||||
* @param socket the socket to send the server list to
|
||||
*/
|
||||
public sendServerList(socket: Socket): void {
|
||||
logger.debug(`ws: Sending server list to ${socket.id}`);
|
||||
|
||||
const servers = [];
|
||||
for (const server of serverManager.getServers()) {
|
||||
servers.push({
|
||||
id: server.getID(),
|
||||
name: server.getName(),
|
||||
ip: server.getIP(),
|
||||
port: server.getPort(),
|
||||
favicon: server.getFavicon(),
|
||||
});
|
||||
}
|
||||
socket.emit("serverList", servers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the latest ping data for the given server to all clients.
|
||||
*
|
||||
* @param server the server to send the ping for
|
||||
* @param pingResponse the ping data to send
|
||||
* @param isNewRecord whether a new record has been set
|
||||
*/
|
||||
public sendNewPing(
|
||||
server: Server,
|
||||
pingResponse: Ping,
|
||||
isNewRecord: boolean
|
||||
): void {
|
||||
logger.debug(`ws: Sending new ping for ${server.getName()}`);
|
||||
this.server.emit("newPing", {
|
||||
server: server.getID(),
|
||||
timestamp: pingResponse.timestamp,
|
||||
playerCount: pingResponse.playerCount,
|
||||
});
|
||||
if (isNewRecord) {
|
||||
logger.debug(`ws: Sending new record for ${server.getName()}`);
|
||||
this.server.emit("newRecord", {
|
||||
server: server.getID(),
|
||||
timestamp: pingResponse.timestamp,
|
||||
playerCount: pingResponse.playerCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the server status for the given server to all clients.
|
||||
*
|
||||
* @param server the server to send the status for
|
||||
* @param status the status to send
|
||||
*/
|
||||
public sendServerError(server: Server, status: ServerStatus): void {
|
||||
logger.debug(`ws: Sending server status for ${server.getName()}`);
|
||||
this.server.emit("serverStatus", {
|
||||
server: server.getID(),
|
||||
status: status,
|
||||
});
|
||||
}
|
||||
}
|
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