diff --git a/.claude/skills/logging-helper/SKILL.md b/.claude/skills/logging-helper/SKILL.md new file mode 100644 index 000000000..39e27d53a --- /dev/null +++ b/.claude/skills/logging-helper/SKILL.md @@ -0,0 +1,272 @@ +--- +name: logging-helper +description: Ensures consistent usage of the @isa/core/logging library across the codebase with best practices for performance and maintainability +--- + +# Logging Helper Skill + +Ensures consistent and efficient logging using `@isa/core/logging` library. + +## When to Use + +- Adding logging to new components/services +- Refactoring existing logging code +- Reviewing code for proper logging patterns +- Debugging logging issues + +## Core Principles + +### 1. Always Use Factory Pattern + +```typescript +import { logger } from '@isa/core/logging'; + +// ✅ DO +#logger = logger(); + +// ❌ DON'T +constructor(private loggingService: LoggingService) {} +``` + +### 2. Choose Appropriate Log Levels + +- **Trace**: Fine-grained debugging (method entry/exit) +- **Debug**: Development debugging (variable states) +- **Info**: Runtime information (user actions, events) +- **Warn**: Potentially harmful situations +- **Error**: Errors affecting functionality + +### 3. Context Patterns + +**Static Context** (component level): +```typescript +#logger = logger({ component: 'UserProfileComponent' }); +``` + +**Dynamic Context** (instance level): +```typescript +#logger = logger(() => ({ + userId: this.authService.currentUserId, + storeId: this.config.storeId +})); +``` + +**Message Context** (use functions for performance): +```typescript +// ✅ Recommended - lazy evaluation +this.#logger.info('Order processed', () => ({ + orderId: order.id, + total: order.total, + timestamp: Date.now() +})); + +// ✅ Acceptable - static values +this.#logger.info('Order processed', { + orderId: order.id, + status: 'completed' +}); +``` + +## Essential Patterns + +### Component Logging +```typescript +@Component({ + selector: 'app-product-list', + standalone: true, +}) +export class ProductListComponent { + #logger = logger({ component: 'ProductListComponent' }); + + ngOnInit(): void { + this.#logger.info('Component initialized'); + } + + onAction(id: string): void { + this.#logger.debug('Action triggered', { id }); + } +} +``` + +### Service Logging +```typescript +@Injectable({ providedIn: 'root' }) +export class DataService { + #logger = logger({ service: 'DataService' }); + + fetchData(endpoint: string): Observable { + this.#logger.debug('Fetching data', { endpoint }); + + return this.http.get(endpoint).pipe( + tap((data) => this.#logger.info('Data fetched', () => ({ + endpoint, + size: data.length + }))), + catchError((error) => { + this.#logger.error('Fetch failed', error, () => ({ + endpoint, + status: error.status + })); + return throwError(() => error); + }) + ); + } +} +``` + +### Error Handling +```typescript +try { + await this.processOrder(orderId); +} catch (error) { + this.#logger.error('Order processing failed', error as Error, () => ({ + orderId, + step: this.currentStep, + attemptNumber: this.retryCount + })); + throw error; +} +``` + +### Hierarchical Context +```typescript +@Component({ + providers: [ + provideLoggerContext({ feature: 'checkout', module: 'sales' }) + ] +}) +export class CheckoutComponent { + #logger = logger(() => ({ userId: this.userService.currentUserId })); + + // Logs include: feature, module, userId + message context +} +``` + +## Common Mistakes to Avoid + +```typescript +// ❌ Don't use console.log +console.log('User logged in'); +// ✅ Use logger +this.#logger.info('User logged in'); + +// ❌ Don't create expensive context eagerly +this.#logger.debug('Processing', { + data: this.computeExpensive() // Always executes +}); +// ✅ Use function for lazy evaluation +this.#logger.debug('Processing', () => ({ + data: this.computeExpensive() // Only if debug enabled +})); + +// ❌ Don't log in tight loops +for (const item of items) { + this.#logger.debug('Item', { item }); +} +// ✅ Log aggregates +this.#logger.debug('Batch processed', () => ({ + count: items.length +})); + +// ❌ Don't log sensitive data +this.#logger.info('User auth', { password: user.password }); +// ✅ Log safe identifiers only +this.#logger.info('User auth', { userId: user.id }); + +// ❌ Don't miss error object +this.#logger.error('Failed'); +// ✅ Include error object +this.#logger.error('Failed', error as Error); +``` + +## Configuration + +### App Configuration +```typescript +// app.config.ts +import { ApplicationConfig, isDevMode } from '@angular/core'; +import { + provideLogging, withLogLevel, withSink, + LogLevel, ConsoleLogSink +} from '@isa/core/logging'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideLogging( + withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn), + withSink(ConsoleLogSink), + withContext({ app: 'ISA', version: '1.0.0' }) + ) + ] +}; +``` + +## Testing + +```typescript +import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { LoggingService } from '@isa/core/logging'; + +describe('MyComponent', () => { + const createComponent = createComponentFactory({ + component: MyComponent, + mocks: [LoggingService] + }); + + it('should log error', () => { + const spectator = createComponent(); + const loggingService = spectator.inject(LoggingService); + + spectator.component.riskyOperation(); + + expect(loggingService.error).toHaveBeenCalledWith( + expect.any(String), + expect.any(Error), + expect.any(Function) + ); + }); +}); +``` + +## Code Review Checklist + +- [ ] Uses `logger()` factory, not `LoggingService` injection +- [ ] Appropriate log level for each message +- [ ] Context functions for expensive operations +- [ ] No sensitive information (passwords, tokens, PII) +- [ ] No `console.log` statements +- [ ] Error logs include error object +- [ ] No logging in tight loops +- [ ] Component/service identified in context +- [ ] E2E attributes on interactive elements + +## Quick Reference + +```typescript +// Import +import { logger, provideLoggerContext } from '@isa/core/logging'; + +// Create logger +#logger = logger(); // Basic +#logger = logger({ component: 'Name' }); // Static context +#logger = logger(() => ({ id: this.id })); // Dynamic context + +// Log messages +this.#logger.trace('Detailed trace'); +this.#logger.debug('Debug info'); +this.#logger.info('General info', () => ({ key: value })); +this.#logger.warn('Warning'); +this.#logger.error('Error', error, () => ({ context })); + +// Component context +@Component({ + providers: [provideLoggerContext({ feature: 'users' })] +}) +``` + +## Additional Resources + +- Full documentation: `libs/core/logging/README.md` +- Examples: `.claude/skills/logging-helper/examples.md` +- Quick reference: `.claude/skills/logging-helper/reference.md` +- Troubleshooting: `.claude/skills/logging-helper/troubleshooting.md` diff --git a/.claude/skills/logging-helper/examples.md b/.claude/skills/logging-helper/examples.md new file mode 100644 index 000000000..90e6d114c --- /dev/null +++ b/.claude/skills/logging-helper/examples.md @@ -0,0 +1,350 @@ +# Logging Examples + +Concise real-world examples of logging patterns. + +## 1. Component with Observable + +```typescript +import { Component, OnInit } from '@angular/core'; +import { logger } from '@isa/core/logging'; + +@Component({ + selector: 'app-product-list', + standalone: true, +}) +export class ProductListComponent implements OnInit { + #logger = logger({ component: 'ProductListComponent' }); + + constructor(private productService: ProductService) {} + + ngOnInit(): void { + this.#logger.info('Component initialized'); + this.loadProducts(); + } + + private loadProducts(): void { + this.productService.getProducts().subscribe({ + next: (products) => { + this.#logger.info('Products loaded', () => ({ count: products.length })); + }, + error: (error) => { + this.#logger.error('Failed to load products', error); + } + }); + } +} +``` + +## 2. Service with HTTP + +```typescript +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { logger } from '@isa/core/logging'; +import { catchError, tap } from 'rxjs/operators'; + +@Injectable({ providedIn: 'root' }) +export class OrderService { + private http = inject(HttpClient); + #logger = logger({ service: 'OrderService' }); + + getOrder(id: string): Observable { + this.#logger.debug('Fetching order', { id }); + + return this.http.get(`/api/orders/${id}`).pipe( + tap((order) => this.#logger.info('Order fetched', () => ({ + id, + status: order.status + }))), + catchError((error) => { + this.#logger.error('Fetch failed', error, () => ({ id, status: error.status })); + throw error; + }) + ); + } +} +``` + +## 3. Hierarchical Context + +```typescript +import { Component } from '@angular/core'; +import { logger, provideLoggerContext } from '@isa/core/logging'; + +@Component({ + selector: 'oms-return-process', + standalone: true, + providers: [ + provideLoggerContext({ feature: 'returns', module: 'oms' }) + ], +}) +export class ReturnProcessComponent { + #logger = logger(() => ({ + processId: this.currentProcessId, + step: this.currentStep + })); + + private currentProcessId = crypto.randomUUID(); + private currentStep = 1; + + startProcess(orderId: string): void { + // Logs include: feature, module, processId, step, orderId + this.#logger.info('Process started', { orderId }); + } +} +``` + +## 4. NgRx Effect + +```typescript +import { Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { logger } from '@isa/core/logging'; +import { map, catchError, tap } from 'rxjs/operators'; +import { of } from 'rxjs'; + +@Injectable() +export class OrdersEffects { + #logger = logger({ effect: 'OrdersEffects' }); + + loadOrders$ = createEffect(() => + this.actions$.pipe( + ofType(OrdersActions.loadOrders), + tap((action) => this.#logger.debug('Loading orders', () => ({ + page: action.page + }))), + mergeMap((action) => + this.orderService.getOrders(action.filters).pipe( + map((orders) => { + this.#logger.info('Orders loaded', () => ({ count: orders.length })); + return OrdersActions.loadOrdersSuccess({ orders }); + }), + catchError((error) => { + this.#logger.error('Load failed', error); + return of(OrdersActions.loadOrdersFailure({ error })); + }) + ) + ) + ) + ); + + constructor( + private actions$: Actions, + private orderService: OrderService + ) {} +} +``` + +## 5. Guard with Authorization + +```typescript +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { logger } from '@isa/core/logging'; + +export const authGuard: CanActivateFn = (route, state) => { + const authService = inject(AuthService); + const router = inject(Router); + const log = logger({ guard: 'AuthGuard' }); + + if (authService.isAuthenticated()) { + log.debug('Access granted', () => ({ route: state.url })); + return true; + } + + log.warn('Access denied', () => ({ + attemptedRoute: state.url, + redirectTo: '/login' + })); + return router.createUrlTree(['/login']); +}; +``` + +## 6. HTTP Interceptor + +```typescript +import { HttpInterceptorFn } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { tap, catchError } from 'rxjs/operators'; +import { LoggingService } from '@isa/core/logging'; + +export const loggingInterceptor: HttpInterceptorFn = (req, next) => { + const loggingService = inject(LoggingService); + const startTime = performance.now(); + + loggingService.debug('HTTP Request', () => ({ + method: req.method, + url: req.url + })); + + return next(req).pipe( + tap((event) => { + if (event.type === HttpEventType.Response) { + loggingService.info('HTTP Response', () => ({ + method: req.method, + url: req.url, + status: event.status, + duration: `${(performance.now() - startTime).toFixed(2)}ms` + })); + } + }), + catchError((error) => { + loggingService.error('HTTP Error', error, () => ({ + method: req.method, + url: req.url, + status: error.status + })); + return throwError(() => error); + }) + ); +}; +``` + +## 7. Form Validation + +```typescript +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { logger } from '@isa/core/logging'; + +@Component({ + selector: 'shared-user-form', + standalone: true, +}) +export class UserFormComponent implements OnInit { + #logger = logger({ component: 'UserFormComponent' }); + form!: FormGroup; + + constructor(private fb: FormBuilder) {} + + ngOnInit(): void { + this.form = this.fb.group({ + name: ['', Validators.required], + email: ['', [Validators.required, Validators.email]] + }); + } + + onSubmit(): void { + if (this.form.invalid) { + this.#logger.warn('Invalid form submission', () => ({ + errors: this.getFormErrors() + })); + return; + } + + this.#logger.info('Form submitted'); + } + + private getFormErrors(): Record { + const errors: Record = {}; + Object.keys(this.form.controls).forEach((key) => { + const control = this.form.get(key); + if (control?.errors) errors[key] = control.errors; + }); + return errors; + } +} +``` + +## 8. Async Progress Tracking + +```typescript +import { Injectable } from '@angular/core'; +import { logger } from '@isa/core/logging'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +@Injectable({ providedIn: 'root' }) +export class ImportService { + #logger = logger({ service: 'ImportService' }); + + importData(file: File): Observable { + const importId = crypto.randomUUID(); + + this.#logger.info('Import started', () => ({ + importId, + fileName: file.name, + fileSize: file.size + })); + + return this.processImport(file).pipe( + tap((progress) => { + if (progress % 25 === 0) { + this.#logger.debug('Import progress', () => ({ + importId, + progress: `${progress}%` + })); + } + }), + tap({ + complete: () => this.#logger.info('Import completed', { importId }), + error: (error) => this.#logger.error('Import failed', error, { importId }) + }) + ); + } + + private processImport(file: File): Observable { + // Implementation + } +} +``` + +## 9. Global Error Handler + +```typescript +import { Injectable, ErrorHandler } from '@angular/core'; +import { logger } from '@isa/core/logging'; + +@Injectable() +export class GlobalErrorHandler implements ErrorHandler { + #logger = logger({ handler: 'GlobalErrorHandler' }); + + handleError(error: Error): void { + this.#logger.error('Uncaught error', error, () => ({ + url: window.location.href, + userAgent: navigator.userAgent, + timestamp: new Date().toISOString() + })); + } +} +``` + +## 10. WebSocket Component + +```typescript +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { logger } from '@isa/core/logging'; +import { Subject, takeUntil } from 'rxjs'; + +@Component({ + selector: 'oms-live-orders', + standalone: true, +}) +export class LiveOrdersComponent implements OnInit, OnDestroy { + #logger = logger({ component: 'LiveOrdersComponent' }); + private destroy$ = new Subject(); + + constructor(private wsService: WebSocketService) {} + + ngOnInit(): void { + this.#logger.info('Connecting to WebSocket'); + + this.wsService.connect('orders').pipe( + takeUntil(this.destroy$) + ).subscribe({ + next: (msg) => this.#logger.debug('Message received', () => ({ + type: msg.type, + orderId: msg.orderId + })), + error: (error) => this.#logger.error('WebSocket error', error), + complete: () => this.#logger.info('WebSocket closed') + }); + } + + ngOnDestroy(): void { + this.#logger.debug('Component destroyed'); + this.destroy$.next(); + this.destroy$.complete(); + } +} +``` diff --git a/.claude/skills/logging-helper/reference.md b/.claude/skills/logging-helper/reference.md new file mode 100644 index 000000000..54b138865 --- /dev/null +++ b/.claude/skills/logging-helper/reference.md @@ -0,0 +1,192 @@ +# Logging Quick Reference + +## API Signatures + +```typescript +// Factory +function logger(ctx?: MaybeLoggerContextFn): LoggerApi + +// Logger API +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; +} + +// Types +type MaybeLoggerContextFn = LoggerContext | (() => LoggerContext); +interface LoggerContext { [key: string]: unknown; } +``` + +## Common Patterns + +| Pattern | Code | +|---------|------| +| Basic logger | `#logger = logger()` | +| Static context | `#logger = logger({ component: 'Name' })` | +| Dynamic context | `#logger = logger(() => ({ id: this.id }))` | +| Log info | `this.#logger.info('Message')` | +| Log with context | `this.#logger.info('Message', () => ({ key: value }))` | +| Log error | `this.#logger.error('Error', error)` | +| Error with context | `this.#logger.error('Error', error, () => ({ id }))` | +| Component context | `providers: [provideLoggerContext({ feature: 'x' })]` | + +## Configuration + +```typescript +// app.config.ts +import { provideLogging, withLogLevel, withSink, withContext, + LogLevel, ConsoleLogSink } from '@isa/core/logging'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideLogging( + withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn), + withSink(ConsoleLogSink), + withContext({ app: 'ISA', version: '1.0.0' }) + ) + ] +}; +``` + +## Log Levels + +| Level | Use Case | Example | +|-------|----------|---------| +| `Trace` | Method entry/exit | `this.#logger.trace('Entering processData')` | +| `Debug` | Development info | `this.#logger.debug('Variable state', () => ({ x }))` | +| `Info` | Runtime events | `this.#logger.info('User logged in', { userId })` | +| `Warn` | Warnings | `this.#logger.warn('Deprecated API used')` | +| `Error` | Errors | `this.#logger.error('Operation failed', error)` | +| `Off` | Disable logging | `withLogLevel(LogLevel.Off)` | + +## Decision Trees + +### Context Type Decision +``` +Value changes at runtime? +├─ Yes → () => ({ value: this.getValue() }) +└─ No → { value: 'static' } + +Computing value is expensive? +├─ Yes → () => ({ data: this.compute() }) +└─ No → Either works +``` + +### Log Level Decision +``` +Method flow details? → Trace +Development debug? → Debug +Runtime information? → Info +Potential problem? → Warn +Error occurred? → Error +``` + +## Performance Tips + +```typescript +// ✅ DO: Lazy evaluation +this.#logger.debug('Data', () => ({ + result: this.expensive() // Only runs if debug enabled +})); + +// ❌ DON'T: Eager evaluation +this.#logger.debug('Data', { + result: this.expensive() // Always runs +}); + +// ✅ DO: Log aggregates +this.#logger.info('Batch done', () => ({ count: items.length })); + +// ❌ DON'T: Log in loops +for (const item of items) { + this.#logger.debug('Item', { item }); // Performance hit +} +``` + +## Testing + +```typescript +import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { LoggingService } from '@isa/core/logging'; + +describe('MyComponent', () => { + const createComponent = createComponentFactory({ + component: MyComponent, + mocks: [LoggingService] + }); + + it('logs error', () => { + const spectator = createComponent(); + const logger = spectator.inject(LoggingService); + + spectator.component.operation(); + + expect(logger.error).toHaveBeenCalled(); + }); +}); +``` + +## Custom Sink + +```typescript +import { Injectable } from '@angular/core'; +import { Sink, LogLevel, LoggerContext } from '@isa/core/logging'; + +@Injectable() +export class CustomSink implements Sink { + log(level: LogLevel, message: string, context?: LoggerContext, error?: Error): void { + // Implementation + } +} + +// Register +provideLogging(withSink(CustomSink)) +``` + +## Sink Function (with DI) + +```typescript +import { inject } from '@angular/core'; +import { SinkFn, LogLevel } from '@isa/core/logging'; + +export const remoteSink: SinkFn = () => { + const http = inject(HttpClient); + + return (level, message, context, error) => { + if (level === LogLevel.Error) { + http.post('/api/logs', { level, message, context, error }).subscribe(); + } + }; +}; + +// Register +provideLogging(withSinkFn(remoteSink)) +``` + +## Common Imports + +```typescript +// Main imports +import { logger, provideLoggerContext } from '@isa/core/logging'; + +// Configuration imports +import { + provideLogging, + withLogLevel, + withSink, + withContext, + LogLevel, + ConsoleLogSink +} from '@isa/core/logging'; + +// Type imports +import { + LoggerApi, + Sink, + SinkFn, + LoggerContext +} from '@isa/core/logging'; +``` diff --git a/.claude/skills/logging-helper/troubleshooting.md b/.claude/skills/logging-helper/troubleshooting.md new file mode 100644 index 000000000..1df623b13 --- /dev/null +++ b/.claude/skills/logging-helper/troubleshooting.md @@ -0,0 +1,235 @@ +# Logging Troubleshooting + +## 1. Logs Not Appearing + +**Problem:** Logger called but nothing in console. + +**Solutions:** +```typescript +// Check log level +provideLogging( + withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn) +) + +// Add sink +provideLogging( + withLogLevel(LogLevel.Debug), + withSink(ConsoleLogSink) // Required! +) + +// Verify configuration in app.config.ts +export const appConfig: ApplicationConfig = { + providers: [ + provideLogging(...) // Must be present + ] +}; +``` + +## 2. NullInjectorError + +**Error:** `NullInjectorError: No provider for LoggingService!` + +**Solution:** +```typescript +// app.config.ts +import { provideLogging, withLogLevel, withSink, + LogLevel, ConsoleLogSink } from '@isa/core/logging'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideLogging( + withLogLevel(LogLevel.Debug), + withSink(ConsoleLogSink) + ) + ] +}; +``` + +## 3. Context Not Showing + +**Problem:** Context passed but doesn't appear. + +**Check:** +```typescript +// ✅ Both work: +this.#logger.info('Message', () => ({ id: '123' })); // Function +this.#logger.info('Message', { id: '123' }); // Object + +// ❌ Common mistake: +const ctx = { id: '123' }; +this.#logger.info('Message', ctx); // Actually works! + +// Verify hierarchical merge: +// Global → Component → Instance → Message +``` + +## 4. Performance Issues + +**Problem:** Slow when debug logging enabled. + +**Solutions:** +```typescript +// ✅ Use lazy evaluation +this.#logger.debug('Data', () => ({ + expensive: this.compute() // Only if debug enabled +})); + +// ✅ Reduce log frequency +this.#logger.debug('Batch', () => ({ + count: items.length // Not each item +})); + +// ✅ Increase production level +provideLogging( + withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn) +) +``` + +## 5. Error Object Not Logged + +**Problem:** Error shows as `[object Object]`. + +**Solution:** +```typescript +// ❌ Wrong +this.#logger.error('Failed', { error }); // Don't wrap in object + +// ✅ Correct +this.#logger.error('Failed', error as Error, () => ({ + additionalContext: 'value' +})); +``` + +## 6. TypeScript Errors + +**Error:** `Type 'X' is not assignable to 'MaybeLoggerContextFn'` + +**Solution:** +```typescript +// ❌ Wrong type +this.#logger.info('Message', 'string'); // Invalid + +// ✅ Correct types +this.#logger.info('Message', { key: 'value' }); +this.#logger.info('Message', () => ({ key: 'value' })); +``` + +## 7. Logs in Tests + +**Problem:** Test output cluttered with logs. + +**Solutions:** +```typescript +// Mock logging service +import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; +import { LoggingService } from '@isa/core/logging'; + +const createComponent = createComponentFactory({ + component: MyComponent, + mocks: [LoggingService] // Mocks all log methods +}); + +// Or disable in tests +TestBed.configureTestingModule({ + providers: [ + provideLogging(withLogLevel(LogLevel.Off)) + ] +}); +``` + +## 8. Undefined Property Error + +**Error:** `Cannot read property 'X' of undefined` + +**Problem:** Accessing uninitialized property in logger context. + +**Solutions:** +```typescript +// ❌ Problem +#logger = logger(() => ({ + userId: this.userService.currentUserId // May be undefined +})); + +// ✅ Solution 1: Optional chaining +#logger = logger(() => ({ + userId: this.userService?.currentUserId ?? 'unknown' +})); + +// ✅ Solution 2: Delay access +ngOnInit() { + this.#logger.info('Init', () => ({ + userId: this.userService.currentUserId // Safe here + })); +} +``` + +## 9. Circular Dependency + +**Error:** `NG0200: Circular dependency in DI detected` + +**Cause:** Service A ← → Service B both inject LoggingService. + +**Solution:** +```typescript +// ❌ Creates circular dependency +constructor(private loggingService: LoggingService) {} + +// ✅ Use factory (no circular dependency) +#logger = logger({ service: 'MyService' }); +``` + +## 10. Custom Sink Not Working + +**Problem:** Sink registered but never called. + +**Solutions:** +```typescript +// ✅ Correct registration +provideLogging( + withSink(MySink) // Add to config +) + +// ✅ Correct signature +export class MySink implements Sink { + log( + level: LogLevel, + message: string, + context?: LoggerContext, + error?: Error + ): void { + // Implementation + } +} + +// ✅ Sink function must return function +export const mySinkFn: SinkFn = () => { + const http = inject(HttpClient); + return (level, message, context, error) => { + // Implementation + }; +}; +``` + +## Quick Diagnostics + +```typescript +// Enable all logs temporarily +provideLogging(withLogLevel(LogLevel.Trace)) + +// Check imports +import { logger } from '@isa/core/logging'; // ✅ Correct +import { logger } from '@isa/core/logging/src/lib/logger.factory'; // ❌ Wrong + +// Verify console filters in browser DevTools +// Ensure Info, Debug, Warnings are enabled +``` + +## Common Error Messages + +| Error | Cause | Fix | +|-------|-------|-----| +| `NullInjectorError: LoggingService` | Missing config | Add `provideLogging()` | +| `Type 'X' not assignable` | Wrong context type | Use object or function | +| `Cannot read property 'X'` | Undefined property | Use optional chaining | +| `Circular dependency` | Service injection | Use `logger()` factory | +| Stack overflow | Infinite loop in context | Don't call logger in context |