[KARAF-2487] Add centralized log feature
diff --git a/assembly/src/main/resources/features.xml b/assembly/src/main/resources/features.xml
index f4889d5..24634e1 100644
--- a/assembly/src/main/resources/features.xml
+++ b/assembly/src/main/resources/features.xml
@@ -127,5 +127,11 @@
         <bundle>mvn:org.apache.karaf.cellar.http/org.apache.karaf.cellar.http.balancer/${project.version}</bundle>
     </feature>
 
+    <feature name="cellar-log" description="Cellar central log support" version="${project.version}">
+        <feature>log</feature>
+        <feature>cellar-hazelcast</feature>
+        <bundle>mvn:org.apache.karaf.cellar/org.apache.karaf.cellar.log/${project.version}</bundle>
+    </feature>
+
 </features>
 
diff --git a/assembly/src/main/resources/hazelcast.xml b/assembly/src/main/resources/hazelcast.xml
index 43634e1..eb23a04 100644
--- a/assembly/src/main/resources/hazelcast.xml
+++ b/assembly/src/main/resources/hazelcast.xml
@@ -160,6 +160,14 @@
         -->
         <hz:merge-policy>com.hazelcast.map.merge.PassThroughMergePolicy</hz:merge-policy>
     </hz:map>
+    <hz:map name="org.apache.karaf.cellar.log">
+        <hz:time-to-live-seconds>0</hz:time-to-live-seconds>
+        <hz:max-idle-seconds>0</hz:max-idle-seconds>
+        <hz:eviction-policy>LRU</hz:eviction-policy>
+        <hz:max-size policy="PER_PARTITION">5000</hz:max-size>
+        <hz:eviction-percentage>25</hz:eviction-percentage>
+        <hz:backup-count>0</hz:backup-count>
+    </hz:map>
 
     <hz:multimap name="default">
         <hz:backup-count>1</hz:backup-count>
diff --git a/log/NOTICE.txt b/log/NOTICE.txt
new file mode 100644
index 0000000..64cb235
--- /dev/null
+++ b/log/NOTICE.txt
@@ -0,0 +1,39 @@
+Apache Karaf Cellar

+Copyright 2011-2015 The Apache Software Foundation

+

+I. Used Software

+

+This product includes software developed at

+The Apache Software Foundation (http://www.apache.org/).

+Licensed under the Apache License 2.0.

+

+This product uses software developed at

+The OSGi Alliance (http://www.osgi.org/).

+Licensed under the Apache License 2.0.

+

+This product uses software developed at

+Hazelcast (http://www.hazelcast.com/).

+Licensed under the Apache License 2.0.

+

+This product uses software developed at

+OPS4J (http://www.ops4j.org/).

+Licensed under the Apache License 2.0.

+

+This product uses software developed at

+FUSE Source (http://www.fusesource.org/).

+Licensed under the Apache License 2.0.

+

+This product uses software developed at

+JClouds (http://www.jclouds.org/).

+Licensed under the Apache License 2.0.

+

+This product uses software developed at

+SLF4J (http://www.slf4j.org/).

+Licensed under the MIT License.

+

+This product includes software from http://www.json.org.

+Copyright (c) 2002 JSON.org

+

+II. License Summary

+- Apache License 2.0

+- MIT License

diff --git a/log/pom.xml b/log/pom.xml
new file mode 100644
index 0000000..1675f79
--- /dev/null
+++ b/log/pom.xml
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+    <!--
+
+        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.
+    -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.karaf</groupId>
+        <artifactId>cellar</artifactId>
+        <version>4.0.1-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <groupId>org.apache.karaf.cellar</groupId>
+    <artifactId>org.apache.karaf.cellar.log</artifactId>
+    <packaging>bundle</packaging>
+    <name>Apache Karaf :: Cellar :: Log</name>
+
+    <dependencies>
+        <!-- Core dependencies -->
+        <dependency>
+            <groupId>org.apache.karaf.cellar</groupId>
+            <artifactId>org.apache.karaf.cellar.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.logging</groupId>
+            <artifactId>pax-logging-service</artifactId>
+            <version>1.7.3</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.karaf.log</groupId>
+            <artifactId>org.apache.karaf.log.core</artifactId>
+            <version>${karaf.version}</version>
+        </dependency>
+
+        <!-- Logging dependencies -->
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.karaf.tooling</groupId>
+                <artifactId>karaf-services-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <configuration>
+                    <instructions>
+                        <Export-Package>
+                            !org.apache.karaf.cellar.log.internal.osgi,
+                            !org.apache.karaf.cellar.log.management.internal,
+                            org.apache.karaf.cellar.log*
+                        </Export-Package>
+                        <Import-Package>
+                            org.slf4j;version="[1.6,2)";resolution:=optional,
+                            *
+                        </Import-Package>
+                        <Private-Package>
+                            org.apache.karaf.cellar.log.internal.osgi,
+                            org.apache.karaf.cellar.log.management.internal,
+                            org.apache.karaf.util.tracker;-split-package:=merge-first
+                        </Private-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/ClusterLogKey.java b/log/src/main/java/org/apache/karaf/cellar/log/ClusterLogKey.java
new file mode 100644
index 0000000..095655e
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/ClusterLogKey.java
@@ -0,0 +1,51 @@
+/*
+ * 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.karaf.cellar.log;
+
+import java.io.Serializable;
+
+public class ClusterLogKey implements Serializable {
+
+    private String nodeId;
+    private long timeStamp;
+    private String id;
+
+    public String getNodeId() {
+        return nodeId;
+    }
+
+    public void setNodeId(String nodeId) {
+        this.nodeId = nodeId;
+    }
+
+    public long getTimeStamp() {
+        return timeStamp;
+    }
+
+    public void setTimeStamp(long timeStamp) {
+        this.timeStamp = timeStamp;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/ClusterLogRecord.java b/log/src/main/java/org/apache/karaf/cellar/log/ClusterLogRecord.java
new file mode 100644
index 0000000..9cca581
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/ClusterLogRecord.java
@@ -0,0 +1,87 @@
+/*
+ * 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.karaf.cellar.log;
+
+import java.io.Serializable;
+
+public class ClusterLogRecord implements Serializable {
+
+    private String level;
+    private String loggerName;
+    private String message;
+    private String renderedMessage;
+    private String threadName;
+    private String FQNOfLoggerClass;
+    private String[] throwableStringRep;
+
+    public String getLevel() {
+        return level;
+    }
+
+    public void setLevel(String level) {
+        this.level = level;
+    }
+
+    public String getLoggerName() {
+        return loggerName;
+    }
+
+    public void setLoggerName(String loggerName) {
+        this.loggerName = loggerName;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public String getRenderedMessage() {
+        return renderedMessage;
+    }
+
+    public void setRenderedMessage(String renderedMessage) {
+        this.renderedMessage = renderedMessage;
+    }
+
+    public String getThreadName() {
+        return threadName;
+    }
+
+    public void setThreadName(String threadName) {
+        this.threadName = threadName;
+    }
+
+    public String getFQNOfLoggerClass() {
+        return FQNOfLoggerClass;
+    }
+
+    public void setFQNOfLoggerClass(String FQNOfLoggerClass) {
+        this.FQNOfLoggerClass = FQNOfLoggerClass;
+    }
+
+    public String[] getThrowableStringRep() {
+        return throwableStringRep;
+    }
+
+    public void setThrowableStringRep(String[] throwableStringRep) {
+        this.throwableStringRep = throwableStringRep;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/GetLogCommand.java b/log/src/main/java/org/apache/karaf/cellar/log/GetLogCommand.java
new file mode 100644
index 0000000..5aea23a
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/GetLogCommand.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed 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.karaf.cellar.log;
+
+import org.apache.karaf.cellar.core.command.Command;
+
+public class GetLogCommand extends Command<GetLogResult> {
+
+    protected String logger;
+
+    public GetLogCommand(String id) {
+        super(id);
+    }
+
+    public GetLogCommand(String id, String logger) {
+        super(id);
+        this.logger = logger;
+    }
+
+    public String getLogger() {
+        return logger;
+    }
+
+    public void setLogger(String logger) {
+        this.logger = logger;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/GetLogCommandHandler.java b/log/src/main/java/org/apache/karaf/cellar/log/GetLogCommandHandler.java
new file mode 100644
index 0000000..98dfa57
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/GetLogCommandHandler.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed 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.karaf.cellar.log;
+
+import org.apache.karaf.cellar.core.command.CommandHandler;
+import org.apache.karaf.cellar.core.control.BasicSwitch;
+import org.apache.karaf.cellar.core.control.Switch;
+import org.apache.karaf.log.core.LogService;
+
+import java.util.Map;
+
+public class GetLogCommandHandler extends CommandHandler<GetLogCommand, GetLogResult> {
+
+    public static final String SWITCH_ID = "org.apache.karaf.cellar.log.get.switch";
+    private final Switch commandSwitch = new BasicSwitch(SWITCH_ID);
+
+    private LogService logService;
+
+    @Override
+    public GetLogResult execute(GetLogCommand command) {
+        Map<String, String> levels = logService.getLevel(command.getLogger());
+        GetLogResult result = new GetLogResult(command.getId(), levels);
+        return result;
+    }
+
+    @Override
+    public Class<GetLogCommand> getType() {
+        return GetLogCommand.class;
+    }
+
+    @Override
+    public Switch getSwitch() {
+        return commandSwitch;
+    }
+
+    public LogService getLogService() {
+        return logService;
+    }
+
+    public void setLogService(LogService logService) {
+        this.logService = logService;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/GetLogResult.java b/log/src/main/java/org/apache/karaf/cellar/log/GetLogResult.java
new file mode 100644
index 0000000..96e570e
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/GetLogResult.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed 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.karaf.cellar.log;
+
+import org.apache.karaf.cellar.core.command.Result;
+
+import java.util.Map;
+
+public class GetLogResult extends Result {
+
+    protected Map<String, String> loggers;
+
+    public GetLogResult(String id) {
+        super(id);
+    }
+
+    public GetLogResult(String id, Map<String, String> loggers) {
+        super(id);
+        this.loggers = loggers;
+    }
+
+    public Map<String, String> getLoggers() {
+        return loggers;
+    }
+
+    public void setLoggers(Map<String, String> loggers) {
+        this.loggers = loggers;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/GetLogResultHandler.java b/log/src/main/java/org/apache/karaf/cellar/log/GetLogResultHandler.java
new file mode 100644
index 0000000..9c73f96
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/GetLogResultHandler.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed 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.karaf.cellar.log;
+
+import org.apache.karaf.cellar.core.command.ResultHandler;
+
+public class GetLogResultHandler extends ResultHandler<GetLogResult> {
+
+    @Override
+    public Class<GetLogResult> getType() {
+        return GetLogResult.class;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/LogAppender.java b/log/src/main/java/org/apache/karaf/cellar/log/LogAppender.java
new file mode 100644
index 0000000..8c59769
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/LogAppender.java
@@ -0,0 +1,66 @@
+/*
+ * 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.karaf.cellar.log;
+
+import org.apache.karaf.cellar.core.ClusterManager;
+import org.apache.karaf.cellar.core.Node;
+import org.ops4j.pax.logging.spi.PaxAppender;
+import org.ops4j.pax.logging.spi.PaxLoggingEvent;
+
+import java.util.Map;
+
+public class LogAppender implements PaxAppender {
+
+    public static final String LOG_MAP = "org.apache.karaf.cellar.log";
+
+    private ClusterManager clusterManager;
+
+    @Override
+    public void doAppend(PaxLoggingEvent event) {
+
+        Map<ClusterLogKey, ClusterLogRecord> clusterLog = clusterManager.getMap(LOG_MAP);
+
+        ClusterLogRecord record = new ClusterLogRecord();
+        ClusterLogKey key = new ClusterLogKey();
+
+        Node node = clusterManager.getNode();
+        String nodeId = node.getId();
+
+        key.setNodeId(nodeId);
+        key.setTimeStamp(event.getTimeStamp());
+        key.setId(clusterManager.generateId());
+
+        record.setFQNOfLoggerClass(event.getFQNOfLoggerClass());
+        record.setLevel(event.getLevel().toString());
+        record.setLoggerName(event.getLoggerName());
+        record.setMessage(event.getMessage());
+        record.setRenderedMessage(event.getRenderedMessage());
+        record.setThreadName(event.getThreadName());
+        record.setThrowableStringRep(event.getThrowableStrRep());
+
+        clusterLog.put(key, record);
+    }
+
+    public ClusterManager getClusterManager() {
+        return clusterManager;
+    }
+
+    public void setClusterManager(ClusterManager clusterManager) {
+        this.clusterManager = clusterManager;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/SetLogCommand.java b/log/src/main/java/org/apache/karaf/cellar/log/SetLogCommand.java
new file mode 100644
index 0000000..6b171b2
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/SetLogCommand.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed 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.karaf.cellar.log;
+
+import org.apache.karaf.cellar.core.command.Command;
+
+public class SetLogCommand extends Command<SetLogResult> {
+
+    protected String logger;
+    protected String level;
+
+    public SetLogCommand(String id) {
+        super(id);
+    }
+
+    public SetLogCommand(String id, String logger, String level) {
+        super(id);
+        this.logger = logger;
+        this.level = level;
+    }
+
+    public String getLogger() {
+        return logger;
+    }
+
+    public void setLogger(String logger) {
+        this.logger = logger;
+    }
+
+    public String getLevel() {
+        return level;
+    }
+
+    public void setLevel(String level) {
+        this.level = level;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/SetLogCommandHandler.java b/log/src/main/java/org/apache/karaf/cellar/log/SetLogCommandHandler.java
new file mode 100644
index 0000000..6a97ceb
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/SetLogCommandHandler.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed 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.karaf.cellar.log;
+
+import org.apache.karaf.cellar.core.command.CommandHandler;
+import org.apache.karaf.cellar.core.control.BasicSwitch;
+import org.apache.karaf.cellar.core.control.Switch;
+import org.apache.karaf.log.core.LogService;
+
+import java.util.Map;
+
+public class SetLogCommandHandler extends CommandHandler<SetLogCommand, SetLogResult> {
+
+    public static final String SWITCH_ID = "org.apache.karaf.cellar.log.set.switch";
+    private final Switch commandSwitch = new BasicSwitch(SWITCH_ID);
+
+    private LogService logService;
+
+    @Override
+    public SetLogResult execute(SetLogCommand command) {
+        logService.setLevel(command.getLogger(), command.getLevel());
+        SetLogResult result = new SetLogResult(command.getId());
+        return result;
+    }
+
+    @Override
+    public Class<SetLogCommand> getType() {
+        return SetLogCommand.class;
+    }
+
+    @Override
+    public Switch getSwitch() {
+        return commandSwitch;
+    }
+
+    public LogService getLogService() {
+        return logService;
+    }
+
+    public void setLogService(LogService logService) {
+        this.logService = logService;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/SetLogResult.java b/log/src/main/java/org/apache/karaf/cellar/log/SetLogResult.java
new file mode 100644
index 0000000..5b49cae
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/SetLogResult.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed 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.karaf.cellar.log;
+
+import org.apache.karaf.cellar.core.command.Result;
+
+public class SetLogResult extends Result {
+
+    public SetLogResult(String id) {
+        super(id);
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/SetLogResultHandler.java b/log/src/main/java/org/apache/karaf/cellar/log/SetLogResultHandler.java
new file mode 100644
index 0000000..ebef243
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/SetLogResultHandler.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed 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.karaf.cellar.log;
+
+import org.apache.karaf.cellar.core.command.ResultHandler;
+
+public class SetLogResultHandler extends ResultHandler<SetLogResult> {
+
+    @Override
+    public Class<SetLogResult> getType() {
+        return SetLogResult.class;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/internal/osgi/Activator.java b/log/src/main/java/org/apache/karaf/cellar/log/internal/osgi/Activator.java
new file mode 100644
index 0000000..95a0d6f
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/internal/osgi/Activator.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed 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.karaf.cellar.log.internal.osgi;
+
+import org.apache.karaf.cellar.core.ClusterManager;
+import org.apache.karaf.cellar.core.Producer;
+import org.apache.karaf.cellar.core.command.CommandStore;
+import org.apache.karaf.cellar.core.command.ExecutionContext;
+import org.apache.karaf.cellar.core.event.EventHandler;
+import org.apache.karaf.cellar.core.event.EventProducer;
+import org.apache.karaf.cellar.log.*;
+import org.apache.karaf.cellar.log.management.internal.CellarLogMBeanImpl;
+import org.apache.karaf.log.core.LogService;
+import org.apache.karaf.util.tracker.BaseActivator;
+import org.apache.karaf.util.tracker.annotation.ProvideService;
+import org.apache.karaf.util.tracker.annotation.RequireService;
+import org.apache.karaf.util.tracker.annotation.Services;
+import org.ops4j.pax.logging.spi.PaxAppender;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Hashtable;
+
+@Services(
+        provides = {
+                @ProvideService(PaxAppender.class),
+                @ProvideService(EventHandler.class)
+        },
+        requires = {
+                @RequireService(ClusterManager.class),
+                @RequireService(EventProducer.class),
+                @RequireService(CommandStore.class),
+                @RequireService(ExecutionContext.class),
+                @RequireService(ConfigurationAdmin.class),
+                @RequireService(LogService.class)
+        }
+)
+public class Activator extends BaseActivator {
+
+    private final static Logger LOGGER = LoggerFactory.getLogger(Activator.class);
+
+    private ServiceRegistration mbeanRegistration;
+
+    @Override
+    public void doStart() throws Exception {
+        LOGGER.debug("CELLAR LOG: retrieve cluster manager service");
+        ClusterManager clusterManager = getTrackedService(ClusterManager.class);
+        if (clusterManager == null) {
+            return;
+        }
+
+        LOGGER.debug("CELLAR LOG: retrieve producer service");
+        EventProducer producer = getTrackedService(EventProducer.class);
+        if (producer == null) {
+            return;
+        }
+
+        LOGGER.debug("CELLAR LOG: retrieve command store service");
+        CommandStore commandStore = getTrackedService(CommandStore.class);
+        if (commandStore == null) {
+            return;
+        }
+
+        LOGGER.debug("CELLAR LOG: retrieve execution context service");
+        ExecutionContext executionContext = getTrackedService(ExecutionContext.class);
+        if (executionContext == null) {
+            return;
+        }
+
+        LOGGER.debug("CELLAR LOG: retrieve configuration admin service");
+        ConfigurationAdmin configurationAdmin = getTrackedService(ConfigurationAdmin.class);
+        if (configurationAdmin == null) {
+            return;
+        }
+
+        LOGGER.debug("CELLAR LOG: retrieve log service");
+        LogService logService = getTrackedService(LogService.class);
+        if (logService == null) {
+            return;
+        }
+
+        LOGGER.debug("CELLAR LOG: init PaxAppender");
+        LogAppender paxAppender = new LogAppender();
+        paxAppender.setClusterManager(clusterManager);
+        Hashtable props = new Hashtable();
+        props.put("org.ops4j.pax.logging.appender.name", "CellarLogAppender");
+        register(PaxAppender.class, paxAppender, props);
+
+        LOGGER.debug("CELLAR LOG: register get log command handler");
+        GetLogCommandHandler getLogCommandHandler = new GetLogCommandHandler();
+        getLogCommandHandler.setProducer(producer);
+        getLogCommandHandler.setConfigurationAdmin(configurationAdmin);
+        getLogCommandHandler.setLogService(logService);
+        register(EventHandler.class, getLogCommandHandler);
+
+        LOGGER.debug("CELLAR LOG: register get log result handler");
+        GetLogResultHandler getLogResultHandler = new GetLogResultHandler();
+        getLogResultHandler.setCommandStore(commandStore);
+        register(EventHandler.class, getLogResultHandler);
+
+        LOGGER.debug("CELLAR LOG: register set log command handler");
+        SetLogCommandHandler setLogCommandHandler = new SetLogCommandHandler();
+        setLogCommandHandler.setProducer(producer);
+        setLogCommandHandler.setConfigurationAdmin(configurationAdmin);
+        setLogCommandHandler.setLogService(logService);
+        register(EventHandler.class, setLogCommandHandler);
+
+        LOGGER.debug("CELLAR LOG: register set log result handler");
+        SetLogResultHandler setLogResultHandler = new SetLogResultHandler();
+        setLogResultHandler.setCommandStore(commandStore);
+        register(EventHandler.class, setLogResultHandler);
+
+        LOGGER.debug("CELLAR LOG: registre MBean");
+        CellarLogMBeanImpl mbean = new CellarLogMBeanImpl();
+        mbean.setClusterManager(clusterManager);
+        mbean.setExecutionContext(executionContext);
+        props = new Hashtable();
+        props.put("jmx.objectname", "org.apache.karaf.cellar:type=log,name=" + System.getProperty("karaf.name"));
+        mbeanRegistration = bundleContext.registerService(getInterfaceNames(mbean), mbean, props);
+    }
+
+    @Override
+    public void doStop() {
+        super.doStop();
+
+        if (mbeanRegistration != null) {
+            mbeanRegistration.unregister();
+            mbeanRegistration = null;
+        }
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/management/CellarLogMBean.java b/log/src/main/java/org/apache/karaf/cellar/log/management/CellarLogMBean.java
new file mode 100644
index 0000000..8d6fcf2
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/management/CellarLogMBean.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed 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.karaf.cellar.log.management;
+
+import javax.management.openmbean.TabularData;
+import java.util.List;
+
+/**
+ * Describe the operations and attributes on the CellarLogMBean.
+ */
+public interface CellarLogMBean {
+
+    /**
+     * Display the cluster log messages.
+     *
+     * @param logger The logger name to filter or null.
+     * @param nodeId The node ID or alias to filter or null for all nodes.
+     * @param entries The number of entries to display limit (0 means no limit).
+     * @return The list of log messages.
+     */
+    List<String> displayLog(String logger, String nodeId, int entries);
+
+    /**
+     * Add a log message on the cluster.
+     *
+     * @param message The message to log.
+     * @param level The log message level (default is INFO when null).
+     * @param nodeId The node ID or alias or null for all nodes.
+     */
+    void logMessage(String message, String level, String nodeId);
+
+    /**
+     * Set the log level on the cluster.
+     *
+     * @param logger The logger to set the level or null for the root logger.
+     * @param level The level to set.
+     * @param nodeId The node ID or alias or null for all nodes.
+     */
+    void setLevel(String level, String logger, String nodeId) throws Exception;
+
+    /**
+     * Get the log level on the cluster.
+     *
+     * @param logger The logger to filter or null for all loggers.
+     * @param nodeId The node ID or alias or null for all nodes.
+     * @return A tabular data reprepsenting the levels.
+     */
+    TabularData getLevel(String logger, String nodeId) throws Exception;
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/management/internal/CellarLogMBeanImpl.java b/log/src/main/java/org/apache/karaf/cellar/log/management/internal/CellarLogMBeanImpl.java
new file mode 100644
index 0000000..c5c2e2a
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/management/internal/CellarLogMBeanImpl.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed 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.karaf.cellar.log.management.internal;
+
+import org.apache.karaf.cellar.core.ClusterManager;
+import org.apache.karaf.cellar.core.Node;
+import org.apache.karaf.cellar.core.command.ExecutionContext;
+import org.apache.karaf.cellar.log.*;
+import org.apache.karaf.cellar.log.management.CellarLogMBean;
+
+import javax.management.NotCompliantMBeanException;
+import javax.management.StandardMBean;
+import javax.management.openmbean.*;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+public class CellarLogMBeanImpl extends StandardMBean implements CellarLogMBean {
+
+    private ClusterManager clusterManager;
+    private ExecutionContext executionContext;
+
+    public CellarLogMBeanImpl() throws NotCompliantMBeanException {
+        super(CellarLogMBean.class);
+    }
+
+    public ClusterManager getClusterManager() {
+        return clusterManager;
+    }
+
+    public void setClusterManager(ClusterManager clusterManager) {
+        this.clusterManager = clusterManager;
+    }
+
+    public ExecutionContext getExecutionContext() {
+        return executionContext;
+    }
+
+    public void setExecutionContext(ExecutionContext executionContext) {
+        this.executionContext = executionContext;
+    }
+
+    @Override
+    public List<String> displayLog(String logger, String nodeId, int entries) {
+        List<String> result = new ArrayList<String>();
+
+        String node = null;
+        if (nodeId != null && clusterManager.findNodeByIdOrAlias(nodeId) == null) {
+            throw new IllegalArgumentException("Node " + nodeId + " doesn't exist");
+        }
+        if (nodeId != null && clusterManager.findNodeByIdOrAlias(nodeId) != null) {
+            node = clusterManager.findNodeByIdOrAlias(nodeId).getId();
+        }
+        ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
+        Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
+        try {
+            Map<ClusterLogKey, ClusterLogRecord> clusterLog = clusterManager.getMap(LogAppender.LOG_MAP);
+            int index = 0;
+            for (ClusterLogKey key : clusterLog.keySet()) {
+                if (entries == 0 || (entries != 0 && index < entries)) {
+                    ClusterLogRecord record = clusterLog.get(key);
+                    if (node == null || (node != null && key.getNodeId().equals(node))) {
+                        if (logger == null || (logger != null && logger.equals("ALL")) || (logger != null && record.getLoggerName() != null && record.getLoggerName().contains(logger))) {
+                            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
+                            String message = key.getNodeId() + " | "
+                                    + dateFormat.format(new Date(key.getTimeStamp())) + " | "
+                                    + record.getLevel() + " | "
+                                    + record.getThreadName() + " | "
+                                    + record.getLoggerName() + " | "
+                                    + record.getMessage();
+                            result.add(message);
+                            index++;
+                        }
+                    }
+                }
+            }
+        } finally {
+            Thread.currentThread().setContextClassLoader(originalClassLoader);
+        }
+
+        return result;
+    }
+
+    @Override
+    public void logMessage(String message, String nodeId, String level) {
+        if (message == null) {
+            throw new IllegalArgumentException("Message is required");
+        }
+        if (level != null && !level.equalsIgnoreCase("INFO") && !level.equalsIgnoreCase("DEBUG")
+                && !level.equalsIgnoreCase("WARN") && !level.equalsIgnoreCase("ERROR")) {
+            throw new IllegalArgumentException("Incorrect level value");
+        }
+        if (level == null) {
+            level = "INFO";
+        }
+        if (nodeId != null && clusterManager.findNodeByIdOrAlias(nodeId) == null) {
+            throw new IllegalArgumentException("Node " + nodeId + " doesn't exist");
+        }
+        long timestamp = System.currentTimeMillis();
+        String id = clusterManager.generateId();
+
+        ClusterLogKey key = new ClusterLogKey();
+        key.setNodeId(nodeId);
+        key.setTimeStamp(timestamp);
+        key.setId(id);
+
+        ClusterLogRecord record = new ClusterLogRecord();
+        record.setMessage(message);
+        record.setLevel(level);
+
+        ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
+        Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
+        try {
+            Map<ClusterLogKey, ClusterLogRecord> clusterLog = clusterManager.getMap(LogAppender.LOG_MAP);
+            clusterLog.put(key, record);
+        } finally {
+            Thread.currentThread().setContextClassLoader(originalClassLoader);
+        }
+    }
+
+    @Override
+    public void setLevel(String level, String logger, String nodeId) throws Exception {
+        SetLogCommand command = new SetLogCommand(clusterManager.generateId());
+        command.setTimeout(30 * 1000);
+
+        Set<Node> recipientList = new HashSet<Node>();
+        if (nodeId != null && clusterManager.findNodeByIdOrAlias(nodeId) == null) {
+            throw new IllegalArgumentException("Node " + nodeId + " doesn't exist");
+        }
+        if (nodeId == null) {
+            recipientList = clusterManager.listNodes();
+        } else {
+            recipientList.add(clusterManager.findNodeByIdOrAlias(nodeId));
+        }
+
+        if (recipientList.size() < 1)
+            throw new IllegalArgumentException("No recipient list");
+
+        command.setDestination(recipientList);
+        command.setLogger(logger);
+        command.setLevel(level);
+
+        Map<Node, SetLogResult> results = executionContext.execute(command);
+        if (results == null || results.isEmpty()) {
+            throw new IllegalStateException("No result received within given timeout");
+        }
+    }
+
+    @Override
+    public TabularData getLevel(String logger, String nodeId) throws Exception {
+        if (nodeId != null && clusterManager.findNodeByIdOrAlias(nodeId) == null) {
+            throw new IllegalArgumentException("Node " + nodeId + " doesn't exist");
+        }
+
+        Set<Node> recipientList = new HashSet<Node>();
+        if (nodeId == null) {
+            recipientList = clusterManager.listNodes();
+        } else {
+            recipientList.add(clusterManager.findNodeByIdOrAlias(nodeId));
+        }
+
+        CompositeType levelType = new CompositeType("Level", "Log Levels",
+                new String[]{ "node", "logger", "level" },
+                new String[]{ "Node ID", "Logger name", "Log level"},
+                new OpenType[]{ SimpleType.STRING, SimpleType.STRING, SimpleType.STRING });
+
+        TabularType tabularType = new TabularType("Levels", "Table of all log levels",
+                levelType, new String[]{ "node", "logger" });
+        TabularData tabularData = new TabularDataSupport(tabularType);
+
+        if (recipientList.size() < 1)
+            return null;
+
+        GetLogCommand command = new GetLogCommand(clusterManager.generateId());
+        command.setTimeout(30 * 1000);
+        command.setDestination(recipientList);
+        command.setLogger(logger);
+
+        Map<Node, GetLogResult> results = executionContext.execute(command);
+        if (results == null || results.isEmpty()) {
+            throw new IllegalStateException("No result received within given timeout");
+        } else {
+            for (Node node : results.keySet()) {
+                GetLogResult result = results.get(node);
+                Map<String, String> loggers = result.getLoggers();
+                for (String l : loggers.keySet()) {
+                    CompositeData data = new CompositeDataSupport(levelType,
+                            new String[]{ "node", "logger", "level" },
+                            new Object[]{ node.getId(), l, loggers.get(l) });
+                    tabularData.put(data);
+                }
+            }
+        }
+
+        return tabularData;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/shell/GetLog.java b/log/src/main/java/org/apache/karaf/cellar/log/shell/GetLog.java
new file mode 100644
index 0000000..3b9129a
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/shell/GetLog.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed 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.karaf.cellar.log.shell;
+
+import org.apache.karaf.cellar.core.Node;
+import org.apache.karaf.cellar.core.command.ExecutionContext;
+import org.apache.karaf.cellar.core.shell.CellarCommandSupport;
+import org.apache.karaf.cellar.core.shell.completer.AllNodeCompleter;
+import org.apache.karaf.cellar.log.GetLogCommand;
+import org.apache.karaf.cellar.log.GetLogResult;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.support.table.ShellTable;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Command(scope = "cluster", name = "log-get", description = "Show the current set log loggers")
+@Service
+public class GetLog extends CellarCommandSupport {
+
+    @Argument(index = 0, name = "logger", description = "The name of the logger, ALL or ROOT (default)", required = false, multiValued = false)
+    String logger;
+
+    @Argument(index = 1, name = "node", description = "The node(s) ID or alias", required = false, multiValued = true)
+    @Completion(AllNodeCompleter.class)
+    List<String> nodes;
+
+    @Option(name = "-t", aliases = { "--timeout" }, description = "Consumer command timeout (in seconds)", required = false, multiValued = false)
+    protected long timeout = 30;
+
+    @Reference
+    protected ExecutionContext executionContext;
+
+    public ExecutionContext getExecutionContext() {
+        return executionContext;
+    }
+
+    public void setExecutionContext(ExecutionContext executionContext) {
+        this.executionContext = executionContext;
+    }
+
+    @Override
+    public Object doExecute() throws Exception {
+        GetLogCommand command = new GetLogCommand(clusterManager.generateId());
+        command.setTimeout(timeout * 1000);
+
+        ShellTable table = new ShellTable();
+        table.column(" ");
+        table.column("Node");
+        table.column("Logger");
+        table.column("Level");
+
+        Set<Node> recipientList = new HashSet<Node>();
+        if (nodes != null && !nodes.isEmpty()) {
+            for (String nodeId : nodes) {
+                Node node = clusterManager.findNodeByIdOrAlias(nodeId);
+                if (node == null) {
+                    System.err.println("Node " + nodeId + " doesn't exist");
+                } else {
+                    recipientList.add(node);
+                }
+            }
+        } else {
+            recipientList = clusterManager.listNodes();
+        }
+
+        if (recipientList.size() < 1)
+            return null;
+
+        command.setDestination(recipientList);
+        command.setLogger(logger);
+
+        Map<Node, GetLogResult> results = executionContext.execute(command);
+        if (results == null || results.isEmpty()) {
+            System.err.println("No result received within given timeout");
+        } else {
+            for (Node node : results.keySet()) {
+                String local = "";
+                if (node.equals(clusterManager.getNode()))
+                    local = "x";
+                GetLogResult result = results.get(node);
+                Map<String, String> loggers = result.getLoggers();
+                for (String logger : loggers.keySet()) {
+                    table.addRow().addContent(local, node.getId(), logger, loggers.get(logger));
+                }
+            }
+        }
+
+        table.print(System.out);
+
+        return null;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/shell/LogClear.java b/log/src/main/java/org/apache/karaf/cellar/log/shell/LogClear.java
new file mode 100644
index 0000000..0a746c1
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/shell/LogClear.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed 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.karaf.cellar.log.shell;
+
+import org.apache.karaf.cellar.core.Node;
+import org.apache.karaf.cellar.core.shell.CellarCommandSupport;
+import org.apache.karaf.cellar.core.shell.completer.AllNodeCompleter;
+import org.apache.karaf.cellar.log.ClusterLogKey;
+import org.apache.karaf.cellar.log.ClusterLogRecord;
+import org.apache.karaf.cellar.log.LogAppender;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+
+import java.util.Map;
+
+@Command(scope = "cluster", name = "log-clear", description = "Clean the cluster log messages")
+@Service
+public class LogClear extends CellarCommandSupport {
+
+    @Argument(index = 0, name = "nodeIdOrAlias", description = "The node ID or alias", required = false, multiValued = false)
+    @Completion(AllNodeCompleter.class)
+    String nodeIdOrAlias;
+
+    @Override
+    public Object doExecute() throws Exception {
+        ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
+        Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
+        try {
+            Map<ClusterLogKey, ClusterLogRecord> clusterLog = clusterManager.getMap(LogAppender.LOG_MAP);
+            if (nodeIdOrAlias == null) {
+                clusterLog.clear();
+            } else {
+                Node node = clusterManager.findNodeByIdOrAlias(nodeIdOrAlias);
+                if (node == null) {
+                    System.err.println("Node " + nodeIdOrAlias + " doesn't exist");
+                    return null;
+                }
+                for (ClusterLogKey key : clusterLog.keySet()) {
+                    if (key.getNodeId().equals(node.getId())) {
+                        clusterLog.remove(key);
+                    }
+                }
+            }
+        } finally {
+            Thread.currentThread().setContextClassLoader(originalClassLoader);
+        }
+        return null;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/shell/LogDisplay.java b/log/src/main/java/org/apache/karaf/cellar/log/shell/LogDisplay.java
new file mode 100644
index 0000000..a4bc432
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/shell/LogDisplay.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed 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.karaf.cellar.log.shell;
+
+import org.apache.karaf.cellar.core.shell.CellarCommandSupport;
+import org.apache.karaf.cellar.core.shell.completer.AllNodeCompleter;
+import org.apache.karaf.cellar.log.ClusterLogKey;
+import org.apache.karaf.cellar.log.ClusterLogRecord;
+import org.apache.karaf.cellar.log.LogAppender;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Map;
+
+@Command(scope = "cluster", name = "log-display", description = "Display the cluster log messages")
+@Service
+public class LogDisplay extends CellarCommandSupport {
+
+    @Argument(index = 0, name = "logger", description = "The logger name", required = false, multiValued = false)
+    String logger;
+
+    @Argument(index = 1, name = "node", description = "The node ID or alias", required = false, multiValued = false)
+    @Completion(AllNodeCompleter.class)
+    String nodeIdOrAlias;
+
+    @Option(name = "-n", description = "Number of entries to display", required = false, multiValued = false)
+    int entries;
+
+    @Override
+    public Object doExecute() throws Exception {
+        String nodeId = null;
+        if (nodeIdOrAlias != null && clusterManager.findNodeByIdOrAlias(nodeIdOrAlias) == null) {
+            System.err.println("Node " + nodeIdOrAlias + " doesn't exist");
+            return null;
+        }
+        if (nodeIdOrAlias != null && clusterManager.findNodeByIdOrAlias(nodeIdOrAlias) != null) {
+            nodeId = clusterManager.findNodeByIdOrAlias(nodeIdOrAlias).getId();
+        }
+        ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
+        Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
+        try {
+            Map<ClusterLogKey, ClusterLogRecord> clusterLog = clusterManager.getMap(LogAppender.LOG_MAP);
+            int index = 0;
+            for (ClusterLogKey key : clusterLog.keySet()) {
+                if (entries == 0 || (entries != 0 && index < entries)) {
+                    ClusterLogRecord record = clusterLog.get(key);
+                    if (nodeId == null || (nodeId != null && key.getNodeId().equals(nodeId))) {
+                        if (logger == null || (logger != null && logger.equals("ALL")) || (logger != null && record.getLoggerName() != null && record.getLoggerName().contains(logger))) {
+                            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
+                            System.out.println(key.getNodeId() + " | "
+                                    + dateFormat.format(new Date(key.getTimeStamp())) + " | "
+                                    + record.getLevel() + " | "
+                                    + record.getThreadName() + " | "
+                                    + record.getLoggerName() + " | "
+                                    + record.getMessage());
+                            index++;
+                        }
+                    }
+                }
+            }
+        } finally {
+            Thread.currentThread().setContextClassLoader(originalClassLoader);
+        }
+        return null;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/shell/LogDisplayException.java b/log/src/main/java/org/apache/karaf/cellar/log/shell/LogDisplayException.java
new file mode 100644
index 0000000..9468c89
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/shell/LogDisplayException.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed 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.karaf.cellar.log.shell;
+
+import org.apache.karaf.cellar.core.Node;
+import org.apache.karaf.cellar.core.shell.CellarCommandSupport;
+import org.apache.karaf.cellar.core.shell.completer.AllNodeCompleter;
+import org.apache.karaf.cellar.log.ClusterLogKey;
+import org.apache.karaf.cellar.log.ClusterLogRecord;
+import org.apache.karaf.cellar.log.LogAppender;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+
+import java.util.Map;
+
+@Command(scope = "cluster", name = "log-exception-display", description = "Display the last occurred exception from the cluster log")
+@Service
+public class LogDisplayException extends CellarCommandSupport {
+
+    @Argument(index = 0, name = "logger", description = "The name of the logger. This can be ROOT, ALL, or specific logger", required = false, multiValued = false)
+    String logger;
+
+    @Argument(index = 1, name = "node", description = "The node ID or alias", required = false, multiValued = false)
+    @Completion(AllNodeCompleter.class)
+    String nodeIdOrAlias;
+
+    @Override
+    public Object doExecute() throws Exception {
+        String nodeId = null;
+        if (nodeIdOrAlias != null) {
+            Node node = clusterManager.findNodeByIdOrAlias(nodeIdOrAlias);
+            if (node == null) {
+                System.err.println("Node " + nodeIdOrAlias + " doesn't exist");
+                return null;
+            }
+            nodeId = node.getId();
+        }
+        ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
+        Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
+        try {
+            Map<ClusterLogKey, ClusterLogRecord> clusterLog = clusterManager.getMap(LogAppender.LOG_MAP);
+            for (ClusterLogKey key : clusterLog.keySet()) {
+                ClusterLogRecord record = clusterLog.get(key);
+                if (record.getThrowableStringRep() != null && record.getThrowableStringRep().length > 0) {
+                    if (nodeId == null || (nodeId != null && key.getNodeId().equals(nodeId))) {
+                        if (logger == null || (logger != null && record.getLoggerName().contains(logger))) {
+                            for (String throwable : record.getThrowableStringRep()) {
+                                System.out.println(throwable);
+                            }
+                            System.out.println();
+                        }
+                    }
+                }
+            }
+        } finally {
+            Thread.currentThread().setContextClassLoader(originalClassLoader);
+        }
+        return null;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/shell/LogMessage.java b/log/src/main/java/org/apache/karaf/cellar/log/shell/LogMessage.java
new file mode 100644
index 0000000..d0cdafb
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/shell/LogMessage.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed 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.karaf.cellar.log.shell;
+
+import org.apache.karaf.cellar.core.shell.CellarCommandSupport;
+import org.apache.karaf.cellar.core.shell.completer.AllNodeCompleter;
+import org.apache.karaf.cellar.log.ClusterLogKey;
+import org.apache.karaf.cellar.log.ClusterLogRecord;
+import org.apache.karaf.cellar.log.LogAppender;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+
+import java.util.Map;
+
+@Command(scope = "cluster", name = "log-log", description = "Add a message in the cluster log")
+@Service
+public class LogMessage extends CellarCommandSupport {
+
+    @Argument(index = 0, name = "message", description = "The message to log", required = true, multiValued = false)
+    String message;
+
+    @Argument(index = 1, name = "node", description = "The node ID or alias", required = false, multiValued = false)
+    @Completion(AllNodeCompleter.class)
+    String nodeId;
+
+    @Option(name = "--level", aliases = { "-l" }, description = "The level the message will be logged at", required = false, multiValued = false)
+    @Completion(value = StringsCompleter.class, values = { "DEBUG", "INFO", "WARN", "ERROR" })
+    String level = "INFO";
+
+    public Object doExecute() throws Exception {
+        if (nodeId != null && clusterManager.findNodeByIdOrAlias(nodeId) == null) {
+            System.err.println("Node " + nodeId + " doesn't exist");
+            return null;
+        }
+
+        if (nodeId == null) {
+            nodeId = clusterManager.getNode().getId();
+        }
+        long timestamp = System.currentTimeMillis();
+        String id = clusterManager.generateId();
+
+        ClusterLogKey key = new ClusterLogKey();
+        key.setNodeId(nodeId);
+        key.setTimeStamp(timestamp);
+        key.setId(id);
+
+        ClusterLogRecord record = new ClusterLogRecord();
+        record.setMessage(message);
+        record.setLevel(level);
+
+        ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
+        Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
+        try {
+            Map<ClusterLogKey, ClusterLogRecord> clusterLog = clusterManager.getMap(LogAppender.LOG_MAP);
+            clusterLog.put(key, record);
+        } finally {
+            Thread.currentThread().setContextClassLoader(originalClassLoader);
+        }
+
+        return null;
+    }
+
+}
diff --git a/log/src/main/java/org/apache/karaf/cellar/log/shell/SetLog.java b/log/src/main/java/org/apache/karaf/cellar/log/shell/SetLog.java
new file mode 100644
index 0000000..5a5eb70
--- /dev/null
+++ b/log/src/main/java/org/apache/karaf/cellar/log/shell/SetLog.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed 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.karaf.cellar.log.shell;
+
+import org.apache.karaf.cellar.core.Node;
+import org.apache.karaf.cellar.core.command.ExecutionContext;
+import org.apache.karaf.cellar.core.shell.CellarCommandSupport;
+import org.apache.karaf.cellar.core.shell.completer.AllNodeCompleter;
+import org.apache.karaf.cellar.log.SetLogCommand;
+import org.apache.karaf.cellar.log.SetLogResult;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Command(scope = "cluster", name = "log-set", description = "Set the log level")
+@Service
+public class SetLog extends CellarCommandSupport {
+
+    @Argument(index = 0, name = "level", description = "The log level to set (TRACE, DEBUG, INFO, WARN, ERROR) or DEFAULT to unset", required = true, multiValued = false)
+    @Completion(value = StringsCompleter.class, values = { "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "DEFAULT" })
+    String level;
+
+    @Argument(index = 1, name = "logger", description = "Logger name or ROOT (default)", required = false, multiValued = false)
+    String logger;
+
+    @Argument(index = 2, name = "node", description = "The node(s) ID or alias", required = false, multiValued = true)
+    @Completion(AllNodeCompleter.class)
+    List<String> nodes;
+
+    @Option(name = "-t", aliases = { "--timeout" }, description = "Consumer command timeout (in seconds)", required = false, multiValued = false)
+    protected long timeout = 30;
+
+    @Reference
+    protected ExecutionContext executionContext;
+
+    public ExecutionContext getExecutionContext() {
+        return executionContext;
+    }
+
+    public void setExecutionContext(ExecutionContext executionContext) {
+        this.executionContext = executionContext;
+    }
+
+    @Override
+    public Object doExecute() throws Exception {
+        SetLogCommand command = new SetLogCommand(clusterManager.generateId());
+        command.setTimeout(timeout * 1000);
+
+        Set<Node> recipientList = new HashSet<Node>();
+        if (nodes != null && !nodes.isEmpty()) {
+            for (String nodeId : nodes) {
+                Node node = clusterManager.findNodeByIdOrAlias(nodeId);
+                if (node == null) {
+                    System.err.println("Node " + nodeId + " doesn't exist");
+                } else {
+                    recipientList.add(node);
+                }
+            }
+        } else {
+            recipientList = clusterManager.listNodes();
+        }
+
+        if (recipientList.size() < 1)
+            return null;
+
+        command.setDestination(recipientList);
+        command.setLogger(logger);
+        command.setLevel(level);
+
+        Map<Node, SetLogResult> results = executionContext.execute(command);
+        if (results == null || results.isEmpty()) {
+            System.err.println("No result received within given timeout");
+        }
+
+        return null;
+    }
+
+}
diff --git a/manual/src/main/asciidoc/index.adoc b/manual/src/main/asciidoc/index.adoc
index c8200ae..a098d79 100644
--- a/manual/src/main/asciidoc/index.adoc
+++ b/manual/src/main/asciidoc/index.adoc
@@ -40,6 +40,8 @@
 
 include::user-guide/event.adoc[]
 
+include::user-guide/log.adoc[]
+
 include::user-guide/http-balancer.adoc[]
 
 include::user-guide/http-session.adoc[]
diff --git a/manual/src/main/asciidoc/user-guide/deploy.adoc b/manual/src/main/asciidoc/user-guide/deploy.adoc
index c98be09..a04b1e2 100644
--- a/manual/src/main/asciidoc/user-guide/deploy.adoc
+++ b/manual/src/main/asciidoc/user-guide/deploy.adoc
@@ -81,4 +81,16 @@
 karaf@root()> feature:install cellar-cloud
 ----
 
+The cellar-http feature adds HTTP support:
+
+----
+karaf@root()> feature:install cellar-http
+----
+
+The cellar-log feature adds centralized log support:
+
+----
+karaf@root()> feature:install cellar-log
+----
+
 Please, see the sections dedicated to these features for details.
diff --git a/manual/src/main/asciidoc/user-guide/log.adoc b/manual/src/main/asciidoc/user-guide/log.adoc
new file mode 100644
index 0000000..06ad183
--- /dev/null
+++ b/manual/src/main/asciidoc/user-guide/log.adoc
@@ -0,0 +1,122 @@
+//
+// Licensed 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.
+//
+
+=== Centralized Log
+
+Apache Karaf Cellar is able to centralize and manage log service on each node.
+
+==== Enable Cellar log
+
+Centralized log support is an optionnal feature that you can install using:
+
+----
+karaf@root()> feature:install cellar-log
+----
+
+==== Display log messages and exceptions
+
+The `cluster:log-display` command allows you to display all log messages on the cluster or on a specific node in the
+cluster.
+
+Without argument, `cluster:log-display` command will display log messages from all nodes in the cluster:
+
+----
+karaf@node1()> cluster:log-display
+172.17.42.1:5701 | 2016-07-09T22:21:34+0200 | INFO | pool-52-thread-1 | org.apache.karaf.shell.impl.action.osgi.CommandExtension | Registering commands for bundle org.apache.karaf.cellar.log/4.0.1.SNAPSHOT
+172.17.42.1:5701 | 2016-07-09T22:21:34+0200 | INFO | pool-52-thread-1 | org.apache.karaf.features.internal.service.FeaturesServiceImpl | Done.
+----
+
+You can also specify a node (ID or alias) to display only log messages for this specific node:
+
+----
+karaf@node1()> cluster:log-display ALL 172.17.42.1:5701
+172.17.42.1:5701 | 2016-07-09T22:21:34+0200 | INFO | pool-52-thread-1 | org.apache.karaf.shell.impl.action.osgi.CommandExtension | Registering commands for bundle org.apache.karaf.cellar.log/4.0.1.SNAPSHOT
+172.17.42.1:5701 | 2016-07-09T22:21:34+0200 | INFO | pool-52-thread-1 | org.apache.karaf.features.internal.service.FeaturesServiceImpl | Done.
+----
+
+`ALL` keyword means log messages for any logger.
+
+It's also possible to display the log message only for a specific logger:
+
+----
+karaf@node1()> cluster:log-display org.apache.karaf.features
+172.17.42.1:5701 | 2016-07-09T22:21:34+0200 | INFO | pool-52-thread-1 | org.apache.karaf.features.internal.service.FeaturesServiceImpl | Done.
+----
+
+The `cluster:log-exception-display` displays the last occurred exception.
+
+Like the `cluster:log-display` command, you can also filter by logger and node.
+
+==== Log a message
+
+At any time, you can log a message of your choice using `cluster:log-log` command, from the local node and any node
+in the cluster:
+
+----
+karaf@root()> cluster:log-log "My own message"
+karaf@root()> cluster:log-log "My own message" 172.17.42.1:5701
+----
+
+==== Clear log messages
+
+The `cluster:log-clear` command cleans the log messages.
+
+Without argument, the command cleans all log messages on the cluster:
+
+----
+karaf@root()> cluster:log-clear
+----
+
+You can specify the node (ID or alias) for which we remove only the log message:
+
+----
+karaf@root()> cluster:log-clear
+----
+
+===== Get and set log levels
+
+You can change the log level for any logger on all nodes in the cluster or on a specific node using the
+`cluster:log-set` command.
+
+----
+karaf@root()> cluster:log-set DEBUG
+----
+
+You can specify the logger to change:
+
+----
+karaf@root()> cluster:log-set DEBUG org.apache.karaf.cellar
+----
+
+And you can also change on a specific node in the cluster instead of all nodes:
+
+----
+karaf@root()> cluster:log-set DEBUG ROOT 172.17.42.1:5701
+----
+
+On the other hand, you can display the current log level on all nodes, for each logger using the `cluster:log-set`
+command:
+
+----
+karaf@node1()> cluster:log-get
+Node 172.17.42.1:5701 (x)
+Logger | Level
+--------------
+ROOT   | INFO
+...
+----
+
+==== JMX MBeans
+
+All actions you can do using the shell commands can be done with the `CellarLogMBean`.
\ No newline at end of file
diff --git a/manual/src/main/asciidoc/user-guide/nodes.adoc b/manual/src/main/asciidoc/user-guide/nodes.adoc
index b0f6558..bda3b75 100644
--- a/manual/src/main/asciidoc/user-guide/nodes.adoc
+++ b/manual/src/main/asciidoc/user-guide/nodes.adoc
@@ -25,10 +25,10 @@
 
 ----
 karaf@root()> cluster:node-list
-  | Id             | Host Name | Port
--------------------------------------
-x | node2:5702     | node2 | 5702
-  | node1:5701     | node1 | 5701
+  | Id             | Alias | Host Name | Port
+----------------------------------------------
+x | node2:5702     |       | node2 | 5702
+  | node1:5701     |       | node1 | 5701
 ----
 
 The starting 'x' indicates that it's the Karaf instance on which you are logged on (the local node).
@@ -48,6 +48,9 @@
 See link:hazelcast[Core Configuration section] for details.
 ====
 
+The `cluster:node-alias` command allows you to define an alias to a node. Any Cellar command using a node as argument
+or option can use either node ID or node alias.
+
 ==== Testing nodes
 
 You can ping a node to test it:
diff --git a/pom.xml b/pom.xml
index c5c281a..f2ffabf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -65,6 +65,7 @@
         <module>shell</module>
         <module>hazelcast</module>
         <module>utils</module>
+        <module>log</module>
         <module>http</module>
         <module>cloud</module>
         <module>kubernetes</module>