diff --git a/.clang-format b/.clang-format index 865f732..1bfe00b 100644 --- a/.clang-format +++ b/.clang-format @@ -1,4 +1,3 @@ ---- Language: Cpp # BasedOnStyle: Google AccessModifierOffset: -1 @@ -20,22 +19,22 @@ AlwaysBreakTemplateDeclarations: Yes BinPackArguments: true BinPackParameters: true BraceWrapping: - AfterClass: false - AfterControlStatement: Never - AfterEnum: false - AfterFunction: false - AfterNamespace: false - AfterObjCDeclaration: false - AfterStruct: false - AfterUnion: false - BeforeCatch: false - BeforeElse: false + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + BeforeCatch: true + BeforeElse: true IndentBraces: false BreakBeforeBinaryOperators: None -BreakBeforeBraces: Attach +BreakBeforeBraces: Allman BreakBeforeTernaryOperators: true BreakConstructorInitializersBeforeComma: false -ColumnLimit: 100 +ColumnLimit: 120 CommentPragmas: '^ IWYU pragma:' ConstructorInitializerAllOnOneLineOrOnePerLine: true ConstructorInitializerIndentWidth: 4 @@ -53,7 +52,7 @@ IncludeCategories: - Regex: '.*' Priority: 3 IndentCaseLabels: true -IndentWidth: 2 +IndentWidth: 4 IndentWrappedFunctionNames: false KeepEmptyLinesAtTheStartOfBlocks: false MacroBlockBegin: '' @@ -84,5 +83,4 @@ SpacesInParentheses: false SpacesInSquareBrackets: false Standard: Auto TabWidth: 8 -UseTab: Never -... +UseTab: Never \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 01ef513..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Build - -on: - pull_request: - push: - branches: - - main - -jobs: - build: - if: ${{ github.repository == 'Netflix/spectator-cpp' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Restore Conan Cache - id: conan-cache-restore - uses: actions/cache/restore@v4 - with: - path: | - /home/runner/.conan2 - /home/runner/work/spectator-cpp/spectator-cpp/cmake-build - key: ${{ runner.os }}-conan - - - name: Install System Dependencies - run: | - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test - sudo apt-get update && sudo apt-get install -y binutils-dev g++-13 libiberty-dev - - - name: Build - run: | - ./setup-venv.sh - source venv/bin/activate - ./build.sh - - - name: Save Conan Cache - id: conan-cache-save - uses: actions/cache/save@v4 - with: - path: | - /home/runner/.conan2 - /home/runner/work/spectator-cpp/spectator-cpp/cmake-build - key: ${{ steps.conan-cache-restore.outputs.cache-primary-key }} diff --git a/.gitignore b/.gitignore index faa4c63..656c726 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ .DS_Store .idea/ +.vscode/ CMakeUserPresets.json cmake-build/ +cmake-build-debug/ +cmake-build-release/ conan_provider.cmake spectator/valid_chars.inc -venv/ +venv/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ae3e8c..902405d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,70 +1,25 @@ -cmake_minimum_required(VERSION 3.23) - -project(spectator-cpp) +cmake_minimum_required(VERSION 3.15) +project(spectator-cpp VERSION 2.0 LANGUAGES CXX) +# Set C++ standard set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED True) +set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) -add_compile_options(-pedantic -Werror -Wall -Wno-missing-braces -fno-omit-frame-pointer "$<$:-fsanitize=address>") +# Options +option(BUILD_TESTS "Build the test suite" ON) -find_package(absl REQUIRED) -find_package(asio REQUIRED) -find_package(Backward REQUIRED) -find_package(fmt REQUIRED) -find_package(GTest REQUIRED) +# Find dependencies (handled by Conan) find_package(spdlog REQUIRED) +find_package(GTest REQUIRED) +find_package(Boost REQUIRED COMPONENTS system) -include(CTest) - -#-- spectator_test test executable -file(GLOB spectator_test_source_files - "spectator/*_test.cc" - "spectator/test_*.cc" - "spectator/test_*.h" -) -add_executable(spectator_test ${spectator_test_source_files}) -target_link_libraries(spectator_test - spectator - gtest::gtest -) -add_test( - NAME spectator_test - COMMAND spectator_test - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} -) - -#-- spectator library -add_library(spectator SHARED - "spectator/logger.cc" - "spectator/publisher.cc" - "spectator/config.h" - "spectator/id.h" - "spectator/logger.h" - "spectator/measurement.h" - "spectator/meter_type.h" - "spectator/publisher.h" - "spectator/registry.h" - "spectator/stateful_meters.h" - "spectator/stateless_meters.h" - "spectator/valid_chars.inc" -) -target_link_libraries(spectator - abseil::abseil - asio::asio - Backward::Backward - fmt::fmt - spdlog::spdlog -) - -#-- generator tools -add_executable(gen_valid_chars "tools/gen_valid_chars.cc") +# Build tests if enabled +if(BUILD_TESTS) + enable_testing() +endif() -#-- file generators, must exist where the outputs are referenced -add_custom_command( - OUTPUT "spectator/valid_chars.inc" - COMMAND "${CMAKE_BINARY_DIR}/bin/gen_valid_chars" > "${CMAKE_SOURCE_DIR}/spectator/valid_chars.inc" - DEPENDS gen_valid_chars -) +# Add subdirectories +add_subdirectory(libs) +add_subdirectory(spectator) +add_subdirectory(performance_tests) diff --git a/Dockerfiles/README.md b/Dockerfiles/README.md new file mode 100644 index 0000000..1adfc42 --- /dev/null +++ b/Dockerfiles/README.md @@ -0,0 +1,32 @@ +# Docker Build :hammer_and_wrench: + +The `spectator-cpp` project also supports a platform-agnostic build. The only prerequisite is the +installation of `Docker`. Once `Docker` is installed, you can build the project by running the +following commands from the root directory of the project. + +## Linux & Mac :penguin: + +##### Warning: + +- Do not prepend the command with `sudo` on Mac +- Start `Docker` before opening terminal on Mac + +```shell +sudo docker build -t spectator-cpp-image -f Dockerfiles/Ubuntu.Dockerfile . +sudo docker run -it spectator-cpp-image +./build.sh +``` + +## Windows + +##### Warning: + +- Start `Docker` before opening `Powershell` + +```shell +docker build -t spectator-cpp-image -f Dockerfiles/Ubuntu.Dockerfile . +docker run -it spectator-cpp-image /bin/bash +apt-get install dos2unix +dos2unix build.sh +./build.sh +``` \ No newline at end of file diff --git a/Dockerfiles/Ubuntu.Dockerfile b/Dockerfiles/Ubuntu.Dockerfile new file mode 100644 index 0000000..97662a8 --- /dev/null +++ b/Dockerfiles/Ubuntu.Dockerfile @@ -0,0 +1,26 @@ +# Use the official Ubuntu base image from Docker Hub +FROM ubuntu:latest + +# Add a few required packages for building and developer tools +RUN apt-get update && apt-get install -y \ + vim \ + git \ + python3 \ + python3-venv \ + gcc-13\ + g++-13 \ + cmake \ + build-essential + +# Create a default working directory +WORKDIR /home/ubuntu/spectator-cpp + +# Copy all files & folders in the projects root directory +# Exclude files listed in the dockerignore file +COPY ../ /home/ubuntu/spectator-cpp + +# Setup Python virtual environment using the existing script +RUN chmod +x setup-venv.sh && ./setup-venv.sh + +# When container starts, activate the virtual environment +ENTRYPOINT ["/bin/bash", "-c", "source venv/bin/activate && exec /bin/bash"] \ No newline at end of file diff --git a/Dockerfiles/Ubuntu.Dockerfile.dockerignore b/Dockerfiles/Ubuntu.Dockerfile.dockerignore new file mode 100644 index 0000000..462844a --- /dev/null +++ b/Dockerfiles/Ubuntu.Dockerfile.dockerignore @@ -0,0 +1,2 @@ +# Ignore copying the default build folder if it exists +cmake-build/ \ No newline at end of file diff --git a/LICENSE b/LICENSE index 7f8ced0..4841759 100644 --- a/LICENSE +++ b/LICENSE @@ -199,4 +199,4 @@ 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. + limitations under the License. \ No newline at end of file diff --git a/OSSMETADATA b/OSSMETADATA index b96d4a4..6c7e106 100644 --- a/OSSMETADATA +++ b/OSSMETADATA @@ -1 +1 @@ -osslifecycle=active +osslifecycle=active \ No newline at end of file diff --git a/README.md b/README.md index 748636c..d80fe38 100644 --- a/README.md +++ b/README.md @@ -8,72 +8,7 @@ consists of a thin client designed to send metrics through [spectatord](https:// ## Instrumenting Code ```C++ -#include -// use default values -static constexpr auto kDefault = 0; - -struct Request { - std::string country; -}; - -struct Response { - int status; - int size; -}; - -class Server { - public: - explicit Server(spectator::Registry* registry) - : registry_{registry}, - request_count_id_{registry->CreateId("server.requestCount", spectator::Tags{})}, - request_latency_{registry->GetTimer("server.requestLatency")}, - response_size_{registry->GetDistributionSummary("server.responseSizes")} {} - - Response Handle(const Request& request) { - auto start = std::chrono::steady_clock::now(); - - // do some work and obtain a response... - Response res{200, 64}; - - // Update the Counter id with dimensions, based on information in the request. The Counter - // will be looked up in the Registry, which is a fairly cheap operation, about the same as - // the lookup of an id object in a map. However, it is more expensive than having a local - // variable set to the Counter. - auto cnt_id = request_count_id_ - ->WithTag("country", request.country) - ->WithTag("status", std::to_string(res.status)); - registry_->GetCounter(std::move(cnt_id))->Increment(); - request_latency_->Record(std::chrono::steady_clock::now() - start); - response_size_->Record(res.size); - return res; - } - - private: - spectator::Registry* registry_; - std::shared_ptr request_count_id_; - std::shared_ptr request_latency_; - std::shared_ptr response_size_; -}; - -Request get_next_request() { - return Request{"US"}; -} - -int main() { - auto logger = spdlog::stdout_color_mt("console"); - std::unordered_map common_tags{{"xatlas.process", "some-sidecar"}}; - spectator::Config cfg{"unix:/run/spectatord/spectatord.unix", common_tags}; - spectator::Registry registry{std::move(cfg), logger); - - Server server{®istry}; - - for (auto i = 1; i <= 3; ++i) { - // get a request - auto req = get_next_request(); - server.Handle(req); - } -} ``` ## High-Volume Publishing @@ -82,7 +17,7 @@ By default, the library sends every meter change to the spectatord sidecar immed `send` call and underlying system calls, and may not be the most efficient way to publish metrics in high-volume use cases. For this purpose a simple buffering functionality in `Publisher` is implemented, and it can be turned on by passing a buffer size to the `spectator::Config` constructor. It is important to note that, until this buffer -fills up, the `Publisher` will not send nay meters to the sidecar. Therefore, if your application doesn't emit +fills up, the `Publisher` will not send any meters to the sidecar. Therefore, if your application doesn't emit meters at a high rate, you should either keep the buffer very small, or do not configure a buffer size at all, which will fall back to the "publish immediately" mode of operation. @@ -106,4 +41,4 @@ source venv/bin/activate * Open the project. The wizard will show three CMake profiles. * Disable the default Cmake `Debug` profile. * Enable the CMake `conan-debug` profile. - * CLion > View > Tool Windows > Conan > (gear) > Conan Executable: `$PROJECT_HOME/venv/bin/conan` + * CLion > View > Tool Windows > Conan > (gear) > Conan Executable: `$PROJECT_HOME/venv/bin/conan` \ No newline at end of file diff --git a/build.sh b/build.sh index 5dd1479..03ce752 100755 --- a/build.sh +++ b/build.sh @@ -19,7 +19,7 @@ NC="\033[0m" if [[ "$1" == "clean" ]]; then echo -e "${BLUE}==== clean ====${NC}" rm -rf "$BUILD_DIR" - rm -f spectator/*.inc + rm -rf lib/spectator if [[ "$2" == "--confirm" ]]; then # remove all packages from the conan cache, to allow swapping between Release/Debug builds conan remove "*" --confirm @@ -27,18 +27,13 @@ if [[ "$1" == "clean" ]]; then fi if [[ "$OSTYPE" == "linux-gnu"* ]]; then - if [[ -z "$CC" || -z "$CXX" ]]; then - export CC=gcc-13 - export CXX=g++-13 + source /etc/os-release + if [[ "$NAME" == "Ubuntu" ]]; then + if [[ -z "$CC" ]]; then export CC=gcc-13; fi + if [[ -z "$CXX" ]]; then export CXX=g++-13; fi fi fi -echo -e "${BLUE}==== env configuration ====${NC}" -echo "BUILD_DIR=$BUILD_DIR" -echo "BUILD_TYPE=$BUILD_TYPE" -echo "CC=$CC" -echo "CXX=$CXX" - if [[ ! -f "$HOME/.conan2/profiles/default" ]]; then echo -e "${BLUE}==== create default profile ====${NC}" conan profile detect @@ -47,13 +42,16 @@ fi if [[ ! -d $BUILD_DIR ]]; then echo -e "${BLUE}==== install required dependencies ====${NC}" if [[ "$BUILD_TYPE" == "Debug" ]]; then - conan install . --output-folder="$BUILD_DIR" --build="*" --settings=build_type="$BUILD_TYPE" --profile=./sanitized + conan install . --output-folder="$BUILD_DIR" --build="*" --settings=build_type="$BUILD_TYPE" else - conan install . --output-folder="$BUILD_DIR" --build=missing --settings=build_type="$BUILD_TYPE" + conan install . --output-folder="$BUILD_DIR" --build=missing fi + + echo -e "${BLUE}==== install source dependencies ====${NC}" + conan source . fi -pushd $BUILD_DIR +pushd "$BUILD_DIR" echo -e "${BLUE}==== configure conan environment to access tools ====${NC}" source conanbuild.sh @@ -63,7 +61,7 @@ if [[ $OSTYPE == "darwin"* ]]; then fi echo -e "${BLUE}==== generate build files ====${NC}" -cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=$BUILD_TYPE +cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE="$BUILD_TYPE" echo -e "${BLUE}==== build ====${NC}" cmake --build . @@ -73,4 +71,4 @@ if [[ "$1" != "skiptest" ]]; then GTEST_COLOR=1 ctest --verbose fi -popd +popd \ No newline at end of file diff --git a/conanfile.py b/conanfile.py index a975f22..ada0578 100644 --- a/conanfile.py +++ b/conanfile.py @@ -4,12 +4,9 @@ class SpectatorCppConan(ConanFile): settings = "os", "compiler", "build_type", "arch" requires = ( - "abseil/20240116.2", - "asio/1.32.0", - "backward-cpp/1.6", - "fmt/11.0.2", - "gtest/1.15.0", "spdlog/1.15.0", + "gtest/1.14.0", + "boost/1.83.0", ) tool_requires = () generators = "CMakeDeps", "CMakeToolchain" diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt new file mode 100644 index 0000000..a289f84 --- /dev/null +++ b/libs/CMakeLists.txt @@ -0,0 +1,5 @@ +add_subdirectory(config) +add_subdirectory(logger) +add_subdirectory(meter) +add_subdirectory(utils) +add_subdirectory(writer) \ No newline at end of file diff --git a/libs/README.md b/libs/README.md new file mode 100644 index 0000000..95ab922 --- /dev/null +++ b/libs/README.md @@ -0,0 +1,21 @@ +# Spectator-CPP Libraries + +This directory contains all the core libraries that make up the Spectator-CPP framework. These modular components work together to build the implementation of the main Spectator Registry interface. + +## Library Overview + +| Library | Description | +|----------|----------------------------------------------------------------------------------------------| +| `config` | Configuration handling for the Spectator Registry metrics including output location and tags | +| `logger` | Logging facilities used throughout the framework | +| `meter` | Core metrics implementations including counters, gauges, timers, and meter identification | +| `utils` | Utility classes and helpers including singleton patterns | +| `writer` | Output writers for metrics data (file, memory, UDP, Unix Domain Socket) | + +## Usage + +Each library subfolder contains additional documentation describing its specific API and usage examples. See the main project README for complete integration instructions. + +## Dependencies + +Most libraries have minimal external dependencies, though some writers may require specific system capabilities for networking. \ No newline at end of file diff --git a/libs/config/CMakeLists.txt b/libs/config/CMakeLists.txt new file mode 100644 index 0000000..f324ef3 --- /dev/null +++ b/libs/config/CMakeLists.txt @@ -0,0 +1,31 @@ +add_library(spectator-config + config.cpp +) + +target_include_directories(spectator-config + PUBLIC ${CMAKE_SOURCE_DIR} +) + +target_link_libraries(spectator-config + PUBLIC + spectator-logger + spectator-writer-config + spectator-utils +) + +add_executable(config-tests + test_config.cpp +) + +target_link_libraries(config-tests PRIVATE + spectator-config + GTest::gtest + GTest::gtest_main + spectator-writer-config +) + +add_test( + NAME config-tests + COMMAND config-tests + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} +) diff --git a/libs/config/README.md b/libs/config/README.md new file mode 100644 index 0000000..d6e53255 --- /dev/null +++ b/libs/config/README.md @@ -0,0 +1,41 @@ +# Config Library + +## Overview + +The `Config` class serves as the configuration object for the Registry class, which manages how metrics are sent to SpectatorD. +The `Config` constructor takes two parameters. The first parameter is required and is a `WriterConfig` object. This object defines +how metrics will be sent to `SpectatorD`. The second parameter `extraTags` is an unordered map of strings allowing you to provide additional tags to +all of your metrics. Extra tags are additional key-value pairs attached to all metrics and will be merged with environment-derived tags. + +## Usage + +The `Config` class provides two constructors: + +```cpp +// Constructor 1: Output location only + +// Example 1 +Config config(WriterConfig(WriterTypes::Memory)); +Config config(WriterConfig(WriterTypes::UDP)) + +// Example 2 +const std::string udpUrl = std::string(WriterTypes::UDPURL) + "192.168.1.100:8125"; +Config config(WriterConfig(udpUrl)); + +// Constructor 2: Output location and tags + +// Example 1 +std::unordered_map tags = {{"app", "test-app"}, {"env", "testing"}, {"region", "us-east-1"}}; +Config config(WriterConfig(WriterTypes::Memory), tags); +``` + +## Environment Variables + +If the following environment variables are set and not empty, there key and value will also be read and applied to your tags +- `TITUS_CONTAINER_NAME`: Set the `nf.container` tag automatically +- `TITUS_PROCESS_NAME`: Set the `nf.process` tag automatically + +### Warning + +If the environment variable `SPECTATOR_OUTPUT_LOCATION` is set this will override the value specified in the `WriterConfig` +read the `WriterConfig` readme.md for more details. \ No newline at end of file diff --git a/libs/config/config.cpp b/libs/config/config.cpp new file mode 100644 index 0000000..3f16412 --- /dev/null +++ b/libs/config/config.cpp @@ -0,0 +1,71 @@ +#include + +#include +#include +#include + +#include +#include + +struct ConfigConstants +{ + static constexpr auto container = "nf.container"; + static constexpr auto process = "nf.process"; + static constexpr auto envVarContainer = "TITUS_CONTAINER_NAME"; + static constexpr auto envVarProcess = "TITUS_PROCESS_NAME"; +}; + +std::unordered_map CalculateTags( + const std::unordered_map& tags) +{ + std::unordered_map valid_tags; + + const char* container_name = std::getenv(ConfigConstants::envVarContainer); + const char* process_name = std::getenv(ConfigConstants::envVarProcess); + + if (container_name != nullptr) + { + std::string container_str(container_name); + if (IsEmptyOrWhitespace(container_str) == false) + { + valid_tags[ConfigConstants::container] = container_str; + } + } + + if (process_name != nullptr) + { + std::string process_str(process_name); + if (IsEmptyOrWhitespace(process_str) == false) + { + valid_tags[ConfigConstants::process] = process_str; + } + } + + for (const auto& [fst, snd] : tags) + { + if (IsEmptyOrWhitespace(fst) == false && IsEmptyOrWhitespace(snd) == false) + { + valid_tags[fst] = snd; + } + } + + return valid_tags; +} + +Config::Config(const WriterConfig& writerConfig, const std::unordered_map& extraTags) + : m_extraTags(CalculateTags(extraTags)), m_writerConfig(writerConfig) +{ + Logger::info("Config initialized with writer type: {}, buffer size: {}, location: {}", + WriterTypeToString(m_writerConfig.GetType()), m_writerConfig.GetBufferSize(), m_writerConfig.GetLocation()); + if (m_extraTags.empty() == true) + { + Logger::info("Config initialized with no extra tags provided."); + return; + } + + Logger::info("Config initialized with the following extra tags:"); + for (const auto& [key, value] : m_extraTags) + { + Logger::info(" {}: {}", key, value); + } +} diff --git a/libs/config/config.h b/libs/config/config.h new file mode 100644 index 0000000..0ecd7dd --- /dev/null +++ b/libs/config/config.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +#include + +class Config +{ + public: + Config(const WriterConfig& writerConfig, const std::unordered_map& extraTags = {}); + + ~Config() = default; + Config(const Config& other) = default; + Config& operator=(const Config& other) = delete; + Config(Config&& other) noexcept = delete; + Config& operator=(Config&& other) noexcept = delete; + + const std::unordered_map& GetExtraTags() const noexcept { return m_extraTags; } + + const std::string& GetWriterLocation() const noexcept { return m_writerConfig.GetLocation(); } + const WriterType& GetWriterType() const noexcept { return m_writerConfig.GetType(); } + const unsigned int GetWriterBufferSize() const noexcept { return m_writerConfig.GetBufferSize(); } + + + private: + std::unordered_map m_extraTags; + WriterConfig m_writerConfig; +}; \ No newline at end of file diff --git a/libs/config/test_config.cpp b/libs/config/test_config.cpp new file mode 100644 index 0000000..716adf1 --- /dev/null +++ b/libs/config/test_config.cpp @@ -0,0 +1,218 @@ +#include +#include + +#include +#include + +// Enhanced helper to temporarily modify an environment variable for testing +class EnvironmentVariableGuard +{ + public: + explicit EnvironmentVariableGuard(const std::string& name) : m_name(name) + { + if (const char* value = std::getenv(name.c_str())) + { + m_originalValue = value; + } + } + + void setValue(const std::string& value) const { setenv(m_name.c_str(), value.c_str(), 1); } + + void unsetValue() const { unsetenv(m_name.c_str()); } + + ~EnvironmentVariableGuard() + { + if (m_originalValue.has_value()) + { + setenv(m_name.c_str(), m_originalValue->c_str(), 1); + } + else + { + unsetenv(m_name.c_str()); + } + } + + private: + std::string m_name; + std::optional m_originalValue; +}; + +class ConfigTest : public testing::Test +{ + protected: + // Create guards for each environment variable + EnvironmentVariableGuard containerGuard{"TITUS_CONTAINER_NAME"}; + EnvironmentVariableGuard processGuard{"TITUS_PROCESS_NAME"}; + + void SetUp() override + { + // Ensure environment variables are unset before each test + containerGuard.unsetValue(); + processGuard.unsetValue(); + } +}; + +// Test initialization with different writer configs +TEST_F(ConfigTest, WriterConfigInitialization) +{ + // Test with memory writer + { + WriterConfig writerConfig(WriterTypes::Memory); + Config config(writerConfig); + + EXPECT_EQ(config.GetWriterLocation(), ""); + EXPECT_EQ(config.GetWriterType(), WriterType::Memory); + EXPECT_TRUE(config.GetExtraTags().empty()); + } + + // Test with UDP writer + { + WriterConfig writerConfig(WriterTypes::UDP); + Config config(writerConfig); + + EXPECT_EQ(config.GetWriterLocation(), DefaultLocations::UDP); + EXPECT_EQ(config.GetWriterType(), WriterType::UDP); + } + + // Test UDP URL + { + const std::string udpUrl = std::string(WriterTypes::UDPURL) + "192.168.1.100:8125"; + WriterConfig writerConfig(udpUrl); + Config config(writerConfig); + + EXPECT_EQ(config.GetWriterType(), WriterType::UDP); + EXPECT_EQ(config.GetWriterLocation(), udpUrl); + } +} + +// Test extra tags handling +TEST_F(ConfigTest, ExtraTags) +{ + WriterConfig writerConfig(WriterTypes::Memory); + + // Empty tags + { + Config config(writerConfig, {}); + EXPECT_TRUE(config.GetExtraTags().empty()); + } + + // Valid tags + { + std::unordered_map tags = { + {"app", "test-app"}, {"env", "testing"}, {"region", "us-east-1"}}; + + Config config(writerConfig, tags); + + EXPECT_EQ(config.GetExtraTags().size(), 3); + EXPECT_EQ(config.GetExtraTags().at("app"), "test-app"); + EXPECT_EQ(config.GetExtraTags().at("env"), "testing"); + EXPECT_EQ(config.GetExtraTags().at("region"), "us-east-1"); + } + + // Invalid tags (empty keys or values should be ignored) + { + std::unordered_map tags = { + {"valid", "value"}, {"", "empty-key"}, {"empty-value", ""}}; + + Config config(writerConfig, tags); + + EXPECT_EQ(config.GetExtraTags().size(), 1); + EXPECT_EQ(config.GetExtraTags().at("valid"), "value"); + EXPECT_FALSE(config.GetExtraTags().count("")); + EXPECT_FALSE(config.GetExtraTags().count("empty-value")); + } +} + +// Test environment variable integration +TEST_F(ConfigTest, EnvironmentVariables) +{ + WriterConfig writerConfig(WriterTypes::Memory); + + // No environment variables - already unset in SetUp() + { + Config config(writerConfig); + EXPECT_TRUE(config.GetExtraTags().empty()); + } + + // With container name + { + containerGuard.setValue("test-container"); + // Process already unset from SetUp() + + Config config(writerConfig); + + EXPECT_EQ(config.GetExtraTags().size(), 1); + EXPECT_EQ(config.GetExtraTags().at("nf.container"), "test-container"); + } + + // With process name + { + containerGuard.unsetValue(); + processGuard.setValue("test-process"); + + Config config(writerConfig); + + EXPECT_EQ(config.GetExtraTags().size(), 1); + EXPECT_EQ(config.GetExtraTags().at("nf.process"), "test-process"); + } + + // With both environment variables + { + containerGuard.setValue("test-container"); + processGuard.setValue("test-process"); + + Config config(writerConfig); + + EXPECT_EQ(config.GetExtraTags().size(), 2); + EXPECT_EQ(config.GetExtraTags().at("nf.container"), "test-container"); + EXPECT_EQ(config.GetExtraTags().at("nf.process"), "test-process"); + } +} + +// Test merging of environment variables and explicit tags +TEST_F(ConfigTest, MergingTags) +{ + WriterConfig writerConfig(WriterTypes::Memory); + + // Environment variables with additional tags + { + containerGuard.setValue("test-container"); + processGuard.setValue("test-process"); + + std::unordered_map tags = {{"custom", "value"}, {"env", "test"}}; + + Config config(writerConfig, tags); + + EXPECT_EQ(config.GetExtraTags().size(), 4); + EXPECT_EQ(config.GetExtraTags().at("nf.container"), "test-container"); + EXPECT_EQ(config.GetExtraTags().at("nf.process"), "test-process"); + EXPECT_EQ(config.GetExtraTags().at("custom"), "value"); + EXPECT_EQ(config.GetExtraTags().at("env"), "test"); + } + + // Override environment variables with explicit tags + { + containerGuard.setValue("test-container"); + processGuard.unsetValue(); + + std::unordered_map tags = {{"nf.container", "override-container"}}; + + Config config(writerConfig, tags); + + EXPECT_EQ(config.GetExtraTags().size(), 1); + EXPECT_EQ(config.GetExtraTags().at("nf.container"), "override-container"); + } + + { + containerGuard.setValue(" "); + processGuard.setValue(""); + + std::unordered_map tags = {{"custom", "value"}, {"env", "test"}}; + + Config config(writerConfig, tags); + + EXPECT_EQ(config.GetExtraTags().size(), 2); + EXPECT_EQ(config.GetExtraTags().at("custom"), "value"); + EXPECT_EQ(config.GetExtraTags().at("env"), "test"); + } +} \ No newline at end of file diff --git a/libs/logger/CMakeLists.txt b/libs/logger/CMakeLists.txt new file mode 100644 index 0000000..4d73d98 --- /dev/null +++ b/libs/logger/CMakeLists.txt @@ -0,0 +1,12 @@ +add_library(spectator-logger INTERFACE) + +target_include_directories(spectator-logger + INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(spectator-logger + INTERFACE + spectator-utils + spdlog::spdlog +) \ No newline at end of file diff --git a/libs/logger/logger.h b/libs/logger/logger.h new file mode 100644 index 0000000..5450ecc --- /dev/null +++ b/libs/logger/logger.h @@ -0,0 +1,95 @@ +#pragma once + +#include + +#include +#include +#include + +#include +#include +#include +#include + +constexpr const char* kMainLogger = "spectator"; + +class Logger final : public Singleton +{ + private: + spdlog::logger* m_logger; // Use raw pointer, not unique_ptr + inline static bool s_loggingEnabled = true; // Static flag to control logging (C++17 inline initialization) + + friend class Singleton; + + Logger() + { + try + { + spdlog::init_thread_pool(8192, 1); + auto sink = std::make_shared(); + auto shared_logger = std::make_shared(kMainLogger, sink, spdlog::thread_pool(), + spdlog::async_overflow_policy::block); + shared_logger->set_level(spdlog::level::debug); + spdlog::register_logger(shared_logger); + m_logger = shared_logger.get(); + } + catch (const spdlog::spdlog_ex& ex) + { + std::cerr << "Log initialization failed: " << ex.what() << "\n"; + m_logger = nullptr; + } + } + + ~Logger() = default; + Logger(const Logger&) = delete; + Logger& operator=(const Logger&) = delete; + Logger(Logger&&) = delete; + Logger& operator=(Logger&&) = delete; + + public: + static spdlog::logger* GetLogger() { return GetInstance().m_logger; } + + static void debug(const std::string& msg) + { + if (s_loggingEnabled) GetLogger()->debug(msg); + } + + static void info(const std::string& msg) + { + if (s_loggingEnabled) GetLogger()->info(msg); + } + + static void warn(const std::string& msg) + { + if (s_loggingEnabled) GetLogger()->warn(msg); + } + + static void error(const std::string& msg) + { + if (s_loggingEnabled) GetLogger()->error(msg); + } + + template + static void debug(fmt::format_string fmt, Args&&... args) + { + if (s_loggingEnabled) GetLogger()->debug(fmt, std::forward(args)...); + } + + template + static void info(fmt::format_string fmt, Args&&... args) + { + if (s_loggingEnabled) GetLogger()->info(fmt, std::forward(args)...); + } + + template + static void warn(fmt::format_string fmt, Args&&... args) + { + if (s_loggingEnabled) GetLogger()->warn(fmt, std::forward(args)...); + } + + template + static void error(fmt::format_string fmt, Args&&... args) + { + if (s_loggingEnabled) GetLogger()->error(fmt, std::forward(args)...); + } +}; diff --git a/libs/meter/CMakeLists.txt b/libs/meter/CMakeLists.txt new file mode 100644 index 0000000..8d6af59 --- /dev/null +++ b/libs/meter/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(meter_id) +add_subdirectory(meter_types) \ No newline at end of file diff --git a/libs/meter/meter_id/CMakeLists.txt b/libs/meter/meter_id/CMakeLists.txt new file mode 100644 index 0000000..52dcbc6 --- /dev/null +++ b/libs/meter/meter_id/CMakeLists.txt @@ -0,0 +1,23 @@ +add_library(spectator-meter-id + meter_id.cpp +) + +target_include_directories(spectator-meter-id + PUBLIC + ${CMAKE_SOURCE_DIR} +) + +target_link_libraries(spectator-meter-id + PUBLIC + spectator-utils +) + +add_executable(MeterID-test + test_meter_id.cpp +) + +target_link_libraries(MeterID-test PRIVATE + GTest::gtest + GTest::gtest_main + spectator-meter-id +) \ No newline at end of file diff --git a/libs/meter/meter_id/meter_id.cpp b/libs/meter/meter_id/meter_id.cpp new file mode 100644 index 0000000..cd7ba48 --- /dev/null +++ b/libs/meter/meter_id/meter_id.cpp @@ -0,0 +1,101 @@ +#include + +#include +#include +#include + +// Define the static member +const std::regex INVALID_CHARS("[^-._A-Za-z0-9~^]"); + +std::unordered_map ValidateTags(const std::unordered_map& tags) +{ + std::unordered_map validTags{}; + + for (const auto& [key, value] : tags) + { + if (IsEmptyOrWhitespace(key) == false && IsEmptyOrWhitespace(value) == false) + { + validTags[key] = value; + } + } + return validTags; +} + +std::string ReplaceInvalidChars(const std::string& s) { return std::regex_replace(s, INVALID_CHARS, "_"); } + +std::string ToSpectatorId(const std::string& name, const std::unordered_map& tags) +{ + std::ostringstream ss; + ss << ReplaceInvalidChars(name); + if (!tags.empty()) + { + for (const auto& [fst, snd] : tags) + { + ss << "," << ReplaceInvalidChars(fst) << "=" << ReplaceInvalidChars(snd); + } + } + return ss.str(); +} + +MeterId::MeterId(const std::string& name, const std::unordered_map& tags) + : m_name(name), m_tags(ValidateTags(tags)) +{ + m_spectatord_id = ToSpectatorId(name, tags); +} + +MeterId MeterId::WithTag(const std::string& key, const std::string& value) const +{ + auto new_tags = m_tags; + new_tags[key] = value; + return MeterId(m_name, new_tags); +} + +MeterId MeterId::WithTags(const std::unordered_map& additional_tags) const +{ + auto new_tags = m_tags; + for (const auto& [fst, snd] : additional_tags) + { + new_tags[fst] = snd; + } + return MeterId(m_name, new_tags); +} + +bool MeterId::operator==(const MeterId& other) const { return m_name == other.m_name && m_tags == other.m_tags; } + +std::string MeterId::to_string() const +{ + std::ostringstream ss; + ss << "MeterId(name=" << m_name << ", tags={"; + bool first = true; + for (const auto& [fst, snd] : m_tags) + { + if (!first) + { + ss << ", "; + } + ss << "'" << fst << "': '" << snd << "'"; + first = false; + } + ss << "})"; + return ss.str(); +} + +// Implementation of the hash function for MeterId +size_t std::hash::operator()(const MeterId& id) const +{ + // Hash the name first + const size_t name_hash = std::hash{}(id.GetName()); + + // Hash the tags + size_t tags_hash = 0; + for (const auto& [fst, snd] : id.GetTags()) + { + // Combine key and value hashes + const size_t pair_hash = std::hash{}(fst) ^ std::hash{}(snd) << 1; + // Combine with the accumulated tags hash + tags_hash ^= pair_hash + 0x9e3779b9 + (tags_hash << 6) + (tags_hash >> 2); + } + + // Combine name hash and tags hash + return name_hash ^ tags_hash << 1; +} diff --git a/libs/meter/meter_id/meter_id.h b/libs/meter/meter_id/meter_id.h new file mode 100644 index 0000000..d546abf --- /dev/null +++ b/libs/meter/meter_id/meter_id.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include +#include + +class MeterId +{ + public: + MeterId(const std::string& name, const std::unordered_map& tags = {}); + + const std::string& GetName() const noexcept { return m_name; }; + const std::string& GetSpectatordId() const noexcept { return m_spectatord_id; } + const std::unordered_map& GetTags() const noexcept { return m_tags; }; + + MeterId WithTag(const std::string& key, const std::string& value) const; + + MeterId WithTags(const std::unordered_map& additional_tags) const; + + bool operator==(const MeterId& other) const; + + std::string to_string() const; + + private: + std::string m_name; + std::unordered_map m_tags; + std::string m_spectatord_id; +}; + +template <> +struct std::hash +{ + size_t operator()(const MeterId& id) const; +}; diff --git a/libs/meter/meter_id/test_meter_id.cpp b/libs/meter/meter_id/test_meter_id.cpp new file mode 100644 index 0000000..b024972 --- /dev/null +++ b/libs/meter/meter_id/test_meter_id.cpp @@ -0,0 +1,109 @@ +#include + +#include + +TEST(MeterIdTest, EqualsSameName) +{ + const MeterId id1("foo"); + const MeterId id2("foo"); + EXPECT_EQ(id1, id2); +} + +TEST(MeterIdTest, EqualsSameTags) +{ + const MeterId id1("foo", {{"a", "1"}, {"b", "2"}, {"c", "3"}}); + const MeterId id2("foo", {{"c", "3"}, {"b", "2"}, {"a", "1"}}); + EXPECT_EQ(id1, id2); +} + +TEST(MeterIdTest, HashSameTags) +{ + const MeterId id1("foo", {{"a", "1"}, {"b", "2"}, {"c", "3"}}); + const MeterId id2("foo", {{"c", "3"}, {"b", "2"}, {"a", "1"}}); + EXPECT_EQ(std::hash{}(id1), std::hash{}(id2)); +} + +TEST(MeterIdTest, IllegalCharsAreReplaced) +{ + const MeterId id("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo"); + EXPECT_EQ("test______^____-_~______________.___foo", id.GetSpectatordId()); +} + +TEST(MeterIdTest, LookupTags) +{ + const MeterId id1("foo", {{"a", "1"}, {"b", "2"}, {"c", "3"}}); + const MeterId id2("foo", {{"c", "3"}, {"b", "2"}, {"a", "1"}}); + std::unordered_map d; + d[id1] = "test"; + EXPECT_EQ("test", d[id2]); +} + +TEST(MeterIdTest, Name) +{ + const MeterId id1("foo", {{"a", "1"}}); + EXPECT_EQ("foo", id1.GetName()); +} + +TEST(MeterIdTest, SpectatordId) +{ + MeterId id1("foo"); + EXPECT_EQ("foo", id1.GetSpectatordId()); + + MeterId id2("bar", {{"a", "1"}}); + EXPECT_EQ("bar,a=1", id2.GetSpectatordId()); + + MeterId id3("baz", {{"a", "1"}, {"b", "2"}}); + EXPECT_EQ("baz,a=1,b=2", id3.GetSpectatordId()); +} + +TEST(MeterIdTest, ToString) +{ + MeterId id1("foo"); + EXPECT_EQ("MeterId(name=foo, tags={})", id1.to_string()); + + MeterId id2("bar", {{"a", "1"}}); + EXPECT_EQ("MeterId(name=bar, tags={'a': '1'})", id2.to_string()); + + MeterId id3("baz", {{"a", "1"}, {"b", "2"}, {"c", "3"}}); + EXPECT_EQ("MeterId(name=baz, tags={'a': '1', 'b': '2', 'c': '3'})", id3.to_string()); +} + +TEST(MeterIdTest, Tags) +{ + const MeterId id1("foo", {{"a", "1"}}); + const std::unordered_map expected = {{"a", "1"}}; + EXPECT_EQ(expected, id1.GetTags()); +} + +TEST(MeterIdTest, TagsDefensiveCopy) +{ + MeterId id1("foo", {{"a", "1"}}); + auto tags = id1.GetTags(); + tags["b"] = "2"; + std::unordered_map expected = {{"a", "1"}, {"b", "2"}}; + EXPECT_EQ(expected, tags); + std::unordered_map expected_original = {{"a", "1"}}; + EXPECT_EQ(expected_original, id1.GetTags()); +} + +TEST(MeterIdTest, WithTagReturnsNewObject) +{ + MeterId id1("foo"); + MeterId id2 = id1.WithTag("a", "1"); + EXPECT_NE(id1, id2); + std::unordered_map empty; + EXPECT_EQ(empty, id1.GetTags()); + std::unordered_map expected = {{"a", "1"}}; + EXPECT_EQ(expected, id2.GetTags()); +} + +TEST(MeterIdTest, WithTagsReturnsNewObject) +{ + MeterId id1("foo"); + MeterId id2 = id1.WithTags({{"a", "1"}, {"b", "2"}}); + EXPECT_NE(id1, id2); + std::unordered_map empty; + EXPECT_EQ(empty, id1.GetTags()); + std::unordered_map expected = {{"a", "1"}, {"b", "2"}}; + EXPECT_EQ(expected, id2.GetTags()); +} \ No newline at end of file diff --git a/libs/meter/meter_types/CMakeLists.txt b/libs/meter/meter_types/CMakeLists.txt new file mode 100644 index 0000000..77dbc9f --- /dev/null +++ b/libs/meter/meter_types/CMakeLists.txt @@ -0,0 +1,41 @@ +add_library(spectator-meter-types INTERFACE) + +target_include_directories(spectator-meter-types + INTERFACE + ${CMAKE_SOURCE_DIR} +) + +# List all the test files +set(TEST_SOURCES + test/test_age_gauge.cpp + test/test_counter.cpp + test/test_dist_summary.cpp + test/test_gauge.cpp + test/test_max_gauge.cpp + test/test_monotonic_counter.cpp + test/test_monotonic_counter_uint.cpp + test/test_percentile_dist_summary.cpp + test/test_percentile_timer.cpp + test/test_timer.cpp +) + +# Create individual test executables for each test file +foreach(test_file ${TEST_SOURCES}) + # Get the filename without extension and path + get_filename_component(test_name ${test_file} NAME_WE) + + # Create an executable for this test file + add_executable(${test_name} ${test_file}) + + # Link against GTest and other required libraries + target_link_libraries(${test_name} PRIVATE + GTest::GTest + GTest::Main + spectator-meter-types + spectator-meter-id + spectator-writer-wrapper + ) + + # Add the test to CTest + add_test(NAME ${test_name} COMMAND ${test_name}) +endforeach() \ No newline at end of file diff --git a/libs/meter/meter_types/include/age_gauge.h b/libs/meter/meter_types/include/age_gauge.h new file mode 100644 index 0000000..d6f64fd --- /dev/null +++ b/libs/meter/meter_types/include/age_gauge.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +#include + +static constexpr auto AGE_GAUGE_TYPE_SYMBOL = "A"; + +class AgeGauge final : public Meter +{ + public: + explicit AgeGauge(const MeterId& meter_id) : Meter(meter_id, AGE_GAUGE_TYPE_SYMBOL) {} + + void Now() + { + auto line = this->ConstructLine(0); + Writer::GetInstance().Write(line); + } + + void Set(const int& seconds) + { + auto line = this->ConstructLine(seconds); + Writer::GetInstance().Write(line); + } +}; diff --git a/libs/meter/meter_types/include/counter.h b/libs/meter/meter_types/include/counter.h new file mode 100644 index 0000000..49b2f8a --- /dev/null +++ b/libs/meter/meter_types/include/counter.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +#include + +static constexpr auto COUNTER_TYPE_SYMBOL = "c"; + +class Counter final : public Meter +{ + public: + explicit Counter(const MeterId& meter_id) : Meter(meter_id, COUNTER_TYPE_SYMBOL) {} + + void Increment(const double& delta = 1) + { + if (delta > 0) + { + auto line = this->ConstructLine(delta); + Writer::GetInstance().Write(line); + } + } +}; diff --git a/libs/meter/meter_types/include/dist_summary.h b/libs/meter/meter_types/include/dist_summary.h new file mode 100644 index 0000000..aaa9981 --- /dev/null +++ b/libs/meter/meter_types/include/dist_summary.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +#include + +static constexpr auto DisTRIBUTION_SUMMARY_TYPE_SYMBOL = "d"; + +class DistributionSummary final : public Meter +{ + public: + explicit DistributionSummary(const MeterId& meter_id) : Meter(meter_id, DisTRIBUTION_SUMMARY_TYPE_SYMBOL) {} + + void Record(const int& amount) + { + if (amount >= 0) + { + auto line = this->ConstructLine(amount); + Writer::GetInstance().Write(line); + } + } +}; \ No newline at end of file diff --git a/libs/meter/meter_types/include/gauge.h b/libs/meter/meter_types/include/gauge.h new file mode 100644 index 0000000..d51ad76 --- /dev/null +++ b/libs/meter/meter_types/include/gauge.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +#include +#include + +static constexpr auto GAUGE_TYPE_SYMBOL = "g"; + +class Gauge final : public Meter +{ + public: + explicit Gauge(const MeterId& meter_id, const std::optional& ttl_seconds = std::nullopt) + : Meter(meter_id, ttl_seconds.has_value() + ? GAUGE_TYPE_SYMBOL + std::string(",") + std::to_string(ttl_seconds.value()) + : GAUGE_TYPE_SYMBOL) + { + } + + void Set(const double& value) + { + auto line = this->ConstructLine(value); + Writer::GetInstance().Write(line); + } +}; \ No newline at end of file diff --git a/libs/meter/meter_types/include/max_gauge.h b/libs/meter/meter_types/include/max_gauge.h new file mode 100644 index 0000000..b741caa --- /dev/null +++ b/libs/meter/meter_types/include/max_gauge.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +#include + +static constexpr auto MAX_GAUGE_TYPE_SYMBOL = "m"; + +class MaxGauge final : public Meter +{ + public: + explicit MaxGauge(const MeterId& meter_id) : Meter(meter_id, MAX_GAUGE_TYPE_SYMBOL) {} + + void Set(const double& value) + { + auto line = this->ConstructLine(value); + Writer::GetInstance().Write(line); + } +}; \ No newline at end of file diff --git a/libs/meter/meter_types/include/meter.h b/libs/meter/meter_types/include/meter.h new file mode 100644 index 0000000..1f102b0 --- /dev/null +++ b/libs/meter/meter_types/include/meter.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +class Meter +{ + public: + static constexpr auto FIELD_SEPARATOR = ":"; + + Meter(const MeterId& meter_id, const std::string& meter_type_symbol) + : m_id(meter_id), m_meterTypeSymbol(meter_type_symbol) + { + } + virtual ~Meter() = default; + + const MeterId& GetId() const noexcept { return m_id; } + + const std::string& GetMeterTypeSymbol() const noexcept { return m_meterTypeSymbol; } + + template + inline std::string ConstructLine(const T& value) const + { + // Pre-calculate the required size to avoid reallocations + const auto& id_str = m_id.GetSpectatordId(); + const auto value_str = std::to_string(value); + std::string result; + result.reserve(m_meterTypeSymbol.size() + id_str.size() + value_str.size() + 2); // +2 for two separators + + // Build the string with append operations (more efficient than + operator) + result.append(m_meterTypeSymbol); + result.append(FIELD_SEPARATOR); + result.append(id_str); + result.append(FIELD_SEPARATOR); + result.append(value_str); + + return result; + } + + protected: + MeterId m_id; + std::string m_meterTypeSymbol; +}; diff --git a/libs/meter/meter_types/include/meter_types.h b/libs/meter/meter_types/include/meter_types.h new file mode 100644 index 0000000..6689c90 --- /dev/null +++ b/libs/meter/meter_types/include/meter_types.h @@ -0,0 +1,15 @@ +#pragma once + +// Umbrella header for all meter types + +// Individual meter type headers +#include "age_gauge.h" +#include "counter.h" +#include "dist_summary.h" +#include "gauge.h" +#include "max_gauge.h" +#include "monotonic_counter.h" +#include "monotonic_counter_uint.h" +#include "percentile_dist_summary.h" +#include "percentile_timer.h" +#include "timer.h" diff --git a/libs/meter/meter_types/include/monotonic_counter.h b/libs/meter/meter_types/include/monotonic_counter.h new file mode 100644 index 0000000..51751db --- /dev/null +++ b/libs/meter/meter_types/include/monotonic_counter.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +#include + +static constexpr auto MONOTONIC_COUNTER_TYPE_SYMBOL = "C"; + +class MonotonicCounter final : public Meter +{ + public: + explicit MonotonicCounter(const MeterId& meter_id) : Meter(meter_id, MONOTONIC_COUNTER_TYPE_SYMBOL) {} + + void Set(const double& amount) + { + auto line = this->ConstructLine(amount); + Writer::GetInstance().Write(line); + } +}; diff --git a/libs/meter/meter_types/include/monotonic_counter_uint.h b/libs/meter/meter_types/include/monotonic_counter_uint.h new file mode 100644 index 0000000..e761603 --- /dev/null +++ b/libs/meter/meter_types/include/monotonic_counter_uint.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +#include + +static constexpr auto MONOTONIC_COUNTER_UINT_TYPE_SYMBOL = "U"; + +class MonotonicCounterUint final : public Meter +{ + public: + explicit MonotonicCounterUint(const MeterId& meter_id) : Meter(meter_id, MONOTONIC_COUNTER_UINT_TYPE_SYMBOL) {} + + void Set(const uint64_t& amount) + { + auto line = this->ConstructLine(amount); + Writer::GetInstance().Write(line); + } +}; diff --git a/libs/meter/meter_types/include/percentile_dist_summary.h b/libs/meter/meter_types/include/percentile_dist_summary.h new file mode 100644 index 0000000..da4bb2b --- /dev/null +++ b/libs/meter/meter_types/include/percentile_dist_summary.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +#include + +static constexpr auto PERCENTILE_DISTRIBUTION_SUMMARY_TYPE_SYMBOL = "D"; + +class PercentileDistributionSummary final : public Meter +{ + public: + explicit PercentileDistributionSummary(const MeterId& meter_id) + : Meter(meter_id, PERCENTILE_DISTRIBUTION_SUMMARY_TYPE_SYMBOL) + { + } + + void Record(const int& amount) + { + if (amount >= 0) + { + auto line = this->ConstructLine(amount); + Writer::GetInstance().Write(line); + } + } +}; diff --git a/libs/meter/meter_types/include/percentile_timer.h b/libs/meter/meter_types/include/percentile_timer.h new file mode 100644 index 0000000..e05da8e --- /dev/null +++ b/libs/meter/meter_types/include/percentile_timer.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +#include + +static constexpr auto PERCENTILE_TIMER_TYPE_SYMBOL = "T"; + +class PercentileTimer final : public Meter +{ + public: + explicit PercentileTimer(const MeterId& meter_id) : Meter(meter_id, PERCENTILE_TIMER_TYPE_SYMBOL) {} + + void Record(const double& seconds) + { + if (seconds >= 0) + { + auto line = this->ConstructLine(seconds); + Writer::GetInstance().Write(line); + } + } +}; diff --git a/libs/meter/meter_types/include/timer.h b/libs/meter/meter_types/include/timer.h new file mode 100644 index 0000000..5ead1a3 --- /dev/null +++ b/libs/meter/meter_types/include/timer.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +#include + +static constexpr auto TIMER_TYPE_SYMBOL = "t"; + +class Timer final : public Meter +{ + public: + explicit Timer(const MeterId& meter_id) : Meter(meter_id, TIMER_TYPE_SYMBOL) {} + + void Record(const double& seconds) + { + if (seconds >= 0) + { + auto line = this->ConstructLine(seconds); + Writer::GetInstance().Write(line); + } + } +}; \ No newline at end of file diff --git a/libs/meter/meter_types/test/test_age_gauge.cpp b/libs/meter/meter_types/test/test_age_gauge.cpp new file mode 100644 index 0000000..c6cefb7 --- /dev/null +++ b/libs/meter/meter_types/test/test_age_gauge.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include + +class AgeGaugeTest : public testing::Test +{ + protected: + MeterId tid = MeterId("age_gauge"); +}; + +TEST_F(AgeGaugeTest, Now) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + AgeGauge g(tid); + EXPECT_TRUE(writer->IsEmpty()); + g.Now(); + EXPECT_EQ("A:age_gauge:0\n", writer->LastLine()); +} + +TEST_F(AgeGaugeTest, Set) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + AgeGauge g(tid); + EXPECT_TRUE(writer->IsEmpty()); + g.Set(10); + EXPECT_EQ("A:age_gauge:10\n", writer->LastLine()); +} \ No newline at end of file diff --git a/libs/meter/meter_types/test/test_counter.cpp b/libs/meter/meter_types/test/test_counter.cpp new file mode 100644 index 0000000..3a8e21e --- /dev/null +++ b/libs/meter/meter_types/test/test_counter.cpp @@ -0,0 +1,33 @@ +#include +#include + +#include + +class CounterTest : public testing::Test +{ + protected: + MeterId tid = MeterId("counter"); +}; + +TEST_F(CounterTest, increment) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + + Counter c(tid); + EXPECT_TRUE(writer->IsEmpty()); + c.Increment(); + EXPECT_EQ("c:counter:1.000000\n", writer->LastLine()); + c.Increment(2); + EXPECT_EQ("c:counter:2.000000\n", writer->LastLine()); +} + +TEST_F(CounterTest, incrementNegative) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + + Counter c(tid); + c.Increment(-1); + EXPECT_TRUE(writer->IsEmpty()); +} \ No newline at end of file diff --git a/libs/meter/meter_types/test/test_dist_summary.cpp b/libs/meter/meter_types/test/test_dist_summary.cpp new file mode 100644 index 0000000..7695515 --- /dev/null +++ b/libs/meter/meter_types/test/test_dist_summary.cpp @@ -0,0 +1,43 @@ +#include +#include + +#include + +class DistSummaryTest : public testing::Test +{ + protected: + MeterId tid = MeterId("dist_summary"); +}; + +TEST_F(DistSummaryTest, record) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + DistributionSummary ds(tid); + EXPECT_TRUE(writer->IsEmpty()); + + ds.Record(42); + EXPECT_EQ("d:dist_summary:42\n", writer->LastLine()); +} + +TEST_F(DistSummaryTest, recordNegative) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + DistributionSummary ds(tid); + EXPECT_TRUE(writer->IsEmpty()); + + ds.Record(-42); + EXPECT_TRUE(writer->IsEmpty()); +} + +TEST_F(DistSummaryTest, recordZero) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + DistributionSummary ds(tid); + EXPECT_TRUE(writer->IsEmpty()); + + ds.Record(0); + EXPECT_EQ("d:dist_summary:0\n", writer->LastLine()); +} \ No newline at end of file diff --git a/libs/meter/meter_types/test/test_gauge.cpp b/libs/meter/meter_types/test/test_gauge.cpp new file mode 100644 index 0000000..8de2060 --- /dev/null +++ b/libs/meter/meter_types/test/test_gauge.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include + +class GaugeTest : public testing::Test +{ + protected: + MeterId tid = MeterId("gauge"); +}; + +TEST_F(GaugeTest, Now) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + Gauge g(tid); + EXPECT_TRUE(writer->IsEmpty()); + g.Set(1); + EXPECT_EQ("g:gauge:1.000000\n", writer->LastLine()); +} + +TEST_F(GaugeTest, TTL) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + Gauge g(tid, 10); + EXPECT_TRUE(writer->IsEmpty()); + g.Set(42); + EXPECT_EQ("g,10:gauge:42.000000\n", writer->LastLine()); +} \ No newline at end of file diff --git a/libs/meter/meter_types/test/test_max_gauge.cpp b/libs/meter/meter_types/test/test_max_gauge.cpp new file mode 100644 index 0000000..b941bed --- /dev/null +++ b/libs/meter/meter_types/test/test_max_gauge.cpp @@ -0,0 +1,20 @@ +#include +#include + +#include + +class MaxGaugeTest : public testing::Test +{ + protected: + MeterId tid = MeterId("max_gauge"); +}; + +TEST_F(MaxGaugeTest, Set) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + MaxGauge g(tid); + EXPECT_TRUE(writer->IsEmpty()); + g.Set(0); + EXPECT_EQ("m:max_gauge:0.000000\n", writer->LastLine()); +} \ No newline at end of file diff --git a/libs/meter/meter_types/test/test_monotonic_counter.cpp b/libs/meter/meter_types/test/test_monotonic_counter.cpp new file mode 100644 index 0000000..dbef4af --- /dev/null +++ b/libs/meter/meter_types/test/test_monotonic_counter.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include + +class MonotonicCounterTest : public testing::Test +{ + protected: + MeterId tid = MeterId("monotonic_counter"); +}; + +TEST_F(MonotonicCounterTest, SetValue) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + MonotonicCounter mc(tid); + EXPECT_TRUE(writer->IsEmpty()); + mc.Set(1); + EXPECT_EQ("C:monotonic_counter:1.000000\n", writer->LastLine()); +} + +TEST_F(MonotonicCounterTest, SetNegativeValue) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + MonotonicCounter mc(tid); + EXPECT_TRUE(writer->IsEmpty()); + mc.Set(-1); + EXPECT_EQ("C:monotonic_counter:-1.000000\n", writer->LastLine()); +} \ No newline at end of file diff --git a/libs/meter/meter_types/test/test_monotonic_counter_uint.cpp b/libs/meter/meter_types/test/test_monotonic_counter_uint.cpp new file mode 100644 index 0000000..c921f83 --- /dev/null +++ b/libs/meter/meter_types/test/test_monotonic_counter_uint.cpp @@ -0,0 +1,32 @@ +#include +#include + +#include + +class MonoCounterTest : public testing::Test +{ + protected: + MeterId tid = MeterId("monotonic_counter_uint"); +}; + +TEST_F(MonoCounterTest, set) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + MonotonicCounterUint mc(tid); + EXPECT_TRUE(writer->IsEmpty()); + + mc.Set(1); + EXPECT_EQ("U:monotonic_counter_uint:1\n", writer->LastLine()); +} + +TEST_F(MonoCounterTest, setNegative) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + MonotonicCounterUint mc(tid); + EXPECT_TRUE(writer->IsEmpty()); + + mc.Set(-1); + EXPECT_EQ("U:monotonic_counter_uint:18446744073709551615\n", writer->LastLine()); +} \ No newline at end of file diff --git a/libs/meter/meter_types/test/test_percentile_dist_summary.cpp b/libs/meter/meter_types/test/test_percentile_dist_summary.cpp new file mode 100644 index 0000000..907d456 --- /dev/null +++ b/libs/meter/meter_types/test/test_percentile_dist_summary.cpp @@ -0,0 +1,43 @@ +#include +#include + +#include + +class PercentileDistSummaryTest : public testing::Test +{ + protected: + MeterId tid = MeterId("percentile_dist_summary"); +}; + +TEST_F(PercentileDistSummaryTest, record) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + PercentileDistributionSummary pds(tid); + EXPECT_TRUE(writer->IsEmpty()); + + pds.Record(42); + EXPECT_EQ("D:percentile_dist_summary:42\n", writer->LastLine()); +} + +TEST_F(PercentileDistSummaryTest, recordNegative) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + PercentileDistributionSummary pds(tid); + EXPECT_TRUE(writer->IsEmpty()); + + pds.Record(-42); + EXPECT_TRUE(writer->IsEmpty()); +} + +TEST_F(PercentileDistSummaryTest, recordZero) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + PercentileDistributionSummary pds(tid); + EXPECT_TRUE(writer->IsEmpty()); + + pds.Record(0); + EXPECT_EQ("D:percentile_dist_summary:0\n", writer->LastLine()); +} \ No newline at end of file diff --git a/libs/meter/meter_types/test/test_percentile_timer.cpp b/libs/meter/meter_types/test/test_percentile_timer.cpp new file mode 100644 index 0000000..8f6731e --- /dev/null +++ b/libs/meter/meter_types/test/test_percentile_timer.cpp @@ -0,0 +1,43 @@ +#include +#include + +#include + +class PercentileTimerTest : public testing::Test +{ + protected: + MeterId tid = MeterId("percentile_timer"); +}; + +TEST_F(PercentileTimerTest, record) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + PercentileTimer pt(tid); + EXPECT_TRUE(writer->IsEmpty()); + + pt.Record(42); + EXPECT_EQ("T:percentile_timer:42.000000\n", writer->LastLine()); +} + +TEST_F(PercentileTimerTest, recordNegative) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + PercentileTimer pt(tid); + EXPECT_TRUE(writer->IsEmpty()); + + pt.Record(-42); + EXPECT_TRUE(writer->IsEmpty()); +} + +TEST_F(PercentileTimerTest, recordZero) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + PercentileTimer pt(tid); + EXPECT_TRUE(writer->IsEmpty()); + + pt.Record(0); + EXPECT_EQ("T:percentile_timer:0.000000\n", writer->LastLine()); +} \ No newline at end of file diff --git a/libs/meter/meter_types/test/test_timer.cpp b/libs/meter/meter_types/test/test_timer.cpp new file mode 100644 index 0000000..3675c15 --- /dev/null +++ b/libs/meter/meter_types/test/test_timer.cpp @@ -0,0 +1,43 @@ +#include +#include + +#include + +class TimerTest : public testing::Test +{ + protected: + MeterId tid = MeterId("timer"); +}; + +TEST_F(TimerTest, record) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + Timer t(tid); + EXPECT_TRUE(writer->IsEmpty()); + + t.Record(42); + EXPECT_EQ("t:timer:42.000000\n", writer->LastLine()); +} + +TEST_F(TimerTest, recordNegative) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + Timer t(tid); + EXPECT_TRUE(writer->IsEmpty()); + + t.Record(-42); + EXPECT_TRUE(writer->IsEmpty()); +} + +TEST_F(TimerTest, recordZero) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + const auto* writer = dynamic_cast(WriterTestHelper::GetImpl()); + Timer t(tid); + EXPECT_TRUE(writer->IsEmpty()); + + t.Record(0); + EXPECT_EQ("t:timer:0.000000\n", writer->LastLine()); +} \ No newline at end of file diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt new file mode 100644 index 0000000..1d7e6af --- /dev/null +++ b/libs/utils/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(spectator-utils STATIC + src/util.cpp + include/singleton.h + include/util.h +) + +target_include_directories(spectator-utils + PUBLIC + ${CMAKE_SOURCE_DIR} +) + +target_link_libraries(spectator-utils + PUBLIC + spectator-meter-id +) \ No newline at end of file diff --git a/libs/utils/include/singleton.h b/libs/utils/include/singleton.h new file mode 100644 index 0000000..3f1a300 --- /dev/null +++ b/libs/utils/include/singleton.h @@ -0,0 +1,23 @@ +#pragma once +// Templated Singleton for derived singleton classes + +template +class Singleton +{ + protected: + // Protected constructor & destructor allow derived classes to instantiate + Singleton() = default; + virtual ~Singleton() = default; + + public: + // Prevent copying + Singleton(const Singleton&) = delete; + Singleton& operator=(const Singleton&) = delete; + + // Get the singleton instance + static T& GetInstance() + { + static T instance; + return instance; + } +}; diff --git a/libs/utils/include/util.h b/libs/utils/include/util.h new file mode 100644 index 0000000..b320327 --- /dev/null +++ b/libs/utils/include/util.h @@ -0,0 +1,55 @@ +# pragma once + +#include +#include +#include +#include +#include + +#include + +struct ProtocolLine +{ + char symbol; + MeterId id; + std::string value; + + bool operator==(const ProtocolLine& other) const + { + return symbol == other.symbol && id == other.id && value == other.value; + } + + [[nodiscard]] std::string to_string() const + { + std::stringstream ss; + ss << symbol << ":" << id.GetName(); + + // Sort tags by key + std::map sorted_tags(id.GetTags().begin(), id.GetTags().end()); + + // Add tags if there are any + if (!sorted_tags.empty()) + { + ss << ","; + bool first = true; + for (const auto& [key, value] : sorted_tags) + { + if (!first) + { + ss << ","; + } + ss << key << "=" << value; + first = false; + } + } + + // Add the value + ss << ":" << value; + + return ss.str(); + } +}; + +std::optional ParseProtocolLine(const std::string& line); + +bool IsEmptyOrWhitespace(const std::string& str); \ No newline at end of file diff --git a/libs/utils/src/util.cpp b/libs/utils/src/util.cpp new file mode 100644 index 0000000..6e482f1 --- /dev/null +++ b/libs/utils/src/util.cpp @@ -0,0 +1,62 @@ +#include + +#include +#include + +// Utility: split a string by a delimiter +std::vector split(const std::string& str, const char delimiter) +{ + std::vector tokens; + std::stringstream ss(str); + std::string item; + while (std::getline(ss, item, delimiter)) + { + tokens.push_back(item); + } + return tokens; +} + +std::optional ParseProtocolLine(const std::string& line) +{ + char symbol{}; + std::string name{}; + std::unordered_map tags{}; + std::string value{}; + + const auto mainParts = split(line, ':'); + + if (mainParts.size() < 3) + { + return std::nullopt; + } + + auto symbolParts = split(mainParts[0], ','); + if (!symbolParts.empty() && !symbolParts[0].empty()) + { + symbol = symbolParts[0][0]; + } + + auto idParts = split(mainParts[1], ','); + if (!idParts.empty()) + { + name = idParts[0]; + + for (size_t i = 1; i < idParts.size(); ++i) + { + auto tagParts = split(idParts[i], '='); + if (tagParts.size() == 2) + { + tags[tagParts[0]] = tagParts[1]; + } + } + } + + // The last part is the value + value = mainParts[2]; + return ProtocolLine{symbol, MeterId{name, tags}, value}; +} + +bool IsEmptyOrWhitespace(const std::string& str) +{ + return str.empty() || std::all_of(str.begin(), str.end(), [](unsigned char c) { return std::isspace(c); }); +} \ No newline at end of file diff --git a/libs/writer/CMakeLists.txt b/libs/writer/CMakeLists.txt new file mode 100644 index 0000000..b874a0b --- /dev/null +++ b/libs/writer/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(writer_wrapper) +add_subdirectory(writer_types) +add_subdirectory(writer_config) \ No newline at end of file diff --git a/libs/writer/README.md b/libs/writer/README.md new file mode 100644 index 0000000..fd20412 --- /dev/null +++ b/libs/writer/README.md @@ -0,0 +1,89 @@ +# Writer Module + +The Writer module provides a flexible system for metric data output in the Spectator C++ library. It consists of several components that handle different aspects of data writing. + +## Components + +### Writer Types (`writer_types`) + +This component defines the available writer types and their associated configurations: + +- **Supported Writer Types:** + - `Memory` - Writes data to an in-memory buffer (primarily for testing) + - `UDP` - Writes data over UDP to a specified endpoint + - `Unix` - Writes data to a Unix Domain Socket + +- **Key Features:** + - Type enumeration via `WriterType` enum class + - String constants for type names in `WriterTypes` struct + - Default locations for different writer types in `DefaultLocations` struct + - Type-to-location mapping in `TypeToLocationMap` + - String conversion via `WriterTypeToString()` function + +### Writer Config (`writer_config`) + +This component handles configuration of writers: + +- **Key Features:** + - Parses writer configuration from string identifiers + - Handles environment variable overrides via `SPECTATOR_OUTPUT_LOCATION` + - Error handling for invalid writer types + - Provides a clean API for specifying writer type and location + +- **Usage Example:** + ```cpp + // Create a writer config with a specific type + WriterConfig config(WriterTypes::UDP); + + // Or with a URL-style location + WriterConfig config("udp://192.168.1.100:8125"); + + // Environment variable overrides any provided value + // SPECTATOR_OUTPUT_LOCATION=unix:///custom/path/socket.sock + WriterConfig config(WriterTypes::UDP); // Will use Unix socket instead + ``` + +### Writer Wrapper (`writer_wrapper`) + +This component provides a wrapper around the different writer implementations: + +- **Key Features:** + - Common interface for all writer types + - Factory pattern for creating writers based on configuration + - Handles serialization and transmission of metric data + - Abstracts transport details from the rest of the library + +## Integration + +These components work together to provide a flexible metric output system: + +1. `writer_types` defines the available writer types and their default configurations +2. `writer_config` handles parsing and validating configuration values +3. `writer_wrapper` instantiates the appropriate writer implementation + +## Best Practices + +- Use the environment variable `SPECTATOR_OUTPUT_LOCATION` for runtime configuration +- Prefer URL-style configurations for explicit endpoint specification +- For UDP: `udp://host:port` +- For Unix Domain Sockets: `unix:///path/to/socket` + +## Example + +```cpp +// Create a configuration +WriterConfig writerConfig("udp://localhost:8125"); + +// Create a configuration object with the writer config +Config config(writerConfig); + +// Additional tags can be added +std::unordered_map tags = { + {"app", "my-application"}, + {"env", "production"} +}; +Config config(writerConfig, tags); + +// The config can be used to create a registry +auto registry = spectator::Registry(config); +``` \ No newline at end of file diff --git a/libs/writer/writer_config/CMakeLists.txt b/libs/writer/writer_config/CMakeLists.txt new file mode 100644 index 0000000..20c2f6a --- /dev/null +++ b/libs/writer/writer_config/CMakeLists.txt @@ -0,0 +1,30 @@ +add_library(spectator-writer-config STATIC + writer_config.cpp + writer_config.h +) + +target_include_directories(spectator-writer-config + PUBLIC + ${CMAKE_SOURCE_DIR} +) + + +target_link_libraries(spectator-writer-config + PUBLIC + spectator-writer-types + spectator-logger +) + +add_executable(test_writer_config + test_writer_config.cpp) + +target_link_libraries(test_writer_config + PRIVATE + spectator-writer-config + spectator-writer-types + GTest::gtest + GTest::gmock + GTest::gtest_main +) + +add_test(NAME writer_config_test COMMAND test_writer_config) diff --git a/libs/writer/writer_config/test_writer_config.cpp b/libs/writer/writer_config/test_writer_config.cpp new file mode 100644 index 0000000..228fb75 --- /dev/null +++ b/libs/writer/writer_config/test_writer_config.cpp @@ -0,0 +1,121 @@ +#include + +#include +#include + +#include +#include + +// Enhanced helper to temporarily modify an environment variable for testing +class EnvironmentVariableGuard +{ + public: + explicit EnvironmentVariableGuard(const std::string& name) : m_name(name) + { + if (const char* value = std::getenv(name.c_str())) + { + m_originalValue = value; + } + } + + void setValue(const std::string& value) const { setenv(m_name.c_str(), value.c_str(), 1); } + + void unsetValue() const { unsetenv(m_name.c_str()); } + + ~EnvironmentVariableGuard() + { + if (m_originalValue.has_value()) + { + setenv(m_name.c_str(), m_originalValue->c_str(), 1); + } + else + { + unsetenv(m_name.c_str()); + } + } + + private: + std::string m_name; + std::optional m_originalValue; +}; + +class WriterConfigTest : public testing::Test +{ + protected: + EnvironmentVariableGuard envGuard{"SPECTATOR_OUTPUT_LOCATION"}; + + void SetUp() override { envGuard.unsetValue(); } +}; + +TEST_F(WriterConfigTest, BasicWriterTypes) +{ + // Test "memory" type + { + const WriterConfig config(WriterTypes::Memory); + EXPECT_EQ(config.GetType(), WriterType::Memory); + EXPECT_EQ(config.GetLocation(), DefaultLocations::NoLocation); + } + + // Test "udp" type + { + const WriterConfig config(WriterTypes::UDP); + EXPECT_EQ(config.GetType(), WriterType::UDP); + EXPECT_EQ(config.GetLocation(), DefaultLocations::UDP); + } + + // Test "unix" type + { + const WriterConfig config(WriterTypes::Unix); + EXPECT_EQ(config.GetType(), WriterType::Unix); + EXPECT_EQ(config.GetLocation(), DefaultLocations::UDS); + } +} + +TEST_F(WriterConfigTest, URLBasedWriterTypes) +{ + // Test UDP URL + { + const std::string udpUrl = std::string(WriterTypes::UDPURL) + "192.168.1.100:8125"; + const WriterConfig config(udpUrl); + EXPECT_EQ(config.GetType(), WriterType::UDP); + EXPECT_EQ(config.GetLocation(), udpUrl); + } + + // Test Unix domain socket URL + { + const std::string unixUrl = std::string(WriterTypes::UnixURL) + "/var/run/custom/socket.sock"; + const WriterConfig config(unixUrl); + EXPECT_EQ(config.GetType(), WriterType::Unix); + EXPECT_EQ(config.GetLocation(), unixUrl); + } +} + +TEST_F(WriterConfigTest, BufferingConstructor) +{ + + const WriterConfig config(WriterTypes::UDP, 2048); + EXPECT_EQ(config.GetType(), WriterType::UDP); + EXPECT_EQ(config.GetBufferSize(), 2048); + EXPECT_TRUE(config.IsBufferingEnabled()); +} + +TEST_F(WriterConfigTest, InvalidWriterType) +{ + EXPECT_THROW({ WriterConfig config("invalid_type"); }, std::runtime_error); + EXPECT_THROW({ WriterConfig config(""); }, std::runtime_error); + + { + envGuard.setValue("invalid_env_value"); + EXPECT_THROW({ WriterConfig config(WriterTypes::Memory); }, std::runtime_error); + } +} + +TEST_F(WriterConfigTest, EdgeCases) +{ + // Test with just URL scheme but no path + { + const WriterConfig config("udp://"); + EXPECT_EQ(config.GetType(), WriterType::UDP); + EXPECT_EQ(config.GetLocation(), "udp://"); + } +} \ No newline at end of file diff --git a/libs/writer/writer_config/writer_config.cpp b/libs/writer/writer_config/writer_config.cpp new file mode 100644 index 0000000..d1936a0 --- /dev/null +++ b/libs/writer/writer_config/writer_config.cpp @@ -0,0 +1,57 @@ +#include + +#include + +struct WriterConfigConstants +{ + static constexpr auto RuntimeErrorMessage = "Invalid writer type: "; +}; + +std::pair GetWriterConfigFromString(const std::string& type) +{ + // Check exact matches first + if (const auto it = TypeToLocationMap.find(type); it != TypeToLocationMap.end()) + { + return {it->second.first, std::string(it->second.second)}; + } + + if (type.rfind(WriterTypes::UDPURL, 0) == 0) + { + return {WriterType::UDP, type}; + } + + if (type.rfind(WriterTypes::UnixURL, 0) == 0) + { + return {WriterType::Unix, type}; + } + + throw std::runtime_error(WriterConfigConstants::RuntimeErrorMessage + type); +} + +WriterConfig::WriterConfig(const std::string& type) +{ + if (const char* envLocation = std::getenv("SPECTATOR_OUTPUT_LOCATION"); envLocation != nullptr) + { + Logger::info("Environment variable set, SPECTATOR_OUTPUT_LOCATION: {}", envLocation); + const std::string envValue(envLocation); + auto [writer_type, location] = GetWriterConfigFromString(envValue); + m_type = writer_type; + m_location = location; + } + else + { + auto [writer_type, location] = GetWriterConfigFromString(type); + m_type = writer_type; + m_location = location; + } + Logger::info("WriterConfig initialized with type: {}, location: {}", WriterTypeToString(m_type), m_location); +} + +WriterConfig::WriterConfig(const std::string& type, const unsigned int bufferSize) + : WriterConfig(type) // Constructor delegation +{ + m_bufferSize = bufferSize; + m_isBufferingEnabled = true; + Logger::info("WriterConfig buffering enabled with size: {}", m_bufferSize); + +} \ No newline at end of file diff --git a/libs/writer/writer_config/writer_config.h b/libs/writer/writer_config/writer_config.h new file mode 100644 index 0000000..0da7610 --- /dev/null +++ b/libs/writer/writer_config/writer_config.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include +#include + +class WriterConfig +{ + public: + explicit WriterConfig(const std::string& type); + WriterConfig(const std::string& type, unsigned int bufferSize); + + [[nodiscard]] const WriterType& GetType() const noexcept { return m_type; } + [[nodiscard]] unsigned int GetBufferSize() const noexcept { return m_bufferSize; } + [[nodiscard]] bool IsBufferingEnabled() const noexcept { return m_isBufferingEnabled; } + [[nodiscard]] const std::string& GetLocation() const noexcept { return m_location; } + + private: + WriterType m_type; + std::string m_location; + unsigned int m_bufferSize = 0; + bool m_isBufferingEnabled = false; +}; \ No newline at end of file diff --git a/libs/writer/writer_types/CMakeLists.txt b/libs/writer/writer_types/CMakeLists.txt new file mode 100644 index 0000000..989d21b --- /dev/null +++ b/libs/writer/writer_types/CMakeLists.txt @@ -0,0 +1,55 @@ +add_subdirectory(test_utils) + +add_library(spectator-writer-types + src/memory_writer.cpp + src/udp_writer.cpp + src/uds_writer.cpp +) + +target_include_directories(spectator-writer-types + PUBLIC + ${CMAKE_SOURCE_DIR} +) + +target_link_libraries(spectator-writer-types + PUBLIC + spectator-logger + Boost::system +) + +set(TEST_SOURCES + test/test_memory_writer.cpp + test/test_udp_writer.cpp + test/test_uds_writer.cpp +) + + +# Create individual test executables for each test file +foreach(test_file ${TEST_SOURCES}) + # Get the filename without extension and path + get_filename_component(test_name ${test_file} NAME_WE) + + # Create an executable for this test file + add_executable(${test_name} ${test_file}) + + # Include test directory and server directories + target_include_directories(${test_name} + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/test + ${CMAKE_CURRENT_SOURCE_DIR}/test_utils/udp_server + ${CMAKE_CURRENT_SOURCE_DIR}/test_utils/uds_server + ) + + # Link against GTest and other required libraries + target_link_libraries(${test_name} PRIVATE + GTest::GTest + GTest::Main + spectator-writer-types + udp_server_lib + uds_server_lib + ) + + # Add the test to CTest + add_test(NAME ${test_name} COMMAND ${test_name}) +endforeach() + diff --git a/libs/writer/writer_types/include/base_writer.h b/libs/writer/writer_types/include/base_writer.h new file mode 100644 index 0000000..f1e88bd --- /dev/null +++ b/libs/writer/writer_types/include/base_writer.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +class BaseWriter +{ + public: + BaseWriter() = default; + virtual ~BaseWriter() = default; + + BaseWriter(const BaseWriter&) = delete; + BaseWriter& operator=(const BaseWriter&) = delete; + BaseWriter(BaseWriter&&) = delete; + BaseWriter& operator=(BaseWriter&&) = delete; + + virtual void Write(const std::string& message) = 0; + virtual void Close() = 0; +}; \ No newline at end of file diff --git a/libs/writer/writer_types/include/memory_writer.h b/libs/writer/writer_types/include/memory_writer.h new file mode 100644 index 0000000..32d38fe --- /dev/null +++ b/libs/writer/writer_types/include/memory_writer.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +class MemoryWriter final : public BaseWriter +{ + public: + MemoryWriter() = default; + ~MemoryWriter() override = default; + + void Write(const std::string& message) override; + void Close() override; + void Clear(); + + const std::vector& GetMessages() const noexcept { return m_messages; } + + const std::string& LastLine() const noexcept; + + bool IsEmpty() const noexcept { return m_messages.empty(); } + + private: + std::vector m_messages; +}; diff --git a/libs/writer/writer_types/include/udp_writer.h b/libs/writer/writer_types/include/udp_writer.h new file mode 100644 index 0000000..72d9119 --- /dev/null +++ b/libs/writer/writer_types/include/udp_writer.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include + +class UDPWriter final : public BaseWriter +{ + public: + UDPWriter(const std::string& host, int port); + ~UDPWriter() override; + void Write(const std::string& message) override; + void Close() override; + + private: + std::string m_host; + int m_port; + std::unique_ptr m_io_context; + std::unique_ptr m_socket; + boost::asio::ip::udp::endpoint m_endpoint; +}; diff --git a/libs/writer/writer_types/include/uds_writer.h b/libs/writer/writer_types/include/uds_writer.h new file mode 100644 index 0000000..22adac3 --- /dev/null +++ b/libs/writer/writer_types/include/uds_writer.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include +#include +#include + +class UDSWriter final : public BaseWriter +{ + public: + UDSWriter(const std::string& socketPath); + ~UDSWriter() override; + void Write(const std::string& message) override; + void Close() override; + + private: + std::string m_socketPath; + std::unique_ptr m_ioContext; + std::unique_ptr m_socket; + boost::asio::local::datagram_protocol::endpoint m_endpoint; + bool m_isOpen; + + // Helper method to initialize the connection + bool connect(); +}; diff --git a/libs/writer/writer_types/include/writer_types.h b/libs/writer/writer_types/include/writer_types.h new file mode 100644 index 0000000..69dce83 --- /dev/null +++ b/libs/writer/writer_types/include/writer_types.h @@ -0,0 +1,58 @@ +#pragma once + +#include "memory_writer.h" +#include "udp_writer.h" +#include "uds_writer.h" + +#include +#include +#include +#include +#include + +// Enum to specify which writer type to create +enum class WriterType +{ + Memory, + UDP, + Unix +}; + +struct WriterTypes +{ + static constexpr auto Memory = "memory"; + static constexpr auto UDP = "udp"; + static constexpr auto Unix = "unix"; + + // URL prefixes + static constexpr auto UDPURL = "udp://"; + static constexpr auto UnixURL = "unix://"; +}; + +struct DefaultLocations +{ + static constexpr auto NoLocation = ""; + static constexpr auto UDP = "udp://127.0.0.1:1234"; + static constexpr auto UDS = "unix:///run/spectatord/spectatord.unix"; +}; + +inline const std::map> TypeToLocationMap = { + {WriterTypes::Memory, {WriterType::Memory, DefaultLocations::NoLocation}}, + {WriterTypes::UDP, {WriterType::UDP, DefaultLocations::UDP}}, + {WriterTypes::Unix, {WriterType::Unix, DefaultLocations::UDS}}, +}; + +inline std::string WriterTypeToString(WriterType type) +{ + switch (type) + { + case WriterType::Memory: + return WriterTypes::Memory; + case WriterType::UDP: + return WriterTypes::UDP; + case WriterType::Unix: + return WriterTypes::Unix; + default: + return "Unknown"; + } +} \ No newline at end of file diff --git a/libs/writer/writer_types/src/memory_writer.cpp b/libs/writer/writer_types/src/memory_writer.cpp new file mode 100644 index 0000000..07e1f7c --- /dev/null +++ b/libs/writer/writer_types/src/memory_writer.cpp @@ -0,0 +1,33 @@ +#include + +#include + +void MemoryWriter::Write(const std::string& message) +{ + this->m_messages.push_back(message); + Logger::debug("MemoryWriter::Writing: {}", message); +} + +void MemoryWriter::Close() +{ + this->Clear(); + Logger::debug("MemoryWriter::Closed"); +} + +void MemoryWriter::Clear() +{ + this->m_messages.clear(); + Logger::debug("MemoryWriter::Cleared messages"); +} + +const std::string& MemoryWriter::LastLine() const noexcept +{ + static const std::string emptyString{}; + + if (true == m_messages.empty()) + { + return emptyString; + } + + return this->m_messages.back(); +} diff --git a/libs/writer/writer_types/src/udp_writer.cpp b/libs/writer/writer_types/src/udp_writer.cpp new file mode 100644 index 0000000..5d1cdf0 --- /dev/null +++ b/libs/writer/writer_types/src/udp_writer.cpp @@ -0,0 +1,73 @@ +#include + +#include + +UDPWriter::UDPWriter(const std::string& host, int port) : m_host(host), m_port(port) +{ + try + { + // Create io_context + m_io_context = std::make_unique(); + + // Create socket + m_socket = std::make_unique(*m_io_context); + m_socket->open(boost::asio::ip::udp::v4()); + + // Resolve the endpoint + boost::asio::ip::udp::resolver resolver(*m_io_context); + m_endpoint = *resolver.resolve(boost::asio::ip::udp::v4(), m_host, std::to_string(m_port)).begin(); + } + catch (const boost::system::system_error& e) + { + Logger::error("UDPWriter: Failed to initialize connection: {}", e.what()); + Close(); + } +} + +UDPWriter::~UDPWriter() { Close(); } + +void UDPWriter::Write(const std::string& message) try +{ + if (m_socket == nullptr || m_socket->is_open() == false) + { + Logger::error("UDPWriter: Socket not initialized or closed"); + return; + } + + boost::system::error_code ec; + size_t sent = m_socket->send_to(boost::asio::buffer(message.data(), message.size()), m_endpoint, 0, ec); + + if (ec) + { + Logger::error("UDPWriter: Failed to send message: {}", ec.message()); + } + else if (sent != message.size()) + { + Logger::warn("UDPWriter: Sent only {} bytes out of {} bytes", sent, message.size()); + } +} +catch (const std::exception& e) +{ + Logger::error("UDPWriter: Exception during write: {}", e.what()); +} + +void UDPWriter::Close() try +{ + if (m_socket && m_socket->is_open()) + { + boost::system::error_code ec; + m_socket->close(ec); + if (ec) + { + Logger::warn("UDPWriter: Error when closing socket: {}", ec.message()); + } + } + + // Reset the unique_ptr to deallocate resources + m_socket.reset(); + m_io_context.reset(); +} +catch (const std::exception& e) +{ + Logger::error("UDPWriter: Exception during close: {}", e.what()); +} \ No newline at end of file diff --git a/libs/writer/writer_types/src/uds_writer.cpp b/libs/writer/writer_types/src/uds_writer.cpp new file mode 100644 index 0000000..4dea66c --- /dev/null +++ b/libs/writer/writer_types/src/uds_writer.cpp @@ -0,0 +1,112 @@ +#include + +#include + +#include + +namespace local = boost::asio::local; + +UDSWriter::UDSWriter(const std::string& socketPath) + : m_socketPath(socketPath), + m_ioContext(std::make_unique()), + m_socket(nullptr), + m_isOpen(false) +{ + connect(); +} + +UDSWriter::~UDSWriter() { Close(); } + +bool UDSWriter::connect() +{ + if (m_isOpen) + { + return true; // Already connected + } + + try + { + // Create a new socket if needed + if (!m_socket) + { + m_socket = std::make_unique(*m_ioContext); + } + + // Open the socket and prepare the endpoint + boost::system::error_code ec; + m_socket->open(local::datagram_protocol(), ec); + + // Set up the server endpoint + m_endpoint = local::datagram_protocol::endpoint(m_socketPath); + + if (ec) + { + Logger::error("UDS Writer: Failed to connect to {} - {}", m_socketPath, ec.message()); + return false; + } + + m_isOpen = true; + Logger::debug("UDS Writer: Connected to {}", m_socketPath); + return true; + } + catch (const std::exception& e) + { + Logger::error("UDS Writer: Exception while connecting - {}", e.what()); + m_isOpen = false; + return false; + } +} + +void UDSWriter::Write(const std::string& message) +{ + if (!m_isOpen && !connect()) + { + Logger::error("UDS Writer: Cannot write - not connected to {}", m_socketPath); + return; + } + + try + { + boost::system::error_code ec; + size_t sent = m_socket->send_to(boost::asio::buffer(message), m_endpoint, 0, ec); + + if (ec) + { + Logger::error("UDS Writer: Failed to send message - {}", ec.message()); + m_isOpen = false; // Mark as disconnected on error + } + else + { + Logger::debug("UDS Writer: Sent message ({} bytes)", sent); + } + } + catch (const std::exception& e) + { + Logger::error("UDS Writer: Exception while sending message - {}", e.what()); + m_isOpen = false; // Mark as disconnected on exception + } +} + +void UDSWriter::Close() +{ + if (m_socket && m_isOpen) + { + try + { + boost::system::error_code ec; + m_socket->close(ec); + + if (ec) + { + Logger::warn("UDS Writer: Error closing socket - {}", ec.message()); + } + } + catch (const std::exception& e) + { + Logger::warn("UDS Writer: Exception while closing socket - {}", e.what()); + } + } + + m_isOpen = false; + Logger::debug("UDS Writer: Connection closed"); +} diff --git a/libs/writer/writer_types/test/test_memory_writer.cpp b/libs/writer/writer_types/test/test_memory_writer.cpp new file mode 100644 index 0000000..d35b76b --- /dev/null +++ b/libs/writer/writer_types/test/test_memory_writer.cpp @@ -0,0 +1,50 @@ +#include +#include + +TEST(MemoryWriterTest, IsEmpty) +{ + const auto writer = MemoryWriter(); + EXPECT_TRUE(writer.IsEmpty()); +} + +TEST(MemoryWriterTest, Write) +{ + auto writer = MemoryWriter(); + writer.Write("Test message"); + EXPECT_FALSE(writer.IsEmpty()); + EXPECT_EQ(writer.LastLine(), "Test message"); +} + +TEST(MemoryWriterTest, Clear) +{ + auto writer = MemoryWriter(); + writer.Write("Test message"); + EXPECT_FALSE(writer.IsEmpty()); + + writer.Clear(); + EXPECT_TRUE(writer.IsEmpty()); +} + +TEST(MemoryWriterTest, GetMessages) +{ + auto writer = MemoryWriter(); + writer.Write("First message"); + writer.Write("Second message"); + + const auto& messages = writer.GetMessages(); + EXPECT_EQ(messages.size(), 2); + EXPECT_EQ(messages[0], "First message"); + EXPECT_EQ(messages[1], "Second message"); +} + +TEST(MemoryWriterTest, LastLine) +{ + auto writer = MemoryWriter(); + writer.Write("First message"); + writer.Write("Second message"); + + EXPECT_EQ(writer.LastLine(), "Second message"); + + writer.Clear(); + EXPECT_EQ(writer.LastLine(), ""); +} \ No newline at end of file diff --git a/libs/writer/writer_types/test/test_udp_writer.cpp b/libs/writer/writer_types/test/test_udp_writer.cpp new file mode 100644 index 0000000..5261dd6 --- /dev/null +++ b/libs/writer/writer_types/test/test_udp_writer.cpp @@ -0,0 +1,136 @@ +#include + +#include + +#include "../test_utils/udp_server/udp_server.h" // Include our new header for UDP server interaction +#include +#include +#include // For std::find + +// Test fixture for UDP Writer tests +class UDPWriterTest : public testing::Test +{ + protected: + void SetUp() override + { + // Set the server to run + server_running = true; + + // Clear any existing messages from previous tests + clear_messages(); + + // Start the UDP server in a separate thread + server_thread = std::thread( + [] + { + // This calls our server function directly + listen_for_udp_messages(); + }); + + // Give the server time to start + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + + void TearDown() override + { + // Signal the server to stop + server_running = false; + + // Terminate the server thread + if (server_thread.joinable()) + { + server_thread.join(); + } + } + + std::thread server_thread; +}; + +TEST_F(UDPWriterTest, SendMessage) +{ + // Create a UDP writer that will connect to localhost:1234 + UDPWriter writer("127.0.0.1", 12345); + + // Define our test message + const std::string test_message = "Hello from UDP Writer Test"; + + // Send a test message + writer.Write(test_message); + + // Give time for the message to be processed + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Get current messages and verify the message was received + const auto messages = get_messages(); + ASSERT_FALSE(messages.empty()); + + // Check that our message is in the vector + bool message_found = false; + for (const auto& msg : messages) + { + if (msg == test_message) + { + message_found = true; + break; + } + } + ASSERT_TRUE(message_found); +} + +TEST_F(UDPWriterTest, CloseAndReopen) +{ + UDPWriter writer("127.0.0.1", 12345); + std::string message1 = "Initial message"; + writer.Write(message1); + + // Wait for message processing + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Verify first message + auto messages = get_messages(); + ASSERT_FALSE(messages.empty()); + ASSERT_EQ(messages.back(), message1); + + // Close the writer + writer.Close(); + + // Create a new writer + UDPWriter writer2("127.0.0.1", 12345); + std::string message2 = "Message after reopening"; + writer2.Write(message2); + + // Wait for message processing + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Verify second message + messages = get_messages(); + ASSERT_GE(messages.size(), 2); + ASSERT_EQ(messages.back(), message2); +} + +TEST_F(UDPWriterTest, SendMultipleMessages) +{ + UDPWriter writer("127.0.0.1", 12345); + + // Define test messages + const std::vector test_messages = {"Message 1", "Message 2", "Message 3"}; + + // Send several messages in succession + for (const auto& msg : test_messages) + { + writer.Write(msg); + } + + // Give time for messages to be processed + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Get received messages and verify + const auto received_messages = get_messages(); + + // Verify we received at least the number of messages we sent + ASSERT_EQ(received_messages.size(), test_messages.size()); + + ASSERT_EQ(test_messages.at(0), received_messages.at(0)); + ASSERT_EQ(test_messages.at(1), received_messages.at(1)); + ASSERT_EQ(test_messages.at(2), received_messages.at(2)); +} \ No newline at end of file diff --git a/libs/writer/writer_types/test/test_uds_writer.cpp b/libs/writer/writer_types/test/test_uds_writer.cpp new file mode 100644 index 0000000..0274edf --- /dev/null +++ b/libs/writer/writer_types/test/test_uds_writer.cpp @@ -0,0 +1,139 @@ + +#include + +#include +#include "../test_utils/uds_server/uds_server.h" +#include +#include +#include + +// Test fixture for UDS Writer tests +class UDSWriterTest : public testing::Test +{ + protected: + void SetUp() override + { + // Set the server to run + uds_server_running = true; + + // Clear any existing messages from previous tests + clear_uds_messages(); + + // Start the UDS server in a separate thread + server_thread = std::thread( + [] + { + // This calls our server function directly + listen_for_uds_messages(); + }); + + // Give the server time to start + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + + void TearDown() override + { + // Signal the server to stop + uds_server_running = false; + + // Terminate the server thread + if (server_thread.joinable()) + { + server_thread.join(); + } + } + + std::thread server_thread; +}; + +TEST_F(UDSWriterTest, SendMessage) +{ + // Create a UDS writer that will connect to the test socket + UDSWriter writer("/tmp/test_uds_socket"); + + // Define our test message + const std::string test_message = "Hello from UDS Writer Test"; + + // Send a test message + writer.Write(test_message); + + // Give time for the message to be processed + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Get current messages and verify the message was received + const auto messages = get_uds_messages(); + ASSERT_FALSE(messages.empty()); + + // Check that our message is in the vector + bool message_found = false; + for (const auto& msg : messages) + { + if (msg == test_message) + { + message_found = true; + break; + } + } + + ASSERT_TRUE(message_found); +} + +TEST_F(UDSWriterTest, CloseAndReopen) +{ + UDSWriter writer("/tmp/test_uds_socket"); + const std::string message1 = "Initial message"; + writer.Write(message1); + + // Wait for message processing + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Verify first message + auto messages = get_uds_messages(); + ASSERT_FALSE(messages.empty()); + ASSERT_EQ(messages.back(), message1); + + // Close the writer + writer.Close(); + + // Create a new writer + UDSWriter writer2("/tmp/test_uds_socket"); + const std::string message2 = "Message after reopening"; + writer2.Write(message2); + + // Wait for message processing + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + // Verify second message + messages = get_uds_messages(); + ASSERT_GE(messages.size(), 2); + ASSERT_EQ(messages.back(), message2); +} + +TEST_F(UDSWriterTest, SendMultipleMessages) +{ + UDSWriter writer("/tmp/test_uds_socket"); + + // Define test messages + const std::vector test_messages = {"Message 1", "Message 2", "Message 3"}; + + // Send messages one by one, with a separate connection for each + for (const auto& msg : test_messages) + { + writer.Write(msg); + // Wait for the message to be processed + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + // Get received messages and verify + const auto received_messages = get_uds_messages(); + + // Verify we received the number of messages we sent + ASSERT_EQ(received_messages.size(), test_messages.size()); + + // Verify each message was received correctly + for (size_t i = 0; i < test_messages.size(); i++) + { + ASSERT_EQ(test_messages.at(i), received_messages.at(i)); + } +} \ No newline at end of file diff --git a/libs/writer/writer_types/test_utils/CMakeLists.txt b/libs/writer/writer_types/test_utils/CMakeLists.txt new file mode 100644 index 0000000..1c5c70d --- /dev/null +++ b/libs/writer/writer_types/test_utils/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(udp_server) +add_subdirectory(uds_server) \ No newline at end of file diff --git a/libs/writer/writer_types/test_utils/udp_server/CMakeLists.txt b/libs/writer/writer_types/test_utils/udp_server/CMakeLists.txt new file mode 100644 index 0000000..9f8c88c --- /dev/null +++ b/libs/writer/writer_types/test_utils/udp_server/CMakeLists.txt @@ -0,0 +1,31 @@ + # Create a shared UDP server library for tests +add_library(udp_server_lib OBJECT + udp_server.cpp +) +target_include_directories(udp_server_lib + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) +target_compile_definitions(udp_server_lib + PUBLIC + UDP_SERVER_LIB_ONLY +) +target_link_libraries(udp_server_lib + PUBLIC + Boost::system + pthread +) + +# UDP Server executable +add_executable(udp_server + udp_server.cpp +) +target_include_directories(udp_server + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/../../../include + ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(udp_server + PUBLIC + Boost::system +) \ No newline at end of file diff --git a/libs/writer/writer_types/test_utils/udp_server/udp_server.cpp b/libs/writer/writer_types/test_utils/udp_server/udp_server.cpp new file mode 100644 index 0000000..549bf80 --- /dev/null +++ b/libs/writer/writer_types/test_utils/udp_server/udp_server.cpp @@ -0,0 +1,125 @@ +#include "udp_server.h" +#include +#include +#include +#include +#include +#include +#include +#include + +//---------- Message Storage Implementation ---------- + +// Vector to store all received messages +std::vector messages = {}; +std::mutex messages_mutex; + +// Flag to control server shutdown +std::atomic server_running(true); + +// Expose functions to interact with the messages vector +std::vector get_messages() +{ + std::lock_guard lock(messages_mutex); + return messages; +} + +void clear_messages() +{ + std::lock_guard lock(messages_mutex); + messages.clear(); +} + +void add_message(const std::string& message) +{ + std::lock_guard lock(messages_mutex); + messages.push_back(message); +} + +//---------- UDP Server Implementation ---------- + +void listen_for_udp_messages() +try +{ + constexpr auto port = 12345; + + boost::asio::io_context io_context; + + // Create an IPv4 socket bound to localhost (127.0.0.1) and port 1234 + const boost::asio::ip::udp::endpoint endpoint(boost::asio::ip::address::from_string("127.0.0.1"), port); + boost::asio::ip::udp::socket socket(io_context, boost::asio::ip::udp::v4()); + + try + { + socket.bind(endpoint); + std::cout << "Socket bound to IPv4 localhost (127.0.0.1):" << port << std::endl; + } + catch (const std::exception& e) + { + std::cerr << "Error binding socket: " << e.what() << "\n"; + throw; + } + + std::array buffer{}; + boost::asio::ip::udp::endpoint sender_endpoint; + + std::cout << "UDP server listening on 127.0.0.1:" << port << " (IPv4 localhost)\n"; + + // Configure socket with a small timeout so we can check the run flag + socket.non_blocking(true); + + while (server_running) + { + try + { + const std::size_t bytes_received = socket.receive_from(boost::asio::buffer(buffer), sender_endpoint); + + if (bytes_received == buffer.size()) + { + std::cout << "Warning: Received datagram might have been truncated (buffer full)" << std::endl; + } + + // Create string from received data and store it in the messages vector + std::string message(buffer.data(), bytes_received); + + // Add the message to our global message storage + add_message(message); + + // Get the current count of messages + auto current_messages = get_messages(); + + std::cout << "Received from " << sender_endpoint << ": " << message << std::endl; + std::cout << "Total messages stored: " << current_messages.size() << std::endl; + } + catch (const boost::system::system_error& e) + { + if (e.code() == boost::asio::error::would_block) + { + // No data available, just wait a bit and try again + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + else + { + std::cerr << "Error receiving data: " << e.what() << std::endl; + break; + } + } + } + std::cout << "UDP server shutting down..." << std::endl; +} +catch (const std::exception& e) +{ + std::cerr << "Exception in UDP server: " << e.what() << "\n"; + return; +} + +//---------- Main Function ---------- + +// Only compile the main function when building the executable, not the library +#ifndef UDP_SERVER_LIB_ONLY +int main() +{ + listen_for_udp_messages(); + return 0; +} +#endif diff --git a/libs/writer/writer_types/test_utils/udp_server/udp_server.h b/libs/writer/writer_types/test_utils/udp_server/udp_server.h new file mode 100644 index 0000000..76ffac8 --- /dev/null +++ b/libs/writer/writer_types/test_utils/udp_server/udp_server.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include +#include + +// Functions to interact with the UDP server's message storage +std::vector get_messages(); +void clear_messages(); +void add_message(const std::string& message); + +// Function to run the server in a thread - can be used by both the server executable and tests +void listen_for_udp_messages(); + +// Flag to control server shutdown - used to gracefully stop the server +extern std::atomic server_running; diff --git a/libs/writer/writer_types/test_utils/uds_server/CMakeLists.txt b/libs/writer/writer_types/test_utils/uds_server/CMakeLists.txt new file mode 100644 index 0000000..2a0a618 --- /dev/null +++ b/libs/writer/writer_types/test_utils/uds_server/CMakeLists.txt @@ -0,0 +1,31 @@ +# Create a shared UDS server library for tests +add_library(uds_server_lib OBJECT + uds_server.cpp +) +target_include_directories(uds_server_lib + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) +target_compile_definitions(uds_server_lib + PUBLIC + UDS_SERVER_LIB_ONLY +) +target_link_libraries(uds_server_lib + PUBLIC + Boost::system + pthread +) + +# UDS Server executable +add_executable(uds_server + uds_server.cpp +) +target_include_directories(uds_server + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/../../../include + ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(uds_server + PUBLIC + Boost::system +) diff --git a/libs/writer/writer_types/test_utils/uds_server/uds_server.cpp b/libs/writer/writer_types/test_utils/uds_server/uds_server.cpp new file mode 100644 index 0000000..96bd9fc --- /dev/null +++ b/libs/writer/writer_types/test_utils/uds_server/uds_server.cpp @@ -0,0 +1,147 @@ +#include "uds_server.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//---------- Message Storage Implementation ---------- + +// Vector to store all received messages +std::vector uds_messages = {}; +std::mutex uds_messages_mutex; + +// Flag to control server shutdown +std::atomic uds_server_running(true); + +// Expose functions to interact with the messages vector +std::vector get_uds_messages() +{ + std::lock_guard lock(uds_messages_mutex); + return uds_messages; +} + +void clear_uds_messages() +{ + std::lock_guard lock(uds_messages_mutex); + uds_messages.clear(); +} + +void add_uds_message(const std::string& message) +{ + std::lock_guard lock(uds_messages_mutex); + uds_messages.push_back(message); +} + +//---------- UDS Server Implementation ---------- + +void listen_for_uds_messages() +try +{ + // Path to the Unix domain socket + const std::string socket_path = "/tmp/test_uds_socket"; + + // Remove any existing socket file + std::filesystem::remove(socket_path); + + boost::asio::io_context io_context; + + // Create and open a Unix domain datagram socket + const boost::asio::local::datagram_protocol::endpoint endpoint(socket_path); + boost::asio::local::datagram_protocol::socket socket(io_context); + + boost::system::error_code ec; + socket.open(boost::asio::local::datagram_protocol(), ec); + if (ec) + { + std::cerr << "Error opening socket: " << ec.message() << std::endl; + return; + } + + socket.bind(endpoint, ec); + if (ec) + { + std::cerr << "Error binding to endpoint: " << ec.message() << std::endl; + return; + } + + // Set to non-blocking mode + socket.non_blocking(true); + + std::cout << "UDS datagram server listening on " << socket_path << std::endl; + + // Buffer for reading data + std::array buffer{}; + while (uds_server_running) + { + try + { + // Client endpoint to receive the sender's address + boost::asio::local::datagram_protocol::endpoint sender_endpoint; + boost::system::error_code read_ec; + std::size_t bytes_read = 0; + + // Try to receive a datagram + bytes_read = socket.receive_from(boost::asio::buffer(buffer), sender_endpoint, 0, read_ec); + + // Handle the case where no data is available + if (read_ec == boost::asio::error::would_block) { + // No data available, wait a bit and try again + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + continue; + } + + if (read_ec) + { + std::cerr << "Error reading from socket: " << read_ec.message() << std::endl; + continue; // Continue to next iteration instead of breaking, as each datagram is independent + } + + if (bytes_read == buffer.size()) + { + std::cout << "Warning: Received data might have been truncated (buffer full)" << std::endl; + } + + // Create string from received data and store it + std::string message(buffer.data(), bytes_read); + add_uds_message(message); + auto current_messages = get_uds_messages(); + std::cout << "Received datagram: " << message << std::endl; + std::cout << "Total messages stored: " << current_messages.size() << std::endl; + } + catch (const std::exception& e) + { + std::cerr << "Exception in datagram server: " << e.what() << std::endl; + } + } + + std::cout << "UDS datagram server shutting down..." << std::endl; + + // Close the socket + socket.close(); + + // Clean up the socket file on exit + std::filesystem::remove(socket_path); +} +catch (const std::exception& e) +{ + std::cerr << "Exception in UDS server: " << e.what() << std::endl; + return; +} + +//---------- Main Function ---------- + +// Only compile the main function when building the executable, not the library +#ifndef UDS_SERVER_LIB_ONLY +int main() +{ + listen_for_uds_messages(); + return 0; +} +#endif \ No newline at end of file diff --git a/libs/writer/writer_types/test_utils/uds_server/uds_server.h b/libs/writer/writer_types/test_utils/uds_server/uds_server.h new file mode 100644 index 0000000..cb37eab --- /dev/null +++ b/libs/writer/writer_types/test_utils/uds_server/uds_server.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include +#include + +// Functions to interact with the UDS server's message storage +std::vector get_uds_messages(); +void clear_uds_messages(); +void add_uds_message(const std::string& message); + +// Function to run the server in a thread - can be used by both the server executable and tests +void listen_for_uds_messages(); + +// Flag to control server shutdown - used to gracefully stop the server +extern std::atomic uds_server_running; \ No newline at end of file diff --git a/libs/writer/writer_wrapper/CMakeLists.txt b/libs/writer/writer_wrapper/CMakeLists.txt new file mode 100644 index 0000000..54fa682 --- /dev/null +++ b/libs/writer/writer_wrapper/CMakeLists.txt @@ -0,0 +1,22 @@ +add_library(spectator-writer-wrapper + writer.cpp +) + +target_link_libraries(spectator-writer-wrapper + PUBLIC + spectator-logger + spectator-writer-types +) + +add_executable(writer_test + test_writer.cpp +) + +target_link_libraries(writer_test + PRIVATE + GTest::GTest + GTest::Main + spectator-writer-wrapper + uds_server_lib +) +add_test(NAME writer_test COMMAND writer_test) \ No newline at end of file diff --git a/libs/writer/writer_wrapper/test_writer.cpp b/libs/writer/writer_wrapper/test_writer.cpp new file mode 100644 index 0000000..c98a418 --- /dev/null +++ b/libs/writer/writer_wrapper/test_writer.cpp @@ -0,0 +1,126 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "../writer_types/test_utils/uds_server/uds_server.h" + +// Test fixture for UDS Writer tests +class WriterWrapperUDSWriterTest : public testing::Test +{ + protected: + void SetUp() override + { + // Set the server to run + uds_server_running = true; + + // Clear any existing messages from previous tests + clear_uds_messages(); + + // Start the UDS server in a separate thread + server_thread = std::thread( + [] + { + // This calls our server function directly + listen_for_uds_messages(); + }); + + // Give the server time to start + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + + void TearDown() override + { + // Signal the server to stop + uds_server_running = false; + + // Terminate the server thread + if (server_thread.joinable()) + { + server_thread.join(); + } + } + + std::thread server_thread; +}; + +TEST_F(WriterWrapperUDSWriterTest, MultithreadedWrite) +{ + Logger::info("Starting multithreaded write test..."); + + // Create a UDS writer with a small buffer size + const std::string unixUrl = "/tmp/test_uds_socket"; + WriterTestHelper::InitializeWriter(WriterType::Unix, unixUrl, 0, 30); + + // Number of threads and counters to create + constexpr auto numThreads = 4; + constexpr auto countersPerThread = 3; + constexpr auto incrementsPerCounter = 5; + + // Function for worker threads + auto worker = [&](int threadId) + { + // Create several counters per thread with unique names + for (int i = 0; i < countersPerThread; i++) + { + std::string counterName = std::format("counter.thread{}.{}", threadId, i); + MeterId meterId(counterName); + Counter counter(meterId); + + // Increment each counter multiple times + for (int j = 0; j < incrementsPerCounter; j++) + { + counter.Increment(); + } + } + }; + + // Start worker threads + std::vector threads; + for (int i = 0; i < numThreads; i++) + { + threads.emplace_back(worker, i); + } + + // Wait for all threads to complete + for (auto& t : threads) + { + t.join(); + } + + // Give some time for messages to be sent + std::this_thread::sleep_for(std::chrono::milliseconds(900)); + + // Check messages + auto msgs = get_uds_messages(); + EXPECT_FALSE(msgs.empty()); + + // Verify total number of increments + int expectedIncrements = numThreads * countersPerThread * incrementsPerCounter; + int actualIncrements = 0; + + // Verify every string in msgs follows the form counter.thread. + std::regex counter_regex(R"(c:counter\.thread\d+\.\d+:1.000000)"); + for (const auto& msg : msgs) + { + std::stringstream ss(msg); + std::string line; + while (std::getline(ss, line)) + { + if (!line.empty()) + { + EXPECT_TRUE(std::regex_match(line, counter_regex)) << "Unexpected counter format: " << line; + actualIncrements++; + } + } + } + + EXPECT_EQ(actualIncrements, expectedIncrements); +} \ No newline at end of file diff --git a/libs/writer/writer_wrapper/writer.cpp b/libs/writer/writer_wrapper/writer.cpp new file mode 100644 index 0000000..020b38c --- /dev/null +++ b/libs/writer/writer_wrapper/writer.cpp @@ -0,0 +1,213 @@ +#include + +#include +#include +#include + +static constexpr auto NEW_LINE = '\n'; + +Writer::~Writer() +{ + auto& instance = GetInstance(); + + if (instance.bufferingEnabled) + { + instance.shutdown.store(true); + instance.cv_receiver.notify_all(); + instance.cv_sender.notify_all(); + if (instance.sendingThread.joinable()) { + instance.sendingThread.join(); + } + } +} + +void Writer::Initialize(WriterType type, const std::string& param, int port, unsigned int bufferSize) +{ + // Get the singleton instance directly + auto& instance = GetInstance(); + + // Create the new writer based on type + try + { + switch (type) + { + case WriterType::Memory: + instance.m_impl = std::make_unique(); + Logger::info("Writer initialized as MemoryWriter"); + break; + case WriterType::UDP: + instance.m_impl = std::make_unique(param, port); + Logger::info("Writer initialized as UDPWriter with host: {} and port: {}", param, port); + break; + case WriterType::Unix: + instance.m_impl = std::make_unique(param); + Logger::info("Writer initialized as UnixWriter with socket path: {}", param); + break; + default: + throw std::runtime_error("Unsupported writer type"); + } + + instance.m_currentType = type; + + if (bufferSize > 0) + { + instance.bufferingEnabled = true; + instance.bufferSize = bufferSize; + instance.buffer.reserve(bufferSize); + instance.writeImpl = &Writer::BufferedWrite; + // Create a thread with proper binding to the instance method + instance.sendingThread = std::thread(&Writer::ThreadSend, &instance); + } + else + { + // Explicitly set to non-buffered if buffer size is 0 + instance.writeImpl = &Writer::NonBufferedWrite; + } + } + catch (const std::exception& e) + { + Logger::error("Failed to initialize writer: {}", e.what()); + throw; + } +} + +void Writer::Reset() +{ + auto& instance = GetInstance(); + + if (instance.m_impl) + { + try + { + instance.m_impl->Close(); + } + catch (const std::exception& e) + { + Logger::warn("Exception while closing writer during reset: {}", e.what()); + } + } + + instance.m_impl.reset(); + Logger::info("Writer has been reset"); +} + + +void Writer::TryToSend(const std::string& message) +{ + const auto& instance = GetInstance(); + + if (!instance.m_impl) + { + Logger::error("Attempted to send with uninitialized writer implementation"); + return; + } + + try + { + instance.m_impl->Write(message); + Logger::debug("Message sent successfully: {}", message); + } + catch (const std::exception& e) + { + Logger::error("Failed to send message: {}", e.what()); + } +} + +void Writer::ThreadSend() +{ + auto& instance = GetInstance(); + std::string message{}; + while (instance.shutdown.load() == false) + { + { + std::unique_lock lock(instance.writeMutex); + instance.cv_sender.wait( + lock, [&instance] { return instance.buffer.size() > instance.bufferSize || instance.shutdown.load(); }); + if (instance.shutdown.load() == true) + { + return; + } + message = std::move(instance.buffer); + instance.buffer = std::string(); + instance.buffer.reserve(instance.bufferSize); + } + instance.cv_receiver.notify_one(); + instance.TryToSend(message); + } +} + +void Writer::BufferedWrite(const std::string& message) +{ + auto& instance = GetInstance(); + + if (!instance.m_impl) + { + Logger::error("Attempted to write with uninitialized writer implementation"); + return; + } + + { + std::unique_lock lock(instance.writeMutex); + // TODO: Optimize memory alloc to not exceed allocated size + instance.cv_receiver.wait( + lock, [&instance] { return instance.buffer.size() < instance.bufferSize || instance.shutdown.load(); }); + if (instance.shutdown.load()) + { + Logger::warn("Write operation aborted due to shutdown signal"); + return; + } + instance.buffer.append(message); + instance.buffer.push_back(NEW_LINE); + } + instance.buffer.size() > instance.bufferSize ? instance.cv_sender.notify_one() : instance.cv_receiver.notify_one(); +} + +void Writer::NonBufferedWrite(const std::string& message) +{ + // Since this is a non-static method, we're already operating on an instance + // and can call the instance method directly + this->TryToSend(message + NEW_LINE); +} + +void Writer::Write(const std::string& message) +{ + auto& instance = GetInstance(); + + if (!instance.m_impl) + { + Logger::error("Attempted to write with uninitialized writer implementation"); + return; + } + + try + { + // Call the member function using the pointer-to-member syntax + (instance.*instance.writeImpl)(message); + Logger::debug("Message written successfully: {}", message); + } + catch (const std::exception& e) + { + Logger::error("Failed to write message: {}", e.what()); + } +} + +void Writer::Close() +{ + const auto& instance = GetInstance(); + + if (!instance.m_impl) + { + Logger::debug("Close called on uninitialized writer"); + return; + } + + try + { + instance.m_impl->Close(); + Logger::debug("Writer closed successfully"); + } + catch (const std::exception& e) + { + Logger::error("Failed to close writer: {}", e.what()); + } +} \ No newline at end of file diff --git a/libs/writer/writer_wrapper/writer.h b/libs/writer/writer_wrapper/writer.h new file mode 100644 index 0000000..b6b9eec --- /dev/null +++ b/libs/writer/writer_wrapper/writer.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include + +#include +#include + +class Writer final : public Singleton +{ + public: + ~Writer() override; + + private: + friend class Singleton; + friend class Registry; + friend class WriterTestHelper; + friend class AgeGauge; + friend class Counter; + friend class DistributionSummary; + friend class Gauge; + friend class MaxGauge; + friend class MonotonicCounter; + friend class MonotonicCounterUint; + friend class PercentileDistributionSummary; + friend class PercentileTimer; + friend class Timer; + + // Private constructor - enforces singleton pattern + Writer() = default; + + static void Initialize(WriterType type, const std::string& param = "", int port = 0, unsigned int bufferSize = 0); + + static void Write(const std::string& message); + + void BufferedWrite(const std::string& message); + + void NonBufferedWrite(const std::string& message); + + void ThreadSend(); + + void TryToSend(const std::string& message); + + void Close(); + + // Get the Writer's implementation for testing purposes + static BaseWriter* GetImpl() { return Writer::GetInstance().m_impl.get(); } + static WriterType GetWriterType() { return GetInstance().m_currentType; } + + + static void Reset(); + + std::unique_ptr m_impl; + WriterType m_currentType = WriterType::Memory; // Default type + bool bufferingEnabled = false; + unsigned int bufferSize = 0; + std::string buffer{}; + + // Function pointer for write strategy - member function pointer + using WriteFunction = void (Writer::*)(const std::string&); + WriteFunction writeImpl = &Writer::NonBufferedWrite; // Default to non-buffered + + + // multithreading writes + std::mutex writeMutex; + std::thread sendingThread; + std::condition_variable cv_receiver; + std::condition_variable cv_sender; + std::atomic shutdown{false}; +}; \ No newline at end of file diff --git a/libs/writer/writer_wrapper/writer_test_helper.h b/libs/writer/writer_wrapper/writer_test_helper.h new file mode 100644 index 0000000..0b1b08c --- /dev/null +++ b/libs/writer/writer_wrapper/writer_test_helper.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +/** + * WriterTestHelper - A utility class to help with testing Writer functionality + * + * This class is a friend of Writer and can provide access to Writer's private + * methods for testing purposes. + */ +class WriterTestHelper +{ + public: + // Initialize the Writer for testing purposes + static void InitializeWriter(WriterType type, const std::string& param = "", int port = 0, unsigned int bufferSize = 0) + { + Writer::Initialize(type, param, port, bufferSize); + } + + // Reset the Writer for testing purposes + static void ResetWriter() { Writer::Reset(); } + + // Get the Writer's implementation for testing purposes + static BaseWriter* GetImpl() { return Writer::GetInstance().m_impl.get(); } +}; diff --git a/performance_tests/CMakeLists.txt b/performance_tests/CMakeLists.txt new file mode 100644 index 0000000..f32acae --- /dev/null +++ b/performance_tests/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(performance_test performance_test.cpp) +target_link_libraries(performance_test PRIVATE registry) \ No newline at end of file diff --git a/performance_tests/performance_test.cpp b/performance_tests/performance_test.cpp new file mode 100644 index 0000000..40351aa --- /dev/null +++ b/performance_tests/performance_test.cpp @@ -0,0 +1,166 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct RunTimeConfig +{ + bool bufferingEnabled; + std::string writerType; + std::string writerTypeName; + std::string counterName; + std::string locationTag; +}; + +void PrintUsage() +{ + std::cerr << "Usage: performance_test [writer_type] [buffering]" << std::endl; + std::cerr << " writer_type: udp or uds" << std::endl; + std::cerr << " buffering: 0 for disabled, 1 for enabled (default is 0)" << std::endl; +} + +std::optional HandleArgs(int argc, char* argv[]) +{ + if (argc != 3 && argc != 2) + { + return std::nullopt; + } + + bool bufferingEnabled = false; + if (argc == 3) + { + std::string bufferingArg = argv[2]; + if (bufferingArg != "0" && bufferingArg != "1") + { + std::cerr << "Invalid buffering argument: " << bufferingArg << std::endl; + return std::nullopt; + } + bufferingEnabled = (bufferingArg == "1"); + } + + std::string writerArg = argv[1]; + if (writerArg != "udp" && writerArg != "uds") + { + std::cerr << "Invalid writer type: " << writerArg << std::endl; + return std::nullopt; + } + + RunTimeConfig config; + if (writerArg == "udp") + { + config.writerType = WriterTypes::UDP; + config.counterName = "udp_test_counter"; + config.locationTag = "udp"; + config.writerTypeName = "UDP"; + } + else + { + config.writerType = WriterTypes::Unix; + config.counterName = "unix_test_counter"; + config.locationTag = "unix"; + config.writerTypeName = "UDS"; + } + config.bufferingEnabled = bufferingEnabled; + + return config; +} + +int main(int argc, char* argv[]) +{ + auto config = HandleArgs(argc, argv); + if (config == std::nullopt) + { + PrintUsage(); + return 1; + } + + std::cout << "Running performance test with the following configuration:" << std::endl; + std::cout << "Writer Type: " << config->writerTypeName << std::endl; + std::cout << "Buffering Enabled: " << (config->bufferingEnabled ? "Yes" : "No") << std::endl; + + // Configure the registry with or without buffering based on the command line argument + auto writerConfig = WriterConfig(config->writerType); + if (config->bufferingEnabled) + { + writerConfig = WriterConfig(config->writerType, 4096); + } + auto r = Registry(Config(writerConfig)); + std::unordered_map tags = { + {"location", config->locationTag}, + {"version", "correct-horse-battery-staple"} + }; + + // Set maximum duration to 2 minutes + constexpr int max_duration_seconds = 2 * 60; + + // Track iterations and timing with atomic counter for thread safety + std::atomic iterations{0}; + auto start_time = std::chrono::steady_clock::now(); + double total_elapsed{}; + + unsigned int num_threads = 4; + num_threads = std::max(1u, std::min(16u, num_threads)); + + std::cout << "Running performance test with " << num_threads << " threads..." << std::endl; + + // Flag to signal threads to stop + std::atomic should_stop{false}; + + // Thread function + auto thread_func = [&r, &config, &tags, &iterations, &should_stop]() + { + while (should_stop == false) + { + r.counter(config->counterName, tags).Increment(); + iterations.fetch_add(1, std::memory_order_relaxed); + } + }; + + // Create and start threads + std::vector threads; + for (unsigned int i = 0; i < num_threads; ++i) + { + threads.emplace_back(thread_func); + } + + // Monitor progress + while (true) + { + std::this_thread::sleep_for(std::chrono::seconds(6)); + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration(now - start_time).count(); + + if (elapsed > max_duration_seconds) + { + total_elapsed = elapsed; + should_stop = true; + break; + } + } + + // Wait for all threads to finish + for (auto& t : threads) + { + if (t.joinable()) + { + t.join(); + } + } + + double rate_per_second = static_cast(iterations) / total_elapsed; + + std::cout << "\nPerformance Test Summary:" << std::endl; + std::cout << "Threads used: " << num_threads << std::endl; + std::cout << "Iterations completed: " << iterations << std::endl; + std::cout << "Total elapsed time: " << std::fixed << std::setprecision(2) << total_elapsed << " seconds" << std::endl; + std::cout << "Rate: " << std::fixed << std::setprecision(2) << rate_per_second << " iterations/second" << std::endl; + std::cout << "Rate per thread: " << std::fixed << std::setprecision(2) << rate_per_second / num_threads << " iterations/second/thread" << std::endl; + return 0; +} diff --git a/requirements.txt b/requirements.txt index 548109e..eba6e90 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -conan==2.16.1 +conan==2.17.1 \ No newline at end of file diff --git a/sanitized b/sanitized deleted file mode 100644 index 316ba9a..0000000 --- a/sanitized +++ /dev/null @@ -1,9 +0,0 @@ -include(default) - -# https://blog.conan.io/2022/04/21/New-conan-release-1-47.html -# -# inject sanitizer flags into conan packages, so that we do not get segfaults when -# we try to run the local build with the address sanitizer for debug builds - -[conf] -tools.build:cxxflags=["-fno-omit-frame-pointer", "-fsanitize=address"] diff --git a/setup-venv.sh b/setup-venv.sh old mode 100755 new mode 100644 index 7bfcded..94532d8 --- a/setup-venv.sh +++ b/setup-venv.sh @@ -18,4 +18,4 @@ if [[ -f requirements.txt ]]; then # use the virtualenv python python -m pip install --upgrade pip wheel python -m pip install --requirement requirements.txt -fi +fi \ No newline at end of file diff --git a/spectator/CMakeLists.txt b/spectator/CMakeLists.txt new file mode 100644 index 0000000..c9ca3e6 --- /dev/null +++ b/spectator/CMakeLists.txt @@ -0,0 +1,24 @@ +add_library(registry + registry.cpp +) +target_include_directories(registry + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include +) +target_link_libraries(registry + PUBLIC + spectator-config + spectator-meter-id + spectator-meter-types + spectator-writer-config + spectator-writer-wrapper + +) +add_executable(registry-test test_registry.cpp) +target_link_libraries(registry-test PRIVATE + GTest::gtest + GTest::gtest_main + registry + spectator-utils +) +add_test(NAME registry-test COMMAND registry-test) \ No newline at end of file diff --git a/spectator/age_gauge_test.cc b/spectator/age_gauge_test.cc deleted file mode 100644 index 844e1f9..0000000 --- a/spectator/age_gauge_test.cc +++ /dev/null @@ -1,36 +0,0 @@ -#include "stateless_meters.h" -#include "test_publisher.h" -#include - -namespace { - -using spectator::Id; -using spectator::AgeGauge; -using spectator::Tags; -using spectator::TestPublisher; - -TEST(AgeGauge, Set) { - TestPublisher publisher; - auto id = std::make_shared("gauge", Tags{}); - auto id2 = std::make_shared("gauge2", Tags{{"key", "val"}}); - AgeGauge g{id, &publisher}; - AgeGauge g2{id2, &publisher}; - - g.Set(1671641328); - g2.Set(1671641028.3); - g.Set(0); - std::vector expected = {"A:gauge:1671641328", - "A:gauge2,key=val:1671641028.3", - "A:gauge:0"}; - EXPECT_EQ(publisher.SentMessages(), expected); -} - -TEST(AgeGauge, InvalidTags) { - TestPublisher publisher; - // test with a single tag, because tags order is not guaranteed in a flat_hash_map - auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", - Tags{{"tag1,:=", "value1,:="}}); - AgeGauge g{id, &publisher}; - EXPECT_EQ("A:test______^____-_~______________.___foo,tag1___=value1___:", g.GetPrefix()); -} -} // namespace diff --git a/spectator/config.h b/spectator/config.h deleted file mode 100644 index f959dbb..0000000 --- a/spectator/config.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include -#include - -namespace spectator { - -struct Config { - std::string endpoint; - std::unordered_map common_tags; - uint32_t bytes_to_buffer; -}; - -} // namespace spectator diff --git a/spectator/counter_test.cc b/spectator/counter_test.cc deleted file mode 100644 index 91267ce..0000000 --- a/spectator/counter_test.cc +++ /dev/null @@ -1,42 +0,0 @@ -#include "stateless_meters.h" -#include "test_publisher.h" -#include - -namespace { - -using spectator::Counter; -using spectator::Id; -using spectator::Tags; -using spectator::TestPublisher; - -TEST(Counter, Activity) { - TestPublisher publisher; - auto id = std::make_shared("ctr.name", Tags{}); - auto id2 = std::make_shared("c2", Tags{{"key", "val"}}); - Counter c{id, &publisher}; - Counter c2{id2, &publisher}; - c.Increment(); - c2.Add(1.2); - c.Add(0.1); - std::vector expected = {"c:ctr.name:1", "c:c2,key=val:1.2", - "c:ctr.name:0.1"}; - EXPECT_EQ(publisher.SentMessages(), expected); -} - -TEST(Counter, Id) { - TestPublisher publisher; - Counter c{std::make_shared("foo", Tags{{"key", "val"}}), - &publisher}; - auto id = std::make_shared("foo", Tags{{"key", "val"}}); - EXPECT_EQ(*(c.MeterId()), *id); -} - -TEST(Counter, InvalidTags) { - TestPublisher publisher; - // test with a single tag, because tags order is not guaranteed in a flat_hash_map - auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", - Tags{{"tag1,:=", "value1,:="}}); - Counter c{id, &publisher}; - EXPECT_EQ("c:test______^____-_~______________.___foo,tag1___=value1___:", c.GetPrefix()); -} -} // namespace diff --git a/spectator/dist_summary_test.cc b/spectator/dist_summary_test.cc deleted file mode 100644 index ed8eec5..0000000 --- a/spectator/dist_summary_test.cc +++ /dev/null @@ -1,33 +0,0 @@ -#include "stateless_meters.h" -#include "test_publisher.h" -#include - -namespace { -using spectator::DistributionSummary; -using spectator::Id; -using spectator::Tags; -using spectator::TestPublisher; - -TEST(DistributionSummary, Record) { - TestPublisher publisher; - auto id = std::make_shared("ds.name", Tags{}); - auto id2 = std::make_shared("ds2", Tags{{"key", "val"}}); - DistributionSummary d{id, &publisher}; - DistributionSummary d2{id2, &publisher}; - d.Record(10); - d2.Record(1.2); - d.Record(0.1); - std::vector expected = {"d:ds.name:10", "d:ds2,key=val:1.2", - "d:ds.name:0.1"}; - EXPECT_EQ(publisher.SentMessages(), expected); -} - -TEST(DistributionSummary, InvalidTags) { - TestPublisher publisher; - // test with a single tag, because tags order is not guaranteed in a flat_hash_map - auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", - Tags{{"tag1,:=", "value1,:="}}); - DistributionSummary d{id, &publisher}; - EXPECT_EQ("d:test______^____-_~______________.___foo,tag1___=value1___:", d.GetPrefix()); -} -} // namespace diff --git a/spectator/gauge_test.cc b/spectator/gauge_test.cc deleted file mode 100644 index 75c5fbf..0000000 --- a/spectator/gauge_test.cc +++ /dev/null @@ -1,51 +0,0 @@ -#include "stateless_meters.h" -#include "test_publisher.h" -#include - -namespace { - -using spectator::Gauge; -using spectator::Id; -using spectator::Tags; -using spectator::TestPublisher; - -TEST(Gauge, Set) { - TestPublisher publisher; - auto id = std::make_shared("gauge", Tags{}); - auto id2 = std::make_shared("gauge2", Tags{{"key", "val"}}); - Gauge g{id, &publisher}; - Gauge g2{id2, &publisher}; - - g.Set(42); - g2.Set(2); - g.Set(1); - std::vector expected = {"g:gauge:42", "g:gauge2,key=val:2", - "g:gauge:1"}; - EXPECT_EQ(publisher.SentMessages(), expected); -} - -TEST(Gauge, SetWithTTL) { - TestPublisher publisher; - auto id = std::make_shared("gauge", Tags{}); - auto id2 = std::make_shared("gauge2", Tags{{"key", "val"}}); - Gauge g{id, &publisher, 1}; - Gauge g2{id2, &publisher, 2}; - - g.Set(42); - g2.Set(2); - g.Set(1); - std::vector expected = {"g,1:gauge:42", "g,2:gauge2,key=val:2", - "g,1:gauge:1"}; - EXPECT_EQ(publisher.SentMessages(), expected); -} - - -TEST(Gauge, InvalidTags) { - TestPublisher publisher; - // test with a single tag, because tags order is not guaranteed in a flat_hash_map - auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", - Tags{{"tag1,:=", "value1,:="}}); - Gauge g{id, &publisher}; - EXPECT_EQ("g:test______^____-_~______________.___foo,tag1___=value1___:", g.GetPrefix()); -} -} // namespace diff --git a/spectator/id.h b/spectator/id.h deleted file mode 100644 index e766874..0000000 --- a/spectator/id.h +++ /dev/null @@ -1,223 +0,0 @@ -#pragma once - -#include "absl/container/flat_hash_map.h" -#include "absl/strings/string_view.h" -#include -#include -#include -#include -#include - -namespace spectator { - -class Tags { - using table_t = absl::flat_hash_map; - table_t entries_; - - public: - Tags() = default; - - Tags(std::initializer_list> vs) { - for (auto& pair : vs) { - add(pair.first, pair.second); - } - } - - template - static Tags from(Cont&& cont) { - Tags tags; - tags.entries_.reserve(cont.size()); - for (auto&& kv : cont) { - tags.add(kv.first, kv.second); - } - return tags; - } - - void add(absl::string_view k, absl::string_view v) { - entries_[k] = std::string(v); - } - - [[nodiscard]] size_t hash() const { - using hs = std::hash; - size_t h = 0; - for (const auto& entry : entries_) { - h += (hs()(entry.first) << 1U) ^ hs()(entry.second); - } - return h; - } - - void move_all(Tags&& source) { - entries_.insert(std::make_move_iterator(source.begin()), - std::make_move_iterator(source.end())); - } - - bool operator==(const Tags& that) const { return that.entries_ == entries_; } - - [[nodiscard]] bool has(absl::string_view key) const { - return entries_.find(key) != entries_.end(); - } - - [[nodiscard]] std::string at(absl::string_view key) const { - auto entry = entries_.find(key); - if (entry != entries_.end()) { - return entry->second; - } - return {}; - } - - [[nodiscard]] size_t size() const { return entries_.size(); } - - [[nodiscard]] table_t::const_iterator begin() const { - return entries_.begin(); - } - - [[nodiscard]] table_t::const_iterator end() const { return entries_.end(); } -}; - -class Id { - public: - Id(absl::string_view name, Tags tags) noexcept - : name_(name), tags_(std::move(tags)), hash_(0u) {} - - static std::shared_ptr of(absl::string_view name, Tags tags = {}) { - return std::make_shared(name, std::move(tags)); - } - - bool operator==(const Id& rhs) const noexcept { - return name_ == rhs.name_ && tags_ == rhs.tags_; - } - - const std::string& Name() const noexcept { return name_; } - - const Tags& GetTags() const noexcept { return tags_; } - - std::unique_ptr WithTag(const std::string& key, - const std::string& value) const { - // Create a copy - Tags tags{GetTags()}; - tags.add(key, value); - return std::make_unique(Name(), tags); - } - - std::unique_ptr WithTags(Tags&& extra_tags) const { - Tags tags{GetTags()}; - tags.move_all(std::move(extra_tags)); - return std::make_unique(Name(), tags); - } - - std::unique_ptr WithTags(const Tags& extra_tags) const { - Tags tags{GetTags()}; - for (const auto& t : extra_tags) { - tags.add(t.first, t.second); - } - return std::make_unique(Name(), tags); - } - - std::unique_ptr WithStat(const std::string& stat) const { - return WithTag("statistic", stat); - }; - - static std::shared_ptr WithDefaultStat(std::shared_ptr baseId, - const std::string& stat) { - if (baseId->GetTags().has("statistic")) { - return baseId; - } else { - return baseId->WithStat(stat); - } - } - - friend struct std::hash; - - friend struct std::hash>; - - private: - std::string name_; - Tags tags_; - mutable size_t hash_; - - size_t Hash() const noexcept { - if (hash_ == 0) { - // compute hash code, and reuse it - hash_ = tags_.hash() ^ std::hash()(name_); - } - return hash_; - } -}; - -using IdPtr = std::shared_ptr; - -} // namespace spectator - -namespace std { -template <> -struct hash { - size_t operator()(const spectator::Id& id) const { return id.Hash(); } -}; - -template <> -struct hash { - size_t operator()(const spectator::Tags& tags) const { return tags.hash(); } -}; - -template <> -struct hash> { - size_t operator()(const shared_ptr& id) const { - return id->Hash(); - } -}; - -template <> -struct equal_to> { - bool operator()(const shared_ptr& lhs, - const shared_ptr& rhs) const { - return *lhs == *rhs; - } -}; - -} // namespace std - -template <> struct fmt::formatter: fmt::formatter { - auto format(const spectator::Tags& tags, format_context& ctx) const -> format_context::iterator { - std::string s; - auto size = tags.size(); - - if (size > 0) { - // sort keys, to ensure stable output - std::vector keys; - for (const auto& pair : tags) { - keys.push_back(pair.first); - } - std::sort(keys.begin(), keys.end()); - - s = "["; - for (const auto &key : keys) { - if (size > 1) { - s += key + "=" + tags.at(key) + ", "; - } else { - s += key + "=" + tags.at(key) + "]"; - } - size -= 1; - } - } else { - s = "[]"; - } - - return fmt::formatter::format(s, ctx); - } -}; - -inline auto operator<<(std::ostream& os, const spectator::Tags& tags) -> std::ostream& { - os << fmt::format("{}", tags); - return os; -} - -template <> struct fmt::formatter: fmt::formatter { - static auto format(const spectator::Id& id, format_context& ctx) -> format_context::iterator { - return fmt::format_to(ctx.out(), "Id(name={}, tags={})", id.Name(), id.GetTags()); - } -}; - -inline auto operator<<(std::ostream& os, const spectator::Id& id) -> std::ostream& { - os << fmt::format("{}", id); - return os; -} diff --git a/spectator/id_test.cc b/spectator/id_test.cc deleted file mode 100644 index 6ac8baf..0000000 --- a/spectator/id_test.cc +++ /dev/null @@ -1,42 +0,0 @@ -#include "../spectator/id.h" -#include - -namespace { - -using spectator::Id; -using spectator::Tags; - -TEST(Id, Create) { - Id id{"foo", Tags{}}; - EXPECT_EQ(id.Name(), "foo"); - EXPECT_EQ(id.GetTags().size(), 0); - EXPECT_EQ(fmt::format("{}", id), "Id(name=foo, tags=[])"); - - Id id_tags_single{"name", Tags{{"k", "v"}}}; - EXPECT_EQ(id_tags_single.Name(), "name"); - EXPECT_EQ(id_tags_single.GetTags().size(), 1); - EXPECT_EQ(fmt::format("{}", id_tags_single), "Id(name=name, tags=[k=v])"); - - Id id_tags_multiple{"name", Tags{{"k", "v"}, {"k1", "v1"}}}; - EXPECT_EQ(id_tags_multiple.Name(), "name"); - EXPECT_EQ(id_tags_multiple.GetTags().size(), 2); - - EXPECT_EQ(fmt::format("{}", id_tags_multiple), "Id(name=name, tags=[k=v, k1=v1])"); - - std::shared_ptr id_of{Id::of("name", Tags{{"k", "v"}, {"k1", "v1"}})}; - EXPECT_EQ(id_of->Name(), "name"); - EXPECT_EQ(id_of->GetTags().size(), 2); - EXPECT_EQ(fmt::format("{}", *id_of), "Id(name=name, tags=[k=v, k1=v1])"); -} - -TEST(Id, Tags) { - Id id{"foo", Tags{}}; - auto withTag = id.WithTag("k", "v"); - Tags tags{{"k", "v"}}; - EXPECT_EQ(tags, withTag->GetTags()); - - auto withStat = withTag->WithStat("count"); - Tags tagsWithStat{{"k", "v"}, {"statistic", "count"}}; - EXPECT_EQ(tagsWithStat, withStat->GetTags()); -} -} // namespace diff --git a/spectator/log_entry.h b/spectator/log_entry.h deleted file mode 100644 index 27cebc4..0000000 --- a/spectator/log_entry.h +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once - -#include "registry.h" -#include "strings.h" -#include "percentile_timer.h" - -namespace spectator { -class LogEntry { - public: - LogEntry(Registry* registry, std::string method, const std::string& url) - : registry_{registry}, - start_{absl::Now()}, - id_{registry_->CreateId("ipc.client.call", - Tags{{"owner", "spectator-cpp"}, - {"ipc.endpoint", PathFromUrl(url)}, - {"http.method", std::move(method)}, - {"http.status", "-1"}})} {} - - absl::Time start() const { return start_; } - - void log() { - using millis = std::chrono::milliseconds; - using std::chrono::seconds; - registry_->GetPercentileTimer(id_, millis(1), seconds(5)) - ->Record(absl::Now() - start_); - } - - void set_status_code(int code) { - id_ = id_->WithTag("http.status", fmt::format("{}", code)); - } - - void set_attempt(int attempt_number, bool is_final) { - id_ = id_->WithTag("ipc.attempt", attempt(attempt_number)) - ->WithTag("ipc.attempt.final", is_final ? "true" : "false"); - } - - void set_error(const std::string& error) { - id_ = id_->WithTag("ipc.result", "failure")->WithTag("ipc.status", error); - } - - void set_success() { - const std::string ipc_success = "success"; - id_ = id_->WithTag("ipc.status", ipc_success) - ->WithTag("ipc.result", ipc_success); - } - - private: - Registry* registry_; - absl::Time start_; - IdPtr id_; - - std::string attempt(int attempt_number) { - static std::string initial = "initial"; - static std::string second = "second"; - static std::string third_up = "third_up"; - - switch (attempt_number) { - case 0: - return initial; - case 1: - return second; - default: - return third_up; - } - } -}; - -} // namespace spectator diff --git a/spectator/logger.cc b/spectator/logger.cc deleted file mode 100644 index 361ee8d..0000000 --- a/spectator/logger.cc +++ /dev/null @@ -1,29 +0,0 @@ -#include "logger.h" -#include -#include -#include - -namespace spectator { - -static constexpr const char* const kMainLogger = "spectator"; - -LogManager& log_manager() noexcept { - static auto* the_log_manager = new LogManager(); - return *the_log_manager; -} - -LogManager::LogManager() noexcept { - try { - logger_ = spdlog::create_async_nb( - kMainLogger); - logger_->set_level(spdlog::level::debug); - } catch (const spdlog::spdlog_ex& ex) { - std::cerr << "Log initialization failed: " << ex.what() << "\n"; - } -} - -std::shared_ptr LogManager::Logger() noexcept { - return logger_; -} - -} // namespace spectator diff --git a/spectator/logger.h b/spectator/logger.h deleted file mode 100644 index 1c78eef..0000000 --- a/spectator/logger.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include - -namespace spectator { - -class LogManager { - public: - LogManager() noexcept; - std::shared_ptr Logger() noexcept; - - private: - std::shared_ptr logger_; -}; - -LogManager& log_manager() noexcept; - -inline std::shared_ptr DefaultLogger() noexcept { - return log_manager().Logger(); -} - -} // namespace spectator diff --git a/spectator/max_gauge_test.cc b/spectator/max_gauge_test.cc deleted file mode 100644 index 10c5bac..0000000 --- a/spectator/max_gauge_test.cc +++ /dev/null @@ -1,35 +0,0 @@ -#include "stateless_meters.h" -#include "test_publisher.h" -#include - -namespace { - -using spectator::Id; -using spectator::MaxGauge; -using spectator::Tags; -using spectator::TestPublisher; - -TEST(MaxGauge, Set) { - TestPublisher publisher; - auto id = std::make_shared("gauge", Tags{}); - auto id2 = std::make_shared("gauge2", Tags{{"key", "val"}}); - MaxGauge g{id, &publisher}; - MaxGauge g2{id2, &publisher}; - - g.Set(42); - g2.Update(2); - g.Update(1); - std::vector expected = {"m:gauge:42", "m:gauge2,key=val:2", - "m:gauge:1"}; - EXPECT_EQ(publisher.SentMessages(), expected); -} - -TEST(MaxGauge, InvalidTags) { - TestPublisher publisher; - // test with a single tag, because tags order is not guaranteed in a flat_hash_map - auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", - Tags{{"tag1,:=", "value1,:="}}); - MaxGauge g{id, &publisher}; - EXPECT_EQ("m:test______^____-_~______________.___foo,tag1___=value1___:", g.GetPrefix()); -} -} // namespace diff --git a/spectator/measurement.h b/spectator/measurement.h deleted file mode 100644 index ad88200..0000000 --- a/spectator/measurement.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "id.h" -#include - -namespace spectator { - -struct Measurement { - IdPtr id; - double value; - - bool operator==(const Measurement& other) const { - return std::abs(value - other.value) < 1e-9 && *id == *(other.id); - } - - Measurement(IdPtr idPtr, double v) : id(std::move(idPtr)), value(v) {} -}; - -} // namespace spectator - -template <> struct fmt::formatter: formatter { - static auto format(const spectator::Measurement& m, format_context& ctx) -> format_context::iterator { - return fmt::format_to(ctx.out(), "Measurement({}, {})", *(m.id), m.value); - } -}; diff --git a/spectator/meter_type.h b/spectator/meter_type.h deleted file mode 100644 index 5b2329c..0000000 --- a/spectator/meter_type.h +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once - -#include - -namespace spectator { -enum class MeterType { - AgeGauge, - Counter, - DistSummary, - Gauge, - MaxGauge, - MonotonicCounter, - MonotonicCounterUint, - PercentileDistSummary, - PercentileTimer, - Timer -}; -} - -template <> struct fmt::formatter: formatter { - auto format(spectator::MeterType meter_type, format_context& ctx) const -> format_context::iterator { - using namespace spectator; - std::string_view s = "unknown"; - - switch (meter_type) { - case MeterType::AgeGauge: - s = "age-gauge"; - break; - case MeterType::Counter: - s = "counter"; - break; - case MeterType::DistSummary: - s = "distribution-summary"; - break; - case MeterType::Gauge: - s = "gauge"; - break; - case MeterType::MaxGauge: - s = "max-gauge"; - break; - case MeterType::MonotonicCounter: - s = "monotonic-counter"; - break; - case MeterType::MonotonicCounterUint: - s = "monotonic-counter-uint"; - break; - case MeterType::PercentileDistSummary: - s = "percentile-distribution-summary"; - break; - case MeterType::PercentileTimer: - s = "percentile-timer"; - break; - case MeterType::Timer: - s = "timer"; - break; - } - - return fmt::formatter::format(s, ctx); - } -}; diff --git a/spectator/meter_type_test.cc b/spectator/meter_type_test.cc deleted file mode 100644 index 0a8e318..0000000 --- a/spectator/meter_type_test.cc +++ /dev/null @@ -1,11 +0,0 @@ -#include "../spectator/meter_type.h" -#include - -namespace { - -using spectator::MeterType; - -TEST(MeterType, Format) { - EXPECT_EQ(fmt::format("{}", MeterType::Counter), "counter"); -} -} // namespace diff --git a/spectator/monotonic_counter_test.cc b/spectator/monotonic_counter_test.cc deleted file mode 100644 index 3cf8e39..0000000 --- a/spectator/monotonic_counter_test.cc +++ /dev/null @@ -1,34 +0,0 @@ -#include "stateless_meters.h" -#include "test_publisher.h" -#include - -namespace { - -using spectator::Id; -using spectator::MonotonicCounter; -using spectator::Tags; -using spectator::TestPublisher; - -TEST(MonotonicCounter, Set) { - TestPublisher publisher; - auto id = std::make_shared("ctr", Tags{}); - auto id2 = std::make_shared("ctr2", Tags{{"key", "val"}}); - MonotonicCounter c{id, &publisher}; - MonotonicCounter c2{id2, &publisher}; - - c.Set(42.1); - c2.Set(2); - c.Set(43); - std::vector expected = {"C:ctr:42.1", "C:ctr2,key=val:2", "C:ctr:43"}; - EXPECT_EQ(publisher.SentMessages(), expected); -} - -TEST(MonotonicCounter, InvalidTags) { - TestPublisher publisher; - // test with a single tag, because tags order is not guaranteed in a flat_hash_map - auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", - Tags{{"tag1,:=", "value1,:="}}); - MonotonicCounter c{id, &publisher}; - EXPECT_EQ("C:test______^____-_~______________.___foo,tag1___=value1___:", c.GetPrefix()); -} -} // namespace diff --git a/spectator/monotonic_counter_uint_test.cc b/spectator/monotonic_counter_uint_test.cc deleted file mode 100644 index d3f55f2..0000000 --- a/spectator/monotonic_counter_uint_test.cc +++ /dev/null @@ -1,34 +0,0 @@ -#include "stateless_meters.h" -#include "test_publisher.h" -#include - -namespace { - -using spectator::Id; -using spectator::MonotonicCounterUint; -using spectator::Tags; -using spectator::TestPublisher; - -TEST(MonotonicCounterUint, Set) { - TestPublisher publisher; - auto id = std::make_shared("ctr", Tags{}); - auto id2 = std::make_shared("ctr2", Tags{{"key", "val"}}); - MonotonicCounterUint c{id, &publisher}; - MonotonicCounterUint c2{id2, &publisher}; - - c.Set(42); - c2.Set(2); - c.Set(-1); - std::vector expected = {"U:ctr:42", "U:ctr2,key=val:2", "U:ctr:18446744073709551615"}; - EXPECT_EQ(publisher.SentMessages(), expected); -} - -TEST(MonotonicCounterUint, InvalidTags) { - TestPublisher publisher; - // test with a single tag, because tags order is not guaranteed in a flat_hash_map - auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", - Tags{{"tag1,:=", "value1,:="}}); - MonotonicCounterUint c{id, &publisher}; - EXPECT_EQ("U:test______^____-_~______________.___foo,tag1___=value1___:", c.GetPrefix()); -} -} // namespace \ No newline at end of file diff --git a/spectator/perc_dist_summary_test.cc b/spectator/perc_dist_summary_test.cc deleted file mode 100644 index 86b730e..0000000 --- a/spectator/perc_dist_summary_test.cc +++ /dev/null @@ -1,31 +0,0 @@ -#include "stateless_meters.h" -#include "test_publisher.h" -#include - -namespace { -using spectator::Id; -using spectator::PercentileDistributionSummary; -using spectator::Tags; -using spectator::TestPublisher; - -TEST(PercDistSum, Record) { - TestPublisher publisher; - auto id = std::make_shared("pds", Tags{}); - PercentileDistributionSummary d{id, &publisher, 0, 1000}; - d.Record(50); - d.Record(5000); - d.Record(-5000); - std::vector expected = {"D:pds:50", "D:pds:1000", - "D:pds:0"}; - EXPECT_EQ(publisher.SentMessages(), expected); -} - -TEST(PercDistSum, InvalidTags) { - TestPublisher publisher; - // test with a single tag, because tags order is not guaranteed in a flat_hash_map - auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", - Tags{{"tag1,:=", "value1,:="}}); - PercentileDistributionSummary d{id, &publisher, 0, 1000}; - EXPECT_EQ("D:test______^____-_~______________.___foo,tag1___=value1___:", d.GetPrefix()); -} -} // namespace diff --git a/spectator/perc_timer_test.cc b/spectator/perc_timer_test.cc deleted file mode 100644 index 6301349..0000000 --- a/spectator/perc_timer_test.cc +++ /dev/null @@ -1,30 +0,0 @@ -#include "stateless_meters.h" -#include "test_publisher.h" -#include - -namespace { -using spectator::Id; -using spectator::PercentileTimer; -using spectator::Tags; -using spectator::TestPublisher; - -TEST(PercentileTimer, Record) { - TestPublisher publisher; - auto id = std::make_shared("pt", Tags{}); - PercentileTimer c{id, &publisher, absl::ZeroDuration(), absl::Seconds(5)}; - c.Record(absl::Milliseconds(42)); - c.Record(std::chrono::microseconds(500)); - c.Record(absl::Seconds(10)); - std::vector expected = {"T:pt:0.042", "T:pt:0.0005", "T:pt:5"}; - EXPECT_EQ(publisher.SentMessages(), expected); -} - -TEST(PercentileTimer, InvalidTags) { - TestPublisher publisher; - // test with a single tag, because tags order is not guaranteed in a flat_hash_map - auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", - Tags{{"tag1,:=", "value1,:="}}); - PercentileTimer t{id, &publisher, absl::ZeroDuration(), absl::Seconds(5)}; - EXPECT_EQ("T:test______^____-_~______________.___foo,tag1___=value1___:", t.GetPrefix()); -} -} // namespace diff --git a/spectator/publisher.cc b/spectator/publisher.cc deleted file mode 100644 index 9658d58..0000000 --- a/spectator/publisher.cc +++ /dev/null @@ -1,166 +0,0 @@ -#include "publisher.h" -#include "logger.h" -#include - -namespace spectator { - -static const char NEW_LINE = '\n'; - -SpectatordPublisher::SpectatordPublisher(absl::string_view endpoint, - uint32_t bytes_to_buffer, - std::shared_ptr logger) - : logger_(std::move(logger)), - udp_socket_(io_context_), - local_socket_(io_context_), bytes_to_buffer_(bytes_to_buffer) { - buffer_.reserve(bytes_to_buffer_ + 1024); - if (absl::StartsWith(endpoint, "unix:")) { - this->unixDomainPath_ = std::string(endpoint.substr(5)); - setup_unix_domain(); - } else if (absl::StartsWith(endpoint, "udp:")) { - auto pos = 4; - // if the user used udp://foo:1234 instead of udp:foo:1234 - // adjust accordingly - if (endpoint.substr(pos, 2) == "//") { - pos += 2; - } - setup_udp(endpoint.substr(pos)); - } else if (endpoint != "disabled") { - logger_->warn( - "Unknown endpoint: '{}'. Expecting: 'unix:/path/to/socket'" - " or 'udp:hostname:port' - Will not send metrics", - std::string(endpoint)); - setup_nop_sender(); - } -} - -void SpectatordPublisher::setup_nop_sender() { - sender_ = [this](std::string_view msg) { logger_->trace("{}", msg); }; -} - -void SpectatordPublisher::local_reconnect(absl::string_view path) { - using endpoint_t = asio::local::datagram_protocol::endpoint; - try { - if (local_socket_.is_open()) { - local_socket_.close(); - } - local_socket_.open(); - local_socket_.connect(endpoint_t(std::string(path))); - } catch (std::exception& e) { - logger_->warn("Unable to connect to {}: {}", std::string(path), e.what()); - } -} - - -bool SpectatordPublisher::try_to_send(const std::string& buffer) { - for (auto i = 0; i < 3; ++i) { - try { - auto sent_bytes = local_socket_.send(asio::buffer(buffer)); - logger_->trace("Sent (local): {} bytes, in total had {}", sent_bytes, - buffer.length()); - return true; - } catch (std::exception& e) { - local_reconnect(this->unixDomainPath_); - logger_->warn("Unable to send {} - attempt {}/3 ({})", buffer, i, e.what()); - } - } - return false; -} - -void SpectatordPublisher::taskThreadFunction() try { - while (shutdown_.load() == false) { - std::string message {}; - { - std::unique_lock lock(mtx_); - cv_sender_.wait(lock, [this] { return buffer_.size() > bytes_to_buffer_ || shutdown_.load();}); - if (shutdown_.load() == true) { - return; - } - message = std::move(buffer_); - buffer_ = std::string(); - buffer_.reserve(bytes_to_buffer_); - } - cv_receiver_.notify_one(); - try_to_send(message); - } -} catch (const std::exception& e) { - logger_->error("Fatal error in message processing thread: {}", e.what()); -} - -void SpectatordPublisher::setup_unix_domain(){ - // Reset connection to the unix domain socket - local_reconnect(this->unixDomainPath_); - if (bytes_to_buffer_ == 0) { - sender_ = [this](std::string_view msg) { - try_to_send(std::string(msg)); - }; - return; - } - else { - sender_ = [this](std::string_view msg) { - unsigned int currentBufferSize = buffer_.size(); - { - std::unique_lock lock(mtx_); - cv_receiver_.wait(lock, [this] { return buffer_.size() <= bytes_to_buffer_ || shutdown_.load(); }); - if (shutdown_.load()) { - return; - } - buffer_.append(msg.data(), msg.size()); - buffer_.append(1, NEW_LINE); - currentBufferSize = buffer_.size(); - } - currentBufferSize > bytes_to_buffer_ ? cv_sender_.notify_one() : cv_receiver_.notify_one(); - }; - this->sendingThread_ = std::thread(&SpectatordPublisher::taskThreadFunction, this); - } -} - -inline asio::ip::udp::endpoint resolve_host_port( - asio::io_context& io_context, // NOLINT - absl::string_view host_port) { - using asio::ip::udp; - udp::resolver resolver{io_context}; - - auto end_host = host_port.find(':'); - if (end_host == std::string_view::npos) { - auto err = fmt::format( - "Unable to parse udp endpoint: '{}'. Expecting hostname:port", - std::string(host_port)); - throw std::runtime_error(err); - } - - auto host = host_port.substr(0, end_host); - auto port = host_port.substr(end_host + 1); - return *resolver.resolve(udp::v6(), std::string(host), std::string(port)); -} - -void SpectatordPublisher::udp_reconnect( - const asio::ip::udp::endpoint& endpoint) { - try { - if (udp_socket_.is_open()) { - udp_socket_.close(); - } - udp_socket_.open(asio::ip::udp::v6()); - udp_socket_.connect(endpoint); - } catch (std::exception& e) { - logger_->warn("Unable to connect to {}: {}", endpoint.address().to_string(), - endpoint.port()); - } -} - -void SpectatordPublisher::setup_udp(absl::string_view host_port) { - auto endpoint = resolve_host_port(io_context_, host_port); - udp_reconnect(endpoint); - sender_ = [endpoint, this](std::string_view msg) { - for (auto i = 0; i < 3; ++i) { - try { - udp_socket_.send(asio::buffer(msg)); - logger_->trace("Sent (udp): {}", msg); - break; - } catch (std::exception& e) { - logger_->warn("Unable to send {} - attempt {}/3", msg, i); - udp_reconnect(endpoint); - } - } - }; -} -} // namespace spectator diff --git a/spectator/publisher.h b/spectator/publisher.h deleted file mode 100644 index 56e37d7..0000000 --- a/spectator/publisher.h +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once - -#include "logger.h" -#include "absl/strings/match.h" -#include "absl/strings/string_view.h" -#include - -namespace spectator { - -class SpectatordPublisher { - public: - explicit SpectatordPublisher( - absl::string_view endpoint, - uint32_t bytes_to_buffer = 0, - std::shared_ptr logger = DefaultLogger()); - SpectatordPublisher(const SpectatordPublisher&) = delete; - - ~SpectatordPublisher() { - shutdown_.store(true); - cv_receiver_.notify_all(); - cv_sender_.notify_all(); - if (sendingThread_.joinable()) { - sendingThread_.join(); - } - } - - void send(std::string_view measurement) { sender_(measurement); }; - - void taskThreadFunction(); - bool try_to_send(const std::string& buffer); - - protected: - using sender_fun = std::function; - sender_fun sender_; - - private: - void setup_nop_sender(); - void setup_unix_domain(); - void setup_udp(absl::string_view host_port); - void local_reconnect(absl::string_view path); - void udp_reconnect(const asio::ip::udp::endpoint& endpoint); - - std::shared_ptr logger_; - asio::io_context io_context_; - asio::ip::udp::socket udp_socket_; - asio::local::datagram_protocol::socket local_socket_; - std::string buffer_; - uint32_t bytes_to_buffer_; - - std::thread sendingThread_; - std::mutex mtx_; - std::condition_variable cv_receiver_; - std::condition_variable cv_sender_; - std::string unixDomainPath_; - std::atomic shutdown_{false}; -}; - -} // namespace spectator diff --git a/spectator/publisher_test.cc b/spectator/publisher_test.cc deleted file mode 100644 index 0358687..0000000 --- a/spectator/publisher_test.cc +++ /dev/null @@ -1,166 +0,0 @@ -#include "id.h" -#include "logger.h" -#include "publisher.h" -#include "stateless_meters.h" -#include "test_server.h" -#include -#include -#include - -namespace { - -using spectator::Counter; -using spectator::Id; -using spectator::SpectatordPublisher; -using spectator::Tags; - -TEST(Publisher, Udp) { - // travis does not support udp on its container - if (std::getenv("TRAVIS_COMPILER") == nullptr) { - TestUdpServer server; - server.Start(); - auto logger = spectator::DefaultLogger(); - logger->info("Udp Server started on port {}", server.GetPort()); - - SpectatordPublisher publisher{ - fmt::format("udp:localhost:{}", server.GetPort()), 0}; - Counter c{std::make_shared("counter", Tags{}), &publisher}; - c.Increment(); - c.Add(2); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - auto msgs = server.GetMessages(); - server.Stop(); - std::vector expected{"c:counter:1", "c:counter:2"}; - EXPECT_EQ(server.GetMessages(), expected); - } -} - -const char* first_not_null(char* a, const char* b) { - if (a != nullptr) return a; - return b; -} - -TEST(Publisher, UnixNoBuffer) { - auto logger = spectator::DefaultLogger(); - const auto* dir = first_not_null(std::getenv("TMPDIR"), "/tmp"); - auto path = fmt::format("{}/testserver.{}", dir, getpid()); - TestUnixServer server{path}; - server.Start(); - logger->info("Unix Server started on path {}", path); - SpectatordPublisher publisher{fmt::format("unix:{}", path), 0}; - Counter c{std::make_shared("counter", Tags{}), &publisher}; - c.Increment(); - c.Add(2); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - auto msgs = server.GetMessages(); - server.Stop(); - unlink(path.c_str()); - std::vector expected{"c:counter:1", "c:counter:2"}; - EXPECT_EQ(msgs, expected); -} - -TEST(Publisher, UnixBuffer) { - auto logger = spectator::DefaultLogger(); - const auto* dir = first_not_null(std::getenv("TMPDIR"), "/tmp"); - auto path = fmt::format("{}/testserver.{}", dir, getpid()); - TestUnixServer server{path}; - server.Start(); - logger->info("Unix Server started on path {}", path); - // Do not send until we buffer 32 bytes of data. - SpectatordPublisher publisher{fmt::format("unix:{}", path), 32}; - Counter c{std::make_shared("counter", Tags{}), &publisher}; - c.Increment(); - c.Increment(); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - auto msgs = server.GetMessages(); - std::vector emptyVector {}; - EXPECT_EQ(msgs, emptyVector); - c.Increment(); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - msgs = server.GetMessages(); - std::vector expected{"c:counter:1\nc:counter:1\nc:counter:1\n"}; - EXPECT_EQ(msgs, expected); - server.Stop(); - unlink(path.c_str()); -} - -TEST(Publisher, Nop) { - SpectatordPublisher publisher{"", 0}; - Counter c{std::make_shared("counter", Tags{}), &publisher}; - c.Increment(); - c.Add(2); -} - -TEST(Publisher, MultiThreadedCounters) { - auto logger = spectator::DefaultLogger(); - const auto* dir = first_not_null(std::getenv("TMPDIR"), "/tmp"); - auto path = fmt::format("{}/testserver.{}", dir, getpid()); - TestUnixServer server{path}; - server.Start(); - logger->info("Unix Server started on path {}", path); - - // Create publisher with a small buffer size to ensure flushing - SpectatordPublisher publisher{fmt::format("unix:{}", path), 50}; - - // Number of threads and counters to create - const int numThreads = 4; - const int countersPerThread = 3; - const int incrementsPerCounter = 5; - - // Function for worker threads - auto worker = [&](int threadId) { - // Create several counters per thread with unique names - for (int i = 0; i < countersPerThread; i++) { - std::string counterName = fmt::format("counter.thread{}.{}", threadId, i); - Counter counter(std::make_shared(counterName, Tags{}), &publisher); - - // Increment each counter multiple times - for (int j = 0; j < incrementsPerCounter; j++) { - counter.Increment(); - } - } - }; - - // Start worker threads - std::vector threads; - for (int i = 0; i < numThreads; i++) { - threads.emplace_back(worker, i); - } - - // Wait for all threads to complete - for (auto& t : threads) { - t.join(); - } - - // Give some time for messages to be sent - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - // Check messages - auto msgs = server.GetMessages(); - EXPECT_FALSE(msgs.empty()); - - // Verify total number of increments - int expectedIncrements = numThreads * countersPerThread * incrementsPerCounter; - int actualIncrements = 0; - - // Verify every string in msgs follows the form counter.thread. - std::regex counter_regex(R"(c:counter\.thread\d+\.\d+:1)"); - for (const auto& msg : msgs) { - std::stringstream ss(msg); - std::string line; - while (std::getline(ss, line)) { - if (!line.empty()) { - EXPECT_TRUE(std::regex_match(line, counter_regex)) - << "Unexpected counter format: " << line; - actualIncrements++; - } - } - } - - EXPECT_EQ(actualIncrements, expectedIncrements); - - server.Stop(); - unlink(path.c_str()); -} - -} // namespace diff --git a/spectator/registry.cpp b/spectator/registry.cpp new file mode 100644 index 0000000..a1f171e --- /dev/null +++ b/spectator/registry.cpp @@ -0,0 +1,160 @@ +#include + + +std::pair ParseUdpAddress(const std::string& address) +{ + std::regex pattern("udp://([0-9.]+):(\\d+)"); + std::smatch matches; + + if (!std::regex_match(address, matches, pattern) || matches.size() != 3) + { + throw std::runtime_error("Invalid UDP address format"); + } + + std::string ip = matches[1].str(); + int port = std::stoi(matches[2]); + + // Optional: Validate port range + if (port < 0 || port > 65535) + throw std::runtime_error("Port number out of valid range (0-65535)"); + + return {ip, port}; +} + +std::string ParseUnixAddress(const std::string& address) +{ + std::regex pattern("unix://(.*)"); + std::smatch matches; + + if (!std::regex_match(address, matches, pattern) || matches.size() != 2) + { + throw std::runtime_error("Invalid Unix address format"); + } + + return matches[1].str(); +} + +Registry::Registry(const Config& config) : m_config(config) +{ + if (config.GetWriterType() == WriterType::Memory) + { + Logger::info("Registry initializing Memory Writer"); + Writer::Initialize(config.GetWriterType()); + } + else if (config.GetWriterType() == WriterType::UDP) + { + auto [ip, port] = ParseUdpAddress(this->m_config.GetWriterLocation()); + Logger::info("Registry initializing UDP Writer at {}:{}", ip, port); + Writer::Initialize(config.GetWriterType(), ip, port, this->m_config.GetWriterBufferSize()); + } + else if (config.GetWriterType() == WriterType::Unix) + { + auto socketPath = ParseUnixAddress(this->m_config.GetWriterLocation()); + Logger::info("Registry initializing UDS Writer at {null}:{null}"); + Writer::Initialize(config.GetWriterType(), socketPath, 0, this->m_config.GetWriterBufferSize()); + } +} + +Registry::~Registry() +{ + // No need to close Writer here as it's a singleton + // and will live beyond Registry instances +} + +MeterId Registry::new_id(const std::string& name, const std::unordered_map& tags) const +{ + MeterId new_meter_id(name, tags); + + if (this->m_config.GetExtraTags().empty() == true) + { + return new_meter_id; + } + return new_meter_id.WithTags(this->m_config.GetExtraTags()); +} + +AgeGauge Registry::age_gauge(const std::string& name, const std::unordered_map& tags) const +{ + return AgeGauge(new_id(name, tags)); +} + +AgeGauge Registry::age_gauge_with_id(const MeterId& meter_id) { return AgeGauge(meter_id); } + +Counter Registry::counter(const std::string& name, const std::unordered_map& tags) const +{ + return Counter(new_id(name, tags)); +} + +Counter Registry::counter_with_id(const MeterId& meter_id) { return Counter(meter_id); } + +DistributionSummary Registry::distribution_summary(const std::string& name, + const std::unordered_map& tags) const +{ + return DistributionSummary(new_id(name, tags)); +} + +DistributionSummary Registry::distribution_summary_with_id(const MeterId& meter_id) +{ + return DistributionSummary(meter_id); +} + +Gauge Registry::gauge(const std::string& name, const std::unordered_map& tags, + const std::optional& ttl_seconds) const +{ + return Gauge(new_id(name, tags), ttl_seconds); +} + +Gauge Registry::gauge_with_id(const MeterId& meter_id, const std::optional& ttl_seconds) +{ + return Gauge(meter_id, ttl_seconds); +} + +MaxGauge Registry::max_gauge(const std::string& name, const std::unordered_map& tags) const +{ + return MaxGauge(new_id(name, tags)); +} + +MaxGauge Registry::max_gauge_with_id(const MeterId& meter_id) { return MaxGauge(meter_id); } + +MonotonicCounter Registry::monotonic_counter(const std::string& name, + const std::unordered_map& tags) const +{ + return MonotonicCounter(new_id(name, tags)); +} + +MonotonicCounter Registry::monotonic_counter_with_id(const MeterId& meter_id) { return MonotonicCounter(meter_id); } + +MonotonicCounterUint Registry::monotonic_counter_uint(const std::string& name, + const std::unordered_map& tags) const +{ + return MonotonicCounterUint(new_id(name, tags)); +} + +MonotonicCounterUint Registry::monotonic_counter_uint_with_id(const MeterId& meter_id) +{ + return MonotonicCounterUint(meter_id); +} + +PercentileDistributionSummary Registry::pct_distribution_summary( + const std::string& name, const std::unordered_map& tags) const +{ + return PercentileDistributionSummary(new_id(name, tags)); +} + +PercentileDistributionSummary Registry::pct_distribution_summary_with_id(const MeterId& meter_id) +{ + return PercentileDistributionSummary(meter_id); +} + +PercentileTimer Registry::pct_timer(const std::string& name, const std::unordered_map& tags) const +{ + return PercentileTimer(new_id(name, tags)); +} + +PercentileTimer Registry::pct_timer_with_id(const MeterId& meter_id) { return PercentileTimer(meter_id); } + +Timer Registry::timer(const std::string& name, const std::unordered_map& tags) const +{ + return Timer(new_id(name, tags)); +} + +Timer Registry::timer_with_id(const MeterId& meter_id) { return Timer(meter_id); } \ No newline at end of file diff --git a/spectator/registry.h b/spectator/registry.h index 9a8e50d..148a52f 100644 --- a/spectator/registry.h +++ b/spectator/registry.h @@ -1,356 +1,82 @@ #pragma once -#include "absl/container/flat_hash_map.h" -#include "absl/synchronization/mutex.h" -#include "config.h" -#include "logger.h" -#include "stateful_meters.h" -#include "stateless_meters.h" -#include "publisher.h" +#include +#include +#include +#include +#include -namespace spectator { +#include +#include +#include +#include +#include +#include -// A registry for tests -// This is a stateful registry that will keep references to all registered -// meters and allows users to fetch the measurements at a later point +class Registry +{ + public: + explicit Registry(const Config& config); + ~Registry(); -namespace detail { -inline void log_type_error(MeterType old_type, MeterType new_type, - const Id& id) { - DefaultLogger()->warn( - "Attempting to register {} as a {} but was previously registered as a {}", - id, new_type, old_type); -} -} // namespace detail + MeterId new_id(const std::string& name, const std::unordered_map& tags = {}) const; -template -struct single_table_state { - using types = Types; + AgeGauge age_gauge(const std::string& name, const std::unordered_map& tags = + std::unordered_map()) const; - template - std::shared_ptr get_or_create(IdPtr id, Args&&... args) { - auto new_meter = - std::make_shared(std::move(id), std::forward(args)...); - absl::MutexLock lock(&mutex_); - auto it = meters_.find(new_meter->MeterId()); - if (it != meters_.end()) { - // already exists, we need to ensure the existing type - // matches the new meter type, otherwise we need to notify the user - // of the error - auto& old_meter = it->second; - if (old_meter->GetType() != new_meter->GetType()) { - detail::log_type_error(old_meter->GetType(), new_meter->GetType(), - *new_meter->MeterId()); - // this is not registered therefore no measurements - // will be reported - return new_meter; - } else { - return std::static_pointer_cast(old_meter); - } - } + static AgeGauge age_gauge_with_id(const MeterId& meter_id); - meters_.emplace(new_meter->MeterId(), new_meter); - return new_meter; - } + Counter counter(const std::string& name, const std::unordered_map& tags = + std::unordered_map()) const; - auto get_age_gauge(IdPtr id) { - return get_or_create(std::move(id)); - } + static Counter counter_with_id(const MeterId& meter_id); - auto get_counter(IdPtr id) { - return get_or_create(std::move(id)); - } + DistributionSummary distribution_summary( + const std::string& name, + const std::unordered_map& tags = std::unordered_map()) const; - auto get_ds(IdPtr id) { - return get_or_create(std::move(id)); - } + static DistributionSummary distribution_summary_with_id(const MeterId& meter_id); - auto get_gauge(IdPtr id) { - return get_or_create(std::move(id)); - } + Gauge gauge( + const std::string& name, + const std::unordered_map& tags = std::unordered_map(), + const std::optional& ttl_seconds = std::nullopt) const; - auto get_gauge_ttl(IdPtr id, unsigned int ttl_seconds) { - return get_or_create(std::move(id), ttl_seconds); - } + static Gauge gauge_with_id(const MeterId& meter_id, const std::optional& ttl_seconds = std::nullopt); - auto get_max_gauge(IdPtr id) { - return get_or_create(std::move(id)); - } + MaxGauge max_gauge(const std::string& name, const std::unordered_map& tags = + std::unordered_map()) const; - auto get_monotonic_counter(IdPtr id) { - return get_or_create(std::move(id)); - } + static MaxGauge max_gauge_with_id(const MeterId& meter_id); - auto get_monotonic_counter_uint(IdPtr id) { - return get_or_create(std::move(id)); - } + MonotonicCounter monotonic_counter( + const std::string& name, + const std::unordered_map& tags = std::unordered_map()) const; - auto get_perc_ds(IdPtr id, int64_t min, int64_t max) { - return get_or_create(std::move(id), min, max); - } + static MonotonicCounter monotonic_counter_with_id(const MeterId& meter_id); - auto get_perc_timer(IdPtr id, std::chrono::nanoseconds min, - std::chrono::nanoseconds max) { - return get_or_create(std::move(id), min, max); - } + MonotonicCounterUint monotonic_counter_uint( + const std::string& name, + const std::unordered_map& tags = std::unordered_map()) const; - auto get_timer(IdPtr id) { - return get_or_create(std::move(id)); - } + static MonotonicCounterUint monotonic_counter_uint_with_id(const MeterId& meter_id); - auto measurements() { - std::vector result; + PercentileDistributionSummary pct_distribution_summary( + const std::string& name, + const std::unordered_map& tags = std::unordered_map()) const; - absl::MutexLock lock(&mutex_); - result.reserve(meters_.size() * 2); - for (auto& m : meters_) { - m.second->Measure(&result); - } - return result; - } + static PercentileDistributionSummary pct_distribution_summary_with_id(const MeterId& meter_id); - absl::Mutex mutex_; - // use a single table, so we can easily check whether a meter - // was previously registered as a different type - absl::flat_hash_map, std::hash, - std::equal_to> - meters_ ABSL_GUARDED_BY(mutex_); -}; + PercentileTimer pct_timer(const std::string& name, const std::unordered_map& tags = + std::unordered_map()) const; -template -class base_registry { - public: - using logger_ptr = std::shared_ptr; - using age_gauge_t = typename Types::age_gauge_t; - using age_gauge_ptr = std::shared_ptr; - using counter_t = typename Types::counter_t; - using counter_ptr = std::shared_ptr; - using dist_summary_t = typename Types::ds_t; - using dist_summary_ptr = std::shared_ptr; - using gauge_t = typename Types::gauge_t; - using gauge_ptr = std::shared_ptr; - using max_gauge_t = typename Types::max_gauge_t; - using max_gauge_ptr = std::shared_ptr; - using monotonic_counter_t = typename Types::monotonic_counter_t; - using monotonic_counter_ptr = std::shared_ptr; - using monotonic_counter_uint_t = typename Types::monotonic_counter_uint_t; - using monotonic_counter_uint_ptr = std::shared_ptr; - using perc_dist_summary_t = typename Types::perc_ds_t; - using perc_dist_summary_ptr = std::shared_ptr; - using perc_timer_t = typename Types::perc_timer_t; - using perc_timer_ptr = std::shared_ptr; - using timer_t = typename Types::timer_t; - using timer_ptr = std::shared_ptr; + static PercentileTimer pct_timer_with_id(const MeterId& meter_id); - explicit base_registry(logger_ptr logger = DefaultLogger()) - : logger_(std::move(logger)) {} + Timer timer(const std::string& name, const std::unordered_map& tags = + std::unordered_map()) const; - auto GetAgeGauge(const IdPtr& id) { - return state_.get_age_gauge(final_id(id)); - } - auto GetAgeGauge(absl::string_view name, Tags tags = {}) { - return GetAgeGauge(Id::of(name, std::move(tags))); - } + static Timer timer_with_id(const MeterId& meter_id); - auto GetCounter(const IdPtr& id) { - return state_.get_counter(final_id(id)); - } - auto GetCounter(absl::string_view name, Tags tags = {}) { - return GetCounter(Id::of(name, std::move(tags))); - } - - auto GetDistributionSummary(const IdPtr& id) { - return state_.get_ds(final_id(id)); - } - auto GetDistributionSummary(absl::string_view name, Tags tags = {}) { - return GetDistributionSummary(Id::of(name, std::move(tags))); - } - - auto GetGauge(const IdPtr& id) { - return state_.get_gauge(final_id(id)); - } - auto GetGauge(absl::string_view name, Tags tags = {}) { - return GetGauge(Id::of(name, std::move(tags))); - } - - auto GetGaugeTTL(const IdPtr& id, unsigned int ttl_seconds) { - return state_.get_gauge_ttl(final_id(id), ttl_seconds); - } - - auto GetGaugeTTL(absl::string_view name, unsigned int ttl_seconds, Tags tags = {}) { - return GetGaugeTTL(Id::of(name, std::move(tags)), ttl_seconds); - } - - auto GetMaxGauge(const IdPtr& id) { - return state_.get_max_gauge(final_id(id)); - } - auto GetMaxGauge(absl::string_view name, Tags tags = {}) { - return GetMaxGauge(Id::of(name, std::move(tags))); - } - - auto GetMonotonicCounter(const IdPtr& id) { - return state_.get_monotonic_counter(final_id(id)); - } - auto GetMonotonicCounter(absl::string_view name, Tags tags = {}) { - return GetMonotonicCounter(Id::of(name, std::move(tags))); - } - - auto GetMonotonicCounterUint(const IdPtr& id) { - return state_.get_monotonic_counter_uint(final_id(id)); - } - auto GetMonotonicCounterUint(absl::string_view name, Tags tags = {}) { - return GetMonotonicCounterUint(Id::of(name, std::move(tags))); - } - - auto GetPercentileDistributionSummary(const IdPtr& id, int64_t min, int64_t max) { - return state_.get_perc_ds(final_id(id), min, max); - } - auto GetPercentileDistributionSummary(absl::string_view name, int64_t min, int64_t max) { - return GetPercentileDistributionSummary(Id::of(name), min, max); - } - auto GetPercentileDistributionSummary(absl::string_view name, Tags tags, - int64_t min, int64_t max) { - return GetPercentileDistributionSummary(Id::of(name, std::move(tags)), min, max); - } - - auto GetPercentileTimer(const IdPtr& id, absl::Duration min, absl::Duration max) { - return state_.get_perc_timer(final_id(id), min, max); - } - auto GetPercentileTimer(const IdPtr& id, std::chrono::nanoseconds min, - std::chrono::nanoseconds max) { - return state_.get_perc_timer(final_id(id), absl::FromChrono(min), absl::FromChrono(max)); - } - auto GetPercentileTimer(absl::string_view name, absl::Duration min, absl::Duration max) { - return GetPercentileTimer(Id::of(name), min, max); - } - auto GetPercentileTimer(absl::string_view name, Tags tags, - absl::Duration min, absl::Duration max) { - return GetPercentileTimer(Id::of(name, std::move(tags)), min, max); - } - auto GetPercentileTimer(absl::string_view name, - std::chrono::nanoseconds min, std::chrono::nanoseconds max) { - return GetPercentileTimer(Id::of(name), absl::FromChrono(min), absl::FromChrono(max)); - } - auto GetPercentileTimer(absl::string_view name, Tags tags, - std::chrono::nanoseconds min, std::chrono::nanoseconds max) { - return GetPercentileTimer(Id::of(name, std::move(tags)), absl::FromChrono(min), - absl::FromChrono(max)); - } - - auto GetTimer(const IdPtr& id) { - return state_.get_timer(final_id(id)); - } - auto GetTimer(absl::string_view name, Tags tags = {}) { - return GetTimer(Id::of(name, std::move(tags))); - } - - auto Measurements() { return state_.measurements(); } - - protected: - logger_ptr logger_; - State state_; - Tags extra_tags_; - - // final Id after adding extra_tags_ if any - IdPtr final_id(const IdPtr& id) { - if (extra_tags_.size() > 0) { - return id->WithTags(extra_tags_); - } - return id; - } -}; - -template -struct stateless_types { - using counter_t = Counter; - using ds_t = DistributionSummary; - using gauge_t = Gauge; - using max_gauge_t = MaxGauge; - using age_gauge_t = AgeGauge; - using monotonic_counter_t = MonotonicCounter; - using monotonic_counter_uint_t = MonotonicCounterUint; - using perc_timer_t = PercentileTimer; - using perc_ds_t = PercentileDistributionSummary; - using timer_t = Timer; - using publisher_t = Pub; -}; - -template -struct stateless { - using types = Types; - std::unique_ptr publisher; - - auto get_age_gauge(IdPtr id) { - return std::make_shared(std::move(id), publisher.get()); - } - - auto get_counter(IdPtr id) { - return std::make_shared(std::move(id), publisher.get()); - } - - auto get_ds(IdPtr id) { - return std::make_shared(std::move(id), publisher.get()); - } - - auto get_gauge(IdPtr id) { - return std::make_shared(std::move(id), publisher.get()); - } - - auto get_gauge_ttl(IdPtr id, unsigned int ttl_seconds) { - return std::make_shared(std::move(id), publisher.get(), ttl_seconds); - } - - auto get_max_gauge(IdPtr id) { - return std::make_shared(std::move(id), publisher.get()); - } - - auto get_monotonic_counter(IdPtr id) { - return std::make_shared(std::move(id), publisher.get()); - } - - auto get_monotonic_counter_uint(IdPtr id) { - return std::make_shared(std::move(id), - publisher.get()); - } - - auto get_perc_ds(IdPtr id, int64_t min, int64_t max) { - return std::make_shared(std::move(id), publisher.get(), min, max); - } - - auto get_perc_timer(IdPtr id, absl::Duration min, absl::Duration max) { - return std::make_shared(std::move(id), publisher.get(), min, max); - } - - auto get_timer(IdPtr id) { - return std::make_shared(std::move(id), publisher.get()); - } - - auto measurements() { return std::vector{}; } -}; - -/// A stateless registry that sends all meter activity immediately -/// to a spectatord agent -class SpectatordRegistry - : public base_registry>> { - public: - using types = stateless_types; - explicit SpectatordRegistry(const Config& config, logger_ptr logger) - : base_registry>>( - std::move(logger)) { - extra_tags_ = Tags::from(config.common_tags); - state_.publisher = - std::make_unique(config.endpoint, config.bytes_to_buffer, logger_); - } -}; - -/// A Registry that can be used for tests. It keeps state about which meters -/// have been registered, and can report the measurements from all the -/// registered meters -struct TestRegistry : base_registry> { - using types = stateful_meters; -}; - -/// The default registry -using Registry = SpectatordRegistry; - -} // namespace spectator + private: + Config m_config; +}; \ No newline at end of file diff --git a/spectator/stateful_meters.h b/spectator/stateful_meters.h deleted file mode 100644 index deacb1a..0000000 --- a/spectator/stateful_meters.h +++ /dev/null @@ -1,299 +0,0 @@ -#pragma once - -#include "id.h" -#include "measurement.h" -#include "meter_type.h" - -namespace spectator { - -namespace detail { -/// Atomically add a delta to an atomic double -/// equivalent to fetch_add for integer types -inline void add_double(std::atomic* n, double delta) { - double current; - do { - current = n->load(std::memory_order_relaxed); - } while (!n->compare_exchange_weak( - current, n->load(std::memory_order_relaxed) + delta)); -} - -/// Atomically set the max value of an atomic number -template -inline void update_max(std::atomic* n, T value) { - T current; - do { - current = n->load(std::memory_order_relaxed); - } while (value > current && !n->compare_exchange_weak(current, value)); -} -} // namespace detail - -class StatefulMeter { - public: - explicit StatefulMeter(IdPtr id) : id_{std::move(id)} {} - StatefulMeter(const StatefulMeter&) = default; - virtual ~StatefulMeter() = default; - virtual void Measure(std::vector* measurements) = 0; - [[nodiscard]] virtual MeterType GetType() const = 0; - [[nodiscard]] IdPtr MeterId() const { return id_; } - - protected: - IdPtr id_; -}; - -template -class TestDistribution : public StatefulMeter { - public: - explicit TestDistribution(IdPtr id) : StatefulMeter(std::move(id)) {} - - int64_t Count() const { return count_; } - - double TotalAmount() const { return total_; } - - MeterType GetType() const override { return DistType::meter_type; } - - void Measure(std::vector* measurements) override { - auto cnt = count_.exchange(0); - if (cnt == 0) { - return; - } - auto total = total_.exchange(0); - auto t_sq = totalSq_.exchange(0); - auto mx = max_.exchange(0); - measurements->emplace_back(id_->WithStat(DistType::total_name), total); - measurements->emplace_back(id_->WithStat("totalOfSquares"), t_sq); - measurements->emplace_back(id_->WithStat("max"), mx); - measurements->emplace_back(id_->WithStat("count"), cnt); - } - - protected: - void record(double amount) { - if (amount >= 0) { - count_.fetch_add(1); - detail::add_double(&total_, amount); - detail::add_double(&totalSq_, amount * amount); - detail::update_max(&max_, amount); - } - } - - private: - std::atomic count_ = 0; - std::atomic total_ = 0; - std::atomic totalSq_ = 0; - std::atomic max_ = 0; -}; - -struct timer_distribution { - static constexpr auto meter_type = MeterType::Timer; - static constexpr auto total_name = "totalTime"; -}; - -struct summary_distribution { - static constexpr auto meter_type = MeterType::DistSummary; - static constexpr auto total_name = "totalAmount"; -}; - -class StatefulAgeGauge : public StatefulMeter { - public: - explicit StatefulAgeGauge(IdPtr id) : StatefulMeter(std::move(id)) {} - - [[nodiscard]] double Get() const { return value_; } - - MeterType GetType() const override { return MeterType::AgeGauge; } - - void Set(double amount) { value_ = amount; } - - void Measure(std::vector* measurements) override { - auto v = value_.exchange(kNaN); - if (std::isnan(v)) { - return; - } - measurements->emplace_back(Id::WithDefaultStat(id_, "gauge"), v); - } - - private: - static constexpr auto kNaN = std::numeric_limits::quiet_NaN(); - std::atomic value_ = kNaN; -}; - -class StatefulCounter : public StatefulMeter { - public: - explicit StatefulCounter(IdPtr id) : StatefulMeter(std::move(id)) {} - - [[nodiscard]] double Count() const { return count_; }; - - MeterType GetType() const override { return MeterType::Counter; } - - void Add(double delta) { - if (delta > 0) { - detail::add_double(&count_, delta); - } - } - - void Increment() { Add(1); } - - void Measure(std::vector* measurements) override { - auto count = count_.exchange(0.0); - if (count > 0) { - measurements->emplace_back(Id::WithDefaultStat(id_, "count"), count); - } - } - - private: - std::atomic count_ = 0.0; -}; - -class StatefulDistSum : public TestDistribution { - public: - explicit StatefulDistSum(IdPtr id): TestDistribution(std::move(id)) {} - - void Record(double amount) { record(amount); } -}; - -class StatefulGauge : public StatefulMeter { - public: - explicit StatefulGauge(IdPtr id) : StatefulMeter(std::move(id)) {} - - [[nodiscard]] double Get() const { return value_; } - - MeterType GetType() const override { return MeterType::Gauge; } - - void Set(double amount) { value_ = amount; } - - void Measure(std::vector* measurements) override { - auto v = value_.exchange(kNaN); - if (std::isnan(v)) { - return; - } - measurements->emplace_back(Id::WithDefaultStat(id_, "gauge"), v); - } - - private: - static constexpr auto kNaN = std::numeric_limits::quiet_NaN(); - std::atomic value_ = kNaN; -}; - -class StatefulMaxGauge : public StatefulMeter { - public: - explicit StatefulMaxGauge(IdPtr id) : StatefulMeter(std::move(id)) {} - - [[nodiscard]] double Get() const { return value_; } - - MeterType GetType() const override { return MeterType::MaxGauge; } - - void Set(double amount) { detail::update_max(&value_, amount); } - - void Update(double amount) { Set(amount); } - - void Measure(std::vector* measurements) override { - auto v = value_.exchange(kMinValue); - if (v == kMinValue) { - return; - } - measurements->emplace_back(Id::WithDefaultStat(id_, "max"), v); - } - - private: - static constexpr auto kMinValue = std::numeric_limits::lowest(); - std::atomic value_ = kMinValue; -}; - -class StatefulMonoCounter : public StatefulMeter { - public: - explicit StatefulMonoCounter(IdPtr id) : StatefulMeter(std::move(id)) {} - - MeterType GetType() const override { return MeterType::MonotonicCounter; } - - [[nodiscard]] double Delta() const { return value_ - prev_value_; } - - void Set(double amount) { value_ = amount; } - - void Measure(std::vector* measurements) override { - auto delta = Delta(); - prev_value_ = value_.load(); - if (delta > 0) { - measurements->emplace_back(id_->WithStat("count"), delta); - } - } - - private: - static constexpr auto kNaN = std::numeric_limits::quiet_NaN(); - std::atomic value_ = kNaN; - std::atomic prev_value_ = kNaN; -}; - -class StatefulMonoCounterUint : public StatefulMeter { - public: - explicit StatefulMonoCounterUint(IdPtr id) : StatefulMeter(std::move(id)) {} - - MeterType GetType() const override { return MeterType::MonotonicCounterUint; } - - [[nodiscard]] double Delta() const { - if (value_ < prev_value_) { - return kMax - prev_value_ + value_ + 1; - } else { - return value_ - prev_value_; - } - } - - void Set(uint64_t amount) { value_ = amount; } - - void Measure(std::vector* measurements) override { - auto delta = Delta(); - prev_value_ = value_.load(); - if (delta > 0) { - measurements->emplace_back(id_->WithStat("count"), delta); - } - } - - private: - static constexpr auto kMax = std::numeric_limits::max(); - std::atomic value_ = 0; - std::atomic prev_value_ = 0; -}; - -class StatefulPercTimer : public StatefulMeter { - public: - StatefulPercTimer(IdPtr id, std::chrono::nanoseconds, std::chrono::nanoseconds) - : StatefulMeter(std::move(id)) {} - - [[nodiscard]] MeterType GetType() const override { return MeterType::PercentileTimer; } - - void Measure(std::vector*) override {} - - private: -}; - -class StatefulPercDistSum : public StatefulMeter { - public: - StatefulPercDistSum(IdPtr id, int64_t, int64_t): StatefulMeter(std::move(id)) {} - - [[nodiscard]] MeterType GetType() const override { - return MeterType::PercentileDistSummary; - } - - void Measure(std::vector*) override {} -}; - -class StatefulTimer : public TestDistribution { - public: - explicit StatefulTimer(IdPtr id): TestDistribution(std::move(id)) {} - - void Record(absl::Duration amount) { record(absl::ToDoubleSeconds(amount)); } - - void Record(std::chrono::nanoseconds amount) { Record(absl::FromChrono(amount)); } -}; - -struct stateful_meters { - using counter_t = StatefulCounter; - using ds_t = StatefulDistSum; - using gauge_t = StatefulGauge; - using max_gauge_t = StatefulMaxGauge; - using age_gauge_t = StatefulAgeGauge; - using monotonic_counter_t = StatefulMonoCounter; - using monotonic_counter_uint_t = StatefulMonoCounterUint; - using perc_timer_t = StatefulPercTimer; - using perc_ds_t = StatefulPercDistSum; - using timer_t = StatefulTimer; -}; - -} // namespace spectator diff --git a/spectator/stateful_test.cc b/spectator/stateful_test.cc deleted file mode 100644 index 2d1134a..0000000 --- a/spectator/stateful_test.cc +++ /dev/null @@ -1,35 +0,0 @@ -#include "registry.h" -#include - -namespace { - -TEST(Stateful, Counter) { - spectator::TestRegistry testRegistry; - - auto ctr = testRegistry.GetCounter( - std::make_shared("foo", spectator::Tags())); - ctr->Increment(); - - EXPECT_EQ(testRegistry.Measurements().size(), 1); - EXPECT_EQ(testRegistry.Measurements().size(), 0); -} - -TEST(Stateful, Timer) { - spectator::TestRegistry testRegistry; - testRegistry.GetTimer("name")->Record(absl::Seconds(0.5)); - EXPECT_EQ(testRegistry.Measurements().size(), 4); -} - -TEST(Stateful, Gauge) { - spectator::TestRegistry testRegistry; - testRegistry.GetGauge("name")->Set(1); - EXPECT_EQ(testRegistry.Measurements().size(), 1); -} - -TEST(Stateful, SameMeter) { - spectator::TestRegistry registry; - registry.GetCounter("foo")->Add(2); - EXPECT_EQ(registry.GetCounter("foo")->Count(), 2); -} - -} // namespace \ No newline at end of file diff --git a/spectator/stateless_meters.h b/spectator/stateless_meters.h deleted file mode 100644 index fdad8e9..0000000 --- a/spectator/stateless_meters.h +++ /dev/null @@ -1,256 +0,0 @@ -#pragma once -#include "id.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/str_format.h" -#include "absl/time/time.h" - -namespace spectator { - -namespace detail { - -#include "valid_chars.inc" - -inline std::string as_string(std::string_view v) { - return {v.data(), v.size()}; -} - -inline bool contains_non_atlas_char(const std::string& input) { - return std::any_of(input.begin(), input.end(), [](char c) { return !kAtlasChars[c]; }); -} - -inline std::string replace_invalid_characters(const std::string& input) { - if (contains_non_atlas_char(input)) { - std::string result{input}; - for (char &c : result) { - if (!kAtlasChars[c]) { - c = '_'; - } - } - return result; - } else { - return input; - } -} - -inline std::string create_prefix(const Id& id, std::string_view type_name) { - std::string res = as_string(type_name) + ":" + replace_invalid_characters(id.Name()); - for (const auto& tags : id.GetTags()) { - auto first = replace_invalid_characters(tags.first); - auto second = replace_invalid_characters(tags.second); - absl::StrAppend(&res, ",", first, "=", second); - } - - absl::StrAppend(&res, ":"); - return res; -} - -template -T restrict(T amount, T min, T max) { - auto r = amount; - if (r > max) { - r = max; - } else if (r < min) { - r = min; - } - return r; -} -} // namespace detail - -template -class StatelessMeter { - public: - StatelessMeter(IdPtr id, Pub* publisher) - : id_(std::move(id)), publisher_(publisher) { - assert(publisher_ != nullptr); - } - virtual ~StatelessMeter() = default; - std::string GetPrefix() { - if (value_prefix_.empty()) { - value_prefix_ = detail::create_prefix(*id_, Type()); - } - return value_prefix_; - } - [[nodiscard]] IdPtr MeterId() const noexcept { return id_; } - [[nodiscard]] virtual std::string_view Type() = 0; - - protected: - void send(double value) { - if (value_prefix_.empty()) { - value_prefix_ = detail::create_prefix(*id_, Type()); - } - auto msg = absl::StrFormat("%s%f", value_prefix_, value); - // remove trailing zeros and decimal points - msg.erase(msg.find_last_not_of('0') + 1, std::string::npos); - msg.erase(msg.find_last_not_of('.') + 1, std::string::npos); - publisher_->send(msg); - } - - void send_uint(uint64_t value) { - if (value_prefix_.empty()) { - value_prefix_ = detail::create_prefix(*id_, Type()); - } - auto msg = absl::StrFormat("%s%u", value_prefix_, value); - publisher_->send(msg); - } - - private: - IdPtr id_; - Pub* publisher_; - std::string value_prefix_; -}; - -template -class AgeGauge : public StatelessMeter { - public: - AgeGauge(IdPtr id, Pub* publisher) - : StatelessMeter(std::move(id), publisher) {} - void Now() noexcept { this->send(0); } - void Set(double value) noexcept { this->send(value); } - - protected: - std::string_view Type() override { return "A"; } -}; - -template -class Counter : public StatelessMeter { - public: - Counter(IdPtr id, Pub* publisher) - : StatelessMeter(std::move(id), publisher) {} - void Increment() noexcept { this->send(1); }; - void Add(double delta) noexcept { this->send(delta); } - - protected: - std::string_view Type() override { return "c"; } -}; - -template -class DistributionSummary : public StatelessMeter { - public: - DistributionSummary(IdPtr id, Pub* publisher) - : StatelessMeter(std::move(id), publisher) {} - void Record(double amount) noexcept { this->send(amount); } - - protected: - std::string_view Type() override { return "d"; } -}; - -template -class Gauge : public StatelessMeter { - public: - Gauge(IdPtr id, Pub* publisher, unsigned int ttl_seconds = 0) - : StatelessMeter(std::move(id), publisher) { - if (ttl_seconds > 0) { - type_str_ = "g," + std::to_string(ttl_seconds); - } - } - void Set(double value) noexcept { this->send(value); } - - protected: - std::string_view Type() override { return type_str_; } - - private: - std::string type_str_{"g"}; -}; - -template -class MaxGauge : public StatelessMeter { - public: - MaxGauge(IdPtr id, Pub* publisher) - : StatelessMeter(std::move(id), publisher) {} - void Update(double value) noexcept { this->send(value); } - // synonym for Update for consistency with the Gauge interface - void Set(double value) noexcept { this->send(value); } - - protected: - std::string_view Type() override { return "m"; } -}; - -template -class MonotonicCounter : public StatelessMeter { - public: - MonotonicCounter(IdPtr id, Pub* publisher) - : StatelessMeter(std::move(id), publisher) {} - void Set(double amount) noexcept { this->send(amount); } - - protected: - std::string_view Type() override { return "C"; } -}; - -template -class MonotonicCounterUint : public StatelessMeter { - public: - MonotonicCounterUint(IdPtr id, Pub* publisher) - : StatelessMeter(std::move(id), publisher) {} - void Set(uint64_t amount) noexcept { this->send_uint(amount); } - - protected: - std::string_view Type() override { return "U"; } -}; - -template -class PercentileDistributionSummary : public StatelessMeter { - public: - PercentileDistributionSummary(IdPtr id, Pub* publisher, int64_t min, - int64_t max) - : StatelessMeter(std::move(id), publisher), min_{min}, max_{max} {} - - void Record(int64_t amount) noexcept { - this->send(detail::restrict(amount, min_, max_)); - } - - protected: - std::string_view Type() override { return "D"; } - - private: - int64_t min_; - int64_t max_; -}; - -template -class PercentileTimer : public StatelessMeter { - public: - PercentileTimer(IdPtr id, Pub* publisher, absl::Duration min, - absl::Duration max) - : StatelessMeter(std::move(id), publisher), min_(min), max_(max) {} - - PercentileTimer(IdPtr id, Pub* publisher, std::chrono::nanoseconds min, - std::chrono::nanoseconds max) - : PercentileTimer(std::move(id), publisher, absl::FromChrono(min), - absl::FromChrono(max)) {} - - void Record(std::chrono::nanoseconds amount) noexcept { - Record(absl::FromChrono(amount)); - } - - void Record(absl::Duration amount) noexcept { - auto duration = detail::restrict(amount, min_, max_); - this->send(absl::ToDoubleSeconds(duration)); - } - - protected: - std::string_view Type() override { return "T"; } - - private: - absl::Duration min_; - absl::Duration max_; -}; - -template -class Timer : public StatelessMeter { - public: - Timer(IdPtr id, Pub* publisher) - : StatelessMeter(std::move(id), publisher) {} - void Record(std::chrono::nanoseconds amount) noexcept { - Record(absl::FromChrono(amount)); - } - - void Record(absl::Duration amount) noexcept { - auto secs = absl::ToDoubleSeconds(amount); - this->send(secs); - } - - protected: - std::string_view Type() override { return "t"; } -}; - -} // namespace spectator diff --git a/spectator/statelessregistry_test.cc b/spectator/statelessregistry_test.cc deleted file mode 100644 index c161bbd..0000000 --- a/spectator/statelessregistry_test.cc +++ /dev/null @@ -1,219 +0,0 @@ -#include "registry.h" -#include "test_publisher.h" -#include - -namespace { - -using spectator::base_registry; -using spectator::stateless; -using spectator::stateless_types; -using spectator::TestPublisher; - -// A stateless registry that uses a test publisher -class TestStatelessRegistry - : public base_registry>> { - public: - TestStatelessRegistry() { - state_.publisher = std::make_unique(); - } - auto SentMessages() { return state_.publisher->SentMessages(); } - void Reset() { return state_.publisher->Reset(); } - void AddExtraTag(absl::string_view k, absl::string_view v) { - extra_tags_.add(k, v); - } -}; - -TEST(StatelessRegistry, AgeGauge) { - TestStatelessRegistry r; - auto ag = r.GetAgeGauge("foo"); - auto ag2 = r.GetAgeGauge("bar", {{"id", "2"}}); - ag->Now(); - ag2->Set(100); - std::vector expected = {"A:foo:0", "A:bar,id=2:100"}; - EXPECT_EQ(r.SentMessages(), expected); -} - -TEST(StatelessRegistry, Counter) { - TestStatelessRegistry r; - auto c = r.GetCounter("foo"); - c->Increment(); - EXPECT_EQ(r.SentMessages().front(), "c:foo:1"); - - r.Reset(); - c = r.GetCounter("foo", {{"k1", "v1"}}); - c->Add(2); - EXPECT_EQ(r.SentMessages().front(), "c:foo,k1=v1:2"); -} - -TEST(StatelessRegistry, DistSummary) { - TestStatelessRegistry r; - auto ds = r.GetDistributionSummary("foo"); - ds->Record(100); - EXPECT_EQ(r.SentMessages().front(), "d:foo:100"); - - r.Reset(); - ds = r.GetDistributionSummary("bar", {{"k1", "v1"}}); - ds->Record(2); - EXPECT_EQ(r.SentMessages().front(), "d:bar,k1=v1:2"); -} - -TEST(StatelessRegistry, Gauge) { - TestStatelessRegistry r; - auto g = r.GetGauge("foo"); - auto g2 = r.GetGauge("bar", {{"id", "2"}}); - auto g3 = r.GetGaugeTTL("baz", 1); - auto g4 = r.GetGaugeTTL("quux", 2, {{"id", "2"}}); - g->Set(100); - g2->Set(101); - g3->Set(102); - g4->Set(103); - std::vector expected = {"g:foo:100", "g:bar,id=2:101", - "g,1:baz:102", "g,2:quux,id=2:103"}; - EXPECT_EQ(r.SentMessages(), expected); -} - -TEST(StatelessRegistry, MaxGauge) { - TestStatelessRegistry r; - auto m = r.GetMaxGauge("foo"); - auto m2 = r.GetMaxGauge("bar", {{"id", "2"}}); - m->Update(100); - m2->Set(101); - std::vector expected = {"m:foo:100", "m:bar,id=2:101"}; - EXPECT_EQ(r.SentMessages(), expected); -} - -TEST(StatelessRegistry, MonotonicCounter) { - TestStatelessRegistry r; - auto m = r.GetMonotonicCounter("foo"); - auto m2 = r.GetMonotonicCounter("bar", {{"id", "2"}}); - m->Set(101.1); - m2->Set(102.2); - std::vector expected = {"C:foo:101.1", "C:bar,id=2:102.2"}; - EXPECT_EQ(r.SentMessages(), expected); -} - -TEST(StatelessRegistry, MonotonicCounterUint) { - TestStatelessRegistry r; - auto m = r.GetMonotonicCounterUint("foo"); - auto m2 = r.GetMonotonicCounterUint("bar", {{"id", "2"}}); - m->Set(100); - m2->Set(101); - std::vector expected = {"U:foo:100", "U:bar,id=2:101"}; - EXPECT_EQ(r.SentMessages(), expected); -} - -TEST(StatelessRegistry, Timer) { - TestStatelessRegistry r; - auto t = r.GetTimer("foo"); - auto t2 = r.GetTimer("bar", {{"id", "2"}}); - t->Record(std::chrono::microseconds(100)); - t2->Record(absl::Seconds(0.1)); - std::vector expected = {"t:foo:0.0001", "t:bar,id=2:0.1"}; - EXPECT_EQ(r.SentMessages(), expected); -} - -TEST(StatelessRegistry, PercentileTimer) { - TestStatelessRegistry r; - auto t = r.GetPercentileTimer("foo", absl::ZeroDuration(), absl::Seconds(10)); - auto t2 = r.GetPercentileTimer("bar", absl::Milliseconds(1), absl::Seconds(1)); - - t->Record(std::chrono::microseconds(100)); - t2->Record(std::chrono::microseconds(100)); - - t->Record(absl::Seconds(5)); - t2->Record(absl::Seconds(5)); - - t->Record(std::chrono::milliseconds(100)); - t2->Record(std::chrono::milliseconds(100)); - - std::vector expected = {"T:foo:0.0001", "T:bar:0.001", - "T:foo:5", "T:bar:1", - "T:foo:0.1", "T:bar:0.1"}; - EXPECT_EQ(r.SentMessages(), expected); -} - -TEST(StatelessRegistry, PercentileDistributionSummary) { - TestStatelessRegistry r; - auto t = r.GetPercentileDistributionSummary("foo", 0, 1000); - auto t2 = r.GetPercentileDistributionSummary("bar", 10, 100); - - t->Record(5); - t2->Record(5); - - t->Record(500); - t2->Record(500); - - t->Record(50); - t2->Record(50); - - std::vector expected = {"D:foo:5", "D:bar:10", - "D:foo:500", "D:bar:100", - "D:foo:50", "D:bar:50"}; - EXPECT_EQ(r.SentMessages(), expected); -} - -template -void test_meter(T&& m1, T&& m2) { - auto id1 = m1->MeterId(); - auto id2 = m2->MeterId(); - EXPECT_EQ(*id1, *id2); - - spectator::Tags expected{{"x.spectator", "v1"}}; - EXPECT_EQ(id1->GetTags(), expected); -} - -TEST(StatelessRegistry, ExtraTags) { - using spectator::Id; - - TestStatelessRegistry r; - r.AddExtraTag("x.spectator", "v1"); - - // Counters - auto c_name = r.GetCounter("name"); - auto c_id = r.GetCounter(Id::of("name")); - test_meter(c_name, c_id); - - // DistSummaries - auto d_name = r.GetDistributionSummary("ds"); - auto d_id = r.GetDistributionSummary(Id::of("ds")); - test_meter(d_name, d_id); - - // Gauges - auto g_name = r.GetGauge("g"); - auto g_id = r.GetGauge(Id::of("g")); - test_meter(g_name, g_id); - - // MaxGauge - auto mx_name = r.GetMaxGauge("m1"); - auto mx_id = r.GetMaxGauge(Id::of("m1")); - test_meter(mx_name, mx_id); - - // MonoCounter - auto mo_name = r.GetMonotonicCounter("mo1"); - auto mo_id = r.GetMonotonicCounter(Id::of("mo1")); - test_meter(mo_name, mo_id); - - // MonoCounter Uint - auto mo_u_name = r.GetMonotonicCounterUint("mo1"); - auto mo_u_id = r.GetMonotonicCounterUint(Id::of("mo1")); - test_meter(mo_name, mo_id); - - // Pct DistSummaries - auto pds_name = r.GetPercentileDistributionSummary("pds", 0, 100); - auto pds_id = r.GetPercentileDistributionSummary(Id::of("pds"), 0, 100); - test_meter(pds_name, pds_id); - - // Pct Timers - auto pt_name = - r.GetPercentileTimer("t", absl::ZeroDuration(), absl::Seconds(1)); - auto pt_id = - r.GetPercentileTimer(Id::of("t"), absl::ZeroDuration(), absl::Seconds(1)); - test_meter(pt_name, pt_id); - - // Timers - auto t_name = r.GetTimer("t1"); - auto t_id = r.GetTimer(Id::of("t1")); - test_meter(t_name, t_id); -} - -} // namespace diff --git a/spectator/test_main.cc b/spectator/test_main.cc deleted file mode 100644 index 90247e1..0000000 --- a/spectator/test_main.cc +++ /dev/null @@ -1,8 +0,0 @@ -#include "backward.hpp" -#include - -int main(int argc, char** argv) { - ::testing::InitGoogleTest(&argc, argv); - backward::SignalHandling sh; - return RUN_ALL_TESTS(); -} diff --git a/spectator/test_publisher.h b/spectator/test_publisher.h deleted file mode 100644 index 78006d5..0000000 --- a/spectator/test_publisher.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "publisher.h" -#include -#include - -namespace spectator { -class TestPublisher { - public: - void send(std::string_view msg) { messages.emplace_back(msg); } - std::vector SentMessages() { return messages; } - void Reset() { messages.clear(); } - - private: - std::vector messages; -}; -} // namespace spectator \ No newline at end of file diff --git a/spectator/test_registry.cpp b/spectator/test_registry.cpp new file mode 100644 index 0000000..7011184 --- /dev/null +++ b/spectator/test_registry.cpp @@ -0,0 +1,354 @@ +#include +#include + +#include +#include + +#include + +TEST(RegistryTest, Close) +{ + auto config = Config(WriterConfig(WriterTypes::Memory)); + auto r = Registry(config); + auto c = r.counter("counter"); + c.Increment(); + + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + EXPECT_EQ("c:counter:1.000000\n", memoryWriter->LastLine()); + + memoryWriter->Close(); + EXPECT_TRUE(memoryWriter->IsEmpty()); +} + +TEST(RegistryTest, AgeGauge) +{ + auto config = Config(WriterConfig(WriterTypes::Memory)); + auto r = Registry(config); + auto g1 = r.age_gauge("age_gauge"); + auto g2 = r.age_gauge("age_gauge", {{"my-tags", "bar"}}); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + EXPECT_TRUE(memoryWriter->IsEmpty()); + + g1.Set(1); + EXPECT_EQ("A:age_gauge:1\n", memoryWriter->LastLine()); + + g2.Set(2); + EXPECT_EQ("A:age_gauge,my-tags=bar:2\n", memoryWriter->LastLine()); +} + +TEST(RegistryTest, AgeGaugeWithId) +{ + Config config(WriterConfig(WriterTypes::Memory), {{"extra-tags", "foo"}}); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto g = Registry::age_gauge_with_id(r.new_id("age_gauge", {{"my-tags", "bar"}})); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + g.Set(0); + EXPECT_EQ("A:age_gauge,extra-tags=foo,my-tags=bar:0\n", + ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, Counter) +{ + auto config = Config(WriterConfig(WriterTypes::Memory)); + auto r = Registry(config); + auto c1 = r.counter("counter"); + auto c2 = r.counter("counter", {{"my-tags", "bar"}}); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + EXPECT_TRUE(memoryWriter->IsEmpty()); + + c1.Increment(); + EXPECT_EQ("c:counter:1.000000\n", memoryWriter->LastLine()); + + c2.Increment(); + EXPECT_EQ("c:counter,my-tags=bar:1.000000\n", memoryWriter->LastLine()); + + c1.Increment(2); + EXPECT_EQ("c:counter:2.000000\n", memoryWriter->LastLine()); + + c2.Increment(2); + EXPECT_EQ("c:counter,my-tags=bar:2.000000\n", memoryWriter->LastLine()); + + r.counter("counter").Increment(3); + EXPECT_EQ("c:counter:3.000000\n", memoryWriter->LastLine()); +} + +TEST(RegistryTest, CounterWithId) +{ + Config config(WriterConfig(WriterTypes::Memory), {{"extra-tags", "foo"}}); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto c = Registry::counter_with_id(r.new_id("counter", {{"my-tags", "bar"}})); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + c.Increment(); + EXPECT_EQ("c:counter,extra-tags=foo,my-tags=bar:1.000000\n", + ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); + + c.Increment(2); + EXPECT_EQ("c:counter,extra-tags=foo,my-tags=bar:2.000000\n", + ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); + + r.counter("counter", {{"my-tags", "bar"}}).Increment(3); + EXPECT_EQ("c:counter,extra-tags=foo,my-tags=bar:3.000000\n", + ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, DistributionSummary) +{ + auto config = Config(WriterConfig(WriterTypes::Memory)); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto d = r.distribution_summary("distribution_summary"); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + d.Record(42); + EXPECT_EQ("d:distribution_summary:42\n", memoryWriter->LastLine()); +} + +TEST(RegistryTest, DistributionSummaryWithId) +{ + Config config(WriterConfig(WriterTypes::Memory), {{"extra-tags", "foo"}}); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto d = Registry::distribution_summary_with_id(r.new_id("distribution_summary", {{"my-tags", "bar"}})); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + d.Record(42); + EXPECT_EQ("d:distribution_summary,extra-tags=foo,my-tags=bar:42\n", + ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, Gauge) +{ + auto config = Config(WriterConfig(WriterTypes::Memory)); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto g = r.gauge("gauge"); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + g.Set(42); + EXPECT_EQ("g:gauge:42.000000\n", memoryWriter->LastLine()); +} + +TEST(RegistryTest, GaugeWithId) +{ + Config config(WriterConfig(WriterTypes::Memory), {{"extra-tags", "foo"}}); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto g = Registry::gauge_with_id(r.new_id("gauge", {{"my-tags", "bar"}})); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + g.Set(42); + EXPECT_EQ("g:gauge,extra-tags=foo,my-tags=bar:42.000000\n", + ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, GaugeWithIdWithTtlSeconds) +{ + // WriterConfig writerConfig(WriterTypes::Memory); + // Config config(writerConfig, {{"extra-tags", "foo"}}); + // auto r = Registry(config); + + // auto g = r.gauge_with_id(r.new_id("gauge", {{"my-tags", "bar"}}), 120); + // EXPECT_TRUE(memoryWriter->IsEmpty()); + + // g->Set(42); + // EXPECT_EQ("g,120:gauge,extra-tags=foo,my-tags=bar:42", memoryWriter->LastLine()); +} + +// TEST_F(RegistryTest, GaugeWithTtlSeconds) { +// WriterConfig writerConfig(WriterTypes::Memory); +// Config config(writerConfig); +// auto r = Registry(config); + +// auto g = r.gauge("gauge", 120); +// EXPECT_TRUE(memoryWriter->IsEmpty()); + +// g.Set(42); +// EXPECT_EQ("g,120:gauge:42", memoryWriter->LastLine()); +// } + +TEST(RegistryTest, MaxGauge) +{ + auto config = Config(WriterConfig(WriterTypes::Memory)); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto g = r.max_gauge("max_gauge"); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + g.Set(42); + EXPECT_EQ("m:max_gauge:42.000000\n", memoryWriter->LastLine()); +} + +TEST(RegistryTest, MaxGaugeWithId) +{ + Config config(WriterConfig(WriterTypes::Memory), {{"extra-tags", "foo"}}); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto g = Registry::max_gauge_with_id(r.new_id("max_gauge", {{"my-tags", "bar"}})); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + g.Set(42); + EXPECT_EQ("m:max_gauge,extra-tags=foo,my-tags=bar:42.000000\n", + ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, MonotonicCounter) +{ + auto config = Config(WriterConfig(WriterTypes::Memory)); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto c = r.monotonic_counter("monotonic_counter"); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + c.Set(42); + EXPECT_EQ("C:monotonic_counter:42.000000\n", memoryWriter->LastLine()); +} + +TEST(RegistryTest, MonotonicCounterWithId) +{ + Config config(WriterConfig(WriterTypes::Memory), {{"extra-tags", "foo"}}); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto c = Registry::monotonic_counter_with_id(r.new_id("monotonic_counter", {{"my-tags", "bar"}})); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + c.Set(42); + EXPECT_EQ("C:monotonic_counter,extra-tags=foo,my-tags=bar:42.000000\n", + ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, MonotonicCounterUint) +{ + auto config = Config(WriterConfig(WriterTypes::Memory)); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto c = r.monotonic_counter_uint("monotonic_counter_uint"); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + c.Set(42); + EXPECT_EQ("U:monotonic_counter_uint:42\n", memoryWriter->LastLine()); +} + +TEST(RegistryTest, MonotonicCounterUintWithId) +{ + Config config(WriterConfig(WriterTypes::Memory), {{"extra-tags", "foo"}}); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto c = Registry::monotonic_counter_uint_with_id(r.new_id("monotonic_counter_uint", {{"my-tags", "bar"}})); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + c.Set(42); + EXPECT_EQ("U:monotonic_counter_uint,extra-tags=foo,my-tags=bar:42\n", + ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, NewId) +{ + auto config1 = Config(WriterConfig(WriterTypes::Memory)); + auto r1 = Registry(config1); + auto id1 = r1.new_id("id"); + EXPECT_EQ("MeterId(name=id, tags={})", id1.to_string()); + + Config config2(WriterConfig(WriterTypes::Memory), {{"extra-tags", "foo"}}); + auto r2 = Registry(config2); + auto id2 = r2.new_id("id"); + EXPECT_EQ("MeterId(name=id, tags={'extra-tags': 'foo'})", id2.to_string()); +} + +TEST(RegistryTest, PctDistributionSummary) +{ + auto config = Config(WriterConfig(WriterTypes::Memory)); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto d = r.pct_distribution_summary("pct_distribution_summary"); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + d.Record(42); + EXPECT_EQ("D:pct_distribution_summary:42\n", memoryWriter->LastLine()); +} + +TEST(RegistryTest, PctDistributionSummaryWithId) +{ + Config config(WriterConfig(WriterTypes::Memory), {{"extra-tags", "foo"}}); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto d = Registry::pct_distribution_summary_with_id(r.new_id("pct_distribution_summary", {{"my-tags", "bar"}})); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + d.Record(42); + EXPECT_EQ("D:pct_distribution_summary,extra-tags=foo,my-tags=bar:42\n", + ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, PctTimer) +{ + auto config = Config(WriterConfig(WriterTypes::Memory)); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto t = r.pct_timer("pct_timer"); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + t.Record(42); + EXPECT_EQ("T:pct_timer:42.000000\n", memoryWriter->LastLine()); +} + +TEST(RegistryTest, PctTimerWithId) +{ + Config config(WriterConfig(WriterTypes::Memory), {{"extra-tags", "foo"}}); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto t = Registry::pct_timer_with_id(r.new_id("pct_timer", {{"my-tags", "bar"}})); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + t.Record(42); + EXPECT_EQ("T:pct_timer,extra-tags=foo,my-tags=bar:42.000000\n", + ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, Timer) +{ + auto config = Config(WriterConfig(WriterTypes::Memory)); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto t = r.timer("timer"); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + t.Record(42); + EXPECT_EQ("t:timer:42.000000\n", memoryWriter->LastLine()); +} + +TEST(RegistryTest, TimerWithId) +{ + Config config(WriterConfig(WriterTypes::Memory), {{"extra-tags", "foo"}}); + auto r = Registry(config); + auto memoryWriter = static_cast(WriterTestHelper::GetImpl()); + + auto t = Registry::timer_with_id(r.new_id("timer", {{"my-tags", "bar"}})); + EXPECT_TRUE(memoryWriter->IsEmpty()); + + t.Record(42); + EXPECT_EQ("t:timer,extra-tags=foo,my-tags=bar:42.000000\n", + ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} \ No newline at end of file diff --git a/spectator/test_server.h b/spectator/test_server.h deleted file mode 100644 index 1c3b29e..0000000 --- a/spectator/test_server.h +++ /dev/null @@ -1,68 +0,0 @@ -#include -#include -#include -#include "logger.h" - -template -class TestServer { - public: - explicit TestServer(typename T::endpoint endpoint) - : socket_{context_, endpoint} {} - void Start() { - start_receiving(); - runner = std::thread([this]() { context_.run(); }); - } - - void Stop() { - spectator::DefaultLogger()->info("Stopping test server"); - context_.stop(); - runner.join(); - } - - ~TestServer() { - if (runner.joinable()) { - spectator::DefaultLogger()->info( - "Test server runner was not stopped properly"); - Stop(); - } - } - - void Reset() { msgs.clear(); } - - [[nodiscard]] std::vector GetMessages() const { return msgs; } - - protected: - std::thread runner; - asio::io_context context_{}; - typename T::socket socket_; - char buf[32768]; - std::vector msgs; - - void start_receiving() { - socket_.async_receive( - asio::buffer(buf, sizeof buf), - [this](const std::error_code& err, size_t bytes_transferred) { - assert(!err); - msgs.emplace_back(std::string(buf, bytes_transferred)); - start_receiving(); - }); - } -}; - -class TestUdpServer : public TestServer { - public: - TestUdpServer() - : TestServer{asio::ip::udp::endpoint{asio::ip::udp::v6(), 0}}, - port_{socket_.local_endpoint().port()} {} - - [[nodiscard]] int GetPort() const { return port_; } - - private: - int port_; -}; - -class TestUnixServer : public TestServer { - public: - explicit TestUnixServer(std::string_view path) - : TestServer{asio::local::datagram_protocol::endpoint{path}} {} -}; diff --git a/spectator/timer_test.cc b/spectator/timer_test.cc deleted file mode 100644 index 0f380e5..0000000 --- a/spectator/timer_test.cc +++ /dev/null @@ -1,32 +0,0 @@ -#include "stateless_meters.h" -#include "test_publisher.h" -#include - -namespace { -using spectator::Id; -using spectator::Tags; -using spectator::TestPublisher; -using spectator::Timer; - -TEST(Timer, Record) { - TestPublisher publisher; - auto id = std::make_shared("t.name", Tags{}); - auto id2 = std::make_shared("t2", Tags{{"key", "val"}}); - Timer t{id, &publisher}; - Timer t2{id2, &publisher}; - t.Record(std::chrono::milliseconds(1)); - t2.Record(absl::Seconds(0.1)); - t2.Record(absl::Microseconds(500)); - std::vector expected = {"t:t.name:0.001", "t:t2,key=val:0.1", "t:t2,key=val:0.0005"}; - EXPECT_EQ(publisher.SentMessages(), expected); -} - -TEST(Timer, InvalidTags) { - TestPublisher publisher; - // test with a single tag, because tags order is not guaranteed in a flat_hash_map - auto id = std::make_shared("timer`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", - Tags{{"tag1,:=", "value1,:="}}); - Timer t{id, &publisher}; - EXPECT_EQ("t:timer______^____-_~______________.___foo,tag1___=value1___:", t.GetPrefix()); -} -} // namespace diff --git a/spectator/util.h b/spectator/util.h deleted file mode 100644 index f971c31..0000000 --- a/spectator/util.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -namespace spectator { - -template -T restrict(T amount, T min, T max) { - auto r = amount; - if (r > max) { - r = max; - } else if (r < min) { - r = min; - } - return r; -} - -} // namespace spectator diff --git a/tools/gen_valid_chars.cc b/tools/gen_valid_chars.cc deleted file mode 100644 index ab046f0..0000000 --- a/tools/gen_valid_chars.cc +++ /dev/null @@ -1,48 +0,0 @@ -// generate the atlas valid charsets - -#include -#include - -void dump_array(std::ostream& os, const std::string& name, const std::array& chars) { - os << "static constexpr std::array " << name << " = {{"; - - os << chars[0]; - for (auto i = 1u; i < chars.size(); ++i) { - os << ", " << chars[i]; - } - - os << "}};\n"; -} - -int main(int argc, char* argv[]) { - std::ofstream of; - if (argc > 1) { - of.open(argv[1]); - } else { - of.open("/dev/stdout"); - } - - // default false - std::array charsAllowed{}; - for (int i = 0; i < 256; ++i) { - charsAllowed[i] = false; - } - - // configure allowed characters - charsAllowed['.'] = true; - charsAllowed['-'] = true; - - for (auto ch = '0'; ch <= '9'; ++ch) { - charsAllowed[ch] = true; - } - for (auto ch = 'a'; ch <= 'z'; ++ch) { - charsAllowed[ch] = true; - } - for (auto ch = 'A'; ch <= 'Z'; ++ch) { - charsAllowed[ch] = true; - } - charsAllowed['~'] = true; - charsAllowed['^'] = true; - - dump_array(of, "kAtlasChars", charsAllowed); -}