Repository: gcovr/gcovr

Issue:
Replace custom logger with python logging module
It would be easier and simplify integration with other modules to use the standard python `logging` package instead of relying on the custom solution.

This way it would be also easier to add different log levels and redirecting of logs to file instead of stdout.

Discussion:
latk: Moving to standard logging sounds sensible. There are some caveats:

* Some output MUST go to stdout/stderr.
* Passing the logger object everywhere seems annoying.
* As currently written, this seems to complicate the tests.

## Some output must go to stdout/stderr

By default, all errors, warnings, and verbose-messages must go to stderr in order to conform to expectations about Unix-y tools. This can likely be achieved by configuring the logger appropriately.

There are a few outputs that also must go to stdout because they are essential output rather than messages:

* the summary-writer output
* the `--version` output
* the `--help` output (bypasses logging anyway)

It could make sense to always print these to stdout directly, without involving the logger.

## Passing the logger object everywhere seems annoying

The current `Logger` class exists so that it's not necessary to pass `options.verbose` around, and to provide basic utilities for formatting.

Real logging libraries (incl. Python's `logging`) typically use a singleton design where the logger can be retrieved using a global function, and the logger is configured once on program startup. So a typical Python source file would start with:

```python
import logging

LOG = logging.getLogger(__name__)
```

(in case the logger hierachy should follow the module hierarchy).

This PR adds `logger` arguments to a lot of functions. If we're already moving to standard logging, it would make sense to embrace this fully and to get rid of these explicit arguments. The formatting tasks of the existing Logger class can likely be moved to a logging-plugin. Unfortunately, the logging library design is so complicated that I wouldn't know how to do this.

Extra problem: some tests abuse logging for fault injection purposes. Untangling this might be tricky.

## Complications with testing

Right now, some tests seem to break. A lot of tests capture/redirect stderr+stdout in order to verify that a certain output was generated.

As we are using Pytest to run the tests, a lot of the `capsys` fixtures can probably be changed to the `caplog` fixture instead, see <https://docs.pytest.org/en/stable/logging.html>. But this might fail to detect actual regressions in the output.

It could also be that Pytest is currently conflicting with the logging. E.g. the tests like `test_empty_root` essentially runs `gcovr --root ''`. In reality, this works fine. In the tests, the capsys-captured stderr output seems to contain a `--- Logging error ---` which originates from within the logging module. Perhaps our `capture()` helper function that runs gcovr's main() needs to somehow isolate gcovr's logging from pytest's logging? I know far too little about the standard-library logging module to be of any help with debugging here.
Verequus: ### Some output MUST go to stdout/stderr.

Currently, the existing logger doesn't do that. Or rather, it entirely depends on the used function where the log ends up. It is only one endpoint. So anything which needs to go to both endpoints would need to call the logger twice already. Which isn't impacted in the first place due to switching to python logging as I implemented it. One can enhance the gcovr logger to provide a function which logs to both endpoints at once. Unless not using the gcovr logger class at all is desired, but that leaves the situation no worse than it is now.

For explicit stdout messages (the summary at least), I'd like to have them still at least in addition provide them in the logging system. In my project, we can't capture the output if it is only going to stdout/stderr. I think the easiest solution is to have to a secondary logger which is swallowing the output by default which stops having the output on stdout twice.

### Passing the logger object everywhere seems annoying.

I wasn't sure how to approach this change. I went with the current approach, because it was closer to what the code currently does. But I can try to get to the singleton approach instead. If you refer with formatting tasks to the pattern like "(ERROR) ", then it should be possible to automatically have them added in depending on the debug level. At least I've seen a similar example when looking for doing the proposed logging configuration.

In regards of BrokenLogger replacement: Patching the call to the log should work, but likely would require a way to trigger that only for DEBUG level. Maybe one could simply add an additional filter to a log handler instead. But that would require removing the additional filter afterwards. From a glance it seems the test can do the cleanup work as well.

### Complications with testing

Unnoticed regressions imply that certain output would never end up on the logging infrastructure. Is that actually possible? Maybe we need an exception handler to ensure capturing the exception messages, but otherwise where could this occur? Inside gcovr we have complete control over the output. And I guess that used libraries don't use stdout/stderr themselves.

In regards to pytest itself do logging, it should be possible to disable that completely. But before I investigate this, is the pytest log output merely an accidental catch?

Spacetown: I'm with @latk to use the singleton object. We should use child loggers for each part of gcovr, removing the currently used arguments. The logging must be done to stderr. The format can be discussed or maybe configured by the user.
Writing output files to stdout should be done directly, bypassing the logger.

Why isn't it possible for you to capture stdout and stderr in your tests? The output shouldn't be added to the log. You can use a output file to get the data as file.

latk: > Why isn't it possible for you to capture stdout and stderr in your tests? The output shouldn't be added to the log. You can use a output file to get the data as file.

In `test_args.py` we call the `main()` function directly (no separate process), and use Pytest's `capsys` fixtures to get stdout/stderr streams. This doesn't seem to properly affect logging though, and the output is garbled. Example output:

```none
_____________________________ test_invalid_objdir ______________________________

capsys = <_pytest.capture.CaptureFixture object at 0x7f8cb35dd080>

    def test_invalid_objdir(capsys):
        c = capture(capsys, ['--object-directory', 'not-existing-dir'])
        assert c.out == ''
>       assert c.err.startswith(
            '(ERROR) Bad --object-directory option.')
E       assert False
E        +  where False = <built-in method startswith of str object at 0x1b9f5a0>('(ERROR) Bad --object-directory option.')
E        +    where <built-in method startswith of str object at 0x1b9f5a0> = '--- Logging error ---\nTraceback (most recent call last):\n  File "/usr/lib/python3.6/logging/__init__.py", line 996,... does not exist.\'\nArguments: ()\n(ERROR) Bad --object-directory option.\n\tThe specified directory does not exist.\n'.startswith
E        +      where '--- Logging error ---\nTraceback (most recent call last):\n  File "/usr/lib/python3.6/logging/__init__.py", line 996,... does not exist.\'\nArguments: ()\n(ERROR) Bad --object-directory option.\n\tThe specified directory does not exist.\n' = <gcovr.tests.test_args.CaptureObject object at 0x7f8cb36b0320>.err

gcovr/tests/test_args.py:98: AssertionError
----------------------------- Captured stderr call -----------------------------
(ERROR) Bad --object-directory option.
        The specified directory does not exist.
(ERROR) Bad --object-directory option.
        The specified directory does not exist.
------------------------------ Captured log call -------------------------------
ERROR    gcovr.utils:utils.py:296 (ERROR) Bad --object-directory option.
        The specified directory does not exist.
```

Observations:

* Pytest captures log calls in addition to stdout/stderr.
* Pytest's captured stderr repeats the same message twice.
* The stderr captured via the `capsys` fixture in our `c` Capture object contains a logging error traceback message, suggesting an internal error in the logging library.

I think this points to a misconfiguration, but it could also be that Pytest is trying to be too clever and is getting in the way here. Using Pytest's `caplog` fixture may or may not help.
Spacetown: > In my project, we can't capture the output if it is only going to stdout/stderr.

@latk I was wondering about this statement. I think our test can be adapted to work with the logger of python.
Verequus: I tried to address to the raised objections. I've now placed logger creation in singleton objects where needed. I've gone with a fixed name to ensure that every file does access the same preconfigured logger. It is possible to change that name for different files, but I wasn't sure which strategy is intended to be used. But any new logger needs to be preconfigured in main() like the main one. It also means that as input you can't use additional arguments for formatting (not sure what those can be used for - Python documentation is completely lacking there). Instead, format strings take over the work. Which just moves the formatting before the log call and not during, so there is no practical issue with that change.

I remembered that you can patch objects in Python for UTs, so I replaced the BrokenLogger instance appropriately. Unfortunately, I'm not sure if that worked since the logging for tests still interferes. Replacing capsys with caplog everywhere merely increased failing tests, so that is not going to work.

I also changed the summary to have a parameter which if not given simply outputs to stdout as before. But if given, uses the logger. For whatever reason, a test breaks but I couldn't trace what the issue is here.

I hope that this overall change is closer to your view of how the code should look like. Still, the breaking tests are beyound my capability to fix.
Spacetown: Oh, there is a problem with your updated branch. For me it seems that this is a merge mistake.
Also bak files from Notepad++ are added.

I've removed the last merge on your branch.
Verequus: > Oh, there is a problem with your updated branch. For me it seems that this is a merge mistake.
> Also bak files from Notepad++ are added.

> I've removed the last merge on your branch.

Now I'm not sure exactly I need to aside of re-removing the bak files (pretty sure I had that in the merge). How can I see what changes you actually removed?
Verequus: I've updated the code to remove erroneous usage of logger and a number of other mistakes buried in the test output. I also managed to fix most tests which did break. Or at least, those which were definitely broken. I'm not sure if the other tests do work or appear to work only because the output by chance matches the checks. I didn't have time to figure that part out.

In any case, only 3 tests still complain and I have no clue how to fix them.
Verequus: No chance to look at this yet? Just wondering about the status.
Spacetown: I fixed the test:
- `logger.info` instead of `print`was used for the output of `--version`.
- The doctest for `_ExclusionRangeWarnings` fails because the add of the handler didn't work as expected. Now the `caplog` fixture is used to check the messages.
- In `test_filter_backslashes_are_detected` the messages where compared with a trailing linebreak.

I try to check the code tomorrow.
codecov[bot]: # [Codecov](https://codecov.io/gh/gcovr/gcovr/pull/540?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr) Report
> Merging [#540](https://codecov.io/gh/gcovr/gcovr/pull/540?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr) (e4dfc51) into [master](https://codecov.io/gh/gcovr/gcovr/commit/8e0c377b2a0bbbcd76deff368b260302e6d0789e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr) (8e0c377) will **decrease** coverage by `0.91%`.
> The diff coverage is `95.61%`.

[![Impacted file tree graph](https://codecov.io/gh/gcovr/gcovr/pull/540/graphs/tree.svg?width=650&height=150&src=pr&token=rXIHMZoRaQ&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr)](https://codecov.io/gh/gcovr/gcovr/pull/540?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr)

```diff
@@            Coverage Diff             @@
##           master     #540      +/-   ##
==========================================
- Coverage   95.73%   94.81%   -0.92%     
==========================================
  Files          23       23              
  Lines        3213     3280      +67     
  Branches      603      603              
==========================================
+ Hits         3076     3110      +34     
- Misses         66       96      +30     
- Partials       71       74       +3     
```

| Flag | Coverage Δ | |
|---|---|---|
| ubuntu-18.04 | `94.81% <95.61%> (-0.02%)` | :arrow_down: |
| windows-2019 | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/gcovr/gcovr/pull/540?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr) | Coverage Δ | |
|---|---|---|
| [gcovr/gcov.py](https://codecov.io/gh/gcovr/gcovr/pull/540/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr#diff-Z2NvdnIvZ2Nvdi5weQ==) | `81.27% <73.07%> (-1.36%)` | :arrow_down: |
| [gcovr/writer/html.py](https://codecov.io/gh/gcovr/gcovr/pull/540/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr#diff-Z2NvdnIvd3JpdGVyL2h0bWwucHk=) | `95.32% <85.71%> (-0.59%)` | :arrow_down: |
| [gcovr/utils.py](https://codecov.io/gh/gcovr/gcovr/pull/540/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr#diff-Z2NvdnIvdXRpbHMucHk=) | `84.07% <90.90%> (-7.39%)` | :arrow_down: |
| [gcovr/\_\_main\_\_.py](https://codecov.io/gh/gcovr/gcovr/pull/540/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr#diff-Z2NvdnIvX19tYWluX18ucHk=) | `91.81% <92.30%> (+0.14%)` | :arrow_up: |
| [gcovr/decision\_analysis.py](https://codecov.io/gh/gcovr/gcovr/pull/540/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr#diff-Z2NvdnIvZGVjaXNpb25fYW5hbHlzaXMucHk=) | `95.45% <100.00%> (+0.06%)` | :arrow_up: |
| [gcovr/gcov\_parser.py](https://codecov.io/gh/gcovr/gcovr/pull/540/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr#diff-Z2NvdnIvZ2Nvdl9wYXJzZXIucHk=) | `100.00% <100.00%> (ø)` | |
| [gcovr/tests/test\_args.py](https://codecov.io/gh/gcovr/gcovr/pull/540/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr#diff-Z2NvdnIvdGVzdHMvdGVzdF9hcmdzLnB5) | `98.84% <100.00%> (-1.16%)` | :arrow_down: |
| [gcovr/tests/test\_gcov\_parser.py](https://codecov.io/gh/gcovr/gcovr/pull/540/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr#diff-Z2NvdnIvdGVzdHMvdGVzdF9nY292X3BhcnNlci5weQ==) | `100.00% <100.00%> (ø)` | |
| [gcovr/writer/json.py](https://codecov.io/gh/gcovr/gcovr/pull/540/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr#diff-Z2NvdnIvd3JpdGVyL2pzb24ucHk=) | `96.66% <100.00%> (ø)` | |
| [gcovr/writer/summary.py](https://codecov.io/gh/gcovr/gcovr/pull/540/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr#diff-Z2NvdnIvd3JpdGVyL3N1bW1hcnkucHk=) | `100.00% <100.00%> (ø)` | |
| ... and [6 more](https://codecov.io/gh/gcovr/gcovr/pull/540/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/gcovr/gcovr/pull/540?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/gcovr/gcovr/pull/540?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr). Last update [8e0c377...e4dfc51](https://codecov.io/gh/gcovr/gcovr/pull/540?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gcovr).

Spacetown: @Verequus The tests are fixed now. Please can you double check the changes?
Verequus: @Spacetown I noticed an inconsistency with my code changes. You used:

>     message = c.record_tuples[0]
>     assert message[1] == logging.WARNING
>     assert message[2] == "HTML output skipped - consider providing an output file with `--html=OUTPUT`."
>     message = c.record_tuples[1]
>     assert message[1] == logging.WARNING
>     assert message[2] == "Sonarqube output skipped - consider providing an output file with `--sonarqube=OUTPUT`."
>     message = c.record_tuples[2]
>     assert message[1] == logging.WARNING
>     assert message[2] == "Coveralls output skipped - consider providing an output file with `--coveralls=OUTPUT`."

So you reused message. I would have written this instead:

>     message0 = c.record_tuples[0]
>     assert message0[1] == logging.WARNING
>     assert message0[2] == "HTML output skipped - consider providing an output file with `--html=OUTPUT`."
>     message1 = c.record_tuples[1]
>     assert message1[1] == logging.WARNING
>     assert message1[2] == "Sonarqube output skipped - consider providing an output file with `--sonarqube=OUTPUT`."
>     message2 = c.record_tuples[2]
>     assert message2[1] == logging.WARNING
>     assert message2[2] == "Coveralls output skipped - consider providing an output file with `--coveralls=OUTPUT`."

So numbering the variables. I don't have any particular preferences (originally expected to have to reuse the variables more), but it should only done one way. What do you want?

Otherwise I'm ok. Just wondering, why are double quotes preferred in the first place?
Spacetown: I also have no preference. The double quotes are required by black. Single quotes shall only be used if you use double quotes inside the string.
Verequus: I've chosen your way since it makes it for you easier to maintain the code. So I'd say you can merge this, if you accept the codecov failures.

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c00bd54a..0dcd2200 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -23,6 +23,7 @@ Improvements and new features:
 - Add :option:`--no-markers` to ignore exclusion markers in code. (:issue:`361`)
 - Generate also a Report of Excluded Coverage. (:issue:`503`)
 - Added :option:`--decisions` to add decision coverage to HTML and JSON output. (:issue:`350`)
+- Replace own logger with python logger module. (:issue:`540`)
 
 Documentation:
 
diff --git a/gcovr/__main__.py b/gcovr/__main__.py
old mode 100755
new mode 100644
index ba8a2b25..695f8763
--- a/gcovr/__main__.py
+++ b/gcovr/__main__.py
@@ -15,7 +15,7 @@
 # For more information, see the README.rst file.
 #
 # ****************************************************************************
-
+import logging
 import os
 import re
 import sys
@@ -31,7 +31,7 @@ from .configuration import (
 from .gcov import (find_existing_gcov_files, find_datafiles,
                    process_existing_gcov_file, process_datafile)
 from .utils import (get_global_stats, AlwaysMatchFilter,
-                    DirectoryPrefixFilter, Logger)
+                    DirectoryPrefixFilter, configure_logging)
 from .version import __version__
 from .workers import Workers
 
@@ -47,10 +47,13 @@ from .writer.sonarqube import print_sonarqube_report
 from .writer.coveralls import print_coveralls_report
 
 
+logger = logging.getLogger("gcovr")
+
+
 #
 # Exits with status 2 if below threshold
 #
-def fail_under(covdata, threshold_line, threshold_branch, logger):
+def fail_under(covdata, threshold_line, threshold_branch):
     (lines_total, lines_covered, percent_lines,
         functions_total, functions_covered, percent_functions,
         branches_total, branches_covered,
@@ -63,10 +66,10 @@ def fail_under(covdata, threshold_line, threshold_branch, logger):
     branch_nok = False
     if percent_lines < threshold_line:
         line_nok = True
-        logger.error("failed minimum line coverage (got {}%, minimum {}%)", percent_lines, threshold_line)
+        logger.error(f"failed minimum line coverage (got {percent_lines}%, minimum {threshold_line}%)")
     if percent_branches < threshold_branch:
         branch_nok = True
-        logger.error("failed minimum branch coverage (got {}%, minimum {}%)", percent_branches, threshold_branch)
+        logger.error(f"failed minimum branch coverage (got {percent_branches}%, minimum {threshold_branch}%)")
     if line_nok and branch_nok:
         sys.exit(6)
     if line_nok:
@@ -135,6 +138,7 @@ class Options(object):
 
 
 def main(args=None):
+    configure_logging()
     parser = create_argument_parser()
     cli_options = parser.parse_args(args=args)
 
@@ -150,14 +154,11 @@ def main(args=None):
         [cfg_options, cli_options.__dict__])
     options = Options(**options_dict)
 
-    logger = Logger(options.verbose)
+    if options.verbose:
+        logger.setLevel(logging.DEBUG)
 
     if cli_options.version:
-        logger.msg(
-            "gcovr {version}\n"
-            "\n"
-            "{copyright}",
-            version=__version__, copyright=COPYRIGHT)
+        print(f"gcovr {__version__}\n\n{COPYRIGHT}")
         sys.exit(0)
 
     if options.html_title == '':
@@ -172,9 +173,8 @@ def main(args=None):
 
     if options.html_medium_threshold > options.html_high_threshold:
         logger.error(
-            "value of --html-medium-threshold={} should be\n"
-            "lower than or equal to the value of --html-high-threshold={}.",
-            options.html_medium_threshold, options.html_high_threshold)
+            f"value of --html-medium-threshold={options.html_medium_threshold} should be\n"
+            f"lower than or equal to the value of --html-high-threshold={options.html_high_threshold}.")
         sys.exit(1)
 
     if options.html_tab_size < 1:
@@ -209,7 +209,7 @@ def main(args=None):
         while os.sep + os.sep in tmp:
             tmp = tmp.replace(os.sep + os.sep, os.sep)
         if normpath(options.objdir) != tmp:
-            logger.warn(
+            logger.warning(
                 "relative referencing in --object-directory.\n"
                 "\tthis could cause strange errors when gcovr attempts to\n"
                 "\tidentify the original gcc working directory.")
@@ -239,16 +239,16 @@ def main(args=None):
 
     if options.exclude_dirs is not None:
         options.exclude_dirs = [
-            f.build_filter(logger) for f in options.exclude_dirs]
+            f.build_filter() for f in options.exclude_dirs]
 
-    options.exclude = [f.build_filter(logger) for f in options.exclude]
-    options.filter = [f.build_filter(logger) for f in options.filter]
+    options.exclude = [f.build_filter() for f in options.exclude]
+    options.filter = [f.build_filter() for f in options.filter]
     if not options.filter:
         options.filter = [DirectoryPrefixFilter(options.root_dir)]
 
     options.gcov_exclude = [
-        f.build_filter(logger) for f in options.gcov_exclude]
-    options.gcov_filter = [f.build_filter(logger) for f in options.gcov_filter]
+        f.build_filter() for f in options.gcov_exclude]
+    options.gcov_filter = [f.build_filter() for f in options.gcov_filter]
     if not options.gcov_filter:
         options.gcov_filter = [AlwaysMatchFilter()]
 
@@ -261,9 +261,9 @@ def main(args=None):
         ('--gcov-exclude', options.gcov_exclude),
         ('--exclude-directories', options.exclude_dirs),
     ]:
-        logger.verbose_msg('Filters for {}: ({})', name, len(filters))
+        logger.debug(f"Filters for {name}: ({len(filters)})")
         for f in filters:
-            logger.verbose_msg('- {}', f)
+            logger.debug(f" - {f}")
 
     if options.exclude_lines_by_pattern:
         try:
@@ -271,20 +271,19 @@ def main(args=None):
         except re.error as e:
             logger.error(
                 "--exclude-lines-by-pattern: "
-                "Invalid regular expression: {}, error: {}",
-                repr(options.exclude_lines_by_pattern), e)
+                f"Invalid regular expression: {repr(options.exclude_lines_by_pattern)}, error: {e}")
             sys.exit(1)
 
     covdata = dict()
     if options.add_tracefile:
-        collect_coverage_from_tracefiles(covdata, options, logger)
+        collect_coverage_from_tracefiles(covdata, options)
     else:
-        collect_coverage_from_gcov(covdata, options, logger)
+        collect_coverage_from_gcov(covdata, options)
 
-    logger.verbose_msg("Gathered coveraged data for {} files", len(covdata))
+    logger.debug(f"Gathered coveraged data for {len(covdata)} files")
 
     # Print reports
-    error_occurred = print_reports(covdata, options, logger)
+    error_occurred = print_reports(covdata, options)
     if error_occurred:
         logger.error(
             "Error occurred while printing reports"
@@ -292,10 +291,10 @@ def main(args=None):
         sys.exit(7)
 
     if options.fail_under_line > 0.0 or options.fail_under_branch > 0.0:
-        fail_under(covdata, options.fail_under_line, options.fail_under_branch, logger)
+        fail_under(covdata, options.fail_under_line, options.fail_under_branch)
 
 
-def collect_coverage_from_tracefiles(covdata, options, logger):
+def collect_coverage_from_tracefiles(covdata, options):
     datafiles = set()
 
     for trace_files_regex in options.add_tracefile:
@@ -313,7 +312,7 @@ def collect_coverage_from_tracefiles(covdata, options, logger):
     gcovr_json_files_to_coverage(datafiles, covdata, options)
 
 
-def collect_coverage_from_gcov(covdata, options, logger):
+def collect_coverage_from_gcov(covdata, options):
     datafiles = set()
 
     find_files = find_datafiles
@@ -330,14 +329,14 @@ def collect_coverage_from_gcov(covdata, options, logger):
             options.search_paths.append(options.objdir)
 
     for search_path in options.search_paths:
-        datafiles.update(find_files(search_path, logger, options.exclude_dirs))
+        datafiles.update(find_files(search_path, options.exclude_dirs))
 
     # Get coverage data
     with Workers(options.gcov_parallel, lambda: {
                  'covdata': dict(),
                  'toerase': set(),
                  'options': options}) as pool:
-        logger.verbose_msg("Pool started with {} threads", pool.size())
+        logger.debug(f"Pool started with {pool.size()} threads")
         for file_ in datafiles:
             pool.add(process_file, file_)
         contexts = pool.wait()
@@ -355,14 +354,14 @@ def collect_coverage_from_gcov(covdata, options, logger):
             os.remove(filepath)
 
 
-def print_reports(covdata, options, logger):
+def print_reports(covdata, options):
     generators = []
 
     if options.txt:
         generators.append((
             [options.txt],
             print_text_report,
-            lambda: logger.warn(
+            lambda: logger.warning(
                 "Text output skipped - "
                 "consider providing an output file with `--txt=OUTPUT`.")))
 
@@ -370,7 +369,7 @@ def print_reports(covdata, options, logger):
         generators.append((
             [options.xml],
             print_xml_report,
-            lambda: logger.warn(
+            lambda: logger.warning(
                 "Cobertura output skipped - "
                 "consider providing an output file with `--xml=OUTPUT`.")))
 
@@ -378,7 +377,7 @@ def print_reports(covdata, options, logger):
         generators.append((
             [options.html, options.html_details],
             print_html_report,
-            lambda: logger.warn(
+            lambda: logger.warning(
                 "HTML output skipped - "
                 "consider providing an output file with `--html=OUTPUT`.")))
 
@@ -386,41 +385,41 @@ def print_reports(covdata, options, logger):
         generators.append((
             [options.sonarqube],
             print_sonarqube_report,
-            lambda: logger.warn(
+            lambda: logger.warning(
                 "Sonarqube output skipped - "
-                "consider providing output file with `--sonarqube=OUTPUT`.")))
+                "consider providing an output file with `--sonarqube=OUTPUT`.")))
 
     if options.json or options.json_pretty:
         generators.append((
             [options.json],
             print_json_report,
-            lambda: logger.warn(
+            lambda: logger.warning(
                 "JSON output skipped - "
-                "consider providing output file with `--json=OUTPUT`.")))
+                "consider providing an output file with `--json=OUTPUT`.")))
 
     if options.json_summary or options.json_summary_pretty:
         generators.append((
             [options.json_summary],
             print_json_summary_report,
-            lambda: logger.warn(
+            lambda: logger.warning(
                 "JSON summary output skipped - "
-                "consider providing output file with `--json-summary=OUTPUT`.")))
+                "consider providing an output file with `--json-summary=OUTPUT`.")))
 
     if options.csv:
         generators.append((
             [options.csv],
             print_csv_report,
-            lambda: logger.warn(
+            lambda: logger.warning(
                 "CSV output skipped - "
-                "consider providing output file with `--csv=OUTPUT`.")))
+                "consider providing an output file with `--csv=OUTPUT`.")))
 
     if options.coveralls or options.coveralls_pretty:
         generators.append((
             [options.coveralls],
             print_coveralls_report,
-            lambda: logger.warn(
+            lambda: logger.warning(
                 "Coveralls output skipped - "
-                "consider providing output file with `--coveralls=OUTPUT`.")))
+                "consider providing an output file with `--coveralls=OUTPUT`.")))
 
     generator_error_occurred = False
     reports_were_written = False
@@ -445,8 +444,7 @@ def print_reports(covdata, options, logger):
         default_output = None
 
     if default_output is not None and default_output.value is not None and not default_output_used:
-        logger.warn("--output={!r} option was provided but not used.",
-                    default_output.value)
+        logger.warning(f"--output={repr(default_output.value)} option was provided but not used.")
 
     if options.print_summary:
         print_summary(covdata)
diff --git a/gcovr/decision_analysis.py b/gcovr/decision_analysis.py
index bf2100ee..9fe076bd 100644
--- a/gcovr/decision_analysis.py
+++ b/gcovr/decision_analysis.py
@@ -5,7 +5,7 @@
 # Copyright 2013-2020 the gcovr authors
 # Copyright 2013 Sandia Corporation
 # This software is distributed under the BSD license.
-
+import logging
 import re
 
 from .coverage import (
@@ -14,6 +14,8 @@ from .coverage import (
     DecisionCoverageSwitch,
 )
 
+logger = logging.getLogger("gcovr")
+
 # for type annotations:
 if False:
     from typing import (  # noqa, pylint: disable=all
@@ -111,15 +113,12 @@ class DecisionParser(object):
             Reference to the active coverage data.
         lines:
             The encoding of the source files
-        logger:
-            The logger to which decision analysis logs should be written to.
     """
 
-    def __init__(self, fname, coverage, lines, logger):
+    def __init__(self, fname, coverage, lines):
         self.fname = fname
         self.coverage = coverage
         self.lines = lines
-        self.logger = logger
 
         # status variables for decision analysis
         self.decision_analysis_active = (
@@ -131,7 +130,7 @@ class DecisionParser(object):
         self.decision_analysis_open_brackets = 0
 
     def parse_all_lines(self):
-        self.logger.verbose_msg("Starting the decision analysis")
+        logger.debug("Starting the decision analysis")
 
         # start to iterate through the lines
         for lineno, code in self.lines:
@@ -182,9 +181,7 @@ class DecisionParser(object):
                                 # it's a compplex decision with more than 2 branches. No accurate detection possible
                                 # Set the decision to uncheckable
                                 line_coverage.decision = DecisionCoverageUncheckable()
-                                self.logger.verbose_msg(
-                                    "Uncheckable decision at line {line}", line=lineno
-                                )
+                                logger.debug(f"Uncheckable decision at line {lineno}")
                         else:
                             # normal (non-compact) branch, analyze execution of following lines
                             self.decision_analysis_active = True
@@ -218,4 +215,4 @@ class DecisionParser(object):
                                 self.coverage.line(lineno + 1).count
                             )
 
-        self.logger.verbose_msg("Decision Analysis finished!")
+        logger.debug("Decision Analysis finished!")
diff --git a/gcovr/gcov.py b/gcovr/gcov.py
index 4944717d..9adb4a54 100644
--- a/gcovr/gcov.py
+++ b/gcovr/gcov.py
@@ -15,18 +15,20 @@
 # For more information, see the README.rst file.
 #
 # ****************************************************************************
-
+import logging
 import os
 import re
 import shlex
 import subprocess
 import io
 
-from .utils import search_file, Logger, commonpath, realpath
+from .utils import search_file, commonpath, realpath
 from .workers import locked_directory
 from .coverage import FileCoverage
 from .gcov_parser import parse_metadata, parse_coverage, ParserFlags
 
+logger = logging.getLogger("gcovr")
+
 output_re = re.compile(r"[Cc]reating [`'](.*)'$")
 source_re = re.compile(
     r"(?:[Cc](?:annot|ould not) open (?:source|graph|output) file|: No such file or directory)"
@@ -34,19 +36,19 @@ source_re = re.compile(
 unknown_cla_re = re.compile(r"Unknown command line argument")
 
 
-def find_existing_gcov_files(search_path, logger, exclude_dirs):
+def find_existing_gcov_files(search_path, exclude_dirs):
     """Find .gcov files under the given search path."""
-    logger.verbose_msg("Scanning directory {} for gcov files...", search_path)
+    logger.debug(f"Scanning directory {search_path} for gcov files...")
     gcov_files = list(
         search_file(
             re.compile(r".*\.gcov$").match, search_path, exclude_dirs=exclude_dirs
         )
     )
-    logger.verbose_msg("Found {} files (and will process all of them)", len(gcov_files))
+    logger.debug(f"Found {len(gcov_files)} files (and will process all of them)")
     return gcov_files
 
 
-def find_datafiles(search_path, logger, exclude_dirs):
+def find_datafiles(search_path, exclude_dirs):
     """Find .gcda and .gcno files under the given search path.
 
     The .gcno files will *only* produce uncovered results.
@@ -54,7 +56,7 @@ def find_datafiles(search_path, logger, exclude_dirs):
     is never actually exercised by the test code.
     So we ONLY return them if there's no corresponding .gcda file.
     """
-    logger.verbose_msg("Scanning directory {} for gcda/gcno files...", search_path)
+    logger.debug(f"Scanning directory {search_path} for gcda/gcno files...")
     files = list(
         search_file(
             re.compile(r".*\.gc(da|no)$").match, search_path, exclude_dirs=exclude_dirs
@@ -76,10 +78,8 @@ def find_datafiles(search_path, logger, exclude_dirs):
         for filename in gcno_files
         if os.path.splitext(filename)[0] not in known_file_stems
     ]
-    logger.verbose_msg(
-        "Found {} files (and will process {})",
-        len(files),
-        len(gcda_files) + len(gcno_files),
+    logger.debug(
+        f"Found {len(files)} files (and will process {len(gcda_files) + len(gcno_files)})"
     )
     return gcda_files + gcno_files
 
@@ -88,8 +88,6 @@ def find_datafiles(search_path, logger, exclude_dirs):
 # Process a single gcov datafile
 #
 def process_gcov_data(data_fname, covdata, source_fname, options, currdir=None):
-    logger = Logger(options.verbose)
-
     with io.open(
         data_fname, "r", encoding=options.source_encoding, errors="replace"
     ) as INPUT:
@@ -105,11 +103,10 @@ def process_gcov_data(data_fname, covdata, source_fname, options, currdir=None):
         root_dir=options.root_dir,
         starting_dir=options.starting_dir,
         obj_dir=None if options.objdir is None else os.path.abspath(options.objdir),
-        logger=logger,
         currdir=currdir,
     )
 
-    logger.verbose_msg("Parsing coverage data for file {}", fname)
+    logger.debug(f"Parsing coverage data for file {fname}")
 
     # Return if the filename does not match the filter
     # Return if the filename matches the exclude pattern
@@ -118,11 +115,11 @@ def process_gcov_data(data_fname, covdata, source_fname, options, currdir=None):
     )
 
     if filtered:
-        logger.verbose_msg("  Filtering coverage data for file {}", fname)
+        logger.debug(f"  Filtering coverage data for file {fname}")
         return
 
     if excluded:
-        logger.verbose_msg("  Excluding coverage data for file {}", fname)
+        logger.debug(f"  Excluding coverage data for file {fname}")
         return
 
     key = os.path.normpath(fname)
@@ -146,7 +143,6 @@ def process_gcov_data(data_fname, covdata, source_fname, options, currdir=None):
     coverage = parse_coverage(
         lines,
         filename=key,
-        logger=logger,
         exclude_lines_by_pattern=options.exclude_lines_by_pattern,
         flags=parser_flags,
     )
@@ -160,7 +156,6 @@ def guess_source_file_name(
     root_dir,
     starting_dir,
     obj_dir,
-    logger,
     currdir=None,
 ):
     if currdir is None:
@@ -172,21 +167,15 @@ def guess_source_file_name(
             gcovname, currdir, root_dir, starting_dir, obj_dir, source_fname
         )
 
-    logger.verbose_msg(
-        "Finding source file corresponding to a gcov data file\n"
-        "  currdir      {currdir}\n"
-        "  gcov_fname   {data_fname}\n"
-        "  source_fname {source_fname}\n"
-        "  root         {root_dir}\n"
-        # '  common_dir   {common_dir}\n'
-        # '  subdir       {subdir}\n'
-        "  fname        {fname}",
-        currdir=currdir,
-        data_fname=data_fname,
-        source_fname=source_fname,
-        root_dir=root_dir,
-        # common_dir=common_dir, subdir=subdir,
-        fname=fname,
+    logger.debug(
+        f"Finding source file corresponding to a gcov data file\n"
+        f"  currdir      {currdir}\n"
+        f"  gcov_fname   {data_fname}\n"
+        f"  source_fname {source_fname}\n"
+        f"  root         {root_dir}\n"
+        # f"  common_dir   {common_dir}\n"
+        # f"  subdir       {subdir}\n"
+        f"  fname        {fname}"
     )
 
     return fname
@@ -296,9 +285,7 @@ def process_datafile(filename, covdata, options, toerase):
     i.e. the object files are in a sibling directory.
     TODO: So far there is no good way to address this case.
     """
-    logger = Logger(options.verbose)
-
-    logger.verbose_msg("Processing file: {}", filename)
+    logger.debug(f"Processing file: {filename}")
 
     abs_filename = os.path.abspath(filename)
 
@@ -328,7 +315,6 @@ def process_datafile(filename, covdata, options, toerase):
             abs_filename,
             covdata,
             options=options,
-            logger=logger,
             toerase=toerase,
             error=errors.append,
             chdir=wd,
@@ -341,12 +327,11 @@ def process_datafile(filename, covdata, options, toerase):
         if done:
             return
 
-    logger.warn(
-        "GCOV produced the following errors processing {filename}:\n"
-        "\t{errors}\n"
-        "\t(gcovr could not infer a working directory that resolved it.)",
-        filename=filename,
-        errors="\n\t".join(errors),
+    errors_output = "\n\t".join(errors)
+    logger.warning(
+        f"GCOV produced the following errors processing {filename}:\n"
+        f"\t{errors_output}\n"
+        "\t(gcovr could not infer a working directory that resolved it.)"
     )
 
 
@@ -382,8 +367,7 @@ class gcov:
     __default_options = []
     __help_output = None
 
-    def __init__(self, cmd, logger):
-        self.logger = logger
+    def __init__(self, cmd):
         if gcov.__cmd is None:
             gcov.__cmd = cmd
             # If the first element of cmd - the executable name - has embedded spaces
@@ -402,7 +386,7 @@ class gcov:
             elif self.__check_gcov_option("--preserve-paths"):
                 gcov.__default_options.append("--preserve-paths")
             else:
-                self.logger.warn(
+                logger.warning(
                     "Options '--hash-filenames' and '--preserve-paths' are not "
                     f"supported by '{cmd}'. Source files with identical file names "
                     "may result in incorrect coverage."
@@ -449,7 +433,7 @@ class gcov:
         if "cwd" not in kwargs:
             kwargs["cwd"] = "."
         cmd = gcov.__cmd_split + args
-        self.logger.verbose_msg(f"Running gcov: '{' '.join(cmd)}' in '{kwargs['cwd']}'")
+        logger.debug(f"Running gcov: '{' '.join(cmd)}' in '{kwargs['cwd']}'")
 
         return subprocess.Popen(
             cmd,
@@ -461,10 +445,8 @@ class gcov:
         )
 
 
-def run_gcov_and_process_files(
-    abs_filename, covdata, options, logger, error, toerase, chdir
-):
-    gcov_cmd = gcov(options.gcov_cmd, logger)
+def run_gcov_and_process_files(abs_filename, covdata, options, error, toerase, chdir):
+    gcov_cmd = gcov(options.gcov_cmd)
 
     # ATTENTION:
     # This lock is essential for parallel processing because without
@@ -485,7 +467,6 @@ def run_gcov_and_process_files(
             out,
             gcov_filter=options.gcov_filter,
             gcov_exclude=options.gcov_exclude,
-            logger=logger,
             chdir=chdir,
         )
 
@@ -508,7 +489,7 @@ def run_gcov_and_process_files(
     return done
 
 
-def select_gcov_files_from_stdout(out, gcov_filter, gcov_exclude, logger, chdir):
+def select_gcov_files_from_stdout(out, gcov_filter, gcov_exclude, chdir):
     active_files = []
     all_files = []
 
@@ -526,11 +507,11 @@ def select_gcov_files_from_stdout(out, gcov_filter, gcov_exclude, logger, chdir)
         )
 
         if filtered:
-            logger.verbose_msg("Filtering gcov file {}", fname)
+            logger.debug(f"Filtering gcov file {fname}")
             continue
 
         if excluded:
-            logger.verbose_msg("Excluding gcov file {}", fname)
+            logger.debug(f"Excluding gcov file {fname}")
             continue
 
         active_files.append(full)
@@ -542,18 +523,16 @@ def select_gcov_files_from_stdout(out, gcov_filter, gcov_exclude, logger, chdir)
 #  Process Already existing gcov files
 #
 def process_existing_gcov_file(filename, covdata, options, toerase):
-    logger = Logger(options.verbose)
-
     filtered, excluded = apply_filter_include_exclude(
         filename, options.gcov_filter, options.gcov_exclude
     )
 
     if filtered:
-        logger.verbose_msg("This gcov file does not match the filter: {}", filename)
+        logger.debug(f"This gcov file does not match the filter: {filename}")
         return
 
     if excluded:
-        logger.verbose_msg("Excluding gcov file: {}", filename)
+        logger.debug(f"Excluding gcov file: {filename}")
         return
 
     process_gcov_data(filename, covdata, None, options)
diff --git a/gcovr/gcov_parser.py b/gcovr/gcov_parser.py
index 799f6c4d..c48aa70d 100644
--- a/gcovr/gcov_parser.py
+++ b/gcovr/gcov_parser.py
@@ -34,6 +34,7 @@ The behavior of this parser was informed by the following sources:
 
 
 import enum
+import logging
 import re
 
 from typing import (
@@ -50,7 +51,8 @@ from typing import (
 
 from .coverage import FileCoverage
 from .decision_analysis import DecisionParser
-from .utils import Logger
+
+logger = logging.getLogger("gcovr")
 
 _EXCLUDE_LINE_FLAG = "_EXCL_"
 _EXCLUDE_LINE_PATTERN = re.compile(r"([GL]COVR?)_EXCL_(LINE|START|STOP)")
@@ -269,7 +271,6 @@ _LineWithError = Tuple[str, Exception]
 
 class _Context(NamedTuple):
     flags: ParserFlags
-    logger: Logger
     filename: str
 
 
@@ -277,7 +278,6 @@ def parse_coverage(
     lines: List[str],
     *,
     filename: str,
-    logger: Logger,
     exclude_lines_by_pattern: Optional[str],
     flags: ParserFlags,
 ) -> FileCoverage:
@@ -291,7 +291,6 @@ def parse_coverage(
     Arguments:
         lines: the lines of the file to be parsed (excluding newlines)
         filename: for error reports
-        logger: for error reports
         exclude_lines_by_pattern: string with regex syntax to exclude
             individual lines
         flags: various choices for the parser behavior
@@ -304,7 +303,7 @@ def parse_coverage(
         is enabled.
     """
 
-    context = _Context(flags, logger, filename)
+    context = _Context(flags, filename)
 
     lines_with_errors: List[_LineWithError] = []
 
@@ -333,7 +332,7 @@ def parse_coverage(
     if flags & ParserFlags.RESPECT_EXCLUSION_MARKERS:
         line_is_excluded = _find_excluded_ranges(
             lines=src_lines,
-            warnings=_ExclusionRangeWarnings(logger, filename),
+            warnings=_ExclusionRangeWarnings(filename),
             exclude_lines_by_pattern=exclude_lines_by_pattern,
         )
     else:
@@ -360,7 +359,7 @@ def parse_coverage(
         _add_coverage_for_function(coverage, state.lineno + 1, function, context)
 
     if flags & ParserFlags.PARSE_DECISIONS:
-        decision_parser = DecisionParser(filename, coverage, src_lines, logger)
+        decision_parser = DecisionParser(filename, coverage, src_lines)
         decision_parser.parse_all_lines()
 
     _report_lines_with_errors(lines_with_errors, context)
@@ -434,11 +433,8 @@ def _gather_coverage_from_line(
 
         exclusion_reason = _branch_can_be_excluded(line, state, context.flags)
         if exclusion_reason:
-            context.logger.verbose_msg(
-                "Excluding unreachable branch on line {} in file {}: {}",
-                state.lineno,
-                context.filename,
-                exclusion_reason,
+            logger.debug(
+                f"Excluding unreachable branch on line {state.lineno} in file {context.filename}: {exclusion_reason}"
             )
             return state
 
@@ -483,25 +479,22 @@ def _report_lines_with_errors(
     lines = [line for line, _ in lines_with_errors]
     errors = [error for _, error in lines_with_errors]
 
-    context.logger.warn(
-        "Unrecognized GCOV output for {source}\n"
-        "\t  {lines}\n"
+    lines_output = "\n\t  ".join(lines)
+    logger.warning(
+        f"Unrecognized GCOV output for {context.filename}\n"
+        f"\t  {lines_output}\n"
         "\tThis is indicative of a gcov output parse error.\n"
         "\tPlease report this to the gcovr developers\n"
-        "\tat <https://github.com/gcovr/gcovr/issues>.",
-        source=context.filename,
-        lines="\n\t  ".join(lines),
+        "\tat <https://github.com/gcovr/gcovr/issues>."
     )
 
     for ex in errors:
-        context.logger.warn(
-            "Exception during parsing:\n\t{type}: {msg}", type=type(ex).__name__, msg=ex
-        )
+        logger.warning(f"Exception during parsing:\n\t{type(ex).__name__}: {ex}")
 
     if context.flags & ParserFlags.IGNORE_PARSE_ERRORS:
         return
 
-    context.logger.error(
+    logger.error(
         "Exiting because of parse errors.\n"
         "\tYou can run gcovr with --gcov-ignore-parse-errors\n"
         "\tto continue anyway."
@@ -591,11 +584,8 @@ def _add_coverage_for_function(
     name, calls, _, _ = function
 
     if _function_can_be_excluded(name, context.flags):
-        context.logger.verbose_msg(
-            "Ignoring Symbol {func_name} in line {line} in file {file_name}",
-            func_name=name,
-            line=lineno,
-            file_name=context.filename,
+        logger.debug(
+            f"Ignoring Symbol {name} in line {lineno} in file {context.filename}"
         )
         return
 
@@ -867,71 +857,56 @@ class _ExclusionRangeWarnings:
     ...  1: 5: baz // GCOV_EXCL_STOP
     ...  1: 6: "GCOVR_EXCL_START"
     ... '''
-    >>> import sys; sys.stderr = sys.stdout  # redirect warnings
+    >>> caplog = getfixture('caplog')
+    >>> caplog.clear()
     >>> _ = parse_coverage(  # doctest: +NORMALIZE_WHITESPACE
-    ...     source.splitlines(), filename='example.cpp', logger=Logger(),
+    ...     source.splitlines(), filename='example.cpp',
     ...    flags=ParserFlags.RESPECT_EXCLUSION_MARKERS, exclude_lines_by_pattern=None)
-    (WARNING) mismatched coverage exclusion flags.
+    >>> for message in caplog.record_tuples:
+    ...     print(f"{message[1]}: {message[2]}")
+    30: mismatched coverage exclusion flags.
               LCOV_EXCL_STOP found on line 2 without corresponding LCOV_EXCL_START, when processing example.cpp.
-    (WARNING) GCOVR_EXCL_LINE found on line 4 in excluded region started on line 3, when processing example.cpp.
-    (WARNING) GCOVR_EXCL_START found on line 3 was terminated by GCOV_EXCL_STOP on line 5, when processing example.cpp.
-    (WARNING) The coverage exclusion region start flag GCOVR_EXCL_START
+    30: GCOVR_EXCL_LINE found on line 4 in excluded region started on line 3, when processing example.cpp.
+    30: GCOVR_EXCL_START found on line 3 was terminated by GCOV_EXCL_STOP on line 5, when processing example.cpp.
+    30: The coverage exclusion region start flag {start}
               on line 6 did not have corresponding GCOVR_EXCL_STOP flag
               in file example.cpp.
     """
 
-    def __init__(self, logger: Logger, filename: str) -> None:
-        self.logger = logger
+    def __init__(self, filename: str) -> None:
         self.filename = filename
 
     def mismatched_start_stop(
         self, start_lineno: int, start: str, stop_lineno: int, stop: str
     ) -> None:
         """warn that start/stop region markers don't match"""
-        self.logger.warn(
-            "{start} found on line {start_lineno} "
-            "was terminated by {stop} on line {stop_lineno}, "
-            "when processing {filename}.",
-            start=start,
-            start_lineno=start_lineno,
-            stop=stop,
-            stop_lineno=stop_lineno,
-            filename=self.filename,
+        logger.warning(
+            f"{start} found on line {start_lineno} "
+            f"was terminated by {stop} on line {stop_lineno}, "
+            f"when processing {self.filename}."
         )
 
     def stop_without_start(self, lineno: int, expected_start: str, stop: str) -> None:
         """warn that a region was ended without corresponding start marker"""
-        self.logger.warn(
+        logger.warning(
             "mismatched coverage exclusion flags.\n"
-            "          {stop} found on line {lineno} without corresponding {start}, "
-            "when processing {filename}.",
-            start=expected_start,
-            stop=stop,
-            lineno=lineno,
-            filename=self.filename,
+            f"          {stop} found on line {lineno} without corresponding {expected_start}, "
+            f"when processing {self.filename}."
         )
 
     def start_without_stop(self, lineno: int, start: str, expected_stop: str) -> None:
         """warn that a region was started but not closed"""
-        self.logger.warn(
+        logger.warning(
             "The coverage exclusion region start flag {start}\n"
-            "          on line {lineno} did not have corresponding {stop} flag\n"
-            "          in file {filename}.",
-            start=start,
-            stop=expected_stop,
-            lineno=lineno,
-            filename=self.filename,
+            f"          on line {lineno} did not have corresponding {expected_stop} flag\n"
+            f"          in file {self.filename}."
         )
 
     def line_after_start(self, lineno: int, start: str, start_lineno: str) -> None:
         """warn that a region was started but an excluded line was found"""
-        self.logger.warn(
-            "{start} found on line {lineno} in excluded region started on line {start_lineno}, "
-            "when processing {filename}.",
-            start=start,
-            lineno=lineno,
-            start_lineno=start_lineno,
-            filename=self.filename,
+        logger.warning(
+            f"{start} found on line {lineno} in excluded region started on line {start_lineno}, "
+            f"when processing {self.filename}."
         )
 
 
diff --git a/gcovr/utils.py b/gcovr/utils.py
index 64a95784..a6e9d1da 100644
--- a/gcovr/utils.py
+++ b/gcovr/utils.py
@@ -17,11 +17,14 @@
 # ****************************************************************************
 
 from argparse import ArgumentTypeError
+import logging
 import os
 import re
 import sys
 from contextlib import contextmanager
 
+logger = logging.getLogger("gcovr")
+
 
 class LoopChecker(object):
     def __init__(self):
@@ -189,7 +192,7 @@ class FilterOption(object):
         self.regex = regex
         self.path_context = os.getcwd() if path_context is None else path_context
 
-    def build_filter(self, logger):
+    def build_filter(self):
         # Try to detect unintended backslashes and warn.
         # Later, the regex engine may or may not raise a syntax error.
         # An unintended backslash is a literal backslash r"\\",
@@ -197,9 +200,9 @@ class FilterOption(object):
         (suggestion, bs_count) = re.subn(
             r'\\\\|\\(?=[^\WabfnrtuUvx0-9AbBdDsSwWZ])', '/', self.regex)
         if bs_count:
-            logger.warn("filters must use forward slashes as path separators")
-            logger.warn("your filter : {}", self.regex)
-            logger.warn("did you mean: {}", suggestion)
+            logger.warning("filters must use forward slashes as path separators")
+            logger.warning(f"your filter : {self.regex}")
+            logger.warning(f"did you mean: {suggestion}")
 
         isabs = self.regex.startswith("/")
         if not isabs and (sys.platform == "win32"):
@@ -290,44 +293,17 @@ class DirectoryPrefixFilter(Filter):
         return super(DirectoryPrefixFilter, self).match(normpath)
 
 
-class Logger(object):
-    def __init__(self, verbose=False):
-        self.verbose = verbose
-
-    def warn(self, pattern, *args, **kwargs):
-        """Write a formatted warning to STDERR.
-
-        pattern: a str.format pattern
-        args, kwargs: str.format arguments
-        """
-        pattern = "(WARNING) " + pattern + "\n"
-        sys.stderr.write(pattern.format(*args, **kwargs))
-
-    def error(self, pattern, *args, **kwargs):
-        """Write a formatted error to STDERR.
-
-        pattern: a str.format pattern
-        args, kwargs: str.format parameters
-        """
-        pattern = "(ERROR) " + pattern + "\n"
-        sys.stderr.write(pattern.format(*args, **kwargs))
-
-    def msg(self, pattern, *args, **kwargs):
-        """Write a formatted message to STDOUT.
-
-        pattern: a str.format pattern
-        args, kwargs: str.format arguments
-        """
-        pattern = pattern + "\n"
-        sys.stdout.write(pattern.format(*args, **kwargs))
+def configure_logging() -> None:
+    logging.basicConfig(
+        format="(%(levelname)s) %(message)s",
+        stream=sys.stderr,
+        level=logging.INFO,
+    )
 
-    def verbose_msg(self, pattern, *args, **kwargs):
-        """Write a formatted message to STDOUT if in verbose mode.
+    def exception_hook(exc_type, exc_value, exc_traceback) -> None:
+        logging.exception("Uncaught EXCEPTION", exc_info=(exc_type, exc_value, exc_traceback))
 
-        see: self.msg()
-        """
-        if self.verbose:
-            self.msg(pattern, *args, **kwargs)
+    sys.excepthook = exception_hook
 
 
 def sort_coverage(covdata, show_branch,
diff --git a/gcovr/writer/html.py b/gcovr/writer/html.py
index 52da27f2..a749bde4 100644
--- a/gcovr/writer/html.py
+++ b/gcovr/writer/html.py
@@ -15,14 +15,16 @@
 # For more information, see the README.rst file.
 #
 # ****************************************************************************
-
+import logging
 import os
 import hashlib
 import io
 from argparse import ArgumentTypeError
 
 from ..version import __version__
-from ..utils import realpath, commonpath, sort_coverage, calculate_coverage, Logger, open_text_for_writing
+from ..utils import realpath, commonpath, sort_coverage, calculate_coverage, open_text_for_writing
+
+logger = logging.getLogger("gcovr")
 
 
 class Lazy:
@@ -144,14 +146,13 @@ class NullHighlighting:
 
 
 class PygmentHighlighting:
-    def __init__(self, options):
-        self.logger = Logger(options.verbose)
+    def __init__(self):
         self.formatter = None
         try:
             from pygments.formatters.html import HtmlFormatter
             self.formatter = HtmlFormatter(nowrap=True)
         except ImportError as e:  # pragma: no cover
-            self.logger.warn("No syntax highlighting available: {}".format(str(e)))
+            logger.warning(f"No syntax highlighting available: {str(e)}")
 
     def get_css(self):
         if self.formatter is None:  # pragma: no cover
@@ -175,7 +176,7 @@ class PygmentHighlighting:
 
 @Lazy
 def get_formatter(options):
-    return PygmentHighlighting(options) if options.html_details_syntax_highlighting else NullHighlighting()
+    return PygmentHighlighting() if options.html_details_syntax_highlighting else NullHighlighting()
 
 
 def coverage_to_class(coverage, medium_threshold, high_threshold):
@@ -339,7 +340,6 @@ class RootInfo:
 # Produce an HTML report
 #
 def print_html_report(covdata, output_file, options):
-    logger = Logger(options.verbose)
     css_data = CssRenderer.render(options)
     medium_threshold = options.html_medium_threshold
     high_threshold = options.html_high_threshold
@@ -495,14 +495,11 @@ def print_html_report(covdata, output_file, options):
                         source_row(ctr, line, cdata.lines.get(ctr))
                     )
                 if ctr < max_line_from_cdata:
-                    logger.warn(
-                        'File {filename} has {file_lines} line(s) but coverage data has {cdata_lines} line(s).',
-                        filename=data['filename'],
-                        file_lines=ctr,
-                        cdata_lines=max_line_from_cdata
+                    logger.warning(
+                        f"File {data['filename']} has {ctr} line(s) but coverage data has {max_line_from_cdata} line(s)."
                     )
         except IOError as e:
-            logger.warn('File {filename} not found: {reason}', filename=data['filename'], reason=repr(e))
+            logger.warning(f'File {data["filename"]} not found: {repr(e)}')
             for ctr in range(1, max_line_from_cdata):
                 data['source_lines'].append(
                     source_row(ctr, '!!! File not found !!!' if ctr == 1 else '', cdata.lines.get(ctr))
diff --git a/gcovr/writer/json.py b/gcovr/writer/json.py
index c35b95e1..62117282 100644
--- a/gcovr/writer/json.py
+++ b/gcovr/writer/json.py
@@ -17,13 +17,13 @@
 # ****************************************************************************
 
 import json
+import logging
 import os
 import functools
 from ..gcov import apply_filter_include_exclude
 
 from ..utils import (
     get_global_stats,
-    Logger,
     presentable_filename,
     sort_coverage,
     summarize_file_coverage,
@@ -37,6 +37,8 @@ from ..coverage import (
     FileCoverage,
 )
 
+logger = logging.getLogger("gcovr")
+
 
 JSON_FORMAT_VERSION = "0.3"
 JSON_SUMMARY_FORMAT_VERSION = "0.4"
@@ -174,11 +176,8 @@ def gcovr_json_files_to_coverage(filenames, covdata, options):
     r"""merge a coverage from multiple reports in the format
     partially compatible with gcov JSON output"""
 
-    logger = Logger(options.verbose)
-
     for filename in filenames:
-        gcovr_json_data = {}
-        logger.verbose_msg("Processing JSON file: {}", filename)
+        logger.debug(f"Processing JSON file: {filename}")
 
         with open(filename, "r") as json_file:
             gcovr_json_data = json.load(json_file)
@@ -202,12 +201,12 @@ def gcovr_json_files_to_coverage(filenames, covdata, options):
 
             # Ignore if the filename does not match the filter
             if filtered:
-                logger.verbose_msg("  Filtering coverage data for file {}", file_path)
+                logger.debug(f"  Filtering coverage data for file {file_path}")
                 continue
 
             # Ignore if the filename matches the exclude pattern
             if excluded:
-                logger.verbose_msg("  Excluding coverage data for file {}", file_path)
+                logger.debug(f"  Excluding coverage data for file {file_path}")
                 continue
 
             file_coverage = FileCoverage(file_path)
diff --git a/gcovr/writer/summary.py b/gcovr/writer/summary.py
index 06192df4..29a049c8 100644
--- a/gcovr/writer/summary.py
+++ b/gcovr/writer/summary.py
@@ -15,11 +15,13 @@
 # For more information, see the README.rst file.
 #
 # ****************************************************************************
-
+import logging
 import sys
 
 from ..utils import get_global_stats
 
+logger = logging.getLogger("gcovr")
+
 
 def print_summary(covdata):
     '''Print a small report to the standard output.
@@ -30,16 +32,16 @@ def print_summary(covdata):
      functions_total, functions_covered, percent_functions,
      branches_total, branches_covered, percent_branches) = get_global_stats(covdata)
 
-    lines_out = "lines: %0.1f%% (%s out of %s)\n" % (
+    lines_out = "lines: %0.1f%% (%s out of %s)" % (
         percent, lines_covered, lines_total
     )
-    functions_out = "functions: %0.1f%% (%s out of %s)\n" % (
+    functions_out = "functions: %0.1f%% (%s out of %s)" % (
         percent_functions, functions_covered, functions_total
     )
-    branches_out = "branches: %0.1f%% (%s out of %s)\n" % (
+    branches_out = "branches: %0.1f%% (%s out of %s)" % (
         percent_branches, branches_covered, branches_total
     )
 
-    sys.stdout.write(lines_out)
-    sys.stdout.write(functions_out)
-    sys.stdout.write(branches_out)
+    sys.stdout.write(lines_out + '\n')
+    sys.stdout.write(functions_out + '\n')
+    sys.stdout.write(branches_out + '\n')
diff --git a/noxfile.py b/noxfile.py
index af3b1fc8..8c8b4d9b 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -17,6 +17,7 @@ BLACK_CONFORM_FILES = [
     "gcovr/gcov.py",
     "gcovr/gcov_parser.py",
     "gcovr/timestamps.py",
+    "gcovr/tests/test_gcov_parser.py",
     "gcovr/writer/json.py",
 ]
 


Test patch:
diff --git a/gcovr/tests/test_args.py b/gcovr/tests/test_args.py
index 8d0b7b1b..970d3942 100644
--- a/gcovr/tests/test_args.py
+++ b/gcovr/tests/test_args.py
@@ -19,6 +19,7 @@
 from ..__main__ import main
 from ..version import __version__
 
+import logging
 import pytest
 import os
 import re
@@ -49,10 +50,32 @@ def capture(capsys, args, other_ex=()):
     return CaptureObject(out, err, e)
 
 
+# The LogCaptureObject class holds the capture method result
+class LogCaptureObject:
+    def __init__(self, record_tuples, exception):
+        self.record_tuples = record_tuples
+        self.exception = exception
+
+
+# The capture method calls the main method and captures its output/error
+# streams and exit code
+def log_capture(caplog, args, other_ex=()):
+    e = None
+    try:
+        main(args)
+        # Explicit SystemExit exception in case main() returns normally
+        sys.exit(0)
+    except SystemExit as exception:
+        e = exception
+    except other_ex as exception:
+        e = exception
+    return LogCaptureObject(caplog.record_tuples, e)
+
+
 def test_version(capsys):
-    c = capture(capsys, ['--version'])
-    assert c.err == ''
-    assert c.out.startswith('gcovr %s' % __version__)
+    c = capture(capsys, ["--version"])
+    assert c.err == ""
+    assert c.out.startswith(f"gcovr {__version__}")
     assert c.exception.code == 0
 
 
@@ -63,10 +86,11 @@ def test_help(capsys):
     assert c.exception.code == 0
 
 
-def test_empty_root(capsys):
-    c = capture(capsys, ['-r', ''])
-    assert c.out == ''
-    assert c.err.startswith('(ERROR) empty --root option.')
+def test_empty_root(caplog):
+    c = log_capture(caplog, ["-r", ""])
+    message = c.record_tuples[0]
+    assert message[1] == logging.ERROR
+    assert message[2].startswith("empty --root option.")
     assert c.exception.code == 1
 
 
@@ -84,19 +108,19 @@ def test_empty_exclude_directories(capsys):
     assert c.exception.code != 0
 
 
-def test_empty_objdir(capsys):
-    c = capture(capsys, ['--object-directory', ''])
-    assert c.out == ''
-    assert c.err.startswith(
-        '(ERROR) empty --object-directory option.')
+def test_empty_objdir(caplog):
+    c = log_capture(caplog, ["--object-directory", ""])
+    message = c.record_tuples[0]
+    assert message[1] == logging.ERROR
+    assert message[2].startswith("empty --object-directory option.")
     assert c.exception.code == 1
 
 
-def test_invalid_objdir(capsys):
-    c = capture(capsys, ['--object-directory', 'not-existing-dir'])
-    assert c.out == ''
-    assert c.err.startswith(
-        '(ERROR) Bad --object-directory option.')
+def test_invalid_objdir(caplog):
+    c = log_capture(caplog, ["--object-directory", "not-existing-dir"])
+    message = c.record_tuples[0]
+    assert message[1] == logging.ERROR
+    assert message[2].startswith("Bad --object-directory option.")
     assert c.exception.code == 1
 
 
@@ -225,10 +249,11 @@ def test_non_writable_directory_csv(capsys):  # pragma: no cover
     helper_test_non_writable_directory_output(capsys, '--csv')
 
 
-def test_no_output_html_details(capsys):
-    c = capture(capsys, ['--html-details'])
-    assert c.out == ''
-    assert 'a named output must be given, if the option --html-details\nis used.' in c.err
+def test_no_output_html_details(caplog):
+    c = log_capture(caplog, ["--html-details"])
+    message = c.record_tuples[0]
+    assert message[1] == logging.ERROR
+    assert message[2] == "a named output must be given, if the option --html-details\nis used."
     assert c.exception.code != 0
 
 
@@ -253,16 +278,21 @@ def test_line_threshold_100_1(capsys):
     assert c.exception.code != 0
 
 
-def test_filter_backslashes_are_detected(capsys):
+def test_filter_backslashes_are_detected(caplog):
     # gcov-exclude all to prevent any coverage data from being found
-    c = capture(
-        capsys,
+    c = log_capture(
+        caplog,
         args=['--filter', r'C:\\foo\moo', '--gcov-exclude', ''],
         other_ex=re.error)
-    assert c.err.startswith(
-        '(WARNING) filters must use forward slashes as path separators\n'
-        '(WARNING) your filter : C:\\\\foo\\moo\n'
-        '(WARNING) did you mean: C:/foo/moo\n')
+    message0 = c.record_tuples[0]
+    assert message0[1] == logging.WARNING
+    assert message0[2].startswith('filters must use forward slashes as path separators')
+    message = c.record_tuples[1]
+    assert message[1] == logging.WARNING
+    assert message[2].startswith('your filter : C:\\\\foo\\moo')
+    message = c.record_tuples[2]
+    assert message[1] == logging.WARNING
+    assert message[2].startswith('did you mean: C:/foo/moo')
     assert isinstance(c.exception, re.error) or c.exception.code == 0
 
 
@@ -273,17 +303,18 @@ def test_html_css_not_exists(capsys):
     assert c.exception.code != 0
 
 
-def test_html_title_empty_string(capsys):
-    c = capture(capsys, ['--html-title', ''])
-    assert c.out == ''
-    assert 'an empty --html_title= is not allowed.' in c.err
+def test_html_title_empty_string(caplog):
+    c = log_capture(caplog, ["--html-title", ""])
+    message = c.record_tuples[0]
+    assert message[1] == logging.ERROR
+    assert message[2] == "an empty --html_title= is not allowed."
     assert c.exception.code != 0
 
 
 def test_html_medium_threshold_nan(capsys):
-    c = capture(capsys, ['--html-medium-threshold', 'nan'])
+    c = capture(capsys, ["--html-medium-threshold", "nan"])
     assert c.out == ''
-    assert 'not in range [0.0, 100.0]' in c.err
+    assert "--html-medium-threshold: nan not in range [0.0, 100.0]" in c.err
     assert c.exception.code != 0
 
 
@@ -294,10 +325,11 @@ def test_html_medium_threshold_negative(capsys):
     assert c.exception.code != 0
 
 
-def test_html_medium_threshold_zero(capsys):
-    c = capture(capsys, ['--html-medium-threshold', '0.0'])
-    assert c.out == ''
-    assert 'value of --html-medium-threshold= should not be zero.' in c.err
+def test_html_medium_threshold_zero(caplog):
+    c = log_capture(caplog, ["--html-medium-threshold", "0.0"])
+    message = c.record_tuples[0]
+    assert message[1] == logging.ERROR
+    assert message[2] == "value of --html-medium-threshold= should not be zero."
     assert c.exception.code != 0
 
 
@@ -315,40 +347,55 @@ def test_html_high_threshold_negative(capsys):
     assert c.exception.code != 0
 
 
-def test_html_medium_threshold_gt_html_high_threshold(capsys):
-    c = capture(capsys, ['--html-medium-threshold', '60', '--html-high-threshold', '50'])
-    assert c.out == ''
-    assert 'value of --html-medium-threshold=60.0 should be\nlower than or equal to the value of --html-high-threshold=50.0.' in c.err
+def test_html_medium_threshold_gt_html_high_threshold(caplog):
+    c = log_capture(caplog, ["--html-medium-threshold", "60", "--html-high-threshold", "50"])
+    message = c.record_tuples[0]
+    assert message[1] == logging.ERROR
+    assert message[2] == "value of --html-medium-threshold=60.0 should be\nlower than or equal to the value of --html-high-threshold=50.0."
     assert c.exception.code != 0
 
 
-def test_html_tab_size_zero(capsys):
-    c = capture(capsys, ['--html-tab-size', '0'])
-    assert c.out == ''
-    assert 'value of --html-tab-size= should be greater 0.' in c.err
+def test_html_tab_size_zero(caplog):
+    c = log_capture(caplog, ["--html-tab-size", "0"])
+    message = c.record_tuples[0]
+    assert message[1] == logging.ERROR
+    assert message[2] == "value of --html-tab-size= should be greater 0."
     assert c.exception.code != 0
 
 
-def test_multiple_output_formats_to_stdout(capsys):
-    c = capture(capsys, ['--xml', '--html', '--sonarqube', '--coveralls'])
-    assert 'HTML output skipped' in c.err
-    assert 'Sonarqube output skipped' in c.err
-    assert 'Coveralls output skipped' in c.err
+def test_multiple_output_formats_to_stdout(caplog):
+    c = log_capture(caplog, ["--xml", "--html", "--sonarqube", "--coveralls"])
+    message = c.record_tuples[0]
+    assert message[1] == logging.WARNING
+    assert message[2] == "HTML output skipped - consider providing an output file with `--html=OUTPUT`."
+    message = c.record_tuples[1]
+    assert message[1] == logging.WARNING
+    assert message[2] == "Sonarqube output skipped - consider providing an output file with `--sonarqube=OUTPUT`."
+    message = c.record_tuples[2]
+    assert message[1] == logging.WARNING
+    assert message[2] == "Coveralls output skipped - consider providing an output file with `--coveralls=OUTPUT`."
     assert c.exception.code == 0
 
 
-def test_multiple_output_formats_to_stdout_1(capsys):
-    c = capture(capsys, ['--xml', '--html', '--sonarqube', '--coveralls', '-o', '-'])
-    assert 'HTML output skipped' in c.err
-    assert 'Sonarqube output skipped' in c.err
-    assert 'Coveralls output skipped' in c.err
+def test_multiple_output_formats_to_stdout_1(caplog):
+    c = log_capture(caplog, ["--xml", "--html", "--sonarqube", "--coveralls", "-o", "-"])
+    message = c.record_tuples[0]
+    assert message[1] == logging.WARNING
+    assert message[2] == "HTML output skipped - consider providing an output file with `--html=OUTPUT`."
+    message = c.record_tuples[1]
+    assert message[1] == logging.WARNING
+    assert message[2] == "Sonarqube output skipped - consider providing an output file with `--sonarqube=OUTPUT`."
+    message = c.record_tuples[2]
+    assert message[1] == logging.WARNING
+    assert message[2] == "Coveralls output skipped - consider providing an output file with `--coveralls=OUTPUT`."
     assert c.exception.code == 0
 
 
-def test_no_self_contained_without_file(capsys):
-    c = capture(capsys, ['--no-html-self-contained', '--html'])
-    assert c.out == ''
-    assert 'can only disable --html-self-contained when a named output is given' in c.err
+def test_no_self_contained_without_file(caplog):
+    c = log_capture(caplog, ["--no-html-self-contained", "--html"])
+    message = c.record_tuples[0]
+    assert message[1] == logging.ERROR
+    assert message[2] == "can only disable --html-self-contained when a named output is given."
     assert c.exception.code != 0
 
 
@@ -377,9 +424,11 @@ def test_html_injection_via_json(capsys, tmp_path):
     assert c.exception.code == 0
 
 
-def test_exclude_lines_by_pattern(capsys):
-    c = capture(capsys, ['--exclude-lines-by-pattern', 'example.**'])
-    assert 'Invalid regular expression' in c.err
+def test_exclude_lines_by_pattern(caplog):
+    c = log_capture(caplog, ["--exclude-lines-by-pattern", "example.**"])
+    message = c.record_tuples[0]
+    assert message[1] == logging.ERROR
+    assert message[2].startswith("--exclude-lines-by-pattern: Invalid regular expression")
     assert c.exception.code != 0
 
 
diff --git a/gcovr/tests/test_gcov_parser.py b/gcovr/tests/test_gcov_parser.py
index e04d723f..1fdea914 100644
--- a/gcovr/tests/test_gcov_parser.py
+++ b/gcovr/tests/test_gcov_parser.py
@@ -17,17 +17,22 @@
 # ****************************************************************************
 
 # pylint: disable=missing-function-docstring,missing-module-docstring
-
+import logging
 import re
 import time
 import textwrap
 from threading import Event
+from unittest import mock
+
 import pytest
 
 from ..gcov_parser import ParserFlags, parse_coverage, UnknownLineType
-from ..utils import Logger
+from ..utils import configure_logging
 from ..workers import Workers
 
+configure_logging()
+
+
 # This example is taken from the GCC 8 Gcov documentation:
 # <https://gcc.gnu.org/onlinedocs/gcc/Invoking-Gcov.html>
 GCOV_8_EXAMPLE = r"""
@@ -309,7 +314,6 @@ def test_gcov_8(capsys, sourcename):
     coverage = parse_coverage(
         filename="tmp.cpp",
         lines=lines,
-        logger=Logger(),
         exclude_lines_by_pattern=None,
         flags=flags,
     )
@@ -329,7 +333,7 @@ def contains_phrases(string, *phrases):
 
 
 @pytest.mark.parametrize("ignore_errors", [True, False])
-def test_unknown_tags(capsys, ignore_errors):
+def test_unknown_tags(caplog, ignore_errors):
     source = r"bananas 7 times 3"
     lines = source.splitlines()
 
@@ -341,7 +345,6 @@ def test_unknown_tags(capsys, ignore_errors):
         return parse_coverage(
             filename="foo.c",
             lines=lines,
-            logger=Logger(),
             exclude_lines_by_pattern=None,
             flags=flags,
         )
@@ -357,19 +360,22 @@ def test_unknown_tags(capsys, ignore_errors):
         with pytest.raises(Exception):
             coverage = run_the_parser()
 
-    out, err = capsys.readouterr()
-    assert out == ""
+    messages = caplog.record_tuples
+    message0 = messages[0]
+    assert message0[1] == logging.WARNING
     err_phrases = [
-        "(WARNING) Unrecognized GCOV output",
+        "Unrecognized GCOV output",
         "bananas",
         "github.com/gcovr/gcovr",
     ]
+    assert contains_phrases(message0[2], *err_phrases)
     if not ignore_errors:
-        err_phrases.append("(ERROR) Exiting")
-    assert contains_phrases(err, *err_phrases)
+        message = messages[2]
+        assert message[1] == logging.ERROR
+        assert "Exiting" in message[2]
 
 
-def test_pathologic_codeline(capsys):
+def test_pathologic_codeline(caplog):
     source = r": 7:haha"
     lines = source.splitlines()
 
@@ -377,25 +383,37 @@ def test_pathologic_codeline(capsys):
         parse_coverage(
             filename="foo.c",
             lines=lines,
-            logger=Logger(),
             exclude_lines_by_pattern=None,
             flags=ParserFlags.NONE,
         )
 
-    out, err = capsys.readouterr()
-    assert out == ""
-    assert contains_phrases(
-        err,
-        "(WARNING) Unrecognized GCOV output",
+    messages = caplog.record_tuples
+    message0 = messages[0]
+    assert message0[1] == logging.WARNING
+    warning_phrases1 = [
+        "Unrecognized GCOV output",
         ": 7:haha",
+    ]
+    assert contains_phrases(message0[2], *warning_phrases1)
+
+    message = messages[1]
+    assert message[1] == logging.WARNING
+    warning_phrases2 = [
         "Exception during parsing",
         "UnknownLineType",
-        "(ERROR) Exiting",
+    ]
+    assert contains_phrases(message[2], *warning_phrases2)
+
+    message = messages[2]
+    assert message[1] == logging.ERROR
+    error_phrases = [
+        "Exiting",
         "run gcovr with --gcov-ignore-parse-errors",
-    )
+    ]
+    assert contains_phrases(message[2], *error_phrases)
 
 
-def test_exception_during_coverage_processing(capsys):
+def test_exception_during_coverage_processing(caplog):
     """
     This cannot happen during normal processing, but as a defense against
     unexpected changes to the format the ``--gcov-ignore-parse-errors`` option
@@ -413,34 +431,43 @@ def test_exception_during_coverage_processing(capsys):
     )
     lines = source.splitlines()
 
-    class BrokenLogger(Logger):  # pylint: disable=missing-class-docstring
-        def verbose_msg(self, pattern, *args, **kwargs):
-            raise AssertionError("totally broken")
-
-    with pytest.raises(AssertionError) as ex_info:
-        parse_coverage(
-            lines,
-            logger=BrokenLogger(verbose=True),
-            filename="test.cpp",
-            exclude_lines_by_pattern=None,
-            flags=ParserFlags.EXCLUDE_INTERNAL_FUNCTIONS,
-        )
+    with mock.patch("logging.Logger.debug") as logger_mock:
+        logger_mock.side_effect = AssertionError("totally broken")
+        with pytest.raises(AssertionError) as ex_info:
+            parse_coverage(
+                lines,
+                filename="test.cpp",
+                exclude_lines_by_pattern=None,
+                flags=ParserFlags.EXCLUDE_INTERNAL_FUNCTIONS,
+            )
 
     # check that this is our exception
     assert ex_info.value.args[0] == "totally broken"
 
-    out, err = capsys.readouterr()
-    assert out == ""
-    assert err != ""
-    assert contains_phrases(
-        err,
-        "(WARNING) Unrecognized GCOV output",
+    messages = caplog.record_tuples
+    message0 = messages[0]
+    assert message0[1] == logging.WARNING
+    warning_phrases1 = [
+        "Unrecognized GCOV output",
         lines[0],
+    ]
+    assert contains_phrases(message0[2], *warning_phrases1)
+
+    message = messages[1]
+    assert message[1] == logging.WARNING
+    warning_phrases2 = [
         "Exception during parsing",
         "AssertionError",
-        "(ERROR) Exiting",
+    ]
+    assert contains_phrases(message[2], *warning_phrases2)
+
+    message = messages[2]
+    assert message[1] == logging.ERROR
+    error_phrases = [
+        "Exiting",
         "run gcovr with --gcov-ignore-parse-errors",
-    )
+    ]
+    assert contains_phrases(message[2], *error_phrases)
 
 
 def test_trailing_function_tag():
@@ -459,7 +486,6 @@ def test_trailing_function_tag():
     coverage = parse_coverage(
         source.splitlines(),
         filename="test.cpp",
-        logger=Logger(),
         flags=ParserFlags.NONE,
         exclude_lines_by_pattern=None,
     )
@@ -506,7 +532,6 @@ def test_branch_exclusion(flags):
 
     coverage = parse_coverage(
         source.splitlines(),
-        logger=Logger(),
         filename="example.cpp",
         exclude_lines_by_pattern=None,
         flags=flags,
@@ -541,7 +566,6 @@ def test_function_exclusion(flags):
 
     coverage = parse_coverage(
         source.splitlines(),
-        logger=Logger(),
         filename="example.cpp",
         exclude_lines_by_pattern=None,
         flags=flags,
@@ -574,7 +598,6 @@ def test_noncode_lines():
         coverage = parse_coverage(
             lines,
             flags=flags,
-            logger=Logger(),
             filename="example.cpp",
             exclude_lines_by_pattern=None,
         )