rename parsers, better journald integration
[logparse.git] / logparse / util.py
index 1a1cca5682bcef4068b36248a04d5c3eea4c3ab8..46efa3a8372a3a8ca64ad6d7e33861b0eeb33238 100644 (file)
-#
-#   utilities.py
-#
-#   Commonly used general functions
-#
+"""
+Commonly used general functions.
 
-import re
+This module provides the following methods:
+    - `hostname`:       get the current machine's hostname
+    - `getlocaldomain`: get the current machine's domain name
+    - `resolve`:        attempt to convert a local/public IP to hostname
+    - `readlog`:        read contents of a log file from disk
+And the following classes:
+    - `LogPeriod`:      period to search logs (wrapper for datetime.timedelata)
+"""
+
+from datetime import datetime, timedelta
+import copy
+from configparser import NoSectionError
+import ipaddress
+import logging
 import os
+from pkg_resources import Requirement, resource_filename
+import re
 import socket
-import inspect
+from sys import exit
 
-import logging
-logger = logging.getLogger(__name__)
+from logparse import config, formatting
+from logparse.timeparse import timeparse
 
-from pkg_resources import Requirement, resource_filename
 
-from . import config
+logger = logging.getLogger(__name__)
+
 
 def hostname(path): # get the hostname of current server
+    """
+    Get the hostname of the current machine using the file supplied in the
+    `hostname-path` config option.
+    """
+
     hnfile = open(path, 'r')
     hn = re.search('^(\w*)\n*', hnfile.read()).group(1)
     return hn
 
+
 def getlocaldomain(): # get the parent fqdn of current server
-    domain = socket.getfqdn().split('.', 1) # Note: if socket.fetfqdn() returns localhost, make sure the first entry in /etc/hosts contains the fqdn
+    """
+    Get parent domain name (possibly FQDN) of the current machine. Note: if
+    `socket.fetfqdn()` returns localhost, make sure the first entry in the
+    hostname file includes the FQDN.
+    """
+
+    domain = socket.getfqdn().split('.', 1)
     if len(domain) != 2:
-        logger.warning('Could not get domain of this server, only hostname. Please consider updating /etc/hosts')
+        logger.warning("Could not get domain of this server, only hostname. "
+            "Please consider updating the hostname file at {0}".format(
+                config.prefs.get("logparse", "hostname-path")))
         return 'localdomain'
     else:
         return domain[-1]
 
-def resolve(ip, fqdn = 'host-only'):        # try to resolve an ip to hostname
-    # Possible values for fqdn:
-    #   fqdn            show full hostname and domain
-    #   fqdn-implicit   show hostname and domain unless local
-    #   host-only       only show hostname
-    #   ip              never resolve anything
-    # resolve-domains defined in individual sections of the config take priority over global config
+
+def resolve(ip, fqdn=None):        # try to resolve an ip to hostname
+    """
+    Attempt to resolve an IP into a hostname or FQDN.
+    Possible values for fqdn:
+        - fqdn            show full hostname and domain
+        - fqdn-implicit   show hostname and domain unless local
+        - host-only       only show hostname
+        - ip              never resolve anything
+    Note resolve-domains settings defined in individual sections of the config
+    take priority over the global config (this is enforced in parser modules)
+    """
     
     if not fqdn:
-        fqdn = config.prefs['resolve-domains']
+        fqdn = config.prefs.get("logparse", "resolve-domains")
 
     if fqdn == 'ip':
         return(ip)
 
     try:
-        socket.inet_aton(ip)  # succeeds if text contains ip
+        ip_obj = ipaddress.ip_address(ip)
+    except ValueError as err:
+        logger.debug("Invalid format: " + str(err))
+        return ip
+
+    try:
         hn = socket.gethostbyaddr(ip)[0] # resolve ip to hostname
-        if fqdn == 'fqdn-implicit' and hn.split('.', 1)[1] == getlocaldomain():
-            return(hn.split('.')[0])
-        elif fqdn == 'fqdn' or fqdn == 'fqdn-implicit':
-            return(hn)
-        elif fqdn == 'host-only':
-            return(hn.split('.')[0])
-        else:
-            logger.warning("invalid value for fqdn config")
-            return(hn)
     except socket.herror:
         # cannot resolve ip
-        logger.debug(ip + " cannot be found, might not exist anymore")
-        return(ip)
-    except (OSError, socket.error): # socket.error for Python 2 compatibility
-        # already a hostname
-        logger.debug(ip + " is already a hostname")
+        # LOGGING DISABLED TO AVOID SPAM
+#        logger.debug(ip + " cannot be found, might not exist anymore")
         return(ip)
     except Exception as err:
-        logger.warning("failed to resolve hostname for " + ip + ": " + str(err))
+        logger.warning("Failed to resolve hostname for " + ip + ": " + str(err))
         return(ip)  # return ip if no hostname exists
 
-def readlog(path = None, mode = 'r'):   # read file
+    if (fqdn == "host-only") or (fqdn == "fqdn-implicit" and ip_obj.is_private):
+        return hn.split('.')[0]
+    if fqdn == 'fqdn' or fqdn == 'fqdn-implicit':
+        return hn
+    return hn
+
+
+
+def readlog(path = None, mode = 'r'):
+    """
+    Read a logfile from disk and return string
+    """
+    
     if (path == None):
-        logger.error("no path provided")
-        return
+        logger.error("No path provided")
+        return 1
     else:
         if (os.path.isfile(path) is False):
-            logger.error("Log at {0} was requested but does not exist".format(path))
+            logger.error("Log at {0} was requested but does not exist"
+                    .format(path))
             return ''
         else:
             try:
                 return open(path, mode).read()
             except IOError or OSError as e:
-                logger.warning("Error reading log at {0}: {1}".format(path, e.strerror))
+                logger.error("Error reading log at {0}: {1}"
+                        .format(path, e.strerror))
                 return 1
+
+class LogPeriod:
+    """
+    Represents a time period for which logs should be parsed (this is given to
+    journald.seek_realtime). Uses timeparse.py by Will Roberts.
+    """
+
+    def __init__(self, section):
+        """
+        If no period is defined for the section config, it is inherited from
+        the global config. Failing that, the program will die.
+        """
+        try:
+            self.startdate = datetime.now() \
+                - timeparse(config.prefs.get(section.split("_")[0], "period"))
+            logger.debug("Parsing logs for {0} since {1}".format(
+                section, self.startdate.strftime(formatting.DATEFMT
+                    + " " + formatting.TIMEFMT)))
+            self.unique = True
+        except (NoSectionError, TypeError):
+            logger.debug("No period defined in section {0} - inheriting "
+                    "global period".format(section))
+            self.startdate = datetime.now() \
+                - timeparse(config.prefs.get("logparse", "period"))
+            self.unique = False
+        except Exception as e:
+            logger.error("Could not find valid time period for parser {0}"
+                    " {1}".format(section, e))
+            return None
+
+    def compare(self, d, ephemeral=False) -> bool:
+        """
+        Compare the datetime `d` of a log record with the starting datetime of 
+        this LogPeriod and return a boolean result representing whether the 
+        log record is after the starting datetime. If either `self.startdate` 
+        or `d` are UTC-naive, an appropriate correction is attempted. The 
+        `ephemeral` boolean argument, if set to true, prevents permanent 
+        modification of `self.startdate` to match the UTC-awareness of the 
+        section's log records, in the case that `d` may represent an anomaly.
+        """
+        try:
+            # First attempt a direct comparison
+            return True if d > self.startdate else False
+
+        except TypeError as e:
+            # If the direct comparison fails, make sure that both objects have 
+            # a UTC offset
+
+            if d.tzinfo is None \
+                    or (d.tzinfo is not None
+                    and not d.tzinfo.utcoffset(d)):
+                # d has no timezone info,
+                # OR d has timezone info but no offset
+
+                if self.startdate.tzinfo is not None \
+                        and self.startdate.tzinfo.utcoffset(self.startdate):
+                    # If d is naive and self.startdate is aware, assume that
+                    # the timezone of d is the same as self.startdate by
+                    # making self.startdate naive
+
+                    logger.warning("{0} checking date {1}: {2}. Time "
+                            "of log record is naive. Inheriting UTC "
+                            "offset for logs from system date".format(
+                        e.__class__.__name__, d, e))
+                    if ephemeral:
+                        # Compare with UTC-naive version of self.startdate
+                        return True if d > self.startdate.replace(tzinfo=None) \
+                                else False
+                    else:
+                        # Remove UTC awareness for self.startdate
+                        self.startdate = self.startdate.replace(tzinfo=None)
+                        return True if d > self.startdate else False
+
+            elif self.startdate.tzinfo is None \
+                    or (self.startdate.tzinfo is not None
+                    and not self.startdate.tzinfo.utcoffset(self.startdate)):
+                # d has timezoneinfo and offset, but self.startdate has either 
+                # no timezone info, or timezone  info and no offset. In this 
+                # case, self.startdate inherits the offset of d.
+
+                logger.warning("{0} checking date {1}: {2}. Time of "
+                        "start date is naive. Inheriting UTC offset "
+                        "for date comparison from logs".format(
+                    e.__class__.__name__, d, e))
+
+                if ephemeral:
+                    # Compare with UTC-aware version of self.startdate
+                    return True if d > self.startdate.astimezone(d.tzinfo) \
+                            else False
+                else:
+                    # Add UTC awareness for self.startdate
+                    self.startdate = self.startdate.astimezone(d.tzinfo)
+                    return True if d > self.startdate else False
+                        
+            else:
+                # Other errors return false (effectively ignore this record)
+                logger.error("{0} comparing date {1}: {2}".format(
+                    e.__class__.__name__, d, e))
+                return False