├── .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 | [](https://pub.dartlang.org/packages/logger)
4 | [](https://github.com/SourceHorizon/logger/actions)
5 | [](https://github.com/SourceHorizon/logger/commits/main)
6 | [](https://github.com/SourceHorizon/logger/pulls)
7 | [](https://github.com/SourceHorizon/logger)
8 | [](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 | 
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 |
--------------------------------------------------------------------------------