diff --git a/changelogs/unreleased/gh-7447-fix-panic-on-invalid-log.md b/changelogs/unreleased/gh-7447-fix-panic-on-invalid-log.md
new file mode 100644
index 0000000000000000000000000000000000000000..519adf4bb47e075f8043d20053846de6065fb8d7
--- /dev/null
+++ b/changelogs/unreleased/gh-7447-fix-panic-on-invalid-log.md
@@ -0,0 +1,3 @@
+## bugfix/core
+
+* Fixed panic on invalid syslog log configuration (gh-7447).
diff --git a/extra/exports b/extra/exports
index f30e7bbfe96552e547ba8802e6023444730807c7..e88a087672a33abdbf09f5edd25164ef8a075899 100644
--- a/extra/exports
+++ b/extra/exports
@@ -236,7 +236,6 @@ lbox_socket_nonblock
 log_format
 log_level
 log_pid
-log_type
 luaJIT_profile_dumpstack
 luaJIT_profile_start
 luaJIT_profile_stop
@@ -426,10 +425,10 @@ prbuf_commit
 prbuf_iterator_create
 prbuf_iterator_next
 random_bytes
+say_check_cfg
 say_logger_init
 say_logger_initialized
 say_logrotate
-say_parse_logger_type
 say_set_log_format
 say_set_log_level
 SHA1internal
diff --git a/src/box/box.cc b/src/box/box.cc
index a722c8ed7f77dc58ff790beda315454f3401f06a..aa43a9afe61e93089fe46316d26ffb6846c73b18 100644
--- a/src/box/box.cc
+++ b/src/box/box.cc
@@ -735,43 +735,53 @@ wal_stream_create(struct wal_stream *ctx)
 
 /* {{{ configuration bindings */
 
-static void
-box_check_say(void)
+/*
+ * Check log configuration validity.
+ *
+ * Used thru Lua FFI.
+ */
+extern "C" int
+say_check_cfg(const char *log,
+	      MAYBE_UNUSED int level,
+	      int nonblock,
+	      const char *format_str)
 {
-	enum say_logger_type type = SAY_LOGGER_STDERR; /* default */
-	const char *log = cfg_gets("log");
+	enum say_logger_type type = SAY_LOGGER_STDERR;
 	if (log != NULL && say_parse_logger_type(&log, &type) < 0) {
-		tnt_raise(ClientError, ER_CFG, "log",
-			  diag_last_error(diag_get())->errmsg);
+		diag_set(ClientError, ER_CFG, "log",
+			 diag_last_error(diag_get())->errmsg);
+		return -1;
 	}
 	if (type == SAY_LOGGER_SYSLOG) {
 		struct say_syslog_opts opts;
 		if (say_parse_syslog_opts(log, &opts) < 0) {
 			if (diag_last_error(diag_get())->type ==
-			    &type_IllegalParams) {
-				tnt_raise(ClientError, ER_CFG, "log",
-					  diag_last_error(diag_get())->errmsg);
-			}
-			diag_raise();
+			    &type_IllegalParams)
+				diag_set(ClientError, ER_CFG, "log",
+					 diag_last_error(diag_get())->errmsg);
+			return -1;
 		}
 		say_free_syslog_opts(&opts);
 	}
 
-	const char *log_format = cfg_gets("log_format");
-	enum say_format format = say_format_by_name(log_format);
-	if (format == say_format_MAX)
-		tnt_raise(ClientError, ER_CFG, "log_format",
+	enum say_format format = say_format_by_name(format_str);
+	if (format == say_format_MAX) {
+		diag_set(ClientError, ER_CFG, "log_format",
 			 "expected 'plain' or 'json'");
+		return -1;
+	}
 	if (type == SAY_LOGGER_SYSLOG && format == SF_JSON) {
-		tnt_raise(ClientError, ER_CFG, "log_format",
-			  "'json' can't be used with syslog logger");
+		diag_set(ClientError, ER_CFG, "log_format",
+			 "'json' can't be used with syslog logger");
+		return -1;
 	}
-	int log_nonblock = cfg_getb("log_nonblock");
-	if (log_nonblock == 1 &&
+	if (nonblock == 1 &&
 	    (type == SAY_LOGGER_FILE || type == SAY_LOGGER_STDERR)) {
-		tnt_raise(ClientError, ER_CFG, "log_nonblock",
-			  "the option is incompatible with file/stderr logger");
+		diag_set(ClientError, ER_CFG, "log_nonblock",
+			 "the option is incompatible with file/stderr logger");
+		return -1;
 	}
+	return 0;
 }
 
 /**
@@ -1399,6 +1409,26 @@ box_check_txn_isolation(void)
 	return (enum txn_isolation_level)level;
 }
 
+static void
+box_check_say()
+{
+	if (luaT_dostring(tarantool_L,
+			  "require('log').box_api.cfg_check()") != 0)
+		diag_raise();
+}
+
+int
+box_init_say()
+{
+	if (luaT_dostring(tarantool_L, "require('log').box_api.cfg()") != 0)
+		return -1;
+
+	if (cfg_geti("background") && say_set_background() != 0)
+		return -1;
+
+	return 0;
+}
+
 void
 box_check_config(void)
 {
diff --git a/src/box/box.h b/src/box/box.h
index 71d72c55e520980c771953cac53f62525a9d1c72..acb76f425253bf8fc7264beef40e7e88866ca13d 100644
--- a/src/box/box.h
+++ b/src/box/box.h
@@ -319,6 +319,12 @@ box_get_flightrec_cfg(struct flight_recorder_cfg *cfg);
 int
 box_configure_flightrec(void);
 
+/**
+ * Initialize logger on box init.
+ */
+int
+box_init_say();
+
 extern "C" {
 #endif /* defined(__cplusplus) */
 
diff --git a/src/box/lua/load_cfg.lua b/src/box/lua/load_cfg.lua
index 6956cdce3eebc4491c1e8102dfdbd09c860aabec..beaf6d5dd286572f4e2967bf2c37b481f84766f5 100644
--- a/src/box/lua/load_cfg.lua
+++ b/src/box/lua/load_cfg.lua
@@ -65,9 +65,10 @@ local default_cfg = {
     vinyl_page_size           = 8 * 1024,
     vinyl_bloom_fpr           = 0.05,
 
-    -- logging options are covered by
-    -- a separate log module; they are
-    -- 'log_' prefixed
+    log                 = log.cfg.log,
+    log_nonblock        = log.cfg.nonblock,
+    log_level           = log.cfg.level,
+    log_format          = log.cfg.format,
 
     audit_log           = nil,
     audit_nonblock      = true,
@@ -133,31 +134,6 @@ local default_cfg = {
     txn_isolation         = "best-effort",
 }
 
--- cfg variables which are covered by modules
-local module_cfg = {
-    -- logging
-    log                 = log.box_api,
-    log_nonblock        = log.box_api,
-    log_level           = log.box_api,
-    log_format          = log.box_api,
-}
-
--- cfg types for modules, probably better to
--- provide some API with type enumeration or
--- similar. Currently it has use for environment
--- processing only.
---
--- get_option_from_env() leans on the set of types
--- in use: don't forget to update it when add a new
--- type or a combination of types here.
-local module_cfg_type = {
-    -- logging
-    log                 = 'string',
-    log_nonblock        = 'boolean',
-    log_level           = 'number, string',
-    log_format          = 'string',
-}
-
 -- types of available options
 -- could be comma separated lua types or 'any' if any type is allowed
 --
@@ -191,10 +167,10 @@ local template_cfg = {
     vinyl_page_size           = 'number',
     vinyl_bloom_fpr           = 'number',
 
-    log                 = 'module',
-    log_nonblock        = 'module',
-    log_level           = 'module',
-    log_format          = 'module',
+    log                 = 'string',
+    log_nonblock        = 'boolean',
+    log_level           = 'number, string',
+    log_format          = 'string',
 
     audit_log           = 'string',
     audit_nonblock      = 'boolean',
@@ -332,8 +308,6 @@ end
 -- fact.
 local dynamic_cfg = {
     replication             = private.cfg_set_replication,
-    log_level               = log.box_api.cfg_set_log_level,
-    log_format              = log.box_api.cfg_set_log_format,
     io_collect_interval     = private.cfg_set_io_collect_interval,
     readahead               = private.cfg_set_readahead,
     too_long_threshold      = private.cfg_set_too_long_threshold,
@@ -423,6 +397,16 @@ local dynamic_cfg_modules = {
             flightrec_requests_max_res_size = true,
         },
     },
+    log = {
+        cfg = log.box_api.cfg,
+        options = {
+            log = true,
+            log_level = true,
+            log_format = true,
+            log_nonblock = true,
+        },
+        skip_at_load = true,
+    }
 }
 
 ifdef_feedback = nil -- luacheck: ignore
@@ -599,26 +583,25 @@ local function upgrade_cfg(cfg, translate_cfg)
     return result_cfg
 end
 
-local function update_module_cfg(cfg, module_cfg)
-    local module_cfg_backup = {}
-    for field, api in pairs(module_cfg) do
-        if cfg[field] ~= nil then
-            module_cfg_backup[field] = api.cfg_get(field) or box.NULL
-
-            local ok, msg = api.cfg_set(cfg, field, cfg[field])
-            if not ok then
-                -- restore back the old values for modules
-                for k, v in pairs(module_cfg_backup) do
-                    module_cfg[k].cfg_set(cfg, k, v)
-                end
-                box.error(box.error.CFG, field, msg)
-            end
+local function check_cfg_option_type(template, name, value)
+    if template == 'any' then
+        return
+    elseif (string.find(template, ',') == nil) then
+        if type(value) ~= template then
+            box.error(box.error.CFG, name, "should be of type " ..
+                      template)
+        end
+    else
+        local prepared_tmpl = ',' .. string.gsub(template, ' ', '') .. ','
+        local prepared_type = ',' .. type(value) .. ','
+        if string.find(prepared_tmpl, prepared_type) == nil then
+            box.error(box.error.CFG, name, "should be one of types " ..
+                      template)
         end
     end
 end
 
-local function prepare_cfg(cfg, default_cfg, template_cfg,
-                           module_cfg, modify_cfg, prefix)
+local function prepare_cfg(cfg, default_cfg, template_cfg, modify_cfg)
     if cfg == nil then
         return {}
     end
@@ -629,39 +612,15 @@ local function prepare_cfg(cfg, default_cfg, template_cfg,
     if cfg.dont_check then
         return
     end
-    local readable_prefix = ''
-    if prefix ~= nil and prefix ~= '' then
-        readable_prefix = prefix .. '.'
-    end
     local new_cfg = {}
-    for k,v in pairs(cfg) do
-        local readable_name = readable_prefix .. k;
+    for k, v in pairs(cfg) do
         if template_cfg[k] == nil then
-            box.error(box.error.CFG, readable_name , "unexpected option")
+            box.error(box.error.CFG, k , "unexpected option")
         elseif v == "" or v == nil then
             -- "" and NULL = ffi.cast('void *', 0) set option to default value
             v = default_cfg[k]
-        elseif template_cfg[k] == 'any' then -- luacheck: ignore
-            -- any type is ok
-        elseif type(template_cfg[k]) == 'table' then
-            if type(v) ~= 'table' then
-                box.error(box.error.CFG, readable_name, "should be a table")
-            end
-            v = prepare_cfg(v, default_cfg[k], template_cfg[k],
-                            module_cfg[k], modify_cfg[k], readable_name)
-        elseif template_cfg[k] ~= 'module' and
-               (string.find(template_cfg[k], ',') == nil) then
-            -- one type
-            if type(v) ~= template_cfg[k] then
-                box.error(box.error.CFG, readable_name, "should be of type "..
-                    template_cfg[k])
-            end
-        elseif template_cfg[k] ~= 'module' then
-            local good_types = string.gsub(template_cfg[k], ' ', '');
-            if (string.find(',' .. good_types .. ',', ',' .. type(v) .. ',') == nil) then
-                box.error(box.error.CFG, readable_name, "should be one of types "..
-                    template_cfg[k])
-            end
+        else
+            check_cfg_option_type(template_cfg[k], k, v)
         end
         if modify_cfg ~= nil and type(modify_cfg[k]) == 'function' then
             v = modify_cfg[k](v)
@@ -683,17 +642,12 @@ local function apply_env_cfg(cfg, env_cfg)
     end
 end
 
-local function apply_default_cfg(cfg, default_cfg, module_cfg)
+local function merge_cfg(cfg, default_cfg)
     for k,v in pairs(default_cfg) do
         if cfg[k] == nil then
             cfg[k] = v
         elseif type(v) == 'table' then
-            apply_default_cfg(cfg[k], v)
-        end
-    end
-    for k in pairs(module_cfg) do
-        if cfg[k] == nil then
-            cfg[k] = module_cfg[k].cfg_get(k)
+            merge_cfg(cfg[k], v)
         end
     end
 end
@@ -786,8 +740,7 @@ end
 
 local function reload_cfg(oldcfg, cfg)
     cfg = upgrade_cfg(cfg, translate_cfg)
-    local newcfg = prepare_cfg(cfg, default_cfg, template_cfg,
-                               module_cfg, modify_cfg)
+    local newcfg = prepare_cfg(cfg, default_cfg, template_cfg, modify_cfg)
 
     local module_keys = {}
     -- iterate over original table because prepare_cfg() may store NILs
@@ -877,6 +830,12 @@ setmetatable(box, {
 -- Use locked() wrapper to obtain reliable results.
 local box_is_configured = false
 
+-- We need to track cfg changes done thru API of distinct modules (log.cfg of
+-- log module for example). We cannot use just box.cfg because it is not
+-- available before box.cfg() call and other modules can be configured before
+-- this moment.
+local pre_load_cfg = table.copy(default_cfg)
+
 local function load_cfg(cfg)
     -- A user may save box.cfg (this function) before box loading
     -- and call it afterwards. We should reconfigure box in the
@@ -891,18 +850,14 @@ local function load_cfg(cfg)
     -- Set options passed through environment variables.
     apply_env_cfg(cfg, box.internal.cfg.env)
 
-    cfg = prepare_cfg(cfg, default_cfg, template_cfg,
-                      module_cfg, modify_cfg)
-    apply_default_cfg(cfg, default_cfg, module_cfg);
+    cfg = prepare_cfg(cfg, default_cfg, template_cfg, modify_cfg)
+    merge_cfg(cfg, pre_load_cfg);
+
     -- Save new box.cfg
     box.cfg = cfg
     local status, err = pcall(private.cfg_check)
-    if status then
-        status, err = pcall(update_module_cfg, cfg, module_cfg)
-    end
     if not status then
         box.cfg = locked(load_cfg) -- restore original box.cfg
-        -- re-throw exception from check_cfg() or update_module_cfg()
         return error(err)
     end
 
@@ -995,11 +950,6 @@ local function get_option_from_env(option)
     local param_type = template_cfg[option]
     assert(type(param_type) == 'string')
 
-    if param_type == 'module' then
-        -- Parameter from module.
-        param_type = module_cfg_type[option]
-    end
-
     local env_var_name = 'TT_' .. option:upper()
     local raw_value = os.getenv(env_var_name)
 
@@ -1010,8 +960,8 @@ local function get_option_from_env(option)
     local err_msg_fmt = 'Environment variable %s has ' ..
         'incorrect value for option "%s": should be %s'
 
-    -- This code lean on the existing set of template_cfg and
-    -- module_cfg_type types for simplicity.
+    -- This code lean on the existing set of template_cfg
+    -- types for simplicity.
     if param_type:find('table') and raw_value:find(',') then
         assert(not param_type:find('boolean'))
         local res = {}
@@ -1043,9 +993,24 @@ local function get_option_from_env(option)
     end
 end
 
---
--- Read box configuration from environment variables.
---
+-- Used to propagate cfg changes done thru API of distinct modules (
+-- log.cfg of log module for example).
+local function update_cfg(option, value)
+    if box_is_configured then
+        rawset(box.cfg, option, value)
+    else
+        pre_load_cfg[option] = value
+    end
+end
+
+box.internal.prepare_cfg = prepare_cfg
+box.internal.merge_cfg = merge_cfg
+box.internal.check_cfg_option_type = check_cfg_option_type
+box.internal.update_cfg = update_cfg
+
+---
+--- Read box configuration from environment variables.
+---
 box.internal.cfg = setmetatable({}, {
     __index = function(self, key)
         if key == 'env' then
diff --git a/src/lib/core/say.c b/src/lib/core/say.c
index 15785d69152029a1bf5fd319b0a493024e890a12..f45c0c642ae9da16723799ce0567d6de3d8560a2 100644
--- a/src/lib/core/say.c
+++ b/src/lib/core/say.c
@@ -81,7 +81,7 @@ static const char logger_syntax_reminder[] =
  * True if Tarantool process runs in background mode, i.e. has no
  * controlling terminal.
  */
-static bool log_background = true;
+static bool log_background;
 
 static void
 say_default(int level, const char *filename, int line, const char *error,
@@ -722,14 +722,17 @@ say_logger_initialized(void)
 
 void
 say_logger_init(const char *init_str, int level, int nonblock,
-		const char *format, int background)
+		const char *format)
 {
 	/*
 	 * The logger may be early configured
 	 * by hands without configuing the whole box.
 	 */
-	if (say_logger_initialized())
+	if (say_logger_initialized()) {
+		say_set_log_level(level);
+		say_set_log_format(say_format_by_name(format));
 		return;
+	}
 
 	if (log_create(&log_std, init_str, nonblock) < 0)
 		goto fail;
@@ -749,33 +752,49 @@ say_logger_init(const char *init_str, int level, int nonblock,
 	}
 	_say = say_default;
 	say_set_log_level(level);
-	log_background = background;
 	log_pid = log_default->pid;
 	say_set_log_format(say_format_by_name(format));
 
-	if (background) {
-		fflush(stderr);
-		fflush(stdout);
-		if (log_default->fd == STDERR_FILENO) {
-			int fd = open("/dev/null", O_WRONLY);
-			if (fd < 0) {
-				diag_set(SystemError, "open /dev/null");
-				goto fail;
-			}
-			dup2(fd, STDERR_FILENO);
-			dup2(fd, STDOUT_FILENO);
-			close(fd);
-		} else {
-			dup2(log_default->fd, STDERR_FILENO);
-			dup2(log_default->fd, STDOUT_FILENO);
-		}
-	}
 	return;
 fail:
 	diag_log();
 	panic("failed to initialize logging subsystem");
 }
 
+int
+say_set_background(void)
+{
+	assert(say_logger_initialized());
+
+	if (log_background)
+		return 0;
+
+	log_background = true;
+
+	fflush(stderr);
+	fflush(stdout);
+
+	int fd;
+	int fd_null = -1;
+	if (log_default->fd == STDERR_FILENO) {
+		fd_null = open("/dev/null", O_WRONLY);
+		if (fd_null < 0) {
+			diag_set(SystemError, "open(/dev/null)");
+			return -1;
+		}
+		fd = fd_null;
+	} else {
+		fd = log_default->fd;
+	}
+
+	dup2(fd, STDERR_FILENO);
+	dup2(fd, STDOUT_FILENO);
+	if (fd_null != -1)
+		close(fd_null);
+
+	return 0;
+}
+
 void
 say_logger_free(void)
 {
diff --git a/src/lib/core/say.h b/src/lib/core/say.h
index 440e8462eed57023e60a55930dcad996fea27c6b..b78e82f1372591f0b67a6d3c8fd6a8adb1eafd45 100644
--- a/src/lib/core/say.h
+++ b/src/lib/core/say.h
@@ -297,8 +297,19 @@ say_logrotate(struct ev_loop *, struct ev_signal *, int /* revents */);
 void
 say_logger_init(const char *init_str,
 		int log_level, int nonblock,
-		const char *log_format,
-		int background);
+		const char *log_format);
+
+/**
+ * Turn on background mode for logger. Should be called after say_logger_init.
+ *
+ * If logger is NULL (writes to stderr) then stdout and stderr will be
+ * redirected to /dev/null.
+ *
+ * Otherwise (logger writes to file, pipe etc) stdout and stderr will be
+ * redirected to logger fd.
+ */
+int
+say_set_background(void);
 
 /** Test if logger is initialized. */
 bool
diff --git a/src/lua/log.lua b/src/lua/log.lua
index 7e734838850e2209e1ce3618909b8aa9a9506f97..2a5582d4a09834f224aeb516833659f34a80aafb 100644
--- a/src/lua/log.lua
+++ b/src/lua/log.lua
@@ -5,26 +5,21 @@ ffi.cdef[[
     typedef void (*sayfunc_t)(int level, const char *filename, int line,
                const char *error, const char *format, ...);
 
-    enum say_logger_type {
-        SAY_LOGGER_BOOT,
-        SAY_LOGGER_STDERR,
-        SAY_LOGGER_FILE,
-        SAY_LOGGER_PIPE,
-        SAY_LOGGER_SYSLOG
-    };
-
-    enum say_logger_type
-    log_type();
-
     void
     say_set_log_level(int new_level);
 
     void
     say_set_log_format(enum say_format format);
 
+    int
+    say_check_cfg(const char *log,
+                  int level,
+                  int nonblock,
+                  const char *format);
+
     extern void
     say_logger_init(const char *init_str, int level, int nonblock,
-                    const char *format, int background);
+                    const char *format);
 
     extern bool
     say_logger_initialized(void);
@@ -54,9 +49,6 @@ ffi.cdef[[
     pid_t log_pid;
     extern int log_level;
     extern int log_format;
-
-    int
-    say_parse_logger_type(const char **str, enum say_logger_type *type);
 ]]
 
 local S_CRIT = ffi.C.S_CRIT
@@ -97,14 +89,6 @@ local fmt_str2num = {
     ["json"]            = ffi.C.SF_JSON,
 }
 
-local function fmt_list()
-    local keyset = {}
-    for k in pairs(fmt_str2num) do
-        keyset[#keyset + 1] = k
-    end
-    return table.concat(keyset, ',')
-end
-
 -- Logging levels symbolic representation.
 local log_level_keys = {
     ['fatal']       = ffi.C.S_FATAL,
@@ -127,13 +111,15 @@ end
 
 -- Default options. The keys are part of
 -- user API , so change with caution.
-local log_cfg = {
+local default_cfg = {
     log             = nil,
     nonblock        = nil,
     level           = S_INFO,
     format          = fmt_num2str[ffi.C.SF_PLAIN],
 }
 
+local log_cfg = table.copy(default_cfg)
+
 -- Name mapping from box to log module and
 -- back. Make sure all required fields
 -- are covered!
@@ -144,141 +130,6 @@ local log2box_keys = {
     ['format']          = 'log_format',
 }
 
-local box2log_keys = {
-    ['log']             = 'log',
-    ['log_nonblock']    = 'nonblock',
-    ['log_level']       = 'level',
-    ['log_format']      = 'format',
-}
-
--- Update cfg value(s) in box.cfg instance conditionally
-local function box_cfg_update(log_key)
-    -- if it is not yet even exist just exit early
-    if type(box.cfg) ~= 'table' then
-        return
-    end
-
-    local update = function(log_key, box_key)
-        -- the box entry may be under configuration
-        -- process thus equal to nil, skip it then
-        if log_cfg[log_key] ~= nil and
-            box.cfg[box_key] ~= nil and
-            box.cfg[box_key] ~= log_cfg[log_key] then
-            box.cfg[box_key] = log_cfg[log_key]
-        end
-    end
-
-    if log_key == nil then
-        for k, v in pairs(log2box_keys) do
-            update(k, v)
-        end
-    else
-        assert(log2box_keys[log_key] ~= nil)
-        update(log_key, log2box_keys[log_key])
-    end
-end
-
--- Log options which can be set ony once.
-local cfg_static_keys = {
-    log         = true,
-    nonblock    = true,
-}
-
--- Test if static key is not changed.
-local function verify_static(k, v)
-    assert(cfg_static_keys[k] ~= nil)
-
-    if ffi.C.say_logger_initialized() == true then
-        if log_cfg[k] ~= v then
-            return false, "can't be set dynamically"
-        end
-    end
-
-    return true
-end
-
-local function parse_format(log)
-    -- There is no easy way to get pointer to ponter via FFI
-    local str_p = ffi.new('const char*[1]')
-    str_p[0] = ffi.cast('char*', log)
-    local logger_type = ffi.new('enum say_logger_type[1]')
-    local rc = ffi.C.say_parse_logger_type(str_p, logger_type)
-
-    if rc ~= 0 then
-        box.error()
-    end
-
-    return logger_type[0]
-end
-
--- Test if format is valid.
-local function verify_format(key, name, cfg)
-    assert(log_cfg[key] ~= nil)
-
-    if not fmt_str2num[name] then
-        local m = "expected %s"
-        return false, m:format(fmt_list())
-    end
-
-    local log_type = ffi.C.log_type()
-
-    -- When comes from log.cfg{} or box.cfg{}
-    -- initial call we might be asked to setup
-    -- syslog with json which is not allowed.
-    --
-    -- Note the cfg table comes from two places:
-    -- box api interface and log module itself.
-    -- The good thing that we're only needed log
-    -- entry which is the same key for both.
-    if cfg ~= nil and cfg['log'] ~= nil then
-        log_type = parse_format(cfg['log'])
-    end
-
-    if fmt_str2num[name] == ffi.C.SF_JSON then
-        if log_type == ffi.C.SAY_LOGGER_SYSLOG then
-            local m = "%s can't be used with syslog logger"
-            return false, m:format(fmt_num2str[ffi.C.SF_JSON])
-        end
-    end
-
-    return true
-end
-
--- Test if level is a valid string. The
--- number may be any for to backward compatibility.
-local function verify_level(key, level)
-    assert(log_cfg[key] ~= nil)
-
-    if type(level) == 'string' then
-        if not log_level_keys[level] then
-            local m = "expected %s"
-            return false, m:format(log_level_list())
-        end
-    elseif type(level) ~= 'number' then
-            return false, "must be a number or a string"
-    end
-
-    return true
-end
-
-local verify_ops = {
-    ['log']         = verify_static,
-    ['nonblock']    = verify_static,
-    ['format']      = verify_format,
-    ['level']       = verify_level,
-}
-
--- Verify a value for the particular key.
-local function verify_option(k, v, ...)
-    assert(k ~= nil)
-
-    if verify_ops[k] ~= nil then
-        return verify_ops[k](k, v, ...)
-    end
-
-    return true
-end
-
 -- Main routine which pass data to C logging code.
 local function say(level, fmt, ...)
     if ffi.C.log_level < level then
@@ -325,72 +176,18 @@ local function say_closure(lvl)
     end
 end
 
+local log_error = say_closure(S_ERROR)
+local log_warn = say_closure(S_WARN)
+local log_info = say_closure(S_INFO)
+local log_verbose = say_closure(S_VERBOSE)
+local log_debug = say_closure(S_DEBUG)
+
 -- Rotate log (basically reopen the log file and
 -- start writting into it).
 local function log_rotate()
     ffi.C.say_logrotate(nil, nil, 0)
 end
 
--- Set new logging level, the level must be valid!
-local function set_log_level(level, update_box_cfg)
-    assert(type(level) == 'number')
-
-    ffi.C.say_set_log_level(level)
-
-    rawset(log_cfg, 'level', level)
-
-    if update_box_cfg then
-        box_cfg_update('level')
-    end
-
-    local m = "log: level set to %s"
-    say(S_DEBUG, m:format(level))
-end
-
--- Tries to set a new level, or print an error.
-local function log_level(level)
-    local ok, msg = verify_option('level', level)
-    if not ok then
-        error(msg)
-    end
-
-    if type(level) == 'string' then
-        level = log_level_keys[level]
-    end
-
-    set_log_level(level, true)
-end
-
--- Set a new logging format, the name must be valid!
-local function set_log_format(name, update_box_cfg)
-    assert(fmt_str2num[name] ~= nil)
-
-    if fmt_str2num[name] == ffi.C.SF_JSON then
-        ffi.C.say_set_log_format(ffi.C.SF_JSON)
-    else
-        ffi.C.say_set_log_format(ffi.C.SF_PLAIN)
-    end
-
-    rawset(log_cfg, 'format', name)
-
-    if update_box_cfg then
-        box_cfg_update('format')
-    end
-
-    local m = "log: format set to '%s'"
-    say(S_DEBUG, m:format(name))
-end
-
--- Tries to set a new format, or print an error.
-local function log_format(name)
-    local ok, msg = verify_option('format', name)
-    if not ok then
-        error(msg)
-    end
-
-    set_log_format(name, true)
-end
-
 -- Returns pid of a pipe process.
 local function log_pid()
     return tonumber(ffi.C.log_pid)
@@ -470,170 +267,122 @@ end
 
 Ratelimit.log_crit = log_ratelimited_closure(S_CRIT)
 
--- Fetch a value from log to box.cfg{}.
-local function box_api_cfg_get(key)
-    return log_cfg[box2log_keys[key]]
-end
+local option_types = {
+    log = 'string',
+    nonblock = 'boolean',
+    level = 'number, string',
+    format = 'string',
+}
 
--- Set value to log from box.cfg{}.
-local function box_api_cfg_set(cfg, key, value)
-    local log_key = box2log_keys[key]
+local log_initialized = false
 
-    -- a special case where we need to restore
-    -- nil value from previous setup attempt.
-    if value == box.NULL then
-        log_cfg[log_key] = nil
-        return true
+-- Convert cfg options to types suitable for ffi say_ functions.
+local function log_C_cfg(cfg)
+    local cfg_C = table.copy(cfg)
+    if type(cfg.level) == 'string' then
+        cfg_C.level = log_level_keys[cfg.level]
     end
-
-    local ok, msg = verify_option(log_key, value, cfg)
-    if not ok then
-        return false, msg
+    local nonblock
+    if cfg.nonblock ~= nil then
+        nonblock = cfg.nonblock and 1 or 0
+    else
+        nonblock = -1
     end
-
-    log_cfg[log_key] = value
-    return true
+    cfg_C.nonblock = nonblock
+    return cfg_C
 end
 
--- Set logging level from reloading box.cfg{}
-local function box_api_cfg_set_log_level()
-    local log_key = box2log_keys['log_level']
-    local v = box.cfg['log_level']
-
-    local ok, msg = verify_option(log_key, v)
-    if not ok then
-        box.error(box.error.CFG, 'log_level', msg)
+-- Check cfg is valid and thus can be applied
+local function log_check_cfg(cfg)
+    if type(cfg.level) == 'string' and
+       log_level_keys[cfg.level] == nil then
+        local err = ("expected %s"):format(log_level_list())
+        box.error(box.error.CFG, "log_level", err)
     end
 
-    if type(v) == 'string' then
-        v = log_level_keys[v]
+    if log_initialized then
+        if log_cfg.log ~= cfg.log then
+            box.error(box.error.RELOAD_CFG, 'log');
+        end
+        if log_cfg.nonblock ~= cfg.nonblock then
+            box.error(box.error.RELOAD_CFG, 'log_nonblock');
+        end
     end
 
-    set_log_level(v, false)
-end
-
--- Set logging format from reloading box.cfg{}
-local function box_api_set_log_format()
-    local log_key = box2log_keys['log_format']
-    local v = box.cfg['log_format']
-
-    local ok, msg = verify_option(log_key, v)
-    if not ok then
-        box.error(box.error.CFG, 'log_format', msg)
+    local cfg_C = log_C_cfg(cfg)
+    if ffi.C.say_check_cfg(cfg_C.log, cfg_C.level,
+                           cfg_C.nonblock, cfg_C.format) ~= 0 then
+        box.error()
     end
-
-    set_log_format(v, false)
 end
 
--- Reload dynamic options.
-local function reload_cfg(cfg)
-    for k in pairs(cfg_static_keys) do
-        if cfg[k] ~= nil then
-            local ok, msg = verify_static(k, cfg[k])
-            if not ok then
-                local m = "log.cfg: \'%s\' %s"
-                error(m:format(k, msg))
-            end
+-- Update box.internal.cfg on log config changes
+local function box_cfg_update(key)
+    if key == nil then
+        for km, kb  in pairs(log2box_keys) do
+            box.internal.update_cfg(kb, log_cfg[km])
         end
-    end
-
-    if cfg.level ~= nil then
-        log_level(cfg.level)
-    end
-
-    if cfg.format ~= nil then
-        log_format(cfg.format)
+    else
+        box.internal.update_cfg(log2box_keys[key], log_cfg[key])
     end
 end
 
--- Load or reload configuration via log.cfg({}) call.
-local function load_cfg(self, cfg)
-    cfg = cfg or {}
+local function set_log_level(level)
+    box.internal.check_cfg_option_type(option_types.level, 'level', level)
+    local cfg = table.copy(log_cfg)
+    cfg.level = level
+    log_check_cfg(cfg)
 
-    -- log option might be zero length string, which
-    -- is fine, we should treat it as nil.
-    if cfg.log ~= nil then
-        if type(cfg.log) ~= 'string' or cfg.log:len() == 0 then
-            cfg.log = nil
-        end
-    end
+    local cfg_C = log_C_cfg(cfg)
+    ffi.C.say_set_log_level(cfg_C.level)
+    log_cfg.level = level
 
-    if cfg.format ~= nil then
-        local ok, msg = verify_option('format', cfg.format, cfg)
-        if not ok then
-            local m = "log.cfg: \'%s\' %s"
-            error(m:format('format', msg))
-        end
-    end
+    box_cfg_update('level')
 
-    if cfg.level ~= nil then
-        local ok, msg = verify_option('level', cfg.level)
-        if not ok then
-            local m = "log.cfg: \'%s\' %s"
-            error(m:format('level', msg))
-        end
-        -- Convert level to a numeric value since
-        -- low level api operates with numbers only.
-        if type(cfg.level) == 'string' then
-            assert(log_level_keys[cfg.level] ~= nil)
-            cfg.level = log_level_keys[cfg.level]
-        end
-    end
-
-    if cfg.nonblock ~= nil then
-        if type(cfg.nonblock) ~= 'boolean' then
-            error("log.cfg: 'nonblock' option must be 'true' or 'false'")
-        end
-    end
+    log_debug("log: level set to %s", level)
+end
 
-    if ffi.C.say_logger_initialized() == true then
-        return reload_cfg(cfg)
-    end
+local function set_log_format(format)
+    box.internal.check_cfg_option_type(option_types.format, 'format', format)
+    local cfg = table.copy(log_cfg)
+    cfg.format = format
+    log_check_cfg(cfg)
 
-    cfg.level = cfg.level or log_cfg.level
-    cfg.format = cfg.format or log_cfg.format
-    cfg.nonblock = cfg.nonblock or log_cfg.nonblock
+    ffi.C.say_set_log_format(fmt_str2num[format])
+    log_cfg.format = format
 
-    -- nonblock is special: it has to become integer
-    -- for ffi call but in config we have to save
-    -- true value only for backward compatibility!
-    local nonblock = cfg.nonblock
+    box_cfg_update('format')
 
-    if nonblock == nil or nonblock == false then
-        nonblock = 0
-    else
-        nonblock = 1
-    end
+    log_debug("log: format set to '%s'", format)
+end
 
-    -- Parsing for validation purposes
-    if cfg.log ~= nil then
-        parse_format(cfg.log)
-    end
+local function log_configure(self, cfg)
+    cfg = box.internal.prepare_cfg(cfg, default_cfg, option_types)
+    box.internal.merge_cfg(cfg, log_cfg);
 
-    -- We never allow confgure the logger in background
-    -- mode since we don't know how the box will be configured
-    -- later.
-    ffi.C.say_logger_init(cfg.log, cfg.level,
-                          nonblock, cfg.format, 0)
+    log_check_cfg(cfg)
+    local cfg_C = log_C_cfg(cfg)
+    ffi.C.say_logger_init(cfg_C.log, cfg_C.level,
+                          cfg_C.nonblock, cfg_C.format)
+    log_initialized = true
 
-    if nonblock == 1 then
-        nonblock = true
-    else
-        nonblock = nil
+    for o in pairs(option_types) do
+        log_cfg[o] = cfg[o]
     end
 
-    -- Update log_cfg vars to show them in module
-    -- configuration output.
-    rawset(log_cfg, 'log', cfg.log)
-    rawset(log_cfg, 'level', cfg.level)
-    rawset(log_cfg, 'nonblock', nonblock)
-    rawset(log_cfg, 'format', cfg.format)
-
-    -- and box.cfg output as well.
     box_cfg_update()
 
-    local m = "log.cfg({log=%s,level=%s,nonblock=%s,format=\'%s\'})"
-    say(S_DEBUG, m:format(cfg.log, cfg.level, cfg.nonblock, cfg.format))
+    log_debug("log.cfg({log=%s, level=%s, nonblock=%s, format=%s})",
+              cfg.log, cfg.level, cfg.nonblock, cfg.format)
+end
+
+local function box_to_log_cfg()
+    return {
+        log = box.cfg.log,
+        level = box.cfg.log_level,
+        format = box.cfg.log_format,
+        nonblock = box.cfg.log_nonblock,
+    }
 end
 
 local compat_warning_said = false
@@ -648,23 +397,21 @@ local compat_v16 = {
 }
 
 local log = {
-    warn = say_closure(S_WARN),
-    info = say_closure(S_INFO),
-    verbose = say_closure(S_VERBOSE),
-    debug = say_closure(S_DEBUG),
-    error = say_closure(S_ERROR),
+    warn = log_warn,
+    info = log_info,
+    verbose = log_verbose,
+    debug = log_debug,
+    error = log_error,
     rotate = log_rotate,
     pid = log_pid,
-    level = log_level,
-    log_format = log_format,
+    level = set_log_level,
+    log_format = set_log_format,
     cfg = setmetatable(log_cfg, {
-        __call = load_cfg,
+        __call = log_configure,
     }),
     box_api = {
-        cfg_get = box_api_cfg_get,
-        cfg_set = box_api_cfg_set,
-        cfg_set_log_level = box_api_cfg_set_log_level,
-        cfg_set_log_format = box_api_set_log_format,
+        cfg = function() log_configure(log_cfg, box_to_log_cfg()) end,
+        cfg_check = function() log_check_cfg(box_to_log_cfg()) end,
     },
     internal = {
         ratelimit = {
diff --git a/src/main.cc b/src/main.cc
index 1c45fa9d051c51995c7d78fbc5c59d298624ef04..b0a2212685de5f2983d7abdc58e84abe0ed0feb2 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -87,6 +87,7 @@
 #include "ssl_cert_paths_discover.h"
 #include "core/errinj.h"
 #include "core/clock_lowres.h"
+#include "lua/utils.h"
 
 static pid_t master_pid = getpid();
 static struct pidfh *pid_file_handle;
@@ -428,7 +429,6 @@ load_cfg(void)
 
 	int background = cfg_geti("background");
 	const char *log = cfg_gets("log");
-	const char *log_format = cfg_gets("log_format");
 	pid_file = (char *)cfg_gets("pid_file");
 	if (pid_file != NULL) {
 		pid_file = abspath(pid_file);
@@ -476,11 +476,10 @@ load_cfg(void)
 	 * logger init must happen before daemonising in order for the error
 	 * to show and for the process to exit with a failure status
 	 */
-	say_logger_init(log,
-			cfg_geti("log_level"),
-			cfg_getb("log_nonblock"),
-			log_format,
-			background);
+	if (box_init_say() != 0) {
+		diag_log();
+		exit(EXIT_FAILURE);
+	}
 
 	/*
 	 * Initialize flight recorder after say logger as we might use
diff --git a/static-build/test/static-build/exports.test.lua b/static-build/test/static-build/exports.test.lua
index 2b23062cf360877a50416cfdaca481ab6b3f8733..32f8035172e3f41d7350f3b02f392860c3dc786d 100755
--- a/static-build/test/static-build/exports.test.lua
+++ b/static-build/test/static-build/exports.test.lua
@@ -72,7 +72,6 @@ local check_symbols = {
     'crc32_calc',
     'decimal_unpack',
 
-    'log_type',
     'say_set_log_level',
     'say_logrotate',
     'say_set_log_format',
diff --git a/test/app-tap/gh-5130-panic-on-invalid-log.test.lua b/test/app-tap/gh-5130-panic-on-invalid-log.test.lua
index 7ebff14259a30b99135e37d89edfab30aeb642ed..277b836bc04d014cda7af68cca53ff522b2f6215 100755
--- a/test/app-tap/gh-5130-panic-on-invalid-log.test.lua
+++ b/test/app-tap/gh-5130-panic-on-invalid-log.test.lua
@@ -18,6 +18,6 @@ test:ok(ok)
 
 -- Dynamic reconfiguration - error, no info about invalid logger type
 _, err = pcall(log.cfg, {log=' :invalid'})
-test:like(err, "can't be set dynamically")
+test:like(err, "Can't set option 'log' dynamically")
 
 os.exit(test:check() and 0 or 1)
diff --git a/test/app-tap/logger.test.lua b/test/app-tap/logger.test.lua
index 25074fcb6d87c00fbc829beabfc2419b9f0003d1..dd3e9a14d75877076f52330097decec9e686263c 100755
--- a/test/app-tap/logger.test.lua
+++ b/test/app-tap/logger.test.lua
@@ -1,22 +1,91 @@
 #!/usr/bin/env tarantool
 
+local log = require('log')
+
 local test = require('tap').test('log')
-test:plan(64)
+test:plan(104)
+
+local function test_invalid_cfg(cfg_method, cfg, name, expected)
+    local _, err = pcall(cfg_method, cfg)
+    test:ok(tostring(err):find(expected) ~= nil, name)
+end
+
+local function test_allowed_types(cfg_method, cfg, name, allowed_types)
+    local prefix
+    if string.find(allowed_types, ',') then
+        prefix = 'should be one of types '
+    else
+        prefix = 'should be of type '
+    end
+    test_invalid_cfg(cfg_method, cfg, name, prefix .. allowed_types)
+end
+
+-- Test allowed types for options
+test_allowed_types(log.cfg, {log =  1},
+                   'log.cfg allowed log types', 'string')
+test_allowed_types(log.cfg, {level = true},
+                   'log.cfg allowed level types', 'number, string')
+test_allowed_types(log.cfg, {format = true},
+                   'log.cfg allowed format types', 'string')
+test_allowed_types(log.cfg, {nonblock = 'hi'},
+                   'log.cfg allowed nonblock types', 'boolean')
+test_allowed_types(box.cfg, {log = 1},
+                   'box.cfg allowed log types', 'string')
+test_allowed_types(box.cfg,{log_level = true},
+                   'box.cfg allowed log_level types', 'number, string')
+test_allowed_types(box.cfg, {log_format = true},
+                   'box.cfg allowed log_format types', 'string')
+test_allowed_types(box.cfg, {log_nonblock = 'hi'},
+                   'box.cfg allowed log_nonblock types', 'boolean')
+
+-- Test other invalid inputs
+
+test_invalid_cfg(log.cfg, {log = 'syslog:', format = 'json'},
+                 "log.cfg syslog and json",
+                 "'json' can't be used with syslog logger")
+test_invalid_cfg(box.cfg, {log = 'syslog:', log_format = 'json'},
+                 "box.cfg syslog and json",
+                 "'json' can't be used with syslog logger")
+
+-- Don't check all invalid inputs for both box.cfg and log.cfg as
+-- now they use same check function.
+
+-- gh-7447
+test_invalid_cfg(log.cfg, {log = 'syslog:xxx'},
+                 "log.cfg invalid syslog",
+                 "bad option 'xxx'")
+
+test_invalid_cfg(log.cfg, {log = 'xxx:'},
+                 "log.cfg invalid logger prefix",
+                 "expecting a file name or a prefix, such as " ..
+                    "'|', 'pipe:', 'syslog:'")
+
+test_invalid_cfg(log.cfg, {format = 'xxx'},
+                 "log.cfg invalid format",
+                 "expected 'plain' or 'json'")
+
+test_invalid_cfg(log.cfg, {nonblock = true},
+                 "log.cfg nonblock and stderr",
+                 'the option is incompatible with file/stderr logger')
+
+test_invalid_cfg(log.cfg, {log = '1.log', nonblock = true},
+                 "log.cfg nonblock and file",
+                 'the option is incompatible with file/stderr logger')
+
+test_invalid_cfg(log.cfg, {format = 'xxx'},
+                 "log.cfg invalid format",
+                 "expected 'plain' or 'json'")
+
+test_invalid_cfg(log.cfg, {xxx = 1},
+                 "log.cfg unexpected option",
+                 'unexpected option')
 
 --
 -- gh-5121: Allow to use 'json' output before box.cfg()
 --
-local log = require('log')
 local _, err = pcall(log.log_format, 'json')
 test:ok(err == nil)
 
--- We're not allowed to use json with syslog though.
-_, err = pcall(log.cfg, {log='syslog:', format='json'})
-test:ok(tostring(err):find("can\'t be used with syslog logger") ~= nil)
-
-_, err = pcall(box.cfg, {log='syslog:', log_format='json'})
-test:ok(tostring(err):find("can\'t be used with syslog logger") ~= nil)
-
 -- switch back to plain to next tests
 log.log_format('plain')
 
@@ -71,6 +140,14 @@ local line = file:read()
 local s = json.decode(line)
 test:ok(s['message'] == message, "message match")
 
+-- Try to change options that can't be changed on first box.cfg()
+test_invalid_cfg(box.cfg, {log = '2.log'},
+                 "reconfigure logger thru first box.cfg",
+                 "Can't set option 'log' dynamically")
+test_invalid_cfg(box.cfg, {log_nonblock = true},
+                 "reconfigure nonblock thru first box.cfg",
+                 "Can't set option 'log_nonblock' dynamically")
+
 -- Now switch to box.cfg interface
 box.cfg{
     log = filename,
@@ -83,21 +160,41 @@ verify_keys("box.cfg")
 
 -- Test symbolic names for loglevels
 log.cfg({level='fatal'})
-test:ok(log.cfg.level == 0 and box.cfg.log_level == 0, 'both got fatal')
+test:ok(log.cfg.level == 'fatal' and box.cfg.log_level == 'fatal',
+        'both got fatal')
 log.cfg({level='syserror'})
-test:ok(log.cfg.level == 1 and box.cfg.log_level == 1, 'both got syserror')
+test:ok(log.cfg.level == 'syserror' and box.cfg.log_level == 'syserror',
+        'both got syserror')
 log.cfg({level='error'})
-test:ok(log.cfg.level == 2 and box.cfg.log_level == 2, 'both got error')
+test:ok(log.cfg.level == 'error' and box.cfg.log_level == 'error',
+        'both got error')
 log.cfg({level='crit'})
-test:ok(log.cfg.level == 3 and box.cfg.log_level == 3, 'both got crit')
+test:ok(log.cfg.level == 'crit' and box.cfg.log_level == 'crit',
+        'both got crit')
 log.cfg({level='warn'})
-test:ok(log.cfg.level == 4 and box.cfg.log_level == 4, 'both got warn')
+test:ok(log.cfg.level == 'warn' and box.cfg.log_level == 'warn',
+        'both got warn')
 log.cfg({level='info'})
-test:ok(log.cfg.level == 5 and box.cfg.log_level == 5, 'both got info')
+test:ok(log.cfg.level == 'info' and box.cfg.log_level == 'info',
+        'both got info')
 log.cfg({level='verbose'})
-test:ok(log.cfg.level == 6 and box.cfg.log_level == 6, 'both got verbose')
+test:ok(log.cfg.level == 'verbose' and box.cfg.log_level == 'verbose',
+        'both got verbose')
 log.cfg({level='debug'})
-test:ok(log.cfg.level == 7 and box.cfg.log_level == 7, 'both got debug')
+test:ok(log.cfg.level == 'debug' and box.cfg.log_level == 'debug',
+        'both got debug')
+
+log.cfg{level = 4}
+test:ok(log.cfg.level == 4, "log.cfg number level then read log.cfg")
+test:ok(box.cfg.log_level == 4, "log.cfg number level then read box.cfg")
+
+box.cfg{log_level = 5}
+test:ok(log.cfg.level == 5, "box.cfg number level then read log.cfg")
+test:ok(box.cfg.log_level == 5, "box.cfg number level then read box.cfg")
+
+box.cfg{log_level = 'warn'}
+test:ok(log.cfg.level == 'warn', "box.cfg string level then read log.cfg")
+test:ok(box.cfg.log_level == 'warn', "box.cfg string level then read box.cfg")
 
 box.cfg{
     log = filename,
@@ -105,24 +202,51 @@ box.cfg{
     memtx_memory = 107374182,
 }
 
--- Now try to change a static field.
-_, err = pcall(box.cfg, {log_level = 5, log = "2.txt"})
-test:ok(tostring(err):find("Can't set option 'log' dynamically") ~= nil,
-        "box.cfg.log cannot be set dynamically")
+-- Try to change options that can't be changed on non-first box.cfg()
+test_invalid_cfg(box.cfg, {log = '2.log'},
+                 "reconfigure logger thru non-first box.cfg",
+                 "Can't set option 'log' dynamically")
 test:ok(box.cfg.log == filename, "filename match")
 test:ok(box.cfg.log_level == 6, "loglevel match")
 verify_keys("box.cfg static error")
 
+test_invalid_cfg(box.cfg, {log_nonblock = true},
+                 "reconfigure nonblock thru non-first box.cfg",
+                 "Can't set option 'log_nonblock' dynamically")
+
+-- Test invalid values for setters
+
+_, err = pcall(log.log_format, {})
+test:ok(tostring(err):find('should be of type string') ~= nil,
+        "invalid format setter value type")
+
+_, err = pcall(log.level, {})
+test:ok(tostring(err):find('should be one of types number, string') ~= nil,
+        "invalid format setter value type")
+
 -- Change format and levels.
+
 _, err = pcall(log.log_format, 'json')
-test:ok(err == nil, "change to json")
+test:ok(err == nil, "format setter result")
+test:ok(log.cfg.format == 'json', "format setter cfg")
+verify_keys("format setter verify keys")
+
 _, err = pcall(log.level, 1)
+test:ok(err == nil, "level setter number result")
+test:ok(log.cfg.level == 1, "level setter number cfg")
+verify_keys("level setter number verify keys")
+
+_, err = pcall(log.level, 'warn')
 test:ok(err == nil, "change log level")
-verify_keys("log change json and level")
+test:ok(log.cfg.level == 'warn', "level setter string")
+verify_keys("level setter string verify keys")
 
--- Restore defaults
-log.log_format('plain')
-log.level(5)
+-- Check reset works thru log.cfg
+log.cfg{level = box.NULL}
+test:ok(log.cfg.level == 5, "reset of level thru log.cfg")
+
+log.cfg{format = ""}
+test:ok(log.cfg.format == 'plain', "reset of plain thru log.cfg")
 
 --
 -- Check that Tarantool creates ADMIN session for #! script
diff --git a/test/app-tap/logmod.test.lua b/test/app-tap/logmod.test.lua
index 6eab77f7e65bea417c0f5178045349826783182f..6fa06d4916763cea8611f970af6cd16babe07cd7 100755
--- a/test/app-tap/logmod.test.lua
+++ b/test/app-tap/logmod.test.lua
@@ -9,27 +9,27 @@ log.log_format('plain')
 
 -- Test symbolic names for loglevels
 local _, err = pcall(log.cfg, {level='fatal'})
-test:ok(err == nil and log.cfg.level == 0, 'both got fatal')
+test:ok(err == nil and log.cfg.level == 'fatal', 'got fatal')
 
 _, err = pcall(log.cfg, {level='syserror'})
-test:ok(err == nil and log.cfg.level == 1, 'got syserror')
+test:ok(err == nil and log.cfg.level == 'syserror', 'got syserror')
 
 _, err = pcall(log.cfg, {level='error'})
-test:ok(err == nil and log.cfg.level == 2, 'got error')
+test:ok(err == nil and log.cfg.level == 'error', 'got error')
 
 _, err = pcall(log.cfg, {level='crit'})
-test:ok(err == nil and log.cfg.level == 3, 'got crit')
+test:ok(err == nil and log.cfg.level == 'crit', 'got crit')
 
 _, err = pcall(log.cfg, {level='warn'})
-test:ok(err == nil and log.cfg.level == 4, 'got warn')
+test:ok(err == nil and log.cfg.level == 'warn', 'got warn')
 
 _, err = pcall(log.cfg, {level='info'})
-test:ok(err == nil and log.cfg.level == 5, 'got info')
+test:ok(err == nil and log.cfg.level == 'info', 'got info')
 
 _, err = pcall(log.cfg, {level='verbose'})
-test:ok(err == nil and log.cfg.level == 6, 'got verbose')
+test:ok(err == nil and log.cfg.level == 'verbose', 'got verbose')
 
 _, err = pcall(log.cfg, {level='debug'})
-test:ok(err == nil and log.cfg.level == 7, 'got debug')
+test:ok(err == nil and log.cfg.level == 'debug', 'got debug')
 
 os.exit(test:check() and 0 or 1)
diff --git a/test/unit/memtx_allocator.cc b/test/unit/memtx_allocator.cc
index 152fc385d2a186f6b82d7e516ce411b3db7d5825..d43852983433151a5b5d3c3a45edd264a0d68b56 100644
--- a/test/unit/memtx_allocator.cc
+++ b/test/unit/memtx_allocator.cc
@@ -499,8 +499,7 @@ test_main()
 int
 main()
 {
-	say_logger_init("/dev/null", S_INFO, /*nonblock=*/true, "plain",
-			/*background=*/false);
+	say_logger_init("/dev/null", S_INFO, /*nonblock=*/true, "plain");
 	clock_lowres_signal_init();
 	memory_init();
 	fiber_init(fiber_c_invoke);
diff --git a/test/unit/popen.c b/test/unit/popen.c
index b4e1b9557d074bbbb02abaef4d6d3ec211632ad6..25195ee63e5a5456c2d1dde38bc26d4a7971d424 100644
--- a/test/unit/popen.c
+++ b/test/unit/popen.c
@@ -236,7 +236,7 @@ int
 main(int argc, char *argv[])
 {
 #if 0
-	say_logger_init(NULL, S_DEBUG, 0, "plain", 0);
+	say_logger_init(NULL, S_DEBUG, 0, "plain");
 #endif
 	memory_init();
 
diff --git a/test/unit/raft_test_utils.c b/test/unit/raft_test_utils.c
index 186eeb8d4fd1d57d445f1fbfd9448695cfd479a9..57fe8af6467a54690e88569c42f746da6b3b81b5 100644
--- a/test/unit/raft_test_utils.c
+++ b/test/unit/raft_test_utils.c
@@ -627,7 +627,7 @@ raft_run_test(const char *log_file, fiber_func test)
 	int fd = open(log_file, O_TRUNC);
 	if (fd != -1)
 		close(fd);
-	say_logger_init(log_file, 5, 1, "plain", 0);
+	say_logger_init(log_file, 5, 1, "plain");
 	/* Print the seed to be able to reproduce a bug with the same seed. */
 	say_info("Random seed = %llu", (unsigned long long) seed);
 
diff --git a/test/unit/say.c b/test/unit/say.c
index bfb72d530157e6fca07b3bd1e02f4f9e082ec432..78cadce6c1ccde1908be700c530ec45d30713122 100644
--- a/test/unit/say.c
+++ b/test/unit/say.c
@@ -167,7 +167,7 @@ int main()
 {
 	memory_init();
 	fiber_init(fiber_c_invoke);
-	say_logger_init("/dev/null", S_INFO, 0, "plain", 0);
+	say_logger_init("/dev/null", S_INFO, 0, "plain");
 
 	plan(33);
 
diff --git a/test/unit/swim_proto.c b/test/unit/swim_proto.c
index 50d27992e59824ffced466cddb757cf1d0379da6..fb0fd30551a3b544f2a4dd24f6a53b2d20559a31 100644
--- a/test/unit/swim_proto.c
+++ b/test/unit/swim_proto.c
@@ -257,7 +257,7 @@ main()
 	int fd = open("log.txt", O_TRUNC);
 	if (fd != -1)
 		close(fd);
-	say_logger_init("log.txt", 6, 1, "plain", 0);
+	say_logger_init("log.txt", 6, 1, "plain");
 
 	swim_test_member_def();
 	swim_test_meta();
diff --git a/test/unit/swim_test_utils.c b/test/unit/swim_test_utils.c
index 6badabcbd87ba62ccce180d2cf9508b4fbac16e4..189979bb918c78d4368ce9401b7f0ffb06a96fce 100644
--- a/test/unit/swim_test_utils.c
+++ b/test/unit/swim_test_utils.c
@@ -866,7 +866,7 @@ swim_run_test(const char *log_file, fiber_func test)
 	int fd = open(log_file, O_TRUNC);
 	if (fd != -1)
 		close(fd);
-	say_logger_init(log_file, 5, 1, "plain", 0);
+	say_logger_init(log_file, 5, 1, "plain");
 	/*
 	 * Print the seed to be able to reproduce a bug with the
 	 * same seed.