# @isa/core/logging

A structured, high-performance logging library for Angular applications with hierarchical context support and flexible sink architecture.

## 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, multiple output destinations, and rich contextual metadata.

## Table of Contents

- [Features](#features)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Configuration](#configuration)
- [Usage Examples](#usage-examples)
- [Creating Custom Sinks](#creating-custom-sinks)
- [Performance Considerations](#performance-considerations)
- [Testing](#testing)
- [Migration Guide](#migration-guide)

## Features

- **Multiple log levels** (trace, debug, info, warn, error, off)
- **Flexible sink architecture** - Multiple output destinations (console, remote server, etc.)
- **Hierarchical context system** - Component, instance, and message-level contexts
- **Performance optimized** - Early filtering and lazy evaluation
- **Type-safe APIs** - Full TypeScript support with comprehensive interfaces
- **Error resilience** - Logging failures don't affect application behavior
- **Angular integration** - Native dependency injection support
- **Factory pattern** - Easy logger creation with `logger()` function
- **Configurable providers** - Simple configuration through Angular providers

## Quick Start

### 1. Install and Configure

Add the logging provider to your application configuration:

```typescript
// app.config.ts
import { ApplicationConfig, isDevMode } from '@angular/core';
import { 
  provideLogging, 
  withLogLevel, 
  withSink, 
  LogLevel, 
  ConsoleLogSink 
} from '@isa/core/logging';

export const appConfig: ApplicationConfig = {
  providers: [
    // Other providers...
    provideLogging(
      withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn),
      withSink(ConsoleLogSink)
    )
  ]
};
```

### 2. Use in Components

```typescript
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');
  }
  
  handleUserAction(): void {
    this.#logger.debug('User action triggered', () => ({ 
      action: 'button-click',
      timestamp: Date.now() 
    }));
  }
}
```

### 3. Error Handling

```typescript
try {
  await this.processData();
} catch (error) {
  this.#logger.error(
    'Data processing failed', 
    error as Error, 
    () => ({ operationId: '12345' })
  );
}
```

## 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

### Hierarchical Context System

The logging system supports a powerful hierarchical context system that merges context from multiple levels:

1. **Global Context** - Set during application configuration
2. **Component Context** - Provided at component/service level
3. **Instance Context** - Provided when creating logger instances
4. **Message Context** - Provided with individual log calls

Contexts are merged in order, with later contexts taking precedence:

```typescript
// 1. Global context (app.config.ts)
provideLogging(
  withContext({ app: 'ISA', version: '1.0.0' })
)

// 2. Component context
@Component({
  providers: [
    provideLoggerContext({ component: 'UserProfile', module: 'CRM' })
  ]
})
export class UserProfileComponent {
  // 3. Instance context
  #logger = logger(() => ({ 
    userId: this.currentUser?.id,
    sessionId: this.sessionService.id 
  }));

  saveProfile(): void {
    // 4. Message context
    this.#logger.info('Saving profile', () => ({ 
      profileId: this.profile.id,
      changedFields: this.getChangedFields() 
    }));
    
    // Final merged context will include all levels:
    // {
    //   app: 'ISA',
    //   version: '1.0.0',
    //   component: 'UserProfile',
    //   module: 'CRM',
    //   userId: '12345',
    //   sessionId: 'sess-abc',
    //   profileId: 'prof-456',
    //   changedFields: ['name', 'email']
    // }
  }
}
```

## API Reference

### Core Interfaces

#### `LoggerApi`

The main interface for logging operations returned by the `logger()` factory:

```typescript
interface LoggerApi {
  trace(message: string, context?: () => LoggerContext): void;
  debug(message: string, context?: () => LoggerContext): void;
  info(message: string, context?: () => LoggerContext): void;
  warn(message: string, context?: () => LoggerContext): void;
  error(message: string, error?: Error, context?: () => LoggerContext): void;
}
```

#### `LoggerContext`

Type definition for context data passed to logging methods:

```typescript
interface LoggerContext {
  [key: string]: unknown;
}
```

#### `Sink`

Interface for creating custom logging destinations:

```typescript
interface Sink {
  log(
    level: LogLevel,
    message: string,
    context?: LoggerContext,
    error?: Error
  ): void;
}
```

### Factory Functions

#### `logger(ctxFn?: () => LoggerContext): LoggerApi`

Creates a logger instance with optional dynamic context:

```typescript
// Basic usage
const basicLogger = logger();

// With dynamic context
const contextLogger = logger(() => ({ 
  userId: getCurrentUserId(),
  sessionId: getSessionId() 
}));
```

**Parameters:**
- `ctxFn` (optional): Function that returns context data to be included with each log message

**Returns:** A `LoggerApi` instance configured with the specified context

### Configuration Functions

#### `provideLogging(...configs: LoggingConfigData[]): EnvironmentProviders`

Main configuration function for setting up the logging system:

```typescript
provideLogging(
  withLogLevel(LogLevel.Debug),
  withSink(ConsoleLogSink),
  withContext({ app: 'MyApp' })
)
```

#### `withLogLevel(level: LogLevel): LoggingConfigData`

Sets the minimum log level for processing:

```typescript
withLogLevel(LogLevel.Debug) // Process debug and above
withLogLevel(LogLevel.Error) // Process only errors
```

#### `withSink(sink: Sink | Type<Sink>): LoggingConfigData`

Adds a logging destination:

```typescript
withSink(ConsoleLogSink)        // Class reference
withSink(new CustomSink())      // Instance
```

#### `withSinkFn(sinkFn: SinkFn): LoggingConfigData`

Adds a sink factory function:

```typescript
withSinkFn(() => {
  const http = inject(HttpClient);
  return (level, message, context) => {
    // Custom sink implementation
  };
})
```

#### `withContext(context: LoggerContext): LoggingConfigData`

Adds global context to all log messages:

```typescript
withContext({ 
  app: 'ISA', 
  version: '1.0.0',
  environment: 'production' 
})
```

#### `provideLoggerContext(context: LoggerContext): Provider[]`

Provides component-level context:

```typescript
@Component({
  providers: [
    provideLoggerContext({ component: 'UserProfile' })
  ]
})
export class UserProfileComponent {
  
  ngOnInit(): void {
    this.#logger.info('Component initialized');
  }
}
```

### Creating Custom Sinks

#### Custom Sink Class

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

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

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

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

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

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

```typescript
// 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

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

```typescript
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)
    );
  });
});
```

## Migration Guide

### Migrating from Previous Versions

#### Context Parameter Changes

**Old API:**
```typescript
// Previous version - context as direct parameter
logger.info('Message', { userId: '123' });
logger.error('Error occurred', error, { operationId: '456' });
```

**New API:**
```typescript
// Current version - context as function
logger.info('Message', () => ({ userId: '123' }));
logger.error('Error occurred', error, () => ({ operationId: '456' }));
```

#### Factory Function Updates

**Old API:**
```typescript
// Previous version - no context support in factory
const logger = logger();
```

**New API:**
```typescript
// Current version - dynamic context support
const logger = logger(() => ({ sessionId: getSessionId() }));
```

#### Configuration Changes

**Old API:**
```typescript
// Previous version - direct service injection
constructor(private loggingService: LoggingService) {}
```

**New API:**
```typescript
// Current version - factory function
#logger = logger();
```

### Breaking Changes

1. **Context parameters** now require function wrappers for lazy evaluation
2. **Error parameter** in LoggerApi is now optional instead of unknown type
3. **Factory function** signature changed to support dynamic context

### Migration Steps

1. **Update context calls** - Wrap context objects in functions
2. **Replace service injection** - Use `logger()` factory instead of injecting `LoggingService`
3. **Update error handling** - Error parameter is now optional in error() method
4. **Add dynamic context** - Leverage new context functionality for better debugging
