pax_global_header00006660000000000000000000000064135223330570014515gustar00rootroot0000000000000052 comment=d73e3870a0be324bf3b39d05cfca896c188ce7f4 daiquiri-1.6.0/000077500000000000000000000000001352233305700133305ustar00rootroot00000000000000daiquiri-1.6.0/.circleci/000077500000000000000000000000001352233305700151635ustar00rootroot00000000000000daiquiri-1.6.0/.circleci/config.yml000066400000000000000000000017311352233305700171550ustar00rootroot00000000000000version: 2 jobs: pep8: docker: - image: circleci/python:3.7 steps: - checkout - run: command: | sudo pip install tox tox -e pep8 docs: docker: - image: circleci/python:3.7 steps: - checkout - run: command: | sudo pip install tox tox -e docs py27: docker: - image: circleci/python:2.7 steps: - checkout - run: command: | sudo pip install tox tox -e py27 py36: docker: - image: circleci/python:3.6 steps: - checkout - run: command: | sudo pip install tox tox -e py36 py37: docker: - image: circleci/python:3.7 steps: - checkout - run: command: | sudo pip install tox tox -e py37 workflows: version: 2 test: jobs: - pep8 - py27 - py36 - py37 daiquiri-1.6.0/.gitignore000066400000000000000000000001511352233305700153150ustar00rootroot00000000000000*.py[cod] # Packages *.egg* *.egg-info dist .tox # pbr generates these AUTHORS ChangeLog doc/build build daiquiri-1.6.0/.mergify.yml000066400000000000000000000007611352233305700155770ustar00rootroot00000000000000pull_request_rules: - name: automatic merge conditions: - base=master - "status-success=ci/circleci: pep8" - "status-success=ci/circleci: docs" - "status-success=ci/circleci: py27" - "status-success=ci/circleci: py36" - "status-success=ci/circleci: py37" - "#approved-reviews-by>=1" - label!=work-in-progress actions: merge: strict: "smart" - name: dismiss reviews conditions: [] actions: dismiss_reviews: {} daiquiri-1.6.0/.testr.conf000066400000000000000000000005101352233305700154120ustar00rootroot00000000000000[DEFAULT] test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ ${PYTHON:-python} -m subunit.run discover -t ./ ./daiquiri $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list daiquiri-1.6.0/LICENSE000066400000000000000000000236361352233305700143470ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. daiquiri-1.6.0/README.rst000066400000000000000000000011631352233305700150200ustar00rootroot00000000000000======================================= daiquiri -- Python logging setup helper ======================================= .. image:: https://circleci.com/gh/jd/daiquiri.svg?style=svg :target: https://circleci.com/gh/jd/daiquiri .. image:: https://img.shields.io/pypi/v/daiquiri.svg :target: https://pypi.python.org/pypi/daiquiri :alt: Latest Version The daiquiri library provides an easy way to configure logging. It also provides some custom formatters and handlers. You can read the whole documentation at http://daiquiri.readthedocs.io/ * Free software: Apache license * Source: https://github.com/jd/daiquiri daiquiri-1.6.0/daiquiri/000077500000000000000000000000001352233305700151375ustar00rootroot00000000000000daiquiri-1.6.0/daiquiri/__init__.py000066400000000000000000000102121352233305700172440ustar00rootroot00000000000000# 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. import logging import logging.config import logging.handlers import sys import traceback from daiquiri import output class KeywordArgumentAdapter(logging.LoggerAdapter): """Logger adapter to add keyword arguments to log record's extra data Keywords passed to the log call are added to the "extra" dictionary passed to the underlying logger so they are emitted with the log message and available to the format string. Special keywords: extra An existing dictionary of extra values to be passed to the logger. If present, the dictionary is copied and extended. """ def process(self, msg, kwargs): # Make a new extra dictionary combining the values we were # given when we were constructed and anything from kwargs. extra = self.extra.copy() if 'extra' in kwargs: extra.update(kwargs.pop('extra')) # Move any unknown keyword arguments into the extra # dictionary. for name in list(kwargs.keys()): if name == 'exc_info': continue extra[name] = kwargs.pop(name) extra['_daiquiri_extra_keys'] = set(extra.keys()) kwargs['extra'] = extra return msg, kwargs if sys.version_info.major == 2: def setLevel(self, level): """ Set the specified level on the underlying logger. """ self.logger.setLevel(level) def getLogger(name=None, **kwargs): """Build a logger with the given name. :param name: The name for the logger. This is usually the module name, ``__name__``. :type name: string """ return KeywordArgumentAdapter(logging.getLogger(name), kwargs) def setup(level=logging.WARNING, outputs=[output.STDERR], program_name=None, capture_warnings=True): """Setup Python logging. This will setup basic handlers for Python logging. :param level: Root log level. :param outputs: Iterable of outputs to log to. :param program_name: The name of the program. Auto-detected if not set. :param capture_warnings: Capture warnings from the `warnings' module. """ root_logger = logging.getLogger(None) # Remove all handlers for handler in list(root_logger.handlers): root_logger.removeHandler(handler) # Add configured handlers for out in outputs: if isinstance(out, str): out = output.preconfigured.get(out) if out is None: raise RuntimeError("Output {} is not available".format(out)) out.add_to_logger(root_logger) root_logger.setLevel(level) program_logger = logging.getLogger(program_name) def logging_excepthook(exc_type, value, tb): program_logger.critical( "".join(traceback.format_exception(exc_type, value, tb))) sys.excepthook = logging_excepthook if capture_warnings: logging.captureWarnings(True) def parse_and_set_default_log_levels(default_log_levels, separator='='): """Set default log levels for some loggers. :param default_log_levels: List of strings with format """ return set_default_log_levels((pair.split(separator, 1) for pair in default_log_levels)) def set_default_log_levels(loggers_and_log_levels): """Set default log levels for some loggers. :param loggers_and_log_levels: List of tuple (logger name, level). """ for logger, level in loggers_and_log_levels: if isinstance(level, str): level = level.upper() logging.getLogger(logger).setLevel(level) daiquiri-1.6.0/daiquiri/formatter.py000066400000000000000000000112551352233305700175200ustar00rootroot00000000000000# 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. import logging try: from pythonjsonlogger import jsonlogger except ImportError: jsonlogger = None DEFAULT_FORMAT = ( "%(asctime)s [%(process)d] %(color)s%(levelname)-8.8s " "%(name)s: %(message)s%(color_stop)s" ) DEFAULT_EXTRAS_FORMAT = ( "%(asctime)s [%(process)d] %(color)s%(levelname)-8.8s " "%(name)s%(extras)s: %(message)s%(color_stop)s" ) class ColorFormatter(logging.Formatter): """Colorizes log output""" # TODO(jd) Allow configuration LEVEL_COLORS = { logging.DEBUG: '\033[00;32m', # GREEN logging.INFO: '\033[00;36m', # CYAN logging.WARN: '\033[01;33m', # BOLD YELLOW logging.ERROR: '\033[01;31m', # BOLD RED logging.CRITICAL: '\033[01;31m', # BOLD RED } COLOR_STOP = '\033[0m' def add_color(self, record): if getattr(record, "_stream_is_a_tty", False): record.color = self.LEVEL_COLORS[record.levelno] record.color_stop = self.COLOR_STOP else: record.color = "" record.color_stop = "" def remove_color(self, record): del record.color del record.color_stop def format(self, record): self.add_color(record) s = super(ColorFormatter, self).format(record) self.remove_color(record) return s class ExtrasFormatter(logging.Formatter): """Formats extra keywords into %(extras)s placeholder. Any keywords passed to a logging call will be formatted into a "extras" string and included in a logging message. Example: logger.info('my message', extra='keyword') will cause an "extras" string of: [extra: keyword] to be inserted into the format in place of %(extras)s. The optional `keywords` argument must be passed into the init function to enable this functionality. Without it normal daiquiri formatting will be applied. Any keywords included in the `keywords` parameter will not be included in the "extras" string. Special keywords: keywords A set of strings containing keywords to filter out of the "extras" string. extras_template A format string to use instead of '[{0}: {1}]' extras_separator What string to "join" multiple "extras" with. extras_prefix and extras_suffix Strings which will be prepended and appended to the "extras" string respectively. These will only be prepended if the "extras" string is not empty. """ def __init__(self, keywords=None, extras_template='[{0}: {1}]', extras_separator=' ', extras_prefix=' ', extras_suffix='', *args, **kwargs): self.keywords = set() if keywords is None else keywords self.extras_template = extras_template self.extras_separator = extras_separator self.extras_prefix = extras_prefix self.extras_suffix = extras_suffix super(ExtrasFormatter, self).__init__(*args, **kwargs) def add_extras(self, record): if not hasattr(record, '_daiquiri_extra_keys'): record.extras = '' return extras = self.extras_separator.join( self.extras_template.format(k, getattr(record, k)) for k in record._daiquiri_extra_keys if k != '_daiquiri_extra_keys' and k not in self.keywords ) if extras != '': extras = self.extras_prefix + extras + self.extras_suffix record.extras = extras def remove_extras(self, record): del record.extras def format(self, record): self.add_extras(record) s = super(ExtrasFormatter, self).format(record) self.remove_extras(record) return s class ColorExtrasFormatter(ColorFormatter, ExtrasFormatter): """Combines functionality of ColorFormatter and ExtrasFormatter.""" def format(self, record): self.add_color(record) s = ExtrasFormatter.format(self, record) self.remove_color(record) return s TEXT_FORMATTER = ColorExtrasFormatter(fmt=DEFAULT_EXTRAS_FORMAT) if jsonlogger: JSON_FORMATTER = jsonlogger.JsonFormatter() daiquiri-1.6.0/daiquiri/handlers.py000066400000000000000000000070121352233305700173110ustar00rootroot00000000000000# -*- coding: utf-8 -*- # 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. import logging import logging.config import logging.handlers try: from systemd import journal except ImportError: journal = None try: import syslog except ImportError: syslog = None # This is a copy of the numerical constants from syslog.h. The # definition of these goes back at least 20 years, and is specifically # 3 bits in a packed field, so these aren't likely to ever need # changing. SYSLOG_MAP = { "CRITICAL": 2, "ERROR": 3, "WARNING": 4, "WARN": 4, "INFO": 6, "DEBUG": 7, } class SyslogHandler(logging.Handler): """Syslog based handler. Only available on UNIX-like platforms.""" def __init__(self, program_name, facility=None): # Default values always get evaluated, for which reason we avoid # using 'syslog' directly, which may not be available. facility = facility if facility is not None else syslog.LOG_USER if not syslog: raise RuntimeError("Syslog not available on this platform") super(SyslogHandler, self).__init__() syslog.openlog(program_name, 0, facility) def emit(self, record): priority = SYSLOG_MAP.get(record.levelname, 7) message = self.format(record) syslog.syslog(priority, message) class JournalHandler(logging.Handler): """Journald based handler. Only available on platforms using systemd.""" def __init__(self, program_name): if not journal: raise RuntimeError("Systemd bindings do not exist") super(JournalHandler, self).__init__() self.program_name = program_name def emit(self, record): priority = SYSLOG_MAP.get(record.levelname, 7) message = self.format(record) extras = { 'CODE_FILE': record.pathname, 'CODE_LINE': record.lineno, 'CODE_FUNC': record.funcName, 'THREAD_NAME': record.threadName, 'PROCESS_NAME': record.processName, 'LOGGER_NAME': record.name, 'LOGGER_LEVEL': record.levelname, 'SYSLOG_IDENTIFIER': self.program_name, 'PRIORITY': priority } if record.exc_text: extras['EXCEPTION_TEXT'] = record.exc_text if record.exc_info: extras['EXCEPTION_INFO'] = record.exc_info if hasattr(record, "_daiquiri_extra_keys"): for k, v in record._daiquiri_extra_keys: if k != "_daiquiri_extra_keys": extras[k.upper()] = getattr(record, k) journal.send(message, **extras) class TTYDetectorStreamHandler(logging.StreamHandler): """Stream handler that adds a hint in the record if the stream is a TTY.""" def format(self, record): if hasattr(self.stream, "isatty"): record._stream_is_a_tty = self.stream.isatty() else: record._stream_is_a_tty = False s = super(TTYDetectorStreamHandler, self).format(record) del record._stream_is_a_tty return s daiquiri-1.6.0/daiquiri/output.py000066400000000000000000000213261352233305700170550ustar00rootroot00000000000000# 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. import datetime import inspect import logging import logging.handlers import numbers import os import sys try: import syslog except ImportError: syslog = None from daiquiri import formatter from daiquiri import handlers def get_program_name(): return os.path.basename(inspect.stack()[-1][1]) class Output(object): """Generic log output.""" def __init__(self, handler, formatter=formatter.TEXT_FORMATTER, level=None): self.handler = handler self.handler.setFormatter(formatter) if level is not None: self.handler.setLevel(level) def add_to_logger(self, logger): """Add this output to a logger.""" logger.addHandler(self.handler) def _get_log_file_path(logfile=None, logdir=None, program_name=None, logfile_suffix=".log"): ret_path = None if not logdir: ret_path = logfile if not ret_path and logfile and logdir: ret_path = os.path.join(logdir, logfile) if not ret_path and logdir: program_name = program_name or get_program_name() ret_path = os.path.join(logdir, program_name) + logfile_suffix if not ret_path: raise ValueError("Unable to determine log file destination") return ret_path class File(Output): """Ouput to a file.""" def __init__(self, filename=None, directory=None, suffix=".log", program_name=None, formatter=formatter.TEXT_FORMATTER, level=None): """Log file output. :param filename: The log file path to write to. If directory is also specified, both will be combined. :param directory: The log directory to write to. If no filename is specified, the program name and suffix will be used to contruct the full path relative to the directory. :param suffix: The log file name suffix. This will be only used if no filename has been provided. :param program_name: Program name. Autodetected by default. """ logpath = _get_log_file_path(filename, directory, program_name, suffix) handler = logging.handlers.WatchedFileHandler(logpath) super(File, self).__init__(handler, formatter, level) class RotatingFile(Output): """Output to a file, rotating after a certain size.""" def __init__(self, filename=None, directory=None, suffix='.log', program_name=None, formatter=formatter.TEXT_FORMATTER, level=None, max_size_bytes=0, backup_count=0): """Rotating log file output. :param filename: The log file path to write to. If directory is also specified, both will be combined. :param directory: The log directory to write to. If no filename is specified, the program name and suffix will be used to contruct the full path relative to the directory. :param suffix: The log file name suffix. This will be only used if no filename has been provided. :param program_name: Program name. Autodetected by default. :param max_size_bytes: allow the file to rollover at a predetermined size. :param backup_count: the maximum number of files to rotate logging output between. """ logpath = _get_log_file_path(filename, directory, program_name, suffix) handler = logging.handlers.RotatingFileHandler( logpath, maxBytes=max_size_bytes, backupCount=backup_count) super(RotatingFile, self).__init__(handler, formatter, level) def do_rollover(self): """Manually forces a log file rotation.""" return self.handler.doRollover() class TimedRotatingFile(Output): """Rotating log file output, triggered by a fixed interval.""" def __init__(self, filename=None, directory=None, suffix='.log', program_name=None, formatter=formatter.TEXT_FORMATTER, level=None, interval=datetime.timedelta(hours=24), backup_count=0): """Rotating log file output, triggered by a fixed interval. :param filename: The log file path to write to. If directory is also specified, both will be combined. :param directory: The log directory to write to. If no filename is specified, the program name and suffix will be used to contruct the full path relative to the directory. :param suffix: The log file name suffix. This will be only used if no filename has been provided. :param program_name: Program name. Autodetected by default. :param interval: datetime.timedelta instance representing how often a new log file should be created. :param backup_count: the maximum number of files to rotate logging output between. """ logpath = _get_log_file_path(filename, directory, program_name, suffix) handler = logging.handlers.TimedRotatingFileHandler( logpath, when='S', interval=self._timedelta_to_seconds(interval), backupCount=backup_count) super(TimedRotatingFile, self).__init__(handler, formatter, level) def do_rollover(self): """Manually forces a log file rotation.""" return self.handler.doRollover() @staticmethod def _timedelta_to_seconds(td): """Convert a datetime.timedelta object into a seconds interval for rotating file ouput. :param td: datetime.timedelta :return: time in seconds :rtype: int """ if isinstance(td, numbers.Real): td = datetime.timedelta(seconds=td) return td.total_seconds() class Stream(Output): """Generic stream output.""" def __init__(self, stream=sys.stderr, formatter=formatter.TEXT_FORMATTER, level=None): super(Stream, self).__init__(handlers.TTYDetectorStreamHandler(stream), formatter, level) STDERR = Stream() STDOUT = Stream(sys.stdout) class Journal(Output): def __init__(self, program_name=None, formatter=formatter.TEXT_FORMATTER, level=None): program_name = program_name or get_program_name super(Journal, self).__init__(handlers.JournalHandler(program_name), formatter, level) class Syslog(Output): def __init__(self, program_name=None, facility="user", formatter=formatter.TEXT_FORMATTER, level=None): if syslog is None: # FIXME(jd) raise something more specific raise RuntimeError("syslog is not available on this platform") super(Syslog, self).__init__( handlers.SyslogHandler( program_name=program_name or get_program_name(), facility=self._find_facility(facility)), formatter, level) @staticmethod def _find_facility(facility): # NOTE(jd): Check the validity of facilities at run time as they differ # depending on the OS and Python version being used. valid_facilities = [f for f in ["LOG_KERN", "LOG_USER", "LOG_MAIL", "LOG_DAEMON", "LOG_AUTH", "LOG_SYSLOG", "LOG_LPR", "LOG_NEWS", "LOG_UUCP", "LOG_CRON", "LOG_AUTHPRIV", "LOG_FTP", "LOG_LOCAL0", "LOG_LOCAL1", "LOG_LOCAL2", "LOG_LOCAL3", "LOG_LOCAL4", "LOG_LOCAL5", "LOG_LOCAL6", "LOG_LOCAL7"] if getattr(syslog, f, None)] facility = facility.upper() if not facility.startswith("LOG_"): facility = "LOG_" + facility if facility not in valid_facilities: raise TypeError('syslog facility must be one of: %s' % ', '.join("'%s'" % fac for fac in valid_facilities)) return getattr(syslog, facility) preconfigured = { 'stderr': STDERR, 'stdout': STDOUT, } if syslog is not None: preconfigured['syslog'] = Syslog() if handlers.journal is not None: preconfigured['journal'] = Journal() daiquiri-1.6.0/daiquiri/tests/000077500000000000000000000000001352233305700163015ustar00rootroot00000000000000daiquiri-1.6.0/daiquiri/tests/__init__.py000066400000000000000000000000001352233305700204000ustar00rootroot00000000000000daiquiri-1.6.0/daiquiri/tests/test_daiquiri.py000066400000000000000000000072241352233305700215260ustar00rootroot00000000000000# 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. import json import logging import unittest import warnings import six.moves import daiquiri class TestDaiquiri(unittest.TestCase): def tearDown(self): # Be sure to reset the warning capture logging.captureWarnings(False) super(TestDaiquiri, self).tearDown() def test_setup(self): daiquiri.setup() daiquiri.setup(level=logging.DEBUG) daiquiri.setup(program_name="foobar") def test_setup_json_formatter(self): stream = six.moves.StringIO() daiquiri.setup(outputs=( daiquiri.output.Stream( stream, formatter=daiquiri.formatter.JSON_FORMATTER), )) daiquiri.getLogger(__name__).warning("foobar") self.assertEqual({"message": "foobar"}, json.loads(stream.getvalue())) def test_setup_json_formatter_with_extras(self): stream = six.moves.StringIO() daiquiri.setup(outputs=( daiquiri.output.Stream( stream, formatter=daiquiri.formatter.JSON_FORMATTER), )) daiquiri.getLogger(__name__).warning("foobar", foo="bar") self.assertEqual({"message": "foobar", "foo": "bar"}, json.loads(stream.getvalue())) def test_get_logger_set_level(self): logger = daiquiri.getLogger(__name__) logger.setLevel(logging.DEBUG) def test_capture_warnings(self): stream = six.moves.StringIO() daiquiri.setup(outputs=( daiquiri.output.Stream(stream), )) warnings.warn("omg!") line = stream.getvalue() self.assertIn("WARNING py.warnings: ", line) self.assertIn("daiquiri/tests/test_daiquiri.py:62: " "UserWarning: omg!\n warnings.warn(\"omg!\")\n", line) def test_no_capture_warnings(self): stream = six.moves.StringIO() daiquiri.setup(outputs=( daiquiri.output.Stream(stream), ), capture_warnings=False) warnings.warn("omg!") self.assertEqual("", stream.getvalue()) def test_set_default_log_levels(self): daiquiri.set_default_log_levels((("amqp", "debug"), ("urllib3", "warn"))) def test_parse_and_set_default_log_levels(self): daiquiri.parse_and_set_default_log_levels( ("urllib3=warn", "foobar=debug")) def test_string_as_setup_outputs_arg(self): daiquiri.setup(outputs=('stderr', 'stdout')) if daiquiri.handlers.syslog is not None: daiquiri.setup(outputs=('syslog',)) if daiquiri.handlers.journal is not None: daiquiri.setup(outputs=('journal',)) def test_extra_with_two_loggers(): stream = six.moves.StringIO() daiquiri.setup(outputs=( daiquiri.output.Stream(stream), )) log1 = daiquiri.getLogger("foobar") log1.error("argh") log2 = daiquiri.getLogger("foobar", key="value") log2.warning("boo") lines = stream.getvalue().strip().split("\n") assert lines[0].endswith("ERROR foobar: argh") assert lines[1].endswith("WARNING foobar [key: value]: boo") daiquiri-1.6.0/daiquiri/tests/test_formatter.py000066400000000000000000000072741352233305700217270ustar00rootroot00000000000000# 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. import logging import unittest import six import daiquiri class TestColorExtrasFormatter(unittest.TestCase): @classmethod def setUpClass(cls): cls.logger = daiquiri.getLogger('my_module') cls.logger.setLevel(logging.INFO) cls.stream = six.moves.StringIO() cls.handler = daiquiri.handlers.TTYDetectorStreamHandler(cls.stream) cls.logger.logger.addHandler(cls.handler) super(TestColorExtrasFormatter, cls).setUpClass() def setUp(self): # Couldn't get readline() to return anything no matter what I tried, so # getvalue() is the only way to see what's in the stream. However this # requires the stream to be reset every time. self.stream.close() self.stream = six.moves.StringIO() self.handler.stream = self.stream super(TestColorExtrasFormatter, self).setUp() def test_no_keywords(self): format_string = '%(levelname)s %(name)s%(extras)s: %(message)s' formatter = daiquiri.formatter.ColorExtrasFormatter(fmt=format_string) self.handler.setFormatter(formatter) self.logger.info('test message') self.assertEqual(self.stream.getvalue(), 'INFO my_module: test message\n') def test_no_keywords_with_extras(self): format_string = '%(levelname)s %(name)s%(extras)s: %(message)s' formatter = daiquiri.formatter.ColorExtrasFormatter(fmt=format_string) self.handler.setFormatter(formatter) self.logger.info('test message', test="a") self.assertEqual(self.stream.getvalue(), 'INFO my_module [test: a]: test message\n') def test_empty_keywords(self): format_string = '%(levelname)s %(name)s%(extras)s: %(message)s' formatter = daiquiri.formatter.ColorExtrasFormatter(fmt=format_string, keywords=[]) self.handler.setFormatter(formatter) self.logger.info('test message', test="a") self.assertEqual(self.stream.getvalue(), 'INFO my_module [test: a]: test message\n') def test_keywords_no_extras(self): format_string = ('%(levelname)s %(name)s' ' %(test)s%(extras)s: %(message)s') formatter = daiquiri.formatter.ColorExtrasFormatter(fmt=format_string, keywords=["test"]) self.handler.setFormatter(formatter) self.logger.info('test message', test="a") self.assertEqual(self.stream.getvalue(), 'INFO my_module a: test message\n') def test_keywords_with_extras(self): format_string = ('%(levelname)s %(name)s' ' %(test)s%(extras)s: %(message)s') formatter = daiquiri.formatter.ColorExtrasFormatter(fmt=format_string, keywords=["test"]) self.handler.setFormatter(formatter) self.logger.info('test message', test="a", test2="b") self.assertEqual(self.stream.getvalue(), 'INFO my_module a [test2: b]: test message\n') daiquiri-1.6.0/daiquiri/tests/test_output.py000066400000000000000000000055471352233305700212650ustar00rootroot00000000000000# 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. import syslog import unittest from datetime import timedelta from daiquiri import output class TestOutput(unittest.TestCase): def test_find_facility(self): self.assertEqual(syslog.LOG_USER, output.Syslog._find_facility("user")) self.assertEqual(syslog.LOG_LOCAL1, output.Syslog._find_facility("log_local1")) self.assertEqual(syslog.LOG_LOCAL2, output.Syslog._find_facility("LOG_local2")) self.assertEqual(syslog.LOG_LOCAL3, output.Syslog._find_facility("LOG_LOCAL3")) self.assertEqual(syslog.LOG_LOCAL4, output.Syslog._find_facility("LOCaL4")) def test_get_log_file_path(self): self.assertEqual("foobar.log", output._get_log_file_path("foobar.log")) self.assertEqual("/var/log/foo/foobar.log", output._get_log_file_path("foobar.log", logdir="/var/log/foo")) self.assertEqual("/var/log/foobar.log", output._get_log_file_path(logdir="/var/log", program_name="foobar")) self.assertEqual("/var/log/foobar.log", output._get_log_file_path(logdir="/var/log", program_name="foobar")) self.assertEqual("/var/log/foobar.journal", output._get_log_file_path( logdir="/var/log", logfile_suffix=".journal", program_name="foobar")) def test_timedelta_seconds(self): fn = output.TimedRotatingFile._timedelta_to_seconds hour = 60 * 60 # seconds * minutes one_hour = [ timedelta(hours=1), timedelta(minutes=60), timedelta(seconds=hour), hour, float(hour) ] for t in one_hour: self.assertEqual(hour, fn(t)) error_cases = [ 'string', ['some', 'list'], ('some', 'tuple',), ('tuple',), {'dict': 'mapping'} ] for t in error_cases: self.assertRaises(AttributeError, fn, t) daiquiri-1.6.0/doc/000077500000000000000000000000001352233305700140755ustar00rootroot00000000000000daiquiri-1.6.0/doc/source/000077500000000000000000000000001352233305700153755ustar00rootroot00000000000000daiquiri-1.6.0/doc/source/conf.py000066400000000000000000000001161352233305700166720ustar00rootroot00000000000000master_doc = 'index' project = "Daiquiri" extensions = ['sphinx.ext.autodoc'] daiquiri-1.6.0/doc/source/index.rst000066400000000000000000000104701352233305700172400ustar00rootroot00000000000000======================================= daiquiri -- Python logging setup helper ======================================= .. image:: https://circleci.com/gh/jd/daiquiri.svg?style=svg :target: https://circleci.com/gh/jd/daiquiri .. image:: https://img.shields.io/pypi/v/daiquiri.svg :target: https://pypi.python.org/pypi/daiquiri :alt: Latest Version The daiquiri library provides an easy way to configure logging. It also provides some custom formatters and handlers. * Free software: Apache license * Source: https://github.com/jd/daiquiri Installation ============ pip install daiquiri If you want to enable support of JSON output, you must also install the `json` flavor:: pip install daiquiri[json] If you want to enable systemd support, you must install the `systemd` flavor:: pip install daiquiri[systemd] Usage ===== The basic usage of daiquiri is to call the `daiquiri.setup` function that will setup logging with the options passed as keyword arguments. If no arguments are passed, the default will log to `stderr`. If `stderr` is a terminal, the output will use colors. .. literalinclude:: ../../examples/basic.py You can specify different outputs with different formatters. The `daiquiri.output` module provides a collection of `Output` classes that you can use to your liking to configure the logging output. Any number of output can be configured. .. literalinclude:: ../../examples/output.py If the default output configurations suit your needs, then for convenience you may pass the name of an output as a string rather than needing to import the class and produce an instance. .. literalinclude:: ../../examples/stringnames.py At the moment the names `'stderr'`, `'stdout'`, `'syslog'`, and `'journal'` are available, assuming the underlying handler is available. Picking format -------------- You can configure the format of any output by passing a formatter as the `formatter` argument to the contructor. Two default formatters are available: `daiquiri.formatter.TEXT_FORMATTER` which prints log messages as text, and the `daiquiri.formatter.JSON_FORMATTER` which prints log messages as parsable JSON (requires `python-json-logger`). You can provide any class of type `logging.Formatter` as a formatter. .. literalinclude:: ../../examples/formatter.py Python warning support ---------------------- The Python `warnings` module is sometimes used by applications and libraries to emit warnings. By default, they are printed on `stderr`. Daiquiri overrides this by default and log warnings to the `py.warnings` logger. This can be disabled by passing the `capture_warnings=False` argument to `daiquiri.setup`. Extra usage ----------- While it's not mandatory to use `daiquiri.getLogger` to get a logger instead of `logging.getLogger`, it is recommended as daiquiri provides an enhanced version of the logger object. It allows any keyword argument to be passed to the logging method and that will be available as part of the record. .. literalinclude:: ../../examples/extra.py Advanced Extra usage ----------- The enhanced logger object provided by `daiquiri.getLogger` is also capable of supporting keyword arguments to the logging method without the logger itself having been configured to expect those specific keywords. This requires the use of the ExtrasFormatter or the ColorExtrasFormatter classes. The documentation for the ExtrasFormatter specifies the various options you can configure on it. .. literalinclude:: ../../examples/advanced_extra.py Syslog support -------------- The `daiquiri.output.Syslog` output provides syslog output. Systemd journal support ----------------------- The `daiquiri.output.Journal` output provides systemd journal support. All the extra arguments passed to the logger will be shipped as extra keys to the journal. File support ------------ The `daiquiri.output.File` output class provides support to log into a file. `daiquiri.output.RotatingFile` class logs to a file that rotates when a maximum file size has been reached. `daiquiri.output.TimedRotatingFile` will rotate the log file on a fixed interval. .. literalinclude:: ../../examples/files.py API === .. automodule:: daiquiri :members: Output ------ .. automodule:: daiquiri.output :members: Handlers -------- .. automodule:: daiquiri.handlers :members: Formatter --------- .. automodule:: daiquiri.formatter :members: daiquiri-1.6.0/examples/000077500000000000000000000000001352233305700151465ustar00rootroot00000000000000daiquiri-1.6.0/examples/advanced_extra.py000066400000000000000000000010601352233305700204650ustar00rootroot00000000000000import logging import daiquiri import daiquiri.formatter daiquiri.setup(level=logging.INFO, outputs=[ daiquiri.output.Stream(formatter=daiquiri.formatter.ColorExtrasFormatter( fmt=(daiquiri.formatter.DEFAULT_FORMAT + " [%(subsystem)s is %(mood)s]" + "%(extras)s"), keywords=['mood', 'subsystem'], ))]) logger = daiquiri.getLogger(__name__, subsystem="example") logger.info("It works and log to stderr by default with color!", mood="happy", arbitrary_context="included" ) daiquiri-1.6.0/examples/basic.py000066400000000000000000000002541352233305700166020ustar00rootroot00000000000000import logging import daiquiri daiquiri.setup(level=logging.INFO) logger = daiquiri.getLogger(__name__) logger.info("It works and log to stderr by default with color!") daiquiri-1.6.0/examples/extra.py000066400000000000000000000006571352233305700166530ustar00rootroot00000000000000import logging import daiquiri import daiquiri.formatter daiquiri.setup(level=logging.INFO, outputs=( daiquiri.output.Stream(formatter=daiquiri.formatter.ColorFormatter( fmt=(daiquiri.formatter.DEFAULT_FORMAT + " [%(subsystem)s is %(mood)s]"))), )) logger = daiquiri.getLogger(__name__, subsystem="example") logger.info("It works and log to stderr by default with color!", mood="happy") daiquiri-1.6.0/examples/files.py000066400000000000000000000007271352233305700166300ustar00rootroot00000000000000import datetime import logging import daiquiri daiquiri.setup( level=logging.DEBUG, outputs=( daiquiri.output.File('errors.log', level=logging.ERROR), daiquiri.output.TimedRotatingFile( 'everything.log', level=logging.DEBUG, interval=datetime.timedelta(hours=1)) ) ) logger = daiquiri.getLogger(__name__) logger.info('only to rotating file logger') logger.error('both log files, including errors only') daiquiri-1.6.0/examples/formatter.py000066400000000000000000000005561352233305700175310ustar00rootroot00000000000000import logging import daiquiri import daiquiri.formatter daiquiri.setup(level=logging.INFO, outputs=( daiquiri.output.Stream(formatter=daiquiri.formatter.ColorFormatter( fmt="%(asctime)s [PID %(process)d] [%(levelname)s] " "%(name)s -> %(message)s")), )) logger = daiquiri.getLogger(__name__) logger.info("It works with a custom format!") daiquiri-1.6.0/examples/output.py000066400000000000000000000007221352233305700170610ustar00rootroot00000000000000import logging import sys import daiquiri # Log both to stdout and as JSON in a file called /dev/null. (Requires # `python-json-logger`) daiquiri.setup(level=logging.INFO, outputs=( daiquiri.output.Stream(sys.stdout), daiquiri.output.File("/dev/null", formatter=daiquiri.formatter.JSON_FORMATTER), )) logger = daiquiri.getLogger(__name__, subsystem="example") logger.info("It works and log to stdout and /dev/null with JSON") daiquiri-1.6.0/examples/stringnames.py000066400000000000000000000003051352233305700200500ustar00rootroot00000000000000import logging import daiquiri daiquiri.setup(level=logging.INFO, outputs=('stdout', 'stderr')) logger = daiquiri.getLogger(__name__) logger.info("It works and logs to both stdout and stderr!") daiquiri-1.6.0/setup.cfg000066400000000000000000000015611352233305700151540ustar00rootroot00000000000000[metadata] name = daiquiri summary = Library to configure Python logging easily description-file = README.rst author = Julien Danjou author-email = julien@danjou.info home-page = https://github.com/jd/daiquiri classifier = Intended Audience :: Information Technology Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 [files] packages = daiquiri [extras] test = pytest six json = python-json-logger systemd = systemd-python>=234 [build_sphinx] all_files = 1 build-dir = doc/build source-dir = doc/source [wheel] universal = 1 daiquiri-1.6.0/setup.py000066400000000000000000000001171352233305700150410ustar00rootroot00000000000000import setuptools setuptools.setup( setup_requires=['pbr'], pbr=True) daiquiri-1.6.0/tox.ini000066400000000000000000000007341352233305700146470ustar00rootroot00000000000000[tox] envlist = py36,py35,py27,pep8,docs [testenv] whitelist_externals = sh deps = -e.[test,json] commands = pytest daiquiri sh -c "for example in examples/*.py; do python $example; done" sh -c "rm errors.log everything.log" [testenv:pep8] deps = flake8 flake8-import-order commands = flake8 [flake8] show-source = True exclude=.git,.tox,dist,build,.eggs application-import-names=daiquiri [testenv:docs] deps = sphinx commands = python setup.py build_sphinx