Structured Logs
Use @nebutra/logger for structured, tenant-aware JSON logging across the Nebutra stack.
@nebutra/logger is Nebutra's structured logging library. It writes newline-delimited JSON to stdout, automatically injects tenant context, and forwards warn and error level events to Sentry as breadcrumbs and captured exceptions.
Why Not console.log?
console.log produces unstructured strings that are difficult to search, parse, and correlate. @nebutra/logger gives you:
- Structured JSON — every log line is a parseable object with consistent fields
- Tenant context —
tenantId,orgId, anduserIdare injected automatically - Log levels — filter noise in production by setting
LOG_LEVEL=warn - Sentry integration —
warnanderrorlogs become Sentry breadcrumbs; uncaught errors become Sentry issues - Zero
console.login production — enforced by the project's ESLint rules
Never use console.log, console.warn, or console.error in application code. Use @nebutra/logger instead. The project's Stop hook will flag console.log in any modified file before the session ends.
Basic Usage
import { logger } from "@nebutra/logger";
// Information — routine operational events
logger.info("Project created", { projectId, tenantId, userId });
// Warning — abnormal but recoverable situations
logger.warn("Quota approaching limit", {
tenantId,
used: 8100,
limit: 10000,
percentage: 81,
});
// Error — failures that require attention
logger.error("Payment processing failed", {
error,
invoiceId,
tenantId,
});Log Levels
| Level | When to Use | Sentry Behaviour |
|---|---|---|
trace | Verbose debugging (loops, low-level I/O) | Not forwarded |
debug | Developer debugging details | Not forwarded |
info | Normal operational events (created, updated, sent) | Not forwarded |
warn | Unexpected but recoverable conditions | Sentry breadcrumb |
error | Failures that affect functionality | Sentry breadcrumb + captured exception |
fatal | Unrecoverable failures requiring immediate attention | Sentry event (high severity) |
Configuring the Active Level
Set LOG_LEVEL to control the minimum level written to stdout:
LOG_LEVEL=debug # development — verbose
LOG_LEVEL=info # staging — normal
LOG_LEVEL=warn # production — warnings and above onlyThe default is info when LOG_LEVEL is unset.
Structured Log Format
Each log line is a JSON object with the following fields:
{
"level": "info",
"time": "2026-03-31T12:34:56.789Z",
"msg": "Project created",
"projectId": "proj_abc123",
"tenantId": "org_xyz789",
"userId": "user_def456",
"service": "api-gateway",
"env": "production",
"release": "v2.4.1"
}service, env, and release are injected automatically from environment variables. You only need to supply the domain-specific fields.
Tenant Context Injection
When called from within a request that has passed through Nebutra's tenant middleware, logger automatically reads the current tenant context from AsyncLocalStorage and adds tenantId, orgId, and userId to every log line. You do not need to pass these manually:
// ✅ Preferred — tenant context is injected automatically
logger.info("Invoice generated", { invoiceId, amount });
// The emitted JSON will include tenantId and userId from the request context
// {
// "msg": "Invoice generated",
// "invoiceId": "inv_xxx",
// "amount": 99.99,
// "tenantId": "org_yyy", ← injected automatically
// "userId": "user_zzz" ← injected automatically
// }If you need to log outside of a request context (e.g. a cron job), pass tenant identifiers explicitly:
logger.info("Scheduled report generated", {
reportId,
tenantId: job.tenantId,
});Error Logging
Always pass the Error object directly. The logger serialises the message, name, stack, and any additional properties:
try {
await stripe.invoices.pay(invoiceId);
} catch (error) {
logger.error("Stripe invoice payment failed", {
error,
invoiceId,
tenantId,
});
throw new Error("Payment processing failed — please try again");
}logger.error automatically calls Sentry.captureException(error) with the extra properties as context. The error will appear in Sentry's Issues tab within seconds.
Sentry Integration Detail
The logger's Sentry integration works as follows:
| Logger call | Sentry behaviour |
|---|---|
logger.warn(msg, ctx) | Sentry.addBreadcrumb({ level: "warning", message: msg, data: ctx }) |
logger.error(msg, { error, ...ctx }) | Sentry.addBreadcrumb(...) + Sentry.captureException(error, { extra: ctx }) |
logger.fatal(msg, { error, ...ctx }) | Same as error but with level: "fatal" in Sentry |
This means Sentry issues always include the breadcrumb trail of warn events that preceded the error, giving full context for debugging.
Querying Logs
In Sentry
All warn and error logs appear as breadcrumbs on Sentry issues. Navigate to any issue and open the Breadcrumbs section to see the sequence of log events that preceded the error.
In Local Development
Pipe the Next.js dev server output through jq for a formatted JSON view:
pnpm dev 2>&1 | jq '.'Or filter by log level:
pnpm dev 2>&1 | jq 'select(.level == "error")'In Production (stdout)
If your deployment platform supports log streaming (Vercel Log Drains, Railway, Render), configure it to ship stdout JSON to a log aggregation service.
Nebutra emits structured JSON logs to stdout. Use your deployment platform's log drain to ship them to a centralised service.
Vercel Log Drains
Configure in Vercel Dashboard → Project → Settings → Log Drains. Supported sinks:
| Sink | Format | Use when |
|---|---|---|
| Datadog | JSON | You already use Datadog APM |
| Grafana Loki | JSON | Self-hosted or Grafana Cloud |
| HTTP endpoint | JSON | Custom ingest (Axiom, Better Stack, etc.) |
Datadog quick-start
- Install the Vercel + Datadog integration from the Vercel Marketplace.
- Add
DATADOG_API_KEYto your Vercel project environment variables. - The integration automatically creates a log drain that forwards all stdout JSON to Datadog Logs.
// logs appear in Datadog with these default attributes:
// service: "nebutra", env: "production", version: <VERCEL_GIT_COMMIT_SHA>
import { logger } from "@nebutra/logger";
logger.info("order.created", { orderId, tenantId });Grafana Loki (self-hosted)
Set up promtail or use the Grafana Cloud Logs drain:
# promtail config snippet
scrape_configs:
- job_name: nebutra
static_configs:
- targets: [localhost]
labels:
app: nebutra
env: production
__path__: /var/log/nebutra/*.logCurrent coverage
| Log level | Where it appears |
|---|---|
error | Sentry breadcrumbs + log drain |
warn | Sentry breadcrumbs + log drain |
info | Log drain only |
debug | Log drain only (not emitted in production by default) |
Set NEBUTRA_LOG_LEVEL=debug temporarily in a specific environment to capture verbose output without affecting production.
Child Loggers
For libraries or modules that always emit a consistent set of context fields, create a child logger:
import { logger } from "@nebutra/logger";
const queueLogger = logger.child({ service: "queue", provider: "bullmq" });
queueLogger.info("Job enqueued", { jobId, queue: "email" });
// → { "service": "queue", "provider": "bullmq", "msg": "Job enqueued", "jobId": "...", "queue": "email", ... }Monitoring Overview
Architecture, alert channels, and key metrics for the full monitoring stack.
Sentry Setup
Configure Sentry error tracking, performance monitoring, and release tracking.
Multi-Tenancy
How tenant context is propagated through AsyncLocalStorage.
Troubleshooting
Diagnose common issues using Sentry and structured log queries.
How is this guide?
Last updated on