This is an automated email from the ASF dual-hosted git repository.

exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new bd33f2c911 NIFI-8294 Added QueryAzureDataExplorer and 
StandardKustoQueryService
bd33f2c911 is described below

commit bd33f2c9111f998ce7a758e24ca47da19e75e248
Author: Tanmaya Panda <[email protected]>
AuthorDate: Wed Mar 10 16:43:24 2021 +0100

    NIFI-8294 Added QueryAzureDataExplorer and StandardKustoQueryService
    
    This closes #7122
    
    Co-authored-by: David Handermann <[email protected]>
    Signed-off-by: David Handermann <[email protected]>
---
 .../nifi-azure-processors/pom.xml                  |   9 ++
 .../data/explorer/QueryAzureDataExplorer.java      | 151 ++++++++++++++++++
 .../data/explorer/KustoAuthenticationStrategy.java |  49 ++++++
 .../data/explorer/StandardKustoQueryService.java   | 168 +++++++++++++++++++++
 .../org.apache.nifi.controller.ControllerService   |   1 +
 .../services/org.apache.nifi.processor.Processor   |   1 +
 .../data/explorer/QueryAzureDataExplorerTest.java  | 118 +++++++++++++++
 .../explorer/StandardKustoQueryServiceTest.java    |  84 +++++++++++
 .../azure/data/explorer/KustoQueryResponse.java    |  53 +++++++
 .../azure/data/explorer/KustoQueryService.java     |  30 ++++
 10 files changed, 664 insertions(+)

diff --git a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/pom.xml 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/pom.xml
index a416336eed..4a4d463a4e 100644
--- a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/pom.xml
+++ b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/pom.xml
@@ -96,6 +96,15 @@
             <groupId>com.azure</groupId>
             <artifactId>azure-storage-queue</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.microsoft.azure.kusto</groupId>
+            <artifactId>kusto-data</artifactId>
+            <version>4.0.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>
diff --git 
a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/data/explorer/QueryAzureDataExplorer.java
 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/data/explorer/QueryAzureDataExplorer.java
new file mode 100644
index 0000000000..574d954645
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/processors/azure/data/explorer/QueryAzureDataExplorer.java
@@ -0,0 +1,151 @@
+/*
+ * 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.nifi.processors.azure.data.explorer;
+
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.WritesAttribute;
+import org.apache.nifi.annotation.behavior.WritesAttributes;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.services.azure.data.explorer.KustoQueryService;
+import org.apache.nifi.services.azure.data.explorer.KustoQueryResponse;
+
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
+@Tags({"Azure", "Data", "Explorer", "ADX", "Kusto"})
+@CapabilityDescription("Query Azure Data Explorer and stream JSON results to 
output FlowFiles")
+@WritesAttributes({
+        @WritesAttribute(attribute = 
QueryAzureDataExplorer.QUERY_ERROR_MESSAGE, description = "Azure Data Explorer 
query error message on failures"),
+        @WritesAttribute(attribute = QueryAzureDataExplorer.QUERY_EXECUTED, 
description = "Azure Data Explorer query executed"),
+        @WritesAttribute(attribute = "mime.type", description = "Content Type 
set to application/json")
+})
+public class QueryAzureDataExplorer extends AbstractProcessor {
+    public static final String QUERY_ERROR_MESSAGE = "query.error.message";
+
+    public static final String QUERY_EXECUTED = "query.executed";
+
+    public static final Relationship SUCCESS = new Relationship.Builder()
+            .name("success")
+            .description("FlowFiles containing results of a successful Query")
+            .build();
+
+    public static final Relationship FAILURE = new Relationship.Builder()
+            .name("failure")
+            .description("FlowFiles containing original input associated with 
a failed Query")
+            .build();
+
+    public static final PropertyDescriptor KUSTO_QUERY_SERVICE = new 
PropertyDescriptor.Builder()
+            .name("Kusto Query Service")
+            .displayName("Kusto Query Service")
+            .description("Azure Data Explorer Kusto Query Service")
+            .required(true)
+            .identifiesControllerService(KustoQueryService.class)
+            .build();
+
+    public static final PropertyDescriptor DATABASE_NAME = new 
PropertyDescriptor.Builder()
+            .name("Database Name")
+            .displayName("Database Name")
+            .description("Azure Data Explorer Database Name for querying")
+            .required(true)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .build();
+
+    public static final PropertyDescriptor QUERY = new 
PropertyDescriptor.Builder()
+            .name("Query")
+            .displayName("Query")
+            .description("Query to be run against Azure Data Explorer")
+            .required(true)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            
.expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+            .build();
+
+    protected static final String APPLICATION_JSON = "application/json";
+
+    private static final Set<Relationship> RELATIONSHIPS = new 
LinkedHashSet<>(Arrays.asList(SUCCESS, FAILURE));
+
+    private static final List<PropertyDescriptor> DESCRIPTORS = 
Arrays.asList(KUSTO_QUERY_SERVICE, DATABASE_NAME, QUERY);
+
+    private volatile KustoQueryService service;
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return RELATIONSHIPS;
+    }
+
+    @Override
+    public final List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return DESCRIPTORS;
+    }
+
+    @OnScheduled
+    public void onScheduled(final ProcessContext context) {
+        service = 
context.getProperty(KUSTO_QUERY_SERVICE).asControllerService(KustoQueryService.class);
+    }
+
+    @Override
+    public void onTrigger(final ProcessContext context, final ProcessSession 
session) throws ProcessException {
+        FlowFile flowFile = session.get();
+        if (flowFile == null) {
+            return;
+        }
+
+        final String query = 
context.getProperty(QUERY).evaluateAttributeExpressions(flowFile).getValue();
+        final String databaseName = 
context.getProperty(DATABASE_NAME).evaluateAttributeExpressions(flowFile).getValue();
+
+        try {
+            flowFile = session.putAttribute(flowFile, QUERY_EXECUTED, query);
+
+            final KustoQueryResponse kustoQueryResponse = 
executeQuery(databaseName, query);
+            if (kustoQueryResponse.isError()) {
+                getLogger().error("Query failed: {}", 
kustoQueryResponse.getErrorMessage());
+                flowFile = session.putAttribute(flowFile, QUERY_ERROR_MESSAGE, 
kustoQueryResponse.getErrorMessage());
+                session.transfer(flowFile, FAILURE);
+            } else {
+                try (final InputStream responseStream = 
kustoQueryResponse.getResponseStream()) {
+                    flowFile = session.importFrom(responseStream, flowFile);
+                    flowFile = session.putAttribute(flowFile, 
CoreAttributes.MIME_TYPE.key(), APPLICATION_JSON);
+                    session.transfer(flowFile, SUCCESS);
+                }
+            }
+        } catch (final Exception e) {
+            getLogger().error("Query failed", e);
+            flowFile = session.putAttribute(flowFile, QUERY_ERROR_MESSAGE, 
e.getMessage());
+            session.transfer(flowFile, FAILURE);
+        }
+    }
+
+    protected KustoQueryResponse executeQuery(String databaseName, String 
adxQuery) {
+        return service.executeQuery(databaseName,adxQuery);
+    }
+}
diff --git 
a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/services/azure/data/explorer/KustoAuthenticationStrategy.java
 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/services/azure/data/explorer/KustoAuthenticationStrategy.java
new file mode 100644
index 0000000000..01946fe625
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/services/azure/data/explorer/KustoAuthenticationStrategy.java
@@ -0,0 +1,49 @@
+/*
+ * 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.nifi.services.azure.data.explorer;
+
+import org.apache.nifi.components.DescribedValue;
+
+enum KustoAuthenticationStrategy implements DescribedValue {
+    APPLICATION_CREDENTIALS("Application Credentials", "Azure Application 
Registration with Application Key"),
+
+    MANAGED_IDENTITY("Managed Identity", "Azure Managed Identity");
+
+    private final String displayName;
+
+    private final String description;
+
+    KustoAuthenticationStrategy(final String displayName, final String 
description) {
+        this.displayName = displayName;
+        this.description = description;
+    }
+
+    @Override
+    public String getValue() {
+        return name();
+    }
+
+    @Override
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    @Override
+    public String getDescription() {
+        return description;
+    }
+}
diff --git 
a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/services/azure/data/explorer/StandardKustoQueryService.java
 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/services/azure/data/explorer/StandardKustoQueryService.java
new file mode 100644
index 0000000000..a7eb5db4dc
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/java/org/apache/nifi/services/azure/data/explorer/StandardKustoQueryService.java
@@ -0,0 +1,168 @@
+/*
+ * 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.nifi.services.azure.data.explorer;
+
+import com.microsoft.azure.kusto.data.ClientFactory;
+import com.microsoft.azure.kusto.data.StreamingClient;
+import com.microsoft.azure.kusto.data.auth.ConnectionStringBuilder;
+
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+@Tags({"Azure", "Data", "Explorer", "ADX", "Kusto"})
+@CapabilityDescription("Standard implementation of Kusto Query Service for 
Azure Data Explorer")
+public class StandardKustoQueryService extends AbstractControllerService 
implements KustoQueryService {
+
+    public static final PropertyDescriptor CLUSTER_URI = new 
PropertyDescriptor.Builder()
+            .name("Cluster URI")
+            .displayName("Cluster URI")
+            .description("Azure Data Explorer Cluster URI")
+            .required(true)
+            .addValidator(StandardValidators.URL_VALIDATOR)
+            .build();
+
+    public static final PropertyDescriptor AUTHENTICATION_STRATEGY = new 
PropertyDescriptor.Builder()
+            .name("Authentication Strategy")
+            .displayName("Authentication Strategy")
+            .description("Authentication method for access to Azure Data 
Explorer")
+            .required(true)
+            
.defaultValue(KustoAuthenticationStrategy.MANAGED_IDENTITY.getValue())
+            .allowableValues(KustoAuthenticationStrategy.class)
+            .build();
+
+    public static final PropertyDescriptor APPLICATION_CLIENT_ID = new 
PropertyDescriptor.Builder()
+            .name("Application Client ID")
+            .displayName("Application Client ID")
+            .description("Azure Data Explorer Application Client Identifier 
for Authentication")
+            .required(true)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .build();
+
+    public static final PropertyDescriptor APPLICATION_TENANT_ID = new 
PropertyDescriptor.Builder()
+            .name("Application Tenant ID")
+            .displayName("Application Tenant ID")
+            .description("Azure Data Explorer Application Tenant Identifier 
for Authentication")
+            .required(true)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .dependsOn(AUTHENTICATION_STRATEGY, 
KustoAuthenticationStrategy.APPLICATION_CREDENTIALS.getValue())
+            .build();
+
+    public static final PropertyDescriptor APPLICATION_KEY = new 
PropertyDescriptor.Builder()
+            .name("Application Key")
+            .displayName("Application Key")
+            .description("Azure Data Explorer Application Key for 
Authentication")
+            .required(true)
+            .sensitive(true)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .dependsOn(AUTHENTICATION_STRATEGY, 
KustoAuthenticationStrategy.APPLICATION_CREDENTIALS.getValue())
+            .build();
+
+    private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = 
Arrays.asList(
+            CLUSTER_URI,
+            AUTHENTICATION_STRATEGY,
+            APPLICATION_CLIENT_ID,
+            APPLICATION_TENANT_ID,
+            APPLICATION_KEY
+    );
+
+    private volatile StreamingClient kustoClient;
+
+    @Override
+    public List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return PROPERTY_DESCRIPTORS;
+    }
+
+    @OnEnabled
+    public void onEnabled(final ConfigurationContext context) throws 
ProcessException, URISyntaxException {
+        if (this.kustoClient == null) {
+            this.kustoClient = createClient(context);
+        }
+    }
+
+    @OnStopped
+    public final void onStopped() {
+        if (this.kustoClient == null) {
+            getLogger().debug("Kusto Client not configured");
+        } else {
+            try {
+                this.kustoClient.close();
+            } catch (final Exception e) {
+                getLogger().error("Kusto Client close failed", e);
+            }
+            this.kustoClient = null;
+        }
+    }
+
+    @Override
+    public KustoQueryResponse executeQuery(final String databaseName, final 
String query)  {
+        Objects.requireNonNull(databaseName, "Database Name required");
+        Objects.requireNonNull(query, "Query required");
+
+        KustoQueryResponse kustoQueryResponse;
+
+        try {
+            final InputStream responseStream = 
this.kustoClient.executeStreamingQuery(databaseName, query);
+            kustoQueryResponse = new KustoQueryResponse(responseStream);
+        } catch (final Exception e) {
+            getLogger().error("Kusto Query execution failed", e);
+            kustoQueryResponse = new KustoQueryResponse(true, e.getMessage());
+        }
+        return kustoQueryResponse;
+    }
+
+    protected StreamingClient createClient(final ConfigurationContext context) 
throws URISyntaxException {
+        final ConnectionStringBuilder connectionStringBuilder = 
getConnectionStringBuilder(context);
+        return ClientFactory.createStreamingClient(connectionStringBuilder);
+    }
+
+    @SuppressWarnings("unchecked")
+    private ConnectionStringBuilder getConnectionStringBuilder(final 
ConfigurationContext context) {
+        final ConnectionStringBuilder builder;
+
+        final String clusterUrl = context.getProperty(CLUSTER_URI).getValue();
+        final String clientId = 
context.getProperty(APPLICATION_CLIENT_ID).getValue();
+
+        final KustoAuthenticationStrategy kustoAuthenticationStrategy = 
KustoAuthenticationStrategy.valueOf(context.getProperty(AUTHENTICATION_STRATEGY).getValue());
+
+        if (KustoAuthenticationStrategy.MANAGED_IDENTITY == 
kustoAuthenticationStrategy) {
+            builder = 
ConnectionStringBuilder.createWithAadManagedIdentity(clusterUrl, clientId);
+        } else {
+            final String applicationKey = 
context.getProperty(APPLICATION_KEY).getValue();
+            final String tenantId = 
context.getProperty(APPLICATION_TENANT_ID).getValue();
+            builder = 
ConnectionStringBuilder.createWithAadApplicationCredentials(clusterUrl, 
clientId, applicationKey, tenantId);
+        }
+
+        final String vendor = System.getProperty("java.vendor");
+        final String version = System.getProperty("java.version");
+
+        builder.setConnectorDetails(vendor, version, null, null, false, null);
+        return builder;
+    }
+}
diff --git 
a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
index 9e3a7ea3c9..c529f9efd0 100644
--- 
a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
+++ 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
@@ -16,6 +16,7 @@ 
org.apache.nifi.services.azure.eventhub.AzureEventHubRecordSink
 org.apache.nifi.services.azure.storage.ADLSCredentialsControllerService
 org.apache.nifi.services.azure.storage.ADLSCredentialsControllerServiceLookup
 org.apache.nifi.services.azure.cosmos.document.AzureCosmosDBClientService
+org.apache.nifi.services.azure.data.explorer.StandardKustoQueryService
 
org.apache.nifi.services.azure.storage.AzureStorageCredentialsControllerService_v12
 
org.apache.nifi.services.azure.storage.AzureStorageCredentialsControllerServiceLookup_v12
 org.apache.nifi.services.azure.StandardAzureCredentialsControllerService
diff --git 
a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
index d82d4e5ea1..5f8458fd93 100644
--- 
a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
+++ 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
@@ -20,6 +20,7 @@ 
org.apache.nifi.processors.azure.storage.DeleteAzureDataLakeStorage
 org.apache.nifi.processors.azure.storage.FetchAzureDataLakeStorage
 org.apache.nifi.processors.azure.storage.ListAzureDataLakeStorage
 org.apache.nifi.processors.azure.cosmos.document.PutAzureCosmosDBRecord
+org.apache.nifi.processors.azure.data.explorer.QueryAzureDataExplorer
 org.apache.nifi.processors.azure.storage.ListAzureBlobStorage_v12
 org.apache.nifi.processors.azure.storage.FetchAzureBlobStorage_v12
 org.apache.nifi.processors.azure.storage.PutAzureBlobStorage_v12
diff --git 
a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/processors/azure/data/explorer/QueryAzureDataExplorerTest.java
 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/processors/azure/data/explorer/QueryAzureDataExplorerTest.java
new file mode 100644
index 0000000000..fd7e702f29
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/processors/azure/data/explorer/QueryAzureDataExplorerTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.nifi.processors.azure.data.explorer;
+
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.services.azure.data.explorer.KustoQueryResponse;
+import org.apache.nifi.services.azure.data.explorer.KustoQueryService;
+import org.apache.nifi.util.MockFlowFile;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class QueryAzureDataExplorerTest {
+    private static final String SERVICE_ID = KustoQueryService.class.getName();
+
+    private static final String DATABASE_NAME = "records";
+
+    private static final String QUERY = ".show table testing";
+
+    private static final byte[] EMPTY = new byte[]{};
+
+    private static final String ERROR_MESSAGE = "Results not found";
+
+    private static final String EMPTY_ARRAY = "[]";
+
+    @Mock
+    private KustoQueryService kustoQueryService;
+
+    private TestRunner runner;
+
+    @BeforeEach
+    void setRunner() throws InitializationException {
+        runner = TestRunners.newTestRunner(QueryAzureDataExplorer.class);
+
+        when(kustoQueryService.getIdentifier()).thenReturn(SERVICE_ID);
+        runner.addControllerService(SERVICE_ID, kustoQueryService);
+        runner.enableControllerService(kustoQueryService);
+    }
+
+    @Test
+    void testProperties() {
+        runner.assertNotValid();
+
+        runner.setProperty(QueryAzureDataExplorer.KUSTO_QUERY_SERVICE, 
SERVICE_ID);
+        runner.setProperty(QueryAzureDataExplorer.DATABASE_NAME, 
DATABASE_NAME);
+        runner.setProperty(QueryAzureDataExplorer.QUERY, QUERY);
+
+        runner.assertValid();
+    }
+
+    @Test
+    void testRunFailure() {
+        runner.setProperty(QueryAzureDataExplorer.KUSTO_QUERY_SERVICE, 
SERVICE_ID);
+        runner.setProperty(QueryAzureDataExplorer.DATABASE_NAME, 
DATABASE_NAME);
+        runner.setProperty(QueryAzureDataExplorer.QUERY, QUERY);
+
+        runner.enqueue(EMPTY);
+
+        final KustoQueryResponse kustoQueryResponse = new 
KustoQueryResponse(true, ERROR_MESSAGE);
+        when(kustoQueryService.executeQuery(eq(DATABASE_NAME), 
eq(QUERY))).thenReturn(kustoQueryResponse);
+
+        runner.run();
+
+        runner.assertAllFlowFilesTransferred(QueryAzureDataExplorer.FAILURE);
+
+        final MockFlowFile flowFile = 
runner.getFlowFilesForRelationship(QueryAzureDataExplorer.FAILURE).iterator().next();
+        
flowFile.assertAttributeEquals(QueryAzureDataExplorer.QUERY_ERROR_MESSAGE, 
ERROR_MESSAGE);
+        flowFile.assertAttributeEquals(QueryAzureDataExplorer.QUERY_EXECUTED, 
QUERY);
+    }
+
+    @Test
+    void testRunSuccess() {
+        runner.setProperty(QueryAzureDataExplorer.KUSTO_QUERY_SERVICE, 
SERVICE_ID);
+        runner.setProperty(QueryAzureDataExplorer.DATABASE_NAME, 
DATABASE_NAME);
+        runner.setProperty(QueryAzureDataExplorer.QUERY, QUERY);
+
+        runner.enqueue(EMPTY);
+
+        final ByteArrayInputStream inputStream = new 
ByteArrayInputStream(EMPTY_ARRAY.getBytes(StandardCharsets.UTF_8));
+        final KustoQueryResponse kustoQueryResponse = new 
KustoQueryResponse(inputStream);
+        when(kustoQueryService.executeQuery(eq(DATABASE_NAME), 
eq(QUERY))).thenReturn(kustoQueryResponse);
+
+        runner.run();
+
+        runner.assertAllFlowFilesTransferred(QueryAzureDataExplorer.SUCCESS);
+
+        final MockFlowFile flowFile = 
runner.getFlowFilesForRelationship(QueryAzureDataExplorer.SUCCESS).iterator().next();
+        flowFile.assertAttributeEquals(QueryAzureDataExplorer.QUERY_EXECUTED, 
QUERY);
+        flowFile.assertAttributeEquals(CoreAttributes.MIME_TYPE.key(), 
QueryAzureDataExplorer.APPLICATION_JSON);
+        flowFile.assertContentEquals(EMPTY_ARRAY);
+    }
+}
diff --git 
a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/services/azure/data/explorer/StandardKustoQueryServiceTest.java
 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/services/azure/data/explorer/StandardKustoQueryServiceTest.java
new file mode 100644
index 0000000000..fc7bc92bdd
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-processors/src/test/java/org/apache/nifi/services/azure/data/explorer/StandardKustoQueryServiceTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.nifi.services.azure.data.explorer;
+
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.util.NoOpProcessor;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.UUID;
+
+public class StandardKustoQueryServiceTest {
+    private static final String SERVICE_ID = 
StandardKustoQueryServiceTest.class.getSimpleName();
+
+    private static final String CLUSTER_URI = 
"https://cluster.region.kusto.windows.net";;
+
+    private static final String APPLICATION_CLIENT_ID = 
String.class.getSimpleName();
+
+    private StandardKustoQueryService service;
+
+    private TestRunner runner;
+
+    @BeforeEach
+    void setRunner() {
+        runner = TestRunners.newTestRunner(NoOpProcessor.class);
+        service = new StandardKustoQueryService();
+    }
+
+    @Test
+    void testProperties() throws InitializationException {
+        runner.addControllerService(SERVICE_ID, service);
+
+        runner.assertNotValid(service);
+
+        runner.setProperty(service, StandardKustoQueryService.CLUSTER_URI, 
CLUSTER_URI);
+        runner.setProperty(service, 
StandardKustoQueryService.APPLICATION_CLIENT_ID, APPLICATION_CLIENT_ID);
+
+        runner.assertValid(service);
+    }
+
+    @Test
+    void testEnableManagedIdentity() throws InitializationException {
+        runner.addControllerService(SERVICE_ID, service);
+
+        runner.setProperty(service, StandardKustoQueryService.CLUSTER_URI, 
CLUSTER_URI);
+        runner.setProperty(service, 
StandardKustoQueryService.AUTHENTICATION_STRATEGY, 
KustoAuthenticationStrategy.MANAGED_IDENTITY.getValue());
+        runner.setProperty(service, 
StandardKustoQueryService.APPLICATION_CLIENT_ID, APPLICATION_CLIENT_ID);
+
+        runner.assertValid(service);
+
+        runner.enableControllerService(service);
+    }
+
+    @Test
+    void testEnableApplicationCredentials() throws InitializationException {
+        runner.addControllerService(SERVICE_ID, service);
+
+        runner.setProperty(service, StandardKustoQueryService.CLUSTER_URI, 
CLUSTER_URI);
+        runner.setProperty(service, 
StandardKustoQueryService.AUTHENTICATION_STRATEGY, 
KustoAuthenticationStrategy.APPLICATION_CREDENTIALS.getValue());
+        runner.setProperty(service, 
StandardKustoQueryService.APPLICATION_CLIENT_ID, APPLICATION_CLIENT_ID);
+        runner.setProperty(service, StandardKustoQueryService.APPLICATION_KEY, 
UUID.randomUUID().toString());
+        runner.setProperty(service, 
StandardKustoQueryService.APPLICATION_TENANT_ID, UUID.randomUUID().toString());
+
+        runner.assertValid(service);
+
+        runner.enableControllerService(service);
+    }
+}
diff --git 
a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-services-api/src/main/java/org/apache/nifi/services/azure/data/explorer/KustoQueryResponse.java
 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-services-api/src/main/java/org/apache/nifi/services/azure/data/explorer/KustoQueryResponse.java
new file mode 100644
index 0000000000..fae6bb41b1
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-services-api/src/main/java/org/apache/nifi/services/azure/data/explorer/KustoQueryResponse.java
@@ -0,0 +1,53 @@
+/*
+ * 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.nifi.services.azure.data.explorer;
+
+import java.io.InputStream;
+import java.util.Objects;
+
+public class KustoQueryResponse {
+
+    private final InputStream responseStream;
+
+    private final boolean error;
+
+    private final String errorMessage;
+
+    public KustoQueryResponse(final boolean error, final String errorMessage) {
+        this.responseStream = null;
+        this.error = error;
+        this.errorMessage = errorMessage;
+    }
+
+    public KustoQueryResponse(final InputStream responseStream) {
+        this.responseStream = Objects.requireNonNull(responseStream, "Response 
Stream required");
+        this.error = false;
+        this.errorMessage = null;
+    }
+
+    public InputStream getResponseStream() {
+        return responseStream;
+    }
+
+    public boolean isError() {
+        return error;
+    }
+
+    public String getErrorMessage() {
+        return errorMessage;
+    }
+}
diff --git 
a/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-services-api/src/main/java/org/apache/nifi/services/azure/data/explorer/KustoQueryService.java
 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-services-api/src/main/java/org/apache/nifi/services/azure/data/explorer/KustoQueryService.java
new file mode 100644
index 0000000000..a78dd4436c
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-azure-bundle/nifi-azure-services-api/src/main/java/org/apache/nifi/services/azure/data/explorer/KustoQueryService.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.nifi.services.azure.data.explorer;
+
+import org.apache.nifi.controller.ControllerService;
+
+public interface KustoQueryService extends ControllerService {
+    /**
+     * Execute Query on Azure Data Explorer
+     *
+     * @param databaseName Database Name for query
+     * @param query Query to be executed
+     * @return Query Response containing stream or error status
+     */
+    KustoQueryResponse executeQuery(String databaseName, String query);
+}


Reply via email to