Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #76 #78

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
11 changes: 9 additions & 2 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@
History
=======

Current (0.4.0) - (unreleased yet)
-----------------------------------

0.5.0 - (2024-12-13)
--------------------

* made the configuration a little bit more Pythonic
* deprecated `configure_from_dict` and `configure_from_file`

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 <https://docs.datalust.co/docs/posting-raw-events>`_.
Expand Down
4 changes: 3 additions & 1 deletion docs/usage-gunicorn.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Usage (Gunicorn)
================


Using seqlog with `Gunicorn <https://gunicorn.org/>` 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`.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
return str(obj)
34 changes: 33 additions & 1 deletion docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
Usage
=====

Recommended way is to use logging.config.dictConfig().

Configure logging programmatically
----------------------------------

Expand Down Expand Up @@ -45,6 +47,9 @@ The formal definition of the configure function is as follows:
Configure logging from a file
-----------------------------

.. deprecated:: 0.5.0
Use logging.config.dictConfig() directly

Seqlog can also use a YAML-format file to describe the desired logging configuration. This file has the schema specified in Python's `logging.config <https://docs.python.org/3/library/logging.config.html#logging-config-dictschema>`_ module.

First, create your configuration file (e.g. ``/foo/bar/my_config.yml``):
Expand Down Expand Up @@ -78,6 +83,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:
Expand Down Expand Up @@ -112,6 +118,9 @@ Then, call ``seqlog.configure_from_file()``:
Configuring logging from a dictionary
-------------------------------------

.. deprecated:: 0.5.0
Use logging.config.dictConfig() directly

Seqlog can also use a dictionary to describe the desired logging configuration.
This dictionary has the schema specified in Python's `logging.config <https://docs.python.org/3/library/logging.config.html#logging-config-dictschema>`_ module.

Expand All @@ -131,6 +140,29 @@ This dictionary has the schema specified in Python's `logging.config <https://do
another_logger = logging.getLogger('another_logger')
another_logger.info('This is another logger.')

Note that you can pass flags that were previously given to :func:`seqlog.configure_from_dict` directly in the dictionary, eg.


.. code-block:: python

a['handlers']['console'] = {
'class': 'seqlog.structured_logging.ConsoleStructuredLogHandler',
'formatter': 'standard'
'override_root_logger': True
'use_structured_logging': True,
'use_clef': True
}
logging.config.dictConfig(a)

Basically all of the arguments in

.. autoclass:: seqlog.structured_logging.BaseStructuredLogHandler

can be put there.

Note that only first arguments that were previously passed globally will be set. Argument configured once in one logger
tintoy marked this conversation as resolved.
Show resolved Hide resolved
won't pass to another.

Batching and auto-flush
-----------------------

Expand Down Expand Up @@ -197,7 +229,7 @@ If the callable returns None, it won't be added.
Note that some properties get different treatment if the CLEF mode is enabled.

Note that there is a short list of these, these won't be attached to Properties. They will get removed from there and
attached according to the `CLEF<https://clef-json.org/>`_ format:
attached according to the `CLEF <https://clef-json.org/>`_ format:

* ``span_id`` - this will get removed and be replaced with ``@sp``
* ``trace_id`` - this will get removed and be replaced with ``@tr``
Expand Down
71 changes: 11 additions & 60 deletions seqlog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
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
Expand All @@ -16,79 +16,39 @@

__author__ = 'Adam Friedman'
__email__ = '[email protected]'
__version__ = '0.4.0a1'
__version__ = '0.5.0a1'


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()`.
.. deprecated: 0.5.0
Use logging.config.fileConfig(). Also, let it be known that Python uses different format natively (your file cannot be YAML)

: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
Uses `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)

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.

Uses `logging.config.dictConfig()`.

.. deprecated: 0.5.0
Use logging.config.dictConfig() directly.

Note that if you provide None to any of the default arguments, it just won't get changed (ie. it will stay the same).

: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
"""

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:
logging.setLoggerClass(StructuredLogger)

logging.config.dictConfig(config)


Expand Down Expand Up @@ -237,12 +197,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)
14 changes: 9 additions & 5 deletions seqlog/feature_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ class FeatureFlag(Enum):
USE_CLEF = 4 #: Use more modern API to submit log entries


# Here None (despite for if purposes being False) carries additional meaning - that this entry was not yet configured
_features = {
FeatureFlag.EXTRA_PROPERTIES: False,
FeatureFlag.STACK_INFO: False,
FeatureFlag.IGNORE_SEQ_SUBMISSION_ERRORS: False,
FeatureFlag.USE_CLEF: False
FeatureFlag.EXTRA_PROPERTIES: None,
FeatureFlag.STACK_INFO: None,
FeatureFlag.IGNORE_SEQ_SUBMISSION_ERRORS: None,
FeatureFlag.USE_CLEF: None
}


Expand Down Expand Up @@ -60,14 +61,17 @@ def disable_feature(feature: FeatureFlag):
configure_feature(feature, False)


def configure_feature(feature: FeatureFlag, enable: tp.Optional[bool]):
def configure_feature(feature: FeatureFlag, enable: tp.Optional[bool], if_not_yet_configured: bool = False):
"""
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.
:param if_not_yet_configured: configure only if this has not yet been configured
"""
if enable is None:
return
if not if_not_yet_configured and _features[feature] is None:
return
_features[feature] = enable
76 changes: 58 additions & 18 deletions seqlog/structured_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import requests

from seqlog.consumer import QueueConsumer
from seqlog.feature_flags import FeatureFlag, is_feature_enabled
from seqlog.feature_flags import FeatureFlag, is_feature_enabled, configure_feature

# Well-known keyword arguments used by the logging system.
_well_known_logger_kwargs = {"extra", "exc_info", "func", "sinfo"}
Expand Down Expand Up @@ -165,19 +165,55 @@ def getMessage(self):
return self.msg


class StructuredLogger(logging.Logger):
_root_logger_overrided = False


def _override_root_logger():
"""
Custom (dummy) logger that understands named log arguments.
Override the root logger with a `StructuredRootLogger`.
"""
global _root_logger_overrided
if _root_logger_overrided:
return
logging.root = StructuredRootLogger(logging.WARNING)
logging.Logger.root = logging.root
logging.Logger.manager = logging.Manager(logging.Logger.root)
_root_logger_overrided = True

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):
"""
Base structured logger to set up all of the arguments previously required.

:param override_root_logger: whether to override the root logger, default is True
:param support_extra_properties: logger will support named arguments instead of passing them via extra
:param stack_info: attach stack info
:param ignore_seq_submission_errors: whether to ignore submission errors
:param use_clef: whether to use CLEF
"""
def __init__(self, *args, level=logging.NOTSET, override_root_logger=True, **kwargs):
logging.Handler.__init__(self, level)

if override_root_logger:
_override_root_logger()

if kwargs.pop('use_structured_logger', None):
logging.setLoggerClass(StructuredLogger)

if kwargs.pop('support_extra_properties', None):
configure_feature(FeatureFlag.EXTRA_PROPERTIES, True, if_not_yet_configured=True)
if kwargs.pop('stack_info', None):
configure_feature(FeatureFlag.STACK_INFO, True, if_not_yet_configured=True)
if kwargs.pop('ignore_seq_submission_errors', None):
configure_feature(FeatureFlag.IGNORE_SEQ_SUBMISSION_ERRORS, True, if_not_yet_configured=True)
if kwargs.pop('use_clef', None):
configure_feature(FeatureFlag.USE_CLEF, True, if_not_yet_configured=True)


class StructuredLogger(logging.Logger):
"""
Custom (dummy) logger that understands named log arguments.
"""

@property
def _support_extra_properties(self):
Expand Down Expand Up @@ -310,16 +346,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):
Expand All @@ -343,12 +382,13 @@ 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,
**kwargs):
"""
Create a new `SeqLogHandler`.

Expand All @@ -360,7 +400,7 @@ def __init__(self, server_url, api_key=None, batch_size=10, auto_flush_timeout=N
:param json_encoder_class: The custom JSON encoder class (or fully-qualified class name), if any, to use.
"""

super().__init__()
super().__init__(**kwargs)

self.base_server_url = server_url
if not self.base_server_url.endswith("/"):
Expand Down
Loading