├── .github └── workflows │ └── dart.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── art ├── log_console_dark.png ├── log_console_light.png └── screenshot.png ├── example ├── .gitignore └── main.dart ├── lib ├── logger.dart ├── src │ ├── ansi_color.dart │ ├── date_time_format.dart │ ├── filters │ │ ├── development_filter.dart │ │ └── production_filter.dart │ ├── log_event.dart │ ├── log_filter.dart │ ├── log_level.dart │ ├── log_output.dart │ ├── log_printer.dart │ ├── logger.dart │ ├── output_event.dart │ ├── outputs │ │ ├── advanced_file_output.dart │ │ ├── advanced_file_output_stub.dart │ │ ├── console_output.dart │ │ ├── file_output.dart │ │ ├── file_output_stub.dart │ │ ├── memory_output.dart │ │ ├── multi_output.dart │ │ └── stream_output.dart │ └── printers │ │ ├── hybrid_printer.dart │ │ ├── logfmt_printer.dart │ │ ├── prefix_printer.dart │ │ ├── pretty_printer.dart │ │ └── simple_printer.dart └── web.dart ├── pubspec.yaml └── test ├── logger_test.dart ├── outputs ├── advanced_file_output_test.dart ├── file_output_test.dart ├── memory_output_test.dart ├── multi_output_test.dart └── stream_output_test.dart └── printers ├── hybrid_printer_test.dart ├── logfmt_printer_test.dart ├── prefix_printer_test.dart ├── pretty_printer_test.dart └── simple_printer_test.dart /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # See documentation here: 7 | # https://github.com/dart-lang/setup-dart/blob/main/README.md 8 | 9 | name: Dart 10 | 11 | on: 12 | push: 13 | branches: [ "main" ] 14 | pull_request: 15 | branches: [ "main" ] 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | sdk: [ stable, 2.17.0 ] 24 | steps: 25 | - uses: actions/checkout@v3 26 | 27 | - uses: dart-lang/setup-dart@v1 28 | with: 29 | sdk: ${{ matrix.sdk }} 30 | 31 | - name: Install dependencies 32 | run: dart pub get 33 | 34 | # Uncomment this step to verify the use of 'dart format' on each commit. 35 | - name: Verify formatting 36 | if: matrix.sdk == 'stable' 37 | run: dart format --output=none --set-exit-if-changed . 38 | 39 | # Consider passing '--fatal-infos' for slightly stricter analysis. 40 | - name: Analyze project source 41 | run: dart analyze --fatal-infos 42 | 43 | # Your project will need to have tests in test/ and a dependency on 44 | # package:test for this step to succeed. Note that Flutter projects will 45 | # want to change this to 'flutter test'. 46 | - name: Run tests 47 | run: dart test 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .DS_Store 3 | .dart_tool/ 4 | .idea/ 5 | .iml 6 | 7 | .packages 8 | .pub/ 9 | 10 | build/ 11 | ios/ 12 | android/ 13 | demo/ 14 | 15 | pubspec.lock 16 | doc/ -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 7a4c33425ddd78c54aba07d86f3f9a4a0051769b 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.5.0 2 | 3 | * AdvancedFileOutput: Added support for custom `fileUpdateDuration`. Thanks to 4 | @shlowdy ([#86](https://github.com/SourceHorizon/logger/pull/86)). 5 | * README: Fixed outdated LogOutput documentation. 6 | 7 | ## 2.4.0 8 | 9 | * Added pub.dev `topics`. Thanks to 10 | @jonasfj ([#74](https://github.com/SourceHorizon/logger/pull/74)). 11 | * PrettyPrinter: Added `dateTimeFormat` option (backwards-compatible with `printTime`). 12 | Fixes [#80](https://github.com/SourceHorizon/logger/issues/80). 13 | 14 | ## 2.3.0 15 | 16 | * AdvancedFileOutput: Added file deletion option. Thanks to 17 | @lomby92 ([#71](https://github.com/SourceHorizon/logger/pull/71)). 18 | 19 | ## 2.2.0 20 | 21 | * Added AdvancedFileOutput. Thanks to 22 | @pyciko ([#65](https://github.com/SourceHorizon/logger/pull/65)). 23 | * Added missing acknowledgments in README. 24 | 25 | ## 2.1.0 26 | 27 | * Improved README explanation about debug mode. Thanks to 28 | @gkuga ([#57](https://github.com/SourceHorizon/logger/pull/57)). 29 | * Added web safe export. Fixes [#58](https://github.com/SourceHorizon/logger/issues/58). 30 | * Added `logger.init` to optionally await any `async` `init()` methods. 31 | Fixes [#61](https://github.com/SourceHorizon/logger/issues/61). 32 | 33 | ## 2.0.2+1 34 | 35 | * Meta update: Updated repository links to https://github.com/SourceHorizon/logger. 36 | 37 | ## 2.0.2 38 | 39 | * Moved the default log level assignment to prevent weird lazy initialization bugs. 40 | Mitigates [#38](https://github.com/SourceHorizon/logger/issues/38). 41 | 42 | ## 2.0.1 43 | 44 | * Updated README to reflect v2.0.0 log signature change. 45 | 46 | ## 2.0.0 47 | 48 | * Fixed supported platforms list. 49 | * Removed reference to outdated `logger_flutter` project. 50 | Thanks to @yangsfang ([#32](https://github.com/SourceHorizon/logger/pull/32)). 51 | * Added override capability for logger defaults. 52 | Thanks to @yangsfang ([#34](https://github.com/SourceHorizon/logger/pull/34)). 53 | * `Level.verbose`, `Level.wtf` and `Level.nothing` have been deprecated and are replaced 54 | by `Level.trace`, `Level.fatal` and `Level.off`. 55 | Additionally `Level.all` has been added. 56 | * PrettyPrinter: Added `levelColors` and `levelEmojis` as constructor parameter. 57 | 58 | ### Breaking changes 59 | 60 | * `log` signature has been changed to closer match dart's developer `log` function and allow for 61 | future optional parameters. 62 | 63 | Additionally, `time` has been added as an optional named parameter to support providing custom 64 | timestamps for LogEvents instead of `DateTime.now()`. 65 | 66 | #### Migration: 67 | * Before: 68 | ```dart 69 | logger.e("An error occurred!", error, stackTrace); 70 | ``` 71 | * After: 72 | ```dart 73 | logger.e("An error occurred!", error: error, stackTrace: stackTrace); 74 | ``` 75 | * `init` and `close` methods of `LogFilter`, `LogOutput` and `LogPrinter` are now async along 76 | with `Logger.close()`. (Fixes FileOutput) 77 | * LogListeners are now called on every LogEvent independent of the filter. 78 | * PrettyPrinter: `includeBox` is now private. 79 | * PrettyPrinter: `errorMethodCount` is now only considered if an error has been provided. 80 | Otherwise `methodCount` is used. 81 | * PrettyPrinter: Static `levelColors` and `levelEmojis` have been renamed to `defaultLevelColors` 82 | and `defaultLevelEmojis` and are used as fallback for their respective constructor parameters. 83 | * Levels are now sorted by their respective value instead of the enum index (Order didn't change). 84 | 85 | ## 1.4.0 86 | 87 | * Bumped upper SDK constraint to `<4.0.0`. 88 | * Added `excludePaths` to PrettyPrinter. Thanks to 89 | @Stitch-Taotao ([#13](https://github.com/SourceHorizon/logger/pull/13)). 90 | * Removed background color for `Level.error` and `Level.wtf` to improve readability. 91 | * Improved PrettyPrinter documentation. 92 | * Corrected README notice about ANSI colors. 93 | 94 | ## 1.3.0 95 | 96 | * Fixed stackTrace count when using `stackTraceBeginIndex`. 97 | Addresses [#114](https://github.com/simc/logger/issues/114). 98 | * Added proper FileOutput stub. Addresses [#94](https://github.com/simc/logger/issues/94). 99 | * Added `isClosed`. Addresses [#130](https://github.com/simc/logger/issues/130). 100 | * Added `time` to LogEvent. 101 | * Added `error` handling to LogfmtPrinter. 102 | 103 | ## 1.2.2 104 | 105 | * Fixed conditional LogOutput export. Credits to 106 | @ChristopheOosterlynck [#4](https://github.com/SourceHorizon/logger/pull/4). 107 | 108 | ## 1.2.1 109 | 110 | * Reverted `${this}` interpolation and added linter 111 | ignore. [#1](https://github.com/SourceHorizon/logger/issues/1) 112 | 113 | ## 1.2.0 114 | 115 | * Added origin LogEvent to OutputEvent. Addresses [#133](https://github.com/simc/logger/pull/133). 116 | * Re-added LogListener and OutputListener (Should restore compatibility with logger_flutter). 117 | * Replaced pedantic with lints. 118 | 119 | ## 1.1.0 120 | 121 | * Enhance boxing control with PrettyPrinter. Credits to @timmaffett 122 | * Add trailing new line to FileOutput. Credits to @narumishi 123 | * Add functions as a log message. Credits to @smotastic 124 | 125 | ## 1.0.0 126 | 127 | * Stable nullsafety 128 | 129 | ## 1.0.0-nullsafety.0 130 | 131 | * Convert to nullsafety. Credits to @DevNico 132 | 133 | ## 0.9.4 134 | 135 | * Remove broken platform detection. 136 | 137 | ## 0.9.3 138 | 139 | * Add `MultiOutput`. Credits to @gmpassos. 140 | * Handle browser Dart stacktraces in PrettyPrinter. Credits to @gmpassos. 141 | * Add platform detection. Credits to @gmpassos. 142 | * Catch output exceptions. Credits to @gmpassos. 143 | * Several documentation fixes. Credits to @gmpassos. 144 | 145 | ## 0.9.2 146 | 147 | * Add `PrefixPrinter`. Credits to @tkutcher. 148 | * Add `HybridPrinter`. Credits to @tkutcher. 149 | 150 | ## 0.9.1 151 | 152 | * Fix logging output for Flutter Web. Credits to @nateshmbhat and @Cocotus. 153 | 154 | ## 0.9.0 155 | 156 | * Remove `OutputCallback` and `LogCallback` 157 | * Rename `SimplePrinter`s argument `useColor` to `colors` 158 | * Rename `DebugFilter` to `DevelopmentFilter` 159 | 160 | ## 0.8.3 161 | 162 | * Add LogfmtPrinter 163 | * Add colored output to SimplePrinter 164 | 165 | ## 0.8.2 166 | 167 | * Add StreamOutput 168 | 169 | ## 0.8.1 170 | 171 | * Deprecate callbacks 172 | 173 | ## 0.8.0 174 | 175 | * Fix SimplePrinter showTime #12 176 | * Remove buffer field 177 | * Update library structure (thanks @marcgraub!) 178 | 179 | ## 0.7.0+2 180 | 181 | * Remove screenshot 182 | 183 | ## 0.7.0+1 184 | 185 | * Fix pedantic 186 | 187 | ## 0.7.0 188 | 189 | * Added `ProductionFilter`, `FileOutput`, `MemoryOutput`, `SimplePrinter` 190 | * Breaking: Changed `LogFilter`, `LogPrinter` and `LogOutput` 191 | 192 | ## 0.6.0 193 | 194 | * Added option to output timestamp 195 | * Added option to disable color 196 | * Added `LogOutput` 197 | * Behaviour change of `LogPrinter` 198 | * Remove dependency 199 | 200 | ## 0.5.0 201 | 202 | * Added emojis 203 | * `LogFilter` is a class now 204 | 205 | ## 0.4.0 206 | 207 | * First version of the new logger 208 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at harm@aarts.email. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Simon Leier 4 | Copyright (c) 2019 Harm Aarts 5 | Copyright (c) 2023 Severin Hamader 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Logger 2 | 3 | [![pub package](https://img.shields.io/pub/v/logger.svg?logo=dart&logoColor=00b9fc)](https://pub.dartlang.org/packages/logger) 4 | [![CI](https://img.shields.io/github/actions/workflow/status/SourceHorizon/logger/dart.yml?branch=main&logo=github-actions&logoColor=white)](https://github.com/SourceHorizon/logger/actions) 5 | [![Last Commits](https://img.shields.io/github/last-commit/SourceHorizon/logger?logo=git&logoColor=white)](https://github.com/SourceHorizon/logger/commits/main) 6 | [![Pull Requests](https://img.shields.io/github/issues-pr/SourceHorizon/logger?logo=github&logoColor=white)](https://github.com/SourceHorizon/logger/pulls) 7 | [![Code size](https://img.shields.io/github/languages/code-size/SourceHorizon/logger?logo=github&logoColor=white)](https://github.com/SourceHorizon/logger) 8 | [![License](https://img.shields.io/github/license/SourceHorizon/logger?logo=open-source-initiative&logoColor=green)](https://github.com/SourceHorizon/logger/blob/main/LICENSE) 9 | 10 | Small, easy to use and extensible logger which prints beautiful logs.
11 | Inspired by [logger](https://github.com/orhanobut/logger) for Android. 12 | 13 | **Show some ❤️ and star the repo to support the project** 14 | 15 | ### Resources: 16 | 17 | - [Documentation](https://pub.dev/documentation/logger/latest/logger/logger-library.html) 18 | - [Pub Package](https://pub.dev/packages/logger) 19 | - [GitHub Repository](https://github.com/SourceHorizon/logger) 20 | 21 | ## Getting Started 22 | 23 | Just create an instance of `Logger` and start logging: 24 | 25 | ```dart 26 | var logger = Logger(); 27 | 28 | logger.d("Logger is working!"); 29 | ``` 30 | 31 | Instead of a string message, you can also pass other objects like `List`, `Map` or `Set`. 32 | 33 | ## Output 34 | 35 | ![](https://raw.githubusercontent.com/SourceHorizon/logger/main/art/screenshot.png) 36 | 37 | # Documentation 38 | 39 | ## Log level 40 | 41 | You can log with different levels: 42 | 43 | ```dart 44 | logger.t("Trace log"); 45 | 46 | logger.d("Debug log"); 47 | 48 | logger.i("Info log"); 49 | 50 | logger.w("Warning log"); 51 | 52 | logger.e("Error log", error: 'Test Error'); 53 | 54 | logger.f("What a fatal log", error: error, stackTrace: stackTrace); 55 | ``` 56 | 57 | To show only specific log levels, you can set: 58 | 59 | ```dart 60 | Logger.level = Level.warning; 61 | ``` 62 | 63 | This hides all `trace`, `debug` and `info` log events. 64 | 65 | ## Options 66 | 67 | When creating a logger, you can pass some options: 68 | 69 | ```dart 70 | var logger = Logger( 71 | filter: null, // Use the default LogFilter (-> only log in debug mode) 72 | printer: PrettyPrinter(), // Use the PrettyPrinter to format and print log 73 | output: null, // Use the default LogOutput (-> send everything to console) 74 | ); 75 | ``` 76 | 77 | If you use the `PrettyPrinter`, there are more options: 78 | 79 | ```dart 80 | var logger = Logger( 81 | printer: PrettyPrinter( 82 | methodCount: 2, // Number of method calls to be displayed 83 | errorMethodCount: 8, // Number of method calls if stacktrace is provided 84 | lineLength: 120, // Width of the output 85 | colors: true, // Colorful log messages 86 | printEmojis: true, // Print an emoji for each log message 87 | // Should each log print contain a timestamp 88 | dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart, 89 | ), 90 | ); 91 | ``` 92 | 93 | ### Auto detecting 94 | 95 | With the `io` package you can auto detect the `lineLength` and `colors` arguments. 96 | Assuming you have imported the `io` package with `import 'dart:io' as io;` you 97 | can auto detect `colors` with `io.stdout.supportsAnsiEscapes` and `lineLength` 98 | with `io.stdout.terminalColumns`. 99 | 100 | You should probably do this unless there's a good reason you don't want to 101 | import `io`, for example when using this library on the web. 102 | 103 | ## LogFilter 104 | 105 | The `LogFilter` decides which log events should be shown and which don't.
106 | The default implementation (`DevelopmentFilter`) shows all logs with `level >= Logger.level` while 107 | in debug mode (i.e., running dart with `--enable-asserts`). 108 | In release mode all logs are omitted. 109 | 110 | You can create your own `LogFilter` like this: 111 | 112 | ```dart 113 | class MyFilter extends LogFilter { 114 | @override 115 | bool shouldLog(LogEvent event) { 116 | return true; 117 | } 118 | } 119 | ``` 120 | 121 | This will show all logs even in release mode. (**NOT** a good idea) 122 | 123 | ## LogPrinter 124 | 125 | The `LogPrinter` creates and formats the output, which is then sent to the `LogOutput`.
126 | You can implement your own `LogPrinter`. This gives you maximum flexibility. 127 | 128 | A very basic printer could look like this: 129 | 130 | ```dart 131 | class MyPrinter extends LogPrinter { 132 | @override 133 | List log(LogEvent event) { 134 | return [event.message]; 135 | } 136 | } 137 | ``` 138 | 139 | If you created a cool `LogPrinter` which might be helpful to others, feel free to open a pull 140 | request. 141 | :) 142 | 143 | ### Colors 144 | 145 | Please note that in some cases ANSI escape sequences do not work under macOS. 146 | These escape sequences are used to colorize the output. 147 | This seems to be related to a Flutter bug that affects iOS builds: 148 | https://github.com/flutter/flutter/issues/64491 149 | 150 | However, if you are using a JetBrains IDE (Android Studio, IntelliJ, etc.) 151 | you can make use of 152 | the [Grep Console Plugin](https://plugins.jetbrains.com/plugin/7125-grep-console) 153 | and the [`PrefixPrinter`](/lib/src/printers/prefix_printer.dart) 154 | decorator to achieve colored logs for any logger: 155 | 156 | ```dart 157 | var logger = Logger( 158 | printer: PrefixPrinter(PrettyPrinter(colors: false)) 159 | ); 160 | ``` 161 | 162 | ## LogOutput 163 | 164 | `LogOutput` sends the log lines to the desired destination.
165 | The default implementation (`ConsoleOutput`) send every line to the system console. 166 | 167 | ```dart 168 | class ConsoleOutput extends LogOutput { 169 | @override 170 | void output(OutputEvent event) { 171 | for (var line in event.lines) { 172 | print(line); 173 | } 174 | } 175 | } 176 | ``` 177 | 178 | Other provided `LogOutput`s are: 179 | 180 | * `FileOutput`/`AdvancedFileOutput` 181 | * `StreamOutput` 182 | 183 | Possible future `LogOutput`s could send to Firebase or to Logcat. Feel free to open pull 184 | requests. 185 | 186 | # Acknowledgments 187 | 188 | This package was originally created by [Simon Choi](https://github.com/simc), with further 189 | development by [Harm Aarts](https://github.com/haarts), greatly enhancing its functionality over 190 | time. 191 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | 6 | linter: 7 | rules: 8 | - prefer_const_constructors 9 | - prefer_relative_imports 10 | - unawaited_futures 11 | -------------------------------------------------------------------------------- /art/log_console_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SourceHorizon/logger/e57dfd352dab49adda266d45318efb1210f24d02/art/log_console_dark.png -------------------------------------------------------------------------------- /art/log_console_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SourceHorizon/logger/e57dfd352dab49adda266d45318efb1210f24d02/art/log_console_light.png -------------------------------------------------------------------------------- /art/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SourceHorizon/logger/e57dfd352dab49adda266d45318efb1210f24d02/art/screenshot.png -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /test 2 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | 3 | var logger = Logger( 4 | printer: PrettyPrinter(), 5 | ); 6 | 7 | var loggerNoStack = Logger( 8 | printer: PrettyPrinter(methodCount: 0), 9 | ); 10 | 11 | void main() { 12 | print( 13 | 'Run with either `dart example/main.dart` or `dart --enable-asserts example/main.dart`.'); 14 | demo(); 15 | } 16 | 17 | void demo() { 18 | logger.d('Log message with 2 methods'); 19 | 20 | loggerNoStack.i('Info message'); 21 | 22 | loggerNoStack.w('Just a warning!'); 23 | 24 | logger.e('Error! Something bad happened', error: 'Test Error'); 25 | 26 | loggerNoStack.t({'key': 5, 'value': 'something'}); 27 | 28 | Logger(printer: SimplePrinter(colors: true)).t('boom'); 29 | } 30 | -------------------------------------------------------------------------------- /lib/logger.dart: -------------------------------------------------------------------------------- 1 | /// Small, easy to use and extensible logger which prints beautiful logs. 2 | library logger; 3 | 4 | export 'src/outputs/file_output_stub.dart' 5 | 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'; 8 | export 'web.dart'; 9 | -------------------------------------------------------------------------------- /lib/src/ansi_color.dart: -------------------------------------------------------------------------------- 1 | /// This class handles colorizing of terminal output. 2 | class AnsiColor { 3 | /// ANSI Control Sequence Introducer, signals the terminal for new settings. 4 | static const ansiEsc = '\x1B['; 5 | 6 | /// Reset all colors and options for current SGRs to terminal defaults. 7 | static const ansiDefault = '${ansiEsc}0m'; 8 | 9 | final int? fg; 10 | final int? bg; 11 | final bool color; 12 | 13 | const AnsiColor.none() 14 | : fg = null, 15 | bg = null, 16 | color = false; 17 | 18 | const AnsiColor.fg(this.fg) 19 | : bg = null, 20 | color = true; 21 | 22 | const AnsiColor.bg(this.bg) 23 | : fg = null, 24 | color = true; 25 | 26 | @override 27 | String toString() { 28 | if (fg != null) { 29 | return '${ansiEsc}38;5;${fg}m'; 30 | } else if (bg != null) { 31 | return '${ansiEsc}48;5;${bg}m'; 32 | } else { 33 | return ''; 34 | } 35 | } 36 | 37 | String call(String msg) { 38 | if (color) { 39 | // ignore: unnecessary_brace_in_string_interps 40 | return '${this}$msg$ansiDefault'; 41 | } else { 42 | return msg; 43 | } 44 | } 45 | 46 | AnsiColor toFg() => AnsiColor.fg(bg); 47 | 48 | AnsiColor toBg() => AnsiColor.bg(fg); 49 | 50 | /// Defaults the terminal's foreground color without altering the background. 51 | String get resetForeground => color ? '${ansiEsc}39m' : ''; 52 | 53 | /// Defaults the terminal's background color without altering the foreground. 54 | String get resetBackground => color ? '${ansiEsc}49m' : ''; 55 | 56 | static int grey(double level) => 232 + (level.clamp(0.0, 1.0) * 23).round(); 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/date_time_format.dart: -------------------------------------------------------------------------------- 1 | import 'printers/pretty_printer.dart'; 2 | 3 | typedef DateTimeFormatter = String Function(DateTime time); 4 | 5 | class DateTimeFormat { 6 | /// Omits the date and time completely. 7 | static const DateTimeFormatter none = _none; 8 | 9 | /// Prints only the time. 10 | /// 11 | /// Example: 12 | /// * `12:30:40.550` 13 | static const DateTimeFormatter onlyTime = _onlyTime; 14 | 15 | /// Prints only the time including the difference since [PrettyPrinter.startTime]. 16 | /// 17 | /// Example: 18 | /// * `12:30:40.550 (+0:00:00.060700)` 19 | static const DateTimeFormatter onlyTimeAndSinceStart = _onlyTimeAndSinceStart; 20 | 21 | /// Prints only the date. 22 | /// 23 | /// Example: 24 | /// * `2019-06-04` 25 | static const DateTimeFormatter onlyDate = _onlyDate; 26 | 27 | /// Prints date and time (combines [onlyDate] and [onlyTime]). 28 | /// 29 | /// Example: 30 | /// * `2019-06-04 12:30:40.550` 31 | static const DateTimeFormatter dateAndTime = _dateAndTime; 32 | 33 | DateTimeFormat._(); 34 | 35 | static String _none(DateTime t) => throw UnimplementedError(); 36 | 37 | static String _onlyTime(DateTime t) { 38 | String threeDigits(int n) { 39 | if (n >= 100) return '$n'; 40 | if (n >= 10) return '0$n'; 41 | return '00$n'; 42 | } 43 | 44 | String twoDigits(int n) { 45 | if (n >= 10) return '$n'; 46 | return '0$n'; 47 | } 48 | 49 | var now = t; 50 | var h = twoDigits(now.hour); 51 | var min = twoDigits(now.minute); 52 | var sec = twoDigits(now.second); 53 | var ms = threeDigits(now.millisecond); 54 | return '$h:$min:$sec.$ms'; 55 | } 56 | 57 | static String _onlyTimeAndSinceStart(DateTime t) { 58 | var timeSinceStart = t.difference(PrettyPrinter.startTime!).toString(); 59 | return '${onlyTime(t)} (+$timeSinceStart)'; 60 | } 61 | 62 | static String _onlyDate(DateTime t) { 63 | String isoDate = t.toIso8601String(); 64 | return isoDate.substring(0, isoDate.indexOf("T")); 65 | } 66 | 67 | static String _dateAndTime(DateTime t) { 68 | return "${_onlyDate(t)} ${_onlyTime(t)}"; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/filters/development_filter.dart: -------------------------------------------------------------------------------- 1 | import '../log_event.dart'; 2 | import '../log_filter.dart'; 3 | 4 | /// Prints all logs with `level >= Logger.level` while in development mode (eg 5 | /// when `assert`s are evaluated, Flutter calls this debug mode). 6 | /// 7 | /// In release mode ALL logs are omitted. 8 | class DevelopmentFilter extends LogFilter { 9 | @override 10 | bool shouldLog(LogEvent event) { 11 | var shouldLog = false; 12 | assert(() { 13 | if (event.level >= level!) { 14 | shouldLog = true; 15 | } 16 | return true; 17 | }()); 18 | return shouldLog; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/filters/production_filter.dart: -------------------------------------------------------------------------------- 1 | import '../log_event.dart'; 2 | import '../log_filter.dart'; 3 | 4 | /// Prints all logs with `level >= Logger.level` even in production. 5 | class ProductionFilter extends LogFilter { 6 | @override 7 | bool shouldLog(LogEvent event) { 8 | return event.level >= level!; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/log_event.dart: -------------------------------------------------------------------------------- 1 | import 'log_level.dart'; 2 | 3 | class LogEvent { 4 | final Level level; 5 | final dynamic message; 6 | final Object? error; 7 | final StackTrace? stackTrace; 8 | 9 | /// Time when this log was created. 10 | final DateTime time; 11 | 12 | LogEvent( 13 | this.level, 14 | this.message, { 15 | DateTime? time, 16 | this.error, 17 | this.stackTrace, 18 | }) : time = time ?? DateTime.now(); 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/log_filter.dart: -------------------------------------------------------------------------------- 1 | import 'log_event.dart'; 2 | import 'log_level.dart'; 3 | import 'logger.dart'; 4 | 5 | /// An abstract filter of log messages. 6 | /// 7 | /// You can implement your own `LogFilter` or use [DevelopmentFilter]. 8 | /// Every implementation should consider [Logger.level]. 9 | abstract class LogFilter { 10 | Level? _level; 11 | 12 | // Still nullable for backwards compatibility. 13 | Level? get level => _level ?? Logger.level; 14 | 15 | set level(Level? value) => _level = value; 16 | 17 | Future init() async {} 18 | 19 | /// Is called every time a new log message is sent and decides if 20 | /// it will be printed or canceled. 21 | /// 22 | /// Returns `true` if the message should be logged. 23 | bool shouldLog(LogEvent event); 24 | 25 | Future destroy() async {} 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/log_level.dart: -------------------------------------------------------------------------------- 1 | /// [Level]s to control logging output. Logging can be enabled to include all 2 | /// levels above certain [Level]. 3 | enum Level { 4 | all(0), 5 | @Deprecated('[verbose] is being deprecated in favor of [trace].') 6 | verbose(999), 7 | trace(1000), 8 | debug(2000), 9 | info(3000), 10 | warning(4000), 11 | error(5000), 12 | @Deprecated('[wtf] is being deprecated in favor of [fatal].') 13 | wtf(5999), 14 | fatal(6000), 15 | @Deprecated('[nothing] is being deprecated in favor of [off].') 16 | nothing(9999), 17 | off(10000), 18 | ; 19 | 20 | final int value; 21 | 22 | const Level(this.value); 23 | 24 | bool operator <(Level other) => value < other.value; 25 | 26 | bool operator <=(Level other) => value <= other.value; 27 | 28 | bool operator >(Level other) => value > other.value; 29 | 30 | bool operator >=(Level other) => value >= other.value; 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/log_output.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'output_event.dart'; 4 | 5 | /// Log output receives a [OutputEvent] from [LogPrinter] and sends it to the 6 | /// desired destination. 7 | /// 8 | /// This can be an output stream, a file or a network target. [LogOutput] may 9 | /// cache multiple log messages. 10 | abstract class LogOutput { 11 | Future init() async {} 12 | 13 | void output(OutputEvent event); 14 | 15 | Future destroy() async {} 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/log_printer.dart: -------------------------------------------------------------------------------- 1 | import 'log_event.dart'; 2 | 3 | /// An abstract handler of log events. 4 | /// 5 | /// A log printer creates and formats the output, which is then sent to 6 | /// [LogOutput]. Every implementation has to use the [LogPrinter.log] 7 | /// method to send the output. 8 | /// 9 | /// You can implement a `LogPrinter` from scratch or extend [PrettyPrinter]. 10 | abstract class LogPrinter { 11 | Future init() async {} 12 | 13 | /// Is called every time a new [LogEvent] is sent and handles printing or 14 | /// storing the message. 15 | List log(LogEvent event); 16 | 17 | Future destroy() async {} 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/logger.dart: -------------------------------------------------------------------------------- 1 | import 'filters/development_filter.dart'; 2 | import 'log_event.dart'; 3 | import 'log_filter.dart'; 4 | import 'log_level.dart'; 5 | import 'log_output.dart'; 6 | import 'log_printer.dart'; 7 | import 'output_event.dart'; 8 | import 'outputs/console_output.dart'; 9 | import 'printers/pretty_printer.dart'; 10 | 11 | typedef LogCallback = void Function(LogEvent event); 12 | 13 | typedef OutputCallback = void Function(OutputEvent event); 14 | 15 | /// Use instances of logger to send log messages to the [LogPrinter]. 16 | class Logger { 17 | /// The current logging level of the app. 18 | /// 19 | /// All logs with levels below this level will be omitted. 20 | static Level level = Level.trace; 21 | 22 | /// The current default implementation of log filter. 23 | static LogFilter Function() defaultFilter = () => DevelopmentFilter(); 24 | 25 | /// The current default implementation of log printer. 26 | static LogPrinter Function() defaultPrinter = () => PrettyPrinter(); 27 | 28 | /// The current default implementation of log output. 29 | static LogOutput Function() defaultOutput = () => ConsoleOutput(); 30 | 31 | static final Set _logCallbacks = {}; 32 | 33 | static final Set _outputCallbacks = {}; 34 | 35 | late final Future _initialization; 36 | final LogFilter _filter; 37 | final LogPrinter _printer; 38 | final LogOutput _output; 39 | bool _active = true; 40 | 41 | /// Create a new instance of Logger. 42 | /// 43 | /// You can provide a custom [printer], [filter] and [output]. Otherwise the 44 | /// defaults: [PrettyPrinter], [DevelopmentFilter] and [ConsoleOutput] will be 45 | /// used. 46 | Logger({ 47 | LogFilter? filter, 48 | LogPrinter? printer, 49 | LogOutput? output, 50 | Level? level, 51 | }) : _filter = filter ?? defaultFilter(), 52 | _printer = printer ?? defaultPrinter(), 53 | _output = output ?? defaultOutput() { 54 | var filterInit = _filter.init(); 55 | if (level != null) { 56 | _filter.level = level; 57 | } 58 | var printerInit = _printer.init(); 59 | var outputInit = _output.init(); 60 | _initialization = Future.wait([filterInit, printerInit, outputInit]); 61 | } 62 | 63 | /// Future indicating if the initialization of the 64 | /// logger components (filter, printer and output) has been finished. 65 | /// 66 | /// This is only necessary if your [LogFilter]/[LogPrinter]/[LogOutput] 67 | /// uses `async` in their `init` method. 68 | Future get init => _initialization; 69 | 70 | /// Log a message at level [Level.verbose]. 71 | @Deprecated( 72 | "[Level.verbose] is being deprecated in favor of [Level.trace], use [t] instead.") 73 | void v( 74 | dynamic message, { 75 | DateTime? time, 76 | Object? error, 77 | StackTrace? stackTrace, 78 | }) { 79 | t(message, time: time, error: error, stackTrace: stackTrace); 80 | } 81 | 82 | /// Log a message at level [Level.trace]. 83 | void t( 84 | dynamic message, { 85 | DateTime? time, 86 | Object? error, 87 | StackTrace? stackTrace, 88 | }) { 89 | log(Level.trace, message, time: time, error: error, stackTrace: stackTrace); 90 | } 91 | 92 | /// Log a message at level [Level.debug]. 93 | void d( 94 | dynamic message, { 95 | DateTime? time, 96 | Object? error, 97 | StackTrace? stackTrace, 98 | }) { 99 | log(Level.debug, message, time: time, error: error, stackTrace: stackTrace); 100 | } 101 | 102 | /// Log a message at level [Level.info]. 103 | void i( 104 | dynamic message, { 105 | DateTime? time, 106 | Object? error, 107 | StackTrace? stackTrace, 108 | }) { 109 | log(Level.info, message, time: time, error: error, stackTrace: stackTrace); 110 | } 111 | 112 | /// Log a message at level [Level.warning]. 113 | void w( 114 | dynamic message, { 115 | DateTime? time, 116 | Object? error, 117 | StackTrace? stackTrace, 118 | }) { 119 | log(Level.warning, message, 120 | time: time, error: error, stackTrace: stackTrace); 121 | } 122 | 123 | /// Log a message at level [Level.error]. 124 | void e( 125 | dynamic message, { 126 | DateTime? time, 127 | Object? error, 128 | StackTrace? stackTrace, 129 | }) { 130 | log(Level.error, message, time: time, error: error, stackTrace: stackTrace); 131 | } 132 | 133 | /// Log a message at level [Level.wtf]. 134 | @Deprecated( 135 | "[Level.wtf] is being deprecated in favor of [Level.fatal], use [f] instead.") 136 | void wtf( 137 | dynamic message, { 138 | DateTime? time, 139 | Object? error, 140 | StackTrace? stackTrace, 141 | }) { 142 | f(message, time: time, error: error, stackTrace: stackTrace); 143 | } 144 | 145 | /// Log a message at level [Level.fatal]. 146 | void f( 147 | dynamic message, { 148 | DateTime? time, 149 | Object? error, 150 | StackTrace? stackTrace, 151 | }) { 152 | log(Level.fatal, message, time: time, error: error, stackTrace: stackTrace); 153 | } 154 | 155 | /// Log a message with [level]. 156 | void log( 157 | Level level, 158 | dynamic message, { 159 | DateTime? time, 160 | Object? error, 161 | StackTrace? stackTrace, 162 | }) { 163 | if (!_active) { 164 | throw ArgumentError('Logger has already been closed.'); 165 | } else if (error != null && error is StackTrace) { 166 | throw ArgumentError('Error parameter cannot take a StackTrace!'); 167 | } else if (level == Level.all) { 168 | throw ArgumentError('Log events cannot have Level.all'); 169 | // ignore: deprecated_member_use_from_same_package 170 | } else if (level == Level.off || level == Level.nothing) { 171 | throw ArgumentError('Log events cannot have Level.off'); 172 | } 173 | 174 | var logEvent = LogEvent( 175 | level, 176 | message, 177 | time: time, 178 | error: error, 179 | stackTrace: stackTrace, 180 | ); 181 | for (var callback in _logCallbacks) { 182 | callback(logEvent); 183 | } 184 | 185 | if (_filter.shouldLog(logEvent)) { 186 | var output = _printer.log(logEvent); 187 | 188 | if (output.isNotEmpty) { 189 | var outputEvent = OutputEvent(logEvent, output); 190 | // Issues with log output should NOT influence 191 | // the main software behavior. 192 | try { 193 | for (var callback in _outputCallbacks) { 194 | callback(outputEvent); 195 | } 196 | _output.output(outputEvent); 197 | } catch (e, s) { 198 | print(e); 199 | print(s); 200 | } 201 | } 202 | } 203 | } 204 | 205 | bool isClosed() { 206 | return !_active; 207 | } 208 | 209 | /// Closes the logger and releases all resources. 210 | Future close() async { 211 | _active = false; 212 | await _filter.destroy(); 213 | await _printer.destroy(); 214 | await _output.destroy(); 215 | } 216 | 217 | /// Register a [LogCallback] which is called for each new [LogEvent]. 218 | static void addLogListener(LogCallback callback) { 219 | _logCallbacks.add(callback); 220 | } 221 | 222 | /// Removes a [LogCallback] which was previously registered. 223 | /// 224 | /// Returns whether the callback was successfully removed. 225 | static bool removeLogListener(LogCallback callback) { 226 | return _logCallbacks.remove(callback); 227 | } 228 | 229 | /// Register an [OutputCallback] which is called for each new [OutputEvent]. 230 | static void addOutputListener(OutputCallback callback) { 231 | _outputCallbacks.add(callback); 232 | } 233 | 234 | /// Removes a [OutputCallback] which was previously registered. 235 | /// 236 | /// Returns whether the callback was successfully removed. 237 | static void removeOutputListener(OutputCallback callback) { 238 | _outputCallbacks.remove(callback); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /lib/src/output_event.dart: -------------------------------------------------------------------------------- 1 | import 'log_event.dart'; 2 | import 'log_level.dart'; 3 | 4 | class OutputEvent { 5 | final List lines; 6 | final LogEvent origin; 7 | 8 | Level get level => origin.level; 9 | 10 | OutputEvent(this.origin, this.lines); 11 | } 12 | -------------------------------------------------------------------------------- /lib/src/outputs/advanced_file_output.dart: -------------------------------------------------------------------------------- 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 | /// 52 | /// [maxRotatedFilesCount] controls the number of rotated files to keep. By default 53 | /// is null, which means no limit. 54 | /// If set to a positive number, the output will keep the last 55 | /// [maxRotatedFilesCount] files. The deletion step will be executed by sorting 56 | /// files following the [fileSorter] ascending strategy and keeping the last files. 57 | /// The [latestFileName] will not be counted. The default [fileSorter] strategy is 58 | /// sorting by last modified date, beware that could be not reliable in some 59 | /// platforms and/or filesystems. 60 | AdvancedFileOutput({ 61 | required String path, 62 | bool overrideExisting = false, 63 | Encoding encoding = utf8, 64 | List? writeImmediately, 65 | Duration maxDelay = const Duration(seconds: 2), 66 | int maxBufferSize = 2000, 67 | int maxFileSizeKB = 1024, 68 | String latestFileName = 'latest.log', 69 | String Function(DateTime timestamp)? fileNameFormatter, 70 | int? maxRotatedFilesCount, 71 | Comparator? fileSorter, 72 | Duration fileUpdateDuration = const Duration(minutes: 1), 73 | }) : _path = path, 74 | _overrideExisting = overrideExisting, 75 | _encoding = encoding, 76 | _maxDelay = maxDelay, 77 | _maxFileSizeKB = maxFileSizeKB, 78 | _maxBufferSize = maxBufferSize, 79 | _fileNameFormatter = fileNameFormatter ?? _defaultFileNameFormat, 80 | _writeImmediately = writeImmediately ?? 81 | [ 82 | Level.error, 83 | Level.fatal, 84 | Level.warning, 85 | // ignore: deprecated_member_use_from_same_package 86 | Level.wtf, 87 | ], 88 | _maxRotatedFilesCount = maxRotatedFilesCount, 89 | _fileSorter = fileSorter ?? _defaultFileSorter, 90 | _fileUpdateDuration = fileUpdateDuration, 91 | _file = maxFileSizeKB > 0 ? File('$path/$latestFileName') : File(path); 92 | 93 | /// Logs directory path by default, particular log file path if [_maxFileSizeKB] is 0. 94 | final String _path; 95 | 96 | final bool _overrideExisting; 97 | final Encoding _encoding; 98 | 99 | final List _writeImmediately; 100 | final Duration _maxDelay; 101 | final int _maxFileSizeKB; 102 | final int _maxBufferSize; 103 | final String Function(DateTime timestamp) _fileNameFormatter; 104 | final int? _maxRotatedFilesCount; 105 | final Comparator _fileSorter; 106 | final Duration _fileUpdateDuration; 107 | 108 | final File _file; 109 | IOSink? _sink; 110 | Timer? _bufferFlushTimer; 111 | Timer? _targetFileUpdater; 112 | 113 | final List _buffer = []; 114 | 115 | bool get _rotatingFilesMode => _maxFileSizeKB > 0; 116 | 117 | /// Formats the file with a full date string. 118 | /// 119 | /// Example: 120 | /// * `2024-01-01-10-05-02-123.log` 121 | static String _defaultFileNameFormat(DateTime t) { 122 | return '${t.year}-${t.month.toDigits(2)}-${t.day.toDigits(2)}' 123 | '-${t.hour.toDigits(2)}-${t.minute.toDigits(2)}-${t.second.toDigits(2)}' 124 | '-${t.millisecond.toDigits(3)}.log'; 125 | } 126 | 127 | /// Sort files by their last modified date. 128 | /// This behaviour is inspired by the Log4j PathSorter. 129 | /// 130 | /// This method fulfills the requirements of the [Comparator] interface. 131 | static int _defaultFileSorter(File a, File b) { 132 | return a.lastModifiedSync().compareTo(b.lastModifiedSync()); 133 | } 134 | 135 | @override 136 | Future init() async { 137 | if (_rotatingFilesMode) { 138 | final dir = Directory(_path); 139 | // We use sync directory check to avoid losing potential initial boot logs 140 | // in early crash scenarios. 141 | if (!dir.existsSync()) { 142 | dir.createSync(recursive: true); 143 | } 144 | 145 | _targetFileUpdater = Timer.periodic( 146 | _fileUpdateDuration, 147 | (_) => _updateTargetFile(), 148 | ); 149 | } 150 | 151 | _bufferFlushTimer = Timer.periodic(_maxDelay, (_) => _flushBuffer()); 152 | await _openSink(); 153 | if (_rotatingFilesMode) { 154 | await _updateTargetFile(); // Run first check without waiting for timer tick 155 | } 156 | } 157 | 158 | @override 159 | void output(OutputEvent event) { 160 | _buffer.add(event); 161 | // If event level is present in writeImmediately, flush the complete buffer 162 | // along with any other possible elements that accumulated since 163 | // the last timer tick. Additionally, if the buffer is full. 164 | if (_buffer.length > _maxBufferSize || 165 | _writeImmediately.contains(event.level)) { 166 | _flushBuffer(); 167 | } 168 | } 169 | 170 | void _flushBuffer() { 171 | if (_sink == null) return; // Wait until _sink becomes available 172 | for (final event in _buffer) { 173 | _sink?.writeAll(event.lines, Platform.isWindows ? '\r\n' : '\n'); 174 | _sink?.writeln(); 175 | } 176 | _buffer.clear(); 177 | } 178 | 179 | Future _updateTargetFile() async { 180 | try { 181 | if (await _file.exists() && 182 | await _file.length() > _maxFileSizeKB * 1024) { 183 | // Rotate the log file 184 | await _closeSink(); 185 | await _file.rename('$_path/${_fileNameFormatter(DateTime.now())}'); 186 | await _deleteRotatedFiles(); 187 | await _openSink(); 188 | } 189 | } catch (e, s) { 190 | print(e); 191 | print(s); 192 | // Try creating another file and working with it 193 | await _closeSink(); 194 | await _openSink(); 195 | } 196 | } 197 | 198 | Future _openSink() async { 199 | _sink = _file.openWrite( 200 | mode: _overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend, 201 | encoding: _encoding, 202 | ); 203 | } 204 | 205 | Future _closeSink() async { 206 | await _sink?.flush(); 207 | await _sink?.close(); 208 | _sink = null; // Explicitly set null until assigned again 209 | } 210 | 211 | Future _deleteRotatedFiles() async { 212 | // If maxRotatedFilesCount is not set, keep all files 213 | if (_maxRotatedFilesCount == null) return; 214 | 215 | final dir = Directory(_path); 216 | final files = dir 217 | .listSync() 218 | .whereType() 219 | // Filter out the latest file 220 | .where((f) => f.path != _file.path) 221 | .toList(); 222 | 223 | // If the number of files is less than the limit, don't delete anything 224 | if (files.length <= _maxRotatedFilesCount!) return; 225 | 226 | files.sort(_fileSorter); 227 | 228 | final filesToDelete = 229 | files.sublist(0, files.length - _maxRotatedFilesCount!); 230 | for (final file in filesToDelete) { 231 | try { 232 | await file.delete(); 233 | } catch (e, s) { 234 | print('Failed to delete file: $e'); 235 | print(s); 236 | } 237 | } 238 | } 239 | 240 | @override 241 | Future destroy() async { 242 | _bufferFlushTimer?.cancel(); 243 | _targetFileUpdater?.cancel(); 244 | try { 245 | _flushBuffer(); 246 | } catch (e, s) { 247 | print('Failed to flush buffer before closing the logger: $e'); 248 | print(s); 249 | } 250 | await _closeSink(); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /lib/src/outputs/advanced_file_output_stub.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import '../log_level.dart'; 5 | import '../log_output.dart'; 6 | import '../output_event.dart'; 7 | 8 | /// Accumulates logs in a buffer to reduce frequent disk, writes while optionally 9 | /// switching to a new log file if it reaches a certain size. 10 | /// 11 | /// [AdvancedFileOutput] offer various improvements over the original 12 | /// [FileOutput]: 13 | /// * Managing an internal buffer which collects the logs and only writes 14 | /// them after a certain period of time to the disk. 15 | /// * Dynamically switching log files instead of using a single one specified 16 | /// by the user, when the current file reaches a specified size limit (optionally). 17 | /// 18 | /// The buffered output can significantly reduce the 19 | /// frequency of file writes, which can be beneficial for (micro-)SD storage 20 | /// and other types of low-cost storage (e.g. on IoT devices). Specific log 21 | /// levels can trigger an immediate flush, without waiting for the next timer 22 | /// tick. 23 | /// 24 | /// New log files are created when the current file reaches the specified size 25 | /// limit. This is useful for writing "archives" of telemetry data and logs 26 | /// while keeping them structured. 27 | class AdvancedFileOutput extends LogOutput { 28 | /// Creates a buffered file output. 29 | /// 30 | /// By default, the log is buffered until either the [maxBufferSize] has been 31 | /// reached, the timer controlled by [maxDelay] has been triggered or an 32 | /// [OutputEvent] contains a [writeImmediately] log level. 33 | /// 34 | /// [maxFileSizeKB] controls the log file rotation. The output automatically 35 | /// switches to a new log file as soon as the current file exceeds it. 36 | /// Use -1 to disable log rotation. 37 | /// 38 | /// [maxDelay] describes the maximum amount of time before the buffer has to be 39 | /// written to the file. 40 | /// 41 | /// Any log levels that are specified in [writeImmediately] trigger an immediate 42 | /// flush to the disk ([Level.warning], [Level.error] and [Level.fatal] by default). 43 | /// 44 | /// [path] is either treated as directory for rotating or as target file name, 45 | /// depending on [maxFileSizeKB]. 46 | /// 47 | /// [maxRotatedFilesCount] controls the number of rotated files to keep. By default 48 | /// is null, which means no limit. 49 | /// If set to a positive number, the output will keep the last 50 | /// [maxRotatedFilesCount] files. The deletion step will be executed by sorting 51 | /// files following the [fileSorter] ascending strategy and keeping the last files. 52 | /// The [latestFileName] will not be counted. The default [fileSorter] strategy is 53 | /// sorting by last modified date, beware that could be not reliable in some 54 | /// platforms and/or filesystems. 55 | AdvancedFileOutput({ 56 | required String path, 57 | bool overrideExisting = false, 58 | Encoding encoding = utf8, 59 | List? writeImmediately, 60 | Duration maxDelay = const Duration(seconds: 2), 61 | int maxBufferSize = 2000, 62 | int maxFileSizeKB = 1024, 63 | String latestFileName = 'latest.log', 64 | String Function(DateTime timestamp)? fileNameFormatter, 65 | int? maxRotatedFilesCount, 66 | Comparator? fileSorter, 67 | Duration fileUpdateDuration = const Duration(minutes: 1), 68 | }) { 69 | throw UnsupportedError("Not supported on this platform."); 70 | } 71 | 72 | @override 73 | void output(OutputEvent event) { 74 | throw UnsupportedError("Not supported on this platform."); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/outputs/console_output.dart: -------------------------------------------------------------------------------- 1 | import '../log_output.dart'; 2 | import '../output_event.dart'; 3 | 4 | /// Default implementation of [LogOutput]. 5 | /// 6 | /// It sends everything to the system console. 7 | class ConsoleOutput extends LogOutput { 8 | @override 9 | void output(OutputEvent event) { 10 | event.lines.forEach(print); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/outputs/file_output.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import '../log_output.dart'; 5 | import '../output_event.dart'; 6 | 7 | /// Writes the log output to a file. 8 | class FileOutput extends LogOutput { 9 | final File file; 10 | final bool overrideExisting; 11 | final Encoding encoding; 12 | IOSink? _sink; 13 | 14 | FileOutput({ 15 | required this.file, 16 | this.overrideExisting = false, 17 | this.encoding = utf8, 18 | }); 19 | 20 | @override 21 | Future init() async { 22 | _sink = file.openWrite( 23 | mode: overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend, 24 | encoding: encoding, 25 | ); 26 | } 27 | 28 | @override 29 | void output(OutputEvent event) { 30 | _sink?.writeAll(event.lines, '\n'); 31 | _sink?.writeln(); 32 | } 33 | 34 | @override 35 | Future destroy() async { 36 | await _sink?.flush(); 37 | await _sink?.close(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/outputs/file_output_stub.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import '../log_output.dart'; 5 | import '../output_event.dart'; 6 | 7 | class FileOutput extends LogOutput { 8 | FileOutput({ 9 | required File file, 10 | bool overrideExisting = false, 11 | Encoding encoding = utf8, 12 | }) { 13 | throw UnsupportedError("Not supported on this platform."); 14 | } 15 | 16 | @override 17 | void output(OutputEvent event) { 18 | throw UnsupportedError("Not supported on this platform."); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/outputs/memory_output.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import '../log_output.dart'; 4 | import '../output_event.dart'; 5 | 6 | /// Buffers [OutputEvent]s. 7 | class MemoryOutput extends LogOutput { 8 | /// Maximum events in [buffer]. 9 | final int bufferSize; 10 | 11 | /// A secondary [LogOutput] to also received events. 12 | final LogOutput? secondOutput; 13 | 14 | /// The buffer of events. 15 | final ListQueue buffer; 16 | 17 | MemoryOutput({this.bufferSize = 20, this.secondOutput}) 18 | : buffer = ListQueue(bufferSize); 19 | 20 | @override 21 | void output(OutputEvent event) { 22 | if (buffer.length == bufferSize) { 23 | buffer.removeFirst(); 24 | } 25 | 26 | buffer.add(event); 27 | 28 | secondOutput?.output(event); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/outputs/multi_output.dart: -------------------------------------------------------------------------------- 1 | import '../log_output.dart'; 2 | import '../output_event.dart'; 3 | 4 | /// Logs simultaneously to multiple [LogOutput] outputs. 5 | class MultiOutput extends LogOutput { 6 | late List _outputs; 7 | 8 | MultiOutput(List? outputs) { 9 | _outputs = _normalizeOutputs(outputs); 10 | } 11 | 12 | List _normalizeOutputs(List? outputs) { 13 | final normalizedOutputs = []; 14 | 15 | if (outputs != null) { 16 | for (final output in outputs) { 17 | if (output != null) { 18 | normalizedOutputs.add(output); 19 | } 20 | } 21 | } 22 | 23 | return normalizedOutputs; 24 | } 25 | 26 | @override 27 | Future init() async { 28 | await Future.wait(_outputs.map((e) => e.init())); 29 | } 30 | 31 | @override 32 | void output(OutputEvent event) { 33 | for (var o in _outputs) { 34 | o.output(event); 35 | } 36 | } 37 | 38 | @override 39 | Future destroy() async { 40 | await Future.wait(_outputs.map((e) => e.destroy())); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/outputs/stream_output.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import '../log_output.dart'; 4 | import '../output_event.dart'; 5 | 6 | class StreamOutput extends LogOutput { 7 | late StreamController> _controller; 8 | bool _shouldForward = false; 9 | 10 | StreamOutput() { 11 | _controller = StreamController>( 12 | onListen: () => _shouldForward = true, 13 | onPause: () => _shouldForward = false, 14 | onResume: () => _shouldForward = true, 15 | onCancel: () => _shouldForward = false, 16 | ); 17 | } 18 | 19 | Stream> get stream => _controller.stream; 20 | 21 | @override 22 | void output(OutputEvent event) { 23 | if (!_shouldForward) { 24 | return; 25 | } 26 | 27 | _controller.add(event.lines); 28 | } 29 | 30 | @override 31 | Future destroy() { 32 | return _controller.close(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/printers/hybrid_printer.dart: -------------------------------------------------------------------------------- 1 | import '../log_event.dart'; 2 | import '../log_level.dart'; 3 | import '../log_printer.dart'; 4 | 5 | /// A decorator for a [LogPrinter] that allows for the composition of 6 | /// different printers to handle different log messages. Provide it's 7 | /// constructor with a base printer, but include named parameters for 8 | /// any levels that have a different printer: 9 | /// 10 | /// ``` 11 | /// HybridPrinter(PrettyPrinter(), debug: SimplePrinter()); 12 | /// ``` 13 | /// 14 | /// Will use the pretty printer for all logs except Level.debug 15 | /// logs, which will use SimplePrinter(). 16 | class HybridPrinter extends LogPrinter { 17 | final Map _printerMap; 18 | 19 | HybridPrinter( 20 | LogPrinter realPrinter, { 21 | LogPrinter? debug, 22 | LogPrinter? trace, 23 | @Deprecated('[verbose] is being deprecated in favor of [trace].') 24 | LogPrinter? verbose, 25 | LogPrinter? fatal, 26 | @Deprecated('[wtf] is being deprecated in favor of [fatal].') 27 | LogPrinter? wtf, 28 | LogPrinter? info, 29 | LogPrinter? warning, 30 | LogPrinter? error, 31 | }) : _printerMap = { 32 | Level.debug: debug ?? realPrinter, 33 | Level.trace: trace ?? verbose ?? realPrinter, 34 | Level.fatal: fatal ?? wtf ?? realPrinter, 35 | Level.info: info ?? realPrinter, 36 | Level.warning: warning ?? realPrinter, 37 | Level.error: error ?? realPrinter, 38 | }; 39 | 40 | @override 41 | List log(LogEvent event) => 42 | _printerMap[event.level]?.log(event) ?? []; 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/printers/logfmt_printer.dart: -------------------------------------------------------------------------------- 1 | import '../log_event.dart'; 2 | import '../log_level.dart'; 3 | import '../log_printer.dart'; 4 | 5 | /// Outputs a logfmt message: 6 | /// ``` 7 | /// level=debug msg="hi there" time="2015-03-26T01:27:38-04:00" animal=walrus number=8 tag=usum 8 | /// ``` 9 | class LogfmtPrinter extends LogPrinter { 10 | static final levelPrefixes = { 11 | Level.trace: 'trace', 12 | Level.debug: 'debug', 13 | Level.info: 'info', 14 | Level.warning: 'warning', 15 | Level.error: 'error', 16 | Level.fatal: 'fatal', 17 | }; 18 | 19 | @override 20 | List log(LogEvent event) { 21 | var output = StringBuffer('level=${levelPrefixes[event.level]}'); 22 | if (event.message is String) { 23 | output.write(' msg="${event.message}"'); 24 | } else if (event.message is Map) { 25 | event.message.entries.forEach((entry) { 26 | if (entry.value is num) { 27 | output.write(' ${entry.key}=${entry.value}'); 28 | } else { 29 | output.write(' ${entry.key}="${entry.value}"'); 30 | } 31 | }); 32 | } 33 | if (event.error != null) { 34 | output.write(' error="${event.error}"'); 35 | } 36 | 37 | return [output.toString()]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/printers/prefix_printer.dart: -------------------------------------------------------------------------------- 1 | import '../log_event.dart'; 2 | import '../log_level.dart'; 3 | import '../log_printer.dart'; 4 | 5 | /// A decorator for a [LogPrinter] that allows for the prepending of every 6 | /// line in the log output with a string for the level of that log. For 7 | /// example: 8 | /// 9 | /// ``` 10 | /// PrefixPrinter(PrettyPrinter()); 11 | /// ``` 12 | /// 13 | /// Would prepend "DEBUG" to every line in a debug log. You can supply 14 | /// parameters for a custom message for a specific log level. 15 | class PrefixPrinter extends LogPrinter { 16 | final LogPrinter _realPrinter; 17 | late Map _prefixMap; 18 | 19 | PrefixPrinter( 20 | this._realPrinter, { 21 | String? debug, 22 | String? trace, 23 | @Deprecated('[verbose] is being deprecated in favor of [trace].') verbose, 24 | String? fatal, 25 | @Deprecated('[wtf] is being deprecated in favor of [fatal].') wtf, 26 | String? info, 27 | String? warning, 28 | String? error, 29 | }) { 30 | _prefixMap = { 31 | Level.debug: debug ?? 'DEBUG', 32 | Level.trace: trace ?? verbose ?? 'TRACE', 33 | Level.fatal: fatal ?? wtf ?? 'FATAL', 34 | Level.info: info ?? 'INFO', 35 | Level.warning: warning ?? 'WARNING', 36 | Level.error: error ?? 'ERROR', 37 | }; 38 | 39 | var len = _longestPrefixLength(); 40 | _prefixMap.forEach((k, v) => _prefixMap[k] = '${v.padLeft(len)} '); 41 | } 42 | 43 | @override 44 | List log(LogEvent event) { 45 | var realLogs = _realPrinter.log(event); 46 | return realLogs.map((s) => '${_prefixMap[event.level]}$s').toList(); 47 | } 48 | 49 | int _longestPrefixLength() { 50 | compFunc(String a, String b) => a.length > b.length ? a : b; 51 | return _prefixMap.values.reduce(compFunc).length; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/printers/pretty_printer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:math'; 3 | 4 | import '../ansi_color.dart'; 5 | import '../date_time_format.dart'; 6 | import '../log_event.dart'; 7 | import '../log_level.dart'; 8 | import '../log_printer.dart'; 9 | 10 | /// Default implementation of [LogPrinter]. 11 | /// 12 | /// Output looks like this: 13 | /// ``` 14 | /// ┌────────────────────────── 15 | /// │ Error info 16 | /// ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ 17 | /// │ Method stack history 18 | /// ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ 19 | /// │ Log message 20 | /// └────────────────────────── 21 | /// ``` 22 | class PrettyPrinter extends LogPrinter { 23 | static const topLeftCorner = '┌'; 24 | static const bottomLeftCorner = '└'; 25 | static const middleCorner = '├'; 26 | static const verticalLine = '│'; 27 | static const doubleDivider = '─'; 28 | static const singleDivider = '┄'; 29 | 30 | static final Map defaultLevelColors = { 31 | Level.trace: AnsiColor.fg(AnsiColor.grey(0.5)), 32 | Level.debug: const AnsiColor.none(), 33 | Level.info: const AnsiColor.fg(12), 34 | Level.warning: const AnsiColor.fg(208), 35 | Level.error: const AnsiColor.fg(196), 36 | Level.fatal: const AnsiColor.fg(199), 37 | }; 38 | 39 | static final Map defaultLevelEmojis = { 40 | Level.trace: '', 41 | Level.debug: '🐛', 42 | Level.info: '💡', 43 | Level.warning: '⚠️', 44 | Level.error: '⛔', 45 | Level.fatal: '👾', 46 | }; 47 | 48 | /// Matches a stacktrace line as generated on Android/iOS devices. 49 | /// 50 | /// For example: 51 | /// * #1 Logger.log (package:logger/src/logger.dart:115:29) 52 | static final _deviceStackTraceRegex = RegExp(r'#[0-9]+\s+(.+) \((\S+)\)'); 53 | 54 | /// Matches a stacktrace line as generated by Flutter web. 55 | /// 56 | /// For example: 57 | /// * packages/logger/src/printers/pretty_printer.dart 91:37 58 | static final _webStackTraceRegex = RegExp(r'^((packages|dart-sdk)/\S+/)'); 59 | 60 | /// Matches a stacktrace line as generated by browser Dart. 61 | /// 62 | /// For example: 63 | /// * dart:sdk_internal 64 | /// * package:logger/src/logger.dart 65 | static final _browserStackTraceRegex = 66 | RegExp(r'^(?:package:)?(dart:\S+|\S+)'); 67 | 68 | static DateTime? startTime; 69 | 70 | /// The index at which the stack trace should start. 71 | /// 72 | /// This can be useful if, for instance, Logger is wrapped in another class and 73 | /// you wish to remove these wrapped calls from stack trace 74 | /// 75 | /// See also: 76 | /// * [excludePaths] 77 | final int stackTraceBeginIndex; 78 | 79 | /// Controls the method count in stack traces 80 | /// when no [LogEvent.error] was provided. 81 | /// 82 | /// In case no [LogEvent.stackTrace] was provided, 83 | /// [StackTrace.current] will be used to create one. 84 | /// 85 | /// * Set to `0` in order to disable printing a stack trace 86 | /// without an error parameter. 87 | /// * Set to `null` to remove the method count limit all together. 88 | /// 89 | /// See also: 90 | /// * [errorMethodCount] 91 | final int? methodCount; 92 | 93 | /// Controls the method count in stack traces 94 | /// when [LogEvent.error] was provided. 95 | /// 96 | /// In case no [LogEvent.stackTrace] was provided, 97 | /// [StackTrace.current] will be used to create one. 98 | /// 99 | /// * Set to `0` in order to disable printing a stack trace 100 | /// in case of an error parameter. 101 | /// * Set to `null` to remove the method count limit all together. 102 | /// 103 | /// See also: 104 | /// * [methodCount] 105 | final int? errorMethodCount; 106 | 107 | /// Controls the length of the divider lines. 108 | final int lineLength; 109 | 110 | /// Whether ansi colors are used to color the output. 111 | final bool colors; 112 | 113 | /// Whether emojis are prefixed to the log line. 114 | final bool printEmojis; 115 | 116 | /// Whether [LogEvent.time] is printed. 117 | @Deprecated("Use `dateTimeFormat` instead.") 118 | bool get printTime => dateTimeFormat != DateTimeFormat.none; 119 | 120 | /// Controls the format of [LogEvent.time]. 121 | final DateTimeFormatter dateTimeFormat; 122 | 123 | /// Controls the ascii 'boxing' of different [Level]s. 124 | /// 125 | /// By default all levels are 'boxed', 126 | /// to prevent 'boxing' of a specific level, 127 | /// include it with `true` in the map. 128 | /// 129 | /// Example to prevent boxing of [Level.trace] and [Level.info]: 130 | /// ```dart 131 | /// excludeBox: { 132 | /// Level.trace: true, 133 | /// Level.info: true, 134 | /// }, 135 | /// ``` 136 | /// 137 | /// See also: 138 | /// * [noBoxingByDefault] 139 | final Map excludeBox; 140 | 141 | /// Whether the implicit `bool`s in [excludeBox] are `true` or `false` by default. 142 | /// 143 | /// By default all levels are 'boxed', 144 | /// this flips the default to no boxing for all levels. 145 | /// Individual boxing can still be turned on for specific 146 | /// levels by setting them manually to `false` in [excludeBox]. 147 | /// 148 | /// Example to specifically activate 'boxing' of [Level.error]: 149 | /// ```dart 150 | /// noBoxingByDefault: true, 151 | /// excludeBox: { 152 | /// Level.error: false, 153 | /// }, 154 | /// ``` 155 | /// 156 | /// See also: 157 | /// * [excludeBox] 158 | final bool noBoxingByDefault; 159 | 160 | /// A list of custom paths that are excluded from the stack trace. 161 | /// 162 | /// For example, to exclude your `MyLog` util that redirects to this logger: 163 | /// ```dart 164 | /// excludePaths: [ 165 | /// // To exclude a whole package 166 | /// "package:test", 167 | /// // To exclude a single file 168 | /// "package:test/util/my_log.dart", 169 | /// ], 170 | /// ``` 171 | /// 172 | /// See also: 173 | /// * [stackTraceBeginIndex] 174 | final List excludePaths; 175 | 176 | /// Contains the parsed rules resulting from [excludeBox] and [noBoxingByDefault]. 177 | late final Map _includeBox; 178 | String _topBorder = ''; 179 | String _middleBorder = ''; 180 | String _bottomBorder = ''; 181 | 182 | /// Controls the colors used for the different log levels. 183 | /// 184 | /// Default fallbacks are modifiable via [defaultLevelColors]. 185 | final Map? levelColors; 186 | 187 | /// Controls the emojis used for the different log levels. 188 | /// 189 | /// Default fallbacks are modifiable via [defaultLevelEmojis]. 190 | final Map? levelEmojis; 191 | 192 | PrettyPrinter({ 193 | this.stackTraceBeginIndex = 0, 194 | this.methodCount = 2, 195 | this.errorMethodCount = 8, 196 | this.lineLength = 120, 197 | this.colors = true, 198 | this.printEmojis = true, 199 | @Deprecated( 200 | "Use `dateTimeFormat` with `DateTimeFormat.onlyTimeAndSinceStart` or `DateTimeFormat.none` instead.") 201 | bool? printTime, 202 | DateTimeFormatter dateTimeFormat = DateTimeFormat.none, 203 | this.excludeBox = const {}, 204 | this.noBoxingByDefault = false, 205 | this.excludePaths = const [], 206 | this.levelColors, 207 | this.levelEmojis, 208 | }) : assert( 209 | (printTime != null && dateTimeFormat == DateTimeFormat.none) || 210 | printTime == null, 211 | "Don't set printTime when using dateTimeFormat"), 212 | dateTimeFormat = printTime == null 213 | ? dateTimeFormat 214 | : (printTime 215 | ? DateTimeFormat.onlyTimeAndSinceStart 216 | : DateTimeFormat.none) { 217 | startTime ??= DateTime.now(); 218 | 219 | var doubleDividerLine = StringBuffer(); 220 | var singleDividerLine = StringBuffer(); 221 | for (var i = 0; i < lineLength - 1; i++) { 222 | doubleDividerLine.write(doubleDivider); 223 | singleDividerLine.write(singleDivider); 224 | } 225 | 226 | _topBorder = '$topLeftCorner$doubleDividerLine'; 227 | _middleBorder = '$middleCorner$singleDividerLine'; 228 | _bottomBorder = '$bottomLeftCorner$doubleDividerLine'; 229 | 230 | // Translate excludeBox map (constant if default) to includeBox map with all Level enum possibilities 231 | _includeBox = {}; 232 | for (var l in Level.values) { 233 | _includeBox[l] = !noBoxingByDefault; 234 | } 235 | excludeBox.forEach((k, v) => _includeBox[k] = !v); 236 | } 237 | 238 | @override 239 | List log(LogEvent event) { 240 | var messageStr = stringifyMessage(event.message); 241 | 242 | String? stackTraceStr; 243 | if (event.error != null) { 244 | if ((errorMethodCount == null || errorMethodCount! > 0)) { 245 | stackTraceStr = formatStackTrace( 246 | event.stackTrace ?? StackTrace.current, 247 | errorMethodCount, 248 | ); 249 | } 250 | } else if (methodCount == null || methodCount! > 0) { 251 | stackTraceStr = formatStackTrace( 252 | event.stackTrace ?? StackTrace.current, 253 | methodCount, 254 | ); 255 | } 256 | 257 | var errorStr = event.error?.toString(); 258 | 259 | String? timeStr; 260 | // Keep backwards-compatibility to `printTime` check 261 | // ignore: deprecated_member_use_from_same_package 262 | if (printTime) { 263 | timeStr = getTime(event.time); 264 | } 265 | 266 | return _formatAndPrint( 267 | event.level, 268 | messageStr, 269 | timeStr, 270 | errorStr, 271 | stackTraceStr, 272 | ); 273 | } 274 | 275 | String? formatStackTrace(StackTrace? stackTrace, int? methodCount) { 276 | List lines = stackTrace 277 | .toString() 278 | .split('\n') 279 | .where( 280 | (line) => 281 | !_discardDeviceStacktraceLine(line) && 282 | !_discardWebStacktraceLine(line) && 283 | !_discardBrowserStacktraceLine(line) && 284 | line.isNotEmpty, 285 | ) 286 | .toList(); 287 | List formatted = []; 288 | 289 | int stackTraceLength = 290 | (methodCount != null ? min(lines.length, methodCount) : lines.length); 291 | for (int count = 0; count < stackTraceLength; count++) { 292 | var line = lines[count]; 293 | if (count < stackTraceBeginIndex) { 294 | continue; 295 | } 296 | formatted.add('#$count ${line.replaceFirst(RegExp(r'#\d+\s+'), '')}'); 297 | } 298 | 299 | if (formatted.isEmpty) { 300 | return null; 301 | } else { 302 | return formatted.join('\n'); 303 | } 304 | } 305 | 306 | bool _isInExcludePaths(String segment) { 307 | for (var element in excludePaths) { 308 | if (segment.startsWith(element)) { 309 | return true; 310 | } 311 | } 312 | return false; 313 | } 314 | 315 | bool _discardDeviceStacktraceLine(String line) { 316 | var match = _deviceStackTraceRegex.matchAsPrefix(line); 317 | if (match == null) { 318 | return false; 319 | } 320 | final segment = match.group(2)!; 321 | if (segment.startsWith('package:logger')) { 322 | return true; 323 | } 324 | return _isInExcludePaths(segment); 325 | } 326 | 327 | bool _discardWebStacktraceLine(String line) { 328 | var match = _webStackTraceRegex.matchAsPrefix(line); 329 | if (match == null) { 330 | return false; 331 | } 332 | final segment = match.group(1)!; 333 | if (segment.startsWith('packages/logger') || 334 | segment.startsWith('dart-sdk/lib')) { 335 | return true; 336 | } 337 | return _isInExcludePaths(segment); 338 | } 339 | 340 | bool _discardBrowserStacktraceLine(String line) { 341 | var match = _browserStackTraceRegex.matchAsPrefix(line); 342 | if (match == null) { 343 | return false; 344 | } 345 | final segment = match.group(1)!; 346 | if (segment.startsWith('package:logger') || segment.startsWith('dart:')) { 347 | return true; 348 | } 349 | return _isInExcludePaths(segment); 350 | } 351 | 352 | String getTime(DateTime time) { 353 | return dateTimeFormat(time); 354 | } 355 | 356 | // Handles any object that is causing JsonEncoder() problems 357 | Object toEncodableFallback(dynamic object) { 358 | return object.toString(); 359 | } 360 | 361 | String stringifyMessage(dynamic message) { 362 | final finalMessage = message is Function ? message() : message; 363 | if (finalMessage is Map || finalMessage is Iterable) { 364 | var encoder = JsonEncoder.withIndent(' ', toEncodableFallback); 365 | return encoder.convert(finalMessage); 366 | } else { 367 | return finalMessage.toString(); 368 | } 369 | } 370 | 371 | AnsiColor _getLevelColor(Level level) { 372 | AnsiColor? color; 373 | if (colors) { 374 | color = levelColors?[level] ?? defaultLevelColors[level]; 375 | } 376 | return color ?? const AnsiColor.none(); 377 | } 378 | 379 | String _getEmoji(Level level) { 380 | if (printEmojis) { 381 | final String? emoji = levelEmojis?[level] ?? defaultLevelEmojis[level]; 382 | if (emoji != null) { 383 | return '$emoji '; 384 | } 385 | } 386 | return ''; 387 | } 388 | 389 | List _formatAndPrint( 390 | Level level, 391 | String message, 392 | String? time, 393 | String? error, 394 | String? stacktrace, 395 | ) { 396 | List buffer = []; 397 | var verticalLineAtLevel = (_includeBox[level]!) ? ('$verticalLine ') : ''; 398 | var color = _getLevelColor(level); 399 | if (_includeBox[level]!) buffer.add(color(_topBorder)); 400 | 401 | if (error != null) { 402 | for (var line in error.split('\n')) { 403 | buffer.add(color('$verticalLineAtLevel$line')); 404 | } 405 | if (_includeBox[level]!) buffer.add(color(_middleBorder)); 406 | } 407 | 408 | if (stacktrace != null) { 409 | for (var line in stacktrace.split('\n')) { 410 | buffer.add(color('$verticalLineAtLevel$line')); 411 | } 412 | if (_includeBox[level]!) buffer.add(color(_middleBorder)); 413 | } 414 | 415 | if (time != null) { 416 | buffer.add(color('$verticalLineAtLevel$time')); 417 | if (_includeBox[level]!) buffer.add(color(_middleBorder)); 418 | } 419 | 420 | var emoji = _getEmoji(level); 421 | for (var line in message.split('\n')) { 422 | buffer.add(color('$verticalLineAtLevel$emoji$line')); 423 | } 424 | if (_includeBox[level]!) buffer.add(color(_bottomBorder)); 425 | 426 | return buffer; 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /lib/src/printers/simple_printer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import '../ansi_color.dart'; 4 | import '../log_event.dart'; 5 | import '../log_level.dart'; 6 | import '../log_printer.dart'; 7 | 8 | /// Outputs simple log messages: 9 | /// ``` 10 | /// [E] Log message ERROR: Error info 11 | /// ``` 12 | class SimplePrinter extends LogPrinter { 13 | static final levelPrefixes = { 14 | Level.trace: '[T]', 15 | Level.debug: '[D]', 16 | Level.info: '[I]', 17 | Level.warning: '[W]', 18 | Level.error: '[E]', 19 | Level.fatal: '[FATAL]', 20 | }; 21 | 22 | static final levelColors = { 23 | Level.trace: AnsiColor.fg(AnsiColor.grey(0.5)), 24 | Level.debug: const AnsiColor.none(), 25 | Level.info: const AnsiColor.fg(12), 26 | Level.warning: const AnsiColor.fg(208), 27 | Level.error: const AnsiColor.fg(196), 28 | Level.fatal: const AnsiColor.fg(199), 29 | }; 30 | 31 | final bool printTime; 32 | final bool colors; 33 | 34 | SimplePrinter({this.printTime = false, this.colors = true}); 35 | 36 | @override 37 | List log(LogEvent event) { 38 | var messageStr = _stringifyMessage(event.message); 39 | var errorStr = event.error != null ? ' ERROR: ${event.error}' : ''; 40 | var timeStr = printTime ? 'TIME: ${event.time.toIso8601String()}' : ''; 41 | return ['${_labelFor(event.level)} $timeStr $messageStr$errorStr']; 42 | } 43 | 44 | String _labelFor(Level level) { 45 | var prefix = levelPrefixes[level]!; 46 | var color = levelColors[level]!; 47 | 48 | return colors ? color(prefix) : prefix; 49 | } 50 | 51 | String _stringifyMessage(dynamic message) { 52 | final finalMessage = message is Function ? message() : message; 53 | if (finalMessage is Map || finalMessage is Iterable) { 54 | var encoder = const JsonEncoder.withIndent(null); 55 | return encoder.convert(finalMessage); 56 | } else { 57 | return finalMessage.toString(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/web.dart: -------------------------------------------------------------------------------- 1 | /// Web-safe logger. 2 | library web; 3 | 4 | export 'src/ansi_color.dart'; 5 | export 'src/date_time_format.dart'; 6 | export 'src/filters/development_filter.dart'; 7 | export 'src/filters/production_filter.dart'; 8 | export 'src/log_event.dart'; 9 | export 'src/log_filter.dart'; 10 | export 'src/log_level.dart'; 11 | export 'src/log_output.dart'; 12 | export 'src/log_printer.dart'; 13 | export 'src/logger.dart'; 14 | export 'src/output_event.dart'; 15 | export 'src/outputs/console_output.dart'; 16 | export 'src/outputs/memory_output.dart'; 17 | export 'src/outputs/multi_output.dart'; 18 | export 'src/outputs/stream_output.dart'; 19 | export 'src/printers/hybrid_printer.dart'; 20 | export 'src/printers/logfmt_printer.dart'; 21 | export 'src/printers/prefix_printer.dart'; 22 | export 'src/printers/pretty_printer.dart'; 23 | export 'src/printers/simple_printer.dart'; 24 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: logger 2 | description: Small, easy to use and extensible logger which prints beautiful logs. 3 | version: 2.5.0 4 | repository: https://github.com/SourceHorizon/logger 5 | 6 | topics: 7 | - cli 8 | - logging 9 | 10 | environment: 11 | sdk: ">=2.17.0 <4.0.0" 12 | 13 | dev_dependencies: 14 | test: ^1.16.8 15 | lints: ^2.0.1 16 | 17 | platforms: 18 | android: 19 | ios: 20 | linux: 21 | macos: 22 | web: 23 | windows: 24 | -------------------------------------------------------------------------------- /test/logger_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:logger/logger.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | typedef PrinterCallback = List Function( 7 | Level level, 8 | dynamic message, 9 | Object? error, 10 | StackTrace? stackTrace, 11 | ); 12 | 13 | class _AlwaysFilter extends LogFilter { 14 | @override 15 | bool shouldLog(LogEvent event) => true; 16 | } 17 | 18 | class _NeverFilter extends LogFilter { 19 | @override 20 | bool shouldLog(LogEvent event) => false; 21 | } 22 | 23 | class _CallbackPrinter extends LogPrinter { 24 | final PrinterCallback callback; 25 | 26 | _CallbackPrinter(this.callback); 27 | 28 | @override 29 | List log(LogEvent event) { 30 | return callback( 31 | event.level, 32 | event.message, 33 | event.error, 34 | event.stackTrace, 35 | ); 36 | } 37 | } 38 | 39 | class _AsyncFilter extends LogFilter { 40 | final Duration delay; 41 | bool initialized = false; 42 | 43 | _AsyncFilter(this.delay); 44 | 45 | @override 46 | Future init() async { 47 | await Future.delayed(delay); 48 | initialized = true; 49 | } 50 | 51 | @override 52 | bool shouldLog(LogEvent event) => false; 53 | } 54 | 55 | class _AsyncPrinter extends LogPrinter { 56 | final Duration delay; 57 | bool initialized = false; 58 | 59 | _AsyncPrinter(this.delay); 60 | 61 | @override 62 | Future init() async { 63 | await Future.delayed(delay); 64 | initialized = true; 65 | } 66 | 67 | @override 68 | List log(LogEvent event) => [event.message.toString()]; 69 | } 70 | 71 | class _AsyncOutput extends LogOutput { 72 | final Duration delay; 73 | bool initialized = false; 74 | 75 | _AsyncOutput(this.delay); 76 | 77 | @override 78 | Future init() async { 79 | await Future.delayed(delay); 80 | initialized = true; 81 | } 82 | 83 | @override 84 | void output(OutputEvent event) { 85 | // No-op. 86 | } 87 | } 88 | 89 | /// Test class for the lazy-initialization of variables. 90 | class LazyLogger { 91 | static bool? printed; 92 | static final filter = ProductionFilter(); 93 | static final printer = _CallbackPrinter((l, m, e, s) { 94 | printed = true; 95 | return []; 96 | }); 97 | static final logger = Logger(filter: filter, printer: printer); 98 | } 99 | 100 | void main() { 101 | Level? printedLevel; 102 | dynamic printedMessage; 103 | dynamic printedError; 104 | StackTrace? printedStackTrace; 105 | var callbackPrinter = _CallbackPrinter((l, m, e, s) { 106 | printedLevel = l; 107 | printedMessage = m; 108 | printedError = e; 109 | printedStackTrace = s; 110 | return []; 111 | }); 112 | 113 | setUp(() { 114 | printedLevel = null; 115 | printedMessage = null; 116 | printedError = null; 117 | printedStackTrace = null; 118 | }); 119 | 120 | test('Logger.log', () { 121 | var logger = Logger(filter: _NeverFilter(), printer: callbackPrinter); 122 | logger.log(Level.debug, 'Some message'); 123 | 124 | expect(printedMessage, null); 125 | 126 | logger = Logger(filter: _AlwaysFilter(), printer: callbackPrinter); 127 | 128 | var levels = [ 129 | Level.trace, 130 | Level.debug, 131 | Level.info, 132 | Level.warning, 133 | Level.error, 134 | Level.fatal, 135 | ]; 136 | for (var level in levels) { 137 | var message = Random().nextInt(999999999).toString(); 138 | logger.log(level, message); 139 | expect(printedLevel, level); 140 | expect(printedMessage, message); 141 | expect(printedError, null); 142 | expect(printedStackTrace, null); 143 | 144 | message = Random().nextInt(999999999).toString(); 145 | logger.log(level, message, error: 'MyError'); 146 | expect(printedLevel, level); 147 | expect(printedMessage, message); 148 | expect(printedError, 'MyError'); 149 | expect(printedStackTrace, null); 150 | 151 | message = Random().nextInt(999999999).toString(); 152 | var stackTrace = StackTrace.current; 153 | logger.log(level, message, error: 'MyError', stackTrace: stackTrace); 154 | expect(printedLevel, level); 155 | expect(printedMessage, message); 156 | expect(printedError, 'MyError'); 157 | expect(printedStackTrace, stackTrace); 158 | } 159 | 160 | expect(() => logger.log(Level.trace, 'Test', error: StackTrace.current), 161 | throwsArgumentError); 162 | expect(() => logger.log(Level.off, 'Test'), throwsArgumentError); 163 | expect(() => logger.log(Level.all, 'Test'), throwsArgumentError); 164 | }); 165 | 166 | test('Multiple Loggers', () { 167 | var logger = Logger(level: Level.info, printer: callbackPrinter); 168 | var secondLogger = Logger(level: Level.debug, printer: callbackPrinter); 169 | 170 | logger.log(Level.debug, 'Test'); 171 | expect(printedLevel, null); 172 | expect(printedMessage, null); 173 | expect(printedError, null); 174 | expect(printedStackTrace, null); 175 | 176 | secondLogger.log(Level.debug, 'Test'); 177 | expect(printedLevel, Level.debug); 178 | expect(printedMessage, 'Test'); 179 | expect(printedError, null); 180 | expect(printedStackTrace, null); 181 | }); 182 | 183 | test('Logger.t', () { 184 | var logger = Logger(filter: _AlwaysFilter(), printer: callbackPrinter); 185 | var stackTrace = StackTrace.current; 186 | logger.t('Test', error: 'Error', stackTrace: stackTrace); 187 | expect(printedLevel, Level.trace); 188 | expect(printedMessage, 'Test'); 189 | expect(printedError, 'Error'); 190 | expect(printedStackTrace, stackTrace); 191 | }); 192 | 193 | test('Logger.d', () { 194 | var logger = Logger(filter: _AlwaysFilter(), printer: callbackPrinter); 195 | var stackTrace = StackTrace.current; 196 | logger.d('Test', error: 'Error', stackTrace: stackTrace); 197 | expect(printedLevel, Level.debug); 198 | expect(printedMessage, 'Test'); 199 | expect(printedError, 'Error'); 200 | expect(printedStackTrace, stackTrace); 201 | }); 202 | 203 | test('Logger.i', () { 204 | var logger = Logger(filter: _AlwaysFilter(), printer: callbackPrinter); 205 | var stackTrace = StackTrace.current; 206 | logger.i('Test', error: 'Error', stackTrace: stackTrace); 207 | expect(printedLevel, Level.info); 208 | expect(printedMessage, 'Test'); 209 | expect(printedError, 'Error'); 210 | expect(printedStackTrace, stackTrace); 211 | }); 212 | 213 | test('Logger.w', () { 214 | var logger = Logger(filter: _AlwaysFilter(), printer: callbackPrinter); 215 | var stackTrace = StackTrace.current; 216 | logger.w('Test', error: 'Error', stackTrace: stackTrace); 217 | expect(printedLevel, Level.warning); 218 | expect(printedMessage, 'Test'); 219 | expect(printedError, 'Error'); 220 | expect(printedStackTrace, stackTrace); 221 | }); 222 | 223 | test('Logger.e', () { 224 | var logger = Logger(filter: _AlwaysFilter(), printer: callbackPrinter); 225 | var stackTrace = StackTrace.current; 226 | logger.e('Test', error: 'Error', stackTrace: stackTrace); 227 | expect(printedLevel, Level.error); 228 | expect(printedMessage, 'Test'); 229 | expect(printedError, 'Error'); 230 | expect(printedStackTrace, stackTrace); 231 | }); 232 | 233 | test('Logger.f', () { 234 | var logger = Logger(filter: _AlwaysFilter(), printer: callbackPrinter); 235 | var stackTrace = StackTrace.current; 236 | logger.f('Test', error: 'Error', stackTrace: stackTrace); 237 | expect(printedLevel, Level.fatal); 238 | expect(printedMessage, 'Test'); 239 | expect(printedError, 'Error'); 240 | expect(printedStackTrace, stackTrace); 241 | }); 242 | 243 | test('setting log level above log level of message', () { 244 | printedMessage = null; 245 | var logger = Logger( 246 | filter: ProductionFilter(), 247 | printer: callbackPrinter, 248 | level: Level.warning, 249 | ); 250 | 251 | logger.d('This isn\'t logged'); 252 | expect(printedMessage, isNull); 253 | 254 | logger.w('This is'); 255 | expect(printedMessage, 'This is'); 256 | }); 257 | 258 | test('Setting filter Levels', () { 259 | var filter = ProductionFilter(); 260 | expect(filter.level, Logger.level); 261 | 262 | final initLevel = Level.warning; 263 | // ignore: unused_local_variable 264 | var logger = Logger( 265 | filter: filter, 266 | printer: callbackPrinter, 267 | level: initLevel, 268 | ); 269 | expect(filter.level, initLevel); 270 | 271 | filter.level = Level.fatal; 272 | expect(filter.level, Level.fatal); 273 | }); 274 | 275 | test('Logger.close', () async { 276 | var logger = Logger(); 277 | expect(logger.isClosed(), false); 278 | await logger.close(); 279 | expect(logger.isClosed(), true); 280 | }); 281 | 282 | test('Lazy Logger Initialization', () { 283 | expect(LazyLogger.printed, isNull); 284 | LazyLogger.filter.level = Level.warning; 285 | LazyLogger.logger.i("This is an info message and should not show"); 286 | expect(LazyLogger.printed, isNull); 287 | }); 288 | 289 | test('Async Filter Initialization', () async { 290 | var comp = _AsyncFilter(const Duration(milliseconds: 100)); 291 | var logger = Logger( 292 | filter: comp, 293 | ); 294 | 295 | expect(comp.initialized, false); 296 | await Future.delayed(const Duration(milliseconds: 50)); 297 | expect(comp.initialized, false); 298 | await logger.init; 299 | expect(comp.initialized, true); 300 | }); 301 | 302 | test('Async Printer Initialization', () async { 303 | var comp = _AsyncPrinter(const Duration(milliseconds: 100)); 304 | var logger = Logger( 305 | printer: comp, 306 | ); 307 | 308 | expect(comp.initialized, false); 309 | await Future.delayed(const Duration(milliseconds: 50)); 310 | expect(comp.initialized, false); 311 | await logger.init; 312 | expect(comp.initialized, true); 313 | }); 314 | 315 | test('Async Output Initialization', () async { 316 | var comp = _AsyncOutput(const Duration(milliseconds: 100)); 317 | var logger = Logger( 318 | output: comp, 319 | ); 320 | 321 | expect(comp.initialized, false); 322 | await Future.delayed(const Duration(milliseconds: 50)); 323 | expect(comp.initialized, false); 324 | await logger.init; 325 | expect(comp.initialized, true); 326 | }); 327 | } 328 | -------------------------------------------------------------------------------- /test/outputs/advanced_file_output_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:logger/logger.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | var file = File("${Directory.systemTemp.path}/dart_advanced_logger_test.log"); 8 | var dir = Directory("${Directory.systemTemp.path}/dart_advanced_logger_dir"); 9 | setUp(() async { 10 | await file.create(recursive: true); 11 | await dir.create(recursive: true); 12 | }); 13 | 14 | tearDown(() async { 15 | await file.delete(); 16 | await dir.delete(recursive: true); 17 | }); 18 | 19 | test('Real file read and write with buffer accumulation', () async { 20 | var output = AdvancedFileOutput( 21 | path: file.path, 22 | maxDelay: const Duration(milliseconds: 500), 23 | maxFileSizeKB: 0, 24 | ); 25 | await output.init(); 26 | 27 | final event0 = OutputEvent(LogEvent(Level.info, ""), ["First event"]); 28 | final event1 = OutputEvent(LogEvent(Level.info, ""), ["Second event"]); 29 | final event2 = OutputEvent(LogEvent(Level.info, ""), ["Third event"]); 30 | 31 | output.output(event0); 32 | output.output(event1); 33 | output.output(event2); 34 | 35 | // Wait until buffer is flushed to file 36 | await Future.delayed(const Duration(seconds: 1)); 37 | 38 | await output.destroy(); 39 | 40 | var content = await file.readAsString(); 41 | expect( 42 | content, 43 | allOf( 44 | contains("First event"), 45 | contains("Second event"), 46 | contains("Third event"), 47 | ), 48 | ); 49 | }); 50 | 51 | test('Real file read and write with rotating file names and immediate output', 52 | () async { 53 | var output = AdvancedFileOutput( 54 | path: dir.path, 55 | writeImmediately: [Level.info], 56 | ); 57 | await output.init(); 58 | 59 | final event0 = OutputEvent(LogEvent(Level.info, ""), ["First event"]); 60 | final event1 = OutputEvent(LogEvent(Level.info, ""), ["Second event"]); 61 | final event2 = OutputEvent(LogEvent(Level.info, ""), ["Third event"]); 62 | 63 | output.output(event0); 64 | output.output(event1); 65 | output.output(event2); 66 | 67 | await output.destroy(); 68 | 69 | final logFile = File('${dir.path}/latest.log'); 70 | var content = await logFile.readAsString(); 71 | expect( 72 | content, 73 | allOf( 74 | contains("First event"), 75 | contains("Second event"), 76 | contains("Third event"), 77 | ), 78 | ); 79 | }); 80 | 81 | test('Rolling files', () async { 82 | var output = AdvancedFileOutput( 83 | path: dir.path, 84 | maxFileSizeKB: 1, 85 | ); 86 | await output.init(); 87 | final event0 = OutputEvent(LogEvent(Level.fatal, ""), ["1" * 1500]); 88 | output.output(event0); 89 | await output.destroy(); 90 | 91 | // Start again to roll files on init without waiting for timer tick 92 | await output.init(); 93 | final event1 = OutputEvent(LogEvent(Level.fatal, ""), ["2" * 1500]); 94 | output.output(event1); 95 | await output.destroy(); 96 | 97 | // And again for another roll 98 | await output.init(); 99 | final event2 = OutputEvent(LogEvent(Level.fatal, ""), ["3" * 1500]); 100 | output.output(event2); 101 | await output.destroy(); 102 | 103 | final files = dir.listSync(); 104 | 105 | expect( 106 | files, 107 | (hasLength(3)), 108 | ); 109 | }); 110 | 111 | test('Rolling files with rotated files deletion', () async { 112 | var output = AdvancedFileOutput( 113 | path: dir.path, 114 | maxFileSizeKB: 1, 115 | maxRotatedFilesCount: 1, 116 | ); 117 | 118 | await output.init(); 119 | final event0 = OutputEvent(LogEvent(Level.fatal, ""), ["1" * 1500]); 120 | output.output(event0); 121 | await output.destroy(); 122 | 123 | // TODO Find out why test is so flaky with durations <1000ms 124 | // Give the OS a chance to flush to the file system (should reduce flakiness) 125 | await Future.delayed(const Duration(milliseconds: 1000)); 126 | 127 | // Start again to roll files on init without waiting for timer tick 128 | await output.init(); 129 | final event1 = OutputEvent(LogEvent(Level.fatal, ""), ["2" * 1500]); 130 | output.output(event1); 131 | await output.destroy(); 132 | 133 | await Future.delayed(const Duration(milliseconds: 1000)); 134 | 135 | // And again for another roll 136 | await output.init(); 137 | final event2 = OutputEvent(LogEvent(Level.fatal, ""), ["3" * 1500]); 138 | output.output(event2); 139 | await output.destroy(); 140 | 141 | await Future.delayed(const Duration(milliseconds: 1000)); 142 | 143 | final files = dir.listSync(); 144 | 145 | // Expect only 2 files: the "latest" that is the current log file 146 | // and only one rotated file. The first created file should be deleted. 147 | expect(files, hasLength(2)); 148 | final latestFile = File('${dir.path}/latest.log'); 149 | final rotatedFile = dir 150 | .listSync() 151 | .whereType() 152 | .firstWhere((file) => file.path != latestFile.path); 153 | expect(await latestFile.readAsString(), contains("3")); 154 | expect(await rotatedFile.readAsString(), contains("2")); 155 | }); 156 | 157 | test('Rolling files with custom file sorter', () async { 158 | var output = AdvancedFileOutput( 159 | path: dir.path, 160 | maxFileSizeKB: 1, 161 | maxRotatedFilesCount: 1, 162 | // Define a custom file sorter that sorts files by their length 163 | // (strange behavior for testing purposes) from the longest to 164 | // the shortest: the longest file should be deleted first. 165 | fileSorter: (a, b) => b.lengthSync().compareTo(a.lengthSync()), 166 | ); 167 | 168 | await output.init(); 169 | final event0 = OutputEvent(LogEvent(Level.fatal, ""), ["1" * 1500]); 170 | output.output(event0); 171 | await output.destroy(); 172 | 173 | // Start again to roll files on init without waiting for timer tick 174 | await output.init(); 175 | // Create a second file with a greater length (it should be deleted first) 176 | final event1 = OutputEvent(LogEvent(Level.fatal, ""), ["2" * 3000]); 177 | output.output(event1); 178 | await output.destroy(); 179 | 180 | // Give the OS a chance to flush to the file system (should reduce flakiness) 181 | await Future.delayed(const Duration(milliseconds: 50)); 182 | 183 | // And again for another roll 184 | await output.init(); 185 | final event2 = OutputEvent(LogEvent(Level.fatal, ""), ["3" * 1500]); 186 | output.output(event2); 187 | await output.destroy(); 188 | 189 | final files = dir.listSync(); 190 | 191 | // Expect only 2 files: the "latest" that is the current log file 192 | // and only one rotated file (the shortest one). 193 | expect(files, hasLength(2)); 194 | final latestFile = File('${dir.path}/latest.log'); 195 | final rotatedFile = dir 196 | .listSync() 197 | .whereType() 198 | .firstWhere((file) => file.path != latestFile.path); 199 | expect(await latestFile.readAsString(), contains("3")); 200 | expect(await rotatedFile.readAsString(), contains("1")); 201 | }); 202 | 203 | test('Flush temporary buffer on destroy', () async { 204 | var output = AdvancedFileOutput(path: dir.path); 205 | await output.init(); 206 | 207 | final event0 = OutputEvent(LogEvent(Level.info, ""), ["Last event"]); 208 | final event1 = OutputEvent(LogEvent(Level.info, ""), ["Very last event"]); 209 | 210 | output.output(event0); 211 | output.output(event1); 212 | 213 | await output.destroy(); 214 | 215 | final logFile = File('${dir.path}/latest.log'); 216 | var content = await logFile.readAsString(); 217 | expect( 218 | content, 219 | allOf( 220 | contains("Last event"), 221 | contains("Very last event"), 222 | ), 223 | ); 224 | }); 225 | } 226 | -------------------------------------------------------------------------------- /test/outputs/file_output_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:logger/logger.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | var file = File("${Directory.systemTemp.path}/dart_logger_test.log"); 8 | setUp(() async { 9 | await file.create(recursive: true); 10 | }); 11 | 12 | tearDown(() async { 13 | await file.delete(); 14 | }); 15 | 16 | test('Real file read and write', () async { 17 | var output = FileOutput(file: file); 18 | await output.init(); 19 | 20 | final event0 = OutputEvent(LogEvent(Level.info, ""), ["First event"]); 21 | final event1 = OutputEvent(LogEvent(Level.info, ""), ["Second event"]); 22 | final event2 = OutputEvent(LogEvent(Level.info, ""), ["Third event"]); 23 | 24 | output.output(event0); 25 | output.output(event1); 26 | output.output(event2); 27 | 28 | await output.destroy(); 29 | 30 | var content = await file.readAsString(); 31 | expect( 32 | content, 33 | allOf( 34 | contains("First event"), 35 | contains("Second event"), 36 | contains("Third event"), 37 | ), 38 | ); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/outputs/memory_output_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('Memory output buffer size is limited', () { 6 | var output = MemoryOutput(bufferSize: 2); 7 | 8 | final event0 = OutputEvent(LogEvent(Level.info, ""), []); 9 | final event1 = OutputEvent(LogEvent(Level.info, ""), []); 10 | final event2 = OutputEvent(LogEvent(Level.info, ""), []); 11 | 12 | output.output(event0); 13 | output.output(event1); 14 | output.output(event2); 15 | 16 | expect(output.buffer.length, 2); 17 | expect(output.buffer, containsAllInOrder([event1, event2])); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /test/outputs/multi_output_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('Multiple outputs are populated with the same events', () { 6 | final output1 = MemoryOutput(bufferSize: 2); 7 | final output2 = MemoryOutput(bufferSize: 2); 8 | 9 | final multiOutput = MultiOutput([output1, output2]); 10 | 11 | final event0 = OutputEvent(LogEvent(Level.info, ""), []); 12 | multiOutput.output(event0); 13 | 14 | expect(output1.buffer.length, 1); 15 | expect(output2.buffer.length, 1); 16 | expect(output1.buffer.elementAt(0), equals(output2.buffer.elementAt(0))); 17 | expect(output1.buffer.elementAt(0), equals(event0)); 18 | 19 | final event1 = OutputEvent(LogEvent(Level.info, ""), []); 20 | multiOutput.output(event1); 21 | 22 | expect(output1.buffer.length, 2); 23 | expect(output2.buffer.length, 2); 24 | expect(output1.buffer.elementAt(0), equals(output2.buffer.elementAt(0))); 25 | expect(output1.buffer.elementAt(0), equals(event0)); 26 | expect(output1.buffer.elementAt(1), equals(output2.buffer.elementAt(1))); 27 | expect(output1.buffer.elementAt(1), equals(event1)); 28 | }); 29 | 30 | test('passing null does not throw an exception', () { 31 | final output = MultiOutput(null); 32 | output.output(OutputEvent(LogEvent(Level.info, ""), [])); 33 | }); 34 | 35 | test('passing null in the list does not throw an exception', () { 36 | final output = MultiOutput([null]); 37 | output.output(OutputEvent(LogEvent(Level.info, ""), [])); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /test/outputs/stream_output_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('writes to a Stream', () { 6 | var out = StreamOutput(); 7 | 8 | out.stream.listen((var e) { 9 | expect(e, ['hi there']); 10 | }); 11 | 12 | out.output(OutputEvent(LogEvent(Level.debug, ""), ['hi there'])); 13 | }); 14 | 15 | test('respects listen', () { 16 | var out = StreamOutput(); 17 | 18 | out.output(OutputEvent(LogEvent(Level.debug, ""), ['dropped'])); 19 | 20 | out.stream.listen((var e) { 21 | expect(e, ['hi there']); 22 | }); 23 | 24 | out.output(OutputEvent(LogEvent(Level.debug, ""), ['hi there'])); 25 | }); 26 | 27 | test('respects pause', () { 28 | var out = StreamOutput(); 29 | 30 | var sub = out.stream.listen((var e) { 31 | expect(e, ['hi there']); 32 | }); 33 | 34 | sub.pause(); 35 | out.output(OutputEvent(LogEvent(Level.debug, ""), ['dropped'])); 36 | sub.resume(); 37 | out.output(OutputEvent(LogEvent(Level.debug, ""), ['hi there'])); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /test/printers/hybrid_printer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | final realPrinter = SimplePrinter(); 5 | 6 | class TestLogPrinter extends LogPrinter { 7 | LogEvent? latestEvent; 8 | 9 | @override 10 | List log(LogEvent event) { 11 | latestEvent = event; 12 | return realPrinter.log(event); 13 | } 14 | } 15 | 16 | void main() { 17 | var printerA = TestLogPrinter(); 18 | var printerB = TestLogPrinter(); 19 | var printerC = TestLogPrinter(); 20 | 21 | var debugEvent = LogEvent(Level.debug, 'debug', 22 | error: 'blah', stackTrace: StackTrace.current); 23 | var infoEvent = LogEvent(Level.info, 'info', 24 | error: 'blah', stackTrace: StackTrace.current); 25 | var warningEvent = LogEvent(Level.warning, 'warning', 26 | error: 'blah', stackTrace: StackTrace.current); 27 | var errorEvent = LogEvent(Level.error, 'error', 28 | error: 'blah', stackTrace: StackTrace.current); 29 | 30 | var hybridPrinter = HybridPrinter(printerA, debug: printerB, error: printerC); 31 | test('uses wrapped printer by default', () { 32 | hybridPrinter.log(infoEvent); 33 | expect(printerA.latestEvent, equals(infoEvent)); 34 | }); 35 | 36 | test('forwards logs to correct logger', () { 37 | hybridPrinter.log(debugEvent); 38 | hybridPrinter.log(errorEvent); 39 | hybridPrinter.log(warningEvent); 40 | expect(printerA.latestEvent, equals(warningEvent)); 41 | expect(printerB.latestEvent, equals(debugEvent)); 42 | expect(printerC.latestEvent, equals(errorEvent)); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /test/printers/logfmt_printer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | var printer = LogfmtPrinter(); 6 | 7 | test('includes level', () { 8 | expect( 9 | printer.log(LogEvent( 10 | Level.debug, 11 | 'some message', 12 | error: Exception('boom'), 13 | stackTrace: StackTrace.current, 14 | ))[0], 15 | contains('level=debug'), 16 | ); 17 | }); 18 | 19 | test('with a string message includes a msg key', () { 20 | expect( 21 | printer.log(LogEvent( 22 | Level.debug, 23 | 'some message', 24 | error: Exception('boom'), 25 | stackTrace: StackTrace.current, 26 | ))[0], 27 | contains('msg="some message"'), 28 | ); 29 | }); 30 | 31 | test('includes random key=value pairs', () { 32 | var output = printer.log(LogEvent( 33 | Level.debug, 34 | {'a': 123, 'foo': 'bar baz'}, 35 | error: Exception('boom'), 36 | stackTrace: StackTrace.current, 37 | ))[0]; 38 | 39 | expect(output, contains('a=123')); 40 | expect(output, contains('foo="bar baz"')); 41 | }); 42 | 43 | test('handles an error/exception', () { 44 | var output = printer.log(LogEvent( 45 | Level.debug, 46 | 'some message', 47 | error: Exception('boom'), 48 | stackTrace: StackTrace.current, 49 | ))[0]; 50 | expect(output, contains('error="Exception: boom"')); 51 | 52 | output = printer.log(LogEvent( 53 | Level.debug, 54 | 'some message', 55 | ))[0]; 56 | expect(output, isNot(contains('error='))); 57 | }); 58 | 59 | test('handles a stacktrace', () {}, skip: 'TODO'); 60 | } 61 | -------------------------------------------------------------------------------- /test/printers/prefix_printer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | var debugEvent = LogEvent(Level.debug, 'debug', 6 | error: 'blah', stackTrace: StackTrace.current); 7 | var infoEvent = LogEvent(Level.info, 'info', 8 | error: 'blah', stackTrace: StackTrace.current); 9 | var warningEvent = LogEvent(Level.warning, 'warning', 10 | error: 'blah', stackTrace: StackTrace.current); 11 | var errorEvent = LogEvent(Level.error, 'debug', 12 | error: 'blah', stackTrace: StackTrace.current); 13 | var traceEvent = LogEvent(Level.trace, 'debug', 14 | error: 'blah', stackTrace: StackTrace.current); 15 | var fatalEvent = LogEvent(Level.fatal, 'debug', 16 | error: 'blah', stackTrace: StackTrace.current); 17 | 18 | var allEvents = [ 19 | debugEvent, 20 | warningEvent, 21 | errorEvent, 22 | traceEvent, 23 | fatalEvent 24 | ]; 25 | 26 | test('prefixes logs', () { 27 | var printer = PrefixPrinter(PrettyPrinter()); 28 | var actualLog = printer.log(infoEvent); 29 | for (var logString in actualLog) { 30 | expect(logString, contains('INFO')); 31 | } 32 | 33 | var debugLog = printer.log(debugEvent); 34 | for (var logString in debugLog) { 35 | expect(logString, contains('DEBUG')); 36 | } 37 | }); 38 | 39 | test('can supply own prefixes', () { 40 | var printer = PrefixPrinter(PrettyPrinter(), debug: 'BLAH'); 41 | var actualLog = printer.log(debugEvent); 42 | for (var logString in actualLog) { 43 | expect(logString, contains('BLAH')); 44 | } 45 | }); 46 | 47 | test('pads to same length', () { 48 | const longPrefix = 'EXTRALONGPREFIX'; 49 | const len = longPrefix.length; 50 | var printer = PrefixPrinter(SimplePrinter(), debug: longPrefix); 51 | for (var event in allEvents) { 52 | var l1 = printer.log(event); 53 | for (var logString in l1) { 54 | expect(logString.substring(0, len), isNot(contains('['))); 55 | } 56 | } 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /test/printers/pretty_printer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | String readMessage(List log) { 6 | return log.reduce((acc, val) => "$acc\n$val"); 7 | } 8 | 9 | test('should print an emoji when option is enabled', () { 10 | final expectedMessage = 'some message with an emoji'; 11 | final emojiPrettyPrinter = PrettyPrinter(printEmojis: true); 12 | 13 | final event = LogEvent( 14 | Level.debug, 15 | expectedMessage, 16 | error: 'some error', 17 | stackTrace: StackTrace.current, 18 | ); 19 | 20 | final actualLog = emojiPrettyPrinter.log(event); 21 | final actualLogString = readMessage(actualLog); 22 | expect(actualLogString, 23 | contains(PrettyPrinter.defaultLevelEmojis[Level.debug])); 24 | expect(actualLogString, contains(expectedMessage)); 25 | }); 26 | 27 | test('should print custom emoji or fallback', () { 28 | final expectedMessage = 'some message with an emoji'; 29 | final emojiPrettyPrinter = PrettyPrinter( 30 | printEmojis: true, 31 | levelEmojis: { 32 | Level.debug: '🧵', 33 | }, 34 | ); 35 | 36 | final firstEvent = LogEvent( 37 | Level.debug, 38 | expectedMessage, 39 | error: 'some error', 40 | stackTrace: StackTrace.current, 41 | ); 42 | final emojiLogString = readMessage(emojiPrettyPrinter.log(firstEvent)); 43 | expect( 44 | emojiLogString, 45 | contains( 46 | '${emojiPrettyPrinter.levelEmojis![Level.debug]!} $expectedMessage'), 47 | ); 48 | 49 | final secondEvent = LogEvent( 50 | Level.info, 51 | expectedMessage, 52 | error: 'some error', 53 | stackTrace: StackTrace.current, 54 | ); 55 | final fallbackEmojiLogString = 56 | readMessage(emojiPrettyPrinter.log(secondEvent)); 57 | expect( 58 | fallbackEmojiLogString, 59 | contains( 60 | '${PrettyPrinter.defaultLevelEmojis[Level.info]!} $expectedMessage'), 61 | ); 62 | }); 63 | 64 | test('should print custom color or fallback', () { 65 | final expectedMessage = 'some message with a color'; 66 | final coloredPrettyPrinter = PrettyPrinter( 67 | colors: true, 68 | levelColors: { 69 | Level.debug: const AnsiColor.fg(50), 70 | }, 71 | ); 72 | 73 | final firstEvent = LogEvent( 74 | Level.debug, 75 | expectedMessage, 76 | error: 'some error', 77 | stackTrace: StackTrace.current, 78 | ); 79 | final coloredLogString = readMessage(coloredPrettyPrinter.log(firstEvent)); 80 | expect(coloredLogString, contains(expectedMessage)); 81 | expect( 82 | coloredLogString, 83 | startsWith(coloredPrettyPrinter.levelColors![Level.debug]!.toString()), 84 | ); 85 | 86 | final secondEvent = LogEvent( 87 | Level.info, 88 | expectedMessage, 89 | error: 'some error', 90 | stackTrace: StackTrace.current, 91 | ); 92 | final fallbackColoredLogString = 93 | readMessage(coloredPrettyPrinter.log(secondEvent)); 94 | expect(fallbackColoredLogString, contains(expectedMessage)); 95 | expect( 96 | fallbackColoredLogString, 97 | startsWith(PrettyPrinter.defaultLevelColors[Level.info]!.toString()), 98 | ); 99 | }); 100 | 101 | test('deal with string type message', () { 102 | final prettyPrinter = PrettyPrinter(); 103 | final expectedMessage = 'normally computed message'; 104 | final withFunction = LogEvent( 105 | Level.debug, 106 | expectedMessage, 107 | error: 'some error', 108 | stackTrace: StackTrace.current, 109 | ); 110 | 111 | final actualLog = prettyPrinter.log(withFunction); 112 | final actualLogString = readMessage(actualLog); 113 | 114 | expect( 115 | actualLogString, 116 | contains(expectedMessage), 117 | ); 118 | }); 119 | 120 | test('deal with Map type message', () { 121 | final prettyPrinter = PrettyPrinter(); 122 | final expectedMsgMap = {'foo': 123, 1: 2, true: 'false'}; 123 | var withMap = LogEvent( 124 | Level.debug, 125 | expectedMsgMap, 126 | error: 'some error', 127 | stackTrace: StackTrace.current, 128 | ); 129 | 130 | final actualLog = prettyPrinter.log(withMap); 131 | final actualLogString = readMessage(actualLog); 132 | for (var expectedMsg in expectedMsgMap.entries) { 133 | expect( 134 | actualLogString, 135 | contains('${expectedMsg.key}: ${expectedMsg.value}'), 136 | ); 137 | } 138 | }); 139 | 140 | test('deal with Iterable type message', () { 141 | final prettyPrinter = PrettyPrinter(); 142 | final expectedMsgItems = ['first', 'second', 'third', 'last']; 143 | var withIterable = LogEvent( 144 | Level.debug, 145 | ['first', 'second', 'third', 'last'], 146 | error: 'some error', 147 | stackTrace: StackTrace.current, 148 | ); 149 | final actualLog = prettyPrinter.log(withIterable); 150 | final actualLogString = readMessage(actualLog); 151 | for (var expectedMsg in expectedMsgItems) { 152 | expect( 153 | actualLogString, 154 | contains(expectedMsg), 155 | ); 156 | } 157 | }); 158 | 159 | test('deal with Function type message', () { 160 | final prettyPrinter = PrettyPrinter(); 161 | final expectedMessage = 'heavily computed very pretty Message'; 162 | final withFunction = LogEvent( 163 | Level.debug, 164 | () => expectedMessage, 165 | error: 'some error', 166 | stackTrace: StackTrace.current, 167 | ); 168 | 169 | final actualLog = prettyPrinter.log(withFunction); 170 | final actualLogString = readMessage(actualLog); 171 | 172 | expect( 173 | actualLogString, 174 | contains(expectedMessage), 175 | ); 176 | }); 177 | 178 | test('stackTraceBeginIndex', () { 179 | final prettyPrinter = PrettyPrinter( 180 | stackTraceBeginIndex: 2, 181 | ); 182 | final withFunction = LogEvent( 183 | Level.debug, 184 | "some message", 185 | error: 'some error', 186 | stackTrace: StackTrace.current, 187 | ); 188 | 189 | final actualLog = prettyPrinter.log(withFunction); 190 | final actualLogString = readMessage(actualLog); 191 | 192 | expect( 193 | actualLogString, 194 | allOf([ 195 | isNot(contains("#0 ")), 196 | isNot(contains("#1 ")), 197 | contains("#2 "), 198 | ]), 199 | ); 200 | }); 201 | } 202 | -------------------------------------------------------------------------------- /test/printers/simple_printer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | const ansiEscapeLiteral = '\x1B'; 5 | 6 | void main() { 7 | var event = LogEvent( 8 | Level.trace, 9 | 'some message', 10 | error: 'some error', 11 | stackTrace: StackTrace.current, 12 | ); 13 | 14 | var plainPrinter = SimplePrinter(colors: false, printTime: false); 15 | 16 | test('represent event on a single line (ignoring stacktrace)', () { 17 | var outputs = plainPrinter.log(event); 18 | 19 | expect(outputs, hasLength(1)); 20 | expect(outputs[0], '[T] some message ERROR: some error'); 21 | }); 22 | 23 | group('color', () { 24 | test('print color', () { 25 | // `useColor` is detected but here we override it because we want to print 26 | // the ANSI control characters regardless for the test. 27 | var printer = SimplePrinter(colors: true); 28 | 29 | expect(printer.log(event)[0], contains(ansiEscapeLiteral)); 30 | }); 31 | 32 | test('toggle color', () { 33 | var printer = SimplePrinter(colors: false); 34 | 35 | expect(printer.log(event)[0], isNot(contains(ansiEscapeLiteral))); 36 | }); 37 | }); 38 | 39 | test('print time', () { 40 | var printer = SimplePrinter(printTime: true); 41 | 42 | expect(printer.log(event)[0], contains('TIME')); 43 | }); 44 | 45 | test('does not print time', () { 46 | var printer = SimplePrinter(printTime: false); 47 | 48 | expect(printer.log(event)[0], isNot(contains('TIME'))); 49 | }); 50 | 51 | test('omits error when null', () { 52 | var withoutError = LogEvent( 53 | Level.debug, 54 | 'some message', 55 | error: null, 56 | stackTrace: StackTrace.current, 57 | ); 58 | var outputs = SimplePrinter().log(withoutError); 59 | 60 | expect(outputs[0], isNot(contains('ERROR'))); 61 | }); 62 | 63 | test('deal with Map type message', () { 64 | var withMap = LogEvent( 65 | Level.debug, 66 | {'foo': 123}, 67 | error: 'some error', 68 | stackTrace: StackTrace.current, 69 | ); 70 | 71 | expect( 72 | plainPrinter.log(withMap)[0], 73 | '[D] {"foo":123} ERROR: some error', 74 | ); 75 | }); 76 | 77 | test('deal with Iterable type message', () { 78 | var withIterable = LogEvent( 79 | Level.debug, 80 | [1, 2, 3, 4], 81 | error: 'some error', 82 | stackTrace: StackTrace.current, 83 | ); 84 | 85 | expect( 86 | plainPrinter.log(withIterable)[0], 87 | '[D] [1,2,3,4] ERROR: some error', 88 | ); 89 | }); 90 | 91 | test('deal with Function type message', () { 92 | var expectedMessage = 'heavily computed Message'; 93 | var withFunction = LogEvent( 94 | Level.debug, 95 | () => expectedMessage, 96 | error: 'some error', 97 | stackTrace: StackTrace.current, 98 | ); 99 | 100 | expect( 101 | plainPrinter.log(withFunction)[0], 102 | '[D] $expectedMessage ERROR: some error', 103 | ); 104 | }); 105 | } 106 | --------------------------------------------------------------------------------