This is an automated email from the ASF dual-hosted git repository.
ChenSammi pushed a commit to branch HDDS-13513_Event_Notification_FeatureBranch
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to
refs/heads/HDDS-13513_Event_Notification_FeatureBranch by this push:
new da6a6e0dc97 HDDS-14009. EventNotification: Generate events according
to S3 schema/strategy (#10318)
da6a6e0dc97 is described below
commit da6a6e0dc97537eee6db79d9a11c1857e882a606
Author: gardenia <[email protected]>
AuthorDate: Thu Jun 11 10:23:03 2026 +0100
HDDS-14009. EventNotification: Generate events according to S3
schema/strategy (#10318)
---
hadoop-ozone/ozone-manager-plugins/pom.xml | 12 +
.../OMEventListenerKafkaPublisher.java | 62 ++-
.../OMEventListenerNotificationStrategy.java | 30 ++
.../eventlistener/s3/DateTimeJsonSerializer.java | 36 ++
.../om/eventlistener/s3/S3EventNotification.java | 560 +++++++++++++++++++++
.../s3/S3EventNotificationBuilder.java | 128 +++++
.../s3/S3EventNotificationStrategy.java | 190 +++++++
.../ozone/om/eventlistener/s3/package-info.java | 22 +
.../TestOMEventListenerKafkaPublisher.java | 213 ++++++--
9 files changed, 1203 insertions(+), 50 deletions(-)
diff --git a/hadoop-ozone/ozone-manager-plugins/pom.xml
b/hadoop-ozone/ozone-manager-plugins/pom.xml
index 95abfcf4ef6..4fe596bb979 100644
--- a/hadoop-ozone/ozone-manager-plugins/pom.xml
+++ b/hadoop-ozone/ozone-manager-plugins/pom.xml
@@ -29,6 +29,18 @@
</properties>
<dependencies>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-annotations</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ </dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
diff --git
a/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/OMEventListenerKafkaPublisher.java
b/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/OMEventListenerKafkaPublisher.java
index 53724129b0d..f42524ba98a 100644
---
a/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/OMEventListenerKafkaPublisher.java
+++
b/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/OMEventListenerKafkaPublisher.java
@@ -18,12 +18,16 @@
package org.apache.hadoop.ozone.om.eventlistener;
import java.io.IOException;
+import java.time.Duration;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.ozone.OzoneIllegalArgumentException;
+import org.apache.hadoop.ozone.om.eventlistener.s3.S3EventNotificationStrategy;
import org.apache.hadoop.ozone.om.helpers.OmCompletedRequestInfo;
import org.apache.kafka.clients.admin.AdminClient;
import org.apache.kafka.clients.admin.NewTopic;
@@ -42,10 +46,16 @@ public class OMEventListenerKafkaPublisher implements
OMEventListener {
public static final Logger LOG =
LoggerFactory.getLogger(OMEventListenerKafkaPublisher.class);
private static final String KAFKA_CONFIG_PREFIX = "ozone.om.plugin.kafka.";
+ private static final String NOTIFICATION_STRATEGY_CONFIG =
KAFKA_CONFIG_PREFIX + "notification.strategy";
+ private static final Class<? extends OMEventListenerNotificationStrategy>
+ DEFAULT_NOTIFICATION_STRATEGY = S3EventNotificationStrategy.class;
+ private static final String KAFKA_SERVICE_INTERVAL_CONFIG =
KAFKA_CONFIG_PREFIX + "service.interval";
+ private static final String KAFKA_SERVICE_TIMEOUT_CONFIG =
KAFKA_CONFIG_PREFIX + "service.timeout";
private static final int COMPLETED_REQUEST_CONSUMER_CORE_POOL_SIZE = 1;
private OMEventListenerLedgerPoller ledgerPoller;
private KafkaClientWrapper kafkaClient;
+ private OMEventListenerNotificationStrategy notificationStrategy;
private OMEventListenerLedgerPollerSeekPosition seekPosition;
@Override
@@ -56,10 +66,24 @@ public void initialize(OzoneConfiguration conf,
OMEventListenerPluginContext plu
this.kafkaClient = new KafkaClientWrapper(kafkaProps);
- // TODO: these constants should be read from config
- long kafkaServiceInterval = 2 * 1000;
- long kafkaServiceTimeout = 300 * 1000;
+ long kafkaServiceInterval = conf.getTimeDuration(
+ KAFKA_SERVICE_INTERVAL_CONFIG, "2s", TimeUnit.MILLISECONDS);
+ long kafkaServiceTimeout = conf.getTimeDuration(
+ KAFKA_SERVICE_TIMEOUT_CONFIG, "5m", TimeUnit.MILLISECONDS);
+ Class<? extends OMEventListenerNotificationStrategy> strategyClass =
conf.getClass(
+ NOTIFICATION_STRATEGY_CONFIG,
+ DEFAULT_NOTIFICATION_STRATEGY,
+ OMEventListenerNotificationStrategy.class);
+ try {
+ this.notificationStrategy =
strategyClass.getDeclaredConstructor().newInstance();
+ } catch (Exception ex) {
+ LOG.error("Failed to instantiate notification strategy: {}",
strategyClass, ex);
+ OzoneIllegalArgumentException exception = new
OzoneIllegalArgumentException(
+ "Failed to instantiate notification strategy: " + strategyClass);
+ exception.initCause(ex);
+ throw exception;
+ }
this.seekPosition = new OMEventListenerLedgerPollerSeekPosition();
LOG.info("Creating OMEventListenerLedgerPoller with serviceInterval={}," +
@@ -102,24 +126,23 @@ public void stop() {
public void handleCompletedRequest(OmCompletedRequestInfo
completedRequestInfo) {
LOG.debug("Processing {}", completedRequestInfo);
- // stub event until we implement a strategy to convert the events to
- // a user facing schema (e.g. S3)
- String event = String.format("{\"key\":\"%s/%s/%s\", \"type\":\"%s\"}",
- completedRequestInfo.getVolumeName(),
- completedRequestInfo.getBucketName(),
- completedRequestInfo.getKeyName(),
- String.valueOf(completedRequestInfo.getCmdType()));
+ List<String> eventsToSend =
notificationStrategy.determineEventsForOperation(completedRequestInfo);
- LOG.debug("Sending {}", event);
-
- try {
- kafkaClient.send(event);
- } catch (IOException ex) {
- LOG.error("Failure to send event {}", event, ex);
- return;
+ // loop over events and send them to our kafka sink
+ for (String event : eventsToSend) {
+ if (event == null) {
+ LOG.warn("Skipping null event for transaction {}",
completedRequestInfo.getTrxLogIndex());
+ continue;
+ }
+ try {
+ kafkaClient.send(event);
+ } catch (IOException ex) {
+ LOG.error("Failure to send event {}", event, ex);
+ return;
+ }
}
- // we can update the seek position
+ // no errors so we can update the seek position
seekPosition.set(String.valueOf(completedRequestInfo.getTrxLogIndex()));
}
@@ -145,7 +168,8 @@ public void initialize() throws IOException {
public void shutdown() throws IOException {
if (producer != null) {
- producer.close();
+ LOG.info("Closing kafka producer for topic {}", topic);
+ producer.close(Duration.ofSeconds(10));
}
}
diff --git
a/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/OMEventListenerNotificationStrategy.java
b/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/OMEventListenerNotificationStrategy.java
new file mode 100644
index 00000000000..c9c82bea60d
--- /dev/null
+++
b/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/OMEventListenerNotificationStrategy.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.eventlistener;
+
+import java.util.List;
+import org.apache.hadoop.ozone.om.helpers.OmCompletedRequestInfo;
+
+/**
+ * Interface for strategies which turn completed events into
+ * notifications.
+ */
+public interface OMEventListenerNotificationStrategy {
+
+ List<String> determineEventsForOperation(OmCompletedRequestInfo
completedRequestInfo);
+}
diff --git
a/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/s3/DateTimeJsonSerializer.java
b/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/s3/DateTimeJsonSerializer.java
new file mode 100644
index 00000000000..1e7a1a8bf36
--- /dev/null
+++
b/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/s3/DateTimeJsonSerializer.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.eventlistener.s3;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import java.io.IOException;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * A simple replacement for com.amazonaws.internal.DateTimeJsonSerializer.
+ */
+public class DateTimeJsonSerializer extends JsonSerializer<OffsetDateTime> {
+ @Override
+ public void serialize(OffsetDateTime value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
+ // AWS SDK typically uses ISO8601 format for S3 events
+ gen.writeString(value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
+ }
+}
diff --git
a/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/s3/S3EventNotification.java
b/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/s3/S3EventNotification.java
new file mode 100644
index 00000000000..d90b068e8d6
--- /dev/null
+++
b/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/s3/S3EventNotification.java
@@ -0,0 +1,560 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.eventlistener.s3;
+
+/* copy of
+ * com.amazonaws.services.s3.event.S3EventNotification
+ * class taken from AWS SDK (1.x) with minor changes for build issues
+ * and removed usage of unnecessary AWS specific extension entities:
+ *
+ * - GlacierEventDataEntity
+ * - LifecycleEventDataEntity
+ * - IntelligentTieringEventDataEntity
+ * - ReplicationEventDataEntity
+ *
+ * NOTE: We may not need to fork this class if we can use the SDK one directly
+ * but conversely we may want to make our own customizations.
+ *
+ * Original copyright below:
+ */
+
+/*
+ * Copyright 2014-2025 Amazon Technologies, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
+ * OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//import com.amazonaws.internal.DateTimeJsonSerializer;
+//import com.amazonaws.util.json.Jackson;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import java.time.OffsetDateTime;
+import java.util.List;
+import java.util.Map;
+
+/**
+* A helper class that represents a strongly typed S3 EventNotification item
sent
+* to SQS, SNS, or Lambda.
+ *
+ * <p>
+ * <b>Migrating to the AWS SDK for Java v2</b>
+ * <p>
+ * The v2 equivalent of this class is
+ * <a
href="https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/eventnotifications/s3/model/S3EventNotification.html">S3EventNotification</a>
+ *
+ * <p>
+ * See <a
href="https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/migration.html">Migration
Guide</a>
+ * for more information.
+*/
+@SuppressWarnings("checkstyle:all")
+public class S3EventNotification {
+
+ private final List<S3EventNotificationRecord> records;
+
+ /**
+ * Keys for Ozone-specific extensions in the S3 Event schema.
+ */
+ public enum OzoneEventDataKey {
+ IS_DIRECTORY("isDirectory"),
+ IS_RECURSIVE("isRecursive"),
+ IS_OVERWRITE("isOverwrite"),
+ RENAME_FROM_KEY("renameFromKey"),
+ TRX_LOG_INDEX("trxLogIndex"),
+ OP_TYPE("opType");
+
+ private final String jsonKey;
+
+ OzoneEventDataKey(String jsonKey) {
+ this.jsonKey = jsonKey;
+ }
+
+ @Override
+ public String toString() {
+ return jsonKey;
+ }
+ }
+
+ @JsonCreator
+ public S3EventNotification(
+ @JsonProperty(value = "Records") List<S3EventNotificationRecord>
records)
+ {
+ this.records = records;
+ }
+
+ /**
+ * <p>
+ * Parse the JSON string into a S3EventNotification object.
+ * </p>
+ * <p>
+ * The function will try its best to parse input JSON string as best as it
can.
+ * It will not fail even if the JSON string contains unknown properties.
+ * The function will throw SdkClientException if the input JSON string is
+ * not valid JSON.
+ * </p>
+ * @param json
+ * JSON string to parse. Typically this is the body of your SQS
+ * notification message body.
+ *
+ * @return The resulting S3EventNotification object.
+ */
+ //public static S3EventNotification parseJson(String json) {
+ // return Jackson.fromJsonString(json, S3EventNotification.class);
+ //}
+
+ /**
+ * @return the records in this notification
+ */
+ @JsonProperty(value = "Records")
+ public List<S3EventNotificationRecord> getRecords() {
+ return records;
+ }
+
+ //public String toJson() {
+ // return Jackson.toJsonString(this);
+ //}
+
+ public static class UserIdentityEntity {
+
+ private final String principalId;
+
+ @JsonCreator
+ public UserIdentityEntity(
+ @JsonProperty(value = "principalId") String principalId) {
+ this.principalId = principalId;
+ }
+
+ public String getPrincipalId() {
+ return principalId;
+ }
+ }
+
+ public static class S3BucketEntity {
+
+ private final String name;
+ private final UserIdentityEntity ownerIdentity;
+ private final String arn;
+
+ @JsonCreator
+ public S3BucketEntity(
+ @JsonProperty(value = "name") String name,
+ @JsonProperty(value = "ownerIdentity") UserIdentityEntity
ownerIdentity,
+ @JsonProperty(value = "arn") String arn)
+ {
+ this.name = name;
+ this.ownerIdentity = ownerIdentity;
+ this.arn = arn;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public UserIdentityEntity getOwnerIdentity() {
+ return ownerIdentity;
+ }
+
+ public String getArn() {
+ return arn;
+ }
+ }
+
+ public static class S3ObjectEntity {
+
+ private final String key;
+ private final Long size;
+ private final String eTag;
+ private final String versionId;
+ private final String sequencer;
+
+ @Deprecated
+ public S3ObjectEntity(
+ String key,
+ Integer size,
+ String eTag,
+ String versionId)
+ {
+ this.key = key;
+ this.size = size == null ? null : size.longValue();
+ this.eTag = eTag;
+ this.versionId = versionId;
+ this.sequencer = null;
+ }
+
+ @Deprecated
+ public S3ObjectEntity(
+ String key,
+ Long size,
+ String eTag,
+ String versionId)
+ {
+ this(key, size, eTag, versionId, null);
+ }
+
+ @JsonCreator
+ public S3ObjectEntity(
+ @JsonProperty(value = "key") String key,
+ @JsonProperty(value = "size") Long size,
+ @JsonProperty(value = "eTag") String eTag,
+ @JsonProperty(value = "versionId") String versionId,
+ @JsonProperty(value = "sequencer") String sequencer)
+ {
+ this.key = key;
+ this.size = size;
+ this.eTag = eTag;
+ this.versionId = versionId;
+ this.sequencer = sequencer;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * S3 URL encodes the key of the object involved in the event. This is
+ * a convenience method to automatically URL decode the key.
+ * @return The URL decoded object key.
+ */
+ //public String getUrlDecodedKey() {
+ // return SdkHttpUtils.urlDecode(getKey());
+ //}
+
+ /**
+ * @deprecated use {@link #getSizeAsLong()} instead.
+ */
+ @Deprecated
+ @JsonIgnore
+ public Integer getSize() {
+ return size == null ? null : size.intValue();
+ }
+
+ @JsonProperty(value = "size")
+ public Long getSizeAsLong() {
+ return size;
+ }
+
+ public String geteTag() {
+ return eTag;
+ }
+
+ public String getVersionId() {
+ return versionId;
+ }
+
+ public String getSequencer() {
+ return sequencer;
+ }
+ }
+
+ public static class S3Entity {
+
+ private final String configurationId;
+ private final S3BucketEntity bucket;
+ private final S3ObjectEntity object;
+ private final String s3SchemaVersion;
+
+ @JsonCreator
+ public S3Entity(
+ @JsonProperty(value = "configurationId") String
configurationId,
+ @JsonProperty(value = "bucket") S3BucketEntity bucket,
+ @JsonProperty(value = "object") S3ObjectEntity object,
+ @JsonProperty(value = "s3SchemaVersion") String
s3SchemaVersion)
+ {
+ this.configurationId = configurationId;
+ this.bucket = bucket;
+ this.object = object;
+ this.s3SchemaVersion = s3SchemaVersion;
+ }
+
+ public String getConfigurationId() {
+ return configurationId;
+ }
+
+ public S3BucketEntity getBucket() {
+ return bucket;
+ }
+
+ public S3ObjectEntity getObject() {
+ return object;
+ }
+
+ public String getS3SchemaVersion() {
+ return s3SchemaVersion;
+ }
+ }
+
+ public static class RequestParametersEntity {
+
+ private final String sourceIPAddress;
+
+ @JsonCreator
+ public RequestParametersEntity(
+ @JsonProperty(value = "sourceIPAddress") String
sourceIPAddress)
+ {
+ this.sourceIPAddress = sourceIPAddress;
+ }
+
+ public String getSourceIPAddress() {
+ return sourceIPAddress;
+ }
+ }
+
+ public static class ResponseElementsEntity {
+
+ private final String xAmzId2;
+ private final String xAmzRequestId;
+
+ @JsonCreator
+ public ResponseElementsEntity(
+ @JsonProperty(value = "x-amz-id-2") String xAmzId2,
+ @JsonProperty(value = "x-amz-request-id") String xAmzRequestId)
+ {
+ this.xAmzId2 = xAmzId2;
+ this.xAmzRequestId = xAmzRequestId;
+ }
+
+ @JsonProperty("x-amz-id-2")
+ public String getxAmzId2() {
+ return xAmzId2;
+ }
+
+ @JsonProperty("x-amz-request-id")
+ public String getxAmzRequestId() {
+ return xAmzRequestId;
+ }
+ }
+
+ public static class LifecycleEventDataEntity {
+
+ private final TransitionEventDataEntity transitionEventData;
+
+ @JsonCreator
+ public LifecycleEventDataEntity(
+ @JsonProperty(value = "transitionEventData")
TransitionEventDataEntity transitionEventData)
+ {
+
+ this.transitionEventData = transitionEventData;
+ }
+
+ public TransitionEventDataEntity getTransitionEventData() {
+ return transitionEventData;
+ }
+ }
+
+ public static class TransitionEventDataEntity {
+ private final String destinationStorageClass;
+
+ @JsonCreator
+ public TransitionEventDataEntity(
+ @JsonProperty("destinationStorageClass") String
destinationStorageClass)
+ {
+ this.destinationStorageClass = destinationStorageClass;
+ }
+
+ public String getDestinationStorageClass() {
+ return destinationStorageClass;
+ }
+ }
+
+ public static class S3EventNotificationRecord {
+
+ private final String awsRegion;
+ private final String eventName;
+ private final String eventSource;
+ private final OffsetDateTime eventTime;
+ private final String eventVersion;
+
+ private final RequestParametersEntity requestParameters;
+ private final ResponseElementsEntity responseElements;
+ private final S3Entity s3;
+ private final UserIdentityEntity userIdentity;
+ private final Map<String, Object> ozoneEventData;
+ //private final GlacierEventDataEntity glacierEventData;
+ //private final LifecycleEventDataEntity lifecycleEventData;
+ //private final IntelligentTieringEventDataEntity
intelligentTieringEventData;
+ //private final ReplicationEventDataEntity replicationEventDataEntity;
+
+ /*
+ @Deprecated
+ public S3EventNotificationRecord(
+ String awsRegion,
+ String eventName,
+ String eventSource,
+ String eventTime,
+ String eventVersion,
+ RequestParametersEntity requestParameters,
+ ResponseElementsEntity responseElements,
+ S3Entity s3,
+ UserIdentityEntity userIdentity)
+ {
+ this(awsRegion,
+ eventName,
+ eventSource,
+ eventTime,
+ eventVersion,
+ requestParameters,
+ responseElements,
+ s3,
+ userIdentity,
+ null,
+ null,
+ null,
+ null);
+ }
+
+ @Deprecated
+ public S3EventNotificationRecord(
+ String awsRegion,
+ String eventName,
+ String eventSource,
+ String eventTime,
+ String eventVersion,
+ RequestParametersEntity requestParameters,
+ ResponseElementsEntity responseElements,
+ S3Entity s3,
+ UserIdentityEntity userIdentity,
+ GlacierEventDataEntity glacierEventData)
+ {
+ this(awsRegion,
+ eventName,
+ eventSource,
+ eventTime,
+ eventVersion,
+ requestParameters,
+ responseElements,
+ s3,
+ userIdentity,
+ glacierEventData,
+ null,
+ null,
+ null);
+ }
+ */
+
+ @JsonCreator
+ public S3EventNotificationRecord(
+ @JsonProperty(value = "awsRegion") String awsRegion,
+ @JsonProperty(value = "eventName") String eventName,
+ @JsonProperty(value = "eventSource") String eventSource,
+ @JsonProperty(value = "eventTime") String eventTime,
+ @JsonProperty(value = "eventVersion") String eventVersion,
+ @JsonProperty(value = "requestParameters")
RequestParametersEntity requestParameters,
+ @JsonProperty(value = "responseElements")
ResponseElementsEntity responseElements,
+ @JsonProperty(value = "s3") S3Entity s3,
+ @JsonProperty(value = "userIdentity") UserIdentityEntity
userIdentity,
+ @JsonProperty(value = "ozoneEventData") Map<String, Object>
ozoneEventData)
+ //@JsonProperty(value = "glacierEventData")
GlacierEventDataEntity glacierEventData,
+ //@JsonProperty(value = "lifecycleEventData")
LifecycleEventDataEntity lifecycleEventData,
+ //@JsonProperty(value = "intelligentTieringEventData")
+ //IntelligentTieringEventDataEntity
intelligentTieringEventData,
+ //@JsonProperty(value = "replicationEventData")
ReplicationEventDataEntity replicationEventData)
+ {
+ this.awsRegion = awsRegion;
+ this.eventName = eventName;
+ this.eventSource = eventSource;
+
+ if (eventTime != null)
+ {
+ this.eventTime = OffsetDateTime.parse(eventTime);
+ } else {
+ this.eventTime = null;
+ }
+
+ this.eventVersion = eventVersion;
+ this.requestParameters = requestParameters;
+ this.responseElements = responseElements;
+ this.s3 = s3;
+ this.userIdentity = userIdentity;
+ this.ozoneEventData = ozoneEventData;
+ //this.glacierEventData = glacierEventData;
+ //this.lifecycleEventData = lifecycleEventData;
+ //this.intelligentTieringEventData = intelligentTieringEventData;
+ //this.replicationEventDataEntity = replicationEventData;
+ }
+
+ public String getAwsRegion() {
+ return awsRegion;
+ }
+
+ public String getEventName() {
+ return eventName;
+ }
+
+ //@JsonIgnore
+ //public S3Event getEventNameAsEnum() {
+ // return S3Event.fromValue(eventName);
+ //}
+
+ public String getEventSource() {
+ return eventSource;
+ }
+
+ @JsonSerialize(using=DateTimeJsonSerializer.class)
+ public OffsetDateTime getEventTime() {
+ return eventTime;
+ }
+
+ public String getEventVersion() {
+ return eventVersion;
+ }
+
+ public RequestParametersEntity getRequestParameters() {
+ return requestParameters;
+ }
+
+ public ResponseElementsEntity getResponseElements() {
+ return responseElements;
+ }
+
+ public S3Entity getS3() {
+ return s3;
+ }
+
+ public UserIdentityEntity getUserIdentity() {
+ return userIdentity;
+ }
+
+ // Ozone extension
+ public Map<String, Object> getOzoneEventData() {
+ return ozoneEventData;
+ }
+
+ //public GlacierEventDataEntity getGlacierEventData() {
+ // return glacierEventData;
+ //}
+
+ //public LifecycleEventDataEntity getLifecycleEventData() { return
lifecycleEventData; }
+
+ //public IntelligentTieringEventDataEntity
getIntelligentTieringEventData() {
+ // return intelligentTieringEventData;
+ //}
+
+ //public ReplicationEventDataEntity getReplicationEventDataEntity() {
return replicationEventDataEntity; }
+
+ }
+}
diff --git
a/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/s3/S3EventNotificationBuilder.java
b/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/s3/S3EventNotificationBuilder.java
new file mode 100644
index 00000000000..2f4489fd3f0
--- /dev/null
+++
b/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/s3/S3EventNotificationBuilder.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.eventlistener.s3;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import
org.apache.hadoop.ozone.om.eventlistener.s3.S3EventNotification.S3BucketEntity;
+import
org.apache.hadoop.ozone.om.eventlistener.s3.S3EventNotification.S3EventNotificationRecord;
+import
org.apache.hadoop.ozone.om.eventlistener.s3.S3EventNotification.S3ObjectEntity;
+import
org.apache.hadoop.ozone.om.eventlistener.s3.S3EventNotification.UserIdentityEntity;
+
+/**
+ * This is a builder for the AWS event notification class
+ * com.amazonaws.services.s3.event.S3EventNotification which is part of
+ * AWS SDK 1.x
+ *
+ * NOTE: the original SDK class is designed primarily for consumer-side
+ * deserialization (parsing incoming JSON). Because it lacks standard public
+ * setters or a fluent API for creation, this builder was added to provide
+ * a clean interface for the serialization path (producing events from Ozone).
+ *
+ * XXX: we may need to fork these classes so that we can customize it to
+ * our needs.
+ */
+public class S3EventNotificationBuilder {
+
+ private static final String REGION = "us-east-1";
+ //private static final String BUCKET_ARN_PREFIX = "arn:aws:s3:" + REGION;
+ private static final String EVENT_SOURCE = "ozone:s3";
+ private static final String EVENT_VERSION = "2.1";
+ private static final String USER_IDENTITY = "some-principalId";
+
+ private static final String SCHEMA_VERSION = "1.0";
+ private static final String CONFIGURATION_ID = "mynotif1";
+
+ private final String objectKey;
+ private final String bucketName;
+ private final String bucketArn;
+ private final String eventName;
+ private final Instant eventTime;
+ private final String etag;
+ private final Map<String, Object> ozoneEventData;
+
+ // mutable fields defaulting to null
+ private Long objectSize;
+ private String objectVersionId;
+ private String objectSequencer;
+
+ public S3EventNotificationBuilder(String objectKey, String bucketName,
String bucketArn, String eventName,
+ Instant eventTime, String etag) {
+ this.objectKey = objectKey;
+ this.bucketName = bucketName;
+ this.bucketArn = bucketArn;
+ this.eventName = eventName;
+ this.eventTime = eventTime;
+ this.etag = etag;
+ this.ozoneEventData = new HashMap<>();
+ }
+
+ public S3EventNotificationBuilder setObjectSize(long objectSize) {
+ this.objectSize = objectSize;
+ return this;
+ }
+
+ public S3EventNotificationBuilder setObjectVersionId(String objectVersionId)
{
+ this.objectVersionId = objectVersionId;
+ return this;
+ }
+
+ public S3EventNotificationBuilder setObjectSequencer(String objectSequencer)
{
+ this.objectSequencer = objectSequencer;
+ return this;
+ }
+
+ public S3EventNotificationBuilder addAllEventData(Map<String, Object>
ozoneEventDataToAdd) {
+ this.ozoneEventData.putAll(ozoneEventDataToAdd);
+ return this;
+ }
+
+ public S3EventNotification build() {
+ UserIdentityEntity userIdentity = new UserIdentityEntity(USER_IDENTITY);
+ S3BucketEntity s3BucketEntity = new S3BucketEntity(bucketName,
userIdentity, bucketArn);
+
+ S3EventNotification.S3ObjectEntity s3ObjectEntity = new S3ObjectEntity(
+ objectKey,
+ objectSize,
+ etag,
+ objectVersionId,
+ objectSequencer);
+
+ S3EventNotification.S3Entity s3Entity = new S3EventNotification.S3Entity(
+ CONFIGURATION_ID,
+ s3BucketEntity,
+ s3ObjectEntity,
+ SCHEMA_VERSION);
+
+ S3EventNotificationRecord eventRecord = new S3EventNotificationRecord(
+ REGION,
+ eventName,
+ EVENT_SOURCE,
+ eventTime.toString(),
+ EVENT_VERSION,
+ new S3EventNotification.RequestParametersEntity(""),
+ new S3EventNotification.ResponseElementsEntity("", ""),
+ s3Entity,
+ new S3EventNotification.UserIdentityEntity(USER_IDENTITY),
+ ozoneEventData);
+
+ return new S3EventNotification(Collections.singletonList(eventRecord));
+ }
+}
diff --git
a/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/s3/S3EventNotificationStrategy.java
b/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/s3/S3EventNotificationStrategy.java
new file mode 100644
index 00000000000..9190fbfa3ce
--- /dev/null
+++
b/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/s3/S3EventNotificationStrategy.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.eventlistener.s3;
+
+import static org.apache.hadoop.ozone.OzoneConsts.OM_KEY_PREFIX;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import org.apache.commons.lang3.StringUtils;
+import
org.apache.hadoop.ozone.om.eventlistener.OMEventListenerNotificationStrategy;
+import
org.apache.hadoop.ozone.om.eventlistener.s3.S3EventNotification.OzoneEventDataKey;
+import org.apache.hadoop.ozone.om.helpers.OmCompletedRequestInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is a notification strategy to generate events according to S3
+ * notification semantics.
+ */
+public class S3EventNotificationStrategy implements
OMEventListenerNotificationStrategy {
+ public static final Logger LOG =
LoggerFactory.getLogger(S3EventNotificationStrategy.class);
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ @Override
+ public List<String> determineEventsForOperation(OmCompletedRequestInfo
requestInfo) {
+
+ switch (requestInfo.getCmdType()) {
+ case CreateVolume:
+ return Collections.singletonList(createS3Event("OzoneVolumeCreated:Put",
+ requestInfo.getVolumeName(), null, null, Collections.emptyMap(),
+ requestInfo.getTrxLogIndex()));
+ case DeleteVolume:
+ return
Collections.singletonList(createS3Event("OzoneVolumeRemoved:Delete",
+ requestInfo.getVolumeName(), null, null, Collections.emptyMap(),
+ requestInfo.getTrxLogIndex()));
+ case CreateBucket:
+ return Collections.singletonList(createS3Event("OzoneBucketCreated:Put",
+ requestInfo.getVolumeName(), requestInfo.getBucketName(), null,
+ Collections.emptyMap(), requestInfo.getTrxLogIndex()));
+ case DeleteBucket:
+ return
Collections.singletonList(createS3Event("OzoneBucketRemoved:Delete",
+ requestInfo.getVolumeName(), requestInfo.getBucketName(), null,
+ Collections.emptyMap(), requestInfo.getTrxLogIndex()));
+ case CreateKey:
+ Map<String, Object> createKeyData = new HashMap<>();
+ createKeyData.put(OzoneEventDataKey.OP_TYPE.toString(), "CreateKey");
+ return Collections.singletonList(createS3Event("ObjectCreated:Put",
+ requestInfo.getVolumeName(), requestInfo.getBucketName(),
requestInfo.getKeyName(),
+ createKeyData, requestInfo.getTrxLogIndex()));
+ case CommitKey:
+ Map<String, Object> commitKeyData = new HashMap<>();
+ commitKeyData.put(OzoneEventDataKey.OP_TYPE.toString(), "CommitKey");
+ return Collections.singletonList(createS3Event("ObjectCreated:Put",
+ requestInfo.getVolumeName(), requestInfo.getBucketName(),
requestInfo.getKeyName(),
+ commitKeyData, requestInfo.getTrxLogIndex()));
+ case CreateFile:
+ OmCompletedRequestInfo.OperationArgs.CreateFileArgs createFileArgs
+ = (OmCompletedRequestInfo.OperationArgs.CreateFileArgs)
requestInfo.getOpArgs();
+
+ // XXX: ozoneEventData is an Ozone extension. It is unclear if this
+ // schema makes sense but the general S3 schema is somewhat
+ // freeform. These arguments are more informational than
+ // required so it is unclear as to their necessity.
+ Map<String, Object> createFileEventData = new HashMap<>();
+ createFileEventData.put(OzoneEventDataKey.IS_DIRECTORY.toString(),
false);
+ createFileEventData.put(OzoneEventDataKey.IS_RECURSIVE.toString(),
+ createFileArgs.isRecursive());
+ createFileEventData.put(OzoneEventDataKey.IS_OVERWRITE.toString(),
+ createFileArgs.isOverwrite());
+ createFileEventData.put(OzoneEventDataKey.OP_TYPE.toString(),
"CreateFile");
+
+ return Collections.singletonList(createS3Event("ObjectCreated:Put",
+ requestInfo.getVolumeName(), requestInfo.getBucketName(),
requestInfo.getKeyName(),
+ createFileEventData, requestInfo.getTrxLogIndex()));
+ case CreateDirectory:
+ // XXX: ozoneEventData is an Ozone extension. It is unclear if this
+ // schema makes sense but the general S3 schema is somewhat
+ // freeform. These arguments are more informational than
+ // required so it is unclear as to their necessity.
+ Map<String, Object> createEventData = new HashMap<>();
+ createEventData.put(OzoneEventDataKey.IS_DIRECTORY.toString(), true);
+ createEventData.put(OzoneEventDataKey.OP_TYPE.toString(),
"CreateDirectory");
+
+ return Collections.singletonList(createS3Event("ObjectCreated:Put",
+ requestInfo.getVolumeName(), requestInfo.getBucketName(),
requestInfo.getKeyName(),
+ createEventData, requestInfo.getTrxLogIndex()));
+ case DeleteKey:
+ return Collections.singletonList(createS3Event("ObjectRemoved:Delete",
+ requestInfo.getVolumeName(), requestInfo.getBucketName(),
requestInfo.getKeyName(),
+ Collections.emptyMap(), requestInfo.getTrxLogIndex()));
+ case RenameKey:
+ OmCompletedRequestInfo.OperationArgs.RenameKeyArgs renameKeyArgs
+ = (OmCompletedRequestInfo.OperationArgs.RenameKeyArgs)
requestInfo.getOpArgs();
+
+ String renameFromKey =
S3OzoneEventKeyFormatter.getOzoneKey(requestInfo.getVolumeName(),
+ requestInfo.getBucketName(), requestInfo.getKeyName());
+
+ // XXX: it would be good to be able to convey that this was a
+ // file vs directory rename
+ Map<String, Object> ozoneEventData = new HashMap<>();
+ ozoneEventData.put(OzoneEventDataKey.RENAME_FROM_KEY.toString(),
renameFromKey);
+
+ // NOTE: ObjectRenamed:Rename is an Ozone extension as is the
+ // ozoneEventData map in the S3 event schema.
+ return Collections.singletonList(createS3Event("ObjectRenamed:Rename",
+ requestInfo.getVolumeName(), requestInfo.getBucketName(),
renameKeyArgs.getToKeyName(),
+ ozoneEventData, requestInfo.getTrxLogIndex()));
+ default:
+ LOG.debug("No events for operation {} on {}",
+ requestInfo.getCmdType(),
+ requestInfo.getKeyName());
+ return Collections.emptyList();
+ }
+ }
+
+ static String createS3Event(String eventName, String volumeName, String
bucketName, String keyName,
+ Map<String, Object> ozoneEventData, long trxLogIndex) {
+ try {
+ String objectKey = S3OzoneEventKeyFormatter.getOzoneKey(volumeName,
bucketName, keyName);
+ String bucketArn = (bucketName == null)
+ ? "arn:aws:s3:::" + volumeName
+ : "arn:aws:s3:::" + volumeName + "." + bucketName;
+ Instant eventTime = Instant.now();
+ String etag = UUID.randomUUID().toString();
+
+ Map<String, Object> eventData = new HashMap<>();
+ if (ozoneEventData != null) {
+ eventData.putAll(ozoneEventData);
+ }
+ eventData.put(OzoneEventDataKey.TRX_LOG_INDEX.toString(), trxLogIndex);
+
+ String sequencer = String.format("%016X", trxLogIndex);
+
+ S3EventNotification event =
+ new S3EventNotificationBuilder(objectKey, bucketName, bucketArn,
eventName, eventTime, etag)
+ .setObjectSequencer(sequencer)
+ .addAllEventData(eventData)
+ .build();
+
+ return MAPPER.writer().writeValueAsString(event);
+ } catch (Exception ex) {
+ LOG.error("Failed to create S3 event for {} on {}/{}", eventName,
volumeName, bucketName, ex);
+ return null;
+ }
+ }
+
+ /**
+ * Formats the Ozone key for S3 events.
+ *
+ * NOTE: This differs from OMMetadataManager#getOzoneKey in that it does NOT
+ * include the leading slash, which is standard for S3 notification keys.
+ */
+ private static class S3OzoneEventKeyFormatter {
+ public static String getOzoneKey(String volume, String bucket, String key)
{
+ StringBuilder builder = new StringBuilder();
+ builder.append(volume);
+ if (StringUtils.isNotBlank(bucket)) {
+ builder.append(OM_KEY_PREFIX).append(bucket);
+ if (StringUtils.isNotBlank(key)) {
+ builder.append(OM_KEY_PREFIX);
+ if (!key.equals(OM_KEY_PREFIX)) {
+ builder.append(key);
+ }
+ }
+ }
+ return builder.toString();
+ }
+ }
+}
diff --git
a/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/s3/package-info.java
b/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/s3/package-info.java
new file mode 100644
index 00000000000..78d825a4648
--- /dev/null
+++
b/hadoop-ozone/ozone-manager-plugins/src/main/java/org/apache/hadoop/ozone/om/eventlistener/s3/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This package contains classes for S3 style event generation for the
+ * OM Event Listener.
+ */
+package org.apache.hadoop.ozone.om.eventlistener.s3;
diff --git
a/hadoop-ozone/ozone-manager-plugins/src/test/java/org/apache/hadoop/ozone/om/eventlistener/TestOMEventListenerKafkaPublisher.java
b/hadoop-ozone/ozone-manager-plugins/src/test/java/org/apache/hadoop/ozone/om/eventlistener/TestOMEventListenerKafkaPublisher.java
index 761f8f3c117..76f3de57968 100644
---
a/hadoop-ozone/ozone-manager-plugins/src/test/java/org/apache/hadoop/ozone/om/eventlistener/TestOMEventListenerKafkaPublisher.java
+++
b/hadoop-ozone/ozone-manager-plugins/src/test/java/org/apache/hadoop/ozone/om/eventlistener/TestOMEventListenerKafkaPublisher.java
@@ -22,10 +22,18 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.server.JsonUtils;
+import org.apache.hadoop.ozone.OzoneIllegalArgumentException;
+import org.apache.hadoop.ozone.om.eventlistener.s3.S3EventNotification;
+import
org.apache.hadoop.ozone.om.eventlistener.s3.S3EventNotification.OzoneEventDataKey;
+import
org.apache.hadoop.ozone.om.eventlistener.s3.S3EventNotification.S3EventNotificationRecord;
import org.apache.hadoop.ozone.om.helpers.OmCompletedRequestInfo;
import org.apache.hadoop.ozone.om.helpers.OmCompletedRequestInfo.OperationArgs;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.Type;
@@ -49,20 +57,6 @@ public class TestOMEventListenerKafkaPublisher {
@Mock
private OMEventListenerPluginContext pluginContext;
- // helper to create json key/val string for non exhaustive JSON
- // attribute checking
- private static String toJsonKeyVal(String key, String val) {
- return new StringBuilder()
- .append('\"')
- .append(key)
- .append('\"')
- .append(':')
- .append('\"')
- .append(val)
- .append('\"')
- .toString();
- }
-
private static OmCompletedRequestInfo buildCompletedRequestInfo(
long trxLogIndex, Type cmdType, String keyName, OperationArgs
opArgs) {
@@ -101,17 +95,50 @@ private List<String>
captureEventsProducedByOperation(OmCompletedRequestInfo op,
return events;
}
+ private S3EventNotificationRecord getFirstRecord(List<String> events) throws
IOException {
+ assertThat(events).hasSize(1);
+ S3EventNotification notification = JsonUtils.getDefaultMapper()
+ .readValue(events.get(0), S3EventNotification.class);
+ assertThat(notification.getRecords()).hasSize(1);
+ return notification.getRecords().get(0);
+ }
+
@Test
public void testCreateKeyRequestProducesS3CreatedEvent() throws
InterruptedException, IOException {
OmCompletedRequestInfo createRequest = buildCompletedRequestInfo(1L,
Type.CreateKey, "some/key1",
new OperationArgs.NoArgs());
List<String> events = captureEventsProducedByOperation(createRequest, 1);
- assertThat(events).hasSize(1);
+ S3EventNotificationRecord record = getFirstRecord(events);
+
+ assertThat(record.getEventName()).isEqualTo("ObjectCreated:Put");
+
assertThat(record.getS3().getObject().getKey()).isEqualTo("vol1/bucket1/some/key1");
+
assertThat(record.getS3().getObject().getSequencer()).isEqualTo(String.format("%016X",
1L));
+
+ Map<String, Object> expectedEventData = new HashMap<>();
+ expectedEventData.put(OzoneEventDataKey.OP_TYPE.toString(), "CreateKey");
+ expectedEventData.put(OzoneEventDataKey.TRX_LOG_INDEX.toString(), 1);
+
+ assertThat(record.getOzoneEventData()).isEqualTo(expectedEventData);
+ }
+
+ @Test
+ public void testCommitKeyRequestProducesS3CreatedEvent() throws
InterruptedException, IOException {
+ OmCompletedRequestInfo commitRequest = buildCompletedRequestInfo(8L,
Type.CommitKey, "some/key1_commit",
+ new OperationArgs.NoArgs());
+
+ List<String> events = captureEventsProducedByOperation(commitRequest, 1);
+ S3EventNotificationRecord record = getFirstRecord(events);
- assertThat(events.get(0))
- .contains(toJsonKeyVal("key", "vol1/bucket1/some/key1"))
- .contains(toJsonKeyVal("type", "CreateKey"));
+ assertThat(record.getEventName()).isEqualTo("ObjectCreated:Put");
+
assertThat(record.getS3().getObject().getKey()).isEqualTo("vol1/bucket1/some/key1_commit");
+
assertThat(record.getS3().getObject().getSequencer()).isEqualTo(String.format("%016X",
8L));
+
+ Map<String, Object> expectedEventData = new HashMap<>();
+ expectedEventData.put(OzoneEventDataKey.OP_TYPE.toString(), "CommitKey");
+ expectedEventData.put(OzoneEventDataKey.TRX_LOG_INDEX.toString(), 8);
+
+ assertThat(record.getOzoneEventData()).isEqualTo(expectedEventData);
}
@Test
@@ -123,11 +150,20 @@ public void testCreateFileRequestProducesS3CreatedEvent()
throws InterruptedExce
new OperationArgs.CreateFileArgs(recursive, overwrite));
List<String> events = captureEventsProducedByOperation(createRequest, 1);
- assertThat(events).hasSize(1);
+ S3EventNotificationRecord record = getFirstRecord(events);
+
+ assertThat(record.getEventName()).isEqualTo("ObjectCreated:Put");
+
assertThat(record.getS3().getObject().getKey()).isEqualTo("vol1/bucket1/some/key2");
+
assertThat(record.getS3().getObject().getSequencer()).isEqualTo(String.format("%016X",
2L));
- assertThat(events.get(0))
- .contains(toJsonKeyVal("key", "vol1/bucket1/some/key2"))
- .contains(toJsonKeyVal("type", "CreateFile"));
+ Map<String, Object> expectedEventData = new HashMap<>();
+ expectedEventData.put(OzoneEventDataKey.IS_DIRECTORY.toString(), false);
+ expectedEventData.put(OzoneEventDataKey.IS_RECURSIVE.toString(), false);
+ expectedEventData.put(OzoneEventDataKey.IS_OVERWRITE.toString(), true);
+ expectedEventData.put(OzoneEventDataKey.OP_TYPE.toString(), "CreateFile");
+ expectedEventData.put(OzoneEventDataKey.TRX_LOG_INDEX.toString(), 2);
+
+ assertThat(record.getOzoneEventData()).isEqualTo(expectedEventData);
}
@Test
@@ -136,23 +172,138 @@ public void
testCreateDirectoryRequestProducesS3CreatedEvent() throws Interrupte
new OperationArgs.NoArgs());
List<String> events = captureEventsProducedByOperation(createRequest, 1);
- assertThat(events).hasSize(1);
+ S3EventNotificationRecord record = getFirstRecord(events);
+
+ assertThat(record.getEventName()).isEqualTo("ObjectCreated:Put");
+
assertThat(record.getS3().getObject().getKey()).isEqualTo("vol1/bucket1/some/key3");
+
assertThat(record.getS3().getObject().getSequencer()).isEqualTo(String.format("%016X",
3L));
- assertThat(events.get(0))
- .contains(toJsonKeyVal("key", "vol1/bucket1/some/key3"))
- .contains(toJsonKeyVal("type", "CreateDirectory"));
+ Map<String, Object> expectedEventData = new HashMap<>();
+ expectedEventData.put(OzoneEventDataKey.IS_DIRECTORY.toString(), true);
+ expectedEventData.put(OzoneEventDataKey.OP_TYPE.toString(),
"CreateDirectory");
+ expectedEventData.put(OzoneEventDataKey.TRX_LOG_INDEX.toString(), 3);
+
+ assertThat(record.getOzoneEventData()).isEqualTo(expectedEventData);
}
@Test
- public void testRenameRequestProducesRenameKeyEvent() throws
InterruptedException, IOException {
+ public void testRenameRequestProducesS3RenamedEvent() throws
InterruptedException, IOException {
OmCompletedRequestInfo renameRequest = buildCompletedRequestInfo(4L,
Type.RenameKey, "some/key4",
new OperationArgs.RenameKeyArgs("some/key_RENAMED"));
List<String> events = captureEventsProducedByOperation(renameRequest, 1);
- assertThat(events).hasSize(1);
+ S3EventNotificationRecord record = getFirstRecord(events);
+
+ assertThat(record.getEventName()).isEqualTo("ObjectRenamed:Rename");
+
assertThat(record.getS3().getObject().getKey()).isEqualTo("vol1/bucket1/some/key_RENAMED");
+
assertThat(record.getS3().getObject().getSequencer()).isEqualTo(String.format("%016X",
4L));
+
+ Map<String, Object> expectedEventData = new HashMap<>();
+ expectedEventData.put(OzoneEventDataKey.RENAME_FROM_KEY.toString(),
"vol1/bucket1/some/key4");
+ expectedEventData.put(OzoneEventDataKey.TRX_LOG_INDEX.toString(), 4);
+
+ assertThat(record.getOzoneEventData()).isEqualTo(expectedEventData);
+ }
+
+ @Test
+ public void testCreateVolumeRequestProducesOzoneVolumeCreatedEvent() throws
IOException {
+ OmCompletedRequestInfo createRequest = new OmCompletedRequestInfo.Builder()
+ .setTrxLogIndex(6L)
+ .setCmdType(Type.CreateVolume)
+ .setVolumeName(VOLUME_NAME)
+ .setBucketName(null)
+ .setKeyName(null)
+ .setCreationTime(Time.now())
+ .setOpArgs(new OperationArgs.NoArgs())
+ .build();
+
+ List<String> events = captureEventsProducedByOperation(createRequest, 1);
+ S3EventNotificationRecord record = getFirstRecord(events);
+
+ assertThat(record.getEventName()).isEqualTo("OzoneVolumeCreated:Put");
+ assertThat(record.getS3().getBucket().getName()).isNull();
+ assertThat(record.getS3().getObject().getKey()).isEqualTo(VOLUME_NAME);
+
assertThat(record.getS3().getObject().getSequencer()).isEqualTo(String.format("%016X",
6L));
+
+ Map<String, Object> expectedEventData = new HashMap<>();
+ expectedEventData.put(OzoneEventDataKey.TRX_LOG_INDEX.toString(), 6);
- assertThat(events.get(0))
- .contains(toJsonKeyVal("key", "vol1/bucket1/some/key4"))
- .contains(toJsonKeyVal("type", "RenameKey"));
+ assertThat(record.getOzoneEventData()).isEqualTo(expectedEventData);
+ }
+
+ @Test
+ public void testCreateBucketRequestProducesOzoneBucketCreatedEvent() throws
IOException {
+ OmCompletedRequestInfo createRequest = new OmCompletedRequestInfo.Builder()
+ .setTrxLogIndex(7L)
+ .setCmdType(Type.CreateBucket)
+ .setVolumeName(VOLUME_NAME)
+ .setBucketName(BUCKET_NAME)
+ .setKeyName(null)
+ .setCreationTime(Time.now())
+ .setOpArgs(new OperationArgs.NoArgs())
+ .build();
+
+ List<String> events = captureEventsProducedByOperation(createRequest, 1);
+ S3EventNotificationRecord record = getFirstRecord(events);
+
+ assertThat(record.getEventName()).isEqualTo("OzoneBucketCreated:Put");
+ assertThat(record.getS3().getBucket().getName()).isEqualTo(BUCKET_NAME);
+ assertThat(record.getS3().getObject().getKey()).isEqualTo(VOLUME_NAME +
"/" + BUCKET_NAME);
+
assertThat(record.getS3().getObject().getSequencer()).isEqualTo(String.format("%016X",
7L));
+
+ Map<String, Object> expectedEventData = new HashMap<>();
+ expectedEventData.put(OzoneEventDataKey.TRX_LOG_INDEX.toString(), 7);
+
+ assertThat(record.getOzoneEventData()).isEqualTo(expectedEventData);
+ }
+
+ @Test
+ public void testEventDateFormatIsIso8601() throws IOException {
+ OmCompletedRequestInfo createRequest = buildCompletedRequestInfo(5L,
Type.CreateKey, "date/test",
+ new OperationArgs.NoArgs());
+
+ List<String> events = captureEventsProducedByOperation(createRequest, 1);
+ String rawJson = events.get(0);
+
+ // Parse raw JSON to extract the eventTime string exactly as it appears in
the message
+ JsonNode root = JsonUtils.readTree(rawJson);
+ String eventTimeStr =
root.path("Records").get(0).path("eventTime").asText();
+
+ // Validate that it matches ISO-8601 offset format (e.g.,
2026-05-20T13:45:00Z or +01:00)
+ // This ensures our DateTimeJsonSerializer is working as expected.
+
assertThat(eventTimeStr).matches("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*");
+
+ // Also ensure it is actually parsable by the standard Java 8 API
+ java.time.OffsetDateTime.parse(eventTimeStr);
+ }
+
+ @Test
+ public void testInitializeFailsWhenStrategyInstantiationThrows() {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ conf.setClass("ozone.om.plugin.kafka.notification.strategy",
+ ThrowingStrategy.class, OMEventListenerNotificationStrategy.class);
+
+ OMEventListenerKafkaPublisher plugin = new OMEventListenerKafkaPublisher();
+ try (MockedConstruction<OMEventListenerKafkaPublisher.KafkaClientWrapper>
mockedKafkaClientWrapper =
+
mockConstruction(OMEventListenerKafkaPublisher.KafkaClientWrapper.class)) {
+
+
org.junit.jupiter.api.Assertions.assertThrows(OzoneIllegalArgumentException.class,
() -> {
+ plugin.initialize(conf, pluginContext);
+ });
+ }
+ }
+
+ /**
+ * A mock strategy that throws an exception during instantiation for testing.
+ */
+ public static class ThrowingStrategy implements
OMEventListenerNotificationStrategy {
+ public ThrowingStrategy() {
+ throw new RuntimeException("Simulated instantiation failure");
+ }
+
+ @Override
+ public List<String> determineEventsForOperation(OmCompletedRequestInfo
completedRequestInfo) {
+ return null;
+ }
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]