You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by jb...@apache.org on 2020/06/17 15:45:51 UTC

[karaf-decanter] branch master updated: [KARAF-5910] Improvements on Rest collector and appender (supporting basic authentication, custom headers, verb, ...)

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

jbonofre pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/karaf-decanter.git


The following commit(s) were added to refs/heads/master by this push:
     new 6781fc6  [KARAF-5910] Improvements on Rest collector and appender (supporting basic authentication, custom headers, verb, ...)
     new 2a4ff7f  Merge pull request #188 from jbonofre/KARAF-5910
6781fc6 is described below

commit 6781fc67a5b0bae2273c7229eaac9f732d4d4e98
Author: jbonofre <jb...@apache.org>
AuthorDate: Wed Jun 17 16:23:32 2020 +0200

    [KARAF-5910] Improvements on Rest collector and appender (supporting basic authentication, custom headers, verb, ...)
---
 appender/rest/pom.xml                              | 32 +++++++-
 .../org.apache.karaf.decanter.appender.rest.cfg    |  8 ++
 .../karaf/decanter/appender/rest/RestAppender.java | 42 ++++++++--
 .../decanter/appender/rest/RestAppenderTest.java   | 93 ++++++++++++----------
 .../karaf/decanter/appender}/rest/TestService.java | 28 ++++---
 .../org.apache.karaf.decanter.collector.rest.cfg   |  7 ++
 .../decanter/collector/rest/RestCollector.java     | 44 +++++++---
 .../decanter/collector/rest/RestCollectorTest.java | 75 +++++++++++++++++
 .../karaf/decanter/collector/rest/TestService.java | 26 ++++--
 manual/src/main/asciidoc/user-guide/appenders.adoc | 45 ++++++++++-
 .../src/main/asciidoc/user-guide/collectors.adoc   | 31 ++++++++
 11 files changed, 348 insertions(+), 83 deletions(-)

diff --git a/appender/rest/pom.xml b/appender/rest/pom.xml
index 650ec1f..cd71553 100644
--- a/appender/rest/pom.xml
+++ b/appender/rest/pom.xml
@@ -33,6 +33,10 @@
     <packaging>bundle</packaging>
     <name>Apache Karaf :: Decanter :: Appender :: REST</name>
 
+    <properties>
+        <cxf.version>3.3.6</cxf.version>
+    </properties>
+
     <dependencies>
         <dependency>
             <groupId>org.apache.karaf.decanter</groupId>
@@ -42,15 +46,35 @@
             <groupId>org.apache.karaf.decanter.appender</groupId>
             <artifactId>org.apache.karaf.decanter.appender.utils</artifactId>
         </dependency>
+
+        <!-- test -->
         <dependency>
             <groupId>org.apache.karaf.decanter.marshaller</groupId>
             <artifactId>org.apache.karaf.decanter.marshaller.json</artifactId>
-            <scope>test</scope> 
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.johnzon</groupId>
+            <artifactId>johnzon-mapper</artifactId>
+            <version>1.2.7</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-transports-http-jetty</artifactId>
+            <version>${cxf.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cxf</groupId>
+            <artifactId>cxf-rt-frontend-jaxrs</artifactId>
+            <version>${cxf.version}</version>
+            <scope>test</scope>
         </dependency>
         <dependency>
-        	<groupId>org.slf4j</groupId>
-        	<artifactId>slf4j-jdk14</artifactId>
-        	<version>1.7.21</version>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-jdk14</artifactId>
+            <version>1.7.21</version>
         </dependency>
     </dependencies>
 
diff --git a/appender/rest/src/main/cfg/org.apache.karaf.decanter.appender.rest.cfg b/appender/rest/src/main/cfg/org.apache.karaf.decanter.appender.rest.cfg
index 369c1aa..7ec9174 100644
--- a/appender/rest/src/main/cfg/org.apache.karaf.decanter.appender.rest.cfg
+++ b/appender/rest/src/main/cfg/org.apache.karaf.decanter.appender.rest.cfg
@@ -24,5 +24,13 @@
 # Mandatory URI where the REST appender connects to
 uri=
 
+#request.method=POST (the REST verb)
+#user= (for basic authentication)
+#password= (for basic authentication)
+#content.type=application/json (the message content type sent)
+#charset=utf-8 (the message charset)
+#header.foo= (HTTP header prefixed with header.)
+#payload.header= (if set the Decanter collected data is sent as HTTP header instead of body)
+
 # Marshaller to use (json is recommended)
 marshaller.target=(dataFormat=json)
diff --git a/appender/rest/src/main/java/org/apache/karaf/decanter/appender/rest/RestAppender.java b/appender/rest/src/main/java/org/apache/karaf/decanter/appender/rest/RestAppender.java
index 6130bbe..0dfffe2 100644
--- a/appender/rest/src/main/java/org/apache/karaf/decanter/appender/rest/RestAppender.java
+++ b/appender/rest/src/main/java/org/apache/karaf/decanter/appender/rest/RestAppender.java
@@ -16,12 +16,16 @@
  */
 package org.apache.karaf.decanter.appender.rest;
 
+import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.HttpURLConnection;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
 import java.util.Dictionary;
+import java.util.Enumeration;
 
 import org.apache.karaf.decanter.api.marshaller.Marshaller;
 import org.apache.karaf.decanter.appender.utils.EventFilter;
@@ -84,12 +88,38 @@ public class RestAppender implements EventHandler {
                 HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
                 connection.setDoOutput(true);
                 connection.setInstanceFollowRedirects(false);
-                connection.setRequestMethod("POST");
-                connection.setRequestProperty("Content-Type", "application/json");
-                connection.setRequestProperty("charset", "utf-8");
-                OutputStream out = connection.getOutputStream();
-                marshaller.marshal(event, out);
-                out.close();
+                String user = config.get("user") != null ? (String) config.get("user") : null;
+                String password = config.get("password") != null ? (String) config.get("password") : null;
+                if (user != null) {
+                    String authentication = user + ":" + password;
+                    byte[] encodedAuthentication = Base64.getEncoder().encode(authentication.getBytes(StandardCharsets.UTF_8));
+                    String authenticationHeader = "Basic " + new String(encodedAuthentication);
+                    connection.setRequestProperty("Authorization", authenticationHeader);
+                }
+                String requestMethod = config.get("request.method") != null ? (String) config.get("request.method") : "POST";
+                connection.setRequestMethod(requestMethod);
+                String contentType = config.get("content.type") != null ? (String) config.get("content.type") : "application/json";
+                connection.setRequestProperty("Content-Type",  contentType);
+                String charset = config.get("charset") != null ? (String) config.get("charset") : "utf-8";
+                connection.setRequestProperty("charset", charset);
+                Enumeration<String> keys = config.keys();
+                while (keys.hasMoreElements()) {
+                    String key = keys.nextElement();
+                    if (key.startsWith("header.")) {
+                        connection.setRequestProperty(key.substring("header.".length()), (String) config.get(key));
+                    }
+                }
+                String payloadHeader = config.get("payload.header") != null ? (String) config.get("payload.header") : null;
+                if (payloadHeader != null) {
+                    try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+                        marshaller.marshal(event, out);
+                        connection.setRequestProperty(payloadHeader, out.toString());
+                    }
+                } else {
+                    try (OutputStream out = connection.getOutputStream()) {
+                        marshaller.marshal(event, out);
+                    }
+                }
                 InputStream is = connection.getInputStream();
                 is.read();
                 is.close();
diff --git a/appender/rest/src/test/java/org/apache/karaf/decanter/appender/rest/RestAppenderTest.java b/appender/rest/src/test/java/org/apache/karaf/decanter/appender/rest/RestAppenderTest.java
index 904d174..a232ef8 100644
--- a/appender/rest/src/test/java/org/apache/karaf/decanter/appender/rest/RestAppenderTest.java
+++ b/appender/rest/src/test/java/org/apache/karaf/decanter/appender/rest/RestAppenderTest.java
@@ -26,72 +26,77 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 
+import org.apache.cxf.endpoint.Server;
+import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
 import org.apache.karaf.decanter.api.marshaller.Marshaller;
 import org.apache.karaf.decanter.marshaller.json.JsonMarshaller;
-import org.junit.Ignore;
-import org.junit.Test;
+import org.junit.*;
 import org.osgi.service.event.Event;
 
 public class RestAppenderTest {
     
     private static final int NUM_MESSAGES = 100000;
 
+    private Server cxfServer;
+    private TestService testService;
+
+    @Before
+    public void setup() throws Exception {
+        JAXRSServerFactoryBean jaxrsServerFactoryBean = new JAXRSServerFactoryBean();
+        testService = new TestService();
+        jaxrsServerFactoryBean.setAddress("http://localhost:9091/test");
+        jaxrsServerFactoryBean.setServiceBean(testService);
+        cxfServer = jaxrsServerFactoryBean.create();
+        cxfServer.start();
+    }
+
+    @After
+    public void teardown() throws Exception {
+        cxfServer.stop();
+    }
+
     @Test(expected = IllegalArgumentException.class)
     public void testEmptyURI() throws URISyntaxException {
         RestAppender appender = new RestAppender();
         Dictionary<String, Object> config = new Hashtable<>();
         appender.activate(config);
     }
-    
-    @Ignore
-    @Test
-    public void testSend() throws URISyntaxException, InterruptedException {
-        RestAppender appender = createAppender();
-        sendMessage(appender);
-    }
 
-    @Ignore
     @Test
-    public void testPerformance() throws URISyntaxException, InterruptedException {
-        RestAppender appender = createAppender();
-        sendMessages(appender);
-        long start = System.currentTimeMillis();
-        sendMessages(appender);
-        long end = System.currentTimeMillis();
-        System.out.println(NUM_MESSAGES * 1000 / (end-start));
-    }
-
-    private RestAppender createAppender() throws URISyntaxException {
+    public void testPost() throws URISyntaxException {
         RestAppender appender = new RestAppender();
-        Marshaller marshaller = new JsonMarshaller();
-        appender.marshaller = marshaller;
         Dictionary<String, Object> config = new Hashtable<>();
-        config.put("uri", "http://localhost:8181/decanter/collect");
+        config.put("uri", "http://localhost:9091/test/echo");
+        appender.marshaller = new JsonMarshaller();
         appender.activate(config);
-        return appender;
-    }
 
-    private void sendMessages(final RestAppender appender) throws InterruptedException {
-        ExecutorService executor = Executors.newFixedThreadPool(20);
-        for (int c=0; c<NUM_MESSAGES; c++) {
-            executor.submit(new Callable<Void>() {
-
-                @Override
-                public Void call() throws Exception {
-                    sendMessage(appender);
-                    return null;
-                }
-            });
-            
-        }
-        executor.shutdown();
-        executor.awaitTermination(100, TimeUnit.SECONDS);
+        HashMap<String, Object> data = new HashMap<>();
+        data.put("foo", "bar");
+        Event event = new Event("post", data);
+
+        appender.handleEvent(event);
+
+        Assert.assertEquals(1, testService.postMessages.size());
+        Assert.assertTrue(testService.postMessages.get(0).contains("\"foo\":\"bar\""));
     }
 
-    private void sendMessage(RestAppender appender) {
-        Map<String, Object> props = new HashMap<>();
-        props.put("key1", "value1");
-        Event event = new Event("decanter/collect", props);
+    @Test
+    public void testPut() throws URISyntaxException {
+        RestAppender appender = new RestAppender();
+        Dictionary<String, Object> config= new Hashtable<>();
+        config.put("uri", "http://localhost:9091/test/echo");
+        config.put("request.method", "PUT");
+        appender.marshaller = new JsonMarshaller();
+        appender.activate(config);
+
+        HashMap<String, Object> data = new HashMap<>();
+        data.put("foo", "bar");
+        Event event = new Event("put", data);
+
         appender.handleEvent(event);
+
+        Assert.assertEquals(1, testService.putMessages.size());
+        Assert.assertTrue(testService.putMessages.get(0).contains("\"foo\":\"bar\""));
     }
+
 }
diff --git a/collector/rest/src/test/java/org/apache/karaf/decanter/collector/rest/TestService.java b/appender/rest/src/test/java/org/apache/karaf/decanter/appender/rest/TestService.java
similarity index 62%
copy from collector/rest/src/test/java/org/apache/karaf/decanter/collector/rest/TestService.java
copy to appender/rest/src/test/java/org/apache/karaf/decanter/appender/rest/TestService.java
index 6e6cc3c..dfb84f3 100644
--- a/collector/rest/src/test/java/org/apache/karaf/decanter/collector/rest/TestService.java
+++ b/appender/rest/src/test/java/org/apache/karaf/decanter/appender/rest/TestService.java
@@ -14,25 +14,35 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.karaf.decanter.collector.rest;
+package org.apache.karaf.decanter.appender.rest;
 
-import javax.ws.rs.GET;
+import javax.ws.rs.Consumes;
 import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
+import java.util.ArrayList;
+import java.util.List;
 
 @Path("/")
 public class TestService {
 
-    @GET
+    public List<String> postMessages = new ArrayList<>();
+    public List<String> putMessages = new ArrayList<>();
+
+    @POST
+    @Consumes("application/json")
     @Path("/echo")
-    public String echo() {
-        return "hello world";
+    public String echoPost(String message) {
+        postMessages.add(message);
+        return message;
     }
 
-    @POST
-    @Path("/submit")
-    public void submit(String foo) {
-        // nothing to do
+    @PUT
+    @Consumes("application/json")
+    @Path("/echo")
+    public String echoPut(String message) {
+        putMessages.add(message);
+        return message;
     }
 
 }
diff --git a/collector/rest/src/main/cfg/org.apache.karaf.decanter.collector.rest.cfg b/collector/rest/src/main/cfg/org.apache.karaf.decanter.collector.rest.cfg
index e0c5dca..f0e4c0f 100644
--- a/collector/rest/src/main/cfg/org.apache.karaf.decanter.collector.rest.cfg
+++ b/collector/rest/src/main/cfg/org.apache.karaf.decanter.collector.rest.cfg
@@ -24,5 +24,12 @@
 url=http://localhost:8080
 paths=metrics
 
+#request.method=GET (possible values are GET, POST, PUT, DELETE, default is GET)
+#request=foo (request payload)
+#header.foo=bar (header passed, prefixed with header.)
+#user=user (used for basic authentication)
+#password=password (used for basic authentication)
+#topic=decanter/collector/rest (Decanter dispatcher topic name to use)
+
 # Unmarshaller to use
 unmarshaller.target=(dataFormat=json)
diff --git a/collector/rest/src/main/java/org/apache/karaf/decanter/collector/rest/RestCollector.java b/collector/rest/src/main/java/org/apache/karaf/decanter/collector/rest/RestCollector.java
index 4aa9064..7c06825 100644
--- a/collector/rest/src/main/java/org/apache/karaf/decanter/collector/rest/RestCollector.java
+++ b/collector/rest/src/main/java/org/apache/karaf/decanter/collector/rest/RestCollector.java
@@ -16,13 +16,13 @@
  */
 package org.apache.karaf.decanter.collector.rest;
 
+import java.io.BufferedWriter;
+import java.io.OutputStreamWriter;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.Dictionary;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.Map;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
 
 import org.apache.karaf.decanter.api.marshaller.Unmarshaller;
 import org.apache.karaf.decanter.collector.utils.PropertiesPreparator;
@@ -60,6 +60,10 @@ public class RestCollector implements Runnable {
     private URL url;
     private String[] paths;
     private String topic;
+    private String requestMethod;
+    private String request;
+    private String user;
+    private String password;
     private Dictionary<String, Object> config;
 
     @Activate
@@ -72,6 +76,10 @@ public class RestCollector implements Runnable {
         this.url = new URL(getProperty(config, "url", "http://localhost:8181"));
         this.paths = getProperty(config, "paths", "").split(",");
         this.topic = getProperty(config, "topic", "decanter/collect/rest");
+        this.requestMethod = getProperty(config, "request.method", "GET");
+        this.user = getProperty(config, "user", null);
+        this.password = getProperty(config, "password", null);
+        this.request = getProperty(config, "request", null);
     }
     
     private String getProperty(Dictionary<String, Object> properties, String key, String defaultValue) {
@@ -94,19 +102,33 @@ public class RestCollector implements Runnable {
             Map<String, Object> data = new HashMap<>();
             try {
                 connection = (HttpURLConnection) urlWithPath.openConnection();
+                if (user != null) {
+                    String authentication = user + ":" + password;
+                    byte[] encodedAuthentication = Base64.getEncoder().encode(authentication.getBytes(StandardCharsets.UTF_8));
+                    String authenticationHeader = "Basic " + new String(encodedAuthentication);
+                    connection.setRequestProperty("Authorization", authenticationHeader);
+                }
+                connection.setRequestMethod(requestMethod);
+                if ((requestMethod.equalsIgnoreCase("POST") || requestMethod.equalsIgnoreCase("PUT")) && request != null) {
+                    connection.setDoInput(true);
+                    connection.setDoOutput(true);
+                    try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()))) {
+                        writer.write(request);
+                    }
+                }
+                Enumeration<String> keys = config.keys();
+                while (keys.hasMoreElements()) {
+                    String key = keys.nextElement();
+                    if (key.startsWith("header.")) {
+                        connection.setRequestProperty(key.substring("header.".length()), (String) config.get(key));
+                    }
+                }
                 data.putAll(unmarshaller.unmarshal(connection.getInputStream()));
                 data.put("http.response.code", connection.getResponseCode());
                 data.put("http.response.message", connection.getResponseMessage());
                 data.put("type", "rest");
                 data.put("url", urlWithPath);
 
-                // custom fields
-                Enumeration<String> keys = config.keys();
-                while (keys.hasMoreElements()) {
-                    String key = keys.nextElement();
-                    data.put(key, config.get(key));
-                }
-
                 PropertiesPreparator.prepare(data, config);
 
                 data.put("service.hostName", url.getHost());
diff --git a/collector/rest/src/test/java/org/apache/karaf/decanter/collector/rest/RestCollectorTest.java b/collector/rest/src/test/java/org/apache/karaf/decanter/collector/rest/RestCollectorTest.java
index 69d5a3d..711ecc1 100644
--- a/collector/rest/src/test/java/org/apache/karaf/decanter/collector/rest/RestCollectorTest.java
+++ b/collector/rest/src/test/java/org/apache/karaf/decanter/collector/rest/RestCollectorTest.java
@@ -80,4 +80,79 @@ public class RestCollectorTest {
         Assert.assertEquals("hello world\n", event.getProperty("payload"));
     }
 
+    @Test
+    public void testPost() throws Exception {
+        EventAdminMock eventAdminMock = new EventAdminMock();
+        RestCollector collector = new RestCollector();
+        Dictionary<String, Object> config = new Hashtable<>();
+        config.put("request.method", "POST");
+        config.put("request", "test");
+        config.put("url", "http://localhost:9090/test/submit");
+        collector.unmarshaller = new RawUnmarshaller();
+        collector.dispatcher = eventAdminMock;
+        collector.activate(config);
+        collector.run();
+
+        Assert.assertEquals(1, eventAdminMock.postedEvents.size());
+        Event event = eventAdminMock.postedEvents.get(0);
+        Assert.assertEquals(200, event.getProperty("http.response.code"));
+        Assert.assertEquals("hello post test\n", event.getProperty("payload"));
+    }
+
+    @Test
+    public void testPut() throws Exception {
+        EventAdminMock eventAdminMock = new EventAdminMock();
+        RestCollector collector = new RestCollector();
+        Dictionary<String, Object> config = new Hashtable<>();
+        config.put("request.method", "PUT");
+        config.put("request", "test");
+        config.put("url", "http://localhost:9090/test/submit");
+        collector.unmarshaller = new RawUnmarshaller();
+        collector.dispatcher = eventAdminMock;
+        collector.activate(config);
+        collector.run();
+
+        Assert.assertEquals(1, eventAdminMock.postedEvents.size());
+        Event event = eventAdminMock.postedEvents.get(0);
+        Assert.assertEquals(200, event.getProperty("http.response.code"));
+        Assert.assertEquals("hello put test\n", event.getProperty("payload"));
+    }
+
+    @Test
+    public void testDelete() throws Exception {
+        EventAdminMock eventAdminMock = new EventAdminMock();
+        RestCollector collector = new RestCollector();
+        Dictionary<String, Object> config = new Hashtable<>();
+        config.put("request.method", "DELETE");
+        config.put("url", "http://localhost:9090/test/delete");
+        collector.unmarshaller = new RawUnmarshaller();
+        collector.dispatcher = eventAdminMock;
+        collector.activate(config);
+        collector.run();
+
+        Assert.assertEquals(1, eventAdminMock.postedEvents.size());
+        Event event = eventAdminMock.postedEvents.get(0);
+        Assert.assertEquals(200, event.getProperty("http.response.code"));
+        Assert.assertEquals("deleted\n", event.getProperty("payload"));
+    }
+
+    @Test
+    public void testHeader() throws Exception {
+        EventAdminMock eventAdminMock = new EventAdminMock();
+        RestCollector collector = new RestCollector();
+        Dictionary<String, Object> config = new Hashtable<>();
+        config.put("request.method", "POST");
+        config.put("header.foo", "test");
+        config.put("url", "http://localhost:9090/test/header");
+        collector.unmarshaller = new RawUnmarshaller();
+        collector.dispatcher = eventAdminMock;
+        collector.activate(config);
+        collector.run();
+
+        Assert.assertEquals(1, eventAdminMock.postedEvents.size());
+        Event event = eventAdminMock.postedEvents.get(0);
+        Assert.assertEquals(200, event.getProperty("http.response.code"));
+        Assert.assertEquals("hello header test\n", event.getProperty("payload"));
+    }
+
 }
diff --git a/collector/rest/src/test/java/org/apache/karaf/decanter/collector/rest/TestService.java b/collector/rest/src/test/java/org/apache/karaf/decanter/collector/rest/TestService.java
index 6e6cc3c..0068982 100644
--- a/collector/rest/src/test/java/org/apache/karaf/decanter/collector/rest/TestService.java
+++ b/collector/rest/src/test/java/org/apache/karaf/decanter/collector/rest/TestService.java
@@ -16,9 +16,7 @@
  */
 package org.apache.karaf.decanter.collector.rest;
 
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
+import javax.ws.rs.*;
 
 @Path("/")
 public class TestService {
@@ -31,8 +29,26 @@ public class TestService {
 
     @POST
     @Path("/submit")
-    public void submit(String foo) {
-        // nothing to do
+    public String submitPost(String foo) {
+        return "hello post " + foo;
+    }
+
+    @PUT
+    @Path("/submit")
+    public String submitPut(String foo) {
+        return "hello put " + foo;
+    }
+
+    @DELETE
+    @Path("/delete")
+    public String delete() {
+        return "deleted";
+    }
+
+    @POST
+    @Path("/header")
+    public String headerPost(@HeaderParam("foo") String foo) {
+        return "hello header " + foo;
     }
 
 }
diff --git a/manual/src/main/asciidoc/user-guide/appenders.adoc b/manual/src/main/asciidoc/user-guide/appenders.adoc
index 639ee30..6fc28aa 100644
--- a/manual/src/main/asciidoc/user-guide/appenders.adoc
+++ b/manual/src/main/asciidoc/user-guide/appenders.adoc
@@ -682,8 +682,6 @@ You can install a test database with Docker for dev:
 docker run -d --name timescaledb -p 5432:5432 -e POSTGRES_PASSWORD=decanter -e POSTGRES_USER=decanter -e POSTGRES_DATABASE=decanter timescale/timescaledb
 ```
 
-===== TimescaleDB appender
-
 The `decanter-appender-timescaledb` feature installs the TimescaleDB appender.
 
 As TimescaleDB is a PostgreSQL database extension, the *timescaledb* feature will install all required features to configure
@@ -718,7 +716,7 @@ The table is simple and contains just two column:
 ** `content` as TEXT
 * `marshaller.target` is the marshaller used to serialize data into the table.
 
-===== WebSocket Servlet
+==== WebSocket Servlet
 
 The `decanter-appender-websocket-servlet` feature exposes a websocket on which clients can register. Then, Decanter will send the collected data to the connected clients.
 
@@ -753,7 +751,7 @@ curl --include \
      http://localhost:8181/decanter-websocket
 ```
 
-===== Prometheus
+==== Prometheus
 
 The `decanter-appender-prometheus` feature collects and exposes metrics on prometheus:
 
@@ -823,3 +821,42 @@ Import-Package: io.prometheus.client;version="[0.8,1)"
 ```
 
 That's the only thing you need: your metrics will be available on the Decanter Prometheus servlet (again on `http://localhost:8181/decanter/prometheus` by default).
+
+==== Rest
+
+Decanter Rest appender send collected data to a remote REST service.
+
+The `decanter-appender-rest` feature installs the Rest appender:
+
+----
+karaf@root()> feature:install decanter-appender-rest
+----
+
+The feature also installs `etc/org.apache.karaf.decanter.appender.rest.cfg` configuration file:
+
+----
+###############################
+# Decanter Appender REST Configuration
+###############################
+
+# Mandatory URI where the REST appender connects to
+uri=
+
+#request.method=POST (the REST verb)
+#user= (for basic authentication)
+#password= (for basic authentication)
+#content.type=application/json (the message content type sent)
+#charset=utf-8 (the message charset)
+#header.foo= (HTTP header prefixed with header.)
+#payload.header= (if set the Decanter collected data is sent as HTTP header instead of body)
+
+# Marshaller to use (json is recommended)
+marshaller.target=(dataFormat=json)
+----
+
+* `uri` is mandatory and contains the location of the Rest service to call
+* `user` and `password` are used if the Rest service uses basic authentication
+* `content.type` is the message type sent to the Rest service (default is `application/json`)
+* `charset` is the message encoding (default is `utf-8`)
+* `header.` allows you to add any custom HTTP headers (parameters) to the request (prefixed by `header.`)
+* `payload.header` allows you to use a HTTP header to send the collected data instead of directly the "body".
\ No newline at end of file
diff --git a/manual/src/main/asciidoc/user-guide/collectors.adoc b/manual/src/main/asciidoc/user-guide/collectors.adoc
index f37dde3..ddc92c3 100644
--- a/manual/src/main/asciidoc/user-guide/collectors.adoc
+++ b/manual/src/main/asciidoc/user-guide/collectors.adoc
@@ -844,6 +844,37 @@ The `decanter-collector-rest-servlet` feature installs the collector:
 karaf@root()> feature:install decanter-collector-rest-servlet
 ----
 
+==== REST
+
+The Decanter REST collector periodically requests a REST service and returns the result (with all HTTP details).
+
+The `decanter-collector-rest` feature installs the collector:
+
+----
+karaf@root()> feature:install decanter-collector-rest
+----
+
+This feature also installs `etc/org.apache.karaf.decanter.collector.rest.cfg` configuration file where you can setup the REST service request:
+
+----
+#
+# Decanter REST collector
+#
+
+url=http://localhost:8080
+paths=metrics
+
+#request.method=GET (possible values are GET, POST, PUT, DELETE, default is GET)
+#request=foo (request payload)
+#header.foo=bar (header passed, prefixed with header.)
+#user=user (used for basic authentication)
+#password=password (used for basic authentication)
+#topic=decanter/collector/rest (Decanter dispatcher topic name to use)
+
+# Unmarshaller to use
+unmarshaller.target=(dataFormat=json)
+----
+
 ==== SOAP
 
 The Decanter SOAP collector periodically requests a SOAP service and returns the result (the SOAP Response, or error details if it failed).