Skip to content

Logging Guide - Pino

CRITICAL: This project uses ONLY Pino for logging. Never use console.log, console.error, or any other logging mechanism in backend code.

⚠️ Project Constraints (ZERO TOLERANCE)

Before using this guide, understand these absolute requirements:

  • ZERO TOLERANCE for any - Never use any under any circumstances
  • ZERO TOLERANCE for as unknown as - Never use as unknown as under any circumstances
  • ZERO TOLERANCE for unknown as a cop-out - Only use unknown when truly unknown, then narrow immediately
  • ZERO TOLERANCE for console.* - Only use Pino loggers (logger, platformLogger, etc.)
  • Explicit types always - Define interfaces for all data structures
  • All types in apps/backend/src/types/ - Never define types inline or in random files

Overview

We use pino for structured, high-performance logging. All logs are:

  • Structured JSON in production (machine-readable)
  • Pretty-printed in development (human-readable)
  • Automatically redacted for sensitive data (tokens, passwords, etc.)
  • Contextual with domain-specific loggers

Basic Usage

Import the Logger

typescript
import { logger, createLogger, aiLogger, platformLogger } from '@/infra/logger.js';

Log Levels (from least to most verbose)

typescript
logger.fatal('System is unusable');      // Rarely used
logger.error('Database connection failed');
logger.warn('API rate limit approaching');
logger.info('User logged in');           // Default in production
logger.debug('Cache hit for key: xyz');  // Default in development
logger.trace('Entering function foo()'); // Most verbose

Environment Variable: Set LOG_LEVEL to control verbosity (e.g., LOG_LEVEL=debug)

Structured Logging

Always include structured data as the first argument:

typescript
// ✅ CORRECT - Structured data first, message second
logger.info({ userId: '123', action: 'login' }, 'User authenticated');

// ❌ WRONG - Message first
logger.info('User authenticated', { userId: '123' });

Output in Production (JSON)

json
{
  "level": "info",
  "time": 1704409200000,
  "service": "tendsocial-backend",
  "env": "production",
  "userId": "123",
  "action": "login",
  "msg": "User authenticated"
}

Output in Development (Pretty-printed)

[2024-01-04 19:00:00.000] INFO: User authenticated
    userId: "123"
    action: "login"

Domain-Specific Loggers

Use pre-configured loggers for different domains:

typescript
import {
    aiLogger,          // AI/generation operations
    platformLogger,    // Social platform operations
    analyticsLogger,   // Analytics operations
    authLogger,        // Authentication operations
    billingLogger,     // Billing operations
    scraperLogger,     // Scraper operations
    webhookLogger,     // Webhook operations
    jobLogger,         // Job queue operations
    dbLogger,          // Database operations
} from '@/infra/logger.js';

// Example usage
aiLogger.info({ model: 'gpt-4', tokensUsed: 1500 }, 'Content generated');
platformLogger.warn({ platform: 'twitter', retryCount: 3 }, 'Rate limited');

Creating Custom Loggers

For service-specific logging:

typescript
import { createLogger } from '@/infra/logger.js';

export class CampaignService {
    private logger = createLogger('campaign-service');

    async create(data: CampaignData) {
        this.logger.info({ campaignId: data.id, type: data.type }, 'Creating campaign');
        // ...
    }
}

With persistent context:

typescript
export class UserService {
    private logger: pino.Logger;

    constructor(userId: string) {
        // All logs from this instance will include userId
        this.logger = createLogger('user-service', { userId });
    }

    async updateProfile() {
        this.logger.info('Profile updated'); // Automatically includes userId
    }
}

Error Logging

Using the logError Utility

For comprehensive error logging with full context:

typescript
import { logError, aiLogger } from '@/infra/logger.js';
import { AppError } from '@/infra/errors.js';

try {
    await riskyOperation();
} catch (error) {
    logError(
        aiLogger,
        error instanceof Error ? error : new Error(String(error)),
        'Failed to generate content',
        { campaignId: '123', model: 'gpt-4' }
    );
    throw error;
}

Direct Error Logging

For simple cases:

typescript
try {
    await operation();
} catch (error) {
    logger.error({ err: error, userId: '123' }, 'Operation failed');
}

AppError Integration

AppError instances automatically serialize with code, statusCode, and context using the toLogJSON() method:

typescript
import { NotFoundError } from '@/infra/errors.js';

try {
    const user = await db.user.findUnique({ where: { id } });
    if (!user) {
        throw new NotFoundError('User', id);
    }
} catch (error) {
    // Logs: { code: "NOT_FOUND", statusCode: 404, resource: "User", id: "123" }
    logError(logger, error, 'User fetch failed');
}

Automatic Redaction

These fields are automatically redacted from logs:

  • req.headers.authorization
  • req.headers.cookie
  • password
  • accessToken
  • refreshToken
  • apiKey
typescript
logger.info({
    userId: '123',
    accessToken: 'secret_token_123',  // Will be logged as [REDACTED]
}, 'Token generated');

Best Practices

✅ DO

typescript
// Use structured data
logger.info({ userId, postId, platform: 'twitter' }, 'Post published');

// Use appropriate log levels
logger.debug({ cacheKey: 'user:123' }, 'Cache lookup');
logger.error({ err: error, operation: 'db-write' }, 'Database error');

// Include error objects properly
logger.error({ err: error }, 'Operation failed');

// Use domain loggers
platformLogger.info({ platform: 'linkedin' }, 'Account connected');

❌ DON'T

typescript
// Don't use string concatenation
logger.info('User ' + userId + ' logged in'); // Bad

// Don't log sensitive data unstructured
logger.info('Token: ' + accessToken); // Bad - might not be redacted

// Don't use console.log
console.log('User logged in'); // Use logger instead

// Don't log structured data as strings
logger.info(JSON.stringify({ userId: '123' })); // Bad

Performance Considerations

Pino is extremely fast, but:

  1. Avoid expensive operations in log statements:

    typescript
    // ❌ BAD - JSON.stringify is expensive
    logger.debug(JSON.stringify(largeObject));
    
    // ✅ GOOD - Let pino handle serialization
    logger.debug({ data: largeObject });
  2. Use appropriate log levels:

    typescript
    // Only computed in debug mode
    if (logger.isLevelEnabled('debug')) {
        logger.debug({ expensiveData: computeExpensiveData() });
    }

Testing

In tests, you can mock or suppress logging:

typescript
import { logger } from '@/infra/logger.js';

// Suppress all logs in tests
beforeAll(() => {
    logger.level = 'silent';
});

CLI Logger for Scripts and Tests

For scripts, CLI tools, and test files, use cliLogger which provides human-readable output while still using Pino infrastructure:

Import CLI Logger

typescript
import { cliLogger } from '@/infra/cli-logger.js';

Usage in Scripts

typescript
// scripts/my-script.ts
import { cliLogger } from '../src/infra/cli-logger.js';

cliLogger.info('Starting data migration...');
cliLogger.debug({ recordCount: 1000 }, 'Processing records');
cliLogger.error({ err: error }, 'Migration failed');

Usage in Tests

typescript
// test/integration/my-test.test.ts
import { cliLogger } from '@/infra/cli-logger.js';

describe('My Feature', () => {
    it('should work', async () => {
        cliLogger.info('Running test scenario...');
        // test code
    });
});

CLI Logger vs Production Logger

Featurelogger (Production)cliLogger (Scripts/Tests)
Output FormatJSONPretty-printed, colorized
Use CaseServices, routes, middlewareScripts, tools, tests
PII Redaction✅ Yes✅ Yes
Structured Data✅ Yes✅ Yes
Log Levels✅ Yes✅ Yes
Human-Readable❌ No✅ Yes

When to Use Each

typescript
// ✅ Production code (services, routes, middleware)
import { logger, createLogger } from '@/infra/logger.js';

// ✅ Scripts (database seeds, migrations, CLI tools)
import { cliLogger } from '@/infra/cli-logger.js';

// ✅ Tests (unit tests, integration tests)
import { cliLogger } from '@/infra/cli-logger.js';

// ❌ NEVER use console.* anywhere in backend
console.log('anything'); // FORBIDDEN

Examples

Service Class with Logging

typescript
import { createLogger, logError } from '@/infra/logger.js';
import { NotFoundError } from '@/infra/errors.js';

export class PostService {
    private logger = createLogger('post-service');

    async publish(postId: string, platforms: string[]) {
        this.logger.info({ postId, platforms }, 'Publishing post');

        try {
            const post = await this.getPost(postId);
            const results = await this.publishToAll(post, platforms);

            this.logger.info(
                { postId, successCount: results.filter(r => r.success).length },
                'Post published'
            );

            return results;
        } catch (error) {
            logError(
                this.logger,
                error instanceof Error ? error : new Error(String(error)),
                'Post publishing failed',
                { postId, platforms }
            );
            throw error;
        }
    }

    private async getPost(id: string) {
        this.logger.debug({ postId: id }, 'Fetching post');
        // ...
    }
}

HTTP Request Logging

typescript
import { logger } from '@/infra/logger.js';

app.addHook('onRequest', (request, reply, done) => {
    logger.info({
        reqId: request.id,
        method: request.method,
        url: request.url,
    }, 'Incoming request');
    done();
});

app.addHook('onResponse', (request, reply, done) => {
    logger.info({
        reqId: request.id,
        statusCode: reply.statusCode,
        responseTime: reply.getResponseTime(),
    }, 'Request completed');
    done();
});

TendSocial Documentation