Skip to content

Commit

Permalink
Add support for generic transports
Browse files Browse the repository at this point in the history
  • Loading branch information
Hexagon committed Mar 18, 2024
1 parent 756d506 commit 719cb5b
Show file tree
Hide file tree
Showing 5 changed files with 336 additions and 155 deletions.
79 changes: 48 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
**@cross/log - Cross-Runtime Logging for Node.js, Deno, and Bun**

**This is work in progress**
**@cross/log - Generic logger for Node.js, Deno, and Bun**

**Installation**

Expand All @@ -10,7 +8,7 @@ version:

```bash
# Deno
deno add https://jsr.io/@cross/log
deno add @cross/log

# Node.js
npx jsr add @cross/log
Expand All @@ -19,49 +17,68 @@ npx jsr add @cross/log
bunx jsr add @cross/log
```

**Getting Started**

**Simplest Use Case**

```javascript
import { Log } from "@cross/log";

const logger = new Log(); // Uses ConsoleLogger with default settings by default

logger.info("Hello log!");
```

**Key Features**

- **Flexible Log Levels:** Use standard log levels (Debug, Info, Warn, Error)
for easy categorization.
- **Customizable Output:** Format log messages for both console and files, with
support for plain text or JSON formatting.
- **Console Styling:** Apply colors and styles (bold, dim, etc.) in the console,
enhancing log readability.
- **Cross-Runtime:** Works seamlessly in Node.js, Deno, and Bun environments.
- **Global Filtering:** Set a minimum log level to control which messages are
processed.
- **Customizable Output:** Choose between plain text and JSON formatting for
both console and file logging.
- **Console Styling:** Enhance readability with colors, bold, dim, and other
styles in console output.
- **Cross-Runtime Compatibility** Works seamlessly across Node.js, Deno, and Bun
environments.
- **Modular Transports:** Easily create custom transports to send logs to
databases, network services, or specialized systems.
- **Global and Transport Filtering:** Set log levels globally or configure
fine-grained filtering per transport.

**Core Classes**

- **Log**
- **Log:** Central class for managing log transports and dispatching log
messages.
- `debug(message: string)`
- `info(message: string)`
- `warn(message: string)`
- `error(message: string)`

**Example Usage**
**Example Usage (with Custom Transports)**

```javascript
import { Log, LogLevel } from "@cross/log";

// Create a logger with file output and console styling
const logger = new Log({
file: {
enabled: true,
stdoutPath: "./app.log",
stderrPath: "./errors.log", // Optional, will by default write errors to stdoutPath
},
logLevel: LogLevel.Debug, // Enable all levels
});

logger.debug("Initializing application...");
logger.info("Database connection established");
logger.warn("Received a potentially invalid request");
logger.error("Critical failure - shutting down!");
import { ConsoleLogger, FileLogger, Log, LogLevel } from "./mod.ts";

const myLogger = new Log([
new ConsoleLogger({
logLevel: LogLevel.Debug, // Include debug in console output
}),
new FileLogger({
filePath: "./app.log",
fileFormat: "txt",
}),
]);

myLogger.debug("Initializing application...");
myLogger.warn("Received a potentially invalid request");
try {
// ... code that might fail
} catch (error) {
myLogger.error("Critical failure!", error);
}
```

**Contributions and Feedback**

The `@cross/log` project is open to contributions and feedback. Feel free to
submit issues, feature requests, or pull requests on the GitHub repository:
We welcome your contributions to improve `@cross/log`! Submit issues, feature
requests, or pull requests on the GitHub repository:
[https://github.com/cross-org/log](https://github.com/cross-org/log).
6 changes: 4 additions & 2 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
{
"name": "@cross/log",
"version": "0.9.1",
"version": "0.10.0",
"exports": {
".": "./mod.ts"
".": "./mod.ts",
"./console": "./transports/console.ts",
"./file": "./transports/file.ts"
},
"imports": {
"@cross/deepmerge": "jsr:@cross/deepmerge@^0.2.1",
Expand Down
223 changes: 101 additions & 122 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import { appendFile } from "node:fs/promises";
import { Colors } from "@cross/utils";
import { deepMerge } from "@cross/deepmerge";
import { ConsoleLogger } from "./transports/console.ts";

export type LoggerFileFormat = "txt" | "json";

export interface LoggerOptions {
file?: {
enabled: boolean;
stdoutPath?: string;
stderrPath?: string;
format?: LoggerFileFormat;
};
console?: {
enabled: boolean;
};
/**
* Base options for Log Transports.
*/
export interface LogTransportBaseOptions {
logLevel?: LogLevel;
}

/**
* Enumeration of available log levels.
*/
export enum LogLevel {
Debug = "DEBUG",
Info = "INFO",
Expand All @@ -25,132 +18,118 @@ export enum LogLevel {
Error = "ERROR",
}

const numericLogLevel = new Map([
/**
* Interface for defining the core functionality of a Log Transport.
*/
export interface LogTransport {
/**
* Logs a message with the specified severity, scope, data, and timestamp.
*
* @param severity - The severity level of the message.
* @param scope - An optional scope to categorize or group the log message.
* @param data - An array of data to be logged.
* @param timestamp - The timestamp of the log entry.
*/
log(
severity: LogLevel,
scope: string,
data: unknown[],
timestamp: Date,
): void;
}

/**
* Map for convenient comparison of log levels by numeric value.
*/
export const NumericLogLevel: Map<string, number> = new Map([
["DEBUG", 100],
["INFO", 200],
["LOG", 300],
["WARN", 400],
["ERROR", 500],
]);

/**
* Main Log class for managing and dispatching log messages to transports.
*/
export class Log {
private options: LoggerOptions;

constructor(options?: LoggerOptions) {
const defaults: LoggerOptions = {
file: {
enabled: false,
stdoutPath: "./app.log",
stderrPath: "./app.log",
format: "txt",
},
console: { enabled: true },
logLevel: LogLevel.Info,
};
this.options = deepMerge(defaults, options)!;
}

debug(message: string) {
this.doLog(LogLevel.Debug, message);
}

info(message: string) {
this.doLog(LogLevel.Info, message);
}

log(message: string) {
this.doLog(LogLevel.Log, message);
}

warn(message: string) {
this.doLog(LogLevel.Warn, message);
/**
* Array of LogTransport objects responsible for handling log messages.
*/
transports: LogTransport[] = [];

constructor(transports?: LogTransport[]) {
if (!transports) {
this.transports.push(new ConsoleLogger()); // Default
} else {
this.transports = transports;
}
}

error(message: string) {
this.doLog(LogLevel.Error, message);
/**
* Logs a message with Debug severity.
* @param data - Data to be logged.
*/
debug(...data: unknown[]) {
this.forward(LogLevel.Debug, "default", data);
}

private doLog(level: LogLevel, message: string) {
if (this.shouldLog(level)) { // Introduce filtering check
const timestamp = new Date();
if (this.options.file?.enabled) {
if (this.options.file?.stderrPath && level === LogLevel.Error) {
this.appendToFile(timestamp, level, message, true);
} else if (this.options.file?.stdoutPath) {
this.appendToFile(timestamp, level, message, false);
}
}
if (this.options.console?.enabled) {
this.writeToConsole(timestamp, message, level);
}
}
/**
* Logs a message with Info severity.
* @param data - Data to be logged.
*/
info(...data: unknown[]) {
this.forward(LogLevel.Info, "default", data);
}

private appendToFile(
timestamp: Date,
level: LogLevel,
message: string,
stderr: boolean,
) {
const timestampText = timestamp.toISOString();
const formattedMessage = this.formatFileMessage(
timestampText,
level,
message,
);
const filePath = stderr
? this.options.file!.stderrPath
: this.options.file!.stdoutPath;
appendFile(filePath as string, formattedMessage)
.catch((err) => console.error(`Error writing to log file:`, err));
/**
* Logs a message with Log severity.
* @param data - Data to be logged.
*/
log(...data: unknown[]) {
this.forward(LogLevel.Log, "default", data);
}

private formatFileMessage(
timestamp: string,
level: LogLevel,
message: string,
): string {
switch (this.options.file?.format) {
case "json":
return JSON.stringify({ timestamp, level, message }) + "\n";
default:
return `[${timestamp}] [${level}] ${message}\n`; // Default remains the same
}
/**
* Logs a message with Warn severity.
* @param data - Data to be logged.
*/
warn(...data: unknown[]) {
this.forward(LogLevel.Warn, "default", data);
}

private writeToConsole(timestamp: Date, message: string, level: LogLevel) {
const timestampText = Colors.dim(timestamp.toISOString());

let styledLevel = level.toString().padEnd(5, " ");

switch (level) {
case LogLevel.Debug:
styledLevel = Colors.dim(styledLevel);
message = Colors.dim(message);
break;
case LogLevel.Info:
styledLevel = Colors.blue(styledLevel);
break;
case LogLevel.Warn:
styledLevel = Colors.yellow(styledLevel);
break;
case LogLevel.Error:
styledLevel = Colors.red(styledLevel);
message = Colors.red(message);
break;
}

const formattedMessage = `${timestampText} ${styledLevel} ${message}`;

if (level === LogLevel.Error) {
console.error(formattedMessage);
} else {
console.log(formattedMessage);
}
/**
* Logs a message with Error severity.
* @param data - Data to be logged.
*/
error(...data: unknown[]) {
this.forward(LogLevel.Error, "default", data);
}

private shouldLog(level: LogLevel): boolean {
const currentLogLevel = this.options.logLevel ?? LogLevel.Debug;
return numericLogLevel.get(level)! >= numericLogLevel.get(currentLogLevel)!;
/**
* Forwards the log message to all registered transports.
* @param severity - The severity of the message.
* @param scope - The scope of the message.
* @param data - Data to be logged.
*/
private forward(severity: LogLevel, scope: string, data: unknown[]) {
const timestamp = new Date();
this.transports.forEach((transport) => {
transport.log(severity, scope, data, timestamp);
});
}
}

/**
* Re-export of common transports
*
* All transports is available by using
*
* ```
* import { FileLogger } from "@cross/log/file";
* import { ConsoleLogger } from "@cross/log/console";
* // ... etc
* ```
*/
export { ConsoleLogger } from "./transports/console.ts";
export { FileLogger } from "./transports/file.ts";
Loading

0 comments on commit 719cb5b

Please sign in to comment.