Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Integrate prometheus converter, client and response builder #119

Merged
merged 51 commits into from
Dec 13, 2021

Conversation

findingrish
Copy link

@findingrish findingrish commented Dec 10, 2021

Update PrometheusBasedRequestHandler to convert queryRequest to Prometheus query, invoke the rest client to fire the query and finally convert the response to query response

kotharironak and others added 30 commits November 24, 2021 20:05
@github-actions

This comment has been minimized.

Function function = expression.getFunction();
String columnName =
QueryRequestUtil.getLogicalColumnNameForSimpleColumnExpression(
function.getArguments(0));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a common function for forming logical name <function_name>:<attributeId>?

@kotharironak
Copy link
Contributor

@rish691 Can you also add a description for this PR?

@github-actions

This comment has been minimized.

this.name = name;
this.processConfig(config);
this.queryRequestEligibilityValidator =
new QueryRequestEligibilityValidator(prometheusViewDefinition);
this.requestToPromqlConverter = new QueryRequestToPromqlConverter(prometheusViewDefinition);
String[] hostPort = clientConfig.split(":");
this.prometheusRestClient =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have a singleton or have a factory way of constructing it? We can use the same RestClient across all the instance of Request Handler.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

}

PrometheusBasedRequestHandler(
String name, Config config, PrometheusRestClient prometheusRestClient) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this requestHanlder config? If, yes, can we rename to requestHandlerConfig?

this.prometheusRestClient = PrometheusRestClientFactory.get().getPrometheusClient(name);
}

PrometheusBasedRequestHandler(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we mark this it to only for test or something?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this constructor?

// Validate QueryContext and tenant id presence
Preconditions.checkNotNull(executionContext);
Preconditions.checkNotNull(executionContext.getTenantId());

// todo call convert and execute request using client here
Map<Request, PromQLMetricResponse> responseMap;
Map<String, String> metricNameToQueryMap = new LinkedHashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metricNameToQueryMap -> logicalAttributeNameToMetricQueryMap?


return null;
private LinkedHashSet<String> prepareColumnSet(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prepareColumnSet-> prepareSelectionColumnSet

return null;
private LinkedHashSet<String> prepareColumnSet(
LinkedHashSet<Expression> expressions, ExecutionContext executionContext) {
LinkedHashSet<String> columnSet = new LinkedHashSet<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

columnSet -> selectionColumnSet


// Create a Prometheus Client.
public static PrometheusRestClient createPrometheusClient(
String configName, String connectionString) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configName -> requestHandlerName?

QueryTimeRange queryTimeRange = getQueryTimeRange(executionContext);
return new PromQLInstantQueries(

metricNameToQueryMap.putAll(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metricNameToQueryMap -> logicalAttributeNameToMetricQueryMap

@github-actions

This comment has been minimized.

Copy link
Contributor

@kotharironak kotharironak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall, lgtm.

@findingrish findingrish merged commit 42b216f into main Dec 13, 2021
@findingrish findingrish deleted the integrate-prom-metric branch December 13, 2021 13:01
@github-actions
Copy link

Unit Test Results

  30 files  +1    30 suites  +1   7s ⏱️ -2s
158 tests +2  158 ✔️ +2  0 💤 ±0  0 ❌ ±0 

Results for commit 42b216f. ± Comparison against base commit cc6be15.

class PrometheusRestClientFactory {

// Singleton instance
private static final PrometheusRestClientFactory INSTANCE = new PrometheusRestClientFactory();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't we already using guice?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried using it, isn't looking very clean

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm - IMO, the current form where we implement a singleton by hand, use statics, call a static method and ignore its return to modify state and then needlessly take in a handler name is significantly harder to understand and objectively more code. There's potential bugs in there too, because if I give a handler name that's already been registered, my connection string is ignored.

diff --git a/query-service-impl/src/main/java/org/hypertrace/core/query/service/prometheus/PrometheusBasedRequestHandler.java b/query-service-impl/src/main/java/org/hypertrace/core/query/service/prometheus/PrometheusBasedRequestHandler.java
index 9f76633..355bbf3 100644
--- a/query-service-impl/src/main/java/org/hypertrace/core/query/service/prometheus/PrometheusBasedRequestHandler.java
+++ b/query-service-impl/src/main/java/org/hypertrace/core/query/service/prometheus/PrometheusBasedRequestHandler.java
@@ -36,13 +36,14 @@ public class PrometheusBasedRequestHandler implements RequestHandler {
   private Optional<String> startTimeAttributeName;
   private PrometheusViewDefinition prometheusViewDefinition;
 
-  PrometheusBasedRequestHandler(String name, Config requestHandlerConfig) {
+  PrometheusBasedRequestHandler(
+      String name, Config requestHandlerConfig, PrometheusRestClient prometheusRestClient) {
     this.name = name;
     this.processConfig(requestHandlerConfig);
     this.queryRequestEligibilityValidator =
         new QueryRequestEligibilityValidator(prometheusViewDefinition);
     this.requestToPromqlConverter = new QueryRequestToPromqlConverter(prometheusViewDefinition);
-    this.prometheusRestClient = PrometheusRestClientFactory.get().getPrometheusClient(name);
+    this.prometheusRestClient = prometheusRestClient;
   }
 
   @Override
diff --git a/query-service-impl/src/main/java/org/hypertrace/core/query/service/prometheus/PrometheusRequestHandlerBuilder.java b/query-service-impl/src/main/java/org/hypertrace/core/query/service/prometheus/PrometheusRequestHandlerBuilder.java
index 4136015..acd0be7 100644
--- a/query-service-impl/src/main/java/org/hypertrace/core/query/service/prometheus/PrometheusRequestHandlerBuilder.java
+++ b/query-service-impl/src/main/java/org/hypertrace/core/query/service/prometheus/PrometheusRequestHandlerBuilder.java
@@ -10,10 +10,14 @@ import org.hypertrace.core.query.service.RequestHandlerClientConfigRegistry;
 public class PrometheusRequestHandlerBuilder implements RequestHandlerBuilder {
 
   private final RequestHandlerClientConfigRegistry clientConfigRegistry;
+  private final PrometheusRestClientFactory prometheusRestClientFactory;
 
   @Inject
-  PrometheusRequestHandlerBuilder(RequestHandlerClientConfigRegistry clientConfigRegistry) {
+  PrometheusRequestHandlerBuilder(
+      RequestHandlerClientConfigRegistry clientConfigRegistry,
+      PrometheusRestClientFactory prometheusRestClientFactory) {
     this.clientConfigRegistry = clientConfigRegistry;
+    this.prometheusRestClientFactory = prometheusRestClientFactory;
   }
 
   @Override
@@ -32,9 +36,9 @@ public class PrometheusRequestHandlerBuilder implements RequestHandlerBuilder {
                     new UnsupportedOperationException(
                         "Client config requested but not registered: " + config.getClientConfig()));
 
-    PrometheusRestClientFactory.createPrometheusClient(
-        config.getName(), clientConfig.getConnectionString());
-
-    return new PrometheusBasedRequestHandler(config.getName(), config.getRequestHandlerInfo());
+    return new PrometheusBasedRequestHandler(
+        config.getName(),
+        config.getRequestHandlerInfo(),
+        prometheusRestClientFactory.getPrometheusClient(clientConfig.getConnectionString()));
   }
 }
diff --git a/query-service-impl/src/main/java/org/hypertrace/core/query/service/prometheus/PrometheusRestClientFactory.java b/query-service-impl/src/main/java/org/hypertrace/core/query/service/prometheus/PrometheusRestClientFactory.java
index a1b7bad..e1c8d60 100644
--- a/query-service-impl/src/main/java/org/hypertrace/core/query/service/prometheus/PrometheusRestClientFactory.java
+++ b/query-service-impl/src/main/java/org/hypertrace/core/query/service/prometheus/PrometheusRestClientFactory.java
@@ -1,43 +1,15 @@
 package org.hypertrace.core.query.service.prometheus;
 
 import java.util.concurrent.ConcurrentHashMap;
+import javax.inject.Singleton;
 
+@Singleton
 class PrometheusRestClientFactory {
 
-  // Singleton instance
-  private static final PrometheusRestClientFactory INSTANCE = new PrometheusRestClientFactory();
-
   private final ConcurrentHashMap<String, PrometheusRestClient> clientMap =
       new ConcurrentHashMap<>();
 
-  private PrometheusRestClientFactory() {}
-
-  // Create a Prometheus Client.
-  public static PrometheusRestClient createPrometheusClient(
-      String requestHandlerName, String connectionString) {
-    if (!get().containsClient(requestHandlerName)) {
-      synchronized (get()) {
-        if (!get().containsClient(requestHandlerName)) {
-          get().addPrometheusClient(requestHandlerName, new PrometheusRestClient(connectionString));
-        }
-      }
-    }
-    return get().getPrometheusClient(requestHandlerName);
-  }
-
-  public static PrometheusRestClientFactory get() {
-    return INSTANCE;
-  }
-
-  private void addPrometheusClient(String requestHandlerName, PrometheusRestClient client) {
-    this.clientMap.put(requestHandlerName, client);
-  }
-
-  public boolean containsClient(String requestHandlerName) {
-    return this.clientMap.containsKey(requestHandlerName);
-  }
-
-  public PrometheusRestClient getPrometheusClient(String requestHandlerName) {
-    return this.clientMap.get(requestHandlerName);
+  public PrometheusRestClient getPrometheusClient(String connectionString) {
+    return clientMap.computeIfAbsent(connectionString, PrometheusRestClient::new);
   }
 }
diff --git a/query-service-impl/src/test/java/org/hypertrace/core/query/service/prometheus/PrometheusBasedRequestHandlerTest.java b/query-service-impl/src/test/java/org/hypertrace/core/query/service/prometheus/PrometheusBasedRequestHandlerTest.java
index 26a4607..b4d4c09 100644
--- a/query-service-impl/src/test/java/org/hypertrace/core/query/service/prometheus/PrometheusBasedRequestHandlerTest.java
+++ b/query-service-impl/src/test/java/org/hypertrace/core/query/service/prometheus/PrometheusBasedRequestHandlerTest.java
@@ -40,11 +40,12 @@ class PrometheusBasedRequestHandlerTest {
     mockWebServer.start(9099);
 
     Config config = PrometheusTestUtils.getDefaultPrometheusConfig();
-    PrometheusRestClientFactory.createPrometheusClient(
-        config.getString(CONFIG_PATH_NAME), config.getString(CONFIG_PATH_CLIENT_KEY));
+
     prometheusBasedRequestHandler =
         new PrometheusBasedRequestHandler(
-            config.getString(CONFIG_PATH_NAME), config.getConfig(CONFIG_PATH_REQUEST_HANDLER_INFO));
+            config.getString(CONFIG_PATH_NAME),
+            config.getConfig(CONFIG_PATH_REQUEST_HANDLER_INFO),
+            new PrometheusRestClient(config.getString(CONFIG_PATH_CLIENT_KEY)));
   }
 
   @AfterAll

return null;
private LinkedHashSet<String> prepareSelectionColumnSet(
LinkedHashSet<Expression> expressions, ExecutionContext executionContext) {
LinkedHashSet<String> selectionColumnSet = new LinkedHashSet<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collect and make immutable rather than returning an immutable type. Also, why are we returning LinkedHashSet If we're concerned about order and need to declare it, return a list; if we're not concerned about order or we are in this function but not elsewhere, return a Set. In other words, does this api need to both declare that it's unique and ordered, or are those implementation details?

Copy link
Author

@findingrish findingrish Dec 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The requirement from this method is that the returned collection has unique element and the order is same as the input collection.

Now returning set from this method would mean losing the order.
A list can be returned as it will maintain the order (the input collection already has unique elements so will the list)

If you see the flow of data,
this method is invoked on a collection in the ExecutionContext class, which returns object of type LinkedHashSet,
then that collection is processed by this method. Finally it is passed into the response builder class a method param.

PrometheusBasedResponseBuilder.buildResponse(
            responseMap,
            prometheusViewDefinition.getAttributeMap(),
            logicalAttributeNameToMetricQueryMap,
            prepareSelectionColumnSet(executionContext.getAllSelections(), executionContext),
            executionContext.getTimeFilterColumn());

Checking the users of executionContext.getAllSelections(), I think this collection itself could have been of List type

I understand that wherever possible we should return interface, but how to convey that the param LinkedHashSet<String> columnSelectionSet (which will be changed to List<String> columnSelectionSet) in this method org.hypertrace.core.query.service.prometheus.PrometheusBasedResponseBuilder#buildResponse must have ordered and unique elements?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that wherever possible we should return interface, but how to convey that the param LinkedHashSet columnSelectionSet (which will be changed to List columnSelectionSet) in this method org.hypertrace.core.query.service.prometheus.PrometheusBasedResponseBuilder#buildResponse must have ordered and unique elements?

The problem here isn't just "use an interface instead of an implementation", it's what that rule of thumb is based on - that returning the type is leaking implementation details. It's much less about the implementation of the collection itself, it's that we're trying to convey the responsibilities of one class to another with it. We only need (or want) one class to understand how to prepare selections, and that's the only place that needs to know if duplicates are allowed or not. The caller can then trust that prepare the selection column set was responsible for doing everything involved in preparing a selection set, but it doesn't need to know what that was.

String columnName =
QueryRequestUtil.getLogicalColumnNameForSimpleColumnExpression(
functionExpression.getFunction().getArguments(0));
return String.join(":", functionName, columnName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me nervous - a lot of the work on the attribute expressions feature has been in fixing similar code where we generate aliases or use attribute IDs as column names in the result metadata and ignore the requested alias. Since this is ignoring the requested alias, is it for internal use only or does this show up in the response?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's for internal mapping and internal use only (It doesn't show up in response). As we have to fire multiple queries for the same QS query, and if that query has two metrics function on the same attributed, we needed a unique mapping.

e.g

select SERVICE.service_id, sum(SERVICE.numCalls), avg(SERVICE.numCalls).

As in the above example, the same attribute id is used for two different metric functions. It wasn't the issue in the case of PinotBase response where it can be mapped using column index. But, here, we will have two PromQL responses - one for sum(SERVICE.numCalls) and one for avg(SERVICE.numCalls). So, internally, we need a unique map of the response, so we are using <funcname>:<attributeId> internally for response building.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it; In other places, we were building the response metadata like this, rather than using the alias. As long as it's switching back to that before returning, we're good.

@findingrish findingrish mentioned this pull request Dec 14, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants