Files
ISA-Frontend/libs/core/logging

Core Logging

A structured, high-performance logging library for Angular applications.

Overview

The Core Logging library provides a centralized logging service for the ISA Frontend application. It offers a structured way to log messages, errors, and other information across the application, with support for different log levels and output destinations.

Features

  • Multiple log levels (trace, debug, info, warn, error)
  • Configurable logging targets (console, remote server, etc.)
  • Context-aware logging with metadata support
  • Production/development mode detection
  • Filtering capabilities based on log level or context
  • Highly optimized for performance
  • Type-safe APIs
  • Error resilience - logging failures don't affect application behavior

Core Concepts

Log Levels

The library supports the following log levels, ordered by increasing severity:

  • Trace - Fine-grained debugging information
  • Debug - Detailed information useful during development
  • Info - General information about application flow
  • Warn - Potentially harmful situations
  • Error - Error conditions
  • Off - No logging

The configured log level acts as a threshold - only messages at or above that level will be processed.

Log Sinks

Log sinks are destinations where log messages are sent. The library supports multiple sinks operating simultaneously, allowing you to:

  • Display logs in the console during development
  • Send critical errors to a monitoring service
  • Store logs for later analysis

API

LoggingService

The main service for logging functionality.

Methods

Method Description
trace(message: string, context?: unknown) Logs trace information
debug(message: string, context?: unknown) Logs debug information
info(message: string, context?: unknown) Logs informational messages
warn(message: string, context?: unknown) Logs warning messages
error(message: string, error?: Error, context?: unknown) Logs error messages

Logger Factory

The recommended way to use the logging system is through the logger() factory function:

export class MyComponent {
  #logger = logger();
  
  ngOnInit(): void {
    this.#logger.info('Component initialized');
  }
}

Creating Custom Sinks

Custom Sink Class

import { Injectable } from '@angular/core';
import { LogLevel, Sink } from '@isa/core/logging';

@Injectable()
export class MyCustomSink implements Sink {
  log(
    level: LogLevel,
    message: string,
    context?: unknown,
    error?: Error,
  ): void {
    // Your custom logging implementation
    if (level === LogLevel.Error) {
      // Send errors to a monitoring service
      this.sendToMonitoringService(message, error, context);
    }
  }
  
  private sendToMonitoringService(message: string, error?: Error, context?: unknown): void {
    // Implementation details
  }
}

Custom Sink Function

You can also create a sink using a factory function, which gives you access to dependency injection:

import { inject } from '@angular/core';
import { LogLevel, SinkFn } from '@isa/core/logging';
import { HttpClient } from '@angular/common/http';

export const remoteLoggingSink: SinkFn = () => {
  // Inject dependencies
  const http = inject(HttpClient);
  const config = inject(ConfigService);
  
  // Return the actual sink function
  return (level: LogLevel, message: string, context?: unknown, error?: Error) => {
    if (level >= LogLevel.Error) {
      http.post(config.loggingEndpoint, {
        level,
        message,
        context,
        error: error ? {
          name: error.name,
          message: error.message,
          stack: error.stack
        } : undefined,
        timestamp: new Date().toISOString()
      }).subscribe();
    }
  };
};

LogLevel Enum

export const enum LogLevel {
  Trace = 'trace',
  Debug = 'debug',
  Info = 'info',
  Warn = 'warn',
  Error = 'error',
  Off = 'off',
}

Configuration

Global Configuration

Configure the logging service globally during application initialization:

// In your app.config.ts or similar initialization file
import { ApplicationConfig, isDevMode } from '@angular/core';
import { provideLogging, withLogLevel, withSink, withSinkFn, LogLevel, ConsoleLogSink } from '@isa/core/logging';
import { remoteLoggingSink } from './path/to/remote-logging.sink';

export const appConfig: ApplicationConfig = {
  providers: [
    // Other providers...
    
    // Configure logging
    provideLogging(
      // Set appropriate level based on environment
      withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn),
      
      // Console sink for development
      withSink(ConsoleLogSink),
      
      // Remote logging sink for production monitoring
      withSinkFn(remoteLoggingSink),
      
      // Add global context to all log messages
      withContext({
        version: '1.0.0',
        environment: isDevMode() ? 'development' : 'production',
      })
    ),
  ],
};

Component-Level Context

Configure context for a specific component:

import { Component } from '@angular/core';
import { logger, provideLoggerContext } from '@isa/core/logging';

@Component({
  selector: 'app-user-profile',
  templateUrl: './user-profile.component.html',
  providers: [
    // This context will be included in all log messages from this component
    provideLoggerContext({ component: 'UserProfile' })
  ]
})
export class UserProfileComponent {
  #logger = logger();
  
  loadUser(userId: string): void {
    this.#logger.debug('Loading user profile', { userId });
    // ...implementation
  }
}

Usage Examples

Basic Usage

import { Component } from '@angular/core';
import { logger } from '@isa/core/logging';

@Component({
  selector: 'app-example',
  template: '...'
})
export class ExampleComponent {
  #logger = logger();
  
  ngOnInit(): void {
    this.#logger.info('Component initialized');
  }
  
  processData(data: unknown): void {
    this.#logger.debug('Processing data', { dataSize: JSON.stringify(data).length });
    
    try {
      // Process data...
    } catch (error) {
      this.#logger.error(
        'Failed to process data', 
        error as Error, 
        { dataSnapshot: data.slice(0, 100) }
      );
      
      // Handle error...
    }
  }
}

Log Contexts

Contexts allow you to add structured data to your log messages:

// Add user information to logs
this.#logger.info('User action completed', { 
  userId: user.id,
  action: 'profile-update',
  duration: performance.now() - startTime
});

// Log errors with context
try {
  // Some operation
} catch (error) {
  this.#logger.error('Operation failed', error as Error, {
    operationId: '12345',
    attemptNumber: 3
  });
}

Service-Level Logging

import { Injectable } from '@angular/core';
import { logger, LoggerApi } from '@isa/core/logging';
import { HttpClient } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class DataService {
  #logger: LoggerApi = logger();
  
  constructor(private http: HttpClient) {
    this.#logger.debug('DataService initialized');
  }
  
  fetchData(endpoint: string) {
    this.#logger.info('Fetching data', { endpoint });
    
    return this.http.get<unknown>(endpoint).pipe(
      catchError((error) => {
        this.#logger.error('API request failed', error, { endpoint });
        return throwError(() => new Error('Failed to fetch data'));
      })
    );
  }
}

Performance Considerations

The logging system is designed with performance in mind:

  1. Early filtering - Logs below the configured level are rejected early to avoid unnecessary processing
  2. Lazy evaluation - Context objects are only processed if the log level passes the threshold
  3. Cached level comparisons - Log level comparisons use cached numeric indices for faster checks
  4. Error resilience - Errors in logging sinks are caught and don't affect application behavior

In production environments, consider:

  • Setting the log level to Warn or Error to minimize processing overhead
  • Using efficient sinks that batch requests or process logs asynchronously
  • Being mindful of the size and complexity of context objects passed to log methods

Testing

When testing components that use logging, no special handling is typically needed since the logger uses dependency injection and doesn't have side effects that would affect test behavior.

However, if you want to verify logging behavior in tests:

import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { LoggingService } from '@isa/core/logging';

describe('MyService', () => {
  let spectator: SpectatorService<MyService>;
  let loggingService: LoggingService;
  
  const createService = createServiceFactory({
    service: MyService,
    mocks: [LoggingService]
  });
  
  beforeEach(() => {
    spectator = createService();
    loggingService = spectator.inject(LoggingService);
  });
  
  it('should log error when operation fails', () => {
    // Arrange
    const error = new Error('Test error');
    spectator.inject(HttpClient).get.mockReturnValue(throwError(() => error));
    
    // Act
    spectator.service.riskyOperation();
    
    // Assert
    expect(loggingService.error).toHaveBeenCalledWith(
      'Operation failed',
      error,
      expect.any(Object)
    );
  });
});