Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Goal: universal logging library (Browser + Node.js) competitive with Pino.js, wi
- [x] `_isSilent` / `_hasTransports` / `_bufferEnabled` cached flags
- [x] **Pino-style method replacement** — `.info()` etc. replaced with `noop` when level is above threshold or logger is silent; `setLevel()` / `addTransport()` re-bind all 7 methods
- [x] Target achieved: 7M ops/sec silent, 13.7M child-no-buffer (was 3.1M / 5.1M)
- [ ] Make `messages: args` storage opt-in or remove — costs ~20-30 bytes per entry in buffer mode
- [x] Make `messages: args` storage opt-in — `keepMessages: false` by default; saves ~20-30 bytes per entry plus the args-array alloc in buffer mode and the per-entry `JSON.stringify` map in worker mode. `BrowserFormatter` falls back to `entry.fields` for expandable DevTools output

- [ ] **OpenTelemetry transport (OTLP/HTTP)**
- [ ] `OtlpTransport` that speaks OTLP/HTTP JSON protocol (no gRPC dependency)
Expand Down
10 changes: 7 additions & 3 deletions docs/api/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Represents a single log entry stored in the circular buffer.
```typescript
type LogEntry = {
msg: string; // primary message string
messages: unknown[]; // original arguments (kept for compatibility)
messages?: unknown[]; // original arguments — only when keepMessages: true
fields: Record<string, unknown>; // structured key-value pairs (includes bindings)
timestamp: Date;
hrTime?: number; // high-res nanosecond offset (when highResolution: true)
Expand All @@ -30,7 +30,7 @@ type LogEntry = {
| Property | Type | Description |
|----------|------|-------------|
| `msg` | `string` | The primary log message |
| `messages` | `unknown[]` | Original arguments passed to the log method |
| `messages` | `unknown[] \| undefined` | Original arguments — populated only when the logger sets `keepMessages: true` (off by default to save per-entry footprint) |
| `fields` | `Record<string, unknown>` | Structured key-value pairs (child bindings merged with call-site fields) |
| `timestamp` | `Date` | When the log was created |
| `hrTime` | `number \| undefined` | High-resolution monotonic timestamp in nanoseconds (present when `highResolution: true`) |
Expand All @@ -49,13 +49,15 @@ logger.error('Something failed', { code: 500, path: '/users' });
const [entry] = logger.getLogs();
// {
// msg: 'Something failed',
// messages: ['Something failed', { code: 500, path: '/users' }],
// fields: { code: 500, path: '/users' },
// timestamp: Date,
// namespace: 'App',
// level: 'error',
// levelValue: 50,
// }
//
// Pass `keepMessages: true` to additionally preserve the original args as
// `entry.messages: ['Something failed', { code: 500, path: '/users' }]`.
```

---
Expand Down Expand Up @@ -120,6 +122,7 @@ interface KonsoleOptions {
retentionPeriod?: number;
cleanupInterval?: number;
useWorker?: boolean;
keepMessages?: boolean;
criteria?: Criteria; // @deprecated
}
```
Expand All @@ -139,6 +142,7 @@ interface KonsoleOptions {
| `retentionPeriod` | `number` | `172800000` | 48 hours in ms |
| `cleanupInterval` | `number` | `3600000` | 1 hour in ms |
| `useWorker` | `boolean` | `false` | Worker mode (Web Worker in browser, `worker_threads` in Node.js) |
| `keepMessages` | `boolean` | `false` | Preserve the original `args` array on each entry as `messages`. Off by default to save per-entry memory; turn on when reading `entry.messages` from `getLogs()` consumers, or to surface every object arg as expandable in `BrowserFormatter` |
| `criteria` | `Criteria` | `true` | Output filter *(deprecated — use `level` and `format`)* |

---
Expand Down
15 changes: 15 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [5.2.0]

### Added

- **`keepMessages` option** — opt-in storage of the original log arguments on each `LogEntry`
- New `KonsoleOptions.keepMessages` (default `false`). Off by default to save ~20–30 bytes per entry plus the args-array allocation in buffer mode and the per-entry `JSON.stringify` map in worker mode
- Children inherit the parent's setting
- Turn on when reading `entry.messages` from `getLogs()` / `viewLogs()` consumers, or to surface every object argument as expandable in `BrowserFormatter`

### Changed

- `LogEntry.messages` and `SerializableLogEntry.messages` are now optional (`unknown[] | undefined`). Previously every entry carried a copy of the args array
- `BrowserFormatter` falls back to `entry.fields` for the expandable DevTools value when `messages` is absent — covers the common `info('msg', { fields })` and object-first calling patterns. Set `keepMessages: true` to restore expansion of every raw object argument

### Migration

- If you read `entry.messages` from `getLogs()` / `viewLogs()` or a custom transport, either pass `keepMessages: true` on the logger or migrate to `entry.msg` + `entry.fields`, which together capture the same information for the supported calling conventions
### Performance

- **`StreamTransport` / `FileTransport` async throughput** — ~20× faster flushing a backlog to a buffered stream (~40K → ~790K lines/sec to `/dev/null`)
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/conditional-logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ Every log entry forwarded to transport filters, `criteria` functions, and `getLo
```typescript
type LogEntry = {
msg: string; // primary message
messages: unknown[]; // original arguments
messages?: unknown[]; // original arguments — only when keepMessages: true
fields: Record<string, unknown>; // structured key-value pairs (includes bindings)
timestamp: Date;
hrTime?: number; // high-res nanosecond offset (when highResolution: true)
Expand Down
11 changes: 9 additions & 2 deletions src/Konsole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export class Konsole implements KonsolePublic {
private cleanupIntervalId?: ReturnType<typeof setInterval>;
private useWorker: boolean;
private transports: Transport[] = [];
/** When true, the original args array is preserved on each entry as `messages`. */
private _keepMessages: boolean = false;

/** Pre-compiled redaction path segments. Empty array = no redaction. */
private _redactPaths: string[][] = [];
Expand Down Expand Up @@ -144,6 +146,7 @@ export class Konsole implements KonsolePublic {
maxLogs = 10000,
buffer,
useWorker = false,
keepMessages = false,
transports = [],
timestamp,
redact = [],
Expand All @@ -164,6 +167,7 @@ export class Konsole implements KonsolePublic {
this.retentionPeriod = retentionPeriod;
this.maxLogs = maxLogs;
this.useWorker = useWorker;
this._keepMessages = keepMessages;

// Buffer defaults: on in browser (for getLogs/viewLogs/exposeToWindow), off in Node.js
this._bufferEnabled = buffer ?? isBrowser;
Expand Down Expand Up @@ -415,6 +419,7 @@ export class Konsole implements KonsolePublic {
// ── Shared references (mutations in parent are visible in child and vice-versa) ──
child.logs = parent.logs; // same circular buffer
child.useWorker = parent.useWorker;
child._keepMessages = parent._keepMessages;

// ── Separate array, same Transport instances (child.addTransport won't affect parent) ──
child.transports = [...parent.transports];
Expand Down Expand Up @@ -813,14 +818,14 @@ export class Konsole implements KonsolePublic {

const rawEntry: LogEntry = {
msg,
messages: args,
fields: entryFields,
timestamp: new Date(),
hrTime: this.highResolution ? getHrTime() : undefined,
namespace: this.namespace,
level,
levelValue: LEVELS[level],
};
if (this._keepMessages) rawEntry.messages = args;

// Apply redaction before any consumer sees the entry.
// The disable flag is only settable via window.__Konsole (browser-only API),
Expand All @@ -838,14 +843,16 @@ export class Konsole implements KonsolePublic {
if (this.useWorker && Konsole.sharedWorker) {
const serializable: SerializableLogEntry = {
msg,
messages: args.map((m) => (typeof m === 'object' ? JSON.stringify(m) : m)),
fields: entry.fields,
timestamp: entry.timestamp.toISOString(),
hrTime: entry.hrTime,
namespace: this.namespace,
level,
levelValue: LEVELS[level],
};
if (this._keepMessages) {
serializable.messages = args.map((m) => (typeof m === 'object' ? JSON.stringify(m) : m));
}
Konsole.sharedWorker.postMessage({
type: 'ADD_LOG',
namespace: this.namespace,
Expand Down
33 changes: 33 additions & 0 deletions src/__tests__/Konsole.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,4 +796,37 @@ describe('Konsole', () => {
(Konsole as unknown as Record<string, unknown>)['_hooksRegistered'] = false;
});
});

describe('keepMessages', () => {
it('omits messages from buffered entries by default', () => {
const logger = makeSilentLogger({ namespace: 'NoMessages' });
logger.info('hello', { a: 1 });
const [entry] = logger.getLogs();
expect(entry.messages).toBeUndefined();
expect(entry.msg).toBe('hello');
expect(entry.fields).toEqual({ a: 1 });
});

it('preserves the original args when keepMessages: true', () => {
const logger = makeSilentLogger({ namespace: 'WithMessages', keepMessages: true });
logger.info('hello', { a: 1 });
const [entry] = logger.getLogs();
expect(entry.messages).toEqual(['hello', { a: 1 }]);
});

it('omits messages from transport entries by default', () => {
const spy = new SpyTransport();
const logger = makeSilentLogger({ namespace: 'NoMessagesT', transports: [spy] });
logger.info('hello', { a: 1 });
expect(spy.entries[0].messages).toBeUndefined();
});

it('child inherits keepMessages from parent', () => {
const parent = makeSilentLogger({ namespace: 'KMParent', keepMessages: true });
const child = parent.child({ requestId: 'r1' });
child.info('hello');
const [entry] = parent.getLogs();
expect(entry.messages).toEqual(['hello']);
});
});
});
18 changes: 14 additions & 4 deletions src/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,20 @@ export class BrowserFormatter implements Formatter {
const ts = formatTimestamp(entry.timestamp, this.tsFormat, entry.hrTime);
const fields = renderFields(entry.fields);

// Any object args passed directly (shown as expandable in DevTools)
const expandable = entry.messages.filter(
(m): m is object => typeof m === 'object' && m !== null,
);
// Any object args passed directly (shown as expandable in DevTools).
// When `messages` is preserved (keepMessages: true), surface every object
// arg. Otherwise fall back to the merged `fields` object — which covers
// the common `info('msg', { fields })` and object-first calling patterns.
let expandable: object[];
if (entry.messages) {
expandable = entry.messages.filter(
(m): m is object => typeof m === 'object' && m !== null,
);
} else if (entry.fields && Object.keys(entry.fields).length > 0) {
expandable = [entry.fields];
} else {
expandable = [];
}

let fmt = '';
const styles: string[] = [];
Expand Down
25 changes: 22 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,13 @@ export interface TimestampOptions {
export type LogEntry = {
/** Primary log message (extracted from the first string argument). */
msg: string;
/** All original arguments passed to the log method (kept for backward compatibility). */
messages: unknown[];
/**
* Original arguments passed to the log method.
* Only present when the logger was constructed with `keepMessages: true`.
* Skipped by default to reduce per-entry memory footprint — use `msg` and
* `fields` for structured access.
*/
messages?: unknown[];
/** Structured key-value fields merged from the call arguments. */
fields: Record<string, unknown>;
timestamp: Date;
Expand All @@ -69,7 +74,8 @@ export type LogEntry = {
*/
export type SerializableLogEntry = {
msg: string;
messages: unknown[];
/** Present only when the source logger has `keepMessages: true`. */
messages?: unknown[];
fields: Record<string, unknown>;
timestamp: string;
hrTime?: number;
Expand Down Expand Up @@ -233,6 +239,19 @@ export interface KonsoleOptions {
buffer?: boolean;
/** Offload log storage to a worker thread — Web Worker (browser) or worker_threads (Node.js) (default: false) */
useWorker?: boolean;
/**
* Preserve the original `args` array on every `LogEntry` as `messages`.
* Off by default to save ~20–30 bytes per entry (and a small allocation)
* when the logger has a circular buffer or worker.
*
* Enable when:
* - You read `entry.messages` from `getLogs()` / `viewLogs()` consumers.
* - You want `BrowserFormatter` to surface every object argument as an
* expandable DevTools value (otherwise only `entry.fields` is expanded).
*
* @default false
*/
keepMessages?: boolean;
/**
* Transports to forward log entries to external destinations.
* Accepts both `TransportConfig` plain objects (auto-wrapped in `HttpTransport`)
Expand Down
Loading