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;
}
function buildApp({ config, initialState = null, onMutation = null }) {
function buildApp({
config,
initialState = null,
onMutation = null,
logger = console,
}) {
const engine = new XArtAudioEngine({
creditConfig: config.credit,
initialState: initialState && initialState.engine ? initialState.engine : null,
@@ -79,8 +84,8 @@ function buildApp({ config, initialState = null, onMutation = null }) {
updatedAt: new Date().toISOString(),
engine: engine.exportState(),
});
} catch {
// avoid breaking request flow on persistence callback issues
} catch (error) {
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}`,
});
} catch (error) {
logger.warn({ err: error }, "x webhook request failed");
return json(400, { error: error.message });
}
}
@@ -192,6 +198,7 @@ function buildApp({ config, initialState = null, onMutation = null }) {
persistMutation();
return json(200, { status: "credited" });
} catch (error) {
logger.warn({ err: error }, "polar webhook request failed");
return json(400, { error: error.message });
}
}

View File

@@ -22,6 +22,7 @@ function strFromEnv(name, fallback) {
const parsed = {
port: intFromEnv("PORT", 3000),
stateFilePath: strFromEnv("STATE_FILE_PATH", "./data/state.json"),
logLevel: strFromEnv("LOG_LEVEL", "info"),
xWebhookSecret: process.env.X_WEBHOOK_SECRET || "dev-x-secret",
polarWebhookSecret: process.env.POLAR_WEBHOOK_SECRET || "dev-polar-secret",
rateLimits: {
@@ -41,6 +42,7 @@ const parsed = {
const ConfigSchema = z.object({
port: z.number().int().positive(),
stateFilePath: z.string().min(1),
logLevel: z.enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"]),
xWebhookSecret: z.string().min(1),
polarWebhookSecret: z.string().min(1),
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 { config } = require("./config");
const { JsonFileStateStore } = require("./lib/state-store");
const { createLogger } = require("./lib/logger");
function readBody(req) {
return new Promise((resolve, reject) => {
@@ -68,7 +69,7 @@ function createMutationPersister({ stateStore, logger = console }) {
queue = queue
.then(() => stateStore.save(state))
.catch((error) => {
logger.error("failed to persist state", error);
logger.error({ err: error }, "failed to persist state");
});
return queue;
@@ -87,6 +88,7 @@ async function createRuntime({ runtimeConfig = config, logger = console } = {})
const app = buildApp({
config: runtimeConfig,
initialState,
logger,
onMutation(state) {
void persister.enqueue(state);
},
@@ -102,7 +104,11 @@ async function createRuntime({ runtimeConfig = config, logger = console } = {})
}
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;
let shuttingDown = false;
@@ -112,7 +118,7 @@ async function start() {
}
shuttingDown = true;
console.log(`received ${signal}, shutting down`);
logger.info({ signal }, "received shutdown signal");
server.close();
await persister.flush();
@@ -126,13 +132,17 @@ async function start() {
});
server.listen(config.port, () => {
console.log(`xartaudio server listening on :${config.port}`);
logger.info({ port: config.port }, "xartaudio server listening");
});
}
if (require.main === module) {
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;
});
}

View File

@@ -7,11 +7,13 @@ test("config uses defaults when env is missing", () => {
const previous = {
PORT: process.env.PORT,
STATE_FILE_PATH: process.env.STATE_FILE_PATH,
LOG_LEVEL: process.env.LOG_LEVEL,
WEBHOOK_RPM: process.env.WEBHOOK_RPM,
};
delete process.env.PORT;
delete process.env.STATE_FILE_PATH;
delete process.env.LOG_LEVEL;
delete process.env.WEBHOOK_RPM;
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.stateFilePath, "./data/state.json");
assert.equal(config.logLevel, "info");
assert.equal(config.rateLimits.webhookPerMinute, 120);
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;
}
if (previous.LOG_LEVEL === undefined) {
delete process.env.LOG_LEVEL;
} else {
process.env.LOG_LEVEL = previous.LOG_LEVEL;
}
if (previous.WEBHOOK_RPM === undefined) {
delete process.env.WEBHOOK_RPM;
} else {
@@ -44,11 +53,13 @@ test("config reads state path and numeric env overrides", () => {
const previous = {
PORT: process.env.PORT,
STATE_FILE_PATH: process.env.STATE_FILE_PATH,
LOG_LEVEL: process.env.LOG_LEVEL,
WEBHOOK_RPM: process.env.WEBHOOK_RPM,
};
process.env.PORT = "8080";
process.env.STATE_FILE_PATH = "/data/prod-state.json";
process.env.LOG_LEVEL = "debug";
process.env.WEBHOOK_RPM = "77";
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.stateFilePath, "/data/prod-state.json");
assert.equal(config.logLevel, "debug");
assert.equal(config.rateLimits.webhookPerMinute, 77);
if (previous.PORT === undefined) {
@@ -69,6 +81,11 @@ test("config reads state path and numeric env overrides", () => {
} else {
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) {
delete process.env.WEBHOOK_RPM;
} 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");
});