@graphty/remote-logger 0.0.1 → 1.2.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/README.md +944 -28
- package/bin/remote-log-server.js +3 -0
- package/dist/client/RemoteLogClient.d.ts +116 -0
- package/dist/client/RemoteLogClient.d.ts.map +1 -0
- package/dist/client/RemoteLogClient.js +269 -0
- package/dist/client/RemoteLogClient.js.map +1 -0
- package/dist/client/index.d.ts +7 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +6 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/types.d.ts +60 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +6 -0
- package/dist/client/types.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +9 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +9 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/mcp-server.d.ts +32 -0
- package/dist/mcp/mcp-server.d.ts.map +1 -0
- package/dist/mcp/mcp-server.js +270 -0
- package/dist/mcp/mcp-server.js.map +1 -0
- package/dist/mcp/tools/index.d.ts +14 -0
- package/dist/mcp/tools/index.d.ts.map +1 -0
- package/dist/mcp/tools/index.js +14 -0
- package/dist/mcp/tools/index.js.map +1 -0
- package/dist/mcp/tools/logs-clear.d.ts +76 -0
- package/dist/mcp/tools/logs-clear.d.ts.map +1 -0
- package/dist/mcp/tools/logs-clear.js +58 -0
- package/dist/mcp/tools/logs-clear.js.map +1 -0
- package/dist/mcp/tools/logs-get-all.d.ts +60 -0
- package/dist/mcp/tools/logs-get-all.d.ts.map +1 -0
- package/dist/mcp/tools/logs-get-all.js +50 -0
- package/dist/mcp/tools/logs-get-all.js.map +1 -0
- package/dist/mcp/tools/logs-get-errors.d.ts +65 -0
- package/dist/mcp/tools/logs-get-errors.d.ts.map +1 -0
- package/dist/mcp/tools/logs-get-errors.js +46 -0
- package/dist/mcp/tools/logs-get-errors.js.map +1 -0
- package/dist/mcp/tools/logs-get-file-path.d.ts +75 -0
- package/dist/mcp/tools/logs-get-file-path.d.ts.map +1 -0
- package/dist/mcp/tools/logs-get-file-path.js +90 -0
- package/dist/mcp/tools/logs-get-file-path.js.map +1 -0
- package/dist/mcp/tools/logs-get-recent.d.ts +89 -0
- package/dist/mcp/tools/logs-get-recent.d.ts.map +1 -0
- package/dist/mcp/tools/logs-get-recent.js +74 -0
- package/dist/mcp/tools/logs-get-recent.js.map +1 -0
- package/dist/mcp/tools/logs-list-sessions.d.ts +64 -0
- package/dist/mcp/tools/logs-list-sessions.d.ts.map +1 -0
- package/dist/mcp/tools/logs-list-sessions.js +48 -0
- package/dist/mcp/tools/logs-list-sessions.js.map +1 -0
- package/dist/mcp/tools/logs-receive.d.ts +150 -0
- package/dist/mcp/tools/logs-receive.d.ts.map +1 -0
- package/dist/mcp/tools/logs-receive.js +68 -0
- package/dist/mcp/tools/logs-receive.js.map +1 -0
- package/dist/mcp/tools/logs-search.d.ts +91 -0
- package/dist/mcp/tools/logs-search.d.ts.map +1 -0
- package/dist/mcp/tools/logs-search.js +68 -0
- package/dist/mcp/tools/logs-search.js.map +1 -0
- package/dist/mcp/tools/logs-status.d.ts +45 -0
- package/dist/mcp/tools/logs-status.d.ts.map +1 -0
- package/dist/mcp/tools/logs-status.js +45 -0
- package/dist/mcp/tools/logs-status.js.map +1 -0
- package/dist/server/dual-server.d.ts +76 -0
- package/dist/server/dual-server.d.ts.map +1 -0
- package/dist/server/dual-server.js +214 -0
- package/dist/server/dual-server.js.map +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +12 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/jsonl-writer.d.ts +93 -0
- package/dist/server/jsonl-writer.d.ts.map +1 -0
- package/dist/server/jsonl-writer.js +205 -0
- package/dist/server/jsonl-writer.js.map +1 -0
- package/dist/server/log-server.d.ts +126 -0
- package/dist/server/log-server.d.ts.map +1 -0
- package/dist/server/log-server.js +589 -0
- package/dist/server/log-server.js.map +1 -0
- package/dist/server/log-storage.d.ts +301 -0
- package/dist/server/log-storage.d.ts.map +1 -0
- package/dist/server/log-storage.js +408 -0
- package/dist/server/log-storage.js.map +1 -0
- package/dist/server/marker-utils.d.ts +69 -0
- package/dist/server/marker-utils.d.ts.map +1 -0
- package/dist/server/marker-utils.js +118 -0
- package/dist/server/marker-utils.js.map +1 -0
- package/dist/server/self-signed-cert.d.ts +30 -0
- package/dist/server/self-signed-cert.d.ts.map +1 -0
- package/dist/server/self-signed-cert.js +83 -0
- package/dist/server/self-signed-cert.js.map +1 -0
- package/dist/ui/ConsoleCaptureUI.d.ts +118 -0
- package/dist/ui/ConsoleCaptureUI.d.ts.map +1 -0
- package/dist/ui/ConsoleCaptureUI.js +571 -0
- package/dist/ui/ConsoleCaptureUI.js.map +1 -0
- package/dist/ui/index.d.ts +15 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/vite/index.d.ts +8 -0
- package/dist/vite/index.d.ts.map +1 -0
- package/dist/vite/index.js +8 -0
- package/dist/vite/index.js.map +1 -0
- package/dist/vite/plugin.d.ts +42 -0
- package/dist/vite/plugin.d.ts.map +1 -0
- package/dist/vite/plugin.js +46 -0
- package/dist/vite/plugin.js.map +1 -0
- package/package.json +90 -7
- package/src/client/RemoteLogClient.ts +328 -0
- package/src/client/index.ts +7 -0
- package/src/client/types.ts +62 -0
- package/src/index.ts +28 -0
- package/src/mcp/index.ts +25 -0
- package/src/mcp/mcp-server.ts +364 -0
- package/src/mcp/tools/index.ts +69 -0
- package/src/mcp/tools/logs-clear.ts +86 -0
- package/src/mcp/tools/logs-get-all.ts +78 -0
- package/src/mcp/tools/logs-get-errors.ts +71 -0
- package/src/mcp/tools/logs-get-file-path.ts +121 -0
- package/src/mcp/tools/logs-get-recent.ts +104 -0
- package/src/mcp/tools/logs-list-sessions.ts +71 -0
- package/src/mcp/tools/logs-receive.ts +96 -0
- package/src/mcp/tools/logs-search.ts +95 -0
- package/src/mcp/tools/logs-status.ts +69 -0
- package/src/server/dual-server.ts +308 -0
- package/src/server/index.ts +54 -0
- package/src/server/jsonl-writer.ts +277 -0
- package/src/server/log-server.ts +763 -0
- package/src/server/log-storage.ts +651 -0
- package/src/server/marker-utils.ts +144 -0
- package/src/server/self-signed-cert.ts +93 -0
- package/src/ui/ConsoleCaptureUI.ts +649 -0
- package/src/ui/index.ts +15 -0
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin.ts +59 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dual server orchestration for running HTTP and MCP interfaces simultaneously.
|
|
3
|
+
*
|
|
4
|
+
* This module provides a unified way to start both HTTP and MCP servers
|
|
5
|
+
* that share the same log storage instance.
|
|
6
|
+
* @module server/dual-server
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import type * as http from "http";
|
|
12
|
+
import type * as https from "https";
|
|
13
|
+
import * as net from "net";
|
|
14
|
+
import * as os from "os";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
|
|
17
|
+
import { createMcpServer } from "../mcp/mcp-server.js";
|
|
18
|
+
import { JsonlWriter } from "./jsonl-writer.js";
|
|
19
|
+
import { createLogServer } from "./log-server.js";
|
|
20
|
+
import { LogStorage, type ServerMode } from "./log-storage.js";
|
|
21
|
+
import { certFilesExist } from "./self-signed-cert.js";
|
|
22
|
+
|
|
23
|
+
// ANSI color codes for terminal output
|
|
24
|
+
const colors = {
|
|
25
|
+
reset: "\x1b[0m",
|
|
26
|
+
yellow: "\x1b[33m",
|
|
27
|
+
red: "\x1b[31m",
|
|
28
|
+
green: "\x1b[32m",
|
|
29
|
+
cyan: "\x1b[36m",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Maximum number of ports to try when scanning for available port */
|
|
33
|
+
const MAX_PORT_SCAN_ATTEMPTS = 100;
|
|
34
|
+
|
|
35
|
+
/** Maximum port number allowed (ports 9000-9099 per project guidelines) */
|
|
36
|
+
const MAX_PORT_NUMBER = 9099;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if a port is available for binding.
|
|
40
|
+
* @param port - Port number to check
|
|
41
|
+
* @param host - Host to bind to
|
|
42
|
+
* @returns Promise resolving to true if port is available, false otherwise
|
|
43
|
+
*/
|
|
44
|
+
async function isPortAvailable(port: number, host: string): Promise<boolean> {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
const server = net.createServer();
|
|
47
|
+
|
|
48
|
+
// Enable SO_REUSEADDR to allow faster port reuse after server shutdown
|
|
49
|
+
server.once("error", (err: NodeJS.ErrnoException) => {
|
|
50
|
+
if (err.code === "EADDRINUSE") {
|
|
51
|
+
resolve(false);
|
|
52
|
+
} else {
|
|
53
|
+
// Other errors (permission, etc.) - port is not usable
|
|
54
|
+
resolve(false);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
server.once("listening", () => {
|
|
59
|
+
server.close(() => {
|
|
60
|
+
resolve(true);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Set SO_REUSEADDR before binding
|
|
65
|
+
server.listen({ port, host, exclusive: false });
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Find an available port starting from the given base port.
|
|
71
|
+
* Increments port number until an available port is found.
|
|
72
|
+
* @param basePort - Starting port number
|
|
73
|
+
* @param host - Host to bind to
|
|
74
|
+
* @param quiet - Suppress output messages
|
|
75
|
+
* @returns Promise resolving to available port number
|
|
76
|
+
* @throws Error if no available port found within MAX_PORT_SCAN_ATTEMPTS
|
|
77
|
+
*/
|
|
78
|
+
export async function findAvailablePort(basePort: number, host: string, quiet: boolean = false): Promise<number> {
|
|
79
|
+
let port = basePort;
|
|
80
|
+
let attempts = 0;
|
|
81
|
+
|
|
82
|
+
while (attempts < MAX_PORT_SCAN_ATTEMPTS) {
|
|
83
|
+
if (port > MAX_PORT_NUMBER) {
|
|
84
|
+
// Wrap around if we exceed max port (shouldn't happen normally)
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (await isPortAvailable(port, host)) {
|
|
89
|
+
if (port !== basePort && !quiet) {
|
|
90
|
+
// eslint-disable-next-line no-console
|
|
91
|
+
console.log(
|
|
92
|
+
`${colors.yellow}Port ${basePort} in use, using port ${port} instead${colors.reset}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return port;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!quiet && attempts === 0) {
|
|
99
|
+
// eslint-disable-next-line no-console
|
|
100
|
+
console.log(
|
|
101
|
+
`${colors.yellow}Port ${port} is already in use, scanning for available port...${colors.reset}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
port++;
|
|
106
|
+
attempts++;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// No available port found
|
|
110
|
+
const errorMsg = `Could not find available port after ${MAX_PORT_SCAN_ATTEMPTS} attempts starting from ${basePort}. ` +
|
|
111
|
+
`Ports ${basePort}-${port - 1} are all in use. ` +
|
|
112
|
+
`Try killing existing processes: pkill -f "remote-log-server"`;
|
|
113
|
+
|
|
114
|
+
if (!quiet) {
|
|
115
|
+
|
|
116
|
+
console.error(`${colors.red}${errorMsg}${colors.reset}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw new Error(errorMsg);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Options for creating a dual server.
|
|
124
|
+
*/
|
|
125
|
+
export interface DualServerOptions {
|
|
126
|
+
/** Port for HTTP server (default: 9080) */
|
|
127
|
+
httpPort?: number;
|
|
128
|
+
/** Host for HTTP server (default: localhost) */
|
|
129
|
+
httpHost?: string;
|
|
130
|
+
/** Enable MCP server (default: true) */
|
|
131
|
+
mcpEnabled?: boolean;
|
|
132
|
+
/** Enable HTTP server (default: true) */
|
|
133
|
+
httpEnabled?: boolean;
|
|
134
|
+
/** Suppress terminal output (default: false) */
|
|
135
|
+
quiet?: boolean;
|
|
136
|
+
/** Path to SSL certificate file (HTTPS only used if both certPath and keyPath provided) */
|
|
137
|
+
certPath?: string;
|
|
138
|
+
/** Path to SSL private key file (HTTPS only used if both certPath and keyPath provided) */
|
|
139
|
+
keyPath?: string;
|
|
140
|
+
/** Path to file for writing logs (optional) */
|
|
141
|
+
logFile?: string;
|
|
142
|
+
/** External LogStorage instance (optional, will create one if not provided) */
|
|
143
|
+
storage?: LogStorage;
|
|
144
|
+
/** External JSONL writer instance (optional, will create one if not provided) */
|
|
145
|
+
jsonlWriter?: JsonlWriter;
|
|
146
|
+
/** Only serve /log POST and /health GET endpoints (default: false) */
|
|
147
|
+
logReceiveOnly?: boolean;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Result of creating a dual server.
|
|
152
|
+
*/
|
|
153
|
+
export interface DualServerResult {
|
|
154
|
+
/** HTTP or HTTPS server instance (if httpEnabled) */
|
|
155
|
+
httpServer?: http.Server | https.Server;
|
|
156
|
+
/** MCP server instance (if mcpEnabled) */
|
|
157
|
+
mcpServer?: McpServer;
|
|
158
|
+
/** Shared log storage instance */
|
|
159
|
+
storage: LogStorage;
|
|
160
|
+
/** JSONL writer instance */
|
|
161
|
+
jsonlWriter?: JsonlWriter;
|
|
162
|
+
/** Actual HTTP port used (may differ from requested if port was in use) */
|
|
163
|
+
httpPort?: number;
|
|
164
|
+
/** Graceful shutdown function */
|
|
165
|
+
shutdown: () => Promise<void>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Create and start a dual server with HTTP and/or MCP interfaces.
|
|
170
|
+
*
|
|
171
|
+
* By default, both HTTP and MCP are enabled. Use httpEnabled/mcpEnabled
|
|
172
|
+
* to control which interfaces to start.
|
|
173
|
+
* @param options - Server configuration options
|
|
174
|
+
* @returns Promise resolving to the dual server result
|
|
175
|
+
*/
|
|
176
|
+
export async function createDualServer(options: DualServerOptions = {}): Promise<DualServerResult> {
|
|
177
|
+
const {
|
|
178
|
+
httpPort: requestedPort = 9080,
|
|
179
|
+
httpHost = "localhost",
|
|
180
|
+
mcpEnabled = true,
|
|
181
|
+
httpEnabled = true,
|
|
182
|
+
quiet = false,
|
|
183
|
+
storage: externalStorage,
|
|
184
|
+
jsonlWriter: externalJsonlWriter,
|
|
185
|
+
logReceiveOnly = false,
|
|
186
|
+
certPath,
|
|
187
|
+
keyPath,
|
|
188
|
+
} = options;
|
|
189
|
+
|
|
190
|
+
// Create or use provided JSONL writer
|
|
191
|
+
const jsonlBaseDir = path.join(os.tmpdir(), "remote-logger");
|
|
192
|
+
const jsonlWriter = externalJsonlWriter ?? new JsonlWriter(jsonlBaseDir);
|
|
193
|
+
|
|
194
|
+
// Create or use provided storage
|
|
195
|
+
const storage = externalStorage ?? new LogStorage({ jsonlWriter });
|
|
196
|
+
|
|
197
|
+
let httpServer: http.Server | https.Server | undefined;
|
|
198
|
+
let mcpServer: McpServer | undefined;
|
|
199
|
+
let actualHttpPort: number | undefined;
|
|
200
|
+
|
|
201
|
+
// Start HTTP server if enabled
|
|
202
|
+
if (httpEnabled) {
|
|
203
|
+
// Find an available port starting from the requested port
|
|
204
|
+
actualHttpPort = await findAvailablePort(requestedPort, httpHost, quiet);
|
|
205
|
+
|
|
206
|
+
const result = createLogServer({
|
|
207
|
+
port: actualHttpPort,
|
|
208
|
+
host: httpHost,
|
|
209
|
+
storage,
|
|
210
|
+
quiet,
|
|
211
|
+
logReceiveOnly,
|
|
212
|
+
certPath,
|
|
213
|
+
keyPath,
|
|
214
|
+
});
|
|
215
|
+
httpServer = result.server;
|
|
216
|
+
|
|
217
|
+
// Wait for HTTP server to be listening
|
|
218
|
+
const serverToStart = httpServer;
|
|
219
|
+
const portToUse = actualHttpPort;
|
|
220
|
+
await new Promise<void>((resolve, reject) => {
|
|
221
|
+
serverToStart.on("error", (err: NodeJS.ErrnoException) => {
|
|
222
|
+
// Provide helpful error message for port conflicts
|
|
223
|
+
if (err.code === "EADDRINUSE") {
|
|
224
|
+
const errorMsg = `Port ${portToUse} is already in use. ` +
|
|
225
|
+
`This shouldn't happen after port scanning - there may be a race condition. ` +
|
|
226
|
+
`Try again or kill existing processes: pkill -f "remote-log-server"`;
|
|
227
|
+
if (!quiet) {
|
|
228
|
+
|
|
229
|
+
console.error(`${colors.red}${errorMsg}${colors.reset}`);
|
|
230
|
+
}
|
|
231
|
+
reject(new Error(errorMsg));
|
|
232
|
+
} else {
|
|
233
|
+
reject(err);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
serverToStart.listen({ port: portToUse, host: httpHost, exclusive: false }, () => {
|
|
237
|
+
serverToStart.removeListener("error", reject);
|
|
238
|
+
if (!quiet) {
|
|
239
|
+
// eslint-disable-next-line no-console
|
|
240
|
+
console.log(
|
|
241
|
+
`${colors.green}HTTP server listening on ${httpHost}:${portToUse}${colors.reset}`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
resolve();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Set server config in storage so MCP tools can report it
|
|
249
|
+
// Determine protocol based on whether valid cert files were provided
|
|
250
|
+
const useHttps = certPath && keyPath && certFilesExist(certPath, keyPath);
|
|
251
|
+
const protocol = useHttps ? "https" : "http";
|
|
252
|
+
// Determine mode based on configuration
|
|
253
|
+
let mode: ServerMode;
|
|
254
|
+
if (logReceiveOnly && mcpEnabled) {
|
|
255
|
+
mode = "mcp-only";
|
|
256
|
+
} else if (!mcpEnabled) {
|
|
257
|
+
mode = "http-only";
|
|
258
|
+
} else {
|
|
259
|
+
mode = "dual";
|
|
260
|
+
}
|
|
261
|
+
storage.setServerConfig({
|
|
262
|
+
httpPort: actualHttpPort,
|
|
263
|
+
httpHost,
|
|
264
|
+
protocol,
|
|
265
|
+
httpEndpoint: `${protocol}://${httpHost}:${actualHttpPort}/log`,
|
|
266
|
+
mode,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Create and connect MCP server if enabled
|
|
271
|
+
if (mcpEnabled) {
|
|
272
|
+
mcpServer = createMcpServer(storage);
|
|
273
|
+
const transport = new StdioServerTransport();
|
|
274
|
+
await mcpServer.connect(transport);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Track if we created the JSONL writer internally
|
|
278
|
+
const ownsJsonlWriter = !externalJsonlWriter;
|
|
279
|
+
|
|
280
|
+
// Shutdown function
|
|
281
|
+
const shutdown = async (): Promise<void> => {
|
|
282
|
+
// Close HTTP server
|
|
283
|
+
if (httpServer?.listening) {
|
|
284
|
+
await new Promise<void>((resolve) => {
|
|
285
|
+
httpServer.close(() => { resolve(); });
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Close JSONL writer only if we created it internally
|
|
290
|
+
if (ownsJsonlWriter) {
|
|
291
|
+
await jsonlWriter.close();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Stop cleanup timer if storage was created internally
|
|
295
|
+
if (!externalStorage) {
|
|
296
|
+
storage.stopCleanupTimer();
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
httpServer,
|
|
302
|
+
mcpServer,
|
|
303
|
+
storage,
|
|
304
|
+
jsonlWriter,
|
|
305
|
+
httpPort: actualHttpPort,
|
|
306
|
+
shutdown,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side logging utilities.
|
|
3
|
+
* These are only meant to be run in Node.js, not in the browser.
|
|
4
|
+
* @module server
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
createDualServer,
|
|
9
|
+
type DualServerOptions,
|
|
10
|
+
type DualServerResult,
|
|
11
|
+
} from "./dual-server.js";
|
|
12
|
+
export {
|
|
13
|
+
type FileStats,
|
|
14
|
+
type JsonlEntry,
|
|
15
|
+
JsonlWriter,
|
|
16
|
+
} from "./jsonl-writer.js";
|
|
17
|
+
export {
|
|
18
|
+
clearLogs,
|
|
19
|
+
createLogServer,
|
|
20
|
+
type CreateLogServerOptions,
|
|
21
|
+
type CreateLogServerResult,
|
|
22
|
+
getJsonlWriter,
|
|
23
|
+
getLogStorage,
|
|
24
|
+
HELP_TEXT,
|
|
25
|
+
type LogEntry,
|
|
26
|
+
type LogServerOptions,
|
|
27
|
+
main,
|
|
28
|
+
parseArgs,
|
|
29
|
+
type ParseArgsResult,
|
|
30
|
+
setLogStorage,
|
|
31
|
+
startLogServer,
|
|
32
|
+
} from "./log-server.js";
|
|
33
|
+
export {
|
|
34
|
+
type AddLogsOptions,
|
|
35
|
+
type ClearFilter,
|
|
36
|
+
type HealthStatus,
|
|
37
|
+
type LogEntryWithSession,
|
|
38
|
+
type LogFilter,
|
|
39
|
+
LogStorage,
|
|
40
|
+
type LogStorageOptions,
|
|
41
|
+
type SearchOptions,
|
|
42
|
+
type ServerConfig,
|
|
43
|
+
type ServerMode,
|
|
44
|
+
type ServerStatus,
|
|
45
|
+
type SessionFilter,
|
|
46
|
+
type SessionMetadata,
|
|
47
|
+
} from "./log-storage.js";
|
|
48
|
+
export {
|
|
49
|
+
extractMarkerFromPath,
|
|
50
|
+
extractMarkerFromSessionId,
|
|
51
|
+
type MarkerResolutionOptions,
|
|
52
|
+
resolveProjectMarker,
|
|
53
|
+
} from "./marker-utils.js";
|
|
54
|
+
export { certFilesExist, type GeneratedCert, generateSelfSignedCert, readCertFiles } from "./self-signed-cert.js";
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSONL file writer for streaming logs to disk.
|
|
3
|
+
*
|
|
4
|
+
* Organizes logs by project marker in the temp directory:
|
|
5
|
+
* {baseDir}/{marker}/logs.jsonl
|
|
6
|
+
* @module server/jsonl-writer
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A JSONL log entry.
|
|
14
|
+
*/
|
|
15
|
+
export interface JsonlEntry {
|
|
16
|
+
/** ISO 8601 timestamp when the log was created */
|
|
17
|
+
time: string;
|
|
18
|
+
/** Log level (e.g., "INFO", "DEBUG", "WARN", "ERROR") */
|
|
19
|
+
level: string;
|
|
20
|
+
/** The log message */
|
|
21
|
+
message: string;
|
|
22
|
+
/** Session ID this log belongs to */
|
|
23
|
+
sessionId: string;
|
|
24
|
+
/** Optional additional data */
|
|
25
|
+
data?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* File statistics.
|
|
30
|
+
*/
|
|
31
|
+
export interface FileStats {
|
|
32
|
+
/** Whether the file exists */
|
|
33
|
+
exists: boolean;
|
|
34
|
+
/** File size in bytes */
|
|
35
|
+
size: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Internal state for a marker's file handle.
|
|
40
|
+
*/
|
|
41
|
+
interface MarkerState {
|
|
42
|
+
/** File descriptor */
|
|
43
|
+
fd: number;
|
|
44
|
+
/** Pending writes buffer */
|
|
45
|
+
buffer: string[];
|
|
46
|
+
/** Write lock to serialize writes */
|
|
47
|
+
writeLock: Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* JSONL file writer that streams log entries to disk.
|
|
52
|
+
*
|
|
53
|
+
* Each project marker gets its own log file in the structure:
|
|
54
|
+
* {baseDir}/{marker}/logs.jsonl
|
|
55
|
+
*/
|
|
56
|
+
export class JsonlWriter {
|
|
57
|
+
private readonly baseDir: string;
|
|
58
|
+
private readonly markerStates = new Map<string, MarkerState>();
|
|
59
|
+
private closed = false;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a new JsonlWriter.
|
|
63
|
+
* @param baseDir - Base directory for log files (e.g., os.tmpdir()/remote-logger)
|
|
64
|
+
*/
|
|
65
|
+
constructor(baseDir: string) {
|
|
66
|
+
this.baseDir = baseDir;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the file path for a project marker.
|
|
71
|
+
* @param projectMarker - The project marker
|
|
72
|
+
* @returns Full path to the JSONL file
|
|
73
|
+
*/
|
|
74
|
+
getFilePath(projectMarker: string): string {
|
|
75
|
+
return path.join(this.baseDir, projectMarker, "logs.jsonl");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get file statistics for a project marker's log file.
|
|
80
|
+
* @param projectMarker - The project marker
|
|
81
|
+
* @returns File stats or null if file doesn't exist
|
|
82
|
+
*/
|
|
83
|
+
getFileStats(projectMarker: string): FileStats | null {
|
|
84
|
+
const filePath = this.getFilePath(projectMarker);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const stats = fs.statSync(filePath);
|
|
88
|
+
return {
|
|
89
|
+
exists: true,
|
|
90
|
+
size: stats.size,
|
|
91
|
+
};
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Write a log entry to the appropriate file.
|
|
99
|
+
* @param projectMarker - The project marker
|
|
100
|
+
* @param entry - The log entry to write
|
|
101
|
+
* @returns Promise that resolves when the write is queued
|
|
102
|
+
*/
|
|
103
|
+
write(projectMarker: string, entry: JsonlEntry): Promise<void> {
|
|
104
|
+
if (this.closed) {
|
|
105
|
+
return Promise.resolve();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const state = this.ensureMarkerState(projectMarker);
|
|
109
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
110
|
+
|
|
111
|
+
// Add to buffer and trigger write
|
|
112
|
+
state.buffer.push(line);
|
|
113
|
+
|
|
114
|
+
// Chain the write to maintain order
|
|
115
|
+
state.writeLock = state.writeLock.then(() => {
|
|
116
|
+
this.flushBuffer(state);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return Promise.resolve();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Flush all pending writes to disk.
|
|
124
|
+
*/
|
|
125
|
+
async flush(): Promise<void> {
|
|
126
|
+
const flushPromises: Promise<void>[] = [];
|
|
127
|
+
|
|
128
|
+
for (const state of this.markerStates.values()) {
|
|
129
|
+
// Wait for pending writes and flush buffer
|
|
130
|
+
flushPromises.push(state.writeLock.then(() => {
|
|
131
|
+
this.flushBuffer(state);
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await Promise.all(flushPromises);
|
|
136
|
+
|
|
137
|
+
// Sync all file descriptors
|
|
138
|
+
for (const state of this.markerStates.values()) {
|
|
139
|
+
try {
|
|
140
|
+
fs.fsyncSync(state.fd);
|
|
141
|
+
} catch {
|
|
142
|
+
// Ignore sync errors
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Close all file handles.
|
|
149
|
+
*/
|
|
150
|
+
async close(): Promise<void> {
|
|
151
|
+
if (this.closed) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.closed = true;
|
|
156
|
+
|
|
157
|
+
// Flush all pending writes
|
|
158
|
+
await this.flush();
|
|
159
|
+
|
|
160
|
+
// Close all file handles
|
|
161
|
+
for (const state of this.markerStates.values()) {
|
|
162
|
+
try {
|
|
163
|
+
fs.closeSync(state.fd);
|
|
164
|
+
} catch {
|
|
165
|
+
// Ignore close errors
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.markerStates.clear();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Ensure the marker state exists, creating file if needed.
|
|
174
|
+
* @param projectMarker - The project marker
|
|
175
|
+
* @returns The marker state
|
|
176
|
+
*/
|
|
177
|
+
private ensureMarkerState(projectMarker: string): MarkerState {
|
|
178
|
+
let state = this.markerStates.get(projectMarker);
|
|
179
|
+
|
|
180
|
+
if (!state) {
|
|
181
|
+
const filePath = this.getFilePath(projectMarker);
|
|
182
|
+
const dir = path.dirname(filePath);
|
|
183
|
+
|
|
184
|
+
// Create directory if it doesn't exist
|
|
185
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
186
|
+
|
|
187
|
+
// Open file for appending
|
|
188
|
+
const fd = fs.openSync(filePath, "a");
|
|
189
|
+
|
|
190
|
+
state = {
|
|
191
|
+
fd,
|
|
192
|
+
buffer: [],
|
|
193
|
+
writeLock: Promise.resolve(),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
this.markerStates.set(projectMarker, state);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return state;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Flush the buffer for a marker state.
|
|
204
|
+
* @param state - The marker state to flush
|
|
205
|
+
*/
|
|
206
|
+
private flushBuffer(state: MarkerState): void {
|
|
207
|
+
if (state.buffer.length === 0) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Take all buffered entries
|
|
212
|
+
const toWrite = state.buffer.join("");
|
|
213
|
+
state.buffer = [];
|
|
214
|
+
|
|
215
|
+
// Write to file
|
|
216
|
+
fs.writeSync(state.fd, toWrite);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Clean up old project log files based on directory modification time.
|
|
221
|
+
* Removes entire project directories that haven't been modified within the retention period.
|
|
222
|
+
* @param retentionDays - Number of days to retain logs
|
|
223
|
+
* @returns Number of project directories removed
|
|
224
|
+
*/
|
|
225
|
+
cleanupOldFiles(retentionDays: number): number {
|
|
226
|
+
// Check if base directory exists
|
|
227
|
+
if (!fs.existsSync(this.baseDir)) {
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const cutoffTime = new Date();
|
|
232
|
+
cutoffTime.setDate(cutoffTime.getDate() - retentionDays);
|
|
233
|
+
const cutoffMs = cutoffTime.getTime();
|
|
234
|
+
|
|
235
|
+
let removed = 0;
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const entries = fs.readdirSync(this.baseDir, { withFileTypes: true });
|
|
239
|
+
|
|
240
|
+
for (const entry of entries) {
|
|
241
|
+
if (!entry.isDirectory()) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const dirPath = path.join(this.baseDir, entry.name);
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const stats = fs.statSync(dirPath);
|
|
249
|
+
|
|
250
|
+
// Check if directory is older than retention period
|
|
251
|
+
if (stats.mtimeMs < cutoffMs) {
|
|
252
|
+
// Close file handle if we have one open for this marker
|
|
253
|
+
const state = this.markerStates.get(entry.name);
|
|
254
|
+
if (state) {
|
|
255
|
+
try {
|
|
256
|
+
fs.closeSync(state.fd);
|
|
257
|
+
} catch {
|
|
258
|
+
// Ignore close errors
|
|
259
|
+
}
|
|
260
|
+
this.markerStates.delete(entry.name);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Remove the directory
|
|
264
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
265
|
+
removed++;
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
// Ignore errors for individual directories
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
// Ignore errors reading base directory
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return removed;
|
|
276
|
+
}
|
|
277
|
+
}
|