Repository: aosedge/aos_core_mp_cpp
Pull request #22: Compress log messages
Author: login: mlohvynenko
Created: 2025-05-06T18:00:41Z | Merged: 2025-05-16T13:24:01Z



Diff:
diff --git a/external/aos_core_common_cpp b/external/aos_core_common_cpp
index 1056c75..d080120 160000
--- a/external/aos_core_common_cpp
+++ b/external/aos_core_common_cpp
@@ -1 +1 @@
-Subproject commit 1056c75f383e3351044e0ddbc18a064ee2870f70
+Subproject commit d080120c24fe13ee0cd5cd558de55e8cc2304042
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index c864646..72e7339 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -27,6 +27,7 @@ add_subdirectory(config)
 add_subdirectory(filechunker)
 add_subdirectory(iamclient)
 add_subdirectory(imageunpacker)
+add_subdirectory(logprovider)
 
 # ######################################################################################################################
 # Sources
diff --git a/src/communication/CMakeLists.txt b/src/communication/CMakeLists.txt
index fe3d38c..edc4bb9 100644
--- a/src/communication/CMakeLists.txt
+++ b/src/communication/CMakeLists.txt
@@ -53,6 +53,7 @@ target_link_libraries(
            aosutils
            filechunker
            imageunpacker
+           logprovider
 )
 
 if(WITH_VCHAN)
diff --git a/src/communication/cmconnection.cpp b/src/communication/cmconnection.cpp
index a6ae7ea..ca8657f 100644
--- a/src/communication/cmconnection.cpp
+++ b/src/communication/cmconnection.cpp
@@ -11,6 +11,9 @@
 #include <logger/logmodule.hpp>
 #include <servicemanager/v4/servicemanager.grpc.pb.h>
 
+#include <aos/common/cloudprotocol/log.hpp>
+#include <pbconvert/sm.hpp>
+
 #include "cmconnection.hpp"
 #include "communication/utils.hpp"
 
@@ -55,6 +58,10 @@ Error CMConnection::Init(const config::Config& cfg, HandlerItf& handler, Communi
 
     mHandler = &handler;
 
+    if (auto err = mArchiveManager.Init(*this, cfg.mLogProviderConfig); !err.IsNone()) {
+        return err;
+    }
+
     try {
         mCMCommOpenChannel = comManager.CreateChannel(cfg.mCMConfig.mOpenPort);
         if (certProvider != nullptr) {
@@ -83,6 +90,10 @@ Error CMConnection::Start()
 {
     LOG_DBG() << "Start CM connection";
 
+    if (auto err = mArchiveManager.Start(); !err.IsNone()) {
+        return err;
+    }
+
     StartTask([this] { RunOpenChannel(); });
     StartTask([this] { RunSecureChannel(); });
 
@@ -91,6 +102,10 @@ Error CMConnection::Start()
 
 Error CMConnection::Stop()
 {
+    if (auto err = mArchiveManager.Stop(); !err.IsNone()) {
+        return err;
+    }
+
     {
         std::lock_guard lock {mMutex};
 
@@ -118,6 +133,28 @@ Error CMConnection::Stop()
     return ErrorEnum::eNone;
 }
 
+// cppcheck-suppress unusedFunction
+Error CMConnection::OnLogReceived(const cloudprotocol::PushLog& log)
+{
+    LOG_DBG() << "On log received: id=" << log.mLogID << ", status=" << log.mStatus << ", part=" << log.mPart
+              << ", count=" << log.mPartsCount << ", size=" << log.mContent.Size();
+
+    auto outMsg            = std::make_unique<servicemanager::v4::SMOutgoingMessages>();
+    *outMsg->mutable_log() = common::pbconvert::ConvertToProto(log);
+
+    std::vector<uint8_t> data(outMsg->ByteSizeLong());
+
+    if (!outMsg->SerializeToArray(data.data(), static_cast<int>(data.size()))) {
+        return Error(ErrorEnum::eFailed, "failed to serialize message");
+    }
+
+    if (auto err = mHandler->SendMessages(std::move(data)); !err.IsNone()) {
+        return err;
+    }
+
+    return ErrorEnum::eNone;
+}
+
 /***********************************************************************************************************************
  * Private
  **********************************************************************************************************************/
@@ -216,6 +253,17 @@ Error CMConnection::ReadSecureMsgHandler()
             continue;
         }
 
+        if (outgoingMessages.has_log()) {
+            LOG_DBG() << "Log message received";
+
+            err = mArchiveManager.HandleLog(std::make_shared<servicemanager::v4::LogData>(outgoingMessages.log()));
+            if (!err.IsNone()) {
+                LOG_ERR() << "Failed to handle log message: error=" << err;
+            }
+
+            continue;
+        }
+
         if (err = mHandler->SendMessages(std::move(message)); !err.IsNone()) {
             return err;
         }
diff --git a/src/communication/cmconnection.hpp b/src/communication/cmconnection.hpp
index 43b41a8..32bfada 100644
--- a/src/communication/cmconnection.hpp
+++ b/src/communication/cmconnection.hpp
@@ -27,6 +27,7 @@
 #include "downloader/downloader.hpp"
 #include "filechunker/filechunker.hpp"
 #include "imageunpacker/imageunpacker.hpp"
+#include "logprovider/archivemanager.hpp"
 #include "types.hpp"
 
 namespace aos::mp::communication {
@@ -34,7 +35,7 @@ namespace aos::mp::communication {
 /**
  * CM connection class.
  */
-class CMConnection {
+class CMConnection : public sm::logprovider::LogObserverItf {
 public:
     /**
      * Constructor.
@@ -67,6 +68,14 @@ class CMConnection {
      */
     Error Stop();
 
+    /**
+     * On log received event handler.
+     *
+     * @param log log.
+     * @return Error.
+     */
+    Error OnLogReceived(const cloudprotocol::PushLog& log) override;
+
 private:
     static constexpr auto cConnectionTimeout = std::chrono::seconds(3);
 
@@ -138,9 +147,10 @@ class CMConnection {
     std::string                                 mDownloadDir;
     std::optional<imageunpacker::ImageUnpacker> mImageUnpacker;
 
-    std::atomic<bool>       mShutdown {};
-    std::mutex              mMutex;
-    std::condition_variable mCondVar;
+    std::atomic<bool>           mShutdown {};
+    std::mutex                  mMutex;
+    std::condition_variable     mCondVar;
+    logprovider::ArchiveManager mArchiveManager;
 };
 
 } // namespace aos::mp::communication
diff --git a/src/config/config.cpp b/src/config/config.cpp
index b2386fd..f07360f 100644
--- a/src/config/config.cpp
+++ b/src/config/config.cpp
@@ -17,7 +17,12 @@ namespace aos::mp::config {
  * Static
  **********************************************************************************************************************/
 
-static Duration GetDuration(const common::utils::CaseInsensitiveObjectWrapper& object, const std::string& key)
+namespace {
+
+constexpr auto cDefaultMaxLogPartSize  = 10 * 1024;
+constexpr auto cDefaultMaxLogPartCount = 10;
+
+Duration GetDuration(const common::utils::CaseInsensitiveObjectWrapper& object, const std::string& key)
 {
     auto value = object.GetValue<std::string>(key);
 
@@ -33,7 +38,7 @@ static Duration GetDuration(const common::utils::CaseInsensitiveObjectWrapper& o
     return ret.mValue;
 }
 
-static Download ParseDownloader(const common::utils::CaseInsensitiveObjectWrapper& object)
+Download ParseDownloader(const common::utils::CaseInsensitiveObjectWrapper& object)
 {
     return Download {
         object.GetValue<std::string>("DownloadDir"),
@@ -43,7 +48,7 @@ static Download ParseDownloader(const common::utils::CaseInsensitiveObjectWrappe
     };
 }
 
-static VChanConfig ParseVChanConfig(const common::utils::CaseInsensitiveObjectWrapper& object)
+VChanConfig ParseVChanConfig(const common::utils::CaseInsensitiveObjectWrapper& object)
 {
     return VChanConfig {
         object.GetValue<int>("Domain"),
@@ -54,7 +59,7 @@ static VChanConfig ParseVChanConfig(const common::utils::CaseInsensitiveObjectWr
     };
 }
 
-static IAMConfig ParseIAMConfig(const common::utils::CaseInsensitiveObjectWrapper& object)
+IAMConfig ParseIAMConfig(const common::utils::CaseInsensitiveObjectWrapper& object)
 {
     return IAMConfig {
         object.GetValue<std::string>("IAMPublicServerURL"),
@@ -65,7 +70,24 @@ static IAMConfig ParseIAMConfig(const common::utils::CaseInsensitiveObjectWrappe
     };
 }
 
-static CMConfig ParseCMConfig(const common::utils::CaseInsensitiveObjectWrapper& object)
+common::logprovider::Config ParseLogProviderConfig(const common::utils::CaseInsensitiveObjectWrapper& object)
+{
+    if (!object.Has("LogProvider")) {
+        return common::logprovider::Config {
+            cDefaultMaxLogPartSize,
+            cDefaultMaxLogPartCount,
+        };
+    }
+
+    auto logProviderObject = object.GetObject("LogProvider");
+
+    return common::logprovider::Config {
+        logProviderObject.GetValue<uint64_t>("MaxPartSize", cDefaultMaxLogPartSize),
+        logProviderObject.GetValue<uint64_t>("MaxPartCount", cDefaultMaxLogPartCount),
+    };
+}
+
+CMConfig ParseCMConfig(const common::utils::CaseInsensitiveObjectWrapper& object)
 {
     return CMConfig {
         object.GetValue<std::string>("CMServerURL"),
@@ -74,6 +96,8 @@ static CMConfig ParseCMConfig(const common::utils::CaseInsensitiveObjectWrapper&
     };
 }
 
+} // namespace
+
 /***********************************************************************************************************************
  * Public functions
  **********************************************************************************************************************/
@@ -98,14 +122,15 @@ RetWithError<Config> ParseConfig(const std::string& filename)
     try {
         common::utils::CaseInsensitiveObjectWrapper object(result.mValue.extract<Poco::JSON::Object::Ptr>());
 
-        config.mWorkingDir    = object.GetValue<std::string>("WorkingDir");
-        config.mVChan         = ParseVChanConfig(object.GetObject("VChan"));
-        config.mCMConfig      = ParseCMConfig(object.GetObject("CMConfig"));
-        config.mCertStorage   = object.GetValue<std::string>("CertStorage");
-        config.mCACert        = object.GetValue<std::string>("CACert");
-        config.mImageStoreDir = object.GetValue<std::string>("ImageStoreDir");
-        config.mDownload      = ParseDownloader(object.GetObject("Downloader"));
-        config.mIAMConfig     = ParseIAMConfig(object.GetObject("IAMConfig"));
+        config.mWorkingDir        = object.GetValue<std::string>("WorkingDir");
+        config.mVChan             = ParseVChanConfig(object.GetObject("VChan"));
+        config.mCMConfig          = ParseCMConfig(object.GetObject("CMConfig"));
+        config.mCertStorage       = object.GetValue<std::string>("CertStorage");
+        config.mCACert            = object.GetValue<std::string>("CACert");
+        config.mImageStoreDir     = object.GetValue<std::string>("ImageStoreDir");
+        config.mDownload          = ParseDownloader(object.GetObject("Downloader"));
+        config.mIAMConfig         = ParseIAMConfig(object.GetObject("IAMConfig"));
+        config.mLogProviderConfig = ParseLogProviderConfig(object);
     } catch (const std::exception& e) {
         return {config, Error(ErrorEnum::eFailed, e.what())};
     }
diff --git a/src/config/config.hpp b/src/config/config.hpp
index 6c5d95c..3204cc7 100644
--- a/src/config/config.hpp
+++ b/src/config/config.hpp
@@ -10,6 +10,7 @@
 #include <string>
 
 #include <aos/common/tools/error.hpp>
+#include <logprovider/config.hpp>
 #include <utils/time.hpp>
 
 namespace aos::mp::config {
@@ -63,14 +64,15 @@ struct CMConfig {
  * Configuration.
  */
 struct Config {
-    std::string mWorkingDir;
-    VChanConfig mVChan;
-    CMConfig    mCMConfig;
-    std::string mCertStorage;
-    std::string mCACert;
-    std::string mImageStoreDir;
-    Download    mDownload;
-    IAMConfig   mIAMConfig;
+    std::string                 mWorkingDir;
+    VChanConfig                 mVChan;
+    CMConfig                    mCMConfig;
+    std::string                 mCertStorage;
+    std::string                 mCACert;
+    std::string                 mImageStoreDir;
+    Download                    mDownload;
+    IAMConfig                   mIAMConfig;
+    common::logprovider::Config mLogProviderConfig;
 };
 
 /***********************************************************************************************************************
diff --git a/src/logprovider/CMakeLists.txt b/src/logprovider/CMakeLists.txt
new file mode 100644
index 0000000..e458d15
--- /dev/null
+++ b/src/logprovider/CMakeLists.txt
@@ -0,0 +1,35 @@
+#
+# Copyright (C) 2025 EPAM Systems, Inc.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+set(TARGET logprovider)
+
+# ######################################################################################################################
+# Compiler flags
+# ######################################################################################################################
+
+add_definitions(-DLOG_MODULE="logprovider")
+
+# ######################################################################################################################
+# Sources
+# ######################################################################################################################
+
+set(SOURCES archivemanager.cpp)
+
+# ######################################################################################################################
+# Includes
+# ######################################################################################################################
+
+# ######################################################################################################################
+# Target
+# ######################################################################################################################
+
+add_library(${TARGET} STATIC ${SOURCES})
+
+# ######################################################################################################################
+# Libraries
+# ######################################################################################################################
+
+target_link_libraries(${TARGET} PUBLIC aoscommon aoscoreapi-gen-sm aoslogprovider aosutils)
diff --git a/src/logprovider/archivemanager.cpp b/src/logprovider/archivemanager.cpp
new file mode 100644
index 0000000..0ed1d3d
--- /dev/null
+++ b/src/logprovider/archivemanager.cpp
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2025 EPAM Systems, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <algorithm>
+#include <filesystem>
+
+#include <aos/common/tools/memory.hpp>
+#include <logger/logmodule.hpp>
+#include <utils/exception.hpp>
+
+#include "archivemanager.hpp"
+
+namespace aos::mp::logprovider {
+
+/***********************************************************************************************************************
+ * Public
+ **********************************************************************************************************************/
+
+Error ArchiveManager::Init(sm::logprovider::LogObserverItf& logReceiver, const common::logprovider::Config& config)
+{
+    LOG_DBG() << "Init archive manager";
+
+    mLogReceiver = &logReceiver;
+    mConfig      = config;
+
+    return ErrorEnum::eNone;
+}
+
+Error ArchiveManager::Start()
+{
+    std::lock_guard lock {mMutex};
+
+    LOG_DBG() << "Start archive manager";
+
+    if (!mStopped) {
+        return Error(ErrorEnum::eRuntime, "already started");
+    }
+
+    Poco::TimerCallback<ArchiveManager> callback(*this, &ArchiveManager::CleanupOutdatedArchives);
+
+    mTimer.setStartInterval(cLogCleanupPeriod.Milliseconds());
+    mTimer.setPeriodicInterval(cLogCleanupPeriod.Milliseconds());
+    mTimer.start(callback);
+
+    mStopped = false;
+
+    mThread = std::thread([this]() { Run(); });
+
+    return ErrorEnum::eNone;
+}
+
+Error ArchiveManager::Stop()
+{
+    {
+        std::lock_guard lock {mMutex};
+
+        LOG_DBG() << "Stop archive manager";
+
+        if (mStopped) {
+            return Error(ErrorEnum::eRuntime, "already stopped");
+        }
+
+        mStopped = true;
+        mCondVar.notify_all();
+    }
+
+    if (mThread.joinable()) {
+        mThread.join();
+    }
+
+    mTimer.stop();
+
+    return ErrorEnum::eNone;
+}
+
+Error ArchiveManager::HandleLog(std::shared_ptr<servicemanager::v4::LogData> log)
+{
+    std::lock_guard lock {mMutex};
+
+    LOG_DBG() << "Handle log: logID=" << log->log_id().c_str() << ", part=" << log->part()
+              << ", status=" << log->status().c_str();
+
+    mLogQueue.push(std::move(log));
+    mCondVar.notify_all();
+
+    return ErrorEnum::eNone;
+}
+
+/***********************************************************************************************************************
+ * Private
+ **********************************************************************************************************************/
+
+Error ArchiveManager::Archive(std::shared_ptr<servicemanager::v4::LogData> log)
+{
+    std::lock_guard lock {mMutex};
+
+    LOG_DBG() << "Archive log: logID=" << log->log_id().c_str() << ", part=" << log->part()
+              << ", status=" << log->status().c_str();
+
+    auto it = std::find_if(mArchiveContexts.begin(), mArchiveContexts.end(),
+        [log](const auto& context) { return context->mLogID == log->log_id(); });
+
+    std::shared_ptr<ArchiveContext> archiveContext = (it == mArchiveContexts.end())
+        ? std::make_shared<ArchiveContext>(log->log_id(), *mLogReceiver, mConfig)
+        : *it;
+
+    if (GetLogStatus(log) == cloudprotocol::LogStatusEnum::eOk) {
+        if (it == mArchiveContexts.end()) {
+            mArchiveContexts.push_back(archiveContext);
+        }
+
+        archiveContext->mUpdated = Time::Now();
+
+        return archiveContext->mArchivator.AddLog(log->data());
+    }
+
+    return SendFinalChunk(archiveContext);
+}
+
+void ArchiveManager::Run()
+{
+    LOG_DBG() << "Run archive manager";
+
+    while (true) {
+        std::unique_lock lock {mMutex};
+
+        mCondVar.wait(lock, [this]() { return mStopped || !mLogQueue.empty(); });
+
+        if (mStopped) {
+            break;
+        }
+
+        auto log = std::move(mLogQueue.front());
+        mLogQueue.pop();
+
+        lock.unlock();
+
+        auto err = Archive(log);
+        if (!err.IsNone()) {
+            LOG_ERR() << "Failed to archive log: err=" << err;
+        }
+    }
+}
+
+// cppcheck-suppress constParameterCallback
+void ArchiveManager::CleanupOutdatedArchives(Poco::Timer& timer)
+{
+    (void)timer;
+
+    std::lock_guard lock {mMutex};
+
+    LOG_DBG() << "Cleanup outdated archives";
+
+    mArchiveContexts.erase(std::remove_if(mArchiveContexts.begin(), mArchiveContexts.end(),
+                               [now = Time::Now()](const auto& context) {
+                                   auto expiredTime = context->mUpdated.Add(cLogPendingTimeout);
+
+                                   return now > expiredTime;
+                               }),
+        mArchiveContexts.end());
+}
+
+Error ArchiveManager::SendFinalChunk(std::shared_ptr<ArchiveContext> archiveContext)
+{
+    LOG_DBG() << "Send final chunk: logID=" << archiveContext->mLogID.c_str();
+
+    mArchiveContexts.erase(
+        std::remove(mArchiveContexts.begin(), mArchiveContexts.end(), archiveContext), mArchiveContexts.end());
+
+    return archiveContext->mArchivator.SendLog(archiveContext->mLogID.c_str());
+}
+
+cloudprotocol::LogStatus ArchiveManager::GetLogStatus(const std::shared_ptr<servicemanager::v4::LogData>& log) const
+{
+    cloudprotocol::LogStatus status;
+
+    status.FromString(log->status().c_str());
+
+    return status;
+}
+
+} // namespace aos::mp::logprovider
diff --git a/src/logprovider/archivemanager.hpp b/src/logprovider/archivemanager.hpp
new file mode 100644
index 0000000..1c42da6
--- /dev/null
+++ b/src/logprovider/archivemanager.hpp
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2025 EPAM Systems, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef ARCHIVERMANAGER_HPP_
+#define ARCHIVERMANAGER_HPP_
+
+#include <condition_variable>
+#include <map>
+#include <mutex>
+#include <queue>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <Poco/Timer.h>
+
+#include <aos/common/tools/error.hpp>
+#include <logprovider/archivator.hpp>
+#include <servicemanager/v4/servicemanager.grpc.pb.h>
+
+namespace aos::mp::logprovider {
+
+/**
+ * Archive manager class.
+ */
+class ArchiveManager {
+public:
+    /**
+     * Initializes archive manager.
+     *
+     * @param logReceiver log receiver.
+     * @param config configuration.
+     * @return Error.
+     */
+    Error Init(sm::logprovider::LogObserverItf& logReceiver, const common::logprovider::Config& config);
+
+    /**
+     * Starts archive manager.
+     *
+     * @return Error.
+     */
+    Error Start();
+
+    /**
+     * Stops archive manager.
+     *
+     * @return Error.
+     */
+    Error Stop();
+
+    /**
+     * Handles log data.
+     *
+     * @param log log data.
+     * @return Error.
+     */
+    Error HandleLog(std::shared_ptr<servicemanager::v4::LogData> log);
+
+private:
+    static constexpr auto cLogCleanupPeriod  = Time::cMinutes * 5;
+    static constexpr auto cLogPendingTimeout = Time::cSeconds * 10;
+
+    struct ArchiveContext {
+        ArchiveContext(const std::string& logID, sm::logprovider::LogObserverItf& logReceiver,
+            const common::logprovider::Config& config)
+            : mLogID(logID)
+            , mUpdated(Time::Now())
+            , mArchivator(logReceiver, config)
+        {
+        }
+
+        std::string                     mLogID;
+        Time                            mUpdated;
+        common::logprovider::Archivator mArchivator;
+    };
+
+    void                     Run();
+    void                     CleanupOutdatedArchives(Poco::Timer& timer);
+    Error                    Archive(std::shared_ptr<servicemanager::v4::LogData> log);
+    Error                    SendFinalChunk(std::shared_ptr<ArchiveContext> archiveContext);
+    cloudprotocol::LogStatus GetLogStatus(const std::shared_ptr<servicemanager::v4::LogData>& log) const;
+
+    Poco::Timer                                              mTimer;
+    bool                                                     mStopped = true;
+    std::thread                                              mThread;
+    std::condition_variable                                  mCondVar;
+    std::mutex                                               mMutex;
+    common::logprovider::Config                              mConfig      = {};
+    sm::logprovider::LogObserverItf*                         mLogReceiver = {};
+    std::vector<std::shared_ptr<ArchiveContext>>             mArchiveContexts;
+    std::queue<std::shared_ptr<servicemanager::v4::LogData>> mLogQueue;
+};
+
+} // namespace aos::mp::logprovider
+
+#endif
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index f1573d7..d1239ee 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -21,3 +21,4 @@ add_subdirectory(config)
 add_subdirectory(filechunker)
 add_subdirectory(iamclient)
 add_subdirectory(imageunpacker)
+add_subdirectory(logprovider)
diff --git a/tests/communication/communication_secure_test.cpp b/tests/communication/communication_secure_test.cpp
index 4cdbb62..7346029 100644
--- a/tests/communication/communication_secure_test.cpp
+++ b/tests/communication/communication_secure_test.cpp
@@ -118,14 +118,16 @@ class CommunicationSecureManagerTest : public ::testing::Test {
         // OSSL_trace_set_prefix(OSSL_TRACE_CATEGORY_TLS, "BEGIN TRACE[TLS]");
         // OSSL_trace_set_suffix(OSSL_TRACE_CATEGORY_TLS, "END TRACE[TLS]");
 
-        mConfig.mIAMConfig.mOpenPort   = 8081;
-        mConfig.mIAMConfig.mSecurePort = 8080;
-        mConfig.mVChan.mIAMCertStorage = "server";
-        mConfig.mVChan.mSMCertStorage  = "server";
-        mConfig.mDownload.mDownloadDir = "download";
-        mConfig.mImageStoreDir         = "images";
-        mConfig.mCMConfig.mOpenPort    = 30001;
-        mConfig.mCMConfig.mSecurePort  = 30002;
+        mConfig.mIAMConfig.mOpenPort             = 8081;
+        mConfig.mIAMConfig.mSecurePort           = 8080;
+        mConfig.mVChan.mIAMCertStorage           = "server";
+        mConfig.mVChan.mSMCertStorage            = "server";
+        mConfig.mDownload.mDownloadDir           = "download";
+        mConfig.mImageStoreDir                   = "images";
+        mConfig.mCMConfig.mOpenPort              = 30001;
+        mConfig.mCMConfig.mSecurePort            = 30002;
+        mConfig.mLogProviderConfig.mMaxPartSize  = 1024;
+        mConfig.mLogProviderConfig.mMaxPartCount = 10;
 
         mConfig.mCACert = CERTIFICATES_MP_DIR "/ca.cer";
 
@@ -562,6 +564,68 @@ TEST_F(CommunicationSecureManagerTest, TestDownload)
     mCMSecurePipe->Close();
 }
 
+TEST_F(CommunicationSecureManagerTest, TestSendLog)
+{
+    CMConnection mCMConnection {};
+
+    mCMClientChannel = mCommManagerClient->CreateCommChannel(30002);
+    mCMSecurePipe.emplace(*mCMClientChannel, mKeyURI, mCertPEM, CERTIFICATES_MP_DIR "/ca.cer");
+
+    auto err = mCommManager->Init(mConfig, mServer.value(), &mCertLoader, &mCryptoProvider);
+    EXPECT_EQ(err, aos::ErrorEnum::eNone);
+
+    err = mCMConnection.Init(mConfig, CMHandler, *mCommManager, &mDownloader, &mCertProvider.value());
+    EXPECT_EQ(err, aos::ErrorEnum::eNone);
+
+    EXPECT_EQ(mCommManager->Start(), aos::ErrorEnum::eNone);
+    EXPECT_EQ(mCMConnection.Start(), aos::ErrorEnum::eNone);
+
+    // connect to CM
+    EXPECT_EQ(mCMSecurePipe->Connect(), aos::ErrorEnum::eNone);
+
+    struct LogData {
+        std::string                   mLogId;
+        std::string                   mLogMessage;
+        aos::cloudprotocol::LogStatus mStatus;
+    } testLogData[] = {
+        {"id1", "test log message1\n", aos::cloudprotocol::LogStatusEnum::eOk},
+        {"id1", "test log message2\n", aos::cloudprotocol::LogStatusEnum::eOk},
+        {"id1", "", aos::cloudprotocol::LogStatusEnum::eEmpty},
+    };
+
+    for (const auto& logData : testLogData) {
+        servicemanager::v4::SMOutgoingMessages outgoingMsg;
+        auto&                                  log = *outgoingMsg.mutable_log();
+
+        log.set_log_id(logData.mLogId);
+        log.set_data(logData.mLogMessage);
+        log.set_status(logData.mStatus.ToString().CStr());
+
+        std::vector<uint8_t> messageData(outgoingMsg.ByteSizeLong());
+        EXPECT_TRUE(outgoingMsg.SerializeToArray(messageData.data(), messageData.size()));
+
+        auto protobufHeader = PrepareProtobufHeader(messageData.size());
+        protobufHeader.insert(protobufHeader.end(), messageData.begin(), messageData.end());
+        EXPECT_EQ(mCMSecurePipe->Write(protobufHeader), aos::ErrorEnum::eNone);
+    }
+
+    auto [receivedMsg, errReceive] = CMHandler.GetOutgoingMessages();
+    EXPECT_EQ(errReceive, aos::ErrorEnum::eNone);
+
+    servicemanager::v4::SMOutgoingMessages outgoingMsg;
+    EXPECT_TRUE(outgoingMsg.ParseFromArray(receivedMsg.data(), receivedMsg.size()));
+
+    EXPECT_EQ(outgoingMsg.SMOutgoingMessage_case(), servicemanager::v4::SMOutgoingMessages::kLog);
+    EXPECT_EQ(outgoingMsg.log().log_id(), "id1");
+    EXPECT_EQ(outgoingMsg.log().status(),
+        aos::cloudprotocol::LogStatus(aos::cloudprotocol::LogStatusEnum::eOk).ToString().CStr());
+
+    mCommManager->Stop();
+    mCommManagerClient->Close();
+    mCMConnection.Stop();
+    mCMSecurePipe->Close();
+}
+
 TEST_F(CommunicationSecureManagerTest, TestCertChange)
 {
     IAMConnection mIAMSecureConnection {};
diff --git a/tests/logprovider/CMakeLists.txt b/tests/logprovider/CMakeLists.txt
new file mode 100644
index 0000000..1d25f17
--- /dev/null
+++ b/tests/logprovider/CMakeLists.txt
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 2025 EPAM Systems, Inc.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+set(TARGET logprovider_test)
+
+# ######################################################################################################################
+# Sources
+# ######################################################################################################################
+
+set(SOURCES archivemanager_test.cpp)
+
+# ######################################################################################################################
+# Target
+# ######################################################################################################################
+
+add_executable(${TARGET} ${SOURCES})
+
+gtest_discover_tests(${TARGET})
+
+# ######################################################################################################################
+# Libraries
+# ######################################################################################################################
+
+target_link_libraries(${TARGET} logprovider GTest::gmock_main)
diff --git a/tests/logprovider/archivemanager_test.cpp b/tests/logprovider/archivemanager_test.cpp
new file mode 100644
index 0000000..1a0ee78
--- /dev/null
+++ b/tests/logprovider/archivemanager_test.cpp
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2025 EPAM Systems, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <filesystem>
+#include <fstream>
+
+#include <Poco/InflatingStream.h>
+#include <Poco/StreamCopier.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <aos/test/log.hpp>
+
+#include "logprovider/archivemanager.hpp"
+
+using namespace testing;
+
+namespace aos::mp::logprovider {
+
+namespace {
+
+/***********************************************************************************************************************
+ * Constants
+ **********************************************************************************************************************/
+
+constexpr auto cLogID       = "test_log";
+const auto     cStatusOk    = cloudprotocol::LogStatus(cloudprotocol::LogStatusEnum::eOk);
+const auto     cStatusEmpty = cloudprotocol::LogStatus(cloudprotocol::LogStatusEnum::eEmpty);
+
+/***********************************************************************************************************************
+ * Static
+ **********************************************************************************************************************/
+
+std::string DecompressGZIP(const std::string& compressedData)
+{
+    std::istringstream         compressedStream(compressedData);
+    Poco::InflatingInputStream inflater(compressedStream, Poco::InflatingStreamBuf::STREAM_GZIP);
+
+    std::ostringstream decompressedStream;
+    Poco::StreamCopier::copyStream(inflater, decompressedStream);
+
+    return decompressedStream.str();
+}
+
+std::string GenerateExpectedLog(const std::vector<servicemanager::v4::LogData>& logs)
+{
+    std::ostringstream oss;
+
+    for (const auto& log : logs) {
+        oss << log.data();
+    }
+
+    return oss.str();
+}
+
+std::vector<servicemanager::v4::LogData> CreateLogChunks(
+    const std::string& logID, const std::vector<std::string>& chunks)
+{
+    std::vector<servicemanager::v4::LogData> logChunks;
+
+    for (size_t i = 0; i < chunks.size(); ++i) {
+        servicemanager::v4::LogData logChunk;
+
+        logChunk.set_log_id(logID);
+        logChunk.set_part_count(chunks.size());
+        logChunk.set_part(i + 1);
+        logChunk.set_status(cStatusOk.ToString().CStr());
+        logChunk.set_data(chunks[i]);
+
+        logChunks.push_back(std::move(logChunk));
+    }
+
+    servicemanager::v4::LogData lastLogChunk;
+    lastLogChunk.set_log_id(logID);
+    lastLogChunk.set_status(cStatusEmpty.ToString().CStr());
+
+    logChunks.push_back(std::move(lastLogChunk));
+
+    return logChunks;
+}
+
+/**
+ * Log observer stub.
+ */
+class LogObserverStub : public aos::sm::logprovider::LogObserverItf {
+public:
+    Error OnLogReceived(const cloudprotocol::PushLog& log) override
+    {
+        std::unique_lock<std::mutex> lock(mMutex);
+
+        mLogQueue.push(log);
+        mCondVar.notify_one();
+
+        return ErrorEnum::eNone;
+    }
+
+    Error WaitLogReceived(
+        cloudprotocol::PushLog& log, std::chrono::milliseconds timeout = std::chrono::milliseconds(1000))
+    {
+        std::unique_lock<std::mutex> lock(mMutex);
+
+        mCondVar.wait_for(lock, timeout, [this] { return !mLogQueue.empty(); });
+
+        if (mLogQueue.empty()) {
+            return ErrorEnum::eTimeout;
+        }
+
+        log = mLogQueue.front();
+        mLogQueue.pop();
+
+        return ErrorEnum::eNone;
+    }
+
+private:
+    std::mutex                         mMutex;
+    std::condition_variable            mCondVar;
+    std::queue<cloudprotocol::PushLog> mLogQueue;
+};
+
+} // namespace
+
+/***********************************************************************************************************************
+ * Suite
+ **********************************************************************************************************************/
+
+class ArchiveManagerTest : public ::testing::Test {
+protected:
+    void SetUp() override
+    {
+        aos::test::InitLog();
+
+        mConfig.mMaxPartCount = 10;
+        mConfig.mMaxPartSize  = 1024;
+
+        EXPECT_EQ(mArchiveManager.Init(mLogObserver, mConfig), ErrorEnum::eNone);
+    }
+
+    void TearDown() override { }
+
+    common::logprovider::Config mConfig;
+    LogObserverStub             mLogObserver;
+    ArchiveManager              mArchiveManager;
+};
+
+/***********************************************************************************************************************
+ * Tests
+ **********************************************************************************************************************/
+
+TEST_F(ArchiveManagerTest, HandleLogChunks)
+{
+    EXPECT_EQ(mArchiveManager.Start(), ErrorEnum::eNone);
+
+    const auto logChunks   = CreateLogChunks(cLogID, {"test_chunk_1", "test_chunk_2"});
+    const auto expectedLog = GenerateExpectedLog(logChunks);
+
+    for (const auto& log : logChunks) {
+        EXPECT_EQ(mArchiveManager.HandleLog(std::make_shared<servicemanager::v4::LogData>(log)), ErrorEnum::eNone);
+    }
+
+    auto receivedLog = std::make_shared<cloudprotocol::PushLog>();
+
+    EXPECT_EQ(mLogObserver.WaitLogReceived(*receivedLog), ErrorEnum::eNone);
+    EXPECT_STREQ(receivedLog->mLogID.CStr(), cLogID);
+    EXPECT_EQ(receivedLog->mPartsCount, 1);
+    EXPECT_EQ(receivedLog->mPart, 1);
+    EXPECT_EQ(receivedLog->mStatus, cStatusOk);
+    EXPECT_EQ(DecompressGZIP(std::string(receivedLog->mContent.begin(), receivedLog->mContent.end())), expectedLog);
+
+    EXPECT_EQ(mArchiveManager.Stop(), ErrorEnum::eNone);
+}
+
+TEST_F(ArchiveManagerTest, HandleEmpty)
+{
+    EXPECT_EQ(mArchiveManager.Start(), ErrorEnum::eNone);
+
+    auto log = std::make_shared<servicemanager::v4::LogData>();
+    log->set_log_id(cLogID);
+    log->set_part_count(1);
+    log->set_part(1);
+    log->set_status(cStatusEmpty.ToString().CStr());
+
+    EXPECT_EQ(mArchiveManager.HandleLog(log), ErrorEnum::eNone);
+
+    auto receivedLog = std::make_shared<cloudprotocol::PushLog>();
+
+    EXPECT_EQ(mLogObserver.WaitLogReceived(*receivedLog), ErrorEnum::eNone);
+    EXPECT_STREQ(receivedLog->mLogID.CStr(), cLogID);
+    EXPECT_EQ(receivedLog->mPartsCount, 1);
+    EXPECT_EQ(receivedLog->mPart, 1);
+    EXPECT_EQ(receivedLog->mStatus, cStatusEmpty);
+
+    EXPECT_EQ(mArchiveManager.Stop(), ErrorEnum::eNone);
+}
+
+} // namespace aos::mp::logprovider


Reviews:
user: login: MykolaSuperman
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-13T08:36:11Z
user: login: MykolaSuperman
state: APPROVED
author_association: NONE
submitted_at: 2025-05-13T16:40:09Z
user: login: al1img
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T18:13:53Z
user: login: al1img
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-15T10:54:23Z

Review comments:
diff_hunk: @@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2025 EPAM Systems, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <algorithm>
+#include <filesystem>
+
+#include <aos/common/tools/memory.hpp>
+#include <logger/logmodule.hpp>
+#include <utils/exception.hpp>
+
+#include "archivatormanager.hpp"
+
+namespace aos::mp::archivatormanager {
+
+/***********************************************************************************************************************
+ * Public
+ **********************************************************************************************************************/
+
+Error ArchivatorManager::Init(sm::logprovider::LogObserverItf& logReceiver, const common::logprovider::Config& config)
+{
+    LOG_DBG() << "Init archivator manager";
+
+    mLogReceiver = &logReceiver;
+    mConfig      = config;
+
+    return ErrorEnum::eNone;
+}
+
+Error ArchivatorManager::Start()
+{
+    LOG_DBG() << "Start archivator manager";
+
+    auto err = mTimer.Start(
+        cLogCleanupPeriod,
+        [this](void*) {
+            if (auto errCleanup = CleanupOutdatedArchives(); !errCleanup.IsNone()) {
+                LOG_ERR() << "Failed to cleanup outdated archives: err=" << errCleanup;
+            }
+        },
+        false);
+
+    if (!err.IsNone()) {
+        return err;
+    }
+
+    if (mThread.joinable()) {
+        return ErrorEnum::eAlreadyExist;
+    }
+
+    mThread = std::thread([this]() { Run(); });
+
+    return ErrorEnum::eNone;
+}
+
+Error ArchivatorManager::Stop()
+{
+    LOG_DBG() << "Stop archivator manager";
+
+    if (!mThread.joinable()) {
+        return ErrorEnum::eNotFound;
+    }
+
+    {
+        std::lock_guard lock {mMutex};
+
+        mShutdown = true;
path: src/logprovider/archivatormanager.cpp
user: login: MykolaSuperman
body: maybe use another name instead of `mShutdown` like `mStop`
created_at: 2025-05-13T07:56:47Z
author_association: NONE
diff_hunk: @@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2025 EPAM Systems, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <algorithm>
+#include <filesystem>
+
+#include <aos/common/tools/memory.hpp>
+#include <logger/logmodule.hpp>
+#include <utils/exception.hpp>
+
+#include "archivatormanager.hpp"
+
+namespace aos::mp::archivatormanager {
+
+/***********************************************************************************************************************
+ * Public
+ **********************************************************************************************************************/
+
+Error ArchivatorManager::Init(sm::logprovider::LogObserverItf& logReceiver, const common::logprovider::Config& config)
+{
+    LOG_DBG() << "Init archivator manager";
+
+    mLogReceiver = &logReceiver;
+    mConfig      = config;
+
+    return ErrorEnum::eNone;
+}
+
+Error ArchivatorManager::Start()
+{
+    LOG_DBG() << "Start archivator manager";
path: src/logprovider/archivatormanager.cpp
user: login: MykolaSuperman
body: i propose to use in here check
```
if (!mStop)
  return;
```

created_at: 2025-05-13T07:57:50Z
author_association: NONE
diff_hunk: @@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2025 EPAM Systems, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <algorithm>
+#include <filesystem>
+
+#include <aos/common/tools/memory.hpp>
+#include <logger/logmodule.hpp>
+#include <utils/exception.hpp>
+
+#include "archivatormanager.hpp"
+
+namespace aos::mp::archivatormanager {
+
+/***********************************************************************************************************************
+ * Public
+ **********************************************************************************************************************/
+
+Error ArchivatorManager::Init(sm::logprovider::LogObserverItf& logReceiver, const common::logprovider::Config& config)
+{
+    LOG_DBG() << "Init archivator manager";
+
+    mLogReceiver = &logReceiver;
+    mConfig      = config;
+
+    return ErrorEnum::eNone;
+}
+
+Error ArchivatorManager::Start()
+{
+    LOG_DBG() << "Start archivator manager";
+
+    auto err = mTimer.Start(
+        cLogCleanupPeriod,
+        [this](void*) {
+            if (auto errCleanup = CleanupOutdatedArchives(); !errCleanup.IsNone()) {
+                LOG_ERR() << "Failed to cleanup outdated archives: err=" << errCleanup;
+            }
+        },
+        false);
+
+    if (!err.IsNone()) {
+        return err;
+    }
+
+    if (mThread.joinable()) {
+        return ErrorEnum::eAlreadyExist;
+    }
+
+    mThread = std::thread([this]() { Run(); });
+
+    return ErrorEnum::eNone;
+}
+
+Error ArchivatorManager::Stop()
+{
+    LOG_DBG() << "Stop archivator manager";
+
+    if (!mThread.joinable()) {
+        return ErrorEnum::eNotFound;
+    }
+
+    {
+        std::lock_guard lock {mMutex};
+
+        mShutdown = true;
+        mCondVar.notify_all();
+    }
+
+    mTimer.Stop();
+
+    mThread.join();
+
+    return ErrorEnum::eNone;
+}
+
+Error ArchivatorManager::HandleLog(std::shared_ptr<servicemanager::v4::LogData> log)
+{
+    std::lock_guard lock {mMutex};
+
+    LOG_DBG() << "Handle log: logID=" << log->log_id().c_str() << ", part=" << log->part()
+              << ", status=" << log->status().c_str();
+
+    mLogQueue.push(std::move(log));
+    mCondVar.notify_all();
+
+    return ErrorEnum::eNone;
+}
+
+/***********************************************************************************************************************
+ * Private
+ **********************************************************************************************************************/
+
+Error ArchivatorManager::Archive(std::shared_ptr<servicemanager::v4::LogData> log)
+{
+    std::lock_guard lock {mMutex};
+
+    LOG_DBG() << "Archive log: logID=" << log->log_id().c_str() << ", part=" << log->part()
+              << ", status=" << log->status().c_str();
+
+    auto it = std::find_if(mArchiveContexts.begin(), mArchiveContexts.end(),
+        [log](const auto& context) { return context->mLogID == log->log_id(); });
+
+    std::shared_ptr<ArchiveContext> archiveContext = (it == mArchiveContexts.end())
+        ? std::make_shared<ArchiveContext>(log->log_id(), *mLogReceiver, mConfig)
+        : *it;
+
+    if (it == mArchiveContexts.end()) {
+        mArchiveContexts.push_back(archiveContext);
+        it = std::prev(mArchiveContexts.end());
+    }
+
+    if (GetLogStatus(log) == cloudprotocol::LogStatusEnum::eOk) {
+        archiveContext->mUpdated = Time::Now();
+
+        return archiveContext->mArchivator.AddLog(log->data());
+    }
+
+    mArchiveContexts.erase(it);
path: src/logprovider/archivatormanager.cpp
user: login: MykolaSuperman
body: I think we can handle this case differently. Right now, when `it == mArchiveContexts.end()` we add it to the vector, and then just below, if
```
if (GetLogStatus(log) != cloudprotocol::LogStatusEnum::eOk)
```
we remove it from the vector. So perhaps if the log status isn’t OK, we shouldn’t add it to the vector in the first place?
created_at: 2025-05-13T08:24:40Z
author_association: NONE
diff_hunk: @@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2025 EPAM Systems, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <algorithm>
+#include <filesystem>
+
+#include <aos/common/tools/memory.hpp>
+#include <logger/logmodule.hpp>
+#include <utils/exception.hpp>
+
+#include "archivatormanager.hpp"
+
+namespace aos::mp::archivatormanager {
+
+/***********************************************************************************************************************
+ * Public
+ **********************************************************************************************************************/
+
+Error ArchivatorManager::Init(sm::logprovider::LogObserverItf& logReceiver, const common::logprovider::Config& config)
+{
+    LOG_DBG() << "Init archivator manager";
+
+    mLogReceiver = &logReceiver;
+    mConfig      = config;
+
+    return ErrorEnum::eNone;
+}
+
+Error ArchivatorManager::Start()
+{
+    LOG_DBG() << "Start archivator manager";
+
+    auto err = mTimer.Start(
+        cLogCleanupPeriod,
+        [this](void*) {
+            if (auto errCleanup = CleanupOutdatedArchives(); !errCleanup.IsNone()) {
+                LOG_ERR() << "Failed to cleanup outdated archives: err=" << errCleanup;
+            }
+        },
+        false);
+
+    if (!err.IsNone()) {
+        return err;
+    }
+
+    if (mThread.joinable()) {
+        return ErrorEnum::eAlreadyExist;
+    }
+
+    mThread = std::thread([this]() { Run(); });
+
+    return ErrorEnum::eNone;
+}
+
+Error ArchivatorManager::Stop()
+{
+    LOG_DBG() << "Stop archivator manager";
+
+    if (!mThread.joinable()) {
+        return ErrorEnum::eNotFound;
+    }
+
+    {
+        std::lock_guard lock {mMutex};
+
+        mShutdown = true;
+        mCondVar.notify_all();
+    }
+
+    mTimer.Stop();
+
+    mThread.join();
+
+    return ErrorEnum::eNone;
+}
+
+Error ArchivatorManager::HandleLog(std::shared_ptr<servicemanager::v4::LogData> log)
+{
+    std::lock_guard lock {mMutex};
+
+    LOG_DBG() << "Handle log: logID=" << log->log_id().c_str() << ", part=" << log->part()
+              << ", status=" << log->status().c_str();
+
+    mLogQueue.push(std::move(log));
+    mCondVar.notify_all();
+
+    return ErrorEnum::eNone;
+}
+
+/***********************************************************************************************************************
+ * Private
+ **********************************************************************************************************************/
+
+Error ArchivatorManager::Archive(std::shared_ptr<servicemanager::v4::LogData> log)
+{
+    std::lock_guard lock {mMutex};
+
+    LOG_DBG() << "Archive log: logID=" << log->log_id().c_str() << ", part=" << log->part()
+              << ", status=" << log->status().c_str();
+
+    auto it = std::find_if(mArchiveContexts.begin(), mArchiveContexts.end(),
+        [log](const auto& context) { return context->mLogID == log->log_id(); });
+
+    std::shared_ptr<ArchiveContext> archiveContext = (it == mArchiveContexts.end())
+        ? std::make_shared<ArchiveContext>(log->log_id(), *mLogReceiver, mConfig)
+        : *it;
+
+    if (it == mArchiveContexts.end()) {
+        mArchiveContexts.push_back(archiveContext);
+        it = std::prev(mArchiveContexts.end());
+    }
+
+    if (GetLogStatus(log) == cloudprotocol::LogStatusEnum::eOk) {
+        archiveContext->mUpdated = Time::Now();
+
+        return archiveContext->mArchivator.AddLog(log->data());
+    }
+
+    mArchiveContexts.erase(it);
+
+    return SendFinalChunk(archiveContext);
+}
+
+void ArchivatorManager::Run()
+{
+    LOG_DBG() << "Run archivator manager";
+
+    while (true) {
+        std::unique_lock lock {mMutex};
+
+        mCondVar.wait(lock, [this]() { return mShutdown || !mLogQueue.empty(); });
+
+        if (mShutdown) {
+            break;
+        }
+
+        auto log = mLogQueue.front();
path: src/logprovider/archivatormanager.cpp
user: login: MykolaSuperman
body: std::move?
created_at: 2025-05-13T08:25:17Z
author_association: NONE
diff_hunk: @@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2025 EPAM Systems, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef ARCHIVATORMANAGER_HPP_
+#define ARCHIVATORMANAGER_HPP_
+
+#include <condition_variable>
+#include <map>
+#include <mutex>
+#include <queue>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <aos/common/tools/error.hpp>
+#include <aos/common/tools/timer.hpp>
+#include <logprovider/archivator.hpp>
+#include <servicemanager/v4/servicemanager.grpc.pb.h>
+
+namespace aos::mp::archivatormanager {
+
+/**
+ * Archivator manager class.
+ */
+class ArchivatorManager {
path: src/logprovider/archivatormanager.hpp
user: login: al1img
body: Name it either just `Archivator` or `ArchiveManager`. 
created_at: 2025-05-14T18:05:58Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2025 EPAM Systems, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef ARCHIVATORMANAGER_HPP_
+#define ARCHIVATORMANAGER_HPP_
+
+#include <condition_variable>
+#include <map>
+#include <mutex>
+#include <queue>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <aos/common/tools/error.hpp>
+#include <aos/common/tools/timer.hpp>
+#include <logprovider/archivator.hpp>
+#include <servicemanager/v4/servicemanager.grpc.pb.h>
+
+namespace aos::mp::archivatormanager {
+
+/**
+ * Archivator manager class.
+ */
+class ArchivatorManager {
+public:
+    /**
+     * Initialize the archivator manager.
+     *
+     * @param logReceiver log receiver.
+     * @param config configuration.
+     * @return Error.
+     */
+    Error Init(sm::logprovider::LogObserverItf& logReceiver, const common::logprovider::Config& config);
+
+    /**
+     * Start the archivator manager.
path: src/logprovider/archivatormanager.hpp
user: login: al1img
body: Starts
created_at: 2025-05-14T18:06:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2025 EPAM Systems, Inc.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef ARCHIVATORMANAGER_HPP_
+#define ARCHIVATORMANAGER_HPP_
+
+#include <condition_variable>
+#include <map>
+#include <mutex>
+#include <queue>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <aos/common/tools/error.hpp>
+#include <aos/common/tools/timer.hpp>
+#include <logprovider/archivator.hpp>
+#include <servicemanager/v4/servicemanager.grpc.pb.h>
+
+namespace aos::mp::archivatormanager {
+
+/**
+ * Archivator manager class.
+ */
+class ArchivatorManager {
+public:
+    /**
+     * Initialize the archivator manager.
+     *
+     * @param logReceiver log receiver.
+     * @param config configuration.
+     * @return Error.
+     */
+    Error Init(sm::logprovider::LogObserverItf& logReceiver, const common::logprovider::Config& config);
+
+    /**
+     * Start the archivator manager.
+     *
+     * @return Error.
+     */
+    Error Start();
+
+    /**
+     * Stop the archivator manager.
+     *
+     * @return Error.
+     */
+    Error Stop();
+
+    /**
+     * Handles log data.
+     *
+     * @param log log data.
+     * @return Error.
+     */
+    Error HandleLog(std::shared_ptr<servicemanager::v4::LogData> log);
+
+private:
+    static constexpr auto cLogCleanupPeriod  = Time::cMinutes * 5;
+    static constexpr auto cLogPendingTimeout = Time::cSeconds * 10;
+
+    struct ArchiveContext {
+        ArchiveContext(const std::string& logID, sm::logprovider::LogObserverItf& logReceiver,
+            const common::logprovider::Config& config)
+            : mLogID(logID)
+            , mUpdated(Time::Now())
+            , mArchivator(logReceiver, config)
+        {
+        }
+
+        std::string                     mLogID;
+        Time                            mUpdated;
+        common::logprovider::Archivator mArchivator;
+    };
+
+    void                     Run();
+    Error                    CleanupOutdatedArchives();
+    Error                    Archive(std::shared_ptr<servicemanager::v4::LogData> log);
+    Error                    SendFinalChunk(std::shared_ptr<ArchiveContext> archiveContext);
+    cloudprotocol::LogStatus GetLogStatus(const std::shared_ptr<servicemanager::v4::LogData>& log) const;
+
+    Timer                                                    mTimer;
path: src/logprovider/archivatormanager.hpp
user: login: al1img
body: Use Poco::Timer
created_at: 2025-05-14T18:08:13Z
author_association: COLLABORATOR

PR comments:
user: login: codecov[bot]
created_at: 2025-05-08T14:25:18Z
body: ## [Codecov](https://app.codecov.io/gh/aosedge/aos_core_mp_cpp/pull/22?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aosedge) Report
Attention: Patch coverage is `90.97744%` with `24 lines` in your changes missing coverage. Please review.
> Please [upload](https://docs.codecov.com/docs/codecov-uploader) report for BASE (`develop@8243ae9`). [Learn more](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aosedge#section-missing-base-commit) about missing BASE report.

| [Files with missing lines](https://app.codecov.io/gh/aosedge/aos_core_mp_cpp/pull/22?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aosedge) | Patch % | Lines |
|---|---|---|
| [src/logprovider/archivemanager.cpp](https://app.codecov.io/gh/aosedge/aos_core_mp_cpp/pull/22?src=pr&el=tree&filepath=src%2Flogprovider%2Farchivemanager.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aosedge#diff-c3JjL2xvZ3Byb3ZpZGVyL2FyY2hpdmVtYW5hZ2VyLmNwcA==) | 86.04% | [12 Missing :warning: ](https://app.codecov.io/gh/aosedge/aos_core_mp_cpp/pull/22?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aosedge) |
| [src/communication/cmconnection.cpp](https://app.codecov.io/gh/aosedge/aos_core_mp_cpp/pull/22?src=pr&el=tree&filepath=src%2Fcommunication%2Fcmconnection.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aosedge#diff-c3JjL2NvbW11bmljYXRpb24vY21jb25uZWN0aW9uLmNwcA==) | 75.00% | [6 Missing :warning: ](https://app.codecov.io/gh/aosedge/aos_core_mp_cpp/pull/22?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aosedge) |
| [src/config/config.cpp](https://app.codecov.io/gh/aosedge/aos_core_mp_cpp/pull/22?src=pr&el=tree&filepath=src%2Fconfig%2Fconfig.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aosedge#diff-c3JjL2NvbmZpZy9jb25maWcuY3Bw) | 77.27% | [5 Missing :warning: ](https://app.codecov.io/gh/aosedge/aos_core_mp_cpp/pull/22?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aosedge) |
| [tests/logprovider/archivemanager\_test.cpp](https://app.codecov.io/gh/aosedge/aos_core_mp_cpp/pull/22?src=pr&el=tree&filepath=tests%2Flogprovider%2Farchivemanager_test.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aosedge#diff-dGVzdHMvbG9ncHJvdmlkZXIvYXJjaGl2ZW1hbmFnZXJfdGVzdC5jcHA=) | 98.78% | [1 Missing :warning: ](https://app.codecov.io/gh/aosedge/aos_core_mp_cpp/pull/22?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aosedge) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             develop      #22   +/-   ##
==========================================
  Coverage           ?   85.11%           
==========================================
  Files              ?       39           
  Lines              ?     3829           
  Branches           ?      319           
==========================================
  Hits               ?     3259           
  Misses             ?      570           
  Partials           ?        0           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/aosedge/aos_core_mp_cpp/pull/22?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aosedge).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aosedge).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-05-15T15:53:21Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=aosedge_aos_core_mp_cpp&pullRequest=22) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [12 New issues](https://sonarcloud.io/project/issues?id=aosedge_aos_core_mp_cpp&pullRequest=22&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=aosedge_aos_core_mp_cpp&pullRequest=22&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=aosedge_aos_core_mp_cpp&pullRequest=22&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=aosedge_aos_core_mp_cpp&pullRequest=22&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=aosedge_aos_core_mp_cpp&pullRequest=22&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aosedge_aos_core_mp_cpp&pullRequest=22)


author_association: NONE