Skip to content
Merged
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
9 changes: 7 additions & 2 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module, OnModuleInit } from "@nestjs/common";
import { Module, NestModule, MiddlewareConsumer, OnModuleInit } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import { validateEnv } from "./config/env.validation";
Expand Down Expand Up @@ -55,6 +55,7 @@ import { RolesGuard } from "./common/guard/roles.guard";
import { KycGuard } from "./common/guard/kyc.guard";
import { StrategyAuthGuard } from "./auth/guards/strategy-auth.guard";
import { SubmissionVerifierService } from "./oracle/submission-verifier.service";
import { LoggingMiddleware } from "./common/middleware/logging.middleware";

@Module({
imports: [
Expand Down Expand Up @@ -170,9 +171,13 @@ import { SubmissionVerifierService } from "./oracle/submission-verifier.service"
},
],
})
export class AppModule implements OnModuleInit {
export class AppModule implements NestModule, OnModuleInit {
constructor(private readonly verifier: SubmissionVerifierService) {}

configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggingMiddleware).forRoutes("*");
}

onModuleInit() {
this.verifier.start();
}
Expand Down
11 changes: 6 additions & 5 deletions src/common/database/database-index.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository, DataSource } from "typeorm";
import { logger } from "../../config/logger";

export interface IndexAnalysis {
tableName: string;
Expand Down Expand Up @@ -114,11 +115,11 @@ export class DatabaseIndexService {
await this.dataSource.query(
`CREATE INDEX CONCURRENTLY IF NOT EXISTS ${recommendation.indexName} ON ${recommendation.tableName} (${recommendation.columns.join(", ")});`,
);
console.log(`Created index: ${recommendation.indexName}`);
logger.info({ indexName: recommendation.indexName }, `Created index: ${recommendation.indexName}`);
} catch (error) {
console.error(
`Failed to create index ${recommendation.indexName}:`,
error,
logger.error(
{ indexName: recommendation.indexName, error },
`Failed to create index ${recommendation.indexName}`,
);
}
}
Expand Down Expand Up @@ -277,7 +278,7 @@ export class DatabaseIndexService {
"CREATE EXTENSION IF NOT EXISTS pg_stat_statements;",
);
} catch (error) {
console.warn("Could not enable pg_stat_statements extension:", error);
logger.warn({ error }, "Could not enable pg_stat_statements extension");
}
}
}
103 changes: 103 additions & 0 deletions src/common/middleware/logging.middleware.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { logger, createLogger } from "../../config/logger";
import { LoggingMiddleware } from "./logging.middleware";
import { Request, Response } from "express";

describe("logger", () => {
it("should be defined", () => {
expect(logger).toBeDefined();
});

it("should have required logging methods", () => {
expect(typeof logger.info).toBe("function");
expect(typeof logger.warn).toBe("function");
expect(typeof logger.error).toBe("function");
expect(typeof logger.debug).toBe("function");
});

it("should include service base field", () => {
expect((logger as any).bindings?.()?.service ?? (logger as any)[Symbol.for('pino.serializers')]).toBeTruthy();
});
});

describe("createLogger", () => {
it("should return a child logger with the given context", () => {
const child = createLogger({ module: "test" });
expect(child).toBeDefined();
expect(typeof child.info).toBe("function");
});
});

describe("LoggingMiddleware", () => {
let middleware: LoggingMiddleware;

beforeEach(() => {
middleware = new LoggingMiddleware();
});

it("should be defined", () => {
expect(middleware).toBeDefined();
});

it("should call next()", () => {
const req = {
headers: {},
method: "GET",
url: "/test",
ip: "127.0.0.1",
} as unknown as Request;

const res = {
setHeader: jest.fn(),
on: jest.fn(),
} as unknown as Response;

const next = jest.fn();

middleware.use(req, res, next);

expect(next).toHaveBeenCalledTimes(1);
});

it("should set x-correlation-id response header", () => {
const req = {
headers: {},
method: "GET",
url: "/test",
ip: "127.0.0.1",
} as unknown as Request;

const setHeader = jest.fn();
const res = {
setHeader,
on: jest.fn(),
} as unknown as Response;

middleware.use(req, res, jest.fn());

expect(setHeader).toHaveBeenCalledWith(
"x-correlation-id",
expect.any(String),
);
});

it("should use provided x-correlation-id from request headers", () => {
const correlationId = "test-correlation-id-123";
const req = {
headers: { "x-correlation-id": correlationId },
method: "GET",
url: "/test",
ip: "127.0.0.1",
} as unknown as Request;

const setHeader = jest.fn();
const res = {
setHeader,
on: jest.fn(),
} as unknown as Response;

middleware.use(req, res, jest.fn());

expect(setHeader).toHaveBeenCalledWith("x-correlation-id", correlationId);
expect((req as any).correlationId).toBe(correlationId);
});
});
44 changes: 44 additions & 0 deletions src/common/middleware/logging.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response, NextFunction } from "express";
import { v4 as uuidv4 } from "uuid";
import { logger } from "../../config/logger";

@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction): void {
const correlationId = (req.headers["x-correlation-id"] as string) || uuidv4();
const startTime = Date.now();

// Attach correlation ID to request and response headers
(req as any).correlationId = correlationId;
res.setHeader("x-correlation-id", correlationId);

const requestLog = logger.child({ correlationId });

requestLog.info(
{
method: req.method,
url: req.url,
userAgent: req.headers["user-agent"],
ip: req.ip,
},
"Incoming request",
);

res.on("finish", () => {
const duration = Date.now() - startTime;
const level = res.statusCode >= 400 ? "warn" : "info";
requestLog[level](
{
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration,
},
"Request completed",
);
});

next();
}
}
15 changes: 12 additions & 3 deletions src/config/logger.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
const pino = require("pino");
import { getCurrentTraceId } from "./tracing";

const isDevelopment = process.env.NODE_ENV === "development";

// Lazy getter to avoid circular dependency with tracing.ts
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getTraceId = (): string | undefined => {
try {
return require("./tracing").getCurrentTraceId();
} catch {
return undefined;
}
};

// Create a Pino logger that automatically includes trace IDs
export const logger = pino({
level: process.env.LOG_LEVEL || "info",
Expand All @@ -28,7 +37,7 @@ export const logger = pino({
timestamp: pino.stdTimeFunctions.isoTime,
// Mixin to add trace ID to every log entry
mixin() {
const traceId = getCurrentTraceId();
const traceId = getTraceId();
if (traceId) {
return {
trace_id: traceId,
Expand All @@ -40,7 +49,7 @@ export const logger = pino({

// Helper function to create child loggers with context
export const createLogger = (context: Record<string, any>) => {
const traceId = getCurrentTraceId();
const traceId = getTraceId();
const contextWithTrace = traceId
? { ...context, trace_id: traceId }
: context;
Expand Down
29 changes: 17 additions & 12 deletions src/config/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import {
} from "@opentelemetry/api";
import { W3CTraceContextPropagator } from "@opentelemetry/core";

// Lazy logger reference to avoid circular dependency with logger.ts
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getLogger = (): any => require("./logger").logger;

// Rate-limited sampling configuration
// Uses TraceIdRatioBasedSampler with configurable sampling rate
// For production, consider implementing custom adaptive sampling based on your needs
Expand All @@ -27,7 +31,7 @@ const createConfiguredSampler = () => {
);
const finalRate = Math.max(Math.min(samplingRate, 1.0), minSamplingRate);

console.log(`Configured sampling rate: ${finalRate * 100}%`);
getLogger().debug({ samplingRate: finalRate }, `Configured sampling rate: ${finalRate * 100}%`);
return new TraceIdRatioBasedSampler(finalRate);
};

Expand All @@ -40,7 +44,7 @@ const createSpanProcessor = (): SpanProcessor => {
process.env.OTEL_EXPORTER_JAEGER_ENDPOINT ||
"http://localhost:14268/api/traces",
});
console.log("Jaeger exporter configured");
getLogger().info("Jaeger exporter configured");
return new BatchSpanProcessor(jaegerExporter);
}

Expand All @@ -51,7 +55,7 @@ const createSpanProcessor = (): SpanProcessor => {
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||
"http://localhost:4318/v1/traces",
});
console.log("OTLP exporter configured");
getLogger().info("OTLP exporter configured");
return new BatchSpanProcessor(otlpExporter);
}

Expand Down Expand Up @@ -90,25 +94,26 @@ export const sdk = new NodeSDK({
export const startTracing = async () => {
try {
sdk.start();
console.log("OpenTelemetry tracing initialized with configurable sampling");
console.log(
"Jaeger endpoint:",
process.env.OTEL_EXPORTER_JAEGER_ENDPOINT ||
"http://localhost:14268/api/traces",
getLogger().info(
{
jaegerEndpoint:
process.env.OTEL_EXPORTER_JAEGER_ENDPOINT ||
"http://localhost:14268/api/traces",
},
"OpenTelemetry tracing initialized with configurable sampling",
);
console.log("Jaeger UI available at:", "http://localhost:16686");
} catch (err) {
console.error("Failed to start OpenTelemetry SDK:", err);
getLogger().error({ err }, "Failed to start OpenTelemetry SDK");
}
};

// Graceful shutdown
export const shutdownTracing = async () => {
try {
await sdk.shutdown();
console.log("OpenTelemetry tracing shut down");
getLogger().info("OpenTelemetry tracing shut down");
} catch (error) {
console.error("Error shutting down tracing:", error);
getLogger().error({ error }, "Error shutting down tracing");
}
};

Expand Down
3 changes: 2 additions & 1 deletion src/oracle/submission-verifier.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { Injectable, Logger } from "@nestjs/common";
import { AuditLogService } from "../audit/audit-log.service";
import { logger } from "../config/logger";

interface OnChainSubmission {
id: string;
Expand Down Expand Up @@ -116,7 +117,7 @@ export class SubmissionVerifierService {
// -------------------------------------
private async triggerAlerts(result: any) {
// 👉 Replace with real integrations
console.warn("ALERT: Submission mismatch detected", result);
logger.warn({ result }, "ALERT: Submission mismatch detected");

// Example webhook
// await axios.post(WEBHOOK_URL, result);
Expand Down
Loading