1# -*- coding: utf-8 -*- 2 3""" 4A basic "plugin loader" implementation which searches for default packaged and 5user-supplied parser modules and verifies them so they can be executed by 6logparse.interface. The requirements for parser modules and classes are 7specified in the docstring of the Parser class. 8 9Classes in this module: 10 - `Parser`: Base class that every parser should inherit 11 - `ParserLoader`: Class used internally by `interface.py` to load parsers 12""" 13 14import importlib 15from os.path import dirname 16from pkgutil import iter_modules 17import inspect 18from pathlib import Path 19from typing import get_type_hints 20 21import logging 22logger = logging.getLogger(__name__) 23 24 25PARSER_DIR ="/usr/share/logparse/user-parsers" 26PARSER_PKG ="logparse.parsers" 27 28 29classParser(): 30""" 31 This is the base class that every parser should inherit. Parsers should 32 each exist in their own module and contain a Parser class whose name is the 33 same as the parser (e.g. `example.py` contains class `Example(Parser)`). 34 Each parser module must contain exactly one Parser class definition, and 35 this class cannot be a redefinition of the base Parser class (i.e. this 36 class). This class must provide the parse_log() method which returns a 37 logparse.formatting.Section object. 38 """ 39 40def__init__(self, name=None, path=None, info=None, deprecated=False, 41 successor=""): 42""" 43 The following variables can be set to display information about the 44 parser. The object `self.logger` can be used for outputting messages to 45 to whatever sink is set up in __init__.py (no setup required in the 46 parser module itself). 47 """ 48 self.name =str(name)if name else None 49 self.path =Path(path)if path else None 50 self.info =dict(info)if info else None 51 self.logger = logging.getLogger(self.__module__) 52 self.deprecated = deprecated 53 self.successor = successor 54 55defload(self): 56""" 57 A generic loading method to import a parser, only used for debugging 58 """ 59 logger.debug("Loading parser{0}from{1}".format( 60 self.name,str(self.path)if self.path !=None else"defaults")) 61return importlib.import_module(self.name) 62 63defparse_log(self, **args) ->None: 64""" 65 Every parser should provide the parse_log method which is executed at 66 runtime to analyse logs. Verification checks should prevent the below 67 exception from ever being raised. 68 """ 69raiseNotImplementedError("Failed to find an entry point for parser") 70 71 72 73class ParserLoader: 74""" 75 This class searches for parsers in the main logparse package 76 (logparser.parsers) and optionally in another external package (default 77 /usr/share/logparse). 78 """ 79 80def__init__(self, pkg=PARSER_PKG, path=PARSER_DIR): 81""" 82 The pkg and path attributes shouldn't need to be set on object 83 creation, the default values should work fine. They are hard-coded here 84 for security so that a module can't force-load a module from another 85 package/location, e.g. from the internet. 86 """ 87 88 self.pkg = pkg 89 self.path = path 90 self.parsers = [] 91 92defsearch(self, pattern): 93""" 94 Basic wrapper for the two search functions below. 95 """ 96 97 default_parser = self._search_default(pattern) 98if default_parser !=None: 99 self.parsers.append(default_parser) 100return default_parser 101else: 102 user_parser = self._search_user(pattern) 103if user_parser !=None: 104 self.parsers.append(user_parser) 105return user_parser 106else: 107 logger.warning("Couldn't find a matching parser module " 108"for search term{0}".format(pattern)) 109return None 110 111def_search_user(self, pattern): 112""" 113 Search for a parser name `pattern` in the user-managed parser directory 114 """ 115 116 logger.debug("Searching for{0}in{1}".format(pattern, self.path)) 117try: 118 spec = importlib.machinery.PathFinder.find_spec( 119 pattern, path=[self.path]) 120 parser_module = spec.loader.load_module(spec.name) 121return self._validate_module(parser_module) 122exceptExceptionas e: 123return None 124 125def_search_default(self, pattern): 126""" 127 Search for a parser name `pattern` in the default parser package 128 TODO use importlib.resources.is_resources() once there is a backport 129 to Python 3.6 or below 130 """ 131 132 logger.debug("Searching for{0}in default parsers".format(pattern)) 133try: 134 parser_module = importlib.import_module(self.pkg +"."+ pattern) 135return self._validate_module(parser_module) 136exceptExceptionas e: 137return None 138 139def_validate_module(self, parser_module): 140""" 141 Some basic security tests for candidate modules: 142 1. Must contain exactly one Parser object 143 3. This class cannot be a redefinition of the base Parser class 144 4. Must provide the parse_log() method 145 5. Must not return None 146 6. Must not match an already-loaded class 147 """ 148 149 logger.debug("Checking validity of module{0}at{1}".format( 150 parser_module.__name__, parser_module.__file__)) 151 available_parsers = [] 152 clsmembers = inspect.getmembers(parser_module, inspect.isclass) 153 154# Check individual classes 155for(_, c)in clsmembers: 156if not(issubclass(c, Parser) & (c is not Parser)): 157continue 158if c in self.parsers: 159 logger.warning("Parser class{0}has already been loaded " 160"from another source, ignoring it".format( 161 c.__class__.__name__, c.__file__)) 162if not inspect.isroutine(c.parse_log): 163 logger.warning("Parser class{0}in{1}does not contain a " 164"parse_log() method".format( 165 c.__class__.__name__, c.__file__)) 166continue 167if None inget_type_hints(c): 168 logger.warning("Parser class{0}in{1}contains a " 169"null-returning parse_log() method".format( 170 c.__class__.__name__, c.__file__)) 171continue 172 parser_obj =c() 173if parser_obj.deprecated: 174 logger.warning("Parser{0}is deprecated - " 175"use{1}instead".format( 176 parser_obj.name, parser_obj.successor)) 177 logger.debug("Found parser{0}.{1}".format( 178 c.__module__, c.__class__.__name__)) 179 available_parsers.append(parser_obj) 180 181# Check module structure 182iflen(available_parsers) ==1: 183 logger.debug("Parser module{0}at{1}passed validity checks" 184.format(parser_module.__name__, parser_module.__file__)) 185return available_parsers[0] 186eliflen(available_parsers) ==0: 187 logger.warning("No valid classes in{0}at{1}". 188format(parser_module.__name__, parser_module.__file__)) 189return None 190eliflen(available_parsers) >1: 191 logger.warning("Found multiple valid parser classes in{0}at{1}" 192"- ignoring this module" 193.format(parser_module.__name__, parser_module.__file__)) 194return None 195 196defload_pkg(self): 197""" 198 Clear the list of currently loaded packages and load all valid and 199 non-deprecated parser classes from self.pkg using importlib. 200 """ 201 202 available_parsers = [name for _, name, _ initer_modules( 203[dirname(importlib.import_module(self.pkg).__file__)])] 204for parser_name in available_parsers: 205 parser_module = importlib.import_module( 206"logparse.parsers."+ parser_name) 207 parser_class = self._validate_module(parser_module) 208if parser_class ==None: 209continue 210if parser_class.deprecated: 211 logger.debug("Ignoring parser{0}because it is deprecated" 212.format(parser_class.__class__.__name__)) 213continue 214 self.parsers.append(parser_class) 215return self.parsers 216 217defignore(self, pattern): 218""" 219 Remove a parser from the list of currently loaded parsers 220 """ 221 222for parser in self.parsers: 223if parser.__module__== pattern: 224 self.parsers.remove(parser) 225 logger.debug("Ignoring parser{0}".format(parser.__name__)) 226return self.parsers