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"); // 60Every 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, latencyMsThe 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 aString(value)fallback for values JSON cannot represent. nullandundefinedvalues are dropped, so an absent field never produces an empty tag.- If nothing survives, no
tagsfield 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 / errorwith 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.