mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
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:
@@ -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)
|
||||
);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user