Skip to content

Commit bcd5729

Browse files
dahliaclaude
andcommitted
Add fingersCrossed() logging pattern
Implement the "fingers crossed" logging pattern that buffers debug logs in memory and only outputs them when a trigger event occurs. This feature helps reduce log noise while preserving debugging context when issues arise. Features: - Basic buffering with configurable trigger levels - Category isolation modes (descendant, ancestor, both, custom) - Buffer overflow protection with configurable max sizes - Comprehensive error handling and edge case coverage - Cross-runtime compatibility (Deno, Node.js, Bun) Includes complete documentation and 28 test cases covering all functionality and edge cases. Closes #59 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 557a49e commit bcd5729

5 files changed

Lines changed: 1294 additions & 4 deletions

File tree

CHANGES.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,24 @@ To be released.
1010

1111
### @logtape/logtape
1212

13+
- Added “fingers crossed” logging feature that buffers debug logs in memory
14+
and only outputs them when a trigger event occurs, reducing log noise while
15+
preserving context for debugging. [[#59]]
16+
17+
- Added `fingersCrossed()` function that implements the fingers crossed
18+
logging pattern with support for trigger levels, buffer management, and
19+
category isolation.
20+
- Added `FingersCrossedOptions` interface for configuring fingers crossed
21+
behavior with options for trigger level, buffer size, and category
22+
isolation modes.
23+
1324
- Added `Logger.emit()` method for emitting log records with custom fields,
1425
particularly useful for integrating external logs while preserving their
1526
original timestamps. [[#78]]
1627

28+
[#59]: https://github.com/dahlia/logtape/issues/59
29+
[#78]: https://github.com/dahlia/logtape/issues/78
30+
1731
### @logtape/file
1832

1933
- Changed `getStreamFileSink()` to implement `AsyncDisposable` instead of
@@ -28,6 +42,9 @@ To be released.
2842
- Added `PrettyFormatterOptions.properties` option for displaying
2943
`LogRecord.properties` (structured data). [[#69], [#70] by Matthias Feist]
3044

45+
[#69]: https://github.com/dahlia/logtape/issues/69
46+
[#70]: https://github.com/dahlia/logtape/pull/70
47+
3148
### @logtape/sentry
3249

3350
- Changed the type of the `getSentrySink()` function to accept any Sentry
@@ -45,9 +62,6 @@ To be released.
4562
type import referenced only by documentation, avoiding module resolution
4663
issues outside Node.js. [[#80] by Sora Morimoto]
4764

48-
[#69]: https://github.com/dahlia/logtape/issues/69
49-
[#70]: https://github.com/dahlia/logtape/pull/70
50-
[#78]: https://github.com/dahlia/logtape/issues/78
5165
[#80]: https://github.com/dahlia/logtape/pull/80
5266

5367

docs/manual/sinks.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,179 @@ For more details, see `getRotatingFileSink()` function and
545545
> flag to use the rotating file sink.
546546
547547

548+
Fingers crossed sink
549+
--------------------
550+
551+
*This API is available since LogTape 1.1.0.*
552+
553+
The fingers crossed sink implements a “fingers crossed” logging pattern where
554+
debug and low-level logs are buffered in memory and only output when
555+
a significant event (like an `"error"`) occurs. This pattern reduces log noise
556+
in normal operations while providing detailed context when issues arise,
557+
making logs more readable and actionable.
558+
559+
### Basic usage
560+
561+
The simplest way to use the fingers crossed sink is to wrap an existing sink:
562+
563+
~~~~ typescript twoslash
564+
// @noErrors: 2345
565+
import { configure, fingersCrossed, getConsoleSink } from "@logtape/logtape";
566+
567+
await configure({
568+
sinks: {
569+
console: fingersCrossed(getConsoleSink()),
570+
},
571+
loggers: [
572+
{ category: [], sinks: ["console"], lowestLevel: "debug" },
573+
],
574+
});
575+
~~~~
576+
577+
With this configuration:
578+
579+
- `"debug"`, `"info"`, and `"warning"` logs are buffered in memory
580+
- When an `"error"` (or higher) occurs, all buffered logs plus the error are
581+
output
582+
- Subsequent logs pass through directly until the next trigger event
583+
584+
### Customizing trigger level
585+
586+
You can customize when the buffer is flushed by setting the trigger level:
587+
588+
~~~~ typescript twoslash
589+
// @noErrors: 2345
590+
import { configure, fingersCrossed, getConsoleSink } from "@logtape/logtape";
591+
592+
await configure({
593+
sinks: {
594+
console: fingersCrossed(getConsoleSink(), {
595+
triggerLevel: "warning", // Trigger on warning or higher
596+
maxBufferSize: 500, // Keep last 500 records
597+
}),
598+
},
599+
// Omitted for brevity
600+
});
601+
~~~~
602+
603+
### Category isolation
604+
605+
By default, all log records share a single buffer. For applications with
606+
multiple modules or components, you can isolate buffers by category to prevent
607+
one component's errors from flushing logs from unrelated components:
608+
609+
~~~~ typescript twoslash
610+
// @noErrors: 2345
611+
import { configure, fingersCrossed, getConsoleSink } from "@logtape/logtape";
612+
613+
await configure({
614+
sinks: {
615+
console: fingersCrossed(getConsoleSink(), {
616+
isolateByCategory: "descendant",
617+
}),
618+
},
619+
// Omitted for brevity
620+
});
621+
~~~~
622+
623+
Category isolation modes:
624+
625+
`"descendant"`
626+
: Flush child category buffers when parent category triggers.
627+
For example, an error in `["app"]` flushes buffers for `["app", "auth"]`
628+
and `["app", "db"]`.
629+
630+
`"ancestor"`
631+
: Flush parent category buffers when child category triggers.
632+
For example, an error in `["app", "auth"]` flushes the `["app"]` buffer.
633+
634+
`"both"`
635+
: Flush both parent and child category buffers, combining descendant and
636+
ancestor modes.
637+
638+
### Custom category matching
639+
640+
For advanced use cases, you can provide a custom function to determine which
641+
categories should be flushed:
642+
643+
~~~~ typescript twoslash
644+
// @noErrors: 2345
645+
import { configure, fingersCrossed, getConsoleSink } from "@logtape/logtape";
646+
647+
await configure({
648+
sinks: {
649+
console: fingersCrossed(getConsoleSink(), {
650+
isolateByCategory: (triggerCategory, bufferedCategory) => {
651+
// Custom logic: flush if categories share the first element
652+
return triggerCategory[0] === bufferedCategory[0];
653+
},
654+
}),
655+
},
656+
// Omitted for brevity
657+
});
658+
~~~~
659+
660+
### Buffer management
661+
662+
The fingers crossed sink automatically manages buffer size to prevent memory
663+
issues:
664+
665+
~~~~ typescript twoslash
666+
// @noErrors: 2345
667+
import { configure, fingersCrossed, getConsoleSink } from "@logtape/logtape";
668+
669+
await configure({
670+
sinks: {
671+
console: fingersCrossed(getConsoleSink(), {
672+
maxBufferSize: 1000, // Keep last 1000 records per buffer
673+
}),
674+
},
675+
// Omitted for brevity
676+
});
677+
~~~~
678+
679+
When the buffer exceeds the maximum size, the oldest records are automatically
680+
dropped to prevent unbounded memory growth.
681+
682+
### Use cases
683+
684+
The fingers crossed sink is ideal for:
685+
686+
Production debugging
687+
: Keep detailed debug logs in memory without cluttering output,
688+
only showing them when errors occur to provide context.
689+
690+
Error investigation
691+
: Capture the sequence of events leading up to an error for thorough
692+
investigation.
693+
694+
Log volume management
695+
: Reduce log noise in normal operations while maintaining detailed visibility
696+
during issues.
697+
698+
Component isolation
699+
: Use category isolation to prevent log noise from one component affecting
700+
debugging of another component.
701+
702+
### Performance considerations
703+
704+
Memory usage
705+
: Buffered logs consume memory. Use appropriate buffer sizes and consider
706+
your application's memory constraints.
707+
708+
Trigger frequency
709+
: Frequent trigger events (like `"warning"`s) may reduce the effectiveness of
710+
buffering. Choose trigger levels carefully.
711+
712+
Category isolation overhead
713+
: Category isolation adds some overhead for category matching.
714+
For high-volume logging, consider using a single buffer
715+
if isolation isn't needed.
716+
717+
For more details, see the `fingersCrossed()` function and
718+
`FingersCrossedOptions` interface in the API reference.
719+
720+
548721
Text formatter
549722
--------------
550723

packages/logtape/src/mod.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export type { LogRecord } from "./record.ts";
4646
export {
4747
type AsyncSink,
4848
type ConsoleSinkOptions,
49+
fingersCrossed,
50+
type FingersCrossedOptions,
4951
fromAsyncSink,
5052
getConsoleSink,
5153
getStreamSink,

0 commit comments

Comments
 (0)