protocols.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. # Licensed to the Apache Software Foundation (ASF) under one
  2. # or more contributor license agreements. See the NOTICE file
  3. # distributed with this work for additional information
  4. # regarding copyright ownership. The ASF licenses this file
  5. # to you under the Apache License, Version 2.0 (the
  6. # "License"); you may not use this file except in compliance
  7. # with the License. You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing,
  12. # software distributed under the License is distributed on an
  13. # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14. # KIND, either express or implied. See the License for the
  15. # specific language governing permissions and limitations
  16. # under the License.
  17. from __future__ import annotations
  18. import datetime
  19. import time
  20. from typing import Union
  21. from airflow.typing_compat import Protocol
  22. DeltaType = Union[int, float, datetime.timedelta]
  23. class TimerProtocol(Protocol):
  24. """Type protocol for StatsLogger.timer."""
  25. def __enter__(self) -> Timer: ...
  26. def __exit__(self, exc_type, exc_value, traceback) -> None: ...
  27. def start(self) -> Timer:
  28. """Start the timer."""
  29. ...
  30. def stop(self, send: bool = True) -> None:
  31. """Stop, and (by default) submit the timer to StatsD."""
  32. ...
  33. class Timer(TimerProtocol):
  34. """
  35. Timer that records duration, and optional sends to StatsD backend.
  36. This class lets us have an accurate timer with the logic in one place (so
  37. that we don't use datetime math for duration -- it is error prone).
  38. Example usage:
  39. .. code-block:: python
  40. with Stats.timer() as t:
  41. # Something to time
  42. frob_the_foos()
  43. log.info("Frobbing the foos took %.2f", t.duration)
  44. Or without a context manager:
  45. .. code-block:: python
  46. timer = Stats.timer().start()
  47. # Something to time
  48. frob_the_foos()
  49. timer.end()
  50. log.info("Frobbing the foos took %.2f", timer.duration)
  51. To send a metric:
  52. .. code-block:: python
  53. with Stats.timer("foos.frob"):
  54. # Something to time
  55. frob_the_foos()
  56. Or both:
  57. .. code-block:: python
  58. with Stats.timer("foos.frob") as t:
  59. # Something to time
  60. frob_the_foos()
  61. log.info("Frobbing the foos took %.2f", t.duration)
  62. """
  63. # pystatsd and dogstatsd both have a timer class, but present different API
  64. # so we can't use this as a mixin on those, instead this class contains the "real" timer
  65. _start_time: float | None
  66. duration: float | None
  67. def __init__(self, real_timer: Timer | None = None) -> None:
  68. self.real_timer = real_timer
  69. def __enter__(self) -> Timer:
  70. return self.start()
  71. def __exit__(self, exc_type, exc_value, traceback) -> None:
  72. self.stop()
  73. def start(self) -> Timer:
  74. """Start the timer."""
  75. if self.real_timer:
  76. self.real_timer.start()
  77. self._start_time = time.perf_counter()
  78. return self
  79. def stop(self, send: bool = True) -> None:
  80. """Stop the timer, and optionally send it to stats backend."""
  81. if self._start_time is not None:
  82. self.duration = time.perf_counter() - self._start_time
  83. if send and self.real_timer:
  84. self.real_timer.stop()