feat: add structured pino logging and validated log-level config

This commit is contained in:
Codex
2026-02-18 13:16:35 +00:00
parent 6b1f9cddbc
commit e3f9a3574e
6 changed files with 74 additions and 8 deletions

View File

@@ -49,7 +49,12 @@ function sanitizeReturnTo(value, fallback = "/app") {
return value; return value;
} }
function buildApp({ config, initialState = null, onMutation = null }) { function buildApp({
config,
initialState = null,
onMutation = null,
logger = console,
}) {
const engine = new XArtAudioEngine({ const engine = new XArtAudioEngine({
creditConfig: config.credit, creditConfig: config.credit,
initialState: initialState && initialState.engine ? initialState.engine : null, initialState: initialState && initialState.engine ? initialState.engine : null,
@@ -79,8 +84,8 @@ function buildApp({ config, initialState = null, onMutation = null }) {
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
engine: engine.exportState(), engine: engine.exportState(),
}); });
} catch { } catch (error) {
// avoid breaking request flow on persistence callback issues logger.error({ err: error }, "failed to persist mutation");
} }
} }
@@ -165,6 +170,7 @@ function buildApp({ config, initialState = null, onMutation = null }) {
publicLink: result.reply ? result.reply.publicLink : `/audio/${result.job.assetId}`, publicLink: result.reply ? result.reply.publicLink : `/audio/${result.job.assetId}`,
}); });
} catch (error) { } catch (error) {
logger.warn({ err: error }, "x webhook request failed");
return json(400, { error: error.message }); return json(400, { error: error.message });
} }
} }
@@ -192,6 +198,7 @@ function buildApp({ config, initialState = null, onMutation = null }) {
persistMutation(); persistMutation();
return json(200, { status: "credited" }); return json(200, { status: "credited" });
} catch (error) { } catch (error) {
logger.warn({ err: error }, "polar webhook request failed");
return json(400, { error: error.message }); return json(400, { error: error.message });
} }
} }

View File

@@ -22,6 +22,7 @@ function strFromEnv(name, fallback) {
const parsed = { const parsed = {
port: intFromEnv("PORT", 3000), port: intFromEnv("PORT", 3000),
stateFilePath: strFromEnv("STATE_FILE_PATH", "./data/state.json"), stateFilePath: strFromEnv("STATE_FILE_PATH", "./data/state.json"),
logLevel: strFromEnv("LOG_LEVEL", "info"),
xWebhookSecret: process.env.X_WEBHOOK_SECRET || "dev-x-secret", xWebhookSecret: process.env.X_WEBHOOK_SECRET || "dev-x-secret",
polarWebhookSecret: process.env.POLAR_WEBHOOK_SECRET || "dev-polar-secret", polarWebhookSecret: process.env.POLAR_WEBHOOK_SECRET || "dev-polar-secret",
rateLimits: { rateLimits: {
@@ -41,6 +42,7 @@ const parsed = {
const ConfigSchema = z.object({ const ConfigSchema = z.object({
port: z.number().int().positive(), port: z.number().int().positive(),
stateFilePath: z.string().min(1), stateFilePath: z.string().min(1),
logLevel: z.enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"]),
xWebhookSecret: z.string().min(1), xWebhookSecret: z.string().min(1),
polarWebhookSecret: z.string().min(1), polarWebhookSecret: z.string().min(1),
rateLimits: z.object({ rateLimits: z.object({

18
src/lib/logger.js Normal file
View File

@@ -0,0 +1,18 @@
"use strict";
const pino = require("pino");
function createLogger({ level = "info", name = "xartaudio" } = {}) {
return pino({
name,
level,
base: {
service: name,
},
timestamp: pino.stdTimeFunctions.isoTime,
});
}
module.exports = {
createLogger,
};

View File

@@ -4,6 +4,7 @@ const http = require("node:http");
const { buildApp } = require("./app"); const { buildApp } = require("./app");
const { config } = require("./config"); const { config } = require("./config");
const { JsonFileStateStore } = require("./lib/state-store"); const { JsonFileStateStore } = require("./lib/state-store");
const { createLogger } = require("./lib/logger");
function readBody(req) { function readBody(req) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -68,7 +69,7 @@ function createMutationPersister({ stateStore, logger = console }) {
queue = queue queue = queue
.then(() => stateStore.save(state)) .then(() => stateStore.save(state))
.catch((error) => { .catch((error) => {
logger.error("failed to persist state", error); logger.error({ err: error }, "failed to persist state");
}); });
return queue; return queue;
@@ -87,6 +88,7 @@ async function createRuntime({ runtimeConfig = config, logger = console } = {})
const app = buildApp({ const app = buildApp({
config: runtimeConfig, config: runtimeConfig,
initialState, initialState,
logger,
onMutation(state) { onMutation(state) {
void persister.enqueue(state); void persister.enqueue(state);
}, },
@@ -102,7 +104,11 @@ async function createRuntime({ runtimeConfig = config, logger = console } = {})
} }
async function start() { async function start() {
const runtime = await createRuntime({ runtimeConfig: config }); const logger = createLogger({
level: config.logLevel,
name: "xartaudio",
});
const runtime = await createRuntime({ runtimeConfig: config, logger });
const { server, persister } = runtime; const { server, persister } = runtime;
let shuttingDown = false; let shuttingDown = false;
@@ -112,7 +118,7 @@ async function start() {
} }
shuttingDown = true; shuttingDown = true;
console.log(`received ${signal}, shutting down`); logger.info({ signal }, "received shutdown signal");
server.close(); server.close();
await persister.flush(); await persister.flush();
@@ -126,13 +132,17 @@ async function start() {
}); });
server.listen(config.port, () => { server.listen(config.port, () => {
console.log(`xartaudio server listening on :${config.port}`); logger.info({ port: config.port }, "xartaudio server listening");
}); });
} }
if (require.main === module) { if (require.main === module) {
start().catch((error) => { start().catch((error) => {
console.error("failed to start server", error); const logger = createLogger({
level: config.logLevel,
name: "xartaudio",
});
logger.error({ err: error }, "failed to start server");
process.exitCode = 1; process.exitCode = 1;
}); });
} }

View File

@@ -7,11 +7,13 @@ test("config uses defaults when env is missing", () => {
const previous = { const previous = {
PORT: process.env.PORT, PORT: process.env.PORT,
STATE_FILE_PATH: process.env.STATE_FILE_PATH, STATE_FILE_PATH: process.env.STATE_FILE_PATH,
LOG_LEVEL: process.env.LOG_LEVEL,
WEBHOOK_RPM: process.env.WEBHOOK_RPM, WEBHOOK_RPM: process.env.WEBHOOK_RPM,
}; };
delete process.env.PORT; delete process.env.PORT;
delete process.env.STATE_FILE_PATH; delete process.env.STATE_FILE_PATH;
delete process.env.LOG_LEVEL;
delete process.env.WEBHOOK_RPM; delete process.env.WEBHOOK_RPM;
delete require.cache[require.resolve("../src/config")]; delete require.cache[require.resolve("../src/config")];
@@ -19,6 +21,7 @@ test("config uses defaults when env is missing", () => {
assert.equal(config.port, 3000); assert.equal(config.port, 3000);
assert.equal(config.stateFilePath, "./data/state.json"); assert.equal(config.stateFilePath, "./data/state.json");
assert.equal(config.logLevel, "info");
assert.equal(config.rateLimits.webhookPerMinute, 120); assert.equal(config.rateLimits.webhookPerMinute, 120);
if (previous.PORT === undefined) { if (previous.PORT === undefined) {
@@ -33,6 +36,12 @@ test("config uses defaults when env is missing", () => {
process.env.STATE_FILE_PATH = previous.STATE_FILE_PATH; process.env.STATE_FILE_PATH = previous.STATE_FILE_PATH;
} }
if (previous.LOG_LEVEL === undefined) {
delete process.env.LOG_LEVEL;
} else {
process.env.LOG_LEVEL = previous.LOG_LEVEL;
}
if (previous.WEBHOOK_RPM === undefined) { if (previous.WEBHOOK_RPM === undefined) {
delete process.env.WEBHOOK_RPM; delete process.env.WEBHOOK_RPM;
} else { } else {
@@ -44,11 +53,13 @@ test("config reads state path and numeric env overrides", () => {
const previous = { const previous = {
PORT: process.env.PORT, PORT: process.env.PORT,
STATE_FILE_PATH: process.env.STATE_FILE_PATH, STATE_FILE_PATH: process.env.STATE_FILE_PATH,
LOG_LEVEL: process.env.LOG_LEVEL,
WEBHOOK_RPM: process.env.WEBHOOK_RPM, WEBHOOK_RPM: process.env.WEBHOOK_RPM,
}; };
process.env.PORT = "8080"; process.env.PORT = "8080";
process.env.STATE_FILE_PATH = "/data/prod-state.json"; process.env.STATE_FILE_PATH = "/data/prod-state.json";
process.env.LOG_LEVEL = "debug";
process.env.WEBHOOK_RPM = "77"; process.env.WEBHOOK_RPM = "77";
delete require.cache[require.resolve("../src/config")]; delete require.cache[require.resolve("../src/config")];
@@ -56,6 +67,7 @@ test("config reads state path and numeric env overrides", () => {
assert.equal(config.port, 8080); assert.equal(config.port, 8080);
assert.equal(config.stateFilePath, "/data/prod-state.json"); assert.equal(config.stateFilePath, "/data/prod-state.json");
assert.equal(config.logLevel, "debug");
assert.equal(config.rateLimits.webhookPerMinute, 77); assert.equal(config.rateLimits.webhookPerMinute, 77);
if (previous.PORT === undefined) { if (previous.PORT === undefined) {
@@ -69,6 +81,11 @@ test("config reads state path and numeric env overrides", () => {
} else { } else {
process.env.STATE_FILE_PATH = previous.STATE_FILE_PATH; process.env.STATE_FILE_PATH = previous.STATE_FILE_PATH;
} }
if (previous.LOG_LEVEL === undefined) {
delete process.env.LOG_LEVEL;
} else {
process.env.LOG_LEVEL = previous.LOG_LEVEL;
}
if (previous.WEBHOOK_RPM === undefined) { if (previous.WEBHOOK_RPM === undefined) {
delete process.env.WEBHOOK_RPM; delete process.env.WEBHOOK_RPM;
} else { } else {

12
test/logger.test.js Normal file
View File

@@ -0,0 +1,12 @@
"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const { createLogger } = require("../src/lib/logger");
test("createLogger returns pino-compatible logger", () => {
const logger = createLogger({ level: "debug", name: "test" });
assert.equal(typeof logger.info, "function");
assert.equal(typeof logger.error, "function");
assert.equal(logger.level, "debug");
});