1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
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.
89
Classes in this module:
10- `Parser`: Base class that every parser should inherit
11- `ParserLoader`: Class used internally by `interface.py` to load parsers
12"""
1314
import importlib
15from os.path import dirname
16from pkgutil import iter_modules
17import inspect
18from pathlib import Path
19from typing import get_type_hints
2021
import logging
22logger = logging.getLogger(__name__)
2324
25
PARSER_DIR = "/usr/share/logparse/user-parsers"
26PARSER_PKG = "logparse.parsers"
2728
29
class Parser():
30"""
31This is the base class that every parser should inherit. Parsers should
32each exist in their own module and contain a Parser class whose name is the
33same as the parser (e.g. `example.py` contains class `Example(Parser)`).
34Each parser module must contain exactly one Parser class definition, and
35this class cannot be a redefinition of the base Parser class (i.e. this
36class). This class must provide the parse_log() method which returns a
37logparse.formatting.Section object.
38"""
3940
def __init__(self, name=None, path=None, info=None, deprecated=False):
41"""
42The following variables can be set to display information about the
43parser. The object `self.logger` can be used as for outputting messages
44to whatever sink is set up in logparse.interface (no setup required in
45the parser module itself).
46"""
47self.name = str(name) if name else None
48self.path = Path(path) if path else None
49self.info = dict(info) if info else None
50self.logger = logging.getLogger(__name__)
51self.deprecated = deprecated
5253
def load(self):
54"""
55A generic loading method to import a parser, only used for debugging
56"""
57logger.debug("Loading parser {0} from {1}".format(self.name, str(self.path) if self.path != None else "defaults"))
58return importlib.import_module(self.name)
5960
def parse_log(self, **args) -> None:
61"""
62Every parser should provide the parse_log method which is executed at
63runtime to analyse logs. Verification checks should prevent the below
64exception from ever being raised.
65"""
66raise NotImplementedError("Failed to find an entry point for parser")
6768
69
70
class ParserLoader:
71"""
72This class searches for parsers in the main logparse package
73(logparser.parsers) and optionally in another external package (default
74/usr/share/logparse).
75"""
7677
def __init__(self, pkg=PARSER_PKG, path=PARSER_DIR):
78"""
79The pkg and path attributes shouldn't need to be set on object
80creation, the default values should work fine. They are hard-coded here
81for security so that a module can't force-load a module from another
82package/location, e.g. from the internet.
83"""
8485
self.pkg = pkg
86self.path = path
87self.parsers = []
8889
def search(self, pattern):
90"""
91Basic wrapper for the two search functions below.
92"""
9394
default_parser = self._search_default(pattern)
95if default_parser != None:
96self.parsers.append(default_parser)
97return default_parser
98else:
99user_parser = self._search_user(pattern)
100if user_parser != None:
101self.parsers.append(user_parser)
102return user_parser
103else:
104logger.warning("Couldn't find a matching parser module for search term {0}".format(pattern))
105return None
106107
def _search_user(self, pattern):
108"""
109Search for a parser name `pattern` in the user-managed parser directory
110"""
111112
logger.debug("Searching for {0} in {1}".format(pattern, self.path))
113try:
114spec = importlib.machinery.PathFinder.find_spec(pattern, path=[self.path])
115parser_module = spec.loader.load_module(spec.name)
116return self._validate_module(parser_module)
117except Exception as e:
118logger.debug("Couldn't find parser {0} in {1}: {2}".format(pattern, self.path, str(e)))
119return None
120121
def _search_default(self, pattern):
122"""
123Search for a parser name `pattern` in the default parser package
124"""
125126
# TODO use importlib.resources.is_resources() once there is a backport to Python 3.6 or below
127logger.debug("Searching for {0} in default parsers".format(pattern))
128try:
129parser_module = importlib.import_module(self.pkg + "." + pattern)
130return self._validate_module(parser_module)
131except Exception as e:
132return None
133134
def _validate_module(self, parser_module):
135"""
136Some basic security tests for candidate modules:
1371. Must contain exactly one Parser object
1383. This class cannot be a redefinition of the base Parser class
1394. Must provide the parse_log() method
1405. Must not return None
1416. Must not match an already-loaded class
142"""
143144
logger.debug("Checking validity of module {0} at {1}".format(parser_module.__name__, parser_module.__file__))
145available_parsers = []
146clsmembers = inspect.getmembers(parser_module, inspect.isclass)
147148
# Check individual classes
149for (_, c) in clsmembers:
150if not (issubclass(c, Parser) & (c is not Parser)):
151continue
152if c in self.parsers:
153logger.warning("Parser class {0} has already been loaded from another source, ignoring it".format(c.__class__.__name__, c.__file__))
154if not inspect.isroutine(c.parse_log):
155logger.warning("Parser class {0} in {1} does not contain a parse_log() method".format(c.__class__.__name__, c.__file__))
156continue
157if None in get_type_hints(c):
158logger.warning("Parser class {0} in {1} contains a null-returning parse_log() method".format(c.__class__.__name__, c.__file__))
159continue
160logger.debug("Found parser {0}.{1}".format(c.__module__, c.__class__.__name__))
161available_parsers.append(c())
162163
# Check module structure
164if len(available_parsers) == 1:
165logger.debug("Parser module {0} at {1} passed validity checks".format(parser_module.__name__, parser_module.__file__))
166return available_parsers[0]
167elif len(available_parsers) == 0:
168logger.warning("No valid classes in {0} at {1}".format(parser_module.__name__, parser_module.__file__))
169return None
170elif len(available_parsers) > 1:
171logger.warning("Found multiple valid parser classes in {0} at {1} - ignoring this module".format(parser_module.__name__, parser_module.__file__))
172return None
173174
def load_pkg(self):
175"""
176Clear the list of currently loaded packages and load all valid and
177non-deprecated parser classes from self.pkg using importlib.
178"""
179180
available_parsers = [name for _, name, _ in iter_modules([dirname(importlib.import_module(self.pkg).__file__)])]
181for parser_name in available_parsers:
182parser_module = importlib.import_module("logparse.parsers." + parser_name)
183parser_class = self._validate_module(parser_module)
184if parser_class == None:
185continue
186parser_obj = parser_class
187if parser_obj.deprecated:
188logger.debug("Ignoring parser {0} because it is deprecated".format(parser_class.__class__.__name__))
189continue
190self.parsers.append(parser_class)
191return self.parsers
192193
def ignore(self, pattern):
194"""
195Remove a parser from the list of currently loaded parsers
196"""
197198
for parser in self.parsers:
199if parser.__module__ == pattern:
200self.parsers.remove(parser)
201logger.debug("Ignoring parser {0}".format(parser.__name__))
202return self.parsers