#!/usr/bin/env python
# coding: utf8
from __future__ import (unicode_literals, absolute_import, division, print_function)
from threading import Thread, Lock
from string import center
import datetime
import io
import os
import time
import sys
[docs]class LogLevel:
"""
Stores the possible values of the log printing parameters ``log_level``.
This is an ersatz to an Enumerate object (not available in python2).
``level_name`` is used to get the ``log_level`` name from its value.
"""
DEBUG = 10
INFO = 20
WARNING = 30
ERROR = 40
CRITICAL = 50
level_name = {10: 'DEBUG', 20: 'INFO', 30: 'WARNING', 40: 'ERROR', 50: 'CRITICAL'}
[docs]class Displayer(Thread):
"""
This thread object is responsible for the logging and the printing of the stats, the alerts and the log messages.
The log messages are messages generated by the different threads to inform the user of a peculiar event.
This thread can keep up to two output log files : one for the traffic alerts, and one for the log messages.
The alert log is always used, the program log is optional, by default activated.
The log messages can also be printed on the console.
Note
----
Once it has been initiated (for instance with ``picasso = Displayer(debug=True)``, its :meth:`log` method can be
simply called from any module with the following code::
import display as d
d.log(self, d.LogLevel.INFO, "This is my log message !")
This is possible because in :meth:`.__init__`, it defines a ``global displayer`` variable.
Attributes
----------
statistician : Statistician
A reference to the statistician that will be contacted to print the stats.
display_period: int
The stats will be printed with a period = display_period
log_level: LogLevel
The limit under which received log messages will simply be ignored in the log method
console_print_program_log: bool
If true, the receive log messages will be printed in the console, if their log_level is above or equal to the log_level
write_program_log_file: bool
If true, the receive log messages will be written in the program log file, if their log_level is above or equal to the log_level
should_run: bool
If False, the thread will shortly end stop its operation. Used to cleanly end the program.
registered_object: list of Thread
The list of other threads, used of INFO log messages
console_lock: Lock
Lock used to make the print function thread-safe
display_width: int
The typical width of the terminal
name: string
"""
def __init__(self, statistician=None, display_period=10,
console_print_program_log=True, write_program_log_file=True, log_level=LogLevel.WARNING,
debug=False):
Thread.__init__(self)
# registers the displayer reference at the module level, so that other module can access it.
global displayer
displayer = self
self.statistician = statistician
self.display_period = display_period
self.log_level = log_level
self.console_print_program_log = console_print_program_log
self.write_program_log_file = write_program_log_file
# this is for the log messages
self.program_log_path = '../log/log'
self.program_log_opened = False
# this is for the alerts
self.alert_log_path = '../log/alert_log'
self.alert_log_opened = False
self.should_run = True
self.registered_object = []
self.console_lock = Lock()
self.display_width = 80
self.name = 'displayer thread'
if debug: # debug is a short way to define a simple Displayer object, printing in the console
self.log_level = LogLevel.DEBUG
self.console_print_program_log = True
self.write_program_log_file = False
# if the log message are written in a file
if self.write_program_log_file:
try:
os.remove(self.program_log_path)
except OSError:
pass
try:
self.program_log = io.open(self.program_log_path, 'at')
self.program_log_opened = True
except (IOError, ValueError):
temp_c_print = self.console_print_program_log
self.console_print_program_log = True
self.log(self, LogLevel.WARNING, "Output log file couldn't have been opened")
self.console_print_program_log = temp_c_print
# we reinitialised and open the alert log file
try:
os.remove(self.alert_log_path)
except OSError:
pass
try:
self.alert_log = io.open(self.alert_log_path, 'at')
self.alert_log_opened = True
except (IOError, ValueError):
temp_c_print = self.console_print_program_log
self.console_print_program_log = True
self.log(self, LogLevel.WARNING, "Alert log file couldn't have been opened")
self.console_print_program_log = temp_c_print
# We log an first WARNING if no statistician reference was given
if self.statistician is None:
self.log(self, LogLevel.WARNING, 'No statistician given')
[docs] def run(self):
"""
Regularly prints the stats, and ends when should_run = False (normally after 0.1s max)
Called when the thread is started.
"""
# Welcome message
program_stat = ' - '.join(
['{} is {}started'.format(thread.__class__.__name__, (not thread.is_alive()) * '*not* ')
for thread in self.registered_object])
welcome_msg = '=' * self.display_width + '\n' + \
center('Welcome! HTTP-log Displayer is now running', self.display_width) + '\n' + \
center(program_stat, self.display_width) + '\n' + '=' * self.display_width
self.lock_print(welcome_msg)
# Never ending loop
last_display_time = time.time() # we don't want to print something just after the beginning
while self.should_run:
delta = time.time() - last_display_time
# print(delta)
if delta >= self.display_period:
if self.registered_object:
self.log(self, LogLevel.INFO, self.stat_of_registered_object())
self.lock_print(self.stat_string())
last_display_time = time.time()
else:
time.sleep(min(self.display_period - delta, 0.1))
try:
self.alert_log.close()
except:
pass
try:
self.program_log.close()
except:
pass
[docs] def stat_string(self):
"""This is called when stats need to be printed
Returns
-------
string
The formatted message
"""
stats = self.statistician.stat.get_last_stats()
self.statistician.stat.reset_short_stat()
# stats['separation'] = '='*self.display_width
stats['time'] = datetime.datetime.now()
stats['total_MB_bytes'] = stats['total_bytes'] / (1024 ** 2)
stats['display_period'] = self.display_period
if stats['total_hits']:
res = '{time:%X} - Most visited section in the past {display_period}s is ' \
'\'{max_section}\' with {max_hit} hits.' \
'\n\tTotal hits: {total_hits},' \
'\n\tTotal sent bytes: {total_bytes}, i.e. {total_MB_bytes:.03f} MB.' \
''.format(**stats)
else:
res = '{time:%X} - No request in the last {display_period}s!'.format(**stats)
return res
[docs] def log(self, sender, level, message):
"""
Receives messages from all the threads and "thread-safely" prints them depending of their level. Writes them on
the log file if needed.
Args
----
sender: Object
A reference to the sender
level: LogLevel
The log level of the message
message: string
The message
"""
if level >= self.log_level:
log_line = " : ".join((datetime.datetime.now().isoformat(),
LogLevel.level_name[level],
sender.__class__.__name__,
message))
if self.console_print_program_log:
self.lock_print(log_line)
# print('statistician', self.statistician, self.console_print_program_log, self.log_level)
if self.write_program_log_file and self.program_log_opened:
self.program_log.write(log_line + '\n')
self.program_log.flush()
[docs] def lock_print(self, *args, **kwargs):
"""Thread-safely print function"""
with self.console_lock:
print(*args, **kwargs)
[docs] def stat_of_registered_object(self):
"""Returns a string describing the registered threads."""
return '; '.join([': '.join((obj.__class__.__name__, obj.state())) for obj in self.registered_object])
[docs] def print_new_alert(self, alert_param, long_average, short_average):
"""Called when a alert is raised, prints and/or logs it
Args
----
alert_param: AlertParam
long_average: float
short_average: float
"""
high_outflow = short_average / (1024 ** 2) / (alert_param.time_resolution * alert_param.short_median)
msg = 'High traffic detected: {:%x %X}: outflow {:.03}MB/s'.format(datetime.datetime.now(), high_outflow)
res = center('/' * 17 + ' ALERT ' + '\\' * 17, self.display_width) + '\n' + center(msg, self.display_width)
self.lock_print(res)
if self.alert_log_opened:
self.alert_log.write(msg + '\n')
self.alert_log.flush()
[docs] def print_end_alert(self, alert_param, long_average, short_average):
"""Called when a alert is shut down, prints and/or logs it
Note
----
Could be merged with ``print_new_alert``
"""
low_outflow = long_average / (1024 ** 2) / (alert_param.time_resolution * alert_param.long_median)
msg = 'End of alert: {:%x %X}: outflow {:.03}MB/s'.format(datetime.datetime.now(), low_outflow)
res = center('\\' * 15 + ' ALERT OVER ' + '/' * 15, self.display_width) + '\n' + \
center(msg, self.display_width)
self.lock_print(res)
if self.alert_log_opened:
self.alert_log.write(msg + '\n')
self.alert_log.flush()
if __name__ == '__main__':
# Starts a Displayer and sends two messages (only one is printed, because of the log level)
from log_writer import random_HTTP_request, uniform_random_local_URL_maker
from reader import get_section
from random import randint
th = Displayer(display_period=1, debug=True)
random_URL = uniform_random_local_URL_maker()
th.stat_string = lambda: 'Most visited section: {0[0]} with {0[1]} hits.' \
''.format((get_section(random_HTTP_request(random_URL())), randint(0, 1e4)))
th.start()
th.log(th, LogLevel.DEBUG, 'Msg debug 1')
th.log_level = LogLevel.DEBUG
th.log(th, LogLevel.DEBUG, 'Msg debug 2')
time.sleep(3)
th.should_run = False