Node.js logging to UptimeEye (pino, winston, OpenTelemetry)

In containers the best practice is unchanged: log JSON to stdout with pino or winston and let the platform agent ship it — pino's default output is exactly what the Kubernetes and Docker guides parse. When there is no agent (serverless, a single VM, a CLI), send batches over HTTP yourself; both variants are below.

Endpoint: https://logs.uptimeeye.com/v1/ingest/jsonline

1.Containers: pino to stdout

pino writes one JSON object per line with msg, level (numeric) and time (epoch ms). Add your service fields once as base bindings.

logger.js
import pino from "pino";

export const log = pino({
  base: { service: "checkout", env: process.env.NODE_ENV ?? "prod" },
  formatters: { level: (label) => ({ level: label }) },   // "info" instead of 30
  timestamp: pino.stdTimeFunctions.isoTime,               // "time":"2026-09-04T11:42:08.877Z"
});

log.info({ orderId: "4711", durationMs: 812 }, "payment authorized");
Note: With the Vector examples, add _time_field: time (already parsed by parse_json) and _msg_field: msg to the sink query.

2.No agent: a small batching transport (no dependencies)

Buffers lines and flushes every two seconds or 200 lines; uses the JSON-lines endpoint. Works as a pino destination or on its own.

uptimeeye-logs.js
const ENDPOINT = "https://logs.uptimeeye.com/v1/ingest/jsonline?_msg_field=msg&_time_field=time&_stream_fields=service,env";
const KEY = process.env.UPTIMEEYE_INGEST_KEY;

let buffer = [];
let timer = null;

export function send(entry) {
  buffer.push(JSON.stringify({ time: new Date().toISOString(), service: "checkout", env: "prod", ...entry }));
  if (buffer.length >= 200) return flush();
  timer ??= setTimeout(flush, 2000);
}

export async function flush() {
  clearTimeout(timer); timer = null;
  if (buffer.length === 0) return;
  const body = buffer.join("\n") + "\n";
  buffer = [];
  try {
    const res = await fetch(ENDPOINT, {
      method: "POST",
      headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/x-ndjson" },
      body,
    });
    if (!res.ok) console.error("uptimeeye logs:", res.status, await res.text());
  } catch (err) {
    console.error("uptimeeye logs:", err);
  }
}

process.on("beforeExit", flush);

// usage
send({ level: "info", msg: "payment authorized", orderId: "4711", durationMs: 812 });

3.OpenTelemetry logs SDK

otel.js
import { logs } from "@opentelemetry/api-logs";
import { LoggerProvider, BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
import { resourceFromAttributes } from "@opentelemetry/resources";

const provider = new LoggerProvider({
  resource: resourceFromAttributes({ "service.name": "checkout", "deployment.environment": "prod" }),
  processors: [
    new BatchLogRecordProcessor(
      new OTLPLogExporter({
        url: "https://logs.uptimeeye.com/v1/ingest/otlp/v1/logs",
        headers: { Authorization: `Bearer ${process.env.UPTIMEEYE_INGEST_KEY}` },
      }),
    ),
  ],
});
logs.setGlobalLoggerProvider(provider);

logs.getLogger("checkout").emit({ severityText: "INFO", body: "payment authorized", attributes: { orderId: "4711" } });
Note: pino users can keep pino and add pino-opentelemetry-transport to feed the same exporter.

Fields you get

These show up in the fields panel and can be used in every filter:

  • service, env
  • level
  • msg as the message
  • pid, hostname (pino defaults)
  • every object key you log

Tips

  • Log objects, not string concatenation: log.info({ orderId }, "…") gives you a filterable orderId field.
  • winston: format.combine(format.timestamp(), format.json()) produces the same shape; set _msg_field=message&_time_field=timestamp.
  • Serverless: flush on the platform's shutdown hook (Lambda: at the end of the handler) — there is no beforeExit you can rely on.

FAQ

Should I send from the process or from an agent?
From an agent whenever one exists: it survives crashes, batches across processes and needs no key in the application. Send directly only where no agent can run.
Where do I get the ingest key?
In the app under API Keys → New API Key → type “Log ingest”. The key starts with ue_ingest_ and is shown once. Management keys (ue_live_) are refused by the ingest endpoint.
How do I check that logs arrive?
Open Logs in the app, pick the 15m range and search for service:=<your service>. New lines are searchable within about a second; Live tail shows them with a ~6 s delay.