From 56f6b5fb5ad4540d77bb24b356270d5c941c529c Mon Sep 17 00:00:00 2001
From: Joao Afonso <joao.afonso@cern.ch>
Date: Fri, 28 Mar 2025 15:44:56 +0100
Subject: [PATCH 1/3] Add option in cta-admin to identify dual tape files that
 only have one copy - min age

---
 catalogue/TapeSearchCriteria.hpp              |  5 ++++
 catalogue/rdbms/RdbmsTapeCatalogue.cpp        |  7 +++++-
 cmdline/CtaAdminCmdParse.hpp                  |  4 +--
 .../configmaps/cta-frontend-xrootd.yaml       |  5 ++++
 .../orchestration/helm/frontend/values.yaml   |  2 ++
 frontend/common/AdminCmd.cpp                  |  3 ++-
 frontend/common/AdminCmd.hpp                  | 25 +++++++++++++------
 frontend/common/FrontendService.cpp           | 14 +++++++++++
 frontend/common/FrontendService.hpp           |  6 +++++
 frontend/grpc/RequestMessage.hpp              | 11 +++++---
 frontend/grpc/ServerTapeLsRequestHandler.cpp  |  4 +--
 frontend/grpc/cta-frontend-grpc.conf.example  |  4 +++
 xroot_plugins/XrdCtaTapeLs.hpp                |  3 +++
 .../cta-frontend-xrootd.conf.example          |  5 ++++
 14 files changed, 82 insertions(+), 16 deletions(-)

diff --git a/catalogue/TapeSearchCriteria.hpp b/catalogue/TapeSearchCriteria.hpp
index 05160b6b2e..61cc0101e6 100644
--- a/catalogue/TapeSearchCriteria.hpp
+++ b/catalogue/TapeSearchCriteria.hpp
@@ -100,6 +100,11 @@ struct TapeSearchCriteria {
    */
   std::optional<bool> checkMissingFileCopies;
 
+  /**
+   * Missing tape file copies minimum age in secs
+   */
+  uint64_t missingFileCopiesMinAgeSecs;
+
   /**
    * The state of the tapes to look for
    */
diff --git a/catalogue/rdbms/RdbmsTapeCatalogue.cpp b/catalogue/rdbms/RdbmsTapeCatalogue.cpp
index 308a297ca2..39856975be 100644
--- a/catalogue/rdbms/RdbmsTapeCatalogue.cpp
+++ b/catalogue/rdbms/RdbmsTapeCatalogue.cpp
@@ -1443,7 +1443,7 @@ std::list<common::dataStructures::Tape> RdbmsTapeCatalogue::getTapes(rdbms::Conn
           WHERE
             SC.NB_COPIES > 1
           GROUP BY
-            AF.ARCHIVE_FILE_ID, SC.NB_COPIES
+            AF.ARCHIVE_FILE_ID, SC.NB_COPIES AND AF.CREATION_TIME < :MAX_CREATION_TIME
           HAVING
             SC.NB_COPIES <> COUNT(TF.ARCHIVE_FILE_ID)
         ) MISSING_COPIES
@@ -1474,6 +1474,11 @@ std::list<common::dataStructures::Tape> RdbmsTapeCatalogue::getTapes(rdbms::Conn
     throw cta::exception::UserError(std::string("The state provided does not exist. Possible values are: ")
       + cta::common::dataStructures::Tape::getAllPossibleStates());
   }
+  if (searchCriteria.checkMissingFileCopies.value_or(false)) {
+    uint64_t max_creation_time = time(nullptr);
+    max_creation_time -= searchCriteria.missingFileCopiesMinAgeSecs;
+    stmt.bindUint64(":MAX_CREATION_TIME", max_creation_time);
+  }
 
   // Disk file ID lookup requires multiple queries
   std::vector<std::string>::const_iterator diskFileId_it;
diff --git a/cmdline/CtaAdminCmdParse.hpp b/cmdline/CtaAdminCmdParse.hpp
index 26cfa749af..96fede545a 100644
--- a/cmdline/CtaAdminCmdParse.hpp
+++ b/cmdline/CtaAdminCmdParse.hpp
@@ -517,7 +517,7 @@ const Option opt_archive_route_type {Option::OPT_STR,
                                      std::string(R"( <")") +
                                        cta::common::dataStructures::toString(ArchiveRouteType::DEFAULT) + R"(" or ")" +
                                        cta::common::dataStructures::toString(ArchiveRouteType::REPACK) + R"(">)"};
-const Option opt_missingfilecopes {Option::OPT_FLAG, "--missingfilecopies", "--mfc", ""};
+const Option opt_missingfilecopies {Option::OPT_FLAG, "--missingfilecopies", "--mfc", ""};
 
 /*!
  * Subset of commands that return streaming output
@@ -1037,7 +1037,7 @@ tape (ta)
     opt_fromcastor.optional(),
     opt_purchase_order.optional(),
     opt_physical_library.optional(),
-    opt_missingfilecopes.optional()}                                                                                         },
+    opt_missingfilecopies.optional()}                                                                                         },
 
   /**md
 tapefile (tf)
diff --git a/continuousintegration/orchestration/helm/frontend/templates/configmaps/cta-frontend-xrootd.yaml b/continuousintegration/orchestration/helm/frontend/templates/configmaps/cta-frontend-xrootd.yaml
index 8955217583..aac4a4ec24 100644
--- a/continuousintegration/orchestration/helm/frontend/templates/configmaps/cta-frontend-xrootd.yaml
+++ b/continuousintegration/orchestration/helm/frontend/templates/configmaps/cta-frontend-xrootd.yaml
@@ -31,6 +31,11 @@ data:
     # CTA Catalogue options
     cta.catalogue.numberofconnections {{ .Values.conf.catalogue.numberOfConnections }}
 
+    # Missing tape file copies minimum age.
+    # Set it to prevent false flagging of multi-copy files in the process of being archived tapes.
+    # Default 24 hours
+    cta.catalogue.missing_file_copies_min_age_secs {{ .Values.conf.catalogue.missingFileCopiesMinAgeSecs }}
+
     # Maximum file size (in GB) that the CTA Frontend will accept for archiving
     cta.archivefile.max_size_gb {{ .Values.conf.frontend.archiveFileMaxSizeGb }}
 
diff --git a/continuousintegration/orchestration/helm/frontend/values.yaml b/continuousintegration/orchestration/helm/frontend/values.yaml
index e320c0f09c..df9a0beaad 100644
--- a/continuousintegration/orchestration/helm/frontend/values.yaml
+++ b/continuousintegration/orchestration/helm/frontend/values.yaml
@@ -6,6 +6,8 @@ replicaCount: 1
 conf:
   catalogue:
     numberOfConnections: 10
+    # Reduced value for test environment
+    missingFileCopiesMinAgeSecs: 5
 
   frontend:
     instanceName: "CI"
diff --git a/frontend/common/AdminCmd.cpp b/frontend/common/AdminCmd.cpp
index 67d7ee6ee6..6422100221 100644
--- a/frontend/common/AdminCmd.cpp
+++ b/frontend/common/AdminCmd.cpp
@@ -37,7 +37,8 @@ AdminCmd::AdminCmd(const frontend::FrontendService& frontendService,
   m_cliIdentity(clientIdentity),
   m_archiveFileMaxSize(frontendService.getArchiveFileMaxSize()),
   m_repackBufferURL(frontendService.getRepackBufferURL()),
-  m_repackMaxFilesToSelect(frontendService.getRepackMaxFilesToSelect())
+  m_repackMaxFilesToSelect(frontendService.getRepackMaxFilesToSelect()),
+  m_missingFileCopiesMinAgeSecs(frontendService.getMissingFileCopiesMinAgeSecs())
 {
   m_lc.pushOrReplace({"user", m_cliIdentity.username + "@" + m_cliIdentity.host});
 
diff --git a/frontend/common/AdminCmd.hpp b/frontend/common/AdminCmd.hpp
index cdacf4fd4d..bf97d5e228 100644
--- a/frontend/common/AdminCmd.hpp
+++ b/frontend/common/AdminCmd.hpp
@@ -102,14 +102,19 @@ public:
    * or not. In the case of flags, they should always have the value true if the flag is
    * present, but we do a redundant check anyway.
    *
-   * @param[in] option    Optional command line option
+   * @param[in]  option        Optional command line option
+   * @param[out] has_option    Set to true if the option exists, unmodified if it does not
    *
    * @retval    true      The flag is present in the options map, and its value is true
    * @retval    false     The flag is either not present or is present and set to false
    */
-  bool has_flag(admin::OptionBoolean::Key option) const {
+  bool has_flag(admin::OptionBoolean::Key option, bool *has_option = nullptr) const {
     auto opt_it = m_option_bool.find(option);
-    return opt_it != m_option_bool.end() && opt_it->second;
+    if (opt_it != m_option_bool.end()) {
+      if(has_option != nullptr) *has_option = true;
+      return opt_it->second;
+    }
+    return false;
   }
 
   /*!
@@ -125,6 +130,11 @@ public:
    */
   std::optional<std::string> getAndValidateDiskFileIdOptional(bool* has_any = nullptr) const;
 
+  /*!
+   * @return       The missing tape file copies minimum age.
+   */
+  uint64_t getMissingFileCopiesMinAgeSecs() const { return m_missingFileCopiesMinAgeSecs; }
+
 protected:
   /*!
    * Convert AdminCmd <Cmd, SubCmd> pair to an integer so that it can be used in a switch statement
@@ -237,10 +247,11 @@ private:
   void processRecycleTapeFile_Restore (xrd::Response& response);
   void processModifyArchiveFile       (xrd::Response& response);
 
-  common::dataStructures::SecurityIdentity    m_cliIdentity;           //!< Client identity: username, host, authentication
-  const uint64_t                              m_archiveFileMaxSize;    //!< Maximum allowed file size for archive requests
-  const std::optional<std::string>            m_repackBufferURL;       //!< Repack buffer URL
-  const std::optional<std::uint64_t>          m_repackMaxFilesToSelect;//!< Repack max files to expand
+  common::dataStructures::SecurityIdentity    m_cliIdentity;                 //!< Client identity: username, host, authentication
+  const uint64_t                              m_archiveFileMaxSize;          //!< Maximum allowed file size for archive requests
+  const std::optional<std::string>            m_repackBufferURL;             //!< Repack buffer URL
+  const std::optional<std::uint64_t>          m_repackMaxFilesToSelect;      //!< Repack max files to expand
+  const uint64_t                              m_missingFileCopiesMinAgeSecs; //!< Missing tape file copies minimum age
 
   // Command options extracted from protobuf
   std::map<admin::OptionBoolean::Key, bool>                        m_option_bool;        //!< Boolean options
diff --git a/frontend/common/FrontendService.cpp b/frontend/common/FrontendService.cpp
index 1d04598ec6..6b16f0a5c8 100644
--- a/frontend/common/FrontendService.cpp
+++ b/frontend/common/FrontendService.cpp
@@ -116,6 +116,20 @@ FrontendService::FrontendService(const std::string& configFilename) : m_archiveF
     log(log::INFO, std::string("Starting cta-frontend"), params);
   }
 
+  auto missingFileCopiesMinAgeSecs = config.getOptionValueUInt("cta.catalogue.missing_file_copies_min_age_secs");
+  m_missingFileCopiesMinAgeSecs = missingFileCopiesMinAgeSecs.value_or(0);
+
+  {
+    // Log cta.archivefile.max_size_gb
+    std::list<log::Param> params;
+    params.push_back(log::Param("source", missingFileCopiesMinAgeSecs.has_value() ? configFilename : "Compile time default"));
+    params.push_back(log::Param("category", "cta.catalogue"));
+    params.push_back(log::Param("key", "missingFileCopiesMinAgeSecs"));
+    params.push_back(
+      log::Param("value", std::to_string(missingFileCopiesMinAgeSecs.value_or(0))));
+    log(log::INFO, "Configuration entry", params);
+  }
+
   // Initialise the Catalogue
   std::string catalogueConfigFile = "/etc/cta/cta-catalogue.conf";
   const rdbms::Login catalogueLogin = rdbms::Login::parseFile(catalogueConfigFile);
diff --git a/frontend/common/FrontendService.hpp b/frontend/common/FrontendService.hpp
index 04d13f1dab..79de900c6c 100644
--- a/frontend/common/FrontendService.hpp
+++ b/frontend/common/FrontendService.hpp
@@ -69,6 +69,11 @@ public:
       return m_schedulerBackendName;
   }
 
+  /*!
+   * Get missing tape file copies minimum age
+   */
+  uint64_t getMissingFileCopiesMinAgeSecs() const { return m_missingFileCopiesMinAgeSecs; }
+
   /*!
    * Get a reference to the Scheduler
    */
@@ -186,6 +191,7 @@ private:
   std::optional<std::string>                    m_tlsKey;                       //!< The TLS service key file
   std::optional<std::string>                    m_tlsCert;                      //!< The TLS service certificate file
   std::optional<std::string>                    m_tlsChain;                     //!< The TLS CA chain file
+  uint64_t                                      m_missingFileCopiesMinAgeSecs;  //!< Missing tape file copies minimum age.
   // clang-format on
 };
 
diff --git a/frontend/grpc/RequestMessage.hpp b/frontend/grpc/RequestMessage.hpp
index de5237b15f..69073e4b28 100644
--- a/frontend/grpc/RequestMessage.hpp
+++ b/frontend/grpc/RequestMessage.hpp
@@ -103,14 +103,19 @@ public:
    * or not. In the case of flags, they should always have the value true if the flag is
    * present, but we do a redundant check anyway.
    *
-   * @param[in] option    Optional command line option
+   * @param[in]  option        Optional command line option
+   * @param[out] has_option    Set to true if the option exists, unmodified if it does not
    *
    * @retval    true      The flag is present in the options map, and its value is true
    * @retval    false     The flag is either not present or is present and set to false
    */
-  bool hasFlag(cta::admin::OptionBoolean::Key option) const {
+  bool has_flag(cta::admin::OptionBoolean::Key option, bool *has_option = nullptr) const {
     auto opt_it = m_option_bool.find(option);
-    return opt_it != m_option_bool.end() && opt_it->second;
+    if (opt_it != m_option_bool.end()) {
+      if(has_option != nullptr) *has_option = true;
+      return opt_it->second;
+    }
+    return false;
   }
   
 private:
diff --git a/frontend/grpc/ServerTapeLsRequestHandler.cpp b/frontend/grpc/ServerTapeLsRequestHandler.cpp
index 47f6881704..89405187d1 100644
--- a/frontend/grpc/ServerTapeLsRequestHandler.cpp
+++ b/frontend/grpc/ServerTapeLsRequestHandler.cpp
@@ -100,13 +100,13 @@ bool cta::frontend::grpc::server::TapeLsRequestHandler::next(const bool bOk) {
               m_searchCriteria.state = common::dataStructures::Tape::stringToState(stateOpt.value());
             }
 
-            if(!(requestMsg.hasFlag(cta::admin::OptionBoolean::ALL) || bHasAny)) {
+            if(!(requestMsg.has_flag(cta::admin::OptionBoolean::ALL) || bHasAny)) {
               lc.log(cta::log::ERR, "In grpc::server::TapeLsRequestHandler::next(): Must specify at least one search option, or --all.");
               m_response.mutable_header()->set_type(cta::xrd::Response::RSP_ERR_USER);
               m_response.mutable_header()->set_show_header(cta::admin::HeaderType::NONE);
               m_response.mutable_header()->set_message_txt("Must specify at least one search option, or --all.");
               m_streamState = StreamState::ERROR;
-            } else if(requestMsg.hasFlag(cta::admin::OptionBoolean::ALL) && bHasAny) {
+            } else if(requestMsg.has_flag(cta::admin::OptionBoolean::ALL) && bHasAny) {
               lc.log(cta::log::ERR, "In grpc::server::TapeLsRequestHandler::next(): Cannot specify --all together with other search options.");
               m_response.mutable_header()->set_type(cta::xrd::Response::RSP_ERR_USER);
               m_response.mutable_header()->set_show_header(cta::admin::HeaderType::NONE);
diff --git a/frontend/grpc/cta-frontend-grpc.conf.example b/frontend/grpc/cta-frontend-grpc.conf.example
index bddb45e40e..73b778f532 100644
--- a/frontend/grpc/cta-frontend-grpc.conf.example
+++ b/frontend/grpc/cta-frontend-grpc.conf.example
@@ -100,6 +100,10 @@ cta.log.url file:/var/log/cta/cta-frontend.log
 # CTA Catalogue options
 cta.catalogue.numberofconnections 10
 
+# Missing tape file copies minimum age.
+# Set it to prevent false flagging of multi-copy files in the process of being archived tapes.
+# Default 24 hours
+cta.catalogue.missing_file_copies_min_age_secs 86400
 
 ####################################
 # Variables used by cta-frontend-async-grpc
diff --git a/xroot_plugins/XrdCtaTapeLs.hpp b/xroot_plugins/XrdCtaTapeLs.hpp
index dfa5a9b5b4..4a5cedafef 100644
--- a/xroot_plugins/XrdCtaTapeLs.hpp
+++ b/xroot_plugins/XrdCtaTapeLs.hpp
@@ -81,6 +81,9 @@ TapeLsStream::TapeLsStream(const frontend::AdminCmdStream& requestMsg, cta::cata
   searchCriteria.physicalLibraryName    = requestMsg.getOptional(OptionString::PHYSICAL_LIBRARY,            &has_any);
   searchCriteria.diskFileIds            = requestMsg.getOptional(OptionStrList::FILE_ID,                    &has_any);
   searchCriteria.checkMissingFileCopies = requestMsg.getOptional(OptionBoolean::MISSING_FILE_COPIES,        &has_any);
+  if (searchCriteria.checkMissingFileCopies.value_or(false)) {
+    searchCriteria.missingFileCopiesMinAgeSecs = requestMsg.getMissingFileCopiesMinAgeSecs();
+  }
   auto stateOpt                      = requestMsg.getOptional(OptionString::STATE,                       &has_any);
 
   if(stateOpt){
diff --git a/xroot_plugins/cta-frontend-xrootd.conf.example b/xroot_plugins/cta-frontend-xrootd.conf.example
index 1c2f62d647..71697bf240 100644
--- a/xroot_plugins/cta-frontend-xrootd.conf.example
+++ b/xroot_plugins/cta-frontend-xrootd.conf.example
@@ -29,6 +29,11 @@ cta.schedulerdb.threadstacksize_mb 1
 # CTA Catalogue options
 cta.catalogue.numberofconnections 10
 
+# Missing tape file copies minimum age.
+# Set it to prevent false flagging of multi-copy files in the process of being archived tapes.
+# Default 24 hours
+cta.catalogue.missing_file_copies_min_age_secs 86400
+
 # Maximum file size (in GB) that the CTA Frontend will accept for archiving
 cta.archivefile.max_size_gb 1000
 
-- 
GitLab


From 0b7e6937f7c4efe06b26863c938cf48e47a4bf8e Mon Sep 17 00:00:00 2001
From: Joao Afonso <joao.afonso@cern.ch>
Date: Fri, 28 Mar 2025 16:03:36 +0100
Subject: [PATCH 2/3] Minor fixes for unit-tests

---
 catalogue/rdbms/RdbmsTapeCatalogue.cpp               | 2 +-
 catalogue/tests/modules/ArchiveFileCatalogueTest.cpp | 3 +++
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/catalogue/rdbms/RdbmsTapeCatalogue.cpp b/catalogue/rdbms/RdbmsTapeCatalogue.cpp
index 39856975be..6117539fd0 100644
--- a/catalogue/rdbms/RdbmsTapeCatalogue.cpp
+++ b/catalogue/rdbms/RdbmsTapeCatalogue.cpp
@@ -1443,7 +1443,7 @@ std::list<common::dataStructures::Tape> RdbmsTapeCatalogue::getTapes(rdbms::Conn
           WHERE
             SC.NB_COPIES > 1
           GROUP BY
-            AF.ARCHIVE_FILE_ID, SC.NB_COPIES AND AF.CREATION_TIME < :MAX_CREATION_TIME
+            AF.ARCHIVE_FILE_ID, SC.NB_COPIES AND AF.CREATION_TIME <= :MAX_CREATION_TIME
           HAVING
             SC.NB_COPIES <> COUNT(TF.ARCHIVE_FILE_ID)
         ) MISSING_COPIES
diff --git a/catalogue/tests/modules/ArchiveFileCatalogueTest.cpp b/catalogue/tests/modules/ArchiveFileCatalogueTest.cpp
index 7ad78e9e3b..bcf08dd2a8 100644
--- a/catalogue/tests/modules/ArchiveFileCatalogueTest.cpp
+++ b/catalogue/tests/modules/ArchiveFileCatalogueTest.cpp
@@ -5069,6 +5069,7 @@ TEST_P(cta_catalogue_ArchiveFileTest, getTapesWithMissingTapeFileCopies) {
   {
     cta::catalogue::TapeSearchCriteria searchCriteria;
     searchCriteria.checkMissingFileCopies = true;
+    searchCriteria.missingFileCopiesMinAgeSecs = 0;
     const std::list<cta::common::dataStructures::Tape> tapes = m_catalogue->Tape()->getTapes(searchCriteria);
 
     ASSERT_EQ(2, tapes.size());
@@ -5110,6 +5111,7 @@ TEST_P(cta_catalogue_ArchiveFileTest, getTapesWithMissingTapeFileCopies) {
   {
     cta::catalogue::TapeSearchCriteria searchCriteria;
     searchCriteria.checkMissingFileCopies = true;
+    searchCriteria.missingFileCopiesMinAgeSecs = 0;
     const std::list<cta::common::dataStructures::Tape> tapes = m_catalogue->Tape()->getTapes(searchCriteria);
 
     ASSERT_EQ(1, tapes.size());
@@ -5149,6 +5151,7 @@ TEST_P(cta_catalogue_ArchiveFileTest, getTapesWithMissingTapeFileCopies) {
   {
     cta::catalogue::TapeSearchCriteria searchCriteria;
     searchCriteria.checkMissingFileCopies = true;
+    searchCriteria.missingFileCopiesMinAgeSecs = 0;
     const std::list<cta::common::dataStructures::Tape> tapes = m_catalogue->Tape()->getTapes(searchCriteria);
 
     ASSERT_TRUE(tapes.empty());
-- 
GitLab


From 63feee1c75ab138054c244ba9ca83ff9c53dfc3a Mon Sep 17 00:00:00 2001
From: Joao Afonso <joao.afonso@cern.ch>
Date: Fri, 28 Mar 2025 16:05:53 +0100
Subject: [PATCH 3/3] Review fixes

---
 catalogue/rdbms/RdbmsTapeCatalogue.cpp | 4 ++--
 frontend/common/FrontendService.cpp    | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/catalogue/rdbms/RdbmsTapeCatalogue.cpp b/catalogue/rdbms/RdbmsTapeCatalogue.cpp
index 6117539fd0..74863f636c 100644
--- a/catalogue/rdbms/RdbmsTapeCatalogue.cpp
+++ b/catalogue/rdbms/RdbmsTapeCatalogue.cpp
@@ -1441,9 +1441,9 @@ std::list<common::dataStructures::Tape> RdbmsTapeCatalogue::getTapes(rdbms::Conn
             INNER JOIN STORAGE_CLASS SC ON AF.STORAGE_CLASS_ID = SC.STORAGE_CLASS_ID
             INNER JOIN TAPE_FILE TF ON AF.ARCHIVE_FILE_ID = TF.ARCHIVE_FILE_ID
           WHERE
-            SC.NB_COPIES > 1
+            SC.NB_COPIES > 1 AND AF.CREATION_TIME <= :MAX_CREATION_TIME
           GROUP BY
-            AF.ARCHIVE_FILE_ID, SC.NB_COPIES AND AF.CREATION_TIME <= :MAX_CREATION_TIME
+            AF.ARCHIVE_FILE_ID, SC.NB_COPIES
           HAVING
             SC.NB_COPIES <> COUNT(TF.ARCHIVE_FILE_ID)
         ) MISSING_COPIES
diff --git a/frontend/common/FrontendService.cpp b/frontend/common/FrontendService.cpp
index 6b16f0a5c8..8b3043d03c 100644
--- a/frontend/common/FrontendService.cpp
+++ b/frontend/common/FrontendService.cpp
@@ -120,7 +120,7 @@ FrontendService::FrontendService(const std::string& configFilename) : m_archiveF
   m_missingFileCopiesMinAgeSecs = missingFileCopiesMinAgeSecs.value_or(0);
 
   {
-    // Log cta.archivefile.max_size_gb
+    // Log cta.catalogue.missing_file_copies_min_age_secs
     std::list<log::Param> params;
     params.push_back(log::Param("source", missingFileCopiesMinAgeSecs.has_value() ? configFilename : "Compile time default"));
     params.push_back(log::Param("category", "cta.catalogue"));
-- 
GitLab