mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
docs(logging): add logging-helper skill documentation
Add comprehensive skill documentation for @isa/core/logging usage: - SKILL.md: Core principles, patterns, and best practices - examples.md: Real-world usage scenarios across components/services - reference.md: Quick API reference and configuration guide - troubleshooting.md: Common issues and solutions Provides guidance for consistent logging patterns throughout the codebase.
This commit is contained in:
272
.claude/skills/logging-helper/SKILL.md
Normal file
272
.claude/skills/logging-helper/SKILL.md
Normal file
@@ -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<Data> {
|
||||
this.#logger.debug('Fetching data', { endpoint });
|
||||
|
||||
return this.http.get<Data>(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`
|
||||
350
.claude/skills/logging-helper/examples.md
Normal file
350
.claude/skills/logging-helper/examples.md
Normal file
@@ -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<Order> {
|
||||
this.#logger.debug('Fetching order', { id });
|
||||
|
||||
return this.http.get<Order>(`/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<string, unknown> {
|
||||
const errors: Record<string, unknown> = {};
|
||||
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<number> {
|
||||
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<number> {
|
||||
// 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<void>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
```
|
||||
192
.claude/skills/logging-helper/reference.md
Normal file
192
.claude/skills/logging-helper/reference.md
Normal file
@@ -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';
|
||||
```
|
||||
235
.claude/skills/logging-helper/troubleshooting.md
Normal file
235
.claude/skills/logging-helper/troubleshooting.md
Normal file
@@ -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 |
|
||||
Reference in New Issue
Block a user