diff --git a/devices/common/lofar_git.py b/devices/common/lofar_git.py
index 353748d985ccf72483792456cbcd8a0a5aa1056b..f4f6217280fe612fa2e9a7830c1f026c1e36a815 100644
--- a/devices/common/lofar_git.py
+++ b/devices/common/lofar_git.py
@@ -2,7 +2,7 @@ import git # pip3 install gitpython
 import os
 from functools import lru_cache
 
-def get_repo(starting_directory: str = os.path.dirname(__file__)) -> git.Repo:
+def get_repo(starting_directory: str = os.path.dirname(os.path.abspath(__file__))) -> git.Repo:
     """ Try finding the repository by traversing up the tree.
 
         By default, the repository containing this module is returned.
@@ -50,14 +50,20 @@ def get_version(repo: git.Repo = None) -> str:
     if repo is None:
         repo = get_repo()
 
-    branch = repo.active_branch
     commit = repo.commit()
     tags = {tag.commit: tag for tag in repo.tags}
 
     if commit in tags:
+        # a tag = production ready
         commit_str = "{}".format(tags[commit])
         production_ready = True
+    elif repo.head.is_detached:
+        # no active branch
+        commit_str = "<detached HEAD> [{}]".format(commit)
+        production_ready = False
     else:
+        # HEAD of a branch
+        branch = repo.active_branch
         commit_str = "{} [{}]".format(branch, commit)
         production_ready = False
 
diff --git a/devices/common/lofar_logging.py b/devices/common/lofar_logging.py
index d84a47467caf860c2839d3ac28752400ca3b2533..b0e1c2ac4d23d67522cbc984280015fa18a042b6 100644
--- a/devices/common/lofar_logging.py
+++ b/devices/common/lofar_logging.py
@@ -1,17 +1,87 @@
 import logging
 from functools import wraps
+from tango.server import Device
 import sys
-
-# Always also log the hostname because it makes the origin of the log clear.
+import traceback
 import socket
-hostname = socket.gethostname()
 
-def configure_logger(logger: logging.Logger, log_extra=None):
+from .lofar_git import get_version
+
+class TangoLoggingHandler(logging.Handler):
+    level_to_device_stream = {
+        logging.DEBUG: Device.debug_stream,
+        logging.INFO:  Device.info_stream,
+        logging.WARN:  Device.warn_stream,
+        logging.ERROR: Device.error_stream,
+        logging.FATAL: Device.fatal_stream,
+    }
+
+    def emit(self, record):
+        try:
+            if record.tango_device is None:
+                # log record is not related to any device
+                return
+        except AttributeError:
+            # log record is not annotated with a tango_device
+            return
+
+        # determine which log stream to use
+        stream = self.level_to_device_stream[record.levelno]
+
+        # send the log message to Tango
+        stream(record.tango_device, record.msg, *record.args)
+
+        self.flush()
+
+class LogAnnotator(logging.Formatter):
+    """ Annotates log records with:
+    
+        record.tango_device: the Tango Device that is executing. """
+
+    @staticmethod
+    def get_current_tango_device() -> Device:
+        """ Return the tango Device we're currently executing for, or None if it can't be detected. 
+        
+            This is derived by traversing the stack and find a Device as 'self'. In some cases,
+            this fails, for example if a separate Thread is started for a certain Device. """
+
+        for frame,lineno in traceback.walk_stack(f=None):
+            if "self" in frame.f_locals and isinstance(frame.f_locals["self"], Device):
+                return frame.f_locals["self"]
+
+        return None
+
+    def filter(self, record):
+        # annotate record with currently executing Tango device, if any
+        record.tango_device = self.get_current_tango_device()
+
+        # annotate record with the current software version
+        record.software_version = get_version()
+
+        # we just annotate, we don't filter
+        return True
+
+def configure_logger(logger: logging.Logger=None, log_extra=None):
+    """
+       Configure the given logger (or root if None) to:
+         - send logs to the ELK stack
+         - send logs to Tango
+         - send logs to stdout
+    """
+
+    # NOTE: We have to attach filters to handlers, instead to this logger,
+    # in order to have the filters be applied to descendent loggers.
+
+    if logger is None:
+        logger = logging.getLogger()
+
+    # By default we want to know everything
     logger.setLevel(logging.DEBUG)
 
     # remove spam from the OPC-UA client connection
     logging.getLogger("opcua").setLevel(logging.WARN)
 
+    # Log to ELK stack
     try:
         from logstash_async.handler import AsynchronousLogstashHandler, LogstashFormatter
 
@@ -21,32 +91,52 @@ def configure_logger(logger: logging.Logger, log_extra=None):
         # configure log messages
         formatter = LogstashFormatter(extra=log_extra, tags=["python", "lofar"])
         handler.setFormatter(formatter)
+        handler.addFilter(LogAnnotator())
 
         # install the handler
         logger.addHandler(handler)
+    except ImportError:
+        logger.exception("Cannot forward logs to ELK: logstash_async module not found.")
+    except Exception:
+        logger.exception("Cannot forward logs to ELK.")
 
-        # for now, also log to stderr
-        # Set up logging in a way that it can be understood by a human reader, be
-        # easily grep'ed, be parsed with a couple of shell commands and
-        # easily fed into an Kibana/Elastic search system.
-        handler = logging.StreamHandler()
-        formatter = logging.Formatter(fmt = '%(asctime)s.%(msecs)d %(levelname)s - HOST="{}" PID="%(process)d" TNAME="%(threadName)s" TID="%(thread)d" LOGGER="%(name)s" FILE="%(pathname)s" LINE="%(lineno)d" FUNC="%(funcName)s" MSG="%(message)s"'.format(hostname), datefmt = '%Y-%m-%dT%H:%M:%S')
-        handler.setFormatter(formatter)
+    # Log to Tango
+    try:
+        handler = TangoLoggingHandler()
+        handler.addFilter(LogAnnotator())
         logger.addHandler(handler)
     except Exception:
-        logger.exception("Cannot import or configure logstash_async module, not forwarding logs to ELK stack.")
+        logger.exception("Cannot forward logs to Tango.")
+
+
+    # for now, also log to stderr
+    # Set up logging in a way that it can be understood by a human reader, be
+    # easily grep'ed, be parsed with a couple of shell commands and
+    # easily fed into an Kibana/Elastic search system.
+    handler = logging.StreamHandler()
+
+    # Always also log the hostname because it makes the origin of the log clear.
+    hostname = socket.gethostname()
+
+    formatter = logging.Formatter(fmt = '%(asctime)s.%(msecs)d %(levelname)s - HOST="{}" DEVICE="%(tango_device)s" PID="%(process)d" TNAME="%(threadName)s" FILE="%(pathname)s" LINE="%(lineno)d" FUNC="%(funcName)s" MSG="%(message)s"'.format(hostname), datefmt = '%Y-%m-%dT%H:%M:%S')
+    handler.setFormatter(formatter)
+    handler.addFilter(LogAnnotator())
+
+    logger.addHandler(handler)
 
     return logger
 
-def device_logging_to_python(log_extra: dict = None):
-    """ Call this on a Tango Device instance or class to have your Tango Device log to python instead. """
+def device_logging_to_python():
+    """ Decorator. Call this on a Tango Device instance or class to have your Tango Device log to python instead. """
 
     def inner(cls):
-        # Create a logger that logs to ELK, dedicated for this class
-        logger = logging.getLogger(cls.__name__)
-        configure_logger(logger, log_extra)
+        # we'll be doing very weird things if this class isnt 
+        if not issubclass(cls, Device):
+            raise ValueError("device_logging_to_python decorator is to be used on Tango Device classes only.")
 
         # Monkey patch the python logger to replace the tango logger
+        logger = logging.getLogger()
+
         cls.debug_stream = logger.debug
         cls.info_stream = logger.info
         cls.warn_stream = logger.warning
@@ -54,11 +144,12 @@ def device_logging_to_python(log_extra: dict = None):
         cls.error_stream = logger.error
         cls.fatal_stream = logger.fatal
         cls.critical_stream = logger.critical
+
         return cls
 
     return inner
 
-def log_exceptions():
+def log_exceptions(logger: logging.Logger=None):
     """ Decorator that logs all exceptions that the function raises. """
 
     def wrapper(func):
@@ -67,8 +158,10 @@ def log_exceptions():
             try:
                 return func(self, *args, **kwargs)
             except Exception as e:
-                self.error_stream("Unhandled exception: {}".format(e))
-                raise e
+                (logger or logging.getLogger()).exception("Unhandled exception: %s: %s", e.__class__.__name__, e)
+
+                # we can log but we cannot hide
+                raise
 
         return inner
 
diff --git a/devices/devices/apsctl.py b/devices/devices/apsctl.py
index 25d8e1080e5ce71a05634fee392aa9eff57ef711..b555cb1b68ac5f76261c73d9723aa050316dc9d8 100644
--- a/devices/devices/apsctl.py
+++ b/devices/devices/apsctl.py
@@ -34,7 +34,7 @@ import numpy
 
 __all__ = ["APSCTL", "main"]
 
-@device_logging_to_python({"device": "APSCTL"})
+@device_logging_to_python()
 class APSCTL(hardware_device):
     """
 
@@ -195,8 +195,7 @@ def main(args=None, **kwargs):
     """Main function of the SDP module."""
 
     from devices.common.lofar_logging import configure_logger
-    import logging
-    configure_logger(logging.getLogger())
+    configure_logger()
 
     return run((APSCTL,), args=args, **kwargs)
 
diff --git a/devices/devices/pcc.py b/devices/devices/pcc.py
index f21bd39a26405e323a7842718f571626f498cd5e..0db21b41e7c609c934345e0b0dafdea9e9e08efb 100644
--- a/devices/devices/pcc.py
+++ b/devices/devices/pcc.py
@@ -35,7 +35,7 @@ from common.lofar_git import get_version
 
 __all__ = ["PCC", "main"]
 
-@device_logging_to_python({"device": "PCC"})
+@device_logging_to_python()
 class PCC(hardware_device):
     """
 
@@ -251,8 +251,7 @@ def main(args=None, **kwargs):
     """Main function of the PCC module."""
 
     from common.lofar_logging import configure_logger
-    import logging
-    configure_logger(logging.getLogger())
+    configure_logger()
 
     return run((PCC,), args=args, **kwargs)
 
diff --git a/devices/devices/sdp/sdp.py b/devices/devices/sdp/sdp.py
index 24e22bf1e8ae8b2f3b5d38265f79dcfb451ebcd8..c78c1d042fae448adac4b9e24901a053f1683fe1 100644
--- a/devices/devices/sdp/sdp.py
+++ b/devices/devices/sdp/sdp.py
@@ -35,7 +35,7 @@ import numpy
 
 __all__ = ["SDP", "main"]
 
-@device_logging_to_python({"device": "SDP"})
+@device_logging_to_python()
 class SDP(hardware_device):
     """
 
@@ -174,8 +174,7 @@ def main(args=None, **kwargs):
     """Main function of the SDP module."""
 
     from common.lofar_logging import configure_logger
-    import logging
-    configure_logger(logging.getLogger())
+    configure_logger()
 
     return run((SDP,), args=args, **kwargs)
 
diff --git a/devices/devices/sdp/sst.py b/devices/devices/sdp/sst.py
index 3961362e916e044cfa833e9929856a0c25493a85..14b9e4ba381fc195f2d3c9997e51530e83e7e048 100644
--- a/devices/devices/sdp/sst.py
+++ b/devices/devices/sdp/sst.py
@@ -37,7 +37,7 @@ import numpy
 
 __all__ = ["SST", "main"]
 
-@device_logging_to_python({"device": "SST"})
+@device_logging_to_python()
 class SST(hardware_device):
 
     # -----------------
@@ -174,8 +174,7 @@ def main(args=None, **kwargs):
     """Main function of the SST Device module."""
 
     from common.lofar_logging import configure_logger
-    import logging
-    configure_logger(logging.getLogger())
+    configure_logger()
 
     return run((SST,), args=args, **kwargs)
 
diff --git a/devices/test/util/__init__.py b/devices/test/common/__init__.py
similarity index 100%
rename from devices/test/util/__init__.py
rename to devices/test/common/__init__.py
diff --git a/devices/test/util/test_lofar_git.py b/devices/test/common/test_lofar_git.py
similarity index 90%
rename from devices/test/util/test_lofar_git.py
rename to devices/test/common/test_lofar_git.py
index 178eac535d52dfc6a2edf393a80e3c699e37a8b7..52a1c7d876fc2827757f082e0f44a0a64b1ffc78 100644
--- a/devices/test/util/test_lofar_git.py
+++ b/devices/test/common/test_lofar_git.py
@@ -35,9 +35,11 @@ class TestLofarGit(base.TestCase):
             m_is_dirty = mock.Mock()
             m_is_dirty.return_value = True
 
+            m_head = mock.Mock(is_detached=False)
+
             m_get_repo.return_value = mock.Mock(
                 active_branch="main", commit=m_commit, tags=[],
-                is_dirty=m_is_dirty)
+                is_dirty=m_is_dirty, head=m_head)
 
             # No need for special string equal in Python
             self.assertEqual("*main [123456]", lofar_git.get_version())
@@ -52,12 +54,14 @@ class TestLofarGit(base.TestCase):
             m_is_dirty = mock.Mock()
             m_is_dirty.return_value = False
 
+            m_head = mock.Mock(is_detached=False)
+
             m_tag = mock.Mock(commit="123456")
             m_tag.__str__ = mock.Mock(return_value= "version-1.2")
 
             m_get_repo.return_value = mock.Mock(
                 active_branch="main", commit=m_commit,
-                tags=[m_tag], is_dirty=m_is_dirty)
+                tags=[m_tag], is_dirty=m_is_dirty, head=m_head)
 
             self.assertEqual("version-1.2", lofar_git.get_version())
 
@@ -71,13 +75,15 @@ class TestLofarGit(base.TestCase):
         m_is_dirty = mock.Mock()
         m_is_dirty.return_value = False
 
+        m_head = mock.Mock(is_detached=False)
+
         m_tag = mock.Mock(commit="123456")
         m_tag.__str__ = mock.Mock(return_value= "version-1.2")
 
         # Now m_get_repo is mocked using a decorator
         m_get_repo.return_value = mock.Mock(
             active_branch="main", commit=m_commit,
-            tags=[m_tag], is_dirty=m_is_dirty)
+            tags=[m_tag], is_dirty=m_is_dirty, head=m_head)
 
         self.assertEqual("version-1.2", lofar_git.get_version())
 
diff --git a/devices/test/common/test_lofar_logging.py b/devices/test/common/test_lofar_logging.py
new file mode 100644
index 0000000000000000000000000000000000000000..534b2650c8d432ef7c7dae9e32448b01cc5913f3
--- /dev/null
+++ b/devices/test/common/test_lofar_logging.py
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of the LOFAR 2.0 Station Software
+#
+#
+#
+# Distributed under the terms of the APACHE license.
+# See LICENSE.txt for more info.
+
+import git
+from unittest import mock
+
+from common import lofar_logging
+import logging
+from tango.server import Device
+from tango import device_server
+
+from test import base
+
+
+class TestLofarLogging(base.TestCase):
+
+
+    def setUp(self):
+        super(TestLofarLogging, self).setUp()
+
+
+        # reset logging system
+        rootLogger = logging.getLogger()
+        rootLogger.filters = []
+        rootLogger.handlers = []
+        rootLogger.manager.loggerDict = {}
+
+        # record everything we log in memory so we can inspect it
+        class MemoryHandler(logging.Handler):
+            """ Handler that provides access to the records emitted. """
+
+            def __init__(self):
+                super().__init__()
+                self.records = []
+
+            def emit(self, record):
+                self.records.append(record)
+
+        self.memory_handler = MemoryHandler()
+        rootLogger.addHandler(self.memory_handler)
+
+
+    def test_configure_logging_basic_usage(self):
+        """ Test whether configure_logger indeed runs smoothly. """
+
+        logger = lofar_logging.configure_logger()
+
+        logger.debug("test debug")
+        logger.info("test info")
+        logger.warning("test warning")
+        logger.error("test error")
+        logger.fatal("test fatal")
+
+
+    def test_configure_logging_log_annotation(self):
+        """ Test whether log records get annotated after using configure_logger(). """
+
+        logger = lofar_logging.configure_logger()
+
+        logger.info("test")
+        self.assertIn("tango_device", self.memory_handler.records[0].__dict__)
+        self.assertIn("software_version", self.memory_handler.records[0].__dict__)
+
+
+    def test_configure_logging_uses_tango_device(self):
+        """ Test whether log records get annotated with the active Tango device after using configure_logger(), and whether logs get forwarded to it. """
+
+        logger = lofar_logging.configure_logger()
+
+        # create a Tango Device that logs something
+        class MyDevice(Device):
+            def __init__(self):
+                self.log_deeper_in_stack()
+
+            def log_deeper_in_stack(self):
+                logger.info("test")
+
+        with mock.patch.object(device_server.DeviceImpl, '__info_stream') as m_info_stream:
+            # logs in the constructor already
+            mydevice = MyDevice()
+
+            self.assertEqual(mydevice, self.memory_handler.records[0].tango_device, msg="configure_logging did not detect active Tango device")
+            self.assertEqual(1, m_info_stream.call_count, msg="configure_logger did not send logs to active Tango device")
+
+
+    def test_log_exceptions(self):
+        """ Test whether log_exceptions actually logs and reraises exceptions. """
+
+        class Foo:
+            @lofar_logging.log_exceptions()
+            def exceptionalFunction(self):
+                raise Exception("test")
+
+        with self.assertRaises(Exception, msg="log_exceptions did not reraise exception"):
+            f = Foo()
+            f.exceptionalFunction()
+
+        self.assertEqual(1, len(self.memory_handler.records), msg="log_exceptions did not log exception")
+