statsd.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. # -*- coding: utf-8 -
  2. #
  3. # This file is part of gunicorn released under the MIT license.
  4. # See the NOTICE for more information.
  5. "Bare-bones implementation of statsD's protocol, client-side"
  6. import logging
  7. import socket
  8. from re import sub
  9. from gunicorn.glogging import Logger
  10. # Instrumentation constants
  11. METRIC_VAR = "metric"
  12. VALUE_VAR = "value"
  13. MTYPE_VAR = "mtype"
  14. GAUGE_TYPE = "gauge"
  15. COUNTER_TYPE = "counter"
  16. HISTOGRAM_TYPE = "histogram"
  17. class Statsd(Logger):
  18. """statsD-based instrumentation, that passes as a logger
  19. """
  20. def __init__(self, cfg):
  21. """host, port: statsD server
  22. """
  23. Logger.__init__(self, cfg)
  24. self.prefix = sub(r"^(.+[^.]+)\.*$", "\\g<1>.", cfg.statsd_prefix)
  25. try:
  26. host, port = cfg.statsd_host
  27. self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  28. self.sock.connect((host, int(port)))
  29. except Exception:
  30. self.sock = None
  31. self.dogstatsd_tags = cfg.dogstatsd_tags
  32. # Log errors and warnings
  33. def critical(self, msg, *args, **kwargs):
  34. Logger.critical(self, msg, *args, **kwargs)
  35. self.increment("gunicorn.log.critical", 1)
  36. def error(self, msg, *args, **kwargs):
  37. Logger.error(self, msg, *args, **kwargs)
  38. self.increment("gunicorn.log.error", 1)
  39. def warning(self, msg, *args, **kwargs):
  40. Logger.warning(self, msg, *args, **kwargs)
  41. self.increment("gunicorn.log.warning", 1)
  42. def exception(self, msg, *args, **kwargs):
  43. Logger.exception(self, msg, *args, **kwargs)
  44. self.increment("gunicorn.log.exception", 1)
  45. # Special treatment for info, the most common log level
  46. def info(self, msg, *args, **kwargs):
  47. self.log(logging.INFO, msg, *args, **kwargs)
  48. # skip the run-of-the-mill logs
  49. def debug(self, msg, *args, **kwargs):
  50. self.log(logging.DEBUG, msg, *args, **kwargs)
  51. def log(self, lvl, msg, *args, **kwargs):
  52. """Log a given statistic if metric, value and type are present
  53. """
  54. try:
  55. extra = kwargs.get("extra", None)
  56. if extra is not None:
  57. metric = extra.get(METRIC_VAR, None)
  58. value = extra.get(VALUE_VAR, None)
  59. typ = extra.get(MTYPE_VAR, None)
  60. if metric and value and typ:
  61. if typ == GAUGE_TYPE:
  62. self.gauge(metric, value)
  63. elif typ == COUNTER_TYPE:
  64. self.increment(metric, value)
  65. elif typ == HISTOGRAM_TYPE:
  66. self.histogram(metric, value)
  67. else:
  68. pass
  69. # Log to parent logger only if there is something to say
  70. if msg:
  71. Logger.log(self, lvl, msg, *args, **kwargs)
  72. except Exception:
  73. Logger.warning(self, "Failed to log to statsd", exc_info=True)
  74. # access logging
  75. def access(self, resp, req, environ, request_time):
  76. """Measure request duration
  77. request_time is a datetime.timedelta
  78. """
  79. Logger.access(self, resp, req, environ, request_time)
  80. duration_in_ms = request_time.seconds * 1000 + float(request_time.microseconds) / 10 ** 3
  81. status = resp.status
  82. if isinstance(status, str):
  83. status = int(status.split(None, 1)[0])
  84. self.histogram("gunicorn.request.duration", duration_in_ms)
  85. self.increment("gunicorn.requests", 1)
  86. self.increment("gunicorn.request.status.%d" % status, 1)
  87. # statsD methods
  88. # you can use those directly if you want
  89. def gauge(self, name, value):
  90. self._sock_send("{0}{1}:{2}|g".format(self.prefix, name, value))
  91. def increment(self, name, value, sampling_rate=1.0):
  92. self._sock_send("{0}{1}:{2}|c|@{3}".format(self.prefix, name, value, sampling_rate))
  93. def decrement(self, name, value, sampling_rate=1.0):
  94. self._sock_send("{0}{1}:-{2}|c|@{3}".format(self.prefix, name, value, sampling_rate))
  95. def histogram(self, name, value):
  96. self._sock_send("{0}{1}:{2}|ms".format(self.prefix, name, value))
  97. def _sock_send(self, msg):
  98. try:
  99. if isinstance(msg, str):
  100. msg = msg.encode("ascii")
  101. # http://docs.datadoghq.com/guides/dogstatsd/#datagram-format
  102. if self.dogstatsd_tags:
  103. msg = msg + b"|#" + self.dogstatsd_tags.encode('ascii')
  104. if self.sock:
  105. self.sock.send(msg)
  106. except Exception:
  107. Logger.warning(self, "Error sending message to statsd", exc_info=True)