Skip to main content

Logging Guide

This project uses Pino for structured logging. Pino is a fast, low-overhead logging library that outputs JSON logs in production and human-readable logs in development.

Quick Start

import logger from "@/lib/utils";

// Basic logging
logger.info("User logged in");
logger.error("Database connection failed");
logger.warn("Deprecation warning");
logger.debug("Debugging information");

// Structured logging with context
logger.info({ userId: "123", action: "login" }, "User logged in");
logger.error({ err: error, userId: "123" }, "Failed to update user");

Log Levels

Logs are filtered by level based on the LOG_LEVEL environment variable:

  • fatal (60): Application crash
  • error (50): Error occurred, but application continues
  • warn (40): Warning messages
  • info (30): General information (default in production)
  • debug (20): Debugging information (default in development)
  • trace (10): Very detailed debugging

Configuration:

  • Development: LOG_LEVEL=debug (default)
  • Production: LOG_LEVEL=info (default)

Set in your .env file:

LOG_LEVEL=debug

Structured Logging

Always include relevant context in your logs:

// ❌ Bad - No context
logger.error("Failed to update activity");

// ✅ Good - With context
logger.error(
{
err: error,
activityId: "act-123",
userId: "user-456",
organizationId: "org-789"
},
"Failed to update activity"
);

Common Context Fields

  • err or error: Error object (automatically serialized)
  • userId: User performing the action
  • activityId, ropaId, organizationId: Entity IDs
  • operation: Operation name (e.g., "updateActivity", "deleteUser")
  • duration: Operation duration in ms
  • requestId: Request identifier (for tracing)

Helper Functions

logError(operation, error, context)

Convenience function for logging errors:

import { logError } from "@/lib/logger";

try {
await updateActivity(activityId);
} catch (error) {
logError("updateActivity", error, { activityId, userId });
// ... handle error
}

logOperation(operation, context, message)

Log successful operations:

import { logOperation } from "@/lib/logger";

logOperation(
"updateActivity",
{ activityId: "123", userId: "456" },
"Activity updated successfully"
);

createChildLogger(context)

Create a child logger with persistent context:

import { createChildLogger } from "@/lib/logger";

// In a request handler or service function
const requestLogger = createChildLogger({
requestId: "req-123",
userId: "user-456"
});

requestLogger.info("Processing request");
requestLogger.info({ activityId: "act-789" }, "Activity loaded");
// Both logs will include requestId and userId automatically

Examples

Service Layer

// src/services/activityService.js
import logger, { logError } from "@/lib/logger";

export const moveActivityToNewOu = async ({
ropaId,
activityId,
oldOuId,
newOuId,
}) => {
logger.debug(
{ ropaId, activityId, oldOuId, newOuId },
"Moving activity to new OU"
);

try {
// ... business logic

logger.info(
{ ropaId, activityId, oldOuId, newOuId },
"Activity moved successfully"
);

return ok(result, "Activity moved successfully");
} catch (error) {
logError("moveActivityToNewOu", error, {
ropaId,
activityId,
oldOuId,
newOuId
});
return fail(error.message);
}
};

Database Layer

// src/services/mongodb.js
import logger from "@/lib/logger";

mongoose.connection.on("error", (err) =>
logger.error({ err }, "MongoDB connection error")
);

mongoose.connection.once("connected", () => {
logger.info(
{ connectionState: mongoose.connection.readyState },
"MongoDB connected (singleton)"
);
});

Server Actions

// Server action with logging
"use server";

import logger from "@/lib/logger";

export async function updateActivityAction(formData) {
const activityId = formData.get("activityId");

logger.debug({ activityId }, "Updating activity from form");

try {
const result = await updateActivity(activityId, formData);

logger.info({ activityId, result }, "Activity updated via form");

return { success: true };
} catch (error) {
logger.error(
{ err: error, activityId },
"Failed to update activity from form"
);
return { success: false, error: error.message };
}
}

Performance Tips

  1. Use appropriate log levels: Debug logs are filtered out in production
  2. Avoid logging in hot loops: Logging has overhead, even with Pino
  3. Don't log sensitive data: PII, passwords, tokens, etc.
  4. Keep context objects small: Only include relevant fields

Production Setup

Output Format

In production, Pino outputs JSON logs:

{
"level": 50,
"time": 1703001234567,
"pid": 12345,
"hostname": "server-1",
"env": "production",
"ropaId": "ropa-123",
"activityId": "act-456",
"userId": "user-789",
"err": {
"type": "Error",
"message": "Failed to update activity",
"stack": "Error: Failed to update activity\n at ..."
},
"msg": "Failed to update activity"
}

Log Aggregation

JSON logs can be easily ingested by:

  • Datadog: Use the Datadog agent
  • Elasticsearch/Kibana: Use Filebeat or Logstash
  • CloudWatch: Use the CloudWatch agent
  • Sentry: Use Sentry's Pino transport
  • LogRocket: Use LogRocket's Pino integration

Example with Sentry transport:

// src/lib/logger.js
import pino from "pino";

const logger = pino({
// ... existing config
transport: {
targets: [
{
target: "pino-pretty", // Development
level: "debug",
},
{
target: "pino-sentry-transport", // Production errors
level: "error",
options: {
sentry: {
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
},
},
},
],
},
});

Best Practices

  1. Log at boundaries: Service entry/exit, external calls, database operations
  2. Include correlation IDs: Request IDs, user IDs, entity IDs
  3. Log business events: User actions, state changes, important operations
  4. Don't log everything: Avoid noise, focus on actionable information
  5. Use child loggers: For request-scoped or operation-scoped context
  6. Test your logs: Verify logs in development before deploying

Troubleshooting

Logs not appearing in development

Check that NODE_ENV is not set to production:

echo $NODE_ENV

Too many/few logs

Adjust LOG_LEVEL:

# Show all logs
LOG_LEVEL=trace npm run dev

# Show only errors
LOG_LEVEL=error npm run dev

Client-side logging

The logger automatically falls back to console.* methods in the browser. Client-side logs are not structured and should be used sparingly.

For better client-side error tracking, consider using:

  • Sentry (recommended)
  • LogRocket
  • Datadog RUM

Toast Logging (src/lib/ui/incidents/toasts.js)

Client components should never call toast.error or toast.success directly. Instead use the wrappers from @/lib/ui/incidents, which combine the visible notification with logging in one call:

FunctionToast typePino levelServer log
toastErrortoast.errorerror✅ via logToServer
toastSuccesstoast.successinfo✅ via logToServer

Both functions fire a client-side pino call and invoke the logToServer Next.js Server Action so the event is captured in the structured server log (JSON in production).

toastError({ message, err?, logMessage? })

import { toastError } from "@/lib/ui/incidents";

// Simple string message
toastError({ message: result.message || "Save failed" });

// With originating error object (attached to the pino log entry as { err })
toastError({ message: error.message, err: error });

// JSX toast content — logMessage provides the plain string for the log
toastError({
message: <div style={{ textAlign: "center" }}>{t("changeOuError")}</div>,
logMessage: t("changeOuError"),
});

toastSuccess({ message, logMessageOrOptions?, toastOptions? })

The second prop is overloaded: a string is treated as a log-message override (for JSX content); an object is passed through as react-hot-toast options.

import { toastSuccess } from "@/lib/ui/incidents";

// Simple string message
toastSuccess({ message: t("saveSuccess") });

// With react-hot-toast style options (second prop is an object → treated as options)
toastSuccess({
message: t("langUpdateSuccess"),
logMessageOrOptions: { style: { textAlign: "center", whiteSpace: "pre-line" } },
});

// JSX toast with both a log string and options
toastSuccess({
message: <div style={{ textAlign: "center" }}>{t("changeOuSuccess")}</div>,
logMessageOrOptions: t("changeOuSuccess"), // string → used as log message
toastOptions: { duration: 4000 },
});

logToServer (internal)

logToServer is a Next.js "use server" action called fire-and-forget by both wrappers. It routes the event through the server-side Pino instance so it appears in structured production logs. Do not call it directly — go through toastError / toastSuccess.

Client component
└─ toastError / toastSuccess
├─ toast.error / toast.success → visible notification
├─ logger.error / logger.info → client-side pino (console in browser)
└─ logToServer(level, msg) → server-side pino (structured JSON)

Migration Checklist

When migrating existing code to use the new logger:

  • Replace console.log with logger.info
  • Replace console.error with logger.error
  • Replace console.warn with logger.warn
  • Replace console.debug with logger.debug
  • Add structured context to all logs
  • Use logError() helper in catch blocks
  • Add operation logging for important business logic
  • Remove or update old logging comments
  • Replace toast.error(...) with toastError({ message, err? }) from @/lib/ui/incidents
  • Replace toast.success(...) with toastSuccess({ message }) from @/lib/ui/incidents

Additional Resources