@devms/livetail 0.0.2
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/README.md +511 -0
- package/dist/console-capture.service.d.ts +46 -0
- package/dist/console-capture.service.js +166 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +18 -0
- package/dist/interfaces/livetail.interface.d.ts +156 -0
- package/dist/interfaces/livetail.interface.js +24 -0
- package/dist/livetail.gateway.d.ts +59 -0
- package/dist/livetail.gateway.js +334 -0
- package/dist/livetail.module.d.ts +52 -0
- package/dist/livetail.module.js +105 -0
- package/dist/livetail.service.d.ts +38 -0
- package/dist/livetail.service.js +84 -0
- package/package.json +46 -0
|
@@ -0,0 +1,334 @@
|
|
|
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
|
+
var LiveTailGateway_1;
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.LiveTailGateway = void 0;
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
const websockets_1 = require("@nestjs/websockets");
|
|
19
|
+
const socket_io_1 = require("socket.io");
|
|
20
|
+
const livetail_service_1 = require("./livetail.service");
|
|
21
|
+
const console_capture_service_1 = require("./console-capture.service");
|
|
22
|
+
const livetail_interface_1 = require("./interfaces/livetail.interface");
|
|
23
|
+
const CONSOLE_ROOM = 'console';
|
|
24
|
+
/** History is replayed in chunks to keep WS frames small. */
|
|
25
|
+
const CONSOLE_HISTORY_CHUNK = 500;
|
|
26
|
+
/** Pending batch hard cap — oldest lines are dropped beyond this. */
|
|
27
|
+
const CONSOLE_MAX_PENDING = 5000;
|
|
28
|
+
let LiveTailGateway = LiveTailGateway_1 = class LiveTailGateway {
|
|
29
|
+
constructor(liveTailService, config, consoleCapture) {
|
|
30
|
+
this.liveTailService = liveTailService;
|
|
31
|
+
this.config = config;
|
|
32
|
+
this.consoleCapture = consoleCapture;
|
|
33
|
+
this.logger = new common_1.Logger(LiveTailGateway_1.name);
|
|
34
|
+
this.clients = new Map();
|
|
35
|
+
// Console streaming state
|
|
36
|
+
this.consoleSubscribers = new Set();
|
|
37
|
+
this.consolePending = [];
|
|
38
|
+
this.consoleDropped = 0;
|
|
39
|
+
this.consoleFlushTimer = null;
|
|
40
|
+
}
|
|
41
|
+
onModuleInit() {
|
|
42
|
+
if (this.config.disabled) {
|
|
43
|
+
this.logger.log('LiveTail gateway is disabled');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.subscription = this.liveTailService.logs$.subscribe((log) => {
|
|
47
|
+
this.broadcastToClients(log);
|
|
48
|
+
});
|
|
49
|
+
if (this.consoleCapture?.enabled) {
|
|
50
|
+
this.consoleSubscription = this.consoleCapture.lines$.subscribe((line) => this.queueConsoleLine(line));
|
|
51
|
+
}
|
|
52
|
+
this.logger.log('LiveTail gateway initialized');
|
|
53
|
+
}
|
|
54
|
+
onModuleDestroy() {
|
|
55
|
+
this.subscription?.unsubscribe();
|
|
56
|
+
this.consoleSubscription?.unsubscribe();
|
|
57
|
+
this.stopConsoleFlusher();
|
|
58
|
+
}
|
|
59
|
+
// ─── Connection Lifecycle ─────────────────────────────
|
|
60
|
+
handleConnection(client) {
|
|
61
|
+
// Enforce max clients limit
|
|
62
|
+
if (this.config.maxClients && this.config.maxClients > 0 && this.clients.size >= this.config.maxClients) {
|
|
63
|
+
this.logger.warn(`Connection rejected: max clients (${this.config.maxClients}) reached`);
|
|
64
|
+
client.emit('error', { message: 'Max clients limit reached. Try again later.' });
|
|
65
|
+
client.disconnect(true);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.clients.set(client.id, { filter: {}, paused: false, connectedAt: Date.now() });
|
|
69
|
+
this.logger.debug(`Client connected: ${client.id} (total: ${this.clients.size})`);
|
|
70
|
+
client.emit('connected', {
|
|
71
|
+
clientId: client.id,
|
|
72
|
+
message: 'Live tail connected',
|
|
73
|
+
connectedClients: this.clients.size,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
handleDisconnect(client) {
|
|
77
|
+
this.clients.delete(client.id);
|
|
78
|
+
if (this.consoleSubscribers.delete(client.id)) {
|
|
79
|
+
this.stopConsoleFlusherIfIdle();
|
|
80
|
+
}
|
|
81
|
+
this.logger.debug(`Client disconnected: ${client.id} (total: ${this.clients.size})`);
|
|
82
|
+
}
|
|
83
|
+
// ─── Client Messages ─────────────────────────────────
|
|
84
|
+
/**
|
|
85
|
+
* Client subscribes with filters.
|
|
86
|
+
* Example payload:
|
|
87
|
+
* { environmentId: "xxx", level: ["error", "fatal"], category: "auth" }
|
|
88
|
+
*/
|
|
89
|
+
handleSubscribe(client, filter) {
|
|
90
|
+
const state = this.clients.get(client.id);
|
|
91
|
+
if (state) {
|
|
92
|
+
state.filter = filter || {};
|
|
93
|
+
state.paused = false;
|
|
94
|
+
}
|
|
95
|
+
this.logger.debug(`Client ${client.id} subscribed: ${JSON.stringify(filter)}`);
|
|
96
|
+
client.emit('subscribed', { filter });
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Client updates filters without disconnecting.
|
|
100
|
+
*/
|
|
101
|
+
handleUpdateFilter(client, filter) {
|
|
102
|
+
const state = this.clients.get(client.id);
|
|
103
|
+
if (state) {
|
|
104
|
+
state.filter = filter || {};
|
|
105
|
+
}
|
|
106
|
+
client.emit('filterUpdated', { filter });
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Client pauses the stream (still connected but no logs sent).
|
|
110
|
+
*/
|
|
111
|
+
handlePause(client) {
|
|
112
|
+
const state = this.clients.get(client.id);
|
|
113
|
+
if (state) {
|
|
114
|
+
state.paused = true;
|
|
115
|
+
}
|
|
116
|
+
client.emit('paused');
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Client resumes the stream.
|
|
120
|
+
*/
|
|
121
|
+
handleResume(client) {
|
|
122
|
+
const state = this.clients.get(client.id);
|
|
123
|
+
if (state) {
|
|
124
|
+
state.paused = false;
|
|
125
|
+
}
|
|
126
|
+
client.emit('resumed');
|
|
127
|
+
}
|
|
128
|
+
// ─── Console Streaming ────────────────────────────────
|
|
129
|
+
/**
|
|
130
|
+
* Client subscribes to the raw stdout/stderr stream.
|
|
131
|
+
* Replays the ring buffer (chunked), then streams live lines
|
|
132
|
+
* batched on `console-lines` events.
|
|
133
|
+
*
|
|
134
|
+
* Payload: { token?: string, tail?: number }
|
|
135
|
+
*/
|
|
136
|
+
handleSubscribeConsole(client, payload) {
|
|
137
|
+
if (!this.consoleCapture?.enabled) {
|
|
138
|
+
client.emit('console-error', {
|
|
139
|
+
code: 'CONSOLE_DISABLED',
|
|
140
|
+
message: 'Console capture is not enabled on this application. Enable it with LiveTailModule.register({ captureConsole: true }).',
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const requiredToken = this.consoleCapture.options.token;
|
|
145
|
+
if (requiredToken && payload?.token !== requiredToken) {
|
|
146
|
+
this.logger.warn(`Console subscribe rejected for ${client.id}: invalid token`);
|
|
147
|
+
client.emit('console-error', {
|
|
148
|
+
code: 'CONSOLE_UNAUTHORIZED',
|
|
149
|
+
message: 'Invalid or missing console token.',
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this.consoleSubscribers.add(client.id);
|
|
154
|
+
client.join(CONSOLE_ROOM);
|
|
155
|
+
const history = this.consoleCapture.getHistory(payload?.tail);
|
|
156
|
+
client.emit('console-subscribed', {
|
|
157
|
+
pid: process.pid,
|
|
158
|
+
bufferSize: this.consoleCapture.options.bufferSize,
|
|
159
|
+
totalCaptured: this.consoleCapture.totalCaptured,
|
|
160
|
+
historyCount: history.length,
|
|
161
|
+
});
|
|
162
|
+
// Replay history in chunks so a full buffer never becomes one giant frame
|
|
163
|
+
for (let i = 0; i < history.length; i += CONSOLE_HISTORY_CHUNK) {
|
|
164
|
+
const chunk = history.slice(i, i + CONSOLE_HISTORY_CHUNK);
|
|
165
|
+
client.emit('console-history', {
|
|
166
|
+
lines: chunk,
|
|
167
|
+
done: i + CONSOLE_HISTORY_CHUNK >= history.length,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (history.length === 0) {
|
|
171
|
+
client.emit('console-history', { lines: [], done: true });
|
|
172
|
+
}
|
|
173
|
+
this.startConsoleFlusher();
|
|
174
|
+
this.logger.debug(`Client ${client.id} subscribed to console (subscribers: ${this.consoleSubscribers.size})`);
|
|
175
|
+
}
|
|
176
|
+
handleUnsubscribeConsole(client) {
|
|
177
|
+
client.leave(CONSOLE_ROOM);
|
|
178
|
+
if (this.consoleSubscribers.delete(client.id)) {
|
|
179
|
+
this.stopConsoleFlusherIfIdle();
|
|
180
|
+
}
|
|
181
|
+
client.emit('console-unsubscribed');
|
|
182
|
+
}
|
|
183
|
+
queueConsoleLine(line) {
|
|
184
|
+
// No one watching → buffer-only mode, zero broadcast cost
|
|
185
|
+
if (this.consoleSubscribers.size === 0)
|
|
186
|
+
return;
|
|
187
|
+
this.consolePending.push(line);
|
|
188
|
+
if (this.consolePending.length > CONSOLE_MAX_PENDING) {
|
|
189
|
+
const overflow = this.consolePending.length - CONSOLE_MAX_PENDING;
|
|
190
|
+
this.consolePending.splice(0, overflow);
|
|
191
|
+
this.consoleDropped += overflow;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
flushConsolePending() {
|
|
195
|
+
if (this.consolePending.length === 0)
|
|
196
|
+
return;
|
|
197
|
+
const lines = this.consolePending;
|
|
198
|
+
this.consolePending = [];
|
|
199
|
+
const dropped = this.consoleDropped;
|
|
200
|
+
this.consoleDropped = 0;
|
|
201
|
+
this.server.to(CONSOLE_ROOM).emit('console-lines', { lines, dropped });
|
|
202
|
+
}
|
|
203
|
+
startConsoleFlusher() {
|
|
204
|
+
if (this.consoleFlushTimer)
|
|
205
|
+
return;
|
|
206
|
+
const interval = this.consoleCapture?.options.batchInterval ?? 150;
|
|
207
|
+
this.consoleFlushTimer = setInterval(() => this.flushConsolePending(), interval);
|
|
208
|
+
// Don't let the flusher keep the process alive on shutdown
|
|
209
|
+
if (typeof this.consoleFlushTimer.unref === 'function') {
|
|
210
|
+
this.consoleFlushTimer.unref();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
stopConsoleFlusher() {
|
|
214
|
+
if (this.consoleFlushTimer) {
|
|
215
|
+
clearInterval(this.consoleFlushTimer);
|
|
216
|
+
this.consoleFlushTimer = null;
|
|
217
|
+
}
|
|
218
|
+
this.consolePending = [];
|
|
219
|
+
this.consoleDropped = 0;
|
|
220
|
+
}
|
|
221
|
+
stopConsoleFlusherIfIdle() {
|
|
222
|
+
if (this.consoleSubscribers.size === 0) {
|
|
223
|
+
this.stopConsoleFlusher();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// ─── Broadcasting ─────────────────────────────────────
|
|
227
|
+
broadcastToClients(log) {
|
|
228
|
+
for (const [clientId, state] of this.clients) {
|
|
229
|
+
if (state.paused)
|
|
230
|
+
continue;
|
|
231
|
+
if (!this.matchesFilter(log, state.filter))
|
|
232
|
+
continue;
|
|
233
|
+
this.server.to(clientId).emit('log', log);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
matchesFilter(log, filter) {
|
|
237
|
+
// Scope filters
|
|
238
|
+
if (filter.environmentId && log.environmentId !== filter.environmentId)
|
|
239
|
+
return false;
|
|
240
|
+
if (filter.appId && log.appId !== filter.appId)
|
|
241
|
+
return false;
|
|
242
|
+
if (filter.orgId && log.orgId !== filter.orgId)
|
|
243
|
+
return false;
|
|
244
|
+
// Level filter (single or array)
|
|
245
|
+
if (filter.level) {
|
|
246
|
+
const levels = Array.isArray(filter.level) ? filter.level : [filter.level];
|
|
247
|
+
if (!levels.includes(log.level))
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
// Category filter (exact match)
|
|
251
|
+
if (filter.category && log.category !== filter.category)
|
|
252
|
+
return false;
|
|
253
|
+
// Action filter (contains, case-insensitive)
|
|
254
|
+
if (filter.action) {
|
|
255
|
+
if (!log.action.toLowerCase().includes(filter.action.toLowerCase()))
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
// User filter
|
|
259
|
+
if (filter.userId && log.userId !== filter.userId)
|
|
260
|
+
return false;
|
|
261
|
+
// Text search (in message, action, category)
|
|
262
|
+
if (filter.search) {
|
|
263
|
+
const term = filter.search.toLowerCase();
|
|
264
|
+
const haystack = [log.message, log.action, log.category]
|
|
265
|
+
.filter(Boolean)
|
|
266
|
+
.join(' ')
|
|
267
|
+
.toLowerCase();
|
|
268
|
+
if (!haystack.includes(term))
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
// Tags filter (log must have at least one matching tag)
|
|
272
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
273
|
+
if (!log.tags || !filter.tags.some((t) => log.tags.includes(t)))
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
exports.LiveTailGateway = LiveTailGateway;
|
|
280
|
+
__decorate([
|
|
281
|
+
(0, websockets_1.WebSocketServer)(),
|
|
282
|
+
__metadata("design:type", socket_io_1.Server)
|
|
283
|
+
], LiveTailGateway.prototype, "server", void 0);
|
|
284
|
+
__decorate([
|
|
285
|
+
(0, websockets_1.SubscribeMessage)('subscribe'),
|
|
286
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
287
|
+
__param(1, (0, websockets_1.MessageBody)()),
|
|
288
|
+
__metadata("design:type", Function),
|
|
289
|
+
__metadata("design:paramtypes", [socket_io_1.Socket, Object]),
|
|
290
|
+
__metadata("design:returntype", void 0)
|
|
291
|
+
], LiveTailGateway.prototype, "handleSubscribe", null);
|
|
292
|
+
__decorate([
|
|
293
|
+
(0, websockets_1.SubscribeMessage)('updateFilter'),
|
|
294
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
295
|
+
__param(1, (0, websockets_1.MessageBody)()),
|
|
296
|
+
__metadata("design:type", Function),
|
|
297
|
+
__metadata("design:paramtypes", [socket_io_1.Socket, Object]),
|
|
298
|
+
__metadata("design:returntype", void 0)
|
|
299
|
+
], LiveTailGateway.prototype, "handleUpdateFilter", null);
|
|
300
|
+
__decorate([
|
|
301
|
+
(0, websockets_1.SubscribeMessage)('pause'),
|
|
302
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
303
|
+
__metadata("design:type", Function),
|
|
304
|
+
__metadata("design:paramtypes", [socket_io_1.Socket]),
|
|
305
|
+
__metadata("design:returntype", void 0)
|
|
306
|
+
], LiveTailGateway.prototype, "handlePause", null);
|
|
307
|
+
__decorate([
|
|
308
|
+
(0, websockets_1.SubscribeMessage)('resume'),
|
|
309
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
310
|
+
__metadata("design:type", Function),
|
|
311
|
+
__metadata("design:paramtypes", [socket_io_1.Socket]),
|
|
312
|
+
__metadata("design:returntype", void 0)
|
|
313
|
+
], LiveTailGateway.prototype, "handleResume", null);
|
|
314
|
+
__decorate([
|
|
315
|
+
(0, websockets_1.SubscribeMessage)('subscribe-console'),
|
|
316
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
317
|
+
__param(1, (0, websockets_1.MessageBody)()),
|
|
318
|
+
__metadata("design:type", Function),
|
|
319
|
+
__metadata("design:paramtypes", [socket_io_1.Socket, Object]),
|
|
320
|
+
__metadata("design:returntype", void 0)
|
|
321
|
+
], LiveTailGateway.prototype, "handleSubscribeConsole", null);
|
|
322
|
+
__decorate([
|
|
323
|
+
(0, websockets_1.SubscribeMessage)('unsubscribe-console'),
|
|
324
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
325
|
+
__metadata("design:type", Function),
|
|
326
|
+
__metadata("design:paramtypes", [socket_io_1.Socket]),
|
|
327
|
+
__metadata("design:returntype", void 0)
|
|
328
|
+
], LiveTailGateway.prototype, "handleUnsubscribeConsole", null);
|
|
329
|
+
exports.LiveTailGateway = LiveTailGateway = LiveTailGateway_1 = __decorate([
|
|
330
|
+
(0, websockets_1.WebSocketGateway)({ namespace: '/live-tail', cors: { origin: '*' } }),
|
|
331
|
+
__param(1, (0, common_1.Inject)(livetail_interface_1.LIVETAIL_CONFIG)),
|
|
332
|
+
__param(2, (0, common_1.Optional)()),
|
|
333
|
+
__metadata("design:paramtypes", [livetail_service_1.LiveTailService, Object, console_capture_service_1.ConsoleCaptureService])
|
|
334
|
+
], LiveTailGateway);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { LiveTailConfig } from './interfaces/livetail.interface';
|
|
3
|
+
export declare class LiveTailModule {
|
|
4
|
+
/**
|
|
5
|
+
* Register the live tail module with static config.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // Default (all options)
|
|
9
|
+
* LiveTailModule.register()
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // Custom configuration
|
|
13
|
+
* LiveTailModule.register({
|
|
14
|
+
* cors: ['https://dashboard.example.com'],
|
|
15
|
+
* maxClients: 100,
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Disable in specific environments
|
|
20
|
+
* LiveTailModule.register({
|
|
21
|
+
* disabled: process.env.NODE_ENV === 'test',
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // Stream raw stdout/stderr (terminal console) to the dashboard
|
|
26
|
+
* LiveTailModule.register({
|
|
27
|
+
* captureConsole: {
|
|
28
|
+
* bufferSize: 2000,
|
|
29
|
+
* token: process.env.LIVETAIL_CONSOLE_TOKEN,
|
|
30
|
+
* },
|
|
31
|
+
* })
|
|
32
|
+
*/
|
|
33
|
+
static register(config?: LiveTailConfig): DynamicModule;
|
|
34
|
+
/**
|
|
35
|
+
* Register the live tail module with async config (e.g. from ConfigService).
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* LiveTailModule.registerAsync({
|
|
39
|
+
* inject: [ConfigService],
|
|
40
|
+
* useFactory: (config: ConfigService) => ({
|
|
41
|
+
* cors: config.get('LIVETAIL_CORS_ORIGIN', '*'),
|
|
42
|
+
* maxClients: parseInt(config.get('LIVETAIL_MAX_CLIENTS', '0')),
|
|
43
|
+
* disabled: config.get('LIVETAIL_DISABLED') === 'true',
|
|
44
|
+
* }),
|
|
45
|
+
* })
|
|
46
|
+
*/
|
|
47
|
+
static registerAsync(options: {
|
|
48
|
+
inject?: any[];
|
|
49
|
+
useFactory: (...args: any[]) => LiveTailConfig | Promise<LiveTailConfig>;
|
|
50
|
+
imports?: any[];
|
|
51
|
+
}): DynamicModule;
|
|
52
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
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 LiveTailModule_1;
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.LiveTailModule = void 0;
|
|
11
|
+
const common_1 = require("@nestjs/common");
|
|
12
|
+
const livetail_interface_1 = require("./interfaces/livetail.interface");
|
|
13
|
+
const livetail_service_1 = require("./livetail.service");
|
|
14
|
+
const livetail_gateway_1 = require("./livetail.gateway");
|
|
15
|
+
const console_capture_service_1 = require("./console-capture.service");
|
|
16
|
+
const DEFAULT_CONFIG = {
|
|
17
|
+
namespace: '/live-tail',
|
|
18
|
+
cors: '*',
|
|
19
|
+
maxClients: 0,
|
|
20
|
+
pingInterval: 25000,
|
|
21
|
+
pingTimeout: 20000,
|
|
22
|
+
disabled: false,
|
|
23
|
+
};
|
|
24
|
+
let LiveTailModule = LiveTailModule_1 = class LiveTailModule {
|
|
25
|
+
/**
|
|
26
|
+
* Register the live tail module with static config.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Default (all options)
|
|
30
|
+
* LiveTailModule.register()
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // Custom configuration
|
|
34
|
+
* LiveTailModule.register({
|
|
35
|
+
* cors: ['https://dashboard.example.com'],
|
|
36
|
+
* maxClients: 100,
|
|
37
|
+
* })
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Disable in specific environments
|
|
41
|
+
* LiveTailModule.register({
|
|
42
|
+
* disabled: process.env.NODE_ENV === 'test',
|
|
43
|
+
* })
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Stream raw stdout/stderr (terminal console) to the dashboard
|
|
47
|
+
* LiveTailModule.register({
|
|
48
|
+
* captureConsole: {
|
|
49
|
+
* bufferSize: 2000,
|
|
50
|
+
* token: process.env.LIVETAIL_CONSOLE_TOKEN,
|
|
51
|
+
* },
|
|
52
|
+
* })
|
|
53
|
+
*/
|
|
54
|
+
static register(config) {
|
|
55
|
+
const merged = { ...DEFAULT_CONFIG, ...config };
|
|
56
|
+
return {
|
|
57
|
+
module: LiveTailModule_1,
|
|
58
|
+
providers: [
|
|
59
|
+
{ provide: livetail_interface_1.LIVETAIL_CONFIG, useValue: merged },
|
|
60
|
+
livetail_service_1.LiveTailService,
|
|
61
|
+
console_capture_service_1.ConsoleCaptureService,
|
|
62
|
+
...(merged.disabled ? [] : [livetail_gateway_1.LiveTailGateway]),
|
|
63
|
+
],
|
|
64
|
+
exports: [livetail_service_1.LiveTailService, console_capture_service_1.ConsoleCaptureService],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Register the live tail module with async config (e.g. from ConfigService).
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* LiveTailModule.registerAsync({
|
|
72
|
+
* inject: [ConfigService],
|
|
73
|
+
* useFactory: (config: ConfigService) => ({
|
|
74
|
+
* cors: config.get('LIVETAIL_CORS_ORIGIN', '*'),
|
|
75
|
+
* maxClients: parseInt(config.get('LIVETAIL_MAX_CLIENTS', '0')),
|
|
76
|
+
* disabled: config.get('LIVETAIL_DISABLED') === 'true',
|
|
77
|
+
* }),
|
|
78
|
+
* })
|
|
79
|
+
*/
|
|
80
|
+
static registerAsync(options) {
|
|
81
|
+
return {
|
|
82
|
+
module: LiveTailModule_1,
|
|
83
|
+
imports: [...(options.imports || [])],
|
|
84
|
+
providers: [
|
|
85
|
+
{
|
|
86
|
+
provide: livetail_interface_1.LIVETAIL_CONFIG,
|
|
87
|
+
inject: options.inject || [],
|
|
88
|
+
useFactory: async (...args) => {
|
|
89
|
+
const config = await options.useFactory(...args);
|
|
90
|
+
return { ...DEFAULT_CONFIG, ...config };
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
livetail_service_1.LiveTailService,
|
|
94
|
+
console_capture_service_1.ConsoleCaptureService,
|
|
95
|
+
livetail_gateway_1.LiveTailGateway,
|
|
96
|
+
],
|
|
97
|
+
exports: [livetail_service_1.LiveTailService, console_capture_service_1.ConsoleCaptureService],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
exports.LiveTailModule = LiveTailModule;
|
|
102
|
+
exports.LiveTailModule = LiveTailModule = LiveTailModule_1 = __decorate([
|
|
103
|
+
(0, common_1.Global)(),
|
|
104
|
+
(0, common_1.Module)({})
|
|
105
|
+
], LiveTailModule);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
import { LiveTailConfig, LiveTailEnvContext, LiveTailLogEvent } from './interfaces/livetail.interface';
|
|
3
|
+
export declare class LiveTailService {
|
|
4
|
+
private readonly logSubject;
|
|
5
|
+
private readonly isDisabled;
|
|
6
|
+
constructor(config?: LiveTailConfig);
|
|
7
|
+
/**
|
|
8
|
+
* Observable stream of all log events.
|
|
9
|
+
* The gateway subscribes to this to broadcast to connected clients.
|
|
10
|
+
*/
|
|
11
|
+
get logs$(): Observable<LiveTailLogEvent>;
|
|
12
|
+
/**
|
|
13
|
+
* Broadcast a single log event to all connected live tail clients.
|
|
14
|
+
* Call this after persisting the log to the database.
|
|
15
|
+
*
|
|
16
|
+
* @param log The log event to broadcast.
|
|
17
|
+
* @param ctx Optional environment context to enrich the log with org/app/env info.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* this.liveTailService.broadcast(logData, {
|
|
21
|
+
* environmentId: env.id,
|
|
22
|
+
* envName: env.name,
|
|
23
|
+
* appId: app.id,
|
|
24
|
+
* appName: app.name,
|
|
25
|
+
* orgId: org.id,
|
|
26
|
+
* orgName: org.name,
|
|
27
|
+
* });
|
|
28
|
+
*/
|
|
29
|
+
broadcast(log: LiveTailLogEvent, ctx?: LiveTailEnvContext): void;
|
|
30
|
+
/**
|
|
31
|
+
* Broadcast multiple log events at once.
|
|
32
|
+
*
|
|
33
|
+
* @param logs Array of log events.
|
|
34
|
+
* @param ctx Optional environment context to enrich all logs.
|
|
35
|
+
*/
|
|
36
|
+
broadcastMany(logs: LiveTailLogEvent[], ctx?: LiveTailEnvContext): void;
|
|
37
|
+
private enrichLog;
|
|
38
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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.LiveTailService = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const rxjs_1 = require("rxjs");
|
|
18
|
+
const livetail_interface_1 = require("./interfaces/livetail.interface");
|
|
19
|
+
let LiveTailService = class LiveTailService {
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.logSubject = new rxjs_1.Subject();
|
|
22
|
+
this.isDisabled = config?.disabled ?? false;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Observable stream of all log events.
|
|
26
|
+
* The gateway subscribes to this to broadcast to connected clients.
|
|
27
|
+
*/
|
|
28
|
+
get logs$() {
|
|
29
|
+
return this.logSubject.asObservable();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Broadcast a single log event to all connected live tail clients.
|
|
33
|
+
* Call this after persisting the log to the database.
|
|
34
|
+
*
|
|
35
|
+
* @param log The log event to broadcast.
|
|
36
|
+
* @param ctx Optional environment context to enrich the log with org/app/env info.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* this.liveTailService.broadcast(logData, {
|
|
40
|
+
* environmentId: env.id,
|
|
41
|
+
* envName: env.name,
|
|
42
|
+
* appId: app.id,
|
|
43
|
+
* appName: app.name,
|
|
44
|
+
* orgId: org.id,
|
|
45
|
+
* orgName: org.name,
|
|
46
|
+
* });
|
|
47
|
+
*/
|
|
48
|
+
broadcast(log, ctx) {
|
|
49
|
+
if (this.isDisabled)
|
|
50
|
+
return;
|
|
51
|
+
this.logSubject.next(ctx ? this.enrichLog(log, ctx) : log);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Broadcast multiple log events at once.
|
|
55
|
+
*
|
|
56
|
+
* @param logs Array of log events.
|
|
57
|
+
* @param ctx Optional environment context to enrich all logs.
|
|
58
|
+
*/
|
|
59
|
+
broadcastMany(logs, ctx) {
|
|
60
|
+
if (this.isDisabled)
|
|
61
|
+
return;
|
|
62
|
+
for (const log of logs) {
|
|
63
|
+
this.logSubject.next(ctx ? this.enrichLog(log, ctx) : log);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
enrichLog(log, ctx) {
|
|
67
|
+
return {
|
|
68
|
+
...log,
|
|
69
|
+
environmentId: log.environmentId || ctx.environmentId,
|
|
70
|
+
envName: log.envName || ctx.envName,
|
|
71
|
+
appId: log.appId || ctx.appId,
|
|
72
|
+
appName: log.appName || ctx.appName,
|
|
73
|
+
orgId: log.orgId || ctx.orgId,
|
|
74
|
+
orgName: log.orgName || ctx.orgName,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
exports.LiveTailService = LiveTailService;
|
|
79
|
+
exports.LiveTailService = LiveTailService = __decorate([
|
|
80
|
+
(0, common_1.Injectable)(),
|
|
81
|
+
__param(0, (0, common_1.Optional)()),
|
|
82
|
+
__param(0, (0, common_1.Inject)(livetail_interface_1.LIVETAIL_CONFIG)),
|
|
83
|
+
__metadata("design:paramtypes", [Object])
|
|
84
|
+
], LiveTailService);
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devms/livetail",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "NestJS WebSocket live tail module for real-time log streaming. Integrates with any NestJS application to broadcast logs via Socket.IO.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"prepublishOnly": "tsc"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"nestjs",
|
|
17
|
+
"websocket",
|
|
18
|
+
"live-tail",
|
|
19
|
+
"logging",
|
|
20
|
+
"real-time",
|
|
21
|
+
"socket.io",
|
|
22
|
+
"monitoring",
|
|
23
|
+
"observability"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
|
31
|
+
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
|
32
|
+
"@nestjs/websockets": "^10.0.0 || ^11.0.0",
|
|
33
|
+
"@nestjs/platform-socket.io": "^10.0.0 || ^11.0.0",
|
|
34
|
+
"rxjs": "^7.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@nestjs/common": "^11.1.3",
|
|
38
|
+
"@nestjs/core": "^11.1.3",
|
|
39
|
+
"@nestjs/websockets": "^11.0.0",
|
|
40
|
+
"@nestjs/platform-socket.io": "^11.0.0",
|
|
41
|
+
"@types/node": "^24.0.0",
|
|
42
|
+
"rxjs": "^7.8.2",
|
|
43
|
+
"socket.io": "^4.8.0",
|
|
44
|
+
"typescript": "^5.8.3"
|
|
45
|
+
}
|
|
46
|
+
}
|