AkurAI Build
Menu

Public repository / 2bbca3874a4bf038785d59f1f8e9a45bea199641

AkurAI-Monitor / clients/pino-monitor-transport.mjs

Latest change 2bbca3874a4bf038785d59f1f8e9a45bea199641 - feat: full-width layout + log ingestion endpoint for shipping from other apps by Olafur Bui

// Dependency-free pino destination that ships logs to rust-monitor's /api/ingest.
//
// Usage:
//   import pino from "pino";
//   import { createMonitorStream } from "./pino-monitor-transport.mjs";
//
//   const monitor = createMonitorStream({
//     url: process.env.MONITOR_INGEST_URL,      // https://monitor.olibuijr.com/api/ingest
//     token: process.env.MONITOR_INGEST_TOKEN,  // shared bearer token
//     source: "akurai-mail",                    // app name shown in the monitor
//   });
//   export const logger = pino({ level: "info" }, monitor);
//
// Notes:
// - Runs in-process (no worker thread, no extra deps). Batches lines and POSTs
//   them; network failures are swallowed so logging never crashes the app.
// - Requires global fetch (Node >= 18 or Bun).

const LEVELS = { 10: "TRACE", 20: "DEBUG", 30: "INFO", 40: "WARN", 50: "ERROR", 60: "FATAL" };

export function createMonitorStream({
  url,
  token,
  source,
  batchSize = 50,
  flushMs = 2000,
  alsoStdout = true,
}) {
  if (!url || !token || !source) {
    throw new Error("createMonitorStream requires { url, token, source }");
  }

  let buf = [];
  let timer = null;

  async function flush() {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    if (buf.length === 0) return;
    const logs = buf;
    buf = [];
    try {
      await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
        body: JSON.stringify({ logs }),
      });
    } catch {
      // Drop on failure — never let log shipping break the app.
    }
  }

  function schedule() {
    if (!timer) timer = setTimeout(flush, flushMs);
  }

  return {
    write(chunk) {
      if (alsoStdout) process.stdout.write(chunk);

      let line = chunk;
      let ts;
      try {
        const o = JSON.parse(chunk);
        ts = o.time ? Math.floor(o.time / 1000) : undefined;
        const lvl = LEVELS[o.level] || String(o.level ?? "");
        const { level, time, pid, hostname, msg, ...rest } = o;
        const extra = Object.keys(rest).length ? " " + JSON.stringify(rest) : "";
        line = `${lvl} ${msg ?? ""}${extra}`.trim();
      } catch {
        line = String(chunk).trim();
      }

      buf.push({ source, line, ts });
      if (buf.length >= batchSize) flush();
      else schedule();
    },
  };
}