From 8de31dc4fd642f67f79963f1a28d172d26e00a06 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Fri, 8 Dec 2023 17:42:31 +0100
Subject: [PATCH 1/7] Ajout de sava-core-jakarta-0.0.1-SNAPSHOT

---
 www-server/pom.xml | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/www-server/pom.xml b/www-server/pom.xml
index 4232369..309a5fb 100644
--- a/www-server/pom.xml
+++ b/www-server/pom.xml
@@ -16,6 +16,7 @@
 
   <properties>
     <mockito-core.version>5.5.0</mockito-core.version>
+    <sava.version>0.0.1-SNAPSHOT</sava.version>
     <swagger.version>2.2.19</swagger.version>
     <checkstyle.config.location>file://${basedir}/../config/sun_checks.xml</checkstyle.config.location>
   </properties>
@@ -154,6 +155,12 @@
       <artifactId>postgresql</artifactId>
       <version>42.7.0</version>
     </dependency>
+    <!-- SAVA -->
+    <dependency>
+      <groupId>fr.inrae.agroclim</groupId>
+      <artifactId>sava-core-jakarta</artifactId>
+      <version>${sava.version}</version>
+    </dependency>
     <!-- Tests -->
     <dependency>
       <groupId>com.h2database</groupId>
-- 
GitLab


From cdabfd03cbf1892b1636142c783780d5c5160e4a Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Fri, 8 Dec 2023 17:42:31 +0100
Subject: [PATCH 2/7] Ajout de sava-core-jakarta-0.0.1-SNAPSHOT

---
 www-server/pom.xml | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/www-server/pom.xml b/www-server/pom.xml
index 59c6abd..c153b7c 100644
--- a/www-server/pom.xml
+++ b/www-server/pom.xml
@@ -16,6 +16,7 @@
 
   <properties>
     <mockito-core.version>5.5.0</mockito-core.version>
+    <sava.version>0.0.1-SNAPSHOT</sava.version>
     <swagger.version>2.2.19</swagger.version>
     <checkstyle.config.location>file://${basedir}/../config/sun_checks.xml</checkstyle.config.location>
   </properties>
@@ -154,6 +155,12 @@
       <artifactId>postgresql</artifactId>
       <version>42.7.0</version>
     </dependency>
+    <!-- SAVA -->
+    <dependency>
+      <groupId>fr.inrae.agroclim</groupId>
+      <artifactId>sava-core-jakarta</artifactId>
+      <version>${sava.version}</version>
+    </dependency>
     <!-- Tests -->
     <dependency>
       <groupId>com.h2database</groupId>
-- 
GitLab


From 30c866f8b69ccc70a434e5f150faf4997b3ec71b Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 14 Dec 2023 10:58:06 +0100
Subject: [PATCH 3/7] Ajouter MetricsBasicAuthServlet. refs #38

---
 .../fr/agrometinfo/www/server/MetricsServlet.java | 15 +++++++++++++++
 1 file changed, 15 insertions(+)
 create mode 100644 www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java

diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java b/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
new file mode 100644
index 0000000..c2aa07f
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
@@ -0,0 +1,15 @@
+package fr.agrometinfo.www.server;
+
+import fr.agroclim.sava.core.MetricsBasicAuthServlet;
+import jakarta.servlet.annotation.WebServlet;
+import lombok.extern.log4j.Log4j2;
+
+/**
+ * Servlet to using SAVA to provide metrics.
+ */
+@Log4j2
+@WebServlet("/metrics")
+public class MetricsServlet extends MetricsBasicAuthServlet  {
+    private static final long serialVersionUID = 1003353995341179826L;
+
+}
-- 
GitLab


From 56a19bb6648f0d291a1a200af1266836b89da75c Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 14 Dec 2023 10:59:53 +0100
Subject: [PATCH 4/7] Ajouter MetricsBasicAuthServlet. refs #38

---
 .../main/java/fr/agrometinfo/www/server/MetricsServlet.java    | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java b/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
index c2aa07f..dc5adf7 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
@@ -2,14 +2,11 @@ package fr.agrometinfo.www.server;
 
 import fr.agroclim.sava.core.MetricsBasicAuthServlet;
 import jakarta.servlet.annotation.WebServlet;
-import lombok.extern.log4j.Log4j2;
 
 /**
  * Servlet to using SAVA to provide metrics.
  */
-@Log4j2
 @WebServlet("/metrics")
 public class MetricsServlet extends MetricsBasicAuthServlet  {
     private static final long serialVersionUID = 1003353995341179826L;
-
 }
-- 
GitLab


From f73c345ebd44899fe87ed3e5b48ec72836193d3a Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 14 Dec 2023 16:55:34 +0100
Subject: [PATCH 5/7] =?UTF-8?q?Ajouter=20les=20informations=20JVM=20et=20T?=
 =?UTF-8?q?omcat=20dans=20les=20m=C3=A9triques.=20refs=20#38?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../www/server/MetricsServlet.java            | 38 +++++++++++++++++++
 1 file changed, 38 insertions(+)

diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java b/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
index dc5adf7..63b91da 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
@@ -1,12 +1,50 @@
 package fr.agrometinfo.www.server;
 
+import java.io.IOException;
+
 import fr.agroclim.sava.core.MetricsBasicAuthServlet;
+import fr.agroclim.sava.core.SavaUtils;
+import jakarta.servlet.ServletException;
 import jakarta.servlet.annotation.WebServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 
 /**
  * Servlet to using SAVA to provide metrics.
  */
 @WebServlet("/metrics")
 public class MetricsServlet extends MetricsBasicAuthServlet  {
+    /**
+     * UID.
+     */
     private static final long serialVersionUID = 1003353995341179826L;
+
+    @Override
+    protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
+        final Runtime runtime = Runtime.getRuntime();
+        SavaUtils.setGaugeValue("jvm_max_memory", (double) runtime.maxMemory());
+        SavaUtils.setGaugeValue("jvm_used_memory", (double) (runtime.totalMemory() - runtime.freeMemory()));
+        SavaUtils.setGaugeValue("jvm_total_memory", (double) runtime.totalMemory());
+        SavaUtils.setGaugeValue("jvm_free_memory", (double) runtime.freeMemory());
+        super.doGet(req, resp);
+    }
+
+    @Override
+    public void init() throws ServletException {
+        super.init();
+        SavaUtils.addCounter("jvm_vendor", "Vendor name of JVM", "", "vendor");
+        SavaUtils.addCounter("jvm_version", "Version number of JVM", "", "version");
+        SavaUtils.addCounter("tomcat_version", "Version number of Tomcat", "", "version");
+        SavaUtils.incrementCounter("jvm_vendor", System.getProperty("java.vm.name"));
+        SavaUtils.incrementCounter("jvm_version", System.getProperty("java.vm.version"));
+        final String info = getServletContext().getServerInfo();
+        SavaUtils.incrementCounter("tomcat_version", info);
+        SavaUtils.addGauge("jvm_max_memory",
+                "The maximum amount of memory that the virtual machine will attempt to use, measured in bytes");
+        SavaUtils.addGauge("jvm_used_memory", "The amount of memory that the virtual machine uses, measured in bytes");
+        SavaUtils.addGauge("jvm_total_memory",
+                "The total amount of memory in the Java virtual machine, measured in bytes");
+        SavaUtils.addGauge("jvm_free_memory",
+                "The amount of free memory in the Java Virtual Machine, measured in bytes");
+    }
 }
-- 
GitLab


From 53a72d67701fdca15939bd00f9189dfb63acc6d0 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Fri, 15 Dec 2023 08:56:43 +0100
Subject: [PATCH 6/7] =?UTF-8?q?informations=20JVM=20et=20Tomcat=20renvoy?=
 =?UTF-8?q?=C3=A9es=20par=20d=C3=A9faut=20par=20SAVA.=20refs=20#35?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../www/server/MetricsServlet.java            | 35 -------------------
 1 file changed, 35 deletions(-)

diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java b/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
index 63b91da..656c170 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
@@ -1,13 +1,7 @@
 package fr.agrometinfo.www.server;
 
-import java.io.IOException;
-
 import fr.agroclim.sava.core.MetricsBasicAuthServlet;
-import fr.agroclim.sava.core.SavaUtils;
-import jakarta.servlet.ServletException;
 import jakarta.servlet.annotation.WebServlet;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
 
 /**
  * Servlet to using SAVA to provide metrics.
@@ -18,33 +12,4 @@ public class MetricsServlet extends MetricsBasicAuthServlet  {
      * UID.
      */
     private static final long serialVersionUID = 1003353995341179826L;
-
-    @Override
-    protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
-        final Runtime runtime = Runtime.getRuntime();
-        SavaUtils.setGaugeValue("jvm_max_memory", (double) runtime.maxMemory());
-        SavaUtils.setGaugeValue("jvm_used_memory", (double) (runtime.totalMemory() - runtime.freeMemory()));
-        SavaUtils.setGaugeValue("jvm_total_memory", (double) runtime.totalMemory());
-        SavaUtils.setGaugeValue("jvm_free_memory", (double) runtime.freeMemory());
-        super.doGet(req, resp);
-    }
-
-    @Override
-    public void init() throws ServletException {
-        super.init();
-        SavaUtils.addCounter("jvm_vendor", "Vendor name of JVM", "", "vendor");
-        SavaUtils.addCounter("jvm_version", "Version number of JVM", "", "version");
-        SavaUtils.addCounter("tomcat_version", "Version number of Tomcat", "", "version");
-        SavaUtils.incrementCounter("jvm_vendor", System.getProperty("java.vm.name"));
-        SavaUtils.incrementCounter("jvm_version", System.getProperty("java.vm.version"));
-        final String info = getServletContext().getServerInfo();
-        SavaUtils.incrementCounter("tomcat_version", info);
-        SavaUtils.addGauge("jvm_max_memory",
-                "The maximum amount of memory that the virtual machine will attempt to use, measured in bytes");
-        SavaUtils.addGauge("jvm_used_memory", "The amount of memory that the virtual machine uses, measured in bytes");
-        SavaUtils.addGauge("jvm_total_memory",
-                "The total amount of memory in the Java virtual machine, measured in bytes");
-        SavaUtils.addGauge("jvm_free_memory",
-                "The amount of free memory in the Java Virtual Machine, measured in bytes");
-    }
 }
-- 
GitLab


From 00c6df1f8b89538cd2a9b3a1af63414c8e7fbe0c Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Fri, 15 Dec 2023 11:13:05 +0100
Subject: [PATCH 7/7] =?UTF-8?q?Ajouter=20une=20m=C3=A9trique=20sur=20la=20?=
 =?UTF-8?q?taille=20du=20sch=C3=A9ma.=20refs=20#38?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../www/server/MetricsServlet.java            |   2 +
 .../www/server/dao/DaoHibernate.java          |  90 +++++++++++-
 .../www/server/dao/IndicatorDao.java          |  10 ++
 .../www/server/dao/IndicatorDaoHibernate.java |   8 +-
 .../scheduled/BackgroundJobManager.java       | 130 ++++++++++++++++++
 .../www/server/scheduled/package-info.java    |   4 +
 6 files changed, 242 insertions(+), 2 deletions(-)
 create mode 100644 www-server/src/main/java/fr/agrometinfo/www/server/scheduled/BackgroundJobManager.java
 create mode 100644 www-server/src/main/java/fr/agrometinfo/www/server/scheduled/package-info.java

diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java b/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
index 656c170..ef33406 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/MetricsServlet.java
@@ -1,5 +1,6 @@
 package fr.agrometinfo.www.server;
 
+
 import fr.agroclim.sava.core.MetricsBasicAuthServlet;
 import jakarta.servlet.annotation.WebServlet;
 
@@ -12,4 +13,5 @@ public class MetricsServlet extends MetricsBasicAuthServlet  {
      * UID.
      */
     private static final long serialVersionUID = 1003353995341179826L;
+
 }
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java
index 5c33432..dc1e1b7 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java
@@ -1,9 +1,14 @@
 package fr.agrometinfo.www.server.dao;
 
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
+import org.hibernate.engine.spi.SessionImplementor;
+
 import jakarta.persistence.NoResultException;
 import jakarta.persistence.Query;
 import jakarta.persistence.TypedQuery;
@@ -17,6 +22,37 @@ import lombok.extern.log4j.Log4j2;
  */
 @Log4j2
 public abstract class DaoHibernate<T> {
+    /**
+     * Get the class name of JDBC driver.
+     *
+     * @param em Entity manager
+     * @return name of JDBC driver
+     */
+    protected static final String getDriver(final ScopedEntityManager em) {
+        final SessionImplementor sessionImp = (SessionImplementor) em.getDelegate();
+        try {
+            final Connection conn = sessionImp.getJdbcConnectionAccess().obtainConnection();
+            return conn.getMetaData().getDriverName();
+        } catch (final SQLException e) {
+            return "";
+        }
+    }
+
+    /**
+     * Checks if the database is connected by a PostgreSQL, useful to call specific
+     * SQL.
+     *
+     * @param em Entity manager
+     * @return if the SGBD is PostgreSQL
+     */
+    protected static final boolean isPostgreSQLDriver(final ScopedEntityManager em) {
+        final var driverName = getDriver(em);
+        if (driverName == null || driverName.isBlank()) {
+            return false;
+        }
+        return driverName.startsWith("PostgreSQL");
+    }
+
     /**
      * Related class.
      */
@@ -125,13 +161,34 @@ public abstract class DaoHibernate<T> {
         }
     }
 
+    /**
+     * Find the first result.
+     *
+     * @param <U> entity class
+     * @param sql SQL statement
+     * @param params parameters for the JPQL statement
+     * @return the found entity or null if not entity matches
+     */
+    @SuppressWarnings("unchecked")
+    protected final <U> U findFirstScalarWithNamedParameters(final String sql, final Map<String, Object> params) {
+        try (ScopedEntityManager em = getScopedEntityManager()) {
+            final Query query = em.createNativeQuery(sql);
+            if (params != null) {
+                params.forEach(query::setParameter);
+            }
+            return (U) query.getSingleResult();
+        } catch (final NoResultException e) {
+            return null;
+        }
+    }
+
     /**
      * Find the first result.
      *
      * @param <U>    entity class
      * @param stmt   Jakarta Persistence Query Language statement
      * @param params parameters for the JPQL statement
-     * @param clazz2  entity class
+     * @param clazz2 entity class
      * @return the found entity or null if not entity matches
      */
     protected final <U> U findOneByJPQL(final String stmt, final Map<String, Object> params, final Class<U> clazz2) {
@@ -146,6 +203,37 @@ public abstract class DaoHibernate<T> {
         }
     }
 
+    /**
+     * @return name of current schema.
+     */
+    public String getSchemaName() {
+        final String sql = """
+                SELECT table_schema FROM information_schema.tables WHERE LOWER(table_name) = 'indicator'
+                """;
+        return findFirstScalar(sql, null);
+    }
+
+    /**
+     * See https://www.postgresql.org/docs/current/functions-admin.html.
+     *
+     * @param schema schema name (usually public)
+     * @return size of all content of a schema, in bytes
+     */
+    public BigDecimal getSchemaSize(final String schema) {
+        try (ScopedEntityManager em = getScopedEntityManager()) {
+            if (isPostgreSQLDriver(em)) {
+                final String sql = "SELECT "
+                        + "SUM(pg_total_relation_size(CONCAT(quote_ident(schemaname), '.', quote_ident(tablename)))) "
+                        + "FROM pg_catalog.pg_tables WHERE schemaname=:schema";
+                final Query query = em.createNativeQuery(sql);
+                query.setParameter("schema", schema);
+                return (BigDecimal) query.getSingleResult();
+            } else {
+                throw new UnsupportedOperationException("Database driver not handled! " + getDriver(em));
+            }
+        }
+    }
+
     /**
      * @return Proxy'ed EntityManager to handle transactions.
      */
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDao.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDao.java
index baf6581..03b4d9c 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDao.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDao.java
@@ -1,5 +1,6 @@
 package fr.agrometinfo.www.server.dao;
 
+import java.math.BigDecimal;
 import java.util.List;
 
 import fr.agrometinfo.www.server.model.Indicator;
@@ -23,4 +24,13 @@ public interface IndicatorDao {
      */
     Indicator findByCodeAndPeriod(String code, String periodCode);
 
+    /**
+     * @return size of all content of the current schema, in bytes
+     */
+    BigDecimal getCurrentSchemaSize();
+
+    /**
+     * @return name of current schema.
+     */
+    String getSchemaName();
 }
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDaoHibernate.java
index edeec7a..feec093 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDaoHibernate.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/IndicatorDaoHibernate.java
@@ -1,5 +1,6 @@
 package fr.agrometinfo.www.server.dao;
 
+import java.math.BigDecimal;
 import java.util.Map;
 
 import fr.agrometinfo.www.server.model.Indicator;
@@ -29,9 +30,14 @@ public class IndicatorDaoHibernate extends DaoHibernate<Indicator> implements In
      * @return found or null
      */
     @Override
-    public Indicator findByCodeAndPeriod(@NotNull final String code, @NotNull final String periodCode) {
+    public final Indicator findByCodeAndPeriod(@NotNull final String code, @NotNull final String periodCode) {
         final var jpql = "SELECT t FROM Indicator AS t WHERE t.code=:code AND t.period.code=:periodCode";
         return super.findOneByJPQL(jpql, Map.of("code", code, "periodCode", periodCode), Indicator.class);
     }
 
+    @Override
+    public final BigDecimal getCurrentSchemaSize() {
+        return super.getSchemaSize(super.getSchemaName());
+    }
+
 }
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/scheduled/BackgroundJobManager.java b/www-server/src/main/java/fr/agrometinfo/www/server/scheduled/BackgroundJobManager.java
new file mode 100644
index 0000000..ff82b7a
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/scheduled/BackgroundJobManager.java
@@ -0,0 +1,130 @@
+package fr.agrometinfo.www.server.scheduled;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import fr.agroclim.sava.core.SavaUtils;
+import fr.agrometinfo.www.server.dao.IndicatorDao;
+import fr.agrometinfo.www.server.dao.PersistenceManager;
+import jakarta.inject.Inject;
+import jakarta.servlet.ServletContextEvent;
+import jakarta.servlet.ServletContextListener;
+import jakarta.servlet.annotation.WebListener;
+import lombok.extern.log4j.Log4j2;
+
+/**
+ * Manager for scheduled tasks.
+ *
+ * Full JavaEE simply uses javax.ejb.Schedule annotation.
+ *
+ * @author Olivier Maury
+ */
+@WebListener
+@Log4j2
+public class BackgroundJobManager implements ServletContextListener {
+
+    /**
+     * Number of seconds in a day.
+     */
+    private static final int DAY_IN_SECONDS = 60 * 60 * 24;
+
+    /**
+     * Number of seconds in an hour.
+     */
+    private static final int HOUR_IN_SECONDS = 60 * 60;
+
+    /**
+     * @param hour   the hour-of-day of delay, from 0 to 23
+     * @param minute the minute-of-hour of delay, from 0 to 59
+     * @param second the second-of-minute of delay, from 0 to 59
+     * @return delays in seconds until the next hour:minute
+     */
+    static long initialDelay(final int hour, final int minute, final int second) {
+        LocalDateTime next = LocalDateTime.of(LocalDate.now(), LocalTime.of(hour, minute, second));
+        final LocalDateTime now = LocalDateTime.now();
+        if (next.isBefore(now)) {
+            next = next.plusDays(1);
+        }
+        return Duration.between(now, next).getSeconds();
+    }
+
+    /**
+     * DAO providing database details.
+     */
+    @Inject
+    private IndicatorDao indicatorDao;
+
+    /**
+     * Scheduler for tasks.
+     */
+    private ScheduledExecutorService scheduler;
+
+    @Override
+    public final void contextDestroyed(final ServletContextEvent event) {
+        LOGGER.traceEntry();
+        PersistenceManager.getInstance().closeEntityManagerFactory();
+        scheduler.shutdownNow();
+    }
+
+    @Override
+    public final void contextInitialized(final ServletContextEvent event) {
+        scheduler = Executors.newSingleThreadScheduledExecutor();
+        scheduleEveryPeriod(this::updateHourlyMetrics, HOUR_IN_SECONDS);
+        scheduleEveryDayAt(this::updateHourlyMetrics, 0, 0, 0);
+        initMetrics();
+        updateHourlyMetrics();
+    }
+
+    /**
+     * Define metrics using SAVA before use.
+     */
+    private void initMetrics() {
+        SavaUtils.addGauge("schema_size", "Database schema size, in bytes", "schema_name");
+    }
+
+    /**
+     * Creates and executes a periodic action.
+     *
+     * @param runnable
+     *            the task to execute
+     * @param hour
+     *            the hour of schedule
+     * @param minute
+     *            the minute of schedule
+     * @param second
+     *            the second of schedule
+     */
+    private void scheduleEveryDayAt(final Runnable runnable, final int hour, final int minute, final int second) {
+        final long initDelay = initialDelay(hour, minute, second);
+        LOGGER.trace("Scheduling {} after {}s at {}:{}:{}", runnable.getClass().getCanonicalName(), initDelay, hour,
+                minute, second);
+        scheduler.scheduleAtFixedRate(runnable, initDelay, DAY_IN_SECONDS, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Creates and executes a periodic action after waiting one period.
+     *
+     * @param runnable
+     *            the task to execute
+     * @param period
+     *            period between 2 runs (in seconds)
+     */
+    private void scheduleEveryPeriod(final Runnable runnable, final int period) {
+        LOGGER.trace("Scheduling {} after {}s.", runnable.getClass().getCanonicalName(), period);
+        scheduler.scheduleAtFixedRate(runnable, period, period, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Update metrics using SAVA.
+     */
+    private void updateHourlyMetrics() {
+        final var schemaName = indicatorDao.getSchemaName();
+        final var schemaSize = indicatorDao.getCurrentSchemaSize();
+        SavaUtils.setGaugeValue("schema_size", schemaSize.doubleValue(), schemaName);
+    }
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/scheduled/package-info.java b/www-server/src/main/java/fr/agrometinfo/www/server/scheduled/package-info.java
new file mode 100644
index 0000000..98d6cde
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/scheduled/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Scheduled jobs.
+ */
+package fr.agrometinfo.www.server.scheduled;
-- 
GitLab