lduck 0.0.1 → 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.
Files changed (2) hide show
  1. package/dist/index.js +148 -5
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1029,6 +1029,134 @@ class WSHandler {
1029
1029
  }
1030
1030
  }
1031
1031
  }
1032
+ function parseRelayArgs(args) {
1033
+ const { values } = parseArgs({
1034
+ args,
1035
+ options: {
1036
+ url: { type: "string", short: "u" },
1037
+ service: { type: "string", short: "s" },
1038
+ "batch-size": { type: "string" },
1039
+ interval: { type: "string" },
1040
+ help: { type: "boolean", short: "h" }
1041
+ },
1042
+ strict: true
1043
+ });
1044
+ return {
1045
+ url: values.url ?? "http://localhost:8080",
1046
+ service: values.service,
1047
+ batchSize: values["batch-size"] !== void 0 ? parseInt(values["batch-size"], 10) : 100,
1048
+ intervalMs: values.interval !== void 0 ? parseInt(values.interval, 10) : 500,
1049
+ help: values.help ?? false
1050
+ };
1051
+ }
1052
+ const RELAY_USAGE = `
1053
+ Usage: <command> | lduck relay [options]
1054
+
1055
+ Relay stdin JSON logs to a running lduck server via HTTP POST.
1056
+
1057
+ Options:
1058
+ -u, --url <url> Target lduck server URL (default: http://localhost:8080)
1059
+ -s, --service <name> Service name to inject into each log entry
1060
+ --batch-size <n> Lines per HTTP batch (default: 100)
1061
+ --interval <ms> Flush interval in milliseconds (default: 500)
1062
+ -h, --help Show this help message
1063
+
1064
+ Examples:
1065
+ kubectl logs -f deploy/api | lduck relay --service api
1066
+ cat app.log | lduck relay --url http://lduck:9090 --service backend
1067
+ docker logs -f myapp | lduck relay -s myapp -u http://localhost:8080
1068
+ `.trimStart();
1069
+ async function runRelay(args) {
1070
+ const opts = parseRelayArgs(args);
1071
+ if (opts.help) {
1072
+ process.stdout.write(RELAY_USAGE);
1073
+ process.exit(0);
1074
+ }
1075
+ const ingestUrl = opts.url.replace(/\/$/, "") + "/api/ingest";
1076
+ let buffer = [];
1077
+ let flushTimer = null;
1078
+ let totalSent = 0;
1079
+ let totalErrors = 0;
1080
+ let stdinClosed = false;
1081
+ async function flush() {
1082
+ if (buffer.length === 0) return;
1083
+ const batch = buffer;
1084
+ buffer = [];
1085
+ try {
1086
+ const res = await fetch(ingestUrl, {
1087
+ method: "POST",
1088
+ headers: { "Content-Type": "application/json" },
1089
+ body: JSON.stringify(batch)
1090
+ });
1091
+ if (!res.ok) {
1092
+ const text = await res.text();
1093
+ console.error(`[relay] HTTP ${res.status}: ${text}`);
1094
+ totalErrors += batch.length;
1095
+ } else {
1096
+ const result = await res.json();
1097
+ totalSent += result.accepted;
1098
+ }
1099
+ } catch (err) {
1100
+ const message = err instanceof Error ? err.message : String(err);
1101
+ console.error(`[relay] Send failed: ${message}`);
1102
+ totalErrors += batch.length;
1103
+ }
1104
+ }
1105
+ function resetFlushTimer() {
1106
+ if (flushTimer !== null) {
1107
+ clearTimeout(flushTimer);
1108
+ }
1109
+ flushTimer = setTimeout(async () => {
1110
+ flushTimer = null;
1111
+ await flush();
1112
+ if (!stdinClosed) {
1113
+ resetFlushTimer();
1114
+ }
1115
+ }, opts.intervalMs);
1116
+ }
1117
+ const rl = createInterface({
1118
+ input: process.stdin,
1119
+ crlfDelay: Infinity
1120
+ });
1121
+ console.error(`[relay] Relaying to ${ingestUrl}${opts.service ? ` (service: ${opts.service})` : ""}`);
1122
+ resetFlushTimer();
1123
+ rl.on("line", (line) => {
1124
+ const trimmed = line.trim();
1125
+ if (trimmed === "") return;
1126
+ let parsed;
1127
+ try {
1128
+ parsed = JSON.parse(trimmed);
1129
+ } catch {
1130
+ return;
1131
+ }
1132
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1133
+ return;
1134
+ }
1135
+ if (opts.service !== void 0 && !("service" in parsed)) {
1136
+ parsed.service = opts.service;
1137
+ }
1138
+ buffer.push(parsed);
1139
+ if (buffer.length >= opts.batchSize) {
1140
+ flush();
1141
+ }
1142
+ });
1143
+ await new Promise((resolve) => {
1144
+ rl.on("close", async () => {
1145
+ stdinClosed = true;
1146
+ if (flushTimer !== null) {
1147
+ clearTimeout(flushTimer);
1148
+ flushTimer = null;
1149
+ }
1150
+ await flush();
1151
+ if (totalSent > 0 || totalErrors > 0) {
1152
+ console.error(
1153
+ `[relay] Done. Sent: ${totalSent}, Errors: ${totalErrors}`
1154
+ );
1155
+ }
1156
+ resolve();
1157
+ });
1158
+ });
1159
+ }
1032
1160
  function parseCLIArgs(args) {
1033
1161
  const { values } = parseArgs({
1034
1162
  args,
@@ -1056,8 +1184,13 @@ function parseCLIArgs(args) {
1056
1184
  }
1057
1185
  const USAGE = `
1058
1186
  Usage: <command> | lduck [options]
1187
+ <command> | lduck relay [options]
1059
1188
 
1060
- Options:
1189
+ Commands:
1190
+ (default) Start the lduck server (Web UI + API)
1191
+ relay Relay stdin logs to a running lduck server via HTTP
1192
+
1193
+ Server Options:
1061
1194
  -p, --port <port> Server port (default: 8080)
1062
1195
  -m, --max-rows <n> Maximum rows to keep (default: 100000)
1063
1196
  --batch-size <n> Batch INSERT size (default: 5000)
@@ -1069,6 +1202,8 @@ Examples:
1069
1202
  kubectl logs -f deploy/api | lduck --port 8080
1070
1203
  cat app.log | lduck --db ./logs.duckdb
1071
1204
  docker logs -f myapp | lduck --no-ui -p 9090
1205
+ kubectl logs -f deploy/api | lduck relay --service api
1206
+ docker logs -f myapp | lduck relay -s myapp -u http://localhost:8080
1072
1207
  `.trimStart();
1073
1208
  async function main() {
1074
1209
  const opts = parseCLIArgs(process.argv.slice(2));
@@ -1121,10 +1256,18 @@ async function main() {
1121
1256
  ingester.start(process.stdin);
1122
1257
  }
1123
1258
  if (!process.env.VITEST) {
1124
- main().catch((err) => {
1125
- console.error("Failed to start lduck:", err);
1126
- process.exit(1);
1127
- });
1259
+ const args = process.argv.slice(2);
1260
+ if (args[0] === "relay") {
1261
+ runRelay(args.slice(1)).catch((err) => {
1262
+ console.error("Failed to run lduck relay:", err);
1263
+ process.exit(1);
1264
+ });
1265
+ } else {
1266
+ main().catch((err) => {
1267
+ console.error("Failed to start lduck:", err);
1268
+ process.exit(1);
1269
+ });
1270
+ }
1128
1271
  }
1129
1272
  export {
1130
1273
  parseCLIArgs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lduck",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Pipe-friendly JSON log viewer powered by DuckDB",
5
5
  "type": "module",
6
6
  "bin": {