Enhanced logging library with new features and improved performance.

-  **Feature**: Added global context support for logging configuration
- 🛠️ **Refactor**: Improved error handling in logging service
- 🚀 **Performance**: Optimized log level checks and error resilience
- 📚 **Docs**: Updated README with detailed logging levels and usage examples
This commit is contained in:
Lorenz Hilpert
2025-04-16 14:07:56 +02:00
parent e0edd7887e
commit 2efc5c3b0d
4 changed files with 303 additions and 83 deletions

View File

@@ -1,5 +1,7 @@
# 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.
@@ -11,9 +13,25 @@ The Core Logging library provides a centralized logging service for the ISA Fron
- 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:
@@ -38,36 +56,78 @@ The main service for logging functionality.
| `warn(message: string, context?: unknown)` | Logs warning messages |
| `error(message: string, error?: Error, context?: unknown)` | Logs error messages |
### Create Custom Sink
### Logger Factory
The recommended way to use the logging system is through the `logger()` factory function:
```typescript
export class MyComponent {
#logger = logger();
ngOnInit(): void {
this.#logger.info('Component initialized');
}
}
```
### Creating Custom Sinks
#### Custom Sink Class
```typescript
class MyCustomSink implements Sink {
#loggerAPI = inject(LoggerApi);
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 {
// ... Do Stuff
// 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
const myCustomSinkFn: SinkFn = () => {
const loggerAPI = inject(LoggerApi);
return (
level: LogLevel,
message: string,
context?: unknown,
error?: Error,
) => {
// ... Do Stuff
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();
}
};
};
```
@@ -93,59 +153,204 @@ 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';
import { provideLogging, withSink, ConsoleLogSink } from '@isa/core/logging';
const isProduction = environment.production;
export const appConfig: ApplicationConfig = {
providers: [
// ...other providers
// Other providers...
// Configure logging
provideLogging(
withLogLevel(isProduction ? LogLevel.Warn : LogLevel.Debug),
// Set appropriate level based on environment
withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn),
// Console sink for development
withSink(ConsoleLogSink),
withSink(MyCustomSink),
withSinkFn(myCustomSinkFn),
// Remote logging sink for production monitoring
withSinkFn(remoteLoggingSink),
// Add global context to all log messages
withContext({
version: '1.0.0',
environment: isDevMode() ? 'development' : 'production',
})
),
],
};
```
### Context Configuration
### Component-Level Context
Configure the logging service for a specific context
Configure context for a specific component:
```typescript
@Component({
providers: [provideLoggerContext({ component: 'MyComponent', ... })]
})
export class MyComponent {}
#logger = logger();
```
## Usage
```typescript
import { logger, LogLevel } from '@isa/core/logging';
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 MyComponent implements OnInit {
export class UserProfileComponent {
#logger = logger();
ngOnInit() {
this.logger.info('Component initialized', { componentName: 'MyComponent' });
}
processData(data: any) {
try {
// Process data
this.logger.debug('Data processed successfully', { dataId: data.id });
} catch (error) {
this.logger.error('Failed to process data', error as Error, {
dataId: data.id,
});
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)
);
});
});
```

View File

@@ -1,6 +1,6 @@
import { inject } from '@angular/core';
import { LoggingService } from './logging.service';
import { LoggerApi } from './logging.types';
import { LoggerApi, LoggerContext } from './logging.types';
import { LOGGER_CONTEXT } from './logging.providers';
/**
@@ -15,7 +15,9 @@ export function logger(): LoggerApi {
const loggingService = inject(LoggingService);
// Try to inject context if available
const context = inject(LOGGER_CONTEXT, { optional: true });
const context = inject(LOGGER_CONTEXT, {
optional: true,
});
// Return an object with methods that forward to the logging service
// with the provided context
@@ -53,7 +55,7 @@ export function logger(): LoggerApi {
* @returns The merged context.
*/
function mergeContexts(
baseContext?: unknown,
baseContext?: LoggerContext | null,
additionalContext?: unknown,
): unknown {
if (!baseContext) {
@@ -66,10 +68,9 @@ function mergeContexts(
// If both contexts are objects, merge them
if (
typeof baseContext === 'object' &&
baseContext !== null &&
typeof additionalContext === 'object' &&
additionalContext !== null
additionalContext !== null &&
!Array.isArray(additionalContext)
) {
return { ...baseContext, ...additionalContext };
}

View File

@@ -61,6 +61,14 @@ export function withSinkFn(sinkFn: SinkFn): LoggingConfigData {
return { sinks: [sinkFn] };
}
/**
* Helper function to add global context to the logger configuration
* @param context The context object to add to all log messages
*/
export function withContext(context: LoggerContext): LoggingConfigData {
return { context };
}
/**
* Provides logging functionality with the given configuration
*/

View File

@@ -8,18 +8,21 @@ import { LoggerApi, LoggingConfig, Sink, SinkFn } from './logging.types';
*/
@Injectable({ providedIn: 'root' })
export class LoggingService implements LoggerApi {
private level: LogLevel;
private sinks: ((
level: LogLevel,
message: string,
context?: unknown,
error?: Error,
) => void)[] = [];
private level: LogLevel = LogLevel.Info; // Default level
private sinks: Array<
(level: LogLevel, message: string, context?: unknown, error?: Error) => void
> = [];
private globalContext?: Record<string, unknown>;
constructor() {
this.level = LogLevel.Info; // Default level
}
// Cache log level indexes for performance
private readonly LOG_LEVEL_ORDER: Record<LogLevel, number> = {
[LogLevel.Trace]: 0,
[LogLevel.Debug]: 1,
[LogLevel.Info]: 2,
[LogLevel.Warn]: 3,
[LogLevel.Error]: 4,
[LogLevel.Off]: 5,
};
/**
* Configures the logging service with the provided options.
@@ -36,10 +39,10 @@ export class LoggingService implements LoggerApi {
// Check if it's a constructor function (class) or a sink function
if (sink.prototype && sink.prototype.log) {
// It's a constructor function (class), instantiate it
const instance = new (sink as any)();
const instance = new (sink as new () => Sink)();
this.sinks.push(instance.log.bind(instance));
} else {
// It's a SinkFn - explicitly cast to SinkFn
// It's a SinkFn
this.sinks.push((sink as SinkFn)());
}
} else {
@@ -108,8 +111,13 @@ export class LoggingService implements LoggerApi {
context?: unknown,
error?: Error,
): void {
// Check if the current log level should be processed
if (!this.shouldLog(level)) {
// Short-circuit if logging is disabled or level is too low (performance optimization)
if (this.level === LogLevel.Off || !this.shouldLog(level)) {
return;
}
// If there are no sinks configured, don't do any work
if (!this.sinks.length) {
return;
}
@@ -118,31 +126,29 @@ export class LoggingService implements LoggerApi {
// Send to all sinks
for (const sink of this.sinks) {
sink(level, message, mergedContext, error);
try {
sink(level, message, mergedContext, error);
} catch (e) {
// Prevent logger failures from affecting application
console.error('Error in logging sink:', e);
}
}
}
/**
* Determines if a message at the specified level should be logged.
* Uses cached level values for better performance.
*
* @param level The log level to check.
* @returns True if the message should be logged, false otherwise.
*/
private shouldLog(level: LogLevel): boolean {
const levels: LogLevel[] = [
LogLevel.Trace,
LogLevel.Debug,
LogLevel.Info,
LogLevel.Warn,
LogLevel.Error,
];
const currentLevelIndex = levels.indexOf(this.level);
const messageLevelIndex = levels.indexOf(level);
// Using cached indexes for better performance
const currentLevelIndex = this.LOG_LEVEL_ORDER[this.level];
const messageLevelIndex = this.LOG_LEVEL_ORDER[level];
// Log the message if its level is higher or equal to the current level
return (
messageLevelIndex >= currentLevelIndex && this.level !== LogLevel.Off
);
return messageLevelIndex >= currentLevelIndex;
}
/**