Skip to content

Commit c5d2c1d

Browse files
committed
+Add support for isolates and write tests
1 parent 36e6589 commit c5d2c1d

File tree

9 files changed

+1257
-27
lines changed

9 files changed

+1257
-27
lines changed

ARTICLE.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,8 @@ Here is a quick reference to all the main features available.
275275

276276
### Configuration (Static Properties on Log)
277277

278-
- `Log.enableStyling = true`: Enables/disables ANSI colors and icons. Set this to false if your terminal does not supprt ANSI colors.
278+
- `Log.context = 'MAIN'`: Labels the current isolate. Shows inside the log brackets so you can tell which isolate printed what.
279+
- `Log.enableStyling = true`: Enables/disables ANSI colors and icons. Set this to false if your terminal does not support ANSI colors.
279280
- `Log.showTimestamps = true`: Shows a HH:mm:ss.SSS timestamp on each log.
280281
- `Log.showTags = true`: Shows tags like #auth #ui on each log.
281282
- `Log.showIds = false`: Shows a unique ID on each log.
@@ -286,8 +287,32 @@ Here is a quick reference to all the main features available.
286287
- `Log.storeLogs = true`: If true, keeps a history of logs in memory.
287288
- `Log.maxStoredLogs = 50`: Sets the max number of LogItem objects to store.
288289
- `Log.items`: A `Queue<LogItem>` containing the stored logs.
290+
- `Log.clear()`: Clears all stored logs.
289291
- `Log.addCallback(callback)`: Registers a function void `Function(LogItem item)` that runs for every log.
290292
- `Log.removeCallback(callback)`: Removes a previously registered callback.
293+
- `Log.onLogDiscarded`: Called when old logs are removed from the queue (when full).
294+
- `Log.exportLogsAsJsonLines()`: Exports all stored logs as a JSONL string.
295+
296+
### Multi-Isolate Debugging
297+
298+
In Dart, each isolate has its own memory. All `Log` statics (`context`, `items`, `activeTags`, etc.) are independent per isolate. Set `Log.context` at the start of each isolate to label it:
299+
300+
```dart
301+
void main() {
302+
Log.context = 'MAIN';
303+
runApp(const App());
304+
}
305+
306+
@pragma("vm:entry-point")
307+
void overlayMain() {
308+
Log.context = 'OVERLAY';
309+
runApp(const Overlay());
310+
}
311+
```
312+
313+
Output: `[🟣 app #43 MAIN] Starting payment...` vs `[🟣 overlay #12 OVERLAY] Rendering widget`
314+
315+
This works on all platforms including web.
291316

292317
### Advanced Output
293318

README.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![tag](https://img.shields.io/badge/Tag-v0.3.30-purple?logo=github)](https://github.com/dev-cetera/df_log/tree/v0.3.30)
55
[![buymeacoffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-FFDD00?logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/dev_cetera)
66
[![sponsor](https://img.shields.io/badge/Sponsor-grey?logo=github-sponsors&logoColor=pink)](https://github.com/sponsors/dev-cetera)
7-
[![patreon](https://img.shields.io/badge/Patreon-grey?logo=patreon)](https://www.patreon.com/t0mb3rr)
7+
[![patreon](https://img.shields.io/badge/Patreon-grey?logo=patreon)](https://www.patreon.com/robelator)
88
[![discord](https://img.shields.io/badge/Discord-5865F2?logo=discord&logoColor=white)](https://discord.gg/gEQ8y2nfyX)
99
[![instagram](https://img.shields.io/badge/Instagram-E4405F?logo=instagram&logoColor=white)](https://www.instagram.com/dev_cetera/)
1010
[![license](https://img.shields.io/badge/License-MIT-blue.svg)](https://raw.githubusercontent.com/dev-cetera/df_log/main/LICENSE)
@@ -24,6 +24,8 @@
2424
- **Release Mode Control:** Configure logs and assertions to be active even in release builds.
2525
- **Customizable Output:** Show or hide timestamps, log IDs, and tags.
2626
- **IDE Integration:** Optionally uses `dart:developer`'s `log` function for a richer experience in some IDEs.
27+
- **Isolate-Friendly:** Set `Log.context` per isolate to instantly see where logs come from. Works on web too.
28+
- **Log Export:** Export stored logs as JSONL with `Log.exportLogsAsJsonLines()`.
2729
- **Extensible:** Add custom callbacks to integrate with other services (e.g., crash reporting).
2830

2931
## 📸 Screenshot
@@ -100,12 +102,40 @@ void main() {
100102
}
101103
```
102104

103-
### 💡 3. Configuration
105+
### 💡 4. Isolate Context
106+
107+
When debugging across isolates, set `Log.context` at the start of each isolate. Since Dart statics are per-isolate, each isolate gets its own value automatically - no locking, no shared state, no complexity.
108+
109+
```dart
110+
void main() {
111+
Log.context = 'MAIN';
112+
Log.info('Starting app');
113+
// Output: [🟣 example #5 MAIN] Starting app
114+
runApp(const App());
115+
}
116+
117+
@pragma("vm:entry-point")
118+
void overlayMain() {
119+
Log.context = 'OVERLAY';
120+
Log.info('Overlay started');
121+
// Output: [🟣 example #5 OVERLAY] Overlay started
122+
runApp(const Overlay());
123+
}
124+
```
125+
126+
Use a short tag like `M` to save space, or a full string like `ISOLATE_MAIN` for clarity.
127+
128+
**How it works:** In Dart, each isolate has its own memory. All `Log` statics (`context`, `items`, `activeTags`, etc.) are independent per isolate. This means `Log.context = 'OVERLAY'` in one isolate has zero effect on another. The only shared thing is the console output (stdout), which is why `Log.context` exists - so you can tell which isolate printed what. This works on all platforms including web.
129+
130+
### 💡 5. Configuration
104131

105132
You can customize the logging behavior to suit your needs, including styling, output format, and storage options. The Log class provides various settings to control how logs are displayed and managed.
106133

107134
```dart
108135
void main() {
136+
// Set a label for the current isolate (useful for multi-isolate debugging).
137+
Log.context = 'MAIN';
138+
109139
// Enable or disable ANSI colors and icons. Disable this if your console doesn't support it.
110140
Log.enableStyling = true;
111141
@@ -142,6 +172,17 @@ void main() {
142172
// Remove an existing callback.
143173
Log.removeCallback(callback);
144174
175+
// Get notified when old logs are discarded from the queue.
176+
Log.onLogDiscarded = (discardedItem) {
177+
// TODO: Forward to analytics before it's gone.
178+
};
179+
180+
// Clear log history.
181+
Log.clear();
182+
183+
// Export all stored logs as a JSONL string.
184+
final jsonl = Log.exportLogsAsJsonLines();
185+
145186
runApp(MyApp());
146187
}
147188
```

TODO.md

Lines changed: 0 additions & 4 deletions
This file was deleted.

_README_CONTENT.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
- **Release Mode Control:** Configure logs and assertions to be active even in release builds.
1010
- **Customizable Output:** Show or hide timestamps, log IDs, and tags.
1111
- **IDE Integration:** Optionally uses `dart:developer`'s `log` function for a richer experience in some IDEs.
12+
- **Isolate-Friendly:** Set `Log.context` per isolate to instantly see where logs come from. Works on web too.
13+
- **Log Export:** Export stored logs as JSONL with `Log.exportLogsAsJsonLines()`.
1214
- **Extensible:** Add custom callbacks to integrate with other services (e.g., crash reporting).
1315

1416
## 📸 Screenshot
@@ -85,12 +87,40 @@ void main() {
8587
}
8688
```
8789

88-
### 💡 3. Configuration
90+
### 💡 4. Isolate Context
91+
92+
When debugging across isolates, set `Log.context` at the start of each isolate. Since Dart statics are per-isolate, each isolate gets its own value automatically - no locking, no shared state, no complexity.
93+
94+
```dart
95+
void main() {
96+
Log.context = 'MAIN';
97+
Log.info('Starting app');
98+
// Output: [🟣 example #5 MAIN] Starting app
99+
runApp(const App());
100+
}
101+
102+
@pragma("vm:entry-point")
103+
void overlayMain() {
104+
Log.context = 'OVERLAY';
105+
Log.info('Overlay started');
106+
// Output: [🟣 example #5 OVERLAY] Overlay started
107+
runApp(const Overlay());
108+
}
109+
```
110+
111+
Use a short tag like `M` to save space, or a full string like `ISOLATE_MAIN` for clarity.
112+
113+
**How it works:** In Dart, each isolate has its own memory. All `Log` statics (`context`, `items`, `activeTags`, etc.) are independent per isolate. This means `Log.context = 'OVERLAY'` in one isolate has zero effect on another. The only shared thing is the console output (stdout), which is why `Log.context` exists - so you can tell which isolate printed what. This works on all platforms including web.
114+
115+
### 💡 5. Configuration
89116

90117
You can customize the logging behavior to suit your needs, including styling, output format, and storage options. The Log class provides various settings to control how logs are displayed and managed.
91118

92119
```dart
93120
void main() {
121+
// Set a label for the current isolate (useful for multi-isolate debugging).
122+
Log.context = 'MAIN';
123+
94124
// Enable or disable ANSI colors and icons. Disable this if your console doesn't support it.
95125
Log.enableStyling = true;
96126
@@ -127,6 +157,17 @@ void main() {
127157
// Remove an existing callback.
128158
Log.removeCallback(callback);
129159
160+
// Get notified when old logs are discarded from the queue.
161+
Log.onLogDiscarded = (discardedItem) {
162+
// TODO: Forward to analytics before it's gone.
163+
};
164+
165+
// Clear log history.
166+
Log.clear();
167+
168+
// Export all stored logs as a JSONL string.
169+
final jsonl = Log.exportLogsAsJsonLines();
170+
130171
runApp(MyApp());
131172
}
132173
```

analysis_options.yaml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
# Dev-Cetera analysis_options.yaml for Dart - Version 1
1+
# For dev-cetera.com
2+
# Version: 1
23

34
include: package:lints/recommended.yaml
45

@@ -9,7 +10,6 @@ linter:
910
avoid_web_libraries_in_flutter: true
1011
camel_case_types: false
1112
constant_identifier_names: false
12-
directives_ordering: true
1313
file_names: false
1414
library_private_types_in_public_api: false
1515
non_constant_identifier_names: false
@@ -19,6 +19,7 @@ linter:
1919
prefer_final_fields: true
2020
prefer_function_declarations_over_variables: false
2121
prefer_relative_imports: true
22+
prefer_single_quotes: true
2223
require_trailing_commas: true
2324
unawaited_futures: true
2425
unnecessary_this: true
@@ -32,24 +33,24 @@ analyzer:
3233
strict-raw-types: true
3334
exclude:
3435
- build/**
36+
# - '**.g.dart'
3537

3638
errors:
3739
always_declare_return_types: error
3840
avoid_renaming_method_parameters: error
3941
avoid_type_to_string: error
42+
close_sinks: error
43+
collection_methods_unrelated_type: error
4044
depend_on_referenced_packages: error
4145
flutter_style_todos: error
4246
invalid_override_of_non_virtual_member: error
4347
invalid_use_of_protected_member: error
48+
missing_return: error
4449
no_leading_underscores_for_local_identifiers: error
4550
prefer_final_in_for_each: error
4651
prefer_relative_imports: error
47-
prefer_single_quotes: error
48-
unnecessary_async: error
49-
unnecessary_await_in_return: error
50-
unnecessary_late: error
52+
record_literal_one_positional_no_trailing_comma: error
5153
unnecessary_new: error
52-
unnecessary_unawaited: error
5354
unrelated_type_equality_checks: error
5455
unused_label: ignore
5556
use_key_in_widget_constructors: error

lib/src/log.dart

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,28 @@ final class Log {
2525
//
2626
//
2727

28-
/// A filter for console output. A log is printed if untagged, or if ALL of
28+
/// A custom label for the current isolate or execution context.
29+
///
30+
/// Set this at the start of each isolate to identify where logs come from.
31+
/// Displayed inside the square brackets of the log output.
32+
///
33+
/// Dart statics are per-isolate, so each isolate gets its own value.
34+
///
35+
/// ```dart
36+
/// void main() {
37+
/// Log.context = 'MAIN';
38+
/// runApp(const App());
39+
/// }
40+
///
41+
/// @pragma("vm:entry-point")
42+
/// void overlayMain() {
43+
/// Log.context = 'OVERLAY';
44+
/// runApp(const Overlay());
45+
/// }
46+
/// ```
47+
static String? context;
48+
49+
/// A filter for console output. A log is printed if untagged, or if any of
2950
/// its tags are present in this set.
3051
static var activeTags = {#debug, ..._IconCategory.values.map((e) => e.tag)};
3152

@@ -52,13 +73,18 @@ final class Log {
5273
static set maxStoredLogs(int value) {
5374
_maxStoredLogs = value < 0 ? 0 : value;
5475
while (items.length > _maxStoredLogs) {
55-
items.removeFirst();
76+
final discarded = items.removeFirst();
77+
onLogDiscarded?.call(discarded);
5678
}
5779
}
5880

5981
/// An in-memory queue of the most recent log items, capped by `maxStoredLogs`.
6082
static final items = Queue<LogItem>();
6183

84+
/// Called when the log queue is full and the oldest log is about to be
85+
/// discarded. Receives the [LogItem] being removed.
86+
static void Function(LogItem discardedItem)? onLogDiscarded;
87+
6288
/// If `true`, enables colors and other ANSI styling in the console output.
6389
static var enableStyling = false;
6490

@@ -105,6 +131,20 @@ final class Log {
105131
/// The internal function used for printing. Defaults to the standard `print`.
106132
static void Function(Object?) _printFunction = print;
107133

134+
/// Clears the in-memory log history.
135+
static void clear() => items.clear();
136+
137+
/// Exports all stored logs as a JSONL string (one JSON object per line).
138+
///
139+
/// Web-safe. Use however you want: save to file, send to a server, etc.
140+
static String exportLogsAsJsonLines() {
141+
final buffer = StringBuffer();
142+
for (final item in items) {
143+
buffer.writeln(item.toJson(pretty: false));
144+
}
145+
return buffer.toString();
146+
}
147+
108148
//
109149
//
110150
//
@@ -536,14 +576,15 @@ final class Log {
536576
showTags: showTags,
537577
showTimestamp: showTimestamps,
538578
frame: frame,
579+
context: context,
539580
);
540581

541582
// Remove old logs.
542-
if (storeLogs) {
543-
if (items.length >= maxStoredLogs) {
544-
items.removeFirst();
583+
if (storeLogs && maxStoredLogs > 0) {
584+
while (items.length >= maxStoredLogs) {
585+
final discarded = items.removeFirst();
586+
onLogDiscarded?.call(discarded);
545587
}
546-
// Maybe store new logs.
547588
items.add(logItem);
548589
}
549590

lib/src/log_item.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ final class LogItem {
2929
final Object? message;
3030
final Object? metadata;
3131
final Set<Symbol> tags;
32+
final String? context;
3233
final int internalIndex;
3334
static int _internalCount = 0;
3435

@@ -55,6 +56,7 @@ final class LogItem {
5556
required this.showTags,
5657
required this.showTimestamp,
5758
required this.frame,
59+
required this.context,
5860
}) : id = const Uuid().v4(),
5961
timestamp = DateTime.now(),
6062
internalIndex = _internalCount++;
@@ -73,6 +75,9 @@ final class LogItem {
7375
buffer.write('$icon ');
7476
}
7577
buffer.write(location);
78+
if (context != null && context!.isNotEmpty) {
79+
buffer.write(' $context');
80+
}
7681
if (showTimestamp) {
7782
final isoString = timestamp.toLocal().toIso8601String();
7883
final timeStr = isoString.substring(11, 23);
@@ -121,7 +126,9 @@ final class LogItem {
121126
}
122127
buffer.write('['.withAnsiStyle(bracketStyle));
123128
buffer.write(location1.withAnsiStyle(pathTextStyle));
124-
129+
if (context != null && context!.isNotEmpty) {
130+
buffer.write(' $context'.withAnsiStyle(bracketStyle));
131+
}
125132
if (showTimestamp) {
126133
final isoString = timestamp.toLocal().toIso8601String();
127134
final timeStr = isoString.substring(11, 23);
@@ -163,6 +170,7 @@ final class LogItem {
163170
return {
164171
'id': id,
165172
'column': column,
173+
'context': context,
166174
'icon': icon != null && (location != null && location!.isNotEmpty)
167175
? icon
168176
: null,

0 commit comments

Comments
 (0)