diff --git a/nsfarm/board/_board.py b/nsfarm/board/_board.py
index 3944042d345a89cb97cf0fbe3071bbafdc551d6f..f3375b39277448ed493fa5e492e5e9b56393fedb 100644
--- a/nsfarm/board/_board.py
+++ b/nsfarm/board/_board.py
@@ -2,6 +2,7 @@
 """
 import sys
 import time
+import logging
 import serial
 import serial.tools.miniterm
 from pexpect import fdpexpect
@@ -24,9 +25,8 @@ class Board:
         self.config = target_config
         # Open serial console to board
         self._serial = serial.Serial(self.config['serial'], 115200)
-        # TODO better and configurable logging
-        self.logfile = open("./{}.log".format(target), "wb")
-        self._pexpect = fdpexpect.fdspawn(self._serial, logfile=self.logfile)
+        self._pexpect = fdpexpect.fdspawn(self._serial)
+        self._pexpect.logfile_read = cli.PexpectLogging(logging.getLogger('{}[{}]'.format(__package__, target)))
         # Set board to some known state
         self.reset(True)  # Hold in reset state
 
diff --git a/nsfarm/cli.py b/nsfarm/cli.py
index ce27f2e05839b7f8dffdbb248a3aa79d72753c91..80534a985d48b1d08269ec11a7ac570adc992309 100644
--- a/nsfarm/cli.py
+++ b/nsfarm/cli.py
@@ -3,7 +3,7 @@
 This ensures systematic logging and access to terminals. We implement two special terminal types at the moment. We have
 support for shell and u-boot.  They differ in a way how they handle prompt and methods they provide to user.
 """
-import pexpect
+import logging
 import base64
 
 
@@ -162,6 +162,41 @@ class Uboot(Cli):
         return self._output
 
 
+class PexpectLogging:
+    """Logging for pexpect.
+
+    This emulates file object and is intended to be used with pexpect handler as logger.
+    """
+    _EXPECTED_EOL = b'\n\r'
+
+    def __init__(self, logger):
+        self._level = logging.INFO
+        self.logger = logger
+        self.linebuf = b''
+
+    def __del__(self):
+        if self.linebuf:
+            self._log(self.linebuf)
+
+    def _log(self, line):
+        self.logger.log(self._level, repr(line.rstrip(self._EXPECTED_EOL).expandtabs())[2:-1])
+
+    def write(self, buf):
+        """Standard-like file write function.
+        """
+        jbuf = self.linebuf + buf
+        lines = jbuf.splitlines(keepends=True)
+        if lines[-1] or lines[-1][-1] not in self._EXPECTED_EOL:
+            # The last line is not terminated (no new line character) so just preserve it
+            self.linebuf = lines.pop()
+        for line in lines:
+            self._log(line)
+
+    def flush(self):
+        """Standard-like flush function.
+        """
+        # Just ignore flush
+
 # Notest on some of the hacks in this file
 #
 # There are new line character matches in regular expressions. Correct one is \r\n but some serial controlles for some
diff --git a/nsfarm/lxd/_lxd.py b/nsfarm/lxd/_lxd.py
index 21263b8fae5218681b287b4c57538308c7a5136d..38e0a826e898b2df087983541a419b914ef0ba94 100644
--- a/nsfarm/lxd/_lxd.py
+++ b/nsfarm/lxd/_lxd.py
@@ -1,5 +1,6 @@
 """Internal global LXD handle
 """
+import logging
 import pylxd
 
 IMAGES_SOURCE = "https://images.linuxcontainers.org"
@@ -16,9 +17,14 @@ def _profile_device(profile, checkfunc):
 def connect():
     """Make sure that we are connected to LXD.
     """
+    # Suppress logging of pylxd components
+    logging.getLogger('ws4py').setLevel(logging.ERROR)
+    logging.getLogger('urllib3').setLevel(logging.ERROR)
+    # Initialize LXD connection to linuximages.org
     global IMAGES
     if IMAGES is None:
         IMAGES = pylxd.Client(IMAGES_SOURCE)
+    # Initialize LXD connection to local server
     global LOCAL
     if LOCAL is None:
         LOCAL = pylxd.Client()
diff --git a/nsfarm/lxd/container.py b/nsfarm/lxd/container.py
index 535c5255ece1e602236dcccfd4adb2c73d588cfd..7a6194c38b66245d77129fa7a33dc59b07640f46 100644
--- a/nsfarm/lxd/container.py
+++ b/nsfarm/lxd/container.py
@@ -6,23 +6,27 @@ import itertools
 import hashlib
 import logging
 import pexpect
+from .. import cli
 from . import _lxd
 
 IMAGE_INIT_PATH = "/nsfarm-init.sh"  # Where we deploy initialization script for image
 
 IMGS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "imgs")
+LOGGER = logging.getLogger(__package__)
 
 
 class Container():
     """Generic container handle.
     """
+    # TODO log syslog somehow
 
-    def __init__(self, img_name, devices=[], internet=True):
+    def __init__(self, img_name, devices=(), internet=True):
         self._name = img_name
         self._internet = internet
         self._devices = tuple(devices)
         self._dpath = os.path.join(IMGS_DIR, img_name)
         self._fpath = self._dpath + ".sh"
+        self._logger = logging.getLogger("{}[{}]".format(__package__, img_name))
         # Verify existence of image definition
         if not os.path.isfile(self._fpath):
             raise Exception("There seems to be no file describing image: {}".format(self._fpath))
@@ -102,7 +106,7 @@ class Container():
             self._lxd_image = _lxd.LOCAL.images.get_by_alias(self._image_alias)
             return
         # We do not have appropriate image so prepare it
-        logging.info("Bootstrapping image: %s", self._image_alias)
+        LOGGER.warning("Bootstrapping image: %s", self._image_alias)
         image_source = {
             'type': 'image',
         }
@@ -126,13 +130,14 @@ class Container():
             # TODO found other way to match reason
             if not str(elxd).endswith("This container already exists"):
                 raise
-            logging.warning("Other instance is already bootsrapping image probably. "
+            LOGGER.warning("Other instance is already bootsrapping image probably. "
                             "Waiting for following container to go away: %s", container_name)
             while _lxd.LOCAL.containers.exists(container_name):
                 time.sleep(1)
             self.prepare_image()  # possibly get created image or try again
             return
         try:
+            # TODO log boostrap process
             # Copy script and files to container
             with open(self._fpath) as file:
                 container.files.put(IMAGE_INIT_PATH, file.read(), mode=700)
@@ -180,6 +185,7 @@ class Container():
             },
         }, wait=True)
         self._lxd_container.start(wait=True)
+        self._logger.debug("Container prepared: %s", self._lxd_container.name)
         # TODO we could somehow just let it create it and return from this method and wait later on when we realy need
         # container.
 
@@ -190,6 +196,7 @@ class Container():
         """
         if self._lxd_container is None:
             return  # No cleanup is required
+        self._logger.debug("Removing container: %s", self._lxd_container.name)
         # First freeze and remove devices
         self._lxd_container.freeze(wait=True)
         self._lxd_container.devices = dict()
@@ -200,12 +207,14 @@ class Container():
         self._lxd_container.stop()
         self._lxd_container = None
 
-    def pexpect(self, shell="/bin/sh"):
-        """Returns pexpect handle for shell in container.
+    def pexpect(self, command=["/bin/sh"]):
+        """Returns pexpect handle for command running in container.
         """
-        # TODO some logging of this session
         assert self._lxd_container is not None
-        return pexpect.spawn('lxc', ["exec", self._lxd_container.name, shell])
+        self._logger.debug("Running command: %s", command)
+        pexp = pexpect.spawn('lxc', ["exec", self._lxd_container.name] + command)
+        pexp.logfile_read = cli.PexpectLogging(logging.getLogger(self._logger.name + str(command)))
+        return pexp
 
     def __enter__(self):
         self.prepare()
diff --git a/pytest.ini b/pytest.ini
index a7c66248cf4af9294650a259822b6eb1f96715e2..b509f12500db3a4199778517fe6b43c585624305 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,5 +1,8 @@
 [pytest]
 addopts = --strict-markers
+log_level = DEBUG
+log_format = %(levelname)s[%(relativeCreated)d]%(name)s %(message)s
+log_cli_format = %(levelname)s %(name)s %(message)s
 markers =
 	deploy: quick tests to be run as part of deployment process
 	board(boards): mark test to run only on specified board