conftest.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. """
  2. Opt-in tracing for the cloud integration suites.
  3. The AWS suite's wall time is dominated by one or two tests, and end-to-end
  4. timings have proved too noisy to attribute - test_create_and_list_image alone
  5. has measured 26.2, 60.6 and 33.8 minutes across three runs of the same 123
  6. tests. Answering *where* that time goes needs the individual waits timed
  7. rather than the test as a whole.
  8. Set CB_TEST_TRACE=1 to capture, per xdist worker:
  9. * every ``wait_for`` poll, which brackets each wait - the span from a wait's
  10. first poll to its last is the wait itself, and the gap between consecutive
  11. polls shows whether the state call is slow (the interval is 1s for real
  12. providers, so a larger gap is the API, not the sleep);
  13. * botocore retries, which is how throttling appears client-side. Those
  14. loggers stay silent unless a request is actually retried, so anything they
  15. emit is signal.
  16. Records go to cb-trace-<worker>.log rather than stderr because pytest
  17. captures stderr at file-descriptor level, and captured output is discarded
  18. for passing tests - which these are. tox prints the files once the run
  19. finishes.
  20. CB_TEST_TRACE=2 additionally logs one line per boto invocation. Level 1
  21. answers whether time is going into polling or into throttled retries; when
  22. the answer is neither - as it was for test_create_and_list_image, where 1184
  23. of 1400 seconds passed in a single stretch with no polls and no retries at
  24. all - level 2 is what names the call that blocked, since the gap between two
  25. consecutive request lines is that request.
  26. Deliberately opt-in: at a 1s poll interval a 30 minute wait is ~1800 lines
  27. for a single waiting resource, and level 2 is roughly 20x that again.
  28. """
  29. import logging
  30. import os
  31. TRACE_FILE_PREFIX = 'cb-trace-'
  32. def _trace_level():
  33. """
  34. 0 off; 1 waits and retries; 2 also every provider request.
  35. Level 1 answers "is the time going into polling, or into throttled
  36. retries" - it is cheap enough to leave on. Level 2 additionally logs each
  37. boto invocation, so the gap between two consecutive lines names the call
  38. that blocked; that is what level 1 cannot show, at perhaps 20x the volume.
  39. """
  40. raw = (os.environ.get('CB_TEST_TRACE') or '').lower()
  41. if raw in ('2', 'requests', 'all'):
  42. return 2
  43. return 1 if raw in ('1', 'true', 'yes') else 0
  44. def _trace_path():
  45. # Each xdist worker needs its own file; they run in one directory and
  46. # would otherwise interleave mid-line.
  47. worker = os.environ.get('PYTEST_XDIST_WORKER', 'main')
  48. return '{0}{1}.log'.format(TRACE_FILE_PREFIX, worker)
  49. class _TraceFilter(logging.Filter):
  50. """
  51. Keep the test markers, the poll lines and genuine retries; at level 2
  52. keep the provider request lines too, and drop everything else.
  53. cloudbridge at DEBUG is far too chatty to keep wholesale - roughly 20x
  54. the volume - and most of it says nothing about where the time went.
  55. """
  56. def __init__(self, level):
  57. super(_TraceFilter, self).__init__()
  58. self.level = level
  59. def filter(self, record):
  60. message = record.getMessage()
  61. if record.name.startswith('botocore'):
  62. # botocore logs a line per request whether or not it retried;
  63. # only an actual backoff says anything about throttling. The two
  64. # retry implementations word the negative case differently.
  65. return not (message.startswith('Not retrying')
  66. or message.startswith('No retry needed'))
  67. if record.name.startswith('cloudbridge.providers'):
  68. return self.level >= 2
  69. return message.startswith('=== ') or 'Waiting another' in message
  70. def pytest_configure(config):
  71. level = _trace_level()
  72. if not level:
  73. return
  74. handler = logging.FileHandler(_trace_path(), mode='w')
  75. handler.setFormatter(logging.Formatter(
  76. '%(asctime)s %(name)s %(message)s'))
  77. handler.addFilter(_TraceFilter(level))
  78. # wait_for logs one line per poll at DEBUG, naming the object and the
  79. # state it is waiting on. Scope the level to that module rather than the
  80. # cloudbridge tree: raising the whole tree to DEBUG would have every
  81. # provider request build a log record that the filter then discards, and
  82. # those records still propagate to pytest's own capture handler, which
  83. # holds them for the duration of the test.
  84. loggers = ['cloudbridge.base.resources',
  85. # Quiet unless a request is actually retried, which is how
  86. # throttling shows up client-side. Enabling botocore wholesale
  87. # would log every request and response.
  88. 'botocore.retries', 'botocore.retryhandler']
  89. if level >= 2:
  90. # One line per boto invocation, so a stretch with no wait_for polling
  91. # can still be attributed to the call that blocked.
  92. loggers.append('cloudbridge.providers')
  93. for name in loggers:
  94. target = logging.getLogger(name)
  95. target.addHandler(handler)
  96. target.setLevel(logging.DEBUG)
  97. def pytest_runtest_logstart(nodeid, location):
  98. # Stamps the trace with test boundaries, so a span of polls can be
  99. # attributed to the test that caused it.
  100. if _trace_level():
  101. logging.getLogger('cloudbridge.base.resources').debug(
  102. '=== START %s', nodeid)
  103. def pytest_runtest_logfinish(nodeid, location):
  104. if _trace_level():
  105. logging.getLogger('cloudbridge.base.resources').debug(
  106. '=== END %s', nodeid)