Esempio n. 1
0
class ScribeLogger(object):
    """Implementation that logs to a scribe server. If errors are encountered,
    drop lines and retry occasionally.

    :param host: hostname of the scribe server
    :param port: port number of the scribe server
    :param retry_interval: number of seconds to wait between retries
    :param report_status: a function `report_status(is_error, msg)` which is
        called to print out errors and status messages. The first
        argument indicates whether what is being printed is an error or not,
        and the second argument is the actual message.
    :param logging_timeout: milliseconds to time out scribe logging; "0" means
        blocking (no timeout)
    """
    def __init__(self,
                 host,
                 port,
                 retry_interval,
                 report_status=None,
                 logging_timeout=None):
        # set up thrift and scribe objects
        timeout = logging_timeout if logging_timeout is not None else config.scribe_logging_timeout
        self.socket = thriftpy.transport.socket.TSocket(
            six.text_type(host), int(port))
        if timeout:
            self.socket.set_timeout(timeout)

        self.transport = TFramedTransportFactory().get_transport(self.socket)
        protocol = TBinaryProtocolFactory(strict_read=False).get_protocol(
            self.transport)
        self.client = TClient(scribe_thrift.scribe, protocol)

        # our own bookkeeping for connection
        self.connected = False  # whether or not we think we're currently connected to the scribe server
        self.last_connect_time = 0  # last time we got disconnected or failed to reconnect

        self.retry_interval = retry_interval
        self.report_status = report_status or get_default_reporter()
        self.__lock = threading.RLock()
        self._birth_pid = os.getpid()

    def _maybe_reconnect(self):
        """Try (re)connecting to the server if it's been long enough since our
        last attempt.
        """
        assert self.connected == False

        # don't retry too often
        now = time.time()
        if (now - self.last_connect_time) > self.retry_interval:
            try:
                self.transport.open()
                self.connected = True
            except TTransportException:
                self.last_connect_time = now
                self.report_status(
                    True, 'yelp_clog failed to connect to scribe server')

    def _log_line_no_size_limit(self, stream, line):
        """Log a single line without size limit. It should not include any newline characters.
           Since this method is called in log_line, the line should be in utf-8 format and
           less than MAX_LINE_SIZE_IN_BYTES already. We don't limit traceback size.
        """
        with self.__lock:
            if os.getpid() != self._birth_pid:
                raise ScribeIsNotForkSafeError
            if not self.connected:
                self._maybe_reconnect()

            if self.connected:
                log_entry = scribe_thrift.LogEntry(category=scribify(stream),
                                                   message=line + b'\n')
                try:
                    return self.client.Log(messages=[log_entry])
                except Exception as e:
                    try:
                        self.report_status(
                            True,
                            'yelp_clog failed to log to scribe server with '
                            ' exception: %s(%s)' % (type(e), six.text_type(e)))
                    finally:
                        self.close()
                        self.last_connect_time = time.time()

                    # Don't reconnect if report_status raises an exception
                    self._maybe_reconnect()

    def log_line(self, stream, line):
        """Log a single line. It should not include any newline characters.
           If the line size is over 50 MB, an exception raises and the line will be dropped.
           If the line size is over 5 MB, a message consisting origin stream information
           will be recorded at WHO_CLOG_LARGE_LINE_STREAM (in json format).
        """
        # log unicodes as their utf-8 encoded representation
        if isinstance(line, six.text_type):
            line = line.encode('UTF-8')

        # check log line size
        if len(line) <= WARNING_LINE_SIZE_IN_BYTES:
            self._log_line_no_size_limit(stream, line)
        elif len(line) <= MAX_LINE_SIZE_IN_BYTES:
            self._log_line_no_size_limit(stream, line)

            # log the origin of the stream with traceback to WHO_CLOG_LARGE_LINE_STREAM category
            origin_info = {}
            origin_info['stream'] = stream
            origin_info['line_size'] = len(line)
            origin_info['traceback'] = ''.join(traceback.format_stack())
            log_line = json.dumps(origin_info).encode('UTF-8')
            self._log_line_no_size_limit(WHO_CLOG_LARGE_LINE_STREAM, log_line)
            self.report_status(
                False,
                'The log line size is larger than %r bytes (monitored in \'%s\')'
                % (WARNING_LINE_SIZE_IN_BYTES, WHO_CLOG_LARGE_LINE_STREAM))
        else:
            # raise an exception if too large
            self.report_status(
                True,
                'The log line is dropped (line size larger than %r bytes)' %
                MAX_LINE_SIZE_IN_BYTES)
            raise LogLineIsTooLongError(
                'The max log line size allowed is %r bytes' %
                MAX_LINE_SIZE_IN_BYTES)

    def close(self):
        self.transport.close()
        self.connected = False