Skip to content

Commit 54a3e0a

Browse files
passsyclaude
andcommitted
Add lazy log builders for every level (#44)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent aa415de commit 54a3e0a

7 files changed

Lines changed: 698 additions & 12 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/// Example: Lazy message and lazy log construction.
2+
///
3+
/// Run with: dart run bin/lazy_messages.dart
4+
import 'dart:convert';
5+
6+
import 'package:chirp/chirp.dart';
7+
8+
void main() {
9+
// Configure logger with warning as minimum level.
10+
// trace and debug messages will be filtered out.
11+
Chirp.root =
12+
ChirpLogger().setMinLogLevel(ChirpLogLevel.warning).addConsoleWriter();
13+
14+
final hugeMap = {
15+
'users': List.generate(1000, (i) => {'id': i, 'name': 'User $i'}),
16+
};
17+
18+
// Without lazy: jsonEncode() runs even though trace is filtered out.
19+
Chirp.trace('User data: ${jsonEncode(hugeMap)}');
20+
21+
// Lazy message: the lambda is never called because trace is filtered.
22+
Chirp.trace(() => 'User data: ${jsonEncode(hugeMap)}');
23+
24+
// traceLazy / warningLazy / etc.: defer EVERY argument (message, data,
25+
// error, …) until the logger has decided to actually emit. Use this when
26+
// expensive work is in the structured `data` map, not just the message.
27+
Chirp.traceLazy(
28+
(log) => log('User data', data: {'snapshot': jsonEncode(hugeMap)}),
29+
);
30+
31+
// Works at every level.
32+
Chirp.warningLazy(
33+
(log) => log('Rendered users', data: {'count': hugeMap.length}),
34+
);
35+
Chirp.errorLazy((log) => log('This error is logged'));
36+
37+
// Plain strings and lazy messages still work as before.
38+
Chirp.warning('Plain string warning');
39+
Chirp.warning(() => 'Lazy string warning');
40+
}
41+
42+
// Output (only warning and above are shown):
43+
// ... [warning] Rendered users {count: 1}
44+
// ... [error] This error is logged
45+
// ... [warning] Plain string warning
46+
// ... [warning] Lazy string warning

packages/chirp/CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
## Unreleased
2+
3+
- **New** Lazy message construction — pass `() => 'expensive $message'` to any log method.
4+
The lambda is only called when the log level passes the filter, avoiding unnecessary string allocations.
5+
- **New** Lazy log builders — `traceLazy`, `debugLazy`, `infoLazy`, `noticeLazy`, `successLazy`, `warningLazy`, `errorLazy`, `criticalLazy`, `wtfLazy`, and `logLazy` (with a `level:` argument) — defer the entire log call (message, `data`, `error`, `stackTrace`, `formatOptions`) until the level filter passes.
6+
Useful when expensive work lives in the structured `data` map, not just the message string.
7+
```dart
8+
Chirp.warningLazy(
9+
(log) => log('snapshot', data: {'state': renderState()}),
10+
);
11+
```
12+
Matching `Chirp.*Lazy` static forwarders are exposed on the global logger.
13+
Caller resolution lands on the `xxxLazy(...)` invocation line, not the inner `(log) => ...` lambda body, so eager and lazy calls report consistent source locations.
14+
Adds the `ChirpLogFn` typedef for the inner `log` callback.
15+
116
## 0.8.0
217

318
### New Features

packages/chirp/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,56 @@ Chirp.root = ChirpLogger().addConsoleWriter();
467467
Chirp.root.addWriter(myWriter);
468468
```
469469

470+
### Lazy Logging
471+
472+
Defer expensive log construction until the logger has decided to actually emit.
473+
Two flavors, picked by what's expensive.
474+
475+
#### Lazy Messages
476+
477+
Pass a lambda instead of a string to defer expensive message construction.
478+
The lambda is only called when the log level passes the filter — avoiding unnecessary allocations when the logger is "off":
479+
480+
```dart
481+
// Always builds the string, even if trace is filtered out
482+
logger.trace('User data: ${jsonEncode(hugeMap)}');
483+
484+
// Lambda only called when trace level is active
485+
logger.trace(() => 'User data: ${jsonEncode(hugeMap)}');
486+
```
487+
488+
This is especially useful for `trace` and `debug` messages that are typically disabled in production but contain expensive-to-build strings.
489+
490+
#### Lazy Builders
491+
492+
When the expensive work lives in `data`, `error`, `stackTrace`, or any other named argument — not just the message — use the `xxxLazy` variant.
493+
The builder is invoked only when the level passes, so every argument it constructs is paid for only on records that will actually be emitted.
494+
495+
```dart
496+
// Defers EVERY argument (message, data, error, ...) until the logger emits
497+
Chirp.warningLazy(
498+
(log) => log('snapshot', data: {'state': renderState(), 'metrics': dump()}),
499+
);
500+
501+
// Works for every level
502+
Chirp.errorLazy((log) => log('failed', error: e, stackTrace: st));
503+
504+
// logLazy takes a level: argument
505+
Chirp.logLazy(
506+
(log) => log('msg', data: {'state': render()}),
507+
level: ChirpLogLevel.debug,
508+
);
509+
```
510+
511+
The inner `log` callback has the same shape as the eager method (typedef `ChirpLogFn`), so call sites read identically — minus the wasted work when the log will be discarded.
512+
513+
Caller resolution is handled automatically: the `xxxLazy(...)` invocation line is reported, not the inner `(log) => ...` lambda body, so source locations look the same whether the call is eager or lazy.
514+
515+
Available variants: `traceLazy`, `debugLazy`, `infoLazy`, `noticeLazy`, `successLazy`, `warningLazy`, `errorLazy`, `criticalLazy`, `wtfLazy`, plus the generic `logLazy` on both `ChirpLogger` instances and the global `Chirp` static API.
516+
517+
Lazy builders cost one closure allocation per call when the level passes; the level-filtered hot path stays as cheap as the eager methods.
518+
Reach for `xxxLazy` when constructing `data` (or other arguments) is expensive; reach for the lazy-message form when only the message is.
519+
470520
### Filtering
471521

472522
Chirp provides two ways to filter logs:
@@ -893,6 +943,7 @@ See [`examples/simple/bin/`](https://github.com/passsy/chirp/tree/main/examples/
893943
| [`instance_tracking.dart`](https://github.com/passsy/chirp/blob/main/examples/simple/bin/instance_tracking.dart) | The `.chirp` extension |
894944
| [`multiple_writers.dart`](https://github.com/passsy/chirp/blob/main/examples/simple/bin/multiple_writers.dart) | Console + JSON output |
895945
| [`file_writer.dart`](https://github.com/passsy/chirp/blob/main/examples/simple/bin/file_writer.dart) | File logging with rotation |
946+
| [`lazy_messages.dart`](https://github.com/passsy/chirp/blob/main/examples/simple/bin/lazy_messages.dart) | Lazy message construction for performance |
896947
| [`interceptors.dart`](https://github.com/passsy/chirp/blob/main/examples/simple/bin/interceptors.dart) | Filtering and transforming logs |
897948
| [`library.dart`](https://github.com/passsy/chirp/blob/main/examples/simple/bin/library.dart) / [`app.dart`](https://github.com/passsy/chirp/blob/main/examples/simple/bin/app.dart) | Library logger adoption |
898949
| [`main.dart`](https://github.com/passsy/chirp/blob/main/examples/simple/bin/main.dart) | Span transformers (advanced) |

packages/chirp/lib/chirp.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export 'package:chirp/src/ansi/console_color.dart'
3131
export 'package:chirp/src/core/chirp_formatter.dart'
3232
show ChirpFormatter, MessageBuffer;
3333
export 'package:chirp/src/core/chirp_interceptor.dart' show ChirpInterceptor;
34-
export 'package:chirp/src/core/chirp_logger.dart' show ChirpLogger;
34+
export 'package:chirp/src/core/chirp_logger.dart' show ChirpLogFn, ChirpLogger;
3535
export 'package:chirp/src/core/chirp_root.dart'
3636
show Chirp, ChirpInstanceLogger, ChirpLoggerConsoleWriterExt, LogRecordExt;
3737
export 'package:chirp/src/core/chirp_writer.dart' show ChirpWriter;

0 commit comments

Comments
 (0)