# Native Logger

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:

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

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

const log = BugWatch.createLogger();
```

Or from a client instance:

```ts
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:

```ts
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:

```ts
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.stringify`d, 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.

```ts
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:

```ts
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.

```text
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`:

```ts
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:

```ts
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

```ts
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](https://www.newinstance.cloud) to confirm arrival.
