Logger Usage Guide
This guide provides comprehensive examples of using the Pino logger throughout the B3API application.
Overview
The B3API logger is built on Pino and outputs structured JSON logs. All logs include:
level: Log level (debug, info, warn, error)time: ISO 8601 timestamppid: Process IDhostname: Server hostnameservice: Service name (b3api)msg: Human-readable message- Additional context fields specific to your log
Importing the Logger
The logger module provides three main functions:
import { logger, createLogger, createChildLogger } from './utils/logger';- logger: Default global logger instance
- createLogger(context): Creates a logger with context (service/controller name)
- createChildLogger(parent, bindings): Creates a child logger with additional context
Basic Usage Patterns
1. Global Logger
Use the default logger for quick logging:
import { logger } from './utils/logger';
// Info log
logger.info({ msg: 'Application started', port: 3000 });
// Error log
logger.error({
msg: 'Database connection failed',
error: err.message,
stack: err.stack
});
// Warning
logger.warn({
msg: 'Memory usage high',
usage: 0.85,
threshold: 0.8
});
// Debug (only shows if LOG_LEVEL=debug)
logger.debug({ msg: 'Cache lookup', key: 'user:123', hit: true });2. Context-Specific Logger
Create a logger with a context identifier for better log organization:
import { createLogger } from './utils/logger';
export class UserService {
private logger = createLogger('UserService');
async createUser(data: CreateUserDto) {
this.logger.info({ msg: 'Creating user', email: data.email });
try {
const user = await this.userRepository.create(data);
this.logger.info({
msg: 'User created successfully',
user_id: user.id,
email: user.email
});
return user;
} catch (err: any) {
this.logger.error({
msg: 'Failed to create user',
error: err.message,
stack: err.stack,
email: data.email
});
throw err;
}
}
}Output:
{
"level": "info",
"time": "2025-12-16T10:30:45.123Z",
"service": "b3api",
"context": "UserService",
"msg": "User created successfully",
"user_id": 123,
"email": "user@example.com"
}3. Child Loggers
Create child loggers to add context without repeating it:
import { createLogger, createChildLogger } from './utils/logger';
export class ReportService {
private logger = createLogger('ReportService');
async generateReport(reportId: number, userId: number) {
// Create child logger with report context
const reportLogger = createChildLogger(this.logger, {
report_id: reportId,
user_id: userId
});
reportLogger.info({ msg: 'Starting report generation' });
try {
const data = await this.fetchReportData(reportId);
reportLogger.debug({ msg: 'Data fetched', rows: data.length });
const pdf = await this.generatePDF(data);
reportLogger.debug({ msg: 'PDF generated', size_kb: pdf.length / 1024 });
await this.saveReport(reportId, pdf);
reportLogger.info({ msg: 'Report saved', size_kb: pdf.length / 1024 });
return { success: true, reportId };
} catch (err: any) {
reportLogger.error({
msg: 'Report generation failed',
error: err.message,
stack: err.stack
});
throw err;
}
}
}Benefits:
report_idanduser_idare automatically included in all logs- No need to repeat context in every log statement
- Easy to track a specific operation across multiple log entries
Usage by Component Type
Controllers
Log incoming requests, validation, and responses:
import { post, requestBody } from '@loopback/rest';
import { createLogger } from '../utils/logger';
export class UserController {
private logger = createLogger('UserController');
@post('/users')
async create(@requestBody() userData: CreateUserDto) {
this.logger.info({
msg: 'POST /users',
email: userData.email
});
try {
// Validate
this.logger.debug({ msg: 'Validating user data', email: userData.email });
await this.validator.validate(userData);
// Create user
const user = await this.userService.create(userData);
this.logger.info({
msg: 'User created',
user_id: user.id,
status: 'success'
});
return user;
} catch (err: any) {
this.logger.error({
msg: 'User creation failed',
error: err.message,
stack: err.stack,
email: userData.email
});
throw err; // Re-throw - will be caught by global error handler
}
}
@get('/users/{id}')
async findById(@param.path.number('id') id: number) {
this.logger.info({ msg: 'GET /users/:id', user_id: id });
const user = await this.userRepository.findById(id);
if (!user) {
this.logger.warn({ msg: 'User not found', user_id: id });
throw new HttpErrors.NotFound('User not found');
}
this.logger.info({ msg: 'User retrieved', user_id: id });
return user;
}
}Services
Log business logic operations and their outcomes:
import { createLogger } from '../utils/logger';
export class OrderService {
private logger = createLogger('OrderService');
async processOrder(orderId: number) {
const startTime = Date.now();
this.logger.info({ msg: 'Processing order', order_id: orderId });
try {
// Fetch order
this.logger.debug({ msg: 'Fetching order', order_id: orderId });
const order = await this.orderRepository.findById(orderId);
// Validate inventory
this.logger.debug({ msg: 'Validating inventory', order_id: orderId });
await this.inventoryService.validate(order.items);
// Process payment
this.logger.info({ msg: 'Processing payment', order_id: orderId, amount: order.total });
const payment = await this.paymentService.charge(order);
// Update order status
await this.orderRepository.updateById(orderId, {
status: 'completed',
paymentId: payment.id
});
const duration = Date.now() - startTime;
this.logger.info({
msg: 'Order processed successfully',
order_id: orderId,
payment_id: payment.id,
duration_ms: duration,
status: 'success'
});
return { success: true, orderId, paymentId: payment.id };
} catch (err: any) {
const duration = Date.now() - startTime;
this.logger.error({
msg: 'Order processing failed',
order_id: orderId,
error: err.message,
stack: err.stack,
duration_ms: duration,
status: 'error'
});
throw err;
}
}
}Repositories
Log database operations and query results:
import { DefaultCrudRepository } from '@loopback/repository';
import { createLogger } from '../utils/logger';
export class UserRepository extends DefaultCrudRepository<
User,
typeof User.prototype.id
> {
private logger = createLogger('UserRepository');
async findByEmail(email: string): Promise<User | null> {
this.logger.debug({ msg: 'Finding user by email', email });
try {
const user = await this.findOne({ where: { email } });
if (user) {
this.logger.debug({
msg: 'User found',
user_id: user.id,
email
});
} else {
this.logger.debug({ msg: 'User not found', email });
}
return user;
} catch (err: any) {
this.logger.error({
msg: 'Database query failed',
error: err.message,
stack: err.stack,
email
});
throw err;
}
}
async createWithAudit(userData: Partial<User>): Promise<User> {
this.logger.info({
msg: 'Creating user with audit',
email: userData.email
});
const startTime = Date.now();
try {
const user = await this.create(userData);
const duration = Date.now() - startTime;
this.logger.info({
msg: 'User created',
user_id: user.id,
email: user.email,
duration_ms: duration
});
return user;
} catch (err: any) {
const duration = Date.now() - startTime;
this.logger.error({
msg: 'User creation failed',
error: err.message,
stack: err.stack,
email: userData.email,
duration_ms: duration
});
throw err;
}
}
}Background Workers / Cron Jobs
Log scheduled task execution:
import { createLogger } from '../utils/logger';
export class DailyReportWorker {
private logger = createLogger('DailyReportWorker');
async execute() {
this.logger.info({ msg: 'Starting daily report generation' });
const startTime = Date.now();
try {
// Fetch data
this.logger.info({ msg: 'Fetching report data' });
const data = await this.fetchData();
this.logger.debug({ msg: 'Data fetched', rows: data.length });
// Generate report
this.logger.info({ msg: 'Generating report' });
const report = await this.generateReport(data);
this.logger.debug({ msg: 'Report generated', pages: report.pages });
// Send email
this.logger.info({ msg: 'Sending report email', recipients: 5 });
await this.emailService.sendReport(report);
const duration = Date.now() - startTime;
this.logger.info({
msg: 'Daily report completed',
rows: data.length,
pages: report.pages,
duration_ms: duration,
status: 'success'
});
} catch (err: any) {
const duration = Date.now() - startTime;
this.logger.error({
msg: 'Daily report failed',
error: err.message,
stack: err.stack,
duration_ms: duration,
status: 'error'
});
}
}
}Middleware / Interceptors
Log cross-cutting concerns:
import { Interceptor, InvocationContext, ValueOrPromise, InvocationResult, Provider } from '@loopback/core';
import { logger } from '../utils/logger';
export class LoggingInterceptor implements Provider<Interceptor> {
value() {
return async (
context: InvocationContext,
next: () => ValueOrPromise<InvocationResult>
) => {
const startTime = Date.now();
const methodName = `${context.targetClass.name}.${context.methodName}`;
logger.info({
msg: 'Method invocation started',
method: methodName,
args_count: context.args.length
});
try {
const result = await next();
const duration = Date.now() - startTime;
logger.info({
msg: 'Method invocation completed',
method: methodName,
duration_ms: duration,
status: 'success'
});
return result;
} catch (err: any) {
const duration = Date.now() - startTime;
logger.error({
msg: 'Method invocation failed',
method: methodName,
error: err.message,
stack: err.stack,
duration_ms: duration,
status: 'error'
});
throw err;
}
};
}
}Advanced Patterns
Tracking Multi-Step Operations
Use a transaction ID to track complex operations:
import { v4 as uuidv4 } from 'uuid';
import { createLogger, createChildLogger } from './utils/logger';
export class OrderProcessingService {
private logger = createLogger('OrderProcessingService');
async processOrder(orderId: number) {
const transactionId = uuidv4();
const txLogger = createChildLogger(this.logger, {
transaction_id: transactionId,
order_id: orderId
});
txLogger.info({ msg: 'Transaction started' });
try {
// Step 1: Validate
txLogger.info({ msg: 'Step 1: Validating order' });
await this.validateOrder(orderId, txLogger);
// Step 2: Reserve inventory
txLogger.info({ msg: 'Step 2: Reserving inventory' });
await this.reserveInventory(orderId, txLogger);
// Step 3: Process payment
txLogger.info({ msg: 'Step 3: Processing payment' });
await this.processPayment(orderId, txLogger);
// Step 4: Confirm order
txLogger.info({ msg: 'Step 4: Confirming order' });
await this.confirmOrder(orderId, txLogger);
txLogger.info({ msg: 'Transaction completed successfully' });
} catch (err: any) {
txLogger.error({
msg: 'Transaction failed',
error: err.message,
stack: err.stack
});
throw err;
}
}
private async validateOrder(orderId: number, logger: any) {
logger.debug({ msg: 'Validation started' });
// ... validation logic ...
logger.debug({ msg: 'Validation passed' });
}
}Performance Monitoring
Track operation performance:
import { createLogger } from './utils/logger';
export class DataService {
private logger = createLogger('DataService');
async processLargeDataset(datasetId: string) {
const startTime = Date.now();
const perfLogger = createChildLogger(this.logger, { dataset_id: datasetId });
perfLogger.info({ msg: 'Processing started' });
// Track each phase
const t1 = Date.now();
const data = await this.loadData(datasetId);
perfLogger.debug({ msg: 'Load phase', duration_ms: Date.now() - t1, rows: data.length });
const t2 = Date.now();
const transformed = await this.transformData(data);
perfLogger.debug({ msg: 'Transform phase', duration_ms: Date.now() - t2 });
const t3 = Date.now();
await this.saveData(transformed);
perfLogger.debug({ msg: 'Save phase', duration_ms: Date.now() - t3 });
const totalDuration = Date.now() - startTime;
perfLogger.info({
msg: 'Processing completed',
duration_ms: totalDuration,
rows_processed: data.length,
avg_ms_per_row: totalDuration / data.length
});
}
}Conditional Debug Logging
Use debug logs for detailed diagnostics:
import { createLogger } from './utils/logger';
export class CacheService {
private logger = createLogger('CacheService');
async get(key: string): Promise<any> {
this.logger.debug({ msg: 'Cache lookup', key });
const value = await this.redis.get(key);
if (value) {
this.logger.debug({ msg: 'Cache hit', key, size_bytes: value.length });
return JSON.parse(value);
} else {
this.logger.debug({ msg: 'Cache miss', key });
return null;
}
}
async set(key: string, value: any, ttl: number) {
const serialized = JSON.stringify(value);
this.logger.debug({
msg: 'Cache write',
key,
ttl_seconds: ttl,
size_bytes: serialized.length
});
await this.redis.setex(key, ttl, serialized);
}
}Log Levels
debug
Detailed diagnostic information, disabled by default in production:
logger.debug({ msg: 'Cache hit', key: 'user:123', ttl: 3600 });
logger.debug({ msg: 'Query executed', sql: 'SELECT * FROM users', rows: 10 });info
Normal operational messages:
logger.info({ msg: 'Server started', port: 3000 });
logger.info({ msg: 'User logged in', user_id: 123 });
logger.info({ msg: 'Report generated', report_id: 456 });warn
Warning messages for non-critical issues:
logger.warn({ msg: 'Slow query detected', duration_ms: 5000 });
logger.warn({ msg: 'API rate limit approaching', usage: 0.95 });
logger.warn({ msg: 'Using deprecated endpoint', path: '/api/old' });error
Error messages requiring attention:
logger.error({
msg: 'Database connection failed',
error: err.message,
stack: err.stack
});
logger.error({
msg: 'Payment processing failed',
order_id: 123,
error: err.message,
amount: 99.99
});Best Practices
1. Always Include a Message
// ✅ Good
logger.info({ msg: 'User created', user_id: 123 });
// ❌ Bad
logger.info({ user_id: 123 }); // No message2. Use Structured Data
// ✅ Good
logger.info({
msg: 'Payment processed',
amount: 99.99,
currency: 'USD',
payment_id: 'pay_123'
});
// ❌ Bad
logger.info({ msg: `Payment processed: $99.99 USD (pay_123)` });3. Include Context Identifiers
Always include IDs that help trace operations:
logger.info({
msg: 'Order updated',
order_id: orderId,
user_id: userId,
status: 'shipped'
});4. Log Errors with Stack Traces
// ✅ Good
logger.error({
msg: 'Operation failed',
error: err.message,
stack: err.stack,
user_id: userId
});
// ❌ Bad
logger.error({ msg: 'Operation failed' }); // No error details5. Track Duration for Operations
const startTime = Date.now();
// ... operation ...
const duration = Date.now() - startTime;
logger.info({
msg: 'Operation completed',
operation: 'generateReport',
duration_ms: duration
});6. Use Child Loggers for Related Operations
const requestLogger = createChildLogger(this.logger, { request_id: requestId });
// All logs include request_id automatically
requestLogger.info({ msg: 'Request received' });
requestLogger.info({ msg: 'Validation passed' });
requestLogger.info({ msg: 'Processing complete' });Environment Configuration
Control logging behavior with environment variables:
# Development - show all logs including debug
LOG_LEVEL=debug
# Production - show info, warn, and error
LOG_LEVEL=info
# Critical issues only
LOG_LEVEL=errorQuerying Logs
Find Logs by Context
{service="b3api"} | json | context="UserService"Find Logs by Custom Field
{service="b3api"} | json | user_id="123"Find Slow Operations
{service="b3api"} | json | duration_ms > 1000Error Analysis
# All errors in the last hour
{service="b3api"} | json | level="error" | line_format "{{.time}} {{.context}} {{.error}}"
# Count errors by type
sum by (error_name) (count_over_time({service="b3api"} | json | level="error" [1h]))Related Documentation
- Logging Overview - System architecture and features
- Global Error Handling - Automatic error catching
- Console Interception - Console redirection details
Summary
The Pino logger provides a powerful, structured logging system for the B3API. By following these patterns and best practices, you can create comprehensive, queryable logs that provide visibility into your application's behavior and simplify debugging and monitoring.