diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000000000000000000000000000000000000..927313b843e2965f03890748ce23193edf43457b
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,6 @@
+[run]
+omit = 
+    pyotb/depreciation.py
+    pyotb/functions.py
+    pyotb/helpers.py
+    pyotb/install.py
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 715e8a36f4c45d0169c1a6eb5f27d3e9e9ca7c51..b91c24c775dfb90d491525bf125a6c19044bb5f4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,5 @@
 default:
+  tags: [stable]
   image: mdl4eo/otbtf:4.3.0-cpu
   interruptible: true
 
@@ -45,7 +46,7 @@ pylint:
   before_script:
     - python3 -m pip install pylint
   script:
-    - pylint $PWD/pyotb --disable=fixme
+    - pylint $PWD/pyotb --disable=fixme --ignore=functions.py
 
 codespell:
   extends: .static_analysis
@@ -74,6 +75,7 @@ test_install:
     - changes:
         - "**/*.py"
         - .gitlab-ci.yml
+        - .coveragerc
   variables:
     SPOT_IMG_URL: https://gitlab.orfeo-toolbox.org/orfeotoolbox/otb/-/raw/develop/Data/Input/SP67_FR_subset_1.tif
     PLEIADES_IMG_URL: https://gitlab.orfeo-toolbox.org/orfeotoolbox/otb/-/raw/develop/Data/Baseline/OTB/Images/prTvOrthoRectification_pleiades-1_noDEM.tif
diff --git a/README.md b/README.md
index f5aa1841c554fd75311e815b95dd4dd3a5d2892a..93f61f51e82f83a6c6caf09e72f35a450088cfd6 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
 # pyotb: Orfeo ToolBox for Python
 
-[![latest release](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/badges/release.svg)](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/releases)
-[![pipeline status](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/badges/develop/pipeline.svg)](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/commits/develop)
-[![coverage report](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/badges/develop/coverage.svg)](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb/-/commits/develop)
+[![latest release](https://forgemia.inra.fr/orfeo-toolbox/pyotb/-/badges/release.svg)](https://forgemia.inra.fr/orfeo-toolbox/pyotb/-/releases)
+[![pipeline status](https://forgemia.inra.fr/orfeo-toolbox/pyotb/badges/develop/pipeline.svg)](https://forgemia.inra.fr/orfeo-toolbox/pyotb/-/commits/develop)
+[![coverage report](https://forgemia.inra.fr/orfeo-toolbox/pyotb/badges/develop/coverage.svg)](https://forgemia.inra.fr/orfeo-toolbox/pyotb/-/commits/develop)
 [![read the docs status](https://readthedocs.org/projects/pyotb/badge/?version=master)](https://pyotb.readthedocs.io/en/master/)
 
 **pyotb** wraps the [Orfeo Toolbox](https://www.orfeo-toolbox.org/) in a pythonic, developer friendly 
diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt
index 7a36b430f1927e58becbe83fb8bb31adc0c3a71d..ff64cbe1e06b385e302e4cc6fbb944e7baf1809b 100644
--- a/RELEASE_NOTES.txt
+++ b/RELEASE_NOTES.txt
@@ -1,3 +1,9 @@
+---------------------------------------------------------------------
+2.1.0 (Oct 9, 2024) - Changes since version 2.0.2
+
+- Fix memory leak due to circular references to Output objects in list App.outputs
+- Breaking change : replaced App.outputs by a tuple of out image keys (App._out_image_keys)
+
 ---------------------------------------------------------------------
 2.0.2 (Apr 5, 2024) - Changes since version 2.0.1
 
@@ -5,13 +11,11 @@
 - Fix a bug with parameters of type "field" for vector files
 - Fix wrong output parameter key in ImageClassifier and ImageClassifierFromDeepFeatures
 
-
 ---------------------------------------------------------------------
 2.0.1 (Dec 18, 2023) - Changes since version 2.0.0
 
 - Fix a bug when writing outputs in uint8
 
-
 ---------------------------------------------------------------------
 2.0.0 (Nov 23, 2023) - Changes since version 1.5.4
 
diff --git a/doc/index.md b/doc/index.md
index 4ecfaf4462efc96263c3e39b4de2237bc6968d16..52586a42be14067af656ebd147dc204d6e848f48 100644
--- a/doc/index.md
+++ b/doc/index.md
@@ -37,7 +37,7 @@ on github or gitlab!
 Contributions are welcome !
 Open a PR/MR, or file an issue if you spot a bug or have any suggestion:
 
-- [Github](https://github.com/orfeotoolbox/pyotb) 
-- [Orfeo ToolBox GitLab instance](https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb).
+- [Github](https://github.com/orfeotoolbox/pyotb)
+- [Orfeo ToolBox GitLab instance](https://forgemia.inra.fr/orfeo-toolbox/pyotb).
 
-Thank you!
\ No newline at end of file
+Thank you!
diff --git a/doc/installation.md b/doc/installation.md
index 404d7bd7d228ec6a0e850b7dcd8ba214f57b69c3..0414989a1c4f499bb07ee1840dde9f481b0ce17a 100644
--- a/doc/installation.md
+++ b/doc/installation.md
@@ -17,7 +17,7 @@ pip install pyotb --upgrade
 For development, use the following:
 
 ```bash
-git clone https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb
+git clone https://forgemia.inra.fr/orfeo-toolbox/pyotb
 cd pyotb
 pip install -e ".[dev]"
 ```
diff --git a/doc/managing_loggers.md b/doc/managing_loggers.md
index 7220af482d781ad00fb62a69557def803b8d2d71..20a67257cb6756231a76e9e1b1be683062ff9232 100644
--- a/doc/managing_loggers.md
+++ b/doc/managing_loggers.md
@@ -4,25 +4,43 @@ Several environment variables are used in order to adjust logger level and
 behaviour. It should be set before importing pyotb.  
 
 - `OTB_LOGGER_LEVEL` : used to set the default OTB logger level.
-- `PYOTB_LOGGER_LEVEL` : used to set the pyotb logger level. if not set, 
-- `OTB_LOGGER_LEVEL` will be used.
+- `PYOTB_LOGGER_LEVEL` : used to set the pyotb logger level.
 
+If `PYOTB_LOGGER_LEVEL` isn't set, `OTB_LOGGER_LEVEL` will be used.  
 If none of those two variables is set, the logger level will be set to 'INFO'.  
 Available levels are : DEBUG, INFO, WARNING, ERROR, CRITICAL  
 
-You may also change the logger level after import (for pyotb only) with the 
-function `set_logger_level`.
+You may also change the logger level after import (for pyotb only) 
+using pyotb.logger.setLevel(level).
 
 ```python
 import pyotb
-pyotb.set_logger_level('DEBUG')
+pyotb.logger.setLevel('DEBUG')
 ```
 
-Bonus : in some cases, you may want to silence the GDAL driver logger (for 
-example you will see a lot of errors when reading GML files with OGR).  
+Bonus : in some cases, you may want to silence the GDAL driver logger 
+(for example you will see a lot of errors when reading GML files with OGR).  
 One useful trick is to redirect these logs to a file. This can be done using 
 the variable `CPL_LOG`.
 
+## Log to file
+It is possible to change the behaviour of the default pyotb logger as follow
+
+```py
+import logging
+import pyotb
+# Optional : remove default stdout handler (but OTB will still print its own log)
+pyotb.logger.handlers.pop()
+# Add file handler
+handler = logging.FileHandler("/my/log/file.log")
+handler.setLevel("DEBUG")
+pyotb.logger.addHandler(handler)
+```
+
+For more advanced configuration and to manage conflicts between several loggers, 
+see the [logging module docs](https://docs.python.org/3/howto/logging-cookbook.html) 
+and use the `dictConfig()` function to configure your own logger.  
+
 ## Named applications in logs
 
 It is possible to change an app name in order to track it easily in the logs :  
diff --git a/mkdocs.yml b/mkdocs.yml
index 88589b11d42843d02b111f71b1d0e20546412d3e..02da75c78c363d2af9fbcf5c4100e2ace43e3918 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -53,7 +53,7 @@ extra:
     tabs: true
   social:
     - icon: fontawesome/brands/gitlab
-      link: https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb
+      link: https://forgemia.inra.fr/orfeo-toolbox/pyotb
 extra_css:
   - https://gitlab.orfeo-toolbox.org/orfeotoolbox/otb/-/raw/8.1.2-rc1/Documentation/Cookbook/_static/css/otb_theme.css
   - extra.css
@@ -76,6 +76,6 @@ markdown_extensions:
 
 # Rest of the navigation.
 site_name: "pyotb: Orfeo ToolBox for Python"
-repo_url: https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb
+repo_url: https://forgemia.inra.fr/orfeo-toolbox/pyotb
 repo_name: pyotb
 docs_dir: doc/
diff --git a/pyotb/__init__.py b/pyotb/__init__.py
index 673fd2e9aab2f596d71050d84efcb77f9304668b..5e5831636cf19b8419a9d9c33afb655ca35fbab4 100644
--- a/pyotb/__init__.py
+++ b/pyotb/__init__.py
@@ -1,9 +1,9 @@
 # -*- coding: utf-8 -*-
 """This module provides convenient python wrapping of otbApplications."""
-__version__ = "2.0.2"
+__version__ = "2.1.0"
 
 from .install import install_otb
-from .helpers import logger, set_logger_level
+from .helpers import logger
 from .core import (
     OTBObject,
     App,
diff --git a/pyotb/core.py b/pyotb/core.py
index b78efd478d6c42533f78996ebfba0e70ff449fea..ee73fcbcaa91754afeb41d67b26c35aa49b2274e 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -579,7 +579,7 @@ class App(OTBObject):
         self._exports_dic = {}
         self._settings, self._auto_parameters = {}, {}
         self._time_start, self._time_end = 0.0, 0.0
-        self.data, self.outputs = {}, {}
+        self.data = {}
         self.quiet, self.frozen = quiet, frozen
 
         # Param keys and types
@@ -597,17 +597,15 @@ class App(OTBObject):
             for key in self.parameters_keys
             if self.app.GetParameterType(key) == otb.ParameterType_Choice
         }
+        self._out_image_keys = tuple(
+            key
+            for key, param in self._out_param_types.items()
+            if param == otb.ParameterType_OutputImage
+        )
 
         # Init, execute and write (auto flush only when output param was provided)
         if args or kwargs:
             self.set_parameters(*args, **kwargs)
-        # Create Output image objects
-        for key in (
-            key
-            for key, param in self._out_param_types.items()
-            if param == otb.ParameterType_OutputImage
-        ):
-            self.outputs[key] = Output(self, key, self._settings.get(key))
 
         if not self.frozen:
             self.execute()
@@ -643,8 +641,8 @@ class App(OTBObject):
         return self._all_param_types[key] in param_types
 
     def __is_multi_output(self):
-        """Check if app has multiple outputs to ensure re-execution during write()."""
-        return len(self.outputs) > 1
+        """Check if app has multiple image outputs to ensure re-execution in write()."""
+        return len(self._out_image_keys) > 1
 
     def is_input(self, key: str) -> bool:
         """Returns True if the parameter key is an input."""
@@ -745,10 +743,8 @@ class App(OTBObject):
                     f"{self.name}: error before execution,"
                     f" while setting '{key}' to '{obj}': {e})"
                 ) from e
-            # Save / update setting value and update the Output object initialized in __init__ without a filepath
+            # Save / update setting value
             self._settings[key] = obj
-            if key in self.outputs:
-                self.outputs[key].filepath = obj
             if key in self._auto_parameters:
                 del self._auto_parameters[key]
 
@@ -1104,8 +1100,8 @@ class App(OTBObject):
         if isinstance(key, str):
             if key in self.data:
                 return self.data[key]
-            if key in self.outputs:
-                return self.outputs[key]
+            if key in self._out_image_keys:
+                return Output(self, key, self._settings.get(key))
             if key in self.parameters:
                 return self.parameters[key]
             raise KeyError(f"{self.name}: unknown or undefined parameter '{key}'")
@@ -1538,7 +1534,7 @@ class Output(OTBObject):
         mkdir: bool = True,
     ):
         """Constructor for an Output object, initialized during App.__init__."""
-        self.parent_pyotb_app = pyotb_app  # keep trace of parent app
+        self.parent_pyotb_app = pyotb_app  # keep a reference to parent app
         self.param_key = param_key
         self.filepath = filepath
         if mkdir and filepath is not None:
diff --git a/pyotb/helpers.py b/pyotb/helpers.py
index 0e6ea2a06eee8b1b7e8cb7502d03b95861c4ef7f..96536155763a5acee7de8ef449d62c81902bd511 100644
--- a/pyotb/helpers.py
+++ b/pyotb/helpers.py
@@ -1,5 +1,7 @@
 """This module ensure we properly initialize pyotb, or raise SystemExit in case of broken install."""
+
 import logging
+import logging.config
 import os
 import sys
 import sysconfig
@@ -13,33 +15,36 @@ OTB_ROOT = os.environ.get("OTB_ROOT")
 DOCS_URL = "https://www.orfeo-toolbox.org/CookBook/Installation.html"
 
 # Logging
-# User can also get logger with `logging.getLogger("pyOTB")`
+# User can also get logger with `logging.getLogger("pyotb")`
 # then use pyotb.set_logger_level() to adjust logger verbosity
-logger = logging.getLogger("pyotb")
-logger_handler = logging.StreamHandler(sys.stdout)
-formatter = logging.Formatter(
-    fmt="%(asctime)s (%(levelname)-4s) [pyotb] %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
-)
-logger_handler.setFormatter(formatter)
+
 # Search for PYOTB_LOGGER_LEVEL, else use OTB_LOGGER_LEVEL as pyotb level, or fallback to INFO
 LOG_LEVEL = (
     os.environ.get("PYOTB_LOGGER_LEVEL") or os.environ.get("OTB_LOGGER_LEVEL") or "INFO"
 )
-logger.setLevel(getattr(logging, LOG_LEVEL))
-# Here it would be possible to use a different level for a specific handler
-# A more verbose one can go to text file while print only errors to stdout
-logger_handler.setLevel(getattr(logging, LOG_LEVEL))
-logger.addHandler(logger_handler)
 
+logger = logging.getLogger("pyotb")
 
-def set_logger_level(level: str):
-    """Allow user to change the current logging level.
-
-    Args:
-        level: logging level string ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
-    """
-    logger.setLevel(getattr(logging, level))
-    logger_handler.setLevel(getattr(logging, level))
+logging_cfg = {
+    "version": 1,
+    "disable_existing_loggers": False,
+    "formatters": {
+        "default": {
+            "format": "%(asctime)s (%(levelname)-4s) [pyotb] %(message)s",
+            "datefmt": "%Y-%m-%d %H:%M:%S",
+        },
+    },
+    "handlers": {
+        "stdout": {
+            "class": "logging.StreamHandler",
+            "level": "DEBUG",
+            "formatter": "default",
+            "stream": "ext://sys.stdout",
+        }
+    },
+    "loggers": {"pyotb": {"level": LOG_LEVEL, "handlers": ["stdout"]}},
+}
+logging.config.dictConfig(logging_cfg)
 
 
 def find_otb(prefix: str = OTB_ROOT, scan: bool = True):