From 09e76e686601c70c7b4080a084e8143b1861825e Mon Sep 17 00:00:00 2001 From: William Braeckman Date: Mon, 10 Mar 2025 09:55:34 +0100 Subject: [PATCH] [IMP] runbot: build error frequency graphs Adds frequency graphs for build errors as small html widgets. All data is gathered from the last 30 days. - Daily frequency (how many times per day) - Hourly frequency (how many times per individual hour, 0-23) - Day Of Week frequency (how many times per day of week, 0-6) - Day of month frequency (how many times per day of month, 0-30) Co-Authored-By: Xavier-Do --- runbot/models/build_error.py | 81 ++++++++++++++++++++++++++++++ runbot/views/build_error_views.xml | 18 +++++++ 2 files changed, 99 insertions(+) diff --git a/runbot/models/build_error.py b/runbot/models/build_error.py index 8a5b9db7..c2eccda4 100644 --- a/runbot/models/build_error.py +++ b/runbot/models/build_error.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import datetime import hashlib import json import logging @@ -6,6 +7,7 @@ import re from collections import defaultdict from dateutil.relativedelta import relativedelta +from dateutil import rrule from markupsafe import Markup from werkzeug.urls import url_join from odoo import models, fields, api @@ -17,6 +19,18 @@ from ..fields import JsonDictField _logger = logging.getLogger(__name__) +def get_color(value: int): + if value >= 10: + return 'red' + elif value >= 5: + return 'orange' + return 'green' + +def draw_svg(values: list[int], max_value: int = 10, height: int = 30): + lines = ''.join(f'' for v in range(0, max_value, 2)) + rects = ''.join(f'' for idx, value in enumerate(values)) + return f'
{lines}{rects}
' + class BuildErrorLink(models.Model): _name = 'runbot.build.error.link' _description = 'Build Build Error Extended Relation' @@ -123,6 +137,12 @@ class BuildError(models.Model): random = fields.Boolean('Random', compute="_compute_random", store=True) + graph_history = fields.Html('30 days history', compute='_compute_graph', sanitize=False) + graph_hourly_recurence = fields.Html('Hourly recurence', compute='_compute_graph', sanitize=False) + graph_day_of_week_recurence = fields.Html('Weekly recurence', compute='_compute_graph', sanitize=False) + graph_day_of_month_recurence = fields.Html('Monthly recurence', compute='_compute_graph', sanitize=False) + + @api.constrains('tags_min_version_id', 'tags_max_version_id') def _check_min_max_version(self): for build_error in self: @@ -263,6 +283,67 @@ class BuildError(models.Model): else: record.analogous_content_ids = False + def _get_log_dates(self, start_date: datetime.datetime, end_date: datetime.datetime): + """ + Returns an count of build_error per hour for the last 30 days. + -> Dict[Self, Dict[datetime, int]] + """ + assert self, 'Method does not work if called with empty recordset.' + result = defaultdict(dict) + if not self._origin.ids: + return result + self.env.cr.execute(""" + SELECT error.id as error_id, date_trunc('hour', link.log_date) as time, count(*) as count + FROM runbot_build_error AS error + JOIN runbot_build_error_content AS content ON content.error_id = error.id + JOIN runbot_build_error_link AS link ON link.error_content_id = content.id + WHERE error.id IN %s AND link.log_date BETWEEN %s AND %s + GROUP BY error.id, date_trunc('hour', link.log_date) + """, (tuple(self.ids), start_date, end_date)) + data = self.env.cr.dictfetchall() + for d in data: + result[self.browse(d['error_id'])][d['time']] = d['count'] + return result + + @api.depends('build_error_link_ids') + def _compute_graph(self): + end_date = fields.Date.today() + relativedelta(days=1) + start_date = end_date - relativedelta(days=30) + log_date_per_error = self._get_log_dates(start_date, end_date) + for error in self: + dates = log_date_per_error[error] + daily_freq = [ + sum( + count + for hour, count in dates.items() if hour.date() == date.date() + ) + for date in rrule.rrule(rrule.DAILY, dtstart=start_date, until=end_date) + ] + error.graph_history = draw_svg(daily_freq, max_value=max(daily_freq)) + hourly_freq = [ + sum( + count + for hour, count in dates.items() if hour.hour == h + ) + for h in range(24) + ] + error.graph_hourly_recurence = draw_svg(hourly_freq) + day_of_week_freq = [ + sum( + count + for hour, count in dates.items() if hour.isoweekday() -1 == day + ) + for day in range(7) + ] + error.graph_day_of_week_recurence = draw_svg(day_of_week_freq) + day_of_month_recurrence = [ + sum( + count + for hour, count in dates.items() if hour.day - 1 == day + ) + for day in range(31) + ] + error.graph_day_of_month_recurence = draw_svg(day_of_month_recurrence) @api.constrains('test_tags') def _check_test_tags(self): diff --git a/runbot/views/build_error_views.xml b/runbot/views/build_error_views.xml index 9a607672..5b55f33b 100644 --- a/runbot/views/build_error_views.xml +++ b/runbot/views/build_error_views.xml @@ -48,6 +48,7 @@ + @@ -188,6 +189,18 @@ + + + + + + + + + + + + @@ -347,6 +360,11 @@ + + + + +