Add multi-host support

This refactoring includes:
- added multi-host mode support;
- added support for graceful shutdown of the service;
- added support to flush the log buffer as it fills;
- D-Bus service xyz.openbmc_project.HostLogger replaced with SIGUSR1
  signal handler;
- self diagnostic messages now registered via phosphor-logging;
- added unit tests;
- build system migrated from autotools to meson;
- source code aligned with OpenBMC conventions.

Change-Id: If6c1dfde278af685d8563450543a6587a282c7e4
Signed-off-by: Artem Senichev <a.senichev@yadro.com>
diff --git a/.clang-format b/.clang-format
index ea71ad6..52ac0a1 100644
--- a/.clang-format
+++ b/.clang-format
@@ -5,7 +5,7 @@
 AlignAfterOpenBracket: Align
 AlignConsecutiveAssignments: false
 AlignConsecutiveDeclarations: false
-AlignEscapedNewlinesLeft: false
+AlignEscapedNewlines: Right
 AlignOperands:   true
 AlignTrailingComments: true
 AllowAllParametersOfDeclarationOnNextLine: true
@@ -14,13 +14,13 @@
 AllowShortFunctionsOnASingleLine: None
 AllowShortIfStatementsOnASingleLine: false
 AllowShortLoopsOnASingleLine: false
-AlwaysBreakAfterDefinitionReturnType: None
 AlwaysBreakAfterReturnType: None
 AlwaysBreakBeforeMultilineStrings: false
-AlwaysBreakTemplateDeclarations: true
+AlwaysBreakTemplateDeclarations: Yes
 BinPackArguments: true
 BinPackParameters: true
 BraceWrapping:
+  AfterCaseLabel:  true
   AfterClass:      true
   AfterControlStatement: true
   AfterEnum:       true
@@ -29,15 +29,22 @@
   AfterObjCDeclaration: true
   AfterStruct:     true
   AfterUnion:      true
+  AfterExternBlock: true
   BeforeCatch:     true
   BeforeElse:      true
   IndentBraces:    false
+  SplitEmptyFunction:   false
+  SplitEmptyRecord:     false
+  SplitEmptyNamespace:  false
 BreakBeforeBinaryOperators: None
 BreakBeforeBraces: Custom
 BreakBeforeTernaryOperators: true
 BreakConstructorInitializers: AfterColon
+BreakInheritanceList: AfterColon
+BreakStringLiterals: true
 ColumnLimit:     80
 CommentPragmas:  '^ IWYU pragma:'
+CompactNamespaces: false
 ConstructorInitializerAllOnOneLineOrOnePerLine: false
 ConstructorInitializerIndentWidth: 4
 ContinuationIndentWidth: 4
@@ -51,17 +58,21 @@
 IncludeBlocks: Regroup
 IncludeCategories:
   - Regex:           '^[<"](gtest|gmock)'
-    Priority:        5
+    Priority:        7
   - Regex:           '^"config.h"'
     Priority:        -1
-  - Regex:           '^".*\.hpp"'
+  - Regex:           '^".*\.h"'
     Priority:        1
-  - Regex:           '^<.*\.h>'
+  - Regex:           '^".*\.hpp"'
     Priority:        2
-  - Regex:           '^<.*'
+  - Regex:           '^<.*\.h>'
     Priority:        3
-  - Regex:           '.*'
+  - Regex:           '^<.*\.hpp>'
     Priority:        4
+  - Regex:           '^<.*'
+    Priority:        5
+  - Regex:           '.*'
+    Priority:        6
 IndentCaseLabels: true
 IndentWidth:     4
 IndentWrappedFunctionNames: true
@@ -83,8 +94,13 @@
 SortIncludes:    true
 SortUsingDeclarations: true
 SpaceAfterCStyleCast: false
+SpaceAfterTemplateKeyword: true
 SpaceBeforeAssignmentOperators: true
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
 SpaceBeforeParens: ControlStatements
+SpaceBeforeRangeBasedForLoopColon: true
 SpaceInEmptyParentheses: false
 SpacesBeforeTrailingComments: 1
 SpacesInAngles:  false
@@ -92,7 +108,7 @@
 SpacesInCStyleCastParentheses: false
 SpacesInParentheses: false
 SpacesInSquareBrackets: false
-Standard:        Cpp11
+Standard:        Latest
 TabWidth:        4
 UseTab:          Never
 ...
diff --git a/Makefile.am b/Makefile.am
deleted file mode 100644
index 32adb6d..0000000
--- a/Makefile.am
+++ /dev/null
@@ -1,55 +0,0 @@
-YAML_IFACE = xyz/openbmc_project/HostLogger.interface.yaml
-DBUS_IFACE = xyz.openbmc_project.HostLogger
-GENERATED_HPP = xyz/openbmc_project/HostLogger/server.hpp
-GENERATED_CPP = xyz/openbmc_project/HostLogger/server.cpp
-
-bin_PROGRAMS = hostlogger
-
-nobase_nodist_include_HEADERS = \
-	$(GENERATED_HPP)
-
-hostlogger_SOURCES = \
-	$(GENERATED_CPP) \
-	src/main.cpp \
-	src/dbus_server.hpp \
-	src/dbus_server.cpp \
-	src/dbus_watch.hpp \
-	src/dbus_watch.cpp \
-	src/log_file.hpp \
-	src/log_file.cpp \
-	src/log_manager.hpp \
-	src/log_manager.cpp \
-	src/log_storage.hpp \
-	src/log_storage.cpp \
-	src/zlib_exception.hpp \
-	src/zlib_exception.cpp
-
-hostlogger_CXXFLAGS = $(SDBUSPLUS_CFLAGS) $(PHOSPHOR_DBUS_INTERFACES_CFLAGS)
-hostlogger_LDADD = $(SDBUSPLUS_LIBS) $(PHOSPHOR_DBUS_INTERFACES_LIBS)
-
-if DEBUG
-hostlogger_CXXFLAGS += -DDEBUG -g3 -O0
-else
-hostlogger_CXXFLAGS += -DNDEBUG
-endif  # DEBUG
-
-BUILT_SOURCES = $(GENERATED_HPP) $(GENERATED_CPP)
-CLEANFILES = $(BUILT_SOURCES)
-
-if HAVE_SYSTEMD
-SYSTEMD_UNIT = hostlogger.service
-systemdsystemunit_DATA = $(SYSTEMD_UNIT)
-CLEANFILES += $(SYSTEMD_UNIT)
-endif  # HAVE_SYSTEMD
-
-if HAVE_DREPORT
-CLEANFILES += hostlogger.dreport
-endif  # HAVE_DREPORT
-
-$(GENERATED_HPP): $(YAML_IFACE)
-	@mkdir -p $(@D)
-	$(SDBUSPLUSPLUS) -r $(srcdir) interface server-header $(DBUS_IFACE) > $@
-
-$(GENERATED_CPP): $(YAML_IFACE) $(GENERATED_HPP)
-	@mkdir -p $(@D)
-	$(SDBUSPLUSPLUS) -r $(srcdir) interface server-cpp $(DBUS_IFACE) > $@
diff --git a/README.md b/README.md
index e5fec90..5c7dd3c 100644
--- a/README.md
+++ b/README.md
@@ -4,87 +4,98 @@
 console output data, such as boot logs or Linux kernel messages printed to the
 system console.
 
+Host logs are stored in a temporary buffer and flushed to a file according to
+the policy that can be defined with service parameters. It gives the ability to
+save the last boot log and subsequent messages in separate files.
+
 ## Architecture
+
 Host Logger is a standalone service (daemon) that works on top of the
-obmc-console-server module and uses its UNIX domain socket to read the console
-output.
+obmc-console and uses its UNIX domain socket to read the console output.
+
 ```
 +-------------+                                       +----------------+
-|     Host    | State  +---------------------+ Event  |  Host Logger   |
+|    Host     | State  +---------------------+ Event  |   Host Logger  |
 |             |------->|        D-Bus        |------->|                |
 |             |        +---------------------+        | +------------+ |
 |             |                                  D +--->| Log buffer | |
 |             |        +---------------------+   a |  | +------+-----+ |
-|             |        | obmc-console-server |   t |  |        |       |
-| +---------+ |  Data  |   +-------------+   |   a |  | +------+-----+ |
+|             |        | obmc-console-server |   t |  |        V       |
+| +---------+ |  Data  |   +-------------+   |   a |  | +------------+ |
 | | console |--------->|   | UNIX socket |---------+  | |  Log file  | |
 | +---------+ |        |   +-------------+   |        | +------------+ |
 +-------------+        +---------------------+        +----------------+
 ```
-Unlike the obmc-console project, where buffer with console output is a
-"black box" and exist only as a binary byte array, the Host Logger service
-interprets the input stream: splits it into separated messages, adds a time
-stamp and pushes the message into an internal buffer.
-The buffer will be written to a file when one of the external events occurs,
-such as a request for turn on the host or direct call of the function `Flush`
-via D-Bus.
-It gives us the ability to save the last boot log and subsequent messages in
-separated files.
-The service uses a set of rules (policy) that defines how large a buffer can
-be (limit by time or message numbers), in which cases the buffer should be
-flushed to a file, and how many previous log files can be saved on a flash
-drive (log rotation).
 
+Unlike the obmc-console project, where console output is a binary byte stream,
+the Host Logger service interprets this stream: splits it into separate
+messages, adds a time stamp and pushes the message into an internal buffer.
+Maximum size of the buffer and flush conditions are controlled by service
+parameters.
+
+## Log buffer rotation policy
+
+Maximum buffer size can be defined in the service configuration using two ways:
+- Limits by size: buffer will store the last N messages, the oldest messages are
+  removed. Controlled by `BUF_MAXSIZE` option.
+- Limits by time: buffer will store messages for the last N minutes, oldest
+  messages are removed. Controlled by `BUF_MAXTIME` option.
+
+Any of these parameters can be combined.
+
+## Log buffer flush policy
+
+Messages from the buffer will be written to a file when one of the following
+events occurs:
+- Host changes its state (start, reboot or shut down). The service watches the
+  state via the D-Bus object specified in `HOST_STATE` parameter.
+- Size of the buffer reaches its limits controlled by `BUF_MAXSIZE` and
+  `BUF_MAXTIME` parameters, this mode can be activated by `FLUSH_FULL` flag.
+- Signal `SIGUSR1` is received (manual flush).
 
 ## Configuration
 
-The policy can be defined in two ways:
-- By options passed to the "configure" script during the build process, this
-  method sets up the default policy used by the service;
-- By options passed to the executable module of the service, they can be
-  specified as command line parameters inside the systemd service file
-  used to launch the service.
+Configuration of the service is loaded from environment variables, so each
+instance of the service can have its own set of parameters.
+Environment files are stored in the directory `/etc/hostlogger` and must have
+the extension `conf`. The file name is the name of the associated Host logger
+instance and the instance of the obmc-console service (e.g. `ttyVUART0`).
 
-### Limit of message buffer size
-- `--szlimit`: Set maximum number of messages stored inside the buffer. Default
-  value is `3000`. The value `0` will disable the rule.
-- `--tmlimit`: Set maximum age of a message (in hours), all messages that older
-  then the specified value will be cleaned. This rule can be used to store, for
-  example, only messages for the latest hour. Default value is `0` (rule
-  disabled).
-Both of these options can be combined together, in this case a message will
-be cleaned if it complies any of limit types. You can create an unlimited
-buffer by passing `0` for both of these options.
+### Environment variables
 
-### File write policy (buffer flush)
-- `--path`:  Set the path used to store log files, default values is
-  /var/lib/obmc/hostlogs.
-- `--flush`: Set the period (in hours) to flush buffer to a file. Default value
-  is `0`, which means that the flush operation will be started at every
-  significant host state change - power on/off or OS boot complete events.
+Any of these variables can be omitted, in which cases default values are used.
+If variable's value has an invalid format, the service fails with an error.
 
-### File rotation policy
-- `--rotate`: Set the maximum number of files stored on the flash memory.
-  Default value is `10`. The service will remove the oldest files during the
-  buffer flush procedure.
+- `SOCKET_ID`: Socket Id used for connection with the host console. This Id
+  shall match the "socket-id" parameter of obmc-console server.
+  The default value is empty (single-host mode).
 
-## D-Bus
-The service provides an implementation of a D-Bus interface
-`xyz.openbmc_project.HostLogger.service`.
-Currently it has only one method `Flush` used for manually flushing logs.
+- `BUF_MAXSIZE`: Max number of stored messages in the buffer. The default value
+  is `3000` (0=unlimited).
 
+- `BUF_MAXTIME`: Max age of stored messages in minutes. The default value is
+  `0` (unlimited).
 
-## Example
-```
-# ls /var/lib/obmc/hostlogs
-host_20181024_092655.log.gz  host_20181025_120520.log.gz
-host_20181024_114342.log.gz  host_20181025_120910.log.gz
+- `FLUSH_FULL`: Flush collected messages from buffer to a file when one of the
+  buffer limits reaches a threshold value. At least one of `BUF_MAXSIZE` or
+  `BUF_MAXTIME` must be defined. Possible values: `true` or `false`. The default
+  value is `false`.
 
-# zcat /var/lib/obmc/hostlogs/host_20181025_120910.log.gz
-[ 12:05:30 ]: >>> Log collection started at 25.10.2018 12:05:30
-[ 12:05:30 ]: --== Welcome to Hostboot v2.1-199-g38a9f30/hbicore.bin/ ==--
-[ 12:05:34 ]:   5.52713|ISTEP  6. 3 - host_init_fsi
-[ 12:05:34 ]:   6.13176|ISTEP  6. 4 - host_set_ipl_parms
-[ 12:05:34 ]:   6.13309|ISTEP  6. 5 - host_discover_targets
-...
-```
+- `HOST_STATE`: Flush collected messages from buffer to a file when the host
+  changes its state. This variable must contain a valid path to the D-Bus object
+  that provides host's state information. Object shall implement interfaces
+  `xyz.openbmc_project.State.Host` and `xyz.openbmc_project.State.OperatingSystem.Status`.
+  The default value is `/xyz/openbmc_project/state/host0`.
+
+- `OUT_DIR`: Absolute path to the output directory for log files. The default
+  value is `/var/lib/obmc/hostlogs`.
+
+- `MAX_FILES`: Log files rotation, max number of files in the output directory,
+  oldest files are removed. The default value is `10` (0=unlimited).
+
+## Multi-host support
+
+The single instance of the service can handle only one host console at a time.
+If OpenBMC has multiple hosts, the console of each host must be associated with
+its own instance of the Host Logger service. This can be achieved using the
+systemd unit template.
diff --git a/bootstrap.sh b/bootstrap.sh
deleted file mode 100755
index 50b75b7..0000000
--- a/bootstrap.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/sh
-
-AUTOCONF_FILES="Makefile.in aclocal.m4 ar-lib autom4te.cache compile \
-        config.guess config.h.in config.sub configure depcomp install-sh \
-        ltmain.sh missing *libtool test-driver"
-
-case $1 in
-    clean)
-        test -f Makefile && make maintainer-clean
-        for file in ${AUTOCONF_FILES}; do
-            find -name "$file" | xargs -r rm -rf
-        done
-        exit 0
-        ;;
-esac
-
-autoreconf -i
-echo 'Run "./configure ${CONFIGURE_FLAGS} && make"'
diff --git a/configure.ac b/configure.ac
deleted file mode 100644
index e7faf70..0000000
--- a/configure.ac
+++ /dev/null
@@ -1,103 +0,0 @@
-AC_PREREQ([2.69])
-AC_INIT([phosphor-hostlogger],
-        [m4_esyscmd_s([git describe --always --dirty --long])],
-        [https://github.com/openbmc/phosphor-hostlogger/issues])
-AC_LANG([C++])
-AM_INIT_AUTOMAKE([subdir-objects -Wall -Werror foreign])
-AM_SILENT_RULES([yes])
-
-# Check for programs
-AC_PROG_CXX
-AC_PROG_INSTALL
-AC_PROG_MAKE_SET
-AC_PROG_MKDIR_P
-AC_HEADER_STDC
-
-# Compiler flags
-AX_CXX_COMPILE_STDCXX([17], [noext])
-AX_APPEND_COMPILE_FLAGS([-fpic -Wall], [CXXFLAGS])
-
-# Checks for external dependencies
-AC_CHECK_LIB([z], [gzopen], [],
-             [AC_MSG_ERROR([zlib required but not found])])
-PKG_CHECK_MODULES([SYSTEMD], [libsystemd >= 221])
-PKG_CHECK_MODULES([PHOSPHOR_DBUS_INTERFACES], [phosphor-dbus-interfaces])
-PKG_CHECK_MODULES([SDBUSPLUS], [sdbusplus])
-AC_PATH_PROG([SDBUSPLUSPLUS], [sdbus++])
-AS_IF([test "x$SDBUSPLUSPLUS" = "x"],
-      [AC_MSG_ERROR([sdbus++ required but not found])])
-
-# systemd unit general configuration
-PKG_PROG_PKG_CONFIG
-AC_ARG_WITH([systemdsystemunitdir],
-            [AS_HELP_STRING([--with-systemdsystemunitdir=DIR],
-                            [Directory for systemd service files])], [],
-            [with_systemdsystemunitdir=auto])
-AS_IF([test "x$with_systemdsystemunitdir" = "xyes" -o "x$with_systemdsystemunitdir" = "xauto"],
-      [def_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd)
-       AS_IF([test "x$def_systemdsystemunitdir" = "x"],
-             [AS_IF([test "x$with_systemdsystemunitdir" = "xyes"],
-                    [AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])],
-                    [with_systemdsystemunitdir=no])],
-             [with_systemdsystemunitdir="$def_systemdsystemunitdir"])])
-AS_IF([test "x$with_systemdsystemunitdir" != "xno"],
-      [AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir])])
-AM_CONDITIONAL([HAVE_SYSTEMD], [test "x$with_systemdsystemunitdir" != "xno"])
-
-# systemd unit parameters
-AC_ARG_VAR([SYSTEMD_TARGET], [systemd target for starting service, default is multi-user.target])
-AS_IF([test "x$SYSTEMD_TARGET" = "x"], [SYSTEMD_TARGET=multi-user.target])
-AC_ARG_VAR([HOST_TTY], [host teletype (TTY), default is ttyVUART0])
-AS_IF([test "x$HOST_TTY" = "x"], [HOST_TTY=ttyVUART0])
-
-# Configuration property: path to the ouput directory of log files
-AC_ARG_VAR([LOG_OUTPUT_PATH], [Path used to store logs, default is /var/lib/obmc/hostlogs])
-AS_IF([test "x$LOG_OUTPUT_PATH" = "x"], [LOG_OUTPUT_PATH="/var/lib/obmc/hostlogs"])
-AC_DEFINE_UNQUOTED([LOG_OUTPUT_PATH], ["${LOG_OUTPUT_PATH}"])
-
-# Configuration property: log storage limit (maximum number of messages to store)
-AC_ARG_VAR([LOG_STORAGE_SIZE_LIMIT], [Limit number of messages in store, default is 3000])
-AS_IF([test "x$LOG_STORAGE_SIZE_LIMIT" = "x"], [LOG_STORAGE_SIZE_LIMIT=3000])
-AC_DEFINE_UNQUOTED([LOG_STORAGE_SIZE_LIMIT], [${LOG_STORAGE_SIZE_LIMIT}])
-
-# Configuration property: log storage limit (oldest time in hours)
-AC_ARG_VAR([LOG_STORAGE_TIME_LIMIT], [Limit message store by oldest time in hours, default is 0])
-AS_IF([test "x$LOG_STORAGE_TIME_LIMIT" = "x"], [LOG_STORAGE_TIME_LIMIT=0])
-AC_DEFINE_UNQUOTED([LOG_STORAGE_TIME_LIMIT], [${LOG_STORAGE_TIME_LIMIT}])
-
-# Configuration property: flush period policy.
-AC_ARG_VAR([LOG_FLUSH_PERIOD], [Set the default flush period time, default is 0])
-AS_IF([test "x$LOG_FLUSH_PERIOD" = "x"], [LOG_FLUSH_PERIOD=0])
-AC_DEFINE_UNQUOTED([LOG_FLUSH_PERIOD], [${LOG_FLUSH_PERIOD}])
-
-# Configuration property: limit for log rotation - maximum number of files to store in the log's directory
-AC_ARG_VAR([LOG_ROTATION_LIMIT], [Limit for log rotation, default is 10])
-AS_IF([test "x$LOG_ROTATION_LIMIT" = "x"], [LOG_ROTATION_LIMIT=10])
-AC_DEFINE_UNQUOTED([LOG_ROTATION_LIMIT], [${LOG_ROTATION_LIMIT}])
-
-# dreport integration
-AC_ARG_ENABLE([dreport],
-              [AS_HELP_STRING([--enable-dreport], [create dreport plugin script [no]])],
-              [case "${enableval}" in
-                 yes) dreport=true ;;
-                 no)  dreport=false ;;
-                 *)   AC_MSG_ERROR([bad value ${enableval} for --enable-dreport]) ;;
-               esac],
-              [dreport=false])
-AM_CONDITIONAL(HAVE_DREPORT, test "x$dreport" = "xtrue")
-
-# Debug build configuration
-AC_ARG_ENABLE([debug],
-              [AS_HELP_STRING([--enable-debug], [turn on debugging [no]])],
-              [case "${enableval}" in
-                 yes) debug=true ;;
-                 no)  debug=false ;;
-                 *)   AC_MSG_ERROR([bad value ${enableval} for --enable-debug]) ;;
-               esac],
-              [debug=false])
-AM_CONDITIONAL(DEBUG, test "x$debug" = "xtrue")
-
-AC_CONFIG_FILES([Makefile])
-AC_CONFIG_FILES([hostlogger.service])
-AC_CONFIG_FILES([hostlogger.dreport])
-AC_OUTPUT
diff --git a/default.conf b/default.conf
new file mode 100644
index 0000000..c751864
--- /dev/null
+++ b/default.conf
@@ -0,0 +1,7 @@
+SOCKET_ID=
+BUF_MAXSIZE=3000
+BUF_MAXTIME=0
+FLUSH_FULL=false
+HOST_STATE=/xyz/openbmc_project/state/host0
+OUT_DIR=/var/lib/obmc/hostlogs
+MAX_FILES=10
diff --git a/hostlogger.dreport b/hostlogger.dreport
new file mode 100644
index 0000000..1832453
--- /dev/null
+++ b/hostlogger.dreport
@@ -0,0 +1,25 @@
+#!/bin/bash
+#
+# config: 123 20
+# @brief: Collect host's logs
+#
+
+. ${DREPORT_INCLUDE}/functions
+
+DESCRIPTION="Host logs"
+LOGS_PATH="/var/lib/obmc/hostlogs"
+
+if [[ -d ${LOGS_PATH} ]]; then
+  # Manual flush of the log buffer for all service instances
+  INSTANCES="$(systemctl list-units --type=service --state=running --full | \
+               awk '/hostlogger@/{print $1}')"
+  for SVC in ${INSTANCES}; do
+    log_info "Flush ${SVC}..."
+    if ! systemctl kill --signal SIGUSR1 ${SVC}; then
+      log_warning "Unable to flush ${SVC}"
+    fi
+  done
+
+  # Copy log directory
+  add_copy_file "${LOGS_PATH}" "${DESCRIPTION}"
+fi
diff --git a/hostlogger.dreport.in b/hostlogger.dreport.in
deleted file mode 100644
index bbbf168..0000000
--- a/hostlogger.dreport.in
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/bash
-#
-# config: 123 20
-# @brief: Collect host's logs
-#
-
-. ${DREPORT_INCLUDE}/functions
-
-DESCRIPTION="Host logs"
-LOGS_PATH="@LOG_OUTPUT_PATH@"
-
-if [[ -d ${LOGS_PATH} ]]; then
-  # Flush currently collected messages
-  busctl --no-pager --verbose call \
-    xyz.openbmc_project.HostLogger /xyz/openbmc_project/HostLogger \
-    xyz.openbmc_project.HostLogger Flush
-  # Copy log directory
-  add_copy_file "${LOGS_PATH}" "${DESCRIPTION}"
-fi
diff --git a/hostlogger.service.in b/hostlogger.service.in
deleted file mode 100644
index 694d0b2..0000000
--- a/hostlogger.service.in
+++ /dev/null
@@ -1,13 +0,0 @@
-[Unit]
-Description=Host logging
-Wants=obmc-console@@HOST_TTY@.service
-After=obmc-console@@HOST_TTY@.service
-
-[Service]
-ExecStart=@bindir@/hostlogger
-Restart=always
-Type=dbus
-BusName=xyz.openbmc_project.HostLogger
-
-[Install]
-WantedBy=@SYSTEMD_TARGET@
diff --git a/hostlogger@.service b/hostlogger@.service
new file mode 100644
index 0000000..73ebd00
--- /dev/null
+++ b/hostlogger@.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=Host logger for %i
+BindsTo=obmc-console@%i.service
+After=obmc-console@%i.service
+
+[Service]
+ExecStart=/usr/bin/env hostlogger
+EnvironmentFile=/etc/hostlogger/%i.conf
+Restart=always
+SyslogIdentifier=hostlogger-%i
+
+[Install]
+WantedBy=multi-user.target
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..75958fe
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,57 @@
+# Rules for building with Meson
+
+project(
+  'hostlogger',
+  'cpp',
+  default_options: [
+   'warning_level=3',
+   'werror=true',
+   'cpp_std=c++17',
+  ],
+  license: 'Apache-2.0',
+)
+
+# version info from git
+version = vcs_tag(command: [ 'git', 'describe', '--always', '--dirty', '--long' ],
+                  input: 'src/version.hpp.in',
+                  output: 'version.hpp')
+
+# unit tests
+build_tests = get_option('tests')
+subdir('test')
+
+# install systemd unit template file
+systemd = dependency('systemd')
+systemd_system_unit_dir = systemd.get_pkgconfig_variable(
+  'systemdsystemunitdir',
+  define_variable: ['prefix', get_option('prefix')],
+)
+configure_file(
+  copy: true,
+  input: 'hostlogger@.service',
+  install: true,
+  install_dir: systemd_system_unit_dir,
+  output: 'hostlogger@.service'
+)
+
+executable(
+  'hostlogger',
+  [
+    version,
+    'src/config.cpp',
+    'src/dbus_loop.cpp',
+    'src/file_storage.cpp',
+    'src/host_console.cpp',
+    'src/log_buffer.cpp',
+    'src/main.cpp',
+    'src/service.cpp',
+    'src/zlib_exception.cpp',
+    'src/zlib_file.cpp',
+  ],
+  dependencies: [
+    systemd,
+    dependency('phosphor-logging'),
+    dependency('zlib'),
+  ],
+  install: true
+)
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..8b3977f
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,4 @@
+# Unit tests support
+option('tests',
+       type: 'feature',
+       description: 'Build tests')
diff --git a/src/config.cpp b/src/config.cpp
new file mode 100644
index 0000000..d7f2ee1
--- /dev/null
+++ b/src/config.cpp
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "config.hpp"
+
+#include <algorithm>
+#include <climits>
+#include <cstring>
+#include <stdexcept>
+#include <string>
+
+/**
+ * @brief Set boolean value from environment variable.
+ *
+ * @param[in] name name of environment variable
+ * @param[out] value value to set
+ *
+ * @throw std::invalid_argument in case of errors
+ */
+static void safeSet(const char* name, bool& value)
+{
+    const char* envVal = std::getenv(name);
+    if (envVal)
+    {
+        if (strcmp(envVal, "true") == 0)
+        {
+            value = true;
+        }
+        else if (strcmp(envVal, "false") == 0)
+        {
+            value = false;
+        }
+        else
+        {
+            std::string err = "Invalid value of environment variable ";
+            err += name;
+            err += ": '";
+            err += envVal;
+            err += "', expected 'true' or 'false'";
+            throw std::invalid_argument(err);
+        }
+    }
+}
+
+/**
+ * @brief Set unsigned numeric value from environment variable.
+ *
+ * @param[in] name name of environment variable
+ * @param[out] value value to set
+ *
+ * @throw std::invalid_argument in case of errors
+ */
+static void safeSet(const char* name, size_t& value)
+{
+    const char* envVal = std::getenv(name);
+    if (envVal)
+    {
+        const size_t num = strtoul(envVal, nullptr, 0);
+        if (std::all_of(envVal, envVal + strlen(envVal), isdigit) &&
+            num != ULONG_MAX)
+        {
+            value = num;
+        }
+        else
+        {
+            std::string err = "Invalid argument: ";
+            err += envVal;
+            err += ", expected unsigned numeric value";
+            throw std::invalid_argument(err);
+        }
+    }
+}
+
+/**
+ * @brief Set string value from environment variable.
+ *
+ * @param[in] name name of environment variable
+ * @param[out] value value to set
+ */
+static void safeSet(const char* name, const char*& value)
+{
+    const char* envVal = std::getenv(name);
+    if (envVal)
+    {
+        value = envVal;
+    }
+}
+
+Config::Config()
+{
+    safeSet("SOCKET_ID", socketId);
+    safeSet("BUF_MAXSIZE", bufMaxSize);
+    safeSet("BUF_MAXTIME", bufMaxTime);
+    safeSet("FLUSH_FULL", bufFlushFull);
+    safeSet("HOST_STATE", hostState);
+    safeSet("OUT_DIR", outDir);
+    safeSet("MAX_FILES", maxFiles);
+
+    // Validate parameters
+    if (bufFlushFull && !bufMaxSize && !bufMaxTime)
+    {
+        throw std::invalid_argument(
+            "Flush policy is set to save the buffer as it fills, but buffer's "
+            "limits are not defined");
+    }
+}
diff --git a/src/config.hpp b/src/config.hpp
index 5264a5e..a2aa614 100644
--- a/src/config.hpp
+++ b/src/config.hpp
@@ -1,41 +1,35 @@
-/**
- * @brief Logger configuration.
- *
- * This file is part of HostLogger project.
- *
- * Copyright (c) 2018 YADRO
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
 
 #pragma once
 
-/** @struct Config
- *  @brief Represent the configuration of the logger.
+#include <cstddef>
+
+/**
+ * @struct Config
+ * @brief Configuration of the service, initialized with default values.
  */
 struct Config
 {
-    /** @brief Path to write log files. */
-    const char* path;
-    /** @brief Storage limit (message count). */
-    int storageSizeLimit;
-    /** @brief Storage limit (max time). */
-    int storageTimeLimit;
-    /** @brief Flush policy: save logs every flushPeriod hours. */
-    int flushPeriod;
-    /** @brief Rotation limit (max files to store). */
-    int rotationLimit;
-};
+    /**
+     * @brief Constructor: load configuration from environment variables.
+     *
+     * @throw std::invalid_argument invalid format in one of the variables
+     */
+    Config();
 
-/** @brief Global logger configuration instance. */
-extern Config loggerConfig;
+    /** @brief Socket ID used for connection with host console. */
+    const char* socketId = "";
+    /** @brief Max number of messages stored inside intermediate buffer. */
+    size_t bufMaxSize = 3000;
+    /** @brief Max age of messages (in minutes) inside intermediate buffer. */
+    size_t bufMaxTime = 0;
+    /** @brief Flag indicated we need to flush console buffer as it fills. */
+    bool bufFlushFull = false;
+    /** @brief Path to D-Bus object that provides host's state information. */
+    const char* hostState = "/xyz/openbmc_project/state/host0";
+    /** @brief Absolute path to the output directory for log files. */
+    const char* outDir = "/var/lib/obmc/hostlogs";
+    /** @brief Max number of log files in the output directory. */
+    size_t maxFiles = 10;
+};
diff --git a/src/dbus_loop.cpp b/src/dbus_loop.cpp
new file mode 100644
index 0000000..e628d24
--- /dev/null
+++ b/src/dbus_loop.cpp
@@ -0,0 +1,230 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "dbus_loop.hpp"
+
+#include <phosphor-logging/log.hpp>
+
+#include <system_error>
+
+using namespace phosphor::logging;
+
+DbusLoop::DbusLoop() : bus(nullptr), event(nullptr)
+{
+    int rc;
+
+    rc = sd_bus_default(&bus);
+    if (rc < 0)
+    {
+        std::error_code ec(-rc, std::generic_category());
+        throw std::system_error(ec, "Unable to initiate D-Bus connection");
+    }
+
+    rc = sd_event_default(&event);
+    if (rc < 0)
+    {
+        sd_bus_unref(bus);
+        std::error_code ec(-rc, std::generic_category());
+        throw std::system_error(ec, "Unable to create D-Bus event loop");
+    }
+
+    rc = sd_bus_attach_event(bus, event, SD_EVENT_PRIORITY_NORMAL);
+    if (rc < 0)
+    {
+        sd_bus_unref(bus);
+        sd_event_unref(event);
+        std::error_code ec(-rc, std::generic_category());
+        throw std::system_error(ec, "Unable to attach D-Bus event");
+    }
+}
+
+DbusLoop::~DbusLoop()
+{
+    sd_bus_unref(bus);
+    sd_event_unref(event);
+}
+
+int DbusLoop::run() const
+{
+    return sd_event_loop(event);
+}
+
+void DbusLoop::stop(int code) const
+{
+    sd_event_exit(event, code);
+}
+
+void DbusLoop::addPropertyHandler(const std::string& objPath,
+                                  const WatchProperties& props,
+                                  std::function<void()> callback)
+{
+    // Add match handler
+    const int rc = sd_bus_match_signal(bus, nullptr, nullptr, objPath.c_str(),
+                                       "org.freedesktop.DBus.Properties",
+                                       "PropertiesChanged", msgCallback, this);
+    if (rc < 0)
+    {
+        std::error_code ec(-rc, std::generic_category());
+        throw std::system_error(ec, "Unable to register property watcher");
+    }
+
+    propWatch = props;
+    propHandler = callback;
+}
+
+void DbusLoop::addIoHandler(int fd, std::function<void()> callback)
+{
+    ioHandler = callback;
+    const int rc = sd_event_add_io(event, nullptr, fd, EPOLLIN,
+                                   &DbusLoop::ioCallback, this);
+    if (rc < 0)
+    {
+        std::error_code ec(-rc, std::generic_category());
+        throw std::system_error(ec, "Unable to register IO handler");
+    }
+}
+
+void DbusLoop::addSignalHandler(int signal, std::function<void()> callback)
+{
+    // Block the signal
+    sigset_t ss;
+    if (sigemptyset(&ss) < 0 || sigaddset(&ss, signal) < 0 ||
+        sigprocmask(SIG_BLOCK, &ss, nullptr) < 0)
+    {
+        std::error_code ec(errno, std::generic_category());
+        std::string err = "Unable to block signal ";
+        err += strsignal(signal);
+        throw std::system_error(ec, err);
+    }
+
+    signalHandlers.insert(std::make_pair(signal, callback));
+
+    // Register handler
+    const int rc = sd_event_add_signal(event, nullptr, signal,
+                                       &DbusLoop::signalCallback, this);
+    if (rc < 0)
+    {
+        std::error_code ec(-rc, std::generic_category());
+        std::string err = "Unable to register handler for signal ";
+        err += strsignal(signal);
+        throw std::system_error(ec, err);
+    }
+}
+
+int DbusLoop::msgCallback(sd_bus_message* msg, void* userdata,
+                          sd_bus_error* /*err*/)
+{
+    const WatchProperties& propWatch =
+        static_cast<DbusLoop*>(userdata)->propWatch;
+
+    try
+    {
+        int rc;
+
+        // Filter out by interface name
+        const char* interface;
+        rc = sd_bus_message_read(msg, "s", &interface);
+        if (rc < 0)
+        {
+            std::error_code ec(-rc, std::generic_category());
+            throw std::system_error(ec, "Unable to read interface name");
+        }
+        const auto& itIface = propWatch.find(interface);
+        if (itIface == propWatch.end())
+        {
+            return 0; // Interface is now watched
+        }
+        const Properties& props = itIface->second;
+
+        // Read message: go through list of changed properties
+        rc = sd_bus_message_enter_container(msg, SD_BUS_TYPE_ARRAY, "{sv}");
+        if (rc < 0)
+        {
+            std::error_code ec(-rc, std::generic_category());
+            throw std::system_error(ec, "Unable to open message container");
+        }
+        while ((rc = sd_bus_message_enter_container(msg, SD_BUS_TYPE_DICT_ENTRY,
+                                                    "sv")) > 0)
+        {
+            // Get property's name
+            const char* name;
+            rc = sd_bus_message_read(msg, "s", &name);
+            if (rc < 0)
+            {
+                sd_bus_message_exit_container(msg);
+                std::error_code ec(-rc, std::generic_category());
+                throw std::system_error(ec, "Unable to get property name");
+            }
+
+            // Get and check property's type
+            const char* type;
+            rc = sd_bus_message_peek_type(msg, nullptr, &type);
+            if (rc < 0 || strcmp(type, "s"))
+            {
+                sd_bus_message_exit_container(msg);
+                continue;
+            }
+
+            // Get property's value
+            const char* value;
+            rc = sd_bus_message_enter_container(msg, SD_BUS_TYPE_VARIANT, type);
+            if (rc < 0)
+            {
+                sd_bus_message_exit_container(msg);
+                std::error_code ec(-rc, std::generic_category());
+                throw std::system_error(ec, "Unable to open property value");
+            }
+            rc = sd_bus_message_read(msg, type, &value);
+            if (rc < 0)
+            {
+                sd_bus_message_exit_container(msg);
+                sd_bus_message_exit_container(msg);
+                std::error_code ec(-rc, std::generic_category());
+                throw std::system_error(ec, "Unable to get property value");
+            }
+            sd_bus_message_exit_container(msg);
+
+            // Check property name/value and handle the match
+            const auto& itProps = props.find(name);
+            if (itProps != props.end() &&
+                itProps->second.find(value) != itProps->second.end())
+            {
+                static_cast<DbusLoop*>(userdata)->propHandler();
+            }
+
+            sd_bus_message_exit_container(msg);
+        }
+        sd_bus_message_exit_container(msg);
+    }
+    catch (const std::exception& ex)
+    {
+        log<level::WARNING>(ex.what());
+    }
+
+    return 0;
+}
+
+int DbusLoop::signalCallback(sd_event_source* /*src*/,
+                             const struct signalfd_siginfo* si, void* userdata)
+{
+    DbusLoop* instance = static_cast<DbusLoop*>(userdata);
+    const auto it = instance->signalHandlers.find(si->ssi_signo);
+    if (it != instance->signalHandlers.end())
+    {
+        it->second();
+    }
+    else
+    {
+        std::string msg = "Unhandled signal ";
+        msg += strsignal(si->ssi_signo);
+        log<level::WARNING>(msg.c_str());
+    }
+    return 0;
+}
+
+int DbusLoop::ioCallback(sd_event_source* /*src*/, int /*fd*/,
+                         uint32_t /*revents*/, void* userdata)
+{
+    static_cast<DbusLoop*>(userdata)->ioHandler();
+    return 0;
+}
diff --git a/src/dbus_loop.hpp b/src/dbus_loop.hpp
new file mode 100644
index 0000000..eeb5327
--- /dev/null
+++ b/src/dbus_loop.hpp
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#pragma once
+
+#include <systemd/sd-bus.h>
+
+#include <functional>
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+
+/**
+ * @class DbusLoop
+ * @brief D-Bus based event loop.
+ */
+class DbusLoop
+{
+  public:
+    /** @brief Set of possible values of the property. */
+    using PropertyValues = std::set<std::string>;
+    /** @brief Map of properties: name -> watched values. */
+    using Properties = std::map<std::string, PropertyValues>;
+    /** @brief Map of watched properties: interface -> properties. */
+    using WatchProperties = std::map<std::string, Properties>;
+
+    DbusLoop();
+    ~DbusLoop();
+
+    /**
+     * @brief Run worker loop.
+     *
+     * @return exit code from loop
+     */
+    int run() const;
+
+    /**
+     * @brief Stop worker loop.
+     *
+     * @param[in] code exit code
+     */
+    void stop(int code) const;
+
+    /**
+     * @brief Add property change handler.
+     *
+     * @param[in] service D-Bus service name (object owner)
+     * @param[in] objPath path to the D-Bus object
+     * @param[in] props watched properties description
+     * @param[in] callback function to call when property get one the listed
+     *            values
+     *
+     * @throw std::system_error in case of errors
+     */
+    void addPropertyHandler(const std::string& objPath,
+                            const WatchProperties& props,
+                            std::function<void()> callback);
+
+    /**
+     * @brief Add IO event handler.
+     *
+     * @param[in] fd file descriptor to watch
+     * @param[in] callback function to call when IO event is occurred
+     *
+     * @throw std::system_error in case of errors
+     */
+    void addIoHandler(int fd, std::function<void()> callback);
+
+    /**
+     * @brief Add signal handler.
+     *
+     * @param[in] signal signal to watch
+     * @param[in] callback function to call when signal is triggered
+     *
+     * @throw std::system_error in case of errors
+     */
+    void addSignalHandler(int signal, std::function<void()> callback);
+
+  private:
+    /**
+     * @brief D-Bus callback: message handler.
+     *        See sd_bus_message_handler_t for details.
+     */
+    static int msgCallback(sd_bus_message* msg, void* userdata,
+                           sd_bus_error* err);
+
+    /**
+     * @brief D-Bus callback: signal handler.
+     *        See sd_event_signal_handler_t for details.
+     */
+    static int signalCallback(sd_event_source* src,
+                              const struct signalfd_siginfo* si,
+                              void* userdata);
+
+    /**
+     * @brief D-Bus callback: IO handler.
+     *        See sd_event_io_handler_t for details.
+     */
+    static int ioCallback(sd_event_source* src, int fd, uint32_t revents,
+                          void* userdata);
+
+  private:
+    /** @brief D-Bus connection. */
+    sd_bus* bus;
+    /** @brief D-Bus event loop. */
+    sd_event* event;
+
+    /** @brief Watched properties. */
+    WatchProperties propWatch;
+    /** @brief Property change handler. */
+    std::function<void()> propHandler;
+
+    /** @brief IO handler. */
+    std::function<void()> ioHandler;
+
+    /** @brief Signal handlers. */
+    std::map<int, std::function<void()>> signalHandlers;
+};
diff --git a/src/dbus_server.cpp b/src/dbus_server.cpp
deleted file mode 100644
index aba7694..0000000
--- a/src/dbus_server.cpp
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * @brief Server side implementation of the D-Bus interface
- *
- * This file is part of HostLogger project.
- *
- * Copyright (c) 2018 YADRO
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "dbus_server.hpp"
-
-#include <xyz/openbmc_project/Common/File/error.hpp>
-
-DbusServer::DbusServer(LogManager& logManager, sdbusplus::bus::bus& bus,
-                       const char* path /*= HOSTLOGGER_DBUS_PATH*/) :
-    server_inherit(bus, path),
-    logManager_(logManager)
-{
-}
-
-void DbusServer::flush()
-{
-    const int rc = logManager_.flush();
-    if (rc != 0)
-        throw sdbusplus::xyz::openbmc_project::Common::File::Error::Write();
-}
diff --git a/src/dbus_server.hpp b/src/dbus_server.hpp
deleted file mode 100644
index e058edf..0000000
--- a/src/dbus_server.hpp
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * @brief Server side implementation of the D-Bus interface
- *
- * This file is part of HostLogger project.
- *
- * Copyright (c) 2018 YADRO
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include "log_manager.hpp"
-
-#include <xyz/openbmc_project/HostLogger/server.hpp>
-
-/** @brief D-Bus interface name. */
-#define HOSTLOGGER_DBUS_IFACE "xyz.openbmc_project.HostLogger"
-/** @brief D-Bus path. */
-#define HOSTLOGGER_DBUS_PATH "/xyz/openbmc_project/HostLogger"
-
-/** @brief unique_ptr functor to release an event reference. */
-struct EventDeleter
-{
-    void operator()(sd_event* event) const
-    {
-        sd_event_unref(event);
-    }
-};
-
-/* @brief Alias 'event' to a unique_ptr type for auto-release. */
-using EventPtr = std::unique_ptr<sd_event, EventDeleter>;
-
-// Typedef for super class
-using server_inherit = sdbusplus::server::object_t<
-    sdbusplus::xyz::openbmc_project::server::HostLogger>;
-
-/** @class DbusServer
- *  @brief D-Bus service by host logger.
- */
-class DbusServer : public server_inherit
-{
-  public:
-    /** @brief Constructor.
-     *
-     *  @param[in] logManager - log manager
-     *  @param[in] bus - bus to attach to
-     *  @param[in] path - bath to attach at, optional, default is
-     * HOSTLOGGER_DBUS_PATH
-     */
-    DbusServer(LogManager& logManager, sdbusplus::bus::bus& bus,
-               const char* path = HOSTLOGGER_DBUS_PATH);
-
-    // From server_inherit
-    void flush();
-
-  private:
-    /** @brief Log manager instance. */
-    LogManager& logManager_;
-};
diff --git a/src/dbus_watch.cpp b/src/dbus_watch.cpp
deleted file mode 100644
index ed57b87..0000000
--- a/src/dbus_watch.cpp
+++ /dev/null
@@ -1,165 +0,0 @@
-/**
- * @brief D-Bus signal watcher.
- *
- * This file is part of HostLogger project.
- *
- * Copyright (c) 2018 YADRO
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "dbus_watch.hpp"
-
-#include "config.hpp"
-
-#include <chrono>
-#include <set>
-#include <string>
-
-// D-Bus path to the host state object
-#define DBUS_HOST_OBJECT_PATH "/xyz/openbmc_project/state/host0"
-
-// Macro to normalize SDBus status code:
-// positive code is not an error in the systemd dbus implementation.
-#define DBUS_RC_TO_ERR(c) (c = (c <= 0 ? -c : 0))
-
-DbusWatcher::DbusWatcher(LogManager& logManager, sdbusplus::bus::bus& bus) :
-    logManager_(logManager), bus_(bus)
-{
-}
-
-int DbusWatcher::initialize()
-{
-    int rc;
-
-    // Add IO callback for host's log stream socket
-    rc = sd_event_add_io(bus_.get_event(), NULL, logManager_.getHostLogFd(),
-                         EPOLLIN, &DbusWatcher::ioCallback, this);
-    if (DBUS_RC_TO_ERR(rc))
-    {
-        fprintf(stderr, "Unable to add IO handler: %i %s\n", rc, strerror(rc));
-        return rc;
-    }
-
-    // Add flush handler
-    if (loggerConfig.flushPeriod == 0)
-        registerEventHandler();
-    else
-        rc = registerTimerHandler();
-
-    return rc;
-}
-
-void DbusWatcher::registerEventHandler()
-{
-    conds_["xyz.openbmc_project.State.Host"] = {
-        .property = "RequestedHostTransition",
-        .values = {"xyz.openbmc_project.State.Host.Transition.On"}};
-    conds_["xyz.openbmc_project.State.OperatingSystem.Status"] = {
-        .property = "OperatingSystemState",
-        .values = {"xyz.openbmc_project.State.OperatingSystem.Status.OSStatus."
-                   "BootComplete",
-                   "xyz.openbmc_project.State.OperatingSystem.Status.OSStatus."
-                   "Inactive"}};
-    for (auto& cond : conds_)
-    {
-        cond.second.match = std::make_unique<sdbusplus::bus::match_t>(
-            bus_,
-            sdbusplus::bus::match::rules::propertiesChanged(
-                DBUS_HOST_OBJECT_PATH, cond.first),
-            [this](auto& msg) { this->hostStateHandler(msg); });
-    }
-}
-
-int DbusWatcher::registerTimerHandler()
-{
-    int rc;
-    sd_event_source* ev = NULL;
-
-    rc = sd_event_add_time(bus_.get_event(), &ev, CLOCK_MONOTONIC, UINT64_MAX,
-                           0, &DbusWatcher::timerCallback, this);
-    if (DBUS_RC_TO_ERR(rc))
-    {
-        fprintf(stderr, "Unable to add timer handler: %i %s\n", rc,
-                strerror(rc));
-        return rc;
-    }
-
-    rc = sd_event_source_set_enabled(ev, SD_EVENT_ON);
-    if (DBUS_RC_TO_ERR(rc))
-    {
-        fprintf(stderr, "Unable to enable timer handler: %i %s\n", rc,
-                strerror(rc));
-        return rc;
-    }
-
-    return setupTimer(ev);
-}
-
-int DbusWatcher::setupTimer(sd_event_source* event)
-{
-    // Get the current time and add the delta (flush period)
-    using namespace std::chrono;
-    auto now = steady_clock::now().time_since_epoch();
-    hours timeOut(loggerConfig.flushPeriod);
-    auto expireTime =
-        duration_cast<microseconds>(now) + duration_cast<microseconds>(timeOut);
-
-    // Set the time
-    int rc = sd_event_source_set_time(event, expireTime.count());
-    if (DBUS_RC_TO_ERR(rc))
-        fprintf(stderr, "Unable to set timer handler: %i %s\n", rc,
-                strerror(rc));
-
-    return rc;
-}
-
-void DbusWatcher::hostStateHandler(sdbusplus::message::message& msg)
-{
-    std::map<std::string, std::variant<std::string>> properties;
-    std::string interface;
-
-    msg.read(interface, properties);
-
-    bool needFlush = false;
-    const auto itc = conds_.find(interface);
-    if (itc != conds_.end())
-    {
-        const auto itp = properties.find(itc->second.property);
-        if (itp != properties.end())
-        {
-            const auto& propVal = std::get<std::string>(itp->second);
-            needFlush =
-                itc->second.values.find(propVal) != itc->second.values.end();
-        }
-    }
-
-    if (needFlush)
-        logManager_.flush();
-}
-
-int DbusWatcher::ioCallback(sd_event_source* /*event*/, int /*fd*/,
-                            uint32_t /*revents*/, void* data)
-{
-    DbusWatcher* instance = static_cast<DbusWatcher*>(data);
-    instance->logManager_.handleHostLog();
-    return 0;
-}
-
-int DbusWatcher::timerCallback(sd_event_source* event, uint64_t /*usec*/,
-                               void* data)
-{
-    DbusWatcher* instance = static_cast<DbusWatcher*>(data);
-    instance->logManager_.flush();
-    return instance->setupTimer(event);
-}
diff --git a/src/dbus_watch.hpp b/src/dbus_watch.hpp
deleted file mode 100644
index 6a284b4..0000000
--- a/src/dbus_watch.hpp
+++ /dev/null
@@ -1,110 +0,0 @@
-/**
- * @brief D-Bus signal watcher.
- *
- * This file is part of HostLogger project.
- *
- * Copyright (c) 2018 YADRO
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include "log_manager.hpp"
-
-#include <map>
-#include <sdbusplus/bus/match.hpp>
-#include <set>
-#include <string>
-
-/** @class DbusServer
- *  @brief D-Bus service by host logger.
- */
-class DbusWatcher
-{
-  public:
-    /** @brief Constructor.
-     *
-     *  @param[in] logManager - log manager
-     *  @param[in] bus - bus to attach to
-     */
-    DbusWatcher(LogManager& logManager, sdbusplus::bus::bus& bus);
-
-    /** @brief Initialize watcher.
-     *
-     *  @return error code, 0 if operation completed successfully
-     */
-    int initialize();
-
-  private:
-    /** @brief Register D-Bus event handler. */
-    void registerEventHandler();
-
-    /** @brief Register D-Bus timer handler.
-     *
-     *  @return error code, 0 if operation completed successfully
-     */
-    int registerTimerHandler();
-
-    /** @brief Setup D-Bus timer.
-     *
-     *  @param[in] event - event source to setup
-     *
-     *  @return error code, 0 if operation completed successfully
-     */
-    int setupTimer(sd_event_source* event);
-
-    /** @brief Callback function for host state change.
-     *
-     *  @param[in] msg - data associated with subscribed signal
-     */
-    void hostStateHandler(sdbusplus::message::message& msg);
-
-    /** @brief D-Bus IO callback used to handle incoming data on the opened
-     * file. See sd_event_io_handler_t for details.
-     */
-    static int ioCallback(sd_event_source* event, int fd, uint32_t revents,
-                          void* data);
-
-    /** @brief D-Bus timer callback used to flush log store.
-     *         See sd_event_add_time for details.
-     */
-    static int timerCallback(sd_event_source* event, uint64_t usec, void* data);
-
-  private:
-    /** @struct FlushCondition
-     *  @brief Describes flush conditions for log manager based on host state
-     * event.
-     */
-    struct FlushCondition
-    {
-        /** @brief D-Bus property name to watch. */
-        std::string property;
-        /** @brief Set of possible values for property watched. */
-        std::set<std::string> values;
-        /** @brief Match object to create D-Bus handler. */
-        std::unique_ptr<sdbusplus::bus::match_t> match;
-    };
-
-  private:
-    /** @brief Log manager instance. */
-    LogManager& logManager_;
-
-    /** @brief D-Bus bus. */
-    sdbusplus::bus::bus& bus_;
-
-    /** @brief Log storage flush conditions.
-     *         Map D-Bus interface name to condition description.
-     */
-    std::map<std::string, FlushCondition> conds_;
-};
diff --git a/src/file_storage.cpp b/src/file_storage.cpp
new file mode 100644
index 0000000..ace7a1b
--- /dev/null
+++ b/src/file_storage.cpp
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "file_storage.hpp"
+
+#include "zlib_file.hpp"
+
+#include <set>
+
+namespace fs = std::filesystem;
+
+/** @brief File extension for log files. */
+static const std::string fileExt = ".log.gz";
+
+FileStorage::FileStorage(const std::string& path, const std::string& prefix,
+                         size_t maxFiles) :
+    outDir(path),
+    filePrefix(prefix), filesLimit(maxFiles)
+{
+    // Check path
+    if (!outDir.is_absolute())
+    {
+        throw std::invalid_argument("Path must be absolute");
+    }
+    fs::create_directories(outDir);
+
+    // Normalize file name prefix
+    if (filePrefix.empty())
+    {
+        filePrefix = "host";
+    }
+}
+
+std::string FileStorage::save(const LogBuffer& buf) const
+{
+    if (buf.empty())
+    {
+        return std::string(); // Buffer is empty, nothing to save
+    }
+
+    const std::string fileName = newFile();
+    ZlibFile logFile(fileName);
+
+    // Write full datetime stamp as the first record
+    tm tmLocal;
+    localtime_r(&buf.begin()->timeStamp, &tmLocal);
+    char tmText[20]; // asciiz for YYYY-MM-DD HH:MM:SS
+    strftime(tmText, sizeof(tmText), "%F %T", &tmLocal);
+    std::string titleMsg = ">>> Log collection started at ";
+    titleMsg += tmText;
+    logFile.write(tmLocal, titleMsg);
+
+    // Write messages
+    for (const auto& msg : buf)
+    {
+        localtime_r(&msg.timeStamp, &tmLocal);
+        logFile.write(tmLocal, msg.text);
+    }
+
+    logFile.close();
+
+    rotate();
+
+    return fileName;
+}
+
+std::string FileStorage::newFile() const
+{
+    // Prepare directory
+    fs::create_directories(outDir);
+
+    // Construct log file name: {prefix}_{timestamp}[_N].{ext}
+    std::string fileName = outDir / (filePrefix + '_');
+
+    time_t tmCurrent;
+    time(&tmCurrent);
+    tm tmLocal;
+    localtime_r(&tmCurrent, &tmLocal);
+    char tmText[16]; // asciiz for YYYYMMDD_HHMMSS
+    strftime(tmText, sizeof(tmText), "%Y%m%d_%H%M%S", &tmLocal);
+    fileName += tmText;
+
+    // Handle duplicate files
+    std::string dupPostfix;
+    size_t dupCounter = 0;
+    while (fs::exists(fileName + dupPostfix + fileExt))
+    {
+        dupPostfix = '_' + std::to_string(++dupCounter);
+    }
+    fileName += dupPostfix;
+    fileName += fileExt;
+
+    return fileName;
+}
+
+void FileStorage::rotate() const
+{
+    if (!filesLimit)
+    {
+        return; // Unlimited
+    }
+
+    // Get file list to ordered set
+    std::set<std::string> logFiles;
+    for (const auto& file : fs::directory_iterator(outDir))
+    {
+        if (!fs::is_regular_file(file))
+        {
+            continue;
+        }
+        const std::string fileName = file.path().filename();
+
+        const size_t minFileNameLen = filePrefix.length() +
+                                      15 + // time stamp YYYYMMDD_HHMMSS
+                                      fileExt.length();
+        if (fileName.length() < minFileNameLen)
+        {
+            continue;
+        }
+
+        if (fileName.compare(fileName.length() - fileExt.length(),
+                             fileExt.length(), fileExt))
+        {
+            continue;
+        }
+
+        const std::string fullPrefix = filePrefix + '_';
+        if (fileName.compare(0, fullPrefix.length(), fullPrefix))
+        {
+            continue;
+        }
+
+        logFiles.insert(fileName);
+    }
+
+    // Log file has a name with a timestamp generated. The sorted set contains
+    // the oldest file on the top, remove them.
+    if (logFiles.size() > filesLimit)
+    {
+        size_t removeCount = logFiles.size() - filesLimit;
+        for (const auto& fileName : logFiles)
+        {
+            fs::remove(outDir / fileName);
+            if (!--removeCount)
+            {
+                break;
+            }
+        }
+    }
+}
diff --git a/src/file_storage.hpp b/src/file_storage.hpp
new file mode 100644
index 0000000..0292440
--- /dev/null
+++ b/src/file_storage.hpp
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#pragma once
+
+#include "log_buffer.hpp"
+
+#include <filesystem>
+
+/**
+ * @class FileStorage
+ * @brief Persistent file storage with automatic log file rotation.
+ */
+class FileStorage
+{
+  public:
+    /**
+     * @brief Constructor.
+     *
+     * @param[in] path absolute path to the output directory
+     * @param[in] prefix prefix used for log file names
+     * @param[in] maxFiles max number of log files that can be stored
+     *
+     * @throw std::exception in case of errors
+     */
+    FileStorage(const std::string& path, const std::string& prefix,
+                size_t maxFiles);
+
+    /**
+     * @brief Save log buffer to a file.
+     *
+     * @param[in] buf buffer with log message to save
+     *
+     * @throw std::exception in case of errors
+     *
+     * @return path to saved file
+     */
+    std::string save(const LogBuffer& buf) const;
+
+  private:
+    /**
+     * @brief Prepare output directory for a new log file and construct path.
+     *
+     * @throw std::exception in case of errors
+     *
+     * @return full path to the new file
+     */
+    std::string newFile() const;
+
+    /**
+     * @brief Rotate log files in the output directory by removing the oldest
+     *        logs.
+     *
+     * @throw std::exception in case of errors
+     */
+    void rotate() const;
+
+  private:
+    /** @brief Output directory. */
+    std::filesystem::path outDir;
+    /** @brief Prefix used for log file names. */
+    std::string filePrefix;
+    /** @brief Max number of log files that can be stored. */
+    size_t filesLimit;
+};
diff --git a/src/host_console.cpp b/src/host_console.cpp
new file mode 100644
index 0000000..bddc08e
--- /dev/null
+++ b/src/host_console.cpp
@@ -0,0 +1,132 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "host_console.hpp"
+
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#include <cstring>
+#include <system_error>
+
+/**
+ * @brief Base path to the console's socket.
+ *        See obmc-console for details.
+ */
+static constexpr char socketPath[] = "\0obmc-console";
+
+HostConsole::HostConsole(const std::string& socketId) :
+    socketId(socketId), socketFd(-1)
+{}
+
+HostConsole::~HostConsole()
+{
+    if (socketFd != -1)
+    {
+        close(socketFd);
+    }
+}
+
+void HostConsole::connect()
+{
+    if (socketFd != -1)
+    {
+        throw std::runtime_error("Socket already opened");
+    }
+
+    // Create socket
+    socketFd = socket(AF_UNIX, SOCK_STREAM, 0);
+    if (socketFd == -1)
+    {
+        std::error_code ec(errno ? errno : EIO, std::generic_category());
+        throw std::system_error(ec, "Unable to create socket");
+    }
+
+    // Set non-blocking mode for socket
+    int opt = 1;
+    if (ioctl(socketFd, FIONBIO, &opt))
+    {
+        std::error_code ec(errno ? errno : EIO, std::generic_category());
+        throw std::system_error(ec, "Unable to set non-blocking mode");
+    }
+
+    // Construct path to the socket file (see obmc-console for details)
+    std::string path(socketPath, socketPath + sizeof(socketPath) - 1);
+    if (!socketId.empty())
+    {
+        path += '.';
+        path += socketId;
+    }
+    if (path.length() > sizeof(sockaddr_un::sun_path))
+    {
+        throw std::invalid_argument("Invalid socket ID");
+    }
+
+    sockaddr_un sa;
+    sa.sun_family = AF_UNIX;
+    memcpy(&sa.sun_path, path.c_str(), path.length());
+
+    // Connect to host's log stream via socket.
+    // The owner of the socket (server) is obmc-console service and
+    // we have a dependency on it written in the systemd unit file, but
+    // we can't guarantee that the socket is initialized at the moment.
+    size_t connectAttempts = 60; // Number of attempts
+    const socklen_t len = sizeof(sa) - sizeof(sa.sun_path) + path.length();
+    int rc = -1;
+    while (connectAttempts--)
+    {
+        rc = ::connect(socketFd, reinterpret_cast<const sockaddr*>(&sa), len);
+        if (!rc)
+        {
+            break;
+        }
+        else
+        {
+            sleep(1); // Make 1 second pause between attempts
+        }
+    }
+    if (rc)
+    {
+        std::string err = "Unable to connect to console";
+        if (!socketId.empty())
+        {
+            err += ' ';
+            err += socketId;
+        }
+        std::error_code ec(errno ? errno : EIO, std::generic_category());
+        throw std::system_error(ec, err);
+    }
+}
+
+size_t HostConsole::read(char* buf, size_t sz) const
+{
+    ssize_t rsz = ::read(socketFd, buf, sz);
+    if (rsz < 0)
+    {
+        if (errno == EAGAIN || errno == EWOULDBLOCK)
+        {
+            // We are in non-blocking mode, so ignore these codes
+            rsz = 0;
+        }
+        else
+        {
+            std::string err = "Unable to read socket";
+            if (!socketId.empty())
+            {
+                err += ' ';
+                err += socketId;
+            }
+            std::error_code ec(errno ? errno : EIO, std::generic_category());
+            throw std::system_error(ec, err);
+        }
+    }
+
+    return static_cast<size_t>(rsz);
+}
+
+HostConsole::operator int() const
+{
+    return socketFd;
+}
diff --git a/src/host_console.hpp b/src/host_console.hpp
new file mode 100644
index 0000000..b0369af
--- /dev/null
+++ b/src/host_console.hpp
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#pragma once
+
+#include <string>
+
+/**
+ * @class HostConsole
+ * @brief Connection with host's console.
+ */
+class HostConsole
+{
+  public:
+    /**
+     * @brief Constructor.
+     *
+     * @param[in] socketId socket ID used for construction path to the socket
+     */
+    HostConsole(const std::string& socketId);
+
+    ~HostConsole();
+
+    /**
+     * @brief Connect to the host's console via socket.
+     *
+     * @throw std::invalid_argument if socket ID is invalid
+     * @throw std::system_error in case of other errors
+     */
+    void connect();
+
+    /**
+     * @brief Non-blocking read data from console's socket.
+     *
+     * @param[out] buf buffer to write the incoming data
+     * @param[in] sz size of the buffer
+     *
+     * @throw std::system_error in case of errors
+     *
+     * @return number of actually read bytes
+     */
+    size_t read(char* buf, size_t sz) const;
+
+    /** @brief Get socket file descriptor, used for watching IO. */
+    operator int() const;
+
+  private:
+    /** @brief Socket Id. */
+    std::string socketId;
+    /** @brief File descriptor of the socket. */
+    int socketFd;
+};
diff --git a/src/log_buffer.cpp b/src/log_buffer.cpp
new file mode 100644
index 0000000..f511a95
--- /dev/null
+++ b/src/log_buffer.cpp
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "log_buffer.hpp"
+
+/** @brief Check if a character is EOL symbol. */
+constexpr bool isEol(char c)
+{
+    return c == '\r' || c == '\n';
+}
+
+LogBuffer::LogBuffer(size_t maxSize, size_t maxTime) :
+    lastComplete(true), sizeLimit(maxSize), timeLimit(maxTime)
+{}
+
+void LogBuffer::append(const char* data, size_t sz)
+{
+    // Split raw data into separate messages by EOL symbols (\r or \n).
+    // Stream may not be ended with EOL, so we handle this situation by
+    // lastComplete flag.
+    size_t pos = 0;
+    while (pos < sz)
+    {
+        // Search for EOL ('\r' or '\n')
+        size_t eol = pos;
+        while (eol < sz)
+        {
+            if (isEol(data[eol]))
+            {
+                break;
+            }
+            ++eol;
+        }
+        const bool eolFound = eol < sz;
+        const char* msgText = data + pos;
+        const size_t msgLen = (eolFound ? eol : sz) - pos;
+
+        // Append message to the container
+        if (!lastComplete && !messages.empty())
+        {
+            // The last message is incomplete, add data as part of it
+            messages.back().text.append(msgText, msgLen);
+        }
+        else
+        {
+            Message msg;
+            time(&msg.timeStamp);
+            msg.text.assign(msgText, msgLen);
+            messages.push_back(msg);
+        }
+        lastComplete = eolFound;
+
+        // Move current position and skip EOL character
+        pos = eol + 1;
+        // Handle EOL sequences '\r\n' or '\n\r' as one delimiter
+        if (eolFound && pos < sz && isEol(data[pos]) && data[eol] != data[pos])
+        {
+            ++pos;
+        }
+    }
+
+    shrink();
+}
+
+void LogBuffer::setFullHandler(std::function<void()> cb)
+{
+    fullHandler = cb;
+}
+
+void LogBuffer::clear()
+{
+    messages.clear();
+    lastComplete = true;
+}
+
+bool LogBuffer::empty() const
+{
+    return messages.empty();
+}
+
+LogBuffer::container_t::const_iterator LogBuffer::begin() const
+{
+    return messages.begin();
+}
+
+LogBuffer::container_t::const_iterator LogBuffer::end() const
+{
+    return messages.end();
+}
+
+void LogBuffer::shrink()
+{
+    if (sizeLimit && messages.size() > sizeLimit)
+    {
+        if (fullHandler)
+        {
+            fullHandler();
+        }
+        while (messages.size() > sizeLimit)
+        {
+            messages.pop_front();
+        }
+    }
+    if (timeLimit && !messages.empty())
+    {
+        time_t oldest;
+        time(&oldest);
+        oldest -= timeLimit * 60 /* sec */;
+        if (messages.begin()->timeStamp < oldest)
+        {
+            if (fullHandler)
+            {
+                fullHandler();
+            }
+            while (!messages.empty() && messages.begin()->timeStamp < oldest)
+            {
+                messages.pop_front();
+            }
+        }
+    }
+}
diff --git a/src/log_buffer.hpp b/src/log_buffer.hpp
new file mode 100644
index 0000000..24e7ad7
--- /dev/null
+++ b/src/log_buffer.hpp
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#pragma once
+
+#include <ctime>
+#include <functional>
+#include <list>
+#include <string>
+
+/**
+ * @class LogBuffer
+ * @brief Container with automatic log message rotation.
+ */
+class LogBuffer
+{
+  public:
+    /**
+     * @struct Message
+     * @brief Single log message.
+     */
+    struct Message
+    {
+        /** @brief Message creation time. */
+        time_t timeStamp;
+        /** @brief Text of the message. */
+        std::string text;
+    };
+
+    using container_t = std::list<Message>;
+
+    /**
+     * @brief Constructor.
+     *
+     * @param[in] maxSize max number of messages that can be stored
+     * @param[in] maxTime max age of messages that can be stored, in minutes
+     */
+    LogBuffer(size_t maxSize, size_t maxTime);
+
+    /**
+     * @brief Add raw data from host's console output.
+     *
+     * @param[in] data pointer to raw data buffer
+     * @param[in] sz size of the buffer in bytes
+     */
+    void append(const char* data, size_t sz);
+
+    /**
+     * @brief Set handler called if buffer is full.
+     *
+     * @param[in] cb callback function
+     */
+    void setFullHandler(std::function<void()> cb);
+
+    /** @brief Clear (reset) container. */
+    void clear();
+    /** @brief Check container for empty. */
+    bool empty() const;
+    /** @brief Get container's iterator. */
+    container_t::const_iterator begin() const;
+    /** @brief Get container's iterator. */
+    container_t::const_iterator end() const;
+
+  private:
+    /** @brief Remove the oldest messages from container. */
+    void shrink();
+
+  private:
+    /** @brief Log message list. */
+    container_t messages;
+    /** @brief Flag to indicate that the last message is incomplete. */
+    bool lastComplete;
+    /** @brief Max number of messages that can be stored. */
+    size_t sizeLimit;
+    /** @brief Max age of messages (in minutes) that can be stored. */
+    size_t timeLimit;
+    /** @brief Callback function called if buffer is full. */
+    std::function<void()> fullHandler;
+};
diff --git a/src/log_file.cpp b/src/log_file.cpp
deleted file mode 100644
index 0f7a20a..0000000
--- a/src/log_file.cpp
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * @brief Log file.
- *
- * This file is part of HostLogger project.
- *
- * Copyright (c) 2020 YADRO
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "log_file.hpp"
-
-#include "zlib_exception.hpp"
-
-LogFile::LogFile(const char* fileName)
-{
-    fd_ = gzopen(fileName, "w");
-    if (fd_ == Z_NULL)
-        throw ZlibException(ZlibException::Create, Z_ERRNO, fd_, fileName);
-    fileName_ = fileName;
-}
-
-LogFile::~LogFile()
-{
-    if (fd_ != Z_NULL)
-        gzclose_w(fd_);
-}
-
-void LogFile::close()
-{
-    if (fd_ != Z_NULL)
-    {
-        const int rc = gzclose_w(fd_);
-        if (rc != Z_OK)
-            throw ZlibException(ZlibException::Close, rc, fd_, fileName_);
-        fd_ = Z_NULL;
-        fileName_.clear();
-    }
-}
-
-void LogFile::write(time_t timeStamp, const std::string& message) const
-{
-    int rc;
-
-    // Convert time stamp and write it
-    tm tmLocal;
-    localtime_r(&timeStamp, &tmLocal);
-    rc = gzprintf(fd_, "[ %02i:%02i:%02i ]: ", tmLocal.tm_hour, tmLocal.tm_min,
-                  tmLocal.tm_sec);
-    if (rc <= 0)
-        throw ZlibException(ZlibException::Write, rc, fd_, fileName_);
-
-    // Write message
-    const size_t len = message.length();
-    if (len)
-    {
-        rc = gzwrite(fd_, message.data(), static_cast<unsigned int>(len));
-        if (rc <= 0)
-            throw ZlibException(ZlibException::Write, rc, fd_, fileName_);
-    }
-
-    // Write EOL
-    rc = gzputc(fd_, '\n');
-    if (rc <= 0)
-        throw ZlibException(ZlibException::Write, rc, fd_, fileName_);
-}
diff --git a/src/log_file.hpp b/src/log_file.hpp
deleted file mode 100644
index 8494f8b..0000000
--- a/src/log_file.hpp
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * @brief Log file.
- *
- * This file is part of HostLogger project.
- *
- * Copyright (c) 2020 YADRO
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include <zlib.h>
-
-#include <ctime>
-#include <string>
-
-/** @class LogFile
- *  @brief Log file writer.
- */
-class LogFile
-{
-  public:
-    /** @brief Constructor - open new file for writing logs.
-     *
-     *  @param[in] fileName - path to the file
-     *
-     *  @throw ZlibException in case of errors
-     */
-    LogFile(const char* fileName);
-
-    ~LogFile();
-
-    LogFile(const LogFile&) = delete;
-    LogFile& operator=(const LogFile&) = delete;
-
-    /** @brief Close file.
-     *
-     *  @throw ZlibException in case of errors
-     */
-    void close();
-
-    /** @brief Write log message to file.
-     *
-     *  @param[in] timeStamp - time stamp of the log message
-     *  @param[in] message - log message text
-     *
-     *  @throw ZlibException in case of errors
-     */
-    void write(time_t timeStamp, const std::string& message) const;
-
-  private:
-    /** @brief File name. */
-    std::string fileName_;
-    /** @brief zLib file descriptor. */
-    gzFile fd_;
-};
diff --git a/src/log_manager.cpp b/src/log_manager.cpp
deleted file mode 100644
index fde2f6c..0000000
--- a/src/log_manager.cpp
+++ /dev/null
@@ -1,279 +0,0 @@
-/**
- * @brief Log manager.
- *
- * This file is part of HostLogger project.
- *
- * Copyright (c) 2018 YADRO
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "log_manager.hpp"
-
-#include "config.hpp"
-
-#include <dirent.h>
-#include <sys/ioctl.h>
-#include <sys/socket.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <sys/un.h>
-#include <unistd.h>
-
-#include <cstring>
-#include <set>
-#include <vector>
-
-// Path to the Unix Domain socket file used to read host's logs
-#define HOSTLOG_SOCKET_PATH "\0obmc-console"
-// Number of connection attempts
-#define HOSTLOG_SOCKET_ATTEMPTS 60
-// Pause between connection attempts in seconds
-#define HOSTLOG_SOCKET_PAUSE 1
-// Max buffer size to read from socket
-#define MAX_SOCKET_BUFFER_SIZE 512
-
-LogManager::LogManager() : fd_(-1)
-{
-}
-
-LogManager::~LogManager()
-{
-    closeHostLog();
-}
-
-int LogManager::openHostLog()
-{
-    int rc;
-
-    do
-    {
-        // Create socket
-        fd_ = socket(AF_UNIX, SOCK_STREAM, 0);
-        if (fd_ == -1)
-        {
-            rc = errno;
-            fprintf(stderr, "Unable to create socket: error [%i] %s\n", rc,
-                    strerror(rc));
-            break;
-        }
-
-        // Set non-blocking mode for socket
-        int opt = 1;
-        rc = ioctl(fd_, FIONBIO, &opt);
-        if (rc != 0)
-        {
-            rc = errno;
-            fprintf(stderr,
-                    "Unable to set non-blocking mode for log socket: error "
-                    "[%i] %s\n",
-                    rc, strerror(rc));
-            break;
-        }
-
-        sockaddr_un sa = {0};
-        sa.sun_family = AF_UNIX;
-        constexpr int min_path =
-            sizeof(HOSTLOG_SOCKET_PATH) < sizeof(sa.sun_path)
-                ? sizeof(HOSTLOG_SOCKET_PATH)
-                : sizeof(sa.sun_path);
-        memcpy(&sa.sun_path, HOSTLOG_SOCKET_PATH, min_path);
-
-        // Connect to host's log stream via socket.
-        // The owner of the socket (server) is obmc-console service and
-        // we have a dependency on it written in the systemd unit file, but
-        // we can't guarantee that the socket is initialized at the moment.
-        rc = -1;
-        for (int attempt = 0; rc != 0 && attempt < HOSTLOG_SOCKET_ATTEMPTS;
-             ++attempt)
-        {
-            rc = connect(fd_, reinterpret_cast<const sockaddr*>(&sa),
-                         sizeof(sa) - sizeof(sa.sun_path) +
-                             sizeof(HOSTLOG_SOCKET_PATH) - 1);
-            sleep(HOSTLOG_SOCKET_PAUSE);
-        }
-        if (rc < 0)
-        {
-            rc = errno;
-            fprintf(stderr,
-                    "Unable to connect to host log socket: error [%i] %s\n", rc,
-                    strerror(rc));
-        }
-    } while (false);
-
-    if (rc != 0)
-        closeHostLog();
-
-    return rc;
-}
-
-void LogManager::closeHostLog()
-{
-    if (fd_ != -1)
-    {
-        ::close(fd_);
-        fd_ = -1;
-    }
-}
-
-int LogManager::getHostLogFd() const
-{
-    return fd_;
-}
-
-int LogManager::handleHostLog()
-{
-    int rc = 0;
-    std::vector<char> buff(MAX_SOCKET_BUFFER_SIZE);
-    size_t readLen = MAX_SOCKET_BUFFER_SIZE;
-
-    // Read all existing data from log stream
-    while (rc == 0 && readLen != 0)
-    {
-        rc = readHostLog(&buff[0], buff.size(), readLen);
-        if (rc == 0 && readLen != 0)
-            storage_.parse(&buff[0], readLen);
-    }
-
-    return rc;
-}
-
-int LogManager::flush()
-{
-    int rc;
-
-    if (storage_.empty())
-        return 0; // Nothing to save
-
-    const std::string logFile = prepareLogPath();
-    if (logFile.empty())
-        return EIO;
-
-    rc = storage_.save(logFile.c_str());
-    if (rc != 0)
-        return rc;
-
-    storage_.clear();
-
-    // Non critical tasks, don't check returned status
-    rotateLogFiles();
-
-    return 0;
-}
-
-int LogManager::readHostLog(char* buffer, size_t bufferLen,
-                            size_t& readLen) const
-{
-    int rc = 0;
-
-    const ssize_t rsz = ::read(fd_, buffer, bufferLen);
-    if (rsz >= 0)
-        readLen = static_cast<size_t>(rsz);
-    else
-    {
-        readLen = 0;
-        if (errno != EAGAIN && errno != EWOULDBLOCK)
-        {
-            rc = errno;
-            fprintf(stderr, "Unable to read host log: error [%i] %s\n", rc,
-                    strerror(rc));
-        }
-    }
-
-    return rc;
-}
-
-std::string LogManager::prepareLogPath() const
-{
-    // Create path for logs
-    if (::access(loggerConfig.path, F_OK) != 0)
-    {
-        const std::string logPath(loggerConfig.path);
-        const size_t len = logPath.length();
-        size_t pos = 0;
-        while (pos < len - 1)
-        {
-            pos = logPath.find('/', pos + 1);
-            const std::string createPath = logPath.substr(0, pos);
-            if (::mkdir(createPath.c_str(),
-                        S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) < 0 &&
-                errno != EEXIST)
-            {
-                const int rc = errno;
-                fprintf(stderr, "Unable to create dir %s: error [%i] %s\n",
-                        createPath.c_str(), rc, strerror(rc));
-                return std::string();
-            }
-        }
-    }
-
-    // Construct log file name
-    time_t ts;
-    time(&ts);
-    tm lt = {0};
-    localtime_r(&ts, &lt);
-    char fileName[64];
-    snprintf(fileName, sizeof(fileName), "/host_%i%02i%02i_%02i%02i%02i.log.gz",
-             lt.tm_year + 1900, lt.tm_mon + 1, lt.tm_mday, lt.tm_hour,
-             lt.tm_min, lt.tm_sec);
-
-    return std::string(loggerConfig.path) + fileName;
-}
-
-int LogManager::rotateLogFiles() const
-{
-    if (loggerConfig.rotationLimit == 0)
-        return 0; // Not applicable
-
-    int rc = 0;
-
-    // Get file list to std::set
-    std::set<std::string> logFiles;
-    DIR* dh = opendir(loggerConfig.path);
-    if (!dh)
-    {
-        rc = errno;
-        fprintf(stderr, "Unable to open directory %s: error [%i] %s\n",
-                loggerConfig.path, rc, strerror(rc));
-        return rc;
-    }
-    dirent* dir;
-    while ((dir = readdir(dh)))
-    {
-        if (dir->d_type != DT_DIR)
-            logFiles.insert(dir->d_name);
-    }
-    closedir(dh);
-
-    // Log file has a name with a timestamp generated by prepareLogPath().
-    // The sorted array of names (std::set) will contain the oldest file on the
-    // top.
-    // Remove oldest files.
-    int filesToRemove =
-        static_cast<int>(logFiles.size()) - loggerConfig.rotationLimit;
-    while (rc == 0 && --filesToRemove >= 0)
-    {
-        std::string fileToRemove = loggerConfig.path;
-        fileToRemove += '/';
-        fileToRemove += *logFiles.begin();
-        if (::unlink(fileToRemove.c_str()) == -1)
-        {
-            rc = errno;
-            fprintf(stderr, "Unable to delete file %s: error [%i] %s\n",
-                    fileToRemove.c_str(), rc, strerror(rc));
-        }
-        logFiles.erase(fileToRemove);
-    }
-
-    return rc;
-}
diff --git a/src/log_manager.hpp b/src/log_manager.hpp
deleted file mode 100644
index 3513dac..0000000
--- a/src/log_manager.hpp
+++ /dev/null
@@ -1,106 +0,0 @@
-/**
- * @brief Log manager.
- *
- * This file is part of HostLogger project.
- *
- * Copyright (c) 2018 YADRO
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include "log_storage.hpp"
-
-/** @class LogManager
- *  @brief Log manager.
- *         All functions within this class are not thread-safe.
- */
-class LogManager
-{
-  public:
-    /** @brief Constructor. */
-    LogManager();
-
-    /** @brief Destructor. */
-    ~LogManager();
-
-    /** @brief Open the host's log stream.
-     *
-     *  @return error code, 0 if operation completed successfully
-     */
-    int openHostLog();
-
-    /** @brief Close the host's log stream.
-     */
-    void closeHostLog();
-
-    /** @brief Get file descriptor by host's log stream.
-     *         Descriptor can be used to register it in an external polling
-     * manager.
-     *
-     *  @return file descriptor (actually it is an opened socket)
-     */
-    int getHostLogFd() const;
-
-    /** @brief Handle incoming data from host's log stream.
-     *
-     *  @return error code, 0 if operation completed successfully
-     */
-    int handleHostLog();
-
-    /** @brief Flush log storage: save currently collected messages to a file,
-     *         reset the storage and rotate log files.
-     *
-     *  @return error code, 0 if operation completed successfully
-     */
-    int flush();
-
-  private:
-    /** @brief Read incoming data from host's log stream.
-     *
-     *  @param[out] buffer - buffer to write incoming data
-     *  @param[in] bufferLen - maximum size of the buffer in bytes
-     *  @param[out] readLen - on output contain number of bytes read from stream
-     *
-     *  @return error code, 0 if operation completed successfully
-     */
-    int readHostLog(char* buffer, size_t bufferLen, size_t& readLen) const;
-
-    /** @brief Prepare the path to save the log.
-     *         Warning: the name is used in function rotateLogFiles(),
-     *                  make sure you don't brake sorting rules.
-     *
-     * @return path to new log file including its name
-     */
-    std::string prepareLogPath() const;
-
-    /** @brief Create path for log files.
-     *
-     *  @return error code, 0 if operation completed successfully
-     */
-    int createLogPath() const;
-
-    /** @brief Rotate log files in the directory.
-     *         Function remove oldest files and keep up to maxFiles_ log files.
-     *
-     *  @return error code, 0 if operation completed successfully
-     */
-    int rotateLogFiles() const;
-
-  private:
-    /** @brief Log storage. */
-    LogStorage storage_;
-    /** @brief File descriptor of the input log stream. */
-    int fd_;
-};
diff --git a/src/log_storage.cpp b/src/log_storage.cpp
deleted file mode 100644
index ff9b213..0000000
--- a/src/log_storage.cpp
+++ /dev/null
@@ -1,151 +0,0 @@
-/**
- * @brief Log storage.
- *
- * This file is part of HostLogger project.
- *
- * Copyright (c) 2018 YADRO
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "log_storage.hpp"
-
-#include "config.hpp"
-#include "log_file.hpp"
-
-#include <fcntl.h>
-#include <string.h>
-#include <sys/stat.h>
-#include <unistd.h>
-
-#include <exception>
-
-void LogStorage::parse(const char* data, size_t len)
-{
-    // Split log stream to separate messages.
-    // Stream may not be ended with EOL, so we handle this situation by
-    // last_complete_ flag.
-    size_t pos = 0;
-    while (pos < len)
-    {
-        // Search for EOL ('\n')
-        size_t eol = pos;
-        while (eol < len && data[eol] != '\n')
-            ++eol;
-        const bool eolFound = eol < len;
-        const char* msgText = data + pos;
-        size_t msgLen = (eolFound ? eol : len) - pos;
-
-        // Remove '\r' from the end of message
-        while (msgLen && msgText[msgLen - 1] == '\r')
-            --msgLen;
-
-        // Append message to store
-        if (msgLen)
-            append(msgText, msgLen);
-
-        pos = eol + 1; // Skip '\n'
-        last_complete_ = eolFound;
-    }
-}
-
-void LogStorage::append(const char* msg, size_t len)
-{
-    if (!last_complete_)
-    {
-        // The last message is incomplete, add msg as part of it
-        if (!messages_.empty())
-        {
-            Message& last_msg = *messages_.rbegin();
-            last_msg.text.append(msg, len);
-        }
-    }
-    else
-    {
-        Message new_msg;
-        time(&new_msg.timeStamp);
-        new_msg.text.assign(msg, len);
-        messages_.push_back(new_msg);
-        shrink();
-    }
-}
-
-void LogStorage::clear()
-{
-    messages_.clear();
-    last_complete_ = true;
-}
-
-bool LogStorage::empty() const
-{
-    return messages_.empty();
-}
-
-int LogStorage::save(const char* fileName) const
-{
-    int rc = 0;
-
-    if (empty())
-    {
-        printf("No messages to write\n");
-        return 0;
-    }
-
-    try
-    {
-        LogFile log(fileName);
-
-        // Write full datetime stamp as the first record
-        const time_t& tmStart = messages_.begin()->timeStamp;
-        tm tmLocal;
-        localtime_r(&tmStart, &tmLocal);
-        char tmText[20]; // size of "%F %T" asciiz (YYYY-MM-DD HH:MM:SS)
-        strftime(tmText, sizeof(tmText), "%F %T", &tmLocal);
-        std::string titleMsg = ">>> Log collection started at ";
-        titleMsg += tmText;
-        log.write(tmStart, titleMsg);
-
-        // Write messages
-        for (auto it = messages_.begin(); it != messages_.end(); ++it)
-            log.write(it->timeStamp, it->text);
-
-        log.close();
-    }
-    catch (std::exception& e)
-    {
-        rc = EIO;
-        fprintf(stderr, "%s\n", e.what());
-    }
-
-    return rc;
-}
-
-void LogStorage::shrink()
-{
-    if (loggerConfig.storageSizeLimit)
-    {
-        while (messages_.size() >
-               static_cast<size_t>(loggerConfig.storageSizeLimit))
-            messages_.pop_front();
-    }
-    if (loggerConfig.storageTimeLimit)
-    {
-        // Get time for N hours ago
-        time_t oldestTimeStamp;
-        time(&oldestTimeStamp);
-        oldestTimeStamp -= loggerConfig.storageTimeLimit * 60 * 60;
-        while (!messages_.empty() &&
-               messages_.begin()->timeStamp < oldestTimeStamp)
-            messages_.pop_front();
-    }
-}
diff --git a/src/log_storage.hpp b/src/log_storage.hpp
deleted file mode 100644
index 5361c88..0000000
--- a/src/log_storage.hpp
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * @brief Log storage.
- *
- * This file is part of HostLogger project.
- *
- * Copyright (c) 2018 YADRO
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include <zlib.h>
-
-#include <ctime>
-#include <list>
-#include <string>
-
-/** @class LogStorage
- *  @brief Log storage implementation.
- *         All functions within this class are not thread-safe.
- */
-class LogStorage
-{
-  public:
-    /** @brief Parse input log stream and append messages to the storage.
-     *
-     *  @param[in] data - pointer to the message buffer
-     *  @param[in] len - length of the buffer in bytes
-     */
-    void parse(const char* data, size_t len);
-
-    /** @brief Clear (reset) storage. */
-    void clear();
-
-    /** @brief Check storage for empty.
-     *
-     *  @return true if storage is empty
-     */
-    bool empty() const;
-
-    /** @brief Save messages from storage to the specified file.
-     *
-     *  @param[in] fileName - path to the file
-     *
-     *  @return error code, 0 if operation completed successfully
-     */
-    int save(const char* fileName) const;
-
-  private:
-    /** @struct Message
-     *  @brief Represent log message (single line from host log).
-     */
-    struct Message
-    {
-        /** @brief Timestamp (message creation time). */
-        time_t timeStamp;
-        /** @brief Text of the message. */
-        std::string text;
-    };
-
-    /** @brief Append new message to the storage.
-     *
-     *  @param[in] msg - pointer to the message buffer
-     *  @param[in] len - length of the buffer in bytes
-     */
-    void append(const char* msg, size_t len);
-
-    /** @brief Shrink storage by removing oldest messages. */
-    void shrink();
-
-  private:
-    /** @brief List of messages. */
-    std::list<Message> messages_;
-    /** @brief Flag to indicate that the last message is incomplete. */
-    bool last_complete_ = true;
-};
diff --git a/src/main.cpp b/src/main.cpp
index 89b4f58..cb8d2bc 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,201 +1,77 @@
-/**
- * @brief Host logger service entry point.
- *
- * This file is part of HostLogger project.
- *
- * Copyright (c) 2018 YADRO
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
 
 #include "config.hpp"
-#include "dbus_server.hpp"
-#include "dbus_watch.hpp"
-#include "log_manager.hpp"
+#include "service.hpp"
+#include "version.hpp"
 
 #include <getopt.h>
 
-#include <climits>
-#include <cstdio>
-#include <cstdlib>
-
-// Global logger configuration instance
-Config loggerConfig = {.path = LOG_OUTPUT_PATH,
-                       .storageSizeLimit = LOG_STORAGE_SIZE_LIMIT,
-                       .storageTimeLimit = LOG_STORAGE_TIME_LIMIT,
-                       .flushPeriod = LOG_FLUSH_PERIOD,
-                       .rotationLimit = LOG_ROTATION_LIMIT};
-
-/** @brief Print title with version info. */
-static void printTitle()
+/** @brief Print version info. */
+static void printVersion()
 {
-    printf("Host logger service " PACKAGE_VERSION ".\n");
+    puts("Host logger service rev." HOSTLOGGER_VERSION ".");
 }
 
-/** @brief Print help usage info.
+/**
+ * @brief Print help usage info.
  *
- *  @param[in] app - application's file name
+ * @param[in] app application's file name
  */
 static void printHelp(const char* app)
 {
-    printTitle();
-    printf("Copyright (c) 2018 YADRO.\n");
-    printf("Usage: %s [options]\n", app);
-    printf(
-        "Options (defaults are specified in brackets):\n"
-        "  -p, --path=PATH   Path used to store logs [%s]\n"
-        "Intermediate storage buffer capacity setup:\n"
-        "  -s, --szlimit=N   Store up to N last messages [%i], 0=unlimited\n"
-        "  -t, --tmlimit=N   Store messages for last N hours [%i], "
-        "0=unlimited\n"
-        "Flush storage buffer policy:\n"
-        "  -f, --flush=N     Flush logs every N hours [%i]\n"
-        "                    If this option is set to 0 flush will be called "
-        "at\n"
-        "                    every host state change event from D-Bus.\n"
-        "Log files rotation policy:\n"
-        "  -r, --rotate=N    Store up to N files in the log directory [%i],\n"
-        "                    0=unlimited\n"
-        "Common options:\n"
-        "  -v, --version     Print version and exit\n"
-        "  -h, --help        Print this help and exit\n",
-        loggerConfig.path, loggerConfig.storageSizeLimit,
-        loggerConfig.storageTimeLimit, loggerConfig.flushPeriod,
-        loggerConfig.rotationLimit);
-}
-
-/** @brief Get numeric positive value from string argument.
- *
- *  @param[in] param - parameter name
- *  @param[in] arg - parameter text value
- *
- *  @return positive numeric value from string argument or -1 on errors
- */
-static int getNumericArg(const char* param, const char* arg)
-{
-    char* ep = nullptr;
-    const unsigned long val = strtoul(arg, &ep, 0);
-    if (val > INT_MAX || !ep || ep == arg || *ep != 0)
-    {
-        fprintf(stderr, "Invalid %s param: %s, expected 0<=N<=%i\n", param, arg,
-                INT_MAX);
-        return -1;
-    }
-    return static_cast<int>(val);
+    printVersion();
+    puts("Copyright (c) 2020 YADRO.");
+    printf("Usage: %s [OPTION...]\n", app);
+    puts("  -v, --version  Print version and exit");
+    puts("  -h, --help     Print this help and exit");
 }
 
 /** @brief Application entry point. */
 int main(int argc, char* argv[])
 {
-    int opt_val;
     // clang-format off
-    const struct option opts[] = {
-        { "path",    required_argument, 0, 'p' },
-        { "szlimit", required_argument, 0, 's' },
-        { "tmlimit", required_argument, 0, 't' },
-        { "flush",   required_argument, 0, 'f' },
-        { "rotate",  required_argument, 0, 'r' },
-        { "version", no_argument,       0, 'v' },
-        { "help",    no_argument,       0, 'h' },
-        { 0,         0,                 0,  0  }
+    const struct option longOpts[] = {
+        { "version", no_argument, nullptr, 'v' },
+        { "help",    no_argument, nullptr, 'h' },
+        { nullptr,   0,           nullptr,  0  }
     };
     // clang-format on
-
-    opterr = 0;
-    while ((opt_val = getopt_long(argc, argv, "p:s:t:f:r:vh", opts, NULL)) !=
-           -1)
+    const char* shortOpts = "vh";
+    opterr = 0; // prevent native error messages
+    int val;
+    while ((val = getopt_long(argc, argv, shortOpts, longOpts, nullptr)) != -1)
     {
-        switch (opt_val)
+        switch (val)
         {
-            case 'p':
-                loggerConfig.path = optarg;
-                if (*loggerConfig.path != '/')
-                {
-                    fprintf(stderr,
-                            "Invalid directory: %s, expected absolute path\n",
-                            loggerConfig.path);
-                    return EXIT_FAILURE;
-                }
-                break;
-            case 's':
-                loggerConfig.storageSizeLimit =
-                    getNumericArg(opts[optind - 1].name, optarg);
-                if (loggerConfig.storageSizeLimit < 0)
-                    return EXIT_FAILURE;
-                break;
-            case 't':
-                loggerConfig.storageTimeLimit =
-                    getNumericArg(opts[optind - 1].name, optarg);
-                if (loggerConfig.storageTimeLimit < 0)
-                    return EXIT_FAILURE;
-                break;
-            case 'f':
-                loggerConfig.flushPeriod =
-                    getNumericArg(opts[optind - 1].name, optarg);
-                if (loggerConfig.flushPeriod < 0)
-                    return EXIT_FAILURE;
-                break;
-            case 'r':
-                loggerConfig.rotationLimit =
-                    getNumericArg(opts[optind - 1].name, optarg);
-                if (loggerConfig.rotationLimit < 0)
-                    return EXIT_FAILURE;
-                break;
             case 'v':
-                printTitle();
+                printVersion();
                 return EXIT_SUCCESS;
             case 'h':
                 printHelp(argv[0]);
                 return EXIT_SUCCESS;
             default:
-                fprintf(stderr, "Invalid option: %s\n", argv[optind - 1]);
+                fprintf(stderr, "Invalid argument: %s\n", argv[optind - 1]);
                 return EXIT_FAILURE;
         }
     }
-
-    int rc;
-
-    // Initialize log manager
-    LogManager logManager;
-    rc = logManager.openHostLog();
-    if (rc != 0)
-        return rc;
-
-    // Initialize D-Bus server
-    sdbusplus::bus::bus bus = sdbusplus::bus::new_default();
-    sd_event* event = nullptr;
-    rc = sd_event_default(&event);
-    if (rc < 0)
+    if (optind < argc)
     {
-        fprintf(stderr, "Error occurred during the sd_event_default: %i\n", rc);
+        fprintf(stderr, "Unexpected argument: %s\n", argv[optind - 1]);
         return EXIT_FAILURE;
     }
-    EventPtr eventPtr(event);
-    bus.attach_event(eventPtr.get(), SD_EVENT_PRIORITY_NORMAL);
 
-    DbusServer dbusMgr(logManager, bus);
-    bus.request_name(HOSTLOGGER_DBUS_IFACE);
-
-    // Initialize D-Bus watcher
-    DbusWatcher dbusWatch(logManager, bus);
-    rc = dbusWatch.initialize();
-    if (rc < 0)
+    try
+    {
+        Config cfg;
+        Service svc(cfg);
+        svc.run();
+    }
+    catch (const std::exception& ex)
+    {
+        fprintf(stderr, "%s\n", ex.what());
         return EXIT_FAILURE;
+    }
 
-    // D-Bus event processing
-    rc = sd_event_loop(eventPtr.get());
-    if (rc != 0)
-        fprintf(stderr, "Error occurred during the sd_event_loop: %i\n", rc);
-
-    return rc ? rc : -1; // Allways retrun an error code
+    return EXIT_SUCCESS;
 }
diff --git a/src/service.cpp b/src/service.cpp
new file mode 100644
index 0000000..eda8545
--- /dev/null
+++ b/src/service.cpp
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "service.hpp"
+
+#include <phosphor-logging/log.hpp>
+
+#include <vector>
+
+using namespace phosphor::logging;
+
+// clang-format off
+/** @brief Host state properties. */
+static const DbusLoop::WatchProperties watchProperties{
+  {"xyz.openbmc_project.State.Host", {{
+    "RequestedHostTransition", {
+      "xyz.openbmc_project.State.Host.Transition.On"}}}},
+  {"xyz.openbmc_project.State.OperatingSystem.Status", {{
+    "OperatingSystemState", {
+      "xyz.openbmc_project.State.OperatingSystem.Status.OSStatus.BootComplete",
+      "xyz.openbmc_project.State.OperatingSystem.Status.OSStatus.Inactive"}}}}
+};
+// clang-format on
+
+Service::Service(const Config& config) :
+    config(config), hostConsole(config.socketId),
+    logBuffer(config.bufMaxSize, config.bufMaxTime),
+    fileStorage(config.outDir, config.socketId, config.maxFiles)
+{}
+
+void Service::run()
+{
+    if (config.bufFlushFull)
+    {
+        logBuffer.setFullHandler([this]() { this->flush(); });
+    }
+
+    hostConsole.connect();
+
+    // Add SIGUSR1 signal handler for manual flushing
+    dbusLoop.addSignalHandler(SIGUSR1, [this]() { this->flush(); });
+    // Add SIGTERM signal handler for service shutdown
+    dbusLoop.addSignalHandler(SIGTERM, [this]() { this->dbusLoop.stop(0); });
+
+    // Register callback for socket IO
+    dbusLoop.addIoHandler(hostConsole, [this]() { this->readConsole(); });
+
+    // Register host state watcher
+    if (*config.hostState)
+    {
+        dbusLoop.addPropertyHandler(config.hostState, watchProperties,
+                                    [this]() { this->flush(); });
+    }
+
+    if (!*config.hostState && !config.bufFlushFull)
+    {
+        log<level::WARNING>("Automatic flush disabled");
+    }
+
+    log<level::DEBUG>("Initialization complete",
+                      entry("SocketId=%s", config.socketId),
+                      entry("BufMaxSize=%lu", config.bufMaxSize),
+                      entry("BufMaxTime=%lu", config.bufMaxTime),
+                      entry("BufFlushFull=%s", config.bufFlushFull ? "y" : "n"),
+                      entry("HostState=%s", config.hostState),
+                      entry("OutDir=%s", config.outDir),
+                      entry("MaxFiles=%lu", config.maxFiles));
+
+    // Run D-Bus event loop
+    const int rc = dbusLoop.run();
+    if (!logBuffer.empty())
+    {
+        flush();
+    }
+    if (rc < 0)
+    {
+        std::error_code ec(-rc, std::generic_category());
+        throw std::system_error(ec, "Error in event loop");
+    }
+}
+
+void Service::flush()
+{
+    if (logBuffer.empty())
+    {
+        log<level::INFO>("Ignore flush: buffer is empty");
+        return;
+    }
+    try
+    {
+        const std::string fileName = fileStorage.save(logBuffer);
+        logBuffer.clear();
+
+        std::string msg = "Host logs flushed to ";
+        msg += fileName;
+        log<level::INFO>(msg.c_str());
+    }
+    catch (const std::exception& ex)
+    {
+        log<level::ERR>(ex.what());
+    }
+}
+
+void Service::readConsole()
+{
+    constexpr size_t bufSize = 128; // enough for most line-oriented output
+    std::vector<char> bufData(bufSize);
+    char* buf = bufData.data();
+
+    try
+    {
+        while (const size_t rsz = hostConsole.read(buf, bufSize))
+        {
+            logBuffer.append(buf, rsz);
+        }
+    }
+    catch (const std::system_error& ex)
+    {
+        log<level::ERR>(ex.what());
+    }
+}
diff --git a/src/service.hpp b/src/service.hpp
new file mode 100644
index 0000000..86c02c7
--- /dev/null
+++ b/src/service.hpp
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#pragma once
+
+#include "config.hpp"
+#include "dbus_loop.hpp"
+#include "file_storage.hpp"
+#include "host_console.hpp"
+#include "log_buffer.hpp"
+
+/**
+ * @class Service
+ * @brief Log service: watches for events and handles them.
+ */
+class Service
+{
+  public:
+    /**
+     * @brief Constructor.
+     *
+     * @param[in] config service configuration
+     *
+     * @throw std::exception in case of errors
+     */
+    Service(const Config& config);
+
+    /**
+     * @brief Run the service.
+     *
+     * @throw std::exception in case of errors
+     */
+    void run();
+
+  private:
+    /**
+     * @brief Flush log buffer to a file.
+     */
+    void flush();
+
+    /**
+     * @brief Read data from host console and put it into the log buffer.
+     */
+    void readConsole();
+
+  private:
+    /** @brief Service configuration. */
+    const Config& config;
+    /** @brief D-Bus event loop. */
+    DbusLoop dbusLoop;
+    /** @brief Host console connection. */
+    HostConsole hostConsole;
+    /** @brief Intermediate storage: container for parsed log messages. */
+    LogBuffer logBuffer;
+    /** @brief Persistent storage. */
+    FileStorage fileStorage;
+};
diff --git a/src/version.hpp.in b/src/version.hpp.in
new file mode 100644
index 0000000..99fe644
--- /dev/null
+++ b/src/version.hpp.in
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#pragma once
+
+#define HOSTLOGGER_VERSION "@VCS_TAG@"
diff --git a/src/zlib_exception.cpp b/src/zlib_exception.cpp
index e88c7b3..1769700 100644
--- a/src/zlib_exception.cpp
+++ b/src/zlib_exception.cpp
@@ -1,22 +1,5 @@
-/**
- * @brief zLib exception.
- *
- * This file is part of HostLogger project.
- *
- * Copyright (c) 2020 YADRO
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
 
 #include "zlib_exception.hpp"
 
@@ -52,26 +35,26 @@
         details += ')';
     }
 
-    what_ = "Unable to ";
+    errDesc = "Unable to ";
     switch (op)
     {
-        case Create:
-            what_ += "create";
+        case create:
+            errDesc += "create";
             break;
-        case Close:
-            what_ += "close";
+        case close:
+            errDesc += "close";
             break;
-        case Write:
-            what_ += "write";
+        case write:
+            errDesc += "write";
             break;
     }
-    what_ += " file ";
-    what_ += fileName;
-    what_ += ": ";
-    what_ += details;
+    errDesc += " file ";
+    errDesc += fileName;
+    errDesc += ": ";
+    errDesc += details;
 }
 
 const char* ZlibException::what() const noexcept
 {
-    return what_.c_str();
+    return errDesc.c_str();
 }
diff --git a/src/zlib_exception.hpp b/src/zlib_exception.hpp
index b147301..54a57ea 100644
--- a/src/zlib_exception.hpp
+++ b/src/zlib_exception.hpp
@@ -1,22 +1,5 @@
-/**
- * @brief zLib exception.
- *
- * This file is part of HostLogger project.
- *
- * Copyright (c) 2020 YADRO
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
 
 #pragma once
 
@@ -25,8 +8,9 @@
 #include <exception>
 #include <string>
 
-/** @class ZlibException
- *  @brief zLib exception.
+/**
+ * @class ZlibException
+ * @brief zLib exception.
  */
 class ZlibException : public std::exception
 {
@@ -34,17 +18,18 @@
     /** @brief File operation types. */
     enum Operation
     {
-        Create,
-        Write,
-        Close
+        create,
+        write,
+        close
     };
 
-    /** @brief Constructor.
+    /**
+     * @brief Constructor.
      *
-     *  @param[in] op - type of operation
-     *  @param[in] code - zLib status code
-     *  @param[in] fd - zLib file descriptor
-     *  @param[in] fileName - file name
+     * @param[in] op type of operation
+     * @param[in] code zLib status code
+     * @param[in] fd zLib file descriptor
+     * @param[in] fileName file name
      */
     ZlibException(Operation op, int code, gzFile fd,
                   const std::string& fileName);
@@ -54,5 +39,5 @@
 
   private:
     /** @brief Error description buffer. */
-    std::string what_;
+    std::string errDesc;
 };
diff --git a/src/zlib_file.cpp b/src/zlib_file.cpp
new file mode 100644
index 0000000..042dafb
--- /dev/null
+++ b/src/zlib_file.cpp
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "zlib_file.hpp"
+
+#include "zlib_exception.hpp"
+
+ZlibFile::ZlibFile(const std::string& fileName)
+{
+    fd = gzopen(fileName.c_str(), "w");
+    if (fd == Z_NULL)
+    {
+        throw ZlibException(ZlibException::create, Z_ERRNO, fd, fileName);
+    }
+    this->fileName = fileName;
+}
+
+ZlibFile::~ZlibFile()
+{
+    if (fd != Z_NULL)
+    {
+        gzclose_w(fd);
+    }
+}
+
+void ZlibFile::close()
+{
+    if (fd != Z_NULL)
+    {
+        const int rc = gzclose_w(fd);
+        if (rc != Z_OK)
+        {
+            throw ZlibException(ZlibException::close, rc, fd, fileName);
+        }
+        fd = Z_NULL;
+        fileName.clear();
+    }
+}
+
+void ZlibFile::write(const tm& timeStamp, const std::string& message) const
+{
+    int rc;
+
+    // Write time stamp
+    rc = gzprintf(fd, "[ %02i:%02i:%02i ] ", timeStamp.tm_hour,
+                  timeStamp.tm_min, timeStamp.tm_sec);
+    if (rc <= 0)
+    {
+        throw ZlibException(ZlibException::write, rc, fd, fileName);
+    }
+
+    // Write message
+    const size_t len = message.length();
+    if (len)
+    {
+        rc = gzwrite(fd, message.data(), static_cast<unsigned int>(len));
+        if (rc <= 0)
+        {
+            throw ZlibException(ZlibException::write, rc, fd, fileName);
+        }
+    }
+
+    // Write EOL
+    rc = gzputc(fd, '\n');
+    if (rc <= 0)
+    {
+        throw ZlibException(ZlibException::write, rc, fd, fileName);
+    }
+}
diff --git a/src/zlib_file.hpp b/src/zlib_file.hpp
new file mode 100644
index 0000000..5f501ab
--- /dev/null
+++ b/src/zlib_file.hpp
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#pragma once
+
+#include <zlib.h>
+
+#include <ctime>
+#include <string>
+
+/**
+ * @class ZlibFile
+ * @brief Log file writer.
+ */
+class ZlibFile
+{
+  public:
+    /**
+     * @brief Constructor create new file for writing logs.
+     *
+     * @param[in] fileName path to the file
+     *
+     * @throw ZlibException in case of errors
+     */
+    ZlibFile(const std::string& fileName);
+
+    ~ZlibFile();
+
+    ZlibFile(const ZlibFile&) = delete;
+    ZlibFile& operator=(const ZlibFile&) = delete;
+
+    /**
+     * @brief Close file.
+     *
+     * @throw ZlibException in case of errors
+     */
+    void close();
+
+    /**
+     * @brief Write single log message to the file.
+     *
+     * @param[in] timeStamp time stamp of the log message
+     * @param[in] message log message text
+     *
+     * @throw ZlibException in case of errors
+     */
+    void write(const tm& timeStamp, const std::string& message) const;
+
+  private:
+    /** @brief File name. */
+    std::string fileName;
+    /** @brief zLib file descriptor. */
+    gzFile fd;
+};
diff --git a/test/config_test.cpp b/test/config_test.cpp
new file mode 100644
index 0000000..15fbc4b
--- /dev/null
+++ b/test/config_test.cpp
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "config.hpp"
+
+#include <gtest/gtest.h>
+
+// Names of environment variables
+static const char* SOCKET_ID = "SOCKET_ID";
+static const char* BUF_MAXSIZE = "BUF_MAXSIZE";
+static const char* BUF_MAXTIME = "BUF_MAXTIME";
+static const char* FLUSH_FULL = "FLUSH_FULL";
+static const char* HOST_STATE = "HOST_STATE";
+static const char* OUT_DIR = "OUT_DIR";
+static const char* MAX_FILES = "MAX_FILES";
+
+/**
+ * @class ConfigTest
+ * @brief Configuration tests.
+ */
+class ConfigTest : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        resetEnv();
+    }
+
+    void TearDown() override
+    {
+        resetEnv();
+    }
+
+    /** @brief Reset environment variables. */
+    void resetEnv() const
+    {
+        unsetenv(SOCKET_ID);
+        unsetenv(BUF_MAXSIZE);
+        unsetenv(BUF_MAXTIME);
+        unsetenv(FLUSH_FULL);
+        unsetenv(HOST_STATE);
+        unsetenv(OUT_DIR);
+        unsetenv(MAX_FILES);
+    }
+};
+
+TEST_F(ConfigTest, Defaults)
+{
+    Config cfg;
+    EXPECT_STREQ(cfg.socketId, "");
+    EXPECT_EQ(cfg.bufMaxSize, 3000);
+    EXPECT_EQ(cfg.bufMaxTime, 0);
+    EXPECT_EQ(cfg.bufFlushFull, false);
+    EXPECT_STREQ(cfg.hostState, "/xyz/openbmc_project/state/host0");
+    EXPECT_STREQ(cfg.outDir, "/var/lib/obmc/hostlogs");
+    EXPECT_EQ(cfg.maxFiles, 10);
+}
+
+TEST_F(ConfigTest, Load)
+{
+    setenv(SOCKET_ID, "id123", 1);
+    setenv(BUF_MAXSIZE, "1234", 1);
+    setenv(BUF_MAXTIME, "4321", 1);
+    setenv(FLUSH_FULL, "true", 1);
+    setenv(HOST_STATE, "host123", 1);
+    setenv(OUT_DIR, "path123", 1);
+    setenv(MAX_FILES, "1122", 1);
+
+    Config cfg;
+    EXPECT_STREQ(cfg.socketId, "id123");
+    EXPECT_EQ(cfg.bufMaxSize, 1234);
+    EXPECT_EQ(cfg.bufMaxTime, 4321);
+    EXPECT_EQ(cfg.bufFlushFull, true);
+    EXPECT_STREQ(cfg.hostState, "host123");
+    EXPECT_STREQ(cfg.outDir, "path123");
+    EXPECT_EQ(cfg.maxFiles, 1122);
+}
+
+TEST_F(ConfigTest, InvalidNumeric)
+{
+    setenv(BUF_MAXSIZE, "-1234", 1);
+    ASSERT_THROW(Config(), std::invalid_argument);
+}
+
+TEST_F(ConfigTest, InvalidBoolean)
+{
+    setenv(FLUSH_FULL, "invalid", 1);
+    ASSERT_THROW(Config(), std::invalid_argument);
+}
+
+TEST_F(ConfigTest, InvalidConfig)
+{
+    setenv(BUF_MAXSIZE, "0", 1);
+    setenv(BUF_MAXTIME, "0", 1);
+    setenv(FLUSH_FULL, "true", 1);
+    ASSERT_THROW(Config(), std::invalid_argument);
+}
diff --git a/test/file_storage_test.cpp b/test/file_storage_test.cpp
new file mode 100644
index 0000000..ba74bd4
--- /dev/null
+++ b/test/file_storage_test.cpp
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "file_storage.hpp"
+
+#include <fstream>
+
+#include <gtest/gtest.h>
+
+namespace fs = std::filesystem;
+
+/**
+ * @class FileStorageTest
+ * @brief Persistent file storage tests.
+ */
+class FileStorageTest : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        fs::remove_all(logPath);
+    }
+
+    void TearDown() override
+    {
+        fs::remove_all(logPath);
+    }
+
+    const fs::path logPath =
+        fs::temp_directory_path() / "file_storage_test_out";
+};
+
+TEST_F(FileStorageTest, InvalidPath)
+{
+    ASSERT_THROW(FileStorage("", "", 0), std::invalid_argument);
+    ASSERT_THROW(FileStorage("relative/path", "", 0), std::invalid_argument);
+    ASSERT_THROW(FileStorage("/noaccess", "", 0), fs::filesystem_error);
+}
+
+TEST_F(FileStorageTest, Save)
+{
+    const char* data = "test message\n";
+    LogBuffer buf(0, 0);
+    buf.append(data, strlen(data));
+
+    FileStorage fs(logPath, "", 0);
+    fs.save(buf);
+
+    const auto itBegin = fs::recursive_directory_iterator(logPath);
+    const auto itEnd = fs::recursive_directory_iterator{};
+    ASSERT_EQ(std::distance(itBegin, itEnd), 1);
+
+    const fs::path file = *fs::directory_iterator(logPath);
+    EXPECT_NE(fs::file_size(file), 0);
+}
+
+TEST_F(FileStorageTest, Rotation)
+{
+    const size_t limit = 5;
+    const std::string prefix = "host123";
+
+    const char* data = "test message\n";
+    LogBuffer buf(0, 0);
+    buf.append(data, strlen(data));
+
+    FileStorage fs(logPath, prefix, limit);
+    for (size_t i = 0; i < limit + 3; ++i)
+    {
+        fs.save(buf);
+    }
+
+    // Dir and other files that can not be removed
+    const fs::path dir = logPath / (prefix + "_11111111_222222.log.gz");
+    const fs::path files[] = {logPath / "short",
+                              logPath / (prefix + "_11111111_222222.bad.ext"),
+                              logPath / (prefix + "x_11111111_222222.log.gz")};
+    fs::create_directory(dir);
+    for (const auto& i : files)
+    {
+        std::ofstream dummy(i);
+    }
+
+    const auto itBegin = fs::recursive_directory_iterator(logPath);
+    const auto itEnd = fs::recursive_directory_iterator{};
+    EXPECT_EQ(std::distance(itBegin, itEnd),
+              limit + 1 /*dir*/ + sizeof(files) / sizeof(files[0]));
+    EXPECT_TRUE(fs::exists(dir));
+    for (const auto& i : files)
+    {
+        EXPECT_TRUE(fs::exists(i));
+    }
+}
diff --git a/test/host_console_test.cpp b/test/host_console_test.cpp
new file mode 100644
index 0000000..1581725
--- /dev/null
+++ b/test/host_console_test.cpp
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "host_console.hpp"
+
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include <gtest/gtest.h>
+
+static constexpr char socketPath[] = "\0obmc-console";
+
+/**
+ * @class HostConsoleTest
+ * @brief Persistent file storage tests.
+ */
+class HostConsoleTest : public ::testing::Test
+{
+  protected:
+    void startServer(const char* socketId)
+    {
+        // Start server
+        serverSocket = socket(AF_UNIX, SOCK_STREAM, 0);
+        ASSERT_NE(serverSocket, -1);
+        std::string path(socketPath, socketPath + sizeof(socketPath) - 1);
+        if (*socketId)
+        {
+            path += '.';
+            path += socketId;
+        }
+        sockaddr_un sa;
+        sa.sun_family = AF_UNIX;
+        memcpy(&sa.sun_path, path.c_str(), path.length());
+        const socklen_t len = sizeof(sa) - sizeof(sa.sun_path) + path.length();
+        ASSERT_NE(
+            bind(serverSocket, reinterpret_cast<const sockaddr*>(&sa), len),
+            -1);
+        ASSERT_NE(listen(serverSocket, 1), -1);
+    }
+
+    void TearDown() override
+    {
+        // Stop server
+        if (serverSocket != -1)
+        {
+            close(serverSocket);
+        }
+    }
+
+    int serverSocket = -1;
+};
+
+TEST_F(HostConsoleTest, SingleHost)
+{
+    const char* socketId = "";
+    startServer(socketId);
+
+    HostConsole con(socketId);
+    con.connect();
+
+    const int clientSocket = accept(serverSocket, nullptr, nullptr);
+    EXPECT_NE(clientSocket, -1);
+    close(clientSocket);
+}
+
+TEST_F(HostConsoleTest, MultiHost)
+{
+    const char* socketId = "host123";
+    startServer(socketId);
+
+    HostConsole con(socketId);
+    con.connect();
+
+    const int clientSocket = accept(serverSocket, nullptr, nullptr);
+    EXPECT_NE(clientSocket, -1);
+
+    const char* data = "test data";
+    const size_t len = strlen(data);
+    EXPECT_EQ(send(clientSocket, data, len, 0), len);
+
+    char buf[64];
+    memset(buf, 0, sizeof(buf));
+    EXPECT_EQ(con.read(buf, sizeof(buf)), len);
+    EXPECT_STREQ(buf, data);
+    EXPECT_EQ(con.read(buf, sizeof(buf)), 0);
+
+    close(clientSocket);
+}
diff --git a/test/log_buffer_test.cpp b/test/log_buffer_test.cpp
new file mode 100644
index 0000000..d7638be
--- /dev/null
+++ b/test/log_buffer_test.cpp
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "log_buffer.hpp"
+
+#include <gtest/gtest.h>
+
+TEST(LogBufferTest, Append)
+{
+    const std::string msg = "Test message";
+
+    LogBuffer buf(0, 0);
+
+    buf.append(msg.data(), msg.length());
+    ASSERT_EQ(std::distance(buf.begin(), buf.end()), 1);
+    EXPECT_EQ(buf.begin()->text, msg);
+    EXPECT_NE(buf.begin()->timeStamp, 0);
+
+    // must be merged with previous message
+    const std::string append = "Append";
+    buf.append(append.data(), append.length());
+    ASSERT_EQ(std::distance(buf.begin(), buf.end()), 1);
+    EXPECT_EQ(buf.begin()->text, msg + append);
+
+    // end of line, we still have 1 message
+    buf.append("\n", 1);
+    ASSERT_EQ(std::distance(buf.begin(), buf.end()), 1);
+
+    // second message
+    buf.append(append.data(), append.length());
+    ASSERT_EQ(std::distance(buf.begin(), buf.end()), 2);
+    EXPECT_EQ((++buf.begin())->text, append);
+}
+
+TEST(LogBufferTest, AppendEol)
+{
+    LogBuffer buf(0, 0);
+
+    buf.append("\r\r\r\r", 4);
+    EXPECT_EQ(std::distance(buf.begin(), buf.end()), 4);
+
+    buf.clear();
+    buf.append("\n\n\n\n", 4);
+    EXPECT_EQ(std::distance(buf.begin(), buf.end()), 4);
+
+    buf.clear();
+    buf.append("\r\n\r\n", 4);
+    EXPECT_EQ(std::distance(buf.begin(), buf.end()), 2);
+
+    buf.clear();
+    buf.append("\n\r\n\r", 4);
+    EXPECT_EQ(std::distance(buf.begin(), buf.end()), 2);
+
+    buf.clear();
+    buf.append("\r\r\r\n\n\n", 6);
+    EXPECT_EQ(std::distance(buf.begin(), buf.end()), 5);
+}
+
+TEST(LogBufferTest, Clear)
+{
+    const std::string msg = "Test message";
+
+    LogBuffer buf(0, 0);
+    buf.append(msg.data(), msg.length());
+    EXPECT_FALSE(buf.empty());
+    buf.clear();
+    EXPECT_TRUE(buf.empty());
+}
+
+TEST(LogBufferTest, SizeLimit)
+{
+    const size_t limit = 5;
+    const std::string msg = "Test message\n";
+
+    LogBuffer buf(limit, 0);
+    for (size_t i = 0; i < limit + 3; ++i)
+    {
+        buf.append(msg.data(), msg.length());
+    }
+    EXPECT_EQ(std::distance(buf.begin(), buf.end()), limit);
+}
+
+TEST(LogBufferTest, FullHandler)
+{
+    const size_t limit = 5;
+    const std::string msg = "Test message\n";
+
+    size_t count = 0;
+
+    LogBuffer buf(limit, 0);
+    buf.setFullHandler([&count, &buf]() {
+        ++count;
+        buf.clear();
+    });
+    for (size_t i = 0; i < limit + 3; ++i)
+    {
+        buf.append(msg.data(), msg.length());
+    }
+    EXPECT_EQ(count, 1);
+    EXPECT_EQ(std::distance(buf.begin(), buf.end()), 2);
+}
diff --git a/test/meson.build b/test/meson.build
new file mode 100644
index 0000000..1a10b6f
--- /dev/null
+++ b/test/meson.build
@@ -0,0 +1,26 @@
+# Rules for building tests
+
+test(
+  'hostlogger',
+  executable(
+    'hostlogger_test',
+    [
+      'config_test.cpp',
+      'file_storage_test.cpp',
+      'host_console_test.cpp',
+      'log_buffer_test.cpp',
+      'zlib_file_test.cpp',
+      '../src/config.cpp',
+      '../src/file_storage.cpp',
+      '../src/host_console.cpp',
+      '../src/log_buffer.cpp',
+      '../src/zlib_exception.cpp',
+      '../src/zlib_file.cpp',
+    ],
+    dependencies: [
+      dependency('gtest', main: true, disabler: true, required: build_tests),
+      dependency('zlib'),
+    ],
+    include_directories: '../src',
+  )
+)
diff --git a/test/zlib_file_test.cpp b/test/zlib_file_test.cpp
new file mode 100644
index 0000000..ef49a6f
--- /dev/null
+++ b/test/zlib_file_test.cpp
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "zlib_exception.hpp"
+#include "zlib_file.hpp"
+
+#include <gtest/gtest.h>
+
+TEST(ZlibFileTest, Exception)
+{
+    ASSERT_THROW(ZlibFile("invalid/path"), ZlibException);
+}
+
+TEST(ZlibFileTest, Write)
+{
+    const std::string msg = "Test message";
+    time_t currTime;
+    time(&currTime);
+    tm localTime;
+    localtime_r(&currTime, &localTime);
+
+    const std::string path = "/tmp/zlib_file_test.out";
+    ZlibFile file(path);
+    file.write(localTime, msg);
+    file.close();
+
+    char expect[64];
+    const int len = snprintf(expect, sizeof(expect), "[ %02i:%02i:%02i ] %s\n",
+                             localTime.tm_hour, localTime.tm_min,
+                             localTime.tm_sec, msg.c_str());
+
+    gzFile fd = gzopen(path.c_str(), "r");
+    ASSERT_TRUE(fd);
+    char buf[64];
+    memset(buf, 0, sizeof(buf));
+    EXPECT_EQ(gzread(fd, buf, sizeof(buf)), len);
+    EXPECT_STREQ(buf, expect);
+    EXPECT_EQ(gzclose(fd), 0);
+
+    unlink(path.c_str());
+}
diff --git a/xyz/openbmc_project/HostLogger.interface.yaml b/xyz/openbmc_project/HostLogger.interface.yaml
deleted file mode 100644
index 2c6331f..0000000
--- a/xyz/openbmc_project/HostLogger.interface.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-description: >
-    Implementation of the host log storage service.
-
-methods:
-    - name: Flush
-      description: >
-        Flush currently collected logs to the file
-        and switch to the next one.
-      errors:
-        - xyz.openbmc_project.Common.File.Error.Write