Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Add logger package for node server side apps #6188

Merged
merged 9 commits into from
Dec 13, 2024
3 changes: 3 additions & 0 deletions packages/logger/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build/*
dist/*
out/*
9 changes: 9 additions & 0 deletions packages/logger/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};
5 changes: 5 additions & 0 deletions packages/logger/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
58 changes: 58 additions & 0 deletions packages/logger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Logger Package

This package provides a singleton-based logger utility built using [Winston](https://github.com/winstonjs/winston). It offers customizable log levels and supports structured logging for general application logs and HTTP requests.

## Features
- Singleton pattern ensures a single logger instance.
- Dynamic log level configuration and log filename prefix.
- Pre-configured winston logger for general usage (`logger`).

## Usage

### Adding as a package
Add this package as a dependency in package.json
```typescript
dependency: {
...
@plane/logger":"*",
...
}
sriramveeraghanta marked this conversation as resolved.
Show resolved Hide resolved
```

### Importing the Logger
```typescript
import PlaneLogger from "@plane/logger";
```

### `logger`: General Logger
Use this for general application logs.

```typescript
const loggerOptions: ILoggerOptions = { logLevel:"info", logFilePrefix: "log-file-prefix" }

import ClientLogger from "@plane/logger/client"
const logger = ClientLogger.getLogger(loggerOptions);
logger.log("test logs on web")


import ServerLogger from "@plane/logger/server"
const logger = ServerLogger.getLogger(loggerOptions);
logger.log("test logs on server")

logger.info("This is an info log");
logger.warn("This is a warning");
logger.error("This is an error");
```

## Available Log Levels
- `error`
- `warn`
- `info` (default)
- `http`
- `verbose`
- `debug`
- `silly`

## Configuration
- By default, the log level is set to `info`.
- You can specify a log level during the first import of logger by passing optional logLevel param in getLogger function.
sriramveeraghanta marked this conversation as resolved.
Show resolved Hide resolved
19 changes: 19 additions & 0 deletions packages/logger/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface ILoggerOptions {
logLevel?: string,
logFilePrefix?: string
}
export interface ILogger {
private instance?: any;
private logger?: WinstonLogger; // The Winston logger instance
sriramveeraghanta marked this conversation as resolved.
Show resolved Hide resolved
sriramveeraghanta marked this conversation as resolved.
Show resolved Hide resolved
logLevel?: string; // The current logging level
logFilePrefix?: string;
// Method to get the logger instance
getLogger(loggerOptions: ILoggerOptions): WinstonLogger;
}

export interface IClientLogMethods {
error: (message: string, ...metadata: any[]) => void;
warn: (message: string, ...metadata: any[]) => void;
info: (message: string, ...metadata: any[]) => void;
debug: (message: string, ...metadata: any[]) => void;
}
39 changes: 39 additions & 0 deletions packages/logger/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@plane/logger",
"version": "1.0.0",
"description": "Logger shared across multiple apps internally",
"private": true,
"main": "./dist/server-logger.js",
"browser": "./dist/client-logger.js",
"type":"module",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist/**"
],
"exports": {
".": {
"require": "./dist/server-logger.js",
"import": "./dist/index.js",
"browser": "./dist/client-logger.js"
},
"./client": "./dist/client-logger.js",
"./server": "./dist/server-logger.js"
},
"scripts": {
"build": "tsup ./src/index.ts --format esm,cjs --dts --external react --minify",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

donot create a build for this package. You can directly import them and use it. Let the application that is using it transpile these packages.

"lint": "eslint src --ext .ts,.tsx",
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@plane/eslint-config": "*",
"@types/node": "^22.5.4",
"@types/react": "^18.3.11",
"tsup": "^7.2.0",
"typescript": "^5.3.3"
}
}
55 changes: 55 additions & 0 deletions packages/logger/src/client-logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { IClientLogMethods, ILoggerOptions } from "index";

export default class ClientLogger {
private logLevel: string;
private logLevels: string[];
logMethods: IClientLogMethods;
static instance: any;

constructor(loggerOptions: ILoggerOptions = { logLevel: "info", logFilePrefix: "log" }) {
this.logLevel = loggerOptions.logLevel || 'info';
this.logMethods = {
error: this.logWithLevel.bind(this, "error"),
warn: this.logWithLevel.bind(this, "warn"),
info: this.logWithLevel.bind(this, "info"),
debug: this.logWithLevel.bind(this, "debug"),
};
this.logLevels = ["error", "warn", "info", "debug"];
}

static getInstance(loggerOptions?: ILoggerOptions) {
if (!ClientLogger.instance) {
ClientLogger.instance = new ClientLogger(loggerOptions);
}
return ClientLogger.instance;
}

public static getLogger(loggerOptions?: ILoggerOptions): IClientLogMethods {
const instance = this.getInstance(loggerOptions);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use class name instead of 'this' in static method

In static methods, using this can be confusing because it refers to the class itself. Replace this with the class name ClientLogger for clarity.

Apply this diff:

 public static getLogger(loggerOptions?: ILoggerOptions): IClientLogMethods {
-    const instance = this.getInstance(loggerOptions);
+    const instance = ClientLogger.getInstance(loggerOptions);
     return instance.logMethods;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const instance = this.getInstance(loggerOptions);
const instance = ClientLogger.getInstance(loggerOptions);
🧰 Tools
🪛 Biome (1.9.4)

[error] 28-28: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

return instance.logMethods;
}

logWithLevel(level: string, message: string, ...metadata: any[]) {
if (this.logLevels.indexOf(level) <= this.logLevels.indexOf(this.logLevel)) {
const timestamp = new Date().toISOString();
const formattedMessage = `[${timestamp}] "${level.toUpperCase()}" ${message}`;
const metaString = metadata.length ? ` ${JSON.stringify(metadata)}` : "";

// Override to console.log equivalent
switch (level) {
case "error":
console.error(formattedMessage + metaString);
break;
case "warn":
console.warn(formattedMessage + metaString);
break;
case "info":
console.info(formattedMessage + metaString);
break;
case "debug":
console.log(formattedMessage + metaString);
break;
}
}
}
}
5 changes: 5 additions & 0 deletions packages/logger/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// src/index.ts
// export { default as Logger} from './server-logger'
// export { default as ClientLogger } from './client-logger'

export {default} from './logger';
16 changes: 16 additions & 0 deletions packages/logger/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ILogger } from 'index';

let Logger: ILogger;

if (typeof window !== "undefined") {
// Client-side logic
console.log("inside client logger import");
const { default: ClientLogger } = require('./client-logger');
Logger = ClientLogger;
} else {
// Server-side logic
const { default: ServerLogger } = require('./server-logger');
Logger = ServerLogger;
}

export default Logger;
88 changes: 88 additions & 0 deletions packages/logger/src/server-logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { createLogger, format, LoggerOptions, transports, Logger as WinstonLogger } from "winston";
import winstonRotate from "winston-daily-rotate-file";
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import fs from 'fs';
import { ILoggerOptions } from "index";

// Get current directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Set up log directory
const logDirectory = `${__dirname}/logs`;

if (!fs.existsSync(logDirectory)) {
fs.mkdirSync(logDirectory);
console.log('Logs folder created');
}


export default class Logger {
static instance?: Logger;
private logger: WinstonLogger;
private logLevel?: string;
private logFilePrefix?: string;

private constructor(loggerOptions: ILoggerOptions = { logLevel: "info", logFilePrefix: "plane-log" }) {
this.logLevel = loggerOptions.logLevel;
this.logFilePrefix = loggerOptions.logFilePrefix;

this.logger = createLogger({
level: this.logLevel,
format: format.combine(
format.colorize(),
format.timestamp({
format: "DD/MMM/YYYY HH:mm:ss",
}),
format.printf(({ timestamp, level, message, ...metadata }) => {
const msg = `[${timestamp}] "${level}" ${message}`;
const metaString = Object.keys(metadata).length ? ` ${JSON.stringify(metadata)}` : "";
return msg + metaString;
})
),
transports: [
new winstonRotate({
filename: `${logDirectory}/${this.logFilePrefix}-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
maxSize: '20m', // Optional: maximum size per log file
maxFiles: '7d', // Keep logs for 7 days
zippedArchive: true, // Optional: compress archived logs
}),
],
});

this.logger.transports.forEach((transport) => {
transport.on("error", (err) => {
// Handle the error, log it, or notify as necessary
const transportType: string = this.getTransportType(transport);
console.error(`Logging transport error: ${transportType}`, err);
});
});

}

private getTransportType(transport: any): string {
if (transport instanceof transports.Console) {
return "Console";
} else if (transport instanceof winstonRotate) {
return "File (Rotation)";
} else {
return "Unknown";
}
}

private static getInstance(loggerOptions?: ILoggerOptions) {
if (!Logger.instance) {
Logger.instance = new Logger(loggerOptions);
}
return Logger.instance;
}

public static getLogger(loggerOptions?: ILoggerOptions) {
const instance = Logger.getInstance(loggerOptions);
return instance.logger
}

}

19 changes: 19 additions & 0 deletions packages/logger/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"extends": "@plane/typescript-config/base.json",
"compilerOptions": {
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"outDir": "./dist",
"rootDir": "./src",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"experimentalDecorators": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Loading
Loading