Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,12 @@ APP_VERSION=1.0.0

# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
LOG_LEVEL=info
ACCESS_LOG_SAMPLE_RATE=1
# ACCESS_LOG_REDACT_FIELDS=path,correlationId

# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# Profiling
# -----------------------------------------------------------------------------
GATEWAY_PROFILING_ENABLED=false
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ For request-id validation, AsyncLocalStorage propagation, structured logging, an
| `HEALTH_CHECK_DB_TIMEOUT` | No | `2000` | DB health check timeout (ms) |
| `APP_VERSION` | No | `1.0.0` | Reported in health check responses |
| `LOG_LEVEL` | No | `info` | `trace` / `debug` / `info` / `warn` / `error` / `fatal` |
| `ACCESS_LOG_SAMPLE_RATE` | No | `1` | Fraction of requests logged as access events (`1` = 100%) |
| `ACCESS_LOG_REDACT_FIELDS` | No | `""` | Comma-separated access-log fields to redact (`path`, `correlationId`, etc.) |
| `GATEWAY_PROFILING_ENABLED` | No | `false` | Enable request profiling |

### Health Check Behavior
Expand Down
3 changes: 3 additions & 0 deletions docs/request-id-propagation.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Both logger paths attach the active request id:

- `src/logger.ts` prefixes console-style logs with `[request_id:<id>]`.
- `src/middleware/logging.ts` injects `requestId` into Pino structured payloads.
- `src/middleware/accessLog.ts` emits JSON access logs with `method`, `path`, `status`, `ms`, request/response byte counts, and a `correlationId`.
- Access-log sampling defaults to 100% and can be reduced with `ACCESS_LOG_SAMPLE_RATE`.
- Access-log redaction is configurable with `ACCESS_LOG_REDACT_FIELDS`.

Sensitive values are still redacted before logging.

Expand Down
53 changes: 50 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 21 additions & 9 deletions src/__tests__/api-key-redaction-regression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import type { Request, Response } from 'express';
import { REDACTED_LOG_VALUE, redactLogValue } from '../logger.js';
import { logger, requestLogger } from '../middleware/logging.js';
import { logger } from '../middleware/logging.js';
import { requestLogger } from '../middleware/accessLog.js';

describe('API Key Redaction Regression Tests', () => {
describe('redactLogValue - common API key formats', () => {
Expand Down Expand Up @@ -184,7 +185,12 @@ describe('API Key Redaction Regression Tests', () => {
assert(!('headers' in payload), 'headers should not be in log payload');
assert(!('body' in payload), 'body should not be in log payload');
assert(payload.requestId, 'requestId should be in log payload');
assert(payload.correlationId, 'correlationId should be in log payload');
assert.equal(payload.method, 'POST');
assert.equal(payload.status, 200);
assert.equal(payload.statusCode, 200);
assert.equal(payload.requestBytes, 0);
assert.equal(payload.responseBytes, 0);
} finally {
infoSpy.mockRestore();
}
Expand Down Expand Up @@ -215,14 +221,20 @@ describe('API Key Redaction Regression Tests', () => {
res.emit('finish');

// Verify that the authorization header doesn't leak into the request ID or logs
expect(infoSpy.mock.calls[0][0]).toEqual({
requestId: 'safe-request-id-123',
method: 'GET',
path: '/api/data',
statusCode: 200,
durationMs: expect.any(Number),
clientIp: expect.any(String),
});
expect(infoSpy.mock.calls[0][0]).toEqual(
expect.objectContaining({
requestId: 'safe-request-id-123',
correlationId: 'safe-request-id-123',
method: 'GET',
path: '/api/data',
status: 200,
statusCode: 200,
ms: expect.any(Number),
durationMs: expect.any(Number),
requestBytes: 0,
responseBytes: 0,
}),
);
} finally {
infoSpy.mockRestore();
}
Expand Down
11 changes: 8 additions & 3 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { VaultController } from './controllers/vaultController.js';
import { TransactionBuilderService } from './services/transactionBuilder.js';
import { requestIdMiddleware } from './middleware/requestId.js';
import { validate } from './middleware/validate.js';
import { requestLogger } from './middleware/logging.js';
import { createAccessLogMiddleware } from './middleware/accessLog.js';
import { auditEnrichMiddleware } from './middleware/auditEnrich.js';
import { createConfiguredRestRateLimitMiddleware } from './middleware/restRateLimit.js';
import { metricsMiddleware, metricsEndpoint } from './metrics.js';
Expand Down Expand Up @@ -148,7 +148,12 @@ export const createApp = (dependencies?: Partial<AppDependencies>) => {
app.use(requestIdMiddleware);
app.use(metricsMiddleware);

app.use(requestLogger);
app.use(
createAccessLogMiddleware({
sampleRate: config.accessLog.sampleRate,
redactFields: config.accessLog.redactFields,
}),
);

// Parse allowed origins with validation
const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS ?? 'http://localhost:5173')
Expand Down Expand Up @@ -552,4 +557,4 @@ export const createApp = (dependencies?: Partial<AppDependencies>) => {
app.use(errorHandler);

return app;
};
};
2 changes: 2 additions & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ export const envSchema = z
LOG_LEVEL: z
.enum(["trace", "debug", "info", "warn", "error", "fatal"])
.default("info"),
ACCESS_LOG_SAMPLE_RATE: z.coerce.number().min(0).max(1).default(1),
ACCESS_LOG_REDACT_FIELDS: z.string().optional(),

// Profiling
GATEWAY_PROFILING_ENABLED: z
Expand Down
7 changes: 7 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ export const config = {
port: env.PORT,
nodeEnv: env.NODE_ENV,
version: env.APP_VERSION,
accessLog: {
sampleRate: env.ACCESS_LOG_SAMPLE_RATE,
redactFields: (env.ACCESS_LOG_REDACT_FIELDS ?? '')
.split(',')
.map((field) => field.trim())
.filter((field) => field.length > 0),
},

databaseUrl: env.DATABASE_URL,
replicaUrls: env.REPLICA_URLS,
Expand Down
12 changes: 11 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import helmet from 'helmet';
import { initializeDb, closeDb } from './db/index.js';
import { closePgPool, pool } from './db.js';
import { closeDbPool } from './config/health.js';
import { config } from './config/index.js';
import { disconnectPrisma } from './lib/prisma.js';
import { legacyV1DeprecationMiddleware } from './middleware/deprecation.js';
import { errorHandler } from './middleware/errorHandler.js';
import { createGatewayIpAllowlist } from './middleware/ipAllowlist.js';
import { createAccessLogMiddleware } from './middleware/accessLog.js';
import { requestIdMiddleware } from './middleware/requestId.js';
import { metricsEndpoint } from './metrics.js';
import { awaitWebhookDispatcherIdle, stopWebhookDispatching } from './webhooks/webhook.dispatcher.js';
import {
Expand Down Expand Up @@ -36,7 +39,6 @@ import { createPostgresUsageStore } from './services/usageStore.js';
import { createPostgresSettlementStore } from './services/settlementStore.js';
import { createApiRegistry } from './data/apiRegistry.js';
import { ApiKey } from './types/gateway.js';
import { config } from './config/index.js';
import { listingsCache } from './lib/listingsCache.js';

// Helper for Jest/CommonJS compat
Expand All @@ -47,6 +49,14 @@ export { createGracefulShutdownHandler, createInFlightDrainTracker, type Drainab

export const app = express();

app.use(requestIdMiddleware);
app.use(
createAccessLogMiddleware({
sampleRate: config.accessLog.sampleRate,
redactFields: config.accessLog.redactFields,
}),
);

// Standard JSON middleware for non-webhook routes
app.use((req, res, next) => {
if (req.path === '/api/webhooks') {
Expand Down
Loading