diff --git a/.clang-format b/.clang-format deleted file mode 100644 index 865f732..0000000 --- a/.clang-format +++ /dev/null @@ -1,88 +0,0 @@ ---- -Language: Cpp -# BasedOnStyle: Google -AccessModifierOffset: -1 -AlignAfterOpenBracket: Align -AlignConsecutiveAssignments: None -AlignConsecutiveDeclarations: None -AlignOperands: true -AlignTrailingComments: true -AllowAllParametersOfDeclarationOnNextLine: true -AllowShortBlocksOnASingleLine: Never -AllowShortCaseLabelsOnASingleLine: false -AllowShortFunctionsOnASingleLine: All -AllowShortIfStatementsOnASingleLine: true -AllowShortLoopsOnASingleLine: true -AlwaysBreakAfterDefinitionReturnType: None -AlwaysBreakAfterReturnType: None -AlwaysBreakBeforeMultilineStrings: true -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 - IndentBraces: false -BreakBeforeBinaryOperators: None -BreakBeforeBraces: Attach -BreakBeforeTernaryOperators: true -BreakConstructorInitializersBeforeComma: false -ColumnLimit: 100 -CommentPragmas: '^ IWYU pragma:' -ConstructorInitializerAllOnOneLineOrOnePerLine: true -ConstructorInitializerIndentWidth: 4 -ContinuationIndentWidth: 4 -Cpp11BracedListStyle: true -DerivePointerAlignment: false -DisableFormat: false -ExperimentalAutoDetectBinPacking: false -ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] -IncludeCategories: - - Regex: '^<.*\.h>' - Priority: 1 - - Regex: '^<.*' - Priority: 2 - - Regex: '.*' - Priority: 3 -IndentCaseLabels: true -IndentWidth: 2 -IndentWrappedFunctionNames: false -KeepEmptyLinesAtTheStartOfBlocks: false -MacroBlockBegin: '' -MacroBlockEnd: '' -MaxEmptyLinesToKeep: 1 -NamespaceIndentation: None -ObjCBlockIndentWidth: 2 -ObjCSpaceAfterProperty: false -ObjCSpaceBeforeProtocolList: false -PenaltyBreakBeforeFirstCallParameter: 1 -PenaltyBreakComment: 300 -PenaltyBreakFirstLessLess: 120 -PenaltyBreakString: 1000 -PenaltyExcessCharacter: 1000000 -PenaltyReturnTypeOnItsOwnLine: 200 -PointerAlignment: Left -ReflowComments: true -SortIncludes: Never -SpaceAfterCStyleCast: false -SpaceBeforeAssignmentOperators: true -SpaceBeforeParens: ControlStatements -SpaceInEmptyParentheses: false -SpacesBeforeTrailingComments: 2 -SpacesInAngles: false -SpacesInContainerLiterals: true -SpacesInCStyleCastParentheses: false -SpacesInParentheses: false -SpacesInSquareBrackets: false -Standard: Auto -TabWidth: 8 -UseTab: Never -... 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..b193668 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ .DS_Store .idea/ +.vscode/ CMakeUserPresets.json cmake-build/ 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..84a356f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,70 +1,26 @@ -cmake_minimum_required(VERSION 3.23) - -project(spectator-cpp) +# filepath: /home/ebadeaux/Saturday/spec2-cpp/CMakeLists.txt +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 -) +# Build tests if enabled +if(BUILD_TESTS) + enable_testing() +endif() -#-- generator tools -add_executable(gen_valid_chars "tools/gen_valid_chars.cc") +# Add subdirectories +add_subdirectory(libs) +add_subdirectory(spectator) -#-- 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 -) diff --git a/Dockerfiles/README.md b/Dockerfiles/README.md new file mode 100644 index 0000000..43e97c5 --- /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 spectatord-cpp-image +./build.sh +``` + +## Windows + +##### Warning: + +- Start `Docker` before opening `Powershell` + +```shell +docker build -t spectatord-cpp-image -f Dockerfiles/Ubuntu.Dockerfile . +docker run -it spectatord-cpp-image +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..8d957a9 --- /dev/null +++ b/Dockerfiles/Ubuntu.Dockerfile @@ -0,0 +1,25 @@ +# 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 + +# 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..d5d3791 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 @@ -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..478e9be --- /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..87d5bba --- /dev/null +++ b/libs/config/CMakeLists.txt @@ -0,0 +1,24 @@ +add_library(spectator-config + src/config.cpp +) + +target_include_directories(spectator-config + PUBLIC ${CMAKE_SOURCE_DIR} +) + +add_executable(config-tests + test/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..db69325 --- /dev/null +++ b/libs/config/README.md @@ -0,0 +1,69 @@ +# Config Library + +## Overview + +The `Config` class serves as the configuration object for the Registry class, which manages metrics for the spectatorD system. It defines how and where metrics are written and what additional tags are applied to them. + +## Usage + +The `Config` class provides two constructors: + +```cpp +// Constructor 1: Specify output location as string +Config(const std::string &location = "udp", + const std::unordered_map &extra_tags = {}); + +// Constructor 2: Specify writer type directly (recommended) +Config(WriterType type, + const std::unordered_map &extra_tags = {}); + +// Examples: + +// Example 1: Create a default config (UDP output) +auto defaultConfig = Config(); + +// Example 2: Config with memory storage and custom tags +auto memoryConfig = Config("memory", { + {"app", "myService"}, + {"zone", "us-west-2"} +}); + +// Example 3: Config with direct writer type +auto stdoutConfig = Config(WriterTypes::Memory, { + {"env", "production"} +}); + +// Example 4: Config writing to a specific file +auto fileConfig = Config("file:///var/log/metrics.log"); + +// Example 5: Config sending to specific UDP endpoint +auto customUdpConfig = Config("udp://metrics-collector.example.com:8125"); +``` + +### When to Use Each Constructor + +- Use the first constructor when you need to specify a custom output location. +- Use the second constructor (preferred) when working with standard writer types. + +### Output Locations + +Valid output locations include: +- `none`: Disable output +- `memory`: Store metrics in memory +- `stdout`: Write to standard output +- `stderr`: Write to standard error +- `udp`: Send metrics over UDP (default) +- `unix`: Use Unix domain sockets +- `file://path`: Write to a file +- `udp://host:port`: Send to specific UDP endpoint +- `unix://path`: Use specific Unix socket + +### Environment Variables + +- `SPECTATOR_OUTPUT_LOCATION`: Override the configured output location +- `TITUS_CONTAINER_NAME`: Set the `nf.container` tag automatically +- `TITUS_PROCESS_NAME`: Set the `nf.process` tag automatically + +## Extra Tags + +Extra tags are additional key-value pairs attached to all metrics. They can be specified during initialization and will be merged with environment-derived tags. \ No newline at end of file diff --git a/libs/config/include/config.h b/libs/config/include/config.h new file mode 100644 index 0000000..27c5a61 --- /dev/null +++ b/libs/config/include/config.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include + +class Config +{ + public: + + Config(WriterConfig writerConfig, const std::unordered_map &extra_tags = {}); + + // Rule of 5 + ~Config() = default; // Destructor + Config(const Config &other) = default; // Copy constructor + Config &operator=(const Config &other) = delete; // Copy assignment operator + Config(Config &&other) noexcept = delete; // Move constructor + Config &operator=(Config &&other) noexcept = delete; // Move assignment operator + + const std::string &GetLocation() const { return m_writerConfig.GetLocation(); } + + const std::unordered_map &GetExtraTags() const{ return m_extraTags;} + + const WriterType &GetWriterType() const { return m_writerConfig.GetType();} + + private: + std::unordered_map CalculateTags(const std::unordered_map &tags); + + std::unordered_map m_extraTags; + WriterConfig m_writerConfig; +}; \ No newline at end of file diff --git a/libs/config/src/config.cpp b/libs/config/src/config.cpp new file mode 100644 index 0000000..964562f --- /dev/null +++ b/libs/config/src/config.cpp @@ -0,0 +1,39 @@ +#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"; +}; + +Config::Config(WriterConfig writerConfig, const std::unordered_map &extra_tags) + : m_extraTags(CalculateTags(extra_tags)), m_writerConfig(writerConfig) {} + +std::unordered_map Config::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) + { + valid_tags[ConfigConstants::container] = container_name; + } + + if (process_name) + { + valid_tags[ConfigConstants::process] = process_name; + } + + for (const auto &kv : tags) + { + if (!kv.first.empty() && !kv.second.empty()) + { + valid_tags[kv.first] = kv.second; + } + } + + return valid_tags; +} diff --git a/libs/config/test/test_config.cpp b/libs/config/test/test_config.cpp new file mode 100644 index 0000000..2f1f221 --- /dev/null +++ b/libs/config/test/test_config.cpp @@ -0,0 +1,209 @@ +#include +#include + +#include + +// Helper to temporarily set an environment variable for testing +class EnvVarSetter +{ + public: + EnvVarSetter(const std::string &name, const std::string &value) : m_name(name) + { + // Store original value (might be nullptr) + m_originalValue = std::getenv(name.c_str()); + + // Set the new value + setenv(name.c_str(), value.c_str(), 1); + } + + ~EnvVarSetter() + { + // Restore original state + if (m_originalValue) + setenv(m_name.c_str(), m_originalValue, 1); + else + unsetenv(m_name.c_str()); + } + + private: + std::string m_name; + const char *m_originalValue; +}; + +// Helper to temporarily unset an environment variable +class EnvVarUnset +{ + public: + EnvVarUnset(const std::string &name) : m_name(name) + { + // Store original value (might be nullptr) + m_originalValue = std::getenv(name.c_str()); + + // Always unset regardless of whether it was set before + unsetenv(name.c_str()); + } + + ~EnvVarUnset() + { + // Only restore if there was an original value + if (m_originalValue) + setenv(m_name.c_str(), m_originalValue, 1); + } + + private: + std::string m_name; + const char *m_originalValue; +}; + +class ConfigTest : public ::testing::Test +{ + protected: + void SetUp() override + { + // Ensure environment variables are unset before each test + unsetenv("TITUS_CONTAINER_NAME"); + unsetenv("TITUS_PROCESS_NAME"); + } +}; + +// Test initialization with different writer configs +TEST_F(ConfigTest, WriterConfigInitialization) +{ + // Test with memory writer + { + WriterConfig writerConfig(WriterTypes::Memory); + Config config(writerConfig); + + EXPECT_EQ(config.GetLocation(), ""); + 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.GetLocation(), DefaultLocations::UDP); + EXPECT_EQ(config.GetWriterType(), WriterType::UDP); + } +} + +// 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 + { + EnvVarUnset container("TITUS_CONTAINER_NAME"); + EnvVarUnset process("TITUS_PROCESS_NAME"); + + Config config(writerConfig); + EXPECT_TRUE(config.GetExtraTags().empty()); + } + + // With container name + { + EnvVarSetter container("TITUS_CONTAINER_NAME", "test-container"); + EnvVarUnset process("TITUS_PROCESS_NAME"); + + Config config(writerConfig); + + EXPECT_EQ(config.GetExtraTags().size(), 1); + EXPECT_EQ(config.GetExtraTags().at("nf.container"), "test-container"); + } + + // With process name + { + EnvVarUnset container("TITUS_CONTAINER_NAME"); + EnvVarSetter process("TITUS_PROCESS_NAME", "test-process"); + + Config config(writerConfig); + + EXPECT_EQ(config.GetExtraTags().size(), 1); + EXPECT_EQ(config.GetExtraTags().at("nf.process"), "test-process"); + } + + // With both environment variables + { + EnvVarSetter container("TITUS_CONTAINER_NAME", "test-container"); + EnvVarSetter process("TITUS_PROCESS_NAME", "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 + { + EnvVarSetter container("TITUS_CONTAINER_NAME", "test-container"); + EnvVarSetter process("TITUS_PROCESS_NAME", "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 + { + EnvVarSetter container("TITUS_CONTAINER_NAME", "test-container"); + + 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"); + } +} 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..36ff640 --- /dev/null +++ b/libs/logger/logger.h @@ -0,0 +1,93 @@ +#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 + + 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) + { + GetLogger()->debug(msg); + } + + static void info(const std::string &msg) + { + GetLogger()->info(msg); + } + + static void warn(const std::string &msg) + { + GetLogger()->warn(msg); + } + + static void error(const std::string &msg) + { + GetLogger()->error(msg); + } + + template static void debug(fmt::format_string fmt, Args &&...args) + { + GetLogger()->debug(fmt, std::forward(args)...); + } + + template static void info(fmt::format_string fmt, Args &&...args) + { + GetLogger()->info(fmt, std::forward(args)...); + } + + template static void warn(fmt::format_string fmt, Args &&...args) + { + GetLogger()->warn(fmt, std::forward(args)...); + } + + template static void error(fmt::format_string fmt, Args &&...args) + { + GetLogger()->error(fmt, std::forward(args)...); + } +}; \ No newline at end of file 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..255cd06 --- /dev/null +++ b/libs/meter/meter_id/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library(spectator-meter-id + src/meter_id.cpp +) + +target_include_directories(spectator-meter-id + PUBLIC + ${CMAKE_SOURCE_DIR} +) + +add_executable(MeterID-test + 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/include/meter_id.h b/libs/meter/meter_id/include/meter_id.h new file mode 100644 index 0000000..c8f4fbf --- /dev/null +++ b/libs/meter/meter_id/include/meter_id.h @@ -0,0 +1,48 @@ +#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::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; + + std::string spectatord_id; + + bool operator==(const MeterId &other) const; + + std::string to_string() const; + + std::string GetSpectatordId() const + { + return ToSpectatorId(m_name, m_tags); + } + + private: + std::string RepleaceInvalidChars(const std::string &s) const; + std::string ToSpectatorId(const std::string &name, const std::unordered_map &tags) const; + + static const std::regex INVALID_CHARS; + std::string m_name; + std::unordered_map m_tags; +}; + +namespace std +{ +template <> struct hash +{ + size_t operator()(const MeterId &id) const; +}; +} // namespace std \ No newline at end of file diff --git a/libs/meter/meter_id/src/meter_id.cpp b/libs/meter/meter_id/src/meter_id.cpp new file mode 100644 index 0000000..cfec332 --- /dev/null +++ b/libs/meter/meter_id/src/meter_id.cpp @@ -0,0 +1,108 @@ +#include + +#include +#include + +// Define the static member +const std::regex MeterId::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 (key.empty() == false && value.empty() == false) + { + validTags[key] = value; + } + } + + return validTags; +} + +MeterId::MeterId(const std::string &name, const std::unordered_map &tags) + : m_name(name), m_tags(ValidateTags(tags)) +{ + spectatord_id = ToSpectatorId(m_name, m_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 &pair : additional_tags) + { + new_tags[pair.first] = pair.second; + } + 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 &pair : m_tags) + { + if (!first) + { + ss << ", "; + } + ss << "'" << pair.first << "': '" << pair.second << "'"; + first = false; + } + ss << "})"; + return ss.str(); +} + +std::string MeterId::RepleaceInvalidChars(const std::string &s) const +{ + return std::regex_replace(s, INVALID_CHARS, "_"); +} + +std::string MeterId::ToSpectatorId(const std::string &name, const std::unordered_map &tags) const +{ + std::ostringstream ss; + ss << RepleaceInvalidChars(name); + if (!tags.empty()) + { + for (const auto &tag : tags) + { + ss << "," << RepleaceInvalidChars(tag.first) << "=" << RepleaceInvalidChars(tag.second); + } + } + return ss.str(); +} + +// Implementation of the hash function for MeterId +size_t std::hash::operator()(const MeterId &id) const +{ + // Hash the name first + size_t name_hash = std::hash{}(id.GetName()); + + // Hash the tags + size_t tags_hash = 0; + for (const auto &tag : id.GetTags()) + { + // Combine key and value hashes + size_t pair_hash = std::hash{}(tag.first) ^ (std::hash{}(tag.second) << 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/test/test_meter_id.cpp b/libs/meter/meter_id/test/test_meter_id.cpp new file mode 100644 index 0000000..9638ea4 --- /dev/null +++ b/libs/meter/meter_id/test/test_meter_id.cpp @@ -0,0 +1,109 @@ +#include + +#include + +TEST(MeterIdTest, EqualsSameName) +{ + MeterId id1("foo"); + MeterId id2("foo"); + EXPECT_EQ(id1, id2); +} + +TEST(MeterIdTest, EqualsSameTags) +{ + MeterId id1("foo", {{"a", "1"}, {"b", "2"}, {"c", "3"}}); + MeterId id2("foo", {{"c", "3"}, {"b", "2"}, {"a", "1"}}); + EXPECT_EQ(id1, id2); +} + +TEST(MeterIdTest, HashSameTags) +{ + MeterId id1("foo", {{"a", "1"}, {"b", "2"}, {"c", "3"}}); + MeterId id2("foo", {{"c", "3"}, {"b", "2"}, {"a", "1"}}); + EXPECT_EQ(std::hash{}(id1), std::hash{}(id2)); +} + +TEST(MeterIdTest, IllegalCharsAreReplaced) +{ + MeterId id("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo"); + EXPECT_EQ("test______^____-_~______________.___foo", id.GetSpectatordId()); +} + +TEST(MeterIdTest, LookupTags) +{ + MeterId id1("foo", {{"a", "1"}, {"b", "2"}, {"c", "3"}}); + MeterId id2("foo", {{"c", "3"}, {"b", "2"}, {"a", "1"}}); + std::unordered_map d; + d[id1] = "test"; + EXPECT_EQ("test", d[id2]); +} + +TEST(MeterIdTest, Name) +{ + 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) +{ + MeterId id1("foo", {{"a", "1"}}); + 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..fc478ae --- /dev/null +++ b/libs/meter/meter_types/include/age_gauge.h @@ -0,0 +1,29 @@ +#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->m_meterTypeSymbol + FIELD_SEPARATOR + this->m_id.spectatord_id + FIELD_SEPARATOR + "0"; + Writer::GetInstance().Write(line); + } + + void Set(const int &seconds) + { + auto line = this->m_meterTypeSymbol + FIELD_SEPARATOR + this->m_id.spectatord_id + FIELD_SEPARATOR + std::to_string(seconds); + Writer::GetInstance().Write(line); + } +}; diff --git a/libs/meter/meter_types/include/base/meter.h b/libs/meter/meter_types/include/base/meter.h new file mode 100644 index 0000000..4401d74 --- /dev/null +++ b/libs/meter/meter_types/include/base/meter.h @@ -0,0 +1,29 @@ +#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; + } + + protected: + MeterId m_id; + std::string m_meterTypeSymbol; +}; diff --git a/libs/meter/meter_types/include/counter.h b/libs/meter/meter_types/include/counter.h new file mode 100644 index 0000000..cec6209 --- /dev/null +++ b/libs/meter/meter_types/include/counter.h @@ -0,0 +1,26 @@ +#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->m_meterTypeSymbol + FIELD_SEPARATOR + this->m_id.spectatord_id + FIELD_SEPARATOR + std::to_string(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..05fd5a2 --- /dev/null +++ b/libs/meter/meter_types/include/dist_summary.h @@ -0,0 +1,26 @@ +#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->m_meterTypeSymbol + FIELD_SEPARATOR + this->m_id.spectatord_id + FIELD_SEPARATOR + std::to_string(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..d29a96c --- /dev/null +++ b/libs/meter/meter_types/include/gauge.h @@ -0,0 +1,26 @@ +#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->m_meterTypeSymbol + FIELD_SEPARATOR + this->m_id.spectatord_id + FIELD_SEPARATOR + std::to_string(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..5d84319 --- /dev/null +++ b/libs/meter/meter_types/include/max_gauge.h @@ -0,0 +1,23 @@ +#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->m_meterTypeSymbol + FIELD_SEPARATOR + this->m_id.spectatord_id + FIELD_SEPARATOR + std::to_string(value); + Writer::GetInstance().Write(line); + } +}; \ No newline at end of file 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..86a12a9 --- /dev/null +++ b/libs/meter/meter_types/include/monotonic_counter.h @@ -0,0 +1,23 @@ +#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->m_meterTypeSymbol + FIELD_SEPARATOR + this->m_id.spectatord_id + FIELD_SEPARATOR + std::to_string(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..48b51cb --- /dev/null +++ b/libs/meter/meter_types/include/monotonic_counter_uint.h @@ -0,0 +1,23 @@ +#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->m_meterTypeSymbol + FIELD_SEPARATOR + this->m_id.spectatord_id + FIELD_SEPARATOR + std::to_string(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..10147ab --- /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->m_meterTypeSymbol + FIELD_SEPARATOR + this->m_id.spectatord_id + FIELD_SEPARATOR + std::to_string(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..379f564 --- /dev/null +++ b/libs/meter/meter_types/include/percentile_timer.h @@ -0,0 +1,26 @@ +#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->m_meterTypeSymbol + FIELD_SEPARATOR + this->m_id.spectatord_id + FIELD_SEPARATOR + std::to_string(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..3ae106d --- /dev/null +++ b/libs/meter/meter_types/include/timer.h @@ -0,0 +1,26 @@ +#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->m_meterTypeSymbol + FIELD_SEPARATOR + this->m_id.spectatord_id + FIELD_SEPARATOR + std::to_string(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..56198b1 --- /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); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + AgeGauge g(tid); + EXPECT_TRUE(writer->IsEmpty()); + g.Now(); + EXPECT_EQ("A:age_gauge:0", writer->LastLine()); +} + +TEST_F(AgeGaugeTest, Set) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + AgeGauge g(tid); + EXPECT_TRUE(writer->IsEmpty()); + g.Set(10); + EXPECT_EQ("A:age_gauge:10", 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..c31948c --- /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); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + + Counter c(tid); + EXPECT_TRUE(writer->IsEmpty()); + c.Increment(); + EXPECT_EQ("c:counter:1.000000", writer->LastLine()); + c.Increment(2); + EXPECT_EQ("c:counter:2.000000", writer->LastLine()); +} + +TEST_F(CounterTest, incrementNegative) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + 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..21cccae --- /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); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + DistributionSummary ds(tid); + EXPECT_TRUE(writer->IsEmpty()); + + ds.Record(42); + EXPECT_EQ("d:dist_summary:42", writer->LastLine()); +} + +TEST_F(DistSummaryTest, recordNegative) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + 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); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + DistributionSummary ds(tid); + EXPECT_TRUE(writer->IsEmpty()); + + ds.Record(0); + EXPECT_EQ("d:dist_summary:0", 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..d0c5d4e --- /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); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + Gauge g(tid); + EXPECT_TRUE(writer->IsEmpty()); + g.Set(1); + EXPECT_EQ("g:gauge:1.000000", writer->LastLine()); +} + +TEST_F(GaugeTest, TTL) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + Gauge g(tid, 10); + EXPECT_TRUE(writer->IsEmpty()); + g.Set(42); + EXPECT_EQ("g,10:gauge:42.000000", 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..1598d64 --- /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); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + MaxGauge g(tid); + EXPECT_TRUE(writer->IsEmpty()); + g.Set(0); + EXPECT_EQ("m:max_gauge:0.000000", 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..7e19de6 --- /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); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + MonotonicCounter mc(tid); + EXPECT_TRUE(writer->IsEmpty()); + mc.Set(1); + EXPECT_EQ("C:monotonic_counter:1.000000", writer->LastLine()); +} + +TEST_F(MonotonicCounterTest, SetNegativeValue) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + MonotonicCounter mc(tid); + EXPECT_TRUE(writer->IsEmpty()); + mc.Set(-1); + EXPECT_EQ("C:monotonic_counter:-1.000000", 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..61580f4 --- /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); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + MonotonicCounterUint mc(tid); + EXPECT_TRUE(writer->IsEmpty()); + + mc.Set(1); + EXPECT_EQ("U:monotonic_counter_uint:1", writer->LastLine()); +} + +TEST_F(MonoCounterTest, setNegative) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + MonotonicCounterUint mc(tid); + EXPECT_TRUE(writer->IsEmpty()); + + mc.Set(-1); + EXPECT_EQ("U:monotonic_counter_uint:18446744073709551615", 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..9235c2f --- /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); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + PercentileDistributionSummary pds(tid); + EXPECT_TRUE(writer->IsEmpty()); + + pds.Record(42); + EXPECT_EQ("D:percentile_dist_summary:42", writer->LastLine()); +} + +TEST_F(PercentileDistSummaryTest, recordNegative) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + 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); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + PercentileDistributionSummary pds(tid); + EXPECT_TRUE(writer->IsEmpty()); + + pds.Record(0); + EXPECT_EQ("D:percentile_dist_summary:0", 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..5616ce0 --- /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); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + PercentileTimer pt(tid); + EXPECT_TRUE(writer->IsEmpty()); + + pt.Record(42); + EXPECT_EQ("T:percentile_timer:42.000000", writer->LastLine()); +} + +TEST_F(PercentileTimerTest, recordNegative) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + 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); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + PercentileTimer pt(tid); + EXPECT_TRUE(writer->IsEmpty()); + + pt.Record(0); + EXPECT_EQ("T:percentile_timer:0.000000", 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..df885d2 --- /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); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + Timer t(tid); + EXPECT_TRUE(writer->IsEmpty()); + + t.Record(42); + EXPECT_EQ("t:timer:42.000000", writer->LastLine()); +} + +TEST_F(TimerTest, recordNegative) +{ + WriterTestHelper::InitializeWriter(WriterType::Memory); + 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); + auto *writer = dynamic_cast(WriterTestHelper::GetImpl()); + Timer t(tid); + EXPECT_TRUE(writer->IsEmpty()); + + t.Record(0); + EXPECT_EQ("t:timer:0.000000", 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..5be62b3 --- /dev/null +++ b/libs/utils/include/singleton.h @@ -0,0 +1,22 @@ +#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..238b70e --- /dev/null +++ b/libs/utils/include/util.h @@ -0,0 +1,50 @@ +#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; + } + + 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); \ 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..7d3723c --- /dev/null +++ b/libs/utils/src/util.cpp @@ -0,0 +1,57 @@ +#include + +#include +#include + +// Utility: split a string by a delimiter +std::vector split(const std::string &str, 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{}; + + 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}; +} \ 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/writer_config/CMakeLists.txt b/libs/writer/writer_config/CMakeLists.txt new file mode 100644 index 0000000..58d1d53 --- /dev/null +++ b/libs/writer/writer_config/CMakeLists.txt @@ -0,0 +1,29 @@ +add_library(spectator-writer-config STATIC + src/writer_config.cpp + include/writer_config.h +) + +target_include_directories(spectator-writer-config + PUBLIC + ${CMAKE_SOURCE_DIR} +) + + +target_link_libraries(spectator-writer-config + PUBLIC + spectator-writer-types +) + +add_executable(test_writer_config + test/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/include/writer_config.h b/libs/writer/writer_config/include/writer_config.h new file mode 100644 index 0000000..01059d7 --- /dev/null +++ b/libs/writer/writer_config/include/writer_config.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include +#include + +class WriterConfig +{ + public: + WriterConfig(std::string type); + + const WriterType &GetType() const noexcept { return m_type; }; + + const std::string &GetLocation() const noexcept { return m_location;}; + + private: + WriterType m_type; + std::string m_location; +}; \ No newline at end of file diff --git a/libs/writer/writer_config/src/writer_config.cpp b/libs/writer/writer_config/src/writer_config.cpp new file mode 100644 index 0000000..6c288a4 --- /dev/null +++ b/libs/writer/writer_config/src/writer_config.cpp @@ -0,0 +1,52 @@ +#include + +std::pair GetWriterConfigFromString(const std::string &type) +{ + // Check exact matches first + auto it = TypeToLocationMap.find(type); + if (it != TypeToLocationMap.end()) + { + return {it->second.first, std::string(it->second.second)}; + } + + else if (type.rfind(WriterTypes::UDPURL, 0) == 0) + { + return {WriterType::UDP, type}; + } + else if (type.rfind(WriterTypes::UnixURL, 0) == 0) + { + return {WriterType::Unix, type}; + } + + throw std::invalid_argument("Invalid writer type: " + type); +} + + + +WriterConfig::WriterConfig(std::string type) +{ + const char *envLocation = std::getenv("SPECTATOR_OUTPUT_LOCATION"); + + try + { + if (envLocation != nullptr) + { + // If environment variable is set, use it instead of the constructor parameter + std::string envValue(envLocation); + auto [writer_type, location] = GetWriterConfigFromString(envValue); + m_type = writer_type; + m_location = location; + } + else + { + // If no environment variable, use the type passed to the constructor + auto [writer_type, location] = GetWriterConfigFromString(type); + m_type = writer_type; + m_location = location; + } + } + catch (const std::invalid_argument &e) + { + throw std::invalid_argument("Invalid writer type: " + (envLocation != nullptr ? std::string(envLocation) : type)); + } +} \ No newline at end of file diff --git a/libs/writer/writer_config/test/test_writer_config.cpp b/libs/writer/writer_config/test/test_writer_config.cpp new file mode 100644 index 0000000..2056290 --- /dev/null +++ b/libs/writer/writer_config/test/test_writer_config.cpp @@ -0,0 +1,169 @@ +#include + +#include +#include + +#include + +// Helper to temporarily set an environment variable for testing +class EnvironmentVariableSetter +{ + public: + EnvironmentVariableSetter(const std::string &name, const std::string &value) : m_name(name) + { + // Store original value (might be nullptr) + m_originalValue = std::getenv(name.c_str()); + + // Set the new value + setenv(name.c_str(), value.c_str(), 1); + } + + ~EnvironmentVariableSetter() + { + // Restore original state + if (m_originalValue) + setenv(m_name.c_str(), m_originalValue, 1); + else + unsetenv(m_name.c_str()); + } + + private: + std::string m_name; + const char *m_originalValue; +}; + +// Helper to temporarily unset an environment variable for testing +class EnvironmentVariableUnset +{ + public: + EnvironmentVariableUnset(const std::string &name) : m_name(name) + { + // Store original value (might be nullptr) + m_originalValue = std::getenv(name.c_str()); + + // Always unset regardless of whether it was set before + unsetenv(name.c_str()); + } + + ~EnvironmentVariableUnset() + { + // Only restore if there was an original value + if (m_originalValue) + setenv(m_name.c_str(), m_originalValue, 1); + } + + private: + std::string m_name; + const char *m_originalValue; +}; + +// Test fixture for WriterConfig tests +class WriterConfigTest : public ::testing::Test +{ + protected: + void SetUp() override + { + // Ensure environment variable is unset before each test + unsetenv("SPECTATOR_OUTPUT_LOCATION"); + } +}; + +// Test basic writer type initialization +TEST_F(WriterConfigTest, BasicWriterTypes) +{ + + + // Test "memory" type + { + WriterConfig config(WriterTypes::Memory); + EXPECT_EQ(config.GetType(), WriterType::Memory); + EXPECT_EQ(config.GetLocation(), DefaultLocations::NoLocation); + } + + // Test "udp" type + { + WriterConfig config(WriterTypes::UDP); + EXPECT_EQ(config.GetType(), WriterType::UDP); + EXPECT_EQ(config.GetLocation(), DefaultLocations::UDP); + } + + // Test "unix" type + { + WriterConfig config(WriterTypes::Unix); + EXPECT_EQ(config.GetType(), WriterType::Unix); + EXPECT_EQ(config.GetLocation(), DefaultLocations::UDS); + } +} + +// Test URL-based writer initialization +TEST_F(WriterConfigTest, URLBasedWriterTypes) +{ + + // Test UDP URL + { + const std::string udpUrl = std::string(WriterTypes::UDPURL) + "192.168.1.100:8125"; + 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"; + WriterConfig config(unixUrl); + EXPECT_EQ(config.GetType(), WriterType::Unix); + EXPECT_EQ(config.GetLocation(), unixUrl); + } +} + +// Test environment variable override +TEST_F(WriterConfigTest, EnvironmentVariableOverride) +{ + // Test environment variable overriding constructor parameter + { + EnvironmentVariableSetter setter("SPECTATOR_OUTPUT_LOCATION", WriterTypes::Memory); + WriterConfig config(WriterTypes::UDP); // This should be ignored due to env var + EXPECT_EQ(config.GetType(), WriterType::Memory); + EXPECT_EQ(config.GetLocation(), DefaultLocations::NoLocation); + } + + + // Test with environment variable unset + { + EnvironmentVariableUnset unset("SPECTATOR_OUTPUT_LOCATION"); + WriterConfig config(WriterTypes::Memory); + EXPECT_EQ(config.GetType(), WriterType::Memory); + EXPECT_EQ(config.GetLocation(), DefaultLocations::NoLocation); + } +} + +// Test invalid writer type handling +TEST_F(WriterConfigTest, InvalidWriterType) +{ + // Test invalid type from constructor + EXPECT_THROW({ WriterConfig config("invalid_type"); }, std::invalid_argument); + + // Test invalid type from environment variable + { + EnvironmentVariableSetter setter("SPECTATOR_OUTPUT_LOCATION", "invalid_env_value"); + EXPECT_THROW( + { + WriterConfig config("none"); // This should be ignored, env var used instead + }, + std::invalid_argument); + } +} + +// Test case for edge cases +TEST_F(WriterConfigTest, EdgeCases) +{ + // Test empty string + EXPECT_THROW({ WriterConfig config(""); }, std::invalid_argument); + + // Test with just URL scheme but no path + { + 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_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/base_writer.h b/libs/writer/writer_types/include/base/base_writer.h new file mode 100644 index 0000000..3b7ca8b --- /dev/null +++ b/libs/writer/writer_types/include/base/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..757b064 --- /dev/null +++ b/libs/writer/writer_types/include/memory_writer.h @@ -0,0 +1,28 @@ +#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..4f10cdb --- /dev/null +++ b/libs/writer/writer_types/include/udp_writer.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#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..9ac404b --- /dev/null +++ b/libs/writer/writer_types/include/uds_writer.h @@ -0,0 +1,25 @@ +#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; + 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..0409d0e --- /dev/null +++ b/libs/writer/writer_types/include/writer_types.h @@ -0,0 +1,43 @@ +#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}}, +}; \ 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..e6b6efe --- /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..7a15a6d --- /dev/null +++ b/libs/writer/writer_types/src/udp_writer.cpp @@ -0,0 +1,81 @@ +#include + +#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..4acafa4 --- /dev/null +++ b/libs/writer/writer_types/src/uds_writer.cpp @@ -0,0 +1,116 @@ +#include + +#include + + +#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); + } + + // Connect to the UDS server + local::stream_protocol::endpoint endpoint(m_socketPath); + + boost::system::error_code ec; + m_socket->connect(endpoint, ec); + + 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; + boost::asio::write(*m_socket, boost::asio::buffer(message), 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)", message.size()); + } + } + 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->shutdown(local::stream_protocol::socket::shutdown_both, 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..fb61302 --- /dev/null +++ b/libs/writer/writer_types/test/test_memory_writer.cpp @@ -0,0 +1,50 @@ +#include +#include + +TEST(MemoryWriterTest, IsEmpty) +{ + 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..152d8cb --- /dev/null +++ b/libs/writer/writer_types/test/test_udp_writer.cpp @@ -0,0 +1,137 @@ +#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", 1234); + + // Define our test message + 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 + 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; + } + } +} + +TEST_F(UDPWriterTest, CloseAndReopen) +{ + + UDPWriter writer("127.0.0.1", 1234); + 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", 1234); + 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", 1234); + + // Define test messages + 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 + 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..9bc40e9 --- /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 + 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 + 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"); + 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"); + 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_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 + 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 + 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..ec4bcf2 --- /dev/null +++ b/libs/writer/writer_types/test_utils/udp_server/udp_server.cpp @@ -0,0 +1,126 @@ +#include "udp_server.h" +#include +#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 +{ + const unsigned short port = 1234; + + boost::asio::io_context io_context; + + // Create an IPv4 socket bound to localhost (127.0.0.1) and port 1234 + 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 + { + 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..2514ec9 --- /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..173887a --- /dev/null +++ b/libs/writer/writer_types/test_utils/uds_server/uds_server.cpp @@ -0,0 +1,153 @@ +#include "uds_server.h" +#include +#include +#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 socket + boost::asio::local::stream_protocol::endpoint endpoint(socket_path); + boost::asio::local::stream_protocol::acceptor acceptor(io_context, endpoint); + + std::cout << "UDS server listening on " << socket_path << std::endl; + + while (uds_server_running) + { + // Create a socket for the client connection + boost::asio::local::stream_protocol::socket socket(io_context); + + try + { + // Set acceptor to non-blocking so we can check the run flag + acceptor.non_blocking(true); + + boost::system::error_code ec; + acceptor.accept(socket, ec); + + if (ec) + { + if (ec == boost::asio::error::would_block) + { + // No connection available, wait a bit and try again + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + continue; + } + else + { + std::cerr << "Error accepting connection: " << ec.message() << std::endl; + continue; + } + } + + // Connection accepted, set back to blocking for data reading + socket.non_blocking(false); + + // Buffer for reading data + std::array buffer; + while (true) + { + boost::system::error_code read_ec; + std::size_t bytes_read = socket.read_some(boost::asio::buffer(buffer), read_ec); + + if (read_ec == boost::asio::error::eof) + { + // Connection closed cleanly by peer + break; + } + else if (read_ec) + { + std::cerr << "Error reading from socket: " << read_ec.message() << std::endl; + break; + } + + 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 message: " << message << std::endl; + std::cout << "Total messages stored: " << current_messages.size() << std::endl; + } + // Close the connection + socket.close(); + } + catch (const std::exception &e) + { + std::cerr << "Exception handling client: " << e.what() << std::endl; + } + } + + std::cout << "UDS server shutting down..." << std::endl; + + // 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..b03873f --- /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..5267439 --- /dev/null +++ b/libs/writer/writer_wrapper/CMakeLists.txt @@ -0,0 +1,9 @@ +add_library(spectator-writer-wrapper + src/Writer.cpp +) + +target_link_libraries(spectator-writer-wrapper + PUBLIC + spectator-logger + spectator-writer-types +) \ No newline at end of file diff --git a/libs/writer/writer_wrapper/include/Writer.h b/libs/writer/writer_wrapper/include/Writer.h new file mode 100644 index 0000000..1ffd5f9 --- /dev/null +++ b/libs/writer/writer_wrapper/include/Writer.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +#include +#include + + +class Writer final : public Singleton +{ + public: + virtual ~Writer(); + + + 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 ¶m = "", int port = 0); + + static void Write(const std::string &message); + + static void Close(); + + static WriterType GetWriterType(); + + // Get the Writer's implementation for testing purposes + static BaseWriter *GetImpl() + { + return Writer::GetInstance().m_impl.get(); + } + /** + * Reset the writer to uninitialized state + * This method is private and can only be called by Registry + */ + static void Reset(); + + // Implementation details + std::unique_ptr m_impl; + WriterType m_currentType = WriterType::Memory; // Default type +}; \ No newline at end of file diff --git a/libs/writer/writer_wrapper/include/writer_test_helper.h b/libs/writer/writer_wrapper/include/writer_test_helper.h new file mode 100644 index 0000000..81e82cc --- /dev/null +++ b/libs/writer/writer_wrapper/include/writer_test_helper.h @@ -0,0 +1,31 @@ +#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 ¶m = "", int port = 0) + { + Writer::Initialize(type, param, port); + } + + // 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/libs/writer/writer_wrapper/src/Writer.cpp b/libs/writer/writer_wrapper/src/Writer.cpp new file mode 100644 index 0000000..e1ba3de --- /dev/null +++ b/libs/writer/writer_wrapper/src/Writer.cpp @@ -0,0 +1,111 @@ +#include + +#include +#include +#include + +Writer::~Writer() +{ + // No need to explicitly close here as unique_ptr will clean up +} + +void Writer::Initialize(WriterType type, const std::string ¶m, int port) +{ + // 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; + } + 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::Write(const std::string &message) +{ + auto &instance = GetInstance(); + + if (!instance.m_impl) + { + Logger::error("Attempted to write with uninitialized writer implementation"); + return; + } + + try + { + instance.m_impl->Write(message); + } + catch (const std::exception &e) + { + Logger::error("Write operation failed: {}", e.what()); + } +} + +void Writer::Close() +{ + 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()); + } +} + +WriterType Writer::GetWriterType() +{ + return GetInstance().m_currentType; +} diff --git a/requirements.txt b/requirements.txt index 548109e..b047f71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -conan==2.16.1 +conan==2.16.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..40e14d2 --- /dev/null +++ b/spectator/CMakeLists.txt @@ -0,0 +1,24 @@ +add_library(registry + src/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/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/include/registry.h b/spectator/include/registry.h new file mode 100644 index 0000000..162fab9 --- /dev/null +++ b/spectator/include/registry.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include // Include Writer.h + +#include +#include +#include +#include +#include +#include + +class Registry +{ + public: + explicit Registry(const Config &config); + ~Registry(); + + MeterId new_id(const std::string &name, const std::unordered_map &tags = {}) const; + + AgeGauge age_gauge(const std::string &name, + const std::unordered_map &tags = std::unordered_map()); + + AgeGauge age_gauge_with_id(const MeterId &meter_id); + + Counter counter(const std::string &name, + const std::unordered_map &tags = std::unordered_map()); + + Counter counter_with_id(const MeterId &meter_id); + + DistributionSummary distribution_summary( + const std::string &name, const std::unordered_map &tags = std::unordered_map()); + + DistributionSummary distribution_summary_with_id(const MeterId &meter_id); + + Gauge gauge(const std::string &name, + const std::unordered_map &tags = std::unordered_map(), + const std::optional &ttl_seconds = std::nullopt); + + Gauge gauge_with_id(const MeterId &meter_id, const std::optional &ttl_seconds = std::nullopt); + + MaxGauge max_gauge(const std::string &name, + const std::unordered_map &tags = std::unordered_map()); + + MaxGauge max_gauge_with_id(const MeterId &meter_id); + + MonotonicCounter monotonic_counter( + const std::string &name, const std::unordered_map &tags = std::unordered_map()); + + MonotonicCounter monotonic_counter_with_id(const MeterId &meter_id); + + MonotonicCounterUint monotonic_counter_uint( + const std::string &name, const std::unordered_map &tags = std::unordered_map()); + + MonotonicCounterUint monotonic_counter_uint_with_id(const MeterId &meter_id); + + PercentileDistributionSummary pct_distribution_summary( + const std::string &name, const std::unordered_map &tags = std::unordered_map()); + + PercentileDistributionSummary pct_distribution_summary_with_id(const MeterId &meter_id); + + PercentileTimer pct_timer(const std::string &name, + const std::unordered_map &tags = std::unordered_map()); + + PercentileTimer pct_timer_with_id(const MeterId &meter_id); + + Timer timer(const std::string &name, + const std::unordered_map &tags = std::unordered_map()); + + Timer timer_with_id(const MeterId &meter_id); + + private: + Config m_config; +}; \ No newline at end of file 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.h b/spectator/registry.h deleted file mode 100644 index 9a8e50d..0000000 --- a/spectator/registry.h +++ /dev/null @@ -1,356 +0,0 @@ -#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" - -namespace spectator { - -// 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 - -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 - -template -struct single_table_state { - using types = Types; - - 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); - } - } - - meters_.emplace(new_meter->MeterId(), new_meter); - return new_meter; - } - - auto get_age_gauge(IdPtr id) { - return get_or_create(std::move(id)); - } - - auto get_counter(IdPtr id) { - return get_or_create(std::move(id)); - } - - auto get_ds(IdPtr id) { - return get_or_create(std::move(id)); - } - - auto get_gauge(IdPtr id) { - return get_or_create(std::move(id)); - } - - auto get_gauge_ttl(IdPtr id, unsigned int ttl_seconds) { - return get_or_create(std::move(id), ttl_seconds); - } - - auto get_max_gauge(IdPtr id) { - return get_or_create(std::move(id)); - } - - auto get_monotonic_counter(IdPtr id) { - return get_or_create(std::move(id)); - } - - auto get_monotonic_counter_uint(IdPtr id) { - return get_or_create(std::move(id)); - } - - auto get_perc_ds(IdPtr id, int64_t min, int64_t max) { - return get_or_create(std::move(id), min, max); - } - - auto get_perc_timer(IdPtr id, std::chrono::nanoseconds min, - std::chrono::nanoseconds max) { - return get_or_create(std::move(id), min, max); - } - - auto get_timer(IdPtr id) { - return get_or_create(std::move(id)); - } - - auto measurements() { - std::vector result; - - absl::MutexLock lock(&mutex_); - result.reserve(meters_.size() * 2); - for (auto& m : meters_) { - m.second->Measure(&result); - } - return result; - } - - 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_); -}; - -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; - - explicit base_registry(logger_ptr logger = DefaultLogger()) - : logger_(std::move(logger)) {} - - 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))); - } - - 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 diff --git a/spectator/src/registry.cpp b/spectator/src/registry.cpp new file mode 100644 index 0000000..314fb17 --- /dev/null +++ b/spectator/src/registry.cpp @@ -0,0 +1,129 @@ +#include + +Registry::Registry(const Config &config) : m_config(config) +{ + Writer::Initialize(config.GetWriterType()); +} + +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; + } + else + { + return new_meter_id.WithTags(this->m_config.GetExtraTags()); + } +} + +AgeGauge Registry::age_gauge(const std::string &name, const std::unordered_map &tags) +{ + 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) +{ + 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) +{ + 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) +{ + 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) +{ + 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) +{ + 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) +{ + 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) +{ + 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) +{ + 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) +{ + 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/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/test_registry.cpp b/spectator/test/test_registry.cpp new file mode 100644 index 0000000..b3dc4c6 --- /dev/null +++ b/spectator/test/test_registry.cpp @@ -0,0 +1,342 @@ +#include +#include + +#include "../include/registry.h" +#include + +#include + +TEST(RegistryTest, Close) +{ + 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", memoryWriter->LastLine()); + + memoryWriter->Close(); + EXPECT_TRUE(memoryWriter->IsEmpty()); +} + +TEST(RegistryTest, AgeGauge) +{ + 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", memoryWriter->LastLine()); + + g2.Set(2); + EXPECT_EQ("A:age_gauge,my-tags=bar:2", 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 = r.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", ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, Counter) +{ + 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", memoryWriter->LastLine()); + + c2.Increment(); + EXPECT_EQ("c:counter,my-tags=bar:1.000000", memoryWriter->LastLine()); + + c1.Increment(2); + EXPECT_EQ("c:counter:2.000000", memoryWriter->LastLine()); + + c2.Increment(2); + EXPECT_EQ("c:counter,my-tags=bar:2.000000", memoryWriter->LastLine()); + + r.counter("counter").Increment(3); + EXPECT_EQ("c:counter:3.000000", 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 = r.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", ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); + + c.Increment(2); + EXPECT_EQ("c:counter,extra-tags=foo,my-tags=bar:2.000000", 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", ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, DistributionSummary) +{ + 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", 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 = r.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", ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, Gauge) +{ + 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", 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 = r.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", 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) +{ + 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", 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 = r.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", ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, MonotonicCounter) +{ + 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", 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 = r.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", ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, MonotonicCounterUint) +{ + 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", 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 = r.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", ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, NewId) +{ + Config config1(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) +{ + 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", 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 = r.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", ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, PctTimer) +{ + 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", 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 = r.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", ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} + +TEST(RegistryTest, Timer) +{ + 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", 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 = r.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", ParseProtocolLine(memoryWriter->LastLine()).value().to_string()); +} \ No newline at end of file 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_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); -}