@21jumpclick/nestjs-tools 1.0.0
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/21jumpclick-nestjs-tools-1.0.0.tgz +0 -0
- package/README.md +138 -0
- package/dist/main.js +13 -0
- package/lefthook.yml +42 -0
- package/nest-cli.json +10 -0
- package/package.json +54 -0
- package/src/index.ts +3 -0
- package/src/interceptors/index.ts +1 -0
- package/src/interceptors/logging.interceptor.ts +20 -0
- package/src/logging/index.ts +3 -0
- package/src/logging/logging.module.ts +43 -0
- package/src/logging/logging.service.ts +223 -0
- package/src/logging/rmq-client.interceptor.ts +27 -0
- package/src/rmq/index.ts +2 -0
- package/src/rmq/rmq.module.ts +87 -0
- package/src/rmq/rmq.service.ts +25 -0
- package/tsconfig.json +25 -0
- package/webpack.config.js +24 -0
|
Binary file
|
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# @21jumpclick/nestjs-tools
|
|
2
|
+
|
|
3
|
+
Bibliothèque d'outils NestJS pour les projets 21JumpClick, avec un focus sur le logging et les utilitaires RMQ.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @21jumpclick/nestjs-tools
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Utilisation
|
|
12
|
+
|
|
13
|
+
### Logging Module
|
|
14
|
+
|
|
15
|
+
Le module de logging fournit des fonctionnalités de logging avancées avec Pino et des utilitaires pour RabbitMQ.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { Module } from '@nestjs/common';
|
|
19
|
+
import { LoggingModule } from '@21jumpclick/nestjs-tools';
|
|
20
|
+
|
|
21
|
+
@Module({
|
|
22
|
+
imports: [
|
|
23
|
+
LoggingModule, // Module global - automatiquement disponible partout
|
|
24
|
+
],
|
|
25
|
+
})
|
|
26
|
+
export class AppModule {}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Logging Service
|
|
30
|
+
|
|
31
|
+
Utilisez le service de logging pour enregistrer les messages RMQ et créer des clients avec logging intégré.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { Injectable } from '@nestjs/common';
|
|
35
|
+
import { LoggingService } from '@21jumpclick/nestjs-tools';
|
|
36
|
+
|
|
37
|
+
@Injectable()
|
|
38
|
+
export class MyService {
|
|
39
|
+
constructor(private readonly loggingService: LoggingService) {}
|
|
40
|
+
|
|
41
|
+
async someMethod() {
|
|
42
|
+
// Logger un message RMQ entrant
|
|
43
|
+
this.loggingService.logRmqMessage('pattern.name', { data: 'example' });
|
|
44
|
+
|
|
45
|
+
// Logger un message RMQ sortant
|
|
46
|
+
this.loggingService.logRmqOutgoing('pattern.name', { data: 'example' }, {
|
|
47
|
+
durationMs: 150,
|
|
48
|
+
status: 'sent'
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### RMQ Module
|
|
55
|
+
|
|
56
|
+
Module simplifié pour la configuration des clients RabbitMQ.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { Module } from '@nestjs/common';
|
|
60
|
+
import { RmqModule } from '@21jumpclick/nestjs-tools';
|
|
61
|
+
|
|
62
|
+
@Module({
|
|
63
|
+
imports: [
|
|
64
|
+
RmqModule.register({
|
|
65
|
+
name: 'MY_SERVICE',
|
|
66
|
+
urls: ['amqp://localhost:5672'],
|
|
67
|
+
queue: 'my_queue',
|
|
68
|
+
isGlobal: true,
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
71
|
+
})
|
|
72
|
+
export class AppModule {}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Configuration RMQ Avancée
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { Module } from '@nestjs/common';
|
|
79
|
+
import { RmqModule } from '@21jumpclick/nestjs-tools';
|
|
80
|
+
|
|
81
|
+
@Module({
|
|
82
|
+
imports: [
|
|
83
|
+
RmqModule.registerAsync({
|
|
84
|
+
name: 'MY_SERVICE',
|
|
85
|
+
useFactory: (configService: ConfigService) => ({
|
|
86
|
+
urls: configService.get('RMQ_URLS'),
|
|
87
|
+
queue: configService.get('MY_QUEUE'),
|
|
88
|
+
queueOptions: {
|
|
89
|
+
durable: true,
|
|
90
|
+
},
|
|
91
|
+
prefetchCount: 20,
|
|
92
|
+
}),
|
|
93
|
+
inject: [ConfigService],
|
|
94
|
+
}),
|
|
95
|
+
],
|
|
96
|
+
})
|
|
97
|
+
export class AppModule {}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Interceptors
|
|
101
|
+
|
|
102
|
+
Interceptors pour le logging automatique des requêtes HTTP et messages RMQ.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { Module } from '@nestjs/common';
|
|
106
|
+
import { APP_INTERCEPTOR } from '@nestjs/core';
|
|
107
|
+
import { LoggingInterceptor } from '@21jumpclick/nestjs-tools';
|
|
108
|
+
|
|
109
|
+
@Module({
|
|
110
|
+
providers: [
|
|
111
|
+
{
|
|
112
|
+
provide: APP_INTERCEPTOR,
|
|
113
|
+
useClass: LoggingInterceptor,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
})
|
|
117
|
+
export class AppModule {}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Features
|
|
121
|
+
|
|
122
|
+
- **Logging structuré** avec Pino
|
|
123
|
+
- **Support RMQ** avec logging automatique
|
|
124
|
+
- **Masquage des données sensibles** (passwords, tokens, etc.)
|
|
125
|
+
- **Configuration flexible** pour différents environnements
|
|
126
|
+
- **Interceptors** pour le logging automatique
|
|
127
|
+
- **Types TypeScript** complets
|
|
128
|
+
|
|
129
|
+
## Configuration
|
|
130
|
+
|
|
131
|
+
Le module de logging utilise les variables d'environnement suivantes :
|
|
132
|
+
|
|
133
|
+
- `NODE_ENV`: Détermine si le logging est en mode production ou développement
|
|
134
|
+
- `APP_NAME`: Nom de l'application pour les headers RMQ
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
UNLICENSED
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/******/ (() => { // webpackBootstrap
|
|
2
|
+
/******/ "use strict";
|
|
3
|
+
var __webpack_exports__ = {};
|
|
4
|
+
// This entry needs to be wrapped in an IIFE because it uses a non-standard name for the exports (exports).
|
|
5
|
+
(() => {
|
|
6
|
+
var exports = __webpack_exports__;
|
|
7
|
+
|
|
8
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
9
|
+
|
|
10
|
+
})();
|
|
11
|
+
|
|
12
|
+
/******/ })()
|
|
13
|
+
;
|
package/lefthook.yml
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# EXAMPLE USAGE:
|
|
2
|
+
#
|
|
3
|
+
# Refer for explanation to following link:
|
|
4
|
+
# https://lefthook.dev/configuration/
|
|
5
|
+
#
|
|
6
|
+
# pre-push:
|
|
7
|
+
# jobs:
|
|
8
|
+
# - name: packages audit
|
|
9
|
+
# tags:
|
|
10
|
+
# - frontend
|
|
11
|
+
# - security
|
|
12
|
+
# run: yarn audit
|
|
13
|
+
#
|
|
14
|
+
# - name: gems audit
|
|
15
|
+
# tags:
|
|
16
|
+
# - backend
|
|
17
|
+
# - security
|
|
18
|
+
# run: bundle audit
|
|
19
|
+
#
|
|
20
|
+
# pre-commit:
|
|
21
|
+
# parallel: true
|
|
22
|
+
# jobs:
|
|
23
|
+
# - run: yarn eslint {staged_files}
|
|
24
|
+
# glob: "*.{js,ts,jsx,tsx}"
|
|
25
|
+
#
|
|
26
|
+
# - name: rubocop
|
|
27
|
+
# glob: "*.rb"
|
|
28
|
+
# exclude:
|
|
29
|
+
# - config/application.rb
|
|
30
|
+
# - config/routes.rb
|
|
31
|
+
# run: bundle exec rubocop --force-exclusion {all_files}
|
|
32
|
+
#
|
|
33
|
+
# - name: govet
|
|
34
|
+
# files: git ls-files -m
|
|
35
|
+
# glob: "*.go"
|
|
36
|
+
# run: go vet {files}
|
|
37
|
+
#
|
|
38
|
+
# - script: "hello.js"
|
|
39
|
+
# runner: node
|
|
40
|
+
#
|
|
41
|
+
# - script: "hello.go"
|
|
42
|
+
# runner: go run
|
package/nest-cli.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@21jumpclick/nestjs-tools",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "NestJS tools library with logging and other utilities",
|
|
5
|
+
"author": "21JumpClick",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "nest build",
|
|
14
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
15
|
+
"start": "nest start",
|
|
16
|
+
"start:dev": "nest build --webpack --webpackPath webpack-hmr.config.js --watch",
|
|
17
|
+
"start:debug": "nest start --debug --watch",
|
|
18
|
+
"start:prod": "node dist/main",
|
|
19
|
+
"lint": "npx @biomejs/biome check",
|
|
20
|
+
"lint:fix": "npx @biomejs/biome check --fix",
|
|
21
|
+
"prepublish": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@nestjs/common": "^11.0.7",
|
|
25
|
+
"@nestjs/core": "^11.0.7",
|
|
26
|
+
"@nestjs/microservices": "^11.0.7",
|
|
27
|
+
"nestjs-pino": "^4.5.0",
|
|
28
|
+
"pino-http": "^11.0.0",
|
|
29
|
+
"pino-pretty": "^13.1.3",
|
|
30
|
+
"reflect-metadata": "^0.2.2",
|
|
31
|
+
"rxjs": "^7.8.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@biomejs/biome": "^1.9.4",
|
|
35
|
+
"@nestjs/cli": "^11.0.2",
|
|
36
|
+
"@nestjs/schematics": "^11.0.0",
|
|
37
|
+
"@swc/cli": "^0.6.0",
|
|
38
|
+
"@swc/core": "^1.10.12",
|
|
39
|
+
"@types/node": "^22.13.0",
|
|
40
|
+
"lefthook": "^1.10.10",
|
|
41
|
+
"run-script-webpack-plugin": "^0.2.0",
|
|
42
|
+
"source-map-support": "^0.5.21",
|
|
43
|
+
"ts-loader": "^9.5.2",
|
|
44
|
+
"ts-node": "^10.9.2",
|
|
45
|
+
"tsconfig-paths": "^4.2.0",
|
|
46
|
+
"typescript": "^5.7.3",
|
|
47
|
+
"webpack": "^5.97.1",
|
|
48
|
+
"webpack-node-externals": "^3.0.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
|
52
|
+
"@nestjs/core": "^10.0.0 || ^11.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './logging.interceptor';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from "@nestjs/common";
|
|
2
|
+
import { Observable } from "rxjs";
|
|
3
|
+
import { tap } from "rxjs/operators";
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class LoggingInterceptor implements NestInterceptor {
|
|
7
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
8
|
+
const startedAt = Date.now();
|
|
9
|
+
const request = context.switchToHttp().getRequest();
|
|
10
|
+
|
|
11
|
+
console.log(`Incoming request: ${request.method} ${request.url}`);
|
|
12
|
+
|
|
13
|
+
return next.handle().pipe(
|
|
14
|
+
tap(() => {
|
|
15
|
+
const durationMs = Date.now() - startedAt;
|
|
16
|
+
console.log(`Request completed in ${durationMs}ms`);
|
|
17
|
+
})
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Global, Module } from "@nestjs/common";
|
|
2
|
+
import { LoggerModule } from "nestjs-pino";
|
|
3
|
+
import { LoggingService } from "./logging.service";
|
|
4
|
+
import { RmqClientInterceptor } from "./rmq-client.interceptor";
|
|
5
|
+
|
|
6
|
+
export interface LoggingModuleOptions {
|
|
7
|
+
pinoHttp?: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@Global()
|
|
11
|
+
@Module({
|
|
12
|
+
imports: [
|
|
13
|
+
LoggerModule.forRoot({
|
|
14
|
+
pinoHttp: {
|
|
15
|
+
formatters: {
|
|
16
|
+
level: (label) => ({ level: label.toUpperCase() }),
|
|
17
|
+
},
|
|
18
|
+
genReqId: (req) => req.headers["x-request-id"] || crypto.randomUUID(),
|
|
19
|
+
transport:
|
|
20
|
+
process.env.NODE_ENV !== "production"
|
|
21
|
+
? {
|
|
22
|
+
target: "pino-pretty",
|
|
23
|
+
options: {
|
|
24
|
+
colorize: true,
|
|
25
|
+
levelFirst: true,
|
|
26
|
+
translateTime: "HH:MM:ss",
|
|
27
|
+
messageFormat: "{context}: {msg}",
|
|
28
|
+
ignore: "pid,hostname",
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
: undefined,
|
|
32
|
+
redact: [
|
|
33
|
+
"req.headers.authorization",
|
|
34
|
+
"req.body.password",
|
|
35
|
+
"res.headers.cookie",
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
providers: [LoggingService, RmqClientInterceptor],
|
|
41
|
+
exports: [LoggingService, LoggerModule],
|
|
42
|
+
})
|
|
43
|
+
export class LoggingModule {}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { Injectable, Logger } from "@nestjs/common";
|
|
2
|
+
import { ClientProxy, MicroserviceOptions, Transport } from "@nestjs/microservices";
|
|
3
|
+
import { catchError, finalize, Observable, throwError } from "rxjs";
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class LoggingService {
|
|
7
|
+
private readonly rmqLogger = new Logger("RMQ-Traffic");
|
|
8
|
+
|
|
9
|
+
logRmqMessage(pattern: string, data: any) {
|
|
10
|
+
const logData = {
|
|
11
|
+
transport: "RMQ_IN",
|
|
12
|
+
pattern,
|
|
13
|
+
data: this.sanitizeData(data),
|
|
14
|
+
duration: "0ms",
|
|
15
|
+
status: "received",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
this.rmqLogger.log(logData);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
logRmqOutgoing(
|
|
22
|
+
pattern: string,
|
|
23
|
+
data: any,
|
|
24
|
+
options?: {
|
|
25
|
+
durationMs?: number;
|
|
26
|
+
status?: "sent" | "error";
|
|
27
|
+
error?: unknown;
|
|
28
|
+
},
|
|
29
|
+
) {
|
|
30
|
+
const logData = {
|
|
31
|
+
transport: "RMQ_OUT",
|
|
32
|
+
pattern,
|
|
33
|
+
data: this.sanitizeData(data),
|
|
34
|
+
duration:
|
|
35
|
+
typeof options?.durationMs === "number"
|
|
36
|
+
? `${options.durationMs}ms`
|
|
37
|
+
: "0ms",
|
|
38
|
+
status: options?.status ?? "sent",
|
|
39
|
+
...(options?.error ? { error: this.sanitizeData(options.error) } : {}),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
this.rmqLogger.log(logData);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
createLoggedClientProxy(client: ClientProxy, clientName: string): ClientProxy {
|
|
46
|
+
const logging = this;
|
|
47
|
+
|
|
48
|
+
const wrapper: Partial<ClientProxy> = {
|
|
49
|
+
send<TResult = any, TInput = any>(pattern: any, data: TInput): Observable<TResult> {
|
|
50
|
+
const startedAt = Date.now();
|
|
51
|
+
let hadError = false;
|
|
52
|
+
|
|
53
|
+
return client.send<TResult, TInput>(pattern, data).pipe(
|
|
54
|
+
catchError((err) => {
|
|
55
|
+
hadError = true;
|
|
56
|
+
const durationMs = Date.now() - startedAt;
|
|
57
|
+
logging.logRmqOutgoing(`${clientName}:${logging.patternToString(pattern)}`, data, {
|
|
58
|
+
durationMs,
|
|
59
|
+
status: "error",
|
|
60
|
+
error: err,
|
|
61
|
+
});
|
|
62
|
+
return throwError(() => err);
|
|
63
|
+
}),
|
|
64
|
+
finalize(() => {
|
|
65
|
+
const durationMs = Date.now() - startedAt;
|
|
66
|
+
if (!hadError) {
|
|
67
|
+
logging.logRmqOutgoing(`${clientName}:${logging.patternToString(pattern)}`, data, {
|
|
68
|
+
durationMs,
|
|
69
|
+
status: "sent",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
emit<TResult = any, TInput = any>(pattern: any, data: TInput): Observable<TResult> {
|
|
77
|
+
const startedAt = Date.now();
|
|
78
|
+
let hadError = false;
|
|
79
|
+
|
|
80
|
+
logging.logRmqOutgoing(`${clientName}:${logging.patternToString(pattern)}`, data, {
|
|
81
|
+
status: "sent",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return client.emit<TResult, TInput>(pattern, data).pipe(
|
|
85
|
+
catchError((err) => {
|
|
86
|
+
hadError = true;
|
|
87
|
+
const durationMs = Date.now() - startedAt;
|
|
88
|
+
logging.logRmqOutgoing(`${clientName}:${logging.patternToString(pattern)}`, data, {
|
|
89
|
+
durationMs,
|
|
90
|
+
status: "error",
|
|
91
|
+
error: err,
|
|
92
|
+
});
|
|
93
|
+
return throwError(() => err);
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
connect(): Promise<any> {
|
|
99
|
+
return client.connect();
|
|
100
|
+
},
|
|
101
|
+
close(): any {
|
|
102
|
+
return client.close();
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return new Proxy(client as any, {
|
|
107
|
+
get(target, prop, receiver) {
|
|
108
|
+
if (prop in wrapper) {
|
|
109
|
+
const v = (wrapper as any)[prop];
|
|
110
|
+
return typeof v === "function" ? v.bind(receiver) : v;
|
|
111
|
+
}
|
|
112
|
+
const value = Reflect.get(target, prop, receiver);
|
|
113
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
114
|
+
},
|
|
115
|
+
}) as ClientProxy;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
createRmqMicroservice(options: {
|
|
119
|
+
urls: string[];
|
|
120
|
+
queue: string;
|
|
121
|
+
queueOptions?: { durable?: boolean };
|
|
122
|
+
customDeserializer?: boolean;
|
|
123
|
+
}): MicroserviceOptions {
|
|
124
|
+
return {
|
|
125
|
+
transport: Transport.RMQ,
|
|
126
|
+
options: {
|
|
127
|
+
urls: options.urls,
|
|
128
|
+
queue: options.queue,
|
|
129
|
+
queueOptions: {
|
|
130
|
+
durable: true,
|
|
131
|
+
...options.queueOptions,
|
|
132
|
+
},
|
|
133
|
+
...(options.customDeserializer && {
|
|
134
|
+
deserializer: {
|
|
135
|
+
deserialize: (value: any) => {
|
|
136
|
+
const pattern =
|
|
137
|
+
typeof value === "object" && !value.pattern
|
|
138
|
+
? options.queue
|
|
139
|
+
: value?.pattern || "unknown";
|
|
140
|
+
const data = value?.data || value;
|
|
141
|
+
|
|
142
|
+
this.logRmqMessage(pattern, data);
|
|
143
|
+
|
|
144
|
+
if (typeof value === "object" && !value.pattern) {
|
|
145
|
+
return { pattern: options.queue, data: value };
|
|
146
|
+
}
|
|
147
|
+
return value;
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
createRmqClient(name: string, urls: string[], queue: string): any {
|
|
156
|
+
return {
|
|
157
|
+
name,
|
|
158
|
+
transport: Transport.RMQ,
|
|
159
|
+
options: {
|
|
160
|
+
urls,
|
|
161
|
+
queue,
|
|
162
|
+
queueOptions: {
|
|
163
|
+
durable: true,
|
|
164
|
+
},
|
|
165
|
+
headers: {
|
|
166
|
+
origin: process.env.APP_NAME || "unknown",
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
serializer: {
|
|
170
|
+
serialize: (value: any) => {
|
|
171
|
+
this.logRmqOutgoing(queue, value);
|
|
172
|
+
return value;
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private patternToString(pattern: any): string {
|
|
179
|
+
if (typeof pattern === "string") return pattern;
|
|
180
|
+
if (typeof pattern === "number") return pattern.toString();
|
|
181
|
+
try {
|
|
182
|
+
return JSON.stringify(pattern);
|
|
183
|
+
} catch {
|
|
184
|
+
return "[unserializable-pattern]";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private sanitizeData(data: any): any {
|
|
189
|
+
if (!data) return null;
|
|
190
|
+
|
|
191
|
+
const sanitized = JSON.parse(JSON.stringify(data));
|
|
192
|
+
const sensitiveKeys = [
|
|
193
|
+
"password",
|
|
194
|
+
"token",
|
|
195
|
+
"secret",
|
|
196
|
+
"key",
|
|
197
|
+
"authorization",
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const maskSensitive = (obj: any): any => {
|
|
201
|
+
if (typeof obj !== "object" || obj === null) return obj;
|
|
202
|
+
|
|
203
|
+
if (Array.isArray(obj)) {
|
|
204
|
+
return obj.map(maskSensitive);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const result: any = {};
|
|
208
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
209
|
+
const lowerKey = key.toLowerCase();
|
|
210
|
+
if (sensitiveKeys.some((sensitive) => lowerKey.includes(sensitive))) {
|
|
211
|
+
result[key] = "[MASKED]";
|
|
212
|
+
} else if (typeof value === "object") {
|
|
213
|
+
result[key] = maskSensitive(value);
|
|
214
|
+
} else {
|
|
215
|
+
result[key] = value;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return maskSensitive(sanitized);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from "@nestjs/common";
|
|
2
|
+
import { Observable } from "rxjs";
|
|
3
|
+
import { tap } from "rxjs/operators";
|
|
4
|
+
import { LoggingService } from "../logging/logging.service";
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class RmqClientInterceptor implements NestInterceptor {
|
|
8
|
+
constructor(private readonly loggingService: LoggingService) {}
|
|
9
|
+
|
|
10
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
11
|
+
const startedAt = Date.now();
|
|
12
|
+
const pattern = context.getPattern();
|
|
13
|
+
const data = context.getArgByIndex(0);
|
|
14
|
+
|
|
15
|
+
this.loggingService.logRmqMessage(
|
|
16
|
+
typeof pattern === "string" ? pattern : JSON.stringify(pattern),
|
|
17
|
+
data
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return next.handle().pipe(
|
|
21
|
+
tap(() => {
|
|
22
|
+
const durationMs = Date.now() - startedAt;
|
|
23
|
+
console.log(`RMQ message processed in ${durationMs}ms`);
|
|
24
|
+
})
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/rmq/index.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common";
|
|
2
|
+
import { ClientsModule, Transport } from "@nestjs/microservices";
|
|
3
|
+
import { LoggingService } from "../logging/logging.service";
|
|
4
|
+
|
|
5
|
+
export interface RmqModuleOptions {
|
|
6
|
+
name: string;
|
|
7
|
+
urls: string[];
|
|
8
|
+
queue: string;
|
|
9
|
+
queueOptions?: { durable?: boolean };
|
|
10
|
+
noAck?: boolean;
|
|
11
|
+
prefetchCount?: number;
|
|
12
|
+
isGlobal?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Module({})
|
|
16
|
+
export class RmqModule {
|
|
17
|
+
static register(options: RmqModuleOptions) {
|
|
18
|
+
const moduleOptions = {
|
|
19
|
+
name: options.name,
|
|
20
|
+
transport: Transport.RMQ,
|
|
21
|
+
options: {
|
|
22
|
+
urls: options.urls,
|
|
23
|
+
queue: options.queue,
|
|
24
|
+
queueOptions: {
|
|
25
|
+
durable: true,
|
|
26
|
+
...options.queueOptions,
|
|
27
|
+
},
|
|
28
|
+
noAck: options.noAck ?? false,
|
|
29
|
+
prefetchCount: options.prefetchCount ?? 10,
|
|
30
|
+
headers: {
|
|
31
|
+
origin: process.env.APP_NAME || "unknown",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const module = ClientsModule.register([moduleOptions]);
|
|
37
|
+
|
|
38
|
+
if (options.isGlobal) {
|
|
39
|
+
return {
|
|
40
|
+
...module,
|
|
41
|
+
global: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return module;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static registerAsync(options: RmqModuleOptions & {
|
|
49
|
+
useFactory: (...args: any[]) => Promise<RmqModuleOptions> | RmqModuleOptions;
|
|
50
|
+
inject?: any[];
|
|
51
|
+
}) {
|
|
52
|
+
const asyncOptions = {
|
|
53
|
+
name: options.name,
|
|
54
|
+
useFactory: async (...args: any[]) => {
|
|
55
|
+
const factoryOptions = await options.useFactory(...args);
|
|
56
|
+
return {
|
|
57
|
+
transport: Transport.RMQ,
|
|
58
|
+
options: {
|
|
59
|
+
urls: factoryOptions.urls,
|
|
60
|
+
queue: factoryOptions.queue,
|
|
61
|
+
queueOptions: {
|
|
62
|
+
durable: true,
|
|
63
|
+
...factoryOptions.queueOptions,
|
|
64
|
+
},
|
|
65
|
+
noAck: factoryOptions.noAck ?? false,
|
|
66
|
+
prefetchCount: factoryOptions.prefetchCount ?? 10,
|
|
67
|
+
headers: {
|
|
68
|
+
origin: process.env.APP_NAME || "unknown",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
inject: options.inject || [],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const module = ClientsModule.registerAsync([asyncOptions]);
|
|
77
|
+
|
|
78
|
+
if (options.isGlobal) {
|
|
79
|
+
return {
|
|
80
|
+
...module,
|
|
81
|
+
global: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return module;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Injectable } from "@nestjs/common";
|
|
2
|
+
import { ClientProxy } from "@nestjs/microservices";
|
|
3
|
+
import { LoggingService } from "../logging/logging.service";
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class RmqService {
|
|
7
|
+
constructor(private readonly loggingService: LoggingService) {}
|
|
8
|
+
|
|
9
|
+
createLoggedClientProxy(client: ClientProxy, clientName: string): ClientProxy {
|
|
10
|
+
return this.loggingService.createLoggedClientProxy(client, clientName);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
createRmqClient(name: string, urls: string[], queue: string): any {
|
|
14
|
+
return this.loggingService.createRmqClient(name, urls, queue);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
createRmqMicroservice(options: {
|
|
18
|
+
urls: string[];
|
|
19
|
+
queue: string;
|
|
20
|
+
queueOptions?: { durable?: boolean };
|
|
21
|
+
customDeserializer?: boolean;
|
|
22
|
+
}) {
|
|
23
|
+
return this.loggingService.createRmqMicroservice(options);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "commonjs",
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"removeComments": true,
|
|
6
|
+
"emitDecoratorMetadata": true,
|
|
7
|
+
"experimentalDecorators": true,
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"target": "ES2021",
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"baseUrl": "./",
|
|
13
|
+
"incremental": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"strictNullChecks": false,
|
|
16
|
+
"noImplicitAny": false,
|
|
17
|
+
"strictBindCallApply": false,
|
|
18
|
+
"forceConsistentCasingInFileNames": false,
|
|
19
|
+
"noFallthroughCasesInSwitch": false,
|
|
20
|
+
"paths": {
|
|
21
|
+
"@/*": ["src/*"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"exclude": ["node_modules", "dist"]
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
module.exports = (options, webpack) => {
|
|
4
|
+
const lazyImports = [
|
|
5
|
+
'@nestjs/microservices',
|
|
6
|
+
'@nestjs/websockets',
|
|
7
|
+
'cache-manager',
|
|
8
|
+
'class-validator',
|
|
9
|
+
'class-transformer',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
...options,
|
|
14
|
+
externals: [],
|
|
15
|
+
plugins: [
|
|
16
|
+
...options.plugins,
|
|
17
|
+
new webpack.IgnorePlugin({
|
|
18
|
+
checkResource(resource) {
|
|
19
|
+
return lazyImports.includes(resource);
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
};
|