From 90585acf122f1d938ad15bdc24867e7f4ca28124 Mon Sep 17 00:00:00 2001 From: C9luster <138663536+C9luster@users.noreply.github.com> Date: Thu, 2 Jan 2025 21:38:54 +0800 Subject: [PATCH] =?UTF-8?q?SDK=E6=96=B0=E5=A2=9E=E6=BB=9A=E5=8A=A8?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=8A=9F=E8=83=BD=20(#706)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * SDK新增滚动日志功能 * update-error-filename * update * update * update * 增加多进程日志支持 --------- Co-authored-by: yinjiaqi Co-authored-by: zhouhao10 --- docs/BasisModule/Trace/Debug.md | 35 ++++ python/__init__.py | 3 + python/tests/test_log_set_log_config.py | 138 +++++++++++++++ python/utils/logger_file_headler.py | 98 +++++++++++ python/utils/logger_util.py | 216 +++++++++++++++++++++--- 5 files changed, 465 insertions(+), 25 deletions(-) create mode 100644 python/tests/test_log_set_log_config.py create mode 100644 python/utils/logger_file_headler.py diff --git a/docs/BasisModule/Trace/Debug.md b/docs/BasisModule/Trace/Debug.md index 11d4239e3..f70baa3f1 100644 --- a/docs/BasisModule/Trace/Debug.md +++ b/docs/BasisModule/Trace/Debug.md @@ -51,4 +51,39 @@ System.setProperty("APPBUILDER_LOGLFILE", "/tmp/appbuilder.log"); ```golang // golang os.Setenv("APPBUILDER_LOGLEVEL", "/tmp/appbuilder.log") +``` + +## `setLogConfig`功能 + +Appbuilder-SDK新增滚动日志功能 + +主要参数: +- console_output: 数据类型bool,默认值True,LOG日志是否在控制台输出 +- loglevel: 数据类型str,默认值"DEBUG",LOG日志级别 +- log_path: 数据类型str,默认值"/tmp",默认日志存放路径。 +- file_name: 数据类型str,默认值为进程id,日志名前缀 +- rotate_frequency: 数据类型str,默认值"MIDNIGHT",LOG日志滚动更新时间单位 + - "S": 以秒为单位 + - "M": 以分钟为单位 + - "H": 以小时为单位 + - "D": 以天为时间单位 + - "MIDNIGHT": 每日凌晨更新 +- rotate_interval: 数据类型int,默认值1,LOG日志按时间滚动的参数,默认值为1,与when参数联合使用 +- max_file_size: 数据类型Optional[int],默认值None,传入`None`或负数会自动更新为系统最大整数`sys.maxsize`,单个滚动的LOG日志文件的最大大小,例:10M即为10\*1024\*1024 即需要传入 # 以B为单位 +- total_log_size: 数据类型Optional[int],默认值None,传入`None`或负数会自动更新为系统最大整数`sys.maxsize`,当前目录下可储存的LOG日志文件的最大大小,例:10M即为10\*1024\*1024 # 以B为单位 +- max_log_files: 数据类型Optional[int],默认值None,传入`None`或负数会自动更新为系统最大整数`sys.maxsize`,当前目录下可储存的LOG日志文件的最大数量 + +**注意:`setLogConfig`会自动生成error.file_name日志与file_name日志文件分别储存`error`级别日志和`loglevel`级别的日志,且两种日志文件的滚动逻辑是独立的,不相互影响。** +```python +# python +appbuilder.logger.setLogConfig( + console_output = False, + loglevel="DEBUG" + log_path="/tmp",, + rotate_frequency="MIDNIGHT", # 每日凌晨更新 + rotate_interval=1, + max_file_size=100 * 1024 *1024, # 最大日志大小为100MB + total_log_size=1024 * 1024 *1024, # 最大储存1GB的日志 + max_log_files=10, # 当前目录储存的最大LOG日志数 + ) ``` \ No newline at end of file diff --git a/python/__init__.py b/python/__init__.py index 09fb670ae..8f0ca46c3 100644 --- a/python/__init__.py +++ b/python/__init__.py @@ -203,8 +203,11 @@ def get_default_header(): from appbuilder.utils.trace.tracer import AppBuilderTracer, AppbuilderInstrumentor +from .utils.logger_file_headler import SizeAndTimeRotatingFileHandler + __all__ = [ "logger", + "SizeAndTimeRotatingFileHandler", "BadRequestException", "ForbiddenException", "NotFoundException", diff --git a/python/tests/test_log_set_log_config.py b/python/tests/test_log_set_log_config.py new file mode 100644 index 000000000..a4560b7b8 --- /dev/null +++ b/python/tests/test_log_set_log_config.py @@ -0,0 +1,138 @@ +# Copyright (c) 2024 Baidu, Inc. All Rights Reserved. +# +# 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. +import os +import time +import logging +import unittest + + +from appbuilder import SizeAndTimeRotatingFileHandler +from appbuilder.utils.logger_util import LoggerWithLoggerId + +class TestLogSetLogConfig(unittest.TestCase): + def test_set_log_config(self): + lwl=LoggerWithLoggerId(logger='test_logger',extra={'logid':'test_logid'},loglevel='INFO') + lwl.setLogConfig( + console_output = True, + loglevel='DEBUG', + file_name='test.log', + rotate_frequency='D', + rotate_interval=0, # 测试rotate_interval<1时,自动更新为1 + max_file_size=None, # 测试not max_file_size or max_file_size <= 0时,自动更新为sys.maxsize + total_log_size=None, # 测试not total_log_size or total_log_size <= 0时,自动更新为sys.maxsize + max_log_files=None, # 测试not max_log_files or max_log_files <= 0时,自动更新为sys.maxsize + ) + + def test_set_log_config_log_path(self): + os.environ["APPBUILDER_LOGPATH"] = "/tmp" + lwl=LoggerWithLoggerId(logger='test_logger',extra={'logid':'test_logid'},loglevel='INFO') + lwl.setLogConfig( + console_output = True, + loglevel='DEBUG', + log_path='/tmp', + file_name='test.log', + rotate_frequency='D', + rotate_interval=0, # 测试rotate_interval<1时,自动更新为1 + max_file_size=None, # 测试not max_file_size or max_file_size <= 0时,自动更新为sys.maxsize + total_log_size=None, # 测试not total_log_size or total_log_size <= 0时,自动更新为sys.maxsize + max_log_files=None, # 测试not max_log_files or max_log_files <= 0时,自动更新为sys.maxsize + ) + + def test_set_log_config_raise_error(self): + lwl=LoggerWithLoggerId(logger='test_logger',extra={'logid':'test_logid'},loglevel='INFO') + with self.assertRaises(ValueError): + lwl.setLogConfig( + console_output = True, + loglevel='DEBUG', + file_name='test.log', + rotate_frequency='ERROR-FREQUENCY', + rotate_interval=0, # 测试rotate_interval<1时,自动更新为1 + max_file_size=None, # 测试not max_file_size or max_file_size <= 0时,自动更新为sys.maxsize + total_log_size=None, # 测试not total_log_size or total_log_size <= 0时,自动更新为sys.maxsize + max_log_files=None, # 测试not max_log_files or max_log_files <= 0时,自动更新为sys.maxsize + ) + + with self.assertRaises(ValueError): + lwl.setLogConfig( + console_output = True, + loglevel='ERROR-LEVEL', + file_name='test.log', + rotate_frequency='D', + rotate_interval=0, # 测试rotate_interval<1时,自动更新为1 + max_file_size=0, # 测试not max_file_size or max_file_size <= 0时,自动更新为sys.maxsize + total_log_size=None, # 测试not total_log_size or total_log_size <= 0时,自动更新为sys.maxsize + max_log_files=None, # 测试not max_log_files or max_log_files <= 0时,自动更新为sys.maxsize + ) + + def test_rolling_with_time(self): + time_msgs = ['S', 'M', 'H', 'D', 'MIDNIGHT'] + for time_msg in time_msgs: + logger = logging.getLogger('CustomLogger') + logger.setLevel(logging.DEBUG) + handler = SizeAndTimeRotatingFileHandler( + file_name ='test.log', + rotate_frequency=time_msg, + rotate_interval=1, + max_file_size=1024*100*1024, + max_log_files=10, + total_log_size=1024*300*1024 + ) + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + + for _ in range(2): + logger.info("This is a test log message.") + time.sleep(0.1) + + def test_rolling_with_size(self): + logger = logging.getLogger('CustomLogger') + logger.setLevel(logging.DEBUG) + handler = SizeAndTimeRotatingFileHandler( + file_name ='test.log', + rotate_frequency='S', + rotate_interval=10, + max_file_size=1*1024, + max_log_files=2, + total_log_size=1024*300*1024 + ) + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + + for i in range(100): + logger.info("This is a test log message."*100) + time.sleep(0.001) + + def test_rolling_to_total_max_size(self): + logger = logging.getLogger('CustomLogger') + logger.setLevel(logging.DEBUG) + handler = SizeAndTimeRotatingFileHandler( + file_name ='test.log', + rotate_frequency='S', + rotate_interval=100, + max_file_size=10*1024, + max_log_files=10000, + total_log_size=20*1024 + ) + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + + for _ in range(100): + logger.info("This is a test log message."*100) + time.sleep(0.001) + +if __name__ == '__main__': + unittest.main() diff --git a/python/utils/logger_file_headler.py b/python/utils/logger_file_headler.py new file mode 100644 index 000000000..c27b97b58 --- /dev/null +++ b/python/utils/logger_file_headler.py @@ -0,0 +1,98 @@ +# Copyright (c) 2024 Baidu, Inc. All Rights Reserved. +# +# 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. +import os +import time +import glob +import logging +from datetime import datetime, timedelta + +class SizeAndTimeRotatingFileHandler(logging.Handler): + def __init__(self, + file_name, + rotate_frequency='MIDNIGHT', + rotate_interval=1, + max_file_size=0, + max_log_files=0, + total_log_size=0 + ): + super().__init__() + self.file_name = file_name + self.rotate_frequency = rotate_frequency.upper() + self.rotate_interval = rotate_interval + self.max_file_size = max_file_size + self.max_log_files = max_log_files + self.total_log_size = total_log_size + self.current_time = datetime.now() + self.current_file = self.file_name + self.stream = open(self.current_file, 'a') + self.last_rollover = time.time() + + def _get_new_filename(self): + suffix = self.current_time.strftime("%Y-%m-%d_%H-%M-%S") + return f"{self.file_name}.{suffix}" + + def emit(self, record): + if self.shouldRollover(record): + self.doRollover() + self.stream.write(self.format(record) + '\n') + self.stream.flush() + + def shouldRollover(self, record): + current_time = time.time() + current_size = os.path.getsize(self.current_file) + + time_rollover = False + if self.rotate_frequency == 'S': + time_rollover = current_time >= self.last_rollover + self.rotate_interval + elif self.rotate_frequency == 'M': + time_rollover = current_time >= self.last_rollover + self.rotate_interval * 60 + elif self.rotate_frequency == 'H': + time_rollover = current_time >= self.last_rollover + self.rotate_interval * 3600 + elif self.rotate_frequency == 'D': + time_rollover = current_time >= self.last_rollover + self.rotate_interval * 86400 + elif self.rotate_frequency == 'MIDNIGHT': + time_rollover = datetime.fromtimestamp(current_time).date() != datetime.fromtimestamp(self.last_rollover).date() + + size_rollover = current_size >= self.max_file_size if self.max_file_size > 0 else False + + return time_rollover or size_rollover + + def doRollover(self): + self.stream.close() + self.current_time = datetime.now() + new_filename = self._get_new_filename() + os.rename(self.current_file, new_filename) # Rename current file to new name + self.current_file = self.file_name + self.stream = open(self.current_file, 'a') + self.last_rollover = time.time() + self.manage_log_files() + + def manage_log_files(self): + log_files = sorted(glob.glob(f"{self.file_name}.*"), key=os.path.getmtime) + + while len(log_files) > self.max_log_files: + oldest_log = log_files.pop(0) + os.remove(oldest_log) + + while self._total_size(log_files) > self.total_log_size: + if log_files: + oldest_log = log_files.pop(0) + os.remove(oldest_log) + + def _total_size(self, files): + return sum(os.path.getsize(f) for f in files if os.path.exists(f)) + + def close(self): + self.stream.close() + super().close() \ No newline at end of file diff --git a/python/utils/logger_util.py b/python/utils/logger_util.py index 3396567d4..fa25385e4 100644 --- a/python/utils/logger_util.py +++ b/python/utils/logger_util.py @@ -16,12 +16,14 @@ """ 日志 """ -import uuid import os import sys +import uuid +import logging +import logging.handlers import logging.config from threading import current_thread - +from typing import Optional LOGGING_CONFIG = { "version": 1, @@ -31,29 +33,60 @@ "format": "[%(asctime)s.%(msecs)03d] %(filename)s [line:%(lineno)d] %(levelname)s [%(logid)s] %(message)s", }, }, - "handlers": { - "console": { - "level": "INFO", - "class": "logging.StreamHandler", - "formatter": "standard", - "stream": "ext://sys.stdout", # Use standard output - }, - "file": { - "level": "INFO", - "class": "logging.FileHandler", - "filename": "tmp.log", - "formatter": "standard", - }, - }, + "handlers": {}, "loggers": { "appbuilder": { - "handlers": ["console"], + "handlers": [], "level": "INFO", "propagate": True, }, }, } +CONSOLE_HEADER = { + "level": "INFO", + "class": "logging.StreamHandler", + "formatter": "standard", + "stream": "ext://sys.stdout", # Use standard output +} + +ERROR_FILE_HEADER = { + "level": "ERROR", + "class": "logging.FileHandler", + "filename": "tmp.error.log", + "formatter": "standard", +} + +FILE_HEADER = { + "level": "DEBUG", + "class": "logging.FileHandler", + "filename": "tmp.info.log", + "formatter": "standard", +} + +ERROR_SET_CONFIG_HEADER = { + 'level': 'ERROR', + 'formatter': 'standard', + 'class': 'appbuilder.SizeAndTimeRotatingFileHandler', + 'file_name': 'tmp.error.log', + 'rotate_frequency': 'MIDDNIGHT', + 'rotate_interval': 1, + 'max_file_size': 5*1024*1024, + 'max_log_files': 20, + 'total_log_size': 100*1024*1024 +} + +SET_CONFIG_HEADER = { + 'level': 'DEBUG', + 'formatter': 'standard', + 'class': 'appbuilder.SizeAndTimeRotatingFileHandler', + 'file_name': 'tmp.info.log', + 'rotate_frequency': 'MIDDNIGHT', + 'rotate_interval': 1, + 'max_file_size': 5*1024*1024, + 'max_log_files': 20, + 'total_log_size': 100*1024*1024 +} class LoggerWithLoggerId(logging.LoggerAdapter): """ @@ -63,12 +96,43 @@ def __init__(self, logger, extra, loglevel): """ init """ + LOGGING_CONFIG["handlers"] = {} + LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"] = [] log_file = os.environ.get("APPBUILDER_LOGFILE", "") - if log_file: - LOGGING_CONFIG["handlers"]["file"]["filename"] = log_file + log_path = os.environ.get("APPBUILDER_LOGPATH", "") + loglevel = loglevel.strip().lower() + if loglevel not in ["debug", "info", "warning", "error"]: + raise ValueError("expected APPBUILDER_LOGLEVEL in [debug, info, warning, error], but got %s" % loglevel) + loglevel = loglevel.upper() + + if log_path: + current_pid = str(os.getpid()) + full_log_path = os.path.join(log_path, "log") + if not os.path.exists(full_log_path): + os.makedirs(full_log_path) + info_log_file = os.path.join(log_path, "log", current_pid + ".info.log") + error_log_file = os.path.join(log_path, "log", current_pid + ".error.log") + FILE_HEADER["filename"] = info_log_file + ERROR_FILE_HEADER["filename"] = error_log_file + FILE_HEADER["level"] = loglevel + LOGGING_CONFIG["handlers"]["file"] = FILE_HEADER + LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"].append("file") + if loglevel in ("DEBUG", "INFO", "WARNING"): + LOGGING_CONFIG["handlers"]["error_file"] = ERROR_FILE_HEADER + LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"].append("error_file") + elif log_file: + ERROR_FILE_HEADER["filename"] = self._add_error_to_file_name(log_file) + FILE_HEADER["filename"] = log_file + FILE_HEADER["level"] = loglevel + LOGGING_CONFIG["handlers"]["file"] = FILE_HEADER LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"].append("file") - LOGGING_CONFIG["handlers"]["file"]["level"] = loglevel - LOGGING_CONFIG['handlers']['console']['level'] = loglevel + if loglevel in ("DEBUG", "INFO", "WARNING"): + LOGGING_CONFIG["handlers"]["error_file"] = ERROR_FILE_HEADER + LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"].append("error_file") + + CONSOLE_HEADER["level"] = loglevel + LOGGING_CONFIG["handlers"]["console"] = CONSOLE_HEADER + LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"].append("console") LOGGING_CONFIG['loggers']['appbuilder']['level'] = loglevel logging.config.dictConfig(LOGGING_CONFIG) logging.LoggerAdapter.__init__(self, logger, extra) @@ -102,13 +166,29 @@ def level(self): """ return self.logger.level + @staticmethod + def _add_error_to_file_name(filename): + prefix = "error." + dir_name, base_name = os.path.split(filename) + new_base_name = f"{prefix}{base_name}" + return os.path.join(dir_name, new_base_name) + def setFilename(self, filename): """ set filename """ if "file" not in LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"]: + FILE_HEADER["filename"] = filename + LOGGING_CONFIG["handlers"]["file"] = FILE_HEADER LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"].append("file") - LOGGING_CONFIG["handlers"]["file"]["filename"] = filename + if "error_file" not in LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"]: + ERROR_FILE_HEADER["filename"] = self._add_error_to_file_name(filename) + LOGGING_CONFIG["handlers"]["error_file"] = ERROR_FILE_HEADER + LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"].append("error_file") + FILE_HEADER["filename"] = filename + ERROR_FILE_HEADER["filename"] = self._add_error_to_file_name(filename) + LOGGING_CONFIG["handlers"]["file"] = FILE_HEADER + LOGGING_CONFIG["handlers"]["error_file"] = ERROR_FILE_HEADER logging.config.dictConfig(LOGGING_CONFIG) def setLoglevel(self, level): @@ -116,13 +196,99 @@ def setLoglevel(self, level): set log level """ log_level = level.strip().lower() - if log_level not in ["debug", "info", "warning", "error"]: raise ValueError("expected APPBUILDER_LOGLEVEL in [debug, info, warning, error], but got %s" % log_level) log_level = log_level.upper() - LOGGING_CONFIG['handlers']['console']['level'] = log_level + if "file" in LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"]: + FILE_HEADER["level"] = log_level + LOGGING_CONFIG["handlers"]["file"] = FILE_HEADER + if "console" in LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"] or not LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"]: + CONSOLE_HEADER["level"] = log_level + LOGGING_CONFIG["handlers"]["console"] = CONSOLE_HEADER LOGGING_CONFIG['loggers']['appbuilder']['level'] = log_level - LOGGING_CONFIG["handlers"]["file"]["level"] = log_level + logging.config.dictConfig(LOGGING_CONFIG) + + def setLogConfig(self, + console_output: bool = True, + loglevel: str = "DEBUG", + log_path: str = "/tmp", + rotate_frequency: str = "MIDNIGHT", + rotate_interval: int = 1, + max_file_size: Optional[int] = None, # 以B为单位 + total_log_size: Optional[int] = None, # 以B为单位 + max_log_files: Optional[int] = None, + file_name: Optional[str] = None + ): + LOGGING_CONFIG["handlers"] = {} + LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"] = [] + + # log_level 数据校验 + log_level = loglevel.strip().lower() + if log_level not in ["debug", "info", "warning", "error"]: + raise ValueError("expected APPBUILDER_LOGLEVEL in [debug, info, warning, error], but got %s" % log_level) + log_level = log_level.upper() + + # 设置console输出日志 + if console_output: + CONSOLE_HEADER['level'] = loglevel + LOGGING_CONFIG["handlers"]["console"] = CONSOLE_HEADER + LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"].append("console") + else: + LOGGING_CONFIG["loggers"]["appbuilder"]["propagate"] = False + + # 参数验证 + if not max_file_size or max_file_size <= 0: + max_file_size = sys.maxsize + if not total_log_size or total_log_size <= 0: + total_log_size = sys.maxsize + if not max_log_files or max_log_files <= 0: + max_log_files = sys.maxsize + if rotate_interval < 1: + rotate_interval = 1 + rotate_frequency = rotate_frequency.strip().lower() + if rotate_frequency not in ["s", "m", "h", "d", "midnight"]: + raise ValueError("expected rotate_frequency in [S, M, H, D, MIDNIGHT], but got %s" % rotate_frequency) + + # 设置文件输出日志 + # 设置日志级别 + SET_CONFIG_HEADER['level'] = loglevel + + # 设置文件名称 + if not file_name: + current_pid = str(os.getpid()) + else: + current_pid = file_name + full_log_path = os.path.join(log_path, "log") + if not os.path.exists(full_log_path): + os.makedirs(full_log_path) + info_log_file = os.path.join(log_path, "log", current_pid + ".info.log") + error_log_file = os.path.join(log_path, "log", current_pid + ".error.log") + SET_CONFIG_HEADER["file_name"] = info_log_file + ERROR_SET_CONFIG_HEADER["file_name"] = error_log_file + + # 设置滚动时间 + SET_CONFIG_HEADER['rotate_frequency'] = rotate_frequency + ERROR_SET_CONFIG_HEADER['rotate_frequency'] = rotate_frequency + SET_CONFIG_HEADER['rotate_interval'] = rotate_interval + ERROR_SET_CONFIG_HEADER['rotate_interval'] = rotate_interval + + # 设置最大文件大小 + + SET_CONFIG_HEADER['max_file_size'] = max_file_size + ERROR_SET_CONFIG_HEADER['max_file_size'] = max_file_size + + # 设置总大小限制 + SET_CONFIG_HEADER['total_log_size'] = total_log_size + ERROR_SET_CONFIG_HEADER['total_log_size'] = total_log_size + + # 设置备份数量 + SET_CONFIG_HEADER['max_log_files'] = max_log_files + ERROR_SET_CONFIG_HEADER['max_log_files'] = max_log_files + + LOGGING_CONFIG["handlers"]["file"] = SET_CONFIG_HEADER + LOGGING_CONFIG["handlers"]["error_file"] = ERROR_SET_CONFIG_HEADER + LOGGING_CONFIG["loggers"]["appbuilder"]["handlers"].extend(["file", "error_file"]) + LOGGING_CONFIG['loggers']['appbuilder']['level'] = loglevel logging.config.dictConfig(LOGGING_CONFIG) def process(self, msg, kwargs):