Bunyan
Forwarding Bunyan logs to BugWatchlink
The Bunyan adapter is a Bunyan raw stream. It sits alongside your existing streams, so your normal stdout/file output is untouched and every record that passes the stream's level threshold is also captured by BugWatch.
Installlink
npm install @newinstance/bugwatch bunyanWiringlink
createBugWatchBunyanStream(client) returns an object with a single write(record) method.
Bunyan only hands it a record object when the stream entry is declared with type: "raw".
import bunyan from "bunyan";
import { createClient } from "@newinstance/bugwatch";
import { createBugWatchBunyanStream } from "@newinstance/bugwatch/bunyan";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
const logger = bunyan.createLogger({
name: "my-app",
streams: [
{ stream: process.stdout },
{ level: "info", type: "raw", stream: createBugWatchBunyanStream(client) },
],
});
logger.error({ err: new Error("connection refused") }, "redis down");The level on the BugWatch stream entry is your forwarding threshold. Set it to "warn" to keep
debug noise local while still shipping warnings and errors.
Level mappinglink
Bunyan's numeric levels map 1:1 to BugWatch levels, no translation table involved:
| Bunyan | Numeric | BugWatch |
|---|---|---|
trace | 10 | trace |
debug | 20 | debug |
info | 30 | info |
warn | 40 | warn |
error | 50 | error |
fatal | 60 | fatal |
If a record arrives without a numeric level, the adapter falls back to 30 (info).
What becomes tagslink
Every remaining top-level field on the record becomes a tag, stringified with String(v).
Skipped keys (Bunyan's own record plumbing): level, msg, time, v, name, hostname,
pid, err, src.
Only scalar values are kept: string, number, boolean, bigint. Objects, arrays, null
and undefined are silently dropped, so logger.info({ order: { id: "o_1" } }, "created") sends
no tags at all.
Workaround: flatten at the call site, or serialize the value yourself.
// Dropped: nested object.
logger.info({ order }, "order created");
// Kept: three scalar tags.
logger.info({ orderId: order.id, itemCount: order.items.length, currency: "GBP" }, "order created");
// Kept: pre-serialized as a single string tag.
logger.info({ order: JSON.stringify(order) }, "order created");Note that record.msg becomes the event message only when it is a string; logger.info(obj) with
no message string produces an event with tags and no message.
Error handlinglink
The adapter reads the err key specifically. If record.err is an object it becomes the captured
error on the event. Any other key holding an Error (for example error or cause) is not a
scalar, so it is dropped entirely.
logger.error({ err }, "payment failed"); // err captured, full stack frames
logger.error({ error: err }, "payment failed"); // error dropped, message onlyPass the real Error instance if you want a stack trace. If you configure
serializers: { err: bunyan.stdSerializers.err }, the raw stream receives Bunyan's plain
{ name, message, stack } object instead, and BugWatch still records the type and message but
cannot parse stack frames from it.
The stream never throws back into Bunyan; any internal failure is swallowed so logging can never break your request path.
Request scope and trace correlationlink
The adapter calls client.captureLog synchronously inside your logger.*() call, so it runs
inside the same AsyncLocalStorage async tree that the framework adapter opened. That means
forwarded logs pick up request enrichment for free:
- user set via the adapter's
getUseroption orsetRequestUser - request tags set via
setRequestTag - global tags set with
BugWatch.setTag traceId/spanIdfrom the active request scope,withSpan, ortraceContextProvider
Per-call tags from the record win over scope tags on any key collision. For trace correlation with an existing OpenTelemetry setup, wire the provider once at init and every forwarded Bunyan record carries the current trace:
import { otelTraceContextProvider } from "@newinstance/bugwatch/otel";
const client = createClient({
projectKey: process.env.BUGWATCH_KEY!,
traceContextProvider: otelTraceContextProvider(),
});Complete examplelink
// src/server.ts
import express, { type NextFunction, type Request, type Response } from "express";
import bunyan from "bunyan";
import { createClient } from "@newinstance/bugwatch";
import { createBugWatchBunyanStream } from "@newinstance/bugwatch/bunyan";
import {
bugWatchExpressRequestHandler,
bugWatchExpressErrorHandler,
} from "@newinstance/bugwatch/express";
import { installNodeErrorHandlers, setRequestUser } from "@newinstance/bugwatch/node";
const client = createClient({
projectKey: process.env.BUGWATCH_KEY!,
release: process.env.GIT_SHA,
environment: process.env.NODE_ENV ?? "development",
});
installNodeErrorHandlers(client);
const logger = bunyan.createLogger({
name: "checkout-api",
streams: [
{ level: "debug", stream: process.stdout },
{ level: "info", type: "raw", stream: createBugWatchBunyanStream(client) },
],
});
const app = express();
app.use(express.json());
// FIRST: opens the per-request AsyncLocalStorage scope.
app.use(bugWatchExpressRequestHandler(client));
app.use((req: Request, _res: Response, next: NextFunction) => {
const userId = req.headers["x-user-id"];
if (typeof userId === "string") setRequestUser({ id: userId });
next();
});
app.post("/checkout", async (req: Request, res: Response) => {
const orderId = String(req.body.orderId);
// Scalars only: orderId and amount become tags, request user and trace are added by the scope.
logger.info({ orderId, amount: Number(req.body.amount) }, "checkout initiated");
try {
await processOrder(req.body);
res.json({ ok: true });
} catch (err) {
logger.error({ err, orderId, step: "process" }, "checkout failed");
res.status(500).json({ ok: false });
}
});
// LAST: captures anything thrown downstream.
app.use(bugWatchExpressErrorHandler(client));
const server = app.listen(3000, () => logger.info({ port: 3000 }, "listening"));
process.on("SIGTERM", () => {
server.close(() => {
void client.close();
});
});
async function processOrder(_body: unknown): Promise<void> {
throw new Error("payment gateway timeout");
}Troubleshootinglink
- Garbled or empty events. The stream entry is missing
type: "raw". Without it Bunyan writes a JSON string, not a record object, and the adapter has nolevelormsgto read. - Nothing arrives. Check both thresholds: the logger's own level and the
levelon the BugWatch stream entry. Also confirmenabledis notfalseandsampleRateis not throttling events. - Fields missing as tags. They were non-scalar or in the skip list. Flatten them or stringify.
- Error has no stack. Use the
errkey, pass the realError, and dropstdSerializers.errfrom the logger config if you need parsed frames. - Events lost on exit. The queue flushes every 5s by default. Call
await client.close()(orawait client.flush()) in your shutdown hook. - Nothing at all in a short script. Set
debug: trueat init: it prints delivery failures (successes are silent).