diff --git a/eslint.config.js b/eslint.config.js index 7b8693da..559d3d48 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -94,9 +94,16 @@ export default [ { argsIgnorePattern: "^_" }, ], "@typescript-eslint/no-explicit-any": "warn", + "no-console": "error", "custom/no-unwrapped-async-handlers": "error", }, }, + { + files: ["src/scripts/**/*.ts", "src/logger.ts", "src/**/*.test.ts"], + rules: { + "no-console": "off", + }, + }, { ignores: ["dist/**", "node_modules/**"], }, diff --git a/src/__tests__/security-headers.test.ts b/src/__tests__/security-headers.test.ts index 4f5a4307..bb60d350 100644 --- a/src/__tests__/security-headers.test.ts +++ b/src/__tests__/security-headers.test.ts @@ -6,6 +6,7 @@ import request from 'supertest'; import { createApp } from '../app.js'; +import { logger } from '../logger.js'; // Mock better-sqlite3 to prevent native binding errors jest.mock('better-sqlite3', () => { @@ -289,20 +290,20 @@ describe('Security Headers and CORS Configuration', () => { process.env.NODE_ENV = 'production'; delete process.env.CORS_ALLOWED_ORIGINS; - // Mock console.warn to capture warning - const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + // Mock logger.warn to capture warning + const loggerSpy = jest.spyOn(logger, 'warn').mockImplementation(); try { const app = createApp(); await request(app).get('/api/health'); // Should have logged a warning - expect(consoleSpy).toHaveBeenCalledWith( + expect(loggerSpy).toHaveBeenCalledWith( expect.stringContaining('WARNING: No CORS_ALLOWED_ORIGINS configured in production') ); } finally { process.env = originalEnv; - consoleSpy.mockRestore(); + loggerSpy.mockRestore(); } }); diff --git a/src/app.ts b/src/app.ts index 47eb6658..34abd6a1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,4 +1,5 @@ import express from 'express'; +import { logger } from './logger.js'; import cors from 'cors'; import helmet from 'helmet'; import adminRouter from './routes/admin.js'; @@ -130,6 +131,7 @@ interface AppDependencies extends SorobanBillingDependencies { * @example Wire into shutdown handler * ```ts * import { quotasDrainTracker } from './app.js'; +import { logger } from './logger.js'; * * const shutdown = createGracefulShutdownHandler({ * server, @@ -273,7 +275,7 @@ export const createApp = (dependencies?: Partial) => { // Validate origins in production if (isProduction && allowedOrigins.length === 0) { - console.warn("WARNING: No CORS_ALLOWED_ORIGINS configured in production"); + logger.warn("WARNING: No CORS_ALLOWED_ORIGINS configured in production"); } // Regex for localhost with optional port (e.g., http://localhost:5173) @@ -302,7 +304,7 @@ export const createApp = (dependencies?: Partial) => { // Log blocked attempts in production if (isProduction) { - console.warn(`CORS blocked origin: ${origin}`); + logger.warn(`CORS blocked origin: ${origin}`); } // Pass false instead of Error to prevent Express from returning 500 diff --git a/src/config/env.ts b/src/config/env.ts index 95523758..d364c6b2 100644 --- a/src/config/env.ts +++ b/src/config/env.ts @@ -1,5 +1,6 @@ import "dotenv/config"; import { z } from "zod"; +import { logger } from '../logger.js'; const stellarNetworkSchema = z.enum(["testnet", "mainnet"]); @@ -587,9 +588,9 @@ export const envSchema = z const parsed = envSchema.safeParse(process.env); if (!parsed.success) { - console.error("❌ Invalid environment configuration:"); + logger.error("❌ Invalid environment configuration:"); parsed.error.issues.forEach((issue) => { - console.error(` - ${issue.path.join(".")}: ${issue.message}`); + logger.error(` - ${issue.path.join(".")}: ${issue.message}`); }); process.exit(1); } diff --git a/src/controllers/depositController.ts b/src/controllers/depositController.ts index ade2f430..e085de61 100644 --- a/src/controllers/depositController.ts +++ b/src/controllers/depositController.ts @@ -17,6 +17,7 @@ import type { VaultRepository } from '../repositories/vaultRepository.js'; import { config } from '../config/index.js'; import { redactSimulationDetails } from '../lib/simulationDiagnostics.js'; import { successEnvelope, errorEnvelope, getRequestId } from '../lib/envelope.js'; +import { logger } from '../middleware/logging.js'; export interface DepositPrepareRequest { amount_usdc: string; @@ -242,7 +243,7 @@ export class DepositController { }); } else if (error instanceof SimulationError) { // Log full diagnostics at warning level, but only expose a redacted summary. - console.warn('Soroban simulation diagnostics:', error.simulationDetails); + logger.warn('Soroban simulation diagnostics:', error.simulationDetails); const redacted = redactSimulationDetails(error.simulationDetails); res.status(502).json({ error: 'Soroban simulation failed. See diagnostics for details.', diff --git a/src/index.ts b/src/index.ts index b5b90c95..3c6b34ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -81,6 +81,7 @@ import { ApiKey } from './types/gateway.js'; import { listingsCache } from './lib/listingsCache.js'; import { createSlowQueryAlerterJob } from './workers/slowQueryAlerter.js'; import { createAnomalyDetectorJob } from './workers/anomalyDetector.js'; +import { logger } from './logger.js'; // Helper for Jest/CommonJS compat const isDirectExecution = @@ -430,7 +431,7 @@ if (isDirectExecution) { sloAlertJob?.start(); const server = app.listen(PORT, () => { - console.log(`Callora backend listening on http://localhost:${PORT}`); + logger.info(`Callora backend listening on http://localhost:${PORT}`); }); // Track active connections so we can wait for them to finish @@ -459,7 +460,7 @@ if (isDirectExecution) { process.once("SIGTERM", () => onSignal("SIGTERM")); process.once("SIGINT", () => onSignal("SIGINT")); } catch (error) { - console.error("Failed to start server:", error); + logger.error("Failed to start server:", error); process.exit(1); } } diff --git a/src/middleware/envelopeValidator.ts b/src/middleware/envelopeValidator.ts index abe94b70..426fff85 100644 --- a/src/middleware/envelopeValidator.ts +++ b/src/middleware/envelopeValidator.ts @@ -1,7 +1,6 @@ import type { Request, Response, NextFunction } from 'express'; -import { - ENVELOPE_REQUIRED_FIELDS, -} from '../types/ResponseEnvelope.js'; +import { ENVELOPE_REQUIRED_FIELDS } from '../types/ResponseEnvelope.js'; +import { logger } from '../logger.js'; /** * Validates that every response sent through res.json() conforms @@ -27,7 +26,7 @@ export function envelopeValidator( // Fail fast in development so violations are caught immediately throw new Error(message); } else { - console.warn(message); + logger.warn(message); } } } diff --git a/src/routes/billing.ts b/src/routes/billing.ts index 2d78e40c..acec99c6 100644 --- a/src/routes/billing.ts +++ b/src/routes/billing.ts @@ -86,7 +86,7 @@ function sendSimulationFailure( res: Response, result: Pick, ): void { - console.warn("Soroban simulation diagnostics:", result.simulationDetails); + logger.warn("Soroban simulation diagnostics:", result.simulationDetails); res.status(502).json({ error: "Soroban simulation failed", code: "SIMULATION_FAILED", @@ -265,7 +265,7 @@ router.post( } catch (error) { if (error instanceof SorobanRpcError) { if (error.simulationDetails) { - console.warn( + logger.warn( "Soroban simulation diagnostics:", error.simulationDetails, ); diff --git a/src/routes/billing/deduct.ts b/src/routes/billing/deduct.ts index 892cdfc2..2098e3a1 100644 --- a/src/routes/billing/deduct.ts +++ b/src/routes/billing/deduct.ts @@ -71,9 +71,9 @@ const idempotencyHandler = ( * {@link redactSimulationDetails} is emitted. */ function logSimulationFailure(details: unknown): void { - logger.warn("[billing/deduct] Soroban simulation failed", { + logger.warn({ simulationDetails: redactSimulationDetails(details), - }); + }, "[billing/deduct] Soroban simulation failed"); } /** diff --git a/src/routes/proxyRoutes.ts b/src/routes/proxyRoutes.ts index cafcf43d..59c607ef 100644 --- a/src/routes/proxyRoutes.ts +++ b/src/routes/proxyRoutes.ts @@ -428,16 +428,16 @@ export function createProxyRouter(deps: ProxyDeps): Router { // before (idempotency guard inside usageStore.record). if (recorded && endpoint.priceUsdc > 0) { billing.deductCredit(keyRecord.userId, endpoint.priceUsdc).catch((err) => { - console.error('Background billing deduction failed:', err); + logger.error({ error: err }, 'Background billing deduction failed'); }); } } catch (err) { recordUsageRecordFailure(); - logger.error('Background usage recording failed', { + logger.error({ requestId, apiId: String(apiEntry.id), error: err, - }); + }, 'Background usage recording failed'); } })(); }); diff --git a/src/services/billing.ts b/src/services/billing.ts index 292daa22..adb1c683 100644 --- a/src/services/billing.ts +++ b/src/services/billing.ts @@ -34,6 +34,7 @@ import type { Pool, PoolClient } from "pg"; import type { SimulationDetails } from "../lib/simulationDiagnostics.js"; import { computeJitteredDelay, type RandomSource } from "../lib/retry.js"; import { DeveloperSemaphore } from "../utils/developerSemaphore.js"; +import { logger } from "../middleware/logging.js"; const USDC_7_DECIMAL_FACTOR = 10_000_000n; const DEFAULT_RETRY_DELAYS_MS = [150, 500, 1_000]; @@ -685,9 +686,9 @@ export class BillingService { // a data-integrity concern but NOT a reason to report failure to the // caller — the charge happened. Log and return success; the // reconciliation job will back-fill the hash. - console.error( - `[BillingService] Phase 3 UPDATE failed for usageEventId=${usageEventId} ` + - `txHash=${deductResult.txHash}: ${normalizeErrorMessage(error)}`, + logger.error( + { usageEventId, txHash: deductResult.txHash, error: normalizeErrorMessage(error) }, + '[BillingService] Phase 3 UPDATE failed', ); } @@ -889,10 +890,13 @@ export class BillingService { deductResult.txHash, ); } catch (error) { - console.error( - `[BillingService] Bulk Phase 3 UPDATE failed for usageEventIds=` + - `${phase1.inserted.map((entry) => entry.usageEventId).join(",")} ` + - `txHash=${deductResult.txHash}: ${normalizeErrorMessage(error)}`, + logger.error( + { + usageEventIds: phase1.inserted.map((entry) => entry.usageEventId), + txHash: deductResult.txHash, + error: normalizeErrorMessage(error) + }, + '[BillingService] Bulk Phase 3 UPDATE failed' ); } diff --git a/src/services/rateLimiter.ts b/src/services/rateLimiter.ts index b1ecb5cc..67b9ed09 100644 --- a/src/services/rateLimiter.ts +++ b/src/services/rateLimiter.ts @@ -1,6 +1,6 @@ import type { PoolClient } from 'pg'; import type { RateLimiter, RateLimitResult } from '../types/gateway.js'; -import { logger } from '../logger.js'; +import { logger } from '../middleware/logging.js'; import { recordRateLimiterStoreOutage, recordRateLimiterStoreRecovery, @@ -444,7 +444,7 @@ export class StoreBackedRateLimiter implements RateLimiter { private resolvePolicy(tier?: string): TierPolicy { if (!tier || !(tier in this.tierPolicies)) { if (tier) { - console.warn(`[rateLimiter] Unknown tier "${tier}", using default`); + logger.warn(`[rateLimiter] Unknown tier "${tier}", using default`); } return { maxRequests: this.maxRequests, windowMs: this.windowMs }; } diff --git a/src/services/revenueSettlementService.ts b/src/services/revenueSettlementService.ts index 937197c9..bccec89d 100644 --- a/src/services/revenueSettlementService.ts +++ b/src/services/revenueSettlementService.ts @@ -3,6 +3,7 @@ import { ApiRegistry, UsageEvent, UsageStore } from '../types/gateway.js'; import { SorobanSettlementClient } from './sorobanSettlement.js'; import { randomUUID } from 'node:crypto'; import { calloraEvents } from '../events/event.emitter.js'; +import { logger } from '../middleware/logging.js'; import { RETRIABLE_HTTP_STATUSES, TransientError, @@ -151,10 +152,7 @@ export class RevenueSettlementService { await this.settlementStore.create(settlement); } catch (error) { errors++; - console.error( - `Settlement ${settlementId} failed for dev ${developerId}:`, - this.getErrorMessage(error) - ); + logger.error({ settlementId, developerId, error: this.getErrorMessage(error) }, 'Settlement failed for dev'); continue; } @@ -264,7 +262,7 @@ export class RevenueSettlementService { completed++; } catch (updateError) { errors++; - console.warn( + logger.warn( { settlementId: settlement.id, error: updateError }, 'Failed to update settlement to completed — skipping', ); @@ -279,7 +277,7 @@ export class RevenueSettlementService { failed++; } catch (updateError) { errors++; - console.warn( + logger.warn( { settlementId: settlement.id, error: updateError }, 'Failed to update settlement to failed — skipping', ); @@ -292,7 +290,7 @@ export class RevenueSettlementService { retried++; } catch (updateError) { errors++; - console.warn( + logger.warn( { settlementId: settlement.id, error: updateError }, 'Failed to schedule retry for settlement — skipping', ); @@ -305,14 +303,14 @@ export class RevenueSettlementService { failed++; } catch (updateError) { errors++; - console.warn( + logger.warn( { settlementId: settlement.id, error: updateError }, 'Failed to update settlement to failed — skipping', ); } if (!transactionCode) { - console.warn( + logger.warn( { settlementId: settlement.id }, 'Horizon returned tx_failed but missing result_codes', ); @@ -325,20 +323,20 @@ export class RevenueSettlementService { failed++; } catch (updateError) { errors++; - console.warn( + logger.warn( { settlementId: settlement.id, error: updateError }, 'Failed to update settlement to failed (not found) — skipping', ); } - console.warn( + logger.warn( { settlementId: settlement.id }, 'Horizon did not find transaction', ); } else { // Unexpected response shape; leave as pending and log warning errors++; - console.warn( + logger.warn( { settlementId: settlement.id }, 'Unexpected Horizon response shape — leaving settlement pending', ); @@ -346,7 +344,7 @@ export class RevenueSettlementService { } catch (error) { // Catch any exception during per-settlement processing to continue batch errors++; - console.warn( + logger.warn( { settlementId: settlement.id, error }, 'Failed to sync settlement status — skipping', ); @@ -438,16 +436,10 @@ export class RevenueSettlementService { clearTxHash ? null : undefined, ); } catch (statusError) { - console.error( - `Settlement ${settlementId} failed for dev ${developerId} and could not persist failure status:`, - this.getErrorMessage(statusError), - ); + logger.error({ settlementId, developerId, error: this.getErrorMessage(statusError) }, 'Settlement failed for dev and could not persist failure status'); } - console.error( - `Settlement ${settlementId} failed for dev ${developerId}:`, - errorMessage ?? 'Unknown settlement failure', - ); + logger.error({ settlementId, developerId, error: errorMessage ?? 'Unknown settlement failure' }, 'Settlement failed for dev'); } private getErrorMessage(error: unknown): string { diff --git a/tests/integration/ipAllowlist.integration.test.ts b/tests/integration/ipAllowlist.integration.test.ts index 51fb5343..1e5ecd97 100644 --- a/tests/integration/ipAllowlist.integration.test.ts +++ b/tests/integration/ipAllowlist.integration.test.ts @@ -1,6 +1,7 @@ import request from 'supertest'; import { app } from '../../src/index.js'; import { createIpAllowlist } from '../../src/middleware/ipAllowlist.js'; +import { logger } from '../../src/logger.js'; import express from 'express'; describe('IP Allowlist Integration Tests', () => { @@ -216,9 +217,9 @@ describe('IP Allowlist Integration Tests', () => { // Mock logger to capture logs const mockLogs: any[] = []; - const originalWarn = console.warn; - console.warn = (message: string, data: any) => { - mockLogs.push({ message, data }); + const originalWarn = logger.warn; + logger.warn = (...args: any[]) => { + mockLogs.push({ message: args[0], data: args[1] }); }; try { @@ -242,7 +243,7 @@ describe('IP Allowlist Integration Tests', () => { expect(securityLog.data.timestamp).toBeDefined(); } finally { - console.warn = originalWarn; + logger.warn = originalWarn; } }); });