├── .github ├── dependabot.yaml └── workflows │ ├── no-response.yml │ ├── publish.yaml │ └── test-package.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── main.dart ├── lib ├── logging.dart └── src │ ├── level.dart │ ├── log_record.dart │ └── logger.dart ├── pubspec.yaml └── test └── logging_test.dart /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | version: 2 3 | 4 | updates: 5 | - package-ecosystem: github-actions 6 | directory: / 7 | schedule: 8 | interval: monthly 9 | labels: 10 | - autosubmit 11 | groups: 12 | github-actions: 13 | patterns: 14 | - "*" 15 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | # A workflow to close issues where the author hasn't responded to a request for 2 | # more information; see https://github.com/actions/stale. 3 | 4 | name: No Response 5 | 6 | # Run as a daily cron. 7 | on: 8 | schedule: 9 | # Every day at 8am 10 | - cron: '0 8 * * *' 11 | 12 | # All permissions not specified are set to 'none'. 13 | permissions: 14 | issues: write 15 | pull-requests: write 16 | 17 | jobs: 18 | no-response: 19 | runs-on: ubuntu-latest 20 | if: ${{ github.repository_owner == 'dart-lang' }} 21 | steps: 22 | - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e 23 | with: 24 | # Don't automatically mark inactive issues+PRs as stale. 25 | days-before-stale: -1 26 | # Close needs-info issues and PRs after 14 days of inactivity. 27 | days-before-close: 14 28 | stale-issue-label: "needs-info" 29 | close-issue-message: > 30 | Without additional information we're not able to resolve this issue. 31 | Feel free to add more info or respond to any questions above and we 32 | can reopen the case. Thanks for your contribution! 33 | stale-pr-label: "needs-info" 34 | close-pr-message: > 35 | Without additional information we're not able to resolve this PR. 36 | Feel free to add more info or respond to any questions above. 37 | Thanks for your contribution! 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # A CI configuration to auto-publish pub packages. 2 | 3 | name: Publish 4 | 5 | on: 6 | pull_request: 7 | branches: [ master ] 8 | push: 9 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 10 | 11 | jobs: 12 | publish: 13 | if: ${{ github.repository_owner == 'dart-lang' }} 14 | uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main 15 | permissions: 16 | id-token: write # Required for authentication using OIDC 17 | pull-requests: write # Required for writing the pull request note 18 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | # Run on PRs and pushes to the default branch. 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | env: 13 | PUB_ENVIRONMENT: bot.github 14 | 15 | jobs: 16 | # Check code formatting and static analysis. 17 | analyze: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | sdk: [dev] 23 | steps: 24 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 25 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 26 | with: 27 | sdk: ${{ matrix.sdk }} 28 | - id: install 29 | name: Install dependencies 30 | run: dart pub get 31 | - name: Check formatting 32 | run: dart format --output=none --set-exit-if-changed . 33 | if: always() && steps.install.outcome == 'success' 34 | - name: Analyze code 35 | run: dart analyze --fatal-infos 36 | if: always() && steps.install.outcome == 'success' 37 | 38 | # Run tests on a matrix of platforms and sdk versions. 39 | test: 40 | needs: analyze 41 | runs-on: ${{ matrix.os }} 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | # Add macos-latest and/or windows-latest if relevant for this package. 46 | os: [ubuntu-latest] 47 | sdk: [3.4, dev] 48 | steps: 49 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 50 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 51 | with: 52 | sdk: ${{ matrix.sdk }} 53 | - id: install 54 | name: Install dependencies 55 | run: dart pub get 56 | - name: Run VM tests 57 | run: dart test --platform vm 58 | if: always() && steps.install.outcome == 'success' 59 | - name: Run Chrome tests 60 | run: dart test --platform chrome 61 | if: always() && steps.install.outcome == 'success' 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool 2 | .packages 3 | pubspec.lock 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Names should be added to this file with this pattern: 2 | # 3 | # For individuals: 4 | # Name 5 | # 6 | # For organizations: 7 | # Organization 8 | # 9 | Google Inc. <*@google.com> 10 | Anton Astashov 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.0-wip 2 | 3 | * Override empty stack traces for trace level events. 4 | * Require Dart 3.4 5 | 6 | ## 1.2.0 7 | 8 | * Add notification when the log level is changed. Logger `onLevelChanged` broadcasts a stream of level values. 9 | * Require Dart 2.19. 10 | 11 | ## 1.1.1 12 | 13 | * Add a check that throws if a logger name ends with '.'. 14 | * Require Dart 2.18 15 | 16 | ## 1.1.0 17 | 18 | * Add `Logger.attachedLoggers` which exposes all loggers created with the 19 | default constructor. 20 | * Enable the `avoid_dynamic_calls` lint. 21 | 22 | ## 1.0.2 23 | 24 | * Update description. 25 | * Add example. 26 | 27 | ## 1.0.1 28 | 29 | * List log levels in README. 30 | 31 | ## 1.0.0 32 | 33 | * Stable null safety release. 34 | 35 | ## 1.0.0-nullsafety.0 36 | 37 | * Migrate to null safety. 38 | * Removed the deprecated `LoggerHandler` typedef. 39 | 40 | ## 0.11.4 41 | 42 | * Add top level `defaultLevel`. 43 | * Require Dart `>=2.0.0`. 44 | * Make detached loggers work regardless of `hierarchicalLoggingEnabled`. 45 | 46 | ## 0.11.3+2 47 | 48 | * Set max SDK version to `<3.0.0`, and adjust other dependencies. 49 | 50 | ## 0.11.3+1 51 | 52 | * Fixed several documentation comments. 53 | 54 | ## 0.11.3 55 | 56 | * Added optional `LogRecord.object` field. 57 | 58 | * `Logger.log` sets `LogRecord.object` if the message is not a string or a 59 | function that returns a string. So that a handler can access the original 60 | object instead of just its `toString()`. 61 | 62 | ## 0.11.2 63 | 64 | * Added `Logger.detached` - a convenience factory to obtain a logger that is not 65 | attached to this library's logger hierarchy. 66 | 67 | ## 0.11.1+1 68 | 69 | * Include default error with the auto-generated stack traces. 70 | 71 | ## 0.11.1 72 | 73 | * Add support for automatically logging the stack trace on error messages. Note 74 | this can be expensive, so it is off by default. 75 | 76 | ## 0.11.0 77 | 78 | * Revert change in `0.10.0`. `stackTrace` must be an instance of `StackTrace`. 79 | Use the `Trace` class from the [stack_trace package][] to convert strings. 80 | 81 | [stack_trace package]: https://pub.dev/packages/stack_trace 82 | 83 | ## 0.10.0 84 | 85 | * Change type of `stackTrace` from `StackTrace` to `Object`. 86 | 87 | ## 0.9.3 88 | 89 | * Added optional `LogRecord.zone` field. 90 | 91 | * Record current zone (or user specified zone) when creating new `LogRecord`s. 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repo has moved to https://github.com/dart-lang/core/tree/main/pkgs/logging 3 | 4 | [![Build Status](https://github.com/dart-lang/logging/workflows/Dart%20CI/badge.svg)](https://github.com/dart-lang/logging/actions?query=workflow%3A"Dart+CI"+branch%3Amaster) 5 | [![Pub](https://img.shields.io/pub/v/logging.svg)](https://pub.dev/packages/logging) 6 | [![package publisher](https://img.shields.io/pub/publisher/logging.svg)](https://pub.dev/packages/logging/publisher) 7 | 8 | ## Initializing 9 | 10 | By default, the logging package does not do anything useful with the log 11 | messages. You must configure the logging level and add a handler for the log 12 | messages. 13 | 14 | Here is a simple logging configuration that logs all messages via `print`. 15 | 16 | ```dart 17 | Logger.root.level = Level.ALL; // defaults to Level.INFO 18 | Logger.root.onRecord.listen((record) { 19 | print('${record.level.name}: ${record.time}: ${record.message}'); 20 | }); 21 | ``` 22 | 23 | First, set the root `Level`. All messages at or above the current level are sent to the 24 | `onRecord` stream. Available levels are: 25 | 26 | + `Level.OFF` 27 | + `Level.SHOUT` 28 | + `Level.SEVERE` 29 | + `Level.WARNING` 30 | + `Level.INFO` 31 | + `Level.CONFIG` 32 | + `Level.FINE` 33 | + `Level.FINER` 34 | + `Level.FINEST` 35 | 36 | Then, listen on the `onRecord` stream for `LogRecord` events. The `LogRecord` 37 | class has various properties for the message, error, logger name, and more. 38 | 39 | To listen for changed level notifications use: 40 | 41 | ```dart 42 | Logger.root.onLevelChanged.listen((level) { 43 | print('The new log level is $level'); 44 | }); 45 | ``` 46 | 47 | ## Logging messages 48 | 49 | Create a `Logger` with a unique name to easily identify the source of the log 50 | messages. 51 | 52 | ```dart 53 | final log = Logger('MyClassName'); 54 | ``` 55 | 56 | Here is an example of logging a debug message and an error: 57 | 58 | ```dart 59 | var future = doSomethingAsync().then((result) { 60 | log.fine('Got the result: $result'); 61 | processResult(result); 62 | }).catchError((e, stackTrace) => log.severe('Oh noes!', e, stackTrace)); 63 | ``` 64 | 65 | When logging more complex messages, you can pass a closure instead that will be 66 | evaluated only if the message is actually logged: 67 | 68 | ```dart 69 | log.fine(() => [1, 2, 3, 4, 5].map((e) => e * 4).join("-")); 70 | ``` 71 | 72 | Available logging methods are: 73 | 74 | + `log.shout(logged_content);` 75 | + `log.severe(logged_content);` 76 | + `log.warning(logged_content);` 77 | + `log.info(logged_content);` 78 | + `log.config(logged_content);` 79 | + `log.fine(logged_content);` 80 | + `log.finer(logged_content);` 81 | + `log.finest(logged_content);` 82 | 83 | ## Configuration 84 | 85 | Loggers can be individually configured and listened to. When an individual logger has no 86 | specific configuration, it uses the configuration and any listeners found at `Logger.root`. 87 | 88 | To begin, set the global boolean `hierarchicalLoggingEnabled` to `true`. 89 | 90 | Then, create unique loggers and configure their `level` attributes and assign any listeners to 91 | their `onRecord` streams. 92 | 93 | 94 | ```dart 95 | hierarchicalLoggingEnabled = true; 96 | Logger.root.level = Level.WARNING; 97 | Logger.root.onRecord.listen((record) { 98 | print('[ROOT][WARNING+] ${record.message}'); 99 | }); 100 | 101 | final log1 = Logger('FINE+'); 102 | log1.level = Level.FINE; 103 | log1.onRecord.listen((record) { 104 | print('[LOG1][FINE+] ${record.message}'); 105 | }); 106 | 107 | // log2 inherits LEVEL value of WARNING from `Logger.root` 108 | final log2 = Logger('WARNING+'); 109 | log2.onRecord.listen((record) { 110 | print('[LOG2][WARNING+] ${record.message}'); 111 | }); 112 | 113 | 114 | // Will NOT print because FINER is too low level for `Logger.root`. 115 | log1.finer('LOG_01 FINER (X)'); 116 | 117 | // Will print twice ([LOG1] & [ROOT]) 118 | log1.fine('LOG_01 FINE (√√)'); 119 | 120 | // Will print ONCE because `log1` only uses root listener. 121 | log1.warning('LOG_01 WARNING (√)'); 122 | 123 | // Will never print because FINE is too low level. 124 | log2.fine('LOG_02 FINE (X)'); 125 | 126 | // Will print twice ([LOG2] & [ROOT]) because warning is sufficient for all 127 | // loggers' levels. 128 | log2.warning('LOG_02 WARNING (√√)'); 129 | 130 | // Will never print because `info` is filtered by `Logger.root.level` of 131 | // `Level.WARNING`. 132 | log2.info('INFO (X)'); 133 | ``` 134 | 135 | Results in: 136 | 137 | ``` 138 | [LOG1][FINE+] LOG_01 FINE (√√) 139 | [ROOT][WARNING+] LOG_01 FINE (√√) 140 | [LOG1][FINE+] LOG_01 WARNING (√) 141 | [ROOT][WARNING+] LOG_01 WARNING (√) 142 | [LOG2][WARNING+] LOG_02 WARNING (√√) 143 | [ROOT][WARNING+] LOG_02 WARNING (√√) 144 | ``` 145 | 146 | ## Publishing automation 147 | 148 | For information about our publishing automation and release process, see 149 | https://github.com/dart-lang/ecosystem/wiki/Publishing-automation. 150 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/language/analysis-options 2 | include: package:dart_flutter_team_lints/analysis_options.yaml 3 | 4 | analyzer: 5 | language: 6 | strict-raw-types: true 7 | 8 | linter: 9 | rules: 10 | - avoid_bool_literals_in_conditional_expressions 11 | - avoid_classes_with_only_static_members 12 | - avoid_private_typedef_functions 13 | - avoid_redundant_argument_values 14 | - avoid_returning_this 15 | - avoid_unused_constructor_parameters 16 | - avoid_void_async 17 | - cancel_subscriptions 18 | - join_return_with_assignment 19 | - literal_only_boolean_expressions 20 | - missing_whitespace_between_adjacent_strings 21 | - no_adjacent_strings_in_list 22 | - no_runtimeType_toString 23 | - package_api_docs 24 | - prefer_const_declarations 25 | - prefer_expression_function_bodies 26 | - prefer_final_locals 27 | - unnecessary_await_in_return 28 | - unnecessary_raw_strings 29 | - use_if_null_to_convert_nulls_to_bools 30 | - use_raw_strings 31 | - use_string_buffers 32 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:logging/logging.dart'; 2 | 3 | final log = Logger('ExampleLogger'); 4 | 5 | /// Example of configuring a logger to print to stdout. 6 | /// 7 | /// This example will print: 8 | /// 9 | /// INFO: 2021-09-13 15:35:10.703401: recursion: n = 4 10 | /// INFO: 2021-09-13 15:35:10.707974: recursion: n = 3 11 | /// Fibonacci(4) is: 3 12 | /// Fibonacci(5) is: 5 13 | /// SHOUT: 2021-09-13 15:35:10.708087: Unexpected negative n: -42 14 | /// Fibonacci(-42) is: 1 15 | void main() { 16 | Logger.root.level = Level.ALL; // defaults to Level.INFO 17 | Logger.root.onRecord.listen((record) { 18 | print('${record.level.name}: ${record.time}: ${record.message}'); 19 | }); 20 | 21 | print('Fibonacci(4) is: ${fibonacci(4)}'); 22 | 23 | Logger.root.level = Level.SEVERE; // skip logs less then severe. 24 | print('Fibonacci(5) is: ${fibonacci(5)}'); 25 | 26 | print('Fibonacci(-42) is: ${fibonacci(-42)}'); 27 | } 28 | 29 | int fibonacci(int n) { 30 | if (n <= 2) { 31 | if (n < 0) log.shout('Unexpected negative n: $n'); 32 | return 1; 33 | } else { 34 | log.info('recursion: n = $n'); 35 | return fibonacci(n - 2) + fibonacci(n - 1); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/logging.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | export 'src/level.dart'; 6 | export 'src/log_record.dart'; 7 | export 'src/logger.dart'; 8 | -------------------------------------------------------------------------------- /lib/src/level.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | // ignore_for_file: constant_identifier_names 6 | 7 | /// [Level]s to control logging output. Logging can be enabled to include all 8 | /// levels above certain [Level]. [Level]s are ordered using an integer 9 | /// value [Level.value]. The predefined [Level] constants below are sorted as 10 | /// follows (in descending order): [Level.SHOUT], [Level.SEVERE], 11 | /// [Level.WARNING], [Level.INFO], [Level.CONFIG], [Level.FINE], [Level.FINER], 12 | /// [Level.FINEST], and [Level.ALL]. 13 | /// 14 | /// We recommend using one of the predefined logging levels. If you define your 15 | /// own level, make sure you use a value between those used in [Level.ALL] and 16 | /// [Level.OFF]. 17 | class Level implements Comparable { 18 | final String name; 19 | 20 | /// Unique value for this level. Used to order levels, so filtering can 21 | /// exclude messages whose level is under certain value. 22 | final int value; 23 | 24 | const Level(this.name, this.value); 25 | 26 | /// Special key to turn on logging for all levels ([value] = 0). 27 | static const Level ALL = Level('ALL', 0); 28 | 29 | /// Special key to turn off all logging ([value] = 2000). 30 | static const Level OFF = Level('OFF', 2000); 31 | 32 | /// Key for highly detailed tracing ([value] = 300). 33 | static const Level FINEST = Level('FINEST', 300); 34 | 35 | /// Key for fairly detailed tracing ([value] = 400). 36 | static const Level FINER = Level('FINER', 400); 37 | 38 | /// Key for tracing information ([value] = 500). 39 | static const Level FINE = Level('FINE', 500); 40 | 41 | /// Key for static configuration messages ([value] = 700). 42 | static const Level CONFIG = Level('CONFIG', 700); 43 | 44 | /// Key for informational messages ([value] = 800). 45 | static const Level INFO = Level('INFO', 800); 46 | 47 | /// Key for potential problems ([value] = 900). 48 | static const Level WARNING = Level('WARNING', 900); 49 | 50 | /// Key for serious failures ([value] = 1000). 51 | static const Level SEVERE = Level('SEVERE', 1000); 52 | 53 | /// Key for extra debugging loudness ([value] = 1200). 54 | static const Level SHOUT = Level('SHOUT', 1200); 55 | 56 | static const List LEVELS = [ 57 | ALL, 58 | FINEST, 59 | FINER, 60 | FINE, 61 | CONFIG, 62 | INFO, 63 | WARNING, 64 | SEVERE, 65 | SHOUT, 66 | OFF 67 | ]; 68 | 69 | @override 70 | bool operator ==(Object other) => other is Level && value == other.value; 71 | 72 | bool operator <(Level other) => value < other.value; 73 | 74 | bool operator <=(Level other) => value <= other.value; 75 | 76 | bool operator >(Level other) => value > other.value; 77 | 78 | bool operator >=(Level other) => value >= other.value; 79 | 80 | @override 81 | int compareTo(Level other) => value - other.value; 82 | 83 | @override 84 | int get hashCode => value; 85 | 86 | @override 87 | String toString() => name; 88 | } 89 | -------------------------------------------------------------------------------- /lib/src/log_record.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'level.dart'; 8 | import 'logger.dart'; 9 | 10 | /// A log entry representation used to propagate information from [Logger] to 11 | /// individual handlers. 12 | class LogRecord { 13 | final Level level; 14 | final String message; 15 | 16 | /// Non-string message passed to Logger. 17 | final Object? object; 18 | 19 | /// Logger where this record is stored. 20 | final String loggerName; 21 | 22 | /// Time when this record was created. 23 | final DateTime time; 24 | 25 | /// Unique sequence number greater than all log records created before it. 26 | final int sequenceNumber; 27 | 28 | static int _nextNumber = 0; 29 | 30 | /// Associated error (if any) when recording errors messages. 31 | final Object? error; 32 | 33 | /// Associated stackTrace (if any) when recording errors messages. 34 | final StackTrace? stackTrace; 35 | 36 | /// Zone of the calling code which resulted in this LogRecord. 37 | final Zone? zone; 38 | 39 | LogRecord(this.level, this.message, this.loggerName, 40 | [this.error, this.stackTrace, this.zone, this.object]) 41 | : time = DateTime.now(), 42 | sequenceNumber = LogRecord._nextNumber++; 43 | 44 | @override 45 | String toString() => '[${level.name}] $loggerName: $message'; 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/logger.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:collection'; 7 | 8 | import 'level.dart'; 9 | import 'log_record.dart'; 10 | 11 | /// Whether to allow fine-grain logging and configuration of loggers in a 12 | /// hierarchy. 13 | /// 14 | /// When false, all hierarchical logging instead is merged in the root logger. 15 | bool hierarchicalLoggingEnabled = false; 16 | 17 | /// Automatically record stack traces for any message of this level or above. 18 | /// 19 | /// Because this is expensive, this is off by default. 20 | Level recordStackTraceAtLevel = Level.OFF; 21 | 22 | /// The default [Level]. 23 | const defaultLevel = Level.INFO; 24 | 25 | /// Use a [Logger] to log debug messages. 26 | /// 27 | /// [Logger]s are named using a hierarchical dot-separated name convention. 28 | class Logger { 29 | /// Simple name of this logger. 30 | final String name; 31 | 32 | /// The full name of this logger, which includes the parent's full name. 33 | String get fullName => 34 | parent?.name.isNotEmpty ?? false ? '${parent!.fullName}.$name' : name; 35 | 36 | /// Parent of this logger in the hierarchy of loggers. 37 | final Logger? parent; 38 | 39 | /// Logging [Level] used for entries generated on this logger. 40 | /// 41 | /// Only the root logger is guaranteed to have a non-null [Level]. 42 | Level? _level; 43 | 44 | /// Private modifiable map of child loggers, indexed by their simple names. 45 | final Map _children; 46 | 47 | /// Children in the hierarchy of loggers, indexed by their simple names. 48 | /// 49 | /// This is an unmodifiable map. 50 | final Map children; 51 | 52 | /// Controller used to notify when log entries are added to this logger. 53 | /// 54 | /// If hierarchical logging is disabled then this is `null` for all but the 55 | /// root [Logger]. 56 | StreamController? _controller; 57 | 58 | /// Controller used to notify when the log level of this logger is changed. 59 | StreamController? _levelChangedController; 60 | 61 | /// Create or find a Logger by name. 62 | /// 63 | /// Calling `Logger(name)` will return the same instance whenever it is called 64 | /// with the same string name. Loggers created with this constructor are 65 | /// retained indefinitely and available through [attachedLoggers]. 66 | factory Logger(String name) => 67 | _loggers.putIfAbsent(name, () => Logger._named(name)); 68 | 69 | /// Creates a new detached [Logger]. 70 | /// 71 | /// Returns a new [Logger] instance (unlike `new Logger`, which returns a 72 | /// [Logger] singleton), which doesn't have any parent or children, 73 | /// and is not a part of the global hierarchical loggers structure. 74 | /// 75 | /// It can be useful when you just need a local short-living logger, 76 | /// which you'd like to be garbage-collected later. 77 | factory Logger.detached(String name) => 78 | Logger._internal(name, null, {}); 79 | 80 | factory Logger._named(String name) { 81 | if (name.startsWith('.')) { 82 | throw ArgumentError("name shouldn't start with a '.'"); 83 | } 84 | if (name.endsWith('.')) { 85 | throw ArgumentError("name shouldn't end with a '.'"); 86 | } 87 | 88 | // Split hierarchical names (separated with '.'). 89 | final dot = name.lastIndexOf('.'); 90 | Logger? parent; 91 | String thisName; 92 | if (dot == -1) { 93 | if (name != '') parent = Logger(''); 94 | thisName = name; 95 | } else { 96 | parent = Logger(name.substring(0, dot)); 97 | thisName = name.substring(dot + 1); 98 | } 99 | return Logger._internal(thisName, parent, {}); 100 | } 101 | 102 | Logger._internal(this.name, this.parent, Map children) 103 | : _children = children, 104 | children = UnmodifiableMapView(children) { 105 | if (parent == null) { 106 | _level = defaultLevel; 107 | } else { 108 | parent!._children[name] = this; 109 | } 110 | } 111 | 112 | /// Effective level considering the levels established in this logger's 113 | /// parents (when [hierarchicalLoggingEnabled] is true). 114 | Level get level { 115 | Level effectiveLevel; 116 | 117 | if (parent == null) { 118 | // We're either the root logger or a detached logger. Return our own 119 | // level. 120 | effectiveLevel = _level!; 121 | } else if (!hierarchicalLoggingEnabled) { 122 | effectiveLevel = root._level!; 123 | } else { 124 | effectiveLevel = _level ?? parent!.level; 125 | } 126 | 127 | // ignore: unnecessary_null_comparison 128 | assert(effectiveLevel != null); 129 | return effectiveLevel; 130 | } 131 | 132 | /// Override the level for this particular [Logger] and its children. 133 | /// 134 | /// Setting this to `null` makes it inherit the [parent]s level. 135 | set level(Level? value) { 136 | if (!hierarchicalLoggingEnabled && parent != null) { 137 | throw UnsupportedError( 138 | 'Please set "hierarchicalLoggingEnabled" to true if you want to ' 139 | 'change the level on a non-root logger.'); 140 | } 141 | if (parent == null && value == null) { 142 | throw UnsupportedError( 143 | 'Cannot set the level to `null` on a logger with no parent.'); 144 | } 145 | final isLevelChanged = _level != value; 146 | _level = value; 147 | if (isLevelChanged) { 148 | _levelChangedController?.add(value); 149 | } 150 | } 151 | 152 | /// Returns a stream of level values set to this [Logger]. 153 | /// 154 | /// You can listen for set levels using the standard stream APIs, 155 | /// for instance: 156 | /// 157 | /// ```dart 158 | /// logger.onLevelChanged.listen((level) { ... }); 159 | /// ``` 160 | /// A state error will be thrown if the level is changed 161 | /// inside the callback. 162 | Stream get onLevelChanged { 163 | _levelChangedController ??= StreamController.broadcast(sync: true); 164 | return _levelChangedController!.stream; 165 | } 166 | 167 | /// Returns a stream of messages added to this [Logger]. 168 | /// 169 | /// You can listen for messages using the standard stream APIs, for instance: 170 | /// 171 | /// ```dart 172 | /// logger.onRecord.listen((record) { ... }); 173 | /// ``` 174 | Stream get onRecord => _getStream(); 175 | 176 | void clearListeners() { 177 | if (hierarchicalLoggingEnabled || parent == null) { 178 | _controller?.close(); 179 | _controller = null; 180 | } else { 181 | root.clearListeners(); 182 | } 183 | } 184 | 185 | /// Whether a message for [value]'s level is loggable in this logger. 186 | bool isLoggable(Level value) => value >= level; 187 | 188 | /// Adds a log record for a [message] at a particular [logLevel] if 189 | /// `isLoggable(logLevel)` is true. 190 | /// 191 | /// Use this method to create log entries for user-defined levels. To record a 192 | /// message at a predefined level (e.g. [Level.INFO], [Level.WARNING], etc) 193 | /// you can use their specialized methods instead (e.g. [info], [warning], 194 | /// etc). 195 | /// 196 | /// If [message] is a [Function], it will be lazy evaluated. Additionally, if 197 | /// [message] or its evaluated value is not a [String], then 'toString()' will 198 | /// be called on the object and the result will be logged. The log record will 199 | /// contain a field holding the original object. 200 | /// 201 | /// The log record will also contain a field for the zone in which this call 202 | /// was made. This can be advantageous if a log listener wants to handler 203 | /// records of different zones differently (e.g. group log records by HTTP 204 | /// request if each HTTP request handler runs in it's own zone). 205 | /// 206 | /// If this record is logged at a level equal to or higher than 207 | /// [recordStackTraceAtLevel] and [stackTrace] is `null` or [StackTrace.empty] 208 | /// it will be defaulted to the current stack trace for this call. 209 | void log(Level logLevel, Object? message, 210 | [Object? error, StackTrace? stackTrace, Zone? zone]) { 211 | Object? object; 212 | if (isLoggable(logLevel)) { 213 | if (message is Function) { 214 | message = (message as Object? Function())(); 215 | } 216 | 217 | String msg; 218 | if (message is String) { 219 | msg = message; 220 | } else { 221 | msg = message.toString(); 222 | object = message; 223 | } 224 | 225 | if ((stackTrace == null || stackTrace == StackTrace.empty) && 226 | logLevel >= recordStackTraceAtLevel) { 227 | stackTrace = StackTrace.current; 228 | error ??= 'autogenerated stack trace for $logLevel $msg'; 229 | } 230 | zone ??= Zone.current; 231 | 232 | final record = 233 | LogRecord(logLevel, msg, fullName, error, stackTrace, zone, object); 234 | 235 | if (parent == null) { 236 | _publish(record); 237 | } else if (!hierarchicalLoggingEnabled) { 238 | root._publish(record); 239 | } else { 240 | Logger? target = this; 241 | while (target != null) { 242 | target._publish(record); 243 | target = target.parent; 244 | } 245 | } 246 | } 247 | } 248 | 249 | /// Log message at level [Level.FINEST]. 250 | /// 251 | /// See [log] for information on how non-String [message] arguments are 252 | /// handled. 253 | void finest(Object? message, [Object? error, StackTrace? stackTrace]) => 254 | log(Level.FINEST, message, error, stackTrace); 255 | 256 | /// Log message at level [Level.FINER]. 257 | /// 258 | /// See [log] for information on how non-String [message] arguments are 259 | /// handled. 260 | void finer(Object? message, [Object? error, StackTrace? stackTrace]) => 261 | log(Level.FINER, message, error, stackTrace); 262 | 263 | /// Log message at level [Level.FINE]. 264 | /// 265 | /// See [log] for information on how non-String [message] arguments are 266 | /// handled. 267 | void fine(Object? message, [Object? error, StackTrace? stackTrace]) => 268 | log(Level.FINE, message, error, stackTrace); 269 | 270 | /// Log message at level [Level.CONFIG]. 271 | /// 272 | /// See [log] for information on how non-String [message] arguments are 273 | /// handled. 274 | void config(Object? message, [Object? error, StackTrace? stackTrace]) => 275 | log(Level.CONFIG, message, error, stackTrace); 276 | 277 | /// Log message at level [Level.INFO]. 278 | /// 279 | /// See [log] for information on how non-String [message] arguments are 280 | /// handled. 281 | void info(Object? message, [Object? error, StackTrace? stackTrace]) => 282 | log(Level.INFO, message, error, stackTrace); 283 | 284 | /// Log message at level [Level.WARNING]. 285 | /// 286 | /// See [log] for information on how non-String [message] arguments are 287 | /// handled. 288 | void warning(Object? message, [Object? error, StackTrace? stackTrace]) => 289 | log(Level.WARNING, message, error, stackTrace); 290 | 291 | /// Log message at level [Level.SEVERE]. 292 | /// 293 | /// See [log] for information on how non-String [message] arguments are 294 | /// handled. 295 | void severe(Object? message, [Object? error, StackTrace? stackTrace]) => 296 | log(Level.SEVERE, message, error, stackTrace); 297 | 298 | /// Log message at level [Level.SHOUT]. 299 | /// 300 | /// See [log] for information on how non-String [message] arguments are 301 | /// handled. 302 | void shout(Object? message, [Object? error, StackTrace? stackTrace]) => 303 | log(Level.SHOUT, message, error, stackTrace); 304 | 305 | Stream _getStream() { 306 | if (hierarchicalLoggingEnabled || parent == null) { 307 | return (_controller ??= StreamController.broadcast(sync: true)) 308 | .stream; 309 | } else { 310 | return root._getStream(); 311 | } 312 | } 313 | 314 | void _publish(LogRecord record) => _controller?.add(record); 315 | 316 | /// Top-level root [Logger]. 317 | static final Logger root = Logger(''); 318 | 319 | /// All attached [Logger]s in the system. 320 | static final Map _loggers = {}; 321 | 322 | /// All attached [Logger]s in the system. 323 | /// 324 | /// Loggers created with [Logger.detached] are not included. 325 | static Iterable get attachedLoggers => _loggers.values; 326 | } 327 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: logging 2 | version: 1.3.0-wip 3 | description: >- 4 | Provides APIs for debugging and error logging, similar to loggers in other 5 | languages, such as the Closure JS Logger and java.util.logging.Logger. 6 | repository: https://github.com/dart-lang/logging 7 | 8 | topics: 9 | - logging 10 | - debugging 11 | 12 | environment: 13 | sdk: ^3.4.0 14 | 15 | dev_dependencies: 16 | dart_flutter_team_lints: ^3.0.0 17 | test: ^1.16.0 18 | -------------------------------------------------------------------------------- /test/logging_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | 7 | import 'package:logging/logging.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | void main() { 11 | final hierarchicalLoggingEnabledDefault = hierarchicalLoggingEnabled; 12 | 13 | test('level comparison is a valid comparator', () { 14 | const level1 = Level('NOT_REAL1', 253); 15 | expect(level1 == level1, isTrue); 16 | expect(level1 <= level1, isTrue); 17 | expect(level1 >= level1, isTrue); 18 | expect(level1 < level1, isFalse); 19 | expect(level1 > level1, isFalse); 20 | 21 | const level2 = Level('NOT_REAL2', 455); 22 | expect(level1 <= level2, isTrue); 23 | expect(level1 < level2, isTrue); 24 | expect(level2 >= level1, isTrue); 25 | expect(level2 > level1, isTrue); 26 | 27 | const level3 = Level('NOT_REAL3', 253); 28 | expect(level1, isNot(same(level3))); // different instances 29 | expect(level1, equals(level3)); // same value. 30 | }); 31 | 32 | test('default levels are in order', () { 33 | const levels = Level.LEVELS; 34 | 35 | for (var i = 0; i < levels.length; i++) { 36 | for (var j = i + 1; j < levels.length; j++) { 37 | expect(levels[i] < levels[j], isTrue); 38 | } 39 | } 40 | }); 41 | 42 | test('levels are comparable', () { 43 | final unsorted = [ 44 | Level.INFO, 45 | Level.CONFIG, 46 | Level.FINE, 47 | Level.SHOUT, 48 | Level.OFF, 49 | Level.FINER, 50 | Level.ALL, 51 | Level.WARNING, 52 | Level.FINEST, 53 | Level.SEVERE, 54 | ]; 55 | 56 | const sorted = Level.LEVELS; 57 | 58 | expect(unsorted, isNot(orderedEquals(sorted))); 59 | 60 | unsorted.sort(); 61 | expect(unsorted, orderedEquals(sorted)); 62 | }); 63 | 64 | test('levels are hashable', () { 65 | final map = {}; 66 | map[Level.INFO] = 'info'; 67 | map[Level.SHOUT] = 'shout'; 68 | expect(map[Level.INFO], same('info')); 69 | expect(map[Level.SHOUT], same('shout')); 70 | }); 71 | 72 | test('logger name cannot start with a "." ', () { 73 | expect(() => Logger('.c'), throwsArgumentError); 74 | }); 75 | 76 | test('logger name cannot end with a "."', () { 77 | expect(() => Logger('a.'), throwsArgumentError); 78 | expect(() => Logger('a..d'), throwsArgumentError); 79 | }); 80 | 81 | test('root level has proper defaults', () { 82 | expect(Logger.root, isNotNull); 83 | expect(Logger.root.parent, null); 84 | expect(Logger.root.level, defaultLevel); 85 | }); 86 | 87 | test('logger naming is hierarchical', () { 88 | final c = Logger('a.b.c'); 89 | expect(c.name, equals('c')); 90 | expect(c.parent!.name, equals('b')); 91 | expect(c.parent!.parent!.name, equals('a')); 92 | expect(c.parent!.parent!.parent!.name, equals('')); 93 | expect(c.parent!.parent!.parent!.parent, isNull); 94 | }); 95 | 96 | test('logger full name', () { 97 | final c = Logger('a.b.c'); 98 | expect(c.fullName, equals('a.b.c')); 99 | expect(c.parent!.fullName, equals('a.b')); 100 | expect(c.parent!.parent!.fullName, equals('a')); 101 | expect(c.parent!.parent!.parent!.fullName, equals('')); 102 | expect(c.parent!.parent!.parent!.parent, isNull); 103 | }); 104 | 105 | test('logger parent-child links are correct', () { 106 | final a = Logger('a'); 107 | final b = Logger('a.b'); 108 | final c = Logger('a.c'); 109 | expect(a, same(b.parent)); 110 | expect(a, same(c.parent)); 111 | expect(a.children['b'], same(b)); 112 | expect(a.children['c'], same(c)); 113 | }); 114 | 115 | test('loggers are singletons', () { 116 | final a1 = Logger('a'); 117 | final a2 = Logger('a'); 118 | final b = Logger('a.b'); 119 | final root = Logger.root; 120 | expect(a1, same(a2)); 121 | expect(a1, same(b.parent)); 122 | expect(root, same(a1.parent)); 123 | expect(root, same(Logger(''))); 124 | }); 125 | 126 | test('cannot directly manipulate Logger.children', () { 127 | final loggerAB = Logger('a.b'); 128 | final loggerA = loggerAB.parent!; 129 | 130 | expect(loggerA.children['b'], same(loggerAB), reason: 'can read Children'); 131 | 132 | expect(() { 133 | loggerAB.children['test'] = Logger('Fake1234'); 134 | }, throwsUnsupportedError, reason: 'Children is read-only'); 135 | }); 136 | 137 | test('stackTrace gets throw to LogRecord', () { 138 | Logger.root.level = Level.INFO; 139 | 140 | final records = []; 141 | 142 | final sub = Logger.root.onRecord.listen(records.add); 143 | 144 | try { 145 | throw UnsupportedError('test exception'); 146 | } catch (error, stack) { 147 | Logger.root.log(Level.SEVERE, 'severe', error, stack); 148 | Logger.root.warning('warning', error, stack); 149 | } 150 | 151 | Logger.root.log(Level.SHOUT, 'shout'); 152 | 153 | sub.cancel(); 154 | 155 | expect(records, hasLength(3)); 156 | 157 | final severe = records[0]; 158 | expect(severe.message, 'severe'); 159 | expect(severe.error is UnsupportedError, isTrue); 160 | expect(severe.stackTrace is StackTrace, isTrue); 161 | 162 | final warning = records[1]; 163 | expect(warning.message, 'warning'); 164 | expect(warning.error is UnsupportedError, isTrue); 165 | expect(warning.stackTrace is StackTrace, isTrue); 166 | 167 | final shout = records[2]; 168 | expect(shout.message, 'shout'); 169 | expect(shout.error, isNull); 170 | expect(shout.stackTrace, isNull); 171 | }); 172 | 173 | group('zone gets recorded to LogRecord', () { 174 | test('root zone', () { 175 | final root = Logger.root; 176 | 177 | final recordingZone = Zone.current; 178 | final records = []; 179 | root.onRecord.listen(records.add); 180 | root.info('hello'); 181 | 182 | expect(records, hasLength(1)); 183 | expect(records.first.zone, equals(recordingZone)); 184 | }); 185 | 186 | test('child zone', () { 187 | final root = Logger.root; 188 | 189 | late Zone recordingZone; 190 | final records = []; 191 | root.onRecord.listen(records.add); 192 | 193 | runZoned(() { 194 | recordingZone = Zone.current; 195 | root.info('hello'); 196 | }); 197 | 198 | expect(records, hasLength(1)); 199 | expect(records.first.zone, equals(recordingZone)); 200 | }); 201 | 202 | test('custom zone', () { 203 | final root = Logger.root; 204 | 205 | late Zone recordingZone; 206 | final records = []; 207 | root.onRecord.listen(records.add); 208 | 209 | runZoned(() { 210 | recordingZone = Zone.current; 211 | }); 212 | 213 | runZoned(() => root.log(Level.INFO, 'hello', null, null, recordingZone)); 214 | 215 | expect(records, hasLength(1)); 216 | expect(records.first.zone, equals(recordingZone)); 217 | }); 218 | }); 219 | 220 | group('detached loggers', () { 221 | tearDown(() { 222 | hierarchicalLoggingEnabled = hierarchicalLoggingEnabledDefault; 223 | Logger.root.level = defaultLevel; 224 | }); 225 | 226 | test('create new instances of Logger', () { 227 | final a1 = Logger.detached('a'); 228 | final a2 = Logger.detached('a'); 229 | final a = Logger('a'); 230 | 231 | expect(a1, isNot(a2)); 232 | expect(a1, isNot(a)); 233 | expect(a2, isNot(a)); 234 | }); 235 | 236 | test('parent is null', () { 237 | final a = Logger.detached('a'); 238 | expect(a.parent, null); 239 | }); 240 | 241 | test('children is empty', () { 242 | final a = Logger.detached('a'); 243 | expect(a.children, {}); 244 | }); 245 | 246 | test('have levels independent of the root level', () { 247 | void testDetachedLoggerLevel(bool withHierarchy) { 248 | hierarchicalLoggingEnabled = withHierarchy; 249 | 250 | const newRootLevel = Level.ALL; 251 | const newDetachedLevel = Level.OFF; 252 | 253 | Logger.root.level = newRootLevel; 254 | 255 | final detached = Logger.detached('a'); 256 | expect(detached.level, defaultLevel); 257 | expect(Logger.root.level, newRootLevel); 258 | 259 | detached.level = newDetachedLevel; 260 | expect(detached.level, newDetachedLevel); 261 | expect(Logger.root.level, newRootLevel); 262 | } 263 | 264 | testDetachedLoggerLevel(false); 265 | testDetachedLoggerLevel(true); 266 | }); 267 | 268 | test('log messages regardless of hierarchy', () { 269 | void testDetachedLoggerOnRecord(bool withHierarchy) { 270 | var calls = 0; 271 | void handler(_) => calls += 1; 272 | 273 | hierarchicalLoggingEnabled = withHierarchy; 274 | 275 | final detached = Logger.detached('a'); 276 | detached.level = Level.ALL; 277 | detached.onRecord.listen(handler); 278 | 279 | Logger.root.info('foo'); 280 | expect(calls, 0); 281 | 282 | detached.info('foo'); 283 | detached.info('foo'); 284 | expect(calls, 2); 285 | } 286 | 287 | testDetachedLoggerOnRecord(false); 288 | testDetachedLoggerOnRecord(true); 289 | }); 290 | }); 291 | 292 | group('mutating levels', () { 293 | final root = Logger.root; 294 | final a = Logger('a'); 295 | final b = Logger('a.b'); 296 | final c = Logger('a.b.c'); 297 | final d = Logger('a.b.c.d'); 298 | final e = Logger('a.b.c.d.e'); 299 | 300 | setUp(() { 301 | hierarchicalLoggingEnabled = true; 302 | root.level = Level.INFO; 303 | a.level = null; 304 | b.level = null; 305 | c.level = null; 306 | d.level = null; 307 | e.level = null; 308 | root.clearListeners(); 309 | a.clearListeners(); 310 | b.clearListeners(); 311 | c.clearListeners(); 312 | d.clearListeners(); 313 | e.clearListeners(); 314 | hierarchicalLoggingEnabled = false; 315 | root.level = Level.INFO; 316 | }); 317 | 318 | test('cannot set level if hierarchy is disabled', () { 319 | expect(() => a.level = Level.FINE, throwsUnsupportedError); 320 | }); 321 | 322 | test('cannot set the level to null on the root logger', () { 323 | expect(() => root.level = null, throwsUnsupportedError); 324 | }); 325 | 326 | test('cannot set the level to null on a detached logger', () { 327 | expect(() => Logger.detached('l').level = null, throwsUnsupportedError); 328 | }); 329 | 330 | test('loggers effective level - no hierarchy', () { 331 | expect(root.level, equals(Level.INFO)); 332 | expect(a.level, equals(Level.INFO)); 333 | expect(b.level, equals(Level.INFO)); 334 | 335 | root.level = Level.SHOUT; 336 | 337 | expect(root.level, equals(Level.SHOUT)); 338 | expect(a.level, equals(Level.SHOUT)); 339 | expect(b.level, equals(Level.SHOUT)); 340 | }); 341 | 342 | test('loggers effective level - with hierarchy', () { 343 | hierarchicalLoggingEnabled = true; 344 | expect(root.level, equals(Level.INFO)); 345 | expect(a.level, equals(Level.INFO)); 346 | expect(b.level, equals(Level.INFO)); 347 | expect(c.level, equals(Level.INFO)); 348 | 349 | root.level = Level.SHOUT; 350 | b.level = Level.FINE; 351 | 352 | expect(root.level, equals(Level.SHOUT)); 353 | expect(a.level, equals(Level.SHOUT)); 354 | expect(b.level, equals(Level.FINE)); 355 | expect(c.level, equals(Level.FINE)); 356 | }); 357 | 358 | test('loggers effective level - with changing hierarchy', () { 359 | hierarchicalLoggingEnabled = true; 360 | d.level = Level.SHOUT; 361 | hierarchicalLoggingEnabled = false; 362 | 363 | expect(root.level, Level.INFO); 364 | expect(d.level, root.level); 365 | expect(e.level, root.level); 366 | }); 367 | 368 | test('isLoggable is appropriate', () { 369 | hierarchicalLoggingEnabled = true; 370 | root.level = Level.SEVERE; 371 | c.level = Level.ALL; 372 | e.level = Level.OFF; 373 | 374 | expect(root.isLoggable(Level.SHOUT), isTrue); 375 | expect(root.isLoggable(Level.SEVERE), isTrue); 376 | expect(root.isLoggable(Level.WARNING), isFalse); 377 | expect(c.isLoggable(Level.FINEST), isTrue); 378 | expect(c.isLoggable(Level.FINE), isTrue); 379 | expect(e.isLoggable(Level.SHOUT), isFalse); 380 | }); 381 | 382 | test('add/remove handlers - no hierarchy', () { 383 | var calls = 0; 384 | void handler(_) { 385 | calls++; 386 | } 387 | 388 | final sub = c.onRecord.listen(handler); 389 | root.info('foo'); 390 | root.info('foo'); 391 | expect(calls, equals(2)); 392 | sub.cancel(); 393 | root.info('foo'); 394 | expect(calls, equals(2)); 395 | }); 396 | 397 | test('add/remove handlers - with hierarchy', () { 398 | hierarchicalLoggingEnabled = true; 399 | var calls = 0; 400 | void handler(_) { 401 | calls++; 402 | } 403 | 404 | c.onRecord.listen(handler); 405 | root.info('foo'); 406 | root.info('foo'); 407 | expect(calls, equals(0)); 408 | }); 409 | 410 | test('logging methods store appropriate level', () { 411 | root.level = Level.ALL; 412 | final rootMessages = []; 413 | root.onRecord.listen((record) { 414 | rootMessages.add('${record.level}: ${record.message}'); 415 | }); 416 | 417 | root.finest('1'); 418 | root.finer('2'); 419 | root.fine('3'); 420 | root.config('4'); 421 | root.info('5'); 422 | root.warning('6'); 423 | root.severe('7'); 424 | root.shout('8'); 425 | 426 | expect( 427 | rootMessages, 428 | equals([ 429 | 'FINEST: 1', 430 | 'FINER: 2', 431 | 'FINE: 3', 432 | 'CONFIG: 4', 433 | 'INFO: 5', 434 | 'WARNING: 6', 435 | 'SEVERE: 7', 436 | 'SHOUT: 8' 437 | ])); 438 | }); 439 | 440 | test('logging methods store exception', () { 441 | root.level = Level.ALL; 442 | final rootMessages = []; 443 | root.onRecord.listen((r) { 444 | rootMessages.add('${r.level}: ${r.message} ${r.error}'); 445 | }); 446 | 447 | root.finest('1'); 448 | root.finer('2'); 449 | root.fine('3'); 450 | root.config('4'); 451 | root.info('5'); 452 | root.warning('6'); 453 | root.severe('7'); 454 | root.shout('8'); 455 | root.finest('1', 'a'); 456 | root.finer('2', 'b'); 457 | root.fine('3', ['c']); 458 | root.config('4', 'd'); 459 | root.info('5', 'e'); 460 | root.warning('6', 'f'); 461 | root.severe('7', 'g'); 462 | root.shout('8', 'h'); 463 | 464 | expect( 465 | rootMessages, 466 | equals([ 467 | 'FINEST: 1 null', 468 | 'FINER: 2 null', 469 | 'FINE: 3 null', 470 | 'CONFIG: 4 null', 471 | 'INFO: 5 null', 472 | 'WARNING: 6 null', 473 | 'SEVERE: 7 null', 474 | 'SHOUT: 8 null', 475 | 'FINEST: 1 a', 476 | 'FINER: 2 b', 477 | 'FINE: 3 [c]', 478 | 'CONFIG: 4 d', 479 | 'INFO: 5 e', 480 | 'WARNING: 6 f', 481 | 'SEVERE: 7 g', 482 | 'SHOUT: 8 h' 483 | ])); 484 | }); 485 | 486 | test('message logging - no hierarchy', () { 487 | root.level = Level.WARNING; 488 | final rootMessages = []; 489 | final aMessages = []; 490 | final cMessages = []; 491 | c.onRecord.listen((record) { 492 | cMessages.add('${record.level}: ${record.message}'); 493 | }); 494 | a.onRecord.listen((record) { 495 | aMessages.add('${record.level}: ${record.message}'); 496 | }); 497 | root.onRecord.listen((record) { 498 | rootMessages.add('${record.level}: ${record.message}'); 499 | }); 500 | 501 | root.info('1'); 502 | root.fine('2'); 503 | root.shout('3'); 504 | 505 | b.info('4'); 506 | b.severe('5'); 507 | b.warning('6'); 508 | b.fine('7'); 509 | 510 | c.fine('8'); 511 | c.warning('9'); 512 | c.shout('10'); 513 | 514 | expect( 515 | rootMessages, 516 | equals([ 517 | // 'INFO: 1' is not loggable 518 | // 'FINE: 2' is not loggable 519 | 'SHOUT: 3', 520 | // 'INFO: 4' is not loggable 521 | 'SEVERE: 5', 522 | 'WARNING: 6', 523 | // 'FINE: 7' is not loggable 524 | // 'FINE: 8' is not loggable 525 | 'WARNING: 9', 526 | 'SHOUT: 10' 527 | ])); 528 | 529 | // no hierarchy means we all hear the same thing. 530 | expect(aMessages, equals(rootMessages)); 531 | expect(cMessages, equals(rootMessages)); 532 | }); 533 | 534 | test('message logging - with hierarchy', () { 535 | hierarchicalLoggingEnabled = true; 536 | 537 | b.level = Level.WARNING; 538 | 539 | final rootMessages = []; 540 | final aMessages = []; 541 | final cMessages = []; 542 | c.onRecord.listen((record) { 543 | cMessages.add('${record.level}: ${record.message}'); 544 | }); 545 | a.onRecord.listen((record) { 546 | aMessages.add('${record.level}: ${record.message}'); 547 | }); 548 | root.onRecord.listen((record) { 549 | rootMessages.add('${record.level}: ${record.message}'); 550 | }); 551 | 552 | root.info('1'); 553 | root.fine('2'); 554 | root.shout('3'); 555 | 556 | b.info('4'); 557 | b.severe('5'); 558 | b.warning('6'); 559 | b.fine('7'); 560 | 561 | c.fine('8'); 562 | c.warning('9'); 563 | c.shout('10'); 564 | 565 | expect( 566 | rootMessages, 567 | equals([ 568 | 'INFO: 1', 569 | // 'FINE: 2' is not loggable 570 | 'SHOUT: 3', 571 | // 'INFO: 4' is not loggable 572 | 'SEVERE: 5', 573 | 'WARNING: 6', 574 | // 'FINE: 7' is not loggable 575 | // 'FINE: 8' is not loggable 576 | 'WARNING: 9', 577 | 'SHOUT: 10' 578 | ])); 579 | 580 | expect( 581 | aMessages, 582 | equals([ 583 | // 1,2 and 3 are lower in the hierarchy 584 | // 'INFO: 4' is not loggable 585 | 'SEVERE: 5', 586 | 'WARNING: 6', 587 | // 'FINE: 7' is not loggable 588 | // 'FINE: 8' is not loggable 589 | 'WARNING: 9', 590 | 'SHOUT: 10' 591 | ])); 592 | 593 | expect( 594 | cMessages, 595 | equals([ 596 | // 1 - 7 are lower in the hierarchy 597 | // 'FINE: 8' is not loggable 598 | 'WARNING: 9', 599 | 'SHOUT: 10' 600 | ])); 601 | }); 602 | 603 | test('message logging - lazy functions', () { 604 | root.level = Level.INFO; 605 | final messages = []; 606 | root.onRecord.listen((record) { 607 | messages.add('${record.level}: ${record.message}'); 608 | }); 609 | 610 | var callCount = 0; 611 | String myClosure() => '${++callCount}'; 612 | 613 | root.info(myClosure); 614 | root.finer(myClosure); // Should not get evaluated. 615 | root.warning(myClosure); 616 | 617 | expect( 618 | messages, 619 | equals([ 620 | 'INFO: 1', 621 | 'WARNING: 2', 622 | ])); 623 | }); 624 | 625 | test('message logging - calls toString', () { 626 | root.level = Level.INFO; 627 | final messages = []; 628 | final objects = []; 629 | final object = Object(); 630 | root.onRecord.listen((record) { 631 | messages.add('${record.level}: ${record.message}'); 632 | objects.add(record.object); 633 | }); 634 | 635 | root.info(5); 636 | root.info(false); 637 | root.info([1, 2, 3]); 638 | root.info(() => 10); 639 | root.info(object); 640 | 641 | expect( 642 | messages, 643 | equals([ 644 | 'INFO: 5', 645 | 'INFO: false', 646 | 'INFO: [1, 2, 3]', 647 | 'INFO: 10', 648 | "INFO: Instance of 'Object'" 649 | ])); 650 | 651 | expect(objects, [ 652 | 5, 653 | false, 654 | [1, 2, 3], 655 | 10, 656 | object 657 | ]); 658 | }); 659 | }); 660 | 661 | group('recordStackTraceAtLevel', () { 662 | final root = Logger.root; 663 | tearDown(() { 664 | recordStackTraceAtLevel = Level.OFF; 665 | root.clearListeners(); 666 | }); 667 | 668 | test('no stack trace by default', () { 669 | final records = []; 670 | root.onRecord.listen(records.add); 671 | root.severe('hello'); 672 | root.warning('hello'); 673 | root.info('hello'); 674 | expect(records, hasLength(3)); 675 | expect(records[0].stackTrace, isNull); 676 | expect(records[1].stackTrace, isNull); 677 | expect(records[2].stackTrace, isNull); 678 | }); 679 | 680 | test('trace recorded only on requested levels', () { 681 | final records = []; 682 | recordStackTraceAtLevel = Level.WARNING; 683 | root.onRecord.listen(records.add); 684 | root.severe('hello'); 685 | root.warning('hello'); 686 | root.info('hello'); 687 | expect(records, hasLength(3)); 688 | expect(records[0].stackTrace, isNotNull); 689 | expect(records[1].stackTrace, isNotNull); 690 | expect(records[2].stackTrace, isNull); 691 | }); 692 | 693 | test('defaults a missing trace', () { 694 | final records = []; 695 | recordStackTraceAtLevel = Level.SEVERE; 696 | root.onRecord.listen(records.add); 697 | root.severe('hello'); 698 | expect(records.single.stackTrace, isNotNull); 699 | }); 700 | 701 | test('defaults an empty trace', () { 702 | final records = []; 703 | recordStackTraceAtLevel = Level.SEVERE; 704 | root.onRecord.listen(records.add); 705 | root.severe('hello', 'error', StackTrace.empty); 706 | expect(records.single.stackTrace, isNot(StackTrace.empty)); 707 | }); 708 | 709 | test('provided trace is used if given', () { 710 | final trace = StackTrace.current; 711 | final records = []; 712 | recordStackTraceAtLevel = Level.WARNING; 713 | root.onRecord.listen(records.add); 714 | root.severe('hello'); 715 | root.warning('hello', 'a', trace); 716 | expect(records, hasLength(2)); 717 | expect(records[0].stackTrace, isNot(equals(trace))); 718 | expect(records[1].stackTrace, trace); 719 | }); 720 | 721 | test('error also generated when generating a trace', () { 722 | final records = []; 723 | recordStackTraceAtLevel = Level.WARNING; 724 | root.onRecord.listen(records.add); 725 | root.severe('hello'); 726 | root.warning('hello'); 727 | root.info('hello'); 728 | expect(records, hasLength(3)); 729 | expect(records[0].error, isNotNull); 730 | expect(records[1].error, isNotNull); 731 | expect(records[2].error, isNull); 732 | }); 733 | 734 | test('listen for level changed', () { 735 | final levels = []; 736 | root.level = Level.ALL; 737 | root.onLevelChanged.listen(levels.add); 738 | root.level = Level.SEVERE; 739 | root.level = Level.WARNING; 740 | expect(levels, hasLength(2)); 741 | }); 742 | 743 | test('onLevelChanged is not emited if set the level to the same value', () { 744 | final levels = []; 745 | root.level = Level.ALL; 746 | root.onLevelChanged.listen(levels.add); 747 | root.level = Level.ALL; 748 | expect(levels, hasLength(0)); 749 | }); 750 | 751 | test('setting level in a loop throws state error', () { 752 | root.level = Level.ALL; 753 | root.onLevelChanged.listen((event) { 754 | // Cannot fire new event. Controller is already firing an event 755 | expect(() => root.level = Level.SEVERE, throwsStateError); 756 | }); 757 | root.level = Level.WARNING; 758 | expect(root.level, Level.SEVERE); 759 | }); 760 | }); 761 | } 762 | --------------------------------------------------------------------------------