# log4js

The log4js adapter is an inline custom appender. Register it next to your existing appenders and
every event routed to it is forwarded to BugWatch in addition to its normal output.

## Install

```bash
npm install @newinstance/bugwatch log4js
```

## Wiring

`bugWatchLog4jsAppender(client)` returns an object with a `configure()` method, which is exactly
the shape log4js accepts as an inline appender module. Pass that object as the appender's `type`
value; it is an object, not a string, so do not quote it.

```ts
import log4js from "log4js";
import { createClient } from "@newinstance/bugwatch";
import { bugWatchLog4jsAppender } from "@newinstance/bugwatch/log4js";

const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });

log4js.configure({
	appenders: {
		console: { type: "console" },
		bugwatch: { type: bugWatchLog4jsAppender(client) },
	},
	categories: {
		default: { appenders: ["console", "bugwatch"], level: "info" },
	},
});

const logger = log4js.getLogger();
logger.error("unhandled error in checkout flow");
```

The category's `level` is the forwarding threshold. Give a category a higher level, or omit
`bugwatch` from its appender list, to keep chatty subsystems local.

## Level mapping

log4js level names map onto BugWatch numeric levels:

| log4js  | BugWatch     |
| ------- | ------------ |
| `TRACE` | 10 (trace)   |
| `DEBUG` | 20 (debug)   |
| `INFO`  | 30 (info)    |
| `WARN`  | 40 (warn)    |
| `ERROR` | 50 (error)   |
| `FATAL` | 60 (fatal)   |
| `MARK`  | 30 (info)    |

Any custom level name that is not in the table falls back to 30 (info).

## Message construction

log4js hands the appender an array of arguments. The adapter builds the message like this:

1. If any arguments are strings, all of them are joined with a single space.
2. Otherwise, if the first argument is a non-null object, it is `JSON.stringify`-ed.
3. Otherwise, if the first argument is a scalar (`string`, `number`, `boolean`, `bigint`), it is
   `String()`-ified.
4. Otherwise the message is empty.

```ts
logger.info("order", "created");           // message: "order created"
logger.info({ orderId: "o_1" });           // message: '{"orderId":"o_1"}'
logger.info("charged %d cents", 1200);     // message: "charged %d cents"
```

Note that log4js format placeholders are not interpolated by the adapter; the raw strings are
joined as written, and non-string arguments are not folded into the message. Interpolate yourself
with a template literal when the value matters.

## What becomes tags

Exactly one tag is produced: `category`, set from the event's `categoryName`. Nothing else from
the event data becomes a tag.

Workarounds for richer tagging:

- Use a category per subsystem: `log4js.getLogger("payments")` gives every one of those events
  `category: "payments"`, which is enough to filter on in BugWatch.
- Put request-level dimensions on the scope: `setRequestTag("orderId", id)` or
  `BugWatch.setTag("region", "eu-west-1")`. These are merged into every forwarded event.
- For a one-off event needing bespoke tags, call `client.captureLog({ level, message, tags })`
  directly; the appender and a direct call can happily coexist.

## Error handling

The adapter scans the argument list for the first value that is `instanceof Error` and attaches it
as the captured error, complete with parsed stack frames.

```ts
logger.error("charge failed", err);       // message "charge failed", err captured
logger.error(err);                        // err captured; message is JSON.stringify(err), usually "{}"
logger.error("charge failed", { err });   // Error nested in an object: NOT captured
```

Pass the `Error` as its own top-level argument. A wrapped or plain-object error fails the
`instanceof` check, and so does an `Error` that crossed a realm boundary such as a worker thread
or a `vm` context. The appender never throws back into log4js.

## Request scope and trace correlation

log4js calls the appender synchronously from your `logger.*()` call, so it runs inside the
`AsyncLocalStorage` tree opened by the framework adapter. Forwarded events therefore inherit:

- the request user, from the adapter's `getUser` option or `setRequestUser`
- request tags from `setRequestTag` and global tags from `BugWatch.setTag`
- `traceId` / `spanId` from the request scope, an active `withSpan`, or `traceContextProvider`

The `category` tag wins over a scope tag of the same name, so avoid naming a scope tag `category`.

To correlate with an existing OpenTelemetry setup, wire the provider once at init:

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

const client = createClient({
	projectKey: process.env.BUGWATCH_KEY!,
	traceContextProvider: otelTraceContextProvider(),
});
```

## Complete example

```ts
// src/server.ts
import express, { type NextFunction, type Request, type Response } from "express";
import log4js from "log4js";
import { createClient } from "@newinstance/bugwatch";
import { bugWatchLog4jsAppender } from "@newinstance/bugwatch/log4js";
import {
	bugWatchExpressRequestHandler,
	bugWatchExpressErrorHandler,
} from "@newinstance/bugwatch/express";
import { installNodeErrorHandlers, setRequestTag, 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);

log4js.configure({
	appenders: {
		console: { type: "console" },
		bugwatch: { type: bugWatchLog4jsAppender(client) },
	},
	categories: {
		default: { appenders: ["console"], level: "debug" },
		payments: { appenders: ["console", "bugwatch"], level: "info" },
	},
});

const log = log4js.getLogger("payments");

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);
	// Scope tags carry the dimensions the appender cannot.
	setRequestTag("orderId", orderId);

	log.info(`checkout initiated for ${orderId}`);

	try {
		await processOrder(req.body);
		res.json({ ok: true });
	} catch (err) {
		// Error as its own argument so it is captured with a stack.
		log.error("checkout failed", err);
		res.status(500).json({ ok: false });
	}
});

// LAST: captures anything thrown downstream.
app.use(bugWatchExpressErrorHandler(client));

const server = app.listen(3000, () => log.info("listening on 3000"));

process.on("SIGTERM", () => {
	server.close(() => {
		log4js.shutdown(() => {
			void client.close();
		});
	});
});

async function processOrder(_body: unknown): Promise<void> {
	throw new Error("payment gateway timeout");
}
```

Events land with `category: "payments"`, the request user, `orderId` from the request scope, and
the active trace ids. Concurrent requests never share identity: each has its own scope cell.

## Troubleshooting

- **No events at all.** The category using the logger must list `bugwatch` in its `appenders`.
  A logger from `getLogger("other")` falls back to the `default` category, which may not include it.
- **`type` errors on configure.** Pass the returned object itself, `type: bugWatchLog4jsAppender(client)`,
  not a quoted string and not the function reference.
- **Events below the threshold missing.** The category `level` filters before the appender runs, so
  a `debug` call under `level: "info"` never reaches BugWatch.
- **Empty messages.** No string argument was passed and the first argument was not an object or a
  scalar. Lead with a string message.
- **Errors captured without a stack.** The `Error` was nested inside an object or array. Pass it as
  a top-level argument.
- **Events lost on exit.** Call `log4js.shutdown()` then `await client.close()` in your shutdown
  hook; the background flush runs every 5s by default.
- **Still silent.** Set `debug: true` at init (it prints delivery failures; successes are silent), and confirm
  `enabled` is not `false` for the current environment.
