From c238e548087ddbe3fb00cce6743ade6eb976f882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phan=20Sainl=C3=A9ger?= Date: Mon, 15 Jun 2026 17:31:57 +0200 Subject: [PATCH] [IMP] maintenance_service_http_monitoring: rework maintenance.request creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, a single ``maintenance.request`` was created per equipment, regardless of how many services were down on that equipment. The name was ``[HTTP KO] {equipment.name}`` and deduplication relied on a name+date+equipment search that was fragile (manual clear of the field would lose the reference to an existing open request). This commit reworks the whole creation logic: - **1 request per KO service** instead of 1 per equipment. Each failing ``service.instance`` gets its own ``maintenance.request``, allowing fine-grained tracking and independent resolution. - **Request name** is now ``[HTTP KO] {service_url}``, making it immediately identifiable without opening the record. - **Description** includes the error detail: ``HTTP {status_code}`` for HTTP errors, or a human-readable network error label when ``last_http_status_code == -1`` (timeout / DNS / SSL failures). - **Deduplication** is now based solely on whether an open (non-done) ``maintenance.request`` already exists on the ``service.instance`` via the new ``http_maintenance_request`` field. No date boundary — as long as the request is open, no new one is created. - **``http_maintenance_request``** field moved from ``maintenance.equipment`` to ``service.instance``, where it belongs given the 1-request-per-service model. It is exposed as an optional hidden column in the service instance list view. - ``_build_ko_services_description()`` is removed (no longer needed). - ``create_http_maintenance_request()`` now receives a single ``service.instance`` recordset instead of a list. Tests updated accordingly (14 tests total): - Tests 2, 4, 9, 10, 12, 13 now assert on ``service_instance.http_maintenance_request``. - Test 2 also verifies the request name contains the service URL and the description contains the HTTP status code. - New test 14 asserts that two KO services on the same equipment produce two distinct requests with the correct names. --- maintenance_service_http_monitoring/README.md | 9 +-- .../models/maintenance_equipment.py | 48 ++++++--------- .../models/service_instance.py | 7 ++- .../tests/test_http_monitoring.py | 58 ++++++++++++++----- .../views/maintenance_equipment_views.xml | 1 - .../views/service_instance_views.xml | 1 + 6 files changed, 76 insertions(+), 48 deletions(-) diff --git a/maintenance_service_http_monitoring/README.md b/maintenance_service_http_monitoring/README.md index 603b247..eeee667 100644 --- a/maintenance_service_http_monitoring/README.md +++ b/maintenance_service_http_monitoring/README.md @@ -98,10 +98,11 @@ On service instances, you can see: ## Automatic Maintenance Requests When a service fails HTTP checks: -- A corrective maintenance request is created with prefix "[HTTP KO]" -- The request is linked to the equipment -- Only one request per equipment per day is created -- The request description lists all failing services +- A corrective maintenance request is created per failing service, named + ``[HTTP KO] {service_url}`` +- The request description includes the error detail: the HTTP status code, + or a network error label (timeout / DNS / SSL) when no HTTP response was received +- No duplicate is created as long as an open request already exists for that service - A **double-check** is performed before creating the request: the service is retested after 2 seconds. A maintenance request is only created if the service fails **both** checks, reducing noise from transient HTTP errors diff --git a/maintenance_service_http_monitoring/models/maintenance_equipment.py b/maintenance_service_http_monitoring/models/maintenance_equipment.py index 21b107f..2a409fa 100644 --- a/maintenance_service_http_monitoring/models/maintenance_equipment.py +++ b/maintenance_service_http_monitoring/models/maintenance_equipment.py @@ -27,11 +27,6 @@ class MaintenanceEquipment(models.Model): readonly=True, help="Computed from start + configured duration", ) - http_maintenance_request = fields.Many2one( - "maintenance.request", - string="HTTP Maintenance Request", - readonly=True, - ) def action_activate_maintenance_mode(self): for rec in self: @@ -72,31 +67,30 @@ class MaintenanceEquipment(models.Model): ) expired.action_deactivate_maintenance_mode() - def create_http_maintenance_request(self, ko_services): + def create_http_maintenance_request(self, ko_service): + """ + Create or return the open maintenance.request for a single KO service. + + Deduplication: if ko_service already has an open (non-done) request, + return it without creating a new one. + """ self.ensure_one() - today = fields.Date.context_today(self) - name = f"[HTTP KO] {self.name}" - domain = [ - ("name", "=", name), - ("equipment_id", "=", self.id), - ("maintenance_type", "=", "corrective"), - ("create_date", ">=", f"{today} 00:00:00"), - ("create_date", "<=", f"{today} 23:59:59"), - ] - existing = self.env["maintenance.request"].search(domain, limit=1) - # Check if a task with same name already exist for the day, if it’s the case : skip - if existing: - self.http_maintenance_request = existing.id + existing = ko_service.http_maintenance_request + if existing and not existing.stage_id.done: return existing - request = self.http_maintenance_request - if request and not request.stage_id.done: - return request + status_code = ko_service.last_http_status_code + if status_code == -1: + error_detail = "Erreur réseau (timeout / DNS / SSL)" + else: + error_detail = f"HTTP {status_code}" + name = f"[HTTP KO] {ko_service.service_url}" + description = f"Service KO: {ko_service.service_url}\n{error_detail}" vals = { "name": name, "equipment_id": self.id, "priority": "2", "maintenance_type": "corrective", - "description": self._build_ko_services_description(ko_services), + "description": description, } if self.employee_id: vals["employee_id"] = self.employee_id.id @@ -109,8 +103,8 @@ class MaintenanceEquipment(models.Model): if team: vals["maintenance_team_id"] = team.id request = self.env["maintenance.request"].create(vals) - self.http_maintenance_request = request.id - self._notify_webhook(request, ko_services) + ko_service.http_maintenance_request = request.id + self._notify_webhook(request, ko_service) return request def _notify_webhook(self, request, ko_service): @@ -156,7 +150,3 @@ class MaintenanceEquipment(models.Model): request.id, e, ) - - def _build_ko_services_description(self, ko_services): - lines = [f"Service KO: {s.service_url or s.name}" for s in ko_services] - return "\n".join(lines) diff --git a/maintenance_service_http_monitoring/models/service_instance.py b/maintenance_service_http_monitoring/models/service_instance.py index 72ecd5c..2360a1f 100644 --- a/maintenance_service_http_monitoring/models/service_instance.py +++ b/maintenance_service_http_monitoring/models/service_instance.py @@ -31,6 +31,11 @@ class ServiceInstance(models.Model): readonly=True, default=True, ) + http_maintenance_request = fields.Many2one( + "maintenance.request", + string="HTTP Maintenance Request", + readonly=True, + ) def check_http_status(self): """ @@ -98,4 +103,4 @@ class ServiceInstance(models.Model): ko_confirmed = ko_after_pass1.check_http_status() for service in ko_confirmed: - service.equipment_id.create_http_maintenance_request([service]) + service.equipment_id.create_http_maintenance_request(service) diff --git a/maintenance_service_http_monitoring/tests/test_http_monitoring.py b/maintenance_service_http_monitoring/tests/test_http_monitoring.py index 2040f56..e74500c 100644 --- a/maintenance_service_http_monitoring/tests/test_http_monitoring.py +++ b/maintenance_service_http_monitoring/tests/test_http_monitoring.py @@ -55,7 +55,7 @@ class TestHttpMonitoring(TransactionCase): self.assertIsNotNone(self.service_instance.last_http_check_date) # ------------------------------------------------------------------ - # Test 2 -- Two KO passes -> maintenance.request created + # Test 2 -- Two KO passes -> maintenance.request created on the service # ------------------------------------------------------------------ def test_http_500_creates_maintenance_request(self): with ( @@ -69,11 +69,13 @@ class TestHttpMonitoring(TransactionCase): self.assertFalse(self.service_instance.http_status_ok) self.assertEqual(self.service_instance.last_http_status_code, 500) - request = self.equipment.http_maintenance_request + request = self.service_instance.http_maintenance_request self.assertTrue(request) - self.assertEqual(request.name, f"[HTTP KO] {self.equipment.name}") + self.assertEqual(request.name, f"[HTTP KO] {self.service_instance.service_url}") self.assertEqual(request.priority, "2") self.assertEqual(request.maintenance_type, "corrective") + self.assertIn("HTTP 500", request.description) + self.assertIn(self.service_instance.service_url, request.description) # ------------------------------------------------------------------ # Test 3 -- Network error -> KO with code -1 @@ -99,7 +101,7 @@ class TestHttpMonitoring(TransactionCase): mock_requests.exceptions.RequestException = Exception self.env["service.instance"].cron_check_http_services() - request_1 = self.equipment.http_maintenance_request + request_1 = self.service_instance.http_maintenance_request self.assertTrue(request_1) with ( @@ -110,7 +112,7 @@ class TestHttpMonitoring(TransactionCase): mock_requests.exceptions.RequestException = Exception self.env["service.instance"].cron_check_http_services() - self.assertEqual(self.equipment.http_maintenance_request, request_1) + self.assertEqual(self.service_instance.http_maintenance_request, request_1) self.assertEqual( self.env["maintenance.request"].search_count( [("equipment_id", "=", self.equipment.id)] @@ -247,11 +249,9 @@ class TestHttpMonitoring(TransactionCase): mock_requests.exceptions.RequestException = Exception self.env["service.instance"].cron_check_http_services() - # Final status reflects pass 2 result self.assertTrue(self.service_instance.http_status_ok) self.assertEqual(self.service_instance.last_http_status_code, 200) - # No maintenance.request must have been created - self.assertFalse(self.equipment.http_maintenance_request) + self.assertFalse(self.service_instance.http_maintenance_request) self.assertEqual( self.env["maintenance.request"].search_count( [("equipment_id", "=", self.equipment.id)] @@ -271,12 +271,44 @@ class TestHttpMonitoring(TransactionCase): mock_requests.exceptions.RequestException = Exception self.env["service.instance"].cron_check_http_services() - # sleep must have been called between the two passes mock_sleep.assert_called_once_with(2) - # requests.get must have been called twice (pass 1 + pass 2) self.assertEqual(mock_requests.get.call_count, 2) - # Final status is KO self.assertFalse(self.service_instance.http_status_ok) self.assertEqual(self.service_instance.last_http_status_code, 503) - # maintenance.request created - self.assertTrue(self.equipment.http_maintenance_request) + self.assertTrue(self.service_instance.http_maintenance_request) + + # ------------------------------------------------------------------ + # Test 14 -- 2 KO services on same equipment -> 2 distinct requests + # ------------------------------------------------------------------ + def test_two_ko_services_same_equipment_create_two_requests(self): + service2 = self.env["service"].create({"name": "Test Service 2"}) + service_instance2 = self.env["service.instance"].create( + { + "equipment_id": self.equipment.id, + "service_id": service2.id, + "service_url": "https://other.example.com", + } + ) + + with ( + patch(SERVICE_INSTANCE_REQUESTS) as mock_requests, + patch(SERVICE_INSTANCE_SLEEP), + ): + mock_requests.get.return_value = _mock_response(500) + mock_requests.exceptions.RequestException = Exception + self.env["service.instance"].cron_check_http_services() + + req1 = self.service_instance.http_maintenance_request + req2 = service_instance2.http_maintenance_request + + self.assertTrue(req1) + self.assertTrue(req2) + self.assertNotEqual(req1, req2) + self.assertEqual(req1.name, f"[HTTP KO] {self.service_instance.service_url}") + self.assertEqual(req2.name, f"[HTTP KO] {service_instance2.service_url}") + self.assertEqual( + self.env["maintenance.request"].search_count( + [("equipment_id", "=", self.equipment.id)] + ), + 2, + ) diff --git a/maintenance_service_http_monitoring/views/maintenance_equipment_views.xml b/maintenance_service_http_monitoring/views/maintenance_equipment_views.xml index 9031d62..16a49eb 100644 --- a/maintenance_service_http_monitoring/views/maintenance_equipment_views.xml +++ b/maintenance_service_http_monitoring/views/maintenance_equipment_views.xml @@ -36,7 +36,6 @@ - diff --git a/maintenance_service_http_monitoring/views/service_instance_views.xml b/maintenance_service_http_monitoring/views/service_instance_views.xml index 9b89a90..4536ee0 100644 --- a/maintenance_service_http_monitoring/views/service_instance_views.xml +++ b/maintenance_service_http_monitoring/views/service_instance_views.xml @@ -17,6 +17,7 @@ +