Skip to content

Commit 1cb6ac8

Browse files
authored
refactor(logging): fix duplicate logging and add service visibility (#210)
* refactor(logging): migrate to singleton logger service - Created LoggerService singleton with WeakMap-based operation tracking - Prevents duplicate logs and memory leaks - CloudWatch-optimized structured JSON logging - Updated 10 controllers, 2 middleware, 7 services, 2 utils - Improved error handling with centralized error middleware logging - Removed redundant error logs before next(error) calls - Net reduction of 706 lines LFXV2-903 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * refactor(logging): extend logger service for infrastructure operations Extended LoggerService to support both request-scoped and infrastructure operations by making req parameter optional. Migrated all 41 direct serverLogger calls across services, utilities, and routes to use the unified logger service pattern. Changes: - Extended LoggerService methods to accept Request | undefined - Migrated 11 request-scoped operations to pass req parameter - Migrated 30 infrastructure operations to use logger with undefined - Updated ai.service, user.service, project.service method signatures - Replaced serverLogger calls in nats, snowflake, lock-manager services - Updated CLAUDE.md with comprehensive logging documentation - Removed serverLogger imports from all service/utility files Benefits: - Unified logging interface for all operations - Better request correlation when context available - Consistent error handling with err field - Infrastructure operations now use same pattern LFXV2-903 Signed-off-by: Asitha de Silva <[email protected]> * docs(logging): update docs for infrastructure logging Updated logging-monitoring.md to reflect the new dual-mode logger service that supports both request-scoped and infrastructure operations. Added documentation for the new server-logger.ts file that breaks the circular dependency between server.ts and logger.service.ts. Changes: - Added logging architecture layers diagram showing server-logger.ts - Updated all method signatures to show req | undefined pattern - Added infrastructure logging examples for all methods - Documented circular dependency resolution - Clarified when to use request-scoped vs infrastructure logging LFXV2-903 Signed-off-by: Asitha de Silva <[email protected]> * refactor(meetings): move agenda generation to controller Moved AI agenda generation logic from inline route handler to MeetingController.generateAgenda method for better separation of concerns and consistency with other route patterns. Also improved logger usage in middleware and services: - auth.middleware: better error tracking with startTime - error-handler.middleware: use operation startTime when available - access-check.service: move startOperation before try block - etag.service: use debug instead of silent startOperation - persona-helper: use success/error instead of warning LFXV2-903 Signed-off-by: Asitha de Silva <[email protected]> * docs(logging): update architecture diagram for server-logger Updated CLAUDE.md logging architecture diagram to reflect the new server-logger.ts file that breaks the circular dependency between server.ts and logger.service.ts. LFXV2-903 Signed-off-by: Asitha de Silva <[email protected]> * fix: claude.md linting issues Signed-off-by: Asitha de Silva <[email protected]> * refactor(meetings): standardize generateAgenda error handling LFXV2-903 Use ServiceValidationError.fromFieldErrors() and next() for validation errors instead of manual 400 response, consistent with other controller methods. Provides field-level error details and centralized error handling through middleware. Signed-off-by: Asitha de Silva <[email protected]> * fix: linting issues for docs Signed-off-by: Asitha de Silva <[email protected]> * refactor(logging): fix duplicate logging and add service visibility - Added logger.info() method for significant business operations - Fixed 8 duplicate startOperation calls in meeting service - Added comprehensive logging to 7 methods without logging - Enhanced access-check service with debug logging - Updated CLAUDE.md with logging layer responsibility guidelines Pattern established: - Controllers: startOperation/success/error for HTTP operations - Services: debug for tracing, info for significant operations, warning for graceful errors Impact: Eliminated 32 duplicate log entries, added production visibility for V1→V2 transformations and data enrichment operations JIRA: LFXV2-903 Signed-off-by: Asitha de Silva <[email protected]> * refactor(logging): fix processRegistrantOperations error logging - Remove unused _startTime parameter from method signature - Add proper error logging for 403 and partial failure scenarios - Update all call sites to remove startTime parameter - Follow logging standards with err field and structured metadata Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> --------- Signed-off-by: Asitha de Silva <[email protected]>
1 parent 19f0164 commit 1cb6ac8

File tree

5 files changed

+293
-113
lines changed

5 files changed

+293
-113
lines changed

CLAUDE.md

Lines changed: 117 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ logger.success(undefined, 'nats_connect', startTime, metadata);
129129
- `logger.startOperation(req|undefined, 'operation', metadata, options?)` → Returns startTime, logs at INFO (silent option available)
130130
- `logger.success(req|undefined, 'operation', startTime, metadata)` → Logs at INFO with duration
131131
- `logger.error(req|undefined, 'operation', startTime, error, metadata, options?)` → Logs at ERROR with 'err' field
132+
- `logger.info(req|undefined, 'operation', message, metadata)` → Logs at INFO for significant operations
132133
- `logger.warning(req|undefined, 'operation', message, metadata)` → Logs at WARN
133134
- `logger.validation(req|undefined, 'operation', errors[], metadata)` → Logs at ERROR with validation details
134135
- `logger.debug(req|undefined, 'operation', message, metadata)` → Logs at DEBUG
@@ -139,6 +140,34 @@ logger.success(undefined, 'nats_connect', startTime, metadata);
139140
- **Request-scoped** (pass `req`): Controllers, route handlers, service methods called from routes
140141
- **Infrastructure** (pass `undefined`): NATS connections, Snowflake pool, server startup/shutdown, scheduled jobs
141142

143+
### Logging Layer Responsibility
144+
145+
**Controllers (HTTP Boundary):**
146+
147+
-**Always** use `logger.startOperation()` / `logger.success()` / `logger.error()` for HTTP operations
148+
- Operation names should match HTTP semantics (e.g., `get_meeting_rsvps`, `create_meeting`)
149+
- Duration represents full HTTP request → response cycle
150+
- One startOperation per HTTP endpoint
151+
152+
**Services (Business Logic):**
153+
154+
-**Always** use `logger.debug()` for step-by-step tracing
155+
- ✅ Use `logger.info()` for significant business operations visible in production:
156+
- Data transformations (V1→V2 conversions)
157+
- Data enrichment (adding project names, committee data)
158+
- Complex multi-step orchestrations
159+
- Operations with notable business impact
160+
- ✅ Use `logger.warning()` for recoverable errors when returning null/empty arrays
161+
-**Never** use `logger.startOperation()` with the same operation name as the calling controller
162+
-**Never** call `serverLogger` directly
163+
164+
**Why This Pattern:**
165+
166+
- Prevents duplicate logging (no double startOperation calls)
167+
- Clear separation: Controllers log HTTP, Services log business logic
168+
- Production visibility: INFO logs for significant operations, DEBUG for detailed tracing
169+
- Consistent duration semantics: HTTP duration in controllers, not inflated by double-counting
170+
142171
### Error Logging Standard
143172

144173
**Always use `err` field** for proper error serialization:
@@ -165,27 +194,34 @@ req.log.error({ error: error }, 'message'); // Should use logger service
165194

166195
### Log Level Guidelines
167196

168-
**INFO** - Business operation completions:
197+
**INFO** - Business operation completions and significant operations:
169198

170-
- Successful data operations (created, updated, deleted, fetched)
171-
- Important state changes
172-
- Operation success with duration
199+
- **In Controllers**: HTTP operation success with duration (via `startOperation` / `success`)
200+
- **In Services**: Significant business operations visible in production (via `logger.info()`):
201+
- Data transformations (V1→V2 conversions)
202+
- Data enrichment (project names, committee data)
203+
- Complex orchestrations
204+
- Operations with notable business impact
173205

174206
**WARN** - Recoverable issues:
175207

176-
- Error conditions leading to exceptions
208+
- Error conditions with graceful degradation (returning null/empty arrays)
177209
- Data quality issues, user not found
178210
- Fallback behaviors, NATS failures with graceful handling
211+
- Service errors that don't propagate to controller
179212

180-
**DEBUG** - Internal operations:
213+
**DEBUG** - Internal operations and tracing:
181214

215+
- **In Services**: Step-by-step operation tracing
216+
- Method entry/exit with key parameters
182217
- Preparation steps (sanitizing, creating payload)
183218
- Internal lookups (NATS, database queries)
184-
- Intent statements (resolving, fetching)
219+
- Simple data fetches
185220
- Infrastructure operations (connections, pool state)
186221

187222
**ERROR** - Critical failures:
188223

224+
- **In Controllers**: HTTP operation failures (via `logger.error()` with startTime)
189225
- System failures, unhandled exceptions
190226
- Critical errors requiring immediate attention
191227
- Operations that cannot continue
@@ -246,25 +282,77 @@ export const getUser = async (req: Request, res: Response, next: NextFunction) =
246282
};
247283
```
248284

249-
**Service Method with Request Context:**
285+
**Service Method - Simple (DEBUG only):**
286+
287+
```typescript
288+
public async getUserById(req: Request, userId: string): Promise<User> {
289+
logger.debug(req, 'get_user_by_id', 'Fetching user from database', { userId });
290+
291+
const user = await this.database.findUser(userId);
292+
293+
return user;
294+
}
295+
```
296+
297+
**Service Method - Complex (INFO + DEBUG):**
298+
299+
```typescript
300+
public async getMeetings(req: Request, query: QueryParams): Promise<Meeting[]> {
301+
logger.debug(req, 'get_meetings', 'Starting meeting fetch', { query });
302+
303+
const { resources } = await this.microserviceProxy.proxyRequest(...);
304+
305+
logger.debug(req, 'get_meetings', 'Fetched resources', { count: resources.length });
306+
307+
// Significant transformation - use INFO level for production visibility
308+
if (isV1) {
309+
logger.info(req, 'transform_v1_meetings', 'Transforming V1 meetings to V2 format', {
310+
count: resources.length,
311+
});
312+
meetings = meetings.map(transformV1MeetingToV2);
313+
}
314+
315+
// Enrichment step - DEBUG for tracing
316+
logger.debug(req, 'get_meetings', 'Enriching with project names', { count: meetings.length });
317+
meetings = await this.enrichWithProjects(req, meetings);
318+
319+
// Significant enrichment - use INFO level
320+
if (meetings.some((m) => m.committees?.length > 0)) {
321+
logger.info(req, 'enrich_committees', 'Enriching meetings with committee data', {
322+
total_meetings: meetings.length,
323+
});
324+
meetings = await this.getMeetingCommittees(req, meetings);
325+
}
326+
327+
logger.debug(req, 'get_meetings', 'Completed meeting fetch', { final_count: meetings.length });
328+
329+
return meetings;
330+
}
331+
```
332+
333+
**Service Method - Graceful Error Handling:**
250334

251335
```typescript
252-
public async updateUser(req: Request, userId: string, data: UpdateData): Promise<User> {
253-
const startTime = logger.startOperation(req, 'update_user', { userId });
336+
public async getPastMeetingRecording(req: Request, meetingUid: string): Promise<Recording | null> {
337+
logger.debug(req, 'get_past_meeting_recording', 'Fetching recording', { meeting_uid: meetingUid });
254338

255339
try {
256-
// Validation
257-
if (!data.email) {
258-
logger.validation(req, 'update_user', ['Email required'], { userId });
259-
throw new ValidationError('Email required');
340+
const { resources } = await this.microserviceProxy.proxyRequest(...);
341+
342+
if (!resources || resources.length === 0) {
343+
logger.warning(req, 'get_past_meeting_recording', 'No recording found', {
344+
meeting_uid: meetingUid,
345+
});
346+
return null;
260347
}
261348

262-
const user = await this.performUpdate(userId, data);
263-
logger.success(req, 'update_user', startTime, { userId, updatedFields: Object.keys(data) });
264-
return user;
349+
return resources[0].data;
265350
} catch (error) {
266-
logger.error(req, 'update_user', startTime, error, { userId });
267-
throw error;
351+
logger.warning(req, 'get_past_meeting_recording', 'Failed to fetch recording, returning null', {
352+
meeting_uid: meetingUid,
353+
error: error instanceof Error ? error.message : 'Unknown error',
354+
});
355+
return null;
268356
}
269357
}
270358
```
@@ -311,10 +399,18 @@ private async fetchFromNats(req: Request, slug: string): Promise<Project> {
311399
- [ ] Operation name in snake_case?
312400
- [ ] Sensitive data sanitized?
313401

314-
**For operations with duration:**
402+
**For Controllers:**
315403

316-
- [ ] Called `logger.startOperation()` and captured `startTime`?
404+
- [ ] Using `logger.startOperation()` for HTTP operations?
317405
- [ ] Calling `logger.success()` or `logger.error()` with `startTime`?
406+
- [ ] Not duplicating service-level logging?
407+
408+
**For Services:**
409+
410+
- [ ] Using `logger.debug()` for step-by-step tracing?
411+
- [ ] Using `logger.info()` for significant operations (transformations, enrichments)?
412+
- [ ] Using `logger.warning()` for graceful error handling (returning null/empty)?
413+
- [ ] NOT using `logger.startOperation()` if controller already logs the same operation?
318414
- [ ] Passing relevant metadata for debugging?
319415
- All shared types, interfaces, and constants are centralized in @lfx-one/shared package
320416
- **AI Service Integration**: Claude Sonnet 4 model via LiteLLM proxy for meeting agenda generation

apps/lfx-one/src/server/controllers/meeting.controller.ts

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -426,14 +426,8 @@ export class MeetingController {
426426

427427
// Process registrants with fail-fast for 403 errors
428428
// This will stop the processing if a 403 error is encountered
429-
const { results, shouldReturn } = await this.processRegistrantOperations(
430-
req,
431-
next,
432-
startTime,
433-
'add_meeting_registrants',
434-
uid,
435-
registrantData,
436-
(registrant) => this.meetingService.addMeetingRegistrant(req, registrant)
429+
const { results, shouldReturn } = await this.processRegistrantOperations(req, next, 'add_meeting_registrants', uid, registrantData, (registrant) =>
430+
this.meetingService.addMeetingRegistrant(req, registrant)
437431
);
438432

439433
// If the processing should return, return
@@ -527,7 +521,7 @@ export class MeetingController {
527521
}
528522

529523
// Process updates with fail-fast for 403 errors
530-
const { results, shouldReturn } = await this.processRegistrantOperations(req, next, startTime, 'update_meeting_registrants', uid, updateData, (update) =>
524+
const { results, shouldReturn } = await this.processRegistrantOperations(req, next, 'update_meeting_registrants', uid, updateData, (update) =>
531525
this.meetingService.updateMeetingRegistrant(req, uid, update.uid, update.changes)
532526
);
533527

@@ -616,14 +610,8 @@ export class MeetingController {
616610

617611
// Process deletions with fail-fast for 403 errors
618612
// This will stop the processing if a 403 error is encountered
619-
const { results, shouldReturn } = await this.processRegistrantOperations(
620-
req,
621-
next,
622-
startTime,
623-
'delete_meeting_registrants',
624-
uid,
625-
registrantsUid,
626-
(registrantUid) => this.meetingService.deleteMeetingRegistrant(req, uid, registrantUid).then(() => registrantUid)
613+
const { results, shouldReturn } = await this.processRegistrantOperations(req, next, 'delete_meeting_registrants', uid, registrantsUid, (registrantUid) =>
614+
this.meetingService.deleteMeetingRegistrant(req, uid, registrantUid).then(() => registrantUid)
627615
);
628616

629617
// If the processing should return, return
@@ -1232,7 +1220,6 @@ export class MeetingController {
12321220
private async processRegistrantOperations<T, R>(
12331221
req: Request,
12341222
next: NextFunction,
1235-
_startTime: number,
12361223
operationName: string,
12371224
meetingUid: string,
12381225
inputData: T[],
@@ -1268,12 +1255,24 @@ export class MeetingController {
12681255
// Check if it's a 403 error - if so, fail fast
12691256
// This will stop the processing if a 403 error is encountered
12701257
if (error?.status === 403 || error?.statusCode === 403) {
1258+
logger.error(req, `${operationName}_batch_processing`, helperStartTime, error, {
1259+
meeting_uid: meetingUid,
1260+
batch_size: inputData.length,
1261+
error_type: '403_forbidden',
1262+
});
12711263
// Send the error to the next middleware
12721264
next(error);
12731265
return { results: [], shouldReturn: true };
12741266
}
12751267

1276-
// For other errors, continue processing the remaining items
1268+
// For other errors, log and continue processing the remaining items
1269+
logger.error(req, `${operationName}_batch_processing`, helperStartTime, error, {
1270+
meeting_uid: meetingUid,
1271+
batch_size: inputData.length,
1272+
error_type: 'partial_failure',
1273+
continuing: true,
1274+
});
1275+
12771276
let results: PromiseSettledResult<R>[] = [{ status: 'rejected', reason: error }];
12781277

12791278
if (inputData.length > 1) {

apps/lfx-one/src/server/services/access-check.service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ export class AccessCheckService {
165165
resourceType: AccessCheckResourceType,
166166
accessType: AccessCheckAccessType = 'writer'
167167
): Promise<T & { writer?: boolean }> {
168+
logger.debug(req, 'add_access_to_resource', 'Adding access to resource', {
169+
resource_type: resourceType,
170+
resource_id: resource.uid,
171+
access_type: accessType,
172+
});
173+
168174
const hasAccess = await this.checkSingleAccess(req, {
169175
resource: resourceType,
170176
id: resource.uid,

apps/lfx-one/src/server/services/logger.service.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,42 @@ export class LoggerService {
360360
);
361361
}
362362

363+
/**
364+
* Logs info messages with operation context
365+
* Use for significant business operations that should be visible in production
366+
* @param req - Request object (optional - use undefined for infrastructure operations)
367+
*/
368+
public info(req: Request | undefined, operation: string, message: string, metadata: Record<string, unknown> = {}): void {
369+
// Extract err from metadata to place at top level for proper error serialization
370+
const { err, ...rest } = metadata;
371+
372+
// Infrastructure logging (no request context)
373+
if (!req) {
374+
serverLogger.info(
375+
{
376+
operation,
377+
status: 'info',
378+
...(err ? { err } : {}), // err at top level for Pino error serializer
379+
...(Object.keys(rest).length > 0 && { data: rest }),
380+
},
381+
`${this.formatOperation(operation)}: ${message}`
382+
);
383+
return;
384+
}
385+
386+
// Request-scoped logging
387+
req.log.info(
388+
{
389+
operation,
390+
status: 'info',
391+
request_id: req.id,
392+
...(err ? { err } : {}), // err at top level for Pino error serializer
393+
...(Object.keys(rest).length > 0 && { data: rest }),
394+
},
395+
`${this.formatOperation(operation)}: ${message}`
396+
);
397+
}
398+
363399
/**
364400
* Logs debug messages with operation context
365401
* Use for detailed internal state, preparation steps, or verbose information

0 commit comments

Comments
 (0)