Compare commits
	
		
			4 Commits 
		
	
	
		
			main
			...
			log_sys_te
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 8842b758d7 | |
|  | 54ee624632 | |
|  | e8f2dfc088 | |
|  | d2282f4275 | 
|  | @ -0,0 +1,177 @@ | ||||||
|  | ''' | ||||||
|  | `tractor.log`-wrapping unit tests. | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | import importlib | ||||||
|  | from pathlib import Path | ||||||
|  | import shutil | ||||||
|  | import sys | ||||||
|  | from types import ModuleType | ||||||
|  | 
 | ||||||
|  | import pytest | ||||||
|  | import tractor | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_root_pkg_not_duplicated_in_logger_name(): | ||||||
|  |     ''' | ||||||
|  |     When both `pkg_name` and `name` are passed and they have | ||||||
|  |     a common `<root_name>.< >` prefix, ensure that it is not | ||||||
|  |     duplicated in the child's `StackLevelAdapter.name: str`. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     project_name: str = 'pylib' | ||||||
|  |     pkg_path: str = 'pylib.subpkg.mod' | ||||||
|  | 
 | ||||||
|  |     proj_log = tractor.log.get_logger( | ||||||
|  |         pkg_name=project_name, | ||||||
|  |         mk_sublog=False, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     sublog = tractor.log.get_logger( | ||||||
|  |         pkg_name=project_name, | ||||||
|  |         name=pkg_path, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     assert proj_log is not sublog | ||||||
|  |     assert sublog.name.count(proj_log.name) == 1 | ||||||
|  |     assert 'mod' not in sublog.name | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # ?TODO, move this into internal libs? | ||||||
|  | # -[ ] we already use it in `modden.config._pymod` as well | ||||||
|  | def load_module_from_path( | ||||||
|  |     path: Path, | ||||||
|  |     module_name: str|None = None, | ||||||
|  | ) -> ModuleType: | ||||||
|  |     ''' | ||||||
|  |     Taken from SO, | ||||||
|  |     https://stackoverflow.com/a/67208147 | ||||||
|  | 
 | ||||||
|  |     which is based on stdlib docs, | ||||||
|  |     https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     module_name = module_name or path.stem | ||||||
|  |     spec = importlib.util.spec_from_file_location( | ||||||
|  |         module_name, | ||||||
|  |         str(path), | ||||||
|  |     ) | ||||||
|  |     module = importlib.util.module_from_spec(spec) | ||||||
|  |     sys.modules[module_name] = module | ||||||
|  |     spec.loader.exec_module(module) | ||||||
|  |     return module | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_implicit_mod_name_applied_for_child( | ||||||
|  |     testdir: pytest.Pytester, | ||||||
|  |     loglevel: str, | ||||||
|  | ): | ||||||
|  |     ''' | ||||||
|  |     Verify that when `.log.get_logger(pkg_name='pylib')` is called | ||||||
|  |     from a given sub-mod from within the `pylib` pkg-path, we | ||||||
|  |     implicitly set the equiv of `name=__name__` from the caller's | ||||||
|  |     module. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     # tractor.log.get_console_log(level=loglevel) | ||||||
|  |     proj_name: str = 'snakelib' | ||||||
|  |     mod_code: str = ( | ||||||
|  |         f'import tractor\n' | ||||||
|  |         f'\n' | ||||||
|  |         f'log = tractor.log.get_logger(pkg_name="{proj_name}")\n' | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     # create a sub-module for each pkg layer | ||||||
|  |     _lib = testdir.mkpydir(proj_name) | ||||||
|  |     pkg: Path = Path(_lib) | ||||||
|  |     subpkg: Path = pkg / 'subpkg' | ||||||
|  |     subpkg.mkdir() | ||||||
|  | 
 | ||||||
|  |     pkgmod: Path = subpkg / "__init__.py" | ||||||
|  |     pkgmod.touch() | ||||||
|  | 
 | ||||||
|  |     _submod: Path = testdir.makepyfile( | ||||||
|  |         _mod=mod_code, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     pkg_mod = pkg / 'mod.py' | ||||||
|  |     pkg_subpkg_submod = subpkg / 'submod.py' | ||||||
|  |     shutil.copyfile( | ||||||
|  |         _submod, | ||||||
|  |         pkg_mod, | ||||||
|  |     ) | ||||||
|  |     shutil.copyfile( | ||||||
|  |         _submod, | ||||||
|  |         pkg_subpkg_submod, | ||||||
|  |     ) | ||||||
|  |     testdir.chdir() | ||||||
|  | 
 | ||||||
|  |     # XXX NOTE, once the "top level" pkg mod has been | ||||||
|  |     # imported, we can then use `import` syntax to | ||||||
|  |     # import it's sub-pkgs and modules. | ||||||
|  |     pkgmod = load_module_from_path( | ||||||
|  |         Path(pkg / '__init__.py'), | ||||||
|  |         module_name=proj_name, | ||||||
|  |     ) | ||||||
|  |     pkg_root_log = tractor.log.get_logger( | ||||||
|  |         pkg_name=proj_name, | ||||||
|  |         mk_sublog=False, | ||||||
|  |     ) | ||||||
|  |     assert pkg_root_log.name == proj_name | ||||||
|  |     assert not pkg_root_log.logger.getChildren() | ||||||
|  | 
 | ||||||
|  |     from snakelib import mod | ||||||
|  |     assert mod.log.name == proj_name | ||||||
|  | 
 | ||||||
|  |     from snakelib.subpkg import submod | ||||||
|  |     assert ( | ||||||
|  |         submod.log.name | ||||||
|  |         == | ||||||
|  |         submod.__package__  # ?TODO, use this in `.get_logger()` instead? | ||||||
|  |         == | ||||||
|  |         f'{proj_name}.subpkg' | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     sub_logs = pkg_root_log.logger.getChildren() | ||||||
|  |     assert len(sub_logs) == 1  # only one nested sub-pkg module | ||||||
|  |     assert submod.log.logger in sub_logs | ||||||
|  | 
 | ||||||
|  |     # breakpoint() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # TODO, moar tests against existing feats: | ||||||
|  | # ------ - ------ | ||||||
|  | # - [ ] color settings? | ||||||
|  | # - [ ] header contents like, | ||||||
|  | #   - actor + thread + task names from various conc-primitives, | ||||||
|  | # - [ ] `StackLevelAdapter` extensions, | ||||||
|  | #   - our custom levels/methods: `transport|runtime|cance|pdb|devx` | ||||||
|  | # - [ ] custom-headers support? | ||||||
|  | # | ||||||
|  | 
 | ||||||
|  | # TODO, test driven dev of new-ideas/long-wanted feats, | ||||||
|  | # ------ - ------ | ||||||
|  | # - [ ] https://github.com/goodboy/tractor/issues/244 | ||||||
|  | #  - [ ] @catern mentioned using a sync / deterministic sys | ||||||
|  | #       and in particular `svlogd`? | ||||||
|  | #       |_ https://smarden.org/runit/svlogd.8 | ||||||
|  | 
 | ||||||
|  | # - [ ] using adapter vs. filters? | ||||||
|  | #    - https://stackoverflow.com/questions/60691759/add-information-to-every-log-message-in-python-logging/61830838#61830838 | ||||||
|  | 
 | ||||||
|  | # - [ ] `.at_least_level()` optimization which short circuits wtv | ||||||
|  | #      `logging` is doing behind the scenes when the level filters | ||||||
|  | #      the emission..? | ||||||
|  | 
 | ||||||
|  | # - [ ] use of `.log.get_console_log()` in subactors and the | ||||||
|  | #    subtleties of ensuring it actually emits from a subproc. | ||||||
|  | 
 | ||||||
|  | # - [ ] this idea of activating per-subsys emissions with some | ||||||
|  | #    kind of `.name` filter passed to the runtime or maybe configured | ||||||
|  | #    via the root `StackLevelAdapter`? | ||||||
|  | 
 | ||||||
|  | # - [ ] use of `logging.dict.dictConfig()` to simplify the impl | ||||||
|  | #      of any of ^^ ?? | ||||||
|  | #    - https://stackoverflow.com/questions/7507825/where-is-a-complete-example-of-logging-config-dictconfig | ||||||
|  | #    - https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema | ||||||
|  | #    - https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig | ||||||
							
								
								
									
										198
									
								
								tractor/log.py
								
								
								
								
							
							
						
						
									
										198
									
								
								tractor/log.py
								
								
								
								
							|  | @ -14,11 +14,22 @@ | ||||||
| # You should have received a copy of the GNU Affero General Public License | # You should have received a copy of the GNU Affero General Public License | ||||||
| # along with this program.  If not, see <https://www.gnu.org/licenses/>. | # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
| 
 | 
 | ||||||
| """ | ''' | ||||||
| Log like a forester! | An enhanced logging subsys. | ||||||
| 
 | 
 | ||||||
| """ | An extended logging layer using (for now) the stdlib's `logging` | ||||||
|  | + `colorlog` which embeds concurrency-primitive/runtime info into | ||||||
|  | records (headers) to help you better grok your distributed systems | ||||||
|  | built on `tractor`. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
| from collections.abc import Mapping | from collections.abc import Mapping | ||||||
|  | from inspect import ( | ||||||
|  |     FrameInfo, | ||||||
|  |     getmodule, | ||||||
|  |     stack, | ||||||
|  | ) | ||||||
| import sys | import sys | ||||||
| import logging | import logging | ||||||
| from logging import ( | from logging import ( | ||||||
|  | @ -26,8 +37,10 @@ from logging import ( | ||||||
|     Logger, |     Logger, | ||||||
|     StreamHandler, |     StreamHandler, | ||||||
| ) | ) | ||||||
| import colorlog  # type: ignore | from types import ModuleType | ||||||
|  | import warnings | ||||||
| 
 | 
 | ||||||
|  | import colorlog  # type: ignore | ||||||
| import trio | import trio | ||||||
| 
 | 
 | ||||||
| from ._state import current_actor | from ._state import current_actor | ||||||
|  | @ -39,7 +52,7 @@ _default_loglevel: str = 'ERROR' | ||||||
| # Super sexy formatting thanks to ``colorlog``. | # Super sexy formatting thanks to ``colorlog``. | ||||||
| # (NOTE: we use the '{' format style) | # (NOTE: we use the '{' format style) | ||||||
| # Here, `thin_white` is just the layperson's gray. | # Here, `thin_white` is just the layperson's gray. | ||||||
| LOG_FORMAT = ( | LOG_FORMAT: str = ( | ||||||
|     # "{bold_white}{log_color}{asctime}{reset}" |     # "{bold_white}{log_color}{asctime}{reset}" | ||||||
|     "{log_color}{asctime}{reset}" |     "{log_color}{asctime}{reset}" | ||||||
|     " {bold_white}{thin_white}({reset}" |     " {bold_white}{thin_white}({reset}" | ||||||
|  | @ -51,7 +64,7 @@ LOG_FORMAT = ( | ||||||
|     " {reset}{bold_white}{thin_white}{message}" |     " {reset}{bold_white}{thin_white}{message}" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| DATE_FORMAT = '%b %d %H:%M:%S' | DATE_FORMAT: str = '%b %d %H:%M:%S' | ||||||
| 
 | 
 | ||||||
| # FYI, ERROR is 40 | # FYI, ERROR is 40 | ||||||
| # TODO: use a `bidict` to avoid the :155 check? | # TODO: use a `bidict` to avoid the :155 check? | ||||||
|  | @ -75,7 +88,10 @@ STD_PALETTE = { | ||||||
|     'TRANSPORT': 'cyan', |     'TRANSPORT': 'cyan', | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| BOLD_PALETTE = { | BOLD_PALETTE: dict[ | ||||||
|  |     str, | ||||||
|  |     dict[int, str], | ||||||
|  | ] = { | ||||||
|     'bold': { |     'bold': { | ||||||
|         level: f"bold_{color}" for level, color in STD_PALETTE.items()} |         level: f"bold_{color}" for level, color in STD_PALETTE.items()} | ||||||
| } | } | ||||||
|  | @ -97,10 +113,17 @@ def at_least_level( | ||||||
|     return False |     return False | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # TODO: this isn't showing the correct '{filename}' | # TODO, compare with using a "filter" instead? | ||||||
| # as it did before.. | # - https://stackoverflow.com/questions/60691759/add-information-to-every-log-message-in-python-logging/61830838#61830838 | ||||||
|  | #  |_corresponding dict-config, | ||||||
|  | #    https://stackoverflow.com/questions/7507825/where-is-a-complete-example-of-logging-config-dictconfig/7507842#7507842 | ||||||
|  | #  - [ ] what's the benefit/tradeoffs? | ||||||
|  | # | ||||||
| class StackLevelAdapter(LoggerAdapter): | class StackLevelAdapter(LoggerAdapter): | ||||||
|  |     ''' | ||||||
|  |     A (software) stack oriented logger "adapter". | ||||||
| 
 | 
 | ||||||
|  |     ''' | ||||||
|     def at_least_level( |     def at_least_level( | ||||||
|         self, |         self, | ||||||
|         level: str, |         level: str, | ||||||
|  | @ -284,7 +307,9 @@ class ActorContextInfo(Mapping): | ||||||
| 
 | 
 | ||||||
| def get_logger( | def get_logger( | ||||||
|     name: str|None = None, |     name: str|None = None, | ||||||
|     _root_name: str = _proj_name, |     pkg_name: str = _proj_name, | ||||||
|  |     # XXX, deprecated, use ^ | ||||||
|  |     _root_name: str|None = None, | ||||||
| 
 | 
 | ||||||
|     logger: Logger|None = None, |     logger: Logger|None = None, | ||||||
| 
 | 
 | ||||||
|  | @ -293,22 +318,89 @@ def get_logger( | ||||||
|     #  |_https://stackoverflow.com/questions/7507825/where-is-a-complete-example-of-logging-config-dictconfig |     #  |_https://stackoverflow.com/questions/7507825/where-is-a-complete-example-of-logging-config-dictconfig | ||||||
|     #  |_https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema |     #  |_https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema | ||||||
|     subsys_spec: str|None = None, |     subsys_spec: str|None = None, | ||||||
|  |     mk_sublog: bool = True, | ||||||
| 
 | 
 | ||||||
| ) -> StackLevelAdapter: | ) -> StackLevelAdapter: | ||||||
|     ''' |     ''' | ||||||
|     Return the `tractor`-library root logger or a sub-logger for |     Return the `tractor`-library root logger or a sub-logger for | ||||||
|     `name` if provided. |     `name` if provided. | ||||||
| 
 | 
 | ||||||
|  |     When `name` is left null we try to auto-detect the caller's | ||||||
|  |     `mod.__name__` and use that as a the sub-logger key. | ||||||
|  |     This allows for example creating a module level instance like, | ||||||
|  | 
 | ||||||
|  |     .. code:: python | ||||||
|  | 
 | ||||||
|  |         log = tractor.log.get_logger(_root_name='mylib') | ||||||
|  | 
 | ||||||
|  |     and by default all console record headers will show the caller's | ||||||
|  |     (of any `log.<level>()`-method) correct sub-pkg's | ||||||
|  |     + py-module-file. | ||||||
|  | 
 | ||||||
|     ''' |     ''' | ||||||
|  |     if _root_name: | ||||||
|  |         msg: str = ( | ||||||
|  |             'The `_root_name: str` param of `get_logger()` is now deprecated.\n' | ||||||
|  |             'Use the new `pkg_name: str` instead, it is the same usage.\n' | ||||||
|  |         ) | ||||||
|  |         warnings.warn( | ||||||
|  |             msg, | ||||||
|  |             DeprecationWarning, | ||||||
|  |             stacklevel=2, | ||||||
|  |         ) | ||||||
|  |     pkg_name: str = _root_name or pkg_name | ||||||
|     log: Logger |     log: Logger | ||||||
|     log = rlog = logger or logging.getLogger(_root_name) |     log = rlog = logger or logging.getLogger(pkg_name) | ||||||
|  | 
 | ||||||
|  |     # Implicitly introspect the caller's module-name whenever `name` | ||||||
|  |     # if left as the null default. | ||||||
|  |     # | ||||||
|  |     # When the `pkg_name` is `in` in the `mod.__name__` we presume | ||||||
|  |     # this instance can be created as a sub-`StackLevelAdapter` and | ||||||
|  |     # that the intention is get free module-path tracing and | ||||||
|  |     # filtering (well once we implement that) oriented around the | ||||||
|  |     # py-module code hierarchy of the consuming project. | ||||||
|  |     if ( | ||||||
|  |         pkg_name != _proj_name | ||||||
|  |         and | ||||||
|  |         name is None | ||||||
|  |         and | ||||||
|  |         mk_sublog | ||||||
|  |     ): | ||||||
|  |         callstack: list[FrameInfo] = stack() | ||||||
|  |         caller_fi: FrameInfo = callstack[1] | ||||||
|  |         caller_mod: ModuleType = getmodule(caller_fi.frame) | ||||||
|  |         if caller_mod: | ||||||
|  |             # ?how is this `mod.__name__` defined? | ||||||
|  |             # -> well by how the mod is imported.. | ||||||
|  |             # |_https://stackoverflow.com/a/15883682 | ||||||
|  |             mod_name: str = caller_mod.__name__ | ||||||
|  |             mod_pkg: str = caller_mod.__package__ | ||||||
|  |             log.info( | ||||||
|  |                 f'Generating sub-logger name,\n' | ||||||
|  |                 f'{mod_pkg}.{mod_name}\n' | ||||||
|  |             ) | ||||||
|  |             # if pkg_name in caller_mod.__package__: | ||||||
|  |             #     from tractor.devx.debug import mk_pdb | ||||||
|  |             #     mk_pdb().set_trace() | ||||||
| 
 | 
 | ||||||
|             if ( |             if ( | ||||||
|         name |                 pkg_name | ||||||
|         and |                 # and | ||||||
|         name != _proj_name |                 # pkg_name in mod_name | ||||||
|             ): |             ): | ||||||
|  |                 name = mod_name | ||||||
| 
 | 
 | ||||||
|  |     # XXX, lowlevel debuggin.. | ||||||
|  |     # if pkg_name != _proj_name: | ||||||
|  |         # from tractor.devx.debug import mk_pdb | ||||||
|  |         # mk_pdb().set_trace() | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |         name != _proj_name | ||||||
|  |         and | ||||||
|  |         name | ||||||
|  |     ): | ||||||
|         # NOTE: for handling for modules that use `get_logger(__name__)` |         # NOTE: for handling for modules that use `get_logger(__name__)` | ||||||
|         # we make the following stylistic choice: |         # we make the following stylistic choice: | ||||||
|         # - always avoid duplicate project-package token |         # - always avoid duplicate project-package token | ||||||
|  | @ -318,24 +410,63 @@ def get_logger( | ||||||
|         #   since in python the {filename} is always this same |         #   since in python the {filename} is always this same | ||||||
|         #   module-file. |         #   module-file. | ||||||
| 
 | 
 | ||||||
|         sub_name: None|str = None |         rname: str = pkg_name | ||||||
|         rname, _, sub_name = name.partition('.') |         pkg_path: str = name | ||||||
|         pkgpath, _, modfilename = sub_name.rpartition('.') |  | ||||||
| 
 | 
 | ||||||
|         # NOTE: for tractor itself never include the last level |         # ex. modden.runtime.progman | ||||||
|         # module key in the name such that something like: eg. |         # -> rname='modden', _, pkg_path='runtime.progman' | ||||||
|         # 'tractor.trionics._broadcast` only includes the first |         if pkg_name in name: | ||||||
|         # 2 tokens in the (coloured) name part. |             rname, _, pkg_path = name.partition('.') | ||||||
|         if rname == 'tractor': |  | ||||||
|             sub_name = pkgpath |  | ||||||
| 
 | 
 | ||||||
|         if _root_name in sub_name: |         # ex. modden.runtime.progman | ||||||
|             duplicate, _, sub_name = sub_name.partition('.') |         # -> pkgpath='runtime', _, leaf_mod='progman' | ||||||
|  |         subpkg_path, _, leaf_mod = pkg_path.rpartition('.') | ||||||
| 
 | 
 | ||||||
|         if not sub_name: |         # NOTE: special usage for passing `name=__name__`, | ||||||
|  |         # | ||||||
|  |         # - remove duplication of any root-pkg-name in the | ||||||
|  |         #   (sub/child-)logger name; i.e. never include the | ||||||
|  |         #   `pkg_name` *twice* in the top-most-pkg-name/level | ||||||
|  |         # | ||||||
|  |         # -> this happens normally since it is added to `.getChild()` | ||||||
|  |         #   and as the name of its root-logger. | ||||||
|  |         # | ||||||
|  |         # => So for ex. (module key in the name) something like | ||||||
|  |         #   `name='tractor.trionics._broadcast` is passed, | ||||||
|  |         #   only includes the first 2 sub-pkg name-tokens in the | ||||||
|  |         #   child-logger's name; the colored "pkg-namespace" header | ||||||
|  |         #   will then correctly show the same value as `name`. | ||||||
|  |         if rname == pkg_name: | ||||||
|  |             pkg_path = subpkg_path | ||||||
|  | 
 | ||||||
|  |         # XXX, do some double-checks for duplication of, | ||||||
|  |         # - root-pkg-name, already in root logger | ||||||
|  |         # - leaf-module-name already in `{filename}` header-field | ||||||
|  |         if pkg_name in pkg_path: | ||||||
|  |             _duplicate, _, pkg_path = pkg_path.partition('.') | ||||||
|  |             if _duplicate: | ||||||
|  |                 # assert _duplicate == rname | ||||||
|  |                 _root_log.warning( | ||||||
|  |                     f'Duplicate pkg-name in sub-logger key?\n' | ||||||
|  |                     f'pkg_name = {pkg_name!r}\n' | ||||||
|  |                     f'pkg_path = {pkg_path!r}\n' | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |         if ( | ||||||
|  |             leaf_mod | ||||||
|  |             and | ||||||
|  |             leaf_mod in pkg_path | ||||||
|  |         ): | ||||||
|  |             _root_log.warning( | ||||||
|  |                 f'Duplicate leaf-module-name in sub-logger key?\n' | ||||||
|  |                 f'leaf_mod = {leaf_mod!r}\n' | ||||||
|  |                 f'pkg_path = {pkg_path!r}\n' | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         if not pkg_path: | ||||||
|             log = rlog |             log = rlog | ||||||
|         else: |         elif mk_sublog: | ||||||
|             log = rlog.getChild(sub_name) |             log = rlog.getChild(pkg_path) | ||||||
| 
 | 
 | ||||||
|         log.level = rlog.level |         log.level = rlog.level | ||||||
| 
 | 
 | ||||||
|  | @ -350,8 +481,13 @@ def get_logger( | ||||||
|     for name, val in CUSTOM_LEVELS.items(): |     for name, val in CUSTOM_LEVELS.items(): | ||||||
|         logging.addLevelName(val, name) |         logging.addLevelName(val, name) | ||||||
| 
 | 
 | ||||||
|         # ensure customs levels exist as methods |         # ensure our custom adapter levels exist as methods | ||||||
|         assert getattr(logger, name.lower()), f'Logger does not define {name}' |         assert getattr( | ||||||
|  |             logger, | ||||||
|  |             name.lower() | ||||||
|  |         ), ( | ||||||
|  |             f'Logger does not define {name}' | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|     return logger |     return logger | ||||||
| 
 | 
 | ||||||
|  | @ -425,4 +561,4 @@ def get_loglevel() -> str: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # global module logger for tractor itself | # global module logger for tractor itself | ||||||
| log: StackLevelAdapter = get_logger('tractor') | _root_log: StackLevelAdapter = get_logger('tractor') | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue