Skip to main content

Logging Guide

This page outlines the structured logging conventions and best practices for the Talawa-API codebase.

Introduction

Talawa-API uses Pino, a high-performance JSON logger, integrated with Fastify. All logging should follow structured logging conventions to enable consistent log parsing, monitoring, and debugging.

Key Principles:

  1. Never use console.* statements - they are forbidden by linter rules
  2. Use structured logging with objects, not string interpolation
  3. Include correlation IDs for request tracing
  4. Use appropriate log levels

The Root Logger

The root logger is the base logger instance used across the application. Import it from:

import { rootLogger } from "~/src/utilities/logging/logger";

The root logger is pre-configured with:

  • Service name and environment metadata
  • Log level from LOG_LEVEL environment variable
  • Pretty printing in development mode
  • Sensitive field redaction (auth headers, passwords, tokens)

Structured Logging Format

Always use structured logging with an object as the first argument:

// ✅ CORRECT - Structured format
logger.info({ msg: "User created", userId: user.id });
logger.error({ msg: "Failed to fetch data", err: error, endpoint });
logger.warn({ msg: "Rate limit exceeded", key, remaining: 0 });

// ❌ WRONG - String interpolation
logger.info(`User ${userId} created`); // No structured data
console.log("User created:", userId); // Forbidden

Standard Fields

FieldDescriptionExample
msgHuman-readable message"User created"
errError object (for errors)new Error("Failed")
userIdAssociated user ID"abc123"
pluginIdPlugin identifier"my-plugin"
actionOperation being performed"install", "activate"

Log Levels

Use the appropriate log level for each situation:

LevelUsageExample
errorUnexpected failures, exceptionsDatabase connection failed
warnExpected issues, degraded behaviorDocker not available, rate limit hit
infoSignificant eventsPlugin loaded, server started
debugDetailed debugging infoQuery parameters, cache hits
// Error - Something went wrong unexpectedly
logger.error({ msg: "Database connection failed", err: error });

// Warn - Something expected but notable
logger.warn({ msg: "Docker not available. Skipping docker step.", pluginId });

// Info - Standard operational events
logger.info({ msg: "Plugin system initialized", successCount: 5, failedCount: 0 });

// Debug - Detailed information for debugging
logger.debug({ msg: "Query executed", sql, params });

Correlation IDs

Every HTTP request is assigned a correlation ID that flows through the entire request lifecycle. This enables tracing logs across services.

How It Works

  1. Fastify assigns a unique ID to each request (from x-correlation-id header or auto-generated)
  2. The ID is attached to the request logger via request.log
  3. Pass this logger to downstream functions
// In route handlers - use request.log
fastify.get("/api/data", async (request, reply) => {
request.log.info({ msg: "Processing request" }); // Includes correlationId
});

// In GraphQL context - logger is injected
const resolver = async (_, args, ctx) => {
ctx.log.info({ msg: "Resolver called", args });
};

Context-Specific Logging

HTTP Routes

In route handlers, use request.log which automatically includes the correlation ID:

fastify.post("/api/users", async (request, reply) => {
request.log.info({ msg: "Creating user" });

try {
const user = await createUser(request.body);
request.log.info({ msg: "User created", userId: user.id });
return { user };
} catch (error) {
request.log.error({ msg: "User creation failed", err: error });
throw error;
}
});

GraphQL Resolvers

In GraphQL resolvers, use ctx.log:

export const createPlugin = async (_, args, ctx) => {
ctx.log.info({ msg: "Installing plugin", pluginId: args.id });
// ...
};

Plugin System

The plugin context includes a logger. Use optional chaining since the logger might not be available:

this.pluginContext.logger.info?.({
msg: "Plugin activated",
pluginId,
});

this.pluginContext.logger.error?.({
msg: "Failed to load plugin",
pluginId,
err: error,
});

Background Workers

Inject the logger into background workers:

export async function startBackgroundWorker(deps: { logger: Logger }) {
const { logger } = deps;

logger.info({ msg: "Background worker started" });

// Use the injected logger for all operations
await processJob(logger);
}

Utilities (Outside Request Context)

For utilities that don't have a request-scoped logger, use rootLogger:

import { rootLogger } from "~/src/utilities/logging/logger";

export function someUtility() {
rootLogger.info({ msg: "Utility function called" });
}

What NOT To Do

Never Use console.*

// ❌ FORBIDDEN - Will fail linting
console.log("Debug:", data);
console.error("Error:", error);
console.warn("Warning");

// ✅ Use proper logger
logger.debug({ msg: "Debug", data });
logger.error({ msg: "Error", err: error });
logger.warn({ msg: "Warning" });

Never Use String-Only Logging

// ❌ WRONG - No structured data
logger.info("User created successfully");

// ✅ CORRECT - Include relevant context
logger.info({ msg: "User created successfully", userId: user.id });

Never Log Sensitive Data

The logger auto-redacts common sensitive fields, but be mindful:

// ❌ WRONG - Logging sensitive data
logger.info({ msg: "Login attempt", password: req.body.password });

// ✅ CORRECT - Omit sensitive fields
logger.info({ msg: "Login attempt", email: req.body.email });

Testing with Mocked Loggers

When testing code that logs, mock the logger:

function mockLogger() {
const base = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
child: vi.fn().mockImplementation(() => base),
};
return base;
}

it("logs when action occurs", async () => {
const logger = mockLogger();
await someFunction({ logger });

expect(logger.info).toHaveBeenCalledWith(
expect.objectContaining({
msg: "Action completed",
})
);
});

Environment Configuration

VariableDefaultDescription
LOG_LEVELinfoMinimum log level (debug, info, warn, error)
NODE_ENVdevelopmentPretty printing enabled in development

Summary

  1. Import: Use rootLogger or injected loggers
  2. Format: Always use { msg: "...", ...fields } structure
  3. Levels: error > warn > info > debug
  4. Context: Include relevant IDs (userId, pluginId, correlationId)
  5. Errors: Use { msg: "...", err: error } for error objects
  6. Never: Use console.* or log sensitive data