diff --git a/graylog2-server/src/main/java/org/graylog/collectors/FleetTransactionLogService.java b/graylog2-server/src/main/java/org/graylog/collectors/FleetTransactionLogService.java index 27e68fc5c5cf..7fdf4da52eab 100644 --- a/graylog2-server/src/main/java/org/graylog/collectors/FleetTransactionLogService.java +++ b/graylog2-server/src/main/java/org/graylog/collectors/FleetTransactionLogService.java @@ -20,15 +20,17 @@ import com.mongodb.client.MongoCollection; import com.mongodb.client.model.Filters; import com.mongodb.client.model.Indexes; +import com.mongodb.client.model.Sorts; import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.model.Updates; import jakarta.annotation.Nullable; import jakarta.inject.Inject; import jakarta.inject.Singleton; import org.apache.commons.lang3.StringUtils; -import org.bson.Document; import org.bson.conversions.Bson; import org.graylog.collectors.db.CoalescedActions; +import org.graylog.collectors.db.FleetReassignedPayload; +import org.graylog.collectors.db.MarkerPayload; import org.graylog.collectors.db.MarkerType; import org.graylog.collectors.db.TransactionMarker; import org.graylog2.database.MongoCollections; @@ -39,24 +41,29 @@ import java.util.List; import java.util.Set; +import static org.graylog.collectors.db.TransactionMarker.FIELD_CREATED_AT; +import static org.graylog.collectors.db.TransactionMarker.FIELD_CREATED_BY; +import static org.graylog.collectors.db.TransactionMarker.FIELD_CREATED_BY_USER; +import static org.graylog.collectors.db.TransactionMarker.FIELD_ID; +import static org.graylog.collectors.db.TransactionMarker.FIELD_PAYLOAD; +import static org.graylog.collectors.db.TransactionMarker.FIELD_TARGET; +import static org.graylog.collectors.db.TransactionMarker.FIELD_TARGET_ID; +import static org.graylog.collectors.db.TransactionMarker.FIELD_TYPE; +import static org.graylog.collectors.db.TransactionMarker.TARGET_COLLECTOR; +import static org.graylog.collectors.db.TransactionMarker.TARGET_FLEET; + @Singleton public class FleetTransactionLogService { static final String COLLECTION_NAME = "collector_fleet_transaction_log"; static final String SEQUENCE_TOPIC = "fleet_txn_log"; - static final String FIELD_TARGET = "target"; - static final String FIELD_TARGET_ID = "target_id"; - static final String FIELD_TYPE = "type"; - static final String FIELD_PAYLOAD = "payload"; - static final String FIELD_CREATED_AT = "created_at"; - static final String FIELD_CREATED_BY = "created_by"; - static final String FIELD_CREATED_BY_USER = "created_by_user"; + /** * The maximum number of bulk action targets we allow for a transaction log entry. */ public static final int MAX_BULK_TARGET_SIZE = 100; - private final MongoCollection collection; + private final MongoCollection collection; private final MongoSequenceService sequenceService; private final NodeId nodeId; @@ -64,7 +71,7 @@ public class FleetTransactionLogService { public FleetTransactionLogService(MongoCollections mongoCollections, MongoSequenceService sequenceService, NodeId nodeId) { - this.collection = mongoCollections.nonEntityCollection(COLLECTION_NAME, Document.class) + this.collection = mongoCollections.nonEntityCollection(COLLECTION_NAME, TransactionMarker.class) .withWriteConcern(WriteConcern.JOURNALED); this.sequenceService = sequenceService; this.nodeId = nodeId; @@ -73,7 +80,7 @@ public FleetTransactionLogService(MongoCollections mongoCollections, collection.createIndex( Indexes.compoundIndex( Indexes.ascending(FIELD_TARGET, FIELD_TARGET_ID), - Indexes.ascending("_id") + Indexes.ascending(FIELD_ID) ) ); } @@ -84,27 +91,27 @@ public long appendFleetMarker(String fleetId, MarkerType type) { } public long appendFleetMarker(Set fleetIds, MarkerType type) { - return appendMarker(TransactionMarker.TARGET_FLEET, fleetIds, type, null); + return appendMarker(TARGET_FLEET, fleetIds, type, null); } - public long appendCollectorMarker(Set instanceUids, MarkerType type, @Nullable Document payload) { + public long appendCollectorMarker(Set instanceUids, MarkerType type, @Nullable MarkerPayload payload) { if (instanceUids == null || instanceUids.isEmpty()) { throw new IllegalArgumentException("instanceUids must not be empty"); } if (instanceUids.size() > MAX_BULK_TARGET_SIZE) { throw new IllegalArgumentException("instanceUids must not exceed " + MAX_BULK_TARGET_SIZE + " elements, got " + instanceUids.size()); } - return appendMarker(TransactionMarker.TARGET_COLLECTOR, instanceUids, type, payload); + return appendMarker(TARGET_COLLECTOR, instanceUids, type, payload); } - private long appendMarker(String target, Set targetIds, MarkerType type, @Nullable Document payload) { + private long appendMarker(String target, Set targetIds, MarkerType type, @Nullable MarkerPayload payload) { if (targetIds == null || targetIds.isEmpty() || targetIds.stream().noneMatch(StringUtils::isNotBlank)) { throw new IllegalArgumentException("targetIds must not be empty"); } final long seq = sequenceService.incrementAndGet(SEQUENCE_TOPIC); collection.updateOne( - Filters.eq("_id", seq), + Filters.eq(FIELD_ID, seq), Updates.combine( Updates.set(FIELD_TARGET, target), Updates.set(FIELD_TARGET_ID, targetIds), @@ -126,51 +133,35 @@ public List getUnprocessedMarkers(@Nullable String fleetId, throw new IllegalArgumentException("At least one of fleetId or instanceUid must be non-null"); } - final Bson seqFilter = Filters.gt("_id", lastProcessedSeq); + final Bson seqFilter = Filters.gt(FIELD_ID, lastProcessedSeq); final Bson filter; if (fleetId != null && instanceUid != null) { filter = Filters.and(seqFilter, Filters.or( Filters.and( - Filters.eq(FIELD_TARGET, TransactionMarker.TARGET_FLEET), + Filters.eq(FIELD_TARGET, TARGET_FLEET), Filters.eq(FIELD_TARGET_ID, fleetId) ), Filters.and( - Filters.eq(FIELD_TARGET, TransactionMarker.TARGET_COLLECTOR), + Filters.eq(FIELD_TARGET, TARGET_COLLECTOR), Filters.eq(FIELD_TARGET_ID, instanceUid) ) )); } else if (fleetId != null) { filter = Filters.and(seqFilter, - Filters.eq(FIELD_TARGET, TransactionMarker.TARGET_FLEET), + Filters.eq(FIELD_TARGET, TARGET_FLEET), Filters.eq(FIELD_TARGET_ID, fleetId)); } else { filter = Filters.and(seqFilter, - Filters.eq(FIELD_TARGET, TransactionMarker.TARGET_COLLECTOR), + Filters.eq(FIELD_TARGET, TARGET_COLLECTOR), Filters.eq(FIELD_TARGET_ID, instanceUid)); } return collection.find(filter) - .sort(new Document("_id", 1)) - .map(this::documentToMarker) + .sort(Sorts.ascending(FIELD_ID)) .into(new ArrayList<>()); } - private TransactionMarker documentToMarker(Document doc) { - final String rawType = doc.getString(FIELD_TYPE); - final var createdAt = doc.getDate(FIELD_CREATED_AT); - return new TransactionMarker( - doc.getLong("_id"), - doc.getString(FIELD_TARGET), - Set.copyOf(doc.getList(FIELD_TARGET_ID, String.class)), - MarkerType.fromString(rawType), - rawType, - doc.get(FIELD_PAYLOAD, Document.class), - createdAt != null ? createdAt.toInstant() : null, - doc.getString(FIELD_CREATED_BY_USER) - ); - } - @Nullable private static String resolveCurrentUsername() { try { @@ -194,9 +185,8 @@ public List getRecentMarkers(int limit) { MarkerType.FLEET_REASSIGNED.name()); return collection.find(filter) - .sort(new Document("_id", -1)) + .sort(Sorts.descending(FIELD_ID)) .limit(limit) - .map(this::documentToMarker) .into(new ArrayList<>()); } @@ -233,13 +223,13 @@ static CoalescedActions doCoalesce(List markers) { if (latestReassignment != null) { // Fleet reassignment: recompute config from new fleet, discard fleet-level markers recomputeConfig = true; - newFleetId = latestReassignment.payload() != null - ? latestReassignment.payload().getString("new_fleet_id") + newFleetId = latestReassignment.payload() instanceof FleetReassignedPayload(String fleetId) + ? fleetId : null; // Only process collector-level commands (fleet-level ones are from old fleet) for (var marker : markers) { - if (TransactionMarker.TARGET_COLLECTOR.equals(marker.target())) { + if (TARGET_COLLECTOR.equals(marker.target())) { switch (marker.type()) { case RESTART -> restart = true; case DISCOVERY_RUN -> runDiscovery = true; @@ -267,7 +257,7 @@ static CoalescedActions doCoalesce(List markers) { } // Package-private for test access - MongoCollection getCollection() { + MongoCollection getCollection() { return collection; } } diff --git a/graylog2-server/src/main/java/org/graylog/collectors/db/FleetReassignedPayload.java b/graylog2-server/src/main/java/org/graylog/collectors/db/FleetReassignedPayload.java new file mode 100644 index 000000000000..c729852ca1fc --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/collectors/db/FleetReassignedPayload.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.collectors.db; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonTypeName("FLEET_REASSIGNED") +public record FleetReassignedPayload( + @JsonProperty("new_fleet_id") String newFleetId +) implements MarkerPayload { +} diff --git a/graylog2-server/src/main/java/org/graylog/collectors/db/MarkerPayload.java b/graylog2-server/src/main/java/org/graylog/collectors/db/MarkerPayload.java new file mode 100644 index 000000000000..c5803cdec91e --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/collectors/db/MarkerPayload.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.collectors.db; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import static org.graylog.collectors.db.TransactionMarker.FIELD_TYPE; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = FIELD_TYPE, defaultImpl = Void.class) +@JsonSubTypes(@JsonSubTypes.Type(FleetReassignedPayload.class)) +public sealed interface MarkerPayload permits FleetReassignedPayload { +} diff --git a/graylog2-server/src/main/java/org/graylog/collectors/db/MarkerType.java b/graylog2-server/src/main/java/org/graylog/collectors/db/MarkerType.java index 6ff1165b3d3e..26a038e774df 100644 --- a/graylog2-server/src/main/java/org/graylog/collectors/db/MarkerType.java +++ b/graylog2-server/src/main/java/org/graylog/collectors/db/MarkerType.java @@ -16,12 +16,12 @@ */ package org.graylog.collectors.db; +import com.fasterxml.jackson.annotation.JsonCreator; + /** * Types of markers in the fleet transaction log. Stored as strings in MongoDB. * Unknown values (from newer server versions) are parsed as {@link #UNKNOWN} to ensure * forward compatibility across cluster upgrades. - * - * This is persisted in a non-entity collection, thus it has no Jackson mapping. */ public enum MarkerType { CONFIG_CHANGED, @@ -35,6 +35,7 @@ public enum MarkerType { * Parse a marker type from its string representation, returning {@link #UNKNOWN} * for unrecognized values instead of throwing. */ + @JsonCreator public static MarkerType fromString(String value) { try { return valueOf(value); diff --git a/graylog2-server/src/main/java/org/graylog/collectors/db/TransactionMarker.java b/graylog2-server/src/main/java/org/graylog/collectors/db/TransactionMarker.java index 9cb7e63907c8..16128e42df99 100644 --- a/graylog2-server/src/main/java/org/graylog/collectors/db/TransactionMarker.java +++ b/graylog2-server/src/main/java/org/graylog/collectors/db/TransactionMarker.java @@ -16,34 +16,43 @@ */ package org.graylog.collectors.db; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.annotation.Nullable; -import org.bson.Document; +import org.mongojack.Id; import java.time.Instant; import java.util.Set; /** - * A raw marker entry from the fleet transaction log. + * A marker entry from the fleet transaction log. * - * This is persisted in a non-entity collection, thus it has no Jackson mapping. - * - * @param seq sequence number (the _id in MongoDB) - * @param target "fleet" or "collector" - * @param targetIds fleet IDs or collector instance UIDs (always a set, even for single targets) - * @param type parsed marker type - * @param rawType original string from MongoDB (for logging unknown types) - * @param payload optional type-specific data (e.g., new_fleet_id for FLEET_REASSIGNED) - * @param createdAt timestamp when the marker was created - * @param createdByUser username of the actor, or null for system-initiated actions + * @param seq sequence number (the _id in MongoDB) + * @param target "fleet" or "collector" + * @param targetIds fleet IDs or collector instance UIDs (always a set, even for single targets) + * @param type parsed marker type + * @param payload optional type-specific data (e.g., new_fleet_id for FLEET_REASSIGNED) + * @param createdAt timestamp when the marker was created + * @param createdBy node ID of the server that created the marker + * @param createdByUser username of the actor, or null for system-initiated actions */ -public record TransactionMarker(long seq, - String target, - Set targetIds, - MarkerType type, - String rawType, - @Nullable Document payload, - @Nullable Instant createdAt, - @Nullable String createdByUser) { +public record TransactionMarker( + @Id @JsonProperty(FIELD_ID) long seq, + @JsonProperty(FIELD_TARGET) String target, + @JsonProperty(FIELD_TARGET_ID) Set targetIds, + @JsonProperty(FIELD_TYPE) MarkerType type, + @JsonProperty(FIELD_PAYLOAD) @Nullable MarkerPayload payload, + @JsonProperty(FIELD_CREATED_AT) @Nullable Instant createdAt, + @JsonProperty(FIELD_CREATED_BY) @Nullable String createdBy, + @JsonProperty(FIELD_CREATED_BY_USER) @Nullable String createdByUser) { + + public static final String FIELD_ID = "_id"; + public static final String FIELD_TARGET = "target"; + public static final String FIELD_TARGET_ID = "target_id"; + public static final String FIELD_TYPE = "type"; + public static final String FIELD_PAYLOAD = "payload"; + public static final String FIELD_CREATED_AT = "created_at"; + public static final String FIELD_CREATED_BY = "created_by"; + public static final String FIELD_CREATED_BY_USER = "created_by_user"; public static final String TARGET_FLEET = "fleet"; public static final String TARGET_COLLECTOR = "collector"; diff --git a/graylog2-server/src/main/java/org/graylog/collectors/rest/CollectorInstancesResource.java b/graylog2-server/src/main/java/org/graylog/collectors/rest/CollectorInstancesResource.java index 17302bcbe99c..ec200fa5f498 100644 --- a/graylog2-server/src/main/java/org/graylog/collectors/rest/CollectorInstancesResource.java +++ b/graylog2-server/src/main/java/org/graylog/collectors/rest/CollectorInstancesResource.java @@ -40,7 +40,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.apache.shiro.authz.annotation.RequiresAuthentication; -import org.bson.Document; +import org.graylog.collectors.db.FleetReassignedPayload; import org.bson.conversions.Bson; import org.graylog.collectors.CollectorInstanceService; import org.graylog.collectors.CollectorsConfigService; @@ -316,7 +316,7 @@ public Response reassignInstances(@Valid @NotNull @RequestBody(required = true, txnLogService.appendCollectorMarker( permittedInstanceUids, MarkerType.FLEET_REASSIGNED, - new Document("new_fleet_id", targetFleetId)); + new FleetReassignedPayload(targetFleetId)); final AuditActor auditActor = AuditActor.user(getCurrentUser()); permittedInstances.values().forEach(dto -> diff --git a/graylog2-server/src/main/java/org/graylog/collectors/rest/CollectorsActivityResource.java b/graylog2-server/src/main/java/org/graylog/collectors/rest/CollectorsActivityResource.java index 9222221a39a1..b62c15bd0a24 100644 --- a/graylog2-server/src/main/java/org/graylog/collectors/rest/CollectorsActivityResource.java +++ b/graylog2-server/src/main/java/org/graylog/collectors/rest/CollectorsActivityResource.java @@ -19,6 +19,7 @@ import com.codahale.metrics.annotation.Timed; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Nullable; import jakarta.inject.Inject; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; @@ -33,8 +34,12 @@ import org.graylog.collectors.FleetTransactionLogService; import org.graylog.collectors.db.CollectorInstanceDTO; import org.graylog.collectors.db.FleetDTO; +import org.graylog.collectors.db.FleetReassignedPayload; import org.graylog.collectors.db.MarkerType; import org.graylog.collectors.db.TransactionMarker; +import org.graylog.collectors.rest.RecentActivityResponse.ActivityDetails; +import org.graylog.collectors.rest.RecentActivityResponse.FleetReassignedDetails; +import org.graylog.collectors.rest.RecentActivityResponse.TargetInfo; import org.graylog2.shared.rest.resources.RestResource; import org.graylog2.shared.security.RestPermissions; import org.graylog2.shared.users.UserService; @@ -133,7 +138,7 @@ private RecentActivityResponse.ActivityEntry toActivityEntry( } // Resolve targets - final List targets = new ArrayList<>(); + final List targets = new ArrayList<>(); for (final var targetId : marker.targetIds()) { final String id; final String name; @@ -162,19 +167,16 @@ private RecentActivityResponse.ActivityEntry toActivityEntry( name = "[deleted]"; } } - targets.add(new RecentActivityResponse.TargetInfo(id, name, marker.target())); + targets.add(new TargetInfo(id, name, marker.target())); } - // Resolve details - final Map details = resolveDetails(marker, fleetNames); - return new RecentActivityResponse.ActivityEntry( marker.seq(), marker.createdAt(), marker.type().name(), actor, targets, - details); + resolveDetails(marker, fleetNames)); } private String resolveInstanceHostname(Map instances, String instanceUid) { @@ -189,22 +191,21 @@ private String resolveInstanceHostname(Map instanc return instanceUid; } - private Map resolveDetails(TransactionMarker marker, Map fleetNames) { - if (marker.type() == MarkerType.FLEET_REASSIGNED && marker.payload() != null) { - final String newFleetId = marker.payload().getString("new_fleet_id"); - if (newFleetId != null) { - if (fleetNames.containsKey(newFleetId)) { - if (isPermitted(CollectorsPermissions.FLEET_READ, newFleetId)) { - return Map.of( - "new_fleet_id", newFleetId, - "new_fleet_name", fleetNames.get(newFleetId)); - } - return Map.of(); + @Nullable + private ActivityDetails resolveDetails(TransactionMarker marker, Map fleetNames) { + if (marker.type() == MarkerType.FLEET_REASSIGNED + && marker.payload() instanceof FleetReassignedPayload(String newFleetId)) { + if (fleetNames.containsKey(newFleetId)) { + if (isPermitted(CollectorsPermissions.FLEET_READ, newFleetId)) { + return new FleetReassignedDetails( + new TargetInfo(newFleetId, fleetNames.get(newFleetId), TransactionMarker.TARGET_FLEET)); } - return Map.of("new_fleet_name", "[deleted]"); + return null; } + return new FleetReassignedDetails( + new TargetInfo(null, "[deleted]", TransactionMarker.TARGET_FLEET)); } - return Map.of(); + return null; } private Map resolveUserDisplayNames(Set usernames) { diff --git a/graylog2-server/src/main/java/org/graylog/collectors/rest/RecentActivityResponse.java b/graylog2-server/src/main/java/org/graylog/collectors/rest/RecentActivityResponse.java index 5f109ef92a42..16b67e7bcf1d 100644 --- a/graylog2-server/src/main/java/org/graylog/collectors/rest/RecentActivityResponse.java +++ b/graylog2-server/src/main/java/org/graylog/collectors/rest/RecentActivityResponse.java @@ -22,7 +22,6 @@ import java.time.Instant; import java.util.List; -import java.util.Map; public record RecentActivityResponse(@JsonProperty("activities") List activities) { @@ -32,7 +31,7 @@ public record ActivityEntry( @JsonProperty("type") String type, @JsonProperty("actor") @Nullable ActorInfo actor, @JsonProperty("targets") List targets, - @JsonProperty("details") @JsonInclude(JsonInclude.Include.ALWAYS) Map details) { + @JsonProperty("details") @JsonInclude(JsonInclude.Include.ALWAYS) @Nullable ActivityDetails details) { } public record ActorInfo( @@ -45,4 +44,12 @@ public record TargetInfo( @JsonProperty("name") String name, @JsonProperty("type") String type) { } + + public sealed interface ActivityDetails permits FleetReassignedDetails { + } + + public record FleetReassignedDetails( + @JsonProperty("destination_fleet") TargetInfo destinationFleet + ) implements ActivityDetails { + } } diff --git a/graylog2-server/src/test/java/org/graylog/collectors/FleetTransactionLogCoalesceTest.java b/graylog2-server/src/test/java/org/graylog/collectors/FleetTransactionLogCoalesceTest.java index 774491e13a2d..a7ea0d6694e8 100644 --- a/graylog2-server/src/test/java/org/graylog/collectors/FleetTransactionLogCoalesceTest.java +++ b/graylog2-server/src/test/java/org/graylog/collectors/FleetTransactionLogCoalesceTest.java @@ -16,8 +16,8 @@ */ package org.graylog.collectors; -import org.bson.Document; import org.graylog.collectors.db.CoalescedActions; +import org.graylog.collectors.db.FleetReassignedPayload; import org.graylog.collectors.db.MarkerType; import org.graylog.collectors.db.TransactionMarker; import org.junit.jupiter.api.Test; @@ -147,7 +147,7 @@ void fleetReassignedPreservesCollectorLevelCommands() { @Test void unknownMarkerTypesAreSkipped() { var markers = List.of( - new TransactionMarker(10, TARGET_FLEET, Set.of("fleet-1"), MarkerType.UNKNOWN, "FUTURE_TYPE", null, Instant.now(), "system"), + new TransactionMarker(10, TARGET_FLEET, Set.of("fleet-1"), MarkerType.UNKNOWN, null, Instant.now(), "test-node", "system"), marker(15, TARGET_FLEET, "fleet-1", MarkerType.CONFIG_CHANGED) ); CoalescedActions actions = FleetTransactionLogService.doCoalesce(markers); @@ -159,7 +159,7 @@ void unknownMarkerTypesAreSkipped() { @Test void maxSeqIncludesUnknownMarkers() { var markers = List.of( - new TransactionMarker(20, TARGET_FLEET, Set.of("fleet-1"), MarkerType.UNKNOWN, "FUTURE_TYPE", null, Instant.now(), "system") + new TransactionMarker(20, TARGET_FLEET, Set.of("fleet-1"), MarkerType.UNKNOWN, null, Instant.now(), "test-node", "system") ); CoalescedActions actions = FleetTransactionLogService.doCoalesce(markers); assertThat(actions.recomputeConfig()).isFalse(); @@ -184,12 +184,11 @@ void mixedMarkerTypesProduceAllFlags() { // --- helpers --- private static TransactionMarker marker(long seq, String target, String targetId, MarkerType type) { - return new TransactionMarker(seq, target, Set.of(targetId), type, type.name(), null, Instant.now(), "system"); + return new TransactionMarker(seq, target, Set.of(targetId), type, null, Instant.now(), "test-node", "system"); } private static TransactionMarker reassignmentMarker(long seq, String instanceUid, String newFleetId) { return new TransactionMarker(seq, TransactionMarker.TARGET_COLLECTOR, Set.of(instanceUid), - MarkerType.FLEET_REASSIGNED, MarkerType.FLEET_REASSIGNED.name(), - new Document("new_fleet_id", newFleetId), Instant.now(), "system"); + MarkerType.FLEET_REASSIGNED, new FleetReassignedPayload(newFleetId), Instant.now(), "test-node", "system"); } } diff --git a/graylog2-server/src/test/java/org/graylog/collectors/FleetTransactionLogServiceTest.java b/graylog2-server/src/test/java/org/graylog/collectors/FleetTransactionLogServiceTest.java index 83f55810f681..061887b1f782 100644 --- a/graylog2-server/src/test/java/org/graylog/collectors/FleetTransactionLogServiceTest.java +++ b/graylog2-server/src/test/java/org/graylog/collectors/FleetTransactionLogServiceTest.java @@ -16,8 +16,10 @@ */ package org.graylog.collectors; +import com.mongodb.client.MongoCollection; import com.mongodb.client.model.Filters; import org.bson.Document; +import org.graylog.collectors.db.FleetReassignedPayload; import org.graylog.collectors.db.MarkerType; import org.graylog.collectors.db.TransactionMarker; import org.graylog.testing.mongodb.MongoDBExtension; @@ -43,6 +45,7 @@ class FleetTransactionLogServiceTest { private static final NodeId NODE_ID = new SimpleNodeId("test-node-1"); private FleetTransactionLogService service; + private MongoCollection rawCollection; @BeforeEach void setUp(MongoCollections mongoCollections) { @@ -52,6 +55,7 @@ void setUp(MongoCollections mongoCollections) { Set.of(FleetTransactionLogService.SEQUENCE_TOPIC) ); service = new FleetTransactionLogService(mongoCollections, sequenceService, NODE_ID); + rawCollection = mongoCollections.nonEntityCollection(FleetTransactionLogService.COLLECTION_NAME, Document.class); } // --- Write path tests --- @@ -62,15 +66,14 @@ void appendFleetMarkerStoresCorrectDocument() { assertThat(seq).isEqualTo(1L); - var doc = service.getCollection().find(Filters.eq("_id", 1L)).first(); - assertThat(doc).isNotNull(); - assertThat(doc.getString(FleetTransactionLogService.FIELD_TARGET)).isEqualTo("fleet"); - assertThat(doc.getList(FleetTransactionLogService.FIELD_TARGET_ID, String.class)).containsExactly("fleet-1"); - assertThat(doc.getString(FleetTransactionLogService.FIELD_TYPE)).isEqualTo("CONFIG_CHANGED"); - assertThat(doc.get(FleetTransactionLogService.FIELD_PAYLOAD)).isNull(); - assertThat(doc.get(FleetTransactionLogService.FIELD_CREATED_AT)).isNotNull(); - assertThat(doc.getString(FleetTransactionLogService.FIELD_CREATED_BY)).isEqualTo("test-node-1"); - assertThat(doc.getString("created_by_user")).isNull(); + var marker = service.getCollection().find(Filters.eq("_id", 1L)).first(); + assertThat(marker).isNotNull(); + assertThat(marker.target()).isEqualTo("fleet"); + assertThat(marker.targetIds()).containsExactly("fleet-1"); + assertThat(marker.type()).isEqualTo(MarkerType.CONFIG_CHANGED); + assertThat(marker.payload()).isNull(); + assertThat(marker.createdAt()).isNotNull(); + assertThat(marker.createdByUser()).isNull(); } @Test @@ -87,18 +90,18 @@ void appendFleetMarkerDoesntAllowEmptyTarget() { @Test void appendCollectorMarkerWithPayloadStoresPayload() { - var payload = new Document("new_fleet_id", "fleet-B"); + var payload = new FleetReassignedPayload("fleet-B"); long seq = service.appendCollectorMarker(Set.of("inst-1"), MarkerType.FLEET_REASSIGNED, payload); assertThat(seq).isEqualTo(1L); - var doc = service.getCollection().find(Filters.eq("_id", 1L)).first(); - assertThat(doc).isNotNull(); - assertThat(doc.getString(FleetTransactionLogService.FIELD_TARGET)).isEqualTo("collector"); - assertThat(doc.getList(FleetTransactionLogService.FIELD_TARGET_ID, String.class)).containsExactly("inst-1"); - assertThat(doc.getString(FleetTransactionLogService.FIELD_TYPE)).isEqualTo("FLEET_REASSIGNED"); - assertThat(doc.get(FleetTransactionLogService.FIELD_PAYLOAD, Document.class).getString("new_fleet_id")) - .isEqualTo("fleet-B"); + var marker = service.getCollection().find(Filters.eq("_id", 1L)).first(); + assertThat(marker).isNotNull(); + assertThat(marker.target()).isEqualTo("collector"); + assertThat(marker.targetIds()).containsExactly("inst-1"); + assertThat(marker.type()).isEqualTo(MarkerType.FLEET_REASSIGNED); + assertThat(marker.payload()).isInstanceOf(FleetReassignedPayload.class); + assertThat(((FleetReassignedPayload) marker.payload()).newFleetId()).isEqualTo("fleet-B"); } @Test @@ -177,18 +180,17 @@ void getUnprocessedMarkersReturnsEmptyWhenNoneMatch() { @Test void getUnprocessedMarkersDeserializesUnknownTypesAsUnknown() { - // Insert a marker with an unknown type directly into the collection - service.getCollection().insertOne(new Document("_id", 999L) - .append(FleetTransactionLogService.FIELD_TARGET, "fleet") - .append(FleetTransactionLogService.FIELD_TARGET_ID, List.of("fleet-1")) - .append(FleetTransactionLogService.FIELD_TYPE, "FUTURE_MARKER_TYPE") - .append(FleetTransactionLogService.FIELD_CREATED_BY, "test")); + // Insert a marker with an unknown type directly into the raw collection + rawCollection.insertOne(new Document("_id", 999L) + .append("target", "fleet") + .append("target_id", List.of("fleet-1")) + .append("type", "FUTURE_MARKER_TYPE") + .append("created_by", "test")); List markers = service.getUnprocessedMarkers("fleet-1", null, 0L); assertThat(markers).hasSize(1); assertThat(markers.getFirst().type()).isEqualTo(MarkerType.UNKNOWN); - assertThat(markers.getFirst().rawType()).isEqualTo("FUTURE_MARKER_TYPE"); } // --- Recent markers tests --- @@ -210,12 +212,12 @@ void getRecentMarkersReturnsLastNDescending() { void getRecentMarkersExcludesUnknownType() { service.appendFleetMarker("fleet-1", MarkerType.CONFIG_CHANGED); // seq 1 - // Insert an UNKNOWN marker directly - service.getCollection().insertOne(new Document("_id", 999L) - .append(FleetTransactionLogService.FIELD_TARGET, "fleet") - .append(FleetTransactionLogService.FIELD_TARGET_ID, List.of("fleet-1")) - .append(FleetTransactionLogService.FIELD_TYPE, "FUTURE_MARKER_TYPE") - .append(FleetTransactionLogService.FIELD_CREATED_BY, "test")); + // Insert an UNKNOWN marker directly via raw collection + rawCollection.insertOne(new Document("_id", 999L) + .append("target", "fleet") + .append("target_id", List.of("fleet-1")) + .append("type", "FUTURE_MARKER_TYPE") + .append("created_by", "test")); List markers = service.getRecentMarkers(10); diff --git a/graylog2-web-interface/src/components/collectors/overview/RecentActivity.tsx b/graylog2-web-interface/src/components/collectors/overview/RecentActivity.tsx index 3fec9c127af0..87e233243140 100644 --- a/graylog2-web-interface/src/components/collectors/overview/RecentActivity.tsx +++ b/graylog2-web-interface/src/components/collectors/overview/RecentActivity.tsx @@ -125,20 +125,13 @@ const renderDescription = (entry: ActivityEntry) => { ); case 'FLEET_REASSIGNED': { - const newFleetId = entry.details?.new_fleet_id; - const newFleetName = entry.details?.new_fleet_name ?? newFleetId; - return ( - Collector {targetLink(target)} reassigned to fleet{' '} - {newFleetId - ? {newFleetName} - : [deleted]} + Collector {targetLink(target)} reassigned + {entry.details && <> to fleet {targetLink(entry.details.destination_fleet)}} ); } - default: - return {entry.type}; } }; diff --git a/graylog2-web-interface/src/components/collectors/types.ts b/graylog2-web-interface/src/components/collectors/types.ts index b012559dd794..5923caf3374f 100644 --- a/graylog2-web-interface/src/components/collectors/types.ts +++ b/graylog2-web-interface/src/components/collectors/types.ts @@ -167,15 +167,28 @@ export type TargetInfo = { type: 'fleet' | 'collector'; }; -export type ActivityEntry = { +export type FleetReassignedDetails = { + destination_fleet: TargetInfo; +}; + +export type ActivityEntryBase = { seq: number; timestamp: string | null; - type: 'CONFIG_CHANGED' | 'INGEST_CONFIG_CHANGED' | 'RESTART' | 'DISCOVERY_RUN' | 'FLEET_REASSIGNED'; actor: ActorInfo | null; targets: TargetInfo[]; - details: Record; }; +export type SimpleActivityEntry = ActivityEntryBase & { + type: 'CONFIG_CHANGED' | 'INGEST_CONFIG_CHANGED' | 'RESTART' | 'DISCOVERY_RUN'; +}; + +export type FleetReassignedActivityEntry = ActivityEntryBase & { + type: 'FLEET_REASSIGNED'; + details: FleetReassignedDetails | null; +}; + +export type ActivityEntry = SimpleActivityEntry | FleetReassignedActivityEntry; + export type RecentActivityResponse = { activities: ActivityEntry[]; };