@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
package/README.md
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
# @devms/livetail
|
|
2
|
+
|
|
3
|
+
NestJS WebSocket module for **real-time log streaming**. Drop it into any NestJS application to broadcast structured logs to connected clients via Socket.IO — like `tail -f` for your application logs.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @devms/livetail
|
|
11
|
+
# or
|
|
12
|
+
npm install @devms/livetail
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Peer dependencies** (install if not already present):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @nestjs/websockets @nestjs/platform-socket.io
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Register the module
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// app.module.ts
|
|
29
|
+
import { LiveTailModule } from '@devms/livetail';
|
|
30
|
+
|
|
31
|
+
@Module({
|
|
32
|
+
imports: [
|
|
33
|
+
LiveTailModule.register(),
|
|
34
|
+
],
|
|
35
|
+
})
|
|
36
|
+
export class AppModule {}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Broadcast logs from your service
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { LiveTailService } from '@devms/livetail';
|
|
43
|
+
|
|
44
|
+
@Injectable()
|
|
45
|
+
export class LogIngestionService {
|
|
46
|
+
constructor(private readonly liveTail: LiveTailService) {}
|
|
47
|
+
|
|
48
|
+
async ingestLog(log: CreateLogDto) {
|
|
49
|
+
// Save to database
|
|
50
|
+
await this.db.logs.create({ data: log });
|
|
51
|
+
|
|
52
|
+
// Broadcast to live tail clients
|
|
53
|
+
this.liveTail.broadcast({
|
|
54
|
+
environmentId: log.environmentId,
|
|
55
|
+
level: log.level,
|
|
56
|
+
category: log.category,
|
|
57
|
+
action: log.action,
|
|
58
|
+
message: log.message,
|
|
59
|
+
metadata: log.metadata,
|
|
60
|
+
userId: log.userId,
|
|
61
|
+
duration: log.duration,
|
|
62
|
+
tags: log.tags,
|
|
63
|
+
createdAt: new Date().toISOString(),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. Connect from the browser
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { io } from 'socket.io-client';
|
|
73
|
+
|
|
74
|
+
const socket = io('http://localhost:3000/live-tail');
|
|
75
|
+
|
|
76
|
+
socket.on('connected', ({ clientId }) => {
|
|
77
|
+
console.log('Connected:', clientId);
|
|
78
|
+
|
|
79
|
+
// Subscribe with filters
|
|
80
|
+
socket.emit('subscribe', {
|
|
81
|
+
environmentId: 'env-abc-123',
|
|
82
|
+
level: ['error', 'fatal'],
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
socket.on('log', (log) => {
|
|
87
|
+
console.log(`[${log.level}] ${log.category}/${log.action}: ${log.message}`);
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
### Static config
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
LiveTailModule.register({
|
|
99
|
+
namespace: '/live-tail', // WebSocket namespace (default: '/live-tail')
|
|
100
|
+
cors: '*', // CORS origin (default: '*')
|
|
101
|
+
maxClients: 100, // Max simultaneous connections, 0 = unlimited (default: 0)
|
|
102
|
+
pingInterval: 25000, // Ping interval in ms (default: 25000)
|
|
103
|
+
pingTimeout: 20000, // Ping timeout in ms (default: 20000)
|
|
104
|
+
disabled: false, // Disable the gateway entirely (default: false)
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Async config (with ConfigService)
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
LiveTailModule.registerAsync({
|
|
112
|
+
inject: [ConfigService],
|
|
113
|
+
useFactory: (config: ConfigService) => ({
|
|
114
|
+
cors: config.get('LIVETAIL_CORS_ORIGIN', '*'),
|
|
115
|
+
maxClients: parseInt(config.get('LIVETAIL_MAX_CLIENTS', '0')),
|
|
116
|
+
disabled: config.get('LIVETAIL_DISABLED') === 'true',
|
|
117
|
+
}),
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Environment-specific usage
|
|
122
|
+
|
|
123
|
+
Disable live tail in environments where it's not needed:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
LiveTailModule.register({
|
|
127
|
+
disabled: process.env.NODE_ENV === 'test',
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Configuration options
|
|
132
|
+
|
|
133
|
+
| Option | Type | Default | Description |
|
|
134
|
+
|----------------|-------------------------------|----------------|----------------------------------------------------------------|
|
|
135
|
+
| `namespace` | `string` | `'/live-tail'` | WebSocket namespace path |
|
|
136
|
+
| `cors` | `string \| string[] \| bool` | `'*'` | CORS origin for WebSocket connections |
|
|
137
|
+
| `maxClients` | `number` | `0` | Max simultaneous clients (0 = unlimited) |
|
|
138
|
+
| `pingInterval` | `number` | `25000` | Socket.IO ping interval in ms |
|
|
139
|
+
| `pingTimeout` | `number` | `20000` | Socket.IO ping timeout in ms |
|
|
140
|
+
| `disabled` | `boolean` | `false` | Disable the gateway (service still injectable but no-ops) |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Environment Context (Enrichment)
|
|
145
|
+
|
|
146
|
+
When broadcasting logs, you can pass an environment context to enrich each log event with organization, application, and environment names. This allows the UI to display human-readable labels without additional API calls.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { LiveTailService, LiveTailEnvContext } from '@devms/livetail';
|
|
150
|
+
|
|
151
|
+
@Injectable()
|
|
152
|
+
export class AppLogService {
|
|
153
|
+
constructor(private readonly liveTail: LiveTailService) {}
|
|
154
|
+
|
|
155
|
+
async ingestLogs(envId: string, logs: LogDto[], ctx: LiveTailEnvContext) {
|
|
156
|
+
await this.db.appLog.createMany({ data: logs });
|
|
157
|
+
|
|
158
|
+
// Broadcast with enriched context
|
|
159
|
+
this.liveTail.broadcastMany(
|
|
160
|
+
logs.map((log) => ({
|
|
161
|
+
environmentId: envId,
|
|
162
|
+
level: log.level,
|
|
163
|
+
category: log.category,
|
|
164
|
+
action: log.action,
|
|
165
|
+
message: log.message,
|
|
166
|
+
createdAt: new Date().toISOString(),
|
|
167
|
+
})),
|
|
168
|
+
ctx, // <-- enriches each log with orgName, appName, envName, etc.
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### LiveTailEnvContext
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
interface LiveTailEnvContext {
|
|
178
|
+
environmentId: string;
|
|
179
|
+
envName?: string;
|
|
180
|
+
appId?: string;
|
|
181
|
+
appName?: string;
|
|
182
|
+
orgId?: string;
|
|
183
|
+
orgName?: string;
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## WebSocket Protocol
|
|
190
|
+
|
|
191
|
+
### Endpoint
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
ws://<host>:<port>/live-tail
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Client events (send to server)
|
|
198
|
+
|
|
199
|
+
| Event | Payload | Description |
|
|
200
|
+
|----------------|------------------|----------------------------------------------------|
|
|
201
|
+
| `subscribe` | `LiveTailFilter` | Start receiving logs matching the given filter |
|
|
202
|
+
| `updateFilter` | `LiveTailFilter` | Update filters without reconnecting |
|
|
203
|
+
| `pause` | — | Pause the stream (stay connected, no logs sent) |
|
|
204
|
+
| `resume` | — | Resume receiving logs |
|
|
205
|
+
|
|
206
|
+
### Server events (receive from server)
|
|
207
|
+
|
|
208
|
+
| Event | Payload | Description |
|
|
209
|
+
|-----------------|------------------------------------------------|----------------------------------|
|
|
210
|
+
| `connected` | `{ clientId, message, connectedClients }` | Connection confirmed |
|
|
211
|
+
| `subscribed` | `{ filter }` | Subscription confirmed |
|
|
212
|
+
| `filterUpdated` | `{ filter }` | Filter update confirmed |
|
|
213
|
+
| `paused` | — | Stream paused |
|
|
214
|
+
| `resumed` | — | Stream resumed |
|
|
215
|
+
| `log` | `LiveTailLogEvent` | A log entry matching your filter |
|
|
216
|
+
| `error` | `{ message }` | Connection error (e.g. max clients reached) |
|
|
217
|
+
|
|
218
|
+
### LiveTailFilter
|
|
219
|
+
|
|
220
|
+
All fields are optional. Omit a field to accept all values for that dimension.
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
interface LiveTailFilter {
|
|
224
|
+
environmentId?: string; // Filter by specific environment
|
|
225
|
+
appId?: string; // Filter by application
|
|
226
|
+
orgId?: string; // Filter by organization
|
|
227
|
+
level?: LogLevel | LogLevel[]; // 'debug' | 'info' | 'warn' | 'error' | 'fatal'
|
|
228
|
+
category?: string; // Exact match on category
|
|
229
|
+
action?: string; // Contains match (case-insensitive)
|
|
230
|
+
userId?: string; // Exact match on userId
|
|
231
|
+
search?: string; // Full-text search in message, action, category
|
|
232
|
+
tags?: string[]; // Log must have at least one matching tag
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### LiveTailLogEvent
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
interface LiveTailLogEvent {
|
|
240
|
+
id?: string;
|
|
241
|
+
environmentId: string;
|
|
242
|
+
level: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
243
|
+
category: string;
|
|
244
|
+
action: string;
|
|
245
|
+
message?: string;
|
|
246
|
+
metadata?: any;
|
|
247
|
+
userId?: string;
|
|
248
|
+
duration?: number; // milliseconds
|
|
249
|
+
tags?: string[];
|
|
250
|
+
createdAt: string; // ISO 8601
|
|
251
|
+
|
|
252
|
+
// Enriched fields (from environment context)
|
|
253
|
+
orgId?: string;
|
|
254
|
+
orgName?: string;
|
|
255
|
+
appId?: string;
|
|
256
|
+
appName?: string;
|
|
257
|
+
envName?: string;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Integration Examples
|
|
264
|
+
|
|
265
|
+
### NestJS (Monitoring API)
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// app.module.ts
|
|
269
|
+
import { LiveTailModule } from '@devms/livetail';
|
|
270
|
+
|
|
271
|
+
@Module({
|
|
272
|
+
imports: [
|
|
273
|
+
LiveTailModule.register({
|
|
274
|
+
cors: ['https://dashboard.example.com'],
|
|
275
|
+
maxClients: 200,
|
|
276
|
+
}),
|
|
277
|
+
],
|
|
278
|
+
})
|
|
279
|
+
export class AppModule {}
|
|
280
|
+
|
|
281
|
+
// app-log.controller.ts
|
|
282
|
+
@Post('ingest')
|
|
283
|
+
@UseGuards(ClientCredentialsGuard)
|
|
284
|
+
async ingest(@Body() dto: IngestDto, @EnvContext() ctx: EnvironmentContext) {
|
|
285
|
+
return this.appLogService.ingestLogs(ctx.environment.id, dto.logs, {
|
|
286
|
+
environmentId: ctx.environment.id,
|
|
287
|
+
envName: ctx.environment.name,
|
|
288
|
+
appId: ctx.application.id,
|
|
289
|
+
appName: ctx.application.name,
|
|
290
|
+
orgId: ctx.organization.id,
|
|
291
|
+
orgName: ctx.organization.name,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// app-log.service.ts
|
|
296
|
+
import { LiveTailService, LiveTailEnvContext } from '@devms/livetail';
|
|
297
|
+
|
|
298
|
+
@Injectable()
|
|
299
|
+
export class AppLogService {
|
|
300
|
+
constructor(
|
|
301
|
+
private readonly prisma: PrismaService,
|
|
302
|
+
@Optional() private readonly liveTail?: LiveTailService,
|
|
303
|
+
) {}
|
|
304
|
+
|
|
305
|
+
async ingestLogs(envId: string, logs: AppLogDto[], ctx?: LiveTailEnvContext) {
|
|
306
|
+
const result = await this.prisma.appLog.createMany({ data: logs });
|
|
307
|
+
|
|
308
|
+
this.liveTail?.broadcastMany(
|
|
309
|
+
logs.map((log) => ({
|
|
310
|
+
environmentId: envId,
|
|
311
|
+
level: log.level as any,
|
|
312
|
+
category: log.category,
|
|
313
|
+
action: log.action,
|
|
314
|
+
message: log.message,
|
|
315
|
+
metadata: log.metadata,
|
|
316
|
+
userId: log.userId,
|
|
317
|
+
duration: log.duration,
|
|
318
|
+
tags: log.tags ?? [],
|
|
319
|
+
createdAt: new Date().toISOString(),
|
|
320
|
+
})),
|
|
321
|
+
ctx,
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
return { ingested: result.count };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### React / Next.js (Browser Client)
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
import { useEffect, useRef, useState } from 'react';
|
|
333
|
+
import { io, Socket } from 'socket.io-client';
|
|
334
|
+
|
|
335
|
+
function useLiveTail(apiUrl: string) {
|
|
336
|
+
const [logs, setLogs] = useState([]);
|
|
337
|
+
const [status, setStatus] = useState('disconnected');
|
|
338
|
+
const socketRef = useRef<Socket | null>(null);
|
|
339
|
+
|
|
340
|
+
const connect = (filter = {}) => {
|
|
341
|
+
const socket = io(`${apiUrl}/live-tail`, {
|
|
342
|
+
transports: ['websocket', 'polling'],
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
socket.on('connect', () => {
|
|
346
|
+
setStatus('connected');
|
|
347
|
+
socket.emit('subscribe', filter);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
socket.on('disconnect', () => setStatus('disconnected'));
|
|
351
|
+
|
|
352
|
+
socket.on('log', (log) => {
|
|
353
|
+
setLogs((prev) => [log, ...prev].slice(0, 500));
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
socketRef.current = socket;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const disconnect = () => {
|
|
360
|
+
socketRef.current?.disconnect();
|
|
361
|
+
setStatus('disconnected');
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
useEffect(() => () => { socketRef.current?.disconnect(); }, []);
|
|
365
|
+
|
|
366
|
+
return { logs, status, connect, disconnect };
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Python (WebSocket Client)
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
import socketio
|
|
374
|
+
|
|
375
|
+
sio = socketio.Client()
|
|
376
|
+
|
|
377
|
+
@sio.on('connected', namespace='/live-tail')
|
|
378
|
+
def on_connect(data):
|
|
379
|
+
print(f"Connected: {data['clientId']}")
|
|
380
|
+
sio.emit('subscribe', {
|
|
381
|
+
'environmentId': 'env-abc-123',
|
|
382
|
+
'level': ['error', 'fatal'],
|
|
383
|
+
}, namespace='/live-tail')
|
|
384
|
+
|
|
385
|
+
@sio.on('log', namespace='/live-tail')
|
|
386
|
+
def on_log(data):
|
|
387
|
+
print(f"[{data['level']}] {data['category']}/{data['action']}: {data.get('message', '')}")
|
|
388
|
+
|
|
389
|
+
sio.connect('http://localhost:5002', namespaces=['/live-tail'])
|
|
390
|
+
sio.wait()
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### cURL (WebSocket test with wscat)
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
# Install wscat
|
|
397
|
+
npm install -g wscat
|
|
398
|
+
|
|
399
|
+
# Connect (note: Socket.IO uses its own protocol, wscat is for raw WS)
|
|
400
|
+
# For Socket.IO, use a proper client. For testing, use the browser console:
|
|
401
|
+
# const socket = io('http://localhost:5002/live-tail');
|
|
402
|
+
# socket.on('log', console.log);
|
|
403
|
+
# socket.emit('subscribe', { level: ['error'] });
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## Console Capture (raw stdout/stderr streaming)
|
|
409
|
+
|
|
410
|
+
Stream the **raw terminal output** of your application — everything the
|
|
411
|
+
process writes to stdout/stderr (NestJS `Logger`, `console.log`, crash stack
|
|
412
|
+
traces, third-party output, ANSI colors included) — like `docker logs -f`,
|
|
413
|
+
but over the same `/live-tail` socket. Nothing is persisted; lines are held
|
|
414
|
+
in an in-memory ring buffer and replayed to clients on subscribe.
|
|
415
|
+
|
|
416
|
+
### Enable it
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
// app.module.ts
|
|
420
|
+
LiveTailModule.register({
|
|
421
|
+
captureConsole: true, // defaults: 2000-line buffer, 150ms batching
|
|
422
|
+
})
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Or with options:
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
LiveTailModule.register({
|
|
429
|
+
captureConsole: {
|
|
430
|
+
bufferSize: 2000, // lines kept in memory & replayed on subscribe
|
|
431
|
+
maxLineLength: 8192, // longer lines are truncated
|
|
432
|
+
batchInterval: 150, // ms between batched pushes to clients
|
|
433
|
+
token: process.env.LIVETAIL_CONSOLE_TOKEN, // require a shared secret
|
|
434
|
+
stripAnsi: false, // keep colors (dashboard renders them)
|
|
435
|
+
},
|
|
436
|
+
})
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
That's it — no logger changes needed. The module tees `process.stdout` /
|
|
440
|
+
`process.stderr`; your terminal/PM2/Docker output is untouched.
|
|
441
|
+
|
|
442
|
+
### Client protocol
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
const socket = io('http://localhost:5002/live-tail');
|
|
446
|
+
|
|
447
|
+
// Subscribe (replays the buffer, then streams live)
|
|
448
|
+
socket.emit('subscribe-console', { token: '...', tail: 1000 });
|
|
449
|
+
|
|
450
|
+
socket.on('console-subscribed', ({ pid, bufferSize, historyCount }) => {});
|
|
451
|
+
socket.on('console-history', ({ lines, done }) => {}); // chunked replay
|
|
452
|
+
socket.on('console-lines', ({ lines, dropped }) => {}); // live batches
|
|
453
|
+
socket.on('console-error', ({ code, message }) => {}); // disabled / bad token
|
|
454
|
+
|
|
455
|
+
socket.emit('unsubscribe-console');
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Each line: `{ seq: number, stream: 'stdout' | 'stderr', line: string, ts: string }`.
|
|
459
|
+
|
|
460
|
+
### Performance & safety
|
|
461
|
+
|
|
462
|
+
- **Zero subscribers → near-zero cost.** Lines only go to the ring buffer;
|
|
463
|
+
nothing is broadcast.
|
|
464
|
+
- **Batched delivery.** Live lines are flushed every `batchInterval` ms as a
|
|
465
|
+
single frame, and the pending queue is hard-capped (oldest dropped, with a
|
|
466
|
+
`dropped` count reported) so a slow consumer can never grow memory.
|
|
467
|
+
- **Crash-safe tee.** The original write always executes first; any error in
|
|
468
|
+
capture is swallowed and can never break the app's own output.
|
|
469
|
+
- **Security.** Raw console output can contain secrets (connection strings,
|
|
470
|
+
error dumps). Set `token` in production and restrict `cors`.
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## Environment Variables
|
|
475
|
+
|
|
476
|
+
| Variable | Description | Default |
|
|
477
|
+
|--------------------------|--------------------------------------------|---------|
|
|
478
|
+
| `LIVETAIL_CORS_ORIGIN` | Allowed CORS origins (comma-separated) | `*` |
|
|
479
|
+
| `LIVETAIL_MAX_CLIENTS` | Max simultaneous WebSocket connections | `0` |
|
|
480
|
+
| `LIVETAIL_DISABLED` | Set to `true` to disable the gateway | `false` |
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Architecture
|
|
485
|
+
|
|
486
|
+
```
|
|
487
|
+
┌──────────────┐ HTTP POST ┌──────────────┐ save ┌──────────────┐
|
|
488
|
+
│ Client App │ ──────────────────▶│ NestJS API │ ────────────▶│ PostgreSQL │
|
|
489
|
+
│ (any lang) │ /app-logs/ingest │ │ │ │
|
|
490
|
+
└──────────────┘ │ AppLogSvc │ └──────────────┘
|
|
491
|
+
│ │ │
|
|
492
|
+
│ ▼ │
|
|
493
|
+
│ LiveTailSvc │
|
|
494
|
+
│ broadcast() │
|
|
495
|
+
│ │ │
|
|
496
|
+
│ ▼ │
|
|
497
|
+
│ LiveTailGW │ WebSocket
|
|
498
|
+
│ (Socket.IO) │ ◀──────────── Browser / CLI
|
|
499
|
+
└──────────────┘ /live-tail
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
1. **Client apps** send logs via HTTP to the API (using `@devms/applog-client` or raw HTTP)
|
|
503
|
+
2. **API** saves logs to the database, then calls `liveTailService.broadcast()`
|
|
504
|
+
3. **LiveTailGateway** pushes matching logs to connected WebSocket clients in real-time
|
|
505
|
+
4. **Browser/CLI** connects to `/live-tail` namespace, subscribes with filters, receives logs instantly
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## License
|
|
510
|
+
|
|
511
|
+
MIT
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { ConsoleCaptureConfig, ConsoleLineEvent, LiveTailConfig } from './interfaces/livetail.interface';
|
|
4
|
+
/**
|
|
5
|
+
* Tees process.stdout / process.stderr into an in-memory ring buffer
|
|
6
|
+
* and an observable stream, without altering what reaches the real
|
|
7
|
+
* terminal. The original write always runs first and its result is
|
|
8
|
+
* returned unchanged — a failure in capture can never break the app's
|
|
9
|
+
* own output.
|
|
10
|
+
*
|
|
11
|
+
* Overhead with no subscribers is a string split + ring buffer push
|
|
12
|
+
* per write. Broadcasting cost only exists while clients are tailing.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ConsoleCaptureService implements OnModuleInit, OnModuleDestroy {
|
|
15
|
+
private readonly logger;
|
|
16
|
+
private readonly cfg;
|
|
17
|
+
private readonly lineSubject;
|
|
18
|
+
private readonly ring;
|
|
19
|
+
private ringIndex;
|
|
20
|
+
private ringCount;
|
|
21
|
+
private seq;
|
|
22
|
+
private started;
|
|
23
|
+
private capturing;
|
|
24
|
+
private originalStdout?;
|
|
25
|
+
private originalStderr?;
|
|
26
|
+
private readonly carry;
|
|
27
|
+
constructor(config?: LiveTailConfig);
|
|
28
|
+
onModuleInit(): void;
|
|
29
|
+
onModuleDestroy(): void;
|
|
30
|
+
/** Live stream of captured lines. */
|
|
31
|
+
get lines$(): Observable<ConsoleLineEvent>;
|
|
32
|
+
get enabled(): boolean;
|
|
33
|
+
get options(): Readonly<Required<ConsoleCaptureConfig>>;
|
|
34
|
+
/** Total lines captured since process start. */
|
|
35
|
+
get totalCaptured(): number;
|
|
36
|
+
/**
|
|
37
|
+
* Recent lines from the ring buffer, oldest first.
|
|
38
|
+
* @param limit Return at most this many of the newest lines.
|
|
39
|
+
*/
|
|
40
|
+
getHistory(limit?: number): ConsoleLineEvent[];
|
|
41
|
+
clear(): void;
|
|
42
|
+
start(): void;
|
|
43
|
+
stop(): void;
|
|
44
|
+
private tee;
|
|
45
|
+
private ingest;
|
|
46
|
+
}
|