Skip to content
This repository has been archived by the owner on Sep 26, 2022. It is now read-only.

Refactor logs using structlog #168

Merged
merged 22 commits into from
Jul 13, 2020
Merged

Conversation

bigspider
Copy link
Collaborator

@bigspider bigspider commented Jul 6, 2020

By using structlog, it's easier to keep the logs structured and decide how to render them for different purposes.

This PR rewrites the logging module to use structlog, keeping them for now as plaintext, with the following settings:

  • the logs to console is at INFO level
  • the logs to file is at DEBUG level

Also added a "daemon" parameter, that suppresses output to console. It's not a proper daemon support, as the shell is not released; that's not trivial to do and is left for separate work once we figure out the best way to do it.

This should make it relatively easy to integrate any log aggregator, like the kibana dashboard after #148 is merged.

@bigspider bigspider marked this pull request as ready for review July 7, 2020 12:52
@bigspider bigspider requested a review from sr-gi July 7, 2020 13:07
This was referenced Jul 7, 2020
Copy link
Member

@sr-gi sr-gi left a comment

Choose a reason for hiding this comment

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

General comments:

  • Tests for Logger are missing. I think they can be created now.

  • Comment for all classes:

I'm thinking this could actually be part of the object __init__ now, and we can avoid hardcoded strings by using name, e.g:

class Inspector:
    def __init__:
        [...]
        self.logger = get_logger(actor=Inspector.__name__)
  • We may want to change actor, since I don't think no-one ever liked that denomination. What about module or component?

common/logger.py Show resolved Hide resolved
common/logger.py Outdated
Args:
log_name_prefix (:obj:`str`): the prefix of the logger where the data will be stored in (server, client, ...).
actor (:obj:`str`): the system actor that is logging the event (e.g. ``Watcher``, ``Cryptographer``, ...).
Render ``event_dict``. It renders the timestamp, followed by the actor within "[]" (unless it's None),
Copy link
Member

Choose a reason for hiding this comment

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

nit: event_dict (missing `` )

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

common/logger.py Outdated
return msg if self.actor is None else "[{}]: {}".format(self.actor, msg)
def _repr(self, val):
"""
Determine representation of *val* depending on its type.
Copy link
Member

Choose a reason for hiding this comment

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

nit: For docstrings, if it is a one-liner, it should be expressed:

""" <docstring> """ (same line, no full stop)

The return is also missing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changed to a one-line description, and also made the code more compact with a ternary if/else.

common/logger.py Outdated
log_name_prefix (:obj:`str`): the prefix of the logger where the data will be stored in (server, client, ...).
actor (:obj:`str`): the system actor that is logging the event (e.g. ``Watcher``, ``Cryptographer``, ...).
Render ``event_dict``. It renders the timestamp, followed by the actor within "[]" (unless it's None),
followed by the event, then any remaining item in event_dict in the key=value format
Copy link
Member

Choose a reason for hiding this comment

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

docstrings: full-stop.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added.

msg (:obj:`str`): the message to be logged.
kwargs (:obj:`dict`): a ``key:value`` collection parameters to be added to the output.
"""
"""
Copy link
Member

Choose a reason for hiding this comment

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

docstring: Unnecessary empty line at the end

Copy link
Member

@sr-gi sr-gi Jul 9, 2020

Choose a reason for hiding this comment

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

Also some nits: space between parameter and type (param (:obj: ...))

No need of parenthesis fort the Return and Raises

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed.

common/logger.py Outdated
# force event to str for compatibility with standard library
event = event_dict.pop("event")
if not isinstance(event, str):
event = str(event)
Copy link
Member

Choose a reason for hiding this comment

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

I'm guessing you're trying to make sure the event (I'd call it msg or message) is a string here. However you'd have the exact same problem that you solved in key_value_part, if an object with no __str__ method is passed this will fail, so it seems to debunk the purpose of the check.

I'd either use _repr or not have the check + cast at all.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changed using _repr now.

common/logger.py Outdated
global configured

if configured:
raise RuntimeError("logging was already configured.")
Copy link
Member

Choose a reason for hiding this comment

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

Not sure this is really necessary.

The point is that this check is that the function should only be called one, isn't it? But this function is supposed to only be called during bootstrap anyway, isn't it?

In any case, this would raise an exception that no-one captures making either cli or teos to quit execution.

So, either we do not add this, or we implement is as a singleton?

PS: Given that this has no return, but simply configures logging.config.dictConfig, you can simply return None if the condition is met, given that it has been already configured.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well, I think calling this twice means that your code is broken, so it's good if the exception is not handled. Silently failing (and returning None that will likely be ignored) might lead to not detecting that you called it twice - and maybe running with a config you don't expect.

Copy link
Member

Choose a reason for hiding this comment

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

Well, I don't think the latter would apply, since the the first time it will configure it with the original conf and the second time will not run it at all, so it would still run with the original conf.

I don't think this is a big deal though, since there is no chance this is called more than once.

If you rather leaving it like this: Cap "Logging" and remove the tailing dot

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well, that mean we are choosing the first config, but we don't know which one was the intended one if two are given.

Updated error message in 56adb9b.

def _create_console_message(self, msg, **kwargs):
s_message = _StructuredMessage(self._add_prefix(msg), **kwargs).to_dict()
message = "{} {}".format(s_message["time"], s_message["message"])
def __call__(self, _, __, event_dict):
Copy link
Member

Choose a reason for hiding this comment

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

docstrings missing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added a one-liner docstring, not sure there's more to add as it would repeat the class docstring, basically.

teos/__init__.py Outdated
@@ -17,6 +16,7 @@
"BTC_FEED_PROTOCOL": {"value": "tcp", "type": str},
"BTC_FEED_CONNECT": {"value": "localhost", "type": str},
"BTC_FEED_PORT": {"value": 28332, "type": int},
"DAEMON": {"value": 0, "type": int},
Copy link
Member

Choose a reason for hiding this comment

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

Can not this be a boolean?

Copy link
Member

Choose a reason for hiding this comment

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

Actually, read comment in teosd.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Dropped the two commits adding the daemon config.

teos/teosd.py Outdated
@@ -53,7 +53,15 @@ def main(command_line_conf):
config["BTC_RPC_PORT"] = get_default_rpc_port(config.get("BTC_NETWORK"))

setup_data_folder(data_dir)
setup_logging(config.get("LOG_FILE"), LOG_PREFIX)

daemon = config.get("DAEMON") != 0
Copy link
Member

Choose a reason for hiding this comment

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

It may be worth separate this to a different PR to avoid mixing stuff

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Dropped the two commits adding the daemon config.

@bigspider bigspider force-pushed the 165-logs branch 2 times, most recently from 9392c65 to da4439b Compare July 10, 2020 10:05
@bigspider bigspider force-pushed the 165-logs branch 2 times, most recently from f3253f5 to b29eb0d Compare July 10, 2020 10:52
@bigspider
Copy link
Collaborator Author

Should have addressed almost everything, still a comment suspended about what happens if setup_logging is called twice.

I added tests for the expected output of CustomLogRenderer in various combinations; not sure what tests to add for setup_logging and get_logger. For get_logger, the problem is also that the object returned is kinda opaque: a test that would make sense is to check whether the component is indeed bound to the logger, but didn't find a way without "cheating" by reading private variables in the returned object.

@bigspider bigspider requested a review from sr-gi July 10, 2020 11:11
Copy link
Member

@sr-gi sr-gi left a comment

Choose a reason for hiding this comment

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

General comments (there's also some inline):

  • API should have logger as part of the init (using API.name_) instead of globally defined.

  • Cleaner, Responder, and UserDBM can have the name defined using X.name to get rid of a hardcoded value.

  • All the clases that now include logger as part of their attributes are missing the corresponding entry in the docstring. Moreover, entries on args and attributes are sorted by appearance.

  • Needs black formatting (add a file watcher so you can have it applied on every file save).

  • test_logger has a bunch of unused import (install flake8 and use it to check your files).

  • test_logger is only covering CustomLogRenderer

@staticmethod
def _create_file_message(msg, **kwargs):
return json.dumps(_StructuredMessage(msg, **kwargs).to_dict())
# Represent all the key=value elements still in event_dict
Copy link
Member

Choose a reason for hiding this comment

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

We can surround this by parenthesis so it is easier to parse in case we ever need to:

ts [component] msg (key1=value1, ...)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good idea, done in e260149.

@bigspider
Copy link
Collaborator Author

Should have addressed all the comments. Added the logger to the docstrings (without specifying the type, though). Added a test for get_logger which is a bit unusual, but the only way I found to test it using only the public interface of the object returned by get_logger. There are still no tests for setup_logger, but I don't thing there's much to test there, tbh.

@bigspider bigspider requested a review from sr-gi July 13, 2020 08:44
@bigspider bigspider merged commit 9b8c5a2 into talaia-labs:master Jul 13, 2020
@bigspider bigspider deleted the 165-logs branch July 13, 2020 11:08
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants