compass-core/compass/log_analyzor/file_matcher.py
Xicheng Chang 8249415e5d Add unit test for file matcher module
Change-Id: I17746fb891d59b4a1a96adcbc97540a1e6df1f70
2014-03-27 18:14:42 +00:00

348 lines
13 KiB
Python

# Copyright 2014 Huawei Technologies Co. Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Module to update intalling progress by processing log file.
.. moduleauthor:: Xiaodong Wang <xiaodongwang@huawei.com>
"""
import logging
import os.path
from compass.db import database
from compass.db.model import LogProgressingHistory
from compass.log_analyzor.line_matcher import Progress
from compass.utils import setting_wrapper as setting
class FileFilter(object):
"""base class to filter log file."""
def __repr__(self):
return self.__class__.__name__
def filter(self, pathname):
"""Filter log file.
:param pathname: the absolute path name to the log file.
"""
raise NotImplementedError(str(self))
class CompositeFileFilter(FileFilter):
"""filter log file based on the list of filters."""
def __init__(self, filters):
self.filters_ = filters
def __str__(self):
return 'CompositeFileFilter[%s]' % self.filters_
def append_filter(self, file_filter):
"""append filter."""
self.filters_.append(file_filter)
def filter(self, pathname):
"""filter log file."""
for file_filter in self.filters_:
if not file_filter.filter(pathname):
return False
return True
class FilterFileExist(FileFilter):
"""filter log file if not exists."""
def filter(self, pathname):
"""filter log file."""
file_exist = os.path.isfile(pathname)
if not file_exist:
logging.error("%s is not exist", pathname)
return file_exist
def get_file_filter():
"""get file filter"""
composite_filter = CompositeFileFilter([FilterFileExist()])
return composite_filter
class FileReader(object):
"""Class to read log file.
The class provide support to read log file from the position
it has read last time. and update the position when it finish
reading the log.
"""
def __init__(self, pathname):
self.pathname_ = pathname
self.position_ = 0
self.partial_line_ = ''
def __repr__(self):
return (
'%s[pathname:%s, position:%s, partial_line:%s]' % (
self.__class__.__name__, self.pathname_, self.position_,
self.partial_line_
)
)
def get_history(self):
"""Get log file read history from database.
:returns: (line_matcher_name progress)
.. note::
The function should be called out of database session.
It reads the log_progressing_history table to get the
position in the log file it has read in last run,
the partial line of the log, the line matcher name
in the last run, the progress, the message and the
severity it has got in the last run.
"""
with database.session() as session:
history = session.query(
LogProgressingHistory
).filter_by(
pathname=self.pathname_
).first()
if history:
self.position_ = history.position
self.partial_line_ = history.partial_line
line_matcher_name = history.line_matcher_name
progress = Progress(history.progress,
history.message,
history.severity)
else:
line_matcher_name = 'start'
progress = Progress(0.0, '', None)
return line_matcher_name, progress
def update_history(self, line_matcher_name, progress):
"""Update log_progressing_history table.
:param line_matcher_name: the line matcher name.
:param progress: Progress instance to record the installing progress.
.. note::
The function should be called out of database session.
It updates the log_processing_history table.
"""
with database.session() as session:
history = session.query(LogProgressingHistory).filter_by(
pathname=self.pathname_).first()
if history:
if history.position >= self.position_:
logging.error(
'%s history position %s is ahead of current '
'position %s',
self.pathname_,
history.position,
self.position_)
return
history.position = self.position_
history.partial_line = self.partial_line_
history.line_matcher_name = line_matcher_name
history.progress = progress.progress
history.message = progress.message
history.severity = progress.severity
else:
history = LogProgressingHistory(
pathname=self.pathname_, position=self.position_,
partial_line=self.partial_line_,
line_matcher_name=line_matcher_name,
progress=progress.progress,
message=progress.message,
severity=progress.severity)
session.merge(history)
logging.debug('update file %s to history %s',
self.pathname_, history)
def readline(self):
"""Generate each line of the log file."""
old_position = self.position_
try:
with open(self.pathname_) as logfile:
logfile.seek(self.position_)
while True:
line = logfile.readline()
self.partial_line_ += line
position = logfile.tell()
if position > self.position_:
self.position_ = position
if self.partial_line_.endswith('\n'):
yield_line = self.partial_line_
self.partial_line_ = ''
yield yield_line
else:
break
if self.partial_line_:
yield self.partial_line_
except Exception as error:
logging.error('failed to processing file %s', self.pathname_)
raise error
logging.debug(
'processing file %s log %s bytes to position %s',
self.pathname_, self.position_ - old_position,
self.position_)
class FileReaderFactory(object):
"""factory class to create FileReader instance."""
def __init__(self, logdir, filefilter):
self.logdir_ = logdir
self.filefilter_ = filefilter
def __str__(self):
return '%s[logdir: %s filefilter: %s]' % (
self.__class__.__name__, self.logdir_, self.filefilter_)
def get_file_reader(self, fullname, filename):
"""Get FileReader instance.
:param fullname: fullname of installing host.
:param filename: the filename of the log file.
:returns: :class:`FileReader` instance if it is not filtered.
"""
pathname = os.path.join(self.logdir_, fullname, filename)
logging.debug('get FileReader from %s', pathname)
if not self.filefilter_.filter(pathname):
logging.error('%s is filtered', pathname)
return None
return FileReader(pathname)
FILE_READER_FACTORY = FileReaderFactory(
setting.INSTALLATION_LOGDIR, get_file_filter())
class FileMatcher(object):
"""File matcher to get the installing progress from the log file."""
def __init__(self, line_matchers, min_progress, max_progress, filename):
if not 0.0 <= min_progress <= max_progress <= 1.0:
raise IndexError(
'%s restriction is not mat: 0.0 <= min_progress'
'(%s) <= max_progress(%s) <= 1.0' % (
self.__class__.__name__,
min_progress,
max_progress))
self.line_matchers_ = line_matchers
self.min_progress_ = min_progress
self.max_progress_ = max_progress
self.absolute_min_progress_ = 0.0
self.absolute_max_progress_ = 1.0
self.absolute_progress_diff_ = 1.0
self.filename_ = filename
def update_absolute_progress_range(self, min_progress, max_progress):
"""update the min progress and max progress the log file indicates."""
progress_diff = max_progress - min_progress
self.absolute_min_progress_ = (
min_progress + self.min_progress_ * progress_diff)
self.absolute_max_progress_ = (
min_progress + self.max_progress_ * progress_diff)
self.absolute_progress_diff_ = (
self.absolute_max_progress_ - self.absolute_min_progress_)
def __str__(self):
return (
'%s[ filename: %s, progress range: [%s:%s], '
'line_matchers: %s]' % (
self.__class__.__name__, self.filename_,
self.absolute_min_progress_,
self.absolute_max_progress_, self.line_matchers_)
)
def update_total_progress(self, file_progress, total_progress):
"""Get the total progress from file progress."""
if not file_progress.message:
logging.info(
'ignore update file %s progress %s to total progress',
self.filename_, file_progress)
return
total_progress_data = min(
(
self.absolute_min_progress_ + (
file_progress.progress * self.absolute_progress_diff_
)
),
self.absolute_max_progress_
)
# total progress should only be updated when the new calculated
# progress is greater than the recored total progress or the
# progress to update is the same but the message is different.
if (
total_progress.progress < total_progress_data or (
total_progress.progress == total_progress_data and
total_progress.message != file_progress.message
)
):
total_progress.progress = total_progress_data
total_progress.message = file_progress.message
total_progress.severity = file_progress.severity
logging.debug('update file %s total progress %s',
self.filename_, total_progress)
else:
logging.info(
'ignore update file %s progress %s to total progress %s',
self.filename_, file_progress, total_progress)
def update_progress(self, fullname, total_progress):
"""update progress from file.
:param fullname: the fullname of the installing host.
:type fullname: str
:param total_progress: Progress instance to update.
the function update installing progress by reading the log file.
It contains a list of line matcher, when one log line matches
with current line matcher, the installing progress is updated.
and the current line matcher got updated.
Notes: some line may be processed multi times. The case is the
last line of log file is processed in one run, while in the other
run, it will be reprocessed at the beginning because there is
no line end indicator for the last line of the file.
"""
file_reader = FILE_READER_FACTORY.get_file_reader(
fullname, self.filename_)
if not file_reader:
return
line_matcher_name, file_progress = file_reader.get_history()
for line in file_reader.readline():
if line_matcher_name not in self.line_matchers_:
logging.debug('early exit at\n%s\nbecause %s is not in %s',
line, line_matcher_name, self.line_matchers_)
break
index = line_matcher_name
while index in self.line_matchers_:
line_matcher = self.line_matchers_[index]
index, line_matcher_name = line_matcher.update_progress(
line, file_progress)
file_reader.update_history(line_matcher_name, file_progress)
self.update_total_progress(file_progress, total_progress)