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.
- package/.github/workflows/publish.yml +29 -0
- package/README.md +76 -2
- package/dist/context.d.ts +10 -0
- package/dist/context.js +23 -0
- package/dist/fatal.d.ts +1 -0
- package/dist/fatal.js +24 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -0
- package/dist/logger.service.d.ts +29 -0
- package/dist/logger.service.js +128 -0
- package/dist/middleware.d.ts +22 -0
- package/dist/middleware.js +67 -0
- package/package.json +30 -4
- package/src/context.ts +29 -0
- package/src/fatal.ts +32 -0
- package/src/index.ts +4 -0
- package/src/logger.service.ts +140 -0
- package/src/middleware.ts +70 -0
- package/tsconfig.json +22 -0
|
@@ -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
|
-
#
|
|
1
|
+
# cariff-logger
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
}
|
package/dist/context.js
ADDED
|
@@ -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;
|
package/dist/fatal.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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,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
|
+
}
|