Skip to content

Commit 3d3e09f

Browse files
committed
feat(logger): introduce a dedicated logger plugin for improved logging capabilities; update related imports and configurations across the codebase; remove deprecated observability logger files
1 parent 9a32c73 commit 3d3e09f

File tree

12 files changed

+1989
-4918
lines changed

12 files changed

+1989
-4918
lines changed

.claude/settings.local.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@
2626
"Bash(cat:*)",
2727
"Bash(pnpm tbk generate:plugin:*)",
2828
"Bash(find:*)",
29-
"Bash(timeout 15 pnpm start:dev:*)"
29+
"Bash(timeout 15 pnpm start:dev:*)",
30+
"Bash(pnpm lint:*)",
31+
"Bash(if [ -d test-standard ])",
32+
"Bash(then rm -rf test-standard)",
33+
"Bash(rm:*)",
34+
"Bash(mkdir:*)"
3035
],
3136
"deny": [],
3237
"ask": []

packages/create-tbk-app/src/cli.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,8 +503,8 @@ function buildConfigFromOptions(
503503
realtime,
504504
admin,
505505
observability,
506-
agents: options.agents ?? [],
507-
modules: options.modules ?? [],
506+
agents: options.agents ?? presetConfig?.agents ?? [],
507+
modules: options.modules ?? presetConfig?.modules ?? [],
508508
packageManager: options.packageManager ?? 'pnpm',
509509
skipGit: resolveBooleanOption(options.skipGit, false),
510510
skipInstall: resolveBooleanOption(options.skipInstall, false),

packages/create-tbk-app/templates/auth/src/middlewares/extract-jwt.ts.hbs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import type { NextFunction } from 'express';
22
import { type JwtPayload, verifyToken } from '@/utils/jwt.utils';
33
import type { RequestAny, ResponseAny } from '@/plugins/magic/router';
4-
import config from '@/config/env';
5-
6-
import { createChildLogger } from '@/plugins/logger';
7-
84
{{#if AUTH_SESSIONS}}
9-
// Session validation imports
5+
import config from '@/config/env';
106
{{/if}}
7+
import { createChildLogger } from '@/plugins/logger';
118

129
const logger = createChildLogger({ context: 'extract-jwt' });
1310

packages/create-tbk-app/templates/auth/src/modules/auth/auth.controller.ts.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export const handleRegisterUser = async (
9797

9898
// Using new res.ok() helper
9999
export const handleLogout = async (
100-
req: Request,
100+
{{#if AUTH_SESSIONS}}req{{else}}_req{{/if}}: Request,
101101
res: ResponseExtended<LogoutResponseSchema>,
102102
) => {
103103
{{#if AUTH_SESSIONS}}

packages/create-tbk-app/templates/auth/src/modules/auth/auth.service.ts.hbs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export const registerUserByEmail = async (
121121

122122
export const loginUserByEmail = async (
123123
payload: LoginUserByEmailSchemaType,
124-
metadata?: { userAgent?: string; ipAddress?: string },
124+
{{#if AUTH_SESSIONS}}metadata{{else}}_metadata{{/if}}?: { userAgent?: string; ipAddress?: string },
125125
): Promise<{ token: string; sessionId?: string }> => {
126126
const user = await getUserByEmail(payload.email, '+password');
127127

@@ -141,7 +141,9 @@ export const loginUserByEmail = async (
141141
username: user.username,
142142
};
143143

144+
{{#if AUTH_SESSIONS}}
144145
let sessionId: string | undefined;
146+
{{/if}}
145147

146148
{{#if AUTH_SESSIONS}}
147149
if (config.SET_SESSION) {
@@ -177,7 +179,7 @@ export const loginUserByEmail = async (
177179
{{#if AUTH_GOOGLE_OAUTH}}
178180
export const googleLogin = async (
179181
payload: GoogleCallbackSchemaType,
180-
metadata?: { userAgent?: string; ipAddress?: string },
182+
{{#if AUTH_SESSIONS}}metadata{{else}}_metadata{{/if}}?: { userAgent?: string; ipAddress?: string },
181183
): Promise<{ user: UserType; token: string; sessionId?: string }> => {
182184
const { code, error } = payload;
183185

@@ -237,8 +239,10 @@ export const googleLogin = async (
237239
username: user.username,
238240
};
239241

242+
{{#if AUTH_SESSIONS}}
240243
let sessionId: string | undefined;
241244

245+
{{/if}}
242246
{{#if AUTH_SESSIONS}}
243247
if (config.SET_SESSION) {
244248
const sessionManager = getSessionManager();

packages/create-tbk-app/templates/auth/src/plugins/auth/index.ts.hbs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@ import {
55
type SessionManager,
66
} from '@/modules/auth/session/session.manager';
77
import type { SessionStoreConfig } from '@/modules/auth/session/session.types';
8-
{{/if}}
98
import config from '@/config/env';
109
import logger from '@/plugins/logger';
11-
{{#if AUTH_SESSIONS}}
1210
import { scheduleSessionCleanup } from '../../queues/session-cleanup.queue';
1311
{{/if}}
1412
import { extractJwt } from '../../middlewares/extract-jwt';

packages/create-tbk-app/templates/base/src/middlewares/can-access.ts.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ export * from '../../../auth/middlewares/can-access';
55
{{else}}
66
// Auth is disabled - providing stub export
77
import { Request, Response, NextFunction } from 'express';
8-
export const canAccess = () => (req: Request, res: Response, next: NextFunction) => next();
8+
export const canAccess = () => (_req: Request, _res: Response, next: NextFunction) => next();
99
{{/if}}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import pino from 'pino';
2+
import pinoHttp from 'pino-http';
3+
import type { RequestExtended } from '@/types';
4+
import { ServerResponse as ResponseHTTP } from 'node:http';
5+
import type { ToolkitPlugin, PluginFactory } from '@/plugins/types';
6+
7+
// ============================================================================
8+
// Types
9+
// ============================================================================
10+
11+
export type PinoLogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
12+
13+
export interface LoggerOptions {
14+
/** Enable HTTP request logging middleware (default: true) */
15+
enabled?: boolean;
16+
/** Override LOG_LEVEL environment variable */
17+
level?: PinoLogLevel;
18+
/** Force pretty printing even in production (default: auto based on NODE_ENV) */
19+
pretty?: boolean;
20+
/** Paths to redact from logs (e.g., ['req.headers.authorization']) */
21+
redact?: string[];
22+
}
23+
24+
export interface ChildLoggerContext extends Record<string, unknown> {
25+
context?: string;
26+
}
27+
28+
// ============================================================================
29+
// Logger Configuration
30+
// ============================================================================
31+
32+
const isDevelopment = process.env.NODE_ENV === 'development';
33+
const defaultLogLevel: PinoLogLevel = (process.env.LOG_LEVEL as PinoLogLevel) ||
34+
(isDevelopment ? 'debug' : 'info');
35+
36+
/**
37+
* Create a configured pino logger instance
38+
*/
39+
function createLogger(options: LoggerOptions = {}) {
40+
const usePretty = options.pretty !== undefined
41+
? options.pretty
42+
: isDevelopment;
43+
44+
const level = options.level || defaultLogLevel;
45+
46+
return pino({
47+
level,
48+
redact: options.redact || [],
49+
transport: usePretty
50+
? {
51+
target: 'pino-pretty',
52+
options: {
53+
colorize: true,
54+
translateTime: 'HH:MM:ss.l',
55+
ignore: 'pid,hostname',
56+
customColors: 'info:blue,warn:yellow,error:red,debug:gray',
57+
levelFirst: false,
58+
singleLine: false,
59+
},
60+
}
61+
: undefined,
62+
formatters: {
63+
level: (label) => {
64+
return { level: label.toUpperCase() };
65+
},
66+
},
67+
});
68+
}
69+
70+
/**
71+
* Main logger instance - exported for direct use throughout the application
72+
*/
73+
export const logger = createLogger();
74+
75+
// ============================================================================
76+
// HTTP Logger Configuration
77+
// ============================================================================
78+
79+
/**
80+
* HTTP request logging middleware using pino-http
81+
* Automatically logs all HTTP requests with appropriate log levels based on status codes
82+
*/
83+
export const httpLogger = pinoHttp({
84+
logger,
85+
customLogLevel: (_req, res, err) => {
86+
if (res.statusCode >= 500 || err) {
87+
return 'error';
88+
}
89+
if (res.statusCode >= 400) {
90+
return 'warn';
91+
}
92+
return 'info';
93+
},
94+
customSuccessMessage: (req, res) => {
95+
return `${req.method} ${req.url} ${res.statusCode}`;
96+
},
97+
customErrorMessage: (req, res, err) => {
98+
return `${req.method} ${req.url} ${res.statusCode} - ${err.message}`;
99+
},
100+
customAttributeKeys: {
101+
req: 'request',
102+
res: 'response',
103+
err: 'error',
104+
responseTime: 'duration',
105+
},
106+
serializers: {
107+
req: (req) => {
108+
const extended = req as RequestExtended;
109+
return {
110+
id: extended.id,
111+
method: req.method,
112+
url: req.url,
113+
path: req.path,
114+
headers: {
115+
host: req.headers.host,
116+
'user-agent': req.headers['user-agent'],
117+
'x-request-id': req.headers['x-request-id'],
118+
},
119+
remoteAddress: req.remoteAddress,
120+
remotePort: req.remotePort,
121+
};
122+
},
123+
res: (res: unknown) => ({
124+
statusCode:
125+
res instanceof Response
126+
? res.status
127+
: res instanceof ResponseHTTP
128+
? res.statusCode
129+
: 200,
130+
headers: {
131+
'content-type':
132+
res instanceof Response
133+
? res.headers.get('content-type')
134+
: res instanceof ResponseHTTP
135+
? res.getHeader('content-type')
136+
: 'application/json',
137+
'content-length':
138+
res instanceof Response
139+
? res.headers.get('content-length')
140+
: res instanceof ResponseHTTP
141+
? res.getHeader('content-length')
142+
: '100',
143+
},
144+
}),
145+
},
146+
});
147+
148+
// ============================================================================
149+
// Child Logger Factory
150+
// ============================================================================
151+
152+
/**
153+
* Create a child logger with contextual information
154+
*
155+
* @example
156+
* ```typescript
157+
* const logger = createChildLogger({ context: 'AuthService', userId: '123' });
158+
* logger.info('User logged in');
159+
* // Output: [AuthService] User logged in { userId: '123' }
160+
* ```
161+
*
162+
* @param context - Context object to attach to all log messages
163+
* @returns A child logger instance with the provided context
164+
*/
165+
export function createChildLogger<T extends ChildLoggerContext>(context: T) {
166+
const msgPrefix = context.context ? `[${context.context}] ` : '';
167+
return logger.child(context, { msgPrefix });
168+
}
169+
170+
// ============================================================================
171+
// Plugin Factory
172+
// ============================================================================
173+
174+
/**
175+
* Logger plugin for TypeScript Backend Toolkit
176+
*
177+
* Provides structured logging capabilities with:
178+
* - Pino logger with pretty printing in development
179+
* - HTTP request logging middleware
180+
* - Child logger factory for contextual logging
181+
* - Type-safe configuration options
182+
*
183+
* @example
184+
* ```typescript
185+
* import loggerPlugin from '@/plugins/logger';
186+
*
187+
* const plugins = [
188+
* loggerPlugin({
189+
* enabled: true,
190+
* level: 'debug',
191+
* redact: ['req.headers.authorization'],
192+
* }),
193+
* ];
194+
* ```
195+
*/
196+
export const loggerPlugin: PluginFactory<LoggerOptions> = (
197+
options = {},
198+
): ToolkitPlugin<LoggerOptions> => {
199+
const { enabled = true } = options;
200+
201+
return {
202+
name: 'logger',
203+
priority: 100, // High priority - many plugins depend on logger
204+
options,
205+
206+
register({ app }) {
207+
if (!enabled) {
208+
return [];
209+
}
210+
211+
// Register HTTP logging middleware
212+
app.use(httpLogger);
213+
214+
logger.debug('Logger plugin registered', {
215+
level: options.level || defaultLogLevel,
216+
pretty: options.pretty !== undefined ? options.pretty : isDevelopment,
217+
redact: options.redact?.length || 0,
218+
});
219+
220+
return [];
221+
},
222+
223+
onShutdown: async () => {
224+
logger.info('Logger plugin shutting down');
225+
// Pino handles its own cleanup
226+
},
227+
};
228+
};
229+
230+
export default logger;

0 commit comments

Comments
 (0)