marcus67/little_brother

View on GitHub
little_brother/process_statistics.py

Summary

Maintainability
D
2 days
Test Coverage
# -*- coding: utf-8 -*-

# Copyright (C) 2019  Marcus Rickert
#
# See https://github.com/marcus67/little_brother
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

from python_base_app import log_handling
from python_base_app import tools


class HostStat(object):

    def __init__(self, p_hostname, p_percent, p_count=1):

        self._hostname = p_hostname
        self._count = p_count
        self._percent = p_percent

    def add_occurence(self, p_percent, p_count=0):

        self._count += p_count

        if p_percent > self._percent:
            self._percent = p_percent

    @property
    def hostname(self):

        return self._hostname

    @property
    def percent(self):

        return self._percent

    @property
    def count(self):

        return self._count

    @property
    def summary(self):

        if self._percent == 100:
            return "{name}({count})".format(name=self._hostname, count=self._count)

        else:
            return "{name}({count}, {percent}%)".format(name=self._hostname, count=self._count, percent=self._percent)


class Activity(object):

    def __init__(self, p_start_time=None):

        self.host_stats = {}
        self.start_time = p_start_time
        self.end_time = None
        self.downtime = 0

    @property
    def percent(self):

        if len(self.host_stats) == 0:
            return 100

        max_host_stat = max(self.host_stats.values(), key=lambda host_stat: host_stat.percent)
        return max_host_stat.percent

    def add_host_process(self, p_hostname, p_percent=100):

        host_stat : HostStat = self.host_stats.get(p_hostname)

        if host_stat is None:
            host_stat = HostStat(p_hostname=p_hostname, p_percent=p_percent)
            self.host_stats[p_hostname] = host_stat

        else:
            host_stat.add_occurence(p_percent=p_percent)

    def set_end_time(self, p_end_time):

        self.end_time = p_end_time

    def set_downtime(self, p_downtime):

        if p_downtime > self.downtime:
            self.downtime = p_downtime

    @property
    def duration(self):

        if self.end_time is not None and self.start_time is not None:
            return max((self.end_time - self.start_time).total_seconds() * self.percent / 100 - self.downtime, 0)

        else:
            return None

    def current_duration(self, p_reference_time):

        return max((p_reference_time - self.start_time).total_seconds() * self.percent / 100 - self.downtime, 0)

    def __str__(self):

        fmt = "Activity([{start_time}, {end_time}], {duration}, downtime={downtime})"
        return fmt.format(start_time=tools.get_timestamp_as_string(self.start_time),
                          end_time=tools.get_timestamp_as_string(self.end_time),
                          duration=tools.get_duration_as_string(self.duration),
                          downtime=tools.get_duration_as_string(self.downtime))

    @property
    def host_infos(self):

        return ", ".join(host_stat.summary for host_stat in self.host_stats.values())


class DayStatistics(object):

    def __init__(self):

        self.activities = []
        self.min_time = None
        self.max_time = None
        self.host_stats = {}

    def add_activity(self, p_activity):

        self.activities.append(p_activity)

        for host_stat in p_activity.host_stats.values():
            day_host_stat = self.host_stats.get(host_stat.hostname)

            if day_host_stat is None:
                day_host_stat = HostStat(p_hostname=host_stat.hostname, p_count=host_stat.count,
                                         p_percent=host_stat.percent)
                self.host_stats[host_stat.hostname] = day_host_stat

            else:
                day_host_stat.add_occurence(p_count=host_stat.count,
                                            p_percent=host_stat.percent)

        if self.min_time is None or p_activity.start_time < self.min_time:
            self.min_time = p_activity.start_time

        if p_activity.end_time is not None and \
                (self.max_time is None or p_activity.end_time > self.max_time):
            self.max_time = p_activity.end_time

    @property
    def duration(self):

        seconds = 0

        for activity in self.activities:
            secs = activity.duration

            if secs is not None:
                seconds = seconds + secs

        return seconds

    @property
    def downtime(self):

        seconds = 0

        for activity in self.activities:
            secs = activity.downtime

            if secs is not None:
                seconds = seconds + secs

        return seconds

    @property
    def host_infos(self):

        return ", ".join(host_stat.summary for host_stat in self.host_stats.values())


class ProcessStatisticsInfo(object):

    def __init__(self, p_username, p_reference_time, p_max_lookback_in_days, p_min_activity_duration,
                 p_notification_name=None, p_full_name=None):

        self._logger = log_handling.get_logger(self.__class__.__name__)

        self.username = p_username
        self.notification_name = p_notification_name if p_notification_name is not None else self.username
        self.full_name = p_full_name if p_full_name is not None else self.username
        self.reference_time = p_reference_time
        self.reference_date = p_reference_time.date()
        self.max_lookback_in_days = p_max_lookback_in_days
        self.min_activity_duration = p_min_activity_duration

        self.active_processes = 0

        self.last_inactivity_start_time = None
        self.current_activity = None
        self.previous_activity = None

        self.accumulated_break_time = 0
        self.has_downtime = False

        self.day_statistics = [DayStatistics() for _i in range(0, p_max_lookback_in_days + 1)]
        self.currently_active_host_processes = {}

    def add_process_start(self, p_process_info, p_start_time):

        if self.active_processes == 0:
            self.current_activity = Activity(p_start_time=p_start_time)

        self.current_activity.add_host_process(p_process_info.hostlabel, p_percent=p_process_info.percent)
        self.current_activity.set_downtime(p_downtime=p_process_info.downtime)
        self.active_processes = self.active_processes + 1

    def add_process_end(self, p_process_info, p_end_time):

        # if the process is still running we store it as a candidate to be killed if required
        if p_process_info.end_time is None:
            process_list = self.currently_active_host_processes.get(p_process_info.hostname)

            if process_list is None:
                process_list = []
                self.currently_active_host_processes[p_process_info.hostname] = process_list

            process_list.append((p_process_info.processhandler, p_process_info.pid, p_process_info.start_time))

        if self.active_processes == 0:
            fmt = "Active processes less than zero"
            self._logger.warning(fmt)
            return

        self.active_processes = self.active_processes - 1

        if self.active_processes == 0 and p_process_info.end_time is not None:
            self.current_activity.set_end_time(p_end_time=p_end_time)
            self.current_activity.set_downtime(p_downtime=p_process_info.downtime)

            login_date = self.current_activity.start_time.date()
            lookback = int((self.reference_date - login_date).total_seconds() / (24 * 3600))

            if self.current_activity.duration > self.min_activity_duration:
                if lookback <= self.max_lookback_in_days:
                    self.day_statistics[lookback].add_activity(self.current_activity)

                self.last_inactivity_start_time = p_end_time
                self.previous_activity = self.current_activity

            self.current_activity = None

    @property
    def current_activity_start_time(self):

        if self.current_activity is None:
            return None

        else:
            return self.current_activity.start_time

    @property
    def previous_activity_start_time(self):

        if self.previous_activity is None:
            return None

        else:
            return self.previous_activity.start_time

    @property
    def previous_activity_end_time(self):

        if self.previous_activity is None:
            return None

        else:
            return self.previous_activity.end_time

    @property
    def previous_activity_duration(self):

        if self.previous_activity is None:
            return None

        else:
            return self.previous_activity.duration

    @property
    def todays_activity_duration(self):

        duration = self.day_statistics[0].duration

        active_duration = self.current_activity_duration

        if active_duration is not None:
            duration = duration + active_duration

        return duration

    @property
    def todays_downtime(self):

        downtime = self.day_statistics[0].downtime

        active_downtime = self.current_activity_downtime

        if active_downtime is not None:
            downtime = downtime + active_downtime

        # TODO: downtime counted twice!
        return downtime

    @property
    def seconds_since_last_activity(self):

        if self.last_inactivity_start_time is not None:
            return (self.reference_time - self.last_inactivity_start_time).total_seconds()

        if self.active_processes > 0:
            return 0

        return None

    @property
    def current_activity_duration(self):

        if self.current_activity is None:
            return None

        else:
            return self.current_activity.current_duration(p_reference_time=self.reference_time)

    @property
    def current_activity_downtime(self):

        if self.current_activity is None:
            return None

        else:
            return self.current_activity.downtime

    def __str__(self):

        return "StatInfo (user=%s, today:%d[s], yesterday:%d[s], ref-time:%s, previous %s, " \
               "current %s, secs-since-last-activity:%s)" % (
                   self.username,
                   self.day_statistics[0].duration,
                   self.day_statistics[1].duration,
                   tools.get_timestamp_as_string(p_timestamp=self.reference_time),
                   str(self.previous_activity) if self.previous_activity is not None else "---",
                   str(self.current_activity) if self.current_activity is not None else "---",
                   tools.get_duration_as_string(p_seconds=self.seconds_since_last_activity)
               )


def get_empty_stat_infos(
        p_user_map,
        p_reference_time,
        p_max_lookback_in_days,
        p_min_activity_duration):
    stat_infos = {}

    for user in p_user_map.values():
        user_stat_infos = {}

        for ruleset in user.rulesets:
            stat_info = ProcessStatisticsInfo(
                p_username=user.username,
                p_notification_name=user.notification_name,
                p_full_name=user.full_name,
                p_reference_time=p_reference_time,
                p_max_lookback_in_days=p_max_lookback_in_days,
                p_min_activity_duration=p_min_activity_duration)
            user_stat_infos[ruleset.context] = stat_info

        stat_infos[user.username] = user_stat_infos

    return stat_infos


def get_process_statistics(
        p_user_map,
        p_process_infos,
        p_reference_time,
        p_max_lookback_in_days,
        p_min_activity_duration):
    users_stat_infos = get_empty_stat_infos(
        p_user_map=p_user_map,
        p_reference_time=p_reference_time,
        p_max_lookback_in_days=p_max_lookback_in_days,
        p_min_activity_duration=p_min_activity_duration)

    process_info_boundaries = [(pinfo.start_time, pinfo.get_key(), pinfo, "START") for pinfo in
                               p_process_infos.values()]
    process_info_boundaries.extend(
        [(pinfo.end_time, pinfo.get_key(), pinfo, "END") for pinfo in p_process_infos.values() if
         pinfo.end_time is not None])
    sorted_process_info_boundaries = sorted(process_info_boundaries)

    if len(sorted_process_info_boundaries) > 0:
        maximum_boundary_time = sorted_process_info_boundaries[-1][0]

        if p_reference_time > maximum_boundary_time:
            maximum_boundary_time = p_reference_time

        sorted_process_info_boundaries.extend([(maximum_boundary_time, pinfo.get_key(), pinfo, "END")
                                               for pinfo in p_process_infos.values() if pinfo.end_time is None])

        for (boundary_time, _key, pinfo, boundary_type) in sorted_process_info_boundaries:
            user_stat_infos = users_stat_infos.get(pinfo.username)

            if user_stat_infos is None:
                user_stat_infos = {}
                users_stat_infos[pinfo.username] = user_stat_infos

            if pinfo.username in p_user_map:
                user = p_user_map[pinfo.username]
                for ruleset in user.rulesets:

                    if (pinfo.processname is None or
                            (pinfo.processname is not None and user.regex_process_name_pattern.match(
                                pinfo.processname))):
                        stat_info = user_stat_infos.get(ruleset.context)

                        if boundary_type == "START":
                            stat_info.add_process_start(p_process_info=pinfo, p_start_time=boundary_time)

                        else:
                            stat_info.add_process_end(p_process_info=pinfo, p_end_time=boundary_time)

    # Add statistics entries for current entries
    for user_stat_infos in users_stat_infos.values():
        for user_stat_info in user_stat_infos.values():
            if user_stat_info.current_activity is not None:
                login_date = user_stat_info.current_activity.start_time.date()
                lookback = int((user_stat_info.reference_date - login_date).total_seconds() / (24 * 3600))

                # If there's an actity more than lookback days back enlarge the stat array accordingly...
                if lookback >= len(user_stat_info.day_statistics):
                    for i in range(len(user_stat_info.day_statistics), lookback + 1):
                        user_stat_info.day_statistics.append(DayStatistics())

                user_stat_info.day_statistics[lookback].add_activity(user_stat_info.current_activity)

            user_stat_info.has_downtime = False

            for i in range(p_max_lookback_in_days):
                if user_stat_info.day_statistics[i].downtime:
                    user_stat_info.has_downtime = True
                    break

    return users_stat_infos