Winston
Forwarding Winston logs to BugWatchlink
The @newinstance/bugwatch/winston subpath ships a winston-transport implementation that forwards
every log record Winston writes to BugWatch, in addition to your existing transports. It does not
replace your console or file output.
Installlink
npm install @newinstance/bugwatchThat is the whole install. winston (>= 3) is an optional peer dependency you already have (it brings winston-transport with it). There is no separate plugin package.
Get a project key from the BugWatch dashboard at www.newinstance.cloud,
under BugWatch, your project, Settings, DSN keys. Use the sk_test_... key in test and sk_live_...
in production.
Adding the transportlink
import winston from "winston";
import { createClient } from "@newinstance/bugwatch";
import { createBugWatchWinstonTransport } from "@newinstance/bugwatch/winston";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
const logger = winston.createLogger({
level: "info",
transports: [
new winston.transports.Console(),
createBugWatchWinstonTransport(client),
],
});
logger.error("payment gateway timeout", { gateway: "stripe" });The second argument is a standard TransportStreamOptions, so you can forward only the noisy-free
tail of your logs while the console keeps everything:
createBugWatchWinstonTransport(client, { level: "warn" });The transport is failure isolated: anything that goes wrong inside it is swallowed, so a BugWatch problem can never throw into your logging path.
Level mappinglink
Winston level names are translated to BugWatch numeric severities (the Pino-compatible 10 to 60 scale) before the event is sent.
| Winston level | BugWatch level |
|---|---|
fatal | 60 |
error | 50 |
warn | 40 |
info, http | 30 |
verbose, debug | 20 |
silly, trace | 10 |
Any level name not in that table (a custom level from winston.addColors / custom levels) falls
back to info (30). If you use custom level names and care about severity in the dashboard, name
them after one of the rows above.
Tags are scalar onlylink
Every property on the Winston info object other than level, message, and stack becomes a
BugWatch tag, but only if its value is a string, number, boolean, or bigint. Each value is
stringified. Objects, arrays, null, and undefined are dropped so events stay flat.
logger.warn("slow query", {
table: "orders", // kept, tag table="orders"
duration_ms: 1200, // kept, tag duration_ms="1200"
cached: false, // kept, tag cached="false"
query: { where: { id: 7 } }, // DROPPED, object
});Workaround: flatten or stringify before logging.
logger.warn("slow query", {
table: "orders",
duration_ms: 1200,
query: JSON.stringify({ where: { id: 7 } }),
});If the message itself is not a string, it is coerced with String(...).
Errors and stacktraces through Winston (read this)link
Be aware of what the transport does not do. It calls captureLog with a level, a message, and
tags. It never passes an error value, and it explicitly skips the stack key on the info
object.
Consequences:
logger.error(new Error("db down"))arrives as a level 50 log whose message is the error message. No stacktrace, no exception type, no grouping as an exception.winston.format.errors({ stack: true })does not help. It puts the stack oninfo.stack, which is exactly the key the transport skips.logger.error("db down", { err })loseserrentirely, because anErroris an object and the tag filter keeps scalars only.- Cause chains (
new Error(msg, { cause })) are not reconstructed, because no error object is forwarded.
Recommended pattern: keep Winston for logs, and capture exceptions directly.
try {
await processPayment(order);
} catch (err) {
logger.error("payment failed", { orderId: order.id }); // human-readable log line
client.captureException(err, { tags: { orderId: order.id } }); // full stacktrace + grouping
}captureException normalizes the Error, keeps the stacktrace, follows cause chains, and groups
occurrences in the dashboard. Reach for it for anything you want to triage as an exception.
Request scope enrichment and trace correlationlink
Forwarded logs are captured through the same pipeline as captureException, so they inherit the
active scope. When a framework adapter (express, koa, fastify, hapi, nest, next) opens its
per-request AsyncLocalStorage scope, a logger.info(...) inside that request automatically picks
up the request's method, url, route, requestId, its user (from the adapter's getUser
option or setRequestUser), any setRequestTag / setRequestContext values, and the request's
trace and span IDs. Trace IDs come from an inbound traceparent header when present, otherwise a
fresh trace ID is generated per request, so logs and exceptions from one request correlate. If you
configured traceContextProvider (for example the OpenTelemetry bridge), it is consulted per event
too.
Concurrency is safe: each request has its own storage cell, so two overlapping requests never see each other's user or tags.
Outside a web framework (workers, CLIs, queue consumers), wire it up yourself:
import { installAsyncScope, runWithContext } from "@newinstance/bugwatch/node";
installAsyncScope(client);
await runWithContext({ requestId: job.id, route: "job:reindex" }, async () => {
logger.info("job started"); // arrives tagged with requestId and route
await reindex(job);
});Request scope values win over anything set globally with client.setTag / client.setUser.
Complete examplelink
// src/server.ts
import express, { type Request, type Response, type NextFunction } from "express";
import winston from "winston";
import { createClient } from "@newinstance/bugwatch";
import { createBugWatchWinstonTransport } from "@newinstance/bugwatch/winston";
import {
bugWatchExpressRequestHandler,
bugWatchExpressErrorHandler,
} from "@newinstance/bugwatch/express";
import { setRequestTag } from "@newinstance/bugwatch/node";
if (!process.env.BUGWATCH_KEY) throw new Error("BUGWATCH_KEY is not set");
const client = createClient({
projectKey: process.env.BUGWATCH_KEY,
environment: process.env.NODE_ENV ?? "development",
release: process.env.GIT_SHA,
});
const logger = winston.createLogger({
level: "info",
transports: [
new winston.transports.Console({ format: winston.format.simple() }),
createBugWatchWinstonTransport(client, { level: "info" }),
],
});
const app = express();
app.use(express.json());
// First: opens the per-request scope that enriches every forwarded log.
app.use(bugWatchExpressRequestHandler(client, { getUser: (req) => (req as any).user }));
app.post("/orders", async (req: Request, res: Response, next: NextFunction) => {
setRequestTag("plan", (req.headers["x-plan"] as string) ?? "free");
logger.info("creating order", { itemCount: req.body.items?.length ?? 0 });
try {
const order = await createOrder(req.body);
logger.info("order created", { orderId: order.id, total_cents: order.totalCents });
res.json(order);
} catch (err) {
// Log line for humans, plus a real exception with the stacktrace.
logger.error("order creation failed", { step: "createOrder" });
client.captureException(err, { tags: { step: "createOrder" } });
next(err);
}
});
// Last: captures anything thrown further up the stack.
app.use(bugWatchExpressErrorHandler(client));
const server = app.listen(3000, () => logger.info("listening", { port: 3000 }));
process.on("SIGTERM", async () => {
server.close();
await client.close(); // flushes pending events before exit
});
async function createOrder(body: unknown): Promise<{ id: string; totalCents: number }> {
throw new Error("payment gateway timeout");
}Troubleshootinglink
Logs never appear in the dashboard. Events are batched and flushed on an interval. In
short-lived processes (scripts, serverless handlers, containers being torn down) call
await client.flush() or await client.close() before exit. Turn on debug: true in
createClient and watch for [bugwatch] delivery-failure diagnostics (debug mode prints failures, not successes).
Some log fields are missing. Only scalar values become tags. See the scalar-only section above and stringify nested data.
No stacktrace on an error log. Expected. The Winston transport does not forward errors or
stacks. Use client.captureException(err).
Everything shows up as info. Your logger uses custom level names that are not in the mapping
table, so they fall back to 30. Rename them, or capture severity-critical events with
client.captureLog({ level: 50, message }).
Nothing at all is forwarded, but the console transport works. Check the transport's own level
option and the logger's level. Winston filters per transport, and the more restrictive of the two
wins.
401 Invalid or inactive API key in debug output. The key is revoked or from the wrong
environment. Mint a fresh DSN key under BugWatch, your project, Settings, DSN keys, and confirm
sk_live_... in production.