import datetime import logging import time from functools import wraps from Queue import Queue, Full from flask import g, request from trollius import Return logger = logging.getLogger(__name__) # Buckets for the API response times. API_RESPONSE_TIME_BUCKETS = [.01, .025, .05, .1, .25, .5, 1.0, 2.5, 5.0] # Buckets for the builder start times. BUILDER_START_TIME_BUCKETS = [.5, 1.0, 5.0, 10.0, 30.0, 60.0, 120.0, 180.0, 240.0, 300.0, 600.0] class MetricQueue(object): """ Object to which various metrics are written, for distribution to metrics collection system(s) such Prometheus. """ def __init__(self, prom): # Define the various exported metrics. self.resp_time = prom.create_histogram('response_time', 'HTTP response time in seconds', labelnames=['endpoint'], buckets=API_RESPONSE_TIME_BUCKETS) self.resp_code = prom.create_counter('response_code', 'HTTP response code', labelnames=['endpoint', 'code']) self.non_200 = prom.create_counter('response_non200', 'Non-200 HTTP response codes', labelnames=['endpoint']) self.multipart_upload_start = prom.create_counter('multipart_upload_start', 'Multipart upload started') self.multipart_upload_end = prom.create_counter('multipart_upload_end', 'Multipart upload ends.', labelnames=['type']) self.build_capacity_shortage = prom.create_gauge('build_capacity_shortage', 'Build capacity shortage.') self.builder_time_to_start = prom.create_histogram('builder_tts', 'Time from triggering to starting a builder.', labelnames=['builder_type'], buckets=BUILDER_START_TIME_BUCKETS) self.builder_time_to_build = prom.create_histogram('builder_ttb', 'Time from triggering to actually starting a build', labelnames=['builder_type'], buckets=BUILDER_START_TIME_BUCKETS) self.build_time = prom.create_histogram('build_time', 'Time spent buildig', labelnames=['builder_type']) self.builder_fallback = prom.create_counter('builder_fallback', 'Builder fell back to ec2 executor') self.percent_building = prom.create_gauge('build_percent_building', 'Percent building.') self.build_counter = prom.create_counter('builds', 'Number of builds', labelnames=['name']) self.ephemeral_build_workers = prom.create_counter('ephemeral_build_workers', 'Number of started ephemeral build workers', labelnames=['name', 'build_uuid']) self.ephemeral_build_worker_failure = prom.create_counter('ephemeral_build_worker_failure', 'Number of failed-to-start ephemeral build workers', labelnames=['build_uuid']) self.work_queue_running = prom.create_gauge('work_queue_running', 'Running items in a queue', labelnames=['queue_name']) self.work_queue_available = prom.create_gauge('work_queue_available', 'Available items in a queue', labelnames=['queue_name']) self.repository_pull = prom.create_counter('repository_pull', 'Repository Pull Count', labelnames=['namespace', 'repo_name', 'protocol', 'status']) self.repository_push = prom.create_counter('repository_push', 'Repository Push Count', labelnames=['namespace', 'repo_name', 'protocol', 'status']) self.repository_build_completed = prom.create_counter('repository_build_completed', 'Repository Build Complete Count', labelnames=['namespace', 'repo_name', 'status', 'executor']) self.chunk_upload_time = prom.create_histogram('chunk_upload_time', 'Registry blob chunk upload time', labelnames=['chunk_size', 'storage_region']) self.authentication_count = prom.create_counter('authenication_count', 'Authentication count', labelnames=['kind', 'status']) self.repository_count = prom.create_gauge('repository_count', 'Number of repositories') self.user_count = prom.create_gauge('user_count', 'Number of users') self.org_count = prom.create_gauge('org_count', 'Number of Organizations') self.robot_count = prom.create_gauge('robot_count', 'Number of robot accounts') # Deprecated: Define an in-memory queue for reporting metrics to CloudWatch or another # provider. self._queue = None def enable_deprecated(self, maxsize=10000): self._queue = Queue(maxsize) def put_deprecated(self, name, value, **kwargs): if self._queue is None: logger.debug('No metric queue %s %s %s', name, value, kwargs) return try: kwargs.setdefault('timestamp', datetime.datetime.now()) kwargs.setdefault('dimensions', {}) self._queue.put_nowait((name, value, kwargs)) except Full: logger.error('Metric queue full') def get_deprecated(self): return self._queue.get() def get_nowait_deprecated(self): return self._queue.get_nowait() def duration_collector_async(metric, labelvalues): """ Decorates a method to have its duration time logged to the metric. """ def decorator(func): @wraps(func) def wrapper(*args, **kwargs): trigger_time = time.time() try: rv = func(*args, **kwargs) except Return as e: metric.Observe(time.time() - trigger_time, labelvalues=labelvalues) raise e return rv return wrapper return decorator def time_decorator(name, metric_queue): """ Decorates an endpoint method to have its request time logged to the metrics queue. """ after = _time_after_request(name, metric_queue) def decorator(func): @wraps(func) def wrapper(*args, **kwargs): _time_before_request() rv = func(*args, **kwargs) after(rv) return rv return wrapper return decorator def time_blueprint(bp, metric_queue): """ Decorates a blueprint to have its request time logged to the metrics queue. """ bp.before_request(_time_before_request) bp.after_request(_time_after_request(bp.name, metric_queue)) def _time_before_request(): g._request_start_time = time.time() def _time_after_request(name, metric_queue): def f(r): start = getattr(g, '_request_start_time', None) if start is None: return r dur = time.time() - start dims = {'endpoint': request.endpoint} metric_queue.put_deprecated('ResponseTime', dur, dimensions=dims, unit='Seconds') metric_queue.put_deprecated('ResponseCode', r.status_code, dimensions=dims) metric_queue.resp_time.Observe(dur, labelvalues=[request.endpoint]) metric_queue.resp_code.Inc(labelvalues=[request.endpoint, r.status_code]) if r.status_code >= 500: metric_queue.put_deprecated('5XXResponse', 1, dimensions={'name': name}) elif r.status_code < 200 or r.status_code >= 300: metric_queue.put_deprecated('Non200Response', 1, dimensions={'name': name}) metric_queue.non_200.Inc(labelvalues=[request.endpoint]) return r return f