nest-live-flow 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/LICENSE +21 -0
- package/README.md +237 -0
- package/dist/core.d.ts +14 -0
- package/dist/core.js +99 -0
- package/dist/discovery.service.d.ts +56 -0
- package/dist/discovery.service.js +234 -0
- package/dist/exporter.d.ts +8 -0
- package/dist/exporter.js +52 -0
- package/dist/flow.decorator.d.ts +5 -0
- package/dist/flow.decorator.js +22 -0
- package/dist/flow.interceptor.d.ts +12 -0
- package/dist/flow.interceptor.js +65 -0
- package/dist/flow.module.d.ts +2 -0
- package/dist/flow.module.js +30 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +23 -0
- package/dist/service-instrumentor.d.ts +11 -0
- package/dist/service-instrumentor.js +93 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Nest Live Flow
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Nest Live Flow
|
|
2
|
+
|
|
3
|
+
A powerful NestJS library for real-time application architecture visualization and flow tracing with Total.js Flow integration. Monitor your application's dependency graph, trace execution flows, and visualize your architecture in real-time.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔍 **Automatic Architecture Discovery**: Automatically scans and maps your NestJS application's modules, controllers, and services
|
|
8
|
+
- 📊 **Real-time Flow Tracing**: Traces method calls and data flow through your application using OpenTelemetry
|
|
9
|
+
- 🎨 **Visual Architecture Dashboard**: Integrates with Total.js Flow for beautiful architecture visualization
|
|
10
|
+
- 🚀 **Zero Configuration**: Works out of the box with minimal setup
|
|
11
|
+
- 🔧 **Flexible Integration**: Supports both decorator-based and programmatic tracing
|
|
12
|
+
- 📈 **Performance Monitoring**: Track execution times and identify bottlenecks
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install nest-live-flow
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### 1. Initialize in main.ts
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { NestFactory } from '@nestjs/core';
|
|
26
|
+
import { initFlow } from 'nest-live-flow';
|
|
27
|
+
import { AppModule } from './app.module';
|
|
28
|
+
|
|
29
|
+
async function bootstrap() {
|
|
30
|
+
// Initialize flow tracing before creating the app
|
|
31
|
+
initFlow('http://localhost:8000'); // Your Total.js Flow URL
|
|
32
|
+
|
|
33
|
+
const app = await NestFactory.create(AppModule);
|
|
34
|
+
await app.listen(3000);
|
|
35
|
+
}
|
|
36
|
+
bootstrap();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Import the Module
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { Module } from '@nestjs/common';
|
|
43
|
+
import { NestLiveFlowModule } from 'nest-live-flow';
|
|
44
|
+
|
|
45
|
+
@Module({
|
|
46
|
+
imports: [
|
|
47
|
+
NestLiveFlowModule, // Add this to your root module
|
|
48
|
+
// ... your other modules
|
|
49
|
+
],
|
|
50
|
+
})
|
|
51
|
+
export class AppModule {}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3. Use Flow Tracing (Optional)
|
|
55
|
+
|
|
56
|
+
#### Decorator-based Tracing
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { Injectable } from '@nestjs/common';
|
|
60
|
+
import { FlowTrace } from 'nest-live-flow';
|
|
61
|
+
|
|
62
|
+
@Injectable()
|
|
63
|
+
export class UserService {
|
|
64
|
+
@FlowTrace('Get User by ID')
|
|
65
|
+
async getUserById(id: string) {
|
|
66
|
+
// Your service logic
|
|
67
|
+
return { id, name: 'John Doe' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@FlowTrace('Create New User')
|
|
71
|
+
async createUser(userData: any) {
|
|
72
|
+
// Your service logic
|
|
73
|
+
return { id: '123', ...userData };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### Programmatic Tracing
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { Injectable } from '@nestjs/common';
|
|
82
|
+
import { flowTrace } from 'nest-live-flow';
|
|
83
|
+
|
|
84
|
+
@Injectable()
|
|
85
|
+
export class ProductService {
|
|
86
|
+
async getProducts() {
|
|
87
|
+
return flowTrace('Fetch All Products', async () => {
|
|
88
|
+
// Your service logic
|
|
89
|
+
return await this.database.findAll();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
### Environment Variables
|
|
98
|
+
|
|
99
|
+
Create a `.env` file in your project root:
|
|
100
|
+
|
|
101
|
+
```env
|
|
102
|
+
TOTALJS_FLOW_URL=http://localhost:8000
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Advanced Configuration
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { initFlow } from 'nest-live-flow';
|
|
109
|
+
|
|
110
|
+
// Initialize with custom options
|
|
111
|
+
initFlow('http://localhost:8000', {
|
|
112
|
+
serviceName: 'my-nestjs-app',
|
|
113
|
+
enableConsoleLogging: true,
|
|
114
|
+
batchTimeout: 2000,
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Architecture Visualization
|
|
119
|
+
|
|
120
|
+
Once configured, your application architecture will be automatically:
|
|
121
|
+
|
|
122
|
+
1. **Scanned**: All modules, controllers, and services are discovered
|
|
123
|
+
2. **Mapped**: Dependencies and relationships are identified
|
|
124
|
+
3. **Visualized**: Real-time architecture graph in Total.js Flow
|
|
125
|
+
4. **Traced**: Method calls and data flow are tracked and displayed
|
|
126
|
+
|
|
127
|
+
## API Reference
|
|
128
|
+
|
|
129
|
+
### Core Functions
|
|
130
|
+
|
|
131
|
+
#### `initFlow(totalJsUrl: string)`
|
|
132
|
+
Initializes the flow tracing system. Must be called before `NestFactory.create()`.
|
|
133
|
+
|
|
134
|
+
#### `flowTrace<T>(name: string, fn: () => T | Promise<T>): Promise<T>`
|
|
135
|
+
Programmatically traces a function execution.
|
|
136
|
+
|
|
137
|
+
### Decorators
|
|
138
|
+
|
|
139
|
+
#### `@FlowTrace(name?: string)`
|
|
140
|
+
Decorator for automatic method tracing.
|
|
141
|
+
|
|
142
|
+
### Modules
|
|
143
|
+
|
|
144
|
+
#### `NestLiveFlowModule`
|
|
145
|
+
The main module to import in your application.
|
|
146
|
+
|
|
147
|
+
### Services
|
|
148
|
+
|
|
149
|
+
#### `FlowScannerService`
|
|
150
|
+
Service that handles architecture discovery and scanning.
|
|
151
|
+
|
|
152
|
+
#### `ServiceInstrumentor`
|
|
153
|
+
Service for manual instrumentation of existing services.
|
|
154
|
+
|
|
155
|
+
## Integration with Total.js Flow
|
|
156
|
+
|
|
157
|
+
This library is designed to work with [Total.js Flow](https://www.totaljs.com/flow/).
|
|
158
|
+
|
|
159
|
+
### Setting up Total.js Flow
|
|
160
|
+
|
|
161
|
+
1. Install Total.js Flow:
|
|
162
|
+
```bash
|
|
163
|
+
npm install -g @totaljs/flow
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
2. Start Total.js Flow:
|
|
167
|
+
```bash
|
|
168
|
+
flow
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
3. Access the dashboard at `http://localhost:8000`
|
|
172
|
+
|
|
173
|
+
## Examples
|
|
174
|
+
|
|
175
|
+
### Basic NestJS Application
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// main.ts
|
|
179
|
+
import { NestFactory } from '@nestjs/core';
|
|
180
|
+
import { initFlow } from 'nest-live-flow';
|
|
181
|
+
import { AppModule } from './app.module';
|
|
182
|
+
|
|
183
|
+
async function bootstrap() {
|
|
184
|
+
initFlow('http://localhost:8000');
|
|
185
|
+
const app = await NestFactory.create(AppModule);
|
|
186
|
+
await app.listen(3000);
|
|
187
|
+
}
|
|
188
|
+
bootstrap();
|
|
189
|
+
|
|
190
|
+
// app.module.ts
|
|
191
|
+
import { Module } from '@nestjs/common';
|
|
192
|
+
import { NestLiveFlowModule } from 'nest-live-flow';
|
|
193
|
+
import { UsersModule } from './users/users.module';
|
|
194
|
+
|
|
195
|
+
@Module({
|
|
196
|
+
imports: [
|
|
197
|
+
NestLiveFlowModule,
|
|
198
|
+
UsersModule,
|
|
199
|
+
],
|
|
200
|
+
})
|
|
201
|
+
export class AppModule {}
|
|
202
|
+
|
|
203
|
+
// users/users.service.ts
|
|
204
|
+
import { Injectable } from '@nestjs/common';
|
|
205
|
+
import { FlowTrace } from 'nest-live-flow';
|
|
206
|
+
|
|
207
|
+
@Injectable()
|
|
208
|
+
export class UsersService {
|
|
209
|
+
@FlowTrace()
|
|
210
|
+
findAll() {
|
|
211
|
+
return [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@FlowTrace('Create User Operation')
|
|
215
|
+
create(user: any) {
|
|
216
|
+
return { id: Date.now(), ...user };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Requirements
|
|
222
|
+
|
|
223
|
+
- Node.js 18+
|
|
224
|
+
- NestJS 10+ or 11+
|
|
225
|
+
- Total.js Flow (for visualization)
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
MIT
|
|
230
|
+
|
|
231
|
+
## Contributing
|
|
232
|
+
|
|
233
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
234
|
+
|
|
235
|
+
## Support
|
|
236
|
+
|
|
237
|
+
If you encounter any issues or have questions, please file an issue on our GitHub repository.
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initializes the tracing engine. Call this in main.ts before NestFactory.
|
|
3
|
+
* @param totalJsUrl - The URL of the Total.js backend
|
|
4
|
+
* @throws Error if initialization fails
|
|
5
|
+
*/
|
|
6
|
+
export declare function initFlow(totalJsUrl: string): void;
|
|
7
|
+
/**
|
|
8
|
+
* Wraps any function to trace its execution and data flow.
|
|
9
|
+
* @param name - The display name for the trace span
|
|
10
|
+
* @param fn - The function to trace (can be sync or async)
|
|
11
|
+
* @returns Promise resolving to the function result
|
|
12
|
+
* @throws Error if tracing is not initialized
|
|
13
|
+
*/
|
|
14
|
+
export declare function flowTrace<T>(name: string, fn: () => T | Promise<T>): Promise<T>;
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initFlow = initFlow;
|
|
4
|
+
exports.flowTrace = flowTrace;
|
|
5
|
+
const api_1 = require("@opentelemetry/api");
|
|
6
|
+
const resources_1 = require("@opentelemetry/resources");
|
|
7
|
+
const sdk_trace_node_1 = require("@opentelemetry/sdk-trace-node");
|
|
8
|
+
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
|
|
9
|
+
const exporter_1 = require("./exporter");
|
|
10
|
+
let tracer = null;
|
|
11
|
+
/**
|
|
12
|
+
* Initializes the tracing engine. Call this in main.ts before NestFactory.
|
|
13
|
+
* @param totalJsUrl - The URL of the Total.js backend
|
|
14
|
+
* @throws Error if initialization fails
|
|
15
|
+
*/
|
|
16
|
+
function initFlow(totalJsUrl) {
|
|
17
|
+
if (!totalJsUrl) {
|
|
18
|
+
throw new Error('Total.js URL is required for flow initialization');
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const provider = new sdk_trace_node_1.NodeTracerProvider({
|
|
22
|
+
resource: (0, resources_1.resourceFromAttributes)({
|
|
23
|
+
[semantic_conventions_1.ATTR_SERVICE_NAME]: 'nestjs-app',
|
|
24
|
+
}),
|
|
25
|
+
spanProcessors: [
|
|
26
|
+
new sdk_trace_node_1.SimpleSpanProcessor(new exporter_1.TotalJsExporter(totalJsUrl)),
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
provider.register();
|
|
30
|
+
tracer = api_1.trace.getTracer('nest-live-flow');
|
|
31
|
+
console.log('✨ Nest-Live-Flow: OpenTelemetry Engine Initialized');
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error('❌ Failed to initialize Flow tracing:', error);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Wraps any function to trace its execution and data flow.
|
|
40
|
+
* @param name - The display name for the trace span
|
|
41
|
+
* @param fn - The function to trace (can be sync or async)
|
|
42
|
+
* @returns Promise resolving to the function result
|
|
43
|
+
* @throws Error if tracing is not initialized
|
|
44
|
+
*/
|
|
45
|
+
async function flowTrace(name, fn) {
|
|
46
|
+
console.log(`🔍 flowTrace called for: ${name}`);
|
|
47
|
+
if (!tracer) {
|
|
48
|
+
console.error('❌ Flow tracing not initialized. Call initFlow() first.');
|
|
49
|
+
throw new Error('Flow tracing not initialized. Call initFlow() first.');
|
|
50
|
+
}
|
|
51
|
+
if (!name?.trim()) {
|
|
52
|
+
console.error('❌ Trace name cannot be empty');
|
|
53
|
+
throw new Error('Trace name cannot be empty');
|
|
54
|
+
}
|
|
55
|
+
console.log(`🟢 Starting active span for: ${name}`);
|
|
56
|
+
return tracer.startActiveSpan(name, async (span) => {
|
|
57
|
+
console.log(`📍 Inside span for: ${name}`);
|
|
58
|
+
const startTime = Date.now();
|
|
59
|
+
try {
|
|
60
|
+
console.log(`⚙️ Executing function for: ${name}`);
|
|
61
|
+
const result = await fn();
|
|
62
|
+
console.log(`✅ Function completed for: ${name}`);
|
|
63
|
+
// Safely serialize result for data flow visualization
|
|
64
|
+
try {
|
|
65
|
+
const serializedResult = typeof result === 'object' && result !== null
|
|
66
|
+
? JSON.stringify(result)
|
|
67
|
+
: String(result);
|
|
68
|
+
// Limit attribute size to prevent memory issues
|
|
69
|
+
const maxLength = 1000;
|
|
70
|
+
const truncatedResult = serializedResult.length > maxLength
|
|
71
|
+
? `${serializedResult.substring(0, maxLength)}...`
|
|
72
|
+
: serializedResult;
|
|
73
|
+
span.setAttribute('flow.data', truncatedResult);
|
|
74
|
+
console.log(`📊 Set span data for: ${name}`);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
span.setAttribute('flow.data', '[Unable to serialize result]');
|
|
78
|
+
console.log(`⚠️ Unable to serialize result for: ${name}`);
|
|
79
|
+
}
|
|
80
|
+
span.setAttribute('flow.duration', Date.now() - startTime);
|
|
81
|
+
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
|
82
|
+
console.log(`🏆 Span completed successfully for: ${name}`);
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error(`💥 Error in span for ${name}:`, error);
|
|
87
|
+
span.setStatus({
|
|
88
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
89
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
90
|
+
});
|
|
91
|
+
span.setAttribute('flow.error', error instanceof Error ? error.message : String(error));
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
console.log(`🔚 Ending span for: ${name}`);
|
|
96
|
+
span.end();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { OnApplicationBootstrap } from '@nestjs/common';
|
|
2
|
+
import { ModulesContainer } from '@nestjs/core';
|
|
3
|
+
import 'reflect-metadata';
|
|
4
|
+
export declare class FlowScannerService implements OnApplicationBootstrap {
|
|
5
|
+
private readonly modulesContainer;
|
|
6
|
+
constructor(modulesContainer: ModulesContainer);
|
|
7
|
+
onApplicationBootstrap(): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Safely extracts the module name from a module reference
|
|
10
|
+
*/
|
|
11
|
+
private getModuleName;
|
|
12
|
+
/**
|
|
13
|
+
* Safely extracts the wrapper name from a controller/provider wrapper
|
|
14
|
+
*/
|
|
15
|
+
private getWrapperName;
|
|
16
|
+
/**
|
|
17
|
+
* Generic name extraction with type safety
|
|
18
|
+
*/
|
|
19
|
+
private extractName;
|
|
20
|
+
/**
|
|
21
|
+
* Checks if an object has a valid collection (controllers, providers, etc.)
|
|
22
|
+
*/
|
|
23
|
+
private hasValidCollection;
|
|
24
|
+
/**
|
|
25
|
+
* Scans all modules and builds the dependency graph
|
|
26
|
+
*/
|
|
27
|
+
private scanAllModules;
|
|
28
|
+
/**
|
|
29
|
+
* Processes a single module and its components
|
|
30
|
+
*/
|
|
31
|
+
private processModule;
|
|
32
|
+
/**
|
|
33
|
+
* Processes controllers within a module
|
|
34
|
+
*/
|
|
35
|
+
private processControllers;
|
|
36
|
+
/**
|
|
37
|
+
* Processes providers/services within a module
|
|
38
|
+
*/
|
|
39
|
+
private processProviders;
|
|
40
|
+
/**
|
|
41
|
+
* Processes module imports
|
|
42
|
+
*/
|
|
43
|
+
private processImports;
|
|
44
|
+
/**
|
|
45
|
+
* Adds edges between controllers and their injected services
|
|
46
|
+
*/
|
|
47
|
+
private addServiceDependencyEdges;
|
|
48
|
+
/**
|
|
49
|
+
* Extracts constructor dependencies using reflection
|
|
50
|
+
*/
|
|
51
|
+
private extractConstructorDependencies;
|
|
52
|
+
/**
|
|
53
|
+
* Syncs the discovered architecture to Total.js Flow
|
|
54
|
+
*/
|
|
55
|
+
private syncToTotalJs;
|
|
56
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
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 __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.FlowScannerService = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const core_1 = require("@nestjs/core");
|
|
18
|
+
const axios_1 = __importDefault(require("axios"));
|
|
19
|
+
require("reflect-metadata");
|
|
20
|
+
let FlowScannerService = class FlowScannerService {
|
|
21
|
+
constructor(modulesContainer) {
|
|
22
|
+
this.modulesContainer = modulesContainer;
|
|
23
|
+
}
|
|
24
|
+
async onApplicationBootstrap() {
|
|
25
|
+
try {
|
|
26
|
+
const nodeMap = new Map();
|
|
27
|
+
const connections = [];
|
|
28
|
+
this.scanAllModules(nodeMap, connections);
|
|
29
|
+
this.addServiceDependencyEdges(nodeMap, connections);
|
|
30
|
+
const nodes = Array.from(nodeMap.values());
|
|
31
|
+
await this.syncToTotalJs({ nodes, connections });
|
|
32
|
+
console.log(`✅ Discovered ${nodes.length} components and ${connections.length} connections`);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error('❌ Failed to scan modules:', error);
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Safely extracts the module name from a module reference
|
|
41
|
+
*/
|
|
42
|
+
getModuleName(modRef) {
|
|
43
|
+
return this.extractName(modRef);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Safely extracts the wrapper name from a controller/provider wrapper
|
|
47
|
+
*/
|
|
48
|
+
getWrapperName(wrapper) {
|
|
49
|
+
return this.extractName(wrapper);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Generic name extraction with type safety
|
|
53
|
+
*/
|
|
54
|
+
extractName(obj) {
|
|
55
|
+
return obj?.metatype?.name && typeof obj.metatype.name === 'string'
|
|
56
|
+
? obj.metatype.name
|
|
57
|
+
: null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Checks if an object has a valid collection (controllers, providers, etc.)
|
|
61
|
+
*/
|
|
62
|
+
hasValidCollection(obj, property) {
|
|
63
|
+
const moduleRef = obj;
|
|
64
|
+
const collection = moduleRef?.[property];
|
|
65
|
+
// Check if it's a Map-like collection with values method
|
|
66
|
+
return (collection != null &&
|
|
67
|
+
typeof collection === 'object' &&
|
|
68
|
+
'values' in collection &&
|
|
69
|
+
typeof collection.values === 'function');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Scans all modules and builds the dependency graph
|
|
73
|
+
*/
|
|
74
|
+
scanAllModules(nodeMap, connections) {
|
|
75
|
+
for (const modRef of this.modulesContainer.values()) {
|
|
76
|
+
this.processModule(modRef, nodeMap, connections);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Processes a single module and its components
|
|
81
|
+
*/
|
|
82
|
+
processModule(modRef, nodeMap, connections) {
|
|
83
|
+
const moduleName = this.getModuleName(modRef);
|
|
84
|
+
if (!moduleName)
|
|
85
|
+
return;
|
|
86
|
+
// Add module node
|
|
87
|
+
nodeMap.set(moduleName, {
|
|
88
|
+
id: moduleName,
|
|
89
|
+
name: moduleName,
|
|
90
|
+
type: 'module',
|
|
91
|
+
});
|
|
92
|
+
// Process controllers
|
|
93
|
+
this.processControllers(modRef, moduleName, nodeMap, connections);
|
|
94
|
+
// Process providers/services
|
|
95
|
+
this.processProviders(modRef, moduleName, nodeMap, connections);
|
|
96
|
+
// Process module imports
|
|
97
|
+
this.processImports(modRef, moduleName, nodeMap, connections);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Processes controllers within a module
|
|
101
|
+
*/
|
|
102
|
+
processControllers(modRef, moduleName, nodeMap, connections) {
|
|
103
|
+
if (!this.hasValidCollection(modRef, 'controllers'))
|
|
104
|
+
return;
|
|
105
|
+
const controllersMap = modRef.controllers;
|
|
106
|
+
for (const ctrlWrapper of controllersMap.values()) {
|
|
107
|
+
const ctrlName = this.getWrapperName(ctrlWrapper);
|
|
108
|
+
if (!ctrlName)
|
|
109
|
+
continue;
|
|
110
|
+
nodeMap.set(ctrlName, {
|
|
111
|
+
id: ctrlName,
|
|
112
|
+
name: ctrlName,
|
|
113
|
+
type: 'controller',
|
|
114
|
+
});
|
|
115
|
+
connections.push({ from: moduleName, to: ctrlName });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Processes providers/services within a module
|
|
120
|
+
*/
|
|
121
|
+
processProviders(modRef, moduleName, nodeMap, connections) {
|
|
122
|
+
if (!this.hasValidCollection(modRef, 'providers'))
|
|
123
|
+
return;
|
|
124
|
+
const providersMap = modRef.providers;
|
|
125
|
+
for (const provWrapper of providersMap.values()) {
|
|
126
|
+
const provName = this.getWrapperName(provWrapper);
|
|
127
|
+
if (!provName || provName === moduleName)
|
|
128
|
+
continue;
|
|
129
|
+
nodeMap.set(provName, {
|
|
130
|
+
id: provName,
|
|
131
|
+
name: provName,
|
|
132
|
+
type: 'service',
|
|
133
|
+
});
|
|
134
|
+
connections.push({ from: moduleName, to: provName });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Processes module imports
|
|
139
|
+
*/
|
|
140
|
+
processImports(modRef, moduleName, nodeMap, connections) {
|
|
141
|
+
if (!modRef.imports || !modRef.imports.forEach)
|
|
142
|
+
return;
|
|
143
|
+
modRef.imports.forEach((imported) => {
|
|
144
|
+
const importedName = this.getModuleName(imported);
|
|
145
|
+
if (importedName && importedName !== moduleName) {
|
|
146
|
+
nodeMap.set(importedName, {
|
|
147
|
+
id: importedName,
|
|
148
|
+
name: importedName,
|
|
149
|
+
type: 'module',
|
|
150
|
+
});
|
|
151
|
+
connections.push({ from: moduleName, to: importedName });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Adds edges between controllers and their injected services
|
|
157
|
+
*/
|
|
158
|
+
addServiceDependencyEdges(nodeMap, connections) {
|
|
159
|
+
for (const modRef of this.modulesContainer.values()) {
|
|
160
|
+
if (!this.hasValidCollection(modRef, 'controllers'))
|
|
161
|
+
continue;
|
|
162
|
+
const moduleRef = modRef;
|
|
163
|
+
for (const ctrlWrapper of moduleRef.controllers.values()) {
|
|
164
|
+
const ctrlName = this.getWrapperName(ctrlWrapper);
|
|
165
|
+
if (!ctrlName || !ctrlWrapper.instance)
|
|
166
|
+
continue;
|
|
167
|
+
this.extractConstructorDependencies(ctrlWrapper, ctrlName, nodeMap, connections);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Extracts constructor dependencies using reflection
|
|
173
|
+
*/
|
|
174
|
+
extractConstructorDependencies(wrapper, ctrlName, nodeMap, connections) {
|
|
175
|
+
try {
|
|
176
|
+
const instance = wrapper.instance;
|
|
177
|
+
const proto = Object.getPrototypeOf(instance);
|
|
178
|
+
const ctor = proto?.constructor;
|
|
179
|
+
if (!ctor)
|
|
180
|
+
return;
|
|
181
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', ctor) || [];
|
|
182
|
+
for (const param of paramTypes) {
|
|
183
|
+
if (typeof param === 'function' &&
|
|
184
|
+
param['name'] &&
|
|
185
|
+
nodeMap.has(param['name'])) {
|
|
186
|
+
connections.push({ from: ctrlName, to: param['name'] });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
console.warn(`Failed to extract dependencies for ${ctrlName}:`, error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Syncs the discovered architecture to Total.js Flow
|
|
196
|
+
*/
|
|
197
|
+
async syncToTotalJs(data) {
|
|
198
|
+
const TOTAL_JS_URL = 'http://localhost:8000';
|
|
199
|
+
try {
|
|
200
|
+
const response = await axios_1.default.post(`${TOTAL_JS_URL}/api/flow/design`, data, {
|
|
201
|
+
timeout: 5000,
|
|
202
|
+
headers: {
|
|
203
|
+
'Content-Type': 'application/json',
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
if (response.status === 200) {
|
|
207
|
+
console.log('✅ Total.js Flow Design Synced Successfully');
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
console.warn(`⚠️ Unexpected response status: ${response.status}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
215
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
216
|
+
if (error.code === 'ECONNREFUSED') {
|
|
217
|
+
console.error('❌ Total.js Flow server is not running. Please start it on port 8000.');
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
console.error(`❌ Sync Failed: ${error.response?.status || 'Network error'} - ${errorMessage}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
console.error(`❌ Sync Failed: ${errorMessage}`);
|
|
225
|
+
}
|
|
226
|
+
// Don't rethrow - this shouldn't break the application startup
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
exports.FlowScannerService = FlowScannerService;
|
|
231
|
+
exports.FlowScannerService = FlowScannerService = __decorate([
|
|
232
|
+
(0, common_1.Injectable)(),
|
|
233
|
+
__metadata("design:paramtypes", [core_1.ModulesContainer])
|
|
234
|
+
], FlowScannerService);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ExportResult } from '@opentelemetry/core';
|
|
2
|
+
import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
|
|
3
|
+
export declare class TotalJsExporter implements SpanExporter {
|
|
4
|
+
private readonly url;
|
|
5
|
+
constructor(url: string);
|
|
6
|
+
export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void;
|
|
7
|
+
shutdown(): Promise<void>;
|
|
8
|
+
}
|
package/dist/exporter.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TotalJsExporter = void 0;
|
|
7
|
+
const core_1 = require("@opentelemetry/core");
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
class TotalJsExporter {
|
|
10
|
+
constructor(url) {
|
|
11
|
+
this.url = url;
|
|
12
|
+
}
|
|
13
|
+
export(spans, resultCallback) {
|
|
14
|
+
console.log(`📦 TotalJsExporter: Exporting ${spans.length} spans`);
|
|
15
|
+
for (const span of spans) {
|
|
16
|
+
const data = span.attributes?.['flow.data'] || null;
|
|
17
|
+
const duration = span.duration[1] / 1000000;
|
|
18
|
+
console.log(`📡 Exporting span:`, {
|
|
19
|
+
name: span.name,
|
|
20
|
+
duration: duration,
|
|
21
|
+
hasData: !!data,
|
|
22
|
+
url: `${this.url}/api/pulse`,
|
|
23
|
+
});
|
|
24
|
+
const pulseData = {
|
|
25
|
+
id: span.name, // Matches the Controller/Service name
|
|
26
|
+
status: 'pulse',
|
|
27
|
+
duration: duration,
|
|
28
|
+
data,
|
|
29
|
+
};
|
|
30
|
+
console.log(`🚀 Sending pulse data:`, pulseData);
|
|
31
|
+
axios_1.default
|
|
32
|
+
.post(`${this.url}/api/pulse`, pulseData)
|
|
33
|
+
.then((response) => {
|
|
34
|
+
console.log(`✅ Pulse sent successfully for ${span.name}:`, response.status);
|
|
35
|
+
})
|
|
36
|
+
.catch((err) => {
|
|
37
|
+
if (err instanceof Error) {
|
|
38
|
+
console.error(`❌ Pulse send error for ${span.name}:`, err.message);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.error(`❌ Pulse send error for ${span.name}:`, err);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
console.log(`📋 Export completed, calling result callback`);
|
|
46
|
+
resultCallback({ code: core_1.ExportResultCode.SUCCESS });
|
|
47
|
+
}
|
|
48
|
+
shutdown() {
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.TotalJsExporter = TotalJsExporter;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decorator to automatically trace method execution and send pulse data to Total.js Flow
|
|
3
|
+
* @param traceName Optional custom trace name, defaults to ClassName.methodName
|
|
4
|
+
*/
|
|
5
|
+
export declare function FlowTrace(traceName?: string): (target: any, propertyName: string, descriptor: PropertyDescriptor) => PropertyDescriptor;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FlowTrace = FlowTrace;
|
|
4
|
+
const index_1 = require("./index");
|
|
5
|
+
/**
|
|
6
|
+
* Decorator to automatically trace method execution and send pulse data to Total.js Flow
|
|
7
|
+
* @param traceName Optional custom trace name, defaults to ClassName.methodName
|
|
8
|
+
*/
|
|
9
|
+
function FlowTrace(traceName) {
|
|
10
|
+
return function (target, propertyName, descriptor) {
|
|
11
|
+
const method = descriptor.value;
|
|
12
|
+
const className = target?.constructor?.name;
|
|
13
|
+
const defaultTraceName = `${className}`;
|
|
14
|
+
const finalTraceName = traceName || defaultTraceName;
|
|
15
|
+
descriptor.value = async function (...args) {
|
|
16
|
+
return (0, index_1.flowTrace)(finalTraceName, async () => {
|
|
17
|
+
return await method.apply(this, args);
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
return descriptor;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
/**
|
|
4
|
+
* Global interceptor that automatically traces all controller method executions
|
|
5
|
+
*/
|
|
6
|
+
export declare class FlowInterceptor implements NestInterceptor {
|
|
7
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<any>;
|
|
8
|
+
/**
|
|
9
|
+
* Sends flow pulse without blocking the main request
|
|
10
|
+
*/
|
|
11
|
+
private sendFlowPulse;
|
|
12
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.FlowInterceptor = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
const operators_1 = require("rxjs/operators");
|
|
12
|
+
const index_1 = require("./index");
|
|
13
|
+
/**
|
|
14
|
+
* Global interceptor that automatically traces all controller method executions
|
|
15
|
+
*/
|
|
16
|
+
let FlowInterceptor = class FlowInterceptor {
|
|
17
|
+
intercept(context, next) {
|
|
18
|
+
const className = context.getClass().name;
|
|
19
|
+
const methodName = context.getHandler().name;
|
|
20
|
+
const displayName = `${className}.${methodName}`;
|
|
21
|
+
console.log(`🎯 INTERCEPTOR TRIGGERED: ${displayName}`);
|
|
22
|
+
// Execute the handler and capture the result for flow tracing
|
|
23
|
+
return next.handle().pipe((0, operators_1.tap)({
|
|
24
|
+
next: (result) => {
|
|
25
|
+
console.log(`✅ ${displayName} completed, sending pulse...`);
|
|
26
|
+
// Trigger flow trace with the actual result (non-blocking)
|
|
27
|
+
this.sendFlowPulse(displayName, result);
|
|
28
|
+
},
|
|
29
|
+
error: (error) => {
|
|
30
|
+
const errorMessage = typeof error === 'object' && error !== null && 'message' in error
|
|
31
|
+
? error.message
|
|
32
|
+
: String(error);
|
|
33
|
+
console.error(`❌ ${displayName} failed:`, errorMessage);
|
|
34
|
+
// Send pulse for errors too
|
|
35
|
+
this.sendFlowPulse(displayName, { error: errorMessage });
|
|
36
|
+
},
|
|
37
|
+
complete: () => {
|
|
38
|
+
console.log(`🏁 ${displayName} stream completed`);
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Sends flow pulse without blocking the main request
|
|
44
|
+
*/
|
|
45
|
+
sendFlowPulse(displayName, result) {
|
|
46
|
+
console.log(`📡 Sending flow pulse for ${displayName}...`);
|
|
47
|
+
// Use flowTrace to send the pulse
|
|
48
|
+
(0, index_1.flowTrace)(displayName, () => {
|
|
49
|
+
console.log(`🔄 Executing flowTrace for ${displayName}`);
|
|
50
|
+
return result;
|
|
51
|
+
})
|
|
52
|
+
.then(() => {
|
|
53
|
+
console.log(`📊 Flow pulse sent successfully for ${displayName}`);
|
|
54
|
+
})
|
|
55
|
+
.catch((error) => {
|
|
56
|
+
console.error(`⚠️ Flow pulse failed for ${displayName}:`, typeof error === 'object' && error !== null && 'message' in error
|
|
57
|
+
? error.message
|
|
58
|
+
: error);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
exports.FlowInterceptor = FlowInterceptor;
|
|
63
|
+
exports.FlowInterceptor = FlowInterceptor = __decorate([
|
|
64
|
+
(0, common_1.Injectable)()
|
|
65
|
+
], FlowInterceptor);
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.NestLiveFlowModule = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
const core_1 = require("@nestjs/core");
|
|
12
|
+
const discovery_service_1 = require("./discovery.service");
|
|
13
|
+
const flow_interceptor_1 = require("./flow.interceptor");
|
|
14
|
+
let NestLiveFlowModule = class NestLiveFlowModule {
|
|
15
|
+
};
|
|
16
|
+
exports.NestLiveFlowModule = NestLiveFlowModule;
|
|
17
|
+
exports.NestLiveFlowModule = NestLiveFlowModule = __decorate([
|
|
18
|
+
(0, common_1.Global)(),
|
|
19
|
+
(0, common_1.Module)({
|
|
20
|
+
imports: [core_1.DiscoveryModule],
|
|
21
|
+
providers: [
|
|
22
|
+
discovery_service_1.FlowScannerService,
|
|
23
|
+
{
|
|
24
|
+
provide: core_1.APP_INTERCEPTOR,
|
|
25
|
+
useClass: flow_interceptor_1.FlowInterceptor,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
exports: [discovery_service_1.FlowScannerService],
|
|
29
|
+
})
|
|
30
|
+
], NestLiveFlowModule);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { initFlow, flowTrace } from './core';
|
|
2
|
+
export { FlowTrace } from './flow.decorator';
|
|
3
|
+
export { FlowInterceptor } from './flow.interceptor';
|
|
4
|
+
export { NestLiveFlowModule } from './flow.module';
|
|
5
|
+
export { FlowScannerService } from './discovery.service';
|
|
6
|
+
export { ServiceInstrumentor } from './service-instrumentor';
|
|
7
|
+
export { TotalJsExporter } from './exporter';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TotalJsExporter = exports.ServiceInstrumentor = exports.FlowScannerService = exports.NestLiveFlowModule = exports.FlowInterceptor = exports.FlowTrace = exports.flowTrace = exports.initFlow = void 0;
|
|
4
|
+
// Core initialization functions
|
|
5
|
+
var core_1 = require("./core");
|
|
6
|
+
Object.defineProperty(exports, "initFlow", { enumerable: true, get: function () { return core_1.initFlow; } });
|
|
7
|
+
Object.defineProperty(exports, "flowTrace", { enumerable: true, get: function () { return core_1.flowTrace; } });
|
|
8
|
+
// Decorators and Interceptors
|
|
9
|
+
var flow_decorator_1 = require("./flow.decorator");
|
|
10
|
+
Object.defineProperty(exports, "FlowTrace", { enumerable: true, get: function () { return flow_decorator_1.FlowTrace; } });
|
|
11
|
+
var flow_interceptor_1 = require("./flow.interceptor");
|
|
12
|
+
Object.defineProperty(exports, "FlowInterceptor", { enumerable: true, get: function () { return flow_interceptor_1.FlowInterceptor; } });
|
|
13
|
+
// Module and Services
|
|
14
|
+
var flow_module_1 = require("./flow.module");
|
|
15
|
+
Object.defineProperty(exports, "NestLiveFlowModule", { enumerable: true, get: function () { return flow_module_1.NestLiveFlowModule; } });
|
|
16
|
+
var discovery_service_1 = require("./discovery.service");
|
|
17
|
+
Object.defineProperty(exports, "FlowScannerService", { enumerable: true, get: function () { return discovery_service_1.FlowScannerService; } });
|
|
18
|
+
// Service Instrumentor for manual instrumentation
|
|
19
|
+
var service_instrumentor_1 = require("./service-instrumentor");
|
|
20
|
+
Object.defineProperty(exports, "ServiceInstrumentor", { enumerable: true, get: function () { return service_instrumentor_1.ServiceInstrumentor; } });
|
|
21
|
+
// Exporter for custom integrations
|
|
22
|
+
var exporter_1 = require("./exporter");
|
|
23
|
+
Object.defineProperty(exports, "TotalJsExporter", { enumerable: true, get: function () { return exporter_1.TotalJsExporter; } });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { OnApplicationBootstrap } from '@nestjs/common';
|
|
2
|
+
import { ModulesContainer } from '@nestjs/core';
|
|
3
|
+
export declare class ServiceInstrumentor implements OnApplicationBootstrap {
|
|
4
|
+
private readonly modulesContainer;
|
|
5
|
+
constructor(modulesContainer: ModulesContainer);
|
|
6
|
+
onApplicationBootstrap(): Promise<void>;
|
|
7
|
+
private instrumentServices;
|
|
8
|
+
private instrumentInstance;
|
|
9
|
+
private wrapMethod;
|
|
10
|
+
private shouldSkip;
|
|
11
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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.ServiceInstrumentor = void 0;
|
|
13
|
+
const common_1 = require("@nestjs/common");
|
|
14
|
+
const core_1 = require("@nestjs/core");
|
|
15
|
+
const index_1 = require("./index");
|
|
16
|
+
let ServiceInstrumentor = class ServiceInstrumentor {
|
|
17
|
+
constructor(modulesContainer) {
|
|
18
|
+
this.modulesContainer = modulesContainer;
|
|
19
|
+
}
|
|
20
|
+
async onApplicationBootstrap() {
|
|
21
|
+
// Temporarily disable ServiceInstrumentor to avoid recursive instrumentation issues
|
|
22
|
+
// TODO: Implement a better approach that doesn't instrument itself
|
|
23
|
+
console.log('⚠️ ServiceInstrumentor disabled to prevent recursive instrumentation');
|
|
24
|
+
}
|
|
25
|
+
instrumentServices() {
|
|
26
|
+
for (const module of this.modulesContainer.values()) {
|
|
27
|
+
// Instrument providers (services)
|
|
28
|
+
for (const provider of module.providers.values()) {
|
|
29
|
+
if (provider.instance && provider.metatype) {
|
|
30
|
+
this.instrumentInstance(provider.instance, provider.metatype.name);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
instrumentInstance(instance, className) {
|
|
36
|
+
if (!instance || typeof instance !== 'object')
|
|
37
|
+
return;
|
|
38
|
+
// Skip if already instrumented
|
|
39
|
+
if (instance.__flowInstrumented)
|
|
40
|
+
return;
|
|
41
|
+
// Skip internal NestJS classes and our own flow classes
|
|
42
|
+
if (this.shouldSkip(className))
|
|
43
|
+
return;
|
|
44
|
+
const prototype = Object.getPrototypeOf(instance);
|
|
45
|
+
const methodNames = Object.getOwnPropertyNames(prototype).filter((name) => {
|
|
46
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
|
|
47
|
+
return (name !== 'constructor' &&
|
|
48
|
+
typeof descriptor?.value === 'function' &&
|
|
49
|
+
!name.startsWith('_') &&
|
|
50
|
+
!name.includes('Symbol'));
|
|
51
|
+
});
|
|
52
|
+
methodNames.forEach((methodName) => {
|
|
53
|
+
const originalMethod = instance[methodName];
|
|
54
|
+
if (typeof originalMethod === 'function') {
|
|
55
|
+
instance[methodName] = this.wrapMethod(originalMethod, className, methodName);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// Mark as instrumented
|
|
59
|
+
instance.__flowInstrumented = true;
|
|
60
|
+
}
|
|
61
|
+
wrapMethod(originalMethod, className, methodName) {
|
|
62
|
+
return async function (...args) {
|
|
63
|
+
const traceName = `${className}.${methodName}`;
|
|
64
|
+
return (0, index_1.flowTrace)(traceName, async () => {
|
|
65
|
+
const result = originalMethod.apply(this, args);
|
|
66
|
+
// Handle both sync and async methods
|
|
67
|
+
return result instanceof Promise ? await result : result;
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
shouldSkip(className) {
|
|
72
|
+
const skipPrefixes = [
|
|
73
|
+
'Flow',
|
|
74
|
+
'Discovery',
|
|
75
|
+
'Metadata',
|
|
76
|
+
'Module',
|
|
77
|
+
'Reflector',
|
|
78
|
+
'Application',
|
|
79
|
+
'Internal',
|
|
80
|
+
'Logger',
|
|
81
|
+
'Config',
|
|
82
|
+
];
|
|
83
|
+
return (skipPrefixes.some((prefix) => className.startsWith(prefix)) ||
|
|
84
|
+
className.includes('Wrapper') ||
|
|
85
|
+
className.includes('Container') ||
|
|
86
|
+
className.includes('Factory'));
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
exports.ServiceInstrumentor = ServiceInstrumentor;
|
|
90
|
+
exports.ServiceInstrumentor = ServiceInstrumentor = __decorate([
|
|
91
|
+
(0, common_1.Injectable)(),
|
|
92
|
+
__metadata("design:paramtypes", [core_1.ModulesContainer])
|
|
93
|
+
], ServiceInstrumentor);
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nest-live-flow",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A NestJS library for live application architecture visualization and flow tracing with Total.js Flow integration",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/**/*",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"prepare": "npm run build",
|
|
15
|
+
"test": "jest",
|
|
16
|
+
"lint": "eslint \"src/**/*.ts\" --fix",
|
|
17
|
+
"format": "prettier --write \"src/**/*.ts\""
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"nestjs",
|
|
21
|
+
"tracing",
|
|
22
|
+
"architecture",
|
|
23
|
+
"visualization",
|
|
24
|
+
"totaljs",
|
|
25
|
+
"flow",
|
|
26
|
+
"opentelemetry",
|
|
27
|
+
"dependency-injection",
|
|
28
|
+
"live-monitoring"
|
|
29
|
+
],
|
|
30
|
+
"author": "Michael Musni <mike.musni@me.com>",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/michaelmusni/nest-live-flow.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/michaelmusni/nest-live-flow/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/michaelmusni/nest-live-flow#readme",
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
|
42
|
+
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
|
43
|
+
"reflect-metadata": "^0.1.13 || ^0.2.0"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@opentelemetry/api": "^1.9.0",
|
|
47
|
+
"@opentelemetry/resources": "^2.2.0",
|
|
48
|
+
"@opentelemetry/sdk-trace-base": "^2.2.0",
|
|
49
|
+
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
|
50
|
+
"@opentelemetry/semantic-conventions": "^1.38.0",
|
|
51
|
+
"axios": "^1.13.2"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@nestjs/common": "^11.0.1",
|
|
55
|
+
"@nestjs/core": "^11.0.1",
|
|
56
|
+
"@types/node": "^22.10.7",
|
|
57
|
+
"eslint": "^9.18.0",
|
|
58
|
+
"prettier": "^3.4.2",
|
|
59
|
+
"typescript": "^5.7.3",
|
|
60
|
+
"jest": "^30.0.0",
|
|
61
|
+
"ts-jest": "^29.2.5"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=18.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|