diff --git a/HISTORY.rst b/HISTORY.rst index d01524e..60b665b 100755 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,8 +2,18 @@ History ======= -Current (0.4.0) - (unreleased yet) ------------------------------------ + +0.5.0 - (2024-12-20) +-------------------- + +* made the configuration a little bit more Pythonic +* FeatureFlags were removed +* `configure_from_dict` is the official way to configure Seq +* `support_stack_info` was removed from `log_to_console` +* you can choose whether ConsoleStructuredLogHandler will log to stdout or to stderr. + +0.4.0 - (2024-12-8) +-------------------- * You can enable and disable all of the feature flags at runtime * Added support for the `CLEF submission format `_. diff --git a/docs/conf.py b/docs/conf.py index 6473847..f5e0d21 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,6 +54,16 @@ # The master toctree document. master_doc = 'index' +autodoc_default_options = { + +} +autodoc_default_flags = [ + 'show-inheritance' +] +autodoc_typehints = "description" +autoclass_content = 'both' + + # General information about the project. project = u'SeqLog' copyright = u"2016, Adam Friedman" diff --git a/docs/feature_flags.rst b/docs/feature_flags.rst deleted file mode 100644 index 6969e5b..0000000 --- a/docs/feature_flags.rst +++ /dev/null @@ -1,22 +0,0 @@ -Feature flags -------------- - -You can change certain behaviour of the software at runtime, even after you configure Seq. You'll have to use: - -.. autofunction:: seqlog.feature_flags.enable_feature - -.. autofunction:: seqlog.feature_flags.disable_feature - -.. autofunction:: seqlog.feature_flags.configure_feature - -.. autoclass:: seqlog.feature_flags.FeatureFlag - :members: - -An example to disable ignoring of submission errors for Seq failures would look like this: - -.. code-block:: python - - from seqlog.feature_flags import disable_feature, FeatureFlag - - disable_feature(FeatureFlag.IGNORE_SEQ_SUBMISSION_ERRORS) - diff --git a/docs/index.rst b/docs/index.rst index 08a06b9..5146620 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,10 +17,10 @@ Contents: readme installation + migration usage usage-gunicorn Modules - feature_flags contributing authors history diff --git a/docs/migration.rst b/docs/migration.rst new file mode 100644 index 0000000..31579a5 --- /dev/null +++ b/docs/migration.rst @@ -0,0 +1,17 @@ +=============================== +Migration guide from 0.4 to 0.5 +=============================== + +First of all, the official way to configure Seq is via :func:`seqlog.configure_from_dict` + +Alternatively you can call :func:`seqlog.configure_from_file`. + +.. warning:: DO NOT call :code:`logging.config.fromDict` directly. + +Then, FeatureFlags were completely obliterated and moved to SeqLogHandler's constructor. + +Then, the SeqLogHandler accepts way more arguments, that you can define in the dictionary supporting the configuration: + +.. autoclass:: seqlog.structured_logging.SeqLogHandler + + diff --git a/docs/usage-gunicorn.rst b/docs/usage-gunicorn.rst index 33e9f1b..21a6878 100644 --- a/docs/usage-gunicorn.rst +++ b/docs/usage-gunicorn.rst @@ -2,6 +2,7 @@ Usage (Gunicorn) ================ + Using seqlog with `Gunicorn ` involves some additional configuration because of the way Gunicorn uses ``fork`` to create new worker processes. A custom ``JSONEncoder`` is also used to handle objects that are not `JSON serializable`. @@ -118,6 +119,7 @@ A custom ``JSONEncoder`` is also used to handle objects that are not `JSON seria console: class: seqlog.structured_logging.ConsoleStructuredLogHandler formatter: standard + use_stdout: False seq: class: seqlog.structured_logging.SeqLogHandler @@ -148,4 +150,4 @@ A custom ``JSONEncoder`` is also used to handle objects that are not `JSON seria try: return json.JSONEncoder.default(self, obj) except: - return str(obj) \ No newline at end of file + return str(obj) diff --git a/docs/usage.rst b/docs/usage.rst index e602ceb..03c8117 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -2,6 +2,16 @@ Usage ===== +Recommended way is to use + +.. autofunction:: seqlog.configure_from_dict + +or + +.. autofunction:: seqlog.configure_from_file + +This way you can leverage the Python logging configuration syntax. + Configure logging programmatically ---------------------------------- @@ -56,6 +66,8 @@ First, create your configuration file (e.g. ``/foo/bar/my_config.yml``): # Configure logging from scratch. disable_existing_loggers: True + override_root_logger: True + use_structured_logger: True # Configure the root logger to use Seq root: @@ -78,6 +90,7 @@ First, create your configuration file (e.g. ``/foo/bar/my_config.yml``): console: class: seqlog.structured_logging.ConsoleStructuredLogHandler formatter: seq + override_existing_logger: True # Log to Seq seq: @@ -95,7 +108,7 @@ First, create your configuration file (e.g. ``/foo/bar/my_config.yml``): seq: style: '{' -Then, call ``seqlog.configure_from_file()``: +Then, call :func:`seqlog.configure_from_file`: .. code-block:: python @@ -114,6 +127,7 @@ Configuring logging from a dictionary Seqlog can also use a dictionary to describe the desired logging configuration. This dictionary has the schema specified in Python's `logging.config `_ module. +With some extra options described in :func:`seqlog.configure_from_dict`. .. code-block:: python @@ -131,6 +145,7 @@ This dictionary has the schema specified in Python's `logging.config `_ format: +attached according to the `CLEF `_ format: * ``span_id`` - this will get removed and be replaced with ``@sp`` * ``trace_id`` - this will get removed and be replaced with ``@tr`` diff --git a/seqlog/__init__.py b/seqlog/__init__.py index 325f74a..e4213b6 100644 --- a/seqlog/__init__.py +++ b/seqlog/__init__.py @@ -3,10 +3,11 @@ import logging import logging.config import typing +import warnings + import yaml -from seqlog.feature_flags import FeatureFlag, configure_feature -from seqlog.structured_logging import StructuredLogger, StructuredRootLogger +from seqlog.structured_logging import StructuredLogger, StructuredRootLogger, _override_root_logger from seqlog.structured_logging import SeqLogHandler, ConsoleStructuredLogHandler from seqlog.structured_logging import get_global_log_properties as _get_global_log_properties from seqlog.structured_logging import set_global_log_properties as _set_global_log_properties @@ -16,79 +17,37 @@ __author__ = 'Adam Friedman' __email__ = 'tintoy@tintoy.io' -__version__ = '0.4.0a1' +__version__ = '0.5.0' -def configure_from_file(file_name, override_root_logger=True, support_extra_properties=False, support_stack_info=False, ignore_seq_submission_errors=False, - use_clef=False): +def configure_from_file(file_name): """ - Configure Seq logging using YAML-format configuration file. - - Uses `logging.config.dictConfig()`. - - :param file_name: The name of the configuration file to use. - :type file_name: str - :param override_root_logger: Override the root logger to use a Seq-specific implementation? (default: True) - :type override_root_logger: bool - :param support_extra_properties: Support passing of additional properties to log via the `extra` argument? - :type support_extra_properties: bool - :param support_stack_info: Support attaching of stack-trace information (if available) to log records? - :type support_stack_info: bool - :param ignore_seq_submission_errors: Ignore errors encountered while sending log records to Seq? - :type ignore_seq_submission_errors: bool - :param use_clef: use the newer submission format CLEF - :type use_clef: bool + Configure Seq logging using YAML-format configuration file. Essentially loads the YAML, and invokes + :func:`configure_from_dict`. """ - configure_feature(FeatureFlag.EXTRA_PROPERTIES, support_extra_properties) - configure_feature(FeatureFlag.STACK_INFO, support_stack_info) - configure_feature(FeatureFlag.IGNORE_SEQ_SUBMISSION_ERRORS, ignore_seq_submission_errors) - configure_feature(FeatureFlag.USE_CLEF, use_clef) - with open(file_name) as config_file: config = yaml.load(config_file, Loader=yaml.SafeLoader) - configure_from_dict(config, override_root_logger, True) + configure_from_dict(config) -def configure_from_dict(config, override_root_logger=True, use_structured_logger=True, support_extra_properties=None, - support_stack_info=None, ignore_seq_submission_errors=None, - use_clef=None): +def configure_from_dict(config): """ - Configure Seq logging using a dictionary. + Configure Seq logging using a dictionary. Use it instead of logging.config.dictConfig(). - Uses `logging.config.dictConfig()`. + Extra parameters you can specify (as dictionary keys). - Note that if you provide None to any of the default arguments, it just won't get changed (ie. it will stay the same). + * `use_structured_logger` - this will configure Python logging environment to use a StructuredLogger, ie. one that + understands keyword arguments + * `override_root_logger` - overrides root logger with a StructuredLogger - :param config: A dict containing the configuration. - :type config: dict - :param override_root_logger: Override the root logger to use a Seq-specific implementation? (default: True) - :type override_root_logger: bool - :param use_structured_logger: Configure the default logger class to be StructuredLogger, which support named format arguments? (default: True) - :type use_structured_logger: bool - :param support_extra_properties: Support passing of additional properties to log via the `extra` argument? - :type support_extra_properties: bool - :param support_stack_info: Support attaching of stack-trace information (if available) to log records? - :type support_stack_info: bool - :param ignore_seq_submission_errors: Ignore errors encountered while sending log records to Seq? - :type ignore_seq_submission_errors: bool - :param use_clef: use the newer submission format CLEF - :type use_clef: bool + The rest will be passed onto logging.config.dictConfig() """ - - configure_feature(FeatureFlag.EXTRA_PROPERTIES, support_extra_properties) - configure_feature(FeatureFlag.STACK_INFO, support_stack_info) - configure_feature(FeatureFlag.IGNORE_SEQ_SUBMISSION_ERRORS, ignore_seq_submission_errors) - configure_feature(FeatureFlag.USE_CLEF, use_clef) - - if override_root_logger: - _override_root_logger() - - # Must use StructuredLogger to support named format argments. - if use_structured_logger: + if config.pop('use_structured_logger', False): logging.setLoggerClass(StructuredLogger) - + if config.pop('override_root_logger', False): + _override_root_logger() logging.config.dictConfig(config) @@ -126,19 +85,15 @@ def log_to_seq(server_url, api_key=None, level=logging.WARNING, :return: The `SeqLogHandler` that sends events to Seq. Can be used to forcibly flush records to Seq. :rtype: SeqLogHandler """ - - configure_feature(FeatureFlag.EXTRA_PROPERTIES, support_extra_properties) - configure_feature(FeatureFlag.STACK_INFO, support_stack_info) - configure_feature(FeatureFlag.IGNORE_SEQ_SUBMISSION_ERRORS, ignore_seq_submission_errors) - configure_feature(FeatureFlag.USE_CLEF, use_clef) - logging.setLoggerClass(StructuredLogger) if override_root_logger: _override_root_logger() log_handlers = [ - SeqLogHandler(server_url, api_key, batch_size, auto_flush_timeout, json_encoder_class) + SeqLogHandler(server_url, api_key, batch_size, auto_flush_timeout, json_encoder_class, + support_extra_properties=support_extra_properties, support_stack_info=support_stack_info, + use_clef=use_clef, ignore_seq_submission_errors=ignore_seq_submission_errors) ] if additional_handlers: @@ -155,7 +110,7 @@ def log_to_seq(server_url, api_key=None, level=logging.WARNING, return log_handlers[0] -def log_to_console(level=logging.WARNING, override_root_logger=False, support_extra_properties=False, support_stack_info=False, **kwargs): +def log_to_console(level=logging.WARNING, override_root_logger=False, support_extra_properties=False, **kwargs): """ Configure the logging system to send log entries to the console. @@ -167,13 +122,8 @@ def log_to_console(level=logging.WARNING, override_root_logger=False, support_ex when using the logging.XXX functions. :param support_extra_properties: Support passing of additional properties to log via the `extra` argument? :type support_extra_properties: bool - :param support_stack_info: Support attaching of stack-trace information (if available) to log records? - :type support_stack_info: bool """ - configure_feature(FeatureFlag.EXTRA_PROPERTIES, support_extra_properties) - configure_feature(FeatureFlag.STACK_INFO, support_stack_info) - logging.setLoggerClass(StructuredLogger) if override_root_logger: @@ -182,7 +132,7 @@ def log_to_console(level=logging.WARNING, override_root_logger=False, support_ex logging.basicConfig( style='{', handlers=[ - ConsoleStructuredLogHandler() + ConsoleStructuredLogHandler(support_extra_properties=support_extra_properties) ], level=level, **kwargs @@ -237,12 +187,3 @@ def clear_global_log_properties(): _clear_global_log_properties() - -def _override_root_logger(): - """ - Override the root logger with a `StructuredRootLogger`. - """ - - logging.root = StructuredRootLogger(logging.WARNING) - logging.Logger.root = logging.root - logging.Logger.manager = logging.Manager(logging.Logger.root) diff --git a/seqlog/feature_flags.py b/seqlog/feature_flags.py deleted file mode 100644 index e07837f..0000000 --- a/seqlog/feature_flags.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -import typing as tp -from enum import Enum - - -class FeatureFlag(Enum): - """ - Well-known feature flags. - """ - - EXTRA_PROPERTIES = 1 #: Support passing of additional properties to log via the `extra` argument? - - STACK_INFO = 2 #: Support attaching of stack-trace information (if available) to log records? - - IGNORE_SEQ_SUBMISSION_ERRORS = 3 #: Ignore errors encountered while sending log records to Seq? - - USE_CLEF = 4 #: Use more modern API to submit log entries - - -_features = { - FeatureFlag.EXTRA_PROPERTIES: False, - FeatureFlag.STACK_INFO: False, - FeatureFlag.IGNORE_SEQ_SUBMISSION_ERRORS: False, - FeatureFlag.USE_CLEF: False -} - - -def is_feature_enabled(feature: FeatureFlag): - """ - Is a feature enabled? - - :param feature: A `FeatureFlag` value representing the feature. - :type feature: FeatureFlag - :return: `True`, if the feature is enabled; otherwise, `False`. - :rtype: bool - """ - - return _features.get(feature, False) - - -def enable_feature(feature: FeatureFlag): - """ - Enable a feature. - - :param feature: A `FeatureFlag` value representing the feature to enable. - :type feature: FeatureFlag - """ - - configure_feature(feature, True) - - -def disable_feature(feature: FeatureFlag): - """ - Disable a feature. - - :param feature: A `FeatureFlag` value representing the feature to disable. - :type feature: FeatureFlag - """ - - configure_feature(feature, False) - - -def configure_feature(feature: FeatureFlag, enable: tp.Optional[bool]): - """ - Enable or disable a feature. - - :param feature: A `FeatureFlag` value representing the feature to configure. If you pass None, it won't get changed. - :type feature: FeatureFlag - :param enable: `True`, to enable the feature; `False` to disable it. - """ - if enable is None: - return - _features[feature] = enable diff --git a/seqlog/structured_logging.py b/seqlog/structured_logging.py index c6596ac..e532b98 100644 --- a/seqlog/structured_logging.py +++ b/seqlog/structured_logging.py @@ -17,7 +17,6 @@ import requests from seqlog.consumer import QueueConsumer -from seqlog.feature_flags import FeatureFlag, is_feature_enabled # Well-known keyword arguments used by the logging system. _well_known_logger_kwargs = {"extra", "exc_info", "func", "sinfo"} @@ -165,23 +164,25 @@ def getMessage(self): return self.msg -class StructuredLogger(logging.Logger): +def _override_root_logger(): """ - Custom (dummy) logger that understands named log arguments. + Override the root logger with a `StructuredRootLogger`. """ + logging.root = StructuredRootLogger(logging.WARNING) + logging.Logger.root = logging.root + logging.Logger.manager = logging.Manager(logging.Logger.root) - def __init__(self, name, level=logging.NOTSET): - """ - Create a new StructuredLogger - :param name: The logger name. - :param level: The logger minimum level (severity). - """ - super().__init__(name, level) +class BaseStructuredLogHandler(logging.Handler): + def __init__(self, level=logging.NOTSET, *args, support_extra_properties=False, **kwargs): + logging.Handler.__init__(self, level=level) + self.support_extra_properties = support_extra_properties + - @property - def _support_extra_properties(self): - return is_feature_enabled(FeatureFlag.EXTRA_PROPERTIES) +class StructuredLogger(logging.Logger): + """ + Custom (dummy) logger that understands named log arguments. + """ def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False, **kwargs): """ @@ -190,7 +191,7 @@ def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False, ** :param msg: The log message or message template. :param args: Ordinal arguments for the message format template. :param exc_info: Exception information to be included in the log entry. - :param extra: Extra information to be included in the log entry. + :param extra: Extra information to be included in the log entry. Note that this will take precedence over kwargs. :param stack_info: Include stack-trace information in the log entry? :param kwargs: Keyword arguments (if any) passed to the public logger method that called _log. """ @@ -211,7 +212,7 @@ def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False, ** log_props[prop] = kwargs[prop] - if extra and self._support_extra_properties: + if extra: for extra_prop in extra.keys(): log_props['Extra_' + extra_prop] = extra[extra_prop] @@ -310,16 +311,19 @@ def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra return super().makeRecord(name, level, fn, lno, msg, args, exc_info, func, extra, sinfo) -class ConsoleStructuredLogHandler(logging.Handler): - def __init__(self): - super().__init__() +class ConsoleStructuredLogHandler(BaseStructuredLogHandler): + + def __init__(self, *args, use_stdout=True, **kwargs): + super().__init__(*args, **kwargs) + self.use_stdout = use_stdout def emit(self, record): msg = self.format(record) - print(msg) + out = sys.stdout if self.use_stdout else sys.stderr + out.write(msg) if hasattr(record, 'kwargs'): - print("\tLog entry properties: {}".format(repr(record.kwargs))) + out.write("\tLog entry properties: {}\n".format(repr(record.kwargs))) def best_effort_json_encode(arg): @@ -343,12 +347,14 @@ def best_effort_json_encode(arg): return arg -class SeqLogHandler(logging.Handler): +class SeqLogHandler(BaseStructuredLogHandler): """ Log handler that posts to Seq. """ - def __init__(self, server_url, api_key=None, batch_size=10, auto_flush_timeout=None, json_encoder_class=None): + def __init__(self, server_url, api_key=None, batch_size=10, auto_flush_timeout=None, json_encoder_class=None, + use_clef: bool = False, ignore_seq_submission_errors: bool = False, support_stack_info: bool = False, + support_extra_properties: bool = False, **kwargs): """ Create a new `SeqLogHandler`. @@ -358,14 +364,26 @@ def __init__(self, server_url, api_key=None, batch_size=10, auto_flush_timeout=N :param auto_flush_timeout: If specified, the time (in seconds) before the current batch is automatically flushed. :param json_encoder_class: The custom JSON encoder class (or fully-qualified class name), if any, to use. + :param use_clef: whether to use the CLEF format + :param ignore_seq_submission_errors: whether to ignore failures to send logs to Seq + :param support_stack_info: whether to recognize stack_info as an alternative to exc_info (with some limitations) + :param support_extra_properties: whether to natively recognize kwargs (if True) or supply them as extra + (if False). """ - super().__init__() + super().__init__(support_extra_properties=support_extra_properties, **kwargs) - self.base_server_url = server_url - if not self.base_server_url.endswith("/"): - self.base_server_url += "/" + server_url = server_url + if not server_url.endswith("/"): + server_url += "/" + if use_clef: + self.server_url = server_url + 'ingest/clef' + else: + self.server_url = server_url + 'api/events/raw' + + self.support_stack_info = support_stack_info + self.ignore_seq_submission_errors = ignore_seq_submission_errors self.session = requests.Session() if api_key: @@ -373,7 +391,7 @@ def __init__(self, server_url, api_key=None, batch_size=10, auto_flush_timeout=N json_encoder_class = json_encoder_class or json.encoder.JSONEncoder self.json_encoder_class = _ensure_class(json_encoder_class, compatible_class=json.encoder.JSONEncoder) - + self.use_clef = use_clef self.log_queue = Queue() self.consumer = QueueConsumer( name="SeqLogHandler", @@ -384,24 +402,6 @@ def __init__(self, server_url, api_key=None, batch_size=10, auto_flush_timeout=N ) self.consumer.start() - @property - def server_url(self): - if self._use_clef: - return self.base_server_url + 'ingest/clef' - return self.base_server_url + 'api/events/raw' - - @property - def _use_clef(self): - return is_feature_enabled(FeatureFlag.USE_CLEF) - - @property - def _support_stack_info(self): - return is_feature_enabled(FeatureFlag.STACK_INFO) - - @property - def _ignore_seq_submission_errors(self): - return is_feature_enabled(FeatureFlag.IGNORE_SEQ_SUBMISSION_ERRORS) - def flush(self): try: self.consumer.flush() @@ -456,7 +456,7 @@ def publish_log_batch(self, batch): # type: (tp.List[StructuredLogRecord]) - continue processed_records.append(resp) - if self._use_clef: + if self.use_clef: request_body_json = '\r\n'.join(processed_records) else: request_body_json = '{"Events": [%s]}' % (','.join(processed_records), ) @@ -467,12 +467,12 @@ def publish_log_batch(self, batch): # type: (tp.List[StructuredLogRecord]) - response = self.session.post( self.server_url, data=request_body_json, - headers={'Content-Type': "application/vnd.serilog.clef" if self._use_clef else 'application/json'}, + headers={'Content-Type': "application/vnd.serilog.clef" if self.use_clef else 'application/json'}, stream=True # prevent '362' ) response.raise_for_status() except requests.RequestException as requestFailed: - if not self._ignore_seq_submission_errors: + if not self.ignore_seq_submission_errors: # Only notify for the first record in the batch, or we'll be generating too much noise. self.handleError(batch[0]) @@ -504,7 +504,7 @@ def handleError(self, record: StructuredLogRecord): _callback_on_failure(exception) def _build_event_data(self, record): - if self._use_clef: + if self.use_clef: return self._build_event_data_clef(record) else: return self._build_event_data_ingest(record) @@ -548,12 +548,12 @@ def _build_event_data_ingest(self, record): if record.exc_text: # Rendered exception has already been cached event_data["Exception"] = record.exc_text - elif self._support_stack_info and record.stack_info and not record.exc_info: + elif self.support_stack_info and record.stack_info and not record.exc_info: # Feature flag is set: fall back to stack_info (sinfo) if exc_info is not present event_data["Exception"] = record.stack_info elif isinstance(record.exc_info, tuple): # Exception info is present - if record.exc_info[0] is None and self._support_stack_info and record.stack_info: + if record.exc_info[0] is None and self.support_stack_info and record.stack_info: event_data["Exception"] = "{0}--NoException\n{1}".format(logging.getLevelName(record.levelno), record.stack_info) else: event_data["Exception"] = record.exc_text = self.formatter.formatException(record.exc_info) @@ -611,12 +611,12 @@ def _build_event_data_clef(self, record): if record.exc_text: # Rendered exception has already been cached event_data["@x"] = record.exc_text - elif self._support_stack_info and record.stack_info and not record.exc_info: + elif self.support_stack_info and record.stack_info and not record.exc_info: # Feature flag is set: fall back to stack_info (sinfo) if exc_info is not present event_data["@x"] = record.stack_info elif isinstance(record.exc_info, tuple): # Exception info is present - if record.exc_info[0] is None and self._support_stack_info and record.stack_info: + if record.exc_info[0] is None and self.support_stack_info and record.stack_info: event_data["@x"] = "{0}--NoException\n{1}".format(logging.getLevelName(record.levelno), record.stack_info) else: event_data["@x"] = record.exc_text = self.formatter.formatException(record.exc_info) diff --git a/setup.py b/setup.py index aa41972..6d1ae8c 100644 --- a/setup.py +++ b/setup.py @@ -1,65 +1,64 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from setuptools import setup - -with open('README.rst') as readme_file: - readme = readme_file.read() - -with open('HISTORY.rst') as history_file: - history = history_file.read() - -requirements = [ - 'python_dateutil>=2.5.3', - 'requests>=2.10.0', - 'PyYAML>=3.11', -] - -test_requirements = [ - 'pip>=8.1.2', - 'bumpversion>=0.5.3', - 'wheel>=0.29.0', - 'watchdog>=0.8.3', - 'flake8>=2.6.0', - 'tox>=2.3.1', - 'coverage>=4.1', - 'Sphinx>=1.4.4', - 'cryptography==42.0.4', - 'PyYAML>=3.11', - 'pytest>=2.9.2', - 'httmock>=1.2.5' -] - -setup( - name='seqlog', - version='0.5.0', - description="SeqLog enables logging from Python to Seq.", - long_description=readme + '\n\n' + history, - author="Adam Friedman", - author_email='tintoy@tintoy.io', - url='https://github.com/tintoy/seqlog', - packages=[ - 'seqlog', - ], - package_dir={'seqlog': - 'seqlog'}, - include_package_data=True, - install_requires=requirements, - license="MIT license", - zip_safe=False, - keywords='seqlog', - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Natural Language :: English', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - ], - test_suite='tests', - tests_require=test_requirements -) - +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from setuptools import setup + +with open('README.rst') as readme_file: + readme = readme_file.read() + +with open('HISTORY.rst') as history_file: + history = history_file.read() + +requirements = [ + 'python_dateutil>=2.5.3', + 'requests>=2.10.0', + 'PyYAML>=3.11', +] + +test_requirements = [ + 'pip>=8.1.2', + 'bumpversion>=0.5.3', + 'wheel>=0.29.0', + 'watchdog>=0.8.3', + 'flake8>=2.6.0', + 'tox>=2.3.1', + 'coverage>=4.1', + 'Sphinx>=1.4.4', + 'cryptography==42.0.4', + 'PyYAML>=3.11', + 'pytest>=2.9.2', + 'httmock>=1.2.5' +] + +setup( + name='seqlog', + version='0.5.0', + description="SeqLog enables logging from Python to Seq.", + long_description=readme + '\n\n' + history, + author="Adam Friedman", + author_email='tintoy@tintoy.io', + url='https://github.com/tintoy/seqlog', + packages=[ + 'seqlog', + ], + package_dir={'seqlog': + 'seqlog'}, + include_package_data=True, + install_requires=requirements, + license="MIT license", + zip_safe=False, + keywords='seqlog', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + ], + test_suite='tests', + tests_require=test_requirements +) diff --git a/tests/stubs.py b/tests/stubs.py index a223958..28681b4 100644 --- a/tests/stubs.py +++ b/tests/stubs.py @@ -1,9 +1,11 @@ import logging +from seqlog.structured_logging import BaseStructuredLogHandler -class StubStructuredLogHandler(logging.Handler): - def __init__(self): - super().__init__() + +class StubStructuredLogHandler(BaseStructuredLogHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.records = [] self.messages = [] diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 7200ead..09ef81f 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -7,46 +7,66 @@ Tests for `seqlog.config_from_*` module. """ +import logging.config import os import yaml -from seqlog import configure_from_file, configure_from_dict +from seqlog import configure_from_file +from tests.test_structured_logger import create_logger -CFG_CONTENT = """# This is the Python logging schema version (currently, only the value 1 is supported here). +CFG_CONTENT = """ +# This is the Python logging schema version (currently, only the value 1 is supported here). +--- version: 1 # Configure logging from scratch. disable_existing_loggers: True # Configure the root logger to use Seq +override_root_logger: True +use_structured_logger: True root: level: 'DEBUG' handlers: - console - - seq handlers: console: class: seqlog.structured_logging.ConsoleStructuredLogHandler formatter: standard - seq: - class: seqlog.structured_logging.SeqLogHandler - formatter: standard - server_url: 'http://localhost' formatters: standard: format: '[%(levelname)s] %(asctime)s %(name)s: %(message)s' """ + class TestConfiguration(object): - def test_valid_config(self): + def test_valid_config_file(self): + try: - with open('test', 'w', encoding='utf-8') as w_out: + with open('test.yaml', 'w', encoding='utf-8') as w_out: w_out.write(CFG_CONTENT) - configure_from_file('test') - with open('test', 'r', encoding='utf-8') as r_in: + configure_from_file('test.yaml') + + with open('test.yaml', 'r', encoding='utf-8') as r_in: + dct = yaml.load(r_in, Loader=yaml.Loader) + logging.config.dictConfig(dct) + finally: + os.unlink('test.yaml') + logger, handler = create_logger() + logger.warning('This is a {message}', message='message') + assert handler.records[0].getMessage() == 'This is a message' + + def test_valid_config_dict(self): + try: + with open('test.yaml', 'w', encoding='utf-8') as w_out: + w_out.write(CFG_CONTENT) + with open('test.yaml', 'r', encoding='utf-8') as r_in: dct = yaml.load(r_in, Loader=yaml.Loader) - configure_from_dict(dct) + logging.config.dictConfig(dct) finally: - os.unlink('test') + os.unlink('test.yaml') + logger, handler = create_logger() + logger.warning('This is a {message}', message='message') + assert handler.records[0].getMessage() == 'This is a message' diff --git a/tests/test_dict_config.py b/tests/test_dict_config.py deleted file mode 100644 index 1455172..0000000 --- a/tests/test_dict_config.py +++ /dev/null @@ -1,68 +0,0 @@ -from logging import LogRecord -import seqlog -import tests.stubs -from seqlog.structured_logging import StructuredLogger - - -class DictConfigLogger(StructuredLogger): - def __init__(self, name, level=seqlog.logging.NOTSET): - super().__init__(name, level) - - def callHandlers(self, record: LogRecord) -> None: - super().callHandlers(record) - popped = False - for handler in self.handlers: - if isinstance(handler, tests.stubs.StubStructuredLogHandler): - handler.pop_record() - popped = True - if not popped: - raise Exception("Could not pop record from handler! No handler instance found for class 'StubStructuredLogHandler'.!") - - -TEST_CONFIG = { - "version": 1, - "loggers": { - "test_logger": { - "level": seqlog.logging.INFO, - "handlers": ['test_handler'], - "propagate": False, - # "parent": "root", - # "class": TestDictConfigLogger - } - }, - "handlers": { - "test_handler": { - "class": 'tests.stubs.StubStructuredLogHandler', - "formatter": "test_formatter" - } - }, - "formatters": { - "test_formatter": { - "style": '{', - "validate": True, - "format": "[{levelname}] {asctime}: {name} (<{module}:{funcName}> {filename}:{lineno}) {msg}" - } - } -} - - -class TestDictConfig(): - def test_logger_with_dict_config(self): - global TEST_CONFIG - test_levels = { - "critical": seqlog.logging.CRITICAL, - "error": seqlog.logging.ERROR, - "warning": seqlog.logging.WARNING, - "info": seqlog.logging.INFO, - "debug": seqlog.logging.DEBUG, - "notset": seqlog.logging.NOTSET, - } - seqlog.logging.setLoggerClass(DictConfigLogger) - - for level in test_levels.keys(): - TEST_CONFIG['loggers']['test_logger']['level'] = test_levels.get(level) - seqlog.configure_from_dict(TEST_CONFIG) - - for log_level in test_levels: - test_logger = seqlog.logging.getLogger('test_logger') - test_logger.log(msg=f"This is a {log_level} log sent with {level} set.", level=test_levels.get(level)) diff --git a/tests/test_structured_logger.py b/tests/test_structured_logger.py index 6843707..0fd9b00 100644 --- a/tests/test_structured_logger.py +++ b/tests/test_structured_logger.py @@ -12,8 +12,7 @@ import tests.assertions as expect -from seqlog import clear_global_log_properties -from seqlog.structured_logging import StructuredLogger +from seqlog.structured_logging import StructuredLogger, clear_global_log_properties from tests.stubs import StubStructuredLogHandler