Merge branch 'feature/core-logger-lib' into develop

This commit is contained in:
Lorenz Hilpert
2025-04-16 13:20:27 +02:00
18 changed files with 855 additions and 7 deletions

View File

@@ -1,4 +1,8 @@
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import {
HTTP_INTERCEPTORS,
provideHttpClient,
withInterceptorsFromDi,
} from '@angular/common/http';
import {
ErrorHandler,
Injector,
@@ -24,7 +28,10 @@ import { environment } from '../environments/environment';
import { AppSwaggerModule } from './app-swagger.module';
import { AppDomainModule } from './app-domain.module';
import { UiModalModule } from '@ui/modal';
import { NotificationsHubModule, NOTIFICATIONS_HUB_OPTIONS } from '@hub/notifications';
import {
NotificationsHubModule,
NOTIFICATIONS_HUB_OPTIONS,
} from '@hub/notifications';
import { SignalRHubOptions } from '@core/signalr';
import { CoreBreadcrumbModule } from '@core/breadcrumb';
import { UiCommonModule } from '@ui/common';
@@ -36,7 +43,11 @@ import { HttpErrorInterceptor } from './interceptors';
import { CoreLoggerModule, LOG_PROVIDER } from '@core/logger';
import { IsaLogProvider } from './providers';
import { IsaErrorHandler } from './providers/isa.error-handler';
import { ScanAdapterModule, ScanAdapterService, ScanditScanAdapterModule } from '@adapter/scan';
import {
ScanAdapterModule,
ScanAdapterService,
ScanditScanAdapterModule,
} from '@adapter/scan';
import { RootStateService } from './store/root-state.service';
import * as Commands from './commands';
import { PreviewComponent } from './preview';
@@ -45,11 +56,22 @@ import { ShellModule } from '@shared/shell';
import { MainComponent } from './main.component';
import { IconModule } from '@shared/components/icon';
import { NgIconsModule } from '@ng-icons/core';
import { matClose, matWifi, matWifiOff } from '@ng-icons/material-icons/baseline';
import {
matClose,
matWifi,
matWifiOff,
} from '@ng-icons/material-icons/baseline';
import { NetworkStatusService } from './services/network-status.service';
import { firstValueFrom } from 'rxjs';
import { provideMatomo } from 'ngx-matomo-client';
import { withRouter, withRouteData } from 'ngx-matomo-client';
import {
provideLogging,
withLogLevel,
LogLevel,
withSink,
ConsoleLogSink,
} from '@isa/core/logging';
registerLocaleData(localeDe, localeDeExtra);
registerLocaleData(localeDe, 'de', localeDeExtra);
@@ -103,7 +125,13 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
'⚡<br><br><b>Fehler bei der Initialisierung</b><br><br>Bitte prüfen Sie die Netzwerkverbindung (WLAN).<br><br>';
const reload = document.createElement('button');
reload.classList.add('bg-brand', 'text-white', 'p-2', 'rounded', 'cursor-pointer');
reload.classList.add(
'bg-brand',
'text-white',
'p-2',
'rounded',
'cursor-pointer',
);
reload.innerHTML = 'App neu laden';
reload.onclick = () => window.location.reload();
statusElement.appendChild(reload);
@@ -167,7 +195,10 @@ export function _notificationsHubOptionsFactory(
],
providers: [
provideAppInitializer(() => {
const initializerFn = _appInitializerFactory(inject(Config), inject(Injector));
const initializerFn = _appInitializerFactory(
inject(Config),
inject(Injector),
);
return initializerFn();
}),
{
@@ -196,6 +227,7 @@ export function _notificationsHubOptionsFactory(
withRouter(),
withRouteData(),
),
provideLogging(withLogLevel(LogLevel.Debug), withSink(ConsoleLogSink)),
],
})
export class AppModule {}

151
libs/core/logging/README.md Normal file
View File

@@ -0,0 +1,151 @@
# Core Logging
## 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
## Core Concepts
### 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 |
### Create Custom Sink
#### Custom Sink Class
```typescript
class MyCustomSink implements Sink {
#loggerAPI = inject(LoggerApi);
log(
level: LogLevel,
message: string,
context?: unknown,
error?: Error,
): void {
// ... Do Stuff
}
}
```
### Custom Sink Function
```typescript
const myCustomSinkFn: SinkFn = () => {
const loggerAPI = inject(LoggerApi);
return (
level: LogLevel,
message: string,
context?: unknown,
error?: Error,
) => {
// ... Do Stuff
};
};
```
### 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 { provideLogging, withSink, ConsoleLogSink } from '@isa/core/logging';
const isProduction = environment.production;
export const appConfig: ApplicationConfig = {
providers: [
// ...other providers
provideLogging(
withLogLevel(isProduction ? LogLevel.Warn : LogLevel.Debug),
withSink(ConsoleLogSink),
withSink(MyCustomSink),
withSinkFn(myCustomSinkFn),
),
],
};
```
### Context Configuration
Configure the logging service for a specific context
```typescript
@Component({
providers: [provideLoggerContext({ component: 'MyComponent', ... })]
})
export class MyComponent {}
#logger = logger();
```
## Usage
```typescript
import { logger, LogLevel } from '@isa/core/logging';
@Component({
// ...
})
export class MyComponent implements OnInit {
#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,
});
}
}
```

View File

@@ -0,0 +1,34 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../../eslint.config.mjs';
export default [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'core',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'core',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];

View File

@@ -0,0 +1,21 @@
export default {
displayName: 'core-logging',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../../coverage/libs/core/logging',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
],
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
};

View File

@@ -0,0 +1,20 @@
{
"name": "core-logging",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/core/logging/src",
"prefix": "core",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/core/logging/jest.config.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@@ -0,0 +1,19 @@
/**
* Core Logging Library
*
* A centralized logging service for the ISA Frontend application.
*/
// Export core logging functionality
export { LogLevel } from './lib/log-level.enum';
export { LoggerApi, Sink, SinkFn, LoggerContext } from './lib/logging.types';
export { LoggingService } from './lib/logging.service';
export { ConsoleLogSink } from './lib/console-log.sink';
export {
provideLogging,
provideLoggerContext,
withLogLevel,
withSink,
withSinkFn,
} from './lib/logging.providers';
export { logger } from './lib/logger.factory';

View File

@@ -0,0 +1,68 @@
import { Injectable } from '@angular/core';
import { LogLevel } from './log-level.enum';
import { Sink } from './logging.types';
/**
* A sink implementation that outputs logs to the browser console.
*/
@Injectable()
export class ConsoleLogSink implements Sink {
/**
* Logs a message to the browser console with the appropriate log level.
* @param level The log level.
* @param message The log message.
* @param context Optional context data.
* @param error Optional error object.
*/
log(
level: LogLevel,
message: string,
context?: unknown,
error?: Error,
): void {
switch (level) {
case LogLevel.Trace:
if (context) {
console.trace(message, context);
} else {
console.trace(message);
}
break;
case LogLevel.Debug:
if (context) {
console.debug(message, context);
} else {
console.debug(message);
}
break;
case LogLevel.Info:
if (context) {
console.info(message, context);
} else {
console.info(message);
}
break;
case LogLevel.Warn:
if (context) {
console.warn(message, context);
} else {
console.warn(message);
}
break;
case LogLevel.Error:
if (error) {
if (context) {
console.error(message, error, context);
} else {
console.error(message, error);
}
} else if (context) {
console.error(message, context);
} else {
console.error(message);
}
break;
// LogLevel.Off - no logs will be sent to this sink
}
}
}

View File

@@ -0,0 +1,11 @@
/**
* Defines the available log levels for the logging system.
*/
export const enum LogLevel {
Trace = 'trace',
Debug = 'debug',
Info = 'info',
Warn = 'warn',
Error = 'error',
Off = 'off',
}

View File

@@ -0,0 +1,82 @@
import { inject } from '@angular/core';
import { LoggingService } from './logging.service';
import { LoggerApi } from './logging.types';
import { LOGGER_CONTEXT } from './logging.providers';
/**
* Factory function to create a logger instance with optional context.
*
* This is the primary way for components and services to access the logging
* functionality in the application.
*
* @returns Logger API interface that can be used to log messages.
*/
export function logger(): LoggerApi {
const loggingService = inject(LoggingService);
// Try to inject context if available
const context = inject(LOGGER_CONTEXT, { optional: true });
// Return an object with methods that forward to the logging service
// with the provided context
return {
trace: (message: string, additionalContext?: unknown): void => {
loggingService.trace(message, mergeContexts(context, additionalContext));
},
debug: (message: string, additionalContext?: unknown): void => {
loggingService.debug(message, mergeContexts(context, additionalContext));
},
info: (message: string, additionalContext?: unknown): void => {
loggingService.info(message, mergeContexts(context, additionalContext));
},
warn: (message: string, additionalContext?: unknown): void => {
loggingService.warn(message, mergeContexts(context, additionalContext));
},
error: (
message: string,
error?: Error,
additionalContext?: unknown,
): void => {
loggingService.error(
message,
error,
mergeContexts(context, additionalContext),
);
},
};
}
/**
* Merges component-level context with message-specific context.
* @param baseContext The component-level context.
* @param additionalContext The message-specific context.
* @returns The merged context.
*/
function mergeContexts(
baseContext?: unknown,
additionalContext?: unknown,
): unknown {
if (!baseContext) {
return additionalContext;
}
if (!additionalContext) {
return baseContext;
}
// If both contexts are objects, merge them
if (
typeof baseContext === 'object' &&
baseContext !== null &&
typeof additionalContext === 'object' &&
additionalContext !== null
) {
return { ...baseContext, ...additionalContext };
}
// If not both objects, return them separately
return {
baseContext,
additionalContext,
};
}

View File

@@ -0,0 +1,118 @@
import {
APP_INITIALIZER,
EnvironmentProviders,
InjectionToken,
Provider,
Type,
makeEnvironmentProviders,
} from '@angular/core';
import { LogLevel } from './log-level.enum';
import { LoggingService } from './logging.service';
import { LoggerContext, LoggingConfig, Sink, SinkFn } from './logging.types';
// Injection tokens for the Logger API
export const LOGGER_CONFIG = new InjectionToken<LoggingConfig>('LOGGER_CONFIG');
export const LOGGER_CONTEXT = new InjectionToken<LoggerContext>(
'LOGGER_CONTEXT',
);
/**
* Configuration data for the logging system
*/
interface LoggingConfigData {
level?: LogLevel;
sinks?: (Sink | SinkFn | Type<Sink>)[];
context?: LoggerContext;
}
/**
* Configures the LoggingService during app initialization
*/
export function configureLogger(
loggingService: LoggingService,
config: LoggingConfig,
): () => void {
return () => {
loggingService.configure(config);
};
}
/**
* Helper function to set the log level
* @param level The log level to set
*/
export function withLogLevel(level: LogLevel): LoggingConfigData {
return { level };
}
/**
* Helper function to add a sink to the configuration
* @param sink The sink to add (instance or class reference)
*/
export function withSink(sink: Sink | Type<Sink>): LoggingConfigData {
return { sinks: [sink] };
}
/**
* Helper function to add a sink function to the configuration
* @param sinkFn The sink function to add
*/
export function withSinkFn(sinkFn: SinkFn): LoggingConfigData {
return { sinks: [sinkFn] };
}
/**
* Provides logging functionality with the given configuration
*/
export function provideLogging(
...configs: LoggingConfigData[]
): EnvironmentProviders {
// Merge all configurations
const mergedConfig: LoggingConfig = {
level: LogLevel.Info, // Default level
sinks: [],
};
for (const config of configs) {
if (config.level !== undefined) {
mergedConfig.level = config.level;
}
if (config.sinks) {
mergedConfig.sinks.push(...config.sinks);
}
if (config.context) {
mergedConfig.context = {
...mergedConfig.context,
...config.context,
};
}
}
return makeEnvironmentProviders([
{
provide: LOGGER_CONFIG,
useValue: mergedConfig,
},
{
provide: APP_INITIALIZER,
useFactory: configureLogger,
deps: [LoggingService, LOGGER_CONFIG],
multi: true,
},
]);
}
/**
* Provides a context object for logging within a specific component or module
* @param context The context object to provide
*/
export function provideLoggerContext(context: LoggerContext): Provider[] {
return [
{
provide: LOGGER_CONTEXT,
useValue: context,
},
];
}

View File

@@ -0,0 +1,173 @@
import { Injectable, inject } from '@angular/core';
import { LogLevel } from './log-level.enum';
import { LoggerApi, LoggingConfig, Sink, SinkFn } from './logging.types';
/**
* The main service for logging functionality.
* Implements the LoggerApi interface to provide logging methods.
*/
@Injectable({ providedIn: 'root' })
export class LoggingService implements LoggerApi {
private level: LogLevel;
private sinks: ((
level: LogLevel,
message: string,
context?: unknown,
error?: Error,
) => void)[] = [];
private globalContext?: Record<string, unknown>;
constructor() {
this.level = LogLevel.Info; // Default level
}
/**
* Configures the logging service with the provided options.
* @param config The logging configuration options.
*/
configure(config: LoggingConfig): void {
this.level = config.level;
this.globalContext = config.context;
this.sinks = [];
// Initialize all sinks
for (const sink of config.sinks) {
if (typeof sink === 'function') {
// 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)();
this.sinks.push(instance.log.bind(instance));
} else {
// It's a SinkFn - explicitly cast to SinkFn
this.sinks.push((sink as SinkFn)());
}
} else {
// It's a Sink object
this.sinks.push(sink.log.bind(sink));
}
}
}
/**
* Logs trace information.
* @param message The log message.
* @param context Optional context data.
*/
trace(message: string, context?: unknown): void {
this.log(LogLevel.Trace, message, context);
}
/**
* Logs debug information.
* @param message The log message.
* @param context Optional context data.
*/
debug(message: string, context?: unknown): void {
this.log(LogLevel.Debug, message, context);
}
/**
* Logs informational messages.
* @param message The log message.
* @param context Optional context data.
*/
info(message: string, context?: unknown): void {
this.log(LogLevel.Info, message, context);
}
/**
* Logs warning messages.
* @param message The log message.
* @param context Optional context data.
*/
warn(message: string, context?: unknown): void {
this.log(LogLevel.Warn, message, context);
}
/**
* Logs error messages.
* @param message The log message.
* @param error Optional error object.
* @param context Optional context data.
*/
error(message: string, error?: Error, context?: unknown): void {
this.log(LogLevel.Error, message, context, error);
}
/**
* Internal method to handle logging with level filtering.
* @param level The log level.
* @param message The log message.
* @param context Optional context data.
* @param error Optional error object.
*/
private log(
level: LogLevel,
message: string,
context?: unknown,
error?: Error,
): void {
// Check if the current log level should be processed
if (!this.shouldLog(level)) {
return;
}
// Merge global context with the provided context
const mergedContext = this.mergeContext(context);
// Send to all sinks
for (const sink of this.sinks) {
sink(level, message, mergedContext, error);
}
}
/**
* Determines if a message at the specified level should be logged.
* @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);
// Log the message if its level is higher or equal to the current level
return (
messageLevelIndex >= currentLevelIndex && this.level !== LogLevel.Off
);
}
/**
* Merges the global context with the provided context.
* @param context The context provided in the log call.
* @returns The merged context object.
*/
private mergeContext(context?: unknown): unknown {
if (!this.globalContext) {
return context;
}
if (!context) {
return this.globalContext;
}
// Both contexts exist, merge them
if (typeof context === 'object' && context !== null) {
return { ...this.globalContext, ...context };
}
// Context is not an object, return both separately
return {
globalContext: this.globalContext,
logContext: context,
};
}
}

View File

@@ -0,0 +1,46 @@
import { LogLevel } from './log-level.enum';
import { Type } from '@angular/core';
/**
* Represents a destination where log messages are sent.
*/
export interface Sink {
log(level: LogLevel, message: string, context?: unknown, error?: Error): void;
}
/**
* Function implementation of a Sink.
*/
export type SinkFn = () => (
level: LogLevel,
message: string,
context?: unknown,
error?: Error,
) => void;
/**
* Configuration options for the logging service.
*/
export interface LoggingConfig {
level: LogLevel;
sinks: (Sink | SinkFn | Type<Sink>)[];
context?: Record<string, unknown>;
}
/**
* Represents the logger API for logging operations.
*/
export interface LoggerApi {
trace(message: string, context?: unknown): void;
debug(message: string, context?: unknown): void;
info(message: string, context?: unknown): void;
warn(message: string, context?: unknown): void;
error(message: string, error?: Error, context?: unknown): void;
}
/**
* Logger context for context-aware logging.
*/
export interface LoggerContext {
[key: string]: unknown;
}

View File

@@ -0,0 +1,6 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
setupZoneTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es2022",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json",
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"exclude": [
"src/**/*.spec.ts",
"src/test-setup.ts",
"jest.config.ts",
"src/**/*.test.ts"
],
"include": ["src/**/*.ts"]
}

View File

@@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"target": "es2016",
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View File

@@ -23,7 +23,12 @@ import { FilterService } from '../../core';
templateUrl: './filter-menu-button.component.html',
styleUrls: ['./filter-menu-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [IconButtonComponent, OverlayModule, NgIconComponent, FilterMenuComponent],
imports: [
IconButtonComponent,
OverlayModule,
NgIconComponent,
FilterMenuComponent,
],
providers: [provideIcons({ isaActionFilter })],
})
export class FilterMenuButtonComponent {

View File

@@ -42,6 +42,7 @@
"@isa/catalogue/data-access": ["libs/catalogue/data-access/src/index.ts"],
"@isa/common/result": ["libs/common/result/src/index.ts"],
"@isa/core/config": ["libs/core/config/src/index.ts"],
"@isa/core/logging": ["libs/core/logging/src/index.ts"],
"@isa/core/notifications": ["libs/core/notifications/src/index.ts"],
"@isa/core/process": ["libs/core/process/src/index.ts"],
"@isa/core/scanner": ["libs/core/scanner/src/index.ts"],