@egi/smart-log
v1.1.0
Published
Smart logging library with severity levels, flexible formatting, and optional DB persistence
Maintainers
Readme
smart-log
A TypeScript logging library with severity levels, flexible formatting, stack trace integration, and optional persistence via a pluggable db writer.
Installation
npm install @egi/smart-logQuick Start
import {smartLog, SmartSeverityLevel} from "@egi/smart-log";
smartLog.info("Application started");
smartLog.warning("Low disk space");
smartLog.error(new Error("Something went wrong"));A singleton instance smartLog is exported for convenience. Create additional instances as needed:
import {SmartLog} from "@egi/smart-log";
const log = new SmartLog({logLevel: SmartSeverityLevel.Debug});
log.debug("detailed trace");Severity Levels
enum SmartSeverityLevel {
None = 0, Fatal = 1, Error = 2, Warning = 3,
Info = 4, Verbose = 5, Debug = 6, All = 9
}Methods: fatal(), error(), warning(), info(), verbose(), debug(), write() (no severity).
Each method accepts a string, an Error, or a string + SmartMessageOptions:
log.error("fetch failed", {data: err, location: "api.ts:42"});
log.info("user logged in", {type: "BACKEND", timestamp: new Date()});
log.debug("internal detail", {skipConsole: true}); // db writer only
log.info("console only", {skipDatabase: true}); // console only
log.info("request", {user: 42}); // per-entry user
log.info("anonymous", {user: null}); // suppress default userOptions
const log = new SmartLog({
logLevel: SmartSeverityLevel.Info, // minimum level to process
logChannels: true, // route to console.error/warn/info/debug by level
location: true, // include caller location in output
leading: "[APP]", // prefix every line
separator: " | ", // separator between format elements
lowerCase: false, // lowercase severity codes
user: "admin", // user id stamped on each log entry
dbWriter: myWriter, // persist entries (see below)
entryFormatter: (msg, fallback) => fallback(), // raw-entry formatter
outputHook: (text, msg) => {}, // side-effect hook; return false to skip console
formatHook: (text, msg) => text, // transform output text before console
stackProvider: () => new Error(), // custom stack source for caller detection
});SmartMessageOptions.user overrides the logger-wide user for one entry without mutating shared state. This is the appropriate form for concurrent request handling; null deliberately omits the user from that entry.
Log Format
The output format is fully configurable via sequence. Elements can be enum values, fixed strings, or functions:
import {SmartLog, SmartLogFormatElement} from "@egi/smart-log";
const log = new SmartLog({
leading: "%",
lowerCase: true,
location: true,
sequence: [
SmartLogFormatElement.Severity,
"myapp",
SmartLogFormatElement.Timestamp,
SmartLogFormatElement.Message,
SmartLogFormatElement.Location,
() => process.pid.toString()
]
});| Element | Output |
|-------------|-----------------------------------------------|
| Severity | F fatal, E error, W warning, I info/verbose, D debug |
| Type | First letter of the location type (App → A) |
| Timestamp | 2024-03-15 14:30:00.000 |
| Message | The log message text |
| Location | [file.ts:42 (functionName)] |
Default sequence: Severity - Type - Timestamp: Message [Location]
Output Hook
outputHook is called after formatting and before console output. Use it for side effects — writing to a file, sending to a remote service, emitting events — without needing to implement a full ISmartLogDbWriter.
Return false to prevent the entry from appearing on the console. Both sync and async hooks are supported.
import * as fs from "node:fs";
import {SmartLog, LogMessage} from "@egi/smart-log";
const log = new SmartLog({
outputHook: (text: string, message: LogMessage) => {
fs.appendFileSync("app.log", text + "\n");
// return false to hide from console as well
}
});
// or set it later:
log.setOutputHook(async (text, message) => {
await sendToSlack(text);
return false; // already sent, no need to also print
});
// transform text only:
log.setFormatHook((text) => `[MyApp] ${text}`);formatHook and outputHook serve different purposes:
| Hook | Purpose | Must return |
|--------------|---------------------------------|-------------------|
| formatHook | Transform the formatted text | Modified string |
| outputHook | Side effects / suppress console | void or false |
Raw Entry Formatting
Use entryFormatter when formatting depends on the complete raw entry, or when attached data must not pass through SmartLog's default stringification. The second argument invokes the original formatter lazily, so the same extension point supports
both replacement and decoration:
import {SmartLog, smartSeverityNameFromLevel, toIsoTimestamp} from "@egi/smart-log";
const log = new SmartLog({
entryFormatter: (message, defaultFormat) => {
if (process.env.LOG_FORMAT === "JSON") {
return JSON.stringify({
timestamp: toIsoTimestamp(message.timestamp),
severity: smartSeverityNameFromLevel(message.severityLevel),
message: message.message,
user: message.user
});
}
return `[service] ${defaultFormat()}`;
}
});An entryFormatter runs before default data stringification. The existing
formatHook, when configured, still runs afterwards. Passing an explicit sequence to formatMessage() continues to bypass hooks, preserving its existing deterministic formatting behavior. The formatter can also be changed or cleared with
setEntryFormatter().
Reusable helpers exported by the package include:
smartSeverityNameFromLevel()andSmartLog.severityNameFromLevel();toIsoTimestamp()for UTC ISO-8601 timestamps;toSingleLogLine(),truncateUtf8(), andlimitLogLine()for bounded, UTF-8-safe physical log records.
Database Persistence
Implement ISmartLogDbWriter to persist log entries:
import {ISmartLogDbWriter, LogMessage} from "@egi/smart-log";
class MyDbWriter implements ISmartLogDbWriter {
write(entry: LogMessage): Promise<void> | void {
// entry contains: severityLevel, type, location, message, data, timestamp, user
return db.insert("logs", entry);
}
}
const log = new SmartLog({dbWriter: new MyDbWriter()});
// change or disable the writer at runtime:
log.setDbWriter(null);The writer receives the raw LogMessage object — sync or async writers are both supported.
Location Types
enum SmartLocation {
App = "APP",
Frontend = "FRONTEND",
Backend = "BACKEND",
Database = "DATABASE",
UpgradeManager = "UPGRADE-MANAGER"
}Date Utilities
import {toSmartDbDate, toSmartDbTimestamp, smartDbToDate, smartDbToDateTime} from "@egi/smart-log";
toSmartDbDate(new Date()) // "2024-03-15 14:30:00"
toSmartDbTimestamp(new Date()) // "2024-03-15 14:30:00.000"
smartDbToDate("2024-03-15 14:30:00") // Date object
smartDbToDateTime(1710508200000) // Luxon DateTime objectSmartError
A structured error class for use throughout the stack:
import {SmartError, SmartSeverityLevel, SmartLocation} from "@egi/smart-log";
throw new SmartError({
message: "Record not found",
code: 404,
type: SmartSeverityLevel.Error,
location: SmartLocation.Backend
});SmartTools
Utility singleton for string conversions and stack inspection:
import {tools} from "@egi/smart-log";
tools.camelCase("my_field_name") // "myFieldName"
tools.snakeCase("myFieldName") // "my_field_name"
tools.kebabCase("MyComponent") // "my-component"
tools.pascalCase("my_model") // "MyModel"
tools.getCallerFromStack() // "src/app.ts:42 (doSomething)"