Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
## XX.XX.XX
- Added the health check feature. The SDK now sends a health check request once after `start()`. Like any request, it updates the user's last seen time on the server.
- Added the SDK name and version (`sdk_name` and `sdk_version`) to every request, including the remote config and SDK Behavior Settings fetches. Before, only the begin session request carried them.
- Added `multiplyCustomUserDetail` for multiplying the numeric value of a custom user property on the server.
- Added `disableHealthCheck()` for turning off the health check request.
- Added `status_code` and `body` to `HTTPResponse`. A custom HTTP client can fill them so that the health check reports the last request the server refused; a client that leaves them empty works as before.
- Calls made before `start()`, a second `start()`, beginning a session that already runs and ending one that does not are now logged at warning level. Failed and non-JSON server answers, and session calls ignored because the SDK Behavior Settings turned session tracking off, are now logged at debug level.

- Mitigated an issue where reaching the event queue threshold logged a warning that events were dropped, although they were sent.

## 26.8.1
- ! Minor breaking change ! Builds now stop with a clear error when the SDK headers find a nlohmann/json older than 3.11 first on the include path, instead of mixing two versions of the library.
Expand Down
7 changes: 5 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ set(COUNTLY_PUBLIC_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/include/countly/crash_module.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/countly/event.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/countly/logger_module.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/countly/health_check_module.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/countly/remote_config_store.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/countly/request_builder.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/countly/request_module.hpp
Expand All @@ -55,7 +56,8 @@ add_library(countly
${CMAKE_CURRENT_SOURCE_DIR}/src/storage_module_db.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/storage_module_memory.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/event.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/configuration_module.cpp)
${CMAKE_CURRENT_SOURCE_DIR}/src/configuration_module.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/health_check_module.cpp)
add_library(countly::countly ALIAS countly)

target_include_directories(countly
Expand Down Expand Up @@ -153,7 +155,8 @@ if(COUNTLY_BUILD_TESTS)
${CMAKE_CURRENT_SOURCE_DIR}/tests/internal_limits.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/internal_limits_integration.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/multi_instance.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/user_details.cpp)
${CMAKE_CURRENT_SOURCE_DIR}/tests/user_details.cpp
${CMAKE_CURRENT_SOURCE_DIR}/tests/health_check.cpp)

target_compile_options(countly-tests PRIVATE $<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-g>)
target_compile_definitions(countly-tests PRIVATE COUNTLY_BUILD_TESTS)
Expand Down
53 changes: 53 additions & 0 deletions include/countly.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#undef ERROR
#endif
#include "countly/event.hpp"
#include "countly/health_check_module.hpp"
#include "countly/logger_module.hpp"
#include "countly/storage_module_base.hpp"
#include "countly/views_module.hpp"
Expand Down Expand Up @@ -359,6 +360,15 @@ class Countly : public cly::CountlyDelegates {
configuration->sessionDuration = updateInterval;
}

/**
* Disables the SDK health check. Once after start the SDK sends the server a short report about its own state:
* how many warnings and errors it logged, the last request the server refused, how often its session, event,
* view and crash methods were called, and which known problems occurred. It carries no data the app recorded,
* and the server shows it in SDK Manager. With this option the report is not sent; the SDK keeps counting.
* Call it before start().
*/
void disableHealthCheck();

/**
* Disable SDK behavior settings updates that SDK performs periodically from the server.
*/
Expand Down Expand Up @@ -431,6 +441,12 @@ class Countly : public cly::CountlyDelegates {

inline const CountlyConfiguration &getConfiguration() { return *configuration.get(); }

/**
* The health check counters of this instance.
* Warning: This method is for debugging purposes, and it is going to be removed in the future.
*/
inline std::shared_ptr<cly::HealthCheckModule> debugHealthCheck() { return health_check; }

static void halt();

static size_t debugClaimedPathCount();
Expand Down Expand Up @@ -496,6 +512,38 @@ class Countly : public cly::CountlyDelegates {

void updateLoop();
void packEvents();

/**
* Begins a session, counted in the health check as a call of the app or, with 'automatic', of the SDK.
* A call of the app while a session runs is reported as log code w760.
*/
bool beginSessionInternal(bool automatic);

/**
* Updates the session, counted in the health check as a call of the app or, with 'automatic', of the SDK.
* A call of the app in manual session control without a running session is reported as log code w760.
*/
bool updateSessionInternal(bool automatic);

/**
* Ends the session, counted in the health check as a call of the app or, with 'automatic', of the SDK.
* A call of the app without a running session is reported as log code w760.
*/
bool endSessionInternal(bool automatic);

/**
* Sends the health check once, as an immediate request to /i, unless it is disabled, the SDK behavior settings
* turn networking off, or it was sent already. It carries the base request parameters, the app version as
* metrics and the counters. Only what it reported is taken away from the counters, and only once the server
* accepted it with a 2xx answer that is a JSON object with a "result". Runs on the update thread, so the
* request never holds up start() and finishes before the instance is destroyed.
*/
void sendHealthCheck();

/**
* Stores the health check counters when a session update interval passed since they were last stored.
*/
void saveHealthCheckOnTimer();
bool began_session = false;
std::atomic<bool> is_being_disposed{false};
std::atomic<bool> is_sdk_initialized{false};
Expand All @@ -517,6 +565,11 @@ class Countly : public cly::CountlyDelegates {
std::unique_ptr<cly::CrashModule> crash_module;
std::unique_ptr<cly::ViewsModule> views_module;

// Health check counters; created with the instance, so warnings logged before start() are counted too.
std::shared_ptr<cly::HealthCheckModule> health_check = std::make_shared<cly::HealthCheckModule>();
std::atomic<bool> health_check_sent{false};
std::chrono::steady_clock::time_point last_health_check_save = std::chrono::steady_clock::now();

std::shared_ptr<cly::CountlyConfiguration> configuration;
std::shared_ptr<cly::LoggerModule> logger;
std::shared_ptr<cly::RequestBuilder> requestBuilder;
Expand Down
9 changes: 9 additions & 0 deletions include/countly/constants.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ namespace cly {
struct HTTPResponse {
bool success;
nlohmann::json data;
/**
* HTTP status code of the answer, 0 when no answer arrived or the HTTP client did not set it.
* The health check reports the code and the body of the last answer that refused a request.
*/
long status_code = 0;
/**
* Body of the answer as text, empty when there was none or the HTTP client did not set it.
*/
std::string body;
};

using HTTPClientFunction = std::function<HTTPResponse(bool, const std::string &, const std::string &)>;
Expand Down
5 changes: 5 additions & 0 deletions include/countly/countly_configuration.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ struct CountlyConfiguration {

bool sdkBehaviorSettingsUpdatesDisabled = false;

/**
* Whether the SDK sends its health check after start.
*/
bool healthCheckEnabled = true;

std::string sdkBehaviorSettings;

CountlyConfiguration(const std::string appKey, std::string serverUrl) {
Expand Down
112 changes: 112 additions & 0 deletions include/countly/health_check_module.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#ifndef HEALTH_CHECK_MODULE_HPP_
#define HEALTH_CHECK_MODULE_HPP_

#include <map>
#include <memory>
#include <mutex>
#include <string>

#include "nlohmann/json.hpp"

namespace cly {
class LoggerModule;
class StorageModuleBase;

/**
* The counters of the SDK health check: warnings and errors logged, the last request the server
* refused, backoffs, method usage ("fu") and log codes ("lc"), kept since the last health check
* the server accepted. Logs at debug level only, as every warning and error the SDK logs is
* counted here.
*/
class HealthCheckModule {
public:
static const int COUNT_CAP = 65535;
static const size_t MAX_ERROR_MESSAGE_LENGTH = 1000;

HealthCheckModule();
~HealthCheckModule();

/**
* Sets the logger the module writes its debug lines to. Held weakly, as the logger holds this module.
*/
void setLogger(std::weak_ptr<LoggerModule> logger);

/**
* Sets the storage the counters are kept in and adds the counters a previous run stored to the
* current ones. A stored state that is not valid JSON is cleared and reported as log code e503.
* Held weakly, as the storage holds the logger, which holds this module.
*/
void setStorage(std::shared_ptr<StorageModuleBase> storage);

/** Counts one warning the SDK logged. */
void logWarning();

/** Counts one error the SDK logged. */
void logError();

/**
* Keeps the answer of the server to a queued request it did not accept. Only the most recent one
* is kept. A status code outside 1..999 is ignored, and the body is cut to 1000 characters.
*/
void logFailedNetworkRequest(long statusCode, const std::string &errorResponse);

/**
* Counts a backoff of the request queue and extends the current run of backoffs in a row. "cbom"
* holds the longest run right away. Records log code w320, and w321 when the backoff continues a run.
*/
void logBackoffRequest();

/** Ends the current run of backoffs in a row, when a queued request was answered in time. */
void logConsecutiveBackoffRequest();

/**
* Counts one call of a method for the "fu" map. A call the SDK made by itself carries ":a", a
* deprecated method ":d". Empty names are ignored, and every count stops at 65535.
*/
void recordUsage(const std::string &area, const std::string &method);

/** Counts occurrences of a log code for the "lc" map, a level letter and three digits such as "w740". */
void recordLogCode(const std::string &code, int count = 1);

/**
* The health check parameter as it is sent. "fu" and "lc" are always present, empty when nothing
* was counted, so the server can tell an SDK that reports them from one that does not.
*/
nlohmann::json snapshot();

/**
* Takes away what an accepted health check reported, so what was counted while it was on its way
* is reported next time. The last failed request and the longest backoff run are cleared only
* when they did not change since. Stores what is left.
*/
void clearSentValues(const nlohmann::json &sent);

/** Stores the counters, when a storage is set. */
void saveState();

/** Resets every counter to its default and clears the stored state. */
void clearAndSave();

private:
void setupInitialCounters(const std::string &initialState);
void addUsage(const std::string &area, const std::string &method, long long count);
void addLogCode(const std::string &code, long long count);
nlohmann::json toJson(bool forStorage);
void debugLog(const std::string &message);

std::mutex _mutex;
std::weak_ptr<LoggerModule> _logger;
std::weak_ptr<StorageModuleBase> _storage;

long long _countLogWarning = 0;
long long _countLogError = 0;
long _statusCode = -1;
std::string _errorMessage;
long long _countBackoffRequest = 0;
long long _consecutiveBackoffRequest = 0;
long long _consecutiveBackoffRequestCounter = 0;
std::map<std::string, std::map<std::string, long long>> _methodUsage;
std::map<std::string, long long> _logCodes;
};
} // namespace cly
#endif
30 changes: 30 additions & 0 deletions include/countly/logger_module.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
namespace cly {
enum class LogLevel { DEBUG = 1, INFO = 2, WARNING = 3, ERROR = 4, FATAL = 5 };
using LoggerFunction = std::function<void(LogLevel, const std::string &)>;
class HealthCheckModule;

class LoggerModule {
public:
Expand Down Expand Up @@ -45,6 +46,35 @@ class LoggerModule {
*/
static bool isInsideCallback();

/**
* Hands the health check counters to the logger. From then on every WARNING is counted as a
* warning and every ERROR or FATAL as an error, whether or not a log callback is set.
*
* @param healthCheck the counters of this instance
*/
void setHealthCheck(std::shared_ptr<HealthCheckModule> healthCheck);

/**
* @return the health check counters of this instance, empty before they are set
*/
std::shared_ptr<HealthCheckModule> healthCheck() const;

/**
* Records a health check log code, such as "w740", next to the log line of its condition.
*
* @param code the log code
* @param count how many times the condition occurred
*/
void recordLogCode(const std::string &code, int count = 1);

/**
* Counts one call of a method for the health check method usage.
*
* @param area the feature area: "sessions", "events", "views" or "crashes"
* @param method the method name, with ":a" for a call the SDK made by itself
*/
void recordUsage(const std::string &area, const std::string &method);

private:
class LoggerModuleImpl;
std::unique_ptr<LoggerModuleImpl> impl;
Expand Down
12 changes: 12 additions & 0 deletions include/countly/storage_module_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ class StorageModuleBase {
virtual void storeSDKBehaviorSettings(const std::string &sdk_behavior_settings) = 0;

virtual std::string getSDKBehaviorSettings() = 0;

/**
* Store the health check counters.
* @param state: the counters as JSON text, empty to clear them
*/
virtual void storeHealthCheckState(const std::string &state) { (void)state; }

/**
* Read the stored health check counters.
* @return the counters as JSON text, empty when none are stored
*/
virtual std::string getHealthCheckState() { return std::string(); }
};

} // namespace cly
Expand Down
2 changes: 2 additions & 0 deletions include/countly/storage_module_db.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class StorageModuleDB : public StorageModuleBase {
void RQInsertAtEnd(const std::string &request) override;
void storeSDKBehaviorSettings(const std::string &sdk_behavior_settings) override;
std::string getSDKBehaviorSettings() override;
void storeHealthCheckState(const std::string &state) override;
std::string getHealthCheckState() override;
};
} // namespace cly
#endif
2 changes: 1 addition & 1 deletion src/configuration_module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ class ConfigurationModule::ConfigurationModuleImpl {
}

void _stopTimer() {
_logger->log(LogLevel::WARNING, "[Countly] [ConfigurationModule] stopTimer, stopping server config update timer thread.");
_logger->log(LogLevel::DEBUG, "[Countly] [ConfigurationModule] stopTimer, stopping server config update timer thread.");

{
// The flag has to be mutated under the same mutex the waiter evaluates it
Expand Down
Loading
Loading