& so it begins

This commit is contained in:
user
2026-02-28 14:50:04 +02:00
commit f00381f2b6
536 changed files with 26294 additions and 0 deletions

222
packages/logger/client.ts Normal file
View File

@@ -0,0 +1,222 @@
type LogLevel = "error" | "warn" | "info" | "http" | "debug";
export interface LogEntry {
level: LogLevel;
timestamp: string;
message: any;
metadata?: any;
}
interface Error {
code: string;
message: string;
description?: string;
detail?: string;
error?: any;
actionable?: boolean;
}
class BrowserLogger {
private queue: LogEntry[] = [];
private timer: ReturnType<typeof setInterval> | null = null;
private readonly BATCH_INTERVAL = 5000; // 5 seconds
private readonly BATCH_SIZE_LIMIT = 50;
private readonly isDev: boolean;
constructor(isDev: boolean) {
this.isDev = isDev;
if (!this.isDev) {
this.startBatchTimer();
this.setupBeforeUnloadHandler();
}
}
private startBatchTimer() {
this.timer = setInterval(() => this.flush(), this.BATCH_INTERVAL);
}
private setupBeforeUnloadHandler() {
// Flush logs before page unload to avoid losing them
if (typeof window !== "undefined") {
window.addEventListener("beforeunload", () => {
this.flushSync();
});
}
}
private async flush() {
if (this.queue.length === 0) return;
const batch = [...this.queue];
this.queue = [];
try {
// Forward batch to Hono route
await fetch("/api/logs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ logs: batch }),
});
} catch (err) {
console.error("Axiom batch upload failed", err);
// Re-add failed logs back to queue (up to a limit to avoid memory issues)
if (this.queue.length < this.BATCH_SIZE_LIMIT * 2) {
this.queue.push(...batch);
}
}
}
private flushSync() {
// Synchronous flush for beforeunload using sendBeacon
if (this.queue.length === 0) return;
const batch = [...this.queue];
this.queue = [];
try {
const blob = new Blob([JSON.stringify({ logs: batch })], {
type: "application/json",
});
navigator.sendBeacon("/api/logs", blob);
} catch (err) {
console.error("Failed to send logs via sendBeacon", err);
}
}
private serializeError(error: unknown): any {
if (error instanceof Error) {
return {
name: error.name,
message: error.message,
stack: error.stack,
...(error.cause && { cause: this.serializeError(error.cause) }),
};
}
return error;
}
private createLogEntry(
level: LogLevel,
message: any,
metadata?: any,
): LogEntry {
// Handle Error serialization for message
const cleanMessage =
message instanceof Error ? this.serializeError(message) : message;
// Handle Error serialization for metadata
const cleanMetadata =
metadata instanceof Error
? this.serializeError(metadata)
: metadata;
return {
level,
timestamp: new Date().toISOString(),
message: cleanMessage,
metadata: cleanMetadata || {},
};
}
private async sendLog(entry: LogEntry) {
// Always log errors to console, even in production (for user debugging)
// In dev, log everything to console
const shouldConsoleLog = this.isDev || entry.level === "error";
if (shouldConsoleLog) {
const consoleMethod =
entry.level === "http" ? "debug" : entry.level;
console[consoleMethod](
`[client-${entry.level}] ${entry.timestamp}:`,
entry.message,
entry.metadata && Object.keys(entry.metadata).length > 0
? entry.metadata
: "",
);
}
// In production, add to queue for batching
if (!this.isDev) {
this.queue.push(entry);
// Safety flush if queue gets too large
if (this.queue.length >= this.BATCH_SIZE_LIMIT) {
await this.flush();
}
}
}
error(message: any, metadata?: any) {
this.sendLog(this.createLogEntry("error", message, metadata));
}
warn(message: any, metadata?: any) {
this.sendLog(this.createLogEntry("warn", message, metadata));
}
info(message: any, metadata?: any) {
this.sendLog(this.createLogEntry("info", message, metadata));
}
http(message: any, metadata?: any) {
this.sendLog(this.createLogEntry("http", message, metadata));
}
debug(message: any, metadata?: any) {
this.sendLog(this.createLogEntry("debug", message, metadata));
}
// Manual flush method for advanced use cases
async forceFlush() {
await this.flush();
}
// Cleanup method
destroy() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
this.flushSync();
}
}
/**
* Factory function to create a BrowserLogger instance
*
* @param isDev - Whether the app is running in development mode
* @returns A new BrowserLogger instance
*
* @example
* // SvelteKit
* import { dev } from '$app/environment';
* const logger = getLoggerInstance(dev);
*
* @example
* // Next.js
* const logger = getLoggerInstance(process.env.NODE_ENV === 'development');
*
* @example
* // Vite
* const logger = getLoggerInstance(import.meta.env.DEV);
*/
function getLoggerInstance(isDev: boolean): BrowserLogger {
return new BrowserLogger(isDev);
}
function getError(logger: BrowserLogger, payload: Error, error?: any) {
logger.error(payload);
if (error) {
logger.error(error);
}
return {
code: payload.code,
message: payload.message,
description: payload.description,
detail: payload.detail,
error: error,
actionable: payload.actionable,
} as Error;
}
export { getError, getLoggerInstance };

143
packages/logger/index.ts Normal file
View File

@@ -0,0 +1,143 @@
import DailyRotateFile from "winston-daily-rotate-file";
import { settings } from "@pkg/settings";
import type { Err } from "@pkg/result";
import winston from "winston";
import util from "util";
import path from "path";
process.on("warning", (warning) => {
const msg = String(warning?.message || "");
const name = String((warning as any)?.name || "");
// Ignore the noisy timer warning from Node/kafkajs interplay
if (
name === "TimeoutNegativeWarning" ||
msg.includes("TimeoutNegativeWarning") ||
msg.includes("Timeout duration was set to 1")
) {
return;
}
// Keep other warnings visible
console.warn(warning);
});
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4,
};
const colors = {
error: "red",
warn: "yellow",
info: "green",
http: "magenta",
debug: "white",
};
const level = () => {
const envLevel = process.env.LOG_LEVEL?.toLowerCase();
if (envLevel && envLevel in levels) {
return envLevel;
}
return settings.isDevelopment ? "debug" : "warn";
};
// Console format with colors
const consoleFormat = winston.format.combine(
winston.format.errors({ stack: true }),
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss:ms" }),
winston.format.colorize({ all: true }),
winston.format.printf((info) => {
const { level, message, timestamp, ...extra } = info;
let formattedMessage = "";
if (message instanceof Error) {
formattedMessage = message.stack || message.message;
} else if (typeof message === "object") {
formattedMessage = util.inspect(message, {
depth: null,
colors: true,
});
} else {
formattedMessage = message as any as string;
}
// Handle extra fields (if any)
const formattedExtra =
Object.keys(extra).length > 0
? `\n${util.inspect(extra, { depth: null, colors: true })}`
: "";
return `[${level}] ${timestamp}: ${formattedMessage}${formattedExtra}`;
}),
);
// JSON format for file logging
const fileFormat = winston.format.combine(
winston.format.errors({ stack: true }),
winston.format.timestamp(),
winston.format.json(),
);
// File transport with daily rotation
const fileTransport = new DailyRotateFile({
filename: path.join("logs", "app-%DATE%.log"),
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "14d",
format: fileFormat,
});
// Error file transport with daily rotation
const errorFileTransport = new DailyRotateFile({
filename: path.join("logs", "error-%DATE%.log"),
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "14d",
level: "error",
format: fileFormat,
});
const transports: winston.transport[] = [
new winston.transports.Console({ format: consoleFormat }),
fileTransport,
errorFileTransport,
];
winston.addColors(colors);
const logger = winston.createLogger({
level: level(),
levels,
transports,
format: fileFormat,
exceptionHandlers: [
new winston.transports.Console({ format: consoleFormat }),
fileTransport,
],
rejectionHandlers: [
new winston.transports.Console({ format: consoleFormat }),
fileTransport,
],
});
const stream = { write: (message: string) => logger.http(message.trim()) };
function getError(payload: Err, error?: any) {
logger.error(JSON.stringify({ payload, error }, null, 2));
console.error(error);
return {
code: payload.code,
message: payload.message,
description: payload.description,
detail: payload.detail,
error: error instanceof Error ? error.message : error,
actionable: payload.actionable,
} as Err;
}
export { getError, logger, stream };

View File

@@ -0,0 +1,18 @@
{
"name": "@pkg/logger",
"module": "index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@axiomhq/winston": "^1.3.1",
"@pkg/result": "workspace:*",
"@pkg/settings": "workspace:*",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
}
}

View File

@@ -0,0 +1,5 @@
{
"compilerOptions": {
"esModuleInterop": true,
},
}