Skip to content

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 timestamp
  • pid: Process ID
  • hostname: Server hostname
  • service: Service name (b3api)
  • msg: Human-readable message
  • Additional context fields specific to your log

Importing the Logger

The logger module provides three main functions:

typescript
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:

typescript
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:

typescript
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:

json
{
  "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:

typescript
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_id and user_id are 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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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

typescript
// ✅ Good
logger.info({ msg: 'User created', user_id: 123 });

// ❌ Bad
logger.info({ user_id: 123 }); // No message

2. Use Structured Data

typescript
// ✅ 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:

typescript
logger.info({
  msg: 'Order updated',
  order_id: orderId,
  user_id: userId,
  status: 'shipped'
});

4. Log Errors with Stack Traces

typescript
// ✅ Good
logger.error({
  msg: 'Operation failed',
  error: err.message,
  stack: err.stack,
  user_id: userId
});

// ❌ Bad
logger.error({ msg: 'Operation failed' }); // No error details

5. Track Duration for Operations

typescript
const startTime = Date.now();
// ... operation ...
const duration = Date.now() - startTime;

logger.info({
  msg: 'Operation completed',
  operation: 'generateReport',
  duration_ms: duration
});
typescript
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:

bash
# Development - show all logs including debug
LOG_LEVEL=debug

# Production - show info, warn, and error
LOG_LEVEL=info

# Critical issues only
LOG_LEVEL=error

Querying Logs

Find Logs by Context

logql
{service="b3api"} | json | context="UserService"

Find Logs by Custom Field

logql
{service="b3api"} | json | user_id="123"

Find Slow Operations

logql
{service="b3api"} | json | duration_ms > 1000

Error Analysis

logql
# 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]))

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.

Syneo/Barcoding Documentation