You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by sh...@apache.org on 2022/03/09 17:01:30 UTC

[unomi] branch UNOMI-554-improve-server-info created (now 4be5e10)

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

shuber pushed a change to branch UNOMI-554-improve-server-info
in repository https://gitbox.apache.org/repos/asf/unomi.git.


      at 4be5e10  UNOMI-554 Improve server identification & various bug fixes - Add /cxs/privacy/infos endpoint to retrieve all server information - Make it possible to override server information and extend it - Add build number, build date, build timestamp and scm branch reporting - Add possibility to override startup logo - Refactor BundleWatcher - Fix negative test event type bug. - Fix bug in GraphQL JSON schema integration

This branch includes the following new commits:

     new 4be5e10  UNOMI-554 Improve server identification & various bug fixes - Add /cxs/privacy/infos endpoint to retrieve all server information - Make it possible to override server information and extend it - Add build number, build date, build timestamp and scm branch reporting - Add possibility to override startup logo - Refactor BundleWatcher - Fix negative test event type bug. - Fix bug in GraphQL JSON schema integration

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[unomi] 01/01: UNOMI-554 Improve server identification & various bug fixes - Add /cxs/privacy/infos endpoint to retrieve all server information - Make it possible to override server information and extend it - Add build number, build date, build timestamp and scm branch reporting - Add possibility to override startup logo - Refactor BundleWatcher - Fix negative test event type bug. - Fix bug in GraphQL JSON schema integration

Posted by sh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

shuber pushed a commit to branch UNOMI-554-improve-server-info
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit 4be5e1058d926071fa78c6577098c8803d189718
Author: Serge Huber <sh...@jahia.com>
AuthorDate: Wed Mar 9 18:01:24 2022 +0100

    UNOMI-554 Improve server identification & various bug fixes
    - Add /cxs/privacy/infos endpoint to retrieve all server information
    - Make it possible to override server information and extend it
    - Add build number, build date, build timestamp and scm branch reporting
    - Add possibility to override startup logo
    - Refactor BundleWatcher
    - Fix negative test event type bug.
    - Fix bug in GraphQL JSON schema integration
---
 .../main/java/org/apache/unomi/api/ServerInfo.java |  48 ++++
 .../apache/unomi/api/services/PrivacyService.java  |  14 +-
 .../unomi/privacy/rest/PrivacyServiceEndPoint.java |   6 +
 extensions/privacy-extension/services/pom.xml      |   6 +
 .../unomi/privacy/internal/PrivacyServiceImpl.java |  23 +-
 .../resources/OSGI-INF/blueprint/blueprint.xml     |   3 +
 .../types/resolvers/CDPEventInterfaceResolver.java |   2 +-
 .../test/java/org/apache/unomi/itests/AllITs.java  |   1 +
 .../test/java/org/apache/unomi/itests/BaseIT.java  | 198 +++++++++++++++
 .../org/apache/unomi/itests/PrivacyServiceIT.java  |  81 ++++++
 .../schemas/events/negative-test-event-type.json   |  13 +
 lifecycle-watcher/pom.xml                          |   8 +
 .../org/apache/unomi/lifecycle/BundleWatcher.java  | 276 +--------------------
 .../{BundleWatcher.java => BundleWatcherImpl.java} | 118 +++++++--
 .../resources/OSGI-INF/blueprint/blueprint.xml     |   4 +-
 .../main/resources/{logo.txt => unomi-logo.txt}    |   2 -
 .../impl/schemas/UnomiPropertyTypeKeyword.java     |   6 +-
 .../META-INF/cxs/schemas/events/view.json          |   1 -
 18 files changed, 504 insertions(+), 306 deletions(-)

diff --git a/api/src/main/java/org/apache/unomi/api/ServerInfo.java b/api/src/main/java/org/apache/unomi/api/ServerInfo.java
index 2006a1f..7b80f94 100644
--- a/api/src/main/java/org/apache/unomi/api/ServerInfo.java
+++ b/api/src/main/java/org/apache/unomi/api/ServerInfo.java
@@ -17,6 +17,8 @@
 
 package org.apache.unomi.api;
 
+import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -27,10 +29,16 @@ public class ServerInfo {
 
     private String serverIdentifier;
     private String serverVersion;
+    private String serverBuildNumber;
+    private Date serverBuildDate;
+    private String serverTimestamp;
+    private String serverScmBranch;
 
     private List<EventInfo> eventTypes;
     private Map<String,String> capabilities;
 
+    private List<String> logoLines = new ArrayList<>();
+
     public ServerInfo() {
     }
 
@@ -50,6 +58,38 @@ public class ServerInfo {
         this.serverVersion = serverVersion;
     }
 
+    public String getServerBuildNumber() {
+        return serverBuildNumber;
+    }
+
+    public void setServerBuildNumber(String serverBuildNumber) {
+        this.serverBuildNumber = serverBuildNumber;
+    }
+
+    public Date getServerBuildDate() {
+        return serverBuildDate;
+    }
+
+    public void setServerBuildDate(Date serverBuildDate) {
+        this.serverBuildDate = serverBuildDate;
+    }
+
+    public String getServerTimestamp() {
+        return serverTimestamp;
+    }
+
+    public void setServerTimestamp(String serverTimestamp) {
+        this.serverTimestamp = serverTimestamp;
+    }
+
+    public String getServerScmBranch() {
+        return serverScmBranch;
+    }
+
+    public void setServerScmBranch(String serverScmBranch) {
+        this.serverScmBranch = serverScmBranch;
+    }
+
     public List<EventInfo> getEventTypes() {
         return eventTypes;
     }
@@ -65,4 +105,12 @@ public class ServerInfo {
     public void setCapabilities(Map<String, String> capabilities) {
         this.capabilities = capabilities;
     }
+
+    public List<String> getLogoLines() {
+        return logoLines;
+    }
+
+    public void setLogoLines(List<String> logoLines) {
+        this.logoLines = logoLines;
+    }
 }
diff --git a/api/src/main/java/org/apache/unomi/api/services/PrivacyService.java b/api/src/main/java/org/apache/unomi/api/services/PrivacyService.java
index 35f11be..3f7d555 100644
--- a/api/src/main/java/org/apache/unomi/api/services/PrivacyService.java
+++ b/api/src/main/java/org/apache/unomi/api/services/PrivacyService.java
@@ -28,13 +28,23 @@ import java.util.List;
 public interface PrivacyService {
 
     /**
-     * Retrieves the server information, including the name and version of the server, the event types
-     * if recognizes as well as the capabilities supported by the system.
+     * Retrieves the default base Apache Unomi server information, including the name and version of the server, build
+     * time information and the event types
+     * if recognizes as well as the capabilities supported by the system. For more detailed information about the system
+     * and extensions use the getServerInfos method.
      * @return a ServerInfo object with all the server information
      */
     ServerInfo getServerInfo();
 
     /**
+     * Retrieves the list of the server information objects, that include extensions. Each object includes the
+     * name and version of the server, build time information and the event types
+     * if recognizes as well as the capabilities supported by the system.
+     * @return a list of ServerInfo objects with all the server information
+     */
+    List<ServerInfo> getServerInfos();
+
+    /**
      * Deletes the current profile (but has no effect on sessions and events). This will delete the
      * persisted profile and replace it with a new empty one with the same profileId.
      * @param profileId the identifier of the profile to delete and replace
diff --git a/extensions/privacy-extension/rest/src/main/java/org/apache/unomi/privacy/rest/PrivacyServiceEndPoint.java b/extensions/privacy-extension/rest/src/main/java/org/apache/unomi/privacy/rest/PrivacyServiceEndPoint.java
index c4c990a..8c3dafc 100644
--- a/extensions/privacy-extension/rest/src/main/java/org/apache/unomi/privacy/rest/PrivacyServiceEndPoint.java
+++ b/extensions/privacy-extension/rest/src/main/java/org/apache/unomi/privacy/rest/PrivacyServiceEndPoint.java
@@ -57,6 +57,12 @@ public class PrivacyServiceEndPoint {
         return privacyService.getServerInfo();
     }
 
+    @GET
+    @Path("/infos")
+    public List<ServerInfo> getServerInfos() {
+        return privacyService.getServerInfos();
+    }
+
     @DELETE
     @Path("/profiles/{profileId}")
     public Response deleteProfileData(@PathParam("profileId") String profileId, @QueryParam("withData") @DefaultValue("false") boolean withData,
diff --git a/extensions/privacy-extension/services/pom.xml b/extensions/privacy-extension/services/pom.xml
index 5a0ede7..b7e337d 100644
--- a/extensions/privacy-extension/services/pom.xml
+++ b/extensions/privacy-extension/services/pom.xml
@@ -53,6 +53,12 @@
             <artifactId>osgi.core</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-lifecycle-watcher</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/extensions/privacy-extension/services/src/main/java/org/apache/unomi/privacy/internal/PrivacyServiceImpl.java b/extensions/privacy-extension/services/src/main/java/org/apache/unomi/privacy/internal/PrivacyServiceImpl.java
index 3d3d68d..64c3a5f 100644
--- a/extensions/privacy-extension/services/src/main/java/org/apache/unomi/privacy/internal/PrivacyServiceImpl.java
+++ b/extensions/privacy-extension/services/src/main/java/org/apache/unomi/privacy/internal/PrivacyServiceImpl.java
@@ -21,6 +21,7 @@ import org.apache.unomi.api.*;
 import org.apache.unomi.api.services.EventService;
 import org.apache.unomi.api.services.PrivacyService;
 import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.lifecycle.BundleWatcher;
 import org.apache.unomi.persistence.spi.PersistenceService;
 import org.apache.unomi.persistence.spi.aggregate.TermsAggregate;
 import org.osgi.framework.BundleContext;
@@ -40,6 +41,7 @@ public class PrivacyServiceImpl implements PrivacyService {
     private ProfileService profileService;
     private EventService eventService;
     private BundleContext bundleContext;
+    private BundleWatcher bundleWatcher;
 
     public PrivacyServiceImpl() {
         logger.info("Initializing privacy service...");
@@ -61,12 +63,20 @@ public class PrivacyServiceImpl implements PrivacyService {
         this.bundleContext = bundleContext;
     }
 
+    public void setBundleWatcher(BundleWatcher bundleWatcher) {
+        this.bundleWatcher = bundleWatcher;
+    }
+
     @Override
     public ServerInfo getServerInfo() {
-        ServerInfo serverInfo = new ServerInfo();
-        serverInfo.setServerIdentifier("Apache Unomi");
-        serverInfo.setServerVersion(bundleContext.getBundle().getVersion().toString());
+        List<ServerInfo> serverInfos = bundleWatcher.getServerInfos();
+        ServerInfo serverInfo = serverInfos.get(0); // Unomi is always be the first entry
+
+        addUnomiInfo(serverInfo);
+        return serverInfo;
+    }
 
+    private void addUnomiInfo(ServerInfo serverInfo) {
         // let's retrieve all the event types the server has seen.
         Map<String, Long> eventTypeCounts = persistenceService.aggregateWithOptimizedQuery(null, new TermsAggregate("eventType"), Event.ITEM_TYPE);
         List<EventInfo> eventTypes = new ArrayList<EventInfo>();
@@ -79,7 +89,12 @@ public class PrivacyServiceImpl implements PrivacyService {
         serverInfo.setEventTypes(eventTypes);
 
         serverInfo.setCapabilities(new HashMap<String, String>());
-        return serverInfo;
+    }
+
+    public List<ServerInfo> getServerInfos() {
+        List<ServerInfo> serverInfos = bundleWatcher.getServerInfos();
+        addUnomiInfo(serverInfos.get(0));
+        return serverInfos;
     }
 
     @Override
diff --git a/extensions/privacy-extension/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/extensions/privacy-extension/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index 1b56fe9..85f9e3c 100644
--- a/extensions/privacy-extension/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/extensions/privacy-extension/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -29,6 +29,8 @@
 
     <reference id="profileService" interface="org.apache.unomi.api.services.ProfileService"/>
 
+    <reference id="bundleWatcher" interface="org.apache.unomi.lifecycle.BundleWatcher" />
+
     <!-- Privacy service -->
 
     <bean id="privacyServiceImpl" class="org.apache.unomi.privacy.internal.PrivacyServiceImpl">
@@ -36,6 +38,7 @@
         <property name="eventService" ref="eventService" />
         <property name="profileService" ref="profileService" />
         <property name="bundleContext" ref="blueprintBundleContext"/>
+        <property name="bundleWatcher" ref="bundleWatcher"/>
     </bean>
     <service id="privacyService" ref="privacyServiceImpl" interface="org.apache.unomi.api.services.PrivacyService"/>
 </blueprint>
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/resolvers/CDPEventInterfaceResolver.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/resolvers/CDPEventInterfaceResolver.java
index fc15ed4..9f62673 100644
--- a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/resolvers/CDPEventInterfaceResolver.java
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/types/resolvers/CDPEventInterfaceResolver.java
@@ -34,7 +34,7 @@ public class CDPEventInterfaceResolver extends BaseTypeResolver {
         final CDPEventInterface eventInterface = env.getObject();
         final JSONSchema eventSchema = schemaRegistry.getSchema("https://unomi.apache.org/schemas/json/events/" + eventInterface.getEvent().getEventType() + "/1-0-0");
         if (eventSchema != null) {
-            final String typeName = UnomiToGraphQLConverter.convertEventType(eventSchema.getSchemaId());
+            final String typeName = UnomiToGraphQLConverter.convertEventType(eventSchema.getName());
             return env.getSchema().getObjectType(typeName);
         } else {
             return super.getType(env);
diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
index d632872..facb730 100644
--- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
@@ -51,6 +51,7 @@ import org.junit.runners.Suite.SuiteClasses;
         ContextServletIT.class,
         SecurityIT.class,
         RuleServiceIT.class,
+        PrivacyServiceIT.class,
         GroovyActionsServiceIT.class,
         GraphQLEventIT.class,
         GraphQLListIT.class,
diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
index d39569b..102d107 100644
--- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
@@ -19,6 +19,22 @@ package org.apache.unomi.itests;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.commons.io.IOUtils;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.*;
+import org.apache.http.config.Registry;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
 import org.apache.unomi.api.Item;
 import org.apache.unomi.api.conditions.Condition;
 import org.apache.unomi.api.rules.Rule;
@@ -28,6 +44,7 @@ import org.apache.unomi.api.services.SchemaRegistry;
 import org.apache.unomi.lifecycle.BundleWatcher;
 import org.apache.unomi.persistence.spi.CustomObjectMapper;
 import org.apache.unomi.persistence.spi.PersistenceService;
+import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.runner.RunWith;
@@ -51,8 +68,16 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
 import java.util.*;
 import java.util.concurrent.CountDownLatch;
 import java.util.function.Predicate;
@@ -77,6 +102,11 @@ public abstract class BaseIT {
     protected static final String URL = "http://localhost:" + HTTP_PORT;
     protected static final String KARAF_DIR = "target/exam";
     protected static final String UNOMI_KEY = "670c26d1cc413346c3b2fd9ce65dab41";
+    protected static final ContentType JSON_CONTENT_TYPE = ContentType.create("application/json");
+    protected static final String BASE_URL = "http://localhost";
+    protected static final String BASIC_AUTH_USER_NAME = "karaf";
+    protected static final String BASIC_AUTH_PASSWORD = "karaf";
+    protected static final int REQUEST_TIMEOUT = 60000;
 
     @Inject
     @Filter(timeout = 600000)
@@ -104,14 +134,24 @@ public abstract class BaseIT {
     @Filter(timeout = 600000)
     protected SchemaRegistry schemaRegistry;
 
+    private CloseableHttpClient httpClient;
+
     @Before
     public void waitForStartup() throws InterruptedException {
         while (!bundleWatcher.isStartupComplete()) {
             LOGGER.info("Waiting for startup to complete...");
             Thread.sleep(1000);
         }
+        httpClient = initHttpClient();
+    }
+
+    @After
+    public void shutdown() {
+        closeHttpClient(httpClient);
+        httpClient = null;
     }
 
+
     protected void removeItems(final Class<? extends Item> ...classes) throws InterruptedException {
         Condition condition = new Condition(definitionsService.getConditionType("matchAllCondition"));
         for (Class<? extends Item> aClass : classes) {
@@ -362,4 +402,162 @@ public abstract class BaseIT {
                 100);
         rulesService.refreshRules();
     }
+
+    public String getFullUrl(String url) throws Exception {
+        return BASE_URL + ":" + HTTP_PORT + url;
+    }
+
+    protected <T> T get(final String url, Class<T> clazz) {
+        CloseableHttpResponse response = null;
+        try {
+            final HttpGet httpGet = new HttpGet(getFullUrl(url));
+            response = executeHttpRequest(httpGet);
+            if (response.getStatusLine().getStatusCode() == 200) {
+                ObjectMapper objectMapper = new ObjectMapper();
+                return objectMapper.readValue(response.getEntity().getContent(), clazz);
+            } else {
+                return null;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (response != null) {
+                try {
+                    response.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return null;
+    }
+
+    protected CloseableHttpResponse post(final String url, final String resource) {
+        try {
+            final HttpPost request = new HttpPost(getFullUrl(url));
+
+            if (resource != null) {
+                final String resourceAsString = resourceAsString(resource);
+                request.setEntity(new StringEntity(resourceAsString, JSON_CONTENT_TYPE));
+            }
+
+            return executeHttpRequest(request);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    protected void delete(final String url) {
+        CloseableHttpResponse response = null;
+        try {
+            final HttpDelete httpDelete = new HttpDelete(getFullUrl(url));
+            response = executeHttpRequest(httpDelete);
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (response != null) {
+                try {
+                    response.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    protected CloseableHttpResponse executeHttpRequest(HttpUriRequest request) throws IOException {
+        System.out.println("Executing request " + request.getMethod() + " " + request.getURI() + "...");
+        CloseableHttpResponse response = httpClient.execute(request);
+        int statusCode = response.getStatusLine().getStatusCode();
+        if (statusCode != 200) {
+            String content = null;
+            if (response.getEntity() != null) {
+                InputStream contentInputStream = response.getEntity().getContent();
+                if (contentInputStream != null) {
+                    content = IOUtils.toString(response.getEntity().getContent());
+                }
+            }
+            LOGGER.error("Response status code: {}, reason: {}, content:{}", response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(), content);
+        }
+        return response;
+    }
+
+    protected String resourceAsString(final String resource) {
+        final java.net.URL url = bundleContext.getBundle().getResource(resource);
+        try (InputStream stream = url.openStream()) {
+            final ObjectMapper objectMapper = new ObjectMapper();
+            return objectMapper.writeValueAsString(objectMapper.readTree(stream));
+        } catch (final Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static CloseableHttpClient initHttpClient() {
+        long requestStartTime = System.currentTimeMillis();
+        BasicCredentialsProvider credsProvider = null;
+        credsProvider = new BasicCredentialsProvider();
+        credsProvider.setCredentials(
+                AuthScope.ANY,
+                new UsernamePasswordCredentials(BASIC_AUTH_USER_NAME, BASIC_AUTH_PASSWORD));
+        HttpClientBuilder httpClientBuilder = HttpClients.custom().useSystemProperties().setDefaultCredentialsProvider(credsProvider);
+
+        try {
+            SSLContext sslContext = SSLContext.getInstance("SSL");
+            sslContext.init(null, new TrustManager[]{new X509TrustManager() {
+                public X509Certificate[] getAcceptedIssuers() {
+                    return null;
+                }
+
+                public void checkClientTrusted(X509Certificate[] certs,
+                                               String authType) {
+                }
+
+                public void checkServerTrusted(X509Certificate[] certs,
+                                               String authType) {
+                }
+            }}, new SecureRandom());
+
+            Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
+                    .register("http", PlainConnectionSocketFactory.getSocketFactory())
+                    .register("https", new SSLConnectionSocketFactory(sslContext, SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER))
+                    .build();
+
+            PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
+            poolingHttpClientConnectionManager.setMaxTotal(10);
+
+            httpClientBuilder.setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER)
+                    .setConnectionManager(poolingHttpClientConnectionManager);
+
+        } catch (NoSuchAlgorithmException | KeyManagementException e) {
+            LOGGER.error("Error creating SSL Context", e);
+        }
+
+        RequestConfig requestConfig = RequestConfig.custom()
+                .setConnectTimeout(REQUEST_TIMEOUT)
+                .setSocketTimeout(REQUEST_TIMEOUT)
+                .setConnectionRequestTimeout(REQUEST_TIMEOUT)
+                .build();
+        httpClientBuilder.setDefaultRequestConfig(requestConfig);
+
+        if (LOGGER.isDebugEnabled()) {
+            long totalRequestTime = System.currentTimeMillis() - requestStartTime;
+            LOGGER.debug("Init HttpClient executed in " + totalRequestTime + "ms");
+        }
+
+        return httpClientBuilder.build();
+    }
+
+    public static void closeHttpClient(CloseableHttpClient httpClient) {
+        try {
+            if (httpClient != null) {
+                httpClient.close();
+            }
+        } catch (IOException e) {
+            LOGGER.error("Could not close httpClient: " + httpClient, e);
+        }
+    }
+
 }
diff --git a/itests/src/test/java/org/apache/unomi/itests/PrivacyServiceIT.java b/itests/src/test/java/org/apache/unomi/itests/PrivacyServiceIT.java
new file mode 100644
index 0000000..0c4f8a3
--- /dev/null
+++ b/itests/src/test/java/org/apache/unomi/itests/PrivacyServiceIT.java
@@ -0,0 +1,81 @@
+/*
+ * 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.unomi.itests;
+
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.AuthCache;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.impl.client.BasicAuthCache;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.unomi.api.PartialList;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerSuite;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerSuite.class)
+public class PrivacyServiceIT extends BaseIT {
+
+    protected static final String PRIVACY_ENDPOINT = "/cxs/privacy";
+    private static final int DEFAULT_TRYING_TIMEOUT = 2000;
+    private static final int DEFAULT_TRYING_TRIES = 30;
+
+    @Before
+    public void setUp() throws InterruptedException, IOException {
+        keepTrying("Couldn't find privacy endpoint",
+                () -> get(PRIVACY_ENDPOINT + "/info", Map.class),
+                serverInfo -> serverInfo != null,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+    }
+
+    @Test
+    public void testServerInfo() throws IOException {
+        Map<String,Object> serverInfo = get(PRIVACY_ENDPOINT + "/info", Map.class);
+        assertNotNull("Server info is null", serverInfo);
+        assertEquals("Server identifier is incorrect", "Apache Unomi", serverInfo.get("serverIdentifier"));
+    }
+
+    @Test
+    public void testServerInfos() throws IOException {
+        List<Map<String,Object>> serverInfos = get(PRIVACY_ENDPOINT + "/infos", List.class);
+        assertEquals("Server info list is invalid", 1, serverInfos.size());
+        assertEquals("Server identifier is incorrect", "Apache Unomi", serverInfos.get(0).get("serverIdentifier"));
+    }
+
+}
diff --git a/itests/src/test/resources/schemas/events/negative-test-event-type.json b/itests/src/test/resources/schemas/events/negative-test-event-type.json
new file mode 100644
index 0000000..fe4d299
--- /dev/null
+++ b/itests/src/test/resources/schemas/events/negative-test-event-type.json
@@ -0,0 +1,13 @@
+{
+  "$id": "https://unomi.apache.org/schemas/json/events/negative-test-event-type/1-0-0",
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "self":{
+    "vendor":"org.apache.unomi",
+    "name":"events/negative-test-event-type",
+    "format":"jsonschema",
+    "version":"1-0-0"
+  },
+  "title": "TestEvent",
+  "type": "object",
+  "allOf": [{ "$ref": "https://unomi.apache.org/schemas/json/event/1-0-0" }]
+}
\ No newline at end of file
diff --git a/lifecycle-watcher/pom.xml b/lifecycle-watcher/pom.xml
index 93a60ec..f3a5192 100644
--- a/lifecycle-watcher/pom.xml
+++ b/lifecycle-watcher/pom.xml
@@ -34,6 +34,12 @@
             <groupId>org.osgi</groupId>
             <artifactId>osgi.core</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-api</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
 
     <build>
@@ -64,6 +70,8 @@
                         <Embed-Dependency>*;scope=compile|runtime</Embed-Dependency>
                         <Implementation-Build>${buildNumber}</Implementation-Build>
                         <Implementation-TimeStamp>${timestamp}</Implementation-TimeStamp>
+                        <Implementation-ScmBranch>${scmBranch}</Implementation-ScmBranch>
+                        <Implementation-ServerIdentifier>Apache Unomi</Implementation-ServerIdentifier>
                     </instructions>
                 </configuration>
             </plugin>
diff --git a/lifecycle-watcher/src/main/java/org/apache/unomi/lifecycle/BundleWatcher.java b/lifecycle-watcher/src/main/java/org/apache/unomi/lifecycle/BundleWatcher.java
index 31eec3b..06c28a6 100644
--- a/lifecycle-watcher/src/main/java/org/apache/unomi/lifecycle/BundleWatcher.java
+++ b/lifecycle-watcher/src/main/java/org/apache/unomi/lifecycle/BundleWatcher.java
@@ -16,276 +16,24 @@
  */
 package org.apache.unomi.lifecycle;
 
-import org.osgi.framework.*;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.unomi.api.ServerInfo;
 
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.LinkedHashSet;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TimerTask;
-import java.util.concurrent.*;
-import java.util.stream.Collectors;
 
 /**
- * This class listens to the global Apache Unomi bundle lifecycle, to provide statistics and state of the overall
- * system. It notably displays messages for successfull or unsuccessfull startups as well as startup times.
+ * Interface for the bundle watcher system in Apache Unomi. It allows to know if startup has completed as well as
+ * server information such as identifier, versions, build information and more.
  */
-public class BundleWatcher implements SynchronousBundleListener, ServiceListener {
+public interface BundleWatcher {
 
-    private static final Logger logger = LoggerFactory.getLogger(BundleWatcher.class.getName());
+    /**
+     * Retrieves the list of the server information objects, that include extensions. Each object includes the
+     * name and version of the server, build time information and the event types
+     * if recognizes as well as the capabilities supported by the system.
+     * @return a list of ServerInfo objects with all the server information
+     */
+    List<ServerInfo> getServerInfos();
 
-    private long startupTime;
-    private Map<String, Boolean> requiredBundles = new ConcurrentHashMap<>();
-
-    private ScheduledExecutorService scheduler;
-    private ScheduledFuture<?> scheduledFuture;
-
-    private String requiredServices;
-    private Set<Filter> requiredServicesFilters = new LinkedHashSet<>();
-    private long matchedRequiredServicesCount = 0;
-
-    private BundleContext bundleContext;
-    private boolean startupMessageAlreadyDisplayed = false;
-    private boolean shutdownMessageAlreadyDisplayed = false;
-    private List<String> logoLines = new ArrayList<>();
-
-    private Integer checkStartupStateRefreshInterval = 60;
-
-    public void setRequiredBundles(Map<String, Boolean> requiredBundles) {
-        this.requiredBundles = new ConcurrentHashMap<>(requiredBundles);
-    }
-
-    public void setCheckStartupStateRefreshInterval(Integer checkStartupStateRefreshInterval) {
-        this.checkStartupStateRefreshInterval = checkStartupStateRefreshInterval;
-    }
-
-    public void setRequiredServices(String requiredServices) {
-        this.requiredServices = requiredServices;
-        String[] requiredServiceArray = requiredServices.split(",");
-        requiredServicesFilters.clear();
-        for (String requiredService : requiredServiceArray) {
-            try {
-                requiredServicesFilters.add(bundleContext.createFilter(requiredService.trim()));
-            } catch (InvalidSyntaxException e) {
-                logger.error("Error creating require service filter {}", requiredService.trim(), e);
-            }
-        }
-    }
-
-    public void setBundleContext(BundleContext bundleContext) {
-        this.bundleContext = bundleContext;
-    }
-
-    public void init() {
-        scheduler = Executors.newSingleThreadScheduledExecutor();
-        checkExistingBundles();
-        bundleContext.addBundleListener(this);
-        bundleContext.addServiceListener(this);
-        loadLogo();
-        startupTime = System.currentTimeMillis();
-        System.out.println("Initializing Unomi...");
-        logger.info("Bundle watcher initialized.");
-    }
-
-    private boolean allBundleStarted() {
-        return getInactiveBundles().isEmpty();
-    }
-
-    private void displayLogsForInactiveBundles() {
-        getInactiveBundles().forEach(inactiveBundle -> logger
-                .warn("The bundle {} is in not active, some errors could happen when using the application", inactiveBundle));
-    }
-
-    private List<String> getInactiveBundles() {
-        return requiredBundles.entrySet().stream().filter(entry -> !entry.getValue()).map(Map.Entry::getKey).collect(Collectors.toList());
-
-    }
-
-    public void destroy() {
-        bundleContext.removeServiceListener(this);
-        bundleContext.removeBundleListener(this);
-        if (scheduledFuture != null) {
-            scheduledFuture.cancel(true);
-        }
-        logger.info("Bundle watcher shutdown.");
-    }
-
-    public void checkExistingBundles() {
-        for (Bundle bundle : bundleContext.getBundles()) {
-            if (bundle.getSymbolicName().startsWith("org.apache.unomi") && requiredBundles.containsKey(bundle.getSymbolicName())) {
-                if (bundle.getState() == Bundle.ACTIVE) {
-                    requiredBundles.put(bundle.getSymbolicName(), true);
-                } else {
-                    requiredBundles.put(bundle.getSymbolicName(), false);
-                }
-            }
-        }
-        checkStartupComplete();
-    }
-
-    @Override
-    public void bundleChanged(BundleEvent event) {
-        switch (event.getType()) {
-            case BundleEvent.STARTING:
-                break;
-            case BundleEvent.STARTED:
-                if (event.getBundle().getSymbolicName().startsWith("org.apache.unomi") && requiredBundles.containsKey(event.getBundle().getSymbolicName())) {
-                    logger.info("Bundle {} was started.", event.getBundle().getSymbolicName());
-                    requiredBundles.put(event.getBundle().getSymbolicName(), true);
-                    checkStartupComplete();
-                }
-                break;
-            case BundleEvent.STOPPING:
-                break;
-            case BundleEvent.STOPPED:
-                if (event.getBundle().getSymbolicName().startsWith("org.apache.unomi") && requiredBundles.containsKey(event.getBundle().getSymbolicName())) {
-                    logger.info("Bundle {} was stopped", event.getBundle().getSymbolicName());
-                    requiredBundles.put(event.getBundle().getSymbolicName(), false);
-                }
-                break;
-            default:
-                break;
-        }
-    }
-
-    @Override
-    public void serviceChanged(ServiceEvent event) {
-        ServiceReference serviceReference = event.getServiceReference();
-        if (serviceReference == null) {
-            return;
-        }
-        switch (event.getType()) {
-            case ServiceEvent.REGISTERED:
-                addStartedService(serviceReference);
-                checkStartupComplete();
-                break;
-            case ServiceEvent.MODIFIED:
-                break;
-            case ServiceEvent.UNREGISTERING:
-                removeStartedService(serviceReference);
-                break;
-        }
-    }
-
-    private void addStartedService(ServiceReference serviceReference) {
-        for (Filter requiredService : requiredServicesFilters) {
-            if (requiredService.match(serviceReference)) {
-                matchedRequiredServicesCount++;
-            }
-        }
-    }
-
-    private void removeStartedService(ServiceReference serviceReference) {
-        for (Filter requiredService : requiredServicesFilters) {
-            if (requiredService.match(serviceReference)) {
-                matchedRequiredServicesCount--;
-                if (!shutdownMessageAlreadyDisplayed) {
-                    System.out.println("Apache Unomi shutting down...");
-                    logger.info("Apache Unomi no longer available, as required service {} is shutdown !", serviceReference);
-                    shutdownMessageAlreadyDisplayed = true;
-                }
-                startupMessageAlreadyDisplayed = false;
-            }
-        }
-    }
-
-    private void displayLogsForInactiveServices() {
-        requiredServicesFilters.forEach(requiredServicesFilter -> {
-            ServiceReference[] serviceReference = new ServiceReference[0];
-            String filterToString = requiredServicesFilter.toString();
-            try {
-                serviceReference = bundleContext.getServiceReferences((String) null, filterToString);
-            } catch (InvalidSyntaxException e) {
-                logger.error("Failed to get the service reference for {}", filterToString, e);
-            }
-            if (serviceReference == null) {
-                logger.warn("No service found for the filter {}, some errors could happen when using the application", filterToString);
-            }
-        });
-    }
-
-    private void checkStartupComplete() {
-        if (!isStartupComplete()) {
-            if (scheduledFuture == null || scheduledFuture.isCancelled()) {
-                TimerTask task = new TimerTask() {
-                    @Override
-                    public void run() {
-                        displayLogsForInactiveBundles();
-                        displayLogsForInactiveServices();
-                        checkStartupComplete();
-                    }
-                };
-                scheduledFuture = scheduler
-                        .scheduleWithFixedDelay(task, checkStartupStateRefreshInterval, checkStartupStateRefreshInterval, TimeUnit.SECONDS);
-            }
-            return;
-        }
-        if (scheduledFuture != null) {
-            scheduledFuture.cancel(true);
-            scheduledFuture = null;
-        }
-        if (!startupMessageAlreadyDisplayed) {
-            long totalStartupTime = System.currentTimeMillis() - startupTime;
-            if (!logoLines.isEmpty()) {
-                logoLines.forEach(System.out::println);
-            }
-            String buildNumber = "n/a";
-            if (bundleContext.getBundle().getHeaders().get("Implementation-Build") != null) {
-                buildNumber = bundleContext.getBundle().getHeaders().get("Implementation-Build");
-            }
-            String timestamp = "n/a";
-            if (bundleContext.getBundle().getHeaders().get("Implementation-TimeStamp") != null) {
-                timestamp = bundleContext.getBundle().getHeaders().get("Implementation-TimeStamp");
-            }
-            String versionMessage =
-                    "  " + bundleContext.getBundle().getVersion().toString() + "  Build:" + buildNumber + "  Timestamp:" + timestamp;
-            System.out.println(versionMessage);
-            System.out.println("--------------------------------------------------------------------------");
-            System.out.println(
-                    "Successfully started " + requiredBundles.size() + " bundles and " + requiredServicesFilters.size() + " " + "required "
-                            + "services in " + totalStartupTime + " ms");
-            logger.info("Apache Unomi version: {}", versionMessage);
-            logger.info("Apache Unomi successfully started {} bundles and {} required services in {} ms", requiredBundles.size(),
-                    requiredServicesFilters.size(), totalStartupTime);
-            startupMessageAlreadyDisplayed = true;
-            shutdownMessageAlreadyDisplayed = false;
-        }
-    }
-
-    public boolean isStartupComplete() {
-        return allBundleStarted() && matchedRequiredServicesCount == requiredServicesFilters.size();
-    }
-
-    private void loadLogo() {
-        URL logoURL = bundleContext.getBundle().getResource("logo.txt");
-        if (logoURL != null) {
-            BufferedReader bufferedReader = null;
-            try {
-                bufferedReader = new BufferedReader(new InputStreamReader(logoURL.openStream()));
-                String line;
-                while ((line = bufferedReader.readLine()) != null) {
-                    if (!line.trim().startsWith("#")) {
-                        logoLines.add(line);
-                    }
-                }
-            } catch (IOException e) {
-                logger.error("Error loading logo lines", e);
-            } finally {
-                if (bufferedReader != null) {
-                    try {
-                        bufferedReader.close();
-                    } catch (IOException e) {
-                    }
-                }
-            }
-        }
-    }
 
+    boolean isStartupComplete();
 }
diff --git a/lifecycle-watcher/src/main/java/org/apache/unomi/lifecycle/BundleWatcher.java b/lifecycle-watcher/src/main/java/org/apache/unomi/lifecycle/BundleWatcherImpl.java
similarity index 71%
copy from lifecycle-watcher/src/main/java/org/apache/unomi/lifecycle/BundleWatcher.java
copy to lifecycle-watcher/src/main/java/org/apache/unomi/lifecycle/BundleWatcherImpl.java
index 31eec3b..b57fcfa 100644
--- a/lifecycle-watcher/src/main/java/org/apache/unomi/lifecycle/BundleWatcher.java
+++ b/lifecycle-watcher/src/main/java/org/apache/unomi/lifecycle/BundleWatcherImpl.java
@@ -16,6 +16,8 @@
  */
 package org.apache.unomi.lifecycle;
 
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.ServerInfo;
 import org.osgi.framework.*;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -24,12 +26,8 @@ import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.net.URL;
-import java.util.ArrayList;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TimerTask;
+import java.text.MessageFormat;
+import java.util.*;
 import java.util.concurrent.*;
 import java.util.stream.Collectors;
 
@@ -37,9 +35,9 @@ import java.util.stream.Collectors;
  * This class listens to the global Apache Unomi bundle lifecycle, to provide statistics and state of the overall
  * system. It notably displays messages for successfull or unsuccessfull startups as well as startup times.
  */
-public class BundleWatcher implements SynchronousBundleListener, ServiceListener {
+public class BundleWatcherImpl implements SynchronousBundleListener, ServiceListener, BundleWatcher {
 
-    private static final Logger logger = LoggerFactory.getLogger(BundleWatcher.class.getName());
+    private static final Logger logger = LoggerFactory.getLogger(BundleWatcherImpl.class.getName());
 
     private long startupTime;
     private Map<String, Boolean> requiredBundles = new ConcurrentHashMap<>();
@@ -58,6 +56,8 @@ public class BundleWatcher implements SynchronousBundleListener, ServiceListener
 
     private Integer checkStartupStateRefreshInterval = 60;
 
+    private List<ServerInfo> serverInfos = new ArrayList<>();
+
     public void setRequiredBundles(Map<String, Boolean> requiredBundles) {
         this.requiredBundles = new ConcurrentHashMap<>(requiredBundles);
     }
@@ -88,12 +88,16 @@ public class BundleWatcher implements SynchronousBundleListener, ServiceListener
         checkExistingBundles();
         bundleContext.addBundleListener(this);
         bundleContext.addServiceListener(this);
-        loadLogo();
         startupTime = System.currentTimeMillis();
         System.out.println("Initializing Unomi...");
         logger.info("Bundle watcher initialized.");
     }
 
+    @Override
+    public List<ServerInfo> getServerInfos() {
+        return serverInfos;
+    }
+
     private boolean allBundleStarted() {
         return getInactiveBundles().isEmpty();
     }
@@ -118,6 +122,8 @@ public class BundleWatcher implements SynchronousBundleListener, ServiceListener
     }
 
     public void checkExistingBundles() {
+        serverInfos.clear();
+        serverInfos.add(getBundleServerInfo(bundleContext.getBundle())); // make sure the first server info is the default one
         for (Bundle bundle : bundleContext.getBundles()) {
             if (bundle.getSymbolicName().startsWith("org.apache.unomi") && requiredBundles.containsKey(bundle.getSymbolicName())) {
                 if (bundle.getState() == Bundle.ACTIVE) {
@@ -126,6 +132,12 @@ public class BundleWatcher implements SynchronousBundleListener, ServiceListener
                     requiredBundles.put(bundle.getSymbolicName(), false);
                 }
             }
+            if (!bundle.getSymbolicName().equals(bundleContext.getBundle().getSymbolicName())) {
+                ServerInfo serverInfo = getBundleServerInfo(bundle);
+                if (serverInfo != null) {
+                    serverInfos.add(serverInfo);
+                }
+            }
         }
         checkStartupComplete();
     }
@@ -233,39 +245,44 @@ public class BundleWatcher implements SynchronousBundleListener, ServiceListener
         }
         if (!startupMessageAlreadyDisplayed) {
             long totalStartupTime = System.currentTimeMillis() - startupTime;
-            if (!logoLines.isEmpty()) {
+
+            List<String> logoLines = serverInfos.get(serverInfos.size()-1).getLogoLines();
+            if (logoLines != null && !logoLines.isEmpty()) {
                 logoLines.forEach(System.out::println);
             }
-            String buildNumber = "n/a";
-            if (bundleContext.getBundle().getHeaders().get("Implementation-Build") != null) {
-                buildNumber = bundleContext.getBundle().getHeaders().get("Implementation-Build");
-            }
-            String timestamp = "n/a";
-            if (bundleContext.getBundle().getHeaders().get("Implementation-TimeStamp") != null) {
-                timestamp = bundleContext.getBundle().getHeaders().get("Implementation-TimeStamp");
-            }
-            String versionMessage =
-                    "  " + bundleContext.getBundle().getVersion().toString() + "  Build:" + buildNumber + "  Timestamp:" + timestamp;
-            System.out.println(versionMessage);
-            System.out.println("--------------------------------------------------------------------------");
+            System.out.println("--------------------------------------------------------------------------------------------");
+            serverInfos.forEach(serverInfo -> {
+                String versionMessage = MessageFormat.format(" {0} {1} ({2,date,yyyy-MM-dd HH:mm:ssZ} // {3} // {4} // {5}) ",
+                        StringUtils.rightPad(serverInfo.getServerIdentifier(), 12, " "),
+                        serverInfo.getServerVersion(),
+                        serverInfo.getServerBuildDate(),
+                        serverInfo.getServerTimestamp(),
+                        serverInfo.getServerScmBranch(),
+                        serverInfo.getServerBuildNumber()
+                        );
+                System.out.println(versionMessage);
+                logger.info(versionMessage);
+            });
+            System.out.println("--------------------------------------------------------------------------------------------");
             System.out.println(
-                    "Successfully started " + requiredBundles.size() + " bundles and " + requiredServicesFilters.size() + " " + "required "
+                    "Server successfully started " + requiredBundles.size() + " bundles and " + requiredServicesFilters.size() + " required "
                             + "services in " + totalStartupTime + " ms");
-            logger.info("Apache Unomi version: {}", versionMessage);
-            logger.info("Apache Unomi successfully started {} bundles and {} required services in {} ms", requiredBundles.size(),
+            logger.info("Server successfully started {} bundles and {} required services in {} ms", requiredBundles.size(),
                     requiredServicesFilters.size(), totalStartupTime);
             startupMessageAlreadyDisplayed = true;
             shutdownMessageAlreadyDisplayed = false;
         }
     }
 
+    @Override
     public boolean isStartupComplete() {
         return allBundleStarted() && matchedRequiredServicesCount == requiredServicesFilters.size();
     }
 
-    private void loadLogo() {
-        URL logoURL = bundleContext.getBundle().getResource("logo.txt");
+    private List<String> loadLogo(Bundle bundle) {
+        URL logoURL = bundle.getResource("unomi-logo.txt");
         if (logoURL != null) {
+            List<String> logoLines = new ArrayList<>();
             BufferedReader bufferedReader = null;
             try {
                 bufferedReader = new BufferedReader(new InputStreamReader(logoURL.openStream()));
@@ -275,6 +292,7 @@ public class BundleWatcher implements SynchronousBundleListener, ServiceListener
                         logoLines.add(line);
                     }
                 }
+                return logoLines;
             } catch (IOException e) {
                 logger.error("Error loading logo lines", e);
             } finally {
@@ -286,6 +304,52 @@ public class BundleWatcher implements SynchronousBundleListener, ServiceListener
                 }
             }
         }
+        return null;
+    }
+
+    public ServerInfo getBundleServerInfo(Bundle bundle) {
+        String serverIdentifier = "n/a";
+        if (bundle.getHeaders().get("Implementation-ServerIdentifier") != null) {
+            serverIdentifier = bundle.getHeaders().get("Implementation-ServerIdentifier");
+        } else {
+            return null;
+        }
+        List<String> logoLines = loadLogo(bundle);
+        String buildNumber = "n/a";
+        if (bundle.getHeaders().get("Implementation-Build") != null) {
+            buildNumber = bundle.getHeaders().get("Implementation-Build");
+        }
+        String timestamp = "n/a";
+        Date buildDate = null;
+        if (bundle.getHeaders().get("Implementation-TimeStamp") != null) {
+            timestamp = bundle.getHeaders().get("Implementation-TimeStamp");
+            try {
+                buildDate = new Date(Long.parseLong(timestamp));
+            } catch (Throwable t) {
+                // we simply ignore this exception and keep the timestamp as it is
+            }
+        }
+        String scmBranch = "n/a";
+        if (bundle.getHeaders().get("Implementation-ScmBranch") != null) {
+            scmBranch = bundle.getHeaders().get("Implementation-ScmBranch");
+        }
+        if (bundle.getHeaders().get("Implementation-UnomiEventTypes") != null) {
+            // to be implemented
+        }
+        if (bundle.getHeaders().get("Implementation-UnomiCapabilities") != null) {
+            // to be implemented
+        }
+        ServerInfo serverInfo = new ServerInfo();
+        serverInfo.setServerIdentifier(serverIdentifier);
+        serverInfo.setServerVersion(bundle.getVersion().toString());
+        serverInfo.setServerBuildNumber(buildNumber);
+        serverInfo.setServerBuildDate(buildDate);
+        serverInfo.setServerTimestamp(timestamp);
+        serverInfo.setServerScmBranch(scmBranch);
+        if (logoLines != null) {
+            serverInfo.setLogoLines(logoLines);
+        }
+        return serverInfo;
     }
 
 }
diff --git a/lifecycle-watcher/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/lifecycle-watcher/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index 1bb5a53..050a2ae 100644
--- a/lifecycle-watcher/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/lifecycle-watcher/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -30,7 +30,7 @@
         </cm:default-properties>
     </cm:property-placeholder>
 
-    <bean id="bundleWatcher" init-method="init" destroy-method="destroy" class="org.apache.unomi.lifecycle.BundleWatcher">
+    <bean id="bundleWatcherImpl" init-method="init" destroy-method="destroy" class="org.apache.unomi.lifecycle.BundleWatcherImpl">
         <property name="bundleContext" ref="blueprintBundleContext"/>
         <property name="requiredServices" value="${lifecycle.requiredServices}" />
         <property name="checkStartupStateRefreshInterval" value="${lifecycle.checkStartupState.refresh.interval}"/>
@@ -67,7 +67,7 @@
         </property>
     </bean>
 
-    <service id="bundleWatcherService" ref="bundleWatcher">
+    <service id="bundleWatcherService" ref="bundleWatcherImpl">
         <interfaces>
             <value>org.apache.unomi.lifecycle.BundleWatcher</value>
         </interfaces>
diff --git a/lifecycle-watcher/src/main/resources/logo.txt b/lifecycle-watcher/src/main/resources/unomi-logo.txt
similarity index 93%
rename from lifecycle-watcher/src/main/resources/logo.txt
rename to lifecycle-watcher/src/main/resources/unomi-logo.txt
index 0db2b87..1a509ad 100644
--- a/lifecycle-watcher/src/main/resources/logo.txt
+++ b/lifecycle-watcher/src/main/resources/unomi-logo.txt
@@ -22,5 +22,3 @@
        |    |  /   |  (  <_> )  Y Y  \  |
        |______/|___|  /\____/|__|_|  /__|
                     \/             \/
-
---------------------------------------------------------------------------
\ No newline at end of file
diff --git a/services/src/main/java/org/apache/unomi/services/impl/schemas/UnomiPropertyTypeKeyword.java b/services/src/main/java/org/apache/unomi/services/impl/schemas/UnomiPropertyTypeKeyword.java
index 4c5796d..2375e7b 100644
--- a/services/src/main/java/org/apache/unomi/services/impl/schemas/UnomiPropertyTypeKeyword.java
+++ b/services/src/main/java/org/apache/unomi/services/impl/schemas/UnomiPropertyTypeKeyword.java
@@ -33,7 +33,7 @@ class UnomiPropertyTypeKeyword extends AbstractKeyword {
     private final ProfileService profileService;
     private final SchemaRegistryImpl schemaRegistry;
 
-    private static final class PropertyTypeJsonValidator extends AbstractJsonValidator {
+    private static final class UnomiPropertyTypeJsonValidator extends AbstractJsonValidator {
 
         String schemaPath;
         JsonNode schemaNode;
@@ -42,7 +42,7 @@ class UnomiPropertyTypeKeyword extends AbstractKeyword {
         ProfileService profileService;
         SchemaRegistryImpl schemaRegistry;
 
-        public PropertyTypeJsonValidator(String keyword, String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, ProfileService profileService, SchemaRegistryImpl schemaRegistry) {
+        public UnomiPropertyTypeJsonValidator(String keyword, String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, ProfileService profileService, SchemaRegistryImpl schemaRegistry) {
             super(keyword);
             this.schemaPath = schemaPath;
             this.schemaNode = schemaNode;
@@ -105,6 +105,6 @@ class UnomiPropertyTypeKeyword extends AbstractKeyword {
 
     @Override
     public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception {
-        return new PropertyTypeJsonValidator(this.getValue(), schemaPath, schemaNode, parentSchema, validationContext, profileService, schemaRegistry);
+        return new UnomiPropertyTypeJsonValidator(this.getValue(), schemaPath, schemaNode, parentSchema, validationContext, profileService, schemaRegistry);
     }
 }
diff --git a/services/src/main/resources/META-INF/cxs/schemas/events/view.json b/services/src/main/resources/META-INF/cxs/schemas/events/view.json
index a85188a..1840449 100644
--- a/services/src/main/resources/META-INF/cxs/schemas/events/view.json
+++ b/services/src/main/resources/META-INF/cxs/schemas/events/view.json
@@ -14,7 +14,6 @@
   "properties" : {
     "properties" : {
       "type" : "object",
-      "unomiPropertyTypes" : [ "events" ],
       "maxProperties": 50
     },
     "source" : {