From 026a0cddbee64245c0a1e607427d8b4a8c256643 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sun, 1 Sep 2024 12:51:20 -0700 Subject: [PATCH] use structed logging --- TODO.txt | 9 +- package-lock.json | 192 +++++++++++++++++++++++++++++++++++- package.json | 4 +- src/client/Client.ts | 69 ++++++++++--- src/client/ClientGame.ts | 5 +- src/core/Schemas.ts | 4 +- src/server/Client.ts | 6 +- src/server/GameServer.ts | 7 ++ src/server/Server.ts | 6 +- src/server/StructuredLog.ts | 21 ++++ 10 files changed, 292 insertions(+), 31 deletions(-) create mode 100644 src/server/StructuredLog.ts diff --git a/TODO.txt b/TODO.txt index e51194fa9..51097aeb6 100644 --- a/TODO.txt +++ b/TODO.txt @@ -70,11 +70,14 @@ * BUG: island don't check if inscribed, just try to remove it DONE 8/31/2024 * if completely surrounded by same enemy, lose island DONE 8/31/2024 * BUG: fix boat leaves trail DONE 9/1/2024 -* end game when no players left (or after 1 hour or so?) -* use better favicon -* BUG: tiles get left behind during conquer +* end game when no players left (or after 1 hour or so?) DONE 9/1/2024 +* add structured logging DONE 9/1/2024 +* make attack execution stream tiles to pq * Create exit to menu button +* use better favicon +* center map on game start * Make fake humans +* BUG: tiles get left behind during conquer * Load terrain dataImage in background * BUG: shore tiles left behind during conquer * BUG: when sending boat to TerraNullius, only takes one tile diff --git a/package-lock.json b/package-lock.json index d090464e3..a07f337f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "colord": "^2.9.3", "crypto": "^1.0.1", "express": "^4.19.2", + "google-auth-library": "^9.14.0", + "googleapis": "^143.0.0", "jimp": "^0.22.12", "msgpack5": "^6.0.2", "node-addon-api": "^8.1.0", @@ -33,7 +35,7 @@ "@types/chai": "^4.3.17", "@types/jest": "^29.5.12", "@types/mocha": "^10.0.7", - "@types/node": "^22.4.1", + "@types/node": "^22.5.2", "@types/sinon": "^17.0.3", "@types/ws": "^8.5.11", "babel-jest": "^29.7.0", @@ -4138,9 +4140,9 @@ } }, "node_modules/@types/node": { - "version": "22.4.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.1.tgz", - "integrity": "sha512-1tbpb9325+gPnKK0dMm+/LMriX0vKxf6RnB0SZUqfyVkQ4fMgUSySqhxE/y8Jvs4NyF1yHzTfG9KlnkIODxPKg==", + "version": "22.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.2.tgz", + "integrity": "sha512-acJsPTEqYqulZS/Yp/S3GgeE6GZ0qYODUR8aVr/DkhHQ8l9nd4j5x1/ZJy9/gHrRlFMqkO6i0I3E27Alu4jjPg==", "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -4984,6 +4986,15 @@ "node": "*" } }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-base64-loader": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/binary-base64-loader/-/binary-base64-loader-1.0.0.tgz", @@ -5204,6 +5215,12 @@ "node": ">=0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6210,6 +6227,15 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6635,6 +6661,12 @@ "node": ">= 0.10.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -6922,6 +6954,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7068,6 +7142,66 @@ "node": ">=4" } }, + "node_modules/google-auth-library": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.14.0.tgz", + "integrity": "sha512-Y/eq+RWVs55Io/anIsm24sDS8X79Tq948zVLGaa7+KlJYYqaGwp1YI37w48nzrNi12RgnzMrQD4NzdmCowT90g==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "143.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-143.0.0.tgz", + "integrity": "sha512-hGeNM9d9cDQAV/dm8FvdkismWIDCJRV9v11UTLq4nRPP+s/2jPuHQnpI7dR+sWmL0o3XURW0K3a3THKyDRnWVg==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^9.0.0", + "googleapis-common": "^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.7.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -7084,6 +7218,19 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -7817,7 +7964,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "engines": { "node": ">=8" }, @@ -8904,6 +9050,15 @@ "node": ">=4" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -8934,6 +9089,27 @@ "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", "dev": true }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -12535,6 +12711,12 @@ "punycode": "^2.1.0" } }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" + }, "node_modules/utif2": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz", diff --git a/package.json b/package.json index d6156e698..03c881d39 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@types/chai": "^4.3.17", "@types/jest": "^29.5.12", "@types/mocha": "^10.0.7", - "@types/node": "^22.4.1", + "@types/node": "^22.5.2", "@types/sinon": "^17.0.3", "@types/ws": "^8.5.11", "babel-jest": "^29.7.0", @@ -56,6 +56,8 @@ "colord": "^2.9.3", "crypto": "^1.0.1", "express": "^4.19.2", + "google-auth-library": "^9.14.0", + "googleapis": "^143.0.0", "jimp": "^0.22.12", "msgpack5": "^6.0.2", "node-addon-api": "^8.1.0", diff --git a/src/client/Client.ts b/src/client/Client.ts index b1b223ed4..423c34756 100644 --- a/src/client/Client.ts +++ b/src/client/Client.ts @@ -27,6 +27,8 @@ class Client { private random = new PseudoRandom(1234) + private ip: Promise = null + constructor() { this.lobbiesContainer = document.getElementById('lobbies-container'); } @@ -35,6 +37,7 @@ class Client { setFavicon() this.terrainMap = loadTerrainMap() this.startLobbyPolling() + this.ip = getClientIP() setupUsernameCallback((username) => { console.log('Username updated:', username); if (this.game != null) { @@ -100,7 +103,7 @@ class Client { } } - private joinLobby(lobby: Lobby) { + private async joinLobby(lobby: Lobby) { const lobbyButton = document.getElementById('lobby-button'); if (lobbyButton) { this.isLobbyHighlighted = !this.isLobbyHighlighted; @@ -115,21 +118,26 @@ class Client { if (this.game != null) { return; } - this.terrainMap.then(tm => { - this.game = createClientGame(getUsername(), new PseudoRandom(Date.now()).nextID(), lobby.id, getConfig(), tm); - this.game.join(); - const g = this.game; - window.addEventListener('beforeunload', function (event) { - console.log('Browser is closing'); - g.stop(); - }); - }) + const [terrainMap, clientIP] = await Promise.all([ + this.terrainMap, + this.ip + ]); + console.log(`got ip ${clientIP}`) + this.game = createClientGame( + getUsername(), + new PseudoRandom(Date.now()).nextID(), // TODO this can cause dup ids + clientIP, + lobby.id, + getConfig(), + terrainMap + ); + this.game.join(); + const g = this.game; + window.addEventListener('beforeunload', function (event) { + console.log('Browser is closing'); + g.stop(); + }); } - - - - - } function getUsername(): string { @@ -154,6 +162,37 @@ function setupUsernameCallback(callback: (username: string) => void): void { } +async function getClientIP(timeoutMs: number = 1000): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response: Response = await fetch('https://api.ipify.org?format=json', { + signal: controller.signal + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data: {ip: string} = await response.json(); + return data.ip; + } catch (error) { + if (error instanceof Error) { + if (error.name === 'AbortError') { + console.error('Request timed out'); + } else { + console.error('Error fetching IP:', error.message); + } + } else { + console.error('An unknown error occurred'); + } + return null; + } finally { + clearTimeout(timeoutId); + } +} + // Initialize the client when the DOM is loaded diff --git a/src/client/ClientGame.ts b/src/client/ClientGame.ts index f768d0b28..1e59a2977 100644 --- a/src/client/ClientGame.ts +++ b/src/client/ClientGame.ts @@ -12,7 +12,7 @@ import {TerrainRenderer} from "./graphics/TerrainRenderer"; -export function createClientGame(name: string, clientID: ClientID, gameID: GameID, config: Config, terrainMap: TerrainMap): ClientGame { +export function createClientGame(name: string, clientID: ClientID, ip: string | null, gameID: GameID, config: Config, terrainMap: TerrainMap): ClientGame { let eventBus = new EventBus() let game = createGame(terrainMap, eventBus, config) let terrainRenderer = new TerrainRenderer(game) @@ -21,6 +21,7 @@ export function createClientGame(name: string, clientID: ClientID, gameID: GameI return new ClientGame( name, clientID, + ip, gameID, eventBus, game, @@ -47,6 +48,7 @@ export class ClientGame { constructor( public playerName: string, private id: ClientID, + private clientIP: string | null, private gameID: GameID, private eventBus: EventBus, private gs: Game, @@ -67,6 +69,7 @@ export class ClientGame { type: "join", gameID: this.gameID, clientID: this.id, + clientIP: this.clientIP, lastTurn: this.turns.length }) ) diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 5fca9197c..2bca38813 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -114,9 +114,9 @@ export const ClientIntentMessageSchema = ClientBaseMessageSchema.extend({ export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({ type: z.literal('join'), clientID: z.string(), + clientIP: z.string().nullable(), gameID: z.string(), - // The last turn the client saw. - lastTurn: z.number() + lastTurn: z.number() // The last turn the client saw. }) export const ClientLeaveMessageSchema = ClientBaseMessageSchema.extend({ diff --git a/src/server/Client.ts b/src/server/Client.ts index ede75c3b2..b38578d02 100644 --- a/src/server/Client.ts +++ b/src/server/Client.ts @@ -3,5 +3,9 @@ import {ClientID} from '../core/Schemas'; export class Client { - constructor(public readonly id: ClientID, public readonly ws: WebSocket) { } + constructor( + public readonly id: ClientID, + public readonly ip: string | null, + public readonly ws: WebSocket + ) { } } \ No newline at end of file diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index f7e04d0e6..e55ebb862 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -2,6 +2,7 @@ import {ClientMessage, ClientMessageSchema, Intent, ServerStartGameMessage, Serv import {Config} from "../core/configuration/Config"; import {Client} from "./Client"; import WebSocket from 'ws'; +import {slog} from "./StructuredLog"; export enum GamePhase { @@ -30,6 +31,12 @@ export class GameServer { public addClient(client: Client, lastTurn: number) { console.log(`game ${this.id} adding client ${client.id}`) + slog('client_joined_game', `client ${client.id} (re)joining game ${this.id}`, { + clientID: client.id, + clientIP: client.ip, + gameID: this.id, + isRejoin: lastTurn > 0 + }) // Remove stale client if this is a reconnect this.clients = this.clients.filter(c => c.id != client.id) this.clients.push(client) diff --git a/src/server/Server.ts b/src/server/Server.ts index c447614cc..276388abf 100644 --- a/src/server/Server.ts +++ b/src/server/Server.ts @@ -8,6 +8,7 @@ import {Client} from './Client'; import {ClientMessage, ClientMessageSchema} from '../core/Schemas'; import {GamePhase} from './GameServer'; import {getConfig} from '../core/configuration/Config'; +import {LogSeverity, slog} from './StructuredLog'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -31,11 +32,10 @@ app.get('/lobbies', (req, res) => { wss.on('connection', (ws) => { ws.on('message', (message: string) => { - console.log(`got message ${message}`) const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message)) + slog('websocket_msg', 'server received websocket message', clientMsg, LogSeverity.DEBUG) if (clientMsg.type == "join") { - console.log('got join request') - gm.addClient(new Client(clientMsg.clientID, ws), clientMsg.gameID, clientMsg.lastTurn) + gm.addClient(new Client(clientMsg.clientID, clientMsg.clientIP, ws), clientMsg.gameID, clientMsg.lastTurn) } // TODO: send error message }) diff --git a/src/server/StructuredLog.ts b/src/server/StructuredLog.ts new file mode 100644 index 000000000..db629f4ef --- /dev/null +++ b/src/server/StructuredLog.ts @@ -0,0 +1,21 @@ +export enum LogSeverity { + DEBUG = 'DEBUG', + INFO = 'INFO', + WARN = 'WARN', + ERROR = 'ERROR', + FATAL = 'FATAL' +} + +export function slog(eventType: string, description, data: any, severity = LogSeverity.INFO): void { + const logEntry = { + eventType: eventType, + description: description, + severity: severity, + data: data + }; + if (process.env.GAME_ENV == 'dev') { + console.log(description) + } else { + console.log(JSON.stringify(logEntry)); + } +} \ No newline at end of file