feat(logging): add structured server diagnostics
CI / validate (push) Successful in 4m55s
Release / release (push) Successful in 9m46s

This commit is contained in:
dmg
2026-08-01 17:37:52 -04:00
parent 5e693e2cdd
commit 86c87153b4
19 changed files with 331 additions and 13 deletions
+1
View File
@@ -3,3 +3,4 @@ APP_URL=http://localhost:3000
DISCORD_BOT_TOKEN=
DISCORD_APPLICATION_ID=
DISCORD_GUILD_ID=
LOG_LEVEL=info
+1
View File
@@ -13,6 +13,7 @@
"@minecraft-account-manager/auth": "*",
"@minecraft-account-manager/contracts": "*",
"@minecraft-account-manager/database": "*",
"@minecraft-account-manager/logging": "*",
"discord.js": "^14.25.1",
"dotenv": "^17.2.3",
"drizzle-orm": "^0.45.1"
+14 -2
View File
@@ -2,10 +2,22 @@ import "dotenv/config";
import { REST, Routes } from "discord.js";
import { commands } from "./commands";
import { requiredEnvironment } from "./config";
import { logger } from "./logger";
const token = requiredEnvironment("DISCORD_BOT_TOKEN");
const applicationId = requiredEnvironment("DISCORD_APPLICATION_ID");
const rest = new REST({ version: "10" }).setToken(token);
await rest.put(Routes.applicationCommands(applicationId), { body: commands });
console.log(`Deployed ${commands.length} global Discord commands.`);
try {
await rest.put(Routes.applicationCommands(applicationId), { body: commands });
logger.info(
{ event: "discord.commands_deployed", commandCount: commands.length },
"Deployed global Discord commands",
);
} catch (error) {
logger.fatal(
{ err: error, event: "discord.commands_deploy_failed" },
"Failed to deploy global Discord commands",
);
process.exitCode = 1;
}
+15 -3
View File
@@ -14,6 +14,7 @@ import {
GatewayIntentBits,
} from "discord.js";
import { commandNames, requiredEnvironment } from "./config";
import { logger } from "./logger";
const token = requiredEnvironment("DISCORD_BOT_TOKEN");
const appUrl = requiredEnvironment("APP_URL");
@@ -24,7 +25,10 @@ const authRepository = createAuthRepository(db);
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.once(Events.ClientReady, (readyClient) => {
console.log(`Discord bot ready as ${readyClient.user.tag}`);
logger.info(
{ event: "discord.ready", botUserId: readyClient.user.id, botUsername: readyClient.user.username },
"Discord bot is ready",
);
});
client.on(Events.InteractionCreate, async (interaction) => {
@@ -71,9 +75,17 @@ client.on(Events.InteractionCreate, async (interaction) => {
await interaction.editReply("Please wait 30 seconds before requesting another private account link.");
return;
}
console.error("Failed to create Discord account link", error);
logger.error(
{ err: error, event: "discord.magic_link_failed", command: interaction.commandName },
"Failed to create Discord account link",
);
await interaction.editReply("I could not create an account link. Please try again shortly.");
}
});
await client.login(token);
try {
await client.login(token);
} catch (error) {
logger.fatal({ err: error, event: "discord.login_failed" }, "Discord bot login failed");
process.exitCode = 1;
}
+3
View File
@@ -0,0 +1,3 @@
import { createLogger } from "@minecraft-account-manager/logging";
export const logger = createLogger("minecraft-account-manager-discord-bot");
+1
View File
@@ -34,6 +34,7 @@ const nextConfig: NextConfig = {
transpilePackages: [
"@minecraft-account-manager/contracts",
"@minecraft-account-manager/database",
"@minecraft-account-manager/logging",
],
};
+1
View File
@@ -14,6 +14,7 @@
"@minecraft-account-manager/auth": "*",
"@minecraft-account-manager/contracts": "*",
"@minecraft-account-manager/database": "*",
"@minecraft-account-manager/logging": "*",
"@minecraft-account-manager/minecraft": "*",
"@minecraft-account-manager/network": "*",
"drizzle-orm": "^0.45.1",
@@ -14,6 +14,7 @@ import { NextResponse } from "next/server";
import { db } from "@/lib/database";
import { isUniqueConstraintViolation } from "@/lib/database-errors";
import { getIpIntelligence, toAuditIpData } from "@/lib/ip-intelligence";
import { logger } from "@/lib/logger";
import { problemInstance, problemResponse } from "@/lib/problem-response";
const MAX_CLOCK_SKEW_MS = 45_000;
@@ -284,7 +285,10 @@ export async function POST(request: Request) {
));
}
console.error("Velocity access request failed");
logger.error(
{ err: error, event: "velocity.access_failed", instance },
"Velocity access request failed",
);
return problemResponse(problemDetails(
"urn:error:service-unavailable",
"Service unavailable",
+30 -4
View File
@@ -12,6 +12,7 @@ import { ipIntelligence } from "@minecraft-account-manager/database";
import { and, eq, gt } from "drizzle-orm";
import { headers } from "next/headers";
import { db } from "@/lib/database";
import { logger } from "@/lib/logger";
const classifications = new Set<IpClassification>([
"unknown",
@@ -77,6 +78,13 @@ export async function getIpIntelligence(
options: { now?: Date; forceRefresh?: boolean } = {},
): Promise<IpIntelligenceResult> {
if (!isPublicIp(ipAddress)) {
logger.warn(
{
event: "ip_intelligence.skipped",
reason: "non_public_address",
},
"IP intelligence lookup skipped for a non-public client address",
);
return { classification: "unknown", provider: null };
}
@@ -96,14 +104,23 @@ export async function getIpIntelligence(
const result = await provider.classify(ipAddress);
await cacheResult(ipAddress, result, now, cacheHours() * 60 * 60_000);
return result;
} catch {
} catch (error) {
const providerName = process.env.IP_INTELLIGENCE_PROVIDER?.trim().toLowerCase() || null;
const result: IpIntelligenceResult = {
classification: "unknown",
provider: process.env.IP_INTELLIGENCE_PROVIDER?.trim().toLowerCase() || null,
provider: providerName,
lookupError: true,
};
await cacheResult(ipAddress, result, now, 5 * 60_000).catch(() => undefined);
console.error("IP intelligence lookup failed");
await cacheResult(ipAddress, result, now, 5 * 60_000).catch((cacheError) => {
logger.error(
{ err: cacheError, event: "ip_intelligence.cache_failed", provider: providerName },
"Failed to cache an IP intelligence lookup error",
);
});
logger.error(
{ err: error, event: "ip_intelligence.lookup_failed", provider: providerName },
"IP intelligence lookup failed",
);
return result;
}
}
@@ -134,6 +151,15 @@ export async function checkAccountAdditionNetwork() {
const requestHeaders = await headers();
const ipAddress = getClientIp(requestHeaders, process.env.TRUST_PROXY === "true");
if (!ipAddress) {
logger.warn(
{
event: "client_ip.unavailable",
trustProxy: process.env.TRUST_PROXY === "true",
forwardedForPresent: requestHeaders.has("x-forwarded-for"),
realIpPresent: requestHeaders.has("x-real-ip"),
},
"Client IP address was unavailable for account addition",
);
return {
allowed: false as const,
reason: "unavailable" as const,
+3
View File
@@ -0,0 +1,3 @@
import { createLogger } from "@minecraft-account-manager/logging";
export const logger = createLogger("minecraft-account-manager-web");