# -*- coding: utf-8 -*- import logging from collections import defaultdict from ..common import pseudo_markdown from odoo import models, fields, tools, api from odoo.exceptions import UserError _logger = logging.getLogger(__name__) TYPES = [(t, t.capitalize()) for t in 'client server runbot subbuild link markdown'.split()] class runbot_event(models.Model): _inherit = "ir.logging" _order = 'id' build_id = fields.Many2one('runbot.build', 'Build', index=True, ondelete='cascade') active_step_id = fields.Many2one('runbot.build.config.step', 'Active step', index=True) type = fields.Selection(selection_add=TYPES, string='Type', required=True, index=True, ondelete={t[0]: 'cascade' for t in TYPES}) error_id = fields.Many2one('runbot.build.error', compute='_compute_known_error') # remember to never store this field dbname = fields.Char(string='Database Name', index=False) @api.model_create_multi def create(self, vals_list): logs_by_build_id = defaultdict(list) for log in vals_list: if 'build_id' in log: logs_by_build_id[log['build_id']].append(log) builds = self.env['runbot.build'].browse(logs_by_build_id.keys()) for build in builds: build_logs = logs_by_build_id[build.id] for ir_log in build_logs: ir_log['active_step_id'] = build.active_step.id if build.local_state != 'running': if ir_log['level'].upper() == 'WARNING': build.local_result = 'warn' elif ir_log['level'].upper() == 'ERROR': build.local_result = 'ko' return super().create(vals_list) def _markdown(self): """ Apply pseudo markdown parser for message. """ self.ensure_one() return pseudo_markdown(self.message) def _compute_known_error(self): cleaning_regexes = self.env['runbot.error.regex'].search([('re_type', '=', 'cleaning')]) fingerprints = defaultdict(list) for ir_logging in self: ir_logging.error_id = False if ir_logging.level in ('ERROR', 'CRITICAL', 'WARNING') and ir_logging.type == 'server': fingerprints[self.env['runbot.build.error']._digest(cleaning_regexes.r_sub('%', ir_logging.message))].append(ir_logging) for build_error in self.env['runbot.build.error'].search([('fingerprint', 'in', list(fingerprints.keys()))]): for ir_logging in fingerprints[build_error.fingerprint]: ir_logging.error_id = build_error.id def _prepare_create_values(self, vals_list): # keep the given create date result_vals_list = super()._prepare_create_values(vals_list) for result_vals, vals in zip(result_vals_list, vals_list): if 'create_date' in vals: result_vals['create_date'] = vals['create_date'] return result_vals_list class RunbotErrorLog(models.Model): _name = 'runbot.error.log' _description = "Error log" _auto = False _order = 'id desc' id = fields.Many2one('ir.logging', string='Log', readonly=True) name = fields.Char(string='Module', readonly=True) message = fields.Text(string='Message', readonly=True) summary = fields.Text(string='Summary', readonly=True) log_type = fields.Char(string='Type', readonly=True) log_create_date = fields.Datetime(string='Log create date', readonly=True) func = fields.Char(string='Method', readonly=True) path = fields.Char(string='Path', readonly=True) line = fields.Char(string='Line', readonly=True) build_id = fields.Many2one('runbot.build', string='Build', readonly=True) dest = fields.Char(string='Build dest', readonly=True) local_state = fields.Char(string='Local state', readonly=True) local_result = fields.Char(string='Local result', readonly=True) global_state = fields.Char(string='Global state', readonly=True) global_result = fields.Char(string='Global result', readonly=True) bu_create_date = fields.Datetime(string='Build create date', readonly=True) host = fields.Char(string='Host', readonly=True) parent_id = fields.Many2one('runbot.build', string='Parent build', readonly=True) top_parent_id = fields.Many2one('runbot.build', string="Top parent", readonly=True) bundle_ids = fields.Many2many('runbot.bundle', compute='_compute_bundle_id', search='_search_bundle', string='Bundle', readonly=True) sticky = fields.Boolean(string='Bundle Sticky', compute='_compute_bundle_id', search='_search_sticky', readonly=True) build_url = fields.Char(compute='_compute_build_url', readonly=True) def _compute_repo_short_name(self): for l in self: l.repo_short_name = '%s/%s' % (l.repo_id.owner, l.repo_id.repo_name) def _compute_build_url(self): for l in self: l.build_url = '/runbot/build/%s' % l.build_id.id def action_goto_build(self): self.ensure_one() return { "type": "ir.actions.act_url", "url": "runbot/build/%s" % self.build_id.id, "target": "new", } def _compute_bundle_id(self): slots = self.env['runbot.batch.slot'].search([('build_id', 'in', self.mapped('top_parent_id').ids)]) for l in self: l.bundle_ids = slots.filtered(lambda rec: rec.build_id.id == l.top_parent_id.id).batch_id.bundle_id l.sticky = any(l.bundle_ids.filtered('sticky')) def _search_bundle(self, operator, value): query = """ SELECT id FROM runbot_build as build WHERE EXISTS( SELECT * FROM runbot_batch_slot as slot JOIN runbot_batch batch ON batch.id = slot.batch_id JOIN runbot_bundle bundle ON bundle.id = batch.bundle_id %s """ if operator in ('ilike', '=', 'in'): value = '%%%s%%' % value if operator == 'ilike' else value col_name = 'id' if operator == 'in' else 'name' where_condition = "WHERE slot.build_id = build.id AND bundle.%s %s any(%%s));" if operator == 'in' else "WHERE slot.build_id = build.id AND bundle.%s %s %%s);" operator = '=' if operator == 'in' else operator where_condition = where_condition % (col_name, operator) query = query % where_condition self.env.cr.execute(query, (value,)) build_ids = [t[0] for t in self.env.cr.fetchall()] return [('top_parent_id', 'in', build_ids)] raise UserError('Operator `%s` not implemented for bundle search' % operator) def _search_sticky(self, operator, value): if operator == '=': self.env.cr.execute(""" SELECT id FROM runbot_build as build WHERE EXISTS( SELECT * FROM runbot_batch_slot as slot JOIN runbot_batch batch ON batch.id = slot.batch_id JOIN runbot_bundle bundle ON bundle.id = batch.bundle_id WHERE bundle.sticky = %s AND slot.build_id = build.id); """, (value,)) build_ids = [t[0] for t in self.env.cr.fetchall()] return [('top_parent_id', 'in', build_ids)] return [] def _parse_logs(self): BuildError = self.env['runbot.build.error'] return BuildError._parse_logs(self) def init(self): """ Create an SQL view for ir.logging """ tools.drop_view_if_exists(self._cr, 'runbot_error_log') self._cr.execute(""" CREATE VIEW runbot_error_log AS ( SELECT l.id AS id, l.name AS name, l.message AS message, left(l.message, 50) as summary, l.type AS log_type, l.create_date AS log_create_date, l.func AS func, l.path AS path, l.line AS line, bu.id AS build_id, bu.dest AS dest, bu.local_state AS local_state, bu.local_result AS local_result, bu.global_state AS global_state, bu.global_result AS global_result, bu.create_date AS bu_create_date, bu.host AS host, bu.parent_id AS parent_id, split_part(bu.parent_path, '/',1)::int AS top_parent_id FROM ir_logging AS l JOIN runbot_build bu ON l.build_id = bu.id WHERE l.level = 'ERROR' )""")