Repository: tektronix/tm_devices (language: python)

Issue:
[FEAT]: Avoid writing to stdout in DeviceManager when verbose is False
### Description

Methods of the class `DeviceManager` in the file `device_manager.py` use the function `print_with_timestamp` to print to stdout even though the bool `verbose` in `DeviceManager.__init__` is set to `False`.

Here is an example output to stdout:
```
python test.py
2024-10-04 09:52:04.528046+02 - Opening DeviceManager
2024-10-04 09:52:04.528153+02 - Creating Connection to SMU 1
[...]
2024-10-04 10:08:33.820904+02 - Closing Connections to Devices
2024-10-04 10:08:33.821155+02 - Closing Connection to SMU 1
2024-10-04 10:08:33.822089+02 - DeviceManager Closed
```

Output to stdout is annoying, if your scripts are intended to be directly piped into a text file (as common on Linux) or if you need control over your terminal (e.g., you want to show progress bars).

From my point of view, the best option would be to use the `logging` module of Python and let the user of the library configure how and if she/he wants debugging output. Another option would be to at least don't call `print_with_timestamp` if `verbose` was set to False.

### Additional Information

_No response_
[FEAT]: Create a logfile of all commands sent during script execution
### Description

It would be really nice if there was a file saved with the contents of the stdout that could be used to view all commands sent during the script execution.

### Additional Information

This should be implemented using the logging module of Python and should probably replace the current prints used to provide stdout.


Discussion / hints:
Related to #316
The plan is to switch over to the logging module and let users specify what level of logs they want for both stdout and the saved log file.
When working on this, be sure to search the code for links to this issue. There are a few places where changes will be needed that are marked with a link to this issue.

Fix patch:
diff --git a/.gitignore b/.gitignore
index d640ca09..9f7fb95e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ tm_devices.yaml
 tm_devices.yml
 *.log
 *.dat
+**/logs/**
 
 # Poetry lock file
 poetry.lock
@@ -58,6 +59,7 @@ coverage.xml
 .pytest_cache/
 .results*/
 tests/samples/generated_stubs/*
+tests/generated_**/*
 tests/verify_devices.yaml
 prof/
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1c60602..80dc5443 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,12 @@ Things to be included in the next release go here.
 - Added the `get_errors()` method to the `Device` class to enable easy access to the current error code and messages on any device.
 - Added more details to the Architectural Overview page of the documentation as well as highlighting to the device driver diagram on the page.
 - Added regex matching to the `verify_values()` helper function to allow for more flexible value verification.
+- A main logfile is now created by default (can be disabled if desired) that contains all the logging output of the entire tm_devices package during execution.
+    - Use the `configure_logging()` function to set the logging levels for stdout and file logging.
+    - The default settings will log all messages to the log file and maintain the current printout functionality on stdout.
+- A logfile is now created that contains each command sent to a VISA device.
+    - This file is located next to the main log file and will start with the same name, but have the unique address of the device appended.
+    - This file will only be created if file logging is enabled for the package (which is the default behavior).
 
 ### Changed
 
@@ -46,10 +52,14 @@ However, please read through all changes to be aware of what may potentially imp
 - _**<span style="color:red">BREAKING CHANGE</span>**_: Changed the behavior of the `expect_esr()` method to expect an integer error code input and an optional tuple of error messages to compare against the actual error code and messages returned by the `_get_errors()` private method.
 - _**<span style="color:orange">minor breaking change</span>**_: Converted the `device_type` property into an abstract, cached property to force all children of the `Device` class to specify what type of device they are.
 - Updated the auto-generated command mixin classes to no longer use an `__init__()` method to enable the driver API documentation to render in a more usable way.
+- Switched from using standard `print()` calls to using the `logging` module for all logging in the `tm_devices` package.
+    - A configuration function provides the ability to set different logging levels for stdout and file logging.
+    - The config file and environment variable can also be used to control the logging functionality.
+    - The debug logging from the `pyvisa` package is also included in the log file by default.
 
 ### Removed
 
-- _**<span style="color:red">BREAKING CHANGE</span>**_: Removed previously deprecated `TekScopeSW` alias to the `TekScopePC` class
+- _**<span style="color:red">BREAKING CHANGE</span>**_: Removed previously deprecated `TekScopeSW` alias to the `TekScopePC` class.
 - _**<span style="color:red">BREAKING CHANGE</span>**_: Removed previously deprecated `write_buffers()` from the `TSPControl` class.
 - _**<span style="color:red">BREAKING CHANGE</span>**_: Removed Internal AFG methods from the `TekScopePC` driver, since they wouldn't have worked due to its lack of an IAFG.
 - _**<span style="color:red">BREAKING CHANGE</span>**_: Removed previously deprecated `DEVICE_DRIVER_MODEL_MAPPING` constant.
@@ -57,6 +67,8 @@ However, please read through all changes to be aware of what may potentially imp
 - _**<span style="color:red">BREAKING CHANGE</span>**_: Removed many hacky implementations of `total_channels` and `all_channel_names_list` properties from drivers that don't need them anymore.
 - _**<span style="color:red">BREAKING CHANGE</span>**_: Removed the `verify_values()`, `raise_failure()`, and `raise_error()` methods from all device drivers.
     - These methods have been converted to helper functions and can be imported from the `tm_devices.helpers` subpackage now.
+- _**<span style="color:red">BREAKING CHANGE</span>**_: Removed the `print_with_timestamp()` function since this functionality is now handled by the `logging` module.
+- _**<span style="color:red">BREAKING CHANGE</span>**_: Removed the `get_timestamp_string()` function since this functionality is now handled by the `logging` module.
 
 ---
 
diff --git a/README.md b/README.md
index 0fb7d296..7b1e995c 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
 <div markdown="1" class="custom-badge-table">
 
-|                   |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
-| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **Testing**       | [![Code testing status](https://github.com/tektronix/tm_devices/actions/workflows/test-code.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/test-code.yml) [![Docs testing status](https://github.com/tektronix/tm_devices/actions/workflows/test-docs.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/test-docs.yml) [![Coverage status](https://codecov.io/gh/tektronix/tm_devices/branch/main/graph/badge.svg)](https://codecov.io/gh/tektronix/tm_devices)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
-| **Code Quality**  | [![CodeQL status](https://github.com/tektronix/tm_devices/actions/workflows/codeql-analysis.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/codeql-analysis.yml) [![CodeFactor grade](https://www.codefactor.io/repository/github/tektronix/tm_devices/badge)](https://www.codefactor.io/repository/github/tektronix/tm_devices) [![pre-commit status](https://results.pre-commit.ci/badge/github/tektronix/tm_devices/main.svg)](https://results.pre-commit.ci/latest/github/tektronix/tm_devices/main)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
-| **Package**       | [![PyPI: Package status](https://img.shields.io/pypi/status/tm_devices?logo=pypi)](https://pypi.org/project/tm_devices/) [![PyPI: Latest release version](https://img.shields.io/pypi/v/tm_devices?logo=pypi)](https://pypi.org/project/tm_devices/) [![PyPI: Supported Python versions](https://img.shields.io/pypi/pyversions/tm_devices?logo=python)](https://pypi.org/project/tm_devices/) [![PyPI: Downloads](https://pepy.tech/badge/tm-devices)](https://pepy.tech/project/tm_devices) [![License: Apache 2.0](https://img.shields.io/pypi/l/tm_devices)](https://github.com/tektronix/tm_devices/blob/main/LICENSE.md) [![Package build status](https://github.com/tektronix/tm_devices/actions/workflows/package-build.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/package-build.yml) [![PyPI upload status](https://github.com/tektronix/tm_devices/actions/workflows/package-release.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/package-release.yml) |
-| **Documentation** | [![ReadtheDocs Status](https://img.shields.io/readthedocs/tm-devices/stable?logo=readthedocs)](https://tm-devices.readthedocs.io/stable)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
-| **Code Style**    | [![Test style: pytest](https://img.shields.io/badge/test%20style-pytest-blue)](https://github.com/pytest-dev/pytest) [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-black)](https://docs.astral.sh/ruff/formatter/) [![Docstring style: google](https://img.shields.io/badge/docstring%20style-google-tan)](https://google.github.io/styleguide/pyguide.html)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
-| **Linting**       | [![pre-commit enabled](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) [![Docstring formatter: docformatter](https://img.shields.io/badge/docstring%20formatter-docformatter-tan)](https://github.com/PyCQA/docformatter) [![Type Checker: pyright](https://img.shields.io/badge/type%20checker-pyright-yellowgreen)](https://github.com/RobertCraigie/pyright-python) [![Linter: pylint](https://img.shields.io/badge/linter-pylint-purple)](https://github.com/pylint-dev/pylint) [![Linter: Ruff](https://img.shields.io/badge/linter-ruff-purple)](https://github.com/charliermarsh/ruff)                                                                                                                                                                                                                                                                                                                                                                               |
+|                   |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
+| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Testing**       | [![Code testing status](https://github.com/tektronix/tm_devices/actions/workflows/test-code.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/test-code.yml) [![Docs testing status](https://github.com/tektronix/tm_devices/actions/workflows/test-docs.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/test-docs.yml) [![Coverage status](https://codecov.io/gh/tektronix/tm_devices/branch/main/graph/badge.svg)](https://codecov.io/gh/tektronix/tm_devices)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
+| **Code Quality**  | [![CodeQL status](https://github.com/tektronix/tm_devices/actions/workflows/codeql-analysis.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/codeql-analysis.yml) [![CodeFactor grade](https://www.codefactor.io/repository/github/tektronix/tm_devices/badge)](https://www.codefactor.io/repository/github/tektronix/tm_devices) [![pre-commit status](https://results.pre-commit.ci/badge/github/tektronix/tm_devices/main.svg)](https://results.pre-commit.ci/latest/github/tektronix/tm_devices/main)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
+| **Package**       | [![PyPI: Package status](https://img.shields.io/pypi/status/tm_devices?logo=pypi)](https://pypi.org/project/tm_devices/) [![PyPI: Latest release version](https://img.shields.io/pypi/v/tm_devices?logo=pypi)](https://pypi.org/project/tm_devices/) [![PyPI: Supported Python versions](https://img.shields.io/pypi/pyversions/tm_devices?logo=python)](https://pypi.org/project/tm_devices/) [![PyPI: Downloads](https://static.pepy.tech/badge/tm-devices)](https://pepy.tech/project/tm_devices) [![License: Apache 2.0](https://img.shields.io/pypi/l/tm_devices)](https://github.com/tektronix/tm_devices/blob/main/LICENSE.md) [![Package build status](https://github.com/tektronix/tm_devices/actions/workflows/package-build.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/package-build.yml) [![PyPI upload status](https://github.com/tektronix/tm_devices/actions/workflows/package-release.yml/badge.svg?branch=main)](https://github.com/tektronix/tm_devices/actions/workflows/package-release.yml) |
+| **Documentation** | [![ReadtheDocs Status](https://img.shields.io/readthedocs/tm-devices/stable?logo=readthedocs)](https://tm-devices.readthedocs.io/stable)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
+| **Code Style**    | [![Test style: pytest](https://img.shields.io/badge/test%20style-pytest-blue)](https://github.com/pytest-dev/pytest) [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-black)](https://docs.astral.sh/ruff/formatter/) [![Docstring style: google](https://img.shields.io/badge/docstring%20style-google-tan)](https://google.github.io/styleguide/pyguide.html)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
+| **Linting**       | [![pre-commit enabled](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) [![Docstring formatter: docformatter](https://img.shields.io/badge/docstring%20formatter-docformatter-tan)](https://github.com/PyCQA/docformatter) [![Type Checker: pyright](https://img.shields.io/badge/type%20checker-pyright-yellowgreen)](https://github.com/RobertCraigie/pyright-python) [![Linter: pylint](https://img.shields.io/badge/linter-pylint-purple)](https://github.com/pylint-dev/pylint) [![Linter: Ruff](https://img.shields.io/badge/linter-ruff-purple)](https://github.com/charliermarsh/ruff)                                                                                                                                                                                                                                                                                                                                                                                      |
 
 </div>
 
@@ -50,7 +50,7 @@ pip install tm_devices
 ```console
 $ list-visa-resources
 [
-  "TCPIP0::192.168.0.100::inst0::INSTR",
+  "TCPIP0::192.168.0.1::inst0::INSTR",
   "ASRL4::INSTR"
 ]
 ```
@@ -61,7 +61,7 @@ $ list-visa-resources
 from tm_devices import DeviceManager
 
 with DeviceManager() as device_manager:
-    scope = device_manager.add_scope("192.168.0.100")
+    scope = device_manager.add_scope("192.168.0.1")
     scope.query("*IDN?")
     print(scope)
 ```
diff --git a/docs/basic_usage.md b/docs/basic_usage.md
index 47d1c957..59c7c205 100644
--- a/docs/basic_usage.md
+++ b/docs/basic_usage.md
@@ -10,7 +10,7 @@ This will print the available VISA devices to the console when run from a shell
 ```console
 $ list-visa-resources
 [
-  "TCPIP0::192.168.0.100::inst0::INSTR",
+  "TCPIP0::192.168.0.1::inst0::INSTR",
   "ASRL4::INSTR"
 ]
 ```
@@ -59,6 +59,23 @@ outside the Python code for ease of automation
 --8<-- "examples/miscellaneous/adding_devices_with_env_var.py"
 ```
 
+## Customize logging and console output
+
+The amount of console output and logging saved to the log file can be customized as needed. This
+configuration can be done in the Python code itself as demonstrated here, or by using the
+[config file](configuration.md#config-options) or
+[environment variable](configuration.md#environment-variable).
+
+!!! important
+    If any configuration is performed in the Python code prior to instantiating the
+    [`DeviceManager`][tm_devices.DeviceManager], all other logging configuration methods
+    (config file, env var) will be ignored.
+
+```python
+# fmt: off
+--8<-- "examples/miscellaneous/customize_logging.py"
+```
+
 ## Disable command checking
 
 This removes an extra query that verifies the property was set to the expected
diff --git a/docs/configuration.md b/docs/configuration.md
index b9bce014..ab0bfd32 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -215,8 +215,7 @@ devices:
 
 ### Config Options
 
-These options are flags that enable/disable runtime behaviors of the Device
-Manager.
+These options are used to configure runtime behaviors of `tm_devices`.
 
 #### Yaml Options Syntax
 
@@ -230,6 +229,12 @@ options:
   retry_visa_connection: false
   default_visa_timeout: 5000
   check_for_updates: false
+  log_console_level: INFO
+  log_file_level: DEBUG
+  log_file_directory: ./logs
+  log_file_name: tm_devices_<timestamp>.log
+  log_colored_output: false
+  log_pyvisa_messages: false
 ```
 
 These are all `false` by default if not defined, set to `true` to modify the
@@ -259,6 +264,30 @@ runtime behavior configuration.
 - `check_for_updates`
     - This config option will enable a check for any available updates on pypi.org for the
         package when the `DeviceManager` is instantiated.
+- `log_console_level`
+    - This config option is used to set the log level for the console output.
+        The default value of this config option is "INFO". See the
+        [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more information.
+- `log_file_level`
+    - This config option is used to set the log level for the file output.
+        The default value of this config option is "DEBUG". See the
+        [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more information.
+- `log_file_directory`
+    - This config option is used to set the directory where the log files will be saved.
+        The default value of this config option is "./logs". See the
+        [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more information.
+- `log_file_name`
+    - This config option is used to set the name of the log file.
+        The default value of this config option is a timestamped filename with the .log extension. See the
+        [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more information.
+- `log_colored_output`
+    - This config option is used to enable or disable colored output in the console.
+        The default value of this config option is false. See the
+        [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more information.
+- `log_pyvisa_messages`
+    - This config option is used to enable or disable logging of PyVISA messages within the
+        configured log file. The default value of this config option is false. See the
+        [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more information.
 
 ### Sample Config File
 
@@ -320,6 +349,12 @@ options:
   retry_visa_connection: false
   default_visa_timeout: 10000  # 10 second default VISA timeout
   check_for_updates: false
+  log_console_level: NONE    # completely disable console output
+  log_file_level: DEBUG
+  log_file_directory: ./logs
+  log_file_name: custom_logfile.log    # customize the log file name
+  log_colored_output: false
+  log_pyvisa_messages: true  # log PyVISA messages in the log file
 ```
 
 #### TOML
@@ -393,8 +428,14 @@ standalone = false
 verbose_mode = false
 verbose_visa = false
 retry_visa_connection = false
-default_visa_timeout = 10000  # 10 second default VISA timeout
+default_visa_timeout = 10000         # 10 second default VISA timeout
 check_for_updates = false
+log_console_level = "NONE"           # completely disable console output
+log_file_level = "DEBUG"
+log_file_directory = "./logs"
+log_file_name = "custom_logfile.log" # customize the log file name
+log_colored_output = false
+log_pyvisa_messages = true           # log PyVISA messages in the log file
 ```
 
 ---
diff --git a/docs/key_features.md b/docs/key_features.md
index 1fa1f161..555b5dc8 100644
--- a/docs/key_features.md
+++ b/docs/key_features.md
@@ -14,3 +14,4 @@ complex examples.
         IntelliSense.
 - Organize connections to an entire "Test Bench" of devices with one package!
     - Full support for all VISA connection types (some require external drivers).
+- Customizable logging to both the console and a log file.
diff --git a/docs/known_words.txt b/docs/known_words.txt
index 583a3f61..b90d6485 100644
--- a/docs/known_words.txt
+++ b/docs/known_words.txt
@@ -19,11 +19,13 @@ ci
 classdiagram
 codebase
 codecov
+colored
 config
 conftest
 cookiecutter
 cov
 csv
+customizable
 deps
 dev
 disable_command_verification
@@ -33,6 +35,7 @@ docstring
 docstrings
 en
 enum
+env
 executables
 filepath
 generate_function
diff --git a/docs/macros.py b/docs/macros.py
index 8031460b..99c33017 100644
--- a/docs/macros.py
+++ b/docs/macros.py
@@ -265,8 +265,8 @@ def define_env(env: MacrosPlugin) -> None:
             used to perform a transformation
     """
     # Read in the current package version number to use in templates and files
-    with open(
-        pathlib.Path(f"{pathlib.Path(__file__).parents[1]}") / "pyproject.toml", "rb"
+    with (pathlib.Path(f"{pathlib.Path(__file__).parents[1]}") / "pyproject.toml").open(
+        "rb"
     ) as file_handle:
         pyproject_data = tomli.load(file_handle)
         package_version = "v" + pyproject_data["tool"]["poetry"]["version"]
diff --git a/examples/miscellaneous/customize_logging.py b/examples/miscellaneous/customize_logging.py
new file mode 100644
index 00000000..5f6cc3b9
--- /dev/null
+++ b/examples/miscellaneous/customize_logging.py
@@ -0,0 +1,21 @@
+"""The console output and level of logging outputs in the log file can be configured as needed."""
+
+from tm_devices import configure_logging, DeviceManager, LoggingLevels
+from tm_devices.drivers import MSO6B
+
+# NOTE: This configuration will prevent any logging config options from a config file or
+# environment variable from being used.
+configure_logging(
+    log_console_level=LoggingLevels.NONE,  # completely disable console logging
+    log_file_level=LoggingLevels.DEBUG,  # log everything to the file
+    log_file_directory="./log_files",  # save the log file in the "./log_files" directory
+    log_file_name="custom_log_filename.log",  # customize the filename
+    log_pyvisa_messages=True,  # include all the pyvisa debug messages in the same log file
+)
+
+with DeviceManager(verbose=False) as dm:
+    scope: MSO6B = dm.add_scope("192.168.0.1")
+    scope.curve_query(1)
+    scope.check_port_connection(4000)
+    scope.check_network_connection()
+    scope.check_visa_connection()
diff --git a/examples/miscellaneous/register_dm_atexit.py b/examples/miscellaneous/register_dm_atexit.py
index 7207c538..e0ba70aa 100644
--- a/examples/miscellaneous/register_dm_atexit.py
+++ b/examples/miscellaneous/register_dm_atexit.py
@@ -12,7 +12,7 @@
 atexit.register(dm.close)
 
 # Add a device
-scope: MSO6B = dm.add_scope("192.168.1.102")
+scope: MSO6B = dm.add_scope("192.168.0.1")
 
 # Use the device
 print(scope)
diff --git a/examples/scopes/tekscope/basic_curve_query.py b/examples/scopes/tekscope/basic_curve_query.py
index 219b1a8c..aeb70d6d 100644
--- a/examples/scopes/tekscope/basic_curve_query.py
+++ b/examples/scopes/tekscope/basic_curve_query.py
@@ -1,9 +1,11 @@
 """An example showing a basic curve query."""
 
+from pathlib import Path
+
 from tm_devices import DeviceManager
 from tm_devices.drivers import AFG3KC, MSO5
 
-EXAMPLE_CSV_FILE = "example_curve_query.csv"
+EXAMPLE_CSV_FILE = Path("example_curve_query.csv")
 
 with DeviceManager(verbose=True) as dm:
     scope: MSO5 = dm.add_scope("MSO56-100083")
@@ -16,7 +18,7 @@
     curve_returned = scope.curve_query(1, output_csv_file=EXAMPLE_CSV_FILE)
 
 # Read in the curve query from file
-with open(EXAMPLE_CSV_FILE, encoding="utf-8") as csv_content:
+with EXAMPLE_CSV_FILE.open(encoding="utf-8") as csv_content:
     curve_saved = [int(i) for i in csv_content.read().split(",")]
 
 # Verify query saved to csv is the same as the one returned from curve_query function call
diff --git a/examples/scopes/tekscope/basic_save_recall.py b/examples/scopes/tekscope/basic_save_recall.py
index 33ac6b1a..03c6ca3e 100644
--- a/examples/scopes/tekscope/basic_save_recall.py
+++ b/examples/scopes/tekscope/basic_save_recall.py
@@ -5,7 +5,7 @@
 
 with DeviceManager(verbose=True) as dm:
     # Get a scope
-    scope: MSO6B = dm.add_scope("192.168.1.177")
+    scope: MSO6B = dm.add_scope("192.168.0.1")
 
     # Send some commands
     scope.add_new_math("MATH1", "CH1")  # add MATH1 to CH1
diff --git a/examples/scopes/tekscope/generate_internal_afg_signal.py b/examples/scopes/tekscope/generate_internal_afg_signal.py
index e66eca9b..cf0a05f7 100644
--- a/examples/scopes/tekscope/generate_internal_afg_signal.py
+++ b/examples/scopes/tekscope/generate_internal_afg_signal.py
@@ -5,7 +5,7 @@
 
 with DeviceManager(verbose=True) as dm:
     # Create a connection to the scope and indicate that it is a MSO5 scope for type hinting
-    scope: MSO5 = dm.add_scope("192.168.1.102")
+    scope: MSO5 = dm.add_scope("192.168.0.1")
 
     # Generate the signal using individual PI commands.
     scope.commands.afg.frequency.write(10e6)  # set frequency
diff --git a/examples/scopes/tekscope/save_screenshot.py b/examples/scopes/tekscope/save_screenshot.py
index 7146ff3e..3503c3ba 100644
--- a/examples/scopes/tekscope/save_screenshot.py
+++ b/examples/scopes/tekscope/save_screenshot.py
@@ -5,7 +5,7 @@
 
 with DeviceManager(verbose=True) as dm:
     # Add a scope
-    scope: MSO6B = dm.add_scope("192.168.1.5")
+    scope: MSO6B = dm.add_scope("192.168.0.1")
 
     # Send some commands
     scope.add_new_math("MATH1", "CH1")  # add MATH1 to CH1
diff --git a/examples/source_measure_units/2400/smu_2450_leakage_current.py b/examples/source_measure_units/2400/smu_2450_leakage_current.py
index 33cf1288..243e2918 100644
--- a/examples/source_measure_units/2400/smu_2450_leakage_current.py
+++ b/examples/source_measure_units/2400/smu_2450_leakage_current.py
@@ -13,7 +13,7 @@
 from tm_devices.drivers import SMU2450
 
 with DeviceManager(verbose=False) as device_manager:
-    inst: SMU2450 = device_manager.add_smu("192.168.1.4", alias="my2450")
+    inst: SMU2450 = device_manager.add_smu("192.168.0.1", alias="my2450")
 
     # Reset the instrument, which also clears the buffer.
     inst.commands.reset()
diff --git a/examples/source_measure_units/2400/smu_2450_measuring_lowr_devices.py b/examples/source_measure_units/2400/smu_2450_measuring_lowr_devices.py
index aa930091..08c32e3c 100644
--- a/examples/source_measure_units/2400/smu_2450_measuring_lowr_devices.py
+++ b/examples/source_measure_units/2400/smu_2450_measuring_lowr_devices.py
@@ -21,7 +21,7 @@
 with DeviceManager() as device_manager:
     print(device_manager.get_available_devices())
 
-    inst: SMU2450 = device_manager.add_smu("192.168.4.74", alias="my2450")
+    inst: SMU2450 = device_manager.add_smu("192.168.0.1", alias="my2450")
 
     # Configure the Simple Loop trigger model template to make 100 readings.
     inst.commands.trigger.model.load_simple_loop(100)
diff --git a/examples/source_measure_units/2400/smu_2450_rechargeable_battery.py b/examples/source_measure_units/2400/smu_2450_rechargeable_battery.py
index 212b9142..7cc0983e 100644
--- a/examples/source_measure_units/2400/smu_2450_rechargeable_battery.py
+++ b/examples/source_measure_units/2400/smu_2450_rechargeable_battery.py
@@ -19,7 +19,7 @@
 with DeviceManager() as device_manager:
     print(device_manager.get_available_devices())
 
-    inst: SMU2450 = device_manager.add_smu("192.168.4.74", alias="my2450")
+    inst: SMU2450 = device_manager.add_smu("192.168.0.1", alias="my2450")
 
     # Clear the buffer.
     inst.commands.buffer_var["defbuffer1"].clear()
diff --git a/examples/source_measure_units/2400/smu_2450_solar_cell.py b/examples/source_measure_units/2400/smu_2450_solar_cell.py
index 56dba26c..05589d3e 100644
--- a/examples/source_measure_units/2400/smu_2450_solar_cell.py
+++ b/examples/source_measure_units/2400/smu_2450_solar_cell.py
@@ -15,7 +15,7 @@
 with DeviceManager() as device_manager:
     print(device_manager.get_available_devices())
 
-    inst: SMU2450 = device_manager.add_smu("192.168.4.74", alias="my2450")
+    inst: SMU2450 = device_manager.add_smu("192.168.0.1", alias="my2450")
 
     # Define the number of points in the sweep.
     POINTS = 56
diff --git a/pyproject.toml b/pyproject.toml
index 0ae8a008..24e3b86c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -76,6 +76,7 @@ repository = "https://github.com/tektronix/tm_devices"
 version = "2.5.0"
 
 [tool.poetry.dependencies]
+colorlog = "^6.9.0"
 gpib-ctypes = "^0.3.0"
 libusb-package = "^1.0.26.0,!=1.0.26.2"  # 1.0.26.2 doesn't work with Python 3.12
 packaging = "^24.0"
@@ -94,6 +95,7 @@ tomli = "^2.0.1"
 tomli-w = "^1.0.0"
 traceback-with-variables = "^2.0.4"
 typing-extensions = "^4.10.0"
+tzlocal = "^5.2"
 urllib3 = "^2.0"
 zeroconf = "^0.136.0"
 
@@ -110,7 +112,7 @@ pre-commit = [
   {python = "3.8", version = "^3.5"}
 ]
 pylint = "3.2.7"
-pyright = {extras = ["nodejs"], version = "^1.1.387"}
+pyright = {extras = ["nodejs"], version = "1.1.389"}
 pyroma = "^4.2"
 tox = "^4.0"
 tox-gh-actions = "^3.1.0"
@@ -235,6 +237,7 @@ disable = [
   "too-many-lines",  # not necessary to check for
   "too-many-statements",  # caught by ruff
   "too-many-statements",  # caught by ruff
+  "unexpected-keyword-arg",  # caught by pyright
   "unused-argument",  # caught by ruff
   "unused-import",  # caught by ruff
   "use-implicit-booleaness-not-comparison-to-string",  # caught by ruff
@@ -287,6 +290,7 @@ filterwarnings = [
 ]
 junit_family = "xunit2"
 junit_logging = "all"
+log_format = "[%(asctime)s] [%(levelname)8s] %(message)s"
 markers = [
   'docs',
   'order',
@@ -315,27 +319,19 @@ fixable = ["ALL"]
 flake8-pytest-style = {mark-parentheses = false}
 flake8-quotes = {docstring-quotes = "double"}
 ignore = [
-  "ANN101",  # Missing type annotation for self in method
-  "ANN102",  # Missing type annotation for cls in method
-  "ANN401",  # Dynamically typed expressions (typing.Any) are disallowed in *args and **kwargs
-  "COM812",  # Trailing comma missing
-  "EM102",  # Exception must not use an f-string literal, assign to variable first
-  "FA100",  # Missing `from __future__ import annotations`, but uses ...
-  "FBT",  # flake8-boolean-trap
-  "FIX002",  # Line contains TO DO
+  "ANN401",  # Dynamically typed expressions (typing.Any) are disallowed in *args and **kwargs (allowed in this package)
+  "COM812",  # Trailing comma missing (allowed in this package)
+  "FA100",  # Missing `from __future__ import annotations`, but uses ... (allowed in this package)
+  "FBT",  # flake8-boolean-trap  # TODO: enable this
+  "FIX002",  # Line contains TO DO (allowed in this package)
   "ISC001",  # single-line-implicit-string-concatenation (handled by formatter)
-  "PTH109",  # `os.getcwd()` should be replaced by `Path.cwd()`
-  "PTH123",  # `open()` should be replaced by `Path.open()`
-  "PTH207",  # Replace `iglob` with `Path.glob` or `Path.rglob`
-  "PYI021",  # Docstrings should not be included in stubs
-  "T20",  # flake8-print
-  "TD002",  # Missing author in TO DO
-  "TD003",  # Missing issue link on the line following this TO DO
-  "TRY301",  # Abstract raise to an inner function
-  "UP006",  # Use {to} instead of {from} for type annotation
-  "UP007",  # Use `X | Y` for type annotations
-  "UP024",  # Replace aliased errors with `OSError`
-  "UP037"  # Remove quotes from type annotation
+  "PYI021",  # Docstrings should not be included in stubs (allowed in this package)
+  "TD002",  # Missing author in TO DO (allowed in this package)
+  "TD003",  # Missing issue link on the line following this TO DO (allowed in this package)
+  "UP006",  # Use {to} instead of {from} for type annotation (allowed in this package)
+  "UP007",  # Use `X | Y` for type annotations (allowed in this package)
+  "UP024",  # Replace aliased errors with `OSError` (allowed in this package)
+  "UP037"  # Remove quotes from type annotation (allowed in this package)
 ]
 pydocstyle = {convention = "google"}
 pylint = {max-args = 7}
@@ -357,7 +353,9 @@ order-by-type = false
 
 [tool.ruff.lint.per-file-ignores]
 "examples/**" = [
-  "S101"  # Use of assert detected
+  "FBT",  # flake8-boolean-trap
+  "S101",  # Use of assert detected
+  "T201"  # `print` found
 ]
 "examples/miscellaneous/custom_device_driver_support.py" = [
   "ARG002",  # Unused method argument
@@ -445,6 +443,7 @@ setenv =
 commands_pre =
     python -m poetry install --no-root --without=main
 commands =
+    !tests: python -c "import shutil; shutil.rmtree('dist_{envname}', ignore_errors=True)"
     !tests: poetry build --output=dist_{envname}
     !tests: twine check --strict dist_{envname}/*
     !tests: pre-commit run --all-files
diff --git a/scripts/contributor_setup.py b/scripts/contributor_setup.py
index bcb29daf..a5bdca28 100644
--- a/scripts/contributor_setup.py
+++ b/scripts/contributor_setup.py
@@ -24,7 +24,7 @@ def create_virtual_environment(virtual_env_dir: str | os.PathLike[str]) -> None:
     Args:
         virtual_env_dir: The directory where the virtual environment should be created
     """
-    print(f"\nCreating virtualenv located at '{virtual_env_dir}'")
+    print(f"\nCreating virtualenv located at '{virtual_env_dir}'")  # noqa: T201
     _run_cmd_in_subprocess(f'"{sys.executable}" -m venv "{virtual_env_dir}" --clear')
 
 
@@ -35,7 +35,7 @@ def _run_cmd_in_subprocess(command: str) -> None:
         command: The command string to send.
     """
     command = command.replace("\\", "/")
-    print(f"\nExecuting command: {command}")
+    print(f"\nExecuting command: {command}")  # noqa: T201
     subprocess.check_call(shlex.split(command))  # noqa: S603
 
 
@@ -48,7 +48,7 @@ def main() -> None:
     starting_dir = Path.cwd()
     try:
         if RUNNING_IN_VIRTUALENV:
-            raise IndexError
+            raise IndexError  # noqa: TRY301
         # This requires contributors to use newer versions of Python even
         # though the package supports older versions.
         if sys.version_info < (3, 9):
@@ -76,7 +76,7 @@ def main() -> None:
         files = list(
             filter(
                 lambda x: "site-packages" not in x and "pythonw" not in x,
-                glob.iglob(
+                glob.iglob(  # noqa: PTH207
                     f"{virtual_env_dir}/{'bin' if RUNNING_ON_LINUX else 'Scripts'}/**/python*",
                     recursive=True,
                 ),
diff --git a/src/tm_devices/__init__.py b/src/tm_devices/__init__.py
index e4e4cd00..82f8e70d 100644
--- a/src/tm_devices/__init__.py
+++ b/src/tm_devices/__init__.py
@@ -21,12 +21,16 @@
 )
 from tm_devices.helpers.enums import SupportedModels
 from tm_devices.helpers.functions import register_additional_usbtmc_mapping
+from tm_devices.helpers.logging import configure_logging, LoggingLevels
 
 # Read version from installed package.
 __version__ = version(PACKAGE_NAME)
 
+
 __all__ = [
+    "configure_logging",
     "DeviceManager",
+    "LoggingLevels",
     "print_available_visa_devices",
     "PYVISA_PY_BACKEND",
     "register_additional_usbtmc_mapping",
diff --git a/src/tm_devices/commands/helpers/scpi_commands.py b/src/tm_devices/commands/helpers/scpi_commands.py
index 99c73a68..7980d004 100644
--- a/src/tm_devices/commands/helpers/scpi_commands.py
+++ b/src/tm_devices/commands/helpers/scpi_commands.py
@@ -76,7 +76,7 @@ def verify(self, value: Union[float, str]) -> Tuple[bool, str]:
 
         Returns:
             A tuple containing a boolean indicating if the values match and a string with the actual
-            return value from the device.
+                return value from the device.
 
         Raises:
             tm_devices.commands.NoDeviceProvidedError: Indicates that no device connection exists.
@@ -136,7 +136,7 @@ def verify(self, argument: str, value: Union[float, str]) -> Tuple[bool, str]:
 
         Returns:
             A tuple containing a boolean indicating if the values match and a string with the actual
-            return value from the device.
+                return value from the device.
 
         Raises:
             tm_devices.commands.NoDeviceProvidedError: Indicates that no device connection exists.
diff --git a/src/tm_devices/components/dm_config_parser.py b/src/tm_devices/components/dm_config_parser.py
index 7c515c6e..e35d22c0 100644
--- a/src/tm_devices/components/dm_config_parser.py
+++ b/src/tm_devices/components/dm_config_parser.py
@@ -430,7 +430,7 @@ def __parse_config_file(
         if not config_path.is_file():
             raise FileNotFoundError(config_path)
         # read in data
-        with open(config_path, encoding="utf-8") as config_file:
+        with config_path.open(encoding="utf-8") as config_file:
             if config_path.suffix == ".toml":
                 data = tomli.loads(config_file.read())
             else:  # ["yaml", "yml"]
diff --git a/src/tm_devices/device_manager.py b/src/tm_devices/device_manager.py
index c5fa1b4f..58b4647f 100644
--- a/src/tm_devices/device_manager.py
+++ b/src/tm_devices/device_manager.py
@@ -6,6 +6,7 @@
 import contextlib
 import inspect
 import json
+import logging
 import os
 import pathlib
 import socket
@@ -36,6 +37,7 @@
 from tm_devices.helpers import (
     AliasDict,
     check_for_update,
+    configure_logging,
     ConnectionTypes,
     create_visa_connection,
     detect_visa_resource_expression,
@@ -44,7 +46,6 @@
     DMConfigOptions,
     get_model_series,
     PACKAGE_NAME,
-    print_with_timestamp,
     PYVISA_PY_BACKEND,
     SerialConfig,
     Singleton,
@@ -98,6 +99,8 @@
 UnsupportedDeviceAlias = TypeVar("UnsupportedDeviceAlias", bound=Device, default=Device)
 """An alias to a custom device driver for an unsupported device type."""
 
+_logger: logging.Logger = logging.getLogger(__name__)
+
 
 ####################################################################################################
 # DeviceManager class
@@ -124,7 +127,11 @@ def __init__(
         """Create the instance of the DeviceManager.
 
         Args:
-            verbose: A boolean indicating if verbose output should be printed.
+            verbose: A boolean indicating if verbose output should be printed. This flag cascades
+                down to all connected devices. Setting it to False **will not** remove all printouts
+                to stdout. To remove all console output, use
+                [`configure_logging()`][tm_devices.configure_logging] before instantiating the
+                DeviceManager.
             config_options: An optional set of configuration options to use
                 (updates the current configuration options).
             external_device_drivers: An optional dict for passing in additional device drivers.
@@ -151,6 +158,14 @@ def __init__(
         # actually populate the options
         self.__set_options(verbose)
 
+        # Pass in the options from the config file or environment variable to the logger
+        logging_options = {
+            key: value
+            for key, value in self.__config.options.to_dict(ignore_none=True).items()
+            if key.startswith("log_")
+        }
+        configure_logging(**logging_options)
+
         self.open()
 
     def __del__(self) -> None:
@@ -249,8 +264,11 @@ def verbose_visa(self, value: bool) -> None:
             visa.log_to_screen()
         else:
             for handler in visa.logger.handlers.copy():
-                if "DEBUG" in str(handler):
-                    visa.logger.removeHandler(handler)
+                if "DEBUG" in str(handler) and (
+                    isinstance(handler, logging.StreamHandler)
+                    and not isinstance(handler, logging.FileHandler)
+                ):
+                    visa.logger.removeHandler(handler)  # pyright: ignore[reportUnknownArgumentType]
         self.__config.options.verbose_visa = value
         self.__verbose_visa = value
 
@@ -683,21 +701,20 @@ def cleanup_all_devices(self) -> None:
             try:
                 device_object.cleanup(verbose=bool(self.__config.options.verbose_mode))
             except (visa.errors.Error, socket.error, RPCError, AttributeError):  # noqa: PERF203
-                print(f"Device cleanup of {device_name} failed. Retrying...")
+                _logger.warning("Device cleanup of %s failed. Retrying...", device_name)
                 device_object.cleanup()
 
     def close(self) -> None:
         """Close the DeviceManager."""
         self.__protect_access()
-        print()
         if self.__devices:
-            print_with_timestamp("Closing Connections to Devices")
+            _logger.info("Closing Connections to Devices")
             if self.__teardown_cleanup_enabled:
                 self.cleanup_all_devices()
             for device_object in list(self.__devices.values()):
                 device_object.close()
         self.__is_open = False
-        print_with_timestamp(f"{self.__class__.__name__} Closed")
+        _logger.info("%s Closed", self.__class__.__name__)
 
     def disable_device_command_checking(self) -> None:
         """Set the `.enable_verification` attribute of each device to `False`.
@@ -1007,13 +1024,11 @@ def load_config_file(self, config_file_path: Union[str, os.PathLike[str]]) -> No
         Args:
             config_file_path: The path to the config file to load.
         """
-        print_with_timestamp(
-            f"Loading Configuration from {pathlib.Path(config_file_path).resolve()}"
-        )
+        _logger.info("Loading Configuration from %s", pathlib.Path(config_file_path).resolve())
         self.__config.load_config_file(config_file_path)
         self.__set_options(self.__verbose)
         if len(self.__config.devices) != len(self.__devices):
-            print_with_timestamp("Opening Connections to Devices")
+            _logger.info("Opening Connections to Devices")
             for device_name, device_config in self.__config.devices.items():
                 if device_name not in self.__devices:
                     self.__create_device(device_name, device_config, 3)
@@ -1031,10 +1046,10 @@ def open(self) -> bool:
                 f"{self.__class__.__name__} has already been closed."
             )
             raise AssertionError(msg)
-        print_with_timestamp(f"Opening {self.__class__.__name__}")
+        _logger.info("Opening %s", self.__class__.__name__)
         # Create the devices
         if self.__config.devices:
-            print_with_timestamp("Opening Connections to Devices")
+            _logger.info("Opening Connections to Devices")
         for device_name, device_config in self.__config.devices.items():
             self.__create_device(device_name, device_config, 3)
         if self.__setup_cleanup_enabled:
@@ -1093,9 +1108,9 @@ def write_current_configuration_to_config_file(
         Args:
             config_file_path: The path to the config file. If ends in ".toml" will create toml file.
         """
-        print_with_timestamp("Writing Configuration to file")
+        _logger.info("Writing Configuration to file")
         new_file_path = pathlib.Path(self.__config.write_config_to_file(config_file_path)).resolve()
-        print_with_timestamp(f"Wrote Configuration to {new_file_path}")
+        _logger.info("Wrote Configuration to %s", new_file_path)
 
     def get_current_configuration_as_environment_variable_strings(self) -> str:
         """Return the current configuration represented as environment variables."""
@@ -1191,23 +1206,25 @@ def __clear_visa_output_buffer_and_get_idn(visa_resource: MessageBasedResource)
                 # 16 is the MAV bit (Message Available) from the Status Byte register
                 # MAV flag is only one bit and turns off after a single response is
                 # successfully read, even if there is more in the buffer.
-                warnings.warn(
+                msg = (
                     f"\nThe device `{visa_resource.resource_info.resource_name}` had data "
                     "sitting in the VISA Output Buffer on first connection. "
                     "\nDetected data in the buffer via the Status Byte register. "
-                    "\nThe device_clear() will be called so VISA I/O buffers get flushed.",
-                    stacklevel=1,
+                    "\nThe device_clear() will be called so VISA I/O buffers get flushed."
                 )
+                warnings.warn(msg, stacklevel=1)
+                _logger.warning(msg)
             # always flush the VISA I/O Buffers on the device to clean up any stale data.
             # (note: the Events are kept in different buffers, so *ESR? is not impacted)
             visa_resource.clear()
         except visa.VisaIOError as e:
-            warnings.warn(
+            msg = (
                 f"A VISA IO error occurred when attempting to read the status byte or clear the "
                 f"output buffer of the resource `{visa_resource.resource_info.resource_name}`.\n"
-                f"Error: {e}",
-                stacklevel=1,
+                f"Error: {e}"
             )
+            warnings.warn(msg, stacklevel=1)
+            _logger.warning(msg)
         visa_resource.write("*IDN?")
         idn_response = ""
         error_msg = None
@@ -1271,14 +1288,15 @@ def __create_device(
 
         alias_string = f' "{device_config.alias}"' if device_config.alias else ""
         if device_config.device_type == DeviceTypes.UNSUPPORTED:
-            warnings.warn(
+            msg = (
                 f"An unsupported device type is being added to the {self.__class__.__name__}. "
                 f"Not all functionality will be available in the device driver. "
                 f"Please consider contributing to {PACKAGE_NAME} to implement official "
-                f"support for this device type.",
-                stacklevel=warning_stacklevel,
+                f"support for this device type."
             )
-        print_with_timestamp(f"Creating Connection to {device_config_name}{alias_string}")
+            warnings.warn(msg, stacklevel=warning_stacklevel)
+            _logger.warning(msg)
+        _logger.info("Creating Connection to %s%s", device_config_name, alias_string)
         new_device: Device
         if device_config.connection_type == ConnectionTypes.REST_API:
             device_driver_class = device_drivers[str(device_config.device_driver)]
@@ -1436,4 +1454,4 @@ def print_available_visa_devices() -> None:  # pragma: no cover
     """Print all available VISA devices to the console."""
     with contextlib.redirect_stdout(None), contextlib.redirect_stderr(None), DeviceManager() as dm:
         available_devices = dm.get_available_devices()
-    print(json.dumps(available_devices["local"], indent=2))
+    print(json.dumps(available_devices["local"], indent=2))  # noqa: T201
diff --git a/src/tm_devices/driver_mixins/device_control/_abstract_device_control.py b/src/tm_devices/driver_mixins/device_control/_abstract_device_control.py
index 3756411f..2030bc07 100644
--- a/src/tm_devices/driver_mixins/device_control/_abstract_device_control.py
+++ b/src/tm_devices/driver_mixins/device_control/_abstract_device_control.py
@@ -15,7 +15,9 @@ def __init__(self, config_entry: DeviceConfigEntry, verbose: bool) -> None:
 
         Args:
             config_entry: A config entry object parsed by the DMConfigParser.
-            verbose: A boolean indicating if verbose output should be printed.
+            verbose: A boolean indicating if verbose output should be printed. If True,
+                communication printouts will be logged with a level of INFO. If False,
+                communication printouts will be logged with a level of DEBUG.
         """
         self._is_open = False
         self._verbose = verbose
@@ -41,5 +43,5 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
 
         Returns:
             A tuple containing the current error code alongside a tuple of the current error
-            messages.
+                messages.
         """
diff --git a/src/tm_devices/driver_mixins/device_control/_abstract_device_visa_write_query_control.py b/src/tm_devices/driver_mixins/device_control/_abstract_device_visa_write_query_control.py
index bf1a31fb..da654c4d 100644
--- a/src/tm_devices/driver_mixins/device_control/_abstract_device_visa_write_query_control.py
+++ b/src/tm_devices/driver_mixins/device_control/_abstract_device_visa_write_query_control.py
@@ -1,5 +1,7 @@
 """A class defining methods that VISA devices and control mixins must have."""
 
+import logging
+
 from abc import abstractmethod
 from typing import Any, final, Tuple
 
@@ -8,6 +10,8 @@
 )
 from tm_devices.helpers import raise_failure, verify_values
 
+_logger: logging.Logger = logging.getLogger(__name__)
+
 
 class _AbstractDeviceVISAWriteQueryControl(_AbstractDeviceControl):  # pyright: ignore[reportUnusedClass]
     """Abstract class defining methods that VISA devices and control mixins must have."""
@@ -39,8 +43,8 @@ def expect_esr(
 
         Returns:
             A boolean indicating if the check passed or failed, True means the check passed,
-            False means the check failed (however, failing the check will always result in an
-            AssertionError being raised, so the result will not really be usable).
+                False means the check failed (however, failing the check will always result in an
+                AssertionError being raised, so the result will not really be usable).
 
         Raises:
             AssertionError: Indicating that the device's error code and messages don't match the
@@ -64,7 +68,7 @@ def expect_esr(
             )
         except AssertionError as exc:
             check_passed &= False
-            print(exc)  # the exception already contains the timestamp
+            _logger.warning(exc)
 
         # Compare the error messages
         for expected_message, actual_message in zip(error_messages, actual_error_messages):
@@ -79,7 +83,7 @@ def expect_esr(
                 )
             except AssertionError as exc:  # noqa: PERF203
                 check_passed &= False
-                print(exc)
+                _logger.warning(exc)
 
         if not check_passed:
             failure_message = (
diff --git a/src/tm_devices/driver_mixins/device_control/pi_control.py b/src/tm_devices/driver_mixins/device_control/pi_control.py
index 2d3249b2..b743ad65 100644
--- a/src/tm_devices/driver_mixins/device_control/pi_control.py
+++ b/src/tm_devices/driver_mixins/device_control/pi_control.py
@@ -1,12 +1,15 @@
 """Base Programmable Interface (PI) control class module."""
 
+import contextlib
+import logging
+import logging.handlers
 import os
 import socket
 import time
 import warnings
 
 from abc import ABC
-from contextlib import contextmanager
+from pathlib import Path
 from typing import final, Generator, List, Optional, Sequence, Tuple, Union
 
 import pyvisa as visa
@@ -29,12 +32,14 @@
     get_model_series,
     get_version,
     get_visa_backend,
-    print_with_timestamp,
     PYVISA_PY_BACKEND,
     raise_failure,
     verify_values,
 )
 from tm_devices.helpers import ReadOnlyCachedProperty as cached_property  # noqa: N813
+from tm_devices.helpers.constants_and_dataclasses import PACKAGE_NAME
+
+_logger: logging.Logger = logging.getLogger(__name__)
 
 
 class PIControl(_AbstractDeviceVISAWriteQueryControl, _ExtendableMixin, ABC):  # pylint: disable=too-many-public-methods
@@ -64,7 +69,9 @@ def __init__(
 
         Args:
             config_entry: A config entry object parsed by the DMConfigParser.
-            verbose: A boolean indicating if verbose output should be printed.
+            verbose: A boolean indicating if verbose output should be printed. If True,
+                communication printouts will be logged with a level of INFO. If False,
+                communication printouts will be logged with a level of DEBUG.
             visa_resource: The VISA resource object.
             default_visa_timeout: The default VISA timeout value in milliseconds.
         """
@@ -122,8 +129,7 @@ def visa_timeout(self, timeout_ms: float) -> None:
             timeout_ms: The new VISA timeout, in milliseconds.
         """
         self._visa_resource.timeout = timeout_ms
-        if self._verbose:
-            print(f"{self._name_and_alias} VISA timeout set to: {timeout_ms}ms")
+        _logger.debug("%s VISA timeout set to: %.3fms", self._name_and_alias, timeout_ms)
 
     @property
     def visa_resource(self) -> visa.resources.MessageBasedResource:
@@ -190,7 +196,7 @@ def visa_backend(self) -> str:
     ################################################################################################
     # Context Manager Methods
     ################################################################################################
-    @contextmanager
+    @contextlib.contextmanager
     def temporary_visa_timeout(self, temporary_timeout_ms: float) -> Generator[None, None, None]:
         """Set a temporary VISA timeout value for the duration of the context.
 
@@ -210,19 +216,15 @@ def temporary_visa_timeout(self, temporary_timeout_ms: float) -> Generator[None,
     # Public Methods
     ################################################################################################
     @final
-    def check_visa_connection(self, verbose: bool = True) -> bool:
+    def check_visa_connection(self) -> bool:
         """Check if a VISA connection can be made to the device.
 
         Wrapper function for [`check_visa_connection`][tm_devices.helpers.check_visa_connection].
-
-        Args:
-            verbose: Set this to False in order to disable printouts.
         """
         return check_visa_connection(
             self._config_entry,
             self._visa_library_path,
             self._name_and_alias,
-            verbose=verbose and self._verbose,
         )
 
     def device_clear(self) -> None:  # pragma: no cover
@@ -247,7 +249,7 @@ def get_visa_stb(self) -> int:  # pragma: no cover
         """Return the VISA status byte."""
         return self._visa_resource.read_stb()
 
-    def poll_query(  # noqa: PLR0913
+    def poll_query(  # noqa: PLR0913  # pylint: disable=too-many-locals
         self,
         number_of_polls: int,
         query: str,
@@ -312,10 +314,11 @@ def poll_query(  # noqa: PLR0913
                     return
             time.sleep(sleep_time)
             poll_number += 1
-        raise AssertionError(  # noqa: TRY003
+        msg = (
             f"{query} {'never' if not invert_range else 'always'} "
             f"returned {wanted_val}, received:\n{query_list}"
         )
+        raise AssertionError(msg)
 
     def query(  # pylint: disable=arguments-differ
         self,
@@ -340,9 +343,13 @@ def query(  # pylint: disable=arguments-differ
             Error: An error occurred while sending the command.
             SystemError: An empty string was returned from the device.
         """
-        if self._verbose and verbose:
-            print_with_timestamp(f"({self._name_and_alias}) Query >>  {query!r}")
-
+        _logger.log(
+            logging.INFO if self._verbose and verbose else logging.DEBUG,
+            "(%s) Query >>  %r",
+            self._name_and_alias,
+            query,
+        )
+        self._command_logger.debug(query)
         try:
             response = self._visa_resource.query(query).strip()
             if remove_quotes:
@@ -352,8 +359,12 @@ def query(  # pylint: disable=arguments-differ
             msg = f"The query{pi_cmd_repr}failed with the following message: {error!r}"
             raise visa.Error(msg) from error
 
-        if self._verbose and verbose:
-            print_with_timestamp(f"Response from {query!r} >>  {response!r}")
+        _logger.log(
+            logging.INFO if self._verbose and verbose else logging.DEBUG,
+            "Response from %r >>  %r",
+            query,
+            response,
+        )
 
         if not allow_empty and not response:
             pi_cmd_repr = (
@@ -378,9 +389,13 @@ def query_binary(self, query: str, verbose: bool = True) -> Sequence[float]:
             Error: An error occurred while sending the command.
             SystemError: An empty string was returned from the device.
         """
-        if self._verbose and verbose:
-            print_with_timestamp(f"({self._name_and_alias}) Query Binary Values >>  {query!r}")
-
+        _logger.log(
+            logging.INFO if self._verbose and verbose else logging.DEBUG,
+            "(%s) Query Binary Values >>  %r",
+            self._name_and_alias,
+            query,
+        )
+        self._command_logger.debug(query)
         try:
             response = self._visa_resource.query_binary_values(query)  # pyright: ignore[reportUnknownMemberType]
         except (visa.VisaIOError, socket.error) as error:
@@ -388,10 +403,12 @@ def query_binary(self, query: str, verbose: bool = True) -> Sequence[float]:
             msg = f"The query{pi_cmd_repr}failed with the following message: {error!r}"
             raise visa.Error(msg) from error
 
-        if self._verbose and verbose:
-            print_with_timestamp(
-                f"Response from {query!r} >>  {', '.join([str(x) for x in response])!r}"
-            )
+        _logger.log(
+            logging.INFO if self._verbose and verbose else logging.DEBUG,
+            "Response from %r >>  %r",
+            query,
+            response,
+        )
 
         if not response:
             pi_cmd_repr = (
@@ -501,9 +518,13 @@ def query_raw_binary(self, query: str, verbose: bool = True) -> bytes:
             Error: An error occurred while sending the command.
             SystemError: An empty string was returned from the device.
         """
-        if self._verbose and verbose:
-            print_with_timestamp(f"({self._name_and_alias}) Query Raw Binary >>  {query!r}")
-
+        _logger.log(
+            logging.INFO if self._verbose and verbose else logging.DEBUG,
+            "(%s) Query Raw Binary >>  %r",
+            self._name_and_alias,
+            query,
+        )
+        self._command_logger.debug(query)
         try:
             self._visa_resource.write(query)
             response = self.read_raw()
@@ -512,8 +533,12 @@ def query_raw_binary(self, query: str, verbose: bool = True) -> bytes:
             msg = f"The query{pi_cmd_repr}failed with the following message: {error!r}"
             raise visa.Error(msg) from error
 
-        if self._verbose and verbose:
-            print_with_timestamp(f"Response from {query!r} >>  {response!r}")
+        _logger.log(
+            logging.INFO if self._verbose and verbose else logging.DEBUG,
+            "Response from %r >>  %r",
+            query,
+            response,
+        )
 
         if not response.strip():
             pi_cmd_repr = (
@@ -699,7 +724,7 @@ def set_if_needed(  # noqa: PLR0913
 
         Returns:
             Tuple containing the boolean value indicating if the command needed to be set and
-            the value returned from the query.
+                the value returned from the query.
         """
         try:
             query_passed, actual_value = self.query_response(
@@ -749,7 +774,6 @@ def wait_for_visa_connection(
         wait_time: float,
         sleep_seconds: int = 5,
         accept_immediate_connection: bool = False,
-        verbose: bool = True,
     ) -> bool:
         """Wait for a VISA connection to be made to the device.
 
@@ -758,7 +782,6 @@ def wait_for_visa_connection(
             sleep_seconds: The number of seconds to sleep in between connection attempts.
             accept_immediate_connection: A boolean indicating if a connection on the
                                          first attempt is a valid connection.
-            verbose: Set this to False in order to disable printouts.
 
         Returns:
             A boolean indicating if a VISA connection was made within the given time limit.
@@ -768,13 +791,14 @@ def wait_for_visa_connection(
         """
         attempt_num = 0
         visa_connection = False
-        if verbose:
-            print_with_timestamp(
-                f"Attempting to establish a VISA connection with {self._resource_expression}"
-            )
+        _logger.log(
+            logging.INFO if self._verbose else logging.DEBUG,
+            "Attempting to establish a VISA connection with %s",
+            self._resource_expression,
+        )
         start_time = time.perf_counter()
         while (time.perf_counter() - start_time) <= wait_time:
-            if visa_connection := self.check_visa_connection(verbose=False):
+            if visa_connection := self.check_visa_connection():
                 # pylint: disable=compare-to-zero
                 if not (attempt_num == 0 and not accept_immediate_connection):
                     break
@@ -790,17 +814,19 @@ def wait_for_visa_connection(
         end_time = time.perf_counter()
         total_time = end_time - start_time
 
-        if verbose:
-            if visa_connection:
-                print_with_timestamp(
-                    f"Successfully established a VISA connection with {self._resource_expression} "
-                    f"after {total_time:.2f} seconds"
-                )
-            else:
-                print_with_timestamp(
-                    f"Unable to establish a VISA connection with {self._resource_expression} "
-                    f"after {total_time:.2f} seconds"
-                )
+        if visa_connection:
+            _logger.log(
+                logging.INFO if self._verbose else logging.DEBUG,
+                "Successfully established a VISA connection with %s after %.2f seconds",
+                self._resource_expression,
+                total_time,
+            )
+        else:
+            _logger.warning(
+                "Unable to establish a VISA connection with %s after %.2f seconds",
+                self._resource_expression,
+                total_time,
+            )
 
         return visa_connection
 
@@ -816,20 +842,21 @@ def write(self, command: str, opc: bool = False, verbose: bool = True) -> None:
             Error: An error occurred while sending the command.
             SystemError: ``*OPC?`` did not return "1" after sending the command.
         """
-        if self._verbose and verbose:
-            if "\n" in command:
-                # Format any multiline command to print out with a single timestamp
-                # followed by as many (whitespace padded) f">>  {cmd}" lines as it has
-                commands_iter = iter(repr(command.strip()).split("\\n"))
-                spaces = " " * len(
-                    print_with_timestamp(
-                        f"({self._name_and_alias}) Write >>  {next(commands_iter)}"
-                    ).split(">>  ")[0]
-                )
-                print(*[f"{spaces}>>  {cmd}" for cmd in commands_iter], sep="\n")
-            else:
-                print_with_timestamp(f"({self._name_and_alias}) Write >>  {command!r}")
-
+        if "\n" in command:
+            _logger.log(
+                logging.INFO if self._verbose and verbose else logging.DEBUG,
+                "(%s) Write >>\n%s",
+                self._name_and_alias,
+                "\n".join(["    " + x for x in command.split("\n")]),
+            )
+        else:
+            _logger.log(
+                logging.INFO if self._verbose and verbose else logging.DEBUG,
+                "(%s) Write >>  %r",
+                self._name_and_alias,
+                command,
+            )
+        self._command_logger.debug(command)
         try:
             self._visa_resource.write(command)
         except (visa.VisaIOError, socket.error) as error:
@@ -852,9 +879,13 @@ def write_raw(self, command: bytes, verbose: bool = True) -> None:
         Raises:
             Error: An error occurred while sending the command.
         """
-        if self._verbose and verbose:
-            print_with_timestamp(f"({self._name_and_alias}) Write Raw >>  {command!r}")
-
+        _logger.log(
+            logging.INFO if self._verbose and verbose else logging.DEBUG,
+            "(%s) Write Raw >>  %r",
+            self._name_and_alias,
+            command,
+        )
+        self._command_logger.debug(command)
         try:
             self._visa_resource.write_raw(command)
         except (visa.VisaIOError, socket.error) as error:
@@ -918,12 +949,44 @@ def _close(self) -> None:
         try:
             self._visa_resource.close()
         except VisaIOError as error:
-            warnings.warn(
-                f"Error encountered while closing the visa resource:\n{error}", stacklevel=2
-            )
+            error_msg = f"Error encountered while closing the visa resource:\n{error}"
+            warnings.warn(error_msg, stacklevel=2)
+            _logger.exception(error_msg)
         self._visa_resource = None  # pyright: ignore[reportAttributeAccessIssue]
         self._is_open = False
 
+    @cached_property
+    def _command_logger(self) -> logging.Logger:
+        """Create a logger that will be used to log commands sent via write/query-like methods."""
+        # Create the logger
+        command_logger = logging.getLogger(f"{self._config_entry.address}-visa_logger")
+        command_logger.setLevel(logging.DEBUG)
+        command_logger.propagate = False
+        command_logger.addHandler(logging.NullHandler())
+        with contextlib.suppress(IndexError, StopIteration):
+            # Get the top-level log filepath
+            main_log_file = Path(
+                next(
+                    x
+                    for x in logging.getLogger(PACKAGE_NAME).handlers
+                    if isinstance(x, logging.FileHandler)
+                ).baseFilename
+            )
+            # Create the handler with the filename based on the main log file
+            command_log_handler = logging.FileHandler(
+                main_log_file.as_posix().replace(
+                    main_log_file.suffix, f"_visa_commands_{self._config_entry.address}.log"
+                ),
+                mode="w",
+                encoding="utf-8",
+            )
+            # Create the formatter
+            command_log_formatter = logging.Formatter("%(message)s")
+            command_log_handler.setFormatter(command_log_formatter)
+            command_log_handler.setLevel(logging.DEBUG)
+            command_logger.addHandler(command_log_handler)
+        return command_logger
+
     def _open(self) -> bool:
         """Open necessary resources and components and return a boolean indicating success."""
         opened = True
diff --git a/src/tm_devices/driver_mixins/device_control/rest_api_control.py b/src/tm_devices/driver_mixins/device_control/rest_api_control.py
index d103a3d7..227639dd 100644
--- a/src/tm_devices/driver_mixins/device_control/rest_api_control.py
+++ b/src/tm_devices/driver_mixins/device_control/rest_api_control.py
@@ -1,6 +1,7 @@
 """Base REST Application Programming Interface (API) control class module."""
 
 import json
+import logging
 import time
 
 from abc import ABC, abstractmethod
@@ -14,11 +15,12 @@
 )
 from tm_devices.helpers import (
     DeviceConfigEntry,
-    print_with_timestamp,
     raise_failure,
     SupportedRequestTypes,
 )
 
+_logger: logging.Logger = logging.getLogger(__name__)
+
 
 class RESTAPIControl(_AbstractDeviceControl, ABC):
     """Base REST Application Programming Interface (API) control class.
@@ -41,7 +43,9 @@ def __init__(self, config_entry: DeviceConfigEntry, verbose: bool) -> None:
 
         Args:
             config_entry: A config entry object parsed by the DMConfigParser.
-            verbose: A boolean indicating if verbose output should be printed.
+            verbose: A boolean indicating if verbose output should be printed. If True,
+                communication printouts will be logged with a level of INFO. If False,
+                communication printouts will be logged with a level of DEBUG.
         """
         super().__init__(config_entry, verbose)
 
@@ -76,7 +80,6 @@ def delete(  # noqa: PLR0913
         allow_errors: bool = False,
         verify_ssl: bool = True,
         allow_redirects: bool = False,
-        verbose: bool = True,
     ) -> Tuple[bool, Union[Dict[str, Any], bytes], int, Optional[requests.RequestException]]:
         """Perform a DELETE request with the given url and headers.
 
@@ -90,7 +93,6 @@ def delete(  # noqa: PLR0913
             verify_ssl: A bool that indicates if the SSL certificate should be verified.
             allow_redirects: A bool that indicates if URL redirects should be allowed.
             allow_errors: A boolean indicating if errors are allowed.
-            verbose: Set this to False in order to disable printouts.
 
         Returns:
             A tuple containing a success boolean, the response data, status code, and any errors
@@ -105,7 +107,6 @@ def delete(  # noqa: PLR0913
             allow_errors=allow_errors,
             verify_ssl=verify_ssl,
             allow_redirects=allow_redirects,
-            verbose=verbose,
         )
 
     # pylint: disable=too-many-arguments
@@ -119,7 +120,6 @@ def get(  # noqa: PLR0913
         allow_errors: bool = False,
         verify_ssl: bool = True,
         allow_redirects: bool = False,
-        verbose: bool = True,
     ) -> Tuple[bool, Union[Dict[str, Any], bytes], int, Optional[requests.RequestException]]:
         """Perform a GET request with the given url and headers.
 
@@ -133,7 +133,6 @@ def get(  # noqa: PLR0913
             verify_ssl: A bool that indicates if the SSL certificate should be verified.
             allow_redirects: A bool that indicates if URL redirects should be allowed.
             allow_errors: A boolean indicating if errors are allowed.
-            verbose: Set this to False in order to disable printouts.
 
         Returns:
             A tuple containing a success boolean, the response data, status code, and any errors
@@ -148,7 +147,6 @@ def get(  # noqa: PLR0913
             allow_errors=allow_errors,
             verify_ssl=verify_ssl,
             allow_redirects=allow_redirects,
-            verbose=verbose,
         )
 
     # pylint: disable=too-many-arguments
@@ -164,7 +162,6 @@ def patch(  # noqa: PLR0913
         allow_errors: bool = False,
         verify_ssl: bool = True,
         allow_redirects: bool = False,
-        verbose: bool = True,
     ) -> Tuple[bool, Union[Dict[str, Any], bytes], int, Optional[requests.RequestException]]:
         """Perform a PATCH request with the given url and headers.
 
@@ -183,7 +180,6 @@ def patch(  # noqa: PLR0913
             verify_ssl: A bool that indicates if the SSL certificate should be verified.
             allow_redirects: A bool that indicates if URL redirects should be allowed.
             allow_errors: A boolean indicating if errors are allowed.
-            verbose: Set this to False in order to disable printouts.
 
         Returns:
             A tuple containing a success boolean, the response data, status code, and any errors
@@ -200,7 +196,6 @@ def patch(  # noqa: PLR0913
             allow_errors=allow_errors,
             verify_ssl=verify_ssl,
             allow_redirects=allow_redirects,
-            verbose=verbose,
         )
 
     # pylint: disable=too-many-arguments
@@ -216,7 +211,6 @@ def post(  # noqa: PLR0913
         allow_errors: bool = False,
         verify_ssl: bool = True,
         allow_redirects: bool = False,
-        verbose: bool = True,
     ) -> Tuple[bool, Union[Dict[str, Any], bytes], int, Optional[requests.RequestException]]:
         """Perform a POST request with the given url and headers.
 
@@ -235,7 +229,6 @@ def post(  # noqa: PLR0913
             verify_ssl: A bool that indicates if the SSL certificate should be verified.
             allow_redirects: A bool that indicates if URL redirects should be allowed.
             allow_errors: A boolean indicating if errors are allowed.
-            verbose: Set this to False in order to disable printouts.
 
         Returns:
             A tuple containing a success boolean, the response data, status code, and any errors
@@ -252,7 +245,6 @@ def post(  # noqa: PLR0913
             allow_errors=allow_errors,
             verify_ssl=verify_ssl,
             allow_redirects=allow_redirects,
-            verbose=verbose,
         )
 
     # pylint: disable=too-many-arguments
@@ -268,7 +260,6 @@ def put(  # noqa: PLR0913
         allow_errors: bool = False,
         verify_ssl: bool = True,
         allow_redirects: bool = False,
-        verbose: bool = True,
     ) -> Tuple[bool, Union[Dict[str, Any], bytes], int, Optional[requests.RequestException]]:
         """Perform a PUT request with the given url and headers.
 
@@ -287,7 +278,6 @@ def put(  # noqa: PLR0913
             verify_ssl: A bool that indicates if the SSL certificate should be verified.
             allow_redirects: A bool that indicates if URL redirects should be allowed.
             allow_errors: A boolean indicating if errors are allowed.
-            verbose: Set this to False in order to disable printouts.
 
         Returns:
             A tuple containing a success boolean, the response data, status code, and any errors
@@ -304,26 +294,27 @@ def put(  # noqa: PLR0913
             allow_errors=allow_errors,
             verify_ssl=verify_ssl,
             allow_redirects=allow_redirects,
-            verbose=verbose,
         )
 
-    def set_api_version(self, api_version: int, verbose: bool = True) -> None:
+    def set_api_version(self, api_version: int) -> None:
         """Set the API version used by the device.
 
         Args:
             api_version: The API version to use for this device
-            verbose: Set this to False in order to disable printouts.
         """
         self._api_url = self._base_url + self.API_VERSIONS[api_version]
-        if self._verbose and verbose:
-            print(f"{self._name_and_alias} API version set to: {api_version} ({self._api_url})")
+        _logger.debug(
+            "%s API version set to: %d (%s)",
+            self._name_and_alias,
+            api_version,
+            self._api_url,
+        )
 
     def wait_for_api_connection(
         self,
         wait_time: float,
         sleep_seconds: int = 5,
         accept_immediate_connection: bool = False,
-        verbose: bool = True,
     ) -> bool:
         """Wait for an API call to go through to the device.
 
@@ -334,7 +325,6 @@ def wait_for_api_connection(
             sleep_seconds: The number of seconds to sleep in between connection attempts.
             accept_immediate_connection: A boolean indicating if a connection on the
                                          first attempt is a valid connection.
-            verbose: Set this to False in order to disable printouts.
 
         Returns:
             A boolean indicating if an API connection was made within the given time limit.
@@ -344,8 +334,11 @@ def wait_for_api_connection(
         """
         attempt_num = 0
         api_connection = False
-        if verbose:
-            print_with_timestamp(f"Attempting to establish an API connection with {self._api_url}")
+        _logger.log(
+            logging.INFO if self._verbose else logging.DEBUG,
+            "Attempting to establish an API connection with %s",
+            self._api_url,
+        )
         start_time = time.perf_counter()
         while (time.perf_counter() - start_time) <= wait_time:
             if api_connection := self._check_api_connection():
@@ -364,17 +357,19 @@ def wait_for_api_connection(
         end_time = time.perf_counter()
         total_time = end_time - start_time
 
-        if verbose:
-            if api_connection:
-                print_with_timestamp(
-                    f"Successfully established an API connection with {self._api_url} "
-                    f"after {total_time:.2f} seconds"
-                )
-            else:
-                print_with_timestamp(
-                    f"Unable to establish an API connection with {self._api_url} "
-                    f"after {total_time:.2f} seconds"
-                )
+        if api_connection:
+            _logger.log(
+                logging.INFO if self._verbose else logging.DEBUG,
+                "Successfully established an API connection with %s after %.2f seconds",
+                self._api_url,
+                total_time,
+            )
+        else:
+            _logger.warning(
+                "Unable to establish an API connection with %s after %.2f seconds",
+                self._api_url,
+                total_time,
+            )
         return api_connection
 
     ################################################################################################
@@ -403,7 +398,6 @@ def _send_request(  # noqa: PLR0913,PLR0912,C901
         allow_errors: bool = False,
         verify_ssl: bool = True,
         allow_redirects: bool = False,
-        verbose: bool = True,
     ) -> Tuple[bool, Union[Dict[str, Any], bytes], int, Optional[requests.RequestException]]:
         """Perform a request with the given url and headers.
 
@@ -423,7 +417,6 @@ def _send_request(  # noqa: PLR0913,PLR0912,C901
             verify_ssl: A bool that indicates if the SSL certificate should be verified.
             allow_redirects: A bool that indicates if URL redirects should be allowed.
             allow_errors: A boolean indicating if errors are allowed.
-            verbose: Set this to False in order to disable printouts.
 
         Returns:
             A tuple containing a success boolean, the response data, status code, and any errors
@@ -441,13 +434,15 @@ def _send_request(  # noqa: PLR0913,PLR0912,C901
                 url = self._api_url + url
         response = cast(requests.Response, None)
         retval: Union[Dict[str, Any], bytes] = {}
-        if self._verbose and verbose:
-            print_with_timestamp(f"({self._name_and_alias}) {request_type.value} >> {url}", end="")
-            if headers:
-                print(f", {headers=}", end="")
-            if json_body:
-                print(f", {json_body=}", end="")
-            print()
+        _logger.log(
+            logging.INFO if self._verbose else logging.DEBUG,
+            "(%s) %s >> %s%s%s",
+            self._name_and_alias,
+            getattr(request_type, "value", request_type),
+            url,
+            f", {headers=}" if headers else "",
+            f", {json_body=}" if json_body else "",
+        )
         try:
             if request_type == SupportedRequestTypes.DELETE:
                 response = requests.delete(
@@ -503,13 +498,13 @@ def _send_request(  # noqa: PLR0913,PLR0912,C901
             else:
                 msg = f"{request_type} is an unsupported request type."
                 raise ValueError(msg)
-            if self._verbose and verbose:
-                print_with_timestamp(f"Response from {request_type.value} >>")
-                if not return_bytes:
-                    print(f"{json.dumps(response.json(), indent=2)}")
-                else:
-                    # Print .zip files as byte strings, response.content.decode() throws errors
-                    print(f"\n{response.text}")
+
+            _logger.log(
+                logging.INFO if self._verbose else logging.DEBUG,
+                "Response from %s >>\n%s",
+                request_type.value,
+                json.dumps(response.json(), indent=2) if not return_bytes else response.text,
+            )
             retval = response.content if return_bytes else response.json()
             # If the response was successful, no Exception will be raised
             response.raise_for_status()
diff --git a/src/tm_devices/driver_mixins/device_control/tsp_control.py b/src/tm_devices/driver_mixins/device_control/tsp_control.py
index 7cca6753..26ff0caa 100644
--- a/src/tm_devices/driver_mixins/device_control/tsp_control.py
+++ b/src/tm_devices/driver_mixins/device_control/tsp_control.py
@@ -2,7 +2,10 @@
 
 from __future__ import annotations
 
+import logging
+
 from abc import ABC
+from pathlib import Path
 from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union
 
 from tm_devices.driver_mixins.device_control.pi_control import PIControl
@@ -13,6 +16,9 @@
     import os
 
 
+_logger: logging.Logger = logging.getLogger(__name__)
+
+
 class TSPControl(PIControl, ABC):
     """Base Test Script Processing (TSP) control class.
 
@@ -47,7 +53,9 @@ def ieee_cmds(self) -> TSPIEEE4882Commands:
     ################################################################################################
     # Public Methods
     ################################################################################################
-    def export_buffers(self, filepath: str, *args: str, sep: str = ",") -> None:
+    def export_buffers(
+        self, filepath: Union[str, os.PathLike[str]], *args: str, sep: str = ","
+    ) -> None:
         """Export one or more of the device's buffers to the given filepath.
 
         Args:
@@ -55,7 +63,7 @@ def export_buffers(self, filepath: str, *args: str, sep: str = ",") -> None:
             args: The buffer name(s) to export.
             sep: The delimiter used to separate data. Defaults to ",".
         """
-        with open(filepath, mode="w", encoding="utf-8") as file:
+        with Path(filepath).open(mode="w", encoding="utf-8") as file:
             buffer_data = self.get_buffers(*args)
             column_length = max(len(x) for x in buffer_data.values())
             file.write(sep.join(buffer_data) + "\n")
@@ -132,8 +140,7 @@ def load_script(
         """
         if file_path is not None:
             # script_body argument is overwritten by file contents
-            with open(file_path, encoding="utf-8") as script_tsp:
-                script_body = script_tsp.read().strip()
+            script_body = Path(file_path).read_text(encoding="utf-8").strip()
 
         # Check if the script exists, delete it if it does
         self.write(f"if {script_name} ~= nil then script.delete('{script_name}') end")
@@ -164,11 +171,18 @@ def print_buffers(self, *args: str) -> None:
         def fix_width(key: str, value: Any) -> str:  # Function to add spaces if needed
             return str(value) + " " * (column_widths[key] - len(str(value)))
 
-        print(*[fix_width(x, x) for x in buffer_data])
-        for index in range(column_length):
-            print(
-                *[fix_width(k, v[index] if index < len(v) else "") for k, v in buffer_data.items()]
-            )
+        buffer_headers = [fix_width(x, x) for x in buffer_data]
+        buffer_rows: List[List[Any]] = [
+            [fix_width(k, v[index] if index < len(v) else "") for k, v in buffer_data.items()]
+            for index in range(column_length)
+        ]
+        _logger.info(
+            "(%s) Printing Buffers %s >>\n%s\n%s",
+            self._name_and_alias,
+            buffer_headers,
+            " ".join(buffer_headers),
+            "\n".join(" ".join(row) for row in buffer_rows),
+        )
 
     def run_script(self, script_name: str) -> None:
         """Run a TSP script on the instrument.
diff --git a/src/tm_devices/driver_mixins/shared_implementations/_tektronix_pi_scope_mixin.py b/src/tm_devices/driver_mixins/shared_implementations/_tektronix_pi_scope_mixin.py
index 4b20b20f..17e24547 100644
--- a/src/tm_devices/driver_mixins/shared_implementations/_tektronix_pi_scope_mixin.py
+++ b/src/tm_devices/driver_mixins/shared_implementations/_tektronix_pi_scope_mixin.py
@@ -30,7 +30,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
 
         Returns:
             A tuple containing the current error code alongside a tuple of the current error
-            messages.
+                messages.
         """
         result = int(self.query("*ESR?").strip())
         allev_list = [
diff --git a/src/tm_devices/driver_mixins/shared_implementations/common_pi_system_error_check_mixin.py b/src/tm_devices/driver_mixins/shared_implementations/common_pi_system_error_check_mixin.py
index cd153154..40874c95 100644
--- a/src/tm_devices/driver_mixins/shared_implementations/common_pi_system_error_check_mixin.py
+++ b/src/tm_devices/driver_mixins/shared_implementations/common_pi_system_error_check_mixin.py
@@ -30,7 +30,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
 
         Returns:
             A tuple containing the current error code alongside a tuple of the current error
-            messages.
+                messages.
         """
         result = int(self.query("*ESR?").strip())
 
diff --git a/src/tm_devices/driver_mixins/shared_implementations/common_tsp_error_check_mixin.py b/src/tm_devices/driver_mixins/shared_implementations/common_tsp_error_check_mixin.py
index 6ff784d7..184fdea0 100644
--- a/src/tm_devices/driver_mixins/shared_implementations/common_tsp_error_check_mixin.py
+++ b/src/tm_devices/driver_mixins/shared_implementations/common_tsp_error_check_mixin.py
@@ -25,7 +25,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
 
         Returns:
             A tuple containing the current error code alongside a tuple of the current error
-            messages.
+                messages.
         """
         # instrument returns exponential numbers so converting to float before int
         error_code = int(float(self.query("print(status.standard.event)")))
diff --git a/src/tm_devices/drivers/data_acquisition_systems/daq6510.py b/src/tm_devices/drivers/data_acquisition_systems/daq6510.py
index 827b7cfb..ca4d0c64 100644
--- a/src/tm_devices/drivers/data_acquisition_systems/daq6510.py
+++ b/src/tm_devices/drivers/data_acquisition_systems/daq6510.py
@@ -55,7 +55,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
 
         Returns:
             A tuple containing the current error code alongside a tuple of the current error
-            messages.
+                messages.
         """
         # instrument returns exponential numbers so converting to float before int
         error_code = int(float(self.query("print(status.standard.event)")))
diff --git a/src/tm_devices/drivers/device.py b/src/tm_devices/drivers/device.py
index 20f9f326..71658a81 100644
--- a/src/tm_devices/drivers/device.py
+++ b/src/tm_devices/drivers/device.py
@@ -1,6 +1,7 @@
 """Base device driver module."""
 
 import concurrent.futures
+import logging
 import socket
 import time
 
@@ -29,13 +30,14 @@
     check_port_connection,
     ConnectionTypes,
     DeviceConfigEntry,
-    print_with_timestamp,
 )
 from tm_devices.helpers import ReadOnlyCachedProperty as cached_property  # noqa: N813
 
 _T = TypeVar("_T")
 _FAMILY_BASE_CLASS_PROPERTY_NAME = "_product_family_base_class"
 
+_logger: logging.Logger = logging.getLogger(__name__)
+
 
 def family_base_class(cls: _T) -> _T:
     """A decorator indicating a class is the top level of a unique product family tree.
@@ -71,7 +73,9 @@ def __init__(self, config_entry: DeviceConfigEntry, verbose: bool) -> None:
 
         Args:
             config_entry: A config entry object parsed by the DMConfigParser.
-            verbose: A boolean indicating if verbose output should be printed.
+            verbose: A boolean indicating if verbose output should be printed. If True,
+                communication printouts will be logged with a level of INFO. If False,
+                communication printouts will be logged with a level of DEBUG.
         """
         super().__init__(config_entry, verbose)
         self._is_open = True
@@ -146,7 +150,7 @@ def _open(self) -> bool:
     def _reboot(self) -> None:
         """Perform the actual rebooting code."""
         raise NotImplementedError(
-            f"``._reboot()`` is not yet implemented for the {self.__class__.__name__} driver"
+            f"``._reboot()`` is not yet implemented for the {self.__class__.__name__} driver",  # noqa: EM102
         )
 
     ################################################################################################
@@ -177,7 +181,7 @@ def alias(self) -> str:
     def command_argument_constants(self) -> Any:
         """Return the device command argument constants."""
         raise NotImplementedError(
-            f"The {self.__class__.__name__} driver does not have a Python API for its commands yet."
+            f"The {self.__class__.__name__} driver does not have a Python API for its commands yet."  # noqa: EM102
         )
 
     @property
@@ -212,7 +216,7 @@ def command_verification_enabled(self) -> bool:
     def commands(self) -> Any:
         """Return the device commands."""
         raise NotImplementedError(
-            f"The {self.__class__.__name__} driver does not have a Python API for its commands yet."
+            f"The {self.__class__.__name__} driver does not have a Python API for its commands yet."  # noqa: EM102
         )
 
     @cached_property
@@ -362,27 +366,20 @@ def temporary_verbose(self, temporary_verbose: bool) -> Generator[None, None, No
     # Public Methods
     ################################################################################################
     @final
-    def check_network_connection(self, verbose: bool = True) -> Tuple[bool, str]:
+    def check_network_connection(self) -> Tuple[bool, str]:
         """Check the network connection to the device using an external ping command.
 
         Wrapper function for
         [`check_network_connection`][tm_devices.helpers.check_network_connection].
 
-        Args:
-            verbose: Set this to False in order to disable printouts.
-
         Returns:
             A tuple containing a boolean indicating if there is a network connection and
-            a string with the result of the ping command.
+                a string with the result of the ping command.
         """
-        return check_network_connection(
-            self._name_and_alias, self.ip_address, verbose=verbose and self._verbose
-        )
+        return check_network_connection(self._name_and_alias, self.ip_address)
 
     @final
-    def check_port_connection(
-        self, port: int, timeout_seconds: int = 5, verbose: bool = True
-    ) -> bool:
+    def check_port_connection(self, port: int, timeout_seconds: int = 5) -> bool:
         """Check if the given port is open on the device.
 
         Wrapper function for [`check_port_connection`][tm_devices.helpers.check_port_connection].
@@ -390,7 +387,6 @@ def check_port_connection(
         Args:
             port: The port to check.
             timeout_seconds: The number of seconds to use as the socket timeout.
-            verbose: Set this to False in order to disable printouts.
 
         Returns:
             A boolean indicating if the port is open.
@@ -400,7 +396,6 @@ def check_port_connection(
             self.ip_address,
             port,
             timeout_seconds=timeout_seconds,
-            verbose=verbose and self._verbose,
         )
 
     @final
@@ -411,14 +406,14 @@ def cleanup(self, verbose: bool = True) -> None:
             verbose: Set this to False in order to disable printouts.
         """
         with self.temporary_verbose(verbose):
-            print(f"Beginning Device Cleanup on {self._name_and_alias}")
+            _logger.info("Beginning Device Cleanup on %s", self._name_and_alias)
             self._cleanup()
-            print(f"Finished Device Cleanup on {self._name_and_alias}")
+            _logger.info("Finished Device Cleanup on %s", self._name_and_alias)
 
     @final
     def close(self) -> None:
         """Close this device and all its used resources and components."""
-        print_with_timestamp(f"Closing Connection to {self._name_and_alias}")
+        _logger.info("Closing Connection to %s", self._name_and_alias)
         self._close()
 
     @final
@@ -430,7 +425,7 @@ def get_errors(self) -> Tuple[int, Tuple[str, ...]]:
 
         Returns:
             A tuple containing the current error code alongside a tuple of the current error
-            messages.
+                messages.
         """
         return self._get_errors()
 
@@ -443,7 +438,7 @@ def has_errors(self) -> bool:
 
         Returns:
             A boolean indicating if any errors were found in the device. True means there were
-            errors, False means no errors were found.
+                errors, False means no errors were found.
         """
         return bool(self.get_errors()[0])
 
@@ -467,21 +462,19 @@ def reboot(self, quiet_period: int = 0) -> bool:
                     self.__delattr__(prop)  # pylint: disable=unnecessary-dunder-call
 
         # Reboot the device
-        print_with_timestamp(f"Rebooting {self._name_and_alias}")
+        _logger.info("Rebooting %s", self._name_and_alias)
         self._reboot()
         self.close()
         # Depending on the instrument model, shutdown time or reboot time can take longer
         if quiet_period or self._reboot_quiet_period:
             sleep_time = max(self._reboot_quiet_period, quiet_period)
-            print_with_timestamp(f"Waiting for {sleep_time} seconds")
+            _logger.info("Waiting for %d seconds", sleep_time)
             time.sleep(sleep_time)
-        print_with_timestamp(f"Reopening Connection to {self._name_and_alias}")
+        _logger.info("Reopening Connection to %s", self._name_and_alias)
         if rebooted := self._open():
-            print_with_timestamp(f"Connection Reestablished with {self._name_and_alias}")
+            _logger.info("Connection Reestablished with %s", self._name_and_alias)
         else:
-            print_with_timestamp(
-                f"Failed to reestablish the connection with {self._name_and_alias}"
-            )
+            _logger.error("Failed to reestablish the connection with %s", self._name_and_alias)
         return rebooted
 
     @final
@@ -490,7 +483,6 @@ def wait_for_network_connection(
         wait_time: float,
         sleep_seconds: int = 2,
         accept_immediate_connection: bool = False,
-        verbose: bool = True,
     ) -> bool:
         """Wait for a network connection to the device.
 
@@ -499,7 +491,6 @@ def wait_for_network_connection(
             sleep_seconds: The number of seconds to sleep in between connection attempts.
             accept_immediate_connection: A boolean indicating if a connection on the
                                          first attempt is a valid connection.
-            verbose: Set this to False in order to disable printouts.
 
         Returns:
             A boolean indicating if a network connection was made within the given time limit.
@@ -509,13 +500,14 @@ def wait_for_network_connection(
         """
         attempt_num = 0
         network_connection = False
-        if verbose:
-            print_with_timestamp(
-                f"Attempting to establish a network connection with {self.ip_address}"
-            )
+        _logger.log(
+            logging.INFO if self._verbose else logging.DEBUG,
+            "Attempting to establish a network connection with %s",
+            self.ip_address,
+        )
         start_time = time.perf_counter()
         while (time.perf_counter() - start_time) <= wait_time:
-            if network_connection := self.check_network_connection(verbose=False)[0]:
+            if network_connection := self.check_network_connection()[0]:
                 # pylint: disable=compare-to-zero
                 if not (attempt_num == 0 and not accept_immediate_connection):
                     break
@@ -531,17 +523,19 @@ def wait_for_network_connection(
         end_time = time.perf_counter()
         total_time = end_time - start_time
 
-        if verbose:
-            if network_connection:
-                print_with_timestamp(
-                    f"Successfully established a network connection with {self.ip_address} "
-                    f"after {total_time:.2f} seconds"
-                )
-            else:
-                print_with_timestamp(
-                    f"Unable to establish a network connection with {self.ip_address} "
-                    f"after {total_time:.2f} seconds"
-                )
+        if network_connection:
+            _logger.log(
+                logging.INFO if self._verbose else logging.DEBUG,
+                "Successfully established a network connection with %s after %.2f seconds",
+                self.ip_address,
+                total_time,
+            )
+        else:
+            _logger.warning(
+                "Unable to establish a network connection with %s after %.2f seconds",
+                self.ip_address,
+                total_time,
+            )
         return network_connection
 
     def wait_for_port_connection(
@@ -550,7 +544,6 @@ def wait_for_port_connection(
         wait_time: float,
         sleep_seconds: int = 5,
         accept_immediate_connection: bool = False,
-        verbose: bool = True,
     ) -> bool:
         """Wait for a connection to be made to the given port on the device.
 
@@ -560,7 +553,6 @@ def wait_for_port_connection(
             sleep_seconds: The number of seconds to sleep in between connection attempts.
             accept_immediate_connection: A boolean indicating if a connection on the
                                          first attempt is a valid connection.
-            verbose: Set this to False in order to disable printouts.
 
         Returns:
             A boolean indicating if a connection was made to the port within the given time limit.
@@ -570,16 +562,15 @@ def wait_for_port_connection(
         """
         attempt_num = 0
         port_connection = False
-        if verbose:
-            print_with_timestamp(
-                f"Attempting to establish a connection to port {port} on {self.ip_address}"
-            )
+        _logger.log(
+            logging.INFO if self._verbose else logging.DEBUG,
+            "Attempting to establish a connection to port %d on %s",
+            port,
+            self.ip_address,
+        )
         start_time = time.perf_counter()
         while (time.perf_counter() - start_time) <= wait_time:
-            port_connection = self.check_port_connection(
-                port, timeout_seconds=sleep_seconds, verbose=False
-            )
-            if port_connection:
+            if port_connection := self.check_port_connection(port, timeout_seconds=sleep_seconds):
                 # pylint: disable=compare-to-zero
                 if not (attempt_num == 0 and not accept_immediate_connection):
                     break
@@ -595,17 +586,21 @@ def wait_for_port_connection(
         end_time = time.perf_counter()
         total_time = end_time - start_time
 
-        if verbose:
-            if port_connection:
-                print_with_timestamp(
-                    f"Successfully established a connection to port {port} on {self.ip_address} "
-                    f"after {total_time:.2f} seconds"
-                )
-            else:
-                print_with_timestamp(
-                    f"Unable to establish a connection to port {port} on {self.ip_address} "
-                    f"after {total_time:.2f} seconds"
-                )
+        if port_connection:
+            _logger.log(
+                logging.INFO if self._verbose else logging.DEBUG,
+                "Successfully established a connection to port %d on %s after %.2f seconds",
+                port,
+                self.ip_address,
+                total_time,
+            )
+        else:
+            _logger.warning(
+                "Unable to establish a connection to port %d on %s after %.2f seconds",
+                port,
+                self.ip_address,
+                total_time,
+            )
         return port_connection
 
     ################################################################################################
diff --git a/src/tm_devices/drivers/digital_multimeters/dmm6500.py b/src/tm_devices/drivers/digital_multimeters/dmm6500.py
index 16f1acef..4338ad2e 100644
--- a/src/tm_devices/drivers/digital_multimeters/dmm6500.py
+++ b/src/tm_devices/drivers/digital_multimeters/dmm6500.py
@@ -48,7 +48,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
 
         Returns:
             A tuple containing the current error code alongside a tuple of the current error
-            messages.
+                messages.
         """
         # instrument returns exponential numbers so converting to float before int
         error_code = int(float(self.query("print(status.standard.event)")))
diff --git a/src/tm_devices/drivers/digital_multimeters/dmm75xx/dmm75xx.py b/src/tm_devices/drivers/digital_multimeters/dmm75xx/dmm75xx.py
index 9d250751..f1cdb0ca 100644
--- a/src/tm_devices/drivers/digital_multimeters/dmm75xx/dmm75xx.py
+++ b/src/tm_devices/drivers/digital_multimeters/dmm75xx/dmm75xx.py
@@ -48,7 +48,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
 
         Returns:
             A tuple containing the current error code alongside a tuple of the current error
-            messages.
+                messages.
         """
         # instrument returns exponential numbers so converting to float before int
         error_code = int(float(self.query("print(status.standard.event)")))
diff --git a/src/tm_devices/drivers/scopes/tekscope/mso2.py b/src/tm_devices/drivers/scopes/tekscope/mso2.py
index adfe019d..cc108dc6 100644
--- a/src/tm_devices/drivers/scopes/tekscope/mso2.py
+++ b/src/tm_devices/drivers/scopes/tekscope/mso2.py
@@ -27,7 +27,9 @@ def __init__(
 
         Args:
             config_entry: A config entry object parsed by the DMConfigParser.
-            verbose: A boolean indicating if verbose output should be printed.
+            verbose: A boolean indicating if verbose output should be printed. If True,
+                communication printouts will be logged with a level of INFO. If False,
+                communication printouts will be logged with a level of DEBUG.
             visa_resource: The VISA resource object.
             default_visa_timeout: The default VISA timeout value in milliseconds.
         """
diff --git a/src/tm_devices/drivers/scopes/tekscope/tekscope.py b/src/tm_devices/drivers/scopes/tekscope/tekscope.py
index b3aeddaf..ecad6895 100644
--- a/src/tm_devices/drivers/scopes/tekscope/tekscope.py
+++ b/src/tm_devices/drivers/scopes/tekscope/tekscope.py
@@ -1,5 +1,9 @@
 """Base TekScope scope device driver module."""
 
+from __future__ import annotations
+
+import logging
+
 # pylint: disable=too-many-lines
 import math
 import os
@@ -10,20 +14,10 @@
 from dataclasses import dataclass
 from pathlib import Path
 from types import MappingProxyType
-from typing import Any, cast, Dict, List, Literal, Optional, Tuple, Type, Union
+from typing import Any, cast, Dict, List, Literal, Optional, Tuple, Type, TYPE_CHECKING, Union
 
 import pyvisa as visa
 
-from tm_devices.commands import (
-    LPD6Commands,
-    MSO2Commands,
-    MSO4Commands,
-    MSO5BCommands,
-    MSO5Commands,
-    MSO5LPCommands,
-    MSO6BCommands,
-    MSO6Commands,
-)
 from tm_devices.driver_mixins.abstract_device_functionality import (
     BaseAFGSourceChannel,
     BusMixin,
@@ -58,6 +52,20 @@
     SignalGeneratorOutputPathsBase,
 )
 
+if TYPE_CHECKING:
+    from tm_devices.commands import (
+        LPD6Commands,
+        MSO2Commands,
+        MSO4Commands,
+        MSO5BCommands,
+        MSO5Commands,
+        MSO5LPCommands,
+        MSO6BCommands,
+        MSO6Commands,
+    )
+
+_logger: logging.Logger = logging.getLogger(__name__)
+
 
 @dataclass(frozen=True)
 class TekScopeSourceDeviceConstants(SourceDeviceConstants):
@@ -121,7 +129,9 @@ def __init__(
 
         Args:
             config_entry: A config entry object parsed by the DMConfigParser.
-            verbose: A boolean indicating if verbose output should be printed.
+            verbose: A boolean indicating if verbose output should be printed. If True,
+                communication printouts will be logged with a level of INFO. If False,
+                communication printouts will be logged with a level of DEBUG.
             visa_resource: The VISA resource object.
             default_visa_timeout: The default VISA timeout value in milliseconds.
         """
@@ -321,7 +331,7 @@ def curve_query(  # noqa: PLR0912,C901
         self,
         channel_num: int,
         wfm_type: str = "TimeDomain",
-        output_csv_file: Optional[str] = None,
+        output_csv_file: Optional[Union[str, os.PathLike[str]]] = None,
     ) -> List[Any]:
         """Perform a curve query on a specific channel.
 
@@ -376,6 +386,7 @@ def curve_query(  # noqa: PLR0912,C901
                 break  # break out of loop
         if not found:
             warnings.warn(f"source not available for curve query: CH{channel_num}", stacklevel=2)
+            _logger.warning("source not available for curve query: CH%d", channel_num)
             return []
 
         self.set_and_check(":DATA:ENC", "ASCII")
@@ -390,7 +401,7 @@ def curve_query(  # noqa: PLR0912,C901
                 wfm_data.append([float(b) for b in frame.split(",")])  # noqa: PERF401
 
         if output_csv_file:
-            with open(output_csv_file, "w", encoding="UTF-8") as csv_file:
+            with Path(output_csv_file).open("w", encoding="UTF-8") as csv_file:
                 for frame in frames:
                     csv_file.write(frame)
                     csv_file.write(",")
diff --git a/src/tm_devices/drivers/scopes/tekscope/tekscopepc.py b/src/tm_devices/drivers/scopes/tekscope/tekscopepc.py
index 4d1554c7..d4b49811 100644
--- a/src/tm_devices/drivers/scopes/tekscope/tekscopepc.py
+++ b/src/tm_devices/drivers/scopes/tekscope/tekscopepc.py
@@ -1,5 +1,6 @@
 """TekScopePC device driver module."""
 
+import logging
 import warnings
 
 from tm_devices.commands import TekScopePCMixin
@@ -7,6 +8,8 @@
 from tm_devices.drivers.scopes.tekscope.tekscope import AbstractTekScope
 from tm_devices.helpers import ReadOnlyCachedProperty as cached_property  # noqa: N813
 
+_logger: logging.Logger = logging.getLogger(__name__)
+
 
 @family_base_class
 class TekScopePC(TekScopePCMixin, AbstractTekScope):  # pyright: ignore[reportIncompatibleVariableOverride]
@@ -31,8 +34,9 @@ def total_channels(self) -> int:
     ################################################################################################
     def _reboot(self) -> None:
         """Reboot the device."""
-        warnings.warn(
+        msg = (
             f"Rebooting is not supported for the {self.__class__.__name__} driver, "
-            f"{self._name_and_alias} will not be rebooted.",
-            stacklevel=3,
+            f"{self._name_and_alias} will not be rebooted."
         )
+        _logger.warning(msg)
+        warnings.warn(msg, stacklevel=3)
diff --git a/src/tm_devices/drivers/scopes/tekscope_2k/tekscope_2k.py b/src/tm_devices/drivers/scopes/tekscope_2k/tekscope_2k.py
index 82863616..8c2e53a2 100644
--- a/src/tm_devices/drivers/scopes/tekscope_2k/tekscope_2k.py
+++ b/src/tm_devices/drivers/scopes/tekscope_2k/tekscope_2k.py
@@ -1,9 +1,13 @@
 """Base TekScope2k scope device driver module."""
 
+from __future__ import annotations
+
+import logging
 import warnings
 
 from abc import ABC
-from typing import Any, List, Optional
+from pathlib import Path
+from typing import Any, List, Optional, TYPE_CHECKING, Union
 
 from tm_devices.driver_mixins.abstract_device_functionality.channel_control_mixin import (
     ChannelControlMixin,
@@ -16,6 +20,11 @@
 from tm_devices.drivers.scopes.scope import Scope
 from tm_devices.helpers import ReadOnlyCachedProperty as cached_property  # noqa: N813
 
+if TYPE_CHECKING:
+    import os
+
+_logger: logging.Logger = logging.getLogger(__name__)
+
 
 @family_base_class
 class TekScope2k(_TektronixPIScopeMixin, PIControl, Scope, ChannelControlMixin, ABC):
@@ -62,7 +71,7 @@ def curve_query(  # pylint: disable=too-many-locals
         self,
         channel_num: int,
         wfm_type: str = "TimeDomain",
-        output_csv_file: Optional[str] = None,
+        output_csv_file: Optional[Union[str, os.PathLike[str]]] = None,
     ) -> List[Any]:
         """Perform a curve query on a specific channel.
 
@@ -97,6 +106,7 @@ def curve_query(  # pylint: disable=too-many-locals
                 break  # break out of loop
         if not found:
             warnings.warn(f"source not available for curve query: CH{channel_num}", stacklevel=2)
+            _logger.warning("source not available for curve query: CH%d", channel_num)
             return []
 
         self.set_and_check(":DATA:ENC", "ASCI")
@@ -112,7 +122,7 @@ def curve_query(  # pylint: disable=too-many-locals
         wfm_data = [round(i, 3) for i in data]
 
         if output_csv_file:
-            with open(output_csv_file, "w", encoding="UTF-8") as csv_file:
+            with Path(output_csv_file).open("w", encoding="UTF-8") as csv_file:
                 for frame in wfm_data:
                     csv_file.write(str(frame))
                     csv_file.write(",")
diff --git a/src/tm_devices/drivers/scopes/tekscope_5k_7k_70k/tekscope_5k_7k_70k.py b/src/tm_devices/drivers/scopes/tekscope_5k_7k_70k/tekscope_5k_7k_70k.py
index c9c47264..3d0a0289 100644
--- a/src/tm_devices/drivers/scopes/tekscope_5k_7k_70k/tekscope_5k_7k_70k.py
+++ b/src/tm_devices/drivers/scopes/tekscope_5k_7k_70k/tekscope_5k_7k_70k.py
@@ -32,7 +32,9 @@ def __init__(
 
         Args:
             config_entry: A config entry object parsed by the DMConfigParser.
-            verbose: A boolean indicating if verbose output should be printed.
+            verbose: A boolean indicating if verbose output should be printed. If True,
+                communication printouts will be logged with a level of INFO. If False,
+                communication printouts will be logged with a level of DEBUG.
             visa_resource: The VISA resource object.
             default_visa_timeout: The default VISA timeout value in milliseconds.
         """
diff --git a/src/tm_devices/drivers/scopes/tso/tsovu.py b/src/tm_devices/drivers/scopes/tso/tsovu.py
index 01653ffe..0559c6ec 100644
--- a/src/tm_devices/drivers/scopes/tso/tsovu.py
+++ b/src/tm_devices/drivers/scopes/tso/tsovu.py
@@ -30,7 +30,9 @@ def __init__(
 
         Args:
             config_entry: A config entry object parsed by the DMConfigParser.
-            verbose: A boolean indicating if verbose output should be printed.
+            verbose: A boolean indicating if verbose output should be printed. If True,
+                communication printouts will be logged with a level of INFO. If False,
+                communication printouts will be logged with a level of DEBUG.
             visa_resource: The VISA resource object.
             default_visa_timeout: The default VISA timeout value in milliseconds.
         """
diff --git a/src/tm_devices/drivers/source_measure_units/smu24xx/smu24xx_interactive.py b/src/tm_devices/drivers/source_measure_units/smu24xx/smu24xx_interactive.py
index a4e58c1b..dd5eb015 100644
--- a/src/tm_devices/drivers/source_measure_units/smu24xx/smu24xx_interactive.py
+++ b/src/tm_devices/drivers/source_measure_units/smu24xx/smu24xx_interactive.py
@@ -54,7 +54,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
 
         Returns:
             A tuple containing the current error code alongside a tuple of the current error
-            messages.
+                messages.
         """
         # instrument returns exponential numbers so converting to float before int
         error_code = int(float(self.query("print(status.standard.event)")))
diff --git a/src/tm_devices/helpers/__init__.py b/src/tm_devices/helpers/__init__.py
index de918cb5..e733ae69 100644
--- a/src/tm_devices/helpers/__init__.py
+++ b/src/tm_devices/helpers/__init__.py
@@ -30,14 +30,13 @@
     create_visa_connection,
     detect_visa_resource_expression,
     get_model_series,
-    get_timestamp_string,
     get_version,
     get_visa_backend,
     ping_address,
-    print_with_timestamp,
     register_additional_usbtmc_mapping,
     sanitize_enum,
 )
+from tm_devices.helpers.logging import configure_logging, LoggingLevels
 from tm_devices.helpers.read_only_cached_property import ReadOnlyCachedProperty
 from tm_devices.helpers.singleton_metaclass import Singleton
 from tm_devices.helpers.standalone_functions import validate_address
@@ -51,18 +50,18 @@
     "check_network_connection",
     "check_port_connection",
     "check_visa_connection",
+    "configure_logging",
     "create_visa_connection",
     "detect_visa_resource_expression",
     "DeviceConfigEntry",
     "DeviceTypes",
     "DMConfigOptions",
     "get_model_series",
-    "get_timestamp_string",
     "get_version",
     "get_visa_backend",
+    "LoggingLevels",
     "PACKAGE_NAME",
     "ping_address",
-    "print_with_timestamp",
     "PYVISA_PY_BACKEND",
     "raise_error",
     "raise_failure",
diff --git a/src/tm_devices/helpers/constants_and_dataclasses.py b/src/tm_devices/helpers/constants_and_dataclasses.py
index edda8fe7..52feed37 100644
--- a/src/tm_devices/helpers/constants_and_dataclasses.py
+++ b/src/tm_devices/helpers/constants_and_dataclasses.py
@@ -18,6 +18,7 @@
     LoadImpedanceAFG,
     SupportedModels,
 )
+from tm_devices.helpers.logging import LoggingLevels
 from tm_devices.helpers.standalone_functions import validate_address
 
 
@@ -306,7 +307,7 @@ def __post_init__(self) -> None:  # noqa: PLR0912, C901
                     )
                 )
             ):
-                raise ValueError
+                raise ValueError  # noqa: TRY301
         except ValueError as exc:
             msg = f"""Found invalid value of `address={self.address}`
         Must provide an valid address format for the `connection_type={self.connection_type.value}':
@@ -449,6 +450,67 @@ class DMConfigOptions(AsDictionaryMixin):
     """
     check_for_updates: Optional[bool] = None
     """A flag indicating if a check for updates for the package should be performed on creation of the DeviceManager."""  # noqa: E501
+    log_console_level: Optional[str] = None
+    """The logging level to set for the console.
+
+    One of `["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NONE"]`, see the
+    [`LoggingLevels`][tm_devices.helpers.logging.LoggingLevels] enum for details.
+    Defaults to `"INFO"`. See the
+    [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more
+    information.
+    """
+    log_file_level: Optional[str] = None
+    """The logging level to set for the log file.
+
+    One of `["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NONE"]`, see the
+    [`LoggingLevels`][tm_devices.helpers.logging.LoggingLevels] enum for details.
+    Defaults to `"DEBUG"`. See the
+    [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more
+    information.
+    """
+    log_file_directory: Optional[str] = None
+    """The directory to save log files to.
+
+    Defaults to "./logs". See the
+    [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more
+    information and default values.
+    """
+    log_file_name: Optional[str] = None
+    """The name of the log file to save the logs to.
+
+    Defaults to a timestamped filename with the .log extension. See the
+    [`configure_logging()`][tm_devices.helpers.logging.configure_logging] function for more
+    information and default values.
+    """
+    log_colored_output: Optional[bool] = None
+    """Whether to use colored output from the `colorlog` package for the console.
+
+    Defaults to False. See the [`configure_logging()`][tm_devices.helpers.logging.configure_logging]
+    function for more information and default values.
+    """
+    log_pyvisa_messages: Optional[bool] = None
+    """Whether to include logs from the `pyvisa` package in the log file.
+
+    Defaults to False. See the [`configure_logging()`][tm_devices.helpers.logging.configure_logging]
+    function for more information and default values.
+    """
+
+    def __post_init__(self) -> None:
+        """Validate data after creation.
+
+        Raises:
+            ValueError: Indicates a bad input value.
+        """
+        for attribute in ("log_console_level", "log_file_level"):
+            try:
+                if (value := getattr(self, attribute)) is not None:
+                    _ = LoggingLevels(value)
+            except ValueError as error:  # noqa: PERF203
+                msg = (
+                    f'Invalid value for {attribute}: "{getattr(self, attribute)}". '
+                    f"Valid values are {LoggingLevels.list_values()}"
+                )
+                raise ValueError(msg) from error
 
     def __str__(self) -> str:
         """Complete config entry line for an environment variable."""
diff --git a/src/tm_devices/helpers/functions.py b/src/tm_devices/helpers/functions.py
index f3f9859e..36cfc3ed 100644
--- a/src/tm_devices/helpers/functions.py
+++ b/src/tm_devices/helpers/functions.py
@@ -1,9 +1,9 @@
 """Module containing helpers for the `tm_devices` package."""
 
 import contextlib
-import datetime
 import importlib.metadata
 import json
+import logging
 import platform
 import re
 import shlex
@@ -18,7 +18,6 @@
 
 import requests
 
-from dateutil.tz import tzlocal
 from packaging.version import InvalidVersion, Version
 
 from tm_devices.helpers.constants_and_dataclasses import (
@@ -34,6 +33,8 @@
 from tm_devices.helpers.enums import CustomStrEnum, SupportedModels
 from tm_devices.helpers.standalone_functions import validate_address
 
+_logger: logging.Logger = logging.getLogger(__name__)
+
 with warnings.catch_warnings():
     warnings.simplefilter("ignore", UserWarning)
     import pyvisa as visa
@@ -186,54 +187,56 @@ def check_for_update(package_name: str = PACKAGE_NAME, index_name: str = "pypi")
         latest_version = version_list[0]
 
         if installed_version != latest_version:
-            print(
-                f"\n\n\033[91mVersion {latest_version} of "
-                f"{package_name} is available on {index_name}.org.\n"
-                f"Version {installed_version} of "
-                f"{package_name} is currently installed.\n\n"
-                f"To upgrade {package_name} run the following command: "
-                f"python -m pip install -U {package_name}\n\n\033[0m"
+            _logger.debug(
+                "\n\n\033[91mVersion %(latest_version)s of "
+                "%(package_name)s is available on %(index_name)s.org.\n"
+                "Version %(installed_version)s of "
+                "%(package_name)s is currently installed.\n\n"
+                "To upgrade %(package_name)s run the following command: "
+                "python -m pip install -U %(package_name)s\n\n\033[0m",
+                {
+                    "latest_version": latest_version,
+                    "package_name": package_name,
+                    "index_name": index_name,
+                    "installed_version": installed_version,
+                },
             )
     except ModuleNotFoundError:
-        print(
-            f"\n\n\033[91m{package_name} is not installed, "
-            f"unable to check for updates.\n\n\033[0m"
+        _logger.warning(
+            "\n\n\033[91m%s is not installed, unable to check for updates.\n\n\033[0m",
+            package_name,
         )
     except (IndexError, ValueError):
-        print(
-            f"\n\n\033[91m{package_name} is not available on {index_name}.org, "
-            f"unable to check for updates.\n\n\033[0m"
+        _logger.warning(
+            "\n\n\033[91m%s is not available on %s.org, unable to check for updates.\n\n\033[0m",
+            package_name,
+            index_name,
         )
 
 
-def check_network_connection(
-    device_name: str, ip_address: str, verbose: bool = True
-) -> Tuple[bool, str]:
+def check_network_connection(device_name: str, ip_address: str) -> Tuple[bool, str]:
     """Check the network connection to the device using the external ping command.
 
     Args:
         device_name: The name of the device.
         ip_address: The ip address of the device.
-        verbose: Set this to False in order to disable printouts.
 
     Returns:
         A boolean indicating if there is a network connection and
-        a string with the result of the ping command.
+            a string with the result of the ping command.
     """
-    if verbose:
-        print_with_timestamp(f"({device_name}) ping >> {ip_address}")
-        print(f"{get_timestamp_string()} - ")
+    _logger.debug("(%s) ping >> %s", device_name, ip_address)
     ping_result = ping_address(ip_address)
-    if verbose:
-        print_with_timestamp("Response from ping >>")
-        for line in ping_result.strip().split("\n"):
-            print(f"    {line}")
+    _logger.debug(
+        "Response from ping >>\n%s",
+        "\n".join(["    " + x for x in ping_result.strip().split("\n")]),
+    )
 
     return "ttl=" in ping_result.lower(), ping_result
 
 
 def check_port_connection(
-    device_name: str, ip_address: str, port: int, timeout_seconds: int = 5, verbose: bool = True
+    device_name: str, ip_address: str, port: int, timeout_seconds: int = 5
 ) -> bool:
     """Check if the given port is open on the device.
 
@@ -242,13 +245,11 @@ def check_port_connection(
         ip_address: The ip address.
         port: The port to check.
         timeout_seconds: The number of seconds to use as the socket timeout.
-        verbose: Set this to False in order to disable printouts.
 
     Returns:
         A boolean indicating if the port is open.
     """
-    if verbose:
-        print_with_timestamp(f"({device_name}) >> checking if port {port} is open on {ip_address}")
+    _logger.debug("(%s) >> checking if port %d is open on %s", device_name, port, ip_address)
 
     with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as temp_socket:
         try:
@@ -259,13 +260,12 @@ def check_port_connection(
         except (socket.error, socket.gaierror, socket.herror):
             port_open = False
 
-    if verbose:
-        print_with_timestamp(f"Result >> {port_open}")
+    _logger.debug("(%s) port %d open = %s", device_name, port, port_open)
     return port_open
 
 
 def check_visa_connection(
-    config_entry: DeviceConfigEntry, visa_library: str, device_name: str, verbose: bool = True
+    config_entry: DeviceConfigEntry, visa_library: str, device_name: str
 ) -> bool:
     """Check if a VISA connection can be made to the device.
 
@@ -273,16 +273,15 @@ def check_visa_connection(
         config_entry: The device listed in the config of the framework to check.
         visa_library: Indicates which visa library to use for the connection.
         device_name: The name of the device.
-        verbose: Set this to False in order to disable printouts.
 
     Returns:
         a boolean indicating if the visa connection was successful
     """
-    if verbose:
-        print_with_timestamp(
-            f"({device_name}) >> checking if a VISA connection can be made to "
-            f"{config_entry.get_visa_resource_expression()}"
-        )
+    _logger.debug(
+        "(%s) >> checking if a VISA connection can be made to %s",
+        device_name,
+        config_entry.get_visa_resource_expression(),
+    )
     try:
         # Create new VISA connection
         temp_resource = create_visa_connection(
@@ -294,8 +293,7 @@ def check_visa_connection(
     except ConnectionError:  # raised by the create_visa_connection() function
         visa_connected = False
 
-    if verbose:
-        print_with_timestamp(f"Result >> {visa_connected}")
+    _logger.debug("(%s) VISA connected = %s", device_name, visa_connected)
     return visa_connected
 
 
@@ -331,12 +329,15 @@ def create_visa_connection(
         if (
             visa_object.visalib.library_path.path == "py" and visa_library != PYVISA_PY_BACKEND
         ):  # pragma: no cover
-            warnings.warn(
+            warning_msg = (
                 "No VISA installation was detected, defaulting to using "
                 "PyVISA-py as the VISA backend.\n\n"
-                'Add the "STANDALONE" option to the configuration to silence this warning.',
-                stacklevel=4,
+                'Add the "STANDALONE" option to the configuration or set the '
+                "`DeviceManager.visa_library` attribute to a valid VISA library to "
+                "silence this warning."
             )
+            warnings.warn(warning_msg, stacklevel=4)
+            _logger.warning(warning_msg)
     # The broad except is because pyvisa_py can throw a base exception in the tcpip.py file
     except Exception as error_1:
         if not retry_connection:
@@ -466,15 +467,11 @@ def get_model_series(model: str) -> str:
     if not model_series:
         if model not in SupportedModels.list_values():
             warnings.warn(f'The "{model}" model is not supported by {PACKAGE_NAME}', stacklevel=2)
+            _logger.warning('The "%s" model is not supported by %s', model, PACKAGE_NAME)
         model_series = model
     return model_series
 
 
-def get_timestamp_string() -> str:
-    """Return a string containing the current timestamp."""
-    return str(datetime.datetime.now(tz=tzlocal()))[:-3]
-
-
 def get_version(version_string: str) -> Version:
     """Get a Version object from a string.
 
@@ -527,7 +524,7 @@ def get_visa_backend(visa_lib_path: str) -> str:
         if visa_lib_path == "py":
             visa_name = "PyVISA-py"
         else:
-            raise KeyError
+            raise KeyError  # noqa: TRY301
     except KeyError:
         found_visa = False
         for visa_type in visa_backends:
@@ -564,18 +561,6 @@ def ping_address(address: str, timeout: int = 2) -> str:
     return output.replace("\r\n", "\n")
 
 
-def print_with_timestamp(message: str, end: str = "\n") -> str:
-    """Print and return a string prepended with a timestamp.
-
-    Args:
-        message: The message to print.
-        end: The end of the line to print.
-    """
-    message = f"{get_timestamp_string()} - {message}"
-    print(message, end=end)
-    return message
-
-
 def register_additional_usbtmc_mapping(model_series: str, *, model_id: str, vendor_id: str) -> None:
     """Register USBTMC connection information for a device that doesn't have native `tm_devices` USBTMC support.
 
diff --git a/src/tm_devices/helpers/logging.py b/src/tm_devices/helpers/logging.py
new file mode 100644
index 00000000..c109a7b2
--- /dev/null
+++ b/src/tm_devices/helpers/logging.py
@@ -0,0 +1,158 @@
+# pyright: reportUnnecessaryTypeIgnoreComment=none
+"""Helpers for logging."""
+
+from __future__ import annotations
+
+import importlib.metadata
+import logging
+import sys
+import time
+
+from pathlib import Path
+from typing import Optional, TYPE_CHECKING, Union
+
+import colorlog
+import pyvisa
+
+from tzlocal import (
+    get_localzone,  # pyright: ignore[reportUnknownVariableType]
+)
+
+from tm_devices.helpers.enums import CustomStrEnum
+
+if TYPE_CHECKING:
+    import os
+
+_logger_initialized = False
+
+
+class _CustomFormatter(logging.Formatter):  # pragma: no cover
+    def format(self, record: logging.LogRecord) -> str:
+        # Add the package name to the log record
+        record.package_name = record.name.split(".", maxsplit=1)[0]
+        # Call the original format method
+        return super().format(record)
+
+
+class LoggingLevels(CustomStrEnum):
+    """A class holding the valid logging levels supported."""
+
+    DEBUG = "DEBUG"
+    """An enum member representing the DEBUG logging level."""
+    INFO = "INFO"
+    """An enum member representing the INFO logging level."""
+    WARNING = "WARNING"
+    """An enum member representing the WARNING logging level."""
+    ERROR = "ERROR"
+    """An enum member representing the ERROR logging level."""
+    CRITICAL = "CRITICAL"
+    """An enum member representing the CRITICAL logging level."""
+    NONE = "NONE"
+    """An enum member indicating no logging messages should be captured."""
+
+
+def configure_logging(
+    *,
+    log_console_level: Union[str, LoggingLevels] = LoggingLevels.INFO,
+    log_file_level: Union[str, LoggingLevels] = LoggingLevels.DEBUG,
+    log_file_directory: Optional[Union[str, os.PathLike[str], Path]] = None,
+    log_file_name: Optional[str] = None,
+    log_colored_output: bool = False,
+    log_pyvisa_messages: bool = False,
+) -> logging.Logger:
+    """Configure the logging for this package.
+
+    !!! note
+        After this function is called once, if it is called again, it will not perform any
+        additional configuration. It will simply return the base logger for the package. This means
+        that if logging is configured explicitly in Python code, then any configuration options set
+        in the config file or environment variables will be ignored.
+
+    Args:
+        log_console_level: The logging level to set for the console. Defaults to INFO. Set to
+            [`LoggingLevels.NONE`][tm_devices.helpers.logging.LoggingLevels.NONE] to disable all
+            console logging/printouts except for certain warnings and exceptions.
+        log_file_level: The logging level to set for the file. Defaults to DEBUG. Set to
+            [`LoggingLevels.NONE`][tm_devices.helpers.logging.LoggingLevels.NONE] to disable logging
+            to a file entirely.
+        log_file_directory: The directory to save log files to. Defaults to "./logs" in the
+            current working directory.
+        log_file_name: The name of the log file to save the logs to. Defaults to a timestamped name
+            with the .log extension.
+        log_colored_output: Whether to use colored output from the `colorlog` package for the
+            console. Defaults to False.
+        log_pyvisa_messages: Whether to include logs from the `pyvisa` package in the log file. The
+            logging level will match the `file_logging_level`. Defaults to False.
+
+    Returns:
+        The base logger for the package, this base logger can also be accessed using
+            `logging.getLogger(tm_devices.PACKAGE_NAME)`.
+    """
+    from tm_devices.helpers.constants_and_dataclasses import (  # pylint: disable=import-outside-toplevel
+        PACKAGE_NAME,
+    )
+
+    global _logger_initialized  # noqa: PLW0603
+
+    _logger: logging.Logger = logging.getLogger(PACKAGE_NAME)
+    if _logger_initialized:
+        # If the logger was previously initialized, just return it
+        return _logger
+    # Convert object types into enum values
+    log_console_level = LoggingLevels(log_console_level)
+    log_file_level = LoggingLevels(log_file_level)
+    # Set the logger level to the lowest level, the handlers will filter out specific levels
+    # based on user configuration
+    _logger.setLevel(logging.DEBUG)
+    _logger.addHandler(logging.NullHandler())
+    # The logger/module name is not included in the message, since formatting the messages to
+    # be aligned would cause the width of the message prefix to be almost 100 characters before
+    # the message is even added to the line.
+    logging_file_format_string = "[%(asctime)s] [%(package_name)10s] [%(levelname)8s] %(message)s"
+    logging_console_format_string = "%(asctime)s - %(message)s"
+    if not log_file_directory:  # pragma: no cover
+        log_file_directory = Path("./logs")
+    if not log_file_name:  # pragma: no cover
+        log_file_name = f"{PACKAGE_NAME}_{time.strftime('%m-%d-%Y_%H-%M-%S', time.localtime())}.log"
+    log_filepath = Path(log_file_directory) / log_file_name
+
+    if log_file_level != LoggingLevels.NONE:
+        # Set up logger for tm_devices
+        log_filepath.parent.mkdir(parents=True, exist_ok=True)
+        file_handler = logging.FileHandler(log_filepath, mode="w", encoding="utf-8")
+        file_formatter = _CustomFormatter(logging_file_format_string)
+        file_formatter.default_msec_format = "%s.%06d"  # Use 6 digits of precision for milliseconds
+
+        file_handler.setLevel(getattr(logging, log_file_level.value))
+        file_handler.setFormatter(file_formatter)
+        _logger.addHandler(file_handler)
+
+        if log_pyvisa_messages:
+            # Hook into pyvisa's logging to save it into the same file
+            pyvisa.logger.setLevel(getattr(logging, log_file_level.value))
+            pyvisa.logger.addHandler(file_handler)
+
+    # Log a few things to just the file
+    _logger.debug("timezone==%s", get_localzone())  # pyright: ignore[reportUnknownArgumentType,reportUnnecessaryTypeIgnoreComment]
+    _logger.debug("%s==%s", PACKAGE_NAME, importlib.metadata.version(PACKAGE_NAME))
+
+    if log_console_level != LoggingLevels.NONE:
+        if log_colored_output:
+            console_handler = colorlog.StreamHandler(stream=sys.stdout)
+            console_formatter = colorlog.ColoredFormatter(
+                "%(log_color)s" + logging_console_format_string,
+                log_colors=colorlog.default_log_colors,
+            )
+        else:
+            console_handler = logging.StreamHandler(stream=sys.stdout)
+            console_formatter = logging.Formatter(logging_console_format_string)
+        console_formatter.default_msec_format = (
+            "%s.%06d"  # Use 6 digits of precision for milliseconds
+        )
+
+        console_handler.setLevel(getattr(logging, log_console_level.value))
+        console_handler.setFormatter(console_formatter)
+        _logger.addHandler(console_handler)
+
+    _logger_initialized = True
+    return _logger
diff --git a/src/tm_devices/helpers/singleton_metaclass.py b/src/tm_devices/helpers/singleton_metaclass.py
index aa2c224b..c5dad32e 100644
--- a/src/tm_devices/helpers/singleton_metaclass.py
+++ b/src/tm_devices/helpers/singleton_metaclass.py
@@ -1,10 +1,13 @@
 """A Module containing a metaclass that converts any class into a Singleton."""
 
+import logging
 import warnings
 
 from typing import Any, MutableMapping
 from weakref import WeakValueDictionary
 
+_logger: logging.Logger = logging.getLogger(__name__)
+
 
 class Singleton(type):
     """A metaclass that converts a standard class into a Singleton (one instance).
@@ -23,10 +26,11 @@ def __call__(cls, *args: Any, **kwargs: Any) -> Any:
             new_instance = super(Singleton, cls).__call__(*args, **kwargs)  # noqa: UP008
             cls._class_instances[cls] = new_instance
         else:
-            warnings.warn(
+            msg = (
                 f"The {cls.__name__} has already been created and is not "
                 f"allowed to be instantiated twice. Previously created instance "
-                f"will be used instead.\n",
-                stacklevel=3,
+                f"will be used instead."
             )
+            warnings.warn(msg, stacklevel=3)
+            _logger.warning(msg)
         return cls._class_instances[cls]
diff --git a/src/tm_devices/helpers/stubgen.py b/src/tm_devices/helpers/stubgen.py
index 3c7591f6..ecd7f1ef 100644
--- a/src/tm_devices/helpers/stubgen.py
+++ b/src/tm_devices/helpers/stubgen.py
@@ -18,7 +18,7 @@ def _get_data_type(data_object: Any) -> str:
     """
     try:
         if "." in str(data_object):
-            raise AttributeError
+            raise AttributeError  # noqa: TRY301
         data_type = str(data_object.__name__) if data_object else str(data_object)
     except AttributeError:
         data_type = str(
@@ -46,15 +46,14 @@ def add_info_to_stub(cls: Any, method: Any, is_property: bool = False) -> None:
     """
     if stub_dir := os.getenv("TM_DEVICES_STUB_DIR"):
         method_filepath = inspect.getfile(cls)
-        stub_dir = str(
-            Path(stub_dir) / "tm_devices" if not stub_dir.endswith("tm_devices") else stub_dir
+        stub_dir_path = Path(stub_dir) / (
+            "tm_devices" if not stub_dir.endswith("tm_devices") else stub_dir
         )
-        method_filepath = str(
-            Path(stub_dir)
-            / method_filepath.rsplit("tm_devices", maxsplit=1)[-1].lstrip(os.path.sep)
+        # stub files have the .pyi extension
+        method_path_obj = stub_dir_path / (
+            method_filepath.rsplit("tm_devices", maxsplit=1)[-1].lstrip(os.path.sep) + "i"
         )
-        method_filepath += "i"  # stub files have the .pyi extension
-        if not os.path.exists(method_filepath):  # noqa: PTH110
+        if not method_path_obj.exists():
             msg = (
                 f'The stub file "{method_filepath}" must already exist in order to use this '
                 f"functionality to add method stubs."
@@ -86,8 +85,7 @@ def add_info_to_stub(cls: Any, method: Any, is_property: bool = False) -> None:
             method_signature = "    @property\n" + method_signature
         method_stub_content = method_signature + "        " + '"""' + method.__doc__ + '"""' + "\n"
         # Read in the content of the stub file to avoid adding duplicate methods
-        with open(method_filepath, encoding="utf-8") as file_pointer:
-            contents = file_pointer.read()
+        contents = method_path_obj.read_text(encoding="utf-8")
         if f" def {method.__name__}(" not in contents:
             if typing_imports:
                 contents = f"from typing import {', '.join(typing_imports)}\n" + contents
@@ -105,5 +103,4 @@ def add_info_to_stub(cls: Any, method: Any, is_property: bool = False) -> None:
                 msg = f"Could not find the end of the {cls.__name__} class."
                 raise ValueError(msg)
 
-            with open(method_filepath, "w", encoding="utf-8") as file_pointer:
-                file_pointer.write(contents)
+            method_path_obj.write_text(contents, encoding="utf-8")
diff --git a/src/tm_devices/helpers/verification_functions.py b/src/tm_devices/helpers/verification_functions.py
index eaea9938..bcbe9f6f 100644
--- a/src/tm_devices/helpers/verification_functions.py
+++ b/src/tm_devices/helpers/verification_functions.py
@@ -4,8 +4,6 @@
 
 from typing import Tuple, Union
 
-from tm_devices.helpers.functions import get_timestamp_string
-
 
 def raise_error(unique_identifier: str, message: str, *, condense_printout: bool = True) -> None:
     """Raise an AssertionError with the provided message indicating there was an error.
@@ -18,12 +16,10 @@ def raise_error(unique_identifier: str, message: str, *, condense_printout: bool
     Raises:
         AssertionError: Prints out the error message with a traceback.
     """
-    # TODO: integrate this with logging
-    #  https://github.com/tektronix/tm_devices/issues/316
     if condense_printout:
         # Make the message smaller
         message = ", ".join([x.strip() for x in message.split("\n")])
-    message = f"{get_timestamp_string()} - ERROR: ({unique_identifier}) : {message}"
+    message = f"ERROR: ({unique_identifier}) : {message}"
     raise AssertionError(message)
 
 
@@ -38,12 +34,10 @@ def raise_failure(unique_identifier: str, message: str, *, condense_printout: bo
     Raises:
         AssertionError: Prints out the failure message with a traceback.
     """
-    # TODO: integrate this with logging
-    #  https://github.com/tektronix/tm_devices/issues/316
     if condense_printout:
         # Make the message smaller
         message = ", ".join([x.strip() for x in message.split("\n")])
-    message = f"{get_timestamp_string()} - FAILURE: ({unique_identifier}) : {message}"
+    message = f"FAILURE: ({unique_identifier}) : {message}"
     raise AssertionError(message)
 
 


Test patch:
diff --git a/src/tm_devices/drivers/margin_testers/margin_tester.py b/src/tm_devices/drivers/margin_testers/margin_tester.py
index 9cd84f6c..11fba7ad 100644
--- a/src/tm_devices/drivers/margin_testers/margin_tester.py
+++ b/src/tm_devices/drivers/margin_testers/margin_tester.py
@@ -1,12 +1,14 @@
 """Base Margin Tester device driver module."""
 
+from __future__ import annotations
+
 import os
 import time
 
 from abc import ABC, abstractmethod
-from typing import Any, Dict, Mapping, MutableMapping, Tuple
+from pathlib import Path
+from typing import Any, Dict, Mapping, MutableMapping, Tuple, TYPE_CHECKING, Union
 
-from packaging.version import Version
 from requests.structures import CaseInsensitiveDict
 
 from tm_devices.driver_mixins.device_control import RESTAPIControl
@@ -14,6 +16,9 @@
 from tm_devices.helpers import DeviceConfigEntry, DeviceTypes
 from tm_devices.helpers import ReadOnlyCachedProperty as cached_property  # noqa: N813
 
+if TYPE_CHECKING:
+    from packaging.version import Version
+
 
 @family_base_class
 class MarginTester(Device, RESTAPIControl, ABC):
@@ -27,11 +32,13 @@ def __init__(self, config_entry: DeviceConfigEntry, verbose: bool) -> None:
 
         Args:
             config_entry: A config entry object parsed by the DMConfigParser.
-            verbose: A boolean indicating if verbose output should be printed.
+            verbose: A boolean indicating if verbose output should be printed. If True,
+                communication printouts will be logged with a level of INFO. If False,
+                communication printouts will be logged with a level of DEBUG.
         """
         super().__init__(config_entry, verbose)
 
-        self._auth_token_file_path = ""
+        self._auth_token_file_path: Path = Path()
 
     ################################################################################################
     # Abstract Cached Properties
@@ -89,12 +96,12 @@ def device_type(self) -> str:
     @property
     def auth_token_file_path(self) -> str:
         """Return the path to the file containing the auth token."""
-        return self._auth_token_file_path
+        return self._auth_token_file_path.as_posix()
 
     @auth_token_file_path.setter
-    def auth_token_file_path(self, value: str) -> None:
+    def auth_token_file_path(self, value: Union[str, os.PathLike[str]]) -> None:
         """Set the path to the file containing the auth token."""
-        self._auth_token_file_path = value
+        self._auth_token_file_path = Path(value)
 
     ################################################################################################
     # Public Methods
@@ -117,7 +124,11 @@ def _generate_headers(self) -> Mapping[str, str]:
         Raises:
             AssertionError: Indicates that no auth token file is available.
         """
-        if not self._auth_token_file_path:
+        if not (
+            self._auth_token_file_path
+            and self._auth_token_file_path.exists()
+            and self._auth_token_file_path.is_file()
+        ):
             msg = (
                 "No auth token file set! Please set the ``.auth_token_file_path`` attribute "
                 "to point to a file with an authorization token."
@@ -125,8 +136,7 @@ def _generate_headers(self) -> Mapping[str, str]:
             raise AssertionError(msg)
         headers: MutableMapping[str, str] = CaseInsensitiveDict()
         headers["Accept"] = "application/json"
-        with open(self._auth_token_file_path, encoding="utf-8") as auth_file:
-            token = auth_file.read().replace("\n", "")
+        token = self._auth_token_file_path.read_text(encoding="utf-8").replace("\n", "")
         headers["Authorization"] = f"Bearer {token}"
         return headers
 
@@ -138,7 +148,7 @@ def _get_errors(self) -> Tuple[int, Tuple[str, ...]]:
 
         Returns:
             A tuple containing the current error code alongside a tuple of the current error
-            messages.
+                messages.
         """
         return 0, ()
 
@@ -148,12 +158,10 @@ def _open(self) -> bool:
         time.sleep(15)
         self._is_open = self.wait_for_network_connection(
             120,
-            verbose=False,
             accept_immediate_connection=bool(os.environ.get("TM_DEVICES_UNIT_TESTS_RUNNING")),
         )
         self._is_open &= self.wait_for_api_connection(
             120,
-            verbose=False,
             accept_immediate_connection=True,
         )
         return self._is_open
diff --git a/src/tm_devices/drivers/margin_testers/tmt4.py b/src/tm_devices/drivers/margin_testers/tmt4.py
index 719e4e46..bfb687ca 100644
--- a/src/tm_devices/drivers/margin_testers/tmt4.py
+++ b/src/tm_devices/drivers/margin_testers/tmt4.py
@@ -26,7 +26,9 @@ def __init__(self, config_entry: DeviceConfigEntry, verbose: bool) -> None:
 
         Args:
             config_entry: A config entry object parsed by the DMConfigParser.
-            verbose: A boolean indicating if verbose output should be printed.
+            verbose: A boolean indicating if verbose output should be printed. If True,
+                communication printouts will be logged with a level of INFO. If False,
+                communication printouts will be logged with a level of DEBUG.
 
         Notes:
             The default port is 5000, but device could be configured for any specific port.
@@ -110,11 +112,12 @@ def request_about_info(self, verbose: bool = True) -> Dict[str, Any]:
             A Dictionary with information about the Margin device.
         """
         # Make get request to about and version endpoints
-        _, res_json, _, _ = self.get("/device/about", verbose=verbose)
-        res_json = cast(Dict[str, Any], res_json)
-        _, res_json2, _, _ = self.get("/device/version", verbose=verbose)
-        res_json2 = cast(Dict[str, Any], res_json2)
-        res_json.update(res_json2)
+        with self.temporary_verbose(verbose):
+            _, res_json, _, _ = self.get("/device/about")
+            res_json = cast(Dict[str, Any], res_json)
+            _, res_json2, _, _ = self.get("/device/version")
+            res_json2 = cast(Dict[str, Any], res_json2)
+            res_json.update(res_json2)
         return res_json
 
     def wait_till_unlocked(self, timeout: int = 120) -> None:
@@ -144,8 +147,9 @@ def _check_api_connection(self) -> bool:
         Returns:
             A boolean indicating if the call was successful.
         """
-        # allow_errors is set to True since this is just checking if the API is reachable
-        return self.get("/device/about", allow_errors=True, verbose=False)[0]
+        with self.temporary_verbose(False):
+            # allow_errors is set to True since this is just checking if the API is reachable
+            return self.get("/device/about", allow_errors=True)[0]
 
     def _cleanup(self) -> None:
         """Perform the cleanup defined for the device."""
diff --git a/tests/conftest.py b/tests/conftest.py
index e6e04dd2..23d2eeb6 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,7 +1,9 @@
 """Pytest configuration file."""
 
+import logging
 import os
 import socket
+import sys
 
 from pathlib import Path
 from typing import Generator, List, Tuple
@@ -11,7 +13,7 @@
 import pyvisa.constants
 
 from mock_server import mocker_server, PORT
-from tm_devices import DeviceManager
+from tm_devices import configure_logging, DeviceManager, LoggingLevels
 from tm_devices.components import DMConfigParser
 from tm_devices.helpers import DMConfigOptions, validate_address
 
@@ -24,13 +26,38 @@
 UNIT_TEST_TIMEOUT = 50
 
 
+####################################################################################################
+# Configure the logging for the package that will run during unit tests
+class _DynamicStreamHandler(logging.StreamHandler):  # pyright: ignore[reportMissingTypeArgument]
+    def emit(self, record: logging.LogRecord) -> None:
+        self.stream = sys.stdout
+        super().emit(record)
+
+
+_logger = configure_logging(
+    log_console_level=LoggingLevels.NONE,
+    log_file_level=LoggingLevels.DEBUG,
+    log_file_directory=Path(__file__).parent / "logs",
+    log_file_name=f"unit_test_py{sys.version_info.major}{sys.version_info.minor}.log",
+)
+_unit_test_console_handler = _DynamicStreamHandler(stream=sys.stdout)
+_unit_test_console_handler.setLevel(logging.DEBUG)
+_unit_test_console_formatter = logging.Formatter("%(asctime)s - %(message)s")
+_unit_test_console_formatter.default_msec_format = (
+    "%s.%06d"  # Use 6 digits of precision for milliseconds
+)
+_unit_test_console_handler.setFormatter(_unit_test_console_formatter)
+_logger.addHandler(_unit_test_console_handler)
+####################################################################################################
+
+
 def mock_gethostbyname(address: str) -> str:
     """Mock the socket.gethostbyname function."""
     try:
         is_hostname = validate_address(address)
         if not is_hostname:  # pylint: disable=consider-using-assignment-expr
             return address
-        raise ValueError
+        raise ValueError  # noqa: TRY301
     except ValueError as error:
         raise socket.herror from error
 
@@ -41,7 +68,7 @@ def mock_gethostbyaddr(address: str) -> Tuple[str, List[str], List[str]]:
         is_hostname = validate_address(address)
         if is_hostname:  # pylint: disable=consider-using-assignment-expr
             return address, [], []
-        raise ValueError
+        raise ValueError  # noqa: TRY301
     except ValueError as error:
         raise socket.herror from error
 
@@ -58,9 +85,9 @@ def _auto_add_newline_to_test_start() -> (  # pyright: ignore [reportUnusedFunct
     Generator[None, None, None]
 ):
     """Automatically add a newline at the start of each test."""
-    print(f"\n{'#' * 90}\nExecuting {os.environ['PYTEST_CURRENT_TEST'].split(' ')[0]}\n")
+    print(f"\n{'#' * 90}\nExecuting {os.environ['PYTEST_CURRENT_TEST'].split(' ')[0]}\n")  # noqa: T201
     yield
-    print(f"\n\nFinished {os.environ['PYTEST_CURRENT_TEST'].split(' ')[0]}\n{'#' * 90}")
+    print(f"\n\nFinished {os.environ['PYTEST_CURRENT_TEST'].split(' ')[0]}\n{'#' * 90}")  # noqa: T201
 
 
 @pytest.fixture(name="device_manager", scope="session")
@@ -70,7 +97,7 @@ def fixture_device_manager() -> Generator[DeviceManager, None, None]:
     Yields:
         The DeviceManager instance.
     """
-    print()
+    print()  # noqa: T201
     with mock.patch(
         "socket.gethostbyname", mock.MagicMock(side_effect=mock_gethostbyname)
     ), mock.patch(
diff --git a/tests/samples/sample_devices.toml b/tests/samples/sample_devices.toml
index 7d931743..400a2784 100644
--- a/tests/samples/sample_devices.toml
+++ b/tests/samples/sample_devices.toml
@@ -29,6 +29,7 @@ end_input = "none"
 
 [options]
 default_visa_timeout = 1000
+log_file_level = "WARNING"
 setup_cleanup = false
 standalone = true
 teardown_cleanup = false
diff --git a/tests/samples/sample_devices.yaml b/tests/samples/sample_devices.yaml
index 9082a5fd..400ec99e 100644
--- a/tests/samples/sample_devices.yaml
+++ b/tests/samples/sample_devices.yaml
@@ -22,6 +22,7 @@ devices:
       end_input: none
 options:
   default_visa_timeout: 1000
+  log_file_level: WARNING
   setup_cleanup: false
   standalone: true
   teardown_cleanup: false
diff --git a/tests/samples/tsp_script.tsp b/tests/samples/tsp_script.tsp
index 60da9791..4a51ccc6 100644
--- a/tests/samples/tsp_script.tsp
+++ b/tests/samples/tsp_script.tsp
@@ -1,2 +1,2 @@
-"""Sample script used in test_smu.py."""
+-- Sample script used in test_smu.py.
 print("TEK")
diff --git a/tests/sim_devices/smu/smu2601b.yaml b/tests/sim_devices/smu/smu2601b.yaml
index e08146ff..241c9a55 100644
--- a/tests/sim_devices/smu/smu2601b.yaml
+++ b/tests/sim_devices/smu/smu2601b.yaml
@@ -7,8 +7,8 @@ devices:
         r: Keithley Instruments Inc., Model 2601B, 4498311, 3.3.5
       - q: print(available(gpib))
         r: 'true'
-      - q: loadscript loadfuncs\n"""Sample script used in test_smu.py."""\nprint("TEK")\nendscript
-      - q: loadscript tsp_function\n"""Sample script used in test_smu.py."""\nprint("TEK")\nendscript
+      - q: loadscript loadfuncs\n-- Sample script used in test_smu.py.\nprint("TEK")\nendscript
+      - q: loadscript tsp_function\n-- Sample script used in test_smu.py.\nprint("TEK")\nendscript
       - q: loadfuncs.save()
       - q: loadfuncs()
       - q: errorqueue.clear()
diff --git a/tests/sim_devices/smu/smu2601b_pulse.yaml b/tests/sim_devices/smu/smu2601b_pulse.yaml
index 8cfd52d2..9627689c 100644
--- a/tests/sim_devices/smu/smu2601b_pulse.yaml
+++ b/tests/sim_devices/smu/smu2601b_pulse.yaml
@@ -10,7 +10,7 @@ devices:
       - q: loadscript tsp_script
       - q: loadscript tsp_function
       - q: print("TEK")
-      - q: '"""Sample script used in test_smu.py."""'
+      - q: -- Sample script used in test_smu.py.
       - q: endscript
       - q: tsp_script.save()
       - q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2602b.yaml b/tests/sim_devices/smu/smu2602b.yaml
index 6a85a12d..3f29fc5e 100644
--- a/tests/sim_devices/smu/smu2602b.yaml
+++ b/tests/sim_devices/smu/smu2602b.yaml
@@ -10,7 +10,7 @@ devices:
       - q: loadscript tsp_script
       - q: loadscript tsp_function
       - q: print("TEK")
-      - q: '"""Sample script used in test_smu.py."""'
+      - q: -- Sample script used in test_smu.py.
       - q: endscript
       - q: tsp_script.save()
       - q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2604b.yaml b/tests/sim_devices/smu/smu2604b.yaml
index df0a1a25..6c167073 100644
--- a/tests/sim_devices/smu/smu2604b.yaml
+++ b/tests/sim_devices/smu/smu2604b.yaml
@@ -10,7 +10,7 @@ devices:
       - q: loadscript tsp_script
       - q: loadscript tsp_function
       - q: print("TEK")
-      - q: '"""Sample script used in test_smu.py."""'
+      - q: -- Sample script used in test_smu.py.
       - q: endscript
       - q: tsp_script.save()
       - q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2606b.yaml b/tests/sim_devices/smu/smu2606b.yaml
index b7958699..8624bc7c 100644
--- a/tests/sim_devices/smu/smu2606b.yaml
+++ b/tests/sim_devices/smu/smu2606b.yaml
@@ -10,7 +10,7 @@ devices:
       - q: loadscript tsp_script
       - q: loadscript tsp_function
       - q: print("TEK")
-      - q: '"""Sample script used in test_smu.py."""'
+      - q: -- Sample script used in test_smu.py.
       - q: endscript
       - q: tsp_script.save()
       - q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2611b.yaml b/tests/sim_devices/smu/smu2611b.yaml
index f7e01632..c0e17f00 100644
--- a/tests/sim_devices/smu/smu2611b.yaml
+++ b/tests/sim_devices/smu/smu2611b.yaml
@@ -10,7 +10,7 @@ devices:
       - q: loadscript tsp_script
       - q: loadscript tsp_function
       - q: print("TEK")
-      - q: '"""Sample script used in test_smu.py."""'
+      - q: -- Sample script used in test_smu.py.
       - q: endscript
       - q: tsp_script.save()
       - q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2612b.yaml b/tests/sim_devices/smu/smu2612b.yaml
index 3355842c..e686bb88 100644
--- a/tests/sim_devices/smu/smu2612b.yaml
+++ b/tests/sim_devices/smu/smu2612b.yaml
@@ -10,7 +10,7 @@ devices:
       - q: loadscript tsp_script
       - q: loadscript tsp_function
       - q: print("TEK")
-      - q: '"""Sample script used in test_smu.py."""'
+      - q: -- Sample script used in test_smu.py.
       - q: endscript
       - q: tsp_script.save()
       - q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2614b.yaml b/tests/sim_devices/smu/smu2614b.yaml
index a6eda98f..87d56c88 100644
--- a/tests/sim_devices/smu/smu2614b.yaml
+++ b/tests/sim_devices/smu/smu2614b.yaml
@@ -10,7 +10,7 @@ devices:
       - q: loadscript tsp_script
       - q: loadscript tsp_function
       - q: print("TEK")
-      - q: '"""Sample script used in test_smu.py."""'
+      - q: -- Sample script used in test_smu.py.
       - q: endscript
       - q: tsp_script.save()
       - q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2634b.yaml b/tests/sim_devices/smu/smu2634b.yaml
index b35e8f9c..e66283f6 100644
--- a/tests/sim_devices/smu/smu2634b.yaml
+++ b/tests/sim_devices/smu/smu2634b.yaml
@@ -10,7 +10,7 @@ devices:
       - q: loadscript tsp_script
       - q: loadscript tsp_function
       - q: print("TEK")
-      - q: '"""Sample script used in test_smu.py."""'
+      - q: -- Sample script used in test_smu.py.
       - q: endscript
       - q: tsp_script.save()
       - q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2635b.yaml b/tests/sim_devices/smu/smu2635b.yaml
index 271b6e3e..7d2db1c5 100644
--- a/tests/sim_devices/smu/smu2635b.yaml
+++ b/tests/sim_devices/smu/smu2635b.yaml
@@ -10,7 +10,7 @@ devices:
       - q: loadscript tsp_script
       - q: loadscript tsp_function
       - q: print("TEK")
-      - q: '"""Sample script used in test_smu.py."""'
+      - q: -- Sample script used in test_smu.py.
       - q: endscript
       - q: tsp_script.save()
       - q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2636b.yaml b/tests/sim_devices/smu/smu2636b.yaml
index 6fef3be0..c46ac58f 100644
--- a/tests/sim_devices/smu/smu2636b.yaml
+++ b/tests/sim_devices/smu/smu2636b.yaml
@@ -10,7 +10,7 @@ devices:
       - q: loadscript tsp_script
       - q: loadscript tsp_function
       - q: print("TEK")
-      - q: '"""Sample script used in test_smu.py."""'
+      - q: -- Sample script used in test_smu.py.
       - q: endscript
       - q: tsp_script.save()
       - q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2651a.yaml b/tests/sim_devices/smu/smu2651a.yaml
index f99a2492..58abba85 100644
--- a/tests/sim_devices/smu/smu2651a.yaml
+++ b/tests/sim_devices/smu/smu2651a.yaml
@@ -10,7 +10,7 @@ devices:
       - q: loadscript tsp_script
       - q: loadscript tsp_function
       - q: print("TEK")
-      - q: '"""Sample script used in test_smu.py."""'
+      - q: -- Sample script used in test_smu.py.
       - q: endscript
       - q: tsp_script.save()
       - q: tsp_script()
diff --git a/tests/sim_devices/smu/smu2657a.yaml b/tests/sim_devices/smu/smu2657a.yaml
index 32550b72..ca306550 100644
--- a/tests/sim_devices/smu/smu2657a.yaml
+++ b/tests/sim_devices/smu/smu2657a.yaml
@@ -10,7 +10,7 @@ devices:
       - q: loadscript tsp_script
       - q: loadscript tsp_function
       - q: print("TEK")
-      - q: '"""Sample script used in test_smu.py."""'
+      - q: -- Sample script used in test_smu.py.
       - q: endscript
       - q: tsp_script.save()
       - q: tsp_script()
diff --git a/tests/test_all_device_drivers.py b/tests/test_all_device_drivers.py
index e9819d9d..9b3c9c98 100644
--- a/tests/test_all_device_drivers.py
+++ b/tests/test_all_device_drivers.py
@@ -2,8 +2,10 @@
 """Verify that all device drivers and connection types can be used."""
 
 import contextlib
+import sys
 
 from collections import Counter
+from pathlib import Path
 from typing import Generator, List, Optional
 
 import pytest
@@ -205,7 +207,29 @@ def test_all_device_drivers() -> None:
         sorted_created_connections_list == supported_connections_list
     ), f"Some connections are not tested: {connections_without_testing=}"
 
-    print(f"\nVerified all {len(SIMULATED_DEVICE_LIST)} device drivers")
-    print(
+    print(f"\nVerified all {len(SIMULATED_DEVICE_LIST)} device drivers")  # noqa: T201
+    print(  # noqa: T201
         f"{len(drivers_with_auto_generated_commands)} device drivers have auto-generated commands"
     )
+
+
+@pytest.mark.order(3)
+@pytest.mark.depends(on=["test_device_driver"])
+def test_visa_device_command_logging() -> None:
+    """Test the VISA command log file contents."""
+    generated_file = (
+        Path(__file__).parent / f"logs/unit_test_py{sys.version_info.major}{sys.version_info.minor}"
+        f"_visa_commands_SMU2410-HOSTNAME.log"
+    )
+    assert generated_file.exists(), f"File not found: {generated_file}"
+    assert (
+        generated_file.read_text()
+        == """*CLS
+*RST
+*OPC?
+*ESR?
+SYSTEM:ERROR?
+*ESR?
+SYSTEM:ERROR?
+"""
+    )
diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py
index 15f70357..4b72db5b 100644
--- a/tests/test_config_parser.py
+++ b/tests/test_config_parser.py
@@ -32,7 +32,7 @@ def test_nested_config_prefix_mapping() -> None:
 
 def test_environment_variable_config(capsys: pytest.CaptureFixture[str]) -> None:
     """Test the environment variable config method."""
-    options = ["STANDALONE", "DEFAULT_VISA_TIMEOUT=10000"]
+    options = ["STANDALONE", "DEFAULT_VISA_TIMEOUT=10000", "LOG_FILE_LEVEL=NONE"]
     expected_device_string = (
         "address=MSO54-123456,connection_type=TCPIP,device_type=SCOPE,lan_device_name=hislip0"
     )
@@ -54,6 +54,7 @@ def test_environment_variable_config(capsys: pytest.CaptureFixture[str]) -> None
     assert str(expected_device) == expected_device_string
     assert not config.options.teardown_cleanup
     assert config.options.standalone
+    assert config.options.log_file_level == "NONE"
     assert config.options.default_visa_timeout == 10000
     assert (
         config.devices == expected_entry
@@ -61,10 +62,10 @@ def test_environment_variable_config(capsys: pytest.CaptureFixture[str]) -> None
 
     # test that the config string representation looks like env declaration
     expected_entry_string = (
-        f"TM_OPTIONS=DEFAULT_VISA_TIMEOUT=10000,STANDALONE\n"
+        f"TM_OPTIONS=DEFAULT_VISA_TIMEOUT=10000,LOG_FILE_LEVEL=NONE,STANDALONE\n"
         f"TM_DEVICES=~~~{expected_device_string}~~~"
     )
-    print(config)
+    print(config)  # noqa: T201
     assert capsys.readouterr().out.strip() == expected_entry_string
 
     # test smu with serial properties
@@ -101,10 +102,10 @@ def test_environment_variable_config(capsys: pytest.CaptureFixture[str]) -> None
         config.devices == expected_entry
     ), f"\nDevice dictionaries don't match:\n{expected_entry}\n{config.devices}"
     expected_entry_string = (
-        f"TM_OPTIONS=DEFAULT_VISA_TIMEOUT=10000,STANDALONE\n"
+        f"TM_OPTIONS=DEFAULT_VISA_TIMEOUT=10000,LOG_FILE_LEVEL=NONE,STANDALONE\n"
         f"TM_DEVICES=~~~{expected_device_string}~~~"
     )
-    print(config)
+    print(config)  # noqa: T201
     assert capsys.readouterr().out.strip() == expected_entry_string
 
     expected_device = DeviceConfigEntry(
@@ -149,6 +150,7 @@ def test_file_config_default_path() -> None:
   verbose_mode: false
   verbose_visa: false
   default_visa_timeout: 10000
+  log_console_level: DEBUG
   """
     expected_options = DMConfigOptions(
         standalone=False,
@@ -157,6 +159,7 @@ def test_file_config_default_path() -> None:
         verbose_mode=False,
         verbose_visa=False,
         default_visa_timeout=10000,
+        log_console_level="DEBUG",
     )
     expected_devices = {
         "SCOPE 1": DeviceConfigEntry(
@@ -179,7 +182,7 @@ def test_file_config_default_path() -> None:
     }
     with mock.patch.dict("os.environ", {}, clear=True), mock.patch(
         "pathlib.Path.is_file", mock.MagicMock(return_value=True)
-    ), mock.patch("builtins.open", mock.mock_open(read_data=file_contents)):
+    ), mock.patch("pathlib.Path.open", mock.mock_open(read_data=file_contents)):
         config = DMConfigParser()
 
     assert expected_options == config.options
@@ -214,6 +217,7 @@ def test_file_config_non_default_path(
         verbose_mode=False,
         verbose_visa=False,
         default_visa_timeout=1000,
+        log_file_level="WARNING",
     )
     expected_devices = {
         "SCOPE 1": DeviceConfigEntry(
@@ -251,9 +255,7 @@ def test_file_config_non_default_path(
     config_2.load_config_file(os_environ["TM_DEVICES_CONFIG"])
 
     # Read in the golden files
-    with open(os_environ["TM_DEVICES_CONFIG"], encoding="utf-8") as config_file:
-        text = config_file.read()
-
+    text = Path(os_environ["TM_DEVICES_CONFIG"]).read_text(encoding="utf-8")
     assert config.to_config_file_text(file_type) == text, "issue generating config file text"
     assert config_2.to_config_file_text(file_type) == text, "issue generating config file text"
 
@@ -372,6 +374,11 @@ def test_file_config_non_default_path(
             {"TM_DEVICES": "device_type=MT,connection_type=REST_API,address=localhost"},
             ValueError,
         ),
+        # Test invalid configuration options
+        (
+            {"TM_OPTIONS": "STANDALONE,LOG_FILE_LEVEL=INVALID"},
+            ValueError,
+        ),
     ],
 )
 def test_invalid_config_creation(
diff --git a/tests/test_device_manager.py b/tests/test_device_manager.py
index 1eb1a480..6a88d5d8 100644
--- a/tests/test_device_manager.py
+++ b/tests/test_device_manager.py
@@ -69,8 +69,7 @@ def test_get_config_methods_with_devices(self, device_manager: DeviceManager) ->
                 device_manager.write_current_configuration_to_config_file()
                 # respect the env var if no path is given
                 device_manager.write_current_configuration_to_config_file("./temp_config.yaml")
-            with open("./temp_config.toml", encoding="utf-8") as temp_config:
-                text = temp_config.read()
+            text = Path("./temp_config.toml").read_text(encoding="utf-8")
             assert (
                 text
                 == f"""[[devices]]
@@ -101,9 +100,8 @@ def test_get_config_methods_with_devices(self, device_manager: DeviceManager) ->
 verbose_visa = false
 """
             )
-            with open("./temp_config.yaml", encoding="utf-8") as temp_config:
-                text = temp_config.read()
-                print(text)
+            text = Path("./temp_config.yaml").read_text(encoding="utf-8")
+            print(text)  # noqa: T201
             assert (
                 text
                 == f"""---
diff --git a/tests/test_docs.py b/tests/test_docs.py
index 66e2a133..f9713c9e 100644
--- a/tests/test_docs.py
+++ b/tests/test_docs.py
@@ -51,7 +51,7 @@ def fixture_site_dir(pytestconfig: pytest.Config) -> str:
 @pytest.fixture(scope="module", autouse=True)
 def _docs_tests_setup() -> Generator[None, None, None]:  # pyright: ignore [reportUnusedFunction]
     """Setup for docs tests.."""
-    starting_directory = os.getcwd()
+    starting_directory = Path.cwd()
     try:
         os.chdir(PROJECT_ROOT_DIR)
         yield
diff --git a/tests/test_extension_mixin.py b/tests/test_extension_mixin.py
index 96f2b7ec..b261ca67 100644
--- a/tests/test_extension_mixin.py
+++ b/tests/test_extension_mixin.py
@@ -243,7 +243,7 @@ def custom_model_getter_afg3kc(device: AFG3KC, value: str) -> str:
         return f"AFG3KC {device.model} {value}"
 
     ############################################################################################
-    start_dir = os.getcwd()
+    start_dir = Path.cwd()
     try:
         os.chdir(generated_stub_dir)
         subprocess.check_call(  # noqa: S603
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index c9862c23..d2285a80 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -1,7 +1,6 @@
 # pyright: reportPrivateUsage=none
 """Tests for the helpers subpackage."""
 
-import datetime
 import random
 import socket
 
@@ -12,11 +11,9 @@
 from typing import Any, ClassVar, Dict, List, Optional, Tuple
 from unittest import mock
 
-import dateutil.parser
 import pytest
 import pyvisa as visa
 
-from dateutil.tz import tzlocal
 from packaging.version import InvalidVersion, Version
 from requests import Response
 
@@ -35,7 +32,6 @@
     get_version,
     get_visa_backend,
     ping_address,
-    print_with_timestamp,
     sanitize_enum,
     SupportedModels,
     VALID_DEVICE_CONNECTION_TYPES,
@@ -136,11 +132,6 @@ def test_check_network_connection() -> None:
     assert "ping >> 127.0.0.1" in message
     assert "Response from ping >>" in message
 
-    stdout = StringIO()
-    with redirect_stdout(stdout):
-        assert check_network_connection("name", "127.0.0.1", verbose=False)[0]
-    assert stdout.getvalue() == ""
-
 
 def test_check_port_connection() -> None:
     """Test checking a port connection."""
@@ -151,39 +142,10 @@ def test_check_port_connection() -> None:
         assert check_port_connection("name", "127.0.0.1", 80, timeout_seconds=1)
     message = stdout.getvalue()
     assert "(name) >> checking if port 80 is open on 127.0.0.1" in message
-    assert message.endswith("Result >> True\n")
+    assert message.endswith("(name) port 80 open = True\n")
 
-    stdout = StringIO()
-    with redirect_stdout(stdout), mock.patch(
-        "socket.socket.connect", mock.MagicMock(side_effect=socket.error(""))
-    ):
-        assert not check_port_connection(
-            "name", "127.0.0.1", 55555, timeout_seconds=1, verbose=False
-        )
-    assert stdout.getvalue() == ""
-
-
-def test_print_with_timestamp() -> None:
-    """Test the print_with_timestamp helper function."""
-    stdout = StringIO()
-    with redirect_stdout(stdout):
-        now = datetime.datetime.now(tz=tzlocal())
-        print_with_timestamp("message")
-
-    message = stdout.getvalue()
-    message_parts = message.split(" - ")
-    assert len(message_parts) == 2
-    assert message_parts[1] == "message\n"
-    parsed_datetime = dateutil.parser.parse(message_parts[0].strip())
-    allowed_difference = datetime.timedelta(
-        days=0,
-        hours=0,
-        minutes=0,
-        seconds=1,
-        microseconds=0,
-    )
-    calculated_difference = abs(parsed_datetime - now)
-    assert calculated_difference <= allowed_difference
+    with mock.patch("socket.socket.connect", mock.MagicMock(side_effect=socket.error(""))):
+        assert not check_port_connection("name", "127.0.0.1", 55555, timeout_seconds=1)
 
 
 def test_sanitizing_enums() -> None:
@@ -257,18 +219,11 @@ def test_create_and_check_visa_connection(capsys: pytest.CaptureFixture[str]) ->
 
     assert check_visa_connection(dev_config_3, SIMULATED_VISA_LIB, "dev_config_3")
     stdout = capsys.readouterr().out
-    assert "checking if a VISA connection can be made to " in stdout
-    assert "Result >> True" in stdout
-
-    assert check_visa_connection(dev_config_3, SIMULATED_VISA_LIB, "dev_config_3", verbose=False)
-    stdout = capsys.readouterr().out
-    assert "checking if a VISA connection can be made to " not in stdout
-    assert "Result >> True" not in stdout
+    assert "(dev_config_3) >> checking if a VISA connection can be made to " in stdout
+    assert "(dev_config_3) VISA connected = True" in stdout
 
     with mock.patch("pyvisa.ResourceManager", mock.MagicMock(side_effect=visa.Error())):
-        assert not check_visa_connection(
-            dev_config_3, SIMULATED_VISA_LIB, "dev_config_3", verbose=False
-        )
+        assert not check_visa_connection(dev_config_3, SIMULATED_VISA_LIB, "dev_config_3")
 
 
 def test_check_for_update(capsys: pytest.CaptureFixture[str]) -> None:
diff --git a/tests/test_logging.py b/tests/test_logging.py
new file mode 100644
index 00000000..bb9712e0
--- /dev/null
+++ b/tests/test_logging.py
@@ -0,0 +1,120 @@
+"""Tests for the logging functionality."""
+
+import contextlib
+import logging
+import shutil
+import sys
+
+from pathlib import Path
+from typing import Generator, TYPE_CHECKING
+
+import colorlog
+import pytest
+import pyvisa
+
+import tm_devices
+
+from tm_devices import configure_logging, DeviceManager, LoggingLevels, PACKAGE_NAME
+from tm_devices.helpers import logging as tm_devices_logging
+
+if TYPE_CHECKING:
+    from tm_devices.drivers import MSO2
+
+
+@pytest.fixture(name="remove_log_file_handler")
+def _remove_log_file_handler(device_manager: DeviceManager) -> Generator[None, None, None]:  # pyright: ignore[reportUnusedFunction]
+    """Remove the file handler from the logger."""
+    device_manager.remove_all_devices()
+    logger = logging.getLogger(PACKAGE_NAME)
+    file_handler = None
+    with contextlib.suppress(StopIteration):
+        file_handler = next(
+            handler for handler in logger.handlers if isinstance(handler, logging.FileHandler)
+        )
+        logger.removeHandler(file_handler)
+    yield
+    if file_handler is not None:
+        logger.addHandler(file_handler)
+    device_manager.remove_all_devices()
+
+
+def test_visa_command_logging_edge_cases(
+    device_manager: DeviceManager,
+    remove_log_file_handler: None,  # noqa: ARG001
+) -> None:
+    """Test VISA command logging edge cases."""
+    scope: MSO2 = device_manager.add_scope("MSO22-HOSTNAME")
+    assert scope.model == "MSO22"
+
+
+def test_logging_singleton() -> None:
+    """Verify the singleton behavior of the logging configuration function."""
+    package_logger = logging.getLogger(PACKAGE_NAME)
+    logger_handlers_copy = package_logger.handlers.copy()
+    assert len(logger_handlers_copy) == 3
+    logger = configure_logging()
+    assert len(logger.handlers) == 3
+    assert logger.handlers == logger_handlers_copy
+
+
+@pytest.fixture(name="reset_package_logger")
+def _reset_package_logger() -> Generator[None, None, None]:  # pyright: ignore[reportUnusedFunction]
+    """Reset the package logger."""
+    logger = logging.getLogger(PACKAGE_NAME)
+    handlers_copy = logger.handlers.copy()
+    for handler in handlers_copy:
+        logger.removeHandler(handler)
+    tm_devices_logging._logger_initialized = False  # noqa: SLF001  # pyright: ignore[reportPrivateUsage]
+    yield
+    # Reset the handlers back to what they were
+    for handler in logger.handlers.copy():
+        logger.removeHandler(handler)
+    for handler in handlers_copy:
+        logger.addHandler(handler)
+
+
+def test_configure_logger_full(reset_package_logger: None) -> None:  # noqa: ARG001
+    """Test the configuration function with all types of logs."""
+    log_dir = (
+        Path(__file__).parent / f"generated_logs_py{sys.version_info.major}{sys.version_info.minor}"
+    )
+    log_name = "custom_log.log"
+    shutil.rmtree(log_dir, ignore_errors=True)
+
+    assert not any(isinstance(handler, logging.FileHandler) for handler in pyvisa.logger.handlers)
+    assert len(logging.getLogger(PACKAGE_NAME).handlers) == 0  # pylint: disable=use-implicit-booleaness-not-comparison-to-zero
+    logger = configure_logging(
+        log_console_level="DEBUG",
+        log_file_level="DEBUG",
+        log_file_directory=log_dir,
+        log_file_name=log_name,
+        log_colored_output=False,
+        log_pyvisa_messages=True,
+    )
+    assert len(logger.handlers) == 3
+    assert any(isinstance(handler, logging.FileHandler) for handler in pyvisa.logger.handlers)
+    log_contents = (log_dir / log_name).read_text().split("\n")
+    assert len(log_contents) == 3
+    assert f"] [{PACKAGE_NAME}] [   DEBUG] timezone==" in log_contents[0]
+    assert log_contents[1].endswith(
+        f"] [{PACKAGE_NAME}] [   DEBUG] {PACKAGE_NAME}=={tm_devices.__version__}"
+    )
+    assert [type(x) for x in logger.handlers] == [
+        logging.NullHandler,
+        logging.FileHandler,
+        logging.StreamHandler,
+    ]
+
+
+def test_configure_logger_no_file(reset_package_logger: None) -> None:  # noqa: ARG001
+    """Test the configuration function with no file logging."""
+    assert len(logging.getLogger(PACKAGE_NAME).handlers) == 0  # pylint: disable=use-implicit-booleaness-not-comparison-to-zero
+    logger = configure_logging(
+        log_console_level="DEBUG",
+        log_file_level=LoggingLevels.NONE,
+        log_colored_output=True,
+        log_pyvisa_messages=False,
+    )
+    assert len(logger.handlers) == 2
+    assert [type(x) for x in logger.handlers] == [logging.NullHandler, colorlog.StreamHandler]
+    assert isinstance(logger.handlers[1].formatter, colorlog.ColoredFormatter)
diff --git a/tests/test_margin_testers.py b/tests/test_margin_testers.py
index c588894c..f38d2f27 100644
--- a/tests/test_margin_testers.py
+++ b/tests/test_margin_testers.py
@@ -12,7 +12,7 @@
 from tm_devices import DeviceManager
 from tm_devices.drivers.margin_testers.margin_tester import MarginTester
 
-AUTH_TOKEN_FILE_PATH = f"{Path(__file__).parent}/samples/token.auth_token_file_path"  # nosec
+AUTH_TOKEN_FILE_PATH = Path(__file__).parent / "samples/token.auth_token_file_path"  # nosec
 
 
 ################################################################################################
@@ -121,6 +121,6 @@ def test_generate_headers(tmt4: MarginTester) -> None:
         tmt4: Margin Tester device.
     """
     tmt4.auth_token_file_path = AUTH_TOKEN_FILE_PATH
-    assert tmt4.auth_token_file_path == AUTH_TOKEN_FILE_PATH
+    assert tmt4.auth_token_file_path == AUTH_TOKEN_FILE_PATH.as_posix()
     headers = tmt4._generate_headers()  # noqa: SLF001
     assert headers["Authorization"] == "Bearer UNIT-TEST"
diff --git a/tests/test_pi_device.py b/tests/test_pi_device.py
index 5e639415..2923200f 100644
--- a/tests/test_pi_device.py
+++ b/tests/test_pi_device.py
@@ -14,24 +14,23 @@
 
 
 def test_pi_control(  # noqa: PLR0915
-    device_manager: DeviceManager, capsys: pytest.CaptureFixture[str]
+    device_manager: DeviceManager,
+    capsys: pytest.CaptureFixture[str],
+    caplog: pytest.LogCaptureFixture,
 ) -> None:
     """Test generic PIControl functionality.
 
     Args:
         device_manager: The DeviceManager object.
         capsys: The captured stdout and stderr.
+        caplog: The captured log messages.
     """
     scope: MSO2 = device_manager.add_scope("MSO22-HOSTNAME")
     assert scope._open()  # noqa: SLF001
     assert scope.query_binary("CURVE?") == [0.0]
     assert "Query Binary Values >>  " in capsys.readouterr().out
-    assert scope.query_binary("CURVE?", verbose=False) == [0.0]
-    assert "Query Binary Values >>  " not in capsys.readouterr().out
     assert scope.query_raw_binary("CURVE?") == b"#14\x00\x00\x00\x00\n"
     assert "Query Raw Binary >>  " in capsys.readouterr().out
-    assert scope.query_raw_binary("CURVE?", verbose=False) == b"#14\x00\x00\x00\x00\n"
-    assert "Query Raw Binary >>  " not in capsys.readouterr().out
     scope.factory_reset()
     assert "Write >>  'FACTORY'" in capsys.readouterr().out
     with pytest.raises(AssertionError) as error:
@@ -48,8 +47,6 @@ def test_pi_control(  # noqa: PLR0915
     with pytest.raises(AssertionError):
         scope.query_less_than("*OPC?", 1)
 
-    scope.write_raw(b"FACTORY", verbose=False)
-    assert "Write Raw >>  " not in capsys.readouterr().out
     with mock.patch(
         "pyvisa.resources.messagebased.MessageBasedResource.write_raw",
         mock.MagicMock(side_effect=visa.VisaIOError(123)),
@@ -85,12 +82,7 @@ def test_pi_control(  # noqa: PLR0915
     stdout = capsys.readouterr().out
     assert f"Attempting to establish a VISA connection with {scope.resource_expression}" in stdout
     assert f"Successfully established a VISA connection with {scope.resource_expression} " in stdout
-    assert scope.wait_for_visa_connection(
-        0.1, sleep_seconds=1, accept_immediate_connection=True, verbose=False
-    )
-    stdout = capsys.readouterr().out
-    assert "Attempting to establish a VISA connection with " not in stdout
-    assert "Successfully established a VISA connection with " not in stdout
+
     with pytest.raises(AssertionError):
         scope.wait_for_visa_connection(0.1, sleep_seconds=0, accept_immediate_connection=False)
     with mock.patch("pyvisa.ResourceManager", mock.MagicMock(side_effect=visa.Error())):
@@ -106,17 +98,17 @@ def test_pi_control(  # noqa: PLR0915
         stdout = capsys.readouterr().out
         assert scope.visa_timeout != old_timeout
         assert scope.visa_timeout == 6000
-        assert "VISA timeout set to: 6000ms" in stdout
+        assert "VISA timeout set to: 6000.000ms" in stdout
         # test a temporary verbose OFF
         with scope.temporary_verbose(False):
             assert scope.verbose != old_verbose
             # do something that would normally cause a printout
             scope.visa_timeout = 5000
-            stdout = capsys.readouterr().out
-            assert stdout == ""
             assert scope.visa_timeout == 5000
+            assert caplog.records[-1].levelname == "DEBUG"
+            assert caplog.records[-1].message.endswith("VISA timeout set to: 5000.000ms")
     stdout = capsys.readouterr().out
-    assert f"VISA timeout set to: {old_timeout}ms" in stdout
+    assert f"VISA timeout set to: {old_timeout}.000ms" in stdout
     assert scope.visa_timeout == old_timeout
 
     # Test closing a device that is powered off
diff --git a/tests/test_rest_api_device.py b/tests/test_rest_api_device.py
index c609f130..1bd0a4a5 100644
--- a/tests/test_rest_api_device.py
+++ b/tests/test_rest_api_device.py
@@ -28,7 +28,7 @@ class CustomRestApiDevice(RESTAPIControl, Device):
 
     def _check_api_connection(self) -> bool:
         """Define abstract method _check_api_connection."""
-        return self.get("/api", verbose=False, allow_errors=True)[0]
+        return self.get("/api", allow_errors=True)[0]
 
     def _close(self) -> None:
         """Define abstract method _close."""
@@ -182,7 +182,7 @@ def test_set_api_version_non_verbose(rest_api_control: CustomRestApiDevice) -> N
         rest_api_control: Rest API Device.
     """
     rest_api_control.API_VERSIONS = MappingProxyType({1: "/api", 2: "/api2"})
-    rest_api_control.set_api_version(api_version=2, verbose=False)
+    rest_api_control.set_api_version(api_version=2)
     assert rest_api_control.api_url == rest_api_control.base_url + rest_api_control.API_VERSIONS[2]
 
 
diff --git a/tests/test_scopes.py b/tests/test_scopes.py
index 0755ee54..c4201efa 100644
--- a/tests/test_scopes.py
+++ b/tests/test_scopes.py
@@ -155,10 +155,10 @@ def test_tekscope(device_manager: DeviceManager) -> None:  # noqa: PLR0915
     scope.expect_esr(0, ('0,"No events to report - queue empty"',))
 
     # Test curve query write to csv functionality with multi-frame curve
-    filepath = f"temp_{sys.version_info.major}{sys.version_info.minor}.csv"
+    filepath = pathlib.Path(f"temp_{sys.version_info.major}{sys.version_info.minor}.csv")
     try:
-        curve = scope.curve_query(1, wfm_type="TimeDomain", output_csv_file=filepath)
-        with open(filepath, encoding="utf8") as curve_csv:
+        curve = scope.curve_query(1, wfm_type="TimeDomain", output_csv_file=filepath.as_posix())
+        with filepath.open(encoding="utf8") as curve_csv:
             # Remove trailing command a format string as list of ints based on commas
             curve_reformatted_from_file = list(map(int, curve_csv.read()[:-1].split(",")))
             # Flatten list of lists returned from multi-frame curve query
@@ -364,44 +364,51 @@ def test_exceptions(device_manager: DeviceManager) -> None:
     device_manager.remove_all_devices()
 
 
-def test_tekscope70k(device_manager: DeviceManager, capsys: pytest.CaptureFixture[str]) -> None:
+def test_tekscope70k(
+    device_manager: DeviceManager,
+    caplog: pytest.LogCaptureFixture,
+) -> None:
     """Test the tekscope_70k implementation.
 
     Args:
         device_manager: The DeviceManager object.
-        capsys: The captured stdout and stderr.
+        caplog: The captured log messages.
     """
     scope: TekScope5k7k70k = device_manager.add_scope("127.0.0.1")
     assert scope.ip_address == "127.0.0.1"
     assert scope.hostname == ""
     # Test some generic device functionality
     assert scope.wait_for_network_connection(
-        wait_time=0.05, sleep_seconds=1, accept_immediate_connection=True, verbose=True
-    )
-    assert (
-        f"Successfully established a network connection with {scope.ip_address}"
-        in capsys.readouterr().out
-    )
-    assert scope.wait_for_network_connection(
-        wait_time=0.05, sleep_seconds=1, accept_immediate_connection=True, verbose=False
+        wait_time=0.05, sleep_seconds=1, accept_immediate_connection=True
     )
-    assert (
+    assert caplog.records[-1].message.startswith(
         f"Successfully established a network connection with {scope.ip_address}"
-        not in capsys.readouterr().out
     )
+    assert caplog.records[-1].levelname == "INFO"
+
+    with scope.temporary_verbose(False):
+        assert scope.wait_for_network_connection(
+            wait_time=0.05, sleep_seconds=1, accept_immediate_connection=True
+        )
+        assert caplog.records[-1].message.startswith(
+            f"Successfully established a network connection with {scope.ip_address}"
+        )
+        assert caplog.records[-1].levelname == "DEBUG"
+
     with mock.patch(
         "subprocess.check_output", mock.MagicMock(side_effect=subprocess.CalledProcessError(1, ""))
     ):
         assert not scope.wait_for_network_connection(
-            wait_time=0.05, sleep_seconds=1, accept_immediate_connection=True, verbose=True
+            wait_time=0.05, sleep_seconds=1, accept_immediate_connection=True
         )
-        assert (
-            f"Unable to establish a network connection with {scope.ip_address}"
-            in capsys.readouterr().out
+        assert caplog.records[-1].message.startswith(
+            f"Unable to establish a network connection with {scope.ip_address} after"
         )
+        assert caplog.records[-1].levelname == "WARNING"
+
     with pytest.raises(AssertionError):
         scope.wait_for_network_connection(
-            wait_time=0.05, sleep_seconds=1, accept_immediate_connection=False, verbose=False
+            wait_time=0.05, sleep_seconds=1, accept_immediate_connection=False
         )
     scope.expect_esr(0)
     with pytest.raises(SystemError):
@@ -581,7 +588,7 @@ def test_tekscope2k(device_manager: DeviceManager, tmp_path: pathlib.Path) -> No
 
     filename = tmp_path / "temp.txt"
     curve = scope.curve_query(1, wfm_type="TimeDomain", output_csv_file=str(filename))
-    with open(filename, encoding="utf8") as curve_csv:
+    with filename.open(encoding="utf8") as curve_csv:
         curve_reformatted_from_file = list(map(float, curve_csv.read()[:-1].split(",")))
         assert curve == curve_reformatted_from_file
 
diff --git a/tests/test_singleton.py b/tests/test_singleton.py
index 701c2ff5..eeaf8719 100644
--- a/tests/test_singleton.py
+++ b/tests/test_singleton.py
@@ -13,7 +13,7 @@ class DummyClass(metaclass=Singleton):
 
     def __init__(self, value: bool = False) -> None:
         """Create an instance of the dummy class."""
-        print("running init")
+        print("running init")  # noqa: T201
         self.init_count += 1
         self.value = value
 
diff --git a/tests/test_smu.py b/tests/test_smu.py
index 2d02fd6f..f875c7ab 100644
--- a/tests/test_smu.py
+++ b/tests/test_smu.py
@@ -23,13 +23,16 @@
 
 # pylint: disable=too-many-locals
 def test_smu(  # noqa: PLR0915
-    device_manager: DeviceManager, capsys: pytest.CaptureFixture[str]
+    device_manager: DeviceManager,
+    capsys: pytest.CaptureFixture[str],
+    caplog: pytest.LogCaptureFixture,
 ) -> None:
     """Test the SMU driver and TSP's IEEE commands.
 
     Args:
         device_manager: The DeviceManager object.
         capsys: The captured stdout and stderr.
+        caplog: The captured log messages.
     """
     smu: SMU2601B = device_manager.add_smu("smu2601b-hostname", alias="smu-device")
     assert id(device_manager.get_smu(number_or_alias="smu-device")) == id(smu)
@@ -138,7 +141,7 @@ def test_smu(  # noqa: PLR0915
         f"<tm_devices.commands.smu2601b_commands.SMU2601BCommands object at "
         f"{str(smu.commands).split(' at ', maxsplit=1)[-1]}"
     )
-    print(smu)
+    print(smu)  # noqa: T201
     expected_stdout = f"""{'=' * 45} SMU {smu.device_number} {'=' * 45}
   <class 'tm_devices.drivers.source_measure_units.smu26xx.smu2601b.SMU2601B'> object at {id(smu)}
     address='SMU2601B-HOSTNAME'
@@ -183,35 +186,37 @@ def test_smu(  # noqa: PLR0915
         "socket.socket.shutdown", mock.MagicMock(return_value=None)
     ):
         assert smu.wait_for_port_connection(
-            4000, wait_time=0.05, sleep_seconds=0, accept_immediate_connection=True, verbose=True
+            4000, wait_time=0.05, sleep_seconds=0, accept_immediate_connection=True
         )
         assert (
             f"Successfully established a connection to port 4000 on {smu.ip_address}"
-            in capsys.readouterr().out
-        )
-        assert smu.wait_for_port_connection(
-            4000, wait_time=0.05, sleep_seconds=0, accept_immediate_connection=True, verbose=False
+            in caplog.records[-1].message
         )
+        assert caplog.records[-1].levelname == "INFO"
+        with smu.temporary_verbose(False):
+            assert smu.wait_for_port_connection(
+                4000, wait_time=0.05, sleep_seconds=0, accept_immediate_connection=True
+            )
         assert (
             f"Successfully established a connection to port 4000 on {smu.ip_address}"
-            not in capsys.readouterr().out
+            in caplog.records[-1].message
         )
+        assert caplog.records[-1].levelname == "DEBUG"
         with pytest.raises(AssertionError):
             smu.wait_for_port_connection(
                 4000,
                 wait_time=0.05,
                 sleep_seconds=0,
                 accept_immediate_connection=False,
-                verbose=False,
             )
     with mock.patch("socket.socket.connect", mock.MagicMock(side_effect=socket.error(""))):
         assert not smu.wait_for_port_connection(
-            4000, wait_time=0.05, sleep_seconds=0, accept_immediate_connection=True, verbose=True
+            4000, wait_time=0.05, sleep_seconds=0, accept_immediate_connection=True
         )
-        assert (
-            f"Successfully established a connection to port 4000 on {smu.ip_address}"
-            not in capsys.readouterr().out
+        assert caplog.records[-1].message.startswith(
+            f"Unable to establish a connection to port 4000 on {smu.ip_address} after"
         )
+        assert caplog.records[-1].levelname == "WARNING"
 
     buffer = smu.get_buffers("smua.nvbuffer1")
     expected_buffer = {"smua.nvbuffer1": [1.0, 2.0, 3.0, 4.0, 5.0]}
@@ -227,12 +232,12 @@ def test_smu(  # noqa: PLR0915
     for value in (1.0, 2.0, 3.0, 4.0, 5.0):
         assert str(value) in stdout
 
-    filepath = f"./temp_test_{sys.version_info.major}{sys.version_info.minor}.csv"
+    filepath = Path(f"./temp_test_{sys.version_info.major}{sys.version_info.minor}.csv")
 
     try:
-        smu.export_buffers(filepath, "smua.nvbuffer1")
-        assert os.path.exists(filepath)  # noqa: PTH110
-        with open(filepath, encoding="utf-8") as file:
+        smu.export_buffers(filepath.as_posix(), "smua.nvbuffer1")
+        assert filepath.exists()
+        with filepath.open(encoding="utf-8") as file:
             lines = file.readlines()
             for index, value in enumerate(["smua.nvbuffer1", "1.0", "2.0", "3.0", "4.0", "5.0"]):
                 assert value in lines[index]
diff --git a/tests/test_verification_functions.py b/tests/test_verification_functions.py
index 66a3506d..487a89b5 100644
--- a/tests/test_verification_functions.py
+++ b/tests/test_verification_functions.py
@@ -23,7 +23,7 @@ def test_verify_values_fail() -> None:
             log_error=True,
         )
     assert (
-        " - ERROR: (failing-check) : Actual result does not match the expected "
+        "ERROR: (failing-check) : Actual result does not match the expected "
         "result within a tolerance of 0.0, max: 0.1, act: 0.2, min: 0.1"
     ) in str(assertion_info.value)
 
@@ -48,7 +48,7 @@ def test_verify_values_regex_match_fail() -> None:
             use_regex_match=True,
         )
     assert (
-        " - FAILURE: (regex-fail-check) : Actual result does not match the expected result, "
+        "FAILURE: (regex-fail-check) : Actual result does not match the expected result, "
         "exp: ^test.*value$, act: fail123value"
     ) in str(assertion_info.value)
 
@@ -71,7 +71,7 @@ def test_verify_values_condense_printout(log_error: bool, message_level: str) ->
             log_error=log_error,
         )
     assert (
-        f" - {message_level}: (condense-printout-check) : "
+        f"{message_level}: (condense-printout-check) : "
         f"Actual result does not match the expected result"
         "\n  exp: expected"
         "\n  act: actual"
diff --git a/tests/validate_device_manager_delete.py b/tests/validate_device_manager_delete.py
index af27b597..ff3260eb 100644
--- a/tests/validate_device_manager_delete.py
+++ b/tests/validate_device_manager_delete.py
@@ -11,9 +11,11 @@
 import pyvisa.constants
 
 from conftest import mock_gethostbyaddr, mock_gethostbyname, SIMULATED_VISA_LIB
-from tm_devices import DeviceManager, PYVISA_PY_BACKEND
+from tm_devices import configure_logging, DeviceManager, LoggingLevels, PYVISA_PY_BACKEND
 from tm_devices.helpers import DMConfigOptions
 
+configure_logging(log_console_level=LoggingLevels.DEBUG)
+
 
 @mock.patch("socket.gethostbyname", mock.MagicMock(side_effect=mock_gethostbyname))
 @mock.patch("socket.gethostbyaddr", mock.MagicMock(side_effect=mock_gethostbyaddr))
@@ -48,7 +50,7 @@ def verify_deleting_device_manager() -> None:
         del dev_manager
 
     stdout = stdout_buffer.getvalue()
-    print(stdout)
+    print(stdout)  # noqa: T201
     assert "Closing Connections to Devices" in stdout
     assert "Closing Connection to AFG 1" in stdout
     assert "DeviceManager Closed" in stdout
@@ -67,7 +69,7 @@ def verify_deleting_device_manager() -> None:
         del dev_manager
 
     stdout = stdout_buffer.getvalue()
-    print(stdout)
+    print(stdout)  # noqa: T201
     assert stdout == ""
 
     # Test that the closing happens when the python interpreter exits
diff --git a/tests/verify_physical_device_support.py b/tests/verify_physical_device_support.py
index 57b1c9eb..85209727 100644
--- a/tests/verify_physical_device_support.py
+++ b/tests/verify_physical_device_support.py
@@ -18,6 +18,6 @@
         device_manager.setup_cleanup_enabled = False
         device_manager.teardown_cleanup_enabled = False
         for device in device_manager.devices.values():
-            print(device)
+            print(device)  # noqa: T201
             device.cleanup()
             assert not device.has_errors()