a small logging library for javascript.
your local package manager's method of installation.
it has a pretty name.
lily is simple, designed for my very simple mind and has, i think? everything a simpler project would need.
all the other logging libs seemed very old and complex to me (i just didn't want to read the docs actually but same thing).
import logger from "lily";
logger.info("hello world");
logger.error("something went wrong", { userId: 123 });
// create child loggers with scope
const dbLogger = logger.child("database");
dbLogger.info("connected to postgres");
const userLogger = dbLogger.child("users");
userLogger.debug("user query executed", { query: "SELECT * FROM users" });
transports are where your logs go. console, files, http endpoints, databases -
anywhere you want, lily supports a ConsoleTransport
and FileTransport
out of
the box, but you can create your own.
import { Logger, ConsoleTransport, FileTransport } from "lily";
const logger = new Logger("myapp");
logger.addTransport(new ConsoleTransport());
logger.addTransport(new FileTransport({ filename: "app.log" }));
// this goes to both console and file
logger.info("user logged in");
formatters control how your logs look. pretty colours for dev, & structured json for prod.
import { ConsoleFormatter, JsonFormatter } from "lily";
// pretty colours for humans
const prettyFormatter = new ConsoleFormatter({
colourize: true,
timeFormat: "locale",
});
// structured data for machines
const jsonFormatter = new JsonFormatter();
scopes give your logs context, they're like breadcrumbs through your application.
const apiLogger = logger.child("api");
const authLogger = apiLogger.child("auth");
// scope: [app/api/auth]
authLogger.info("login attempt", { email: "user@example.com" });
logger.trace("detailed debugging info");
logger.debug("debug information");
logger.info("general information");
logger.warn("warning messages");
logger.error("error messages");
logger.fatal("critical errors");
set log level globally:
import { Logger, LogLevel } from "lily";
Logger.setGlobalLevel(LogLevel.WARN); // only warnings and above
or via environment:
LOG_LEVEL=DEBUG node app.js
creating a custom transport is simple - just impl the Transport
interface and
wow your logs are going to space or idk wherever you want them to go.
import { Transport, LogEntry } from "lily";
class DiscordTransport implements Transport {
constructor(private webhookUrl: string) {}
async log(entry: LogEntry): Promise<void> {
if (entry.level < LogLevel.ERROR) return; // errors only
await fetch(this.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
content: `(${entry.scope.join("/")}) ${entry.message}`,
}),
});
}
}
class DiscordTransport implements Transport {
constructor(private webhookUrl: string) {}
async log(entry: LogEntry): Promise<void> {
if (entry.level < LogLevel.ERROR) return; // errors only
await fetch(this.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
content: `[${entry.scope.join("/")}] ${entry.message}`,
}),
});
}
}
logger.addTransport(
new DiscordTransport("https://discord.com/api/webhooks/..."),
);
formatters convert LogEntry
objects to strings:
import { LogEntry, LogLevel } from "lily";
class SimpleFormatter {
format(entry: LogEntry): string {
const level = LogLevel[entry.level];
const scope = entry.scope.length > 0 ? `[${entry.scope.join("/")}] ` : "";
return `${level}: ${scope}${entry.message}`;
}
}
// use it
const transport = new ConsoleTransport(new SimpleFormatter());
logger.addTransport(transport);
build your perfect logger:
import {
Logger,
LogLevel,
ConsoleTransport,
FileTransport,
ConsoleFormatter,
JsonFormatter,
} from "lily";
const logger = new Logger("myapp", {
level: LogLevel.DEBUG,
colourize: process.env.NODE_ENV !== "production",
});
// development: pretty console output
if (process.env.NODE_ENV === "development") {
logger.addTransport(
new ConsoleTransport(
new ConsoleFormatter({
colourize: true,
timeFormat: "time",
}),
),
);
}
// production: structured file logs
if (process.env.NODE_ENV === "production") {
logger.addTransport(
new FileTransport({
filename: "app.log",
formatter: new JsonFormatter(),
}),
);
}
export default logger;
LOG_LEVEL
- set log level (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)NO_COLOUR
- disable colours in outputNODE_ENV=test
- automatically disables coloursadd structured data to your logs:
// add metadata to individual logs
logger.info("user action", {
userId: 123,
action: "login",
ip: "192.168.1.1",
});
// create logger with persistent metadata
const requestLogger = logger.withMetadata({
requestId: "req-abc123",
userId: 456,
});
requestLogger.info("processing request"); // includes metadata
requestLogger.error("request failed"); // includes metadata
logger.trace(message: string, ...args: unknown[])
logger.debug(message: string, ...args: unknown[])
logger.info(message: string, ...args: unknown[])
logger.warn(message: string, ...args: unknown[])
logger.error(message: string, ...args: unknown[])
logger.fatal(message: string, ...args: unknown[])
logger.child(scope: string | string[], options?: LoggerOptions): Logger
logger.withMetadata(metadata: Record<string, unknown>): Logger
logger.addTransport(transport: Transport): void
logger.removeTransport(transport: Transport): void
logger.clearTransports(): void
Logger.setGlobalLevel(level: LogLevel): void
Logger.getGlobalLevel(): LogLevel
import { Logger, FileTransport, JsonFormatter } from "lily";
const logger = new Logger("app");
logger.addTransport(
new FileTransport({
filename: "application.log",
formatter: new JsonFormatter(),
}),
);
logger.info("application started");
import {
Logger,
ConsoleTransport,
FileTransport,
ConsoleFormatter,
JsonFormatter,
} from "lily";
const logger = new Logger("api");
// pretty console output
logger.addTransport(
new ConsoleTransport(new ConsoleFormatter({ colourize: true })),
);
// structured file output
logger.addTransport(
new FileTransport({
filename: "api.log",
formatter: new JsonFormatter(),
}),
);
logger.info("server starting on port 3000");
import { logger } from "./logger";
app.use((req, res, next) => {
const requestLogger = logger.child("http").withMetadata({
requestId: crypto.randomUUID(),
method: req.method,
url: req.url,
});
requestLogger.info("request started");
res.on("finish", () => {
requestLogger.info("request completed", {
statusCode: res.statusCode,
});
});
req.logger = requestLogger;
next();
});
gpl-3.0 - see [LICENSE.GPL-3.0] for details.