bitpanda-labs / loggo2 Goto Github PK
View Code? Open in Web Editor NEWOpen source Python logging utilities
License: MIT License
Open source Python logging utilities
License: MIT License
Add typehints to the codebase. We will lose support for Python versions <3.6. This needs to show in setup.py
Loggo is not currently respecting do_print=False
in cilib. Why not?
So it turns out it is fairly possible to decorate entire third party modules (with about 20 additional lines of code). Here is an example of what happens if you log everything that happens while building a pandas dataframe:
In [1]: from loggo import Loggo
In [2]: import pandas as pd
In [3]: loggo = Loggo({'do_print': True})
29.05 2019 15:56:04 Graylog not configured! Disabling it 30
Graylog not configured! Disabling it
In [4]: lp = loggo(pd)
In [5]: df = lp.DataFrame(list(range(2, 20)))
29.05 2019 15:56:18 *Called DatetimeTZDtype.construct_from_string(string=[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) 20
29.05 2019 15:56:18 *Errored during DatetimeTZDtype.construct_from_string(string=[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) with TypeError "Could not construct DatetimeTZDtype" 20 -- see below:
Traceback (most recent call last):
File "/home/dannio/work/loggo/loggo/loggo.py", line 314, in full_decoration
response = function(*args, **kwargs)
File "/home/dannio/.local/lib/python3.6/site-packages/pandas/core/dtypes/dtypes.py", line 701, in construct_from_string
raise TypeError("Could not construct DatetimeTZDtype")
TypeError: Could not construct DatetimeTZDtype
29.05 2019 15:56:18 *Called RangeIndex._validate_dtype(dtype=None) 20
29.05 2019 15:56:18 *Returned None from RangeIndex._validate_dtype(dtype=None) 20
29.05 2019 15:56:18 *Called RangeIndex._simple_new(start=0, stop=18, step=1, protected_name=None) 20
29.05 2019 15:56:18 *Called Index._reset_identity() 20
29.05 2019 15:56:18 *Called RangeIndex._format_data() 20
29.05 2019 15:56:18 *Returned None from RangeIndex._format_data() 20
29.05 2019 15:56:18 *Called RangeIndex._format_attrs() 20
29.05 2019 15:56:18 *Called RangeIndex._get_data_as_items() 20
29.05 2019 15:56:18 *Returned from RangeIndex._get_data_as_items() with list ([('start', 0), ('stop', 18), ('step', 1)])20
29.05 2019 15:56:18 *Returned from RangeIndex._format_attrs() with list ([('start', 0), ('stop', 18), ('step', 1)]) 20
29.05 2019 15:56:18 *Called Index._format_space() 20
29.05 2019 15:56:18 *Returned from Index._format_space() with str (' ') 20
29.05 2019 15:56:18 *Returned from Index._reset_identity() with RangeIndex (RangeIndex(start=0, stop=18, step=1)) 20
29.05 2019 15:56:18 *Called RangeIndex._format_data() 20
29.05 2019 15:56:18 *Returned None from RangeIndex._format_data() 20
29.05 2019 15:56:18 *Called RangeIndex._format_attrs() 20
29.05 2019 15:56:18 *Called RangeIndex._get_data_as_items() 20
29.05 2019 15:56:18 *Returned from RangeIndex._get_data_as_items() with list ([('start', 0), ('stop', 18), ('step', 1)])20
29.05 2019 15:56:18 *Returned from RangeIndex._format_attrs() with list ([('start', 0), ('stop', 18), ('step', 1)]) 20
29.05 2019 15:56:18 *Called Index._format_space() 20
29.05 2019 15:56:18 *Returned from Index._format_space() with str (' ') 20
29.05 2019 15:56:18 *Returned from RangeIndex._simple_new(start=0, stop=18, step=1, protected_name=None) with RangeIndex (RangeIndex(start=0, stop=18, step=1)) 20
29.05 2019 15:56:18 *Called RangeIndex._validate_dtype(dtype=None) 20
29.05 2019 15:56:18 *Returned None from RangeIndex._validate_dtype(dtype=None) 20
29.05 2019 15:56:18 *Called RangeIndex._simple_new(start=0, stop=1, step=1, protected_name=None) 20
29.05 2019 15:56:18 *Called Index._reset_identity() 20
29.05 2019 15:56:18 *Called RangeIndex._format_data() 20
29.05 2019 15:56:18 *Returned None from RangeIndex._format_data() 20
29.05 2019 15:56:18 *Called RangeIndex._format_attrs() 20
29.05 2019 15:56:18 *Called RangeIndex._get_data_as_items() 20
29.05 2019 15:56:18 *Returned from RangeIndex._get_data_as_items() with list ([('start', 0), ('stop', 1), ('step', 1)]) 20
29.05 2019 15:56:18 *Returned from RangeIndex._format_attrs() with list ([('start', 0), ('stop', 1), ('step', 1)]) 20
29.05 2019 15:56:18 *Called Index._format_space() 20
29.05 2019 15:56:18 *Returned from Index._format_space() with str (' ') 20
29.05 2019 15:56:18 *Returned from Index._reset_identity() with RangeIndex (RangeIndex(start=0, stop=1, step=1)) 20
29.05 2019 15:56:18 *Called RangeIndex._format_data() 20
29.05 2019 15:56:18 *Returned None from RangeIndex._format_data() 20
29.05 2019 15:56:18 *Called RangeIndex._format_attrs() 20
29.05 2019 15:56:18 *Called RangeIndex._get_data_as_items() 20
29.05 2019 15:56:18 *Returned from RangeIndex._get_data_as_items() with list ([('start', 0), ('stop', 1), ('step', 1)]) 20
29.05 2019 15:56:18 *Returned from RangeIndex._format_attrs() with list ([('start', 0), ('stop', 1), ('step', 1)]) 20
29.05 2019 15:56:18 *Called Index._format_space() 20
29.05 2019 15:56:18 *Returned from Index._format_space() with str (' ') 20
29.05 2019 15:56:18 *Returned from RangeIndex._simple_new(start=0, stop=1, step=1, protected_name=None) with RangeIndex (RangeIndex(start=0, stop=1, step=1)) 20
29.05 2019 15:56:18 *Called IntervalDtype.is_dtype(dtype=dtype('int64')) 20
29.05 2019 15:56:18 *Returned from IntervalDtype.is_dtype(dtype=dtype('int64')) with bool (False) 20
29.05 2019 15:56:18 *Called PeriodDtype.is_dtype(dtype=dtype('int64')) 20
29.05 2019 15:56:18 *Returned from PeriodDtype.is_dtype(dtype=dtype('int64')) with bool (False) 20
Right now doing some further operations with the dataframe can cause stack overflow (lol), but probably because i'm recursively decorating everything with Loggo. It should be possible to fix this so everything callable is decorated exactly once. But ... is it worth it?
README.md is not blacked.
if you put loggo decorator on class and call a class method via self.__class__.method()
you will get an attribute error because loggo overwrites class property
Right now we are missing a coverage line for callables without __name__
, because these are very rare. functools.partial
objects don't have __name__
but are callable, so I decided to try to write a test, but it threw an unexpected error, which we probably should handle:
from loggo import Loggo
from functools import partial
loggo = Loggo(do_print=True)
@loggo
class WithUnnamed:
basetwo = partial(int, base=2)
WithUnnamed().basetwo()
ValueError Traceback (most recent call last)
<ipython-input-36-8625eace0720> in <module>
----> 1 WithUnnamed().basetwo()
~/work/loggo/loggo/loggo.py in full_decoration(*args, **kwargs)
242 Args and kwargs are for/from the decorated function
243 """
--> 244 bound = self._params_to_dict(function, *args, **kwargs)
245 # bound will be none if inspect signature binding failed. in this
246 # case, error log was created, raised if self.raise_logging_errors
~/work/loggo/loggo/loggo.py in _params_to_dict(self, function, *args, **kwargs)
331 Turn args and kwargs into an OrderedDict of {param_name: value}
332 """
--> 333 sig = inspect.signature(function)
334 bound = sig.bind(*args, **kwargs).arguments
335 if bound:
/usr/lib/python3.6/inspect.py in signature(obj, follow_wrapped)
3055 def signature(obj, *, follow_wrapped=True):
3056 """Get a signature object for the passed callable."""
-> 3057 return Signature.from_callable(obj, follow_wrapped=follow_wrapped)
3058
3059
/usr/lib/python3.6/inspect.py in from_callable(cls, obj, follow_wrapped)
2805 """Constructs Signature for the given callable object."""
2806 return _signature_from_callable(obj, sigcls=cls,
-> 2807 follow_wrapper_chains=follow_wrapped)
2808
2809 @property
/usr/lib/python3.6/inspect.py in _signature_from_callable(obj, follow_wrapper_chains, skip_bound_arg, sigcls)
2276 follow_wrapper_chains=follow_wrapper_chains,
2277 skip_bound_arg=skip_bound_arg,
-> 2278 sigcls=sigcls)
2279 return _signature_get_partial(wrapped_sig, obj)
2280
/usr/lib/python3.6/inspect.py in _signature_from_callable(obj, follow_wrapper_chains, skip_bound_arg, sigcls)
2345 else:
2346 raise ValueError(
-> 2347 'no signature found for builtin type {!r}'.format(obj))
2348
2349 elif not isinstance(obj, _NonUserDefinedCallables):
ValueError: no signature found for builtin type <class 'int'>
@gcarq @hukkinj1 you guys ever used partial
? How should loggo handle it?
We should get coverage to 100% and then disallow any code additions that are not covered.
Right now (coverage 96.42%) there are just a few edge cases that need tests, probably requiring a bit of mocking etc, but it's doable and would be nice.
From black docs:
What on Earth is a pyproject.toml file?
PEP 518 defines pyproject.toml as a configuration file to store build system requirements for Python projects. With the help of tools like Poetry or Flit it can fully replace the need for setup.py and setup.cfg files.
PEP 518: https://www.python.org/dev/peps/pep-0518/
Poetry: https://poetry.eustace.io/
Consider setting up something like this? Especially since we have black running?
Any time you log, whether you like it or not, there is an 'alert level', But what does it mean? It's actually surprisingly complex. We have to ask ourselves, why is there an alert? For whom? And what does changing such a level do? What can it do? What could it do?
Python's logging
module (which we draw upon right now) has what we might call 'named numbers':
CRITICAL | 50 |
---|---|
ERROR | 40 |
WARNING | 30 |
INFO | 20 |
DEBUG | 10 |
NOTSET | 0 |
These are supposed to represent a kind of scale of severity. Adding confusion, our code had to deal with bitpanda legacy features, and graylog compatibility. This is why Loggo allows us to pass strings, minor
, dev
and critical
, as alert level.
We likely all agree that having multiple level types is a bad idea, but it's interesting to consider why it happened in the first place.
In short, there is an ever-present question of what the semantics of the numeric scale actually mean: should the scale progress from expected --> unexpected
, from good --> bad
, or common --> rare
?
Competing standards arose to address these ambiguities. Then, compounding the problem a great deal was that that nobody really gives a shit about logging, and accordingly, nobody ever confronted the complexity of this ridiculous question.
One certainty, therefore, is that we will never do things right in an ontological sense. Internally consistent and expressive code would ideally handle multiple types of alert levels, with a multifaceted set of sequences. Imagine the joy and horror of:
log('overkill', expectedness=6, severity=2, badness=9)
Fortunately for us, however, the different types of alerts tend to pattern together. Unless a product really sucks a lot, you would not its logs to have high values for expectedness
, severity
and badness
all at once. These things all mean different things, but they seem to overlap more than they don't. So, we're back to square one, only now we know that our implementation will stink.
There is an obvious pathway for improving our alert level handling. That is to remove from Loggo the ability to handle legacy bitpanda/graylog stuff, such as 'minor'
, 'dev'
and 'critical'
strings. Instead, the user should use the integer defined in the logging
module.
To make things a bit nicer, we can also do two other things:
Loggo.DEBUG == 10
and so on. I don't want to import loggo
and logging
, and I think at least at times, using the namespaced values is easier to understand than e.g., a random 50
, which means critical.logging
--- log(10, 'msg', data)
:if isinstance(msg, int) and isinstance(level, str):
msg, level = level, msg
Bitpanda's subclasses of Loggo can, if they like, interpret the deprecated strings, and derive graylog
data from them if need be. None of my business really.
the wrong way
I recently set up readthedocs via mkdocs on another module of mine, it was easy and works nicely.
https://buzz.readthedocs.io/en/latest/
Would take 15 mins to set up for loggo using the README as the main page, but I'm not sure we should. Pros and cons are pretty obvious.
Have mentioned this before, but would like to reach a final decision. I lean toward a yes purely because I think it's kind of cool.
Right now it seems like some things should be configurable and are not; other things do need need to be and are.
Meanwhile:
max_dict_depth
or the string with which private data is obscured?Just something to think about.
Would love to have a coverage badge on this project. Any takers?
@jorop to set up travis CI to run tests unless there is nowadays a nicer way like we got with gitlab
Some of us don't like the fact that loggo accounts for about half the traceback when there is an error. Loggo itself is fairly stable now, so pruning the traceback object to remove the Loggo frames would help a bit.
When we do a manual log (Loggo.log('msg', level, extra)
), the extra
data is just whatever we pass in, plus the (probably pointless) 'loggo': True
item.
Question: @hukkinj1 , @gcarq , would it be nice or not nice to inspect
a little bit during these calls, and add a few things to extra data? Would be trivial to get script name, class name, func name, etc., though doing it in a nice consistent way might be a bit harder. Code could likely be shared from how we reconstruct build the call signature string during autologging, though of course it would be hackier, as we aren't passing the callable around, but need to get it through inspect
stuff instead.
Helpful or not?
Here is an example settings.py excerpt:
Using with Django¶
It’s easy to integrate graypy with Django’s logging settings. Just add a new handler in your settings.py like this:
LOGGING = {
...
'handlers': {
'graypy': {
'level': 'WARNING',
'class': 'graypy.GELFHandler',
'host': 'localhost',
'port': 12201,
},
},
'loggers': {
'django.request': {
'handlers': ['graypy'],
'level': 'ERROR',
'propagate': True,
},
},
}
Seems like we could do more in loggo in terms of integration here. could even have a loggo.django_setttings()
which returns all the relevant stuff, or something. I will likely be doing more django in future, so this might be something I'll have time to explore.
class Loggo(object):
or class Loggo:
? Believe we aren't maintaining Python 2 compatibility? @gcarq?
If you call a loggo decorated function with bad arguments, the error comes from loggo when it should come from you.
_emergency_log()
has been around for years and for historical reasons. @hukkinj1 you think we should just remove it? there is an option to raise/not raise errors during logging, i'd probably rather just use that and potentially raise instead.
the wrapt
module provides some helper decorators for making class decorators that attach to all methods. we do some creepy stuff to make this work, so it'd be better to rely on a well-known implementation.
This project shouldn't take too long to hook up to flake8. Why not? @gcarq can do.
repro:
class A:
def f():
pass
@loggo
class B(A):
def g():
pass
results in:
def _decorate_all_methods(self, cls: type, just_errors: bool = False) -> type:
[...]
> if isinstance(vars(cls)[name], (staticmethod, classmethod)):
E KeyError: 'f'
../../venv/lib/python3.8/site-packages/loggo/_loggo.py:228: KeyError
logme
anywayloggo = Loggo(config, called='custom message --- {timestamp}, {call_signature}')
pre
-> called
etc now that they are exposed to usercalled=None
, no logs for calling. Otherwise, existing defaults are usederror_alert_level
to config, default 50build_string
no longer needed, timestamp
and trace
can be in the default format stringsThis will reduce size of codebase, which is good.
Proof:
In [15]: from loggo import Loggo
In [16]: l = Loggo({'do_print': True})
In [17]: @l
...: class X():
...: @staticmethod
...: def f():
...: return 1
...:
In [18]: X().f()
14.01 2019 17:44:39 *Called __main__.f() None
14.01 2019 17:44:39 *Returned from __main__.f() with int (1) None
Out[18]: 1
In [19]: X.f()
Out[19]: 1
Should we use typehints and require Python version to be >=3.6 ? I vote yes.
I believe I saw a traceback
key and an old traceback in a '*Returned' log, which should not be possible. Figure out how to properly clear traceback.
Mike just showed me qualname, which seems to do a lot of the work of combining class name and callable name together. upgrade to use this?
inspect.signature()
is run for Loggo-decorated functions. There's no knowing that it succeeds in getting the signature for all callables however. It may throw ValueError. Try for example inspect.signature(print)
. Result: ValueError.
My suggestion: We simply don't log functions where inspect.signature()
fails, silently eat the ValueError, and clearly document this limitation in the README and code.
cilib
: *Errored with RpcError "Internal Server Error" when calling cilib.rpc.stellarrpc._getquery function with 0 args, 0 kwargs: args=tuple(tuple(friendbot)).
In logs, return values can apparently be truncated too much, according to @hukkinj1. Investigate and make them longer perhaps.
Got this warning log despite not adding anything manually to "log data". If this is a problematic key, would it be nicer if loggo handled it internally, instead of logging a warning?
Thanks Joe!
using listen_to means that there will always be logged 'graylog not configured errors', so the default should be to disable them
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.