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 useanyunder any circumstances - ZERO TOLERANCE for
as unknown as- Never useas unknown asunder any circumstances - ZERO TOLERANCE for
unknownas a cop-out - Only useunknownwhen 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
import { logger, createLogger, aiLogger, platformLogger } from '@/infra/logger.js';Log Levels (from least to most verbose)
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 verboseEnvironment Variable: Set LOG_LEVEL to control verbosity (e.g., LOG_LEVEL=debug)
Structured Logging
Always include structured data as the first argument:
// ✅ 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)
{
"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:
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:
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:
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:
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:
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:
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.authorizationreq.headers.cookiepasswordaccessTokenrefreshTokenapiKey
logger.info({
userId: '123',
accessToken: 'secret_token_123', // Will be logged as [REDACTED]
}, 'Token generated');Best Practices
✅ DO
// 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
// 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' })); // BadPerformance Considerations
Pino is extremely fast, but:
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 });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:
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
import { cliLogger } from '@/infra/cli-logger.js';Usage in Scripts
// 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
// 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
| Feature | logger (Production) | cliLogger (Scripts/Tests) |
|---|---|---|
| Output Format | JSON | Pretty-printed, colorized |
| Use Case | Services, routes, middleware | Scripts, tools, tests |
| PII Redaction | ✅ Yes | ✅ Yes |
| Structured Data | ✅ Yes | ✅ Yes |
| Log Levels | ✅ Yes | ✅ Yes |
| Human-Readable | ❌ No | ✅ Yes |
When to Use Each
// ✅ 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'); // FORBIDDENExamples
Service Class with Logging
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
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();
});