refactor
This commit is contained in:
245
docs/architecture/LOGGING.md
Normal file
245
docs/architecture/LOGGING.md
Normal file
@@ -0,0 +1,245 @@
|
||||
Logging & Correlation ID Design Guide (Clean Architecture)
|
||||
|
||||
This document defines a clean, strict, and production-ready logging architecture with correlation IDs.
|
||||
|
||||
It removes ambiguity around:
|
||||
• where logging belongs
|
||||
• how context is attached
|
||||
• how logs stay machine-readable
|
||||
• how Clean Architecture boundaries are preserved
|
||||
|
||||
The rules below are non-negotiable.
|
||||
|
||||
⸻
|
||||
|
||||
Core Principles
|
||||
|
||||
Logs are data, not text.
|
||||
Context is injected, never constructed in the Core.
|
||||
|
||||
Logging must:
|
||||
• be machine-readable
|
||||
• be framework-agnostic in the Core
|
||||
• support correlation across requests
|
||||
• be safe for parallel execution
|
||||
|
||||
⸻
|
||||
|
||||
Architectural Responsibilities
|
||||
|
||||
Core
|
||||
• describes intent
|
||||
• never knows about correlation IDs
|
||||
• never knows about log destinations
|
||||
|
||||
App Layer (API)
|
||||
• defines runtime context (request, user)
|
||||
• binds correlation IDs
|
||||
|
||||
Adapters
|
||||
• implement concrete loggers (console, file, structured)
|
||||
• decide formatting and transport
|
||||
|
||||
⸻
|
||||
|
||||
1. Logger Port (Core)
|
||||
|
||||
Purpose
|
||||
|
||||
Defines what logging means, without defining how logging works.
|
||||
|
||||
⸻
|
||||
|
||||
Rules
|
||||
• exactly one logging interface
|
||||
• no framework imports
|
||||
• no correlation or runtime context
|
||||
|
||||
⸻
|
||||
|
||||
Location
|
||||
|
||||
core/shared/application/LoggerPort.ts
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
Contract
|
||||
• debug(message, meta?)
|
||||
• info(message, meta?)
|
||||
• warn(message, meta?)
|
||||
• error(message, meta?)
|
||||
|
||||
Messages are semantic.
|
||||
Metadata is optional and structured.
|
||||
|
||||
⸻
|
||||
|
||||
2. Request Context (App Layer)
|
||||
|
||||
Purpose
|
||||
|
||||
Represents runtime execution context.
|
||||
|
||||
⸻
|
||||
|
||||
Contains
|
||||
• correlationId
|
||||
• optional userId
|
||||
|
||||
⸻
|
||||
|
||||
Rules
|
||||
• never visible to Core
|
||||
• created per request
|
||||
|
||||
⸻
|
||||
|
||||
Location
|
||||
|
||||
apps/api/context/RequestContext.ts
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
3. Logger Implementations (Adapters)
|
||||
|
||||
Purpose
|
||||
|
||||
Provide concrete logging behavior.
|
||||
|
||||
⸻
|
||||
|
||||
Rules
|
||||
• implement LoggerPort
|
||||
• accept context via constructor
|
||||
• produce structured logs
|
||||
• no business logic
|
||||
|
||||
⸻
|
||||
|
||||
Examples
|
||||
• ConsoleLogger
|
||||
• FileLogger
|
||||
• PinoLogger
|
||||
• LokiLogger
|
||||
|
||||
⸻
|
||||
|
||||
Location
|
||||
|
||||
adapters/logging/
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
4. Logger Factory
|
||||
|
||||
Purpose
|
||||
|
||||
Creates context-bound logger instances.
|
||||
|
||||
⸻
|
||||
|
||||
Rules
|
||||
• factory is injected
|
||||
• logger instances are short-lived
|
||||
• component name is bound here
|
||||
|
||||
⸻
|
||||
|
||||
Location
|
||||
|
||||
adapters/logging/LoggerFactory.ts
|
||||
adapters/logging/LoggerFactoryImpl.ts
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
5. Correlation ID Handling
|
||||
|
||||
Where it lives
|
||||
• API middleware
|
||||
• message envelopes
|
||||
• background job contexts
|
||||
|
||||
⸻
|
||||
|
||||
Rules
|
||||
• generated once per request
|
||||
• propagated across async boundaries
|
||||
• never generated in the Core
|
||||
|
||||
⸻
|
||||
|
||||
6. Usage Rules by Layer
|
||||
|
||||
Layer Logging Allowed Notes
|
||||
Domain ❌ No Throw domain errors instead
|
||||
Use Cases ⚠️ Minimal Business milestones only
|
||||
API Services ✅ Yes Main logging location
|
||||
Adapters ✅ Yes IO, integration, failures
|
||||
Frontend ⚠️ Limited Errors + analytics only
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
7. Forbidden Patterns
|
||||
|
||||
❌ Manual string prefixes ([ServiceName])
|
||||
❌ Global/singleton loggers with mutable state
|
||||
❌ any in logger abstractions
|
||||
❌ Correlation IDs in Core
|
||||
❌ Logging inside domain entities
|
||||
|
||||
⸻
|
||||
|
||||
8. File Structure (Final)
|
||||
|
||||
core/
|
||||
└── shared/
|
||||
└── application/
|
||||
└── LoggerPort.ts # * required
|
||||
|
||||
apps/api/
|
||||
├── context/
|
||||
│ └── RequestContext.ts # * required
|
||||
├── middleware/
|
||||
│ └── CorrelationMiddleware.ts
|
||||
└── modules/
|
||||
└── */
|
||||
└── *Service.ts
|
||||
|
||||
adapters/
|
||||
└── logging/
|
||||
├── LoggerFactory.ts # * required
|
||||
├── LoggerFactoryImpl.ts # * required
|
||||
├── ConsoleLogger.ts # optional
|
||||
├── FileLogger.ts # optional
|
||||
└── PinoLogger.ts # optional
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
Mental Model (Final)
|
||||
|
||||
The Core describes events.
|
||||
The App provides context.
|
||||
Adapters deliver telemetry.
|
||||
|
||||
If any layer violates this, the architecture is broken.
|
||||
|
||||
⸻
|
||||
|
||||
Summary
|
||||
• one LoggerPort in the Core
|
||||
• context bound outside the Core
|
||||
• adapters implement logging destinations
|
||||
• correlation IDs are runtime concerns
|
||||
• logs are structured, searchable, and safe
|
||||
|
||||
This setup is:
|
||||
• Clean Architecture compliant
|
||||
• production-ready
|
||||
• scalable
|
||||
• refactor-safe
|
||||
@@ -168,6 +168,90 @@ export class SponsorsController {
|
||||
}
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
Payments Example
|
||||
|
||||
Application Layer
|
||||
|
||||
Use Case
|
||||
|
||||
@Injectable()
|
||||
export class CreatePaymentUseCase {
|
||||
constructor(
|
||||
private readonly paymentRepository: IPaymentRepository,
|
||||
private readonly output: UseCaseOutputPort<CreatePaymentResult>,
|
||||
) {}
|
||||
|
||||
async execute(input: CreatePaymentInput): Promise<Result<void, ApplicationErrorCode<CreatePaymentErrorCode>>> {
|
||||
// business logic
|
||||
const payment = await this.paymentRepository.create(payment);
|
||||
|
||||
this.output.present({ payment });
|
||||
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
Result Model
|
||||
|
||||
type CreatePaymentResult = {
|
||||
payment: Payment;
|
||||
};
|
||||
|
||||
API Layer
|
||||
|
||||
Presenter
|
||||
|
||||
@Injectable()
|
||||
export class CreatePaymentPresenter
|
||||
implements UseCaseOutputPort<CreatePaymentResult>
|
||||
{
|
||||
private viewModel: CreatePaymentViewModel | null = null;
|
||||
|
||||
present(result: CreatePaymentResult): void {
|
||||
this.viewModel = {
|
||||
payment: this.mapPaymentToDto(result.payment),
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): CreatePaymentViewModel | null {
|
||||
return this.viewModel;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.viewModel = null;
|
||||
}
|
||||
|
||||
private mapPaymentToDto(payment: Payment): PaymentDto {
|
||||
return {
|
||||
id: payment.id,
|
||||
// ... other fields
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Controller
|
||||
|
||||
@Controller('/payments')
|
||||
export class PaymentsController {
|
||||
constructor(
|
||||
private readonly useCase: CreatePaymentUseCase,
|
||||
private readonly presenter: CreatePaymentPresenter,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
async createPayment(@Body() input: CreatePaymentInput) {
|
||||
const result = await this.useCase.execute(input);
|
||||
|
||||
if (result.isErr()) {
|
||||
throw mapApplicationError(result.unwrapErr());
|
||||
}
|
||||
|
||||
return this.presenter.getViewModel();
|
||||
}
|
||||
}
|
||||
|
||||
⸻
|
||||
|
||||
4. Module Wiring (Composition Root)
|
||||
|
||||
Reference in New Issue
Block a user