@significo/pulse
v1.0.0
Published
TypeScript-first browser logger with source-location preservation and pluggable transports
Downloads
245
Readme
@significo/pulse
TypeScript-first browser logger built around two core ideas:
- Logs appear at the caller's source location in DevTools (via
console.bind()+ double invocation) - A transport system that routes structured log entries to any destination
Installation
yarn add @significo/pulseQuick Start
import { LoggerFactory, ConsoleTransport, LogLevel } from '@significo/pulse';
const factory = new LoggerFactory({
level: LogLevel.Info,
transports: [new ConsoleTransport()],
meta: { app: 'my-app' },
});
const logger = factory.getLogger('auth');
logger.info('User logged in', { userId: '123' })();
logger.warn('Session expiring soon')();
logger.error('Login failed', new Error('bad credentials'))();Why the double ()?
Each log method returns a bound console function. When you invoke it with (), the actual console.* call happens at your call site, so DevTools shows your file and line number -- not Pulse internals. This is the same technique used by lines-logger.
Transports that don't need deferred execution (like MemoryTransport) process the entry immediately when the log method is called -- the () only triggers console output.
API Reference
LogLevel
enum LogLevel {
Trace = 0,
Debug = 1,
Info = 2,
Warn = 3,
Error = 4,
Off = 5,
}LogEntry
Structured object passed to every transport:
interface LogEntry {
level: LogLevel;
tag: string;
message: string;
args: unknown[];
timestamp: Date;
meta?: Record<string, unknown>;
}Transport
Interface for log destinations:
interface Transport {
handle(entry: LogEntry): void | (() => void);
}Return void to process immediately. Return () => void to defer execution to the caller's stack frame (preserves source location in DevTools).
If a transport throws, it is silently caught -- other transports still receive the entry.
AsyncTransport
Extends Transport with a flush() method for batching/draining:
interface AsyncTransport {
handle(entry: LogEntry): void | (() => void);
flush(): Promise<void>;
}Call factory.flush() to drain all async transports (e.g. before page unload).
ConsoleTransport
Default transport. Uses console[method].bind() and returns the bound function for deferred execution, so DevTools shows the caller's file and line.
const transport = new ConsoleTransport();
// Or with a fixed color for all tags (must be a hex color):
const transport = new ConsoleTransport({ defaultColor: '#e91e63' });Tags get deterministic colors via a cyrb53 hash, rendered as colored pill badges in the console. Colors are dark-range so white text is always readable.
MemoryTransport
Stores log entries in memory. Useful for testing.
const mem = new MemoryTransport();
const mem = new MemoryTransport({ maxEntries: 100 }); // ring buffer
// After logging...
mem.entries; // readonly LogEntry[] (defensive copy)
mem.last(); // LogEntry | undefined
mem.clear(); // voidLoggerFactory
Creates and manages logger instances.
const factory = new LoggerFactory({
level: LogLevel.Info, // global minimum level
transports: [new ConsoleTransport()],
meta: { app: 'my-app' }, // global metadata on every entry
});
const logger = factory.getLogger('payments');
const verbose = factory.getLogger('debug-mod', { level: LogLevel.Debug }); // per-logger levelMethods
getLogger(tag, options?)-- Returns a logger. Same tag returns the same cached instance (options from the first call win; subsequent calls with different options for the same tag are ignored). Options:{ level?, meta? }.setLevel(level)-- Changes the global log level at runtime.getLevel()-- Returns the current global log level.setMeta(meta)-- Replaces global metadata entirely.mergeMeta(meta)-- Merges keys into global metadata (overwrites existing keys).addTransport(transport)-- Adds a transport at runtime.removeTransport(transport)-- Removes a transport. Returnstrueif found.flush()-- Callsflush()on allAsyncTransportinstances. Returns aPromise.
Logger Methods
Each logger exposes trace, debug, info, warn, error, and child. Log methods return () => void:
logger.info('message', ...args)();The first call builds the log entry and dispatches to transports. The () invokes any deferred console output at the caller's stack frame.
Child Loggers
Create a child logger that inherits the parent's tag and transports but extends its metadata:
const logger = factory.getLogger('http', { meta: { service: 'api' } });
const reqLogger = logger.child({ requestId: 'abc-123' });
reqLogger.info('Request received')();
// entry.meta = { service: 'api', requestId: 'abc-123' }Children can be nested. Child metadata overrides parent metadata on key conflicts.
Writing a Custom Transport
Implement the Transport interface:
import type { Transport, LogEntry } from '@significo/pulse';
class RemoteTransport implements Transport {
constructor(private url: string) {}
// Returns void -- processes immediately, no deferred execution needed
handle(entry: LogEntry): void {
fetch(this.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
level: entry.level,
tag: entry.tag,
message: entry.message,
args: entry.args,
timestamp: entry.timestamp.toISOString(),
meta: entry.meta,
}),
}).catch(() => {
// Swallow errors to avoid infinite loops
});
}
}For transports that batch and flush, implement AsyncTransport:
import type { AsyncTransport, LogEntry } from '@significo/pulse';
class BatchTransport implements AsyncTransport {
private buffer: LogEntry[] = [];
handle(entry: LogEntry): void {
this.buffer.push(entry);
}
async flush(): Promise<void> {
const batch = this.buffer.splice(0);
if (batch.length > 0) {
await fetch('/api/logs', {
method: 'POST',
body: JSON.stringify(batch),
});
}
}
}Then register and flush before unload:
const batch = new BatchTransport();
const factory = new LoggerFactory({ transports: [new ConsoleTransport(), batch] });
window.addEventListener('beforeunload', () => { factory.flush(); });Development
yarn test # Run tests
yarn test:watch # Run tests in watch mode
yarn test:e2e # Run e2e browser tests
yarn coverage # Run tests with 100% coverage gate
yarn typecheck # Type check src + test
yarn lint # ESLint
yarn build # Build ESM + CJS + .d.tsLicense
Apache 2.0 -- see LICENSE for details.
