feat: implement structured logging for server and client
Server: - Add pino and pino-http for structured logging - Create @Logged class decorator using Proxy pattern for automatic method logging - Add pino redact config for sensitive data (password, token, etc.) - Move AuthMiddleware to controllers folder (per architecture diagram) - Add LoggingMiddleware for HTTP request logging - Replace console.log/error with structured logger in controllers and app.ts - Decorate all repositories and GPTAdapter with @Logged Client: - Add react-native-logs with namespaced loggers (apiLogger, storeLogger) - Add request/response logging to ApiClient with duration tracking
This commit is contained in:
76
apps/server/src/logging/Logged.ts
Normal file
76
apps/server/src/logging/Logged.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { createLogger } from "./logger";
|
||||
|
||||
export function Logged(name: string) {
|
||||
const log = createLogger(name);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return function <T extends { new (...args: any[]): any }>(Constructor: T) {
|
||||
return class extends Constructor {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(...args: any[]) {
|
||||
super(...args);
|
||||
|
||||
// Return a Proxy that intercepts method calls lazily
|
||||
return new Proxy(this, {
|
||||
get(target, propKey, receiver) {
|
||||
const original = Reflect.get(target, propKey, receiver);
|
||||
|
||||
if (typeof original !== "function" || propKey === "constructor") {
|
||||
return original;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const originalFn = original as (...args: any[]) => any;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return function (this: any, ...methodArgs: any[]) {
|
||||
const start = performance.now();
|
||||
const method = String(propKey);
|
||||
|
||||
// Pino's redact handles sanitization - just pass args directly
|
||||
log.debug({ method, args: methodArgs }, `${method} started`);
|
||||
|
||||
const logCompletion = (err?: unknown) => {
|
||||
const duration = Math.round(performance.now() - start);
|
||||
if (err) {
|
||||
const message =
|
||||
err instanceof Error ? err.message : String(err);
|
||||
log.error(
|
||||
{ method, duration, error: message },
|
||||
`${method} failed`,
|
||||
);
|
||||
} else {
|
||||
log.info({ method, duration }, `${method} completed`);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const result = originalFn.apply(this, methodArgs);
|
||||
|
||||
// Check if async - preserves sync/async nature of method
|
||||
if (result instanceof Promise) {
|
||||
return result
|
||||
.then((val) => {
|
||||
logCompletion();
|
||||
return val;
|
||||
})
|
||||
.catch((err) => {
|
||||
logCompletion(err);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// Synchronous completion
|
||||
logCompletion();
|
||||
return result;
|
||||
} catch (err) {
|
||||
logCompletion(err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
2
apps/server/src/logging/index.ts
Normal file
2
apps/server/src/logging/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { logger, createLogger, type Logger } from "./logger";
|
||||
export { Logged } from "./Logged";
|
||||
43
apps/server/src/logging/logger.ts
Normal file
43
apps/server/src/logging/logger.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import pino from "pino";
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV !== "production";
|
||||
|
||||
export const logger = pino({
|
||||
level: process.env.LOG_LEVEL || (isDevelopment ? "debug" : "info"),
|
||||
redact: {
|
||||
paths: [
|
||||
// Root level
|
||||
"password",
|
||||
"passwordHash",
|
||||
"token",
|
||||
// One level deep (e.g. user.password)
|
||||
"*.password",
|
||||
"*.passwordHash",
|
||||
"*.token",
|
||||
// In arrays (for 'args' in decorator)
|
||||
"args[*].password",
|
||||
"args[*].passwordHash",
|
||||
"args[*].token",
|
||||
],
|
||||
censor: "[REDACTED]",
|
||||
},
|
||||
transport: isDevelopment
|
||||
? {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
colorize: true,
|
||||
translateTime: "SYS:HH:MM:ss",
|
||||
ignore: "pid,hostname",
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
base: {
|
||||
service: "caldav-server",
|
||||
},
|
||||
});
|
||||
|
||||
export function createLogger(module: string) {
|
||||
return logger.child({ module });
|
||||
}
|
||||
|
||||
export type Logger = pino.Logger;
|
||||
Reference in New Issue
Block a user