gresockj commented on a change in pull request #4691:
URL: https://github.com/apache/nifi/pull/4691#discussion_r722097750



##########
File path: 
nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecord.java
##########
@@ -209,6 +207,25 @@
             .required(true)
             .build();
 
+    static final PropertyDescriptor AT_TIMESTAMP = new 
PropertyDescriptor.Builder()
+            .name("put-es-record-at-timestamp")
+            .displayName("@timestamp Value")
+            .description("The value to use as the @timestamp field (required 
for Elasticsearch Data Streams)")
+            .required(false)
+            
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .addValidator(StandardValidators.NON_EMPTY_EL_VALIDATOR)
+            .build();
+
+    static final PropertyDescriptor AT_TIMESTAMP_RECORD_PATH = new 
PropertyDescriptor.Builder()
+            .name("put-es-record-at-timestamp-path")
+            .displayName("@timestamp Record Path")
+            .description("A RecordPath pointing to a field in the record(s) 
that contains the @timestamp for the document " +
+                    "(required for Elasticsearch Data Streams). If left blank 
the @timestamp will be determined using the main property type")
+            .required(false)
+            .addValidator(new RecordPathValidator())
+            
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .build();

Review comment:
       It looks like you use `@timestamp Value` as a default timestamp in the 
unit test below -- should we rename it to `Default @timestamp`?  Regardless, I 
think we should make it clearer in the property descriptions what the fallback 
logic is -- right now it looks like both are required properties for Data 
Streams.

##########
File path: 
nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecord.java
##########
@@ -405,11 +424,17 @@ public void onTrigger(final ProcessContext context, final 
ProcessSession session
 
         this.nullSuppression = context.getProperty(SUPPRESS_NULLS).getValue();
 
-        final String id_path = 
context.getProperty(ID_RECORD_PATH).evaluateAttributeExpressions(flowFile).getValue();
-        final RecordPath recordPath = StringUtils.isEmpty(id_path) ? null : 
recordPathCache.getCompiled(id_path);
+        final String idPath = 
context.getProperty(ID_RECORD_PATH).evaluateAttributeExpressions(flowFile).getValue();
+        final RecordPath recordPath = StringUtils.isEmpty(idPath) ? null : 
recordPathCache.getCompiled(idPath);
         final StringBuilder sb = new StringBuilder();
         final Charset charset = 
Charset.forName(context.getProperty(CHARSET).evaluateAttributeExpressions(flowFile).getValue());
 
+        final String atTimestamp = 
context.getProperty(AT_TIMESTAMP).evaluateAttributeExpressions(flowFile).getValue();
+        final String atTimestampPath = 
context.getProperty(AT_TIMESTAMP_RECORD_PATH).isSet()
+                ? 
context.getProperty(AT_TIMESTAMP_RECORD_PATH).evaluateAttributeExpressions(flowFile).getValue()
+                : null;

Review comment:
       If I'm reading `StandardPropertyValue` correctly, this could be 
simplified to the following since `evaluateAttributeExrpessions` gracefully 
handles a null `rawValue`:
   ```suggestion
           final String atTimestampPath = 
context.getProperty(AT_TIMESTAMP_RECORD_PATH)
                   .evaluateAttributeExpressions(flowFile).getValue();
   ```

##########
File path: 
nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecord.java
##########
@@ -209,6 +207,25 @@
             .required(true)
             .build();
 
+    static final PropertyDescriptor AT_TIMESTAMP = new 
PropertyDescriptor.Builder()
+            .name("put-es-record-at-timestamp")
+            .displayName("@timestamp Value")
+            .description("The value to use as the @timestamp field (required 
for Elasticsearch Data Streams)")
+            .required(false)
+            
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .addValidator(StandardValidators.NON_EMPTY_EL_VALIDATOR)
+            .build();
+
+    static final PropertyDescriptor AT_TIMESTAMP_RECORD_PATH = new 
PropertyDescriptor.Builder()
+            .name("put-es-record-at-timestamp-path")
+            .displayName("@timestamp Record Path")
+            .description("A RecordPath pointing to a field in the record(s) 
that contains the @timestamp for the document " +
+                    "(required for Elasticsearch Data Streams). If left blank 
the @timestamp will be determined using the main property type")

Review comment:
       What do you mean by "determined using the main property type"?

##########
File path: 
nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/test/java/org/apache/nifi/processors/elasticsearch/TestPutElasticsearchHttpRecord.java
##########
@@ -498,6 +501,132 @@ public void 
testPutElasticSearchOnTriggerWithInvalidIndexOp() throws IOException
         assertNotNull(out);
     }
 
+    @Test
+    public void testPutElasticsearchOnTriggerWithNoAtTimestampPath() throws 
Exception {
+        PutElasticsearchHttpRecordTestProcessor processor = new 
PutElasticsearchHttpRecordTestProcessor(false);
+        runner = TestRunners.newTestRunner(processor);
+        generateTestData(1);
+        runner.setProperty(AbstractElasticsearchHttpProcessor.ES_URL, 
"http://127.0.0.1:9200";);
+        runner.setProperty(PutElasticsearchHttpRecord.INDEX, "doc");
+
+        runner.removeProperty(PutElasticsearchHttpRecord.AT_TIMESTAMP); // no 
default
+        
runner.setProperty(PutElasticsearchHttpRecord.AT_TIMESTAMP_RECORD_PATH, 
"/none"); // Field does not exist
+        processor.setRecordChecks(record -> assertTimestamp(record, null)); // 
no @timestamp
+        runner.enqueue(new byte[0]);
+        runner.run(1, true, true);
+
+        
runner.assertAllFlowFilesTransferred(PutElasticsearchHttpRecord.REL_SUCCESS, 1);
+        final MockFlowFile out = 
runner.getFlowFilesForRelationship(PutElasticsearchHttpRecord.REL_SUCCESS).get(0);
+        assertNotNull(out);
+        runner.clearTransferState();
+
+        // now add a default @timestamp
+        final String timestamp = "2020-11-27T14:37:00.000Z";
+        runner.setProperty(PutElasticsearchHttpRecord.AT_TIMESTAMP, timestamp);
+        processor.setRecordChecks(record -> assertTimestamp(record, 
timestamp)); // @timestamp defaulted
+        runner.enqueue(new byte[0]);
+        runner.run(1, true, true);
+
+        
runner.assertAllFlowFilesTransferred(PutElasticsearchHttpRecord.REL_SUCCESS, 1);
+        final MockFlowFile out2 = 
runner.getFlowFilesForRelationship(PutElasticsearchHttpRecord.REL_SUCCESS).get(0);
+        assertNotNull(out2);
+    }
+
+    @Test
+    public void testPutElasticsearchOnTriggerWithAtTimestampFromAttribute() 
throws IOException {
+        PutElasticsearchHttpRecordTestProcessor processor = new 
PutElasticsearchHttpRecordTestProcessor(false);
+        runner = TestRunners.newTestRunner(processor);
+        generateTestData(1);
+        runner.setProperty(AbstractElasticsearchHttpProcessor.ES_URL, 
"http://127.0.0.1:9200";);
+        runner.setProperty(PutElasticsearchHttpRecord.INDEX, "${i}");
+        runner.setProperty(PutElasticsearchHttpRecord.AT_TIMESTAMP, 
"${timestamp}");
+
+        final String timestamp = "2020-11-27T15:10:00.000Z";
+        processor.setRecordChecks(record -> assertTimestamp(record, 
timestamp));
+        runner.enqueue(new byte[0], new HashMap<String, String>() {{
+            put("doc_id", "28039652144");
+            put("i", "doc");
+            put("timestamp", timestamp);
+        }});
+        runner.run(1, true, true);
+
+        
runner.assertAllFlowFilesTransferred(PutElasticsearchHttpRecord.REL_SUCCESS, 1);
+        final MockFlowFile out = 
runner.getFlowFilesForRelationship(PutElasticsearchHttpRecord.REL_SUCCESS).get(0);
+        assertNotNull(out);
+        runner.clearTransferState();
+
+        // Now try an empty attribute value, should be no timestamp
+        processor.setRecordChecks(record -> assertTimestamp(record, null));
+        runner.enqueue(new byte[0], new HashMap<String, String>() {{
+            put("doc_id", "28039652144");
+            put("i", "doc");
+        }});
+        runner.run(1, true, true);
+
+        
runner.assertAllFlowFilesTransferred(PutElasticsearchHttpRecord.REL_SUCCESS, 1);
+        final MockFlowFile out2 = 
runner.getFlowFilesForRelationship(PutElasticsearchHttpRecord.REL_SUCCESS).get(0);
+        assertNotNull(out2);
+    }
+
+    @Test
+    public void testPutElasticsearchOnTriggerWithAtTimstampPath() throws 
Exception {
+        PutElasticsearchHttpRecordTestProcessor processor = new 
PutElasticsearchHttpRecordTestProcessor(false);
+        DateTimeFormatter timeFormatter = 
DateTimeFormatter.ofPattern(RecordFieldType.TIME.getDefaultFormat());
+        DateTimeFormatter dateTimeFormatter = 
DateTimeFormatter.ofPattern(RecordFieldType.TIMESTAMP.getDefaultFormat());
+        DateTimeFormatter dateFormatter = 
DateTimeFormatter.ofPattern(RecordFieldType.DATE.getDefaultFormat());
+        runner = TestRunners.newTestRunner(processor);
+        generateTestData(1);
+        runner.setProperty(AbstractElasticsearchHttpProcessor.ES_URL, 
"http://127.0.0.1:9200";);
+        runner.setProperty(PutElasticsearchHttpRecord.INDEX, "doc");
+
+        
runner.setProperty(PutElasticsearchHttpRecord.AT_TIMESTAMP_RECORD_PATH, "/ts"); 
// TIMESTAMP
+        processor.setRecordChecks(record -> assertTimestamp(record, 
LOCAL_DATE_TIME.format(dateTimeFormatter)));
+        runner.enqueue(new byte[0]);
+        runner.run(1, true, true);
+
+        
runner.assertAllFlowFilesTransferred(PutElasticsearchHttpRecord.REL_SUCCESS, 1);
+        
assertNotNull(runner.getFlowFilesForRelationship(PutElasticsearchHttpRecord.REL_SUCCESS).get(0));
+        runner.clearTransferState();
+
+        
runner.setProperty(PutElasticsearchHttpRecord.AT_TIMESTAMP_RECORD_PATH, 
"/date"); // DATE;
+        processor.setRecordChecks(record -> assertTimestamp(record, 
LOCAL_DATE.format(dateFormatter)));
+        runner.enqueue(new byte[0]);
+        runner.run(1, true, true);
+
+        
runner.assertAllFlowFilesTransferred(PutElasticsearchHttpRecord.REL_SUCCESS, 1);
+        
assertNotNull(runner.getFlowFilesForRelationship(PutElasticsearchHttpRecord.REL_SUCCESS).get(0));
+        runner.clearTransferState();
+
+        
runner.setProperty(PutElasticsearchHttpRecord.AT_TIMESTAMP_RECORD_PATH, 
"/time"); // TIME
+        processor.setRecordChecks(record -> assertTimestamp(record, 
LOCAL_TIME.format(timeFormatter)));
+        runner.enqueue(new byte[0]);
+        runner.run(1, true, true);
+
+        
runner.assertAllFlowFilesTransferred(PutElasticsearchHttpRecord.REL_SUCCESS, 1);
+        
assertNotNull(runner.getFlowFilesForRelationship(PutElasticsearchHttpRecord.REL_SUCCESS).get(0));
+        runner.clearTransferState();
+
+        // these INT/STRING values might not make sense from an Elasticsearch 
point of view,
+        // but we want to prove we can handle them being selected from teh 
Record

Review comment:
       the*

##########
File path: 
nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-restapi-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchRecord.java
##########
@@ -377,11 +494,92 @@ private String getFromRecordPath(final Record record, 
final RecordPath path, fin
                 );
             }
 
-            fieldValue.updateValue(null);
+            if (!retain) {
+                fieldValue.updateValue(null);
+            }
 
             return fieldValue.getValue().toString();
         } else {
             return fallback;
         }
     }
+
+    private Object getTimestampFromRecordPath(final Record record, final 
RecordPath path, final String fallback,
+                                              final boolean retain) {
+        if (path == null) {
+            return coerceStringToLong("@timestamp", fallback);
+        }
+
+        final RecordPathResult result = path.evaluate(record);
+        final Optional<FieldValue> value = 
result.getSelectedFields().findFirst();
+        if (value.isPresent() && value.get().getValue() != null) {
+            final FieldValue fieldValue = value.get();
+
+            final DataType dataType = fieldValue.getField().getDataType();
+            final String fieldName = fieldValue.getField().getFieldName();
+            final DataType chosenDataType = dataType.getFieldType() == 
RecordFieldType.CHOICE
+                    ? DataTypeUtils.chooseDataType(value, (ChoiceDataType) 
dataType)
+                    : dataType;
+            final Object coercedValue = 
DataTypeUtils.convertType(fieldValue.getValue(), chosenDataType, fieldName);
+            if (coercedValue == null) {
+                return null;
+            }
+
+            final Object returnValue;
+            switch (chosenDataType.getFieldType()) {
+                case DATE:
+                case TIME:
+                case TIMESTAMP:
+                    final String format;
+                    switch (chosenDataType.getFieldType()) {

Review comment:
       How about moving the nested `switch` to a separate method for readabilty?

##########
File path: 
nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-restapi-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchRecord.java
##########
@@ -147,9 +194,51 @@
         .required(false)
         .build();
 
+    static final PropertyDescriptor AT_TIMESTAMP_DATE_FORMAT = new 
PropertyDescriptor.Builder()
+        .name("put-es-record-at-timestamp-date-format")
+        .displayName("@Timestamp Record Path Date Format")
+        .description("Specifies the format to use when writing Date field for 
@timestamp. "
+                + "If not specified, the default format '" + 
RecordFieldType.DATE.getDefaultFormat() + "' is used. "
+                + "If specified, the value must match the Java Simple Date 
Format (for example, MM/dd/yyyy for a two-digit month, followed by "
+                + "a two-digit day, followed by a four-digit year, all 
separated by '/' characters, as in 01/01/2017).")

Review comment:
       Just to make the example immediately clear, let's use a date like 
`01/25/2017`

##########
File path: 
nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecord.java
##########
@@ -266,9 +283,11 @@
         descriptors.add(RECORD_WRITER);
         descriptors.add(LOG_ALL_ERRORS);
         descriptors.add(ID_RECORD_PATH);
+        descriptors.add(AT_TIMESTAMP_RECORD_PATH);
         descriptors.add(INDEX);
         descriptors.add(TYPE);
         descriptors.add(INDEX_OP);
+        descriptors.add(AT_TIMESTAMP);

Review comment:
       Could we put `AT_TIMESTAMP` right below `AT_TIMESTAMP_RECORD_PATH`?

##########
File path: 
nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-restapi-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchRecord.java
##########
@@ -236,11 +342,18 @@ public void onTrigger(ProcessContext context, 
ProcessSession session) throws Pro
         final String typePath = context.getProperty(TYPE_RECORD_PATH).isSet()
                 ? 
context.getProperty(TYPE_RECORD_PATH).evaluateAttributeExpressions(input).getValue()
                 : null;
+        final String atTimestampPath = 
context.getProperty(AT_TIMESTAMP_RECORD_PATH).isSet()

Review comment:
       Same comment as above




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to