Source code for ornithology.daemons

# Copyright 2019 HTCondor Team, Computer Sciences Department,
# University of Wisconsin-Madison, WI.
#
# 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.

from typing import Callable

import logging
import re
import datetime
import time
from pathlib import Path

from . import exceptions

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)


[docs] class DaemonLog: """ Represents the log file for a particular HTCondor daemon. Can be used to open multiple "views" (:class:`DaemonLogStream`) of the log file. .. warning:: You shouldn't need to create these yourself. Instead, see the ``<daemon>_log`` methods on :class:`Condor`. """ def __init__(self, path: Path): self.path = path
[docs] def open(self): """ Return a :class:`DaemonLogStream` pointing to the daemon's log file. Returns ------- """ return DaemonLogStream(self.path.open(mode="r", encoding="utf-8"))
[docs] class DaemonLogStream: def __init__(self, file): self.file = file self.messages = [] self.line_number = 0 @property def lines(self): """ An iterator over the raw lines that have been read from the daemon log so far. """ yield from (msg.line for msg in self.messages)
[docs] def read(self): """ Read lines from the daemon log and parse them into :class:`DaemonLogMessage`. Returns an iterator over the messages as they are parsed. .. warning:: If you do not consume the iterator, no lines will be read! """ for line in self.file: line = line.strip() if line == "": continue try: msg = DaemonLogMessage(line.strip(), self.file.name, self.line_number) self.messages.append(msg) yield msg except exceptions.DaemonLogParsingFailed as e: logger.warning(e) self.line_number += 1
[docs] def clear(self): """ Clear the internal message store; useful for isolating tests. """ self.messages.clear()
[docs] def display_raw(self): """ Display the raw text of the daemon log as has been read so far. """ print("\n".join(self.lines))
[docs] def wait( self, condition: Callable[["DaemonLogMessage"], bool], timeout=120 ) -> bool: """ Wait for some condition to be true on a :class:`DaemonLogMessage` parsed from the daemon log. Parameters ---------- condition A callback which will be executed on each message. If the callback returns ``True``, this method will return as well. timeout How long to time out after. If the method times out, it will return ``False`` instead of ``True``. Returns ------- success ``True`` if the condition was satisfied, ``False`` otherwise. """ start = time.time() while True: if time.time() - start > timeout: return False for msg in self.read(): if condition(msg): return True time.sleep(0.1)
RE_MESSAGE = re.compile( r"^(?P<timestamp>\d{2}\/\d{2}\/\d{2}\s\d{2}:\d{2}:\d{2}(?P<milliseconds>\.(\d{3}|1000))?)\s(?P<tags>(?:\([^()]+\)\s+)*)(?P<msg>.*)$" ) RE_TAGS = re.compile(r"\(([^()]+)\)") LOG_MESSAGE_TIME_FORMAT = r"%m/%d/%y %H:%M:%S" MILLISECONDS_LOG_MESSAGE_TIME_FORMAT = r"%m/%d/%y %H:%M:%S.%f"
[docs] class DaemonLogMessage: """ A class that represents a single message in a :class:`DaemonLog`. """ def __init__(self, line: str, file_name: str, line_number: int): """ Parameters ---------- line The actual line that appeared in the daemon log file. file_name The file that the line came from. line_number The line number of the message in the log file. """ self.line = line match = RE_MESSAGE.match(line) if match is None: raise exceptions.DaemonLogParsingFailed( "Failed to parse daemon log line {}:{} -> {}".format( file_name, line_number, repr(line) ) ) if match.group("milliseconds"): self._timestamp = datetime.datetime.strptime( match.group("timestamp"), MILLISECONDS_LOG_MESSAGE_TIME_FORMAT ) if match.group("milliseconds") == ".1000": self._timestamp = self._timestamp.replace(microsecond=999999) else: self._timestamp = datetime.datetime.strptime( match.group("timestamp"), LOG_MESSAGE_TIME_FORMAT ) self._tags = RE_TAGS.findall(match.group("tags")) self.message = match.group("msg") @property def timestamp(self) -> datetime.datetime: """The time that the message occurred.""" return self._timestamp @property def tags(self): """The tags (like ``D_ALL``) of the message.""" return self._tags def __iter__(self): return self.timestamp, self.tags, self.message def __str__(self): return self.line def __repr__(self): return 'LogMessage(timestamp = {}, tags = {}, message = "{}")'.format( self.timestamp, self.tags, self.message ) def __contains__(self, item): return item in self.message