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/11 09:56:10 UTC

[unomi] branch master updated: UNOMI-554 Improve server identification & various bug fixes (#392)

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

shuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/master by this push:
     new 0cf8420  UNOMI-554 Improve server identification & various bug fixes (#392)
0cf8420 is described below

commit 0cf8420e75db1998a6ef02ca67983584f18e4c29
Author: Serge Huber <sh...@jahia.com>
AuthorDate: Fri Mar 11 10:56:06 2022 +0100

    UNOMI-554 Improve server identification & various bug fixes (#392)
    
    - 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" : {