@craft-ts/log-server 0.7.0-beta.15
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 +44 -0
- package/dist/http-server.d.ts +12 -0
- package/dist/http-server.js +102 -0
- package/dist/http-server.js.map +1 -0
- package/dist/log-entry.d.ts +40 -0
- package/dist/log-entry.js +58 -0
- package/dist/log-entry.js.map +1 -0
- package/dist/log-store.d.ts +28 -0
- package/dist/log-store.js +73 -0
- package/dist/log-store.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +29 -0
- package/dist/main.js.map +1 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Local log server
|
|
2
|
+
|
|
3
|
+
Receives logs from the demo application over HTTP and appends them to a local
|
|
4
|
+
JSONL file. It does nothing else — reading the logs back is the job of
|
|
5
|
+
[`@craft-ts/log-mcp`](../../packages/log-mcp/README.md).
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm run logs:server
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Endpoints
|
|
12
|
+
|
|
13
|
+
| Method | Path | Description |
|
|
14
|
+
| -------- | --------- | -------------------------------------------------- |
|
|
15
|
+
| `POST` | `/logs` | Ingest a batch. Answers `202 {"accepted": n}`. |
|
|
16
|
+
| `DELETE` | `/logs` | Delete the active and rotated files. |
|
|
17
|
+
| `GET` | `/health` | Target file and its current size. |
|
|
18
|
+
|
|
19
|
+
`POST /logs` accepts three body shapes: a `{ clientId, entries: [...] }`
|
|
20
|
+
envelope (what the demo sends), a bare array of entries, or a single entry.
|
|
21
|
+
Entries missing a known `level` are dropped individually — one bad entry never
|
|
22
|
+
rejects the whole batch. CORS is wide open: this is a loopback dev tool.
|
|
23
|
+
|
|
24
|
+
## Storage
|
|
25
|
+
|
|
26
|
+
One JSON object per line in `.logs/app.jsonl`, oldest first. Each stored entry
|
|
27
|
+
keeps the browser `timestamp` and adds a server-side `receivedAt` plus an
|
|
28
|
+
incrementing `seq`. Once the active file exceeds `LOG_SERVER_MAX_FILE_SIZE` it
|
|
29
|
+
is rotated to `app.jsonl.1`, existing rotated files shift down one slot, and
|
|
30
|
+
anything past `LOG_SERVER_MAX_FILES` is dropped.
|
|
31
|
+
|
|
32
|
+
## Environment
|
|
33
|
+
|
|
34
|
+
| Variable | Default | Description |
|
|
35
|
+
| -------------------------- | ----------- | --------------------------------- |
|
|
36
|
+
| `LOG_SERVER_HOST` | `127.0.0.1` | Listen address |
|
|
37
|
+
| `LOG_SERVER_PORT` | `4319` | Listen port |
|
|
38
|
+
| `LOG_SERVER_DIR` | `./.logs` | Storage directory |
|
|
39
|
+
| `LOG_SERVER_MAX_FILE_SIZE` | `5242880` | Rotation threshold, in bytes |
|
|
40
|
+
| `LOG_SERVER_MAX_FILES` | `5` | Rotated files kept |
|
|
41
|
+
| `LOG_SERVER_QUIET` | unset | Set to `1` to silence ingest echo |
|
|
42
|
+
|
|
43
|
+
`LOG_SERVER_DIR` and `LOG_SERVER_MAX_FILES` must match what the MCP server uses,
|
|
44
|
+
otherwise it reads a different set of files.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Server } from 'node:http';
|
|
2
|
+
import type { LogStore } from './log-store.js';
|
|
3
|
+
export type LogHttpServerOptions = {
|
|
4
|
+
readonly store: LogStore;
|
|
5
|
+
readonly host?: string;
|
|
6
|
+
readonly port?: number;
|
|
7
|
+
/** Reject bodies larger than this, in bytes. */
|
|
8
|
+
readonly maxBodySize?: number;
|
|
9
|
+
/** Called after every accepted batch; used for console echo. */
|
|
10
|
+
readonly onIngest?: (count: number) => void;
|
|
11
|
+
};
|
|
12
|
+
export declare function createLogHttpServer(options: LogHttpServerOptions): Server;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { parseBatch } from './log-entry.js';
|
|
3
|
+
const DEFAULT_MAX_BODY_SIZE = 2 * 1024 * 1024;
|
|
4
|
+
// Dev-only server bound to the loopback interface: any local origin (the demo
|
|
5
|
+
// dev server, storybook, an e2e run) is allowed to post.
|
|
6
|
+
const CORS_HEADERS = {
|
|
7
|
+
'access-control-allow-origin': '*',
|
|
8
|
+
'access-control-allow-methods': 'POST, GET, DELETE, OPTIONS',
|
|
9
|
+
'access-control-allow-headers': 'content-type',
|
|
10
|
+
'access-control-max-age': '86400',
|
|
11
|
+
};
|
|
12
|
+
export function createLogHttpServer(options) {
|
|
13
|
+
const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
|
|
14
|
+
return createServer((request, response) => {
|
|
15
|
+
void handleRequest(request, response, options, maxBodySize).catch((error) => {
|
|
16
|
+
sendJson(response, 500, {
|
|
17
|
+
error: error instanceof Error ? error.message : String(error),
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
async function handleRequest(request, response, options, maxBodySize) {
|
|
23
|
+
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
24
|
+
if (request.method === 'OPTIONS') {
|
|
25
|
+
response.writeHead(204, CORS_HEADERS);
|
|
26
|
+
response.end();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (request.method === 'GET' && url.pathname === '/health') {
|
|
30
|
+
sendJson(response, 200, {
|
|
31
|
+
status: 'ok',
|
|
32
|
+
file: options.store.filePath,
|
|
33
|
+
size: options.store.size(),
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (request.method === 'DELETE' && url.pathname === '/logs') {
|
|
38
|
+
options.store.clear();
|
|
39
|
+
sendJson(response, 200, { cleared: true });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (request.method === 'POST' && url.pathname === '/logs') {
|
|
43
|
+
let body;
|
|
44
|
+
try {
|
|
45
|
+
body = await readBody(request, maxBodySize);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
sendJson(response, 413, {
|
|
49
|
+
error: error instanceof Error ? error.message : String(error),
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let payload;
|
|
54
|
+
try {
|
|
55
|
+
payload = JSON.parse(body);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
sendJson(response, 400, { error: 'Invalid JSON body' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const entries = parseBatch(payload);
|
|
62
|
+
options.store.append(entries);
|
|
63
|
+
options.onIngest?.(entries.length);
|
|
64
|
+
sendJson(response, 202, { accepted: entries.length });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
sendJson(response, 404, { error: 'Not found' });
|
|
68
|
+
}
|
|
69
|
+
function readBody(request, maxBodySize) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const chunks = [];
|
|
72
|
+
let size = 0;
|
|
73
|
+
let overflowed = false;
|
|
74
|
+
request.on('data', (chunk) => {
|
|
75
|
+
if (overflowed)
|
|
76
|
+
return;
|
|
77
|
+
size += chunk.length;
|
|
78
|
+
if (size > maxBodySize) {
|
|
79
|
+
overflowed = true;
|
|
80
|
+
chunks.length = 0;
|
|
81
|
+
// Keep draining instead of destroying the socket, otherwise the client
|
|
82
|
+
// sees a connection reset instead of the 413.
|
|
83
|
+
request.resume();
|
|
84
|
+
reject(new Error(`Body exceeds ${maxBodySize} bytes`));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
chunks.push(chunk);
|
|
88
|
+
});
|
|
89
|
+
request.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
90
|
+
request.on('error', reject);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function sendJson(response, status, body) {
|
|
94
|
+
if (response.writableEnded)
|
|
95
|
+
return;
|
|
96
|
+
response.writeHead(status, {
|
|
97
|
+
...CORS_HEADERS,
|
|
98
|
+
'content-type': 'application/json',
|
|
99
|
+
});
|
|
100
|
+
response.end(JSON.stringify(body));
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=http-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-server.js","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire format shared by the browser forwarder, the ingestion server and the
|
|
3
|
+
* MCP reader. Everything except `level` and `timestamp` is optional: the
|
|
4
|
+
* forwarder ships whatever the craft `Console.*` boundary produced.
|
|
5
|
+
*/
|
|
6
|
+
export type LogLevel = 'debug' | 'info' | 'log' | 'warn' | 'error';
|
|
7
|
+
export declare const LOG_LEVELS: readonly LogLevel[];
|
|
8
|
+
export type IncomingLogEntry = {
|
|
9
|
+
readonly level: LogLevel;
|
|
10
|
+
/** Human readable rendering of the console arguments. */
|
|
11
|
+
readonly message: string;
|
|
12
|
+
/** Original console arguments, JSON-safe. */
|
|
13
|
+
readonly args?: readonly unknown[];
|
|
14
|
+
/** Craft host tag ancestry, e.g. `['App', 'UserCard']`. */
|
|
15
|
+
readonly from?: readonly string[];
|
|
16
|
+
readonly tags?: readonly unknown[];
|
|
17
|
+
readonly trace?: string;
|
|
18
|
+
readonly correlationId?: unknown;
|
|
19
|
+
/** ISO or UTC string produced in the browser. */
|
|
20
|
+
readonly timestamp?: string;
|
|
21
|
+
readonly route?: string;
|
|
22
|
+
readonly browser?: unknown;
|
|
23
|
+
readonly clientId?: string;
|
|
24
|
+
};
|
|
25
|
+
export type StoredLogEntry = IncomingLogEntry & {
|
|
26
|
+
readonly timestamp: string;
|
|
27
|
+
/** Server-side receive time, always ISO 8601. */
|
|
28
|
+
readonly receivedAt: string;
|
|
29
|
+
readonly seq: number;
|
|
30
|
+
};
|
|
31
|
+
export type LogBatch = {
|
|
32
|
+
readonly clientId?: string;
|
|
33
|
+
readonly entries: readonly IncomingLogEntry[];
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Validates and normalises one wire entry. Returns `undefined` when the payload
|
|
37
|
+
* is unusable so a single bad entry never rejects a whole batch.
|
|
38
|
+
*/
|
|
39
|
+
export declare function normalizeEntry(value: unknown, fallbackClientId?: string): IncomingLogEntry | undefined;
|
|
40
|
+
export declare function parseBatch(payload: unknown): readonly IncomingLogEntry[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const LOG_LEVELS = [
|
|
2
|
+
'debug',
|
|
3
|
+
'info',
|
|
4
|
+
'log',
|
|
5
|
+
'warn',
|
|
6
|
+
'error',
|
|
7
|
+
];
|
|
8
|
+
function isLogLevel(value) {
|
|
9
|
+
return (typeof value === 'string' && LOG_LEVELS.includes(value));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Validates and normalises one wire entry. Returns `undefined` when the payload
|
|
13
|
+
* is unusable so a single bad entry never rejects a whole batch.
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeEntry(value, fallbackClientId) {
|
|
16
|
+
if (typeof value !== 'object' || value === null)
|
|
17
|
+
return undefined;
|
|
18
|
+
const raw = value;
|
|
19
|
+
if (!isLogLevel(raw['level']))
|
|
20
|
+
return undefined;
|
|
21
|
+
const message = typeof raw['message'] === 'string'
|
|
22
|
+
? raw['message']
|
|
23
|
+
: JSON.stringify(raw['message'] ?? '');
|
|
24
|
+
return {
|
|
25
|
+
level: raw['level'],
|
|
26
|
+
message,
|
|
27
|
+
args: Array.isArray(raw['args']) ? raw['args'] : undefined,
|
|
28
|
+
from: Array.isArray(raw['from'])
|
|
29
|
+
? raw['from'].map(String)
|
|
30
|
+
: undefined,
|
|
31
|
+
tags: Array.isArray(raw['tags']) ? raw['tags'] : undefined,
|
|
32
|
+
trace: typeof raw['trace'] === 'string' ? raw['trace'] : undefined,
|
|
33
|
+
correlationId: raw['correlationId'],
|
|
34
|
+
timestamp: typeof raw['timestamp'] === 'string' ? raw['timestamp'] : undefined,
|
|
35
|
+
route: typeof raw['route'] === 'string' ? raw['route'] : undefined,
|
|
36
|
+
browser: raw['browser'],
|
|
37
|
+
clientId: typeof raw['clientId'] === 'string' ? raw['clientId'] : fallbackClientId,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function parseBatch(payload) {
|
|
41
|
+
if (Array.isArray(payload)) {
|
|
42
|
+
return payload
|
|
43
|
+
.map((entry) => normalizeEntry(entry))
|
|
44
|
+
.filter((entry) => entry !== undefined);
|
|
45
|
+
}
|
|
46
|
+
if (typeof payload !== 'object' || payload === null)
|
|
47
|
+
return [];
|
|
48
|
+
const batch = payload;
|
|
49
|
+
const clientId = typeof batch['clientId'] === 'string' ? batch['clientId'] : undefined;
|
|
50
|
+
if (Array.isArray(batch['entries'])) {
|
|
51
|
+
return batch['entries']
|
|
52
|
+
.map((entry) => normalizeEntry(entry, clientId))
|
|
53
|
+
.filter((entry) => entry !== undefined);
|
|
54
|
+
}
|
|
55
|
+
const single = normalizeEntry(payload, clientId);
|
|
56
|
+
return single ? [single] : [];
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=log-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-entry.js","sourceRoot":"","sources":["../src/log-entry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IncomingLogEntry, StoredLogEntry } from './log-entry.js';
|
|
2
|
+
export type LogStoreOptions = {
|
|
3
|
+
/** Directory holding `app.jsonl` and its rotated siblings. */
|
|
4
|
+
readonly directory: string;
|
|
5
|
+
readonly fileName?: string;
|
|
6
|
+
/** Rotate once the active file exceeds this size, in bytes. */
|
|
7
|
+
readonly maxFileSize?: number;
|
|
8
|
+
/** Number of rotated files kept next to the active one. */
|
|
9
|
+
readonly maxFiles?: number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Append-only JSONL store. One JSON object per line, newest last, so the MCP
|
|
13
|
+
* reader can stream lines without parsing the whole file.
|
|
14
|
+
*/
|
|
15
|
+
export declare class LogStore {
|
|
16
|
+
readonly directory: string;
|
|
17
|
+
readonly fileName: string;
|
|
18
|
+
readonly maxFileSize: number;
|
|
19
|
+
readonly maxFiles: number;
|
|
20
|
+
private seq;
|
|
21
|
+
constructor(options: LogStoreOptions);
|
|
22
|
+
get filePath(): string;
|
|
23
|
+
rotatedPath(index: number): string;
|
|
24
|
+
append(entries: readonly IncomingLogEntry[]): readonly StoredLogEntry[];
|
|
25
|
+
clear(): void;
|
|
26
|
+
size(): number;
|
|
27
|
+
private rotateIfNeeded;
|
|
28
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, renameSync, statSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const DEFAULT_FILE_NAME = 'app.jsonl';
|
|
4
|
+
const DEFAULT_MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
5
|
+
const DEFAULT_MAX_FILES = 5;
|
|
6
|
+
/**
|
|
7
|
+
* Append-only JSONL store. One JSON object per line, newest last, so the MCP
|
|
8
|
+
* reader can stream lines without parsing the whole file.
|
|
9
|
+
*/
|
|
10
|
+
export class LogStore {
|
|
11
|
+
directory;
|
|
12
|
+
fileName;
|
|
13
|
+
maxFileSize;
|
|
14
|
+
maxFiles;
|
|
15
|
+
seq = 0;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.directory = options.directory;
|
|
18
|
+
this.fileName = options.fileName ?? DEFAULT_FILE_NAME;
|
|
19
|
+
this.maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
20
|
+
this.maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
21
|
+
mkdirSync(this.directory, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
get filePath() {
|
|
24
|
+
return join(this.directory, this.fileName);
|
|
25
|
+
}
|
|
26
|
+
rotatedPath(index) {
|
|
27
|
+
return join(this.directory, `${this.fileName}.${index}`);
|
|
28
|
+
}
|
|
29
|
+
append(entries) {
|
|
30
|
+
if (entries.length === 0)
|
|
31
|
+
return [];
|
|
32
|
+
const receivedAt = new Date().toISOString();
|
|
33
|
+
const stored = entries.map((entry) => ({
|
|
34
|
+
...entry,
|
|
35
|
+
timestamp: entry.timestamp ?? receivedAt,
|
|
36
|
+
receivedAt,
|
|
37
|
+
seq: ++this.seq,
|
|
38
|
+
}));
|
|
39
|
+
this.rotateIfNeeded();
|
|
40
|
+
appendFileSync(this.filePath, stored.map((entry) => `${JSON.stringify(entry)}\n`).join(''), 'utf8');
|
|
41
|
+
return stored;
|
|
42
|
+
}
|
|
43
|
+
clear() {
|
|
44
|
+
rmSync(this.filePath, { force: true });
|
|
45
|
+
for (let index = 1; index <= this.maxFiles; index++) {
|
|
46
|
+
rmSync(this.rotatedPath(index), { force: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
size() {
|
|
50
|
+
try {
|
|
51
|
+
return statSync(this.filePath).size;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
rotateIfNeeded() {
|
|
58
|
+
if (this.size() < this.maxFileSize)
|
|
59
|
+
return;
|
|
60
|
+
// Drop the oldest, then shift every rotated file one slot down.
|
|
61
|
+
rmSync(this.rotatedPath(this.maxFiles), { force: true });
|
|
62
|
+
for (let index = this.maxFiles - 1; index >= 1; index--) {
|
|
63
|
+
try {
|
|
64
|
+
renameSync(this.rotatedPath(index), this.rotatedPath(index + 1));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Slot is empty; nothing to shift.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
renameSync(this.filePath, this.rotatedPath(1));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=log-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-store.js","sourceRoot":"","sources":["../src/log-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAClF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAajC,MAAM,iBAAiB,GAAG,WAAW,CAAC;AACtC,MAAM,qBAAqB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAC9C,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAE5B;;;GAGG;AACH,MAAM,OAAO,QAAQ;IACV,SAAS,CAAS;IAClB,QAAQ,CAAS;IACjB,WAAW,CAAS;IACpB,QAAQ,CAAS;IAElB,GAAG,GAAG,CAAC,CAAC;IAEhB,YAAY,OAAwB;QAClC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;QACnC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,iBAAiB,CAAC;QACtD,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,qBAAqB,CAAC;QAChE,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,iBAAiB,CAAC;QACtD,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7C,CAAC;IAED,WAAW,CAAC,KAAa;QACvB,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,IAAI,CAAC,QAAQ,IAAI,KAAK,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED,MAAM,CAAC,OAAoC;QACzC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAEpC,MAAM,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACrC,GAAG,KAAK;YACR,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,UAAU;YACxC,UAAU;YACV,GAAG,EAAE,EAAE,IAAI,CAAC,GAAG;SAChB,CAAC,CAAC,CAAC;QAEJ,IAAI,CAAC,cAAc,EAAE,CAAC;QACtB,cAAc,CACZ,IAAI,CAAC,QAAQ,EACb,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAC5D,MAAM,CACP,CAAC;QAEF,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,KAAK;QACH,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAC;YACpD,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IAED,IAAI;QACF,IAAI,CAAC;YACH,OAAO,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,CAAC;QACX,CAAC;IACH,CAAC;IAEO,cAAc;QACpB,IAAI,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,WAAW;YAAE,OAAO;QAE3C,gEAAgE;QAChE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACzD,KAAK,IAAI,KAAK,GAAG,IAAI,CAAC,QAAQ,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;YACxD,IAAI,CAAC;gBACH,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;YACnE,CAAC;YAAC,MAAM,CAAC;gBACP,mCAAmC;YACrC,CAAC;QACH,CAAC;QACD,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC;CACF"}
|
package/dist/main.d.ts
ADDED
package/dist/main.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { createLogHttpServer } from './http-server.js';
|
|
4
|
+
import { LogStore } from './log-store.js';
|
|
5
|
+
const host = process.env['LOG_SERVER_HOST'] ?? '127.0.0.1';
|
|
6
|
+
const port = Number(process.env['LOG_SERVER_PORT'] ?? '4319');
|
|
7
|
+
const directory = resolve(process.env['LOG_SERVER_DIR'] ?? resolve(process.cwd(), '.logs'));
|
|
8
|
+
const maxFileSize = Number(process.env['LOG_SERVER_MAX_FILE_SIZE'] ?? String(5 * 1024 * 1024));
|
|
9
|
+
const maxFiles = Number(process.env['LOG_SERVER_MAX_FILES'] ?? '5');
|
|
10
|
+
const quiet = process.env['LOG_SERVER_QUIET'] === '1';
|
|
11
|
+
const store = new LogStore({ directory, maxFileSize, maxFiles });
|
|
12
|
+
const server = createLogHttpServer({
|
|
13
|
+
store,
|
|
14
|
+
onIngest: (count) => {
|
|
15
|
+
if (!quiet && count > 0) {
|
|
16
|
+
console.log(`[log-server] +${count} entries -> ${store.filePath}`);
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
server.listen(port, host, () => {
|
|
21
|
+
console.log(`[log-server] listening on http://${host}:${port}`);
|
|
22
|
+
console.log(`[log-server] writing to ${store.filePath}`);
|
|
23
|
+
});
|
|
24
|
+
const shutdown = () => {
|
|
25
|
+
server.close(() => process.exit(0));
|
|
26
|
+
};
|
|
27
|
+
process.once('SIGINT', shutdown);
|
|
28
|
+
process.once('SIGTERM', shutdown);
|
|
29
|
+
//# sourceMappingURL=main.js.map
|
package/dist/main.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,WAAW,CAAC;AAC3D,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,MAAM,CAAC,CAAC;AAC9D,MAAM,SAAS,GAAG,OAAO,CACvB,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,CACjE,CAAC;AACF,MAAM,WAAW,GAAG,MAAM,CACxB,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,IAAI,MAAM,CAAC,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,CACnE,CAAC;AACF,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,IAAI,GAAG,CAAC,CAAC;AACpE,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,KAAK,GAAG,CAAC;AAEtD,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,CAAC;AACjE,MAAM,MAAM,GAAG,mBAAmB,CAAC;IACjC,KAAK;IACL,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;QAClB,IAAI,CAAC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,GAAG,CAAC,iBAAiB,KAAK,eAAe,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE;IAC7B,OAAO,CAAC,GAAG,CAAC,oCAAoC,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC;IAChE,OAAO,CAAC,GAAG,CAAC,2BAA2B,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;AAC3D,CAAC,CAAC,CAAC;AAEH,MAAM,QAAQ,GAAG,GAAS,EAAE;IAC1B,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACtC,CAAC,CAAC;AAEF,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AACjC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@craft-ts/log-server",
|
|
3
|
+
"version": "0.7.0-beta.15",
|
|
4
|
+
"description": "Local JSONL log ingestion server for CraftTS development",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"README.md",
|
|
9
|
+
"!dist/**/*.spec.js",
|
|
10
|
+
"!dist/**/*.spec.d.ts",
|
|
11
|
+
"!dist/**/*.spec.js.map"
|
|
12
|
+
],
|
|
13
|
+
"bin": {
|
|
14
|
+
"craft-ts-log-server": "dist/main.js"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"start": "npm run build && node dist/main.js",
|
|
19
|
+
"test": "vitest run --config vitest.config.mts"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "20.19.9",
|
|
23
|
+
"typescript": "6.0.3",
|
|
24
|
+
"vitest": "^4.0.8"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
}
|
|
29
|
+
}
|