From 7974404b7f367efc4863d6b8ebda38b84567bdcb Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sat, 11 Jul 2026 09:48:52 +0200 Subject: [PATCH 1/4] UNOMI-875: Close 3-dev REST gaps and add closure IT coverage (PR9) Expose DELETE routes for events and sessions that shell CRUD already used, port event-collector explain and list-invalid-objects integration tests, and document the new REST operations in Postman. --- .../unomi/itests/EventsCollectorIT.java | 52 +++++++++++++ .../unomi/itests/shell/OtherCommandsIT.java | 18 +++++ rest/postman-readme.md | 3 +- .../rest/endpoints/EventServiceEndpoint.java | 11 +++ .../endpoints/ProfileServiceEndPoint.java | 11 +++ rest/unomi-postman-collection.json | 73 ++++++++++++++++++- 6 files changed, 166 insertions(+), 2 deletions(-) diff --git a/itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java b/itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java index 2553c7c59..7c7ed28bf 100644 --- a/itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/EventsCollectorIT.java @@ -127,4 +127,56 @@ public void testEventsCollectorWithPublicApiKey() throws Exception { } } + @Test + public void testEventsCollectorWithExplain() throws Exception { + Event event = new Event(); + event.setEventType(TEST_EVENT_TYPE); + event.setScope(TEST_SCOPE); + event.setSessionId(TEST_SESSION_ID); + event.setProfileId(TEST_PROFILE_ID); + + EventsCollectorRequest eventsCollectorRequest = new EventsCollectorRequest(); + eventsCollectorRequest.setSessionId(TEST_SESSION_ID); + eventsCollectorRequest.setEvents(Collections.singletonList(event)); + + HttpPost request = new HttpPost(getFullUrl(EVENTS_URL + "?explain=true")); + request.addHeader("Content-Type", "application/json"); + String requestBody = getObjectMapper().writeValueAsString(eventsCollectorRequest); + request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); + + try (CloseableHttpResponse response = HttpClientThatWaitsForUnomi.doRequest(request, 200, true, true)) { + String responseContent = EntityUtils.toString(response.getEntity()); + EventCollectorResponse eventResponse = getObjectMapper().readValue(responseContent, EventCollectorResponse.class); + Assert.assertNotNull("Event collector response should not be null", eventResponse); + + int expectedFlags = EventService.SESSION_UPDATED | EventService.PROFILE_UPDATED; + Assert.assertEquals("Response should indicate both session and profile were updated", + expectedFlags, eventResponse.getUpdated()); + Assert.assertNotNull("Tracing information should be present", eventResponse.getRequestTracing()); + } + } + + @Test + public void testEventsCollectorWithExplainUnauthorized() throws Exception { + Event event = new Event(); + event.setEventType(TEST_EVENT_TYPE); + event.setScope(TEST_SCOPE); + event.setSessionId(TEST_SESSION_ID); + event.setProfileId(TEST_PROFILE_ID); + + EventsCollectorRequest eventsCollectorRequest = new EventsCollectorRequest(); + eventsCollectorRequest.setSessionId(TEST_SESSION_ID); + eventsCollectorRequest.setEvents(Collections.singletonList(event)); + + HttpPost request = new HttpPost(getFullUrl(EVENTS_URL + "?explain=true")); + request.addHeader("Content-Type", "application/json"); + String requestBody = getObjectMapper().writeValueAsString(eventsCollectorRequest); + request.setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON)); + + try (CloseableHttpResponse response = HttpClientThatWaitsForUnomi.doRequest(request, 403)) { + Assert.assertEquals("Request with explain parameter but without admin privileges should return 403", + 403, response.getStatusLine().getStatusCode()); + } + } + } diff --git a/itests/src/test/java/org/apache/unomi/itests/shell/OtherCommandsIT.java b/itests/src/test/java/org/apache/unomi/itests/shell/OtherCommandsIT.java index 040f02085..9f5a193fa 100644 --- a/itests/src/test/java/org/apache/unomi/itests/shell/OtherCommandsIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/shell/OtherCommandsIT.java @@ -34,6 +34,24 @@ public void testRuleResetStats() throws Exception { output.contains("Rule statistics successfully reset")); } + @Test + public void testListInvalidObjects() throws Exception { + String output = executeCommandAndGetOutput("unomi:list-invalid-objects"); + assertContainsAny(output, new String[]{ + "Invalid Objects Summary", + "Total invalid objects:", + "No invalid objects found" + }, "Should show invalid objects summary or indicate none found"); + + if (output.contains("Total invalid objects:")) { + validateNumericValuesInOutput(output, new String[]{"Total invalid objects:"}, false); + } + + if (output.contains("Object Type") && output.contains("Object ID")) { + validateTableHeaders(output, new String[]{"Object Type", "Object ID"}); + } + } + @Test public void testDeployDefinition() throws Exception { validateCommandExists("unomi:deploy-definition", "deploy", "definition"); diff --git a/rest/postman-readme.md b/rest/postman-readme.md index 35391d932..c68fc7743 100644 --- a/rest/postman-readme.md +++ b/rest/postman-readme.md @@ -157,6 +157,7 @@ The collection is organized in a logical demo flow order with 12 main folders: ### 11. Profile Management - Get, create, update, delete profiles +- Delete sessions - Search profiles - Get profile segments and sessions - **Authentication**: `tenantId:privateApiKey` (Basic Auth) @@ -168,7 +169,7 @@ The collection is organized in a logical demo flow order with 12 main folders: ### 12. Additional Useful Requests - Event type management -- Event search +- Event search and delete - Condition and action definitions - Property types - **Authentication**: `tenantId:privateApiKey` (Basic Auth) diff --git a/rest/src/main/java/org/apache/unomi/rest/endpoints/EventServiceEndpoint.java b/rest/src/main/java/org/apache/unomi/rest/endpoints/EventServiceEndpoint.java index 46c0c2c39..0dc3df331 100644 --- a/rest/src/main/java/org/apache/unomi/rest/endpoints/EventServiceEndpoint.java +++ b/rest/src/main/java/org/apache/unomi/rest/endpoints/EventServiceEndpoint.java @@ -72,6 +72,17 @@ public Event getEvents(@PathParam("id") final String id) { return eventService.getEvent(id); } + /** + * Deletes an event by id. + * + * @param id the identifier for the event to delete + */ + @DELETE + @Path("/{id}") + public void deleteEvent(@PathParam("id") final String id) { + eventService.deleteEvent(id); + } + /** * Retrieves the list of event types identifiers that the server has processed. * @return a Set of strings that contain event type identifiers. diff --git a/rest/src/main/java/org/apache/unomi/rest/endpoints/ProfileServiceEndPoint.java b/rest/src/main/java/org/apache/unomi/rest/endpoints/ProfileServiceEndPoint.java index 1ac88b474..26fe38b6c 100644 --- a/rest/src/main/java/org/apache/unomi/rest/endpoints/ProfileServiceEndPoint.java +++ b/rest/src/main/java/org/apache/unomi/rest/endpoints/ProfileServiceEndPoint.java @@ -405,6 +405,17 @@ public Session saveSession(Session session) { return profileService.saveSession(session); } + /** + * Delete the session by specifying its unique identifier. + * + * @param sessionId the session identifier for the session to delete + */ + @DELETE + @Path("/sessions/{sessionId}") + public void deleteSession(@PathParam("sessionId") String sessionId) { + profileService.deleteSession(sessionId); + } + /** * Retrieves {@link Event}s for the {@link Session} identified by the provided session identifier, matching any of the provided event types, * ordered according to the specified {@code sortBy} String and paged: only {@code size} of them are retrieved, starting with the {@code offset}-th one. diff --git a/rest/unomi-postman-collection.json b/rest/unomi-postman-collection.json index 979e6bdcd..3adaa96a9 100644 --- a/rest/unomi-postman-collection.json +++ b/rest/unomi-postman-collection.json @@ -2191,6 +2191,42 @@ }, "response": [] }, + { + "name": "Delete Session", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{baseUrl}}/cxs/profiles/sessions/{{sessionId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "cxs", + "profiles", + "sessions", + "{{sessionId}}" + ] + }, + "description": "Delete a session by its identifier", + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "{{tenantId}}", + "type": "string" + }, + { + "key": "password", + "value": "{{privateApiKey}}", + "type": "string" + } + ] + } + }, + "response": [] + }, { "name": "Delete Profile", "request": { @@ -2372,6 +2408,41 @@ }, "response": [] }, + { + "name": "Delete Event", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{baseUrl}}/cxs/events/{{eventId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "cxs", + "events", + "{{eventId}}" + ] + }, + "description": "Delete an event by its identifier", + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "{{tenantId}}", + "type": "string" + }, + { + "key": "password", + "value": "{{privateApiKey}}", + "type": "string" + } + ] + } + }, + "response": [] + }, { "name": "Get Session Events", "request": { @@ -2635,4 +2706,4 @@ "description": "Health check password" } ] -} \ No newline at end of file +} From d5d77826de063aa973c5a70d3de5731df70bc1f3 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sat, 11 Jul 2026 10:01:52 +0200 Subject: [PATCH 2/4] Fix IT compile for EventsCollector explain tracing assertion Add unomi-tracing-api to itests test classpath so EventCollectorResponse getRequestTracing() resolves TraceNode at test-compile time. --- itests/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/itests/pom.xml b/itests/pom.xml index 8f6f55b65..d28e1d2a6 100644 --- a/itests/pom.xml +++ b/itests/pom.xml @@ -178,6 +178,11 @@ unomi-rest test + + org.apache.unomi + unomi-tracing-api + test + org.apache.unomi unomi-api From 6f03f8712732a418f147822d0cba337b6e89d769 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sat, 11 Jul 2026 10:16:06 +0200 Subject: [PATCH 3/4] UNOMI-875: Add REST-level IT coverage for event/session delete and harden invalid-objects test Adds ITs exercising DELETE /cxs/events/{id} and DELETE /cxs/profiles/sessions/{sessionId} end-to-end, seeds a real invalid rule in testListInvalidObjects so its assertions aren't tautological, and adds the missing eventId Postman collection variable. --- .../apache/unomi/itests/EventServiceIT.java | 24 +++++++++++ .../apache/unomi/itests/ProfileServiceIT.java | 22 ++++++++++ .../unomi/itests/shell/OtherCommandsIT.java | 43 ++++++++++++++----- rest/unomi-postman-collection.json | 5 +++ 4 files changed, 83 insertions(+), 11 deletions(-) diff --git a/itests/src/test/java/org/apache/unomi/itests/EventServiceIT.java b/itests/src/test/java/org/apache/unomi/itests/EventServiceIT.java index e853b987b..9fd894912 100644 --- a/itests/src/test/java/org/apache/unomi/itests/EventServiceIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/EventServiceIT.java @@ -16,6 +16,7 @@ */ package org.apache.unomi.itests; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.unomi.api.Event; import org.apache.unomi.api.PartialList; import org.apache.unomi.api.Profile; @@ -184,4 +185,27 @@ public void test_EventGetNestedProperty() { Assert.assertEquals(testValue, value); } + @Test + public void test_DeleteEventViaRest() throws InterruptedException { + String eventId = "test-delete-event-id-" + System.currentTimeMillis(); + String profileId = "test-delete-event-profile-id-" + System.currentTimeMillis(); + String eventType = "test-delete-event-type"; + Profile profile = new Profile(profileId); + Event event = new Event(eventId, eventType, null, profile, null, null, null, new Date()); + + profileService.save(profile); + keepTrying("Profile with id " + profileId + " not found in the required time", () -> profileService.load(profileId), + Objects::nonNull, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); + + eventService.send(event); + keepTrying("Event has not been raised", () -> eventService.getEvent(eventId), Objects::nonNull, DEFAULT_TRYING_TIMEOUT, + DEFAULT_TRYING_TRIES); + + CloseableHttpResponse response = delete("/cxs/events/" + eventId); + Assert.assertEquals("Delete event should return 204 No Content", 204, response.getStatusLine().getStatusCode()); + + waitForNullValue("Event still present after deletion via REST", () -> eventService.getEvent(eventId), DEFAULT_TRYING_TIMEOUT, + DEFAULT_TRYING_TRIES); + } + } diff --git a/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java b/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java index 353bc49a2..dff72a1dc 100644 --- a/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java @@ -16,6 +16,7 @@ */ package org.apache.unomi.itests; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.unomi.api.*; import org.apache.unomi.api.conditions.Condition; import org.apache.unomi.api.query.Query; @@ -535,4 +536,25 @@ public void testPurgeSessions() throws Exception { } } + + @Test + public void testDeleteSessionViaRest() throws Exception { + String profileId = "test-delete-session-profile-id-" + System.currentTimeMillis(); + String sessionId = "test-delete-session-id-" + System.currentTimeMillis(); + Profile profile = new Profile(profileId); + profileService.save(profile); + keepTrying("Profile with id " + profileId + " not found in the required time", () -> profileService.load(profileId), + Objects::nonNull, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); + + Session session = new Session(sessionId, profile, new Date(), "test-delete-session-scope"); + profileService.saveSession(session); + keepTrying("Session with id " + sessionId + " not found in the required time", () -> profileService.loadSession(sessionId), + Objects::nonNull, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); + + CloseableHttpResponse response = delete("/cxs/profiles/sessions/" + sessionId); + assertEquals("Delete session should return 204 No Content", 204, response.getStatusLine().getStatusCode()); + + waitForNullValue("Session still present after deletion via REST", () -> profileService.loadSession(sessionId), + DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); + } } diff --git a/itests/src/test/java/org/apache/unomi/itests/shell/OtherCommandsIT.java b/itests/src/test/java/org/apache/unomi/itests/shell/OtherCommandsIT.java index 9f5a193fa..cde51a261 100644 --- a/itests/src/test/java/org/apache/unomi/itests/shell/OtherCommandsIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/shell/OtherCommandsIT.java @@ -16,9 +16,13 @@ */ package org.apache.unomi.itests.shell; +import org.apache.unomi.api.Metadata; +import org.apache.unomi.api.conditions.Condition; +import org.apache.unomi.api.rules.Rule; import org.junit.Assert; import org.junit.Test; +import java.util.Collections; import java.util.List; /** @@ -30,25 +34,42 @@ public class OtherCommandsIT extends ShellCommandsBaseIT { public void testRuleResetStats() throws Exception { String output = executeCommandAndGetOutput("unomi:rule-reset-stats"); // Should confirm statistics were reset - Assert.assertTrue("Should confirm rule statistics reset", + Assert.assertTrue("Should confirm rule statistics reset", output.contains("Rule statistics successfully reset")); } @Test public void testListInvalidObjects() throws Exception { - String output = executeCommandAndGetOutput("unomi:list-invalid-objects"); - assertContainsAny(output, new String[]{ - "Invalid Objects Summary", - "Total invalid objects:", - "No invalid objects found" - }, "Should show invalid objects summary or indicate none found"); + // Seed a genuinely invalid rule (unresolvable condition type) so the command has + // something to report - otherwise this test only ever exercises the "no invalid + // objects found" branch and never validates the summary/table rendering. + String ruleId = "test-invalid-rule-" + System.currentTimeMillis(); + Metadata metadata = new Metadata(ruleId); + metadata.setName(ruleId + "_name"); + metadata.setScope("systemscope"); - if (output.contains("Total invalid objects:")) { - validateNumericValuesInOutput(output, new String[]{"Total invalid objects:"}, false); - } + Condition invalidCondition = new Condition(); + invalidCondition.setConditionTypeId("nonExistentConditionType-" + System.currentTimeMillis()); + + Rule invalidRule = new Rule(metadata); + invalidRule.setCondition(invalidCondition); + invalidRule.setActions(Collections.emptyList()); - if (output.contains("Object Type") && output.contains("Object ID")) { + try { + rulesService.setRule(invalidRule); + keepTrying("Rule should be tracked as invalid", + () -> definitionsService.getTypeResolutionService().isInvalid("rules", ruleId), + Boolean.TRUE::equals, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); + + String output = executeCommandAndGetOutput("unomi:list-invalid-objects"); + Assert.assertTrue("Should show invalid objects summary", output.contains("Invalid Objects Summary")); + Assert.assertTrue("Should report at least one invalid object", output.contains("Total invalid objects:")); + validateNumericValuesInOutput(output, new String[]{"Total invalid objects:"}, false); validateTableHeaders(output, new String[]{"Object Type", "Object ID"}); + Assert.assertTrue("Should list the invalid rule's id", output.contains(ruleId)); + } finally { + rulesService.removeRule(ruleId); + definitionsService.getTypeResolutionService().markValid("rules", ruleId); } } diff --git a/rest/unomi-postman-collection.json b/rest/unomi-postman-collection.json index 3adaa96a9..3cfe2a25e 100644 --- a/rest/unomi-postman-collection.json +++ b/rest/unomi-postman-collection.json @@ -2648,6 +2648,11 @@ "value": "profile-123", "type": "string" }, + { + "key": "eventId", + "value": "event-123", + "type": "string" + }, { "key": "tenantId", "value": "mytenant", From 50227e4e1b91e27afba602444a0bf5f323ed2a37 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Sat, 11 Jul 2026 18:16:59 +0200 Subject: [PATCH 4/4] UNOMI-875: Fix ES rollover-index delete, ISM auto-attach, and tracing round-trip - ElasticSearchPersistenceServiceImpl.remove() targeted the wildcard search pattern instead of the write alias for rollover types, so ES silently failed to delete events/sessions while still reporting 204 (found via the new delete-endpoint ITs, OpenSearch was unaffected). - OpenSearch ISM policy now includes an ism_template so indices created by ISM's own rollover action get managed too, not just the first index Unomi creates directly. - TraceNode's computed duration getter broke JSON round-tripping; add jackson-annotations to tracing-api and @JsonIgnoreProperties(ignoreUnknown) to tolerate it. - Fix testListInvalidObjects to seed state via TypeResolutionService directly instead of rulesService.setRule(), which throws for a never-before-seen invalid rule instead of marking it invalid. --- .../unomi/itests/shell/OtherCommandsIT.java | 40 +++++++------------ .../ElasticSearchPersistenceServiceImpl.java | 5 ++- .../OpenSearchPersistenceServiceImpl.java | 29 ++++++++++---- tracing/tracing-api/pom.xml | 5 +++ .../apache/unomi/tracing/api/TraceNode.java | 3 ++ 5 files changed, 49 insertions(+), 33 deletions(-) diff --git a/itests/src/test/java/org/apache/unomi/itests/shell/OtherCommandsIT.java b/itests/src/test/java/org/apache/unomi/itests/shell/OtherCommandsIT.java index cde51a261..eba517fc9 100644 --- a/itests/src/test/java/org/apache/unomi/itests/shell/OtherCommandsIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/shell/OtherCommandsIT.java @@ -16,13 +16,10 @@ */ package org.apache.unomi.itests.shell; -import org.apache.unomi.api.Metadata; -import org.apache.unomi.api.conditions.Condition; -import org.apache.unomi.api.rules.Rule; +import org.apache.unomi.api.services.TypeResolutionService; import org.junit.Assert; import org.junit.Test; -import java.util.Collections; import java.util.List; /** @@ -40,36 +37,29 @@ public void testRuleResetStats() throws Exception { @Test public void testListInvalidObjects() throws Exception { - // Seed a genuinely invalid rule (unresolvable condition type) so the command has - // something to report - otherwise this test only ever exercises the "no invalid - // objects found" branch and never validates the summary/table rendering. - String ruleId = "test-invalid-rule-" + System.currentTimeMillis(); - Metadata metadata = new Metadata(ruleId); - metadata.setName(ruleId + "_name"); - metadata.setScope("systemscope"); - - Condition invalidCondition = new Condition(); - invalidCondition.setConditionTypeId("nonExistentConditionType-" + System.currentTimeMillis()); - - Rule invalidRule = new Rule(metadata); - invalidRule.setCondition(invalidCondition); - invalidRule.setActions(Collections.emptyList()); + // Seed a genuinely invalid object directly in the tracking service so the command has + // something to report - otherwise this test only ever exercises the "no invalid objects + // found" branch and never validates the summary/table rendering. Going through + // rulesService.setRule() instead is unreliable here: RulesServiceImpl.ensureRuleResolved() + // skips resolution entirely for a rule that's never been seen before (it only resolves + // rules that were already tracked invalid or already had missingPlugins set), so a + // brand-new invalid rule causes setRule() to throw IllegalArgumentException rather than + // being marked invalid. + String objectType = "rules"; + String objectId = "test-invalid-rule-" + System.currentTimeMillis(); + TypeResolutionService typeResolutionService = definitionsService.getTypeResolutionService(); try { - rulesService.setRule(invalidRule); - keepTrying("Rule should be tracked as invalid", - () -> definitionsService.getTypeResolutionService().isInvalid("rules", ruleId), - Boolean.TRUE::equals, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); + typeResolutionService.markInvalid(objectType, objectId, "Unresolved condition type: nonExistentConditionType"); String output = executeCommandAndGetOutput("unomi:list-invalid-objects"); Assert.assertTrue("Should show invalid objects summary", output.contains("Invalid Objects Summary")); Assert.assertTrue("Should report at least one invalid object", output.contains("Total invalid objects:")); validateNumericValuesInOutput(output, new String[]{"Total invalid objects:"}, false); validateTableHeaders(output, new String[]{"Object Type", "Object ID"}); - Assert.assertTrue("Should list the invalid rule's id", output.contains(ruleId)); + Assert.assertTrue("Should list the invalid object's id", output.contains(objectId)); } finally { - rulesService.removeRule(ruleId); - definitionsService.getTypeResolutionService().markValid("rules", ruleId); + typeResolutionService.markValid(objectType, objectId); } } diff --git a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java index 411273647..d900c6975 100644 --- a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java +++ b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java @@ -1381,7 +1381,10 @@ protected Boolean execute(Object... args) throws Exception { itemType = customItemType; } String documentId = getDocumentIDForItemType(itemId, itemType); - String index = getIndexNameForQuery(itemType); + // Single-document delete needs a concrete index/alias, not the wildcard + // pattern getIndexNameForQuery() returns for rollover types (e.g. "context-session-*"), + // which ES's Delete API rejects with index_not_found_exception. + String index = getIndex(itemType); esClient.delete(DeleteRequest.of(builder -> builder.index(index).id(documentId))); if (MetadataItem.class.isAssignableFrom(clazz)) { diff --git a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java index c8c3969b9..0888dbc36 100644 --- a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java +++ b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; import jakarta.json.stream.JsonParser; @@ -1398,6 +1399,23 @@ protected Boolean execute(Object... args) throws IOException { rolloverActionBuilder.add("min_index_age", rolloverMaxAge); } + // Index patterns for ism_template auto-attachment, using the actual rollover index glob + // (e.g. "context-event-*") rather than raw item type names (e.g. "event") - the latter + // never match real index names, which is why an earlier attempt at ism_template here was + // abandoned in favor of the explicit attachRolloverPolicy() call alone. That explicit call + // only fires when Unomi itself creates the first rollover index (-000001); subsequent + // indices created by ISM's own rollover action never go through Unomi's code path, so they + // only get the inert policy_id setting from the index template and are never actually + // managed. ism_template auto-attaches to any index matching the pattern - including those + // rollover-created indices - so both mechanisms are kept: explicit attach for immediate + // management of the first index, ism_template as the durable fallback for every index after. + JsonArrayBuilder indexPatternsBuilder = Json.createArrayBuilder(); + if (rolloverIndices != null) { + for (String rolloverItemType : rolloverIndices) { + indexPatternsBuilder.add(getRolloverIndexForQuery(rolloverItemType)); + } + } + // Build the policy from the inside out JsonObjectBuilder policyBuilder = Json.createObjectBuilder() .add("states", Json.createArrayBuilder() @@ -1408,13 +1426,10 @@ protected Boolean execute(Object... args) throws IOException { .add("rollover", rolloverActionBuilder))) .add("transitions", Json.createArrayBuilder()))) .add("default_state", "ingest") - .add("description", "Rollover lifecycle policy"); - - // Policy attachment is handled deterministically via the plugins.index_state_management.policy_id - // custom setting on each per-item rollover index template (internalCreateRolloverTemplate), - // not via ism_template pattern-matching: rolloverIndices holds item type names (e.g. "event"), - // which never match real index names (e.g. "context-event-000001"), so an ism_template block - // here would never auto-attach the policy. + .add("description", "Rollover lifecycle policy") + .add("ism_template", Json.createObjectBuilder() + .add("index_patterns", indexPatternsBuilder) + .add("priority", 100)); // Wrap the policy and create JsonObject policyWrapper = Json.createObjectBuilder() diff --git a/tracing/tracing-api/pom.xml b/tracing/tracing-api/pom.xml index 379a3b6c7..f52ea6040 100644 --- a/tracing/tracing-api/pom.xml +++ b/tracing/tracing-api/pom.xml @@ -47,6 +47,11 @@ org.osgi.service.component.annotations provided + + com.fasterxml.jackson.core + jackson-annotations + provided + diff --git a/tracing/tracing-api/src/main/java/org/apache/unomi/tracing/api/TraceNode.java b/tracing/tracing-api/src/main/java/org/apache/unomi/tracing/api/TraceNode.java index 3224ca7dd..42eaf755a 100644 --- a/tracing/tracing-api/src/main/java/org/apache/unomi/tracing/api/TraceNode.java +++ b/tracing/tracing-api/src/main/java/org/apache/unomi/tracing/api/TraceNode.java @@ -16,6 +16,8 @@ */ package org.apache.unomi.tracing.api; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; @@ -25,6 +27,7 @@ * Represents a node in the request tracing tree structure. * Each node contains information about an operation, its timing, and any child operations. */ +@JsonIgnoreProperties(ignoreUnknown = true) public class TraceNode implements Serializable { private String operationType; private String description;