Repository: matchms/matchms
Pull request #271 (2021-11-26 15:43:12)

Add logging
Adds logging to matchms with the option to set the level of logging, e.g. to "INFO" (the default is "WARNING") or even highter to "ERROR" to silence all warnings. This can be done using:
```python
from matchms.logging_functions import set_matchms_logger_level

set_matchms_logger_level("INFO")
```

An alternative is to silence the ouput stream and only write it to a logging file:
```python
from matchms.logging_functions import add_logging_to_file

add_logging_to_file("sample.log", loglevel="INFO", remove_stream_handlers=True)
```
If people add custom things and later want to remove stuff (e.g. handlers) from the matchmslogger, the matchms logger can be reset to its default by:
```python
from matchms.logging_functions import reset_matchms_logger

reset_matchms_logger()
```
The same logger (under different name) can also be re-used in other related libraries such as Spec2Vec:
```python
from matchms.logging_functions import _init_logger

_init_logger("spec2vec")
```

Things done:
- define matchms logger in `logging_functions.py` with several functions for users to adapt the logger.
- replaced all print statements with logger.info or logger.warning
- added a few additional logger.info or logger.warning statements where useful
- update test and code examples
- a few minor linting things here and there


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d688ee0..461023ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,12 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+## Added
+
+- matchms logger, replacing all former `print` statments to better control logging output [#271](https://github.com/matchms/matchms/pull/271)
+- `add_logging_to_file()`, `set_matchms_logger_level()`, `reset_matchms_logger()` functions to adapt logging output to user needs [#271](https://github.com/matchms/matchms/pull/271) 
+
+## [0.10.0] - 2021-11-21
+
 ### Added
 
 - `Spectrum()` objects now also allows generating hashes, e.g. `hash(spectrum)` [#259](https://github.com/matchms/matchms/pull/259)
 - `Spectrum()` objects can generate `.spectrum_hash()` and `.metadata_hash()` to track changes to peaks or metadata [#259](https://github.com/matchms/matchms/pull/259)
 - `load_from_mgf()` now accepts both a path to a mgf file or a file-like object from a preloaded MGF file [#258](https://github.com/matchms/matchms/pull/258)
-- Added `add_retention` filters with function `add_retention_time()` and `add_retention_index()` [#265](https://github.com/matchms/matchms/pull/265)
+- `add_retention` filters with function `add_retention_time()` and `add_retention_index()` [#265](https://github.com/matchms/matchms/pull/265)
+
 ### Changed
 
 - Code linting triggered by pylint update [#257](https://github.com/matchms/matchms/pull/257)
@@ -334,7 +342,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - This is the initial version of Spec2Vec from https://github.com/iomega/Spec2Vec
 
-[Unreleased]: https://github.com/matchms/matchms/compare/0.9.2...HEAD
+[Unreleased]: https://github.com/matchms/matchms/compare/0.10.0...HEAD
+[0.10.0]: https://github.com/matchms/matchms/compare/0.9.2...0.10.0
 [0.9.2]: https://github.com/matchms/matchms/compare/0.9.0...0.9.2
 [0.9.1]: https://github.com/matchms/matchms/compare/0.9.0...0.9.1
 [0.9.0]: https://github.com/matchms/matchms/compare/0.8.2...0.9.0
diff --git a/matchms/__init__.py b/matchms/__init__.py
index e9ce9b5c..0541b009 100644
--- a/matchms/__init__.py
+++ b/matchms/__init__.py
@@ -1,4 +1,3 @@
-import logging
 from . import exporting
 from . import filtering
 from . import importing
@@ -6,12 +5,15 @@ from . import networking
 from . import similarity
 from .__version__ import __version__
 from .calculate_scores import calculate_scores
+from .logging_functions import _init_logger
+from .logging_functions import set_matchms_logger_level
 from .Scores import Scores
 from .Spectrum import Spectrum
 from .Spikes import Spikes
 
 
-logging.getLogger(__name__).addHandler(logging.NullHandler())
+_init_logger()
+
 
 __author__ = "Netherlands eScience Center"
 __email__ = 'generalization@esciencecenter.nl'
@@ -23,6 +25,7 @@ __all__ = [
     "importing",
     "networking",
     "Scores",
+    "set_matchms_logger_level",
     "similarity",
     "Spectrum",
     "Spikes"
diff --git a/matchms/filtering/add_compound_name.py b/matchms/filtering/add_compound_name.py
index 71c14b98..333cc188 100644
--- a/matchms/filtering/add_compound_name.py
+++ b/matchms/filtering/add_compound_name.py
@@ -1,6 +1,10 @@
+import logging
 from ..typing import SpectrumType
 
 
+logger = logging.getLogger("matchms")
+
+
 def add_compound_name(spectrum_in: SpectrumType) -> SpectrumType:
     """Add compound_name to correct field: "compound_name" in metadata."""
     if spectrum_in is None:
@@ -17,6 +21,6 @@ def add_compound_name(spectrum_in: SpectrumType) -> SpectrumType:
             spectrum.set("compound_name", spectrum.get("title"))
             return spectrum
 
-        print("No compound name found in metadata.")
+        logger.warning("No compound name found in metadata.")
 
     return spectrum
diff --git a/matchms/filtering/add_fingerprint.py b/matchms/filtering/add_fingerprint.py
index 1fc3cfad..c44fc1e3 100644
--- a/matchms/filtering/add_fingerprint.py
+++ b/matchms/filtering/add_fingerprint.py
@@ -1,9 +1,13 @@
+import logging
 import numpy
 from ..typing import SpectrumType
 from ..utils import derive_fingerprint_from_inchi
 from ..utils import derive_fingerprint_from_smiles
 
 
+logger = logging.getLogger("matchms")
+
+
 def add_fingerprint(spectrum_in: SpectrumType, fingerprint_type: str = "daylight",
                     nbits: int = 2048) -> SpectrumType:
     """Add molecular finterprint to spectrum.
@@ -42,4 +46,5 @@ def add_fingerprint(spectrum_in: SpectrumType, fingerprint_type: str = "daylight
             spectrum.set("fingerprint", fingerprint)
             return spectrum
 
+    logger.info("No fingerprint was added (name: %s).", spectrum.get("compound_name"))
     return spectrum
diff --git a/matchms/filtering/add_losses.py b/matchms/filtering/add_losses.py
index 291b80a5..2ed63bb9 100644
--- a/matchms/filtering/add_losses.py
+++ b/matchms/filtering/add_losses.py
@@ -1,8 +1,12 @@
+import logging
 import numpy
 from ..Spikes import Spikes
 from ..typing import SpectrumType
 
 
+logger = logging.getLogger("matchms")
+
+
 def add_losses(spectrum_in: SpectrumType, loss_mz_from=0.0, loss_mz_to=1000.0) -> SpectrumType:
     """Derive losses based on precursor mass.
 
@@ -20,7 +24,7 @@ def add_losses(spectrum_in: SpectrumType, loss_mz_from=0.0, loss_mz_to=1000.0) -
 
     spectrum = spectrum_in.clone()
 
-    precursor_mz = spectrum.get("precursor_mz")
+    precursor_mz = spectrum.get("precursor_mz", None)
     if precursor_mz:
         assert isinstance(precursor_mz, (float, int)), ("Expected 'precursor_mz' to be a scalar number.",
                                                         "Consider applying 'add_precursor_mz' filter first.")
@@ -32,5 +36,7 @@ def add_losses(spectrum_in: SpectrumType, loss_mz_from=0.0, loss_mz_to=1000.0) -
                            & (losses_mz <= loss_mz_to))
         spectrum.losses = Spikes(mz=losses_mz[mask],
                                  intensities=losses_intensities[mask])
+    else:
+        logger.warning("No precursor_mz found. Consider applying 'add_precursor_mz' filter first.")
 
     return spectrum
diff --git a/matchms/filtering/add_parent_mass.py b/matchms/filtering/add_parent_mass.py
index e7b3bd81..ac557149 100644
--- a/matchms/filtering/add_parent_mass.py
+++ b/matchms/filtering/add_parent_mass.py
@@ -1,9 +1,13 @@
+import logging
 from ..constants import PROTON_MASS
 from ..importing import load_adducts_dict
 from ..typing import SpectrumType
 from ..utils import clean_adduct
 
 
+logger = logging.getLogger("matchms")
+
+
 def add_parent_mass(spectrum_in: SpectrumType, estimate_from_adduct: bool = True,
                     overwrite_existing_entry: bool = False) -> SpectrumType:
     """Add estimated parent mass to metadata (if not present yet).
@@ -40,7 +44,7 @@ def add_parent_mass(spectrum_in: SpectrumType, estimate_from_adduct: bool = True
     adduct = clean_adduct(spectrum.get("adduct"))
     precursor_mz = spectrum.get("precursor_mz", None)
     if precursor_mz is None:
-        print("Missing precursor m/z to derive parent mass.")
+        logger.warning("Missing precursor m/z to derive parent mass.")
         return spectrum
 
     if estimate_from_adduct and (adduct in adducts_dict):
@@ -55,7 +59,7 @@ def add_parent_mass(spectrum_in: SpectrumType, estimate_from_adduct: bool = True
         parent_mass = precursor_mass - protons_mass
 
     if parent_mass is None:
-        print("Not sufficient spectrum metadata to derive parent mass.")
+        logger.warning("Not sufficient spectrum metadata to derive parent mass.")
     else:
         spectrum.set("parent_mass", float(parent_mass))
     return spectrum
@@ -74,14 +78,17 @@ def _get_charge(spectrum):
     if _is_valid_charge(charge):
         return charge
     if spectrum.get('ionmode') == "positive":
-        print("Missing charge entry, but positive ionmode detected. "
-              "Consider prior run of `correct_charge()` filter.")
+        logger.info(
+            "Missing charge entry, but positive ionmode detected. "
+            "Consider prior run of `correct_charge()` filter.")
         return 1
     if spectrum.get('ionmode') == "negative":
-        print("Missing charge entry, but negative ionmode detected. "
-              "Consider prior run of `correct_charge()` filter.")
+        logger.info(
+            "Missing charge entry, but negative ionmode detected. "
+            "Consider prior run of `correct_charge()` filter.")
         return -1
 
-    print("Missing charge and ionmode entries. "
-          "Consider prior run of `derive_ionmode()` and `correct_charge()` filters.")
+    logger.warning(
+        "Missing charge and ionmode entries. "
+        "Consider prior run of `derive_ionmode()` and `correct_charge()` filters.")
     return 0
diff --git a/matchms/filtering/add_precursor_mz.py b/matchms/filtering/add_precursor_mz.py
index 6aeb8479..cf0b522b 100644
--- a/matchms/filtering/add_precursor_mz.py
+++ b/matchms/filtering/add_precursor_mz.py
@@ -1,7 +1,11 @@
+import logging
 from matchms.utils import get_first_common_element
 from ..typing import SpectrumType
 
 
+logger = logging.getLogger("matchms")
+
+
 _accepted_keys = ["precursor_mz", "precursormz", "precursor_mass"]
 _accepted_types = (float, str, int)
 
@@ -25,7 +29,7 @@ def add_precursor_mz(spectrum_in: SpectrumType) -> SpectrumType:
             try:
                 precursor_mz = float(precursor_mz.strip())
             except ValueError:
-                print("%s can't be converted to float.", precursor_mz)
+                logger.warning("%s can't be converted to float.", precursor_mz)
                 return spectrum
         spectrum.set("precursor_mz", float(precursor_mz))
     elif precursor_mz is None:
@@ -33,6 +37,6 @@ def add_precursor_mz(spectrum_in: SpectrumType) -> SpectrumType:
         if pepmass is not None and isinstance(pepmass[0], float):
             spectrum.set("precursor_mz", pepmass[0])
         else:
-            print("No precursor_mz found in metadata.")
+            logger.warning("No precursor_mz found in metadata.")
 
     return spectrum
diff --git a/matchms/filtering/add_retention.py b/matchms/filtering/add_retention.py
index 782499a4..0766f193 100644
--- a/matchms/filtering/add_retention.py
+++ b/matchms/filtering/add_retention.py
@@ -1,3 +1,4 @@
+import logging
 from typing import Any
 from typing import List
 from typing import Optional
@@ -6,6 +7,9 @@ from matchms.utils import get_common_keys
 from ..typing import SpectrumType
 
 
+logger = logging.getLogger("matchms")
+
+
 _retention_time_keys = ["retention_time", "retentiontime", "rt", "scan_start_time", "RT_Query"]
 _retention_index_keys = ["retention_index", "retentionindex", "ri"]
 
@@ -50,7 +54,7 @@ def safe_convert_to_float(value: Any) -> Optional[float]:
         value = float(value)
         rt = value if value >= 0 else None  # discard negative RT values
     except ValueError:
-        print(f"{value} can't be converted to float.")
+        logger.warning("%s can't be converted to float.", str(value))
         rt = None
     return rt
 
diff --git a/matchms/filtering/clean_compound_name.py b/matchms/filtering/clean_compound_name.py
index 09971c60..f56ae14f 100644
--- a/matchms/filtering/clean_compound_name.py
+++ b/matchms/filtering/clean_compound_name.py
@@ -1,13 +1,17 @@
+import logging
 import re
 from ..typing import SpectrumType
 
 
+logger = logging.getLogger("matchms")
+
+
 def clean_compound_name(spectrum_in: SpectrumType) -> SpectrumType:
     """Clean compound name.
 
     A list of frequently seen name additions that do not belong to the compound
-    name will be removed."""
-
+    name will be removed.
+    """
     def remove_parts_by_regular_expression(name):
         """Clean name string by removing known parts that don't belong there."""
         name = name.strip()
@@ -68,6 +72,6 @@ def clean_compound_name(spectrum_in: SpectrumType) -> SpectrumType:
     name_cleaned = remove_misplaced_mass(name_cleaned)
     if name_cleaned != name:
         spectrum.set("compound_name", name_cleaned)
-        print("Added cleaned compound name:", name_cleaned)
+        logger.info("Added cleaned compound name: %s", name_cleaned)
 
     return spectrum
diff --git a/matchms/filtering/derive_adduct_from_name.py b/matchms/filtering/derive_adduct_from_name.py
index bd417691..0dd05855 100644
--- a/matchms/filtering/derive_adduct_from_name.py
+++ b/matchms/filtering/derive_adduct_from_name.py
@@ -1,8 +1,12 @@
+import logging
 from ..typing import SpectrumType
 from ..utils import clean_adduct
 from ..utils import looks_like_adduct
 
 
+logger = logging.getLogger("matchms")
+
+
 def derive_adduct_from_name(spectrum_in: SpectrumType,
                             remove_adduct_from_name: bool = True) -> SpectrumType:
     """Find adduct in compound name and add to metadata (if not present yet).
@@ -39,12 +43,12 @@ def derive_adduct_from_name(spectrum_in: SpectrumType,
     if adduct_from_name and remove_adduct_from_name:
         name_adduct_removed = " ".join([x for x in name_split if x != adduct_from_name])
         spectrum.set("compound_name", name_adduct_removed)
-        print(f"Removed adduct {adduct_from_name} from compound name.")
+        logger.info("Removed adduct %s from compound name.", adduct_from_name)
 
     # Add found adduct to metadata (if not present yet)
     if adduct_from_name and not looks_like_adduct(spectrum.get("adduct")):
         adduct_cleaned = clean_adduct(adduct_from_name)
         spectrum.set("adduct", adduct_cleaned)
-        print(f"Added adduct {adduct_cleaned} to metadata.")
+        logger.info("Added adduct %s to metadata.", adduct_cleaned)
 
     return spectrum
diff --git a/matchms/filtering/derive_formula_from_name.py b/matchms/filtering/derive_formula_from_name.py
index e4198077..623acf01 100644
--- a/matchms/filtering/derive_formula_from_name.py
+++ b/matchms/filtering/derive_formula_from_name.py
@@ -1,7 +1,11 @@
+import logging
 import re
 from ..typing import SpectrumType
 
 
+logger = logging.getLogger("matchms")
+
+
 def derive_formula_from_name(spectrum_in: SpectrumType,
                              remove_formula_from_name: bool = True) -> SpectrumType:
     """Detect and remove misplaced formula in compound name and add to metadata.
@@ -36,12 +40,12 @@ def derive_formula_from_name(spectrum_in: SpectrumType,
     if formula_from_name and remove_formula_from_name:
         name_formula_removed = " ".join(name.split(" ")[:-1])
         spectrum.set("compound_name", name_formula_removed)
-        print(f"Removed formula {formula_from_name} from compound name.")
+        logger.info("Removed formula %s from compound name.", formula_from_name)
 
     # Add found formula to metadata (if not present yet)
     if formula_from_name and spectrum.get("formula", None) is None:
         spectrum.set("formula", formula_from_name)
-        print(f"Added formula {formula_from_name} to metadata.")
+        logger.info("Added formula %s to metadata.", formula_from_name)
 
     return spectrum
 
diff --git a/matchms/filtering/derive_inchi_from_smiles.py b/matchms/filtering/derive_inchi_from_smiles.py
index 42ff06b5..a028cd32 100644
--- a/matchms/filtering/derive_inchi_from_smiles.py
+++ b/matchms/filtering/derive_inchi_from_smiles.py
@@ -1,9 +1,13 @@
+import logging
 from ..typing import SpectrumType
 from ..utils import convert_smiles_to_inchi
 from ..utils import is_valid_inchi
 from ..utils import is_valid_smiles
 
 
+logger = logging.getLogger("matchms")
+
+
 def derive_inchi_from_smiles(spectrum_in: SpectrumType) -> SpectrumType:
     """Find missing Inchi and derive from smiles where possible."""
     if spectrum_in is None:
@@ -18,7 +22,8 @@ def derive_inchi_from_smiles(spectrum_in: SpectrumType) -> SpectrumType:
         if inchi:
             inchi = inchi.rstrip()
             spectrum.set("inchi", inchi)
+            logger.info("Added InChI (%s) to metadata (was converted from smiles).", inchi)
         else:
-            print("Could not convert smiles", smiles, "to InChI.")
+            logger.warning("Could not convert smiles %s to InChI.", smiles)
 
     return spectrum
diff --git a/matchms/filtering/derive_inchikey_from_inchi.py b/matchms/filtering/derive_inchikey_from_inchi.py
index d8399452..4abe2097 100644
--- a/matchms/filtering/derive_inchikey_from_inchi.py
+++ b/matchms/filtering/derive_inchikey_from_inchi.py
@@ -1,12 +1,15 @@
+import logging
 from ..typing import SpectrumType
 from ..utils import convert_inchi_to_inchikey
 from ..utils import is_valid_inchi
 from ..utils import is_valid_inchikey
 
 
+logger = logging.getLogger("matchms")
+
+
 def derive_inchikey_from_inchi(spectrum_in: SpectrumType) -> SpectrumType:
     """Find missing InchiKey and derive from Inchi where possible."""
-
     if spectrum_in is None:
         return None
 
@@ -18,7 +21,8 @@ def derive_inchikey_from_inchi(spectrum_in: SpectrumType) -> SpectrumType:
         inchikey = convert_inchi_to_inchikey(inchi)
         if inchikey:
             spectrum.set("inchikey", inchikey)
+            logger.info("Added InChIKey %s to metadata (was converted from inchi)", inchikey)
         else:
-            print("Could not convert InChI", inchi, "to inchikey.")
+            logger.warning("Could not convert InChI %s to inchikey.", inchi)
 
     return spectrum
diff --git a/matchms/filtering/derive_ionmode.py b/matchms/filtering/derive_ionmode.py
index 1c6324f2..125b7015 100644
--- a/matchms/filtering/derive_ionmode.py
+++ b/matchms/filtering/derive_ionmode.py
@@ -1,8 +1,12 @@
+import logging
 from ..importing import load_adducts_dict
 from ..typing import SpectrumType
 from ..utils import clean_adduct
 
 
+logger = logging.getLogger("matchms")
+
+
 def derive_ionmode(spectrum_in: SpectrumType) -> SpectrumType:
     """Derive missing ionmode based on adduct.
 
@@ -47,5 +51,6 @@ def derive_ionmode(spectrum_in: SpectrumType) -> SpectrumType:
         ionmode = "n/a"
 
     spectrum.set("ionmode", ionmode)
+    logger.info("Set ionmode to %s.", ionmode)
 
     return spectrum
diff --git a/matchms/filtering/derive_smiles_from_inchi.py b/matchms/filtering/derive_smiles_from_inchi.py
index 7d70c2c8..cffa4ee1 100644
--- a/matchms/filtering/derive_smiles_from_inchi.py
+++ b/matchms/filtering/derive_smiles_from_inchi.py
@@ -1,9 +1,13 @@
+import logging
 from ..typing import SpectrumType
 from ..utils import convert_inchi_to_smiles
 from ..utils import is_valid_inchi
 from ..utils import is_valid_smiles
 
 
+logger = logging.getLogger("matchms")
+
+
 def derive_smiles_from_inchi(spectrum_in: SpectrumType) -> SpectrumType:
     """Find missing smiles and derive from Inchi where possible."""
     if spectrum_in is None:
@@ -18,7 +22,8 @@ def derive_smiles_from_inchi(spectrum_in: SpectrumType) -> SpectrumType:
         if smiles:
             smiles = smiles.rstrip()
             spectrum.set("smiles", smiles)
+            logger.info("Added smiles %s to metadata (was converted from InChI)", smiles)
         else:
-            print("Could not convert InChI", inchi, "to smiles.")
+            logger.warning("Could not convert InChI %s to smiles.", inchi)
 
     return spectrum
diff --git a/matchms/filtering/make_charge_int.py b/matchms/filtering/make_charge_int.py
index fa305e53..e6e7c092 100644
--- a/matchms/filtering/make_charge_int.py
+++ b/matchms/filtering/make_charge_int.py
@@ -1,6 +1,10 @@
+import logging
 from ..typing import SpectrumType
 
 
+logger = logging.getLogger("matchms")
+
+
 def make_charge_int(spectrum_in: SpectrumType) -> SpectrumType:
     """Convert charge field to integer (if possible)."""
     if spectrum_in is None:
@@ -18,6 +22,6 @@ def make_charge_int(spectrum_in: SpectrumType) -> SpectrumType:
             charge_int = int(spectrum.get('charge'))
             spectrum.set("charge", charge_int)
         except ValueError:
-            print(f"Found charge ({spectrum.get('charge')}) cannot be converted to integer.")
+            logger.warning("Found charge (%s) cannot be converted to integer.", str(spectrum.get('charge')))
 
     return spectrum
diff --git a/matchms/filtering/reduce_to_number_of_peaks.py b/matchms/filtering/reduce_to_number_of_peaks.py
index e6ce9c9c..17a76f78 100644
--- a/matchms/filtering/reduce_to_number_of_peaks.py
+++ b/matchms/filtering/reduce_to_number_of_peaks.py
@@ -1,3 +1,4 @@
+import logging
 from math import ceil
 from typing import Optional
 import numpy
@@ -5,6 +6,9 @@ from ..Spikes import Spikes
 from ..typing import SpectrumType
 
 
+logger = logging.getLogger("matchms")
+
+
 def reduce_to_number_of_peaks(spectrum_in: SpectrumType, n_required: int = 1, n_max: int = numpy.inf,
                               ratio_desired: Optional[float] = None) -> SpectrumType:
     """Lowest intensity peaks will be removed when it has more peaks than desired.
@@ -46,6 +50,8 @@ def reduce_to_number_of_peaks(spectrum_in: SpectrumType, n_required: int = 1, n_
     spectrum = spectrum_in.clone()
 
     if spectrum.peaks.intensities.size < n_required:
+        logger.info("Spectrum with %s (<%s) peaks was set to None.",
+                    str(spectrum.peaks.intensities.size), str(n_required))
         return None
 
     threshold = _set_maximum_number_of_peaks_to_keep()
diff --git a/matchms/filtering/require_minimum_number_of_peaks.py b/matchms/filtering/require_minimum_number_of_peaks.py
index cfed8bcf..3c87f377 100644
--- a/matchms/filtering/require_minimum_number_of_peaks.py
+++ b/matchms/filtering/require_minimum_number_of_peaks.py
@@ -1,8 +1,12 @@
+import logging
 from math import ceil
 from typing import Optional
 from ..typing import SpectrumType
 
 
+logger = logging.getLogger("matchms")
+
+
 def require_minimum_number_of_peaks(spectrum_in: SpectrumType,
                                     n_required: int = 10,
                                     ratio_required: Optional[float] = None) -> SpectrumType:
@@ -33,6 +37,8 @@ def require_minimum_number_of_peaks(spectrum_in: SpectrumType,
         threshold = n_required
 
     if spectrum.peaks.intensities.size < threshold:
+        logger.info("Spectrum with %s (<%s) peaks was set to None.",
+                    str(spectrum.peaks.intensities.size), str(threshold))
         return None
 
     return spectrum
diff --git a/matchms/filtering/require_minimum_of_high_peaks.py b/matchms/filtering/require_minimum_of_high_peaks.py
index 47000ee8..299d60a5 100644
--- a/matchms/filtering/require_minimum_of_high_peaks.py
+++ b/matchms/filtering/require_minimum_of_high_peaks.py
@@ -1,7 +1,11 @@
+import logging
 from ..typing import SpectrumType
 from .select_by_relative_intensity import select_by_relative_intensity
 
 
+logger = logging.getLogger("matchms")
+
+
 def require_minimum_of_high_peaks(spectrum_in: SpectrumType, no_peaks: int = 5,
                                   intensity_percent: float = 2.0) -> SpectrumType:
 
@@ -29,6 +33,8 @@ def require_minimum_of_high_peaks(spectrum_in: SpectrumType, no_peaks: int = 5,
     assert 0 <= intensity_percent <= 100, "intensity_percent must be a scalar between 0-100."
     intensities_above_p = select_by_relative_intensity(spectrum, intensity_from=intensity_percent/100, intensity_to=1.0)
     if len(intensities_above_p.peaks) < no_peaks:
+        logger.info("Spectrum with %s (<%s) peaks was set to None.",
+                    str(len(intensities_above_p.peaks)), str(no_peaks))
         return None
 
     return spectrum
diff --git a/matchms/filtering/require_precursor_below_mz.py b/matchms/filtering/require_precursor_below_mz.py
index 45e0f92c..934a1467 100644
--- a/matchms/filtering/require_precursor_below_mz.py
+++ b/matchms/filtering/require_precursor_below_mz.py
@@ -1,6 +1,10 @@
+import logging
 from ..typing import SpectrumType
 
 
+logger = logging.getLogger("matchms")
+
+
 def require_precursor_below_mz(spectrum_in: SpectrumType, max_mz: float = 1000) -> SpectrumType:
 
     """Returns None if the precursor_mz of a spectrum is above
@@ -26,6 +30,8 @@ def require_precursor_below_mz(spectrum_in: SpectrumType, max_mz: float = 1000)
                                                     "Consider applying 'add_precursor_mz' filter first.")
     assert max_mz >= 0, "max_mz must be a positive scalar."
     if precursor_mz >= max_mz:
+        logger.info("Spectrum with precursor_mz %s (>%s) was set to None.",
+                    str(precursor_mz), str(max_mz))
         return None
 
     return spectrum
diff --git a/matchms/filtering/require_precursor_mz.py b/matchms/filtering/require_precursor_mz.py
index fc95cb16..0cf49348 100644
--- a/matchms/filtering/require_precursor_mz.py
+++ b/matchms/filtering/require_precursor_mz.py
@@ -1,7 +1,11 @@
+import logging
 from typing import Union
 from ..typing import SpectrumType
 
 
+logger = logging.getLogger("matchms")
+
+
 def require_precursor_mz(spectrum_in: SpectrumType
                          ) -> Union[SpectrumType, None]:
 
@@ -29,6 +33,7 @@ def require_precursor_mz(spectrum_in: SpectrumType
         ("Expected 'precursor_mz' to be a scalar number.",
          "Consider applying 'add_precursor_mz' filter first.")
     if precursor_mz <= 0:
+        logger.info("Spectrum without precursor_mz was set to None.")
         return None
 
     return spectrum
diff --git a/matchms/importing/load_from_json.py b/matchms/importing/load_from_json.py
index 08ac3bf8..2296fc6d 100644
--- a/matchms/importing/load_from_json.py
+++ b/matchms/importing/load_from_json.py
@@ -1,11 +1,15 @@
 import ast
 import json
+import logging
 from typing import List
 from typing import Union
 import numpy
 from ..Spectrum import Spectrum
 
 
+logger = logging.getLogger("matchms")
+
+
 def load_from_json(filename: str) -> List[Spectrum]:
     """Load spectrum(s) from json file.
 
@@ -101,6 +105,5 @@ def dict2spectrum(spectrum_dict: dict) -> Union[Spectrum, None]:
         return Spectrum(mz=mz,
                         intensities=intensities,
                         metadata=metadata_dict)
-    print("Empty spectrum found (no peaks in 'peaks_json').",
-          "Will not be imported.")
+    logger.info("Empty spectrum found (no peaks in 'peaks_json'). Will not be imported.")
     return None
diff --git a/matchms/importing/load_from_usi.py b/matchms/importing/load_from_usi.py
index 96063e35..c2816169 100644
--- a/matchms/importing/load_from_usi.py
+++ b/matchms/importing/load_from_usi.py
@@ -1,9 +1,13 @@
 import json
+import logging
 import numpy as np
 import requests
 from ..Spectrum import Spectrum
 
 
+logger = logging.getLogger("matchms")
+
+
 def load_from_usi(usi: str, server: str = "https://metabolomics-usi.ucsd.edu"):
     """Load spectrum from metabolomics USI.
 
@@ -36,9 +40,11 @@ def load_from_usi(usi: str, server: str = "https://metabolomics-usi.ucsd.edu"):
     try:
         spectral_data = response.json()
         if spectral_data is None or "peaks" not in spectral_data:
+            logger.info("Empty spectrum found (no data found). Will not be imported.")
             return None
         peaks = spectral_data["peaks"]
         if len(peaks) == 0:
+            logger.info("Empty spectrum found (no peaks in 'peaks_json'). Will not be imported.")
             return None
         mz_list, intensity_list = zip(*peaks)
         mz_array = np.array(mz_list)
@@ -51,5 +57,5 @@ def load_from_usi(usi: str, server: str = "https://metabolomics-usi.ucsd.edu"):
         return s
 
     except json.decoder.JSONDecodeError:
-        # failed to unpack json
+        logger.warning("Failed to unpack json (JSONDecodeError).")
         return None
diff --git a/matchms/logging_functions.py b/matchms/logging_functions.py
new file mode 100644
index 00000000..42798adb
--- /dev/null
+++ b/matchms/logging_functions.py
@@ -0,0 +1,116 @@
+"""Matchms logger.
+
+Matchms functions and method report unexpected or undesired behavior as
+logging WARNING, and additional information as INFO.
+The default logging level is set to WARNING. If you want to output additional
+logging messages, you can lower the logging level to INFO using set_matchms_logger_level:
+
+.. code-block:: python
+
+    from matchms import set_matchms_logger_level
+    from matchms import calculate_scores, Spectrum
+
+
+    set_matchms_logger_level("INFO")
+
+If you want to suppress logging warnings, you can also raise the logging level
+to ERROR by:
+
+.. code-block:: python
+
+    set_matchms_logger_level("ERROR")
+
+To write logging entries to a local file, you can do the following:
+
+.. code-block:: python
+
+    from matchms import calculate_scores, Spectrum
+    from matchms.logging_functions import add_logging_to_file
+
+    add_logging_to_file("sample.log", loglevel="INFO")
+
+"""
+import logging
+import logging.config
+import sys
+
+
+_formatter = logging.Formatter(
+    '%(asctime)s:%(levelname)s:%(name)s:%(module)s:%(message)s')
+
+
+def _init_logger(logger_name="matchms"):
+    """Initialize matchms logger."""
+    logger = logging.getLogger(logger_name)
+    logger.setLevel(logging.WARNING)
+    handler = logging.StreamHandler(sys.stdout)
+    handler.setLevel(logging.WARNING)
+    handler.setFormatter(_formatter)
+    logger.addHandler(handler)
+    logger.info('Completed configuring matchms logger.')
+
+
+def set_matchms_logger_level(loglevel: str, logger_name="matchms"):
+    """Update logging level to given loglevel.
+
+    Parameters
+    ----------
+    loglevels
+        Can be 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'.
+    logger_name
+        Default is "matchms". Change if logger name should be different.
+    """
+    level = logging.getLevelName(loglevel)
+    logger = logging.getLogger(logger_name)
+    logger.setLevel(level)
+    for handler in logger.handlers:
+        handler.setLevel(level)
+
+
+def add_logging_to_file(filename: str, loglevel: str = "INFO",
+                        remove_stream_handlers: bool = False,
+                        logger_name="matchms"):
+    """Add logging to file.
+
+    Current implementation does not change the initial logging stream,
+    but simply adds a FileHandler to write logging entries to a file.
+
+    Parameters
+    ----------
+    filename
+        Name of file for write logging output.
+    loglevels
+        Can be 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'.
+    remove_stream_handlers
+        Set to True if only logging to file is desired.
+    logger_name
+        Default is "matchms". Change if logger name should be different.
+    """
+    level = logging.getLevelName(loglevel)
+    logger = logging.getLogger(logger_name)
+    file_handler = logging.FileHandler(filename)
+    file_handler.setLevel(level)
+    file_handler.setFormatter(_formatter)
+    logger.addHandler(file_handler)
+
+    # Remove the existing handlers if they are not of type FileHandler
+    if remove_stream_handlers is True:
+        for handler in logger.handlers:
+            if not isinstance(handler, logging.FileHandler):
+                logger.removeHandler(handler)
+
+
+def reset_matchms_logger(logger_name="matchms"):
+    """Reset matchms logger to initial state.
+
+    This will remove all logging Handlers and initialize a new matchms logger.
+    Use this function to reset previous changes made to the default matchms logger.
+
+    Parameters
+    ----------
+    logger_name
+        Default is "matchms". Change if logger name should be different.
+    """
+    logger = logging.getLogger(logger_name)
+    logger.handlers.clear()
+    _init_logger()
diff --git a/matchms/utils.py b/matchms/utils.py
index 6bc394c4..88233a74 100644
--- a/matchms/utils.py
+++ b/matchms/utils.py
@@ -28,14 +28,17 @@ rdkit_missing_message = "Conda package 'rdkit' is required for this functionalit
 
 
 def convert_smiles_to_inchi(smiles: str) -> Optional[str]:
+    """Convert smiles to inchi using rdkit."""
     return mol_converter(smiles, "smiles", "inchi")
 
 
 def convert_inchi_to_smiles(inchi: str) -> Optional[str]:
+    """Convert inchi to smiles using rdkit."""
     return mol_converter(inchi, "inchi", "smiles")
 
 
 def convert_inchi_to_inchikey(inchi: str) -> Optional[str]:
+    """Convert inchi to inchikey using rdkit."""
     return mol_converter(inchi, "inchi", "inchikey")
 
 
diff --git a/readthedocs/index.rst b/readthedocs/index.rst
index f2fae2c0..7fe460d9 100644
--- a/readthedocs/index.rst
+++ b/readthedocs/index.rst
@@ -6,7 +6,7 @@
 Welcome to matchms's documentation!
 ===================================
 
-Matchms is an open-access Python package to import, process, clean, and compare mass spectrometry data (MS/MS). It allows to implement and run an easy-to-follow, easy-to-reproduce workflow from raw mass spectra to pre- and post-processed spectral data. 
+Matchms is an open-access Python package to import, process, clean, and compare mass spectrometry data (MS/MS). It allows to implement and run an easy-to-follow, easy-to-reproduce workflow from raw mass spectra to pre- and post-processed spectral data.
 
 .. toctree::
    :maxdepth: 3
@@ -26,9 +26,9 @@ Matchms was designed to easily build custom spectra processing pipelines and to
 Installation
 ============
 
-Prerequisites:  
+Prerequisites:
 
-- Python 3.7 or 3.8
+- Python 3.7, 3.8, or 3.9
 - Anaconda
 
 Install matchms from Anaconda Cloud with
@@ -36,7 +36,7 @@ Install matchms from Anaconda Cloud with
 .. code-block:: console
 
   # install matchms in a new virtual environment to avoid dependency clashes
-  conda create --name matchms python=3.7
+  conda create --name matchms python=3.8
   conda activate matchms
   conda install --channel nlesc --channel bioconda --channel conda-forge matchms
 
@@ -53,7 +53,7 @@ Below is a small example of using matchms to calculate the Cosine score between
     from matchms import calculate_scores
     from matchms.similarity import CosineGreedy
 
-    # Read spectrums from a MGF formatted file, for other formats see https://matchms.readthedocs.io/en/latest/api/matchms.importing.html 
+    # Read spectrums from a MGF formatted file, for other formats see https://matchms.readthedocs.io/en/latest/api/matchms.importing.html
     file = load_from_mgf("../tests/pesticides.mgf")
 
     # Apply filters to clean and enhance each spectrum
@@ -68,16 +68,23 @@ Below is a small example of using matchms to calculate the Cosine score between
 
     # Calculate Cosine similarity scores between all spectrums
     # For other similarity score methods see https://matchms.readthedocs.io/en/latest/api/matchms.similarity.html .
+    # Because references and queries are here the same spectra, we can set is_symmetric=True
     scores = calculate_scores(references=spectrums,
                               queries=spectrums,
-                              similarity_function=CosineGreedy())
+                              similarity_function=CosineGreedy(),
+                              is_symmetric=True)
+
+    # This computed all-vs-all similarity scores, the array of which can be accessed as scores.scores
+    print(f"Size of matrix of computed similarities: {scores.scores.shape}")
+
+    # Matchms allows to get the best matches for any query using scores_by_query
+    query = spectrums[15]  # just an example
+    best_matches = scores.scores_by_query(query, sort=True)
 
     # Print the calculated scores for each spectrum pair
-    for score in scores:
-        (reference, query, score) = score
-        # Ignore scores between same spectrum and
-        # pairs which have less than 20 peaks in common
-        if reference is not query and score['matches'] >= 20:
+    for (reference, score) in best_matches[:10]:
+        # Ignore scores between same spectrum
+        if reference is not query:
             print(f"Reference scan id: {reference.metadata['scans']}")
             print(f"Query scan id: {query.metadata['scans']}")
             print(f"Score: {score['score']:.4f}")
@@ -88,18 +95,17 @@ Should output
 
 .. testoutput::
 
-    Removed adduct M-H from compound name.
-    Added adduct [M-H]- to metadata.
-    ...
-    Reference scan id: 1320
-    Query scan id: 1372
-    Score: 0.9143
-    Number of matching peaks: 25
+    Size of matrix of computed similarities: (76, 76)
+    Reference scan id: 613
+    Query scan id: 2161
+    Score: 0.8646
+    Number of matching peaks: 14
+    ----------------------------
+    Reference scan id: 603
+    Query scan id: 2161
+    Score: 0.8237
+    Number of matching peaks: 14
     ----------------------------
-    Reference scan id: 2833
-    Query scan id: 1320
-    Score: 0.0144
-    Number of matching peaks: 20
     ...
 
 Indices and tables
diff --git a/setup.py b/setup.py
index 061e73fa..85a0605f 100644
--- a/setup.py
+++ b/setup.py
@@ -59,12 +59,14 @@ setup(
     ],
     extras_require={"dev": ["bump2version",
                             "isort>=4.2.5,<5",
+                            "pylint<2.12",
                             "prospector[with_pyroma]",
                             "pytest",
                             "pytest-cov",
                             "sphinx>=3.0.0,!=3.2.0,!=3.5.0,<4.0.0",
                             "sphinx_rtd_theme",
                             "sphinxcontrib-apidoc",
+                            "testfixtures",
                             "yapf",],
                     "chemistry": ["rdkit >=2020.03.1"]},
 )


Test patch:
diff --git a/tests/test_SimilarityNetwork.py b/tests/test_SimilarityNetwork.py
index 32430795..370f0124 100644
--- a/tests/test_SimilarityNetwork.py
+++ b/tests/test_SimilarityNetwork.py
@@ -9,7 +9,7 @@ from matchms.similarity import FingerprintSimilarity
 from matchms.similarity import ModifiedCosine
 
 
-@pytest.yield_fixture
+@pytest.fixture
 def filename():
     with tempfile.TemporaryDirectory() as temp_dir:
         filename = os.path.join(temp_dir, "test.graphml")
diff --git a/tests/test_add_compound_name.py b/tests/test_add_compound_name.py
new file mode 100644
index 00000000..ea0e1c9c
--- /dev/null
+++ b/tests/test_add_compound_name.py
@@ -0,0 +1,48 @@
+import numpy
+from testfixtures import LogCapture
+from matchms import Spectrum
+from matchms.filtering import add_compound_name
+
+
+def test_add_compound_name_entry_name():
+    """Test filled name field."""
+    spectrum_in = Spectrum(mz=numpy.array([], dtype="float"),
+                           intensities=numpy.array([], dtype="float"),
+                           metadata={"name": "Testospectrum"})
+
+    spectrum = add_compound_name(spectrum_in)
+
+    assert spectrum.get("compound_name") == "Testospectrum", "Expected different compound name."
+
+
+def test_add_compound_name_entry_title():
+    """Test filled name field."""
+    spectrum_in = Spectrum(mz=numpy.array([], dtype="float"),
+                           intensities=numpy.array([], dtype="float"),
+                           metadata={"title": "Testospectrum"})
+
+    spectrum = add_compound_name(spectrum_in)
+
+    assert spectrum.get("compound_name") == "Testospectrum", "Expected different compound name."
+
+
+def test_add_compound_name_missing_entry():
+    """Test filled name field."""
+    spectrum_in = Spectrum(mz=numpy.array([], dtype="float"),
+                           intensities=numpy.array([], dtype="float"),
+                           metadata={"othername": "Testospectrum"})
+
+    with LogCapture() as log:
+        spectrum = add_compound_name(spectrum_in)
+
+    assert spectrum.get("compound_name", None) is None, "Expected no compound name."
+    log.check(
+        ('matchms', 'WARNING', 'No compound name found in metadata.')
+    )
+
+
+def test_empty_spectrum():
+    spectrum_in = None
+    spectrum = add_compound_name(spectrum_in)
+
+    assert spectrum is None, "Expected differnt handling of None spectrum."
diff --git a/tests/test_add_fingerprint.py b/tests/test_add_fingerprint.py
index 8f288d8f..6461ed8e 100644
--- a/tests/test_add_fingerprint.py
+++ b/tests/test_add_fingerprint.py
@@ -1,7 +1,10 @@
 import numpy
 import pytest
+from testfixtures import LogCapture
 from matchms import Spectrum
 from matchms.filtering import add_fingerprint
+from matchms.logging_functions import reset_matchms_logger
+from matchms.logging_functions import set_matchms_logger_level
 
 
 def test_add_fingerprint_from_smiles():
@@ -30,12 +33,18 @@ def test_add_fingerprint_from_inchi():
 
 def test_add_fingerprint_no_smiles_no_inchi():
     """Test if fingerprint it generated correctly."""
+    set_matchms_logger_level("INFO")
     spectrum_in = Spectrum(mz=numpy.array([], dtype="float"),
                            intensities=numpy.array([], dtype="float"),
-                           metadata={})
+                           metadata={"compound_name": "test name"})
 
-    spectrum = add_fingerprint(spectrum_in)
+    with LogCapture() as log:
+        spectrum = add_fingerprint(spectrum_in)
     assert spectrum.get("fingerprint", None) is None, "Expected None."
+    log.check(
+        ("matchms", "INFO", "No fingerprint was added (name: test name).")
+    )
+    reset_matchms_logger()
 
 
 def test_add_fingerprint_empty_spectrum():
diff --git a/tests/test_add_losses.py b/tests/test_add_losses.py
index 6fdd73f3..ea50d999 100644
--- a/tests/test_add_losses.py
+++ b/tests/test_add_losses.py
@@ -1,5 +1,6 @@
 import numpy
 import pytest
+from testfixtures import LogCapture
 from matchms import Spectrum
 from matchms.filtering import add_losses
 
@@ -23,9 +24,14 @@ def test_add_losses_without_precursor_mz():
     spectrum_in = Spectrum(mz=numpy.array([100, 150, 200, 300], dtype="float"),
                            intensities=numpy.array([700, 200, 100, 1000], dtype="float"))
 
-    spectrum = add_losses(spectrum_in)
+    with LogCapture() as log:
+        spectrum = add_losses(spectrum_in)
 
     assert spectrum == spectrum_in and spectrum is not spectrum_in
+    log.check(
+        ("matchms", "WARNING",
+         "No precursor_mz found. Consider applying 'add_precursor_mz' filter first.")
+    )
 
 
 def test_add_losses_with_precursor_mz_wrong_type():
diff --git a/tests/test_add_parent_mass.py b/tests/test_add_parent_mass.py
index 3fb5d5f7..a7663eab 100644
--- a/tests/test_add_parent_mass.py
+++ b/tests/test_add_parent_mass.py
@@ -5,7 +5,7 @@ from matchms.constants import PROTON_MASS
 from matchms.filtering import add_parent_mass
 
 
-def test_add_parent_mass_pepmass_no_precursormz(capsys):
+def test_add_parent_mass_pepmass_no_precursormz(caplog):
     """Test if correct exception is returned."""
     mz = numpy.array([], dtype='float')
     intensities = numpy.array([], dtype='float')
@@ -18,10 +18,10 @@ def test_add_parent_mass_pepmass_no_precursormz(capsys):
     spectrum = add_parent_mass(spectrum_in)
 
     assert spectrum.get("parent_mass") is None, "Expected no parent mass"
-    assert "Not sufficient spectrum metadata to derive parent mass." not in capsys.readouterr().out
+    assert "Not sufficient spectrum metadata to derive parent mass." not in caplog.text
 
 
-def test_add_parent_mass_no_precursormz(capsys):
+def test_add_parent_mass_no_precursormz(caplog):
     """Test if correct exception is returned."""
     mz = numpy.array([], dtype='float')
     intensities = numpy.array([], dtype='float')
@@ -33,10 +33,10 @@ def test_add_parent_mass_no_precursormz(capsys):
     spectrum = add_parent_mass(spectrum_in)
 
     assert spectrum.get("parent_mass") is None, "Expected no parent mass"
-    assert "Missing precursor m/z to derive parent mass." in capsys.readouterr().out
+    assert "Missing precursor m/z to derive parent mass." in caplog.text
 
 
-def test_add_parent_mass_precursormz_zero_charge(capsys):
+def test_add_parent_mass_precursormz_zero_charge(caplog):
     """Test if correct exception is returned."""
     mz = numpy.array([], dtype='float')
     intensities = numpy.array([], dtype='float')
@@ -49,10 +49,10 @@ def test_add_parent_mass_precursormz_zero_charge(capsys):
     spectrum = add_parent_mass(spectrum_in)
 
     assert spectrum.get("parent_mass") is None, "Expected no parent mass"
-    assert "Not sufficient spectrum metadata to derive parent mass." in capsys.readouterr().out
+    assert "Not sufficient spectrum metadata to derive parent mass." in caplog.text
 
 
-def test_add_parent_mass_precursormz(capsys):
+def test_add_parent_mass_precursormz(caplog):
     """Test if parent mass is correctly derived if "pepmass" is not present."""
     mz = numpy.array([], dtype='float')
     intensities = numpy.array([], dtype='float')
@@ -66,7 +66,7 @@ def test_add_parent_mass_precursormz(capsys):
 
     assert numpy.abs(spectrum.get("parent_mass") - 445.0) < .01, "Expected parent mass of about 445.0."
     assert isinstance(spectrum.get("parent_mass"), float), "Expected parent mass to be float."
-    assert "Not sufficient spectrum metadata to derive parent mass." not in capsys.readouterr().out
+    assert "Not sufficient spectrum metadata to derive parent mass." not in caplog.text
 
 
 @pytest.mark.parametrize("adduct, expected", [("[M+2Na-H]+", 399.02884),
@@ -111,7 +111,7 @@ def test_add_parent_mass_overwrite(overwrite, expected):
         "Expected parent mass to be replaced by new value."
 
 
-def test_add_parent_mass_not_sufficient_data(capsys):
+def test_add_parent_mass_not_sufficient_data(caplog):
     """Test when there is not enough information to derive parent_mass."""
     mz = numpy.array([], dtype='float')
     intensities = numpy.array([], dtype='float')
@@ -123,7 +123,7 @@ def test_add_parent_mass_not_sufficient_data(capsys):
     spectrum = add_parent_mass(spectrum_in)
 
     assert spectrum.get("parent_mass") is None, "Expected no parent mass"
-    assert "Not sufficient spectrum metadata to derive parent mass." in capsys.readouterr().out
+    assert "Not sufficient spectrum metadata to derive parent mass." in caplog.text
 
 
 def test_empty_spectrum():
diff --git a/tests/test_clean_compound_name.py b/tests/test_clean_compound_name.py
index 0215a201..162378f4 100644
--- a/tests/test_clean_compound_name.py
+++ b/tests/test_clean_compound_name.py
@@ -1,40 +1,39 @@
 import numpy
+import pytest
 from matchms import Spectrum
 from matchms.filtering import clean_compound_name
 
 
-def test_clean_compound_name_removing_known_non_name_parts():
+@pytest.mark.parametrize("input_name, expected_name", [
+    ("MLS000863588-01!2-methoxy-3-methyl-9H-carbazole",
+     "2-methoxy-3-methyl-9H-carbazole"),
+    ("NCGC00160217-01!SOPHOCARPINE",
+     "SOPHOCARPINE"),
+    ("0072_2-Mercaptobenzothiaz",
+     "2-Mercaptobenzothiaz"),
+    (r"MassbankEU:ET110206 NPE_327.1704_12.2|N-succinylnorpheniramine",
+     "N-succinylnorpheniramine"),
+    ("Massbank:CE000307 Trans-Zeatin-[d5]",
+     "Trans-Zeatin-[d5]"),
+    ("HMDB:HMDB00500-718 4-Hydroxybenzoic acid",
+     "4-Hydroxybenzoic acid"),
+    ("MoNA:2346734 Piroxicam (Feldene)",
+     "Piroxicam (Feldene)"),
+    ("ReSpect:PS013405 option1|option2|option3",
+     "option3"),
+    ("ReSpect:PS013405 option1name",
+     "option1name"),
+    ("4,4-Dimethylcholest-8(9),24-dien-3.beta.-ol  231.2",
+     "4,4-Dimethylcholest-8(9),24-dien-3.beta.-ol")])
+def test_clean_compound_name_removing_known_parts(input_name, expected_name):
     """Test difficult but representative examples."""
-    test_name_strings = [
-        ["MLS000863588-01!2-methoxy-3-methyl-9H-carbazole",
-         "2-methoxy-3-methyl-9H-carbazole"],
-        ["NCGC00160217-01!SOPHOCARPINE",
-         "SOPHOCARPINE"],
-        ["0072_2-Mercaptobenzothiaz",
-         "2-Mercaptobenzothiaz"],
-        [r"MassbankEU:ET110206 NPE_327.1704_12.2|N-succinylnorpheniramine",
-         "N-succinylnorpheniramine"],
-        ["Massbank:CE000307 Trans-Zeatin-[d5]",
-         "Trans-Zeatin-[d5]"],
-        ["HMDB:HMDB00500-718 4-Hydroxybenzoic acid",
-         "4-Hydroxybenzoic acid"],
-        ["MoNA:2346734 Piroxicam (Feldene)",
-         "Piroxicam (Feldene)"],
-        ["ReSpect:PS013405 option1|option2|option3",
-         "option3"],
-        ["ReSpect:PS013405 option1name",
-         "option1name"],
-        ["4,4-Dimethylcholest-8(9),24-dien-3.beta.-ol  231.2",
-         "4,4-Dimethylcholest-8(9),24-dien-3.beta.-ol"],
-    ]
-    for name_strings in test_name_strings:
-        spectrum_in = Spectrum(mz=numpy.array([], dtype="float"),
-                               intensities=numpy.array([], dtype="float"),
-                               metadata={"compound_name": name_strings[0]})
-
-        spectrum = clean_compound_name(spectrum_in)
-
-        assert spectrum.get("compound_name") == name_strings[1], "Expected different cleaned name."
+    spectrum_in = Spectrum(mz=numpy.array([], dtype="float"),
+                           intensities=numpy.array([], dtype="float"),
+                           metadata={"compound_name": input_name})
+
+    spectrum = clean_compound_name(spectrum_in)
+
+    assert spectrum.get("compound_name") == expected_name, "Expected different cleaned name."
 
 
 def test_clean_compound_name_empty_name_given():
diff --git a/tests/test_derive_adduct_from_name.py b/tests/test_derive_adduct_from_name.py
index 4a2085cd..311c31ff 100644
--- a/tests/test_derive_adduct_from_name.py
+++ b/tests/test_derive_adduct_from_name.py
@@ -1,18 +1,28 @@
 import numpy
+from testfixtures import LogCapture
 from matchms import Spectrum
 from matchms.filtering import derive_adduct_from_name
+from matchms.logging_functions import reset_matchms_logger
+from matchms.logging_functions import set_matchms_logger_level
 
 
 def test_derive_adduct_from_name():
+    set_matchms_logger_level("INFO")
     spectrum_in = Spectrum(mz=numpy.array([], dtype="float"),
                            intensities=numpy.array([], dtype="float"),
                            metadata={"compound_name": "peptideXYZ [M+H+K]"})
-
-    spectrum = derive_adduct_from_name(spectrum_in)
+    with LogCapture() as log:
+        spectrum = derive_adduct_from_name(spectrum_in)
 
     assert spectrum.get("adduct") == "[M+H+K]", "Expected different adduct."
     assert spectrum.get("compound_name") == "peptideXYZ", "Expected different cleaned name."
 
+    log.check(
+        ('matchms', 'INFO', 'Removed adduct [M+H+K] from compound name.'),
+        ('matchms', 'INFO', 'Added adduct [M+H+K] to metadata.')
+    )
+    reset_matchms_logger()
+
 
 def test_derive_adduct_from_name_dont_overwrite_present_adduct():
     spectrum_in = Spectrum(mz=numpy.array([], dtype="float"),
diff --git a/tests/test_derive_inchi_from_inchikey.py b/tests/test_derive_inchi_from_inchikey.py
index 4382ba47..9e2030e6 100644
--- a/tests/test_derive_inchi_from_inchikey.py
+++ b/tests/test_derive_inchi_from_inchikey.py
@@ -1,19 +1,28 @@
 import numpy
 import pytest
+from testfixtures import LogCapture
 from matchms import Spectrum
 from matchms.filtering import derive_inchikey_from_inchi
+from matchms.logging_functions import reset_matchms_logger
+from matchms.logging_functions import set_matchms_logger_level
 
 
 def test_derive_inchikey_from_inchi():
     """Test if conversion from inchi and inchikey works."""
     pytest.importorskip("rdkit")
+    set_matchms_logger_level("INFO")
     spectrum_in = Spectrum(mz=numpy.array([], dtype='float'),
                            intensities=numpy.array([], dtype='float'),
                            metadata={"inchi": '"InChI=1S/C6H12/c1-2-4-6-5-3-1/h1-6H2"',
                                      "inchikey": 'n/a'})
 
-    spectrum = derive_inchikey_from_inchi(spectrum_in)
+    with LogCapture() as log:
+        spectrum = derive_inchikey_from_inchi(spectrum_in)
     assert spectrum.get("inchikey")[:14] == 'XDTMQSROBMDMFD', "Expected different inchikey"
+    log.check(
+        ('matchms', 'INFO', 'Added InChIKey XDTMQSROBMDMFD-UHFFFAOYSA-N to metadata (was converted from inchi)')
+    )
+    reset_matchms_logger()
 
 
 def test_empty_spectrum():
diff --git a/tests/test_derive_inchi_from_smiles.py b/tests/test_derive_inchi_from_smiles.py
index 2bb6c0fd..faffcc27 100644
--- a/tests/test_derive_inchi_from_smiles.py
+++ b/tests/test_derive_inchi_from_smiles.py
@@ -17,6 +17,19 @@ def test_derive_inchi_from_smiles():
     assert inchi == 'InChI=1S/C6H12/c1-2-4-6-5-3-1/h1-6H2', "Expected different InChI"
 
 
+def test_derive_inchi_from_defect_smiles():
+    """Test if conversion to inchi works when only smiles is given.
+    """
+    pytest.importorskip("rdkit")
+    spectrum_in = Spectrum(mz=numpy.array([], dtype='float'),
+                           intensities=numpy.array([], dtype='float'),
+                           metadata={"smiles": "CX1CCCCC1"})
+
+    spectrum = derive_inchi_from_smiles(spectrum_in)
+    inchi = spectrum.get("inchi", None)
+    assert inchi is None, "Expected no InChI"
+
+
 def test_empty_spectrum():
     spectrum_in = None
     spectrum = derive_inchi_from_smiles(spectrum_in)
diff --git a/tests/test_derive_smiles_from_inchi.py b/tests/test_derive_smiles_from_inchi.py
index 199a51e6..f3403749 100644
--- a/tests/test_derive_smiles_from_inchi.py
+++ b/tests/test_derive_smiles_from_inchi.py
@@ -1,20 +1,42 @@
 import numpy
 import pytest
+from testfixtures import LogCapture
 from matchms import Spectrum
 from matchms.filtering import derive_smiles_from_inchi
+from matchms.logging_functions import reset_matchms_logger
+from matchms.logging_functions import set_matchms_logger_level
 
 
 def test_derive_smiles_from_inchi():
     """Test if conversion to smiles works when only inchi is given.
     """
     pytest.importorskip("rdkit")
+    set_matchms_logger_level("INFO")
     spectrum_in = Spectrum(mz=numpy.array([], dtype='float'),
                            intensities=numpy.array([], dtype='float'),
                            metadata={"inchi": '"InChI=1S/C6H12/c1-2-4-6-5-3-1/h1-6H2"',
                                      "smiles": ""})
 
-    spectrum = derive_smiles_from_inchi(spectrum_in)
+    with LogCapture() as log:
+        spectrum = derive_smiles_from_inchi(spectrum_in)
     assert spectrum.get("smiles") == "C1CCCCC1", "Expected different smiles"
+    log.check(
+        ('matchms', 'INFO', 'Added smiles C1CCCCC1 to metadata (was converted from InChI)')
+    )
+    reset_matchms_logger()
+
+
+def test_derive_smiles_from_defect_inchi():
+    """Test if conversion to smiles works when only inchi is given.
+    """
+    pytest.importorskip("rdkit")
+    spectrum_in = Spectrum(mz=numpy.array([], dtype='float'),
+                           intensities=numpy.array([], dtype='float'),
+                           metadata={"inchi": '"InChI=1S/C6H12/c1-2-XA4-6-5-3-1/h1-6H2"',
+                                     "smiles": ""})
+
+    spectrum = derive_smiles_from_inchi(spectrum_in)
+    assert spectrum.get("smiles", None) == "", "Expected no smiles"
 
 
 def test_empty_spectrum():
diff --git a/tests/test_logging.py b/tests/test_logging.py
new file mode 100644
index 00000000..8bd0bfe1
--- /dev/null
+++ b/tests/test_logging.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+import logging
+import os
+from matchms.logging_functions import add_logging_to_file
+from matchms.logging_functions import reset_matchms_logger
+from matchms.logging_functions import set_matchms_logger_level
+
+
+def test_initial_logging(caplog, capsys):
+    """Test logging functionality."""
+    reset_matchms_logger()
+    logger = logging.getLogger("matchms")
+    logger.info("info test")
+    logger.warning("warning test")
+    assert logger.name == "matchms", "Expected different logger name"
+    assert logger.getEffectiveLevel() == 30, "Expected different logging level"
+    assert "info test" not in caplog.text, "Info log should not be shown."
+    assert "warning test" in caplog.text, "Warning log should have been shown."
+    assert "warning test" in capsys.readouterr().out, \
+        "Warning log should have been shown to stderr/stdout."
+    reset_matchms_logger()
+
+
+def test_set_and_reset_matchms_logger_level(caplog):
+    """Test logging functionality."""
+    logger = logging.getLogger("matchms")
+    assert logger.getEffectiveLevel() == 30, "Expected different logging level"
+
+    set_matchms_logger_level("INFO")
+    logger.debug("debug test")
+    logger.info("info test")
+
+    assert logger.name == "matchms", "Expected different logger name"
+    assert logger.getEffectiveLevel() == 20, "Expected different logging level"
+    assert "debug test" not in caplog.text, "Debug log should not be shown."
+    assert "info test" in caplog.text, "Info log should have been shown."
+
+    reset_matchms_logger()
+    assert logger.getEffectiveLevel() == 30, "Expected different logging level"
+    reset_matchms_logger()
+
+
+def test_add_logging_to_file(tmp_path, caplog, capsys):
+    """Test writing logs to file."""
+    reset_matchms_logger()
+    set_matchms_logger_level("INFO")
+    filename = os.path.join(tmp_path, "test.log")
+    add_logging_to_file(filename)
+    logger = logging.getLogger("matchms")
+    logger.info("test message no.1")
+
+    expected_log_entry = "test message no.1"
+    # Test streamed logs
+    assert expected_log_entry in caplog.text, "Expected different log message."
+    assert expected_log_entry in capsys.readouterr().out, \
+        "Expected different log message in output (stdout/stderr)."
+
+    # Test log file
+    expected_log_entry = "INFO:matchms:test_logging:test message no.1"
+    assert len(logger.handlers) == 2, "Expected two Handler"
+    assert os.path.isfile(filename), "Log file not found."
+    with open(filename, "r", encoding="utf-8") as file:
+        logs = file.read()
+    assert expected_log_entry in logs, "Expected different log file content"
+    reset_matchms_logger()
+
+
+def test_add_logging_to_file_only_file(tmp_path, capsys):
+    """Test writing logs to file."""
+    reset_matchms_logger()
+    set_matchms_logger_level("INFO")
+    filename = os.path.join(tmp_path, "test.log")
+    add_logging_to_file(filename, remove_stream_handlers=True)
+    logger = logging.getLogger("matchms")
+    logger.info("test message no.1")
+
+    # Test streamed logs
+    not_expected_log_entry = "test message no.1"
+    assert len(logger.handlers) == 1, "Expected only one Handler"
+    assert not_expected_log_entry not in capsys.readouterr().out, "Did not expect log message"
+
+    # Test log file
+    expected_log_entry = "INFO:matchms:test_logging:test message no.1"
+    assert os.path.isfile(filename), "Log file not found."
+    with open(filename, "r", encoding="utf-8") as file:
+        logs = file.read()
+    assert expected_log_entry in logs, "Expected different log file content"
+    reset_matchms_logger()
diff --git a/tests/test_reduce_to_number_of_peaks.py b/tests/test_reduce_to_number_of_peaks.py
index 7baf01d0..d20cad5c 100644
--- a/tests/test_reduce_to_number_of_peaks.py
+++ b/tests/test_reduce_to_number_of_peaks.py
@@ -1,7 +1,10 @@
 import numpy
 import pytest
+from testfixtures import LogCapture
 from matchms import Spectrum
 from matchms.filtering import reduce_to_number_of_peaks
+from matchms.logging_functions import reset_matchms_logger
+from matchms.logging_functions import set_matchms_logger_level
 
 
 def test_reduce_to_number_of_peaks_no_params():
@@ -27,6 +30,24 @@ def test_reduce_to_number_of_peaks_no_params_w_parent_mass():
     assert spectrum == spectrum_in, "Expected no changes."
 
 
+def test_reduce_to_number_of_peaks_set_to_none():
+    """Test is spectrum is set to None if not enough peaks."""
+    set_matchms_logger_level("INFO")
+    mz = numpy.array([10, 20], dtype="float")
+    intensities = numpy.array([0.5, 1], dtype="float")
+    spectrum_in = Spectrum(mz=mz, intensities=intensities,
+                           metadata={"parent_mass": 50})
+
+    with LogCapture() as log:
+        spectrum = reduce_to_number_of_peaks(spectrum_in, n_required=5)
+
+    assert spectrum is None, "Expected spectrum to be set to None."
+    log.check(
+        ('matchms', 'INFO', "Spectrum with 2 (<5) peaks was set to None.")
+    )
+    reset_matchms_logger()
+
+
 def test_reduce_to_number_of_peaks_n_max_4():
     """Test setting n_max parameter."""
     mz = numpy.array([10, 20, 30, 40, 50], dtype="float")
diff --git a/tests/test_require_precursor_below_mz.py b/tests/test_require_precursor_below_mz.py
index 8180bd63..6492cdc1 100644
--- a/tests/test_require_precursor_below_mz.py
+++ b/tests/test_require_precursor_below_mz.py
@@ -1,7 +1,10 @@
 import numpy
 import pytest
+from testfixtures import LogCapture
 from matchms import Spectrum
 from matchms.filtering import require_precursor_below_mz
+from matchms.logging_functions import reset_matchms_logger
+from matchms.logging_functions import set_matchms_logger_level
 
 
 def test_require_precursor_below_mz_no_params():
@@ -18,14 +21,20 @@ def test_require_precursor_below_mz_no_params():
 
 def test_require_precursor_below_mz_max_50():
     """Set max_mz to 50."""
+    set_matchms_logger_level("INFO")
     mz = numpy.array([10, 20, 30, 40], dtype="float")
     intensities = numpy.array([0, 1, 10, 100], dtype="float")
     spectrum_in = Spectrum(mz=mz, intensities=intensities)
     spectrum_in.set("precursor_mz", 60.)
 
-    spectrum = require_precursor_below_mz(spectrum_in, max_mz=50)
+    with LogCapture() as log:
+        spectrum = require_precursor_below_mz(spectrum_in, max_mz=50)
 
     assert spectrum is None, "Expected spectrum to be None."
+    log.check(
+        ('matchms', 'INFO', 'Spectrum with precursor_mz 60.0 (>50) was set to None.')
+    )
+    reset_matchms_logger()
 
 
 def test_if_spectrum_is_cloned():
diff --git a/tests/test_require_precursor_mz.py b/tests/test_require_precursor_mz.py
index 2ebe3d0f..fb5f6d73 100644
--- a/tests/test_require_precursor_mz.py
+++ b/tests/test_require_precursor_mz.py
@@ -1,7 +1,10 @@
 import numpy
 import pytest
+from testfixtures import LogCapture
 from matchms import Spectrum
 from matchms.filtering.require_precursor_mz import require_precursor_mz
+from matchms.logging_functions import reset_matchms_logger
+from matchms.logging_functions import set_matchms_logger_level
 
 
 def test_require_precursor_mz_pass():
@@ -18,14 +21,20 @@ def test_require_precursor_mz_pass():
 
 def test_require_precursor_mz_fail_because_zero():
     """Test if spectrum is None when precursor_mz == 0"""
+    set_matchms_logger_level("INFO")
     mz = numpy.array([10, 20, 30, 40], dtype="float")
     intensities = numpy.array([0, 1, 10, 100], dtype="float")
     spectrum_in = Spectrum(mz=mz, intensities=intensities)
     spectrum_in.set("precursor_mz", 0.0)
 
-    spectrum = require_precursor_mz(spectrum_in)
+    with LogCapture() as log:
+        spectrum = require_precursor_mz(spectrum_in)
 
     assert spectrum is None, "Expected spectrum to be None."
+    log.check(
+        ('matchms', 'INFO', 'Spectrum without precursor_mz was set to None.')
+    )
+    reset_matchms_logger()
 
 
 def test_require_precursor_mz_fail_because_below_zero():
diff --git a/tests/test_save_as_msp.py b/tests/test_save_as_msp.py
index 6970e1d1..ae4fdaa6 100644
--- a/tests/test_save_as_msp.py
+++ b/tests/test_save_as_msp.py
@@ -27,7 +27,7 @@ def data(request):
     return list(spectra)
 
 
-@pytest.yield_fixture
+@pytest.fixture
 def filename():
     with tempfile.TemporaryDirectory() as temp_dir:
         filename = os.path.join(temp_dir, "test.msp")