Native Logger

Native logger (createLogger)link

A structured logger with zero dependencies, backed directly by BugWatch. Use it when you have no logging library and do not want to add one.

Creating a logger

From the singleton:

import { BugWatch } from "@newinstance/bugwatch";

BugWatch.init({ projectKey: process.env.BUGWATCH_KEY! });

const log = BugWatch.createLogger();

Or from a client instance:

import { createClient, createLogger } from "@newinstance/bugwatch";

const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
const log = createLogger(client, { service: "api" });

The second argument is the initial bindings object and is optional. BugWatch.createLogger() throws if it is called before BugWatch.init().

Levels

Six methods, mapping to the standard BugWatch severities:

log.trace("cache lookup", { key: "u:42" }); // 10
log.debug("resolved route", { route: "/pay" }); // 20
log.info("charge initiated", { amount: 5000 }); // 30
log.warn("retrying", { attempt: 2 }); // 40
log.error("charge failed", { orderId: "ord_1" }); // 50
log.fatal("unrecoverable state"); // 60

Every method takes (message, meta?). error and fatal additionally accept an Error as the first argument.

Child loggers

child(bindings) returns a new logger whose bindings are merged into every call. Child bindings are shallow-merged over the parent's, so a child can override an inherited key. Children nest freely:

const log = BugWatch.createLogger();
const paymentLog = log.child({ service: "payment", provider: "paystack" });
const retryLog = paymentLog.child({ phase: "retry" });

retryLog.warn("gateway slow", { latencyMs: 1840 });
// tags: service, provider, phase, latencyMs

The parent is untouched. child creates a new logger; it never mutates the one it was called on.

How bindings and meta become tags

Bindings and per-call meta are flattened into a single flat tags map on the event, with meta taking precedence on key collisions. Every value is coerced to a string:

  • Strings pass through unchanged.
  • Everything else is JSON.stringifyd, with a String(value) fallback for values JSON cannot represent.
  • null and undefined values are dropped, so an absent field never produces an empty tag.
  • If nothing survives, no tags field is sent at all.
log.info("order created", {
	orderId: "ord_1",
	total: 4999,
	items: [{ sku: "A1", qty: 2 }],
});
// tags: { orderId: "ord_1", total: "4999", items: '[{"sku":"A1","qty":2}]' }

Difference from the Pino adapter. The Pino destination only converts scalar fields (string, number, boolean, bigint) into tags and silently drops objects and arrays. The native logger keeps them by JSON-stringifying the value. If you are migrating from Pino and wondering why nested objects suddenly appear as tags, this is why.

Errors

Pass an Error directly to error or fatal. The error is attached to the event for stack processing, and the event message becomes the error's message. Meta still becomes tags:

log.error(new Error("gateway timeout"), { attempt: 1, orderId: "ord_1" });
// message: "gateway timeout", error attached, tags: { attempt: "1", orderId: "ord_1" }

A plain string message sends no error object. Use the Error form whenever you have one, so the dashboard can group by stack.

Local console output

The native logger is the only logging path that also prints locally. It writes each call to your console in addition to shipping to BugWatch, so logs stay visible while you develop. The console patch and the library adapters keep their own existing output instead.

The local sink receives the raw structured meta, not the stringified tags, so nested objects render as objects rather than as JSON strings.

  • Node on a TTY: an aligned, colored line per call. Scalars inline, nested objects and arrays and error stacks expanded beneath.
  • Node piped or non-TTY: the same layout with no ANSI codes, clean for files, grep, CI, and log shippers.
  • Browser: routed to console.debug / info / warn / error with a colored level badge, with the metadata object and error appended so devtools keeps them expandable.
14:23:01.482  INFO   payment processed
                     amount=1999  currency=USD  ok=true  userId=42
                     customer: { id: 'cus_123', tier: 'gold', address: { city: 'Lagos' } }
14:23:01.512  ERROR  checkout failed  orderId=99
              └─ Error: gateway timeout
                     at charge (checkout.ts:88:11)

Configure it with the console option on init or createClient:

BugWatch.init({ projectKey: process.env.BUGWATCH_KEY!, console: false });

BugWatch.init({
	projectKey: process.env.BUGWATCH_KEY!,
	console: {
		enabled: true, // default: true
		level: "info", // minimum level to print (default: "trace")
		format: "auto", // "auto" | "pretty" | "json" (default: "auto")
		colors: true, // force color on/off (default: TTY detection plus NO_COLOR)
	},
});

console: true and omitting the option both mean defaults. format: "auto" is pretty output with color when stdout is a TTY. format: "json" emits one NDJSON line per call with time, level, msg, the raw meta fields, and an err object when present, which is the right choice when a collector reads your stdout. The level filter applies only to local printing; shipping to BugWatch is unaffected. The sink never throws into your application.

Trace correlation

The logger calls captureLog on the client, so every log picks up the active trace context from the current scope. Inside a request scope (from @newinstance/bugwatch/node or a framework middleware) or inside withSpan, logs are stamped with the active traceId and spanId and line up with the surrounding span on the dashboard:

await client.withSpan("charge", async () => {
	log.info("charging card", { amount: 5000 }); // correlated to this span
	await charge();
});

Outside any scope or span, logs are sent without trace identifiers.

Complete example

import { BugWatch } from "@newinstance/bugwatch";

BugWatch.init({
	projectKey: process.env.BUGWATCH_KEY!,
	environment: process.env.NODE_ENV ?? "development",
	release: process.env.GIT_SHA,
	console: { level: "info", format: process.env.NODE_ENV === "production" ? "json" : "auto" },
});

const log = BugWatch.createLogger();
const paymentLog = log.child({ service: "payment", provider: "paystack" });

export async function charge(orderId: string, amount: number): Promise<void> {
	const orderLog = paymentLog.child({ orderId });
	orderLog.info("charge initiated", { amount, currency: "NGN" });
	try {
		await gateway.charge(orderId, amount);
		orderLog.info("charge settled", { amount });
	} catch (err) {
		orderLog.error(err as Error, { attempt: 1 });
		throw err;
	}
}

process.on("SIGTERM", async () => {
	await BugWatch.flush();
	process.exit(0);
});

Troubleshooting

BugWatch.createLogger() called before BugWatch.init(). Move init above the first createLogger call, or use createLogger(client) with an explicit client from createClient.

No local output. Check the console option. console: false or enabled: false disables the sink entirely, and level filters out anything below it. Remember only the native logger echoes locally.

Colors in a log file. Set colors: false, or rely on the default detection, which already turns color off for non-TTY output and when NO_COLOR is set.

Tags look like JSON strings. Expected. Objects and arrays are stringified on their way to tags. The local console sink still shows them as real objects because it gets the raw meta.

A field vanished. Values that are null or undefined are dropped before tags are built. Send a sentinel string if the absence itself matters.

Logs are not attached to a trace. They were emitted outside a request scope or span. Wrap the work in a framework middleware, a node request scope, or withSpan.

Nothing reaches the dashboard from a short-lived process. Await BugWatch.flush() (or client.flush()) before exit. Sign in at www.newinstance.cloud to confirm arrival.