feat(logging): add structured server diagnostics
This commit is contained in:
@@ -24,3 +24,6 @@ IP_INTELLIGENCE_PROVIDER=proxycheck
|
|||||||
PROXYCHECK_API_KEY=
|
PROXYCHECK_API_KEY=
|
||||||
IP_INTELLIGENCE_CACHE_HOURS=48
|
IP_INTELLIGENCE_CACHE_HOURS=48
|
||||||
BLOCK_HOSTING_IPS=false
|
BLOCK_HOSTING_IPS=false
|
||||||
|
|
||||||
|
# Structured Pino logging
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|||||||
+4
-1
@@ -10,6 +10,7 @@ COPY apps/discord-bot/package.json ./apps/discord-bot/package.json
|
|||||||
COPY packages/auth/package.json ./packages/auth/package.json
|
COPY packages/auth/package.json ./packages/auth/package.json
|
||||||
COPY packages/contracts/package.json ./packages/contracts/package.json
|
COPY packages/contracts/package.json ./packages/contracts/package.json
|
||||||
COPY packages/database/package.json ./packages/database/package.json
|
COPY packages/database/package.json ./packages/database/package.json
|
||||||
|
COPY packages/logging/package.json ./packages/logging/package.json
|
||||||
COPY packages/minecraft/package.json ./packages/minecraft/package.json
|
COPY packages/minecraft/package.json ./packages/minecraft/package.json
|
||||||
COPY packages/network/package.json ./packages/network/package.json
|
COPY packages/network/package.json ./packages/network/package.json
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
@@ -25,6 +26,7 @@ LABEL org.opencontainers.image.title="Minecraft Account Manager" \
|
|||||||
org.opencontainers.image.source="https://git.garvis.dev/dmg/minecraft-account-manager"
|
org.opencontainers.image.source="https://git.garvis.dev/dmg/minecraft-account-manager"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
|
APP_VERSION=${VERSION} \
|
||||||
HOSTNAME=0.0.0.0 \
|
HOSTNAME=0.0.0.0 \
|
||||||
PORT=3000
|
PORT=3000
|
||||||
RUN addgroup --system app && adduser --system --ingroup app app
|
RUN addgroup --system app && adduser --system --ingroup app app
|
||||||
@@ -40,7 +42,8 @@ LABEL org.opencontainers.image.title="Minecraft Account Manager Discord Bot" \
|
|||||||
org.opencontainers.image.version="${VERSION}" \
|
org.opencontainers.image.version="${VERSION}" \
|
||||||
org.opencontainers.image.source="https://git.garvis.dev/dmg/minecraft-account-manager"
|
org.opencontainers.image.source="https://git.garvis.dev/dmg/minecraft-account-manager"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production \
|
||||||
|
APP_VERSION=${VERSION}
|
||||||
RUN addgroup --system app && adduser --system --ingroup app app
|
RUN addgroup --system app && adduser --system --ingroup app app
|
||||||
COPY --chown=app:app package.json package-lock.json tsconfig.base.json ./
|
COPY --chown=app:app package.json package-lock.json tsconfig.base.json ./
|
||||||
COPY --chown=app:app apps/discord-bot ./apps/discord-bot
|
COPY --chown=app:app apps/discord-bot ./apps/discord-bot
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ APP_URL=http://localhost:3000
|
|||||||
DISCORD_BOT_TOKEN=
|
DISCORD_BOT_TOKEN=
|
||||||
DISCORD_APPLICATION_ID=
|
DISCORD_APPLICATION_ID=
|
||||||
DISCORD_GUILD_ID=
|
DISCORD_GUILD_ID=
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@minecraft-account-manager/auth": "*",
|
"@minecraft-account-manager/auth": "*",
|
||||||
"@minecraft-account-manager/contracts": "*",
|
"@minecraft-account-manager/contracts": "*",
|
||||||
"@minecraft-account-manager/database": "*",
|
"@minecraft-account-manager/database": "*",
|
||||||
|
"@minecraft-account-manager/logging": "*",
|
||||||
"discord.js": "^14.25.1",
|
"discord.js": "^14.25.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"drizzle-orm": "^0.45.1"
|
"drizzle-orm": "^0.45.1"
|
||||||
|
|||||||
@@ -2,10 +2,22 @@ import "dotenv/config";
|
|||||||
import { REST, Routes } from "discord.js";
|
import { REST, Routes } from "discord.js";
|
||||||
import { commands } from "./commands";
|
import { commands } from "./commands";
|
||||||
import { requiredEnvironment } from "./config";
|
import { requiredEnvironment } from "./config";
|
||||||
|
import { logger } from "./logger";
|
||||||
|
|
||||||
const token = requiredEnvironment("DISCORD_BOT_TOKEN");
|
const token = requiredEnvironment("DISCORD_BOT_TOKEN");
|
||||||
const applicationId = requiredEnvironment("DISCORD_APPLICATION_ID");
|
const applicationId = requiredEnvironment("DISCORD_APPLICATION_ID");
|
||||||
const rest = new REST({ version: "10" }).setToken(token);
|
const rest = new REST({ version: "10" }).setToken(token);
|
||||||
|
|
||||||
|
try {
|
||||||
await rest.put(Routes.applicationCommands(applicationId), { body: commands });
|
await rest.put(Routes.applicationCommands(applicationId), { body: commands });
|
||||||
console.log(`Deployed ${commands.length} global Discord 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
GatewayIntentBits,
|
GatewayIntentBits,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { commandNames, requiredEnvironment } from "./config";
|
import { commandNames, requiredEnvironment } from "./config";
|
||||||
|
import { logger } from "./logger";
|
||||||
|
|
||||||
const token = requiredEnvironment("DISCORD_BOT_TOKEN");
|
const token = requiredEnvironment("DISCORD_BOT_TOKEN");
|
||||||
const appUrl = requiredEnvironment("APP_URL");
|
const appUrl = requiredEnvironment("APP_URL");
|
||||||
@@ -24,7 +25,10 @@ const authRepository = createAuthRepository(db);
|
|||||||
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
|
||||||
|
|
||||||
client.once(Events.ClientReady, (readyClient) => {
|
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) => {
|
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.");
|
await interaction.editReply("Please wait 30 seconds before requesting another private account link.");
|
||||||
return;
|
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 interaction.editReply("I could not create an account link. Please try again shortly.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
await client.login(token);
|
await client.login(token);
|
||||||
|
} catch (error) {
|
||||||
|
logger.fatal({ err: error, event: "discord.login_failed" }, "Discord bot login failed");
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { createLogger } from "@minecraft-account-manager/logging";
|
||||||
|
|
||||||
|
export const logger = createLogger("minecraft-account-manager-discord-bot");
|
||||||
@@ -34,6 +34,7 @@ const nextConfig: NextConfig = {
|
|||||||
transpilePackages: [
|
transpilePackages: [
|
||||||
"@minecraft-account-manager/contracts",
|
"@minecraft-account-manager/contracts",
|
||||||
"@minecraft-account-manager/database",
|
"@minecraft-account-manager/database",
|
||||||
|
"@minecraft-account-manager/logging",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@minecraft-account-manager/auth": "*",
|
"@minecraft-account-manager/auth": "*",
|
||||||
"@minecraft-account-manager/contracts": "*",
|
"@minecraft-account-manager/contracts": "*",
|
||||||
"@minecraft-account-manager/database": "*",
|
"@minecraft-account-manager/database": "*",
|
||||||
|
"@minecraft-account-manager/logging": "*",
|
||||||
"@minecraft-account-manager/minecraft": "*",
|
"@minecraft-account-manager/minecraft": "*",
|
||||||
"@minecraft-account-manager/network": "*",
|
"@minecraft-account-manager/network": "*",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { NextResponse } from "next/server";
|
|||||||
import { db } from "@/lib/database";
|
import { db } from "@/lib/database";
|
||||||
import { isUniqueConstraintViolation } from "@/lib/database-errors";
|
import { isUniqueConstraintViolation } from "@/lib/database-errors";
|
||||||
import { getIpIntelligence, toAuditIpData } from "@/lib/ip-intelligence";
|
import { getIpIntelligence, toAuditIpData } from "@/lib/ip-intelligence";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import { problemInstance, problemResponse } from "@/lib/problem-response";
|
import { problemInstance, problemResponse } from "@/lib/problem-response";
|
||||||
|
|
||||||
const MAX_CLOCK_SKEW_MS = 45_000;
|
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(
|
return problemResponse(problemDetails(
|
||||||
"urn:error:service-unavailable",
|
"urn:error:service-unavailable",
|
||||||
"Service unavailable",
|
"Service unavailable",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ipIntelligence } from "@minecraft-account-manager/database";
|
|||||||
import { and, eq, gt } from "drizzle-orm";
|
import { and, eq, gt } from "drizzle-orm";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { db } from "@/lib/database";
|
import { db } from "@/lib/database";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
|
||||||
const classifications = new Set<IpClassification>([
|
const classifications = new Set<IpClassification>([
|
||||||
"unknown",
|
"unknown",
|
||||||
@@ -77,6 +78,13 @@ export async function getIpIntelligence(
|
|||||||
options: { now?: Date; forceRefresh?: boolean } = {},
|
options: { now?: Date; forceRefresh?: boolean } = {},
|
||||||
): Promise<IpIntelligenceResult> {
|
): Promise<IpIntelligenceResult> {
|
||||||
if (!isPublicIp(ipAddress)) {
|
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 };
|
return { classification: "unknown", provider: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,14 +104,23 @@ export async function getIpIntelligence(
|
|||||||
const result = await provider.classify(ipAddress);
|
const result = await provider.classify(ipAddress);
|
||||||
await cacheResult(ipAddress, result, now, cacheHours() * 60 * 60_000);
|
await cacheResult(ipAddress, result, now, cacheHours() * 60 * 60_000);
|
||||||
return result;
|
return result;
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
const providerName = process.env.IP_INTELLIGENCE_PROVIDER?.trim().toLowerCase() || null;
|
||||||
const result: IpIntelligenceResult = {
|
const result: IpIntelligenceResult = {
|
||||||
classification: "unknown",
|
classification: "unknown",
|
||||||
provider: process.env.IP_INTELLIGENCE_PROVIDER?.trim().toLowerCase() || null,
|
provider: providerName,
|
||||||
lookupError: true,
|
lookupError: true,
|
||||||
};
|
};
|
||||||
await cacheResult(ipAddress, result, now, 5 * 60_000).catch(() => undefined);
|
await cacheResult(ipAddress, result, now, 5 * 60_000).catch((cacheError) => {
|
||||||
console.error("IP intelligence lookup failed");
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,6 +151,15 @@ export async function checkAccountAdditionNetwork() {
|
|||||||
const requestHeaders = await headers();
|
const requestHeaders = await headers();
|
||||||
const ipAddress = getClientIp(requestHeaders, process.env.TRUST_PROXY === "true");
|
const ipAddress = getClientIp(requestHeaders, process.env.TRUST_PROXY === "true");
|
||||||
if (!ipAddress) {
|
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 {
|
return {
|
||||||
allowed: false as const,
|
allowed: false as const,
|
||||||
reason: "unavailable" as const,
|
reason: "unavailable" as const,
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { createLogger } from "@minecraft-account-manager/logging";
|
||||||
|
|
||||||
|
export const logger = createLogger("minecraft-account-manager-web");
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## 2026-08-01
|
## 2026-08-01
|
||||||
|
|
||||||
|
* **Extend**: Add shared Pino logging with credential redaction and actionable web and Discord runtime diagnostics.
|
||||||
* **Fix**: Build magic-link redirects from the configured public portal URL instead of the reverse proxy's internal request origin.
|
* **Fix**: Build magic-link redirects from the configured public portal URL instead of the reverse proxy's internal request origin.
|
||||||
* **Verify**: Confirmed `v1.1.1` left all pre-existing `latest` digests unchanged while publishing versioned artifacts.
|
* **Verify**: Confirmed `v1.1.1` left all pre-existing `latest` digests unchanged while publishing versioned artifacts.
|
||||||
* **Refine**: Removed mutable `latest` publication so all deployable artifacts use explicit semantic versions.
|
* **Refine**: Removed mutable `latest` publication so all deployable artifacts use explicit semantic versions.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ type: User Story
|
|||||||
title: Deploy and operate the platform securely
|
title: Deploy and operate the platform securely
|
||||||
description: Operators have repeatable builds, migrations, credential provisioning, configuration, and security checks.
|
description: Operators have repeatable builds, migrations, credential provisioning, configuration, and security checks.
|
||||||
tags: [operations, security, database, deployment]
|
tags: [operations, security, database, deployment]
|
||||||
timestamp: 2026-08-01T19:46:09Z
|
timestamp: 2026-08-01T21:37:26Z
|
||||||
story_id: US-015
|
story_id: US-015
|
||||||
status: verified
|
status: verified
|
||||||
---
|
---
|
||||||
@@ -22,6 +22,7 @@ As a platform operator, I want reproducible deployment and security controls, so
|
|||||||
- [x] Environment examples document database, Keycloak, Discord, trusted proxy, and ProxyCheck settings without secrets.
|
- [x] Environment examples document database, Keycloak, Discord, trusted proxy, and ProxyCheck settings without secrets.
|
||||||
- [x] The web application sets CSP, framing, MIME, referrer, and permissions headers.
|
- [x] The web application sets CSP, framing, MIME, referrer, and permissions headers.
|
||||||
- [x] The web runtime provides a dependency-free health endpoint for orchestration probes.
|
- [x] The web runtime provides a dependency-free health endpoint for orchestration probes.
|
||||||
|
- [x] Web and Discord bot runtimes emit structured Pino logs with credential-field redaction and safe operational context.
|
||||||
- [x] npm dependency audit and Semgrep security review complete without findings at the last verified change.
|
- [x] npm dependency audit and Semgrep security review complete without findings at the last verified change.
|
||||||
- [x] Architecture, Keycloak, API error, security, bot, and Velocity operating documentation is available.
|
- [x] Architecture, Keycloak, API error, security, bot, and Velocity operating documentation is available.
|
||||||
|
|
||||||
@@ -33,10 +34,11 @@ As a platform operator, I want reproducible deployment and security controls, so
|
|||||||
- [`packages/database/scripts/create-plugin-credential.ts`](../packages/database/scripts/create-plugin-credential.ts)
|
- [`packages/database/scripts/create-plugin-credential.ts`](../packages/database/scripts/create-plugin-credential.ts)
|
||||||
- [`plugins/velocity/build.gradle.kts`](../plugins/velocity/build.gradle.kts)
|
- [`plugins/velocity/build.gradle.kts`](../plugins/velocity/build.gradle.kts)
|
||||||
- [`apps/web/next.config.ts`](../apps/web/next.config.ts)
|
- [`apps/web/next.config.ts`](../apps/web/next.config.ts)
|
||||||
|
- [`packages/logging/src/index.ts`](../packages/logging/src/index.ts)
|
||||||
|
|
||||||
# Validation
|
# Validation
|
||||||
|
|
||||||
Use `npm test`, `npm run typecheck`, `npm run lint`, `npm run build`, `npm run velocity:build`, `npm audit`, and `npm run design:validate`.
|
Use `npm test`, `npm run typecheck`, `npm run lint`, `npm run build`, `npm run velocity:build`, `npm audit`, and `npm run design:validate`. Structured logging redaction is covered by [`packages/logging/test/logger.test.ts`](../packages/logging/test/logger.test.ts).
|
||||||
|
|
||||||
# Related Stories
|
# Related Stories
|
||||||
|
|
||||||
|
|||||||
Generated
+155
@@ -22,6 +22,7 @@
|
|||||||
"@minecraft-account-manager/auth": "*",
|
"@minecraft-account-manager/auth": "*",
|
||||||
"@minecraft-account-manager/contracts": "*",
|
"@minecraft-account-manager/contracts": "*",
|
||||||
"@minecraft-account-manager/database": "*",
|
"@minecraft-account-manager/database": "*",
|
||||||
|
"@minecraft-account-manager/logging": "*",
|
||||||
"discord.js": "^14.25.1",
|
"discord.js": "^14.25.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"drizzle-orm": "^0.45.1"
|
"drizzle-orm": "^0.45.1"
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
"@minecraft-account-manager/auth": "*",
|
"@minecraft-account-manager/auth": "*",
|
||||||
"@minecraft-account-manager/contracts": "*",
|
"@minecraft-account-manager/contracts": "*",
|
||||||
"@minecraft-account-manager/database": "*",
|
"@minecraft-account-manager/database": "*",
|
||||||
|
"@minecraft-account-manager/logging": "*",
|
||||||
"@minecraft-account-manager/minecraft": "*",
|
"@minecraft-account-manager/minecraft": "*",
|
||||||
"@minecraft-account-manager/network": "*",
|
"@minecraft-account-manager/network": "*",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
@@ -1781,6 +1783,10 @@
|
|||||||
"resolved": "apps/discord-bot",
|
"resolved": "apps/discord-bot",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@minecraft-account-manager/logging": {
|
||||||
|
"resolved": "packages/logging",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@minecraft-account-manager/minecraft": {
|
"node_modules/@minecraft-account-manager/minecraft": {
|
||||||
"resolved": "packages/minecraft",
|
"resolved": "packages/minecraft",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -2038,6 +2044,12 @@
|
|||||||
"url": "https://github.com/sponsors/panva"
|
"url": "https://github.com/sponsors/panva"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@pinojs/redact": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz",
|
||||||
@@ -3810,6 +3822,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/atomic-sleep": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/available-typed-arrays": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
@@ -7052,6 +7073,15 @@
|
|||||||
"node": "^10.13.0 || >=12.0.0"
|
"node": "^10.13.0 || >=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/on-exit-leak-free": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/openid-client": {
|
"node_modules/openid-client": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
|
||||||
@@ -7220,6 +7250,43 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pino": {
|
||||||
|
"version": "10.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz",
|
||||||
|
"integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@pinojs/redact": "^0.4.0",
|
||||||
|
"atomic-sleep": "^1.0.0",
|
||||||
|
"on-exit-leak-free": "^2.1.0",
|
||||||
|
"pino-abstract-transport": "^3.0.0",
|
||||||
|
"pino-std-serializers": "^7.0.0",
|
||||||
|
"process-warning": "^5.0.0",
|
||||||
|
"quick-format-unescaped": "^4.0.3",
|
||||||
|
"real-require": "^0.2.0",
|
||||||
|
"safe-stable-stringify": "^2.3.1",
|
||||||
|
"sonic-boom": "^4.0.1",
|
||||||
|
"thread-stream": "^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pino": "bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pino-abstract-transport": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"split2": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pino-std-serializers": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
@@ -7317,6 +7384,22 @@
|
|||||||
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/process-warning": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -7360,6 +7443,12 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/quick-format-unescaped": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.8",
|
"version": "19.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
|
||||||
@@ -7388,6 +7477,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/real-require": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
@@ -7600,6 +7698,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-stable-stringify": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.27.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||||
@@ -7834,6 +7941,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/sonic-boom": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"atomic-sleep": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
@@ -7864,6 +7980,15 @@
|
|||||||
"source-map": "^0.6.0"
|
"source-map": "^0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/split2": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stable-hash": {
|
"node_modules/stable-hash": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||||
@@ -8106,6 +8231,24 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/thread-stream": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"real-require": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/thread-stream/node_modules/real-require": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinybench": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
@@ -9215,6 +9358,18 @@
|
|||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/logging": {
|
||||||
|
"name": "@minecraft-account-manager/logging",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"pino": "^10.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.0.3",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vitest": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/minecraft": {
|
"packages/minecraft": {
|
||||||
"name": "@minecraft-account-manager/minecraft",
|
"name": "@minecraft-account-manager/minecraft",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "@minecraft-account-manager/logging",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": { ".": "./src/index.ts" },
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pino": "^10.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.0.3",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vitest": "^4.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import pino, { type DestinationStream, type Logger } from "pino";
|
||||||
|
|
||||||
|
const redactedPaths = [
|
||||||
|
"apiKey",
|
||||||
|
"authorization",
|
||||||
|
"password",
|
||||||
|
"token",
|
||||||
|
"*.apiKey",
|
||||||
|
"*.authorization",
|
||||||
|
"*.password",
|
||||||
|
"*.token",
|
||||||
|
"headers.authorization",
|
||||||
|
"req.headers.authorization",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function createLogger(
|
||||||
|
service: string,
|
||||||
|
options: { destination?: DestinationStream } = {},
|
||||||
|
): Logger {
|
||||||
|
return pino(
|
||||||
|
{
|
||||||
|
level: process.env.LOG_LEVEL?.trim() || "info",
|
||||||
|
base: {
|
||||||
|
service,
|
||||||
|
environment: process.env.NODE_ENV ?? "development",
|
||||||
|
version: process.env.APP_VERSION ?? "development",
|
||||||
|
},
|
||||||
|
redact: {
|
||||||
|
paths: redactedPaths,
|
||||||
|
censor: "[Redacted]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options.destination,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Writable } from "node:stream";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { createLogger } from "../src/index";
|
||||||
|
|
||||||
|
function captureLog() {
|
||||||
|
let output = "";
|
||||||
|
const destination = new Writable({
|
||||||
|
write(chunk, _encoding, callback) {
|
||||||
|
output += chunk.toString();
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { destination, read: () => JSON.parse(output.trim()) as Record<string, unknown> };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("structured application logging", () => {
|
||||||
|
it("emits service metadata and redacts credential fields", () => {
|
||||||
|
const capture = captureLog();
|
||||||
|
const logger = createLogger("account-manager-test", { destination: capture.destination });
|
||||||
|
|
||||||
|
logger.info({ token: "secret-token", apiKey: "secret-key", operation: "test" }, "Test event");
|
||||||
|
|
||||||
|
expect(capture.read()).toMatchObject({
|
||||||
|
service: "account-manager-test",
|
||||||
|
token: "[Redacted]",
|
||||||
|
apiKey: "[Redacted]",
|
||||||
|
operation: "test",
|
||||||
|
msg: "Test event",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": { "types": ["node", "vitest/globals"] },
|
||||||
|
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user