diff --git a/Analytics/Analytics.conf.in b/Analytics/Analytics.conf.in index c8ea66e069..0a6fe352e8 100644 --- a/Analytics/Analytics.conf.in +++ b/Analytics/Analytics.conf.in @@ -21,4 +21,7 @@ if boolean("@PLUGIN_ANALYTICS_SIFT_BACKEND_ENABLED@"): sift.add("minretryperiod", "@PLUGIN_ANALYTICS_SIFT_MIN_RETRY_PERIOD@") sift.add("maxretryperiod", "@PLUGIN_ANALYTICS_SIFT_MAX_RETRY_PERIOD@") sift.add("exponentialperiodicfactor", "@PLUGIN_ANALYTICS_SIFT_EXPONENTIAL_PERIODIC_FACTOR@") + sift.add("storepath", "@PLUGIN_ANALYTICS_SIFT_STORE_PATH@") + sift.add("eventslimit", "@PLUGIN_ANALYTICS_SIFT_STORE_EVENTS_LIMIT@") + sift.add("url", "@PLUGIN_ANALYTICS_SIFT_URL@") configuration.add("sift", sift) \ No newline at end of file diff --git a/Analytics/Analytics.config b/Analytics/Analytics.config index a78833fe54..0d57afeb1a 100644 --- a/Analytics/Analytics.config +++ b/Analytics/Analytics.config @@ -26,6 +26,7 @@ if(PLUGIN_ANALYTICS_SIFT_BACKEND_ENABLED) kv(minretryperiod, ${PLUGIN_ANALYTICS_SIFT_MIN_RETRY_PERIOD}) kv(maxretryperiod, ${PLUGIN_ANALYTICS_SIFT_MAX_RETRY_PERIOD}) kv(exponentialperiodicfactor, ${PLUGIN_ANALYTICS_SIFT_EXPONENTIAL_PERIODIC_FACTOR}) + kv(storepath, ${PLUGIN_ANALYTICS_SIFT_STORE_PATH}) end() ans(siftobject) map_append(${configuration} sift ${siftobject}) diff --git a/Analytics/Analytics.h b/Analytics/Analytics.h index 4bba57c2d0..8b8b1434a3 100644 --- a/Analytics/Analytics.h +++ b/Analytics/Analytics.h @@ -68,6 +68,9 @@ namespace WPEFramework { END_INTERFACE_MAP static const string ANALYTICS_METHOD_SEND_EVENT; + static const string ANALYTICS_METHOD_SET_SESSION_ID; + static const string ANALYTICS_METHOD_SET_TIME_READY; + private: void Deactivated(RPC::IRemoteConnection* connection); // JSONRPC methods @@ -75,6 +78,8 @@ namespace WPEFramework { void UnregisterAll(); uint32_t SendEventWrapper(const JsonObject& parameters, JsonObject& response); + uint32_t SetSessionIdWrapper(const JsonObject& parameters, JsonObject& response); + uint32_t SetTimeReadyWrapper(const JsonObject& parameters, JsonObject& response); private: PluginHost::IShell* mService; diff --git a/Analytics/AnalyticsJsonRpc.cpp b/Analytics/AnalyticsJsonRpc.cpp index 2a0a3f5ade..b3238482ce 100644 --- a/Analytics/AnalyticsJsonRpc.cpp +++ b/Analytics/AnalyticsJsonRpc.cpp @@ -21,6 +21,9 @@ #include "UtilsJsonRpc.h" const string WPEFramework::Plugin::Analytics::ANALYTICS_METHOD_SEND_EVENT = "sendEvent"; +// TODO: To be removed once the Analytics is capable of handling it internally +const string WPEFramework::Plugin::Analytics::ANALYTICS_METHOD_SET_SESSION_ID = "setSessionId"; +const string WPEFramework::Plugin::Analytics::ANALYTICS_METHOD_SET_TIME_READY = "setTimeReady"; namespace WPEFramework { @@ -31,11 +34,15 @@ namespace Plugin { void Analytics::RegisterAll() { Register(_T(ANALYTICS_METHOD_SEND_EVENT), &Analytics::SendEventWrapper, this); + Register(_T(ANALYTICS_METHOD_SET_SESSION_ID), &Analytics::SetSessionIdWrapper, this); + Register(_T(ANALYTICS_METHOD_SET_TIME_READY), &Analytics::SetTimeReadyWrapper, this); } void Analytics::UnregisterAll() { Unregister(_T(ANALYTICS_METHOD_SEND_EVENT)); + Unregister(_T(ANALYTICS_METHOD_SET_SESSION_ID)); + Unregister(_T(ANALYTICS_METHOD_SET_TIME_READY)); } // API implementation @@ -83,6 +90,40 @@ namespace Plugin { returnResponse(result); } + // Method: setSessionId - Set the session ID + // Return codes: + // - ERROR_NONE: Success + // - ERROR_GENERAL: Failed to set the session ID + uint32_t Analytics::SetSessionIdWrapper(const JsonObject& parameters, JsonObject& response) + { + LOGINFOMETHOD(); + + uint32_t result = Core::ERROR_NONE; + + returnIfStringParamNotFound(parameters, "sessionId"); + + string sessionId = parameters["sessionId"].String(); + + result = mAnalytics->SetSessionId(sessionId); + + returnResponse(result); + } + + // Method: setTimeReady - Set the time ready + // Return codes: + // - ERROR_NONE: Success + // - ERROR_GENERAL: Failed to set the time ready + uint32_t Analytics::SetTimeReadyWrapper(const JsonObject& parameters, JsonObject& response) + { + LOGINFOMETHOD(); + + uint32_t result = Core::ERROR_NONE; + + result = mAnalytics->SetTimeReady(); + + returnResponse(result); + } + } } \ No newline at end of file diff --git a/Analytics/CMakeLists.txt b/Analytics/CMakeLists.txt index 07ccd7147e..72814e243f 100644 --- a/Analytics/CMakeLists.txt +++ b/Analytics/CMakeLists.txt @@ -48,7 +48,9 @@ set(PLUGIN_ANALYTICS_SIFT_MAX_RETRIES 10 CACHE STRING "Sift max retries posting set(PLUGIN_ANALYTICS_SIFT_MIN_RETRY_PERIOD 1 CACHE STRING "Sift min retry period seconds") set(PLUGIN_ANALYTICS_SIFT_MAX_RETRY_PERIOD 30 CACHE STRING "Sift max retry period seconds") set(PLUGIN_ANALYTICS_SIFT_EXPONENTIAL_PERIODIC_FACTOR 2 CACHE STRING "Sift exponential periodic factor") - +set(PLUGIN_ANALYTICS_SIFT_STORE_PATH "/opt/persistent/sky/AnalyticsSiftStore" CACHE STRING "Sift store path") +set(PLUGIN_ANALYTICS_SIFT_STORE_EVENTS_LIMIT 1000 CACHE STRING "Sift store events limit") +set(PLUGIN_ANALYTICS_SIFT_URL "https://sift.rdkcloud.com/v1/events" CACHE STRING "Sift URL") message("Setup ${MODULE_NAME} v${MODULE_VERSION}") @@ -63,6 +65,7 @@ add_library(${MODULE_NAME} SHARED target_include_directories(${MODULE_NAME} PRIVATE Implementation) target_include_directories(${MODULE_NAME} PRIVATE ../helpers) +add_subdirectory(Implementation/LocalStore) add_subdirectory(Implementation/Backend) set_target_properties(${MODULE_NAME} PROPERTIES diff --git a/Analytics/Implementation/AnalyticsImplementation.cpp b/Analytics/Implementation/AnalyticsImplementation.cpp index e423717a9e..1d39da461f 100644 --- a/Analytics/Implementation/AnalyticsImplementation.cpp +++ b/Analytics/Implementation/AnalyticsImplementation.cpp @@ -95,6 +95,28 @@ namespace Plugin { return Core::ERROR_NONE; } + uint32_t AnalyticsImplementation::SetSessionId(const string& id) + { + uint32_t ret = Core::ERROR_GENERAL; + // set session id in sift backend + if (mBackends.find(IAnalyticsBackend::SIFT) != mBackends.end()) + { + ret = mBackends.at(IAnalyticsBackend::SIFT).SetSessionId(id); + } + + return ret; + } + + uint32_t AnalyticsImplementation::SetTimeReady() + { + // set time ready action + std::unique_lock lock(mQueueMutex); + mActionQueue.push({ACTION_TYPE_SET_TIME_READY, nullptr}); + lock.unlock(); + mQueueCondition.notify_one(); + return Core::ERROR_NONE; + } + uint32_t AnalyticsImplementation::Configure(PluginHost::IShell* shell) { LOGINFO("Configuring Analytics"); @@ -137,7 +159,7 @@ namespace Plugin { if (mActionQueue.empty() && !mSysTimeValid) { - action = {ACTION_POPULATE_DEVICE_INFO, nullptr}; + action = {ACTION_POPULATE_TIME_INFO, nullptr}; } else { @@ -148,9 +170,9 @@ namespace Plugin { lock.unlock(); switch (action.type) { - case ACTION_POPULATE_DEVICE_INFO: + case ACTION_POPULATE_TIME_INFO: - mSysTimeValid = IsSysTimeValid(); + //mSysTimeValid = IsSysTimeValid(); if ( mSysTimeValid ) { @@ -161,7 +183,7 @@ namespace Plugin { mEventQueue.pop(); } } - break; + break; case ACTION_TYPE_SEND_EVENT: if (mSysTimeValid) @@ -191,6 +213,10 @@ namespace Plugin { break; case ACTION_TYPE_SHUTDOWN: return; + case ACTION_TYPE_SET_TIME_READY: + { + mSysTimeValid = true; + }break; default: break; } @@ -202,14 +228,7 @@ namespace Plugin { bool AnalyticsImplementation::IsSysTimeValid() { bool ret = false; - //TODO: Check here if time is OK - // for now check if /tmp/as_timezone_ready exists - std::ifstream timezoneFile("/tmp/as_timezone_ready"); - if (timezoneFile.is_open()) - { - timezoneFile.close(); - ret = true; - } + //TODO: Add system time validation return ret; } diff --git a/Analytics/Implementation/AnalyticsImplementation.h b/Analytics/Implementation/AnalyticsImplementation.h index ae1b50509f..2dc6f98377 100644 --- a/Analytics/Implementation/AnalyticsImplementation.h +++ b/Analytics/Implementation/AnalyticsImplementation.h @@ -49,9 +49,10 @@ namespace Plugin { enum ActionType { ACTION_TYPE_UNDEF, - ACTION_POPULATE_DEVICE_INFO, + ACTION_POPULATE_TIME_INFO, ACTION_TYPE_SEND_EVENT, - ACTION_TYPE_SHUTDOWN + ACTION_TYPE_SHUTDOWN, + ACTION_TYPE_SET_TIME_READY }; struct Event @@ -70,6 +71,7 @@ namespace Plugin { { ActionType type; std::shared_ptr payload; + std::string id; }; @@ -82,6 +84,9 @@ namespace Plugin { const uint64_t& epochTimestamp, const uint64_t& uptimeTimestamp, const string& eventPayload) override; + uint32_t SetSessionId(const string& id) override; + uint32_t SetTimeReady() override; + // IConfiguration interface uint32_t Configure(PluginHost::IShell* shell); @@ -99,7 +104,7 @@ namespace Plugin { std::thread mThread; std::queue mActionQueue; std::queue mEventQueue; - IAnalyticsBackends mBackends; + const IAnalyticsBackends mBackends; bool mSysTimeValid; PluginHost::IShell* mShell; }; diff --git a/Analytics/Implementation/Backend/AnalyticsBackend.h b/Analytics/Implementation/Backend/AnalyticsBackend.h index 1a714a232f..5ee93b37b7 100644 --- a/Analytics/Implementation/Backend/AnalyticsBackend.h +++ b/Analytics/Implementation/Backend/AnalyticsBackend.h @@ -44,6 +44,7 @@ namespace Plugin { virtual uint32_t Configure(PluginHost::IShell* shell) = 0; virtual uint32_t SendEvent(const Event& event) = 0; + virtual uint32_t SetSessionId(const std::string& sessionId) = 0; }; typedef std::map IAnalyticsBackends; diff --git a/Analytics/Implementation/Backend/Sift/CMakeLists.txt b/Analytics/Implementation/Backend/Sift/CMakeLists.txt index bd727dd9a3..5c7992fc32 100644 --- a/Analytics/Implementation/Backend/Sift/CMakeLists.txt +++ b/Analytics/Implementation/Backend/Sift/CMakeLists.txt @@ -18,7 +18,7 @@ set(TARGET_LIB ${NAMESPACE}${PLUGIN_NAME}SiftBackend) add_library(${TARGET_LIB} STATIC) -target_sources(${TARGET_LIB} PRIVATE SiftBackend.cpp SiftConfig.cpp) +target_sources(${TARGET_LIB} PRIVATE SiftBackend.cpp SiftConfig.cpp SiftStore.cpp SiftUploader.cpp) find_package(CURL) if (CURL_FOUND) @@ -28,8 +28,10 @@ else (CURL_FOUND) message ("Curl/libcurl required.") endif (CURL_FOUND) + target_include_directories(${TARGET_LIB} PUBLIC "${CMAKE_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}") target_include_directories(${TARGET_LIB} PRIVATE ../../../../helpers) +target_include_directories(${TARGET_LIB} PRIVATE ../../LocalStore) set_property(TARGET ${TARGET_LIB} PROPERTY POSITION_INDEPENDENT_CODE ON) set_target_properties(${TARGET_LIB} PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF) -target_link_libraries(${TARGET_LIB} PRIVATE ${NAMESPACE}Plugins::${NAMESPACE}Plugins) \ No newline at end of file +target_link_libraries(${TARGET_LIB} PRIVATE ${NAMESPACE}Plugins::${NAMESPACE}Plugins ${NAMESPACE}${PLUGIN_NAME}LocalStore) \ No newline at end of file diff --git a/Analytics/Implementation/Backend/Sift/SiftBackend.cpp b/Analytics/Implementation/Backend/Sift/SiftBackend.cpp index 57011de74b..a653958dab 100644 --- a/Analytics/Implementation/Backend/Sift/SiftBackend.cpp +++ b/Analytics/Implementation/Backend/Sift/SiftBackend.cpp @@ -25,8 +25,6 @@ #include #include -#include - namespace WPEFramework { namespace Plugin @@ -40,7 +38,8 @@ namespace WPEFramework , mActionQueue() , mShell(nullptr) , mConfigPtr(nullptr) - + , mStorePtr(nullptr) + , mUploaderPtr(nullptr) { mThread = std::thread(&SiftBackend::ActionLoop, this); mThread.detach(); @@ -77,14 +76,29 @@ namespace WPEFramework return Core::ERROR_NONE; } + /* virtual */ uint32_t SiftBackend::SetSessionId(const std::string &sessionId) + { + std::unique_lock lock(mQueueMutex); + if (mConfigPtr != nullptr) + { + mConfigPtr->SetSessionId(sessionId); + return Core::ERROR_NONE; + } + return Core::ERROR_GENERAL; + } + void SiftBackend::ActionLoop() { std::unique_lock lock(mQueueMutex); bool configValid = false; + + SiftConfig::StoreConfig storeConfig; + SiftConfig::UploaderConfig uploaderConfig; + while (true) { std::chrono::milliseconds queueTimeout(std::chrono::milliseconds::max()); - SiftConfig::Config config; + SiftConfig::Attributes attributes; if (!configValid) { @@ -112,10 +126,54 @@ namespace WPEFramework mActionQueue.pop(); } - //Always get the most recent config - if (mConfigPtr != nullptr && mConfigPtr->Get(config)) + //Always get the most recent attributes + bool attributesValid = false; + bool storeConfigValid = (mStorePtr != nullptr); + bool uploaderConfigValid = (mUploaderPtr != nullptr); + if (mConfigPtr != nullptr && mConfigPtr->GetAttributes(attributes)) + { + attributesValid = true; + } + + if (mConfigPtr != nullptr) + { + if (mStorePtr == nullptr && mConfigPtr->GetStoreConfig(storeConfig)) + { + mStorePtr = std::make_shared(storeConfig.path, + storeConfig.eventsLimit); + if (mStorePtr != nullptr) + { + storeConfigValid = true; + } + } + + if (mUploaderPtr == nullptr && mStorePtr != nullptr && mConfigPtr->GetUploaderConfig(uploaderConfig)) + { + mUploaderPtr = std::unique_ptr(new SiftUploader(mStorePtr, + uploaderConfig.url, + uploaderConfig.apiKey, + uploaderConfig.maxRandomisationWindowTime, + uploaderConfig.maxEventsInPost, + uploaderConfig.maxRetries, + uploaderConfig.minRetryPeriod, + uploaderConfig.maxRetryPeriod, + uploaderConfig.exponentialPeriodicFactor)); + if (mUploaderPtr != nullptr) + { + uploaderConfigValid = true; + } + } + } + + if (attributesValid && storeConfigValid && uploaderConfigValid) { configValid = true; + // For Sift 1.0 update uploader with auth values if avaliable + // So they will be added to the events if missing + if (!attributes.schema2Enabled && !attributes.accountId.empty() && !attributes.deviceId.empty() && !attributes.partnerId.empty()) + { + mUploaderPtr->setDeviceInfoRequiredFields(attributes.accountId, attributes.deviceId, attributes.partnerId); + } } else { @@ -131,7 +189,7 @@ namespace WPEFramework if (configValid) { // Try to send the events from the queue - while (!mEventQueue.empty() && SendEventInternal(mEventQueue.front(), config)) + while (!mEventQueue.empty() && SendEventInternal(mEventQueue.front(), attributes)) { mEventQueue.pop(); } @@ -146,7 +204,7 @@ namespace WPEFramework case ACTION_TYPE_SEND_EVENT: if (configValid) { - SendEventInternal(*action.payload, config); + SendEventInternal(*action.payload, attributes); } else { @@ -163,20 +221,20 @@ namespace WPEFramework } } - bool SiftBackend::SendEventInternal(const Event &event, const SiftConfig::Config &config) + bool SiftBackend::SendEventInternal(const Event &event, const SiftConfig::Attributes &attributes) { JsonObject eventJson = JsonObject(); - if (config.schema2Enabled) + if (attributes.schema2Enabled) { // Sift 2.0 schema - eventJson["common_schema"] = config.commonSchema; - if (!config.env.empty()) + eventJson["common_schema"] = attributes.commonSchema; + if (!attributes.env.empty()) { - eventJson["env"] = config.env; + eventJson["env"] = attributes.env; } - eventJson["product_name"] = config.productName; - eventJson["product_version"] = config.productVersion; - eventJson["event_schema"] = config.productName + "/" + event.eventName + "/" + event.eventVersion; + eventJson["product_name"] = attributes.productName; + eventJson["product_version"] = attributes.productVersion; + eventJson["event_schema"] = attributes.productName + "/" + event.eventName + "/" + event.eventVersion; eventJson["event_name"] = event.eventName; eventJson["timestamp"] = event.epochTimestamp; eventJson["event_id"] = GenerateRandomUUID(); @@ -191,65 +249,65 @@ namespace WPEFramework } eventJson["cet_list"] = cetList; } - eventJson["logger_name"] = config.loggerName; - eventJson["logger_version"] = config.loggerVersion; - eventJson["partner_id"] = config.partnerId; - if (config.activated) + eventJson["logger_name"] = attributes.loggerName; + eventJson["logger_version"] = attributes.loggerVersion; + eventJson["partner_id"] = attributes.partnerId; + if (attributes.activated) { - eventJson["xbo_account_id"] = config.xboAccountId; - eventJson["xbo_device_id"] = config.xboDeviceId; - eventJson["activated"] = config.activated; + eventJson["xbo_account_id"] = attributes.xboAccountId; + eventJson["xbo_device_id"] = attributes.xboDeviceId; + eventJson["activated"] = attributes.activated; } - eventJson["device_model"] = config.deviceModel; - eventJson["device_type"] = config.deviceType; - eventJson["device_timezone"] = std::stoi(config.deviceTimeZone); - eventJson["device_os_name"] = config.deviceOsName; - eventJson["device_os_version"] = config.deviceOsVersion; - eventJson["platform"] = config.platform; - eventJson["device_manufacturer"] = config.deviceManufacturer; - eventJson["authenticated"] = config.authenticated; - eventJson["session_id"] = config.sessionId; - eventJson["proposition"] = config.proposition; - if (!config.retailer.empty()) + eventJson["device_model"] = attributes.deviceModel; + eventJson["device_type"] = attributes.deviceType; + eventJson["device_timezone"] = std::stoi(attributes.deviceTimeZone); + eventJson["device_os_name"] = attributes.deviceOsName; + eventJson["device_os_version"] = attributes.deviceOsVersion; + eventJson["platform"] = attributes.platform; + eventJson["device_manufacturer"] = attributes.deviceManufacturer; + eventJson["authenticated"] = attributes.authenticated; + eventJson["session_id"] = attributes.sessionId; + eventJson["proposition"] = attributes.proposition; + if (!attributes.retailer.empty()) { - eventJson["retailer"] = config.retailer; + eventJson["retailer"] = attributes.retailer; } - if (!config.jvAgent.empty()) + if (!attributes.jvAgent.empty()) { - eventJson["jv_agent"] = config.jvAgent; + eventJson["jv_agent"] = attributes.jvAgent; } - if (!config.coam.empty()) + if (!attributes.coam.empty()) { - eventJson["coam"] = JsonValue(config.coam == "true"); + eventJson["coam"] = JsonValue(attributes.coam == "true"); } - eventJson["device_serial_number"] = config.deviceSerialNumber; - if (!config.deviceFriendlyName.empty()) + eventJson["device_serial_number"] = attributes.deviceSerialNumber; + if (!attributes.deviceFriendlyName.empty()) { - eventJson["device_friendly_name"] = config.deviceFriendlyName; + eventJson["device_friendly_name"] = attributes.deviceFriendlyName; } - if (!config.deviceMacAddress.empty()) + if (!attributes.deviceMacAddress.empty()) { - eventJson["device_mac_address"] = config.deviceMacAddress; + eventJson["device_mac_address"] = attributes.deviceMacAddress; } - if (!config.country.empty()) + if (!attributes.country.empty()) { - eventJson["country"] = config.country; + eventJson["country"] = attributes.country; } - if (!config.region.empty()) + if (!attributes.region.empty()) { - eventJson["region"] = config.region; + eventJson["region"] = attributes.region; } - if (!config.accountType.empty()) + if (!attributes.accountType.empty()) { - eventJson["account_type"] = config.accountType; + eventJson["account_type"] = attributes.accountType; } - if (!config.accountOperator.empty()) + if (!attributes.accountOperator.empty()) { - eventJson["operator"] = config.accountOperator; + eventJson["operator"] = attributes.accountOperator; } - if (!config.accountDetailType.empty()) + if (!attributes.accountDetailType.empty()) { - eventJson["account_detail_type"] = config.accountDetailType; + eventJson["account_detail_type"] = attributes.accountDetailType; } eventJson["event_payload"] = JsonObject(event.eventPayload); @@ -258,54 +316,45 @@ namespace WPEFramework { //Sift 1.0 eventJson["event_name"] = event.eventName; - eventJson["event_schema"] = config.productName + "/" + event.eventName + "/" + event.eventVersion; + eventJson["event_schema"] = attributes.productName + "/" + event.eventName + "/" + event.eventVersion; eventJson["event_payload"] = JsonObject(event.eventPayload); - eventJson["session_id"] = config.sessionId; + eventJson["session_id"] = attributes.sessionId; eventJson["event_id"] = GenerateRandomUUID(); - if (!config.accountId.empty() && !config.deviceId.empty() && !config.partnerId.empty()) + if (!attributes.accountId.empty() && !attributes.deviceId.empty() && !attributes.partnerId.empty()) { - eventJson["account_id"] = config.accountId; - eventJson["device_id"] = config.deviceId; - eventJson["partner_id"] = config.partnerId; + eventJson["account_id"] = attributes.accountId; + eventJson["device_id"] = attributes.deviceId; + eventJson["partner_id"] = attributes.partnerId; } else { std::cout << "Sift: Account ID, Device ID, or Partner ID is empty for: " << event.eventName << std::endl; } - eventJson["app_name"] = config.deviceAppName; - eventJson["app_ver"] = config.deviceAppVersion; - eventJson["device_model"] = config.deviceModel; - eventJson["device_timezone"] = std::stoi(config.deviceTimeZone); - eventJson["platform"] = config.platform; - eventJson["os_ver"] = config.deviceSoftwareVersion; + eventJson["app_name"] = attributes.deviceAppName; + eventJson["app_ver"] = attributes.deviceAppVersion; + eventJson["device_model"] = attributes.deviceModel; + eventJson["device_timezone"] = std::stoi(attributes.deviceTimeZone); + eventJson["platform"] = attributes.platform; + eventJson["os_ver"] = attributes.deviceSoftwareVersion; eventJson["device_language"] = ""; // Empty for now eventJson["timestamp"] = event.epochTimestamp; - eventJson["device_type"] = config.deviceType; + eventJson["device_type"] = attributes.deviceType; } - // TODO: push to persistent queue instead of sending array - JsonArray eventArray = JsonArray(); - eventArray.Add(eventJson); - std::string json; - eventArray.ToString(json); - LOGINFO("Sending event: %s", json.c_str()); + eventJson.ToString(json); - // Upload the event to Sift - uint32_t httpCode = PostJson(config.url, config.apiKey, json); - if (httpCode == 400) + if (mStorePtr != nullptr + && mStorePtr->PostEvent(json)) { - LOGINFO("Backend refused data, skipping: %s, HTTP Code: %d", event.eventName.c_str(), httpCode); + LOGINFO("Event %s sent to store", event.eventName.c_str()); return true; } - if (httpCode == 200) - { - LOGINFO("Backend accepted data: %s, HTTP Code: %d", event.eventName.c_str(), httpCode); - return true; - } - LOGERR("Backend failed to accept data: %s, HTTP COde: %d", event.eventName.c_str(), httpCode); + + LOGERR("Failed to send event %s to store", event.eventName.c_str()); + return false; } @@ -346,67 +395,5 @@ namespace WPEFramework return randomUUIDStream.str(); } - static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { - ((std::string*)userp)->append((char*)contents, size * nmemb); - return size * nmemb; - } - - uint32_t SiftBackend::PostJson(const std::string &url, const std::string &apiKey, const std::string &json) - { - CURL *curl; - CURLcode res; - uint32_t retHttpCode = 0; - std::string response; - - if (url.empty() || apiKey.empty() || json.empty()) - { - LOGERR("Invalid parameters for postJson"); - return retHttpCode; - } - - curl = curl_easy_init(); - if (!curl) - { - LOGERR("Failed to initialize curl"); - return retHttpCode; - } - - curl_easy_setopt(curl, CURLOPT_POST, 1L); - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.data()); - - // Create a linked list of custom headers - struct curl_slist *headers = NULL; - std::string keyHeader("X-Api-Key: " + apiKey); - headers = curl_slist_append(headers, "Content-Type: application/json"); - headers = curl_slist_append(headers, keyHeader.data()); - - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); - - // Perform the request, res will get the return code - res = curl_easy_perform(curl); - - // Check for errors - if (res != CURLE_OK) - { - LOGERR("curl_easy_perform() failed: %s", curl_easy_strerror(res)); - } - else - { - LOGINFO("Response: %s", response.c_str()); - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &retHttpCode); - } - - // Clean up the header list - curl_slist_free_all(headers); - - // Clean up curl session - curl_easy_cleanup(curl); - - return retHttpCode; - } } } \ No newline at end of file diff --git a/Analytics/Implementation/Backend/Sift/SiftBackend.h b/Analytics/Implementation/Backend/Sift/SiftBackend.h index cac39d91dd..e7f167a0b4 100644 --- a/Analytics/Implementation/Backend/Sift/SiftBackend.h +++ b/Analytics/Implementation/Backend/Sift/SiftBackend.h @@ -20,6 +20,8 @@ #include "../AnalyticsBackend.h" #include "SiftConfig.h" +#include "SiftStore.h" +#include "SiftUploader.h" #include #include @@ -39,6 +41,7 @@ namespace Plugin { ~SiftBackend(); uint32_t SendEvent(const Event& event) override; uint32_t Configure(PluginHost::IShell* shell) override; + uint32_t SetSessionId(const std::string& sessionId) override; private: @@ -64,12 +67,11 @@ namespace Plugin { }; void ActionLoop(); - bool SendEventInternal(const Event& event, const SiftConfig::Config &config); + bool SendEventInternal(const Event& event, const SiftConfig::Attributes &attributes); static uint8_t GenerateRandomCharacter(); static std::string GenerateRandomOctetString( uint32_t numOctets ); static std::string GenerateRandomUUID(); - static uint32_t PostJson(const std::string& url, const std::string& apiKey, const std::string& json); std::mutex mQueueMutex; std::condition_variable mQueueCondition; @@ -79,6 +81,8 @@ namespace Plugin { PluginHost::IShell* mShell; SiftConfigPtr mConfigPtr; + SiftStorePtr mStorePtr; + SiftUploaderPtr mUploaderPtr; }; } diff --git a/Analytics/Implementation/Backend/Sift/SiftConfig.cpp b/Analytics/Implementation/Backend/Sift/SiftConfig.cpp index 73d1824dca..eadc80c084 100644 --- a/Analytics/Implementation/Backend/Sift/SiftConfig.cpp +++ b/Analytics/Implementation/Backend/Sift/SiftConfig.cpp @@ -21,6 +21,7 @@ #include #include +#include #define AUTHSERVICE_CALLSIGN "org.rdk.AuthService" #define SYSTEM_CALLSIGN "org.rdk.System" @@ -29,8 +30,6 @@ #define PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE "accountProfile" #define JSONRPC_THUNDER_TIMEOUT 20000 -#include - namespace WPEFramework { namespace Plugin @@ -53,9 +52,19 @@ namespace WPEFramework , Schema2(false) , CommonSchema() , Env() - , ProductName() + , ProductName("rdk") , LoggerName() , LoggerVersion() + , MaxRandomisationWindowTime(300) + , MaxEventsInPost(10) + , MaxRetries(10) + , MinRetryPeriod(1) + , MaxRetryPeriod(30) + , ExponentialPeriodicFactor(2) + , StorePath("/opt/persistent/sky/AnalyticsSiftStore") + , EventsLimit(1000) + , Url() + { Add(_T("schema2"), &Schema2); Add(_T("commonschema"), &CommonSchema); @@ -63,6 +72,15 @@ namespace WPEFramework Add(_T("productname"), &ProductName); Add(_T("loggername"), &LoggerName); Add(_T("loggerversion"), &LoggerVersion); + Add(_T("maxrandomisationwindowtime"), &MaxRandomisationWindowTime); + Add(_T("maxeventsinpost"), &MaxEventsInPost); + Add(_T("maxretries"), &MaxRetries); + Add(_T("minretryperiod"), &MinRetryPeriod); + Add(_T("maxretryperiod"), &MaxRetryPeriod); + Add(_T("exponentialperiodicfactor"), &ExponentialPeriodicFactor); + Add(_T("storepath"), &StorePath); + Add(_T("eventslimit"), &EventsLimit); + Add(_T("url"), &Url); } ~SiftConfig() = default; @@ -73,6 +91,15 @@ namespace WPEFramework Core::JSON::String ProductName; Core::JSON::String LoggerName; Core::JSON::String LoggerVersion; + Core::JSON::DecUInt32 MaxRandomisationWindowTime; + Core::JSON::DecUInt32 MaxEventsInPost; + Core::JSON::DecUInt32 MaxRetries; + Core::JSON::DecUInt32 MinRetryPeriod; + Core::JSON::DecUInt32 MaxRetryPeriod; + Core::JSON::DecUInt32 ExponentialPeriodicFactor; + Core::JSON::String StorePath; + Core::JSON::DecUInt32 EventsLimit; + Core::JSON::String Url; }; @@ -279,12 +306,14 @@ namespace WPEFramework SiftConfig::SiftConfig(PluginHost::IShell *shell) : mInitializationThread(), mMonitorKeys(), mMutex(), - mConfig(), + mAttributes(), + mStoreConfig(), + mUploaderConfig(), mShell(shell) { ASSERT(shell != nullptr); - InitializeKeysMap(); ParsePluginConfig(); + InitializeKeysMap(); TriggerInitialization(); } @@ -305,16 +334,8 @@ namespace WPEFramework } } - bool SiftConfig::Get(SiftConfig::Config &config) + bool SiftConfig::GetAttributes(SiftConfig::Attributes &attributes) { - //Read /tmp/sift_session evrytime to get the latest session id - std::ifstream sessionFile("/tmp/sift_session"); - if (sessionFile.is_open()) - { - std::getline(sessionFile, mConfig.sessionId); - sessionFile.close(); - } - // Get latest values from AuthService GetAuthServiceValues(); @@ -322,33 +343,31 @@ namespace WPEFramework bool valid = false; - if (mConfig.schema2Enabled) + if (mAttributes.schema2Enabled) { //Sift 2.0 requires attributes - bool activatedValid = mConfig.activated ? (!mConfig.xboDeviceId.empty() - && !mConfig.xboAccountId.empty()) : true; - - valid = ( !mConfig.url.empty() - && !mConfig.apiKey.empty() - && !mConfig.sessionId.empty() - && !mConfig.commonSchema.empty() - && !mConfig.productName.empty() - && !mConfig.productVersion.empty() - && !mConfig.loggerName.empty() - && !mConfig.loggerVersion.empty() - && !mConfig.partnerId.empty() + bool activatedValid = mAttributes.activated ? (!mAttributes.xboDeviceId.empty() + && !mAttributes.xboAccountId.empty()) : true; + + valid = (!mAttributes.sessionId.empty() + && !mAttributes.commonSchema.empty() + && !mAttributes.productName.empty() + && !mAttributes.productVersion.empty() + && !mAttributes.loggerName.empty() + && !mAttributes.loggerVersion.empty() + && !mAttributes.partnerId.empty() && activatedValid - && !mConfig.deviceModel.empty() - && !mConfig.deviceType.empty() - && !mConfig.deviceTimeZone.empty() - && !mConfig.deviceOsName.empty() - && !mConfig.deviceOsVersion.empty() - && !mConfig.platform.empty() - && !mConfig.deviceManufacturer.empty() - && !mConfig.sessionId.empty() - && !mConfig.proposition.empty() - && !mConfig.deviceSerialNumber.empty() - && !mConfig.deviceMacAddress.empty()); + && !mAttributes.deviceModel.empty() + && !mAttributes.deviceType.empty() + && !mAttributes.deviceTimeZone.empty() + && !mAttributes.deviceOsName.empty() + && !mAttributes.deviceOsVersion.empty() + && !mAttributes.platform.empty() + && !mAttributes.deviceManufacturer.empty() + && !mAttributes.sessionId.empty() + && !mAttributes.proposition.empty() + && !mAttributes.deviceSerialNumber.empty() + && !mAttributes.deviceMacAddress.empty()); LOGINFO(" commonSchema: %s," " productName: %s," @@ -368,70 +387,100 @@ namespace WPEFramework " proposition: %s," " deviceSerialNumber: %s," " deviceMacAddress: %s,", - mConfig.commonSchema.c_str(), - mConfig.productName.c_str(), - mConfig.productVersion.c_str(), - mConfig.loggerName.c_str(), - mConfig.loggerVersion.c_str(), - mConfig.partnerId.c_str(), + mAttributes.commonSchema.c_str(), + mAttributes.productName.c_str(), + mAttributes.productVersion.c_str(), + mAttributes.loggerName.c_str(), + mAttributes.loggerVersion.c_str(), + mAttributes.partnerId.c_str(), activatedValid, - mConfig.deviceModel.c_str(), - mConfig.deviceType.c_str(), - mConfig.deviceTimeZone.c_str(), - mConfig.deviceOsName.c_str(), - mConfig.deviceOsVersion.c_str(), - mConfig.platform.c_str(), - mConfig.deviceManufacturer.c_str(), - mConfig.sessionId.c_str(), - mConfig.proposition.c_str(), - mConfig.deviceSerialNumber.c_str(), - mConfig.deviceMacAddress.c_str()); + mAttributes.deviceModel.c_str(), + mAttributes.deviceType.c_str(), + mAttributes.deviceTimeZone.c_str(), + mAttributes.deviceOsName.c_str(), + mAttributes.deviceOsVersion.c_str(), + mAttributes.platform.c_str(), + mAttributes.deviceManufacturer.c_str(), + mAttributes.sessionId.c_str(), + mAttributes.proposition.c_str(), + mAttributes.deviceSerialNumber.c_str(), + mAttributes.deviceMacAddress.c_str()); if (valid) { - if (mConfig.deviceType == "TV") + if (mAttributes.deviceType == "TV") { - mConfig.deviceType = "IPTV"; + mAttributes.deviceType = "IPTV"; } - else if (mConfig.deviceType == "IPSETTOPBOX") + else if (mAttributes.deviceType == "IPSETTOPBOX") { - mConfig.deviceType = "IPSTB"; + mAttributes.deviceType = "IPSTB"; } } } else //Sift 1.0 required attributes { - valid = (!mConfig.url.empty() - && !mConfig.apiKey.empty() - && !mConfig.sessionId.empty() - && !mConfig.productName.empty() - && !mConfig.deviceAppName.empty() - && !mConfig.deviceAppVersion.empty() - && !mConfig.deviceModel.empty() - && !mConfig.deviceTimeZone.empty() - && !mConfig.platform.empty() - && !mConfig.deviceSoftwareVersion.empty() - && !mConfig.deviceType.empty()); + valid = (!mAttributes.sessionId.empty() + && !mAttributes.productName.empty() + && !mAttributes.deviceAppName.empty() + && !mAttributes.deviceAppVersion.empty() + && !mAttributes.deviceModel.empty() + && !mAttributes.deviceTimeZone.empty() + && !mAttributes.platform.empty() + && !mAttributes.deviceSoftwareVersion.empty() + && !mAttributes.deviceType.empty()); LOGINFO("%s, %s, %s, %s, %s, %s, %s", - mConfig.productName.c_str(), - mConfig.deviceAppName.c_str(), - mConfig.deviceAppVersion.c_str(), - mConfig.deviceModel.c_str(), - mConfig.platform.c_str(), - mConfig.deviceSoftwareVersion.c_str(), - mConfig.deviceType.c_str()); + mAttributes.productName.c_str(), + mAttributes.deviceAppName.c_str(), + mAttributes.deviceAppVersion.c_str(), + mAttributes.deviceModel.c_str(), + mAttributes.platform.c_str(), + mAttributes.deviceSoftwareVersion.c_str(), + mAttributes.deviceType.c_str()); } if (valid) { - config = mConfig; + attributes = mAttributes; + } + + mMutex.unlock(); + return valid; + } + + bool SiftConfig::GetStoreConfig(StoreConfig &config) + { + mMutex.lock(); + bool valid = !mStoreConfig.path.empty(); + if (valid) + { + config = mStoreConfig; } + mMutex.unlock(); + return valid; + } + bool SiftConfig::GetUploaderConfig(UploaderConfig &config) + { + mMutex.lock(); + bool valid = !mUploaderConfig.url.empty() + && !mUploaderConfig.apiKey.empty(); + if (valid) + { + config = mUploaderConfig; + } mMutex.unlock(); return valid; } + void SiftConfig::SetSessionId(const std::string &sessionId) + { + mMutex.lock(); + mAttributes.sessionId = sessionId; + mMutex.unlock(); + } + void SiftConfig::TriggerInitialization() { mInitializationThread = std::thread(&SiftConfig::Initialize, this); @@ -441,38 +490,38 @@ namespace WPEFramework void SiftConfig::InitializeKeysMap() { //SIFT 2.0 attributes from persistent storage - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceHardwareModel"] = &mConfig.deviceModel; - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceType"] = &mConfig.deviceType; - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["devicePlatform"] = &mConfig.platform; - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["modelNumber"] = &mConfig.deviceOsVersion; - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["manufacturer"] = &mConfig.deviceManufacturer; - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["serialNumber"] = &mConfig.deviceSerialNumber; - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["macAddress"] = &mConfig.deviceMacAddress; - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["entertainmentOSVersion"] = &mConfig.productVersion; - mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["proposition"] = &mConfig.proposition; - mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["retailer"] = &mConfig.retailer; - mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["jvagent"] = &mConfig.jvAgent; - mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["coam"] = &mConfig.coam; - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["country"] = &mConfig.country;//TODO - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["region"] = &mConfig.region;//TODO - mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["accountType"] = &mConfig.accountType; - mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["operator"] = &mConfig.accountOperator; - mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["detailType"] = &mConfig.accountDetailType; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceHardwareModel"] = &mAttributes.deviceModel; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceType"] = &mAttributes.deviceType; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["devicePlatform"] = &mAttributes.platform; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["modelNumber"] = &mAttributes.deviceOsVersion; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["manufacturer"] = &mAttributes.deviceManufacturer; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["serialNumber"] = &mAttributes.deviceSerialNumber; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["macAddress"] = &mAttributes.deviceMacAddress; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["entertainmentOSVersion"] = &mAttributes.productVersion; + mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["proposition"] = &mAttributes.proposition; + mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["retailer"] = &mAttributes.retailer; + mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["jvagent"] = &mAttributes.jvAgent; + mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["coam"] = &mAttributes.coam; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["country"] = &mAttributes.country;//TODO + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["region"] = &mAttributes.region;//TODO + mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["accountType"] = &mAttributes.accountType; + mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["operator"] = &mAttributes.accountOperator; + mKeysMap[PERSISTENT_STORE_ACCOUNT_PROFILE_NAMESPACE]["detailType"] = &mAttributes.accountDetailType; //TODO: Values provided by AS but should be provided by RDK - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceTimeZone"] = &mConfig.deviceTimeZone; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceTimeZone"] = &mAttributes.deviceTimeZone; //SIFT 1.0 attributes from persistent storage - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceSoftwareVersion"] = &mConfig.deviceSoftwareVersion; - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceAppName"] = &mConfig.deviceAppName; - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceAppVersion"] = &mConfig.deviceAppVersion; - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["accountId"] = &mConfig.accountId; - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceId"] = &mConfig.deviceId; - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["partnerId"] = &mConfig.partnerId; - - //TODO: Sift cloud configuration - move to plugin config? (at least url) - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["sift_url"] = &mConfig.url; - mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["sift_xapikey"] = &mConfig.apiKey; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceSoftwareVersion"] = &mAttributes.deviceSoftwareVersion; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceAppName"] = &mAttributes.deviceAppName; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceAppVersion"] = &mAttributes.deviceAppVersion; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["accountId"] = &mAttributes.accountId; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["deviceId"] = &mAttributes.deviceId; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["partnerId"] = &mAttributes.partnerId; + + // If Sift url empty, try to get from persistent store + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["sift_url"] = &mUploaderConfig.url; + mKeysMap[PERSISTENT_STORE_ANALYTICS_NAMESPACE]["sift_xapikey"] = &mUploaderConfig.apiKey; } void SiftConfig::ParsePluginConfig() @@ -491,20 +540,32 @@ namespace WPEFramework } else { - mConfig.schema2Enabled = config.Sift.Schema2.Value(); - mConfig.commonSchema = config.Sift.CommonSchema.Value(); - mConfig.env = config.Sift.Env.Value(); - mConfig.productName = config.Sift.ProductName.Value(); - mConfig.loggerName = config.Sift.LoggerName.Value(); - mConfig.loggerVersion = config.Sift.LoggerVersion.Value(); - mConfig.deviceOsName = config.DeviceOsName.Value(); - SYSLOG(Logging::Startup, (_T("Parsed config: '%s', '%s', '%s', '%s', '%s', '%s'."), - mConfig.commonSchema.c_str(), - mConfig.env.c_str(), - mConfig.productName.c_str(), - mConfig.loggerName.c_str(), - mConfig.loggerVersion.c_str(), - mConfig.deviceOsName.c_str() + mAttributes.schema2Enabled = config.Sift.Schema2.Value(); + mAttributes.commonSchema = config.Sift.CommonSchema.Value(); + mAttributes.env = config.Sift.Env.Value(); + mAttributes.productName = config.Sift.ProductName.Value(); + mAttributes.loggerName = config.Sift.LoggerName.Value(); + mAttributes.loggerVersion = config.Sift.LoggerVersion.Value(); + mAttributes.deviceOsName = config.DeviceOsName.Value(); + + mStoreConfig.path = config.Sift.StorePath.Value(); + mStoreConfig.eventsLimit = config.Sift.EventsLimit.Value(); + + mUploaderConfig.url = config.Sift.Url.Value(); + mUploaderConfig.maxRandomisationWindowTime = config.Sift.MaxRandomisationWindowTime.Value(); + mUploaderConfig.maxEventsInPost = config.Sift.MaxEventsInPost.Value(); + mUploaderConfig.maxRetries = config.Sift.MaxRetries.Value(); + mUploaderConfig.minRetryPeriod = config.Sift.MinRetryPeriod.Value(); + mUploaderConfig.maxRetryPeriod = config.Sift.MaxRetryPeriod.Value(); + mUploaderConfig.exponentialPeriodicFactor = config.Sift.ExponentialPeriodicFactor.Value(); + + SYSLOG(Logging::Startup, (_T("Parsed Analytics config: '%s', '%s', '%s', '%s', '%s', '%s'."), + mAttributes.commonSchema.c_str(), + mAttributes.env.c_str(), + mAttributes.productName.c_str(), + mAttributes.loggerName.c_str(), + mAttributes.loggerVersion.c_str(), + mAttributes.deviceOsName.c_str() )); } } @@ -539,7 +600,7 @@ namespace WPEFramework // Set to true if the event is to be SAT authenticated mMutex.lock(); - mConfig.authenticated = false; + mAttributes.authenticated = false; mMutex.unlock(); //Activate AuthService plugin if needed @@ -568,8 +629,8 @@ namespace WPEFramework if (result == Core::ERROR_NONE && response.HasLabel("build_type")) { mMutex.lock(); - mConfig.env = response["build_type"].String(); - std::transform(mConfig.env.begin(), mConfig.env.end(), mConfig.env.begin(), + mAttributes.env = response["build_type"].String(); + std::transform(mAttributes.env.begin(), mAttributes.env.end(), mAttributes.env.begin(), [](unsigned char c){ return std::tolower(c); }); mMutex.unlock(); } @@ -658,8 +719,8 @@ namespace WPEFramework if (result == Core::ERROR_NONE && response.HasLabel("partnerId")) { mMutex.lock(); - mConfig.partnerId = response["partnerId"].String(); - LOGINFO("Got partnerId %s", mConfig.partnerId.c_str()); + mAttributes.partnerId = response["partnerId"].String(); + LOGINFO("Got partnerId %s", mAttributes.partnerId.c_str()); mMutex.unlock(); } @@ -673,9 +734,9 @@ namespace WPEFramework if (result == Core::ERROR_NONE && response.HasLabel("serviceAccountId")) { mMutex.lock(); - mConfig.xboAccountId = response["serviceAccountId"].String(); + mAttributes.xboAccountId = response["serviceAccountId"].String(); mMutex.unlock(); - LOGINFO("Got xboAccountId %s", mConfig.xboAccountId.c_str()); + LOGINFO("Got xboAccountId %s", mAttributes.xboAccountId.c_str()); } // get xboDeviceId from AuthService.getXDeviceId @@ -683,19 +744,19 @@ namespace WPEFramework if (result == Core::ERROR_NONE && response.HasLabel("xDeviceId")) { mMutex.lock(); - mConfig.xboDeviceId = response["xDeviceId"].String(); + mAttributes.xboDeviceId = response["xDeviceId"].String(); mMutex.unlock(); - LOGINFO("Got xboDeviceId %s", mConfig.xboDeviceId.c_str()); + LOGINFO("Got xboDeviceId %s", mAttributes.xboDeviceId.c_str()); } mMutex.lock(); - mConfig.activated = true; + mAttributes.activated = true; mMutex.unlock(); } else { mMutex.lock(); - mConfig.activated = false; + mAttributes.activated = false; mMutex.unlock(); } } diff --git a/Analytics/Implementation/Backend/Sift/SiftConfig.h b/Analytics/Implementation/Backend/Sift/SiftConfig.h index 75c92cfc35..09032dddc2 100644 --- a/Analytics/Implementation/Backend/Sift/SiftConfig.h +++ b/Analytics/Implementation/Backend/Sift/SiftConfig.h @@ -31,7 +31,7 @@ namespace WPEFramework class SiftConfig { public: - struct Config + struct Attributes { bool schema2Enabled; // Sift 2.0 decoration @@ -67,10 +67,6 @@ namespace WPEFramework std::string accountOperator; std::string accountDetailType; - // TODO: read in SiftUploader - std::string url; - std::string apiKey; - // Sift 1.0 atributes that left std::string deviceSoftwareVersion; std::string deviceAppName; @@ -79,13 +75,34 @@ namespace WPEFramework std::string deviceId; }; + struct StoreConfig + { + std::string path; + uint32_t eventsLimit; + }; + + struct UploaderConfig + { + std::string url; + std::string apiKey; + uint32_t maxRandomisationWindowTime; + uint32_t maxEventsInPost; + uint32_t maxRetries; + uint32_t minRetryPeriod; + uint32_t maxRetryPeriod; + uint32_t exponentialPeriodicFactor; + }; + SiftConfig(const SiftConfig &) = delete; SiftConfig &operator=(const SiftConfig &) = delete; SiftConfig(PluginHost::IShell *shell); ~SiftConfig(); - bool Get(Config& config); + bool GetAttributes(Attributes &attributes); + bool GetStoreConfig(StoreConfig &config); + bool GetUploaderConfig(UploaderConfig &config); + void SetSessionId(const std::string &sessionId); private: class MonitorKeys : public Exchange::IStore::INotification { @@ -125,7 +142,9 @@ namespace WPEFramework std::thread mInitializationThread; Core::Sink mMonitorKeys; std::mutex mMutex; - Config mConfig; + Attributes mAttributes; + StoreConfig mStoreConfig; + UploaderConfig mUploaderConfig; PluginHost::IShell *mShell; std::map> mKeysMap; }; diff --git a/Analytics/Implementation/Backend/Sift/SiftStore.cpp b/Analytics/Implementation/Backend/Sift/SiftStore.cpp new file mode 100644 index 0000000000..d040254229 --- /dev/null +++ b/Analytics/Implementation/Backend/Sift/SiftStore.cpp @@ -0,0 +1,95 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2020 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SiftStore.h" +#include "UtilsLogging.h" + +namespace WPEFramework +{ + namespace Plugin + { + const std::string TABLE{"queue"}; + + + SiftStore::SiftStore(const std::string &path, uint32_t eventsLimit): + mEventsLimit(eventsLimit), + mLocalStore(), + mMutex() + { + if(mLocalStore.Open(path)) + { + if(mLocalStore.CreateTable(TABLE)) + { + LOGINFO("Table created"); + if(mLocalStore.SetLimit(TABLE, eventsLimit)) + { + LOGINFO("Limit set"); + } + else + { + LOGERR("Failed to set limit"); + } + } + else + { + LOGERR("Failed to create table"); + } + } + else + { + LOGERR("Failed to open store"); + } + } + + SiftStore::~SiftStore() + { + } + + std::pair SiftStore::GetEventCount() const + { + std::lock_guard lock(mMutex); + + return mLocalStore.GetEntriesCount(TABLE, 0, mEventsLimit); + } + + std::vector SiftStore::GetEvents(uint32_t start, uint32_t count) const + { + std::lock_guard lock(mMutex); + if (!count) + { + LOGWARN("Count is zero which is invalid"); + return std::vector(); + } + + return mLocalStore.GetEntries(TABLE, start, count); + } + + bool SiftStore::RemoveEvents(uint32_t start, uint32_t end) + { + std::lock_guard lock(mMutex); + return mLocalStore.RemoveEntries(TABLE, start, end); + } + + bool SiftStore::PostEvent(const std::string &entry) + { + std::lock_guard lock(mMutex); + return mLocalStore.AddEntry(TABLE, entry); + } + } +} \ No newline at end of file diff --git a/Analytics/Implementation/Backend/Sift/SiftStore.h b/Analytics/Implementation/Backend/Sift/SiftStore.h new file mode 100644 index 0000000000..f52cc1b76b --- /dev/null +++ b/Analytics/Implementation/Backend/Sift/SiftStore.h @@ -0,0 +1,54 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2020 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "../../../Module.h" +#include "../../LocalStore/LocalStore.h" + +#include +#include +#include +#include + +namespace WPEFramework +{ + namespace Plugin + { + + class SiftStore + { + public: + SiftStore(const std::string &path, uint32_t eventsLimit); + ~SiftStore(); + + std::pair GetEventCount() const; + std::vector GetEvents(uint32_t start, uint32_t count) const; + bool RemoveEvents(uint32_t start, uint32_t end); + bool PostEvent(const std::string &entry); + + private: + uint32_t mEventsLimit; + LocalStore mLocalStore; + mutable std::mutex mMutex; + }; + + typedef std::shared_ptr SiftStorePtr; + + } +} diff --git a/Analytics/Implementation/Backend/Sift/SiftUploader.cpp b/Analytics/Implementation/Backend/Sift/SiftUploader.cpp new file mode 100644 index 0000000000..971ab88e11 --- /dev/null +++ b/Analytics/Implementation/Backend/Sift/SiftUploader.cpp @@ -0,0 +1,436 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2020 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SiftUploader.h" +#include "UtilsLogging.h" +#include "../../../Module.h" + +#include +#include +#include + +namespace WPEFramework +{ + namespace Plugin + { + SiftUploader::SiftUploader(SiftStorePtr storePtr, + const std::string &url, + const std::string &apiKey, + const uint32_t &maxRandomisationWindowTime, + const uint32_t &maxEventsInPost, + const uint32_t &maxRetries, + const uint32_t &minRetryPeriod, + const uint32_t &maxRetryPeriod, + const uint32_t &exponentialPeriodicFactor) + : mStorePtr(storePtr) + , mUrl(url) + , mApiKey(apiKey) + , mMaxRandomisationWindowTime(maxRandomisationWindowTime) + , mMaxEventsInPost(maxEventsInPost) + , mMaxRetries(maxRetries) + , mMinRetryPeriod(minRetryPeriod) + , mMaxRetryPeriod(maxRetryPeriod) + , mExponentialPeriodicFactor(exponentialPeriodicFactor) + , mUploaderState(UploaderState::RANDOMISATION_WINDOW_WAIT_STATE) + , mStop(false) + , mCurrentRetryCount(0) + , mEventStartIndex(0) + { + mThread = std::thread(&SiftUploader::Run, this); + mThread.detach(); + } + + SiftUploader::~SiftUploader() + { + { + std::lock_guard lock(mMutex); + mStop = true; + } + mCondition.notify_one(); + mThread.join(); + } + + void SiftUploader::setDeviceInfoRequiredFields(const std::string &accountId, const std::string &deviceId, const std::string &partnerId) + { + std::lock_guard lock(mMutex); + mAccountId = accountId; + mDeviceId = deviceId; + mPartnerId = partnerId; + } + + void SiftUploader::Run() + { + while (true) + { + static std::vector collectedEvents; + + switch (mUploaderState) + { + case SiftUploader::UploaderState::RANDOMISATION_WINDOW_WAIT_STATE: + { + std::unique_lock lock( mMutex ); + mCondition.wait_for(lock, std::chrono::seconds(RandomisationWindowTimeGenerator()), + [this] () { return mStop; } ); + if (mStop) + { + return; + } + + mUploaderState = SiftUploader::UploaderState::COLLECT_ANALYTICS; + } + break; + + case SiftUploader::UploaderState::COLLECT_ANALYTICS: + { + bool eventsCollected = false; + + if (CollectEventsFromAnalyticsStore(mMaxEventsInPost)) + { + collectedEvents.clear(); // ensure previous events are cleared + + if (!mEvents.empty()) + { + collectedEvents = mEvents; + eventsCollected = true; + } + else + { + LOGERR("No collected events to be got"); + } + } + + if (eventsCollected) + { + LOGINFO("Successfully collected events from analytics Store"); + mUploaderState = UploaderState::POST_ANALYTICS; + // Falling through to the POST_ANALYTICS case as we have successfully collected events + } + else + { + LOGINFO("No events collected from analytics Store"); + mUploaderState = UploaderState::RANDOMISATION_WINDOW_WAIT_STATE; + break; + } + } + // NO BREAK HERE, FALLING THROUGH TO THE NEXT CASE (WHICH SHOULD BE POST_ANALYTICS) + + case SiftUploader::UploaderState::POST_ANALYTICS: + { + std::string jsonEventPayload = ComposeJSONEventArrayToBeUploaded(collectedEvents); + + std::string resp; + + uint32_t respcode; + + LOGINFO("Posting analytics events: %s", jsonEventPayload.c_str()); + + do + { + respcode = PostJson(mUrl, mApiKey, jsonEventPayload, resp); + } while ((respcode != 200) && (respcode != 400) && PerformWaitIfRetryNeeded()); + + if ((respcode == 200) || (respcode == 400)) + { + if (respcode == 400) + { + LOGWARN("Received a 400 response - deleting the events as the end point refuses them"); + } + + if (!mEvents.empty() && mStorePtr->RemoveEvents(mEventStartIndex, mEventStartIndex + mEvents.size() - 1)) + { + LOGINFO("Collected events successfully deleted"); + } + else + { + LOGERR("No collected events to be deleted"); + } + + mUploaderState = UploaderState::RANDOMISATION_WINDOW_WAIT_STATE; + } + else + { + LOGERR("Failed to post analytics event - respcode: %u, response: %s", respcode, resp.c_str()); + } + + if (!resp.empty()) + { + validateResponse(resp, collectedEvents); + } + } + break; + + default: + { + LOGERR("Unhandled state: %d", static_cast(mUploaderState)); + } + break; + } + } + } + + bool SiftUploader::PerformWaitIfRetryNeeded() + { + bool retry = false; + + if (mCurrentRetryCount == mMaxRetries) + { + mCurrentRetryCount = 0; + } + else + { + static auto retryTime = mMinRetryPeriod; + + if (retryTime > mMaxRetryPeriod) + { + retryTime = mMinRetryPeriod; + } + + LOGINFO("Failed posting retry wait time: %d seconds, with retries completed: %d", retryTime, mCurrentRetryCount); + + std::unique_lock lock( mMutex ); + mCondition.wait_for(lock, std::chrono::seconds(retryTime), + [this] () { return mStop; } ); + if (mStop) + { + // return immediately if stop is set + return false; + } + + if (retryTime < mMaxRetryPeriod) + { + retryTime *= mExponentialPeriodicFactor; + } + + ++mCurrentRetryCount; + + retry = true; + } + + return retry; + } + + uint32_t SiftUploader::RandomisationWindowTimeGenerator() const + { + uint32_t max = mMaxRandomisationWindowTime; + uint32_t min = 0; + + srand(time(nullptr)); + + return (((rand() % (max + 1 - min)) + min)); + } + + bool SiftUploader::CollectEventsFromAnalyticsStore(uint32_t count) + { + bool success = false; + + uint32_t startIndex{}; + uint32_t eventCount{}; + + std::tie(startIndex, eventCount) = mStorePtr->GetEventCount(); + + // if count is specified in the call, then only those number of events are desired even if more events are available + if ((count > 0) && (eventCount > count)) + { + eventCount = count; + } + + if (eventCount > 0) + { + mEvents = mStorePtr->GetEvents(startIndex, eventCount); + + if (!mEvents.empty()) + { + LOGINFO("Successfully got %zu events from analytics store", mEvents.size()); + mEventStartIndex = startIndex; + success = true; + } + else + { + LOGINFO("Got no events from the analytics store"); + } + } + else + { + LOGINFO("No events available to be collected from analytics store"); + } + + return success; + } + + std::string SiftUploader::ComposeJSONEventArrayToBeUploaded(const std::vector &events) const + { + std::string output; + + auto validateEventLambda = [](const JsonObject &event) + { + // Just perform some basic sanity to check if the event is valid. If event_id is present, + // the event is bound to be valid (since all the other attributes are populated together). + // Otherwise, it is invalid/malformed and can be dropped since it would be rejected by the backend anyway + return (event.HasLabel("event_id") && event["event_id"].Content() == WPEFramework::Core::JSON::Variant::type::STRING && !event["event_id"].String().empty()); + }; + + // check if there are any events in the first place, if not just return empty string + if (!events.empty()) + { + JsonArray eventArray = JsonArray(); + + for (const auto &event : events) + { + JsonObject root(event); + if (validateEventLambda(root)) + { + updateEventDeviceInfoIfRequired(root); + eventArray.Add(root); + } + else + { + LOGWARN("Dropping an invalid/malformed event since it would be rejected by the backend anyway"); + } + } + + eventArray.ToString(output); + } + return output; + } + + void SiftUploader::updateEventDeviceInfoIfRequired(JsonObject &event) const + { + std::lock_guard lock(mMutex); + if (!mAccountId.empty() && !mDeviceId.empty() && !mPartnerId.empty() + && (!event.HasLabel("account_id") || !event.HasLabel("device_id") || !event.HasLabel("partner_id"))) + { + event["account_id"] = mAccountId; + event["device_id"] = mDeviceId; + event["partner_id"] = mPartnerId; + } + } + + void SiftUploader::validateResponse(const std::string &response, const std::vector &events) const + { + JsonObject responseJson(response); + + if (!responseJson.HasLabel("Events") && responseJson["Events"].Content() != WPEFramework::Core::JSON::Variant::type::ARRAY) + { + LOGERR("Response does not contain Events array"); + return; + } + + JsonArray eventsArray(responseJson["Events"].Array()); + + // go over events event_id and find out which ones were rejected + for (const auto &event : events) + { + JsonObject eventJson(event); + const std::string &eventId = eventJson["event_id"].String(); + + bool found = false; + for (int i = 0; i < eventsArray.Length(); i++) + { + JsonObject responseEvent(eventsArray[i].Object()); + if (responseEvent.HasLabel("EventId")) + { + if (responseEvent["event_id"].String() == eventId) + { + found = true; + if (responseEvent.HasLabel("Status") && responseEvent["Status"].String() != "valid") + { + LOGERR("Event was rejected by the backend: %s", event.c_str()); + if (responseEvent.HasLabel("Errors")) + { + std::string errors; + responseEvent.ToString(errors); + LOGERR("Backend response for rejected event: %s", errors.c_str()); + } + } + break; + } + } + } + + if (!found) + { + LOGERR("Event Id '%s' was not found in the response", eventId.c_str()); + } + } + } + + static size_t CurlWriteCallback(void* contents, size_t size, size_t nmemb, void* userp) { + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; + } + + uint32_t SiftUploader::PostJson(const std::string &url, const std::string &apiKey, const std::string &json, std::string &response) + { + CURL *curl; + CURLcode res; + uint32_t retHttpCode = 0; + + if (url.empty() || apiKey.empty() || json.empty()) + { + LOGERR("Invalid parameters for postJson"); + return retHttpCode; + } + + curl = curl_easy_init(); + if (!curl) + { + LOGERR("Failed to initialize curl"); + return retHttpCode; + } + + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json.data()); + + // Create a linked list of custom headers + struct curl_slist *headers = NULL; + std::string keyHeader("X-Api-Key: " + apiKey); + headers = curl_slist_append(headers, "Content-Type: application/json"); + headers = curl_slist_append(headers, keyHeader.data()); + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + // Perform the request, res will get the return code + res = curl_easy_perform(curl); + + // Check for errors + if (res != CURLE_OK) + { + LOGERR("curl_easy_perform() failed: %s", curl_easy_strerror(res)); + } + else + { + LOGINFO("Response: %s", response.c_str()); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &retHttpCode); + } + + // Clean up the header list + curl_slist_free_all(headers); + + // Clean up curl session + curl_easy_cleanup(curl); + + return retHttpCode; + } + + } +} \ No newline at end of file diff --git a/Analytics/Implementation/Backend/Sift/SiftUploader.h b/Analytics/Implementation/Backend/Sift/SiftUploader.h new file mode 100644 index 0000000000..7da53d549d --- /dev/null +++ b/Analytics/Implementation/Backend/Sift/SiftUploader.h @@ -0,0 +1,100 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2020 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "SiftStore.h" + +#include +#include +#include +#include +#include +#include + + +namespace WPEFramework +{ + namespace Plugin + { + class SiftUploader + { + public: + + SiftUploader(SiftStorePtr storePtr, + const std::string &url, + const std::string &apiKey, + const uint32_t &maxRandomisationWindowTime, + const uint32_t &maxEventsInPost, + const uint32_t &maxRetries, + const uint32_t &minRetryPeriod, + const uint32_t &maxRetryPeriod, + const uint32_t &exponentialPeriodicFactor); + ~SiftUploader(); + + void setDeviceInfoRequiredFields( const std::string &accountId, const std::string &deviceId, const std::string &partnerId); + + private: + + enum UploaderState + { + RANDOMISATION_WINDOW_WAIT_STATE, + COLLECT_ANALYTICS, + POST_ANALYTICS + }; + + SiftUploader(const SiftUploader&) = delete; + SiftUploader& operator=(const SiftUploader&) = delete; + + void Run(); + bool PerformWaitIfRetryNeeded(); + uint32_t RandomisationWindowTimeGenerator() const; + bool CollectEventsFromAnalyticsStore(uint32_t count); + std::string ComposeJSONEventArrayToBeUploaded(const std::vector &events) const; + void updateEventDeviceInfoIfRequired(JsonObject &event) const; + void validateResponse(const std::string &response, const std::vector &events) const; + + static uint32_t PostJson(const std::string& url, const std::string& apiKey, const std::string& json, std::string &response); + + SiftStorePtr mStorePtr; + std::string mUrl; + std::string mApiKey; + uint32_t mMaxRandomisationWindowTime; + uint32_t mMaxEventsInPost; + uint32_t mMaxRetries; + uint32_t mMinRetryPeriod; + uint32_t mMaxRetryPeriod; + uint32_t mExponentialPeriodicFactor; + + std::string mAccountId; + std::string mDeviceId; + std::string mPartnerId; + + mutable std::mutex mMutex; + UploaderState mUploaderState; + std::condition_variable mCondition; + std::thread mThread; + bool mStop; + uint32_t mCurrentRetryCount; + uint32_t mEventStartIndex; + std::vector mEvents; + }; + + typedef std::unique_ptr SiftUploaderPtr; + } +} diff --git a/Analytics/Implementation/LocalStore/CMakeLists.txt b/Analytics/Implementation/LocalStore/CMakeLists.txt new file mode 100644 index 0000000000..738f043b5b --- /dev/null +++ b/Analytics/Implementation/LocalStore/CMakeLists.txt @@ -0,0 +1,36 @@ +# If not stated otherwise in this file or this component's license file the +# following copyright and licenses apply: +# +# Copyright 2020 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +set(TARGET_LIB ${NAMESPACE}${PLUGIN_NAME}LocalStore) + +add_library(${TARGET_LIB} STATIC) + +target_sources(${TARGET_LIB} PRIVATE LocalStore.cpp + DatabaseConnection.cpp) + +find_package(Sqlite) +if (SQLITE_FOUND) + include_directories(${SQLITE_INCLUDE_DIRS}) + target_link_libraries(${TARGET_LIB} PRIVATE ${SQLITE_LIBRARIES}) +else (SQLITE_FOUND) + message ("Sqlite3 required.") +endif (SQLITE_FOUND) + +target_include_directories(${TARGET_LIB} PUBLIC "${CMAKE_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}") +target_include_directories(${TARGET_LIB} PRIVATE ../../../helpers) +set_property(TARGET ${TARGET_LIB} PROPERTY POSITION_INDEPENDENT_CODE ON) +set_target_properties(${TARGET_LIB} PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF) +target_link_libraries(${TARGET_LIB} PRIVATE ${NAMESPACE}Plugins::${NAMESPACE}Plugins) \ No newline at end of file diff --git a/Analytics/Implementation/LocalStore/DatabaseConnection.cpp b/Analytics/Implementation/LocalStore/DatabaseConnection.cpp new file mode 100644 index 0000000000..24c2eac6b3 --- /dev/null +++ b/Analytics/Implementation/LocalStore/DatabaseConnection.cpp @@ -0,0 +1,268 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2020 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "DatabaseConnection.h" +#include "UtilsLogging.h" + +namespace WPEFramework { + namespace Plugin { + + DatabaseConnection::DatabaseConnection(): mDatabaseName(), mDataBaseHandle(NULL), mMutex() {} + + DatabaseConnection::~DatabaseConnection() { + DisConnect(); + } + + bool DatabaseConnection::Connect(const std::string & databaseName) { + bool ret = false; + + // Ensure a database name is given + if (!databaseName.empty()) { + // If a connection is already open close it first + if (mDataBaseHandle != NULL && DisConnect() != DB_OK) { + LOGERR("Database %s open failed: could not close existing " + "connection", + databaseName.c_str()); + } else { + // Assign database name + mDatabaseName = databaseName; + + // Opens a database or creates one with create and rw privileges + int32_t queryRet = DB_OPEN((const char * ) databaseName.c_str(), & mDataBaseHandle); + + // Handle result from DB query + if (DB_OK == queryRet) { + ret = true; + LOGINFO("Database %s open succeeded", databaseName.c_str()); + } else { + LOGERR("Database %s open failed: %s db err code %d", + databaseName.c_str(), + DB_ERRMSG(mDataBaseHandle), + queryRet); + } + } + } else { + LOGERR("Database open failed: invalid db name %s", databaseName.c_str()); + } + + return ret; + } + + bool DatabaseConnection::DisConnect() { + bool ret = false; + + // Closes a database reference by the database handle + if (mDataBaseHandle != NULL) { + // Closes a database based on the associated handle + int32_t queryRet = DB_CLOSE(mDataBaseHandle); + + // Handle result from DB query + if (DB_OK == queryRet) { + ret = true; + LOGINFO("Database %s close succeeded", mDatabaseName.c_str()); + mDatabaseName.clear(); + mDataBaseHandle = NULL; + } else { + LOGERR("Database %s close failed: %s db err code %d", + mDatabaseName.c_str(), + DB_ERRMSG(mDataBaseHandle), + queryRet); + } + } + + return ret; + } + + bool DatabaseConnection::Exec(const std::string & query) { + bool ret = false; + char * errmsg = NULL; + + std::lock_guard < std::mutex > lock(mMutex); + + // Verify the database handle was created + if (mDataBaseHandle != NULL) { + DatabaseQuery queryCbData(query, mDatabaseName); + + // Execute a generic query to the database based on the associated handle + int32_t queryRet = DB_QUERY(mDataBaseHandle, + query.c_str(), + DbCallbackOnly, + (void * ) & queryCbData, & + errmsg); + + // Handle result from DB query + if (DB_OK == queryRet) { + // Note that row data could be large and therefore cannot log query + LOGINFO("Database %s query succeeded", mDatabaseName.c_str()); + ret = true; + } else { + LOGERR("Database %s query failed errmsg: %s db err code %d", + mDatabaseName.c_str(), + errmsg, + queryRet); + + // If error, database malloc's the message so it needs to be free'd + DB_FREE(errmsg); + } + } else { + LOGERR("Database connection not established for %s. " + "Query failed.", + mDatabaseName.c_str()); + } + + return ret; + } + + bool DatabaseConnection::ExecAndGetModified(const std::string & query, + uint32_t & modifiedRows) { + + bool ret = false; + char * errmsg = NULL; + + std::lock_guard < std::mutex > lock(mMutex); + + // Verify the database handle was created + if (mDataBaseHandle != NULL) { + DatabaseQuery queryCbData(query, mDatabaseName); + + // Execute a generic query to the database based on the associated handle + int32_t queryRet = DB_QUERY(mDataBaseHandle, + query.c_str(), + DbCallbackOnly, + (void * ) & queryCbData, & + errmsg); + + // Handle result from DB query + if (DB_OK == queryRet) { + // Executes a query to get how many rows in the table were affected + modifiedRows = DB_CHANGES(mDataBaseHandle); + + LOGINFO("Database %s query succeeded %d rows modified", + mDatabaseName.c_str(), + modifiedRows); + ret = true; + } else { + LOGERR("Database %s query failed errmsg: %s db err code %d", + mDatabaseName.c_str(), + errmsg, + queryRet); + + // If error, database malloc's the message so it needs to be free'd + DB_FREE(errmsg); + } + } else { + LOGERR("Database connection not established for %s. " + "Query failed.", + mDatabaseName.c_str()); + } + + return ret; + } + + bool DatabaseConnection::ExecAndGetResults(const std::string & query, + DatabaseTable & table) { + bool ret = false; + char * errmsg = NULL; + + std::lock_guard < std::mutex > lock(mMutex); + + if (mDataBaseHandle != NULL) { + DatabaseTable result; + + // Execute a generic query to the database based on the associated handle + // which also returns a result to client i.e SELECT * FROM blah + int32_t queryRet = DB_QUERY(mDataBaseHandle, + query.c_str(), + DbCallbackGetResults, + (void * ) & result, & + errmsg); + + // Handle result from DB query + if (DB_OK == queryRet) { + ret = true; + LOGINFO("Database %s query succeeded with %d results", + mDatabaseName.c_str(), + result.NumRows()); + table = result; + } else { + LOGERR("Database %s query failed with error: %s db err code %d", + mDatabaseName.c_str(), + errmsg, + queryRet); + DB_FREE(errmsg); + } + } else { + LOGERR("Database connection not established for %s. Query failed.", + mDatabaseName.c_str()); + } + + return ret; + } + + int32_t DatabaseConnection::DbCallbackOnly(void * arg, + int argc, + char ** argv, + char ** colName) { + int32_t ret = DB_ERROR; + DatabaseQuery * query = static_cast < DatabaseQuery * > (arg); + + if (query) { + ret = DB_OK; + LOGINFO("Database %s query executed", query -> mDatabaseName.c_str()); + } else { + LOGERR("Database query executed with no data"); + } + + return ret; + } + + int32_t DatabaseConnection::DbCallbackGetResults(void * arg, + int argc, + char ** argv, + char ** colName) { + int32_t ret = DB_ERROR; + DatabaseTable * table = static_cast < DatabaseTable * > (arg); + + if (table != NULL) { + DatabaseTable::DatabaseRow row; + + for (int index = 0; index < argc; index++) { + std::string name = colName[index]; + std::string value; + if (argv[index]) { + value = argv[index]; + } + + row.AddColEntry( + DatabaseTable::DatabaseRow::DatabaseColumnEntry(name, value)); + } + + table -> AddRow(row); + + ret = DB_OK; + + LOGINFO("Database query executed"); + } else { + LOGERR("Database invalid query, cannot get results"); + } + + return ret; + } + + } // namespace Plugin +} // namespace WPEFramework \ No newline at end of file diff --git a/Analytics/Implementation/LocalStore/DatabaseConnection.h b/Analytics/Implementation/LocalStore/DatabaseConnection.h new file mode 100644 index 0000000000..67d36b54ee --- /dev/null +++ b/Analytics/Implementation/LocalStore/DatabaseConnection.h @@ -0,0 +1,118 @@ +/** + * If not stated otherwise in this file or this component's LICENSE + * file the following copyright and licenses apply: + * + * Copyright 2020 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +#pragma once + +#include "Module.h" +#include "DatabaseInterface.h" + +#include +#include +#include +#include +#include + +namespace WPEFramework { + namespace Plugin { + + class DatabaseTable { + public: class DatabaseRow { + public: class DatabaseColumnEntry { + private: std::string mName; + std::string mValue; + + public: DatabaseColumnEntry(std::string & name, std::string & value): mName(name), + mValue(value) {} + + const std::string & GetName(void) const { + return mName; + } + + const std::string & GetValue(void) const { + return mValue; + } + }; + + DatabaseRow() { + mRow.clear(); + } + DatabaseColumnEntry & operator[](uint32_t idx) { + return mRow[idx]; + } + void AddColEntry(const DatabaseColumnEntry & entry) { + mRow.push_back(entry); + } + + uint32_t NumCols(void) const { + return mRow.size(); + } + + private: std::vector < DatabaseColumnEntry > mRow; + }; + + DatabaseTable() { + mTable.clear(); + } + uint32_t NumRows(void) const { + return mTable.size(); + } + DatabaseRow & operator[](uint32_t idx) { + return mTable[idx]; + } + void AddRow(const DatabaseRow & row) { + mTable.push_back(row); + } + + private: std::vector < DatabaseRow > mTable; + }; + + struct DatabaseQuery { + std::string mQuery; + std::string mDatabaseName; + + DatabaseQuery(std::string query, std::string dbName): mQuery(query), mDatabaseName(dbName) {} + }; + + class DatabaseConnection { + public: DatabaseConnection(); + ~DatabaseConnection(); + + bool Connect(const std::string & databaseName); + bool DisConnect(); + bool Exec(const std::string & query); + bool ExecAndGetModified(const std::string & query, uint32_t & modifiedRows); + bool ExecAndGetResults(const std::string & query, DatabaseTable & table); + const std::string & GetDatabaseName(void) const { + return mDatabaseName; + } + bool IsConnected(void) { + return (mDataBaseHandle != NULL); + } + + private: static int32_t DbCallbackOnly(void * arg, int argc, char ** argv, char ** colName); + static int32_t DbCallbackGetResults(void * arg, int argc, char ** argv, char ** colName); + + std::string mDatabaseName; + DB_HANDLE * mDataBaseHandle; + std::mutex mMutex; + }; + + typedef std::shared_ptr < DatabaseConnection > DatabaseConnectionPtr; + + } +} \ No newline at end of file diff --git a/Analytics/Implementation/LocalStore/DatabaseInterface.h b/Analytics/Implementation/LocalStore/DatabaseInterface.h new file mode 100644 index 0000000000..64e3a3ff5c --- /dev/null +++ b/Analytics/Implementation/LocalStore/DatabaseInterface.h @@ -0,0 +1,76 @@ +/** + * If not stated otherwise in this file or this component's LICENSE + * file the following copyright and licenses apply: + * + * Copyright 2020 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +#pragma once + +#include "sqlite3.h" + +//============================================================================= +// Custom Definitions +#define SQL_MASTER_TABLE_NAME_COL_ID (1) + +//============================================================================= +// Database Definitions +#define DB_OK SQLITE_OK +#define DB_ERROR SQLITE_ERROR + +#define DB_ROW_READY SQLITE_ROW + +#define DB_COL_TYPE_INTEGER SQLITE_INTEGER +#define DB_COL_TYPE_FLOAT SQLITE_FLOAT +#define DB_COL_TYPE_TEXT SQLITE3_TEXT +#define DB_COL_TYPE_BLOB SQLITE_BLOB +#define DB_COL_TYPE_NULL SQLITE_NULL + +//============================================================================= +// Database Types +#define DB_HANDLE sqlite3 +#define DB_STATEMENT sqlite3_stmt + +//============================================================================= +// Database APIs +//Get last error generated by the database +#define DB_ERRMSG(handle) sqlite3_errmsg(handle) + +//Open a new/existing database +#define DB_OPEN(name, handle) sqlite3_open(name, handle) + +//Close a currently open database +#define DB_CLOSE(handle) sqlite3_close(handle) + +//Free memory created internally by the database +#define DB_FREE(ptr) sqlite3_free(ptr) + +//Query database handling 1 row at a time +#define DB_STEP_ROW(smt) sqlite3_step(smt) + +//Number of columns in the database +#define DB_COLUMN_COUNT(smt) sqlite3_column_count(smt) + +//Column name +#define DB_COLUMN_NAME(smt, colIdx) sqlite3_column_name(smt, colIdx) + +//Column Datatype +#define DB_COLUMN_TYPE(smt, colIdx) sqlite3_column_type(smt, colIdx) + +//Perform a database query based on a query string +#define DB_QUERY(handle, query, cb, cbdata, errmsg) \ + sqlite3_exec(handle, query, cb, cbdata, errmsg) + +//Check how many rows were affected on the last query +#define DB_CHANGES(handle) sqlite3_changes(handle) diff --git a/Analytics/Implementation/LocalStore/LocalStore.cpp b/Analytics/Implementation/LocalStore/LocalStore.cpp new file mode 100644 index 0000000000..44afee79e3 --- /dev/null +++ b/Analytics/Implementation/LocalStore/LocalStore.cpp @@ -0,0 +1,275 @@ +/** + * If not stated otherwise in this file or this component's LICENSE + * file the following copyright and licenses apply: + * + * Copyright 2020 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +#include "LocalStore.h" +#include "UtilsLogging.h" +#include "DatabaseConnection.h" + +#include + +namespace WPEFramework +{ + namespace Plugin + { + const std::string DB_EXT = "db"; + + LocalStore::LocalStore(): + mDatabaseConnection(nullptr), + mPath() + { + } + + LocalStore::~LocalStore() + { + } + + bool LocalStore::Open(const std::string &path) + { + bool status = false; + const std::string dbPath = path + "." + DB_EXT; + + if (mPath == dbPath && mDatabaseConnection != nullptr && mDatabaseConnection->IsConnected()) + { + status = true; + LOGINFO("Database %s already opened", dbPath.c_str()); + return status; + } + + // Creates a database connection object + DatabaseConnectionPtr conn = std::make_shared(); + + // Connects to the database, which creates the database file if needed + if (conn->Connect(dbPath)) + { + status = true; + mDatabaseConnection = conn; + mPath = dbPath; + } + else + { + LOGERR("AddDatabase failed to create a new database %s ", dbPath.c_str()); + } + + return status; + } + + bool LocalStore::CreateTable(const std::string &table) + { + bool status = false; + const std::string query = "CREATE TABLE IF NOT EXISTS " + table + + " (id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT)"; + + if (mDatabaseConnection != nullptr && mDatabaseConnection->IsConnected()) + { + if (mDatabaseConnection->Exec(query)) + { + status = true; + } + else + { + LOGERR("Failed to create table %s", table.c_str()); + } + } + else + { + LOGERR("Failed to create table %s, no connection", table.c_str()); + } + + return status; + } + + bool LocalStore::SetLimit(const std::string &table, uint32_t limit) + { + bool status = false; + const std::string query = "PRAGMA max_page_count = " + std::to_string(limit); + + if (mDatabaseConnection != nullptr && mDatabaseConnection->IsConnected()) + { + if (mDatabaseConnection->Exec(query)) + { + status = true; + } + else + { + LOGERR("Failed to set limit %u", limit); + } + } + else + { + LOGERR("Failed to set limit %u, no connection", limit); + } + + return status; + } + + std::pair LocalStore::GetEntriesCount(const std::string &table, uint32_t start, uint32_t maxCount) const + { + std::pair count = std::make_pair(0, 0); + + if (mDatabaseConnection != nullptr && mDatabaseConnection->IsConnected()) + { + std::string query = buildGetEventsQuery(table, start, maxCount); + if (!query.empty()) + { + DatabaseTable table; + if (mDatabaseConnection->ExecAndGetResults(query, table) + && table.NumRows() > 0) + { + // get start from first row's id value + count.first = std::stoi(table[0][0].GetValue()); + // get count from number of rows + count.second = table.NumRows(); + } + else + { + LOGERR("Failed to get entries count, query %s", query.c_str()); + } + } + else + { + LOGERR("Failed to build query with {%u, %u}", start, maxCount); + } + } + else + { + LOGERR("Failed to get entries count, no connection"); + } + + return count; + } + + std::vector LocalStore::GetEntries(const std::string &table, uint32_t start, uint32_t count) const + { + std::vector entries{}; + + if (mDatabaseConnection != nullptr && mDatabaseConnection->IsConnected()) + { + std::string query = buildGetEventsQuery(table, start, count); + if (!query.empty()) + { + DatabaseTable table; + if (mDatabaseConnection->ExecAndGetResults(query, table)) + { + for (uint32_t rowIdx = 0; rowIdx < table.NumRows(); rowIdx++) + { + if (table[rowIdx].NumCols() < 2) + { + LOGERR("Failed to get entries, invalid row"); + continue; + } + + std::string entry = table[rowIdx][1].GetValue(); + entries.push_back(entry); + } + } + else + { + LOGERR("Failed to get entries, query %s", query.c_str()); + } + } + else + { + LOGERR("Failed to build query with {%u, %u}", start, count); + } + } + else + { + LOGERR("Failed to get entries, no connection"); + } + + return entries; + } + + bool LocalStore::RemoveEntries(const std::string &table, uint32_t start, uint32_t end) + { + bool status = false; + + if (mDatabaseConnection != nullptr && mDatabaseConnection->IsConnected()) + { + std::string query = "DELETE FROM " + table + " WHERE id BETWEEN " + std::to_string(start) + " AND " + std::to_string(end); + uint32_t modifiedRows = 0; + if (mDatabaseConnection->ExecAndGetModified(query, modifiedRows)) + { + status = true; + } + else + { + LOGERR("Failed to remove entries, query %s", query.c_str()); + } + } + else + { + LOGERR("Failed to remove entries, no connection"); + } + + return status; + } + + bool LocalStore::AddEntry(const std::string &table, const std::string &entry) + { + bool status = false; + + if (mDatabaseConnection != nullptr && mDatabaseConnection->IsConnected()) + { + std::string query = "INSERT INTO " + table + " (data) VALUES ('" + entry + "')"; + if (mDatabaseConnection->Exec(query)) + { + status = true; + } + else + { + LOGERR("Failed to add entry, query %s", query.c_str()); + } + } + else + { + LOGERR("Failed to add entry, no connection"); + } + + return status; + } + + std::string LocalStore::buildGetEventsQuery(const std::string &table, uint32_t start, uint32_t count) const + { + std::string query{}; + + if (0 == start) + { + query.append("SELECT * FROM " + table); + query.append(" WHERE"); + query.append(" id>((SELECT MAX(id) FROM " + table + ")-" + + std::to_string(count) + ")"); + } + else if (start && count) + { + query.append("SELECT * FROM " + table); + query.append(" WHERE"); + query.append(" id>=" + std::to_string(start) + " LIMIT " + + std::to_string(count)); + } + else + { + LOGERR("Failed to build query with {%u, %u}", start, count); + } + + return query; + } + + } +} \ No newline at end of file diff --git a/Analytics/Implementation/LocalStore/LocalStore.h b/Analytics/Implementation/LocalStore/LocalStore.h new file mode 100644 index 0000000000..f940ccde11 --- /dev/null +++ b/Analytics/Implementation/LocalStore/LocalStore.h @@ -0,0 +1,56 @@ +/** +* If not stated otherwise in this file or this component's LICENSE +* file the following copyright and licenses apply: +* +* Copyright 2020 RDK Management +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +**/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "Module.h" +#include "DatabaseConnection.h" + +namespace WPEFramework +{ + namespace Plugin + { + class LocalStore + { + public: + LocalStore(); + ~LocalStore(); + + bool Open(const std::string &path); + bool CreateTable(const std::string &table); + bool SetLimit(const std::string &table, uint32_t limit); + std::pair GetEntriesCount(const std::string &table, uint32_t start, uint32_t maxCount) const; + std::vector GetEntries(const std::string &table, uint32_t start, uint32_t count) const; + bool RemoveEntries(const std::string &table, uint32_t start, uint32_t end); + bool AddEntry(const std::string &table, const std::string &entry); + private: + + std::string buildGetEventsQuery(const std::string &table, uint32_t start, uint32_t count) const; + + DatabaseConnectionPtr mDatabaseConnection; + std::string mPath; + }; + } +} \ No newline at end of file