@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.
@@ -0,0 +1,166 @@
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 ConsoleCaptureService_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.ConsoleCaptureService = void 0;
17
+ const common_1 = require("@nestjs/common");
18
+ const rxjs_1 = require("rxjs");
19
+ const livetail_interface_1 = require("./interfaces/livetail.interface");
20
+ // Matches ANSI SGR + cursor/erase escape sequences.
21
+ const ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
22
+ /**
23
+ * Tees process.stdout / process.stderr into an in-memory ring buffer
24
+ * and an observable stream, without altering what reaches the real
25
+ * terminal. The original write always runs first and its result is
26
+ * returned unchanged — a failure in capture can never break the app's
27
+ * own output.
28
+ *
29
+ * Overhead with no subscribers is a string split + ring buffer push
30
+ * per write. Broadcasting cost only exists while clients are tailing.
31
+ */
32
+ let ConsoleCaptureService = ConsoleCaptureService_1 = class ConsoleCaptureService {
33
+ constructor(config) {
34
+ this.logger = new common_1.Logger(ConsoleCaptureService_1.name);
35
+ this.lineSubject = new rxjs_1.Subject();
36
+ this.ringIndex = 0;
37
+ this.ringCount = 0;
38
+ this.seq = 0;
39
+ // Patch state
40
+ this.started = false;
41
+ this.capturing = false; // re-entrancy guard
42
+ this.carry = {
43
+ stdout: '',
44
+ stderr: '',
45
+ };
46
+ this.cfg = (0, livetail_interface_1.normalizeConsoleCaptureConfig)(config?.captureConsole);
47
+ this.ring = new Array(Math.max(this.cfg.bufferSize, 1));
48
+ }
49
+ onModuleInit() {
50
+ if (!this.cfg.enabled)
51
+ return;
52
+ this.start();
53
+ this.logger.log(`Console capture started (buffer: ${this.cfg.bufferSize} lines, batch: ${this.cfg.batchInterval}ms${this.cfg.token ? ', token-protected' : ''})`);
54
+ }
55
+ onModuleDestroy() {
56
+ this.stop();
57
+ this.lineSubject.complete();
58
+ }
59
+ /** Live stream of captured lines. */
60
+ get lines$() {
61
+ return this.lineSubject.asObservable();
62
+ }
63
+ get enabled() {
64
+ return this.cfg.enabled;
65
+ }
66
+ get options() {
67
+ return this.cfg;
68
+ }
69
+ /** Total lines captured since process start. */
70
+ get totalCaptured() {
71
+ return this.seq;
72
+ }
73
+ /**
74
+ * Recent lines from the ring buffer, oldest first.
75
+ * @param limit Return at most this many of the newest lines.
76
+ */
77
+ getHistory(limit) {
78
+ const size = this.ring.length;
79
+ const n = limit && limit > 0 ? Math.min(limit, this.ringCount) : this.ringCount;
80
+ const out = new Array(n);
81
+ for (let i = 0; i < n; i++) {
82
+ out[i] = this.ring[(this.ringIndex - n + i + 2 * size) % size];
83
+ }
84
+ return out;
85
+ }
86
+ clear() {
87
+ this.ringIndex = 0;
88
+ this.ringCount = 0;
89
+ this.ring.fill(undefined);
90
+ }
91
+ start() {
92
+ if (this.started)
93
+ return;
94
+ this.started = true;
95
+ this.originalStdout = process.stdout.write.bind(process.stdout);
96
+ this.originalStderr = process.stderr.write.bind(process.stderr);
97
+ process.stdout.write = this.tee('stdout', this.originalStdout);
98
+ process.stderr.write = this.tee('stderr', this.originalStderr);
99
+ }
100
+ stop() {
101
+ if (!this.started)
102
+ return;
103
+ this.started = false;
104
+ if (this.originalStdout)
105
+ process.stdout.write = this.originalStdout;
106
+ if (this.originalStderr)
107
+ process.stderr.write = this.originalStderr;
108
+ }
109
+ tee(stream, original) {
110
+ return (chunk, encoding, cb) => {
111
+ const result = original(chunk, encoding, cb);
112
+ if (this.capturing)
113
+ return result;
114
+ this.capturing = true;
115
+ try {
116
+ const text = typeof chunk === 'string'
117
+ ? chunk
118
+ : Buffer.isBuffer(chunk)
119
+ ? chunk.toString('utf8')
120
+ : String(chunk);
121
+ this.ingest(stream, text);
122
+ }
123
+ catch {
124
+ // Capture must never throw into the app's write path.
125
+ }
126
+ finally {
127
+ this.capturing = false;
128
+ }
129
+ return result;
130
+ };
131
+ }
132
+ ingest(stream, text) {
133
+ const data = this.carry[stream] + text;
134
+ const parts = data.split('\n');
135
+ this.carry[stream] = parts.pop() ?? '';
136
+ // Force-flush a partial line that grows beyond the limit
137
+ if (this.carry[stream].length > this.cfg.maxLineLength) {
138
+ parts.push(this.carry[stream]);
139
+ this.carry[stream] = '';
140
+ }
141
+ const ts = new Date().toISOString();
142
+ for (const raw of parts) {
143
+ let line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
144
+ if (line.length === 0)
145
+ continue;
146
+ if (this.cfg.stripAnsi)
147
+ line = line.replace(ANSI_RE, '');
148
+ if (line.length > this.cfg.maxLineLength) {
149
+ line = line.slice(0, this.cfg.maxLineLength) + ' …[truncated]';
150
+ }
151
+ const event = { seq: ++this.seq, stream, line, ts };
152
+ this.ring[this.ringIndex] = event;
153
+ this.ringIndex = (this.ringIndex + 1) % this.ring.length;
154
+ if (this.ringCount < this.ring.length)
155
+ this.ringCount++;
156
+ this.lineSubject.next(event);
157
+ }
158
+ }
159
+ };
160
+ exports.ConsoleCaptureService = ConsoleCaptureService;
161
+ exports.ConsoleCaptureService = ConsoleCaptureService = ConsoleCaptureService_1 = __decorate([
162
+ (0, common_1.Injectable)(),
163
+ __param(0, (0, common_1.Optional)()),
164
+ __param(0, (0, common_1.Inject)(livetail_interface_1.LIVETAIL_CONFIG)),
165
+ __metadata("design:paramtypes", [Object])
166
+ ], ConsoleCaptureService);
@@ -0,0 +1,5 @@
1
+ export { LiveTailModule } from './livetail.module';
2
+ export { LiveTailService } from './livetail.service';
3
+ export { LiveTailGateway } from './livetail.gateway';
4
+ export { ConsoleCaptureService } from './console-capture.service';
5
+ export { LiveTailConfig, LIVETAIL_CONFIG, LiveTailLogEvent, LiveTailFilter, LiveTailEnvContext, LogLevel, ConsoleCaptureConfig, ConsoleLineEvent, ConsoleStream, ConsoleSubscribePayload, } from './interfaces/livetail.interface';
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LIVETAIL_CONFIG = exports.ConsoleCaptureService = exports.LiveTailGateway = exports.LiveTailService = exports.LiveTailModule = void 0;
4
+ // Module
5
+ var livetail_module_1 = require("./livetail.module");
6
+ Object.defineProperty(exports, "LiveTailModule", { enumerable: true, get: function () { return livetail_module_1.LiveTailModule; } });
7
+ // Service (inject this to broadcast logs)
8
+ var livetail_service_1 = require("./livetail.service");
9
+ Object.defineProperty(exports, "LiveTailService", { enumerable: true, get: function () { return livetail_service_1.LiveTailService; } });
10
+ // Gateway (auto-registered by the module)
11
+ var livetail_gateway_1 = require("./livetail.gateway");
12
+ Object.defineProperty(exports, "LiveTailGateway", { enumerable: true, get: function () { return livetail_gateway_1.LiveTailGateway; } });
13
+ // Console capture (raw stdout/stderr streaming)
14
+ var console_capture_service_1 = require("./console-capture.service");
15
+ Object.defineProperty(exports, "ConsoleCaptureService", { enumerable: true, get: function () { return console_capture_service_1.ConsoleCaptureService; } });
16
+ // Interfaces & types
17
+ var livetail_interface_1 = require("./interfaces/livetail.interface");
18
+ Object.defineProperty(exports, "LIVETAIL_CONFIG", { enumerable: true, get: function () { return livetail_interface_1.LIVETAIL_CONFIG; } });
@@ -0,0 +1,156 @@
1
+ export declare const LIVETAIL_CONFIG: unique symbol;
2
+ export interface LiveTailConfig {
3
+ /**
4
+ * WebSocket namespace (default: '/live-tail')
5
+ */
6
+ namespace?: string;
7
+ /**
8
+ * CORS origin for WebSocket connections (default: '*')
9
+ */
10
+ cors?: string | string[] | boolean;
11
+ /**
12
+ * Max connected clients (0 = unlimited, default: 0).
13
+ * New connections are rejected when the limit is reached.
14
+ */
15
+ maxClients?: number;
16
+ /**
17
+ * Ping interval in ms (default: 25000)
18
+ */
19
+ pingInterval?: number;
20
+ /**
21
+ * Ping timeout in ms (default: 20000)
22
+ */
23
+ pingTimeout?: number;
24
+ /**
25
+ * Disable the live tail gateway entirely (default: false).
26
+ * Useful for environments where real-time streaming is not needed.
27
+ */
28
+ disabled?: boolean;
29
+ /**
30
+ * Capture the process stdout/stderr and stream it to subscribed
31
+ * clients — like `docker logs -f` for this application.
32
+ *
33
+ * Pass `true` for defaults, or an object for fine-grained control.
34
+ * Disabled by default.
35
+ */
36
+ captureConsole?: boolean | ConsoleCaptureConfig;
37
+ }
38
+ export interface ConsoleCaptureConfig {
39
+ /**
40
+ * Enable console capture (default: true when this object is provided).
41
+ */
42
+ enabled?: boolean;
43
+ /**
44
+ * Number of recent lines kept in the in-memory ring buffer and
45
+ * replayed to clients on subscribe (default: 2000).
46
+ */
47
+ bufferSize?: number;
48
+ /**
49
+ * Lines longer than this are truncated (default: 8192 chars).
50
+ */
51
+ maxLineLength?: number;
52
+ /**
53
+ * Lines are batched and flushed to clients on this interval in ms
54
+ * (default: 150). Keeps overhead low under heavy log volume.
55
+ */
56
+ batchInterval?: number;
57
+ /**
58
+ * Optional shared secret. When set, clients must send the same
59
+ * token in the `subscribe-console` payload or they are rejected.
60
+ * Strongly recommended in production — raw console output can
61
+ * contain secrets.
62
+ */
63
+ token?: string;
64
+ /**
65
+ * Strip ANSI escape codes before buffering/streaming
66
+ * (default: false — colors are kept and rendered by the dashboard).
67
+ */
68
+ stripAnsi?: boolean;
69
+ }
70
+ export type ConsoleStream = 'stdout' | 'stderr';
71
+ /**
72
+ * A single captured console line.
73
+ */
74
+ export interface ConsoleLineEvent {
75
+ /** Monotonic sequence number (per process lifetime). */
76
+ seq: number;
77
+ stream: ConsoleStream;
78
+ /** Raw line content. May contain ANSI color codes unless `stripAnsi` is set. */
79
+ line: string;
80
+ /** ISO 8601 capture timestamp. */
81
+ ts: string;
82
+ }
83
+ /**
84
+ * Payload a client sends with the `subscribe-console` message.
85
+ */
86
+ export interface ConsoleSubscribePayload {
87
+ /** Required when the server is configured with a console token. */
88
+ token?: string;
89
+ /** Replay at most this many recent lines (default: full buffer). */
90
+ tail?: number;
91
+ }
92
+ export declare function normalizeConsoleCaptureConfig(input?: boolean | ConsoleCaptureConfig): Required<ConsoleCaptureConfig>;
93
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
94
+ export interface LiveTailLogEvent {
95
+ id?: string;
96
+ environmentId: string;
97
+ level: LogLevel;
98
+ category: string;
99
+ action: string;
100
+ message?: string;
101
+ metadata?: any;
102
+ userId?: string;
103
+ duration?: number;
104
+ tags?: string[];
105
+ createdAt: string;
106
+ /**
107
+ * NestJS-style context label (e.g. 'AppController', 'AuthGuard').
108
+ * Falls back to `category` when not provided.
109
+ */
110
+ context?: string;
111
+ /**
112
+ * Process ID from the source application.
113
+ */
114
+ pid?: number;
115
+ /**
116
+ * HTTP request method (e.g. 'GET', 'POST', 'PUT', 'DELETE').
117
+ */
118
+ method?: string;
119
+ /**
120
+ * HTTP request path (e.g. '/api/users', '/auth/login').
121
+ */
122
+ path?: string;
123
+ /**
124
+ * HTTP response status code (e.g. 200, 404, 500).
125
+ */
126
+ statusCode?: number;
127
+ orgId?: string;
128
+ orgName?: string;
129
+ appId?: string;
130
+ appName?: string;
131
+ envName?: string;
132
+ }
133
+ export interface LiveTailFilter {
134
+ environmentId?: string;
135
+ appId?: string;
136
+ orgId?: string;
137
+ level?: LogLevel | LogLevel[];
138
+ category?: string;
139
+ action?: string;
140
+ userId?: string;
141
+ search?: string;
142
+ tags?: string[];
143
+ }
144
+ /**
145
+ * Context from the authenticated environment.
146
+ * Pass this to broadcastMany/broadcast to enrich log events
147
+ * with organization, application, and environment names.
148
+ */
149
+ export interface LiveTailEnvContext {
150
+ environmentId: string;
151
+ envName?: string;
152
+ appId?: string;
153
+ appName?: string;
154
+ orgId?: string;
155
+ orgName?: string;
156
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LIVETAIL_CONFIG = void 0;
4
+ exports.normalizeConsoleCaptureConfig = normalizeConsoleCaptureConfig;
5
+ exports.LIVETAIL_CONFIG = Symbol('LIVETAIL_CONFIG');
6
+ const CONSOLE_CAPTURE_DEFAULTS = {
7
+ enabled: false,
8
+ bufferSize: 2000,
9
+ maxLineLength: 8192,
10
+ batchInterval: 150,
11
+ token: '',
12
+ stripAnsi: false,
13
+ };
14
+ function normalizeConsoleCaptureConfig(input) {
15
+ if (input === true)
16
+ return { ...CONSOLE_CAPTURE_DEFAULTS, enabled: true };
17
+ if (!input)
18
+ return { ...CONSOLE_CAPTURE_DEFAULTS };
19
+ return {
20
+ ...CONSOLE_CAPTURE_DEFAULTS,
21
+ ...input,
22
+ enabled: input.enabled !== false,
23
+ };
24
+ }
@@ -0,0 +1,59 @@
1
+ import { OnModuleInit } from '@nestjs/common';
2
+ import { OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets';
3
+ import { Server, Socket } from 'socket.io';
4
+ import { LiveTailService } from './livetail.service';
5
+ import { ConsoleCaptureService } from './console-capture.service';
6
+ import { ConsoleSubscribePayload, LiveTailConfig, LiveTailFilter } from './interfaces/livetail.interface';
7
+ export declare class LiveTailGateway implements OnModuleInit, OnGatewayConnection, OnGatewayDisconnect {
8
+ private readonly liveTailService;
9
+ private readonly config;
10
+ private readonly consoleCapture?;
11
+ private readonly logger;
12
+ private readonly clients;
13
+ private subscription;
14
+ private consoleSubscription;
15
+ private readonly consoleSubscribers;
16
+ private consolePending;
17
+ private consoleDropped;
18
+ private consoleFlushTimer;
19
+ server: Server;
20
+ constructor(liveTailService: LiveTailService, config: LiveTailConfig, consoleCapture?: ConsoleCaptureService);
21
+ onModuleInit(): void;
22
+ onModuleDestroy(): void;
23
+ handleConnection(client: Socket): void;
24
+ handleDisconnect(client: Socket): void;
25
+ /**
26
+ * Client subscribes with filters.
27
+ * Example payload:
28
+ * { environmentId: "xxx", level: ["error", "fatal"], category: "auth" }
29
+ */
30
+ handleSubscribe(client: Socket, filter: LiveTailFilter): void;
31
+ /**
32
+ * Client updates filters without disconnecting.
33
+ */
34
+ handleUpdateFilter(client: Socket, filter: LiveTailFilter): void;
35
+ /**
36
+ * Client pauses the stream (still connected but no logs sent).
37
+ */
38
+ handlePause(client: Socket): void;
39
+ /**
40
+ * Client resumes the stream.
41
+ */
42
+ handleResume(client: Socket): void;
43
+ /**
44
+ * Client subscribes to the raw stdout/stderr stream.
45
+ * Replays the ring buffer (chunked), then streams live lines
46
+ * batched on `console-lines` events.
47
+ *
48
+ * Payload: { token?: string, tail?: number }
49
+ */
50
+ handleSubscribeConsole(client: Socket, payload?: ConsoleSubscribePayload): void;
51
+ handleUnsubscribeConsole(client: Socket): void;
52
+ private queueConsoleLine;
53
+ private flushConsolePending;
54
+ private startConsoleFlusher;
55
+ private stopConsoleFlusher;
56
+ private stopConsoleFlusherIfIdle;
57
+ private broadcastToClients;
58
+ private matchesFilter;
59
+ }