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:
2026-01-10 16:59:40 +01:00
parent 675785ec93
commit 71f84d1cc7
24 changed files with 576 additions and 37 deletions

View 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;
}
};
},
});
}
};
};
}

View File

@@ -0,0 +1,2 @@
export { logger, createLogger, type Logger } from "./logger";
export { Logged } from "./Logged";

View 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;