You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2021/11/29 10:46:21 UTC

[camel] 02/04: CAMEL-17141: camel-health - Include error details for HTTP status code.

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

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

commit 7bd9eb0159509df11ac07297f70a785ea5a8fcd8
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Mon Nov 29 11:33:41 2021 +0100

    CAMEL-17141: camel-health - Include error details for HTTP status code.
---
 .../component/telegram/TelegramException.java      | 58 ++++++++++++++++++++++
 .../service/TelegramServiceRestBotAPIAdapter.java  |  7 ++-
 .../TelegramConsumerHealthCheckErrorTest.java      | 23 +++++----
 .../java/org/apache/camel/health/HealthCheck.java  |  1 +
 .../org/apache/camel/spi/HttpResponseAware.java    | 43 ++++++++++++++++
 .../camel/impl/health/ConsumerHealthCheck.java     | 11 +++-
 .../camel/support/ScheduledPollConsumer.java       | 58 ++++++++++++++++++++++
 .../support/ScheduledPollConsumerHealthCheck.java  | 11 ++--
 8 files changed, 198 insertions(+), 14 deletions(-)

diff --git a/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/TelegramException.java b/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/TelegramException.java
new file mode 100644
index 0000000..593b6a7
--- /dev/null
+++ b/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/TelegramException.java
@@ -0,0 +1,58 @@
+/*
+ * 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.camel.component.telegram;
+
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.spi.HttpResponseAware;
+
+public class TelegramException extends RuntimeCamelException implements HttpResponseAware {
+
+    private int httpResponseCode;
+    private String httpResponseStatus;
+
+    public TelegramException(String message) {
+        super(message);
+    }
+
+    public TelegramException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public TelegramException(Throwable cause) {
+        super(cause);
+    }
+
+    @Override
+    public int getHttpResponseCode() {
+        return httpResponseCode;
+    }
+
+    @Override
+    public void setHttpResponseCode(int httpResponseCode) {
+        this.httpResponseCode = httpResponseCode;
+    }
+
+    @Override
+    public String getHttpResponseStatus() {
+        return httpResponseStatus;
+    }
+
+    @Override
+    public void setHttpResponseStatus(String httpResponseStatus) {
+        this.httpResponseStatus = httpResponseStatus;
+    }
+}
diff --git a/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/service/TelegramServiceRestBotAPIAdapter.java b/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/service/TelegramServiceRestBotAPIAdapter.java
index 76f01c0..6b89527 100644
--- a/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/service/TelegramServiceRestBotAPIAdapter.java
+++ b/components/camel-telegram/src/main/java/org/apache/camel/component/telegram/service/TelegramServiceRestBotAPIAdapter.java
@@ -34,6 +34,7 @@ import io.netty.handler.codec.http.HttpHeaders;
 import org.apache.camel.AsyncCallback;
 import org.apache.camel.Exchange;
 import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.component.telegram.TelegramException;
 import org.apache.camel.component.telegram.TelegramService;
 import org.apache.camel.component.telegram.model.EditMessageCaptionMessage;
 import org.apache.camel.component.telegram.model.EditMessageDelete;
@@ -154,6 +155,7 @@ public class TelegramServiceRestBotAPIAdapter implements TelegramService {
         try {
             final Response response = asyncHttpClient.executeRequest(request).get();
             int code = response.getStatusCode();
+            String status = response.getStatusText();
             if (code >= 200 && code < 300) {
                 try {
                     final String responseBody = response.getResponseBody();
@@ -166,9 +168,12 @@ public class TelegramServiceRestBotAPIAdapter implements TelegramService {
                             "Could not parse the response from " + request.getMethod() + " " + request.getUrl(), e);
                 }
             } else {
-                throw new RuntimeCamelException(
+                TelegramException cause = new TelegramException(
                         "Could not " + request.getMethod() + " " + request.getUrl() + ": " + response.getStatusCode() + " "
                                                 + response.getStatusText());
+                cause.setHttpResponseCode(code);
+                cause.setHttpResponseStatus(status);
+                throw cause;
             }
         } catch (ExecutionException e) {
             throw new RuntimeCamelException("Could not request " + request.getMethod() + " " + request.getUrl(), e);
diff --git a/components/camel-telegram/src/test/java/org/apache/camel/component/telegram/TelegramConsumerHealthCheckErrorTest.java b/components/camel-telegram/src/test/java/org/apache/camel/component/telegram/TelegramConsumerHealthCheckErrorTest.java
index b7a4892..c0447b6 100644
--- a/components/camel-telegram/src/test/java/org/apache/camel/component/telegram/TelegramConsumerHealthCheckErrorTest.java
+++ b/components/camel-telegram/src/test/java/org/apache/camel/component/telegram/TelegramConsumerHealthCheckErrorTest.java
@@ -69,24 +69,29 @@ public class TelegramConsumerHealthCheckErrorTest extends TelegramTestSupport {
 
         // if we grab the health check by id, we can also check it afterwards
         HealthCheck hc = hcr.getCheck("consumer:telegram").get();
+
+        // wait until we have the error
+        Awaitility.waitAtMost(5, TimeUnit.SECONDS).until(
+                () -> {
+                    HealthCheck.Result rc = hc.call();
+                    Long count = (Long) rc.getDetails().get(HealthCheck.FAILURE_ERROR_COUNT);
+                    return count != null && count > 0;
+                });
+
         HealthCheck.Result rc = hc.call();
 
         // and get the detailed error message (and exception)
         Assertions.assertEquals(HealthCheck.State.DOWN, rc.getState());
         String msg = rc.getMessage().get();
         long count = (long) rc.getDetails().get(HealthCheck.FAILURE_ERROR_COUNT);
-        if (count == 0) {
-            Assertions.assertEquals("Consumer has not yet polled route: telegram (telegram://bots)", msg);
-        } else {
-            Assertions.assertEquals("Consumer failed polling " + count + " times route: telegram (telegram://bots)", msg);
-        }
+        Assertions.assertEquals("Consumer failed polling " + count + " times route: telegram (telegram://bots)", msg);
         Assertions.assertEquals("telegram://bots?authorizationToken=mock-token",
                 rc.getDetails().get(HealthCheck.FAILURE_ENDPOINT_URI));
 
-        if (rc.getError().isPresent()) {
-            Throwable e = rc.getError().get();
-            Assertions.assertTrue(e.getMessage().contains("401 Unauthorized"));
-        }
+        Throwable e = rc.getError().get();
+        Assertions.assertTrue(e.getMessage().contains("401 Unauthorized"));
+        // TODO: add http status code
+        // Assertions.assertEquals(401, rc.getDetails().get(HealthCheck.HTTP_RESPONSE_CODE));
     }
 
     @Override
diff --git a/core/camel-api/src/main/java/org/apache/camel/health/HealthCheck.java b/core/camel-api/src/main/java/org/apache/camel/health/HealthCheck.java
index bb9fd2a..1258c73 100644
--- a/core/camel-api/src/main/java/org/apache/camel/health/HealthCheck.java
+++ b/core/camel-api/src/main/java/org/apache/camel/health/HealthCheck.java
@@ -39,6 +39,7 @@ public interface HealthCheck extends HasGroup, HasId, Ordered {
     String FAILURE_ENDPOINT_URI = "failure.endpoint.uri";
     String FAILURE_ERROR_COUNT = "failure.error.count";
     String SUCCESS_COUNT = "success.count";
+    String HTTP_RESPONSE_CODE = "http.response.code";
 
     enum State {
         UP,
diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/HttpResponseAware.java b/core/camel-api/src/main/java/org/apache/camel/spi/HttpResponseAware.java
new file mode 100644
index 0000000..a9bf0eb
--- /dev/null
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/HttpResponseAware.java
@@ -0,0 +1,43 @@
+/*
+ * 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.camel.spi;
+
+/**
+ * Represents an object which is aware of HTTP responses.
+ */
+public interface HttpResponseAware {
+
+    /**
+     * The HTTP status code
+     */
+    int getHttpResponseCode();
+
+    /**
+     * Sets the HTTP status code
+     */
+    void setHttpResponseCode(int code);
+
+    /**
+     * The HTTP status line
+     */
+    String getHttpResponseStatus();
+
+    /**
+     * Sets the HTTP status line
+     */
+    void setHttpResponseStatus(String status);
+}
diff --git a/core/camel-health/src/main/java/org/apache/camel/impl/health/ConsumerHealthCheck.java b/core/camel-health/src/main/java/org/apache/camel/impl/health/ConsumerHealthCheck.java
index cd6ccf0..99ddb25 100644
--- a/core/camel-health/src/main/java/org/apache/camel/impl/health/ConsumerHealthCheck.java
+++ b/core/camel-health/src/main/java/org/apache/camel/impl/health/ConsumerHealthCheck.java
@@ -23,6 +23,7 @@ import org.apache.camel.Route;
 import org.apache.camel.health.HealthCheck;
 import org.apache.camel.health.HealthCheckAware;
 import org.apache.camel.health.HealthCheckResultBuilder;
+import org.apache.camel.spi.HttpResponseAware;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -61,7 +62,15 @@ public class ConsumerHealthCheck extends RouteHealthCheck {
                     builder.message(result.getMessage().get());
                 }
                 if (result.getError().isPresent()) {
-                    builder.error(result.getError().get());
+                    Throwable cause = result.getError().get();
+                    builder.error(cause);
+                    // if the caused exception is HTTP response aware then include the response status code
+                    if (cause instanceof HttpResponseAware) {
+                        int code = ((HttpResponseAware) cause).getHttpResponseCode();
+                        if (code > 0) {
+                            builder.detail(HealthCheck.HTTP_RESPONSE_CODE, code);
+                        }
+                    }
                 }
                 builder.details(result.getDetails());
             }
diff --git a/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumer.java b/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumer.java
index 3131442..d0be2cc 100644
--- a/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumer.java
+++ b/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumer.java
@@ -16,10 +16,12 @@
  */
 package org.apache.camel.support;
 
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
 
 import org.apache.camel.Endpoint;
@@ -29,7 +31,9 @@ import org.apache.camel.LoggingLevel;
 import org.apache.camel.PollingConsumerPollingStrategy;
 import org.apache.camel.Processor;
 import org.apache.camel.Suspendable;
+import org.apache.camel.health.HealthCheck;
 import org.apache.camel.health.HealthCheckAware;
+import org.apache.camel.spi.HttpResponseAware;
 import org.apache.camel.spi.PollingConsumerPollStrategy;
 import org.apache.camel.spi.ScheduledPollConsumerScheduler;
 import org.apache.camel.support.service.ServiceHelper;
@@ -73,7 +77,9 @@ public abstract class ScheduledPollConsumer extends DefaultConsumer
     private volatile long errorCounter;
     private volatile long successCounter;
     private volatile Throwable lastError;
+    private volatile Map<String, Object> lastErrorDetails;
     private final AtomicLong counter = new AtomicLong();
+    private final AtomicBoolean firstPoolDone = new AtomicBoolean();
 
     public ScheduledPollConsumer(Endpoint endpoint, Processor processor) {
         super(endpoint, processor);
@@ -255,12 +261,24 @@ public abstract class ScheduledPollConsumer extends DefaultConsumer
             successCounter = 0;
             errorCounter++;
             lastError = cause;
+            // enrich last error with http response code if possible
+            if (cause instanceof HttpResponseAware) {
+                int code = ((HttpResponseAware) cause).getHttpResponseCode();
+                if (code > 0) {
+                    addLastErrorDetail(HealthCheck.HTTP_RESPONSE_CODE, code);
+                }
+            }
         } else {
             idleCounter = polledMessages == 0 ? ++idleCounter : 0;
             successCounter++;
             errorCounter = 0;
             lastError = null;
+            lastErrorDetails = null;
         }
+
+        // first pool is done after starting
+        firstPoolDone.set(true);
+
         LOG.trace("doRun() done with idleCounter={}, successCounter={}, errorCounter={}", idleCounter, successCounter,
                 errorCounter);
 
@@ -459,6 +477,13 @@ public abstract class ScheduledPollConsumer extends DefaultConsumer
     }
 
     /**
+     * Whether a first pool attempt has been done (also if the consumer has been restarted)
+     */
+    protected boolean isFirstPoolDone() {
+        return firstPoolDone.get();
+    }
+
+    /**
      * Gets the last caused error (exception) for the last poll that failed. When the consumer is successfully again,
      * then the error resets to null.
      */
@@ -467,6 +492,38 @@ public abstract class ScheduledPollConsumer extends DefaultConsumer
     }
 
     /**
+     * Gets the last caused error (exception) details for the last poll that failed. When the consumer is successfully
+     * again, then the error resets to null.
+     *
+     * Some consumers can provide additional error details here, besides the caused exception. For example if the
+     * consumer uses HTTP then the {@link org.apache.camel.health.HealthCheck#HTTP_RESPONSE_CODE} can be included.
+     *
+     * @return error details, or null if no details exists.
+     */
+    protected Map<String, Object> getLastErrorDetails() {
+        return lastErrorDetails;
+    }
+
+    /**
+     * Adds a detail to the last caused error (exception) for the last poll that failed. When the consumer is
+     * successfully again, then the error resets to null.
+     *
+     * Some consumers can provide additional error details here, besides the caused exception. For example if the
+     * consumer uses HTTP then the {@link org.apache.camel.health.HealthCheck#HTTP_RESPONSE_CODE} can be included.
+     *
+     * @param key   the key (see {@link org.apache.camel.health.HealthCheck})
+     * @param value the value
+     */
+    protected void addLastErrorDetail(String key, Object value) {
+        if (lastErrorDetails == null) {
+            lastErrorDetails = new HashMap<>();
+        }
+        if (lastErrorDetails != null) {
+            lastErrorDetails.put(key, value);
+        }
+    }
+
+    /**
      * The polling method which is invoked periodically to poll this consumer
      *
      * @return           number of messages polled, will be <tt>0</tt> if no message was polled at all.
@@ -570,6 +627,7 @@ public abstract class ScheduledPollConsumer extends DefaultConsumer
         errorCounter = 0;
         successCounter = 0;
         counter.set(0);
+        firstPoolDone.set(false);
 
         super.doStop();
     }
diff --git a/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumerHealthCheck.java b/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumerHealthCheck.java
index 3839ca0..ec9fa57 100644
--- a/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumerHealthCheck.java
+++ b/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumerHealthCheck.java
@@ -49,11 +49,11 @@ public class ScheduledPollConsumerHealthCheck implements HealthCheck {
         builder.detail(FAILURE_ENDPOINT_URI, consumer.getEndpoint().getEndpointUri());
 
         long ec = consumer.getErrorCounter();
-        long cnt = consumer.getCounter();
+        boolean first = consumer.isFirstPoolDone();
         Throwable cause = consumer.getLastError();
 
-        // can only be healthy if we have at least one poll and there are no errors
-        boolean healthy = cnt > 0 && ec == 0;
+        // can only be healthy if we have at least one poll done and there are no errors
+        boolean healthy = first && ec == 0;
         if (healthy) {
             builder.up();
         } else {
@@ -68,6 +68,11 @@ public class ScheduledPollConsumerHealthCheck implements HealthCheck {
                 builder.message(String.format(msg, rid, sanitizedUri));
             }
             builder.error(cause);
+
+            // include any additional details
+            if (consumer.getLastErrorDetails() != null) {
+                builder.details(consumer.getLastErrorDetails());
+            }
         }
 
         return builder.build();