cariff-logger 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ name: Publish to NPM
2
+
3
+ on:
4
+ release:
5
+ types: [created]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: Checkout repository
13
+ uses: actions/checkout@v4
14
+
15
+ - name: Setup Node.js
16
+ uses: actions/setup-node@v4
17
+ with:
18
+ node-version: '24'
19
+ registry-url: 'https://registry.npmjs.org'
20
+
21
+ - name: Install dependencies & Build
22
+ run: |
23
+ npm install
24
+ npm run build
25
+
26
+ - name: Publish to NPM
27
+ run: npm publish --access public
28
+ env:
29
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md CHANGED
@@ -1,3 +1,77 @@
1
- # Temporary Holding Version
1
+ # cariff-logger
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Стандартизированный модуль структурированного JSON логирования и распределенного трейсинга для микросервисов платформы **Cariff**.
4
+
5
+ ## 🚀 Особенности
6
+
7
+ - **Zero heavy runtime dependencies:** работает нативно поверх `stdout`/`stderr` и Node.js `AsyncLocalStorage`.
8
+ - **Строгая NDJSON схема в production:**
9
+ - `timestamp`: ISO 8601
10
+ - `level`: `INFO`, `WARN`, `ERROR`, `FATAL` (уровень `DEBUG` подавлен в проде)
11
+ - `service`: идентификатор сервиса (`api-gateway`, `auth-service` и т.д.)
12
+ - `message`: понятный текст события
13
+ - `correlationId`: сквозной идентификатор запроса (автоматически пробрасывается из контекста)
14
+ - `context`, `metadata`, `stack`
15
+ - **Readable colored output в development:** цветной читаемый вывод для локальной разработки.
16
+ - **Интеграция с NestJS:** реализует `LoggerService`.
17
+
18
+ ## 📦 Установка
19
+
20
+ ```bash
21
+ yarn add cariff-logger
22
+ # или
23
+ npm install cariff-logger
24
+ ```
25
+
26
+ ## 🛠 Использование в NestJS
27
+
28
+ ### 1. Подключение глобального логгера (`main.ts`)
29
+
30
+ ```typescript
31
+ import { NestFactory } from '@nestjs/core';
32
+ import { AppModule } from './app.module';
33
+ import { CariffLogger, setupGlobalErrorHandlers } from 'cariff-logger';
34
+
35
+ async function bootstrap() {
36
+ const serviceName = 'auth-service';
37
+ setupGlobalErrorHandlers(serviceName);
38
+
39
+ const app = await NestFactory.create(AppModule, {
40
+ logger: new CariffLogger(serviceName),
41
+ });
42
+
43
+ await app.listen(3333);
44
+ }
45
+ bootstrap();
46
+ ```
47
+
48
+ ### 2. Подключение Middleware для Correlation ID (`app.module.ts`)
49
+
50
+ ```typescript
51
+ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
52
+ import { CorrelationIdMiddleware } from 'cariff-logger';
53
+
54
+ @Module({})
55
+ export class AppModule implements NestModule {
56
+ configure(consumer: MiddlewareConsumer) {
57
+ consumer.apply(new CorrelationIdMiddleware('api-gateway')).forRoutes('*');
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### 3. Логирование внутри сервисов
63
+
64
+ Используйте стандартный NestJS `Logger` — все вызовы автоматически будут отформатированы по стандарту:
65
+
66
+ ```typescript
67
+ import { Injectable, Logger } from '@nestjs/common';
68
+
69
+ @Injectable()
70
+ export class UserService {
71
+ private readonly logger = new Logger(UserService.name);
72
+
73
+ async getUser(id: string) {
74
+ this.logger.log('Fetching user details'); // Автоматически прикрепит correlationId!
75
+ }
76
+ }
77
+ ```
@@ -0,0 +1,10 @@
1
+ export interface LogContext {
2
+ correlationId?: string;
3
+ [key: string]: unknown;
4
+ }
5
+ export declare class LoggerContextStore {
6
+ static run<R>(context: LogContext, fn: () => R): R;
7
+ static getCorrelationId(): string | undefined;
8
+ static getContext(): LogContext | undefined;
9
+ static set(key: string, value: unknown): void;
10
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LoggerContextStore = void 0;
4
+ const async_hooks_1 = require("async_hooks");
5
+ const asyncLocalStorage = new async_hooks_1.AsyncLocalStorage();
6
+ class LoggerContextStore {
7
+ static run(context, fn) {
8
+ return asyncLocalStorage.run(context, fn);
9
+ }
10
+ static getCorrelationId() {
11
+ return asyncLocalStorage.getStore()?.correlationId;
12
+ }
13
+ static getContext() {
14
+ return asyncLocalStorage.getStore();
15
+ }
16
+ static set(key, value) {
17
+ const store = asyncLocalStorage.getStore();
18
+ if (store) {
19
+ store[key] = value;
20
+ }
21
+ }
22
+ }
23
+ exports.LoggerContextStore = LoggerContextStore;
@@ -0,0 +1 @@
1
+ export declare function setupGlobalErrorHandlers(serviceName: string): void;
package/dist/fatal.js ADDED
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setupGlobalErrorHandlers = setupGlobalErrorHandlers;
4
+ const logger_service_1 = require("./logger.service");
5
+ function setupGlobalErrorHandlers(serviceName) {
6
+ const logger = new logger_service_1.CariffLogger(serviceName);
7
+ process.on('uncaughtException', (error) => {
8
+ logger.fatal({
9
+ message: `Uncaught Exception: ${error?.message || error}`,
10
+ error: error?.message,
11
+ }, error?.stack, 'Process');
12
+ // Allow stdout/stderr to flush before exiting
13
+ setTimeout(() => {
14
+ process.exit(1);
15
+ }, 100);
16
+ });
17
+ process.on('unhandledRejection', (reason) => {
18
+ const error = reason instanceof Error ? reason : new Error(String(reason));
19
+ logger.fatal({
20
+ message: `Unhandled Rejection: ${error.message}`,
21
+ error: error.message,
22
+ }, error.stack, 'Process');
23
+ });
24
+ }
@@ -0,0 +1,4 @@
1
+ export * from './context';
2
+ export * from './logger.service';
3
+ export * from './middleware';
4
+ export * from './fatal';
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./context"), exports);
18
+ __exportStar(require("./logger.service"), exports);
19
+ __exportStar(require("./middleware"), exports);
20
+ __exportStar(require("./fatal"), exports);
@@ -0,0 +1,29 @@
1
+ import { LoggerService } from '@nestjs/common';
2
+ export type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
3
+ export interface LogEntry {
4
+ timestamp: string;
5
+ level: LogLevel;
6
+ service: string;
7
+ message: string;
8
+ correlationId?: string;
9
+ context?: string;
10
+ stack?: string;
11
+ metadata?: Record<string, unknown>;
12
+ }
13
+ export interface LoggerOptions {
14
+ serviceName: string;
15
+ isProduction?: boolean;
16
+ }
17
+ export declare class CariffLogger implements LoggerService {
18
+ private serviceName;
19
+ private isProduction;
20
+ constructor(serviceNameOrOptions?: string | LoggerOptions);
21
+ setServiceName(name: string): void;
22
+ log(message: any, context?: string): void;
23
+ error(message: any, trace?: string, context?: string): void;
24
+ warn(message: any, context?: string): void;
25
+ debug?(message: any, context?: string): void;
26
+ verbose?(message: any, context?: string): void;
27
+ fatal(message: any, trace?: string, context?: string): void;
28
+ private writeLog;
29
+ }
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.CariffLogger = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const context_1 = require("./context");
15
+ let CariffLogger = class CariffLogger {
16
+ serviceName;
17
+ isProduction;
18
+ constructor(serviceNameOrOptions = 'cariff-service') {
19
+ if (typeof serviceNameOrOptions === 'string') {
20
+ this.serviceName = serviceNameOrOptions;
21
+ this.isProduction = process.env.NODE_ENV === 'production';
22
+ }
23
+ else {
24
+ this.serviceName = serviceNameOrOptions.serviceName;
25
+ this.isProduction = serviceNameOrOptions.isProduction ?? process.env.NODE_ENV === 'production';
26
+ }
27
+ }
28
+ setServiceName(name) {
29
+ this.serviceName = name;
30
+ }
31
+ log(message, context) {
32
+ this.writeLog('INFO', message, context);
33
+ }
34
+ error(message, trace, context) {
35
+ this.writeLog('ERROR', message, context, trace);
36
+ }
37
+ warn(message, context) {
38
+ this.writeLog('WARN', message, context);
39
+ }
40
+ debug(message, context) {
41
+ if (this.isProduction) {
42
+ return; // Suppress DEBUG in production according to spec
43
+ }
44
+ this.writeLog('DEBUG', message, context);
45
+ }
46
+ verbose(message, context) {
47
+ if (this.isProduction) {
48
+ return;
49
+ }
50
+ this.writeLog('DEBUG', message, context);
51
+ }
52
+ fatal(message, trace, context) {
53
+ this.writeLog('FATAL', message, context, trace);
54
+ }
55
+ writeLog(level, message, context, stack) {
56
+ const timestamp = new Date().toISOString();
57
+ const correlationId = context_1.LoggerContextStore.getCorrelationId();
58
+ let textMessage = '';
59
+ let metadata;
60
+ if (typeof message === 'object' && message !== null) {
61
+ if (message instanceof Error) {
62
+ textMessage = message.message;
63
+ stack = stack || message.stack;
64
+ }
65
+ else {
66
+ const { msg, message: msgField, ...rest } = message;
67
+ textMessage = (msgField || msg || JSON.stringify(message));
68
+ if (Object.keys(rest).length > 0) {
69
+ metadata = rest;
70
+ }
71
+ }
72
+ }
73
+ else {
74
+ textMessage = String(message);
75
+ }
76
+ if (this.isProduction) {
77
+ // Production: strict single-line flat NDJSON to stdout/stderr
78
+ const logEntry = {
79
+ timestamp,
80
+ level,
81
+ service: this.serviceName,
82
+ message: textMessage,
83
+ ...(correlationId ? { correlationId } : {}),
84
+ ...(context ? { context } : {}),
85
+ ...(stack ? { stack } : {}),
86
+ ...(metadata ? { metadata } : {}),
87
+ };
88
+ const jsonString = JSON.stringify(logEntry) + '\n';
89
+ if (level === 'ERROR' || level === 'FATAL') {
90
+ process.stderr.write(jsonString);
91
+ }
92
+ else {
93
+ process.stdout.write(jsonString);
94
+ }
95
+ }
96
+ else {
97
+ // Development: Human-friendly colored terminal output
98
+ const colors = {
99
+ DEBUG: '\x1b[34m', // Blue
100
+ INFO: '\x1b[32m', // Green
101
+ WARN: '\x1b[33m', // Yellow
102
+ ERROR: '\x1b[31m', // Red
103
+ FATAL: '\x1b[35;1m', // Magenta bold
104
+ };
105
+ const reset = '\x1b[0m';
106
+ const gray = '\x1b[90m';
107
+ const cyan = '\x1b[36m';
108
+ const color = colors[level] || reset;
109
+ const ctx = context ? ` ${cyan}[${context}]${reset}` : '';
110
+ const corr = correlationId ? ` ${gray}(corr: ${correlationId})${reset}` : '';
111
+ const formatted = `${gray}${timestamp}${reset} ${color}${level.padEnd(5)}${reset} ${gray}[${this.serviceName}]${reset}${ctx}${corr} ${textMessage}`;
112
+ if (level === 'ERROR' || level === 'FATAL') {
113
+ process.stderr.write(formatted + '\n');
114
+ if (stack) {
115
+ process.stderr.write(`${gray}${stack}${reset}\n`);
116
+ }
117
+ }
118
+ else {
119
+ process.stdout.write(formatted + '\n');
120
+ }
121
+ }
122
+ }
123
+ };
124
+ exports.CariffLogger = CariffLogger;
125
+ exports.CariffLogger = CariffLogger = __decorate([
126
+ (0, common_1.Injectable)({ scope: common_1.Scope.DEFAULT }),
127
+ __metadata("design:paramtypes", [Object])
128
+ ], CariffLogger);
@@ -0,0 +1,22 @@
1
+ import { NestMiddleware } from '@nestjs/common';
2
+ export declare const CORRELATION_ID_HEADER = "x-correlation-id";
3
+ export interface HttpRequestLike {
4
+ headers: Record<string, string | string[] | undefined>;
5
+ method: string;
6
+ url: string;
7
+ originalUrl?: string;
8
+ ip?: string;
9
+ socket?: {
10
+ remoteAddress?: string;
11
+ };
12
+ }
13
+ export interface HttpResponseLike {
14
+ statusCode: number;
15
+ setHeader(name: string, value: string): void;
16
+ on(event: 'finish' | string, listener: () => void): this;
17
+ }
18
+ export declare class CorrelationIdMiddleware implements NestMiddleware {
19
+ private readonly logger;
20
+ constructor(serviceName?: string);
21
+ use(req: HttpRequestLike, res: HttpResponseLike, next: (error?: any) => void): void;
22
+ }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.CorrelationIdMiddleware = exports.CORRELATION_ID_HEADER = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const crypto_1 = require("crypto");
18
+ const context_1 = require("./context");
19
+ const logger_service_1 = require("./logger.service");
20
+ exports.CORRELATION_ID_HEADER = 'x-correlation-id';
21
+ let CorrelationIdMiddleware = class CorrelationIdMiddleware {
22
+ logger;
23
+ constructor(serviceName) {
24
+ this.logger = new logger_service_1.CariffLogger(serviceName || 'api-gateway');
25
+ }
26
+ use(req, res, next) {
27
+ const rawId = req.headers[exports.CORRELATION_ID_HEADER] || req.headers['x-request-id'];
28
+ const correlationId = (Array.isArray(rawId) ? rawId[0] : rawId) || (0, crypto_1.randomUUID)();
29
+ // Propagate in response header
30
+ res.setHeader(exports.CORRELATION_ID_HEADER, correlationId);
31
+ const startTime = Date.now();
32
+ context_1.LoggerContextStore.run({ correlationId }, () => {
33
+ // Log on response finish
34
+ res.on('finish', () => {
35
+ const durationMs = Date.now() - startTime;
36
+ const statusCode = res.statusCode;
37
+ const method = req.method;
38
+ const url = req.originalUrl || req.url;
39
+ const message = `${method} ${url} ${statusCode} - ${durationMs}ms`;
40
+ const metadata = {
41
+ method,
42
+ url,
43
+ statusCode,
44
+ durationMs,
45
+ ip: req.ip || req.socket?.remoteAddress,
46
+ userAgent: req.headers['user-agent'],
47
+ };
48
+ if (statusCode >= 500) {
49
+ this.logger.error({ message, ...metadata }, undefined, 'HTTP');
50
+ }
51
+ else if (statusCode >= 400) {
52
+ this.logger.warn({ message, ...metadata }, 'HTTP');
53
+ }
54
+ else {
55
+ this.logger.log({ message, ...metadata }, 'HTTP');
56
+ }
57
+ });
58
+ next();
59
+ });
60
+ }
61
+ };
62
+ exports.CorrelationIdMiddleware = CorrelationIdMiddleware;
63
+ exports.CorrelationIdMiddleware = CorrelationIdMiddleware = __decorate([
64
+ (0, common_1.Injectable)(),
65
+ __param(0, (0, common_1.Optional)()),
66
+ __metadata("design:paramtypes", [String])
67
+ ], CorrelationIdMiddleware);
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "cariff-logger",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "Unified structured JSON logger and correlation ID tracing module for Cariff microservices",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "prepublishOnly": "npm run build"
10
+ },
11
+ "keywords": [
12
+ "cariff",
13
+ "nestjs",
14
+ "logger",
15
+ "structured-logging",
16
+ "json-logging",
17
+ "correlation-id",
18
+ "loki"
19
+ ],
20
+ "author": "Cariff Marketplace",
21
+ "license": "MIT",
22
+ "peerDependencies": {
23
+ "@nestjs/common": ">=9.0.0",
24
+ "@nestjs/core": ">=9.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@nestjs/common": "^11.0.1",
28
+ "@nestjs/core": "^11.0.1",
29
+ "@types/node": "^22.10.0",
30
+ "typescript": "^5.7.0"
31
+ }
32
+ }
package/src/context.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { AsyncLocalStorage } from 'async_hooks';
2
+
3
+ export interface LogContext {
4
+ correlationId?: string;
5
+ [key: string]: unknown;
6
+ }
7
+
8
+ const asyncLocalStorage = new AsyncLocalStorage<LogContext>();
9
+
10
+ export class LoggerContextStore {
11
+ static run<R>(context: LogContext, fn: () => R): R {
12
+ return asyncLocalStorage.run(context, fn);
13
+ }
14
+
15
+ static getCorrelationId(): string | undefined {
16
+ return asyncLocalStorage.getStore()?.correlationId;
17
+ }
18
+
19
+ static getContext(): LogContext | undefined {
20
+ return asyncLocalStorage.getStore();
21
+ }
22
+
23
+ static set(key: string, value: unknown): void {
24
+ const store = asyncLocalStorage.getStore();
25
+ if (store) {
26
+ store[key] = value;
27
+ }
28
+ }
29
+ }
package/src/fatal.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { CariffLogger } from './logger.service';
2
+
3
+ export function setupGlobalErrorHandlers(serviceName: string): void {
4
+ const logger = new CariffLogger(serviceName);
5
+
6
+ process.on('uncaughtException', (error: Error) => {
7
+ logger.fatal(
8
+ {
9
+ message: `Uncaught Exception: ${error?.message || error}`,
10
+ error: error?.message,
11
+ },
12
+ error?.stack,
13
+ 'Process',
14
+ );
15
+ // Allow stdout/stderr to flush before exiting
16
+ setTimeout(() => {
17
+ process.exit(1);
18
+ }, 100);
19
+ });
20
+
21
+ process.on('unhandledRejection', (reason: unknown) => {
22
+ const error = reason instanceof Error ? reason : new Error(String(reason));
23
+ logger.fatal(
24
+ {
25
+ message: `Unhandled Rejection: ${error.message}`,
26
+ error: error.message,
27
+ },
28
+ error.stack,
29
+ 'Process',
30
+ );
31
+ });
32
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './context';
2
+ export * from './logger.service';
3
+ export * from './middleware';
4
+ export * from './fatal';
@@ -0,0 +1,140 @@
1
+ import { Injectable, LoggerService, Scope } from '@nestjs/common';
2
+ import { LoggerContextStore } from './context';
3
+
4
+ export type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
5
+
6
+ export interface LogEntry {
7
+ timestamp: string;
8
+ level: LogLevel;
9
+ service: string;
10
+ message: string;
11
+ correlationId?: string;
12
+ context?: string;
13
+ stack?: string;
14
+ metadata?: Record<string, unknown>;
15
+ }
16
+
17
+ export interface LoggerOptions {
18
+ serviceName: string;
19
+ isProduction?: boolean;
20
+ }
21
+
22
+ @Injectable({ scope: Scope.DEFAULT })
23
+ export class CariffLogger implements LoggerService {
24
+ private serviceName: string;
25
+ private isProduction: boolean;
26
+
27
+ constructor(serviceNameOrOptions: string | LoggerOptions = 'cariff-service') {
28
+ if (typeof serviceNameOrOptions === 'string') {
29
+ this.serviceName = serviceNameOrOptions;
30
+ this.isProduction = process.env.NODE_ENV === 'production';
31
+ } else {
32
+ this.serviceName = serviceNameOrOptions.serviceName;
33
+ this.isProduction = serviceNameOrOptions.isProduction ?? process.env.NODE_ENV === 'production';
34
+ }
35
+ }
36
+
37
+ setServiceName(name: string): void {
38
+ this.serviceName = name;
39
+ }
40
+
41
+ log(message: any, context?: string): void {
42
+ this.writeLog('INFO', message, context);
43
+ }
44
+
45
+ error(message: any, trace?: string, context?: string): void {
46
+ this.writeLog('ERROR', message, context, trace);
47
+ }
48
+
49
+ warn(message: any, context?: string): void {
50
+ this.writeLog('WARN', message, context);
51
+ }
52
+
53
+ debug?(message: any, context?: string): void {
54
+ if (this.isProduction) {
55
+ return; // Suppress DEBUG in production according to spec
56
+ }
57
+ this.writeLog('DEBUG', message, context);
58
+ }
59
+
60
+ verbose?(message: any, context?: string): void {
61
+ if (this.isProduction) {
62
+ return;
63
+ }
64
+ this.writeLog('DEBUG', message, context);
65
+ }
66
+
67
+ fatal(message: any, trace?: string, context?: string): void {
68
+ this.writeLog('FATAL', message, context, trace);
69
+ }
70
+
71
+ private writeLog(level: LogLevel, message: any, context?: string, stack?: string): void {
72
+ const timestamp = new Date().toISOString();
73
+ const correlationId = LoggerContextStore.getCorrelationId();
74
+
75
+ let textMessage = '';
76
+ let metadata: Record<string, unknown> | undefined;
77
+
78
+ if (typeof message === 'object' && message !== null) {
79
+ if (message instanceof Error) {
80
+ textMessage = message.message;
81
+ stack = stack || message.stack;
82
+ } else {
83
+ const { msg, message: msgField, ...rest } = message;
84
+ textMessage = (msgField || msg || JSON.stringify(message)) as string;
85
+ if (Object.keys(rest).length > 0) {
86
+ metadata = rest;
87
+ }
88
+ }
89
+ } else {
90
+ textMessage = String(message);
91
+ }
92
+
93
+ if (this.isProduction) {
94
+ // Production: strict single-line flat NDJSON to stdout/stderr
95
+ const logEntry: LogEntry = {
96
+ timestamp,
97
+ level,
98
+ service: this.serviceName,
99
+ message: textMessage,
100
+ ...(correlationId ? { correlationId } : {}),
101
+ ...(context ? { context } : {}),
102
+ ...(stack ? { stack } : {}),
103
+ ...(metadata ? { metadata } : {}),
104
+ };
105
+
106
+ const jsonString = JSON.stringify(logEntry) + '\n';
107
+ if (level === 'ERROR' || level === 'FATAL') {
108
+ process.stderr.write(jsonString);
109
+ } else {
110
+ process.stdout.write(jsonString);
111
+ }
112
+ } else {
113
+ // Development: Human-friendly colored terminal output
114
+ const colors: Record<LogLevel, string> = {
115
+ DEBUG: '\x1b[34m', // Blue
116
+ INFO: '\x1b[32m', // Green
117
+ WARN: '\x1b[33m', // Yellow
118
+ ERROR: '\x1b[31m', // Red
119
+ FATAL: '\x1b[35;1m', // Magenta bold
120
+ };
121
+ const reset = '\x1b[0m';
122
+ const gray = '\x1b[90m';
123
+ const cyan = '\x1b[36m';
124
+
125
+ const color = colors[level] || reset;
126
+ const ctx = context ? ` ${cyan}[${context}]${reset}` : '';
127
+ const corr = correlationId ? ` ${gray}(corr: ${correlationId})${reset}` : '';
128
+ const formatted = `${gray}${timestamp}${reset} ${color}${level.padEnd(5)}${reset} ${gray}[${this.serviceName}]${reset}${ctx}${corr} ${textMessage}`;
129
+
130
+ if (level === 'ERROR' || level === 'FATAL') {
131
+ process.stderr.write(formatted + '\n');
132
+ if (stack) {
133
+ process.stderr.write(`${gray}${stack}${reset}\n`);
134
+ }
135
+ } else {
136
+ process.stdout.write(formatted + '\n');
137
+ }
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,70 @@
1
+ import { Injectable, NestMiddleware, Optional } from '@nestjs/common';
2
+ import { randomUUID } from 'crypto';
3
+ import { LoggerContextStore } from './context';
4
+ import { CariffLogger } from './logger.service';
5
+
6
+ export const CORRELATION_ID_HEADER = 'x-correlation-id';
7
+
8
+ export interface HttpRequestLike {
9
+ headers: Record<string, string | string[] | undefined>;
10
+ method: string;
11
+ url: string;
12
+ originalUrl?: string;
13
+ ip?: string;
14
+ socket?: { remoteAddress?: string };
15
+ }
16
+
17
+ export interface HttpResponseLike {
18
+ statusCode: number;
19
+ setHeader(name: string, value: string): void;
20
+ on(event: 'finish' | string, listener: () => void): this;
21
+ }
22
+
23
+ @Injectable()
24
+ export class CorrelationIdMiddleware implements NestMiddleware {
25
+ private readonly logger: CariffLogger;
26
+
27
+ constructor(@Optional() serviceName?: string) {
28
+ this.logger = new CariffLogger(serviceName || 'api-gateway');
29
+ }
30
+
31
+ use(req: HttpRequestLike, res: HttpResponseLike, next: (error?: any) => void): void {
32
+ const rawId = req.headers[CORRELATION_ID_HEADER] || req.headers['x-request-id'];
33
+ const correlationId = (Array.isArray(rawId) ? rawId[0] : rawId) || randomUUID();
34
+
35
+ // Propagate in response header
36
+ res.setHeader(CORRELATION_ID_HEADER, correlationId);
37
+
38
+ const startTime = Date.now();
39
+
40
+ LoggerContextStore.run({ correlationId }, () => {
41
+ // Log on response finish
42
+ res.on('finish', () => {
43
+ const durationMs = Date.now() - startTime;
44
+ const statusCode = res.statusCode;
45
+ const method = req.method;
46
+ const url = req.originalUrl || req.url;
47
+
48
+ const message = `${method} ${url} ${statusCode} - ${durationMs}ms`;
49
+ const metadata = {
50
+ method,
51
+ url,
52
+ statusCode,
53
+ durationMs,
54
+ ip: req.ip || req.socket?.remoteAddress,
55
+ userAgent: req.headers['user-agent'],
56
+ };
57
+
58
+ if (statusCode >= 500) {
59
+ this.logger.error({ message, ...metadata }, undefined, 'HTTP');
60
+ } else if (statusCode >= 400) {
61
+ this.logger.warn({ message, ...metadata }, 'HTTP');
62
+ } else {
63
+ this.logger.log({ message, ...metadata }, 'HTTP');
64
+ }
65
+ });
66
+
67
+ next();
68
+ });
69
+ }
70
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "types": ["node"],
4
+ "module": "commonjs",
5
+ "target": "ES2022",
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "experimentalDecorators": true,
14
+ "emitDecoratorMetadata": true,
15
+ "baseUrl": ".",
16
+ "paths": {
17
+ "*": ["/Users/molix/cariff-backend/api-gateway/node_modules/*"]
18
+ }
19
+ },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["dist"]
22
+ }