diff --git a/.dockerignore b/.dockerignore index f5eaa61..9cf8841 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,7 @@ /.env /LICENSE /README.md +/build /dump.sql /flyway diff --git a/.gitignore b/.gitignore index c7191d7..482347a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.env +/build /dump.sql # JetBrains diff --git a/.prettierignore b/.prettierignore index 5f189d0..0115442 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ /.env +/build /dump.sql # Git diff --git a/Dockerfile b/Dockerfile index 9c00484..fa8d745 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,6 +31,6 @@ RUN npm ci COPY . . RUN npm run check \ - && npx tsc + && npm run build -CMD node --no-warnings --enable-source-maps build/index.js +CMD npm run production diff --git a/package.json b/package.json index cbd6a57..46e4d82 100644 --- a/package.json +++ b/package.json @@ -38,14 +38,18 @@ "typescript": "5.2.2" }, "license": "MIT", - "private": true, "name": "pedestrian", + "private": true, "scripts": { + "build": "shx rm -rf ./build && tsc", "check": "tsc --noemit && eslint . && prettier --check .", + "debug": "node --trace-warnings --enable-source-maps ./build/index.js | pino-pretty", "dump": "docker exec --tty postgres pg_dumpall --clean --username=user > dump.sql", "migrate": "docker-compose up --detach --build flyway", "prettier": "prettier --write .", + "production": "node --no-warnings --enable-source-maps ./build/index.js", "restore": "shx cat dump.sql | docker exec --interactive postgres psql --username=user --dbname=postgres", - "start": "docker-compose up --detach --build pedestrian" + "services": "docker-compose up --detach postgres redis flyway", + "start": "docker-compose up --build pedestrian" } } diff --git a/src/commands/carsized/carsized.manager.ts b/src/commands/carsized/carsized.manager.ts index 5e64f45..b8a0d91 100644 --- a/src/commands/carsized/carsized.manager.ts +++ b/src/commands/carsized/carsized.manager.ts @@ -2,19 +2,17 @@ import type { Index } from "lunr"; import lunr from "lunr"; import assert from "node:assert"; -import loggerFactory from "pino"; import type { Car, CompareCars } from "./types"; +import loggerFactory from "../../logger.factory"; import Environment from "../../shared/environment"; import { isNonNullable } from "../../shared/nullable"; import { usePage } from "../../shared/puppeteer"; import RedisKey, * as redis from "../../shared/redis"; import { Perspective } from "./constants"; -const logger = loggerFactory({ - name: __filename, -}); +const logger = loggerFactory(module); const CarsizedBaseUrl = "https://www.carsized.com/en"; diff --git a/src/commands/carsized/context.ts b/src/commands/carsized/context.ts index f930286..2aa4e10 100644 --- a/src/commands/carsized/context.ts +++ b/src/commands/carsized/context.ts @@ -4,10 +4,10 @@ import type { } from "discord.js"; import { AttachmentBuilder } from "discord.js"; -import loggerFactory from "pino"; import type { CompareCars } from "./types"; +import loggerFactory from "../../logger.factory"; import Session from "../../session"; import * as carsized from "./carsized.manager"; import UI from "./ui"; @@ -20,9 +20,7 @@ export type Context = CompareCars & { type Interaction = CommandInteraction | MessageComponentInteraction; // endregion -const logger = loggerFactory({ - name: __filename, -}); +const logger = loggerFactory(module); const session = new Session(); export default session; diff --git a/src/commands/surveys/functions.ts b/src/commands/surveys/functions.ts index 14f92d9..8d5ee1b 100644 --- a/src/commands/surveys/functions.ts +++ b/src/commands/surveys/functions.ts @@ -2,7 +2,6 @@ import type { BaseInteraction } from "discord.js"; import { messageLink } from "discord.js"; import assert from "node:assert"; -import loggerFactory from "pino"; import type { Answer, @@ -16,11 +15,10 @@ import type { Survey, } from "./types"; +import loggerFactory from "../../logger.factory"; import { QuestionType } from "./constants"; -const logger = loggerFactory({ - name: __filename, -}); +const logger = loggerFactory(module); // region Survey export const surveyLink = ({ channelId, guildId, id }: PartialSurvey) => diff --git a/src/creators/post/index.ts b/src/creators/post/index.ts index 10ac80f..4b6528b 100644 --- a/src/creators/post/index.ts +++ b/src/creators/post/index.ts @@ -9,12 +9,12 @@ import { roleMention, } from "discord.js"; import assert from "node:assert"; -import loggerFactory from "pino"; import type Nullable from "../../shared/nullable"; import type { CreatorType } from "../constants"; import type { CreatorSubscription } from "./database"; +import loggerFactory from "../../logger.factory"; import { byDate, isUnique } from "../../shared/array"; import discord from "../../shared/discord"; import { isNullable } from "../../shared/nullable"; @@ -35,9 +35,7 @@ export type Option = { type Poster = (creatorDomainId: string) => Promise; // endregion -const logger = loggerFactory({ - name: __filename, -}); +const logger = loggerFactory(module); const posters = new Map(); export const registerPoster = (creatorType: CreatorType, poster: Poster) => { diff --git a/src/index.ts b/src/index.ts index d5fb644..cbf84fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,14 @@ import { glob } from "glob"; import path from "node:path"; -import loggerFactory from "pino"; import { collectDefaultMetrics } from "prom-client"; +import loggerFactory from "./logger.factory"; import discord from "./shared/discord"; import Environment from "./shared/environment"; import express from "./shared/express"; // region Logger and Metrics -const logger = loggerFactory({ - name: __filename, -}); +const logger = loggerFactory(module); collectDefaultMetrics(); // endregion diff --git a/src/logger.factory.ts b/src/logger.factory.ts new file mode 100644 index 0000000..f62f2d4 --- /dev/null +++ b/src/logger.factory.ts @@ -0,0 +1,17 @@ +import path from "node:path"; +import pino from "pino"; + +export default ({ filename }: NodeModule) => + pino({ + errorKey: "error", + formatters: { + bindings: () => ({ + name: path.relative(__dirname, filename), + }), + level: (label) => ({ + level: label, + }), + }, + level: "debug", + messageKey: "message", + }); diff --git a/src/shared/discord.ts b/src/shared/discord.ts index 9731d3a..18dc3ef 100644 --- a/src/shared/discord.ts +++ b/src/shared/discord.ts @@ -3,28 +3,38 @@ import type { BaseInteraction, CommandInteraction, ContextMenuCommandBuilder, - InteractionResponse, - Message, MessageComponentInteraction, ModalSubmitInteraction, SlashCommandBuilder, } from "discord.js"; -import { Client, Events, Routes, User } from "discord.js"; +import { + Client, + Events, + InteractionResponse, + Message, + Routes, + User, +} from "discord.js"; import assert from "node:assert"; -import loggerFactory from "pino"; -import { Histogram } from "prom-client"; +import { Gauge, Histogram } from "prom-client"; + +import loggerFactory from "../logger.factory"; // region Logger and Metrics -const logger = loggerFactory({ - name: __filename, -}); +const logger = loggerFactory(module); const interactionRequestDuration = new Histogram({ help: "Interaction request duration in milliseconds", labelNames: ["status", "handler"], name: "interaction_request_duration_milliseconds", }); + +const shardPing = new Gauge({ + help: "Shard ping in milliseconds", + labelNames: ["shard"], + name: "shard_ping_milliseconds", +}); // endregion // region Constants @@ -41,8 +51,16 @@ const discord = new Client({ intents: ["Guilds"], }); +const { ws } = discord; +const { shards } = ws; + discord.on(Events.Debug, (debug) => { logger.debug(debug, "DISCORD_DEBUG"); + + for (const [shard, { ping }] of shards) { + const labels = { shard }; + shardPing.set(labels, ping); + } }); discord.on(Events.Warn, (warn) => { @@ -171,14 +189,18 @@ const getHandler = (interaction: BaseInteraction, uiid?: string) => { const onInteraction = ( status: "error" | "success", interaction: BaseInteraction, - startRequestTime: number, uiid?: string, ) => { const handler = getHandler(interaction, uiid); const labels = { handler, status }; return (result: unknown) => { - const endRequestTime = performance.now(); + const endRequestTime = + result instanceof InteractionResponse || result instanceof Message + ? result.createdTimestamp + : Date.now(); + + const startRequestTime = interaction.createdTimestamp; const requestDuration = endRequestTime - startRequestTime; interactionRequestDuration.observe(labels, requestDuration); @@ -200,37 +222,28 @@ const onInteraction = ( }; }; -const onCommand = ( - interaction: CommandInteraction, - startRequestTime: number, -) => { +const onCommand = (interaction: CommandInteraction) => { for (const [name, { onCommand }] of commands) { if (name === interaction.commandName) { return onCommand(interaction) - .then(onInteraction("success", interaction, startRequestTime)) - .catch(onInteraction("error", interaction, startRequestTime)); + .then(onInteraction("success", interaction)) + .catch(onInteraction("error", interaction)); } } }; -const onAutocomplete = ( - interaction: AutocompleteInteraction, - startRequestTime: number, -) => { +const onAutocomplete = (interaction: AutocompleteInteraction) => { for (const [name, { onAutocomplete }] of commands) { if (name === interaction.commandName) { assert(onAutocomplete !== undefined); return onAutocomplete(interaction) - .then(onInteraction("success", interaction, startRequestTime)) - .catch(onInteraction("error", interaction, startRequestTime)); + .then(onInteraction("success", interaction)) + .catch(onInteraction("error", interaction)); } } }; -const onMessageComponent = ( - interaction: MessageComponentInteraction, - startRequestTime: number, -) => { +const onMessageComponent = (interaction: MessageComponentInteraction) => { let { customId } = interaction; for (const [uiid, onComponent] of components) { const legacyPrefix = `GLOBAL_${uiid}_`; @@ -242,39 +255,34 @@ const onMessageComponent = ( if (customId.startsWith(uiid)) { const id = customId.slice(uiid.length); return onComponent(interaction, id) - .then(onInteraction("success", interaction, startRequestTime, uiid)) - .catch(onInteraction("error", interaction, startRequestTime, uiid)); + .then(onInteraction("success", interaction, uiid)) + .catch(onInteraction("error", interaction, uiid)); } } }; -const onModalSubmit = ( - interaction: ModalSubmitInteraction, - startRequestTime: number, -) => { +const onModalSubmit = (interaction: ModalSubmitInteraction) => { const { customId } = interaction; for (const [uiid, onModal] of modals) { if (customId.startsWith(uiid)) { const id = customId.slice(uiid.length); return onModal(interaction, id) - .then(onInteraction("success", interaction, startRequestTime, uiid)) - .catch(onInteraction("error", interaction, startRequestTime, uiid)); + .then(onInteraction("success", interaction, uiid)) + .catch(onInteraction("error", interaction, uiid)); } } }; discord.on(Events.InteractionCreate, async (interaction) => { - const startRequestTime = performance.now(); - let status; if (interaction.isCommand()) { - status = await onCommand(interaction, startRequestTime); + status = await onCommand(interaction); } else if (interaction.isAutocomplete()) { - status = await onAutocomplete(interaction, startRequestTime); + status = await onAutocomplete(interaction); } else if (interaction.isMessageComponent()) { - status = await onMessageComponent(interaction, startRequestTime); + status = await onMessageComponent(interaction); } else if (interaction.isModalSubmit()) { - status = await onModalSubmit(interaction, startRequestTime); + status = await onModalSubmit(interaction); } assert(status !== undefined); diff --git a/src/shared/express.ts b/src/shared/express.ts index b465d5d..f58d8f1 100644 --- a/src/shared/express.ts +++ b/src/shared/express.ts @@ -1,14 +1,13 @@ import express from "express"; import promBundle from "express-prom-bundle"; -import loggerFactory from "pino"; import pinoBundle from "pino-http"; +import loggerFactory from "../logger.factory"; + const server = express(); // region Logger and Metrics -const logger = loggerFactory({ - name: __filename, -}); +const logger = loggerFactory(module); server.use( promBundle({ diff --git a/src/shared/postgresql.ts b/src/shared/postgresql.ts index 465157d..fe7788c 100644 --- a/src/shared/postgresql.ts +++ b/src/shared/postgresql.ts @@ -1,11 +1,11 @@ import type { PoolClient } from "pg"; import { Pool } from "pg"; -import loggerFactory from "pino"; import { Histogram } from "prom-client"; import type { Caller } from "./caller"; +import loggerFactory from "../logger.factory"; import Environment from "./environment"; // region Types @@ -13,9 +13,7 @@ type Callback = (client: PoolClient) => Promise; // endregion // region Logger and Metrics -const logger = loggerFactory({ - name: __filename, -}); +const logger = loggerFactory(module); const databaseRequestDuration = new Histogram({ help: "Database request duration in milliseconds", diff --git a/src/shared/redis.ts b/src/shared/redis.ts index 9bd9185..eccbe3b 100644 --- a/src/shared/redis.ts +++ b/src/shared/redis.ts @@ -2,15 +2,13 @@ import type { Callback, Result } from "ioredis"; import { Redis } from "ioredis"; import * as crypto from "node:crypto"; -import loggerFactory from "pino"; +import loggerFactory from "../logger.factory"; import Environment from "./environment"; import { isNullable } from "./nullable"; import sleep from "./sleep"; -const logger = loggerFactory({ - name: __filename, -}); +const logger = loggerFactory(module); // region redis const node = {