UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

95 lines (86 loc) 3.04 kB
#!/usr/bin/env node /** * mesh dev --local log shipper. * * Sits at the end of each service's launch pipeline: * { <dev command>; } 2>&1 | node log-shipper.mjs * * - Echoes every byte to stderr untouched (the tmux pane keeps colors). * - Ships ANSI-stripped lines to the local platform's OTel collector via * OTLP/HTTP — the SAME ingestion path instrumented apps use, carrying the * resource attributes `mesh dev` injects (OTEL_RESOURCE_ATTRIBUTES / * OTEL_SERVICE_NAME), so Loki indexes k8s_namespace_name / * k8s_deployment_name exactly like the in-cluster k8sattributes processor. * * Why not a log file tailed by the collector: host-written files cross the * macOS Docker mount with stale attributes — appends can stay invisible to * the in-container tailer indefinitely. OTLP over localhost has no such * boundary. Shipping is best-effort: the collector being down never breaks * the dev loop. */ const ENDPOINT = (process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://localhost:4318").replace(/\/+$/, ""); const FLUSH_MS = 1000; const MAX_BATCH = 200; const attributes = []; for (const pair of (process.env.OTEL_RESOURCE_ATTRIBUTES ?? "").split(",")) { const eq = pair.indexOf("="); if (eq > 0) { attributes.push({ key: pair.slice(0, eq).trim(), value: { stringValue: pair.slice(eq + 1).trim() } }); } } if (process.env.OTEL_SERVICE_NAME) { attributes.push({ key: "service.name", value: { stringValue: process.env.OTEL_SERVICE_NAME } }); } const ANSI = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?|\x1b[=>]|\r/g; let queue = []; let flushTimer = null; let flushing = Promise.resolve(); function enqueue(line) { const clean = line.replace(ANSI, ""); if (!clean.trim()) return; queue.push({ timeUnixNano: String(Date.now()) + "000000", body: { stringValue: clean } }); if (queue.length >= MAX_BATCH) flush(); else if (!flushTimer) flushTimer = setTimeout(flush, FLUSH_MS); } function flush() { if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; } if (queue.length === 0) return flushing; const logRecords = queue; queue = []; const payload = JSON.stringify({ resourceLogs: [ { resource: { attributes }, scopeLogs: [{ scope: { name: "mesh-dev-local" }, logRecords }], }, ], }); flushing = fetch(`${ENDPOINT}/v1/logs`, { method: "POST", headers: { "content-type": "application/json" }, body: payload, signal: AbortSignal.timeout(5000), }).catch(() => { /* best-effort — never break the dev loop */ }); return flushing; } let buffer = ""; process.stdin.on("data", (chunk) => { process.stderr.write(chunk); // raw passthrough → the tmux pane buffer += chunk.toString("utf-8"); let newline; while ((newline = buffer.indexOf("\n")) >= 0) { enqueue(buffer.slice(0, newline)); buffer = buffer.slice(newline + 1); } }); process.stdin.on("end", async () => { if (buffer) enqueue(buffer); await flush(); await flushing; process.exit(0); });