Skip to content

Commit 5363714

Browse files
authored
Advanced File Output (#65)
Added Advanced File Output - Temporary buffer to reduce writes frequency - Log file rotation ins the specified directory
1 parent 5e51939 commit 5363714

File tree

4 files changed

+394
-0
lines changed

4 files changed

+394
-0
lines changed

lib/logger.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ library logger;
33

44
export 'src/outputs/file_output_stub.dart'
55
if (dart.library.io) 'src/outputs/file_output.dart';
6+
export 'src/outputs/advanced_file_output_stub.dart'
7+
if (dart.library.io) 'src/outputs/advanced_file_output.dart';
68
export 'web.dart';
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import 'dart:async';
2+
import 'dart:convert';
3+
import 'dart:io';
4+
5+
import '../log_level.dart';
6+
import '../log_output.dart';
7+
import '../output_event.dart';
8+
9+
extension _NumExt on num {
10+
String toDigits(int digits) => toString().padLeft(digits, '0');
11+
}
12+
13+
/// Accumulates logs in a buffer to reduce frequent disk, writes while optionally
14+
/// switching to a new log file if it reaches a certain size.
15+
///
16+
/// [AdvancedFileOutput] offer various improvements over the original
17+
/// [FileOutput]:
18+
/// * Managing an internal buffer which collects the logs and only writes
19+
/// them after a certain period of time to the disk.
20+
/// * Dynamically switching log files instead of using a single one specified
21+
/// by the user, when the current file reaches a specified size limit (optionally).
22+
///
23+
/// The buffered output can significantly reduce the
24+
/// frequency of file writes, which can be beneficial for (micro-)SD storage
25+
/// and other types of low-cost storage (e.g. on IoT devices). Specific log
26+
/// levels can trigger an immediate flush, without waiting for the next timer
27+
/// tick.
28+
///
29+
/// New log files are created when the current file reaches the specified size
30+
/// limit. This is useful for writing "archives" of telemetry data and logs
31+
/// while keeping them structured.
32+
class AdvancedFileOutput extends LogOutput {
33+
/// Creates a buffered file output.
34+
///
35+
/// By default, the log is buffered until either the [maxBufferSize] has been
36+
/// reached, the timer controlled by [maxDelay] has been triggered or an
37+
/// [OutputEvent] contains a [writeImmediately] log level.
38+
///
39+
/// [maxFileSizeKB] controls the log file rotation. The output automatically
40+
/// switches to a new log file as soon as the current file exceeds it.
41+
/// Use -1 to disable log rotation.
42+
///
43+
/// [maxDelay] describes the maximum amount of time before the buffer has to be
44+
/// written to the file.
45+
///
46+
/// Any log levels that are specified in [writeImmediately] trigger an immediate
47+
/// flush to the disk ([Level.warning], [Level.error] and [Level.fatal] by default).
48+
///
49+
/// [path] is either treated as directory for rotating or as target file name,
50+
/// depending on [maxFileSizeKB].
51+
AdvancedFileOutput({
52+
required String path,
53+
bool overrideExisting = false,
54+
Encoding encoding = utf8,
55+
List<Level>? writeImmediately,
56+
Duration maxDelay = const Duration(seconds: 2),
57+
int maxBufferSize = 2000,
58+
int maxFileSizeKB = 1024,
59+
String latestFileName = 'latest.log',
60+
String Function(DateTime timestamp)? fileNameFormatter,
61+
}) : _path = path,
62+
_overrideExisting = overrideExisting,
63+
_encoding = encoding,
64+
_maxDelay = maxDelay,
65+
_maxFileSizeKB = maxFileSizeKB,
66+
_maxBufferSize = maxBufferSize,
67+
_fileNameFormatter = fileNameFormatter ?? _defaultFileNameFormat,
68+
_writeImmediately = writeImmediately ??
69+
[
70+
Level.error,
71+
Level.fatal,
72+
Level.warning,
73+
// ignore: deprecated_member_use_from_same_package
74+
Level.wtf,
75+
],
76+
_file = maxFileSizeKB > 0 ? File('$path/$latestFileName') : File(path);
77+
78+
/// Logs directory path by default, particular log file path if [_maxFileSizeKB] is 0.
79+
final String _path;
80+
81+
final bool _overrideExisting;
82+
final Encoding _encoding;
83+
84+
final List<Level> _writeImmediately;
85+
final Duration _maxDelay;
86+
final int _maxFileSizeKB;
87+
final int _maxBufferSize;
88+
final String Function(DateTime timestamp) _fileNameFormatter;
89+
90+
final File _file;
91+
IOSink? _sink;
92+
Timer? _bufferFlushTimer;
93+
Timer? _targetFileUpdater;
94+
95+
final List<OutputEvent> _buffer = [];
96+
97+
bool get _rotatingFilesMode => _maxFileSizeKB > 0;
98+
99+
/// Formats the file with a full date string.
100+
///
101+
/// Example:
102+
/// * `2024-01-01-10-05-02-123.log`
103+
static String _defaultFileNameFormat(DateTime t) {
104+
return '${t.year}-${t.month.toDigits(2)}-${t.day.toDigits(2)}'
105+
'-${t.hour.toDigits(2)}-${t.minute.toDigits(2)}-${t.second.toDigits(2)}'
106+
'-${t.millisecond.toDigits(3)}.log';
107+
}
108+
109+
@override
110+
Future<void> init() async {
111+
if (_rotatingFilesMode) {
112+
final dir = Directory(_path);
113+
// We use sync directory check to avoid losing potential initial boot logs
114+
// in early crash scenarios.
115+
if (!dir.existsSync()) {
116+
dir.createSync(recursive: true);
117+
}
118+
119+
_targetFileUpdater = Timer.periodic(
120+
const Duration(minutes: 1),
121+
(_) => _updateTargetFile(),
122+
);
123+
}
124+
125+
_bufferFlushTimer = Timer.periodic(_maxDelay, (_) => _flushBuffer());
126+
await _openSink();
127+
if (_rotatingFilesMode) {
128+
await _updateTargetFile(); // Run first check without waiting for timer tick
129+
}
130+
}
131+
132+
@override
133+
void output(OutputEvent event) {
134+
_buffer.add(event);
135+
// If event level is present in writeImmediately, flush the complete buffer
136+
// along with any other possible elements that accumulated since
137+
// the last timer tick. Additionally, if the buffer is full.
138+
if (_buffer.length > _maxBufferSize ||
139+
_writeImmediately.contains(event.level)) {
140+
_flushBuffer();
141+
}
142+
}
143+
144+
void _flushBuffer() {
145+
if (_sink == null) return; // Wait until _sink becomes available
146+
for (final event in _buffer) {
147+
_sink?.writeAll(event.lines, Platform.isWindows ? '\r\n' : '\n');
148+
_sink?.writeln();
149+
}
150+
_buffer.clear();
151+
}
152+
153+
Future<void> _updateTargetFile() async {
154+
try {
155+
if (await _file.exists() &&
156+
await _file.length() > _maxFileSizeKB * 1024) {
157+
// Rotate the log file
158+
await _closeSink();
159+
await _file.rename('$_path/${_fileNameFormatter(DateTime.now())}');
160+
await _openSink();
161+
}
162+
} catch (e, s) {
163+
print(e);
164+
print(s);
165+
// Try creating another file and working with it
166+
await _closeSink();
167+
await _openSink();
168+
}
169+
}
170+
171+
Future<void> _openSink() async {
172+
_sink = _file.openWrite(
173+
mode: _overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend,
174+
encoding: _encoding,
175+
);
176+
}
177+
178+
Future<void> _closeSink() async {
179+
await _sink?.flush();
180+
await _sink?.close();
181+
_sink = null; // Explicitly set null until assigned again
182+
}
183+
184+
@override
185+
Future<void> destroy() async {
186+
_bufferFlushTimer?.cancel();
187+
_targetFileUpdater?.cancel();
188+
try {
189+
_flushBuffer();
190+
} catch (e, s) {
191+
print('Failed to flush buffer before closing the logger: $e');
192+
print(s);
193+
}
194+
await _closeSink();
195+
}
196+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import 'dart:convert';
2+
3+
import '../log_level.dart';
4+
import '../log_output.dart';
5+
import '../output_event.dart';
6+
7+
/// Accumulates logs in a buffer to reduce frequent disk, writes while optionally
8+
/// switching to a new log file if it reaches a certain size.
9+
///
10+
/// [AdvancedFileOutput] offer various improvements over the original
11+
/// [FileOutput]:
12+
/// * Managing an internal buffer which collects the logs and only writes
13+
/// them after a certain period of time to the disk.
14+
/// * Dynamically switching log files instead of using a single one specified
15+
/// by the user, when the current file reaches a specified size limit (optionally).
16+
///
17+
/// The buffered output can significantly reduce the
18+
/// frequency of file writes, which can be beneficial for (micro-)SD storage
19+
/// and other types of low-cost storage (e.g. on IoT devices). Specific log
20+
/// levels can trigger an immediate flush, without waiting for the next timer
21+
/// tick.
22+
///
23+
/// New log files are created when the current file reaches the specified size
24+
/// limit. This is useful for writing "archives" of telemetry data and logs
25+
/// while keeping them structured.
26+
class AdvancedFileOutput extends LogOutput {
27+
/// Creates a buffered file output.
28+
///
29+
/// By default, the log is buffered until either the [maxBufferSize] has been
30+
/// reached, the timer controlled by [maxDelay] has been triggered or an
31+
/// [OutputEvent] contains a [writeImmediately] log level.
32+
///
33+
/// [maxFileSizeKB] controls the log file rotation. The output automatically
34+
/// switches to a new log file as soon as the current file exceeds it.
35+
/// Use -1 to disable log rotation.
36+
///
37+
/// [maxDelay] describes the maximum amount of time before the buffer has to be
38+
/// written to the file.
39+
///
40+
/// Any log levels that are specified in [writeImmediately] trigger an immediate
41+
/// flush to the disk ([Level.warning], [Level.error] and [Level.fatal] by default).
42+
///
43+
/// [path] is either treated as directory for rotating or as target file name,
44+
/// depending on [maxFileSizeKB].
45+
AdvancedFileOutput({
46+
required String path,
47+
bool overrideExisting = false,
48+
Encoding encoding = utf8,
49+
List<Level>? writeImmediately,
50+
Duration maxDelay = const Duration(seconds: 2),
51+
int maxBufferSize = 2000,
52+
int maxFileSizeKB = 1024,
53+
String latestFileName = 'latest.log',
54+
String Function(DateTime timestamp)? fileNameFormatter,
55+
}) {
56+
throw UnsupportedError("Not supported on this platform.");
57+
}
58+
59+
@override
60+
void output(OutputEvent event) {
61+
throw UnsupportedError("Not supported on this platform.");
62+
}
63+
}

0 commit comments

Comments
 (0)