refactor: improve code formatting and readability in provide-filter.ts and filter-menu components fix: delay filter rollback on close in FilterMenuButtonComponent fix: update filter clear button text and method calls in filter-menu.component.html chore: update package-lock.json to remove unnecessary dev flags and add new dependencies Ref: #5125, #5076
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:
- Early filtering - Logs below the configured level are rejected early to avoid unnecessary processing
- Lazy evaluation - Context objects are only processed if the log level passes the threshold
- Cached level comparisons - Log level comparisons use cached numeric indices for faster checks
- Error resilience - Errors in logging sinks are caught and don't affect application behavior
In production environments, consider:
- Setting the log level to
WarnorErrorto 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)
);
});
});
Recent Changes
Logger Factory Enhancements
- Context Functionality: The
loggerfactory function now accepts an optionalctxFnparameter. This allows additional context to be dynamically merged into log messages at runtime. - Enhanced Context Merging: The
mergeContextsfunction has been updated to include injector-level context, component-level context, and message-specific context. This ensures a more comprehensive and flexible logging context.
Example Usage
import { logger } from '@isa/core/logging';
@Component({
selector: 'app-example',
template: '...'
})
export class ExampleComponent {
#logger = logger(() => ({ userId: '12345' }));
ngOnInit(): void {
this.#logger.info('Component initialized');
}
performAction(): void {
this.#logger.debug('Action performed', { action: 'example' });
}
}
Benefits
- Dynamic Context: Add runtime-specific context to log messages without modifying the logger instance.
- Improved Debugging: Enhanced context merging provides richer information for troubleshooting.