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

Structured logging #611

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open

Structured logging #611

wants to merge 12 commits into from

Conversation

drothlis
Copy link
Contributor

@drothlis drothlis commented Aug 7, 2019

TODO:

  • stbt.debug
  • Configuration
  • stderr or stdout? A: stderr, otherwise it messes up doctest output.
  • Should we run init_logging on import stbt?
  • Make all stb-tester code use logging directly, with fine-grained loggers under the stbt logger (e.g. stbt.match).
  • Once all stb-tester code is using logging instead of stbt.debug, switch stbt.debug to use the root logger -- so that log messages from the test script don't get logged as stbt messages.
  • Deprecate stbt.debug.

@drothlis
Copy link
Contributor Author

drothlis commented Aug 7, 2019

I'm using logging.config.fileConfig so that you can change the logging behaviour just by changing stbt.conf. I had to jump through some hoops to get it working on Python 2. Also, the Python documentation recommends using dictConfig instead of fileConfig in the note here. Nevertheless, this is the easiest way of configuring the logging framework without having to write explicit code in the user's test scripts.

@drothlis
Copy link
Contributor Author

drothlis commented Aug 8, 2019

There's no need to run init_logging for the unit-tests because pytest automatically captures the logging calls. Pytest shows separate sections with "Captured stdout call", "Capturred stderr call", and "Captured log call", so it helps if the tests use logging (rather than print) to see the log messages interleaved in the right order.

@drothlis
Copy link
Contributor Author

drothlis commented Aug 8, 2019

Thoughts on log-levels: INFO should be for high(-ish) level actions, and DEBUG should be for things that happen for every frame (such as the result of every match inside wait_for_match).

"High-level" is relative to the logger; for example in your test script's logger, a high-level action would be "Search for EASTENDERS" but in the "stbt.keyboard" logger a high-level action would be "Navigating from A to H by pressing KEY_DOWN, KEY_RIGHT".

Ultimately you could have some kind of javascripty log-file viewer where you could interactively filter by logger name & level.

@wmanley
Copy link
Contributor

wmanley commented Aug 8, 2019

How would you feel about adding a dependency on structlog? Ubuntu 18.04 has 17.2.0

@drothlis
Copy link
Contributor Author

drothlis commented Aug 8, 2019

How would you feel about adding a dependency on structlog? Ubuntu 18.04 has 17.2.0

I thought we could achieve that just via the standard logging module by specifying a custom formatter in the configuration file.

But looking at "Implementing structured logging" from the logging cookbook, it seems we'd also have to change the Python code in every log invocation (or provide our own log functions):
https://docs.python.org/3.6/howto/logging-cookbook.html#implementing-structured-logging

class StructuredMessage(object):
    ...
_ = StructuredMessage   # optional, to improve readability
logging.info(_('message 1', foo='bar', bar='baz', num=123, fnum=123.456))

@drothlis
Copy link
Contributor Author

structlog doesn't seem to have the ability to "tee" the output to 2 different streams (e.g. a structured log in json, as well as the human-readable stdout.log). You'd still have to do that in the underlying stdlib logging framework. This implies that the formatting has to be done within the logging framework; see Rendering Using structlog-based Formatters Within logging.

@drothlis
Copy link
Contributor Author

Alternately we could use structlog.stdlib.render_to_log_kwargs (which puts the structlog kwargs into logging's extra field) and create our own logging.Formatter subclass that formats the keys from extra into the log message. See Rendering Using logging-based Formatters.

In future commits I'll make all of stb-tester's code call the logging
framework directly (`getLogger("stbt").debug(...)` etc). For now this
makes `stbt.debug()` use the Python logging framework, and sets up all
the configuration.

If you import `stbt` as a library then you'll need to configure the
logging framework yourself, e.g. by calling `logging.config.fileConfig`
as we do here. If you run stbt via `stbt run` then we call
`logging.config.fileConfig` pointing at your stbt.conf files.
…on 2

Python 2's `logging.config.fileConfig` has 2 limitations:

* It doesn't accept a `ConfigParser` instance, only a filename or file
  object. But our config isn't in a single file: it's spread over
  /etc/stbt/stbt.conf, the ~/.config/stbt/stbt.conf, $STBT_CONFIG_FILE,
  etc.

* It doesn't pass `raw=True` when reading `format` from the
  ConfigParser. Our ConfigParser doesn't have interpolation disabled,
  so it complains:

  > Bad value substitution: option 'format' in section 'formatter_basic'
  > contains an interpolation key u'name' which is not a valid option
  > name. Raw value: '%(name)s:%(levelname)s: %(message)s'

  (It's complaining about ConfigParser interpolation, not logging
  interpolation.)

  Arguably we should disable ConfigParser interpolation but that would
  be a breaking change (technically; I doubt it would actually affect
  anyone).
Pytest already captures & shows logging calls, similar to how it
captures stdout/stderr. It shows each of these in separate sections:
"Captured stdout call", "Capturred stderr call", and "Captured log
call", so it helps if the tests use logging (rather than print) to see
the log messages interleaved in the right order.

Pytest's default log_format is the same as this, but without the
timestamp. Pytest's default time format is %H:%M:%S which should be
sufficient for unit tests; we don't really need the date or the
timezone.

Note that this captures all log levels, including DEBUG. If we set
"log_level=DEBUG" in pytest.ini, then it *also* logs those calls to
stderr and we end up with duplicate logs.
So that the output is interleaved in the correct order with the logging
from the `stbt.Keyboard` implementation.
Now that we use the logging framework, our default configuration
includes the logger name (stbt.keyboard).
So we can log (internally) using [structlog], but it still gets printed
by the stdlib `logging` framework, so that users can configure the
output in the same way that they configure their own logging. We expect
that users will use `logging` directly, not `structlog`.

In future we can "tee" the output from structlog to a structured
file (e.g. jsonl) while continuing to print it to stderr.log for human
users' consumption.

[structlog]: https://www.structlog.org/
We'll have to fix this when we make it possible to import stbt as a
library (without going via `stbt run`), too.
Avoids several problems:

* I don't have to access protected attribute
  `structlog.stdlib._NAME_TO_LEVEL`.

* Fixes "AttributeError: 'Logger' object has no attribute 'trace'" when
  structlog calls the underlying `logging` logger.

* Fixes "TypeError: _proxy_to_logger() takes at most 3 arguments (4
  given)" if I try to call `structlog.get_logger().log(5, msg, *args)`
  without first initialising structlog (e.g. from pytest; currently
  we only call `init_logging` from `stbt_run.py`, we don't do it when
  importing stbt).
Otherwise `stbt.debug` etc. use structlog's default configuration, which
is to print everything to stdout. This messes up the doctests' expected
output.
@drothlis drothlis removed this from the v31 milestone Sep 18, 2019
@drothlis drothlis added this to the v32 milestone Sep 16, 2020
@drothlis drothlis removed this from the v32 milestone Sep 28, 2020
@drothlis drothlis changed the title Use Python's logging framework Structured logging Jun 30, 2022
@drothlis
Copy link
Contributor Author

Closing. See #763.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants