Update the logging library to accept context as either direct objects or functions (MaybeLoggerContextFn), providing better ergonomics while maintaining performance optimization through lazy evaluation. Changes: - Add MaybeLoggerContextFn type for flexible context handling - Update logger() factory to accept context as object or function - Update all LoggerApi methods to support both context formats - Enhance README with comprehensive examples and migration guide - Document performance benefits of function-based context - Add backward compatibility notes for v2.1.0 enhancement The new API is fully backward compatible - both direct objects and function wrappers work seamlessly.
20 KiB
@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
- Quick Start
- Core Concepts
- API Reference
- Configuration
- Usage Examples
- Creating Custom Sinks
- Performance Considerations
- Testing
- 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
- Flexible context handling - Pass context as direct objects or functions for lazy evaluation
- Performance optimized - Early filtering and lazy evaluation with minimal overhead
- 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:
// 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
import { Component } from '@angular/core';
import { logger } from '@isa/core/logging';
@Component({
selector: 'app-example',
template: '...'
})
export class ExampleComponent {
// Create logger with static context
#logger = logger({ component: 'ExampleComponent' });
ngOnInit(): void {
this.#logger.info('Component initialized');
}
handleUserAction(): void {
// Context as function (lazy evaluation - recommended for performance)
this.#logger.debug('User action triggered', () => ({
action: 'button-click',
timestamp: Date.now()
}));
// Context as object (simpler syntax)
this.#logger.debug('User action completed', {
action: 'button-click',
result: 'success'
});
}
}
3. Error Handling
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:
- Global Context - Set during application configuration
- Component Context - Provided at component/service level
- Instance Context - Provided when creating logger instances
- Message Context - Provided with individual log calls
Contexts are merged in order, with later contexts taking precedence:
// 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:
interface LoggerApi {
trace(message: string, context?: MaybeLoggerContextFn): void;
debug(message: string, context?: MaybeLoggerContextFn): void;
info(message: string, context?: MaybeLoggerContextFn): void;
warn(message: string, context?: MaybeLoggerContextFn): void;
error(message: string, error?: Error, context?: MaybeLoggerContextFn): void;
}
Note: Context parameters accept either a context object directly or a function that returns a context object (MaybeLoggerContextFn), providing flexibility for both static and dynamic context scenarios.
LoggerContext
Type definition for context data passed to logging methods:
interface LoggerContext {
[key: string]: unknown;
}
// Helper types for flexible context handling
type LoggerContextFn = () => LoggerContext;
type MaybeLoggerContextFn = LoggerContext | LoggerContextFn;
The MaybeLoggerContextFn type allows you to pass context either as a direct object or as a function that returns context. This provides flexibility when you need dynamic context evaluation.
Sink
Interface for creating custom logging destinations:
interface Sink {
log(
level: LogLevel,
message: string,
context?: LoggerContext,
error?: Error
): void;
}
Factory Functions
logger(ctx?: MaybeLoggerContextFn): LoggerApi
Creates a logger instance with optional dynamic or static context:
// Basic usage
const basicLogger = logger();
// With dynamic context (function)
const contextLogger = logger(() => ({
userId: getCurrentUserId(),
sessionId: getSessionId()
}));
// With static context (object)
const staticLogger = logger({
component: 'UserService',
module: 'Authentication'
});
Parameters:
ctx(optional): Context data (object) or 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:
provideLogging(
withLogLevel(LogLevel.Debug),
withSink(ConsoleLogSink),
withContext({ app: 'MyApp' })
)
withLogLevel(level: LogLevel): LoggingConfigData
Sets the minimum log level for processing:
withLogLevel(LogLevel.Debug) // Process debug and above
withLogLevel(LogLevel.Error) // Process only errors
withSink(sink: Sink | Type<Sink>): LoggingConfigData
Adds a logging destination:
withSink(ConsoleLogSink) // Class reference
withSink(new CustomSink()) // Instance
withSinkFn(sinkFn: SinkFn): LoggingConfigData
Adds a sink factory function:
withSinkFn(() => {
const http = inject(HttpClient);
return (level, message, context) => {
// Custom sink implementation
};
})
withContext(context: LoggerContext): LoggingConfigData
Adds global context to all log messages:
withContext({
app: 'ISA',
version: '1.0.0',
environment: 'production'
})
provideLoggerContext(context: LoggerContext): Provider[]
Provides component-level context:
@Component({
providers: [
provideLoggerContext({ component: 'UserProfile' })
]
})
export class UserProfileComponent {
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. You can pass context either as a direct object or as a function:
// Add user information to logs - direct object
this.#logger.info('User action completed', {
userId: user.id,
action: 'profile-update',
duration: performance.now() - startTime
});
// Add user information to logs - function (lazy evaluation)
this.#logger.info('User action completed', () => ({
userId: user.id,
action: 'profile-update',
duration: performance.now() - startTime
}));
// Log errors with context - direct object
try {
// Some operation
} catch (error) {
this.#logger.error('Operation failed', error as Error, {
operationId: '12345',
attemptNumber: 3
});
}
// Log errors with context - function (recommended for performance)
try {
// Some operation
} catch (error) {
this.#logger.error('Operation failed', error as Error, () => ({
operationId: '12345',
attemptNumber: 3,
timestamp: Date.now()
}));
}
Performance Tip: Using functions for context (() => {...}) enables lazy evaluation - the context object is only created if the log level passes the threshold, improving performance when logging is disabled or filtered out.
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)
);
});
});
Migration Guide
Recent API Enhancement (v2.1.0)
The logging library now supports flexible context parameters through the MaybeLoggerContextFn type. You can now pass context either as:
- A direct object (for simple cases)
- A function returning an object (for lazy evaluation and performance optimization)
This enhancement is backward compatible - both approaches work seamlessly:
// Both approaches are valid:
logger.info('Message', { userId: '123' }); // Direct object ✅
logger.info('Message', () => ({ userId: '123' })); // Function ✅
Benefits of using functions:
- Performance: Context is only evaluated if the log level passes the threshold
- Dynamic values: Context can include values that change between creation and logging
- Null safety: Avoid errors from accessing properties on potentially null/undefined objects
Migrating from Previous Versions
Context Parameter Changes
Old API (v1.x):
// Previous version - context as direct parameter
logger.info('Message', { userId: '123' });
logger.error('Error occurred', error, { operationId: '456' });
New API (v2.x):
// Current version - context as function (recommended) or direct object (also supported)
logger.info('Message', () => ({ userId: '123' })); // Recommended
logger.info('Message', { userId: '123' }); // Also works
logger.error('Error occurred', error, () => ({ operationId: '456' })); // Recommended
logger.error('Error occurred', error, { operationId: '456' }); // Also works
Factory Function Updates
Old API (v1.x):
// Previous version - no context support in factory
const logger = logger();
New API (v2.x):
// Current version - dynamic or static context support
const logger = logger(() => ({ sessionId: getSessionId() })); // Dynamic
const logger = logger({ component: 'UserService' }); // Static
Configuration Changes
Old API (v1.x):
// Previous version - direct service injection
constructor(private loggingService: LoggingService) {}
New API (v2.x):
// Current version - factory function
#logger = logger();
Breaking Changes (v1.x → v2.x)
- Context parameters support both direct objects and function wrappers (functions recommended for performance)
- Error parameter in LoggerApi is now optional instead of unknown type
- Factory function signature changed to support dynamic context through
MaybeLoggerContextFn
Migration Steps (v1.x → v2.x)
- Update context calls - Optionally wrap context objects in functions for better performance
- Replace service injection - Use
logger()factory instead of injectingLoggingService - Update error handling - Error parameter is now optional in error() method
- Add dynamic context - Leverage new context functionality for better debugging
Migration Steps (v2.0 → v2.1)
No breaking changes - the new MaybeLoggerContextFn type is backward compatible. Optionally update your code to use direct objects where lazy evaluation isn't needed.