@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
95 lines (86 loc) • 3.04 kB
JavaScript
/**
* 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);
});