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

Follow upon cloudwatch formatters #138

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ configuration provided by ``boto3``:
disable_existing_loggers: False
formatters:
json:
(): watchtower.CloudWatchJSONFormatter
fields: [msg, levelname]
format: "[%(asctime)s] %(process)d %(levelname)s %(name)s:%(funcName)s:%(lineno)s - %(message)s"
plaintext:
format: "[%(asctime)s] %(process)d %(levelname)s %(name)s:%(funcName)s:%(lineno)s - %(message)s"
Expand Down
77 changes: 75 additions & 2 deletions watchtower/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,59 @@ class WatchtowerWarning(UserWarning):
pass


class CloudWatchFormatter(logging.Formatter):
"""
Log formatter for CloudWatch message. Transforms the logged record message into a compatible message for CloudWatch.
This is the default formatter for CloudWatchLogHandler

:param json_serialize_default:
The 'default' function to use when serializing dictionaries as JSON. Refer to the Python standard library
documentation on 'json' for more explanation about the 'default' parameter.
https://docs.python.org/3/library/json.html#json.dump
https://docs.python.org/2/library/json.html#json.dump
:type json_serialize_default: Function
"""

def __init__(self, *args, json_serialize_default=None, **kwargs):
super().__init__(*args, **kwargs)
self.json_serialize_default = json_serialize_default or _json_serialize_default

def format(self, message):
if isinstance(message.msg, Mapping):
message.msg = json.dumps(message.msg, default=self.json_serialize_default)

return super().format(message)


class CloudWatchJSONFormatter(CloudWatchFormatter):
"""
JSON log formatter for CloudWatch. Transforms the logged record message into a JSON formatted message.

:param json_serialize_default:
The 'default' function to use when serializing dictionaries as JSON. Refer to the Python standard library
documentation on 'json' for more explanation about the 'default' parameter.
https://docs.python.org/3/library/json.html#json.dump
https://docs.python.org/2/library/json.html#json.dump
:type json_serialize_default: Function
:param fields: A list of fields of the record to include in the CloudWatch Log json object. Defaults to '__all__'.
:type fields: list
"""
def __init__(self, *args, fields='__all__', **kwargs):
super().__init__(*args, **kwargs)
self.fields = fields

def format(self, message):
if self.fields == '__all__':
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the magic value "all" and use None as the placeholder for keeping all field values instead.

message.msg = dict(message.__dict__)
else:
message.msg = {k: v for k, v in message.items() if k in self.fields}

return super().format(message)


_default_formatter = CloudWatchFormatter()


class CloudWatchLogHandler(logging.Handler):
"""
Create a new CloudWatch log handler object. This is the main entry point to the functionality of the module. See
Expand Down Expand Up @@ -91,6 +144,8 @@ class CloudWatchLogHandler(logging.Handler):
Create CloudWatch Logs log stream if it does not exist. **True** by default.
:type create_log_stream: Boolean
:param json_serialize_default:
**DEPRECATED**: use CloudWatchFormatter for JSON formatting instead.

The 'default' function to use when serializing dictionaries as JSON. Refer to the Python standard library
documentation on 'json' for more explanation about the 'default' parameter.
https://docs.python.org/3/library/json.html#json.dump
Expand Down Expand Up @@ -131,7 +186,6 @@ def __init__(self, log_group=__name__, stream_name=None, use_queues=True, send_i
self.stream_name = stream_name
self.use_queues = use_queues
self.send_interval = send_interval
self.json_serialize_default = json_serialize_default or _json_serialize_default
self.max_batch_size = max_batch_size
self.max_batch_count = max_batch_count
self.max_message_size = max_message_size
Expand All @@ -140,6 +194,12 @@ def __init__(self, log_group=__name__, stream_name=None, use_queues=True, send_i
self.creating_log_stream, self.shutting_down = False, False
self.create_log_stream = create_log_stream
self.log_group_retention_days = log_group_retention_days
self.json_serialize_default = json_serialize_default
if json_serialize_default:
warnings.warn(
'Specifying json_serialize_default is deprecated, please create a CloudWatchFormatter instance '
'which accepts a json_serialize_default and set it as a formatter instead',
DeprecationWarning)

# Creating session should be the final call in __init__, after all instance attributes are set.
# This ensures that failing to create the session will not result in any missing attribtues.
Expand Down Expand Up @@ -208,6 +268,19 @@ def _submit_batch(self, batch, stream_name, max_retries=5):
# from the response
self.sequence_tokens[stream_name] = response["nextSequenceToken"]

def format(self, record):
"""
Format the specified record.

If a formatter is set, use it. Otherwise, use the default formatter for the module. This differs from
`logging.Handler.format` as its default is `CloudWatchFormatter`.
"""
if self.formatter:
fmt = self.formatter
else:
fmt = _default_formatter
return fmt.format(record)

def emit(self, message):
if self.creating_log_stream:
return # Avoid infinite recursion when asked to log a message as our own side effect
Expand All @@ -219,7 +292,7 @@ def emit(self, message):
if stream_name not in self.sequence_tokens:
self.sequence_tokens[stream_name] = None

if isinstance(message.msg, Mapping):
if self.json_serialize_default and isinstance(message.msg, Mapping):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dumping JSON will only happen once so it is fine to leave this here instead of removing it.

message.msg = json.dumps(message.msg, default=self.json_serialize_default)

cwl_message = dict(timestamp=int(message.created * 1000), message=self.format(message))
Expand Down