-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added Advanced File Output - Temporary buffer to reduce writes frequency - Log file rotation ins the specified directory
- Loading branch information
Showing
4 changed files
with
394 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
import 'dart:async'; | ||
import 'dart:convert'; | ||
import 'dart:io'; | ||
|
||
import '../log_level.dart'; | ||
import '../log_output.dart'; | ||
import '../output_event.dart'; | ||
|
||
extension _NumExt on num { | ||
String toDigits(int digits) => toString().padLeft(digits, '0'); | ||
} | ||
|
||
/// Accumulates logs in a buffer to reduce frequent disk, writes while optionally | ||
/// switching to a new log file if it reaches a certain size. | ||
/// | ||
/// [AdvancedFileOutput] offer various improvements over the original | ||
/// [FileOutput]: | ||
/// * Managing an internal buffer which collects the logs and only writes | ||
/// them after a certain period of time to the disk. | ||
/// * Dynamically switching log files instead of using a single one specified | ||
/// by the user, when the current file reaches a specified size limit (optionally). | ||
/// | ||
/// The buffered output can significantly reduce the | ||
/// frequency of file writes, which can be beneficial for (micro-)SD storage | ||
/// and other types of low-cost storage (e.g. on IoT devices). Specific log | ||
/// levels can trigger an immediate flush, without waiting for the next timer | ||
/// tick. | ||
/// | ||
/// New log files are created when the current file reaches the specified size | ||
/// limit. This is useful for writing "archives" of telemetry data and logs | ||
/// while keeping them structured. | ||
class AdvancedFileOutput extends LogOutput { | ||
/// Creates a buffered file output. | ||
/// | ||
/// By default, the log is buffered until either the [maxBufferSize] has been | ||
/// reached, the timer controlled by [maxDelay] has been triggered or an | ||
/// [OutputEvent] contains a [writeImmediately] log level. | ||
/// | ||
/// [maxFileSizeKB] controls the log file rotation. The output automatically | ||
/// switches to a new log file as soon as the current file exceeds it. | ||
/// Use -1 to disable log rotation. | ||
/// | ||
/// [maxDelay] describes the maximum amount of time before the buffer has to be | ||
/// written to the file. | ||
/// | ||
/// Any log levels that are specified in [writeImmediately] trigger an immediate | ||
/// flush to the disk ([Level.warning], [Level.error] and [Level.fatal] by default). | ||
/// | ||
/// [path] is either treated as directory for rotating or as target file name, | ||
/// depending on [maxFileSizeKB]. | ||
AdvancedFileOutput({ | ||
required String path, | ||
bool overrideExisting = false, | ||
Encoding encoding = utf8, | ||
List<Level>? writeImmediately, | ||
Duration maxDelay = const Duration(seconds: 2), | ||
int maxBufferSize = 2000, | ||
int maxFileSizeKB = 1024, | ||
String latestFileName = 'latest.log', | ||
String Function(DateTime timestamp)? fileNameFormatter, | ||
}) : _path = path, | ||
_overrideExisting = overrideExisting, | ||
_encoding = encoding, | ||
_maxDelay = maxDelay, | ||
_maxFileSizeKB = maxFileSizeKB, | ||
_maxBufferSize = maxBufferSize, | ||
_fileNameFormatter = fileNameFormatter ?? _defaultFileNameFormat, | ||
_writeImmediately = writeImmediately ?? | ||
[ | ||
Level.error, | ||
Level.fatal, | ||
Level.warning, | ||
// ignore: deprecated_member_use_from_same_package | ||
Level.wtf, | ||
], | ||
_file = maxFileSizeKB > 0 ? File('$path/$latestFileName') : File(path); | ||
|
||
/// Logs directory path by default, particular log file path if [_maxFileSizeKB] is 0. | ||
final String _path; | ||
|
||
final bool _overrideExisting; | ||
final Encoding _encoding; | ||
|
||
final List<Level> _writeImmediately; | ||
final Duration _maxDelay; | ||
final int _maxFileSizeKB; | ||
final int _maxBufferSize; | ||
final String Function(DateTime timestamp) _fileNameFormatter; | ||
|
||
final File _file; | ||
IOSink? _sink; | ||
Timer? _bufferFlushTimer; | ||
Timer? _targetFileUpdater; | ||
|
||
final List<OutputEvent> _buffer = []; | ||
|
||
bool get _rotatingFilesMode => _maxFileSizeKB > 0; | ||
|
||
/// Formats the file with a full date string. | ||
/// | ||
/// Example: | ||
/// * `2024-01-01-10-05-02-123.log` | ||
static String _defaultFileNameFormat(DateTime t) { | ||
return '${t.year}-${t.month.toDigits(2)}-${t.day.toDigits(2)}' | ||
'-${t.hour.toDigits(2)}-${t.minute.toDigits(2)}-${t.second.toDigits(2)}' | ||
'-${t.millisecond.toDigits(3)}.log'; | ||
} | ||
|
||
@override | ||
Future<void> init() async { | ||
if (_rotatingFilesMode) { | ||
final dir = Directory(_path); | ||
// We use sync directory check to avoid losing potential initial boot logs | ||
// in early crash scenarios. | ||
if (!dir.existsSync()) { | ||
dir.createSync(recursive: true); | ||
} | ||
|
||
_targetFileUpdater = Timer.periodic( | ||
const Duration(minutes: 1), | ||
(_) => _updateTargetFile(), | ||
); | ||
} | ||
|
||
_bufferFlushTimer = Timer.periodic(_maxDelay, (_) => _flushBuffer()); | ||
await _openSink(); | ||
if (_rotatingFilesMode) { | ||
await _updateTargetFile(); // Run first check without waiting for timer tick | ||
} | ||
} | ||
|
||
@override | ||
void output(OutputEvent event) { | ||
_buffer.add(event); | ||
// If event level is present in writeImmediately, flush the complete buffer | ||
// along with any other possible elements that accumulated since | ||
// the last timer tick. Additionally, if the buffer is full. | ||
if (_buffer.length > _maxBufferSize || | ||
_writeImmediately.contains(event.level)) { | ||
_flushBuffer(); | ||
} | ||
} | ||
|
||
void _flushBuffer() { | ||
if (_sink == null) return; // Wait until _sink becomes available | ||
for (final event in _buffer) { | ||
_sink?.writeAll(event.lines, Platform.isWindows ? '\r\n' : '\n'); | ||
_sink?.writeln(); | ||
} | ||
_buffer.clear(); | ||
} | ||
|
||
Future<void> _updateTargetFile() async { | ||
try { | ||
if (await _file.exists() && | ||
await _file.length() > _maxFileSizeKB * 1024) { | ||
// Rotate the log file | ||
await _closeSink(); | ||
await _file.rename('$_path/${_fileNameFormatter(DateTime.now())}'); | ||
await _openSink(); | ||
} | ||
} catch (e, s) { | ||
print(e); | ||
print(s); | ||
// Try creating another file and working with it | ||
await _closeSink(); | ||
await _openSink(); | ||
} | ||
} | ||
|
||
Future<void> _openSink() async { | ||
_sink = _file.openWrite( | ||
mode: _overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend, | ||
encoding: _encoding, | ||
); | ||
} | ||
|
||
Future<void> _closeSink() async { | ||
await _sink?.flush(); | ||
await _sink?.close(); | ||
_sink = null; // Explicitly set null until assigned again | ||
} | ||
|
||
@override | ||
Future<void> destroy() async { | ||
_bufferFlushTimer?.cancel(); | ||
_targetFileUpdater?.cancel(); | ||
try { | ||
_flushBuffer(); | ||
} catch (e, s) { | ||
print('Failed to flush buffer before closing the logger: $e'); | ||
print(s); | ||
} | ||
await _closeSink(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import 'dart:convert'; | ||
|
||
import '../log_level.dart'; | ||
import '../log_output.dart'; | ||
import '../output_event.dart'; | ||
|
||
/// Accumulates logs in a buffer to reduce frequent disk, writes while optionally | ||
/// switching to a new log file if it reaches a certain size. | ||
/// | ||
/// [AdvancedFileOutput] offer various improvements over the original | ||
/// [FileOutput]: | ||
/// * Managing an internal buffer which collects the logs and only writes | ||
/// them after a certain period of time to the disk. | ||
/// * Dynamically switching log files instead of using a single one specified | ||
/// by the user, when the current file reaches a specified size limit (optionally). | ||
/// | ||
/// The buffered output can significantly reduce the | ||
/// frequency of file writes, which can be beneficial for (micro-)SD storage | ||
/// and other types of low-cost storage (e.g. on IoT devices). Specific log | ||
/// levels can trigger an immediate flush, without waiting for the next timer | ||
/// tick. | ||
/// | ||
/// New log files are created when the current file reaches the specified size | ||
/// limit. This is useful for writing "archives" of telemetry data and logs | ||
/// while keeping them structured. | ||
class AdvancedFileOutput extends LogOutput { | ||
/// Creates a buffered file output. | ||
/// | ||
/// By default, the log is buffered until either the [maxBufferSize] has been | ||
/// reached, the timer controlled by [maxDelay] has been triggered or an | ||
/// [OutputEvent] contains a [writeImmediately] log level. | ||
/// | ||
/// [maxFileSizeKB] controls the log file rotation. The output automatically | ||
/// switches to a new log file as soon as the current file exceeds it. | ||
/// Use -1 to disable log rotation. | ||
/// | ||
/// [maxDelay] describes the maximum amount of time before the buffer has to be | ||
/// written to the file. | ||
/// | ||
/// Any log levels that are specified in [writeImmediately] trigger an immediate | ||
/// flush to the disk ([Level.warning], [Level.error] and [Level.fatal] by default). | ||
/// | ||
/// [path] is either treated as directory for rotating or as target file name, | ||
/// depending on [maxFileSizeKB]. | ||
AdvancedFileOutput({ | ||
required String path, | ||
bool overrideExisting = false, | ||
Encoding encoding = utf8, | ||
List<Level>? writeImmediately, | ||
Duration maxDelay = const Duration(seconds: 2), | ||
int maxBufferSize = 2000, | ||
int maxFileSizeKB = 1024, | ||
String latestFileName = 'latest.log', | ||
String Function(DateTime timestamp)? fileNameFormatter, | ||
}) { | ||
throw UnsupportedError("Not supported on this platform."); | ||
} | ||
|
||
@override | ||
void output(OutputEvent event) { | ||
throw UnsupportedError("Not supported on this platform."); | ||
} | ||
} |
Oops, something went wrong.