├── .clang-format ├── .github └── workflows │ └── build.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── OSSMETADATA ├── README.md ├── build.sh ├── conanfile.py ├── requirements.txt ├── sanitized ├── setup-venv.sh ├── spectator ├── age_gauge_test.cc ├── config.h ├── counter_test.cc ├── dist_summary_test.cc ├── gauge_test.cc ├── id.h ├── id_test.cc ├── log_entry.h ├── logger.cc ├── logger.h ├── max_gauge_test.cc ├── measurement.h ├── meter_type.h ├── meter_type_test.cc ├── monotonic_counter_test.cc ├── monotonic_counter_uint_test.cc ├── perc_dist_summary_test.cc ├── perc_timer_test.cc ├── publisher.cc ├── publisher.h ├── publisher_test.cc ├── registry.h ├── stateful_meters.h ├── stateful_test.cc ├── stateless_meters.h ├── statelessregistry_test.cc ├── test_main.cc ├── test_publisher.h ├── test_server.h ├── timer_test.cc └── util.h └── tools └── gen_valid_chars.cc /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: Google 4 | AccessModifierOffset: -1 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: None 7 | AlignConsecutiveDeclarations: None 8 | AlignOperands: true 9 | AlignTrailingComments: true 10 | AllowAllParametersOfDeclarationOnNextLine: true 11 | AllowShortBlocksOnASingleLine: Never 12 | AllowShortCaseLabelsOnASingleLine: false 13 | AllowShortFunctionsOnASingleLine: All 14 | AllowShortIfStatementsOnASingleLine: true 15 | AllowShortLoopsOnASingleLine: true 16 | AlwaysBreakAfterDefinitionReturnType: None 17 | AlwaysBreakAfterReturnType: None 18 | AlwaysBreakBeforeMultilineStrings: true 19 | AlwaysBreakTemplateDeclarations: Yes 20 | BinPackArguments: true 21 | BinPackParameters: true 22 | BraceWrapping: 23 | AfterClass: false 24 | AfterControlStatement: Never 25 | AfterEnum: false 26 | AfterFunction: false 27 | AfterNamespace: false 28 | AfterObjCDeclaration: false 29 | AfterStruct: false 30 | AfterUnion: false 31 | BeforeCatch: false 32 | BeforeElse: false 33 | IndentBraces: false 34 | BreakBeforeBinaryOperators: None 35 | BreakBeforeBraces: Attach 36 | BreakBeforeTernaryOperators: true 37 | BreakConstructorInitializersBeforeComma: false 38 | ColumnLimit: 100 39 | CommentPragmas: '^ IWYU pragma:' 40 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 41 | ConstructorInitializerIndentWidth: 4 42 | ContinuationIndentWidth: 4 43 | Cpp11BracedListStyle: true 44 | DerivePointerAlignment: false 45 | DisableFormat: false 46 | ExperimentalAutoDetectBinPacking: false 47 | ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] 48 | IncludeCategories: 49 | - Regex: '^<.*\.h>' 50 | Priority: 1 51 | - Regex: '^<.*' 52 | Priority: 2 53 | - Regex: '.*' 54 | Priority: 3 55 | IndentCaseLabels: true 56 | IndentWidth: 2 57 | IndentWrappedFunctionNames: false 58 | KeepEmptyLinesAtTheStartOfBlocks: false 59 | MacroBlockBegin: '' 60 | MacroBlockEnd: '' 61 | MaxEmptyLinesToKeep: 1 62 | NamespaceIndentation: None 63 | ObjCBlockIndentWidth: 2 64 | ObjCSpaceAfterProperty: false 65 | ObjCSpaceBeforeProtocolList: false 66 | PenaltyBreakBeforeFirstCallParameter: 1 67 | PenaltyBreakComment: 300 68 | PenaltyBreakFirstLessLess: 120 69 | PenaltyBreakString: 1000 70 | PenaltyExcessCharacter: 1000000 71 | PenaltyReturnTypeOnItsOwnLine: 200 72 | PointerAlignment: Left 73 | ReflowComments: true 74 | SortIncludes: Never 75 | SpaceAfterCStyleCast: false 76 | SpaceBeforeAssignmentOperators: true 77 | SpaceBeforeParens: ControlStatements 78 | SpaceInEmptyParentheses: false 79 | SpacesBeforeTrailingComments: 2 80 | SpacesInAngles: false 81 | SpacesInContainerLiterals: true 82 | SpacesInCStyleCastParentheses: false 83 | SpacesInParentheses: false 84 | SpacesInSquareBrackets: false 85 | Standard: Auto 86 | TabWidth: 8 87 | UseTab: Never 88 | ... 89 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | if: ${{ github.repository == 'Netflix/spectator-cpp' }} 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Restore Conan Cache 17 | id: conan-cache-restore 18 | uses: actions/cache/restore@v4 19 | with: 20 | path: | 21 | /home/runner/.conan2 22 | /home/runner/work/spectator-cpp/spectator-cpp/cmake-build 23 | key: ${{ runner.os }}-conan 24 | 25 | - name: Install System Dependencies 26 | run: | 27 | sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test 28 | sudo apt-get update && sudo apt-get install -y binutils-dev g++-13 libiberty-dev 29 | 30 | - name: Build 31 | run: | 32 | ./setup-venv.sh 33 | source venv/bin/activate 34 | ./build.sh 35 | 36 | - name: Save Conan Cache 37 | id: conan-cache-save 38 | uses: actions/cache/save@v4 39 | with: 40 | path: | 41 | /home/runner/.conan2 42 | /home/runner/work/spectator-cpp/spectator-cpp/cmake-build 43 | key: ${{ steps.conan-cache-restore.outputs.cache-primary-key }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | CMakeUserPresets.json 4 | cmake-build/ 5 | conan_provider.cmake 6 | spectator/valid_chars.inc 7 | venv/ 8 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.23) 2 | 3 | project(spectator-cpp) 4 | 5 | set(CMAKE_CXX_STANDARD 20) 6 | set(CMAKE_CXX_STANDARD_REQUIRED True) 7 | set(CMAKE_CXX_EXTENSIONS OFF) 8 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 9 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 10 | 11 | add_compile_options(-pedantic -Werror -Wall -Wno-missing-braces -fno-omit-frame-pointer "$<$:-fsanitize=address>") 12 | 13 | find_package(absl REQUIRED) 14 | find_package(asio REQUIRED) 15 | find_package(Backward REQUIRED) 16 | find_package(fmt REQUIRED) 17 | find_package(GTest REQUIRED) 18 | find_package(spdlog REQUIRED) 19 | 20 | include(CTest) 21 | 22 | #-- spectator_test test executable 23 | file(GLOB spectator_test_source_files 24 | "spectator/*_test.cc" 25 | "spectator/test_*.cc" 26 | "spectator/test_*.h" 27 | ) 28 | add_executable(spectator_test ${spectator_test_source_files}) 29 | target_link_libraries(spectator_test 30 | spectator 31 | gtest::gtest 32 | ) 33 | add_test( 34 | NAME spectator_test 35 | COMMAND spectator_test 36 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 37 | ) 38 | 39 | #-- spectator library 40 | add_library(spectator SHARED 41 | "spectator/logger.cc" 42 | "spectator/publisher.cc" 43 | "spectator/config.h" 44 | "spectator/id.h" 45 | "spectator/logger.h" 46 | "spectator/measurement.h" 47 | "spectator/meter_type.h" 48 | "spectator/publisher.h" 49 | "spectator/registry.h" 50 | "spectator/stateful_meters.h" 51 | "spectator/stateless_meters.h" 52 | "spectator/valid_chars.inc" 53 | ) 54 | target_link_libraries(spectator 55 | abseil::abseil 56 | asio::asio 57 | Backward::Backward 58 | fmt::fmt 59 | spdlog::spdlog 60 | ) 61 | 62 | #-- generator tools 63 | add_executable(gen_valid_chars "tools/gen_valid_chars.cc") 64 | 65 | #-- file generators, must exist where the outputs are referenced 66 | add_custom_command( 67 | OUTPUT "spectator/valid_chars.inc" 68 | COMMAND "${CMAKE_BINARY_DIR}/bin/gen_valid_chars" > "${CMAKE_SOURCE_DIR}/spectator/valid_chars.inc" 69 | DEPENDS gen_valid_chars 70 | ) 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2012 Netflix, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /OSSMETADATA: -------------------------------------------------------------------------------- 1 | osslifecycle=active 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/Netflix/spectator-cpp/actions/workflows/build.yml/badge.svg)](https://github.com/Netflix/spectator-cpp/actions/workflows/build.yml) 2 | 3 | # Spectator-cpp 4 | 5 | This implements a basic [Spectator](https://github.com/Netflix/spectator) library for instrumenting Go applications. It 6 | consists of a thin client designed to send metrics through [spectatord](https://github.com/Netflix-Skunkworks/spectatord). 7 | 8 | ## Instrumenting Code 9 | 10 | ```C++ 11 | #include 12 | 13 | // use default values 14 | static constexpr auto kDefault = 0; 15 | 16 | struct Request { 17 | std::string country; 18 | }; 19 | 20 | struct Response { 21 | int status; 22 | int size; 23 | }; 24 | 25 | class Server { 26 | public: 27 | explicit Server(spectator::Registry* registry) 28 | : registry_{registry}, 29 | request_count_id_{registry->CreateId("server.requestCount", spectator::Tags{})}, 30 | request_latency_{registry->GetTimer("server.requestLatency")}, 31 | response_size_{registry->GetDistributionSummary("server.responseSizes")} {} 32 | 33 | Response Handle(const Request& request) { 34 | auto start = std::chrono::steady_clock::now(); 35 | 36 | // do some work and obtain a response... 37 | Response res{200, 64}; 38 | 39 | // Update the Counter id with dimensions, based on information in the request. The Counter 40 | // will be looked up in the Registry, which is a fairly cheap operation, about the same as 41 | // the lookup of an id object in a map. However, it is more expensive than having a local 42 | // variable set to the Counter. 43 | auto cnt_id = request_count_id_ 44 | ->WithTag("country", request.country) 45 | ->WithTag("status", std::to_string(res.status)); 46 | registry_->GetCounter(std::move(cnt_id))->Increment(); 47 | request_latency_->Record(std::chrono::steady_clock::now() - start); 48 | response_size_->Record(res.size); 49 | return res; 50 | } 51 | 52 | private: 53 | spectator::Registry* registry_; 54 | std::shared_ptr request_count_id_; 55 | std::shared_ptr request_latency_; 56 | std::shared_ptr response_size_; 57 | }; 58 | 59 | Request get_next_request() { 60 | return Request{"US"}; 61 | } 62 | 63 | int main() { 64 | auto logger = spdlog::stdout_color_mt("console"); 65 | std::unordered_map common_tags{{"xatlas.process", "some-sidecar"}}; 66 | spectator::Config cfg{"unix:/run/spectatord/spectatord.unix", common_tags}; 67 | spectator::Registry registry{std::move(cfg), logger); 68 | 69 | Server server{®istry}; 70 | 71 | for (auto i = 1; i <= 3; ++i) { 72 | // get a request 73 | auto req = get_next_request(); 74 | server.Handle(req); 75 | } 76 | } 77 | ``` 78 | 79 | ## High-Volume Publishing 80 | 81 | By default, the library sends every meter change to the spectatord sidecar immediately. This involves a blocking 82 | `send` call and underlying system calls, and may not be the most efficient way to publish metrics in high-volume 83 | use cases. For this purpose a simple buffering functionality in `Publisher` is implemented, and it can be turned 84 | on by passing a buffer size to the `spectator::Config` constructor. It is important to note that, until this buffer 85 | fills up, the `Publisher` will not send nay meters to the sidecar. Therefore, if your application doesn't emit 86 | meters at a high rate, you should either keep the buffer very small, or do not configure a buffer size at all, 87 | which will fall back to the "publish immediately" mode of operation. 88 | 89 | ## Local & IDE Configuration 90 | 91 | ```shell 92 | # setup python venv and activate, to gain access to conan cli 93 | ./setup-venv.sh 94 | source venv/bin/activate 95 | 96 | ./build.sh # [clean|clean --confirm|skiptest] 97 | ``` 98 | 99 | * Install the Conan plugin for CLion. 100 | * CLion > Settings > Plugins > Marketplace > Conan > Install 101 | * Configure the Conan plugin. 102 | * The easiest way to configure CLion to work with Conan is to build the project first from the command line. 103 | * This will establish the `$PROJECT_HOME/CMakeUserPresets.json` file, which will allow you to choose the custom 104 | CMake configuration created by Conan when creating a new CMake project. Using this custom profile will ensure 105 | that sources are properly indexed and explorable. 106 | * Open the project. The wizard will show three CMake profiles. 107 | * Disable the default Cmake `Debug` profile. 108 | * Enable the CMake `conan-debug` profile. 109 | * CLion > View > Tool Windows > Conan > (gear) > Conan Executable: `$PROJECT_HOME/venv/bin/conan` 110 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # usage: ./build.sh [clean|clean --confirm|skiptest] 6 | 7 | if [[ -z "$BUILD_DIR" ]]; then 8 | BUILD_DIR="cmake-build" 9 | fi 10 | 11 | if [[ -z "$BUILD_TYPE" ]]; then 12 | # Choose: Debug, Release, RelWithDebInfo and MinSizeRel. Use Debug for asan checking locally. 13 | BUILD_TYPE="Debug" 14 | fi 15 | 16 | BLUE="\033[0;34m" 17 | NC="\033[0m" 18 | 19 | if [[ "$1" == "clean" ]]; then 20 | echo -e "${BLUE}==== clean ====${NC}" 21 | rm -rf "$BUILD_DIR" 22 | rm -f spectator/*.inc 23 | if [[ "$2" == "--confirm" ]]; then 24 | # remove all packages from the conan cache, to allow swapping between Release/Debug builds 25 | conan remove "*" --confirm 26 | fi 27 | fi 28 | 29 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then 30 | if [[ -z "$CC" || -z "$CXX" ]]; then 31 | export CC=gcc-13 32 | export CXX=g++-13 33 | fi 34 | fi 35 | 36 | echo -e "${BLUE}==== env configuration ====${NC}" 37 | echo "BUILD_DIR=$BUILD_DIR" 38 | echo "BUILD_TYPE=$BUILD_TYPE" 39 | echo "CC=$CC" 40 | echo "CXX=$CXX" 41 | 42 | if [[ ! -f "$HOME/.conan2/profiles/default" ]]; then 43 | echo -e "${BLUE}==== create default profile ====${NC}" 44 | conan profile detect 45 | fi 46 | 47 | if [[ ! -d $BUILD_DIR ]]; then 48 | echo -e "${BLUE}==== install required dependencies ====${NC}" 49 | if [[ "$BUILD_TYPE" == "Debug" ]]; then 50 | conan install . --output-folder="$BUILD_DIR" --build="*" --settings=build_type="$BUILD_TYPE" --profile=./sanitized 51 | else 52 | conan install . --output-folder="$BUILD_DIR" --build=missing --settings=build_type="$BUILD_TYPE" 53 | fi 54 | fi 55 | 56 | pushd $BUILD_DIR 57 | 58 | echo -e "${BLUE}==== configure conan environment to access tools ====${NC}" 59 | source conanbuild.sh 60 | 61 | if [[ $OSTYPE == "darwin"* ]]; then 62 | export MallocNanoZone=0 63 | fi 64 | 65 | echo -e "${BLUE}==== generate build files ====${NC}" 66 | cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=$BUILD_TYPE 67 | 68 | echo -e "${BLUE}==== build ====${NC}" 69 | cmake --build . 70 | 71 | if [[ "$1" != "skiptest" ]]; then 72 | echo -e "${BLUE}==== test ====${NC}" 73 | GTEST_COLOR=1 ctest --verbose 74 | fi 75 | 76 | popd 77 | -------------------------------------------------------------------------------- /conanfile.py: -------------------------------------------------------------------------------- 1 | from conan import ConanFile 2 | 3 | 4 | class SpectatorCppConan(ConanFile): 5 | settings = "os", "compiler", "build_type", "arch" 6 | requires = ( 7 | "abseil/20240116.2", 8 | "asio/1.32.0", 9 | "backward-cpp/1.6", 10 | "fmt/11.0.2", 11 | "gtest/1.15.0", 12 | "spdlog/1.15.0", 13 | ) 14 | tool_requires = () 15 | generators = "CMakeDeps", "CMakeToolchain" 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | conan==2.16.1 2 | -------------------------------------------------------------------------------- /sanitized: -------------------------------------------------------------------------------- 1 | include(default) 2 | 3 | # https://blog.conan.io/2022/04/21/New-conan-release-1-47.html 4 | # 5 | # inject sanitizer flags into conan packages, so that we do not get segfaults when 6 | # we try to run the local build with the address sanitizer for debug builds 7 | 8 | [conf] 9 | tools.build:cxxflags=["-fno-omit-frame-pointer", "-fsanitize=address"] 10 | -------------------------------------------------------------------------------- /setup-venv.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | MAYBE_PYTHON=$(find /apps -maxdepth 1 -type l -name "python*") 4 | 5 | if [[ -n "$MAYBE_PYTHON" ]]; then 6 | PYTHON3="$MAYBE_PYTHON/bin/python3" 7 | echo "using $PYTHON3 ($($PYTHON3 -V))" 8 | else 9 | PYTHON3="python3" 10 | echo "using $(which $PYTHON3) ($($PYTHON3 -V))" 11 | fi 12 | 13 | # create and activate virtualenv 14 | $PYTHON3 -m venv venv 15 | source venv/bin/activate 16 | 17 | if [[ -f requirements.txt ]]; then 18 | # use the virtualenv python 19 | python -m pip install --upgrade pip wheel 20 | python -m pip install --requirement requirements.txt 21 | fi 22 | -------------------------------------------------------------------------------- /spectator/age_gauge_test.cc: -------------------------------------------------------------------------------- 1 | #include "stateless_meters.h" 2 | #include "test_publisher.h" 3 | #include 4 | 5 | namespace { 6 | 7 | using spectator::Id; 8 | using spectator::AgeGauge; 9 | using spectator::Tags; 10 | using spectator::TestPublisher; 11 | 12 | TEST(AgeGauge, Set) { 13 | TestPublisher publisher; 14 | auto id = std::make_shared("gauge", Tags{}); 15 | auto id2 = std::make_shared("gauge2", Tags{{"key", "val"}}); 16 | AgeGauge g{id, &publisher}; 17 | AgeGauge g2{id2, &publisher}; 18 | 19 | g.Set(1671641328); 20 | g2.Set(1671641028.3); 21 | g.Set(0); 22 | std::vector expected = {"A:gauge:1671641328", 23 | "A:gauge2,key=val:1671641028.3", 24 | "A:gauge:0"}; 25 | EXPECT_EQ(publisher.SentMessages(), expected); 26 | } 27 | 28 | TEST(AgeGauge, InvalidTags) { 29 | TestPublisher publisher; 30 | // test with a single tag, because tags order is not guaranteed in a flat_hash_map 31 | auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", 32 | Tags{{"tag1,:=", "value1,:="}}); 33 | AgeGauge g{id, &publisher}; 34 | EXPECT_EQ("A:test______^____-_~______________.___foo,tag1___=value1___:", g.GetPrefix()); 35 | } 36 | } // namespace 37 | -------------------------------------------------------------------------------- /spectator/config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace spectator { 7 | 8 | struct Config { 9 | std::string endpoint; 10 | std::unordered_map common_tags; 11 | uint32_t bytes_to_buffer; 12 | }; 13 | 14 | } // namespace spectator 15 | -------------------------------------------------------------------------------- /spectator/counter_test.cc: -------------------------------------------------------------------------------- 1 | #include "stateless_meters.h" 2 | #include "test_publisher.h" 3 | #include 4 | 5 | namespace { 6 | 7 | using spectator::Counter; 8 | using spectator::Id; 9 | using spectator::Tags; 10 | using spectator::TestPublisher; 11 | 12 | TEST(Counter, Activity) { 13 | TestPublisher publisher; 14 | auto id = std::make_shared("ctr.name", Tags{}); 15 | auto id2 = std::make_shared("c2", Tags{{"key", "val"}}); 16 | Counter c{id, &publisher}; 17 | Counter c2{id2, &publisher}; 18 | c.Increment(); 19 | c2.Add(1.2); 20 | c.Add(0.1); 21 | std::vector expected = {"c:ctr.name:1", "c:c2,key=val:1.2", 22 | "c:ctr.name:0.1"}; 23 | EXPECT_EQ(publisher.SentMessages(), expected); 24 | } 25 | 26 | TEST(Counter, Id) { 27 | TestPublisher publisher; 28 | Counter c{std::make_shared("foo", Tags{{"key", "val"}}), 29 | &publisher}; 30 | auto id = std::make_shared("foo", Tags{{"key", "val"}}); 31 | EXPECT_EQ(*(c.MeterId()), *id); 32 | } 33 | 34 | TEST(Counter, InvalidTags) { 35 | TestPublisher publisher; 36 | // test with a single tag, because tags order is not guaranteed in a flat_hash_map 37 | auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", 38 | Tags{{"tag1,:=", "value1,:="}}); 39 | Counter c{id, &publisher}; 40 | EXPECT_EQ("c:test______^____-_~______________.___foo,tag1___=value1___:", c.GetPrefix()); 41 | } 42 | } // namespace 43 | -------------------------------------------------------------------------------- /spectator/dist_summary_test.cc: -------------------------------------------------------------------------------- 1 | #include "stateless_meters.h" 2 | #include "test_publisher.h" 3 | #include 4 | 5 | namespace { 6 | using spectator::DistributionSummary; 7 | using spectator::Id; 8 | using spectator::Tags; 9 | using spectator::TestPublisher; 10 | 11 | TEST(DistributionSummary, Record) { 12 | TestPublisher publisher; 13 | auto id = std::make_shared("ds.name", Tags{}); 14 | auto id2 = std::make_shared("ds2", Tags{{"key", "val"}}); 15 | DistributionSummary d{id, &publisher}; 16 | DistributionSummary d2{id2, &publisher}; 17 | d.Record(10); 18 | d2.Record(1.2); 19 | d.Record(0.1); 20 | std::vector expected = {"d:ds.name:10", "d:ds2,key=val:1.2", 21 | "d:ds.name:0.1"}; 22 | EXPECT_EQ(publisher.SentMessages(), expected); 23 | } 24 | 25 | TEST(DistributionSummary, InvalidTags) { 26 | TestPublisher publisher; 27 | // test with a single tag, because tags order is not guaranteed in a flat_hash_map 28 | auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", 29 | Tags{{"tag1,:=", "value1,:="}}); 30 | DistributionSummary d{id, &publisher}; 31 | EXPECT_EQ("d:test______^____-_~______________.___foo,tag1___=value1___:", d.GetPrefix()); 32 | } 33 | } // namespace 34 | -------------------------------------------------------------------------------- /spectator/gauge_test.cc: -------------------------------------------------------------------------------- 1 | #include "stateless_meters.h" 2 | #include "test_publisher.h" 3 | #include 4 | 5 | namespace { 6 | 7 | using spectator::Gauge; 8 | using spectator::Id; 9 | using spectator::Tags; 10 | using spectator::TestPublisher; 11 | 12 | TEST(Gauge, Set) { 13 | TestPublisher publisher; 14 | auto id = std::make_shared("gauge", Tags{}); 15 | auto id2 = std::make_shared("gauge2", Tags{{"key", "val"}}); 16 | Gauge g{id, &publisher}; 17 | Gauge g2{id2, &publisher}; 18 | 19 | g.Set(42); 20 | g2.Set(2); 21 | g.Set(1); 22 | std::vector expected = {"g:gauge:42", "g:gauge2,key=val:2", 23 | "g:gauge:1"}; 24 | EXPECT_EQ(publisher.SentMessages(), expected); 25 | } 26 | 27 | TEST(Gauge, SetWithTTL) { 28 | TestPublisher publisher; 29 | auto id = std::make_shared("gauge", Tags{}); 30 | auto id2 = std::make_shared("gauge2", Tags{{"key", "val"}}); 31 | Gauge g{id, &publisher, 1}; 32 | Gauge g2{id2, &publisher, 2}; 33 | 34 | g.Set(42); 35 | g2.Set(2); 36 | g.Set(1); 37 | std::vector expected = {"g,1:gauge:42", "g,2:gauge2,key=val:2", 38 | "g,1:gauge:1"}; 39 | EXPECT_EQ(publisher.SentMessages(), expected); 40 | } 41 | 42 | 43 | TEST(Gauge, InvalidTags) { 44 | TestPublisher publisher; 45 | // test with a single tag, because tags order is not guaranteed in a flat_hash_map 46 | auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", 47 | Tags{{"tag1,:=", "value1,:="}}); 48 | Gauge g{id, &publisher}; 49 | EXPECT_EQ("g:test______^____-_~______________.___foo,tag1___=value1___:", g.GetPrefix()); 50 | } 51 | } // namespace 52 | -------------------------------------------------------------------------------- /spectator/id.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "absl/container/flat_hash_map.h" 4 | #include "absl/strings/string_view.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace spectator { 12 | 13 | class Tags { 14 | using table_t = absl::flat_hash_map; 15 | table_t entries_; 16 | 17 | public: 18 | Tags() = default; 19 | 20 | Tags(std::initializer_list> vs) { 21 | for (auto& pair : vs) { 22 | add(pair.first, pair.second); 23 | } 24 | } 25 | 26 | template 27 | static Tags from(Cont&& cont) { 28 | Tags tags; 29 | tags.entries_.reserve(cont.size()); 30 | for (auto&& kv : cont) { 31 | tags.add(kv.first, kv.second); 32 | } 33 | return tags; 34 | } 35 | 36 | void add(absl::string_view k, absl::string_view v) { 37 | entries_[k] = std::string(v); 38 | } 39 | 40 | [[nodiscard]] size_t hash() const { 41 | using hs = std::hash; 42 | size_t h = 0; 43 | for (const auto& entry : entries_) { 44 | h += (hs()(entry.first) << 1U) ^ hs()(entry.second); 45 | } 46 | return h; 47 | } 48 | 49 | void move_all(Tags&& source) { 50 | entries_.insert(std::make_move_iterator(source.begin()), 51 | std::make_move_iterator(source.end())); 52 | } 53 | 54 | bool operator==(const Tags& that) const { return that.entries_ == entries_; } 55 | 56 | [[nodiscard]] bool has(absl::string_view key) const { 57 | return entries_.find(key) != entries_.end(); 58 | } 59 | 60 | [[nodiscard]] std::string at(absl::string_view key) const { 61 | auto entry = entries_.find(key); 62 | if (entry != entries_.end()) { 63 | return entry->second; 64 | } 65 | return {}; 66 | } 67 | 68 | [[nodiscard]] size_t size() const { return entries_.size(); } 69 | 70 | [[nodiscard]] table_t::const_iterator begin() const { 71 | return entries_.begin(); 72 | } 73 | 74 | [[nodiscard]] table_t::const_iterator end() const { return entries_.end(); } 75 | }; 76 | 77 | class Id { 78 | public: 79 | Id(absl::string_view name, Tags tags) noexcept 80 | : name_(name), tags_(std::move(tags)), hash_(0u) {} 81 | 82 | static std::shared_ptr of(absl::string_view name, Tags tags = {}) { 83 | return std::make_shared(name, std::move(tags)); 84 | } 85 | 86 | bool operator==(const Id& rhs) const noexcept { 87 | return name_ == rhs.name_ && tags_ == rhs.tags_; 88 | } 89 | 90 | const std::string& Name() const noexcept { return name_; } 91 | 92 | const Tags& GetTags() const noexcept { return tags_; } 93 | 94 | std::unique_ptr WithTag(const std::string& key, 95 | const std::string& value) const { 96 | // Create a copy 97 | Tags tags{GetTags()}; 98 | tags.add(key, value); 99 | return std::make_unique(Name(), tags); 100 | } 101 | 102 | std::unique_ptr WithTags(Tags&& extra_tags) const { 103 | Tags tags{GetTags()}; 104 | tags.move_all(std::move(extra_tags)); 105 | return std::make_unique(Name(), tags); 106 | } 107 | 108 | std::unique_ptr WithTags(const Tags& extra_tags) const { 109 | Tags tags{GetTags()}; 110 | for (const auto& t : extra_tags) { 111 | tags.add(t.first, t.second); 112 | } 113 | return std::make_unique(Name(), tags); 114 | } 115 | 116 | std::unique_ptr WithStat(const std::string& stat) const { 117 | return WithTag("statistic", stat); 118 | }; 119 | 120 | static std::shared_ptr WithDefaultStat(std::shared_ptr baseId, 121 | const std::string& stat) { 122 | if (baseId->GetTags().has("statistic")) { 123 | return baseId; 124 | } else { 125 | return baseId->WithStat(stat); 126 | } 127 | } 128 | 129 | friend struct std::hash; 130 | 131 | friend struct std::hash>; 132 | 133 | private: 134 | std::string name_; 135 | Tags tags_; 136 | mutable size_t hash_; 137 | 138 | size_t Hash() const noexcept { 139 | if (hash_ == 0) { 140 | // compute hash code, and reuse it 141 | hash_ = tags_.hash() ^ std::hash()(name_); 142 | } 143 | return hash_; 144 | } 145 | }; 146 | 147 | using IdPtr = std::shared_ptr; 148 | 149 | } // namespace spectator 150 | 151 | namespace std { 152 | template <> 153 | struct hash { 154 | size_t operator()(const spectator::Id& id) const { return id.Hash(); } 155 | }; 156 | 157 | template <> 158 | struct hash { 159 | size_t operator()(const spectator::Tags& tags) const { return tags.hash(); } 160 | }; 161 | 162 | template <> 163 | struct hash> { 164 | size_t operator()(const shared_ptr& id) const { 165 | return id->Hash(); 166 | } 167 | }; 168 | 169 | template <> 170 | struct equal_to> { 171 | bool operator()(const shared_ptr& lhs, 172 | const shared_ptr& rhs) const { 173 | return *lhs == *rhs; 174 | } 175 | }; 176 | 177 | } // namespace std 178 | 179 | template <> struct fmt::formatter: fmt::formatter { 180 | auto format(const spectator::Tags& tags, format_context& ctx) const -> format_context::iterator { 181 | std::string s; 182 | auto size = tags.size(); 183 | 184 | if (size > 0) { 185 | // sort keys, to ensure stable output 186 | std::vector keys; 187 | for (const auto& pair : tags) { 188 | keys.push_back(pair.first); 189 | } 190 | std::sort(keys.begin(), keys.end()); 191 | 192 | s = "["; 193 | for (const auto &key : keys) { 194 | if (size > 1) { 195 | s += key + "=" + tags.at(key) + ", "; 196 | } else { 197 | s += key + "=" + tags.at(key) + "]"; 198 | } 199 | size -= 1; 200 | } 201 | } else { 202 | s = "[]"; 203 | } 204 | 205 | return fmt::formatter::format(s, ctx); 206 | } 207 | }; 208 | 209 | inline auto operator<<(std::ostream& os, const spectator::Tags& tags) -> std::ostream& { 210 | os << fmt::format("{}", tags); 211 | return os; 212 | } 213 | 214 | template <> struct fmt::formatter: fmt::formatter { 215 | static auto format(const spectator::Id& id, format_context& ctx) -> format_context::iterator { 216 | return fmt::format_to(ctx.out(), "Id(name={}, tags={})", id.Name(), id.GetTags()); 217 | } 218 | }; 219 | 220 | inline auto operator<<(std::ostream& os, const spectator::Id& id) -> std::ostream& { 221 | os << fmt::format("{}", id); 222 | return os; 223 | } 224 | -------------------------------------------------------------------------------- /spectator/id_test.cc: -------------------------------------------------------------------------------- 1 | #include "../spectator/id.h" 2 | #include 3 | 4 | namespace { 5 | 6 | using spectator::Id; 7 | using spectator::Tags; 8 | 9 | TEST(Id, Create) { 10 | Id id{"foo", Tags{}}; 11 | EXPECT_EQ(id.Name(), "foo"); 12 | EXPECT_EQ(id.GetTags().size(), 0); 13 | EXPECT_EQ(fmt::format("{}", id), "Id(name=foo, tags=[])"); 14 | 15 | Id id_tags_single{"name", Tags{{"k", "v"}}}; 16 | EXPECT_EQ(id_tags_single.Name(), "name"); 17 | EXPECT_EQ(id_tags_single.GetTags().size(), 1); 18 | EXPECT_EQ(fmt::format("{}", id_tags_single), "Id(name=name, tags=[k=v])"); 19 | 20 | Id id_tags_multiple{"name", Tags{{"k", "v"}, {"k1", "v1"}}}; 21 | EXPECT_EQ(id_tags_multiple.Name(), "name"); 22 | EXPECT_EQ(id_tags_multiple.GetTags().size(), 2); 23 | 24 | EXPECT_EQ(fmt::format("{}", id_tags_multiple), "Id(name=name, tags=[k=v, k1=v1])"); 25 | 26 | std::shared_ptr id_of{Id::of("name", Tags{{"k", "v"}, {"k1", "v1"}})}; 27 | EXPECT_EQ(id_of->Name(), "name"); 28 | EXPECT_EQ(id_of->GetTags().size(), 2); 29 | EXPECT_EQ(fmt::format("{}", *id_of), "Id(name=name, tags=[k=v, k1=v1])"); 30 | } 31 | 32 | TEST(Id, Tags) { 33 | Id id{"foo", Tags{}}; 34 | auto withTag = id.WithTag("k", "v"); 35 | Tags tags{{"k", "v"}}; 36 | EXPECT_EQ(tags, withTag->GetTags()); 37 | 38 | auto withStat = withTag->WithStat("count"); 39 | Tags tagsWithStat{{"k", "v"}, {"statistic", "count"}}; 40 | EXPECT_EQ(tagsWithStat, withStat->GetTags()); 41 | } 42 | } // namespace 43 | -------------------------------------------------------------------------------- /spectator/log_entry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "registry.h" 4 | #include "strings.h" 5 | #include "percentile_timer.h" 6 | 7 | namespace spectator { 8 | class LogEntry { 9 | public: 10 | LogEntry(Registry* registry, std::string method, const std::string& url) 11 | : registry_{registry}, 12 | start_{absl::Now()}, 13 | id_{registry_->CreateId("ipc.client.call", 14 | Tags{{"owner", "spectator-cpp"}, 15 | {"ipc.endpoint", PathFromUrl(url)}, 16 | {"http.method", std::move(method)}, 17 | {"http.status", "-1"}})} {} 18 | 19 | absl::Time start() const { return start_; } 20 | 21 | void log() { 22 | using millis = std::chrono::milliseconds; 23 | using std::chrono::seconds; 24 | registry_->GetPercentileTimer(id_, millis(1), seconds(5)) 25 | ->Record(absl::Now() - start_); 26 | } 27 | 28 | void set_status_code(int code) { 29 | id_ = id_->WithTag("http.status", fmt::format("{}", code)); 30 | } 31 | 32 | void set_attempt(int attempt_number, bool is_final) { 33 | id_ = id_->WithTag("ipc.attempt", attempt(attempt_number)) 34 | ->WithTag("ipc.attempt.final", is_final ? "true" : "false"); 35 | } 36 | 37 | void set_error(const std::string& error) { 38 | id_ = id_->WithTag("ipc.result", "failure")->WithTag("ipc.status", error); 39 | } 40 | 41 | void set_success() { 42 | const std::string ipc_success = "success"; 43 | id_ = id_->WithTag("ipc.status", ipc_success) 44 | ->WithTag("ipc.result", ipc_success); 45 | } 46 | 47 | private: 48 | Registry* registry_; 49 | absl::Time start_; 50 | IdPtr id_; 51 | 52 | std::string attempt(int attempt_number) { 53 | static std::string initial = "initial"; 54 | static std::string second = "second"; 55 | static std::string third_up = "third_up"; 56 | 57 | switch (attempt_number) { 58 | case 0: 59 | return initial; 60 | case 1: 61 | return second; 62 | default: 63 | return third_up; 64 | } 65 | } 66 | }; 67 | 68 | } // namespace spectator 69 | -------------------------------------------------------------------------------- /spectator/logger.cc: -------------------------------------------------------------------------------- 1 | #include "logger.h" 2 | #include 3 | #include 4 | #include 5 | 6 | namespace spectator { 7 | 8 | static constexpr const char* const kMainLogger = "spectator"; 9 | 10 | LogManager& log_manager() noexcept { 11 | static auto* the_log_manager = new LogManager(); 12 | return *the_log_manager; 13 | } 14 | 15 | LogManager::LogManager() noexcept { 16 | try { 17 | logger_ = spdlog::create_async_nb( 18 | kMainLogger); 19 | logger_->set_level(spdlog::level::debug); 20 | } catch (const spdlog::spdlog_ex& ex) { 21 | std::cerr << "Log initialization failed: " << ex.what() << "\n"; 22 | } 23 | } 24 | 25 | std::shared_ptr LogManager::Logger() noexcept { 26 | return logger_; 27 | } 28 | 29 | } // namespace spectator 30 | -------------------------------------------------------------------------------- /spectator/logger.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace spectator { 6 | 7 | class LogManager { 8 | public: 9 | LogManager() noexcept; 10 | std::shared_ptr Logger() noexcept; 11 | 12 | private: 13 | std::shared_ptr logger_; 14 | }; 15 | 16 | LogManager& log_manager() noexcept; 17 | 18 | inline std::shared_ptr DefaultLogger() noexcept { 19 | return log_manager().Logger(); 20 | } 21 | 22 | } // namespace spectator 23 | -------------------------------------------------------------------------------- /spectator/max_gauge_test.cc: -------------------------------------------------------------------------------- 1 | #include "stateless_meters.h" 2 | #include "test_publisher.h" 3 | #include 4 | 5 | namespace { 6 | 7 | using spectator::Id; 8 | using spectator::MaxGauge; 9 | using spectator::Tags; 10 | using spectator::TestPublisher; 11 | 12 | TEST(MaxGauge, Set) { 13 | TestPublisher publisher; 14 | auto id = std::make_shared("gauge", Tags{}); 15 | auto id2 = std::make_shared("gauge2", Tags{{"key", "val"}}); 16 | MaxGauge g{id, &publisher}; 17 | MaxGauge g2{id2, &publisher}; 18 | 19 | g.Set(42); 20 | g2.Update(2); 21 | g.Update(1); 22 | std::vector expected = {"m:gauge:42", "m:gauge2,key=val:2", 23 | "m:gauge:1"}; 24 | EXPECT_EQ(publisher.SentMessages(), expected); 25 | } 26 | 27 | TEST(MaxGauge, InvalidTags) { 28 | TestPublisher publisher; 29 | // test with a single tag, because tags order is not guaranteed in a flat_hash_map 30 | auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", 31 | Tags{{"tag1,:=", "value1,:="}}); 32 | MaxGauge g{id, &publisher}; 33 | EXPECT_EQ("m:test______^____-_~______________.___foo,tag1___=value1___:", g.GetPrefix()); 34 | } 35 | } // namespace 36 | -------------------------------------------------------------------------------- /spectator/measurement.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "id.h" 4 | #include 5 | 6 | namespace spectator { 7 | 8 | struct Measurement { 9 | IdPtr id; 10 | double value; 11 | 12 | bool operator==(const Measurement& other) const { 13 | return std::abs(value - other.value) < 1e-9 && *id == *(other.id); 14 | } 15 | 16 | Measurement(IdPtr idPtr, double v) : id(std::move(idPtr)), value(v) {} 17 | }; 18 | 19 | } // namespace spectator 20 | 21 | template <> struct fmt::formatter: formatter { 22 | static auto format(const spectator::Measurement& m, format_context& ctx) -> format_context::iterator { 23 | return fmt::format_to(ctx.out(), "Measurement({}, {})", *(m.id), m.value); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /spectator/meter_type.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace spectator { 6 | enum class MeterType { 7 | AgeGauge, 8 | Counter, 9 | DistSummary, 10 | Gauge, 11 | MaxGauge, 12 | MonotonicCounter, 13 | MonotonicCounterUint, 14 | PercentileDistSummary, 15 | PercentileTimer, 16 | Timer 17 | }; 18 | } 19 | 20 | template <> struct fmt::formatter: formatter { 21 | auto format(spectator::MeterType meter_type, format_context& ctx) const -> format_context::iterator { 22 | using namespace spectator; 23 | std::string_view s = "unknown"; 24 | 25 | switch (meter_type) { 26 | case MeterType::AgeGauge: 27 | s = "age-gauge"; 28 | break; 29 | case MeterType::Counter: 30 | s = "counter"; 31 | break; 32 | case MeterType::DistSummary: 33 | s = "distribution-summary"; 34 | break; 35 | case MeterType::Gauge: 36 | s = "gauge"; 37 | break; 38 | case MeterType::MaxGauge: 39 | s = "max-gauge"; 40 | break; 41 | case MeterType::MonotonicCounter: 42 | s = "monotonic-counter"; 43 | break; 44 | case MeterType::MonotonicCounterUint: 45 | s = "monotonic-counter-uint"; 46 | break; 47 | case MeterType::PercentileDistSummary: 48 | s = "percentile-distribution-summary"; 49 | break; 50 | case MeterType::PercentileTimer: 51 | s = "percentile-timer"; 52 | break; 53 | case MeterType::Timer: 54 | s = "timer"; 55 | break; 56 | } 57 | 58 | return fmt::formatter::format(s, ctx); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /spectator/meter_type_test.cc: -------------------------------------------------------------------------------- 1 | #include "../spectator/meter_type.h" 2 | #include 3 | 4 | namespace { 5 | 6 | using spectator::MeterType; 7 | 8 | TEST(MeterType, Format) { 9 | EXPECT_EQ(fmt::format("{}", MeterType::Counter), "counter"); 10 | } 11 | } // namespace 12 | -------------------------------------------------------------------------------- /spectator/monotonic_counter_test.cc: -------------------------------------------------------------------------------- 1 | #include "stateless_meters.h" 2 | #include "test_publisher.h" 3 | #include 4 | 5 | namespace { 6 | 7 | using spectator::Id; 8 | using spectator::MonotonicCounter; 9 | using spectator::Tags; 10 | using spectator::TestPublisher; 11 | 12 | TEST(MonotonicCounter, Set) { 13 | TestPublisher publisher; 14 | auto id = std::make_shared("ctr", Tags{}); 15 | auto id2 = std::make_shared("ctr2", Tags{{"key", "val"}}); 16 | MonotonicCounter c{id, &publisher}; 17 | MonotonicCounter c2{id2, &publisher}; 18 | 19 | c.Set(42.1); 20 | c2.Set(2); 21 | c.Set(43); 22 | std::vector expected = {"C:ctr:42.1", "C:ctr2,key=val:2", "C:ctr:43"}; 23 | EXPECT_EQ(publisher.SentMessages(), expected); 24 | } 25 | 26 | TEST(MonotonicCounter, InvalidTags) { 27 | TestPublisher publisher; 28 | // test with a single tag, because tags order is not guaranteed in a flat_hash_map 29 | auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", 30 | Tags{{"tag1,:=", "value1,:="}}); 31 | MonotonicCounter c{id, &publisher}; 32 | EXPECT_EQ("C:test______^____-_~______________.___foo,tag1___=value1___:", c.GetPrefix()); 33 | } 34 | } // namespace 35 | -------------------------------------------------------------------------------- /spectator/monotonic_counter_uint_test.cc: -------------------------------------------------------------------------------- 1 | #include "stateless_meters.h" 2 | #include "test_publisher.h" 3 | #include 4 | 5 | namespace { 6 | 7 | using spectator::Id; 8 | using spectator::MonotonicCounterUint; 9 | using spectator::Tags; 10 | using spectator::TestPublisher; 11 | 12 | TEST(MonotonicCounterUint, Set) { 13 | TestPublisher publisher; 14 | auto id = std::make_shared("ctr", Tags{}); 15 | auto id2 = std::make_shared("ctr2", Tags{{"key", "val"}}); 16 | MonotonicCounterUint c{id, &publisher}; 17 | MonotonicCounterUint c2{id2, &publisher}; 18 | 19 | c.Set(42); 20 | c2.Set(2); 21 | c.Set(-1); 22 | std::vector expected = {"U:ctr:42", "U:ctr2,key=val:2", "U:ctr:18446744073709551615"}; 23 | EXPECT_EQ(publisher.SentMessages(), expected); 24 | } 25 | 26 | TEST(MonotonicCounterUint, InvalidTags) { 27 | TestPublisher publisher; 28 | // test with a single tag, because tags order is not guaranteed in a flat_hash_map 29 | auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", 30 | Tags{{"tag1,:=", "value1,:="}}); 31 | MonotonicCounterUint c{id, &publisher}; 32 | EXPECT_EQ("U:test______^____-_~______________.___foo,tag1___=value1___:", c.GetPrefix()); 33 | } 34 | } // namespace -------------------------------------------------------------------------------- /spectator/perc_dist_summary_test.cc: -------------------------------------------------------------------------------- 1 | #include "stateless_meters.h" 2 | #include "test_publisher.h" 3 | #include 4 | 5 | namespace { 6 | using spectator::Id; 7 | using spectator::PercentileDistributionSummary; 8 | using spectator::Tags; 9 | using spectator::TestPublisher; 10 | 11 | TEST(PercDistSum, Record) { 12 | TestPublisher publisher; 13 | auto id = std::make_shared("pds", Tags{}); 14 | PercentileDistributionSummary d{id, &publisher, 0, 1000}; 15 | d.Record(50); 16 | d.Record(5000); 17 | d.Record(-5000); 18 | std::vector expected = {"D:pds:50", "D:pds:1000", 19 | "D:pds:0"}; 20 | EXPECT_EQ(publisher.SentMessages(), expected); 21 | } 22 | 23 | TEST(PercDistSum, InvalidTags) { 24 | TestPublisher publisher; 25 | // test with a single tag, because tags order is not guaranteed in a flat_hash_map 26 | auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", 27 | Tags{{"tag1,:=", "value1,:="}}); 28 | PercentileDistributionSummary d{id, &publisher, 0, 1000}; 29 | EXPECT_EQ("D:test______^____-_~______________.___foo,tag1___=value1___:", d.GetPrefix()); 30 | } 31 | } // namespace 32 | -------------------------------------------------------------------------------- /spectator/perc_timer_test.cc: -------------------------------------------------------------------------------- 1 | #include "stateless_meters.h" 2 | #include "test_publisher.h" 3 | #include 4 | 5 | namespace { 6 | using spectator::Id; 7 | using spectator::PercentileTimer; 8 | using spectator::Tags; 9 | using spectator::TestPublisher; 10 | 11 | TEST(PercentileTimer, Record) { 12 | TestPublisher publisher; 13 | auto id = std::make_shared("pt", Tags{}); 14 | PercentileTimer c{id, &publisher, absl::ZeroDuration(), absl::Seconds(5)}; 15 | c.Record(absl::Milliseconds(42)); 16 | c.Record(std::chrono::microseconds(500)); 17 | c.Record(absl::Seconds(10)); 18 | std::vector expected = {"T:pt:0.042", "T:pt:0.0005", "T:pt:5"}; 19 | EXPECT_EQ(publisher.SentMessages(), expected); 20 | } 21 | 22 | TEST(PercentileTimer, InvalidTags) { 23 | TestPublisher publisher; 24 | // test with a single tag, because tags order is not guaranteed in a flat_hash_map 25 | auto id = std::make_shared("test`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", 26 | Tags{{"tag1,:=", "value1,:="}}); 27 | PercentileTimer t{id, &publisher, absl::ZeroDuration(), absl::Seconds(5)}; 28 | EXPECT_EQ("T:test______^____-_~______________.___foo,tag1___=value1___:", t.GetPrefix()); 29 | } 30 | } // namespace 31 | -------------------------------------------------------------------------------- /spectator/publisher.cc: -------------------------------------------------------------------------------- 1 | #include "publisher.h" 2 | #include "logger.h" 3 | #include 4 | 5 | namespace spectator { 6 | 7 | static const char NEW_LINE = '\n'; 8 | 9 | SpectatordPublisher::SpectatordPublisher(absl::string_view endpoint, 10 | uint32_t bytes_to_buffer, 11 | std::shared_ptr logger) 12 | : logger_(std::move(logger)), 13 | udp_socket_(io_context_), 14 | local_socket_(io_context_), bytes_to_buffer_(bytes_to_buffer) { 15 | buffer_.reserve(bytes_to_buffer_ + 1024); 16 | if (absl::StartsWith(endpoint, "unix:")) { 17 | this->unixDomainPath_ = std::string(endpoint.substr(5)); 18 | setup_unix_domain(); 19 | } else if (absl::StartsWith(endpoint, "udp:")) { 20 | auto pos = 4; 21 | // if the user used udp://foo:1234 instead of udp:foo:1234 22 | // adjust accordingly 23 | if (endpoint.substr(pos, 2) == "//") { 24 | pos += 2; 25 | } 26 | setup_udp(endpoint.substr(pos)); 27 | } else if (endpoint != "disabled") { 28 | logger_->warn( 29 | "Unknown endpoint: '{}'. Expecting: 'unix:/path/to/socket'" 30 | " or 'udp:hostname:port' - Will not send metrics", 31 | std::string(endpoint)); 32 | setup_nop_sender(); 33 | } 34 | } 35 | 36 | void SpectatordPublisher::setup_nop_sender() { 37 | sender_ = [this](std::string_view msg) { logger_->trace("{}", msg); }; 38 | } 39 | 40 | void SpectatordPublisher::local_reconnect(absl::string_view path) { 41 | using endpoint_t = asio::local::datagram_protocol::endpoint; 42 | try { 43 | if (local_socket_.is_open()) { 44 | local_socket_.close(); 45 | } 46 | local_socket_.open(); 47 | local_socket_.connect(endpoint_t(std::string(path))); 48 | } catch (std::exception& e) { 49 | logger_->warn("Unable to connect to {}: {}", std::string(path), e.what()); 50 | } 51 | } 52 | 53 | 54 | bool SpectatordPublisher::try_to_send(const std::string& buffer) { 55 | for (auto i = 0; i < 3; ++i) { 56 | try { 57 | auto sent_bytes = local_socket_.send(asio::buffer(buffer)); 58 | logger_->trace("Sent (local): {} bytes, in total had {}", sent_bytes, 59 | buffer.length()); 60 | return true; 61 | } catch (std::exception& e) { 62 | local_reconnect(this->unixDomainPath_); 63 | logger_->warn("Unable to send {} - attempt {}/3 ({})", buffer, i, e.what()); 64 | } 65 | } 66 | return false; 67 | } 68 | 69 | void SpectatordPublisher::taskThreadFunction() try { 70 | while (shutdown_.load() == false) { 71 | std::string message {}; 72 | { 73 | std::unique_lock lock(mtx_); 74 | cv_sender_.wait(lock, [this] { return buffer_.size() > bytes_to_buffer_ || shutdown_.load();}); 75 | if (shutdown_.load() == true) { 76 | return; 77 | } 78 | message = std::move(buffer_); 79 | buffer_ = std::string(); 80 | buffer_.reserve(bytes_to_buffer_); 81 | } 82 | cv_receiver_.notify_one(); 83 | try_to_send(message); 84 | } 85 | } catch (const std::exception& e) { 86 | logger_->error("Fatal error in message processing thread: {}", e.what()); 87 | } 88 | 89 | void SpectatordPublisher::setup_unix_domain(){ 90 | // Reset connection to the unix domain socket 91 | local_reconnect(this->unixDomainPath_); 92 | if (bytes_to_buffer_ == 0) { 93 | sender_ = [this](std::string_view msg) { 94 | try_to_send(std::string(msg)); 95 | }; 96 | return; 97 | } 98 | else { 99 | sender_ = [this](std::string_view msg) { 100 | unsigned int currentBufferSize = buffer_.size(); 101 | { 102 | std::unique_lock lock(mtx_); 103 | cv_receiver_.wait(lock, [this] { return buffer_.size() <= bytes_to_buffer_ || shutdown_.load(); }); 104 | if (shutdown_.load()) { 105 | return; 106 | } 107 | buffer_.append(msg.data(), msg.size()); 108 | buffer_.append(1, NEW_LINE); 109 | currentBufferSize = buffer_.size(); 110 | } 111 | currentBufferSize > bytes_to_buffer_ ? cv_sender_.notify_one() : cv_receiver_.notify_one(); 112 | }; 113 | this->sendingThread_ = std::thread(&SpectatordPublisher::taskThreadFunction, this); 114 | } 115 | } 116 | 117 | inline asio::ip::udp::endpoint resolve_host_port( 118 | asio::io_context& io_context, // NOLINT 119 | absl::string_view host_port) { 120 | using asio::ip::udp; 121 | udp::resolver resolver{io_context}; 122 | 123 | auto end_host = host_port.find(':'); 124 | if (end_host == std::string_view::npos) { 125 | auto err = fmt::format( 126 | "Unable to parse udp endpoint: '{}'. Expecting hostname:port", 127 | std::string(host_port)); 128 | throw std::runtime_error(err); 129 | } 130 | 131 | auto host = host_port.substr(0, end_host); 132 | auto port = host_port.substr(end_host + 1); 133 | return *resolver.resolve(udp::v6(), std::string(host), std::string(port)); 134 | } 135 | 136 | void SpectatordPublisher::udp_reconnect( 137 | const asio::ip::udp::endpoint& endpoint) { 138 | try { 139 | if (udp_socket_.is_open()) { 140 | udp_socket_.close(); 141 | } 142 | udp_socket_.open(asio::ip::udp::v6()); 143 | udp_socket_.connect(endpoint); 144 | } catch (std::exception& e) { 145 | logger_->warn("Unable to connect to {}: {}", endpoint.address().to_string(), 146 | endpoint.port()); 147 | } 148 | } 149 | 150 | void SpectatordPublisher::setup_udp(absl::string_view host_port) { 151 | auto endpoint = resolve_host_port(io_context_, host_port); 152 | udp_reconnect(endpoint); 153 | sender_ = [endpoint, this](std::string_view msg) { 154 | for (auto i = 0; i < 3; ++i) { 155 | try { 156 | udp_socket_.send(asio::buffer(msg)); 157 | logger_->trace("Sent (udp): {}", msg); 158 | break; 159 | } catch (std::exception& e) { 160 | logger_->warn("Unable to send {} - attempt {}/3", msg, i); 161 | udp_reconnect(endpoint); 162 | } 163 | } 164 | }; 165 | } 166 | } // namespace spectator 167 | -------------------------------------------------------------------------------- /spectator/publisher.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "logger.h" 4 | #include "absl/strings/match.h" 5 | #include "absl/strings/string_view.h" 6 | #include 7 | 8 | namespace spectator { 9 | 10 | class SpectatordPublisher { 11 | public: 12 | explicit SpectatordPublisher( 13 | absl::string_view endpoint, 14 | uint32_t bytes_to_buffer = 0, 15 | std::shared_ptr logger = DefaultLogger()); 16 | SpectatordPublisher(const SpectatordPublisher&) = delete; 17 | 18 | ~SpectatordPublisher() { 19 | shutdown_.store(true); 20 | cv_receiver_.notify_all(); 21 | cv_sender_.notify_all(); 22 | if (sendingThread_.joinable()) { 23 | sendingThread_.join(); 24 | } 25 | } 26 | 27 | void send(std::string_view measurement) { sender_(measurement); }; 28 | 29 | void taskThreadFunction(); 30 | bool try_to_send(const std::string& buffer); 31 | 32 | protected: 33 | using sender_fun = std::function; 34 | sender_fun sender_; 35 | 36 | private: 37 | void setup_nop_sender(); 38 | void setup_unix_domain(); 39 | void setup_udp(absl::string_view host_port); 40 | void local_reconnect(absl::string_view path); 41 | void udp_reconnect(const asio::ip::udp::endpoint& endpoint); 42 | 43 | std::shared_ptr logger_; 44 | asio::io_context io_context_; 45 | asio::ip::udp::socket udp_socket_; 46 | asio::local::datagram_protocol::socket local_socket_; 47 | std::string buffer_; 48 | uint32_t bytes_to_buffer_; 49 | 50 | std::thread sendingThread_; 51 | std::mutex mtx_; 52 | std::condition_variable cv_receiver_; 53 | std::condition_variable cv_sender_; 54 | std::string unixDomainPath_; 55 | std::atomic shutdown_{false}; 56 | }; 57 | 58 | } // namespace spectator 59 | -------------------------------------------------------------------------------- /spectator/publisher_test.cc: -------------------------------------------------------------------------------- 1 | #include "id.h" 2 | #include "logger.h" 3 | #include "publisher.h" 4 | #include "stateless_meters.h" 5 | #include "test_server.h" 6 | #include 7 | #include 8 | #include 9 | 10 | namespace { 11 | 12 | using spectator::Counter; 13 | using spectator::Id; 14 | using spectator::SpectatordPublisher; 15 | using spectator::Tags; 16 | 17 | TEST(Publisher, Udp) { 18 | // travis does not support udp on its container 19 | if (std::getenv("TRAVIS_COMPILER") == nullptr) { 20 | TestUdpServer server; 21 | server.Start(); 22 | auto logger = spectator::DefaultLogger(); 23 | logger->info("Udp Server started on port {}", server.GetPort()); 24 | 25 | SpectatordPublisher publisher{ 26 | fmt::format("udp:localhost:{}", server.GetPort()), 0}; 27 | Counter c{std::make_shared("counter", Tags{}), &publisher}; 28 | c.Increment(); 29 | c.Add(2); 30 | std::this_thread::sleep_for(std::chrono::milliseconds(50)); 31 | auto msgs = server.GetMessages(); 32 | server.Stop(); 33 | std::vector expected{"c:counter:1", "c:counter:2"}; 34 | EXPECT_EQ(server.GetMessages(), expected); 35 | } 36 | } 37 | 38 | const char* first_not_null(char* a, const char* b) { 39 | if (a != nullptr) return a; 40 | return b; 41 | } 42 | 43 | TEST(Publisher, UnixNoBuffer) { 44 | auto logger = spectator::DefaultLogger(); 45 | const auto* dir = first_not_null(std::getenv("TMPDIR"), "/tmp"); 46 | auto path = fmt::format("{}/testserver.{}", dir, getpid()); 47 | TestUnixServer server{path}; 48 | server.Start(); 49 | logger->info("Unix Server started on path {}", path); 50 | SpectatordPublisher publisher{fmt::format("unix:{}", path), 0}; 51 | Counter c{std::make_shared("counter", Tags{}), &publisher}; 52 | c.Increment(); 53 | c.Add(2); 54 | std::this_thread::sleep_for(std::chrono::milliseconds(50)); 55 | auto msgs = server.GetMessages(); 56 | server.Stop(); 57 | unlink(path.c_str()); 58 | std::vector expected{"c:counter:1", "c:counter:2"}; 59 | EXPECT_EQ(msgs, expected); 60 | } 61 | 62 | TEST(Publisher, UnixBuffer) { 63 | auto logger = spectator::DefaultLogger(); 64 | const auto* dir = first_not_null(std::getenv("TMPDIR"), "/tmp"); 65 | auto path = fmt::format("{}/testserver.{}", dir, getpid()); 66 | TestUnixServer server{path}; 67 | server.Start(); 68 | logger->info("Unix Server started on path {}", path); 69 | // Do not send until we buffer 32 bytes of data. 70 | SpectatordPublisher publisher{fmt::format("unix:{}", path), 32}; 71 | Counter c{std::make_shared("counter", Tags{}), &publisher}; 72 | c.Increment(); 73 | c.Increment(); 74 | std::this_thread::sleep_for(std::chrono::milliseconds(50)); 75 | auto msgs = server.GetMessages(); 76 | std::vector emptyVector {}; 77 | EXPECT_EQ(msgs, emptyVector); 78 | c.Increment(); 79 | std::this_thread::sleep_for(std::chrono::milliseconds(50)); 80 | msgs = server.GetMessages(); 81 | std::vector expected{"c:counter:1\nc:counter:1\nc:counter:1\n"}; 82 | EXPECT_EQ(msgs, expected); 83 | server.Stop(); 84 | unlink(path.c_str()); 85 | } 86 | 87 | TEST(Publisher, Nop) { 88 | SpectatordPublisher publisher{"", 0}; 89 | Counter c{std::make_shared("counter", Tags{}), &publisher}; 90 | c.Increment(); 91 | c.Add(2); 92 | } 93 | 94 | TEST(Publisher, MultiThreadedCounters) { 95 | auto logger = spectator::DefaultLogger(); 96 | const auto* dir = first_not_null(std::getenv("TMPDIR"), "/tmp"); 97 | auto path = fmt::format("{}/testserver.{}", dir, getpid()); 98 | TestUnixServer server{path}; 99 | server.Start(); 100 | logger->info("Unix Server started on path {}", path); 101 | 102 | // Create publisher with a small buffer size to ensure flushing 103 | SpectatordPublisher publisher{fmt::format("unix:{}", path), 50}; 104 | 105 | // Number of threads and counters to create 106 | const int numThreads = 4; 107 | const int countersPerThread = 3; 108 | const int incrementsPerCounter = 5; 109 | 110 | // Function for worker threads 111 | auto worker = [&](int threadId) { 112 | // Create several counters per thread with unique names 113 | for (int i = 0; i < countersPerThread; i++) { 114 | std::string counterName = fmt::format("counter.thread{}.{}", threadId, i); 115 | Counter counter(std::make_shared(counterName, Tags{}), &publisher); 116 | 117 | // Increment each counter multiple times 118 | for (int j = 0; j < incrementsPerCounter; j++) { 119 | counter.Increment(); 120 | } 121 | } 122 | }; 123 | 124 | // Start worker threads 125 | std::vector threads; 126 | for (int i = 0; i < numThreads; i++) { 127 | threads.emplace_back(worker, i); 128 | } 129 | 130 | // Wait for all threads to complete 131 | for (auto& t : threads) { 132 | t.join(); 133 | } 134 | 135 | // Give some time for messages to be sent 136 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 137 | 138 | // Check messages 139 | auto msgs = server.GetMessages(); 140 | EXPECT_FALSE(msgs.empty()); 141 | 142 | // Verify total number of increments 143 | int expectedIncrements = numThreads * countersPerThread * incrementsPerCounter; 144 | int actualIncrements = 0; 145 | 146 | // Verify every string in msgs follows the form counter.thread. 147 | std::regex counter_regex(R"(c:counter\.thread\d+\.\d+:1)"); 148 | for (const auto& msg : msgs) { 149 | std::stringstream ss(msg); 150 | std::string line; 151 | while (std::getline(ss, line)) { 152 | if (!line.empty()) { 153 | EXPECT_TRUE(std::regex_match(line, counter_regex)) 154 | << "Unexpected counter format: " << line; 155 | actualIncrements++; 156 | } 157 | } 158 | } 159 | 160 | EXPECT_EQ(actualIncrements, expectedIncrements); 161 | 162 | server.Stop(); 163 | unlink(path.c_str()); 164 | } 165 | 166 | } // namespace 167 | -------------------------------------------------------------------------------- /spectator/registry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "absl/container/flat_hash_map.h" 4 | #include "absl/synchronization/mutex.h" 5 | #include "config.h" 6 | #include "logger.h" 7 | #include "stateful_meters.h" 8 | #include "stateless_meters.h" 9 | #include "publisher.h" 10 | 11 | namespace spectator { 12 | 13 | // A registry for tests 14 | // This is a stateful registry that will keep references to all registered 15 | // meters and allows users to fetch the measurements at a later point 16 | 17 | namespace detail { 18 | inline void log_type_error(MeterType old_type, MeterType new_type, 19 | const Id& id) { 20 | DefaultLogger()->warn( 21 | "Attempting to register {} as a {} but was previously registered as a {}", 22 | id, new_type, old_type); 23 | } 24 | } // namespace detail 25 | 26 | template 27 | struct single_table_state { 28 | using types = Types; 29 | 30 | template 31 | std::shared_ptr get_or_create(IdPtr id, Args&&... args) { 32 | auto new_meter = 33 | std::make_shared(std::move(id), std::forward(args)...); 34 | absl::MutexLock lock(&mutex_); 35 | auto it = meters_.find(new_meter->MeterId()); 36 | if (it != meters_.end()) { 37 | // already exists, we need to ensure the existing type 38 | // matches the new meter type, otherwise we need to notify the user 39 | // of the error 40 | auto& old_meter = it->second; 41 | if (old_meter->GetType() != new_meter->GetType()) { 42 | detail::log_type_error(old_meter->GetType(), new_meter->GetType(), 43 | *new_meter->MeterId()); 44 | // this is not registered therefore no measurements 45 | // will be reported 46 | return new_meter; 47 | } else { 48 | return std::static_pointer_cast(old_meter); 49 | } 50 | } 51 | 52 | meters_.emplace(new_meter->MeterId(), new_meter); 53 | return new_meter; 54 | } 55 | 56 | auto get_age_gauge(IdPtr id) { 57 | return get_or_create(std::move(id)); 58 | } 59 | 60 | auto get_counter(IdPtr id) { 61 | return get_or_create(std::move(id)); 62 | } 63 | 64 | auto get_ds(IdPtr id) { 65 | return get_or_create(std::move(id)); 66 | } 67 | 68 | auto get_gauge(IdPtr id) { 69 | return get_or_create(std::move(id)); 70 | } 71 | 72 | auto get_gauge_ttl(IdPtr id, unsigned int ttl_seconds) { 73 | return get_or_create(std::move(id), ttl_seconds); 74 | } 75 | 76 | auto get_max_gauge(IdPtr id) { 77 | return get_or_create(std::move(id)); 78 | } 79 | 80 | auto get_monotonic_counter(IdPtr id) { 81 | return get_or_create(std::move(id)); 82 | } 83 | 84 | auto get_monotonic_counter_uint(IdPtr id) { 85 | return get_or_create(std::move(id)); 86 | } 87 | 88 | auto get_perc_ds(IdPtr id, int64_t min, int64_t max) { 89 | return get_or_create(std::move(id), min, max); 90 | } 91 | 92 | auto get_perc_timer(IdPtr id, std::chrono::nanoseconds min, 93 | std::chrono::nanoseconds max) { 94 | return get_or_create(std::move(id), min, max); 95 | } 96 | 97 | auto get_timer(IdPtr id) { 98 | return get_or_create(std::move(id)); 99 | } 100 | 101 | auto measurements() { 102 | std::vector result; 103 | 104 | absl::MutexLock lock(&mutex_); 105 | result.reserve(meters_.size() * 2); 106 | for (auto& m : meters_) { 107 | m.second->Measure(&result); 108 | } 109 | return result; 110 | } 111 | 112 | absl::Mutex mutex_; 113 | // use a single table, so we can easily check whether a meter 114 | // was previously registered as a different type 115 | absl::flat_hash_map, std::hash, 116 | std::equal_to> 117 | meters_ ABSL_GUARDED_BY(mutex_); 118 | }; 119 | 120 | template 121 | class base_registry { 122 | public: 123 | using logger_ptr = std::shared_ptr; 124 | using age_gauge_t = typename Types::age_gauge_t; 125 | using age_gauge_ptr = std::shared_ptr; 126 | using counter_t = typename Types::counter_t; 127 | using counter_ptr = std::shared_ptr; 128 | using dist_summary_t = typename Types::ds_t; 129 | using dist_summary_ptr = std::shared_ptr; 130 | using gauge_t = typename Types::gauge_t; 131 | using gauge_ptr = std::shared_ptr; 132 | using max_gauge_t = typename Types::max_gauge_t; 133 | using max_gauge_ptr = std::shared_ptr; 134 | using monotonic_counter_t = typename Types::monotonic_counter_t; 135 | using monotonic_counter_ptr = std::shared_ptr; 136 | using monotonic_counter_uint_t = typename Types::monotonic_counter_uint_t; 137 | using monotonic_counter_uint_ptr = std::shared_ptr; 138 | using perc_dist_summary_t = typename Types::perc_ds_t; 139 | using perc_dist_summary_ptr = std::shared_ptr; 140 | using perc_timer_t = typename Types::perc_timer_t; 141 | using perc_timer_ptr = std::shared_ptr; 142 | using timer_t = typename Types::timer_t; 143 | using timer_ptr = std::shared_ptr; 144 | 145 | explicit base_registry(logger_ptr logger = DefaultLogger()) 146 | : logger_(std::move(logger)) {} 147 | 148 | auto GetAgeGauge(const IdPtr& id) { 149 | return state_.get_age_gauge(final_id(id)); 150 | } 151 | auto GetAgeGauge(absl::string_view name, Tags tags = {}) { 152 | return GetAgeGauge(Id::of(name, std::move(tags))); 153 | } 154 | 155 | auto GetCounter(const IdPtr& id) { 156 | return state_.get_counter(final_id(id)); 157 | } 158 | auto GetCounter(absl::string_view name, Tags tags = {}) { 159 | return GetCounter(Id::of(name, std::move(tags))); 160 | } 161 | 162 | auto GetDistributionSummary(const IdPtr& id) { 163 | return state_.get_ds(final_id(id)); 164 | } 165 | auto GetDistributionSummary(absl::string_view name, Tags tags = {}) { 166 | return GetDistributionSummary(Id::of(name, std::move(tags))); 167 | } 168 | 169 | auto GetGauge(const IdPtr& id) { 170 | return state_.get_gauge(final_id(id)); 171 | } 172 | auto GetGauge(absl::string_view name, Tags tags = {}) { 173 | return GetGauge(Id::of(name, std::move(tags))); 174 | } 175 | 176 | auto GetGaugeTTL(const IdPtr& id, unsigned int ttl_seconds) { 177 | return state_.get_gauge_ttl(final_id(id), ttl_seconds); 178 | } 179 | 180 | auto GetGaugeTTL(absl::string_view name, unsigned int ttl_seconds, Tags tags = {}) { 181 | return GetGaugeTTL(Id::of(name, std::move(tags)), ttl_seconds); 182 | } 183 | 184 | auto GetMaxGauge(const IdPtr& id) { 185 | return state_.get_max_gauge(final_id(id)); 186 | } 187 | auto GetMaxGauge(absl::string_view name, Tags tags = {}) { 188 | return GetMaxGauge(Id::of(name, std::move(tags))); 189 | } 190 | 191 | auto GetMonotonicCounter(const IdPtr& id) { 192 | return state_.get_monotonic_counter(final_id(id)); 193 | } 194 | auto GetMonotonicCounter(absl::string_view name, Tags tags = {}) { 195 | return GetMonotonicCounter(Id::of(name, std::move(tags))); 196 | } 197 | 198 | auto GetMonotonicCounterUint(const IdPtr& id) { 199 | return state_.get_monotonic_counter_uint(final_id(id)); 200 | } 201 | auto GetMonotonicCounterUint(absl::string_view name, Tags tags = {}) { 202 | return GetMonotonicCounterUint(Id::of(name, std::move(tags))); 203 | } 204 | 205 | auto GetPercentileDistributionSummary(const IdPtr& id, int64_t min, int64_t max) { 206 | return state_.get_perc_ds(final_id(id), min, max); 207 | } 208 | auto GetPercentileDistributionSummary(absl::string_view name, int64_t min, int64_t max) { 209 | return GetPercentileDistributionSummary(Id::of(name), min, max); 210 | } 211 | auto GetPercentileDistributionSummary(absl::string_view name, Tags tags, 212 | int64_t min, int64_t max) { 213 | return GetPercentileDistributionSummary(Id::of(name, std::move(tags)), min, max); 214 | } 215 | 216 | auto GetPercentileTimer(const IdPtr& id, absl::Duration min, absl::Duration max) { 217 | return state_.get_perc_timer(final_id(id), min, max); 218 | } 219 | auto GetPercentileTimer(const IdPtr& id, std::chrono::nanoseconds min, 220 | std::chrono::nanoseconds max) { 221 | return state_.get_perc_timer(final_id(id), absl::FromChrono(min), absl::FromChrono(max)); 222 | } 223 | auto GetPercentileTimer(absl::string_view name, absl::Duration min, absl::Duration max) { 224 | return GetPercentileTimer(Id::of(name), min, max); 225 | } 226 | auto GetPercentileTimer(absl::string_view name, Tags tags, 227 | absl::Duration min, absl::Duration max) { 228 | return GetPercentileTimer(Id::of(name, std::move(tags)), min, max); 229 | } 230 | auto GetPercentileTimer(absl::string_view name, 231 | std::chrono::nanoseconds min, std::chrono::nanoseconds max) { 232 | return GetPercentileTimer(Id::of(name), absl::FromChrono(min), absl::FromChrono(max)); 233 | } 234 | auto GetPercentileTimer(absl::string_view name, Tags tags, 235 | std::chrono::nanoseconds min, std::chrono::nanoseconds max) { 236 | return GetPercentileTimer(Id::of(name, std::move(tags)), absl::FromChrono(min), 237 | absl::FromChrono(max)); 238 | } 239 | 240 | auto GetTimer(const IdPtr& id) { 241 | return state_.get_timer(final_id(id)); 242 | } 243 | auto GetTimer(absl::string_view name, Tags tags = {}) { 244 | return GetTimer(Id::of(name, std::move(tags))); 245 | } 246 | 247 | auto Measurements() { return state_.measurements(); } 248 | 249 | protected: 250 | logger_ptr logger_; 251 | State state_; 252 | Tags extra_tags_; 253 | 254 | // final Id after adding extra_tags_ if any 255 | IdPtr final_id(const IdPtr& id) { 256 | if (extra_tags_.size() > 0) { 257 | return id->WithTags(extra_tags_); 258 | } 259 | return id; 260 | } 261 | }; 262 | 263 | template 264 | struct stateless_types { 265 | using counter_t = Counter; 266 | using ds_t = DistributionSummary; 267 | using gauge_t = Gauge; 268 | using max_gauge_t = MaxGauge; 269 | using age_gauge_t = AgeGauge; 270 | using monotonic_counter_t = MonotonicCounter; 271 | using monotonic_counter_uint_t = MonotonicCounterUint; 272 | using perc_timer_t = PercentileTimer; 273 | using perc_ds_t = PercentileDistributionSummary; 274 | using timer_t = Timer; 275 | using publisher_t = Pub; 276 | }; 277 | 278 | template 279 | struct stateless { 280 | using types = Types; 281 | std::unique_ptr publisher; 282 | 283 | auto get_age_gauge(IdPtr id) { 284 | return std::make_shared(std::move(id), publisher.get()); 285 | } 286 | 287 | auto get_counter(IdPtr id) { 288 | return std::make_shared(std::move(id), publisher.get()); 289 | } 290 | 291 | auto get_ds(IdPtr id) { 292 | return std::make_shared(std::move(id), publisher.get()); 293 | } 294 | 295 | auto get_gauge(IdPtr id) { 296 | return std::make_shared(std::move(id), publisher.get()); 297 | } 298 | 299 | auto get_gauge_ttl(IdPtr id, unsigned int ttl_seconds) { 300 | return std::make_shared(std::move(id), publisher.get(), ttl_seconds); 301 | } 302 | 303 | auto get_max_gauge(IdPtr id) { 304 | return std::make_shared(std::move(id), publisher.get()); 305 | } 306 | 307 | auto get_monotonic_counter(IdPtr id) { 308 | return std::make_shared(std::move(id), publisher.get()); 309 | } 310 | 311 | auto get_monotonic_counter_uint(IdPtr id) { 312 | return std::make_shared(std::move(id), 313 | publisher.get()); 314 | } 315 | 316 | auto get_perc_ds(IdPtr id, int64_t min, int64_t max) { 317 | return std::make_shared(std::move(id), publisher.get(), min, max); 318 | } 319 | 320 | auto get_perc_timer(IdPtr id, absl::Duration min, absl::Duration max) { 321 | return std::make_shared(std::move(id), publisher.get(), min, max); 322 | } 323 | 324 | auto get_timer(IdPtr id) { 325 | return std::make_shared(std::move(id), publisher.get()); 326 | } 327 | 328 | auto measurements() { return std::vector{}; } 329 | }; 330 | 331 | /// A stateless registry that sends all meter activity immediately 332 | /// to a spectatord agent 333 | class SpectatordRegistry 334 | : public base_registry>> { 335 | public: 336 | using types = stateless_types; 337 | explicit SpectatordRegistry(const Config& config, logger_ptr logger) 338 | : base_registry>>( 339 | std::move(logger)) { 340 | extra_tags_ = Tags::from(config.common_tags); 341 | state_.publisher = 342 | std::make_unique(config.endpoint, config.bytes_to_buffer, logger_); 343 | } 344 | }; 345 | 346 | /// A Registry that can be used for tests. It keeps state about which meters 347 | /// have been registered, and can report the measurements from all the 348 | /// registered meters 349 | struct TestRegistry : base_registry> { 350 | using types = stateful_meters; 351 | }; 352 | 353 | /// The default registry 354 | using Registry = SpectatordRegistry; 355 | 356 | } // namespace spectator 357 | -------------------------------------------------------------------------------- /spectator/stateful_meters.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "id.h" 4 | #include "measurement.h" 5 | #include "meter_type.h" 6 | 7 | namespace spectator { 8 | 9 | namespace detail { 10 | /// Atomically add a delta to an atomic double 11 | /// equivalent to fetch_add for integer types 12 | inline void add_double(std::atomic* n, double delta) { 13 | double current; 14 | do { 15 | current = n->load(std::memory_order_relaxed); 16 | } while (!n->compare_exchange_weak( 17 | current, n->load(std::memory_order_relaxed) + delta)); 18 | } 19 | 20 | /// Atomically set the max value of an atomic number 21 | template 22 | inline void update_max(std::atomic* n, T value) { 23 | T current; 24 | do { 25 | current = n->load(std::memory_order_relaxed); 26 | } while (value > current && !n->compare_exchange_weak(current, value)); 27 | } 28 | } // namespace detail 29 | 30 | class StatefulMeter { 31 | public: 32 | explicit StatefulMeter(IdPtr id) : id_{std::move(id)} {} 33 | StatefulMeter(const StatefulMeter&) = default; 34 | virtual ~StatefulMeter() = default; 35 | virtual void Measure(std::vector* measurements) = 0; 36 | [[nodiscard]] virtual MeterType GetType() const = 0; 37 | [[nodiscard]] IdPtr MeterId() const { return id_; } 38 | 39 | protected: 40 | IdPtr id_; 41 | }; 42 | 43 | template 44 | class TestDistribution : public StatefulMeter { 45 | public: 46 | explicit TestDistribution(IdPtr id) : StatefulMeter(std::move(id)) {} 47 | 48 | int64_t Count() const { return count_; } 49 | 50 | double TotalAmount() const { return total_; } 51 | 52 | MeterType GetType() const override { return DistType::meter_type; } 53 | 54 | void Measure(std::vector* measurements) override { 55 | auto cnt = count_.exchange(0); 56 | if (cnt == 0) { 57 | return; 58 | } 59 | auto total = total_.exchange(0); 60 | auto t_sq = totalSq_.exchange(0); 61 | auto mx = max_.exchange(0); 62 | measurements->emplace_back(id_->WithStat(DistType::total_name), total); 63 | measurements->emplace_back(id_->WithStat("totalOfSquares"), t_sq); 64 | measurements->emplace_back(id_->WithStat("max"), mx); 65 | measurements->emplace_back(id_->WithStat("count"), cnt); 66 | } 67 | 68 | protected: 69 | void record(double amount) { 70 | if (amount >= 0) { 71 | count_.fetch_add(1); 72 | detail::add_double(&total_, amount); 73 | detail::add_double(&totalSq_, amount * amount); 74 | detail::update_max(&max_, amount); 75 | } 76 | } 77 | 78 | private: 79 | std::atomic count_ = 0; 80 | std::atomic total_ = 0; 81 | std::atomic totalSq_ = 0; 82 | std::atomic max_ = 0; 83 | }; 84 | 85 | struct timer_distribution { 86 | static constexpr auto meter_type = MeterType::Timer; 87 | static constexpr auto total_name = "totalTime"; 88 | }; 89 | 90 | struct summary_distribution { 91 | static constexpr auto meter_type = MeterType::DistSummary; 92 | static constexpr auto total_name = "totalAmount"; 93 | }; 94 | 95 | class StatefulAgeGauge : public StatefulMeter { 96 | public: 97 | explicit StatefulAgeGauge(IdPtr id) : StatefulMeter(std::move(id)) {} 98 | 99 | [[nodiscard]] double Get() const { return value_; } 100 | 101 | MeterType GetType() const override { return MeterType::AgeGauge; } 102 | 103 | void Set(double amount) { value_ = amount; } 104 | 105 | void Measure(std::vector* measurements) override { 106 | auto v = value_.exchange(kNaN); 107 | if (std::isnan(v)) { 108 | return; 109 | } 110 | measurements->emplace_back(Id::WithDefaultStat(id_, "gauge"), v); 111 | } 112 | 113 | private: 114 | static constexpr auto kNaN = std::numeric_limits::quiet_NaN(); 115 | std::atomic value_ = kNaN; 116 | }; 117 | 118 | class StatefulCounter : public StatefulMeter { 119 | public: 120 | explicit StatefulCounter(IdPtr id) : StatefulMeter(std::move(id)) {} 121 | 122 | [[nodiscard]] double Count() const { return count_; }; 123 | 124 | MeterType GetType() const override { return MeterType::Counter; } 125 | 126 | void Add(double delta) { 127 | if (delta > 0) { 128 | detail::add_double(&count_, delta); 129 | } 130 | } 131 | 132 | void Increment() { Add(1); } 133 | 134 | void Measure(std::vector* measurements) override { 135 | auto count = count_.exchange(0.0); 136 | if (count > 0) { 137 | measurements->emplace_back(Id::WithDefaultStat(id_, "count"), count); 138 | } 139 | } 140 | 141 | private: 142 | std::atomic count_ = 0.0; 143 | }; 144 | 145 | class StatefulDistSum : public TestDistribution { 146 | public: 147 | explicit StatefulDistSum(IdPtr id): TestDistribution(std::move(id)) {} 148 | 149 | void Record(double amount) { record(amount); } 150 | }; 151 | 152 | class StatefulGauge : public StatefulMeter { 153 | public: 154 | explicit StatefulGauge(IdPtr id) : StatefulMeter(std::move(id)) {} 155 | 156 | [[nodiscard]] double Get() const { return value_; } 157 | 158 | MeterType GetType() const override { return MeterType::Gauge; } 159 | 160 | void Set(double amount) { value_ = amount; } 161 | 162 | void Measure(std::vector* measurements) override { 163 | auto v = value_.exchange(kNaN); 164 | if (std::isnan(v)) { 165 | return; 166 | } 167 | measurements->emplace_back(Id::WithDefaultStat(id_, "gauge"), v); 168 | } 169 | 170 | private: 171 | static constexpr auto kNaN = std::numeric_limits::quiet_NaN(); 172 | std::atomic value_ = kNaN; 173 | }; 174 | 175 | class StatefulMaxGauge : public StatefulMeter { 176 | public: 177 | explicit StatefulMaxGauge(IdPtr id) : StatefulMeter(std::move(id)) {} 178 | 179 | [[nodiscard]] double Get() const { return value_; } 180 | 181 | MeterType GetType() const override { return MeterType::MaxGauge; } 182 | 183 | void Set(double amount) { detail::update_max(&value_, amount); } 184 | 185 | void Update(double amount) { Set(amount); } 186 | 187 | void Measure(std::vector* measurements) override { 188 | auto v = value_.exchange(kMinValue); 189 | if (v == kMinValue) { 190 | return; 191 | } 192 | measurements->emplace_back(Id::WithDefaultStat(id_, "max"), v); 193 | } 194 | 195 | private: 196 | static constexpr auto kMinValue = std::numeric_limits::lowest(); 197 | std::atomic value_ = kMinValue; 198 | }; 199 | 200 | class StatefulMonoCounter : public StatefulMeter { 201 | public: 202 | explicit StatefulMonoCounter(IdPtr id) : StatefulMeter(std::move(id)) {} 203 | 204 | MeterType GetType() const override { return MeterType::MonotonicCounter; } 205 | 206 | [[nodiscard]] double Delta() const { return value_ - prev_value_; } 207 | 208 | void Set(double amount) { value_ = amount; } 209 | 210 | void Measure(std::vector* measurements) override { 211 | auto delta = Delta(); 212 | prev_value_ = value_.load(); 213 | if (delta > 0) { 214 | measurements->emplace_back(id_->WithStat("count"), delta); 215 | } 216 | } 217 | 218 | private: 219 | static constexpr auto kNaN = std::numeric_limits::quiet_NaN(); 220 | std::atomic value_ = kNaN; 221 | std::atomic prev_value_ = kNaN; 222 | }; 223 | 224 | class StatefulMonoCounterUint : public StatefulMeter { 225 | public: 226 | explicit StatefulMonoCounterUint(IdPtr id) : StatefulMeter(std::move(id)) {} 227 | 228 | MeterType GetType() const override { return MeterType::MonotonicCounterUint; } 229 | 230 | [[nodiscard]] double Delta() const { 231 | if (value_ < prev_value_) { 232 | return kMax - prev_value_ + value_ + 1; 233 | } else { 234 | return value_ - prev_value_; 235 | } 236 | } 237 | 238 | void Set(uint64_t amount) { value_ = amount; } 239 | 240 | void Measure(std::vector* measurements) override { 241 | auto delta = Delta(); 242 | prev_value_ = value_.load(); 243 | if (delta > 0) { 244 | measurements->emplace_back(id_->WithStat("count"), delta); 245 | } 246 | } 247 | 248 | private: 249 | static constexpr auto kMax = std::numeric_limits::max(); 250 | std::atomic value_ = 0; 251 | std::atomic prev_value_ = 0; 252 | }; 253 | 254 | class StatefulPercTimer : public StatefulMeter { 255 | public: 256 | StatefulPercTimer(IdPtr id, std::chrono::nanoseconds, std::chrono::nanoseconds) 257 | : StatefulMeter(std::move(id)) {} 258 | 259 | [[nodiscard]] MeterType GetType() const override { return MeterType::PercentileTimer; } 260 | 261 | void Measure(std::vector*) override {} 262 | 263 | private: 264 | }; 265 | 266 | class StatefulPercDistSum : public StatefulMeter { 267 | public: 268 | StatefulPercDistSum(IdPtr id, int64_t, int64_t): StatefulMeter(std::move(id)) {} 269 | 270 | [[nodiscard]] MeterType GetType() const override { 271 | return MeterType::PercentileDistSummary; 272 | } 273 | 274 | void Measure(std::vector*) override {} 275 | }; 276 | 277 | class StatefulTimer : public TestDistribution { 278 | public: 279 | explicit StatefulTimer(IdPtr id): TestDistribution(std::move(id)) {} 280 | 281 | void Record(absl::Duration amount) { record(absl::ToDoubleSeconds(amount)); } 282 | 283 | void Record(std::chrono::nanoseconds amount) { Record(absl::FromChrono(amount)); } 284 | }; 285 | 286 | struct stateful_meters { 287 | using counter_t = StatefulCounter; 288 | using ds_t = StatefulDistSum; 289 | using gauge_t = StatefulGauge; 290 | using max_gauge_t = StatefulMaxGauge; 291 | using age_gauge_t = StatefulAgeGauge; 292 | using monotonic_counter_t = StatefulMonoCounter; 293 | using monotonic_counter_uint_t = StatefulMonoCounterUint; 294 | using perc_timer_t = StatefulPercTimer; 295 | using perc_ds_t = StatefulPercDistSum; 296 | using timer_t = StatefulTimer; 297 | }; 298 | 299 | } // namespace spectator 300 | -------------------------------------------------------------------------------- /spectator/stateful_test.cc: -------------------------------------------------------------------------------- 1 | #include "registry.h" 2 | #include 3 | 4 | namespace { 5 | 6 | TEST(Stateful, Counter) { 7 | spectator::TestRegistry testRegistry; 8 | 9 | auto ctr = testRegistry.GetCounter( 10 | std::make_shared("foo", spectator::Tags())); 11 | ctr->Increment(); 12 | 13 | EXPECT_EQ(testRegistry.Measurements().size(), 1); 14 | EXPECT_EQ(testRegistry.Measurements().size(), 0); 15 | } 16 | 17 | TEST(Stateful, Timer) { 18 | spectator::TestRegistry testRegistry; 19 | testRegistry.GetTimer("name")->Record(absl::Seconds(0.5)); 20 | EXPECT_EQ(testRegistry.Measurements().size(), 4); 21 | } 22 | 23 | TEST(Stateful, Gauge) { 24 | spectator::TestRegistry testRegistry; 25 | testRegistry.GetGauge("name")->Set(1); 26 | EXPECT_EQ(testRegistry.Measurements().size(), 1); 27 | } 28 | 29 | TEST(Stateful, SameMeter) { 30 | spectator::TestRegistry registry; 31 | registry.GetCounter("foo")->Add(2); 32 | EXPECT_EQ(registry.GetCounter("foo")->Count(), 2); 33 | } 34 | 35 | } // namespace -------------------------------------------------------------------------------- /spectator/stateless_meters.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "id.h" 3 | #include "absl/strings/str_cat.h" 4 | #include "absl/strings/str_format.h" 5 | #include "absl/time/time.h" 6 | 7 | namespace spectator { 8 | 9 | namespace detail { 10 | 11 | #include "valid_chars.inc" 12 | 13 | inline std::string as_string(std::string_view v) { 14 | return {v.data(), v.size()}; 15 | } 16 | 17 | inline bool contains_non_atlas_char(const std::string& input) { 18 | return std::any_of(input.begin(), input.end(), [](char c) { return !kAtlasChars[c]; }); 19 | } 20 | 21 | inline std::string replace_invalid_characters(const std::string& input) { 22 | if (contains_non_atlas_char(input)) { 23 | std::string result{input}; 24 | for (char &c : result) { 25 | if (!kAtlasChars[c]) { 26 | c = '_'; 27 | } 28 | } 29 | return result; 30 | } else { 31 | return input; 32 | } 33 | } 34 | 35 | inline std::string create_prefix(const Id& id, std::string_view type_name) { 36 | std::string res = as_string(type_name) + ":" + replace_invalid_characters(id.Name()); 37 | for (const auto& tags : id.GetTags()) { 38 | auto first = replace_invalid_characters(tags.first); 39 | auto second = replace_invalid_characters(tags.second); 40 | absl::StrAppend(&res, ",", first, "=", second); 41 | } 42 | 43 | absl::StrAppend(&res, ":"); 44 | return res; 45 | } 46 | 47 | template 48 | T restrict(T amount, T min, T max) { 49 | auto r = amount; 50 | if (r > max) { 51 | r = max; 52 | } else if (r < min) { 53 | r = min; 54 | } 55 | return r; 56 | } 57 | } // namespace detail 58 | 59 | template 60 | class StatelessMeter { 61 | public: 62 | StatelessMeter(IdPtr id, Pub* publisher) 63 | : id_(std::move(id)), publisher_(publisher) { 64 | assert(publisher_ != nullptr); 65 | } 66 | virtual ~StatelessMeter() = default; 67 | std::string GetPrefix() { 68 | if (value_prefix_.empty()) { 69 | value_prefix_ = detail::create_prefix(*id_, Type()); 70 | } 71 | return value_prefix_; 72 | } 73 | [[nodiscard]] IdPtr MeterId() const noexcept { return id_; } 74 | [[nodiscard]] virtual std::string_view Type() = 0; 75 | 76 | protected: 77 | void send(double value) { 78 | if (value_prefix_.empty()) { 79 | value_prefix_ = detail::create_prefix(*id_, Type()); 80 | } 81 | auto msg = absl::StrFormat("%s%f", value_prefix_, value); 82 | // remove trailing zeros and decimal points 83 | msg.erase(msg.find_last_not_of('0') + 1, std::string::npos); 84 | msg.erase(msg.find_last_not_of('.') + 1, std::string::npos); 85 | publisher_->send(msg); 86 | } 87 | 88 | void send_uint(uint64_t value) { 89 | if (value_prefix_.empty()) { 90 | value_prefix_ = detail::create_prefix(*id_, Type()); 91 | } 92 | auto msg = absl::StrFormat("%s%u", value_prefix_, value); 93 | publisher_->send(msg); 94 | } 95 | 96 | private: 97 | IdPtr id_; 98 | Pub* publisher_; 99 | std::string value_prefix_; 100 | }; 101 | 102 | template 103 | class AgeGauge : public StatelessMeter { 104 | public: 105 | AgeGauge(IdPtr id, Pub* publisher) 106 | : StatelessMeter(std::move(id), publisher) {} 107 | void Now() noexcept { this->send(0); } 108 | void Set(double value) noexcept { this->send(value); } 109 | 110 | protected: 111 | std::string_view Type() override { return "A"; } 112 | }; 113 | 114 | template 115 | class Counter : public StatelessMeter { 116 | public: 117 | Counter(IdPtr id, Pub* publisher) 118 | : StatelessMeter(std::move(id), publisher) {} 119 | void Increment() noexcept { this->send(1); }; 120 | void Add(double delta) noexcept { this->send(delta); } 121 | 122 | protected: 123 | std::string_view Type() override { return "c"; } 124 | }; 125 | 126 | template 127 | class DistributionSummary : public StatelessMeter { 128 | public: 129 | DistributionSummary(IdPtr id, Pub* publisher) 130 | : StatelessMeter(std::move(id), publisher) {} 131 | void Record(double amount) noexcept { this->send(amount); } 132 | 133 | protected: 134 | std::string_view Type() override { return "d"; } 135 | }; 136 | 137 | template 138 | class Gauge : public StatelessMeter { 139 | public: 140 | Gauge(IdPtr id, Pub* publisher, unsigned int ttl_seconds = 0) 141 | : StatelessMeter(std::move(id), publisher) { 142 | if (ttl_seconds > 0) { 143 | type_str_ = "g," + std::to_string(ttl_seconds); 144 | } 145 | } 146 | void Set(double value) noexcept { this->send(value); } 147 | 148 | protected: 149 | std::string_view Type() override { return type_str_; } 150 | 151 | private: 152 | std::string type_str_{"g"}; 153 | }; 154 | 155 | template 156 | class MaxGauge : public StatelessMeter { 157 | public: 158 | MaxGauge(IdPtr id, Pub* publisher) 159 | : StatelessMeter(std::move(id), publisher) {} 160 | void Update(double value) noexcept { this->send(value); } 161 | // synonym for Update for consistency with the Gauge interface 162 | void Set(double value) noexcept { this->send(value); } 163 | 164 | protected: 165 | std::string_view Type() override { return "m"; } 166 | }; 167 | 168 | template 169 | class MonotonicCounter : public StatelessMeter { 170 | public: 171 | MonotonicCounter(IdPtr id, Pub* publisher) 172 | : StatelessMeter(std::move(id), publisher) {} 173 | void Set(double amount) noexcept { this->send(amount); } 174 | 175 | protected: 176 | std::string_view Type() override { return "C"; } 177 | }; 178 | 179 | template 180 | class MonotonicCounterUint : public StatelessMeter { 181 | public: 182 | MonotonicCounterUint(IdPtr id, Pub* publisher) 183 | : StatelessMeter(std::move(id), publisher) {} 184 | void Set(uint64_t amount) noexcept { this->send_uint(amount); } 185 | 186 | protected: 187 | std::string_view Type() override { return "U"; } 188 | }; 189 | 190 | template 191 | class PercentileDistributionSummary : public StatelessMeter { 192 | public: 193 | PercentileDistributionSummary(IdPtr id, Pub* publisher, int64_t min, 194 | int64_t max) 195 | : StatelessMeter(std::move(id), publisher), min_{min}, max_{max} {} 196 | 197 | void Record(int64_t amount) noexcept { 198 | this->send(detail::restrict(amount, min_, max_)); 199 | } 200 | 201 | protected: 202 | std::string_view Type() override { return "D"; } 203 | 204 | private: 205 | int64_t min_; 206 | int64_t max_; 207 | }; 208 | 209 | template 210 | class PercentileTimer : public StatelessMeter { 211 | public: 212 | PercentileTimer(IdPtr id, Pub* publisher, absl::Duration min, 213 | absl::Duration max) 214 | : StatelessMeter(std::move(id), publisher), min_(min), max_(max) {} 215 | 216 | PercentileTimer(IdPtr id, Pub* publisher, std::chrono::nanoseconds min, 217 | std::chrono::nanoseconds max) 218 | : PercentileTimer(std::move(id), publisher, absl::FromChrono(min), 219 | absl::FromChrono(max)) {} 220 | 221 | void Record(std::chrono::nanoseconds amount) noexcept { 222 | Record(absl::FromChrono(amount)); 223 | } 224 | 225 | void Record(absl::Duration amount) noexcept { 226 | auto duration = detail::restrict(amount, min_, max_); 227 | this->send(absl::ToDoubleSeconds(duration)); 228 | } 229 | 230 | protected: 231 | std::string_view Type() override { return "T"; } 232 | 233 | private: 234 | absl::Duration min_; 235 | absl::Duration max_; 236 | }; 237 | 238 | template 239 | class Timer : public StatelessMeter { 240 | public: 241 | Timer(IdPtr id, Pub* publisher) 242 | : StatelessMeter(std::move(id), publisher) {} 243 | void Record(std::chrono::nanoseconds amount) noexcept { 244 | Record(absl::FromChrono(amount)); 245 | } 246 | 247 | void Record(absl::Duration amount) noexcept { 248 | auto secs = absl::ToDoubleSeconds(amount); 249 | this->send(secs); 250 | } 251 | 252 | protected: 253 | std::string_view Type() override { return "t"; } 254 | }; 255 | 256 | } // namespace spectator 257 | -------------------------------------------------------------------------------- /spectator/statelessregistry_test.cc: -------------------------------------------------------------------------------- 1 | #include "registry.h" 2 | #include "test_publisher.h" 3 | #include 4 | 5 | namespace { 6 | 7 | using spectator::base_registry; 8 | using spectator::stateless; 9 | using spectator::stateless_types; 10 | using spectator::TestPublisher; 11 | 12 | // A stateless registry that uses a test publisher 13 | class TestStatelessRegistry 14 | : public base_registry>> { 15 | public: 16 | TestStatelessRegistry() { 17 | state_.publisher = std::make_unique(); 18 | } 19 | auto SentMessages() { return state_.publisher->SentMessages(); } 20 | void Reset() { return state_.publisher->Reset(); } 21 | void AddExtraTag(absl::string_view k, absl::string_view v) { 22 | extra_tags_.add(k, v); 23 | } 24 | }; 25 | 26 | TEST(StatelessRegistry, AgeGauge) { 27 | TestStatelessRegistry r; 28 | auto ag = r.GetAgeGauge("foo"); 29 | auto ag2 = r.GetAgeGauge("bar", {{"id", "2"}}); 30 | ag->Now(); 31 | ag2->Set(100); 32 | std::vector expected = {"A:foo:0", "A:bar,id=2:100"}; 33 | EXPECT_EQ(r.SentMessages(), expected); 34 | } 35 | 36 | TEST(StatelessRegistry, Counter) { 37 | TestStatelessRegistry r; 38 | auto c = r.GetCounter("foo"); 39 | c->Increment(); 40 | EXPECT_EQ(r.SentMessages().front(), "c:foo:1"); 41 | 42 | r.Reset(); 43 | c = r.GetCounter("foo", {{"k1", "v1"}}); 44 | c->Add(2); 45 | EXPECT_EQ(r.SentMessages().front(), "c:foo,k1=v1:2"); 46 | } 47 | 48 | TEST(StatelessRegistry, DistSummary) { 49 | TestStatelessRegistry r; 50 | auto ds = r.GetDistributionSummary("foo"); 51 | ds->Record(100); 52 | EXPECT_EQ(r.SentMessages().front(), "d:foo:100"); 53 | 54 | r.Reset(); 55 | ds = r.GetDistributionSummary("bar", {{"k1", "v1"}}); 56 | ds->Record(2); 57 | EXPECT_EQ(r.SentMessages().front(), "d:bar,k1=v1:2"); 58 | } 59 | 60 | TEST(StatelessRegistry, Gauge) { 61 | TestStatelessRegistry r; 62 | auto g = r.GetGauge("foo"); 63 | auto g2 = r.GetGauge("bar", {{"id", "2"}}); 64 | auto g3 = r.GetGaugeTTL("baz", 1); 65 | auto g4 = r.GetGaugeTTL("quux", 2, {{"id", "2"}}); 66 | g->Set(100); 67 | g2->Set(101); 68 | g3->Set(102); 69 | g4->Set(103); 70 | std::vector expected = {"g:foo:100", "g:bar,id=2:101", 71 | "g,1:baz:102", "g,2:quux,id=2:103"}; 72 | EXPECT_EQ(r.SentMessages(), expected); 73 | } 74 | 75 | TEST(StatelessRegistry, MaxGauge) { 76 | TestStatelessRegistry r; 77 | auto m = r.GetMaxGauge("foo"); 78 | auto m2 = r.GetMaxGauge("bar", {{"id", "2"}}); 79 | m->Update(100); 80 | m2->Set(101); 81 | std::vector expected = {"m:foo:100", "m:bar,id=2:101"}; 82 | EXPECT_EQ(r.SentMessages(), expected); 83 | } 84 | 85 | TEST(StatelessRegistry, MonotonicCounter) { 86 | TestStatelessRegistry r; 87 | auto m = r.GetMonotonicCounter("foo"); 88 | auto m2 = r.GetMonotonicCounter("bar", {{"id", "2"}}); 89 | m->Set(101.1); 90 | m2->Set(102.2); 91 | std::vector expected = {"C:foo:101.1", "C:bar,id=2:102.2"}; 92 | EXPECT_EQ(r.SentMessages(), expected); 93 | } 94 | 95 | TEST(StatelessRegistry, MonotonicCounterUint) { 96 | TestStatelessRegistry r; 97 | auto m = r.GetMonotonicCounterUint("foo"); 98 | auto m2 = r.GetMonotonicCounterUint("bar", {{"id", "2"}}); 99 | m->Set(100); 100 | m2->Set(101); 101 | std::vector expected = {"U:foo:100", "U:bar,id=2:101"}; 102 | EXPECT_EQ(r.SentMessages(), expected); 103 | } 104 | 105 | TEST(StatelessRegistry, Timer) { 106 | TestStatelessRegistry r; 107 | auto t = r.GetTimer("foo"); 108 | auto t2 = r.GetTimer("bar", {{"id", "2"}}); 109 | t->Record(std::chrono::microseconds(100)); 110 | t2->Record(absl::Seconds(0.1)); 111 | std::vector expected = {"t:foo:0.0001", "t:bar,id=2:0.1"}; 112 | EXPECT_EQ(r.SentMessages(), expected); 113 | } 114 | 115 | TEST(StatelessRegistry, PercentileTimer) { 116 | TestStatelessRegistry r; 117 | auto t = r.GetPercentileTimer("foo", absl::ZeroDuration(), absl::Seconds(10)); 118 | auto t2 = r.GetPercentileTimer("bar", absl::Milliseconds(1), absl::Seconds(1)); 119 | 120 | t->Record(std::chrono::microseconds(100)); 121 | t2->Record(std::chrono::microseconds(100)); 122 | 123 | t->Record(absl::Seconds(5)); 124 | t2->Record(absl::Seconds(5)); 125 | 126 | t->Record(std::chrono::milliseconds(100)); 127 | t2->Record(std::chrono::milliseconds(100)); 128 | 129 | std::vector expected = {"T:foo:0.0001", "T:bar:0.001", 130 | "T:foo:5", "T:bar:1", 131 | "T:foo:0.1", "T:bar:0.1"}; 132 | EXPECT_EQ(r.SentMessages(), expected); 133 | } 134 | 135 | TEST(StatelessRegistry, PercentileDistributionSummary) { 136 | TestStatelessRegistry r; 137 | auto t = r.GetPercentileDistributionSummary("foo", 0, 1000); 138 | auto t2 = r.GetPercentileDistributionSummary("bar", 10, 100); 139 | 140 | t->Record(5); 141 | t2->Record(5); 142 | 143 | t->Record(500); 144 | t2->Record(500); 145 | 146 | t->Record(50); 147 | t2->Record(50); 148 | 149 | std::vector expected = {"D:foo:5", "D:bar:10", 150 | "D:foo:500", "D:bar:100", 151 | "D:foo:50", "D:bar:50"}; 152 | EXPECT_EQ(r.SentMessages(), expected); 153 | } 154 | 155 | template 156 | void test_meter(T&& m1, T&& m2) { 157 | auto id1 = m1->MeterId(); 158 | auto id2 = m2->MeterId(); 159 | EXPECT_EQ(*id1, *id2); 160 | 161 | spectator::Tags expected{{"x.spectator", "v1"}}; 162 | EXPECT_EQ(id1->GetTags(), expected); 163 | } 164 | 165 | TEST(StatelessRegistry, ExtraTags) { 166 | using spectator::Id; 167 | 168 | TestStatelessRegistry r; 169 | r.AddExtraTag("x.spectator", "v1"); 170 | 171 | // Counters 172 | auto c_name = r.GetCounter("name"); 173 | auto c_id = r.GetCounter(Id::of("name")); 174 | test_meter(c_name, c_id); 175 | 176 | // DistSummaries 177 | auto d_name = r.GetDistributionSummary("ds"); 178 | auto d_id = r.GetDistributionSummary(Id::of("ds")); 179 | test_meter(d_name, d_id); 180 | 181 | // Gauges 182 | auto g_name = r.GetGauge("g"); 183 | auto g_id = r.GetGauge(Id::of("g")); 184 | test_meter(g_name, g_id); 185 | 186 | // MaxGauge 187 | auto mx_name = r.GetMaxGauge("m1"); 188 | auto mx_id = r.GetMaxGauge(Id::of("m1")); 189 | test_meter(mx_name, mx_id); 190 | 191 | // MonoCounter 192 | auto mo_name = r.GetMonotonicCounter("mo1"); 193 | auto mo_id = r.GetMonotonicCounter(Id::of("mo1")); 194 | test_meter(mo_name, mo_id); 195 | 196 | // MonoCounter Uint 197 | auto mo_u_name = r.GetMonotonicCounterUint("mo1"); 198 | auto mo_u_id = r.GetMonotonicCounterUint(Id::of("mo1")); 199 | test_meter(mo_name, mo_id); 200 | 201 | // Pct DistSummaries 202 | auto pds_name = r.GetPercentileDistributionSummary("pds", 0, 100); 203 | auto pds_id = r.GetPercentileDistributionSummary(Id::of("pds"), 0, 100); 204 | test_meter(pds_name, pds_id); 205 | 206 | // Pct Timers 207 | auto pt_name = 208 | r.GetPercentileTimer("t", absl::ZeroDuration(), absl::Seconds(1)); 209 | auto pt_id = 210 | r.GetPercentileTimer(Id::of("t"), absl::ZeroDuration(), absl::Seconds(1)); 211 | test_meter(pt_name, pt_id); 212 | 213 | // Timers 214 | auto t_name = r.GetTimer("t1"); 215 | auto t_id = r.GetTimer(Id::of("t1")); 216 | test_meter(t_name, t_id); 217 | } 218 | 219 | } // namespace 220 | -------------------------------------------------------------------------------- /spectator/test_main.cc: -------------------------------------------------------------------------------- 1 | #include "backward.hpp" 2 | #include 3 | 4 | int main(int argc, char** argv) { 5 | ::testing::InitGoogleTest(&argc, argv); 6 | backward::SignalHandling sh; 7 | return RUN_ALL_TESTS(); 8 | } 9 | -------------------------------------------------------------------------------- /spectator/test_publisher.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "publisher.h" 4 | #include 5 | #include 6 | 7 | namespace spectator { 8 | class TestPublisher { 9 | public: 10 | void send(std::string_view msg) { messages.emplace_back(msg); } 11 | std::vector SentMessages() { return messages; } 12 | void Reset() { messages.clear(); } 13 | 14 | private: 15 | std::vector messages; 16 | }; 17 | } // namespace spectator -------------------------------------------------------------------------------- /spectator/test_server.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "logger.h" 5 | 6 | template 7 | class TestServer { 8 | public: 9 | explicit TestServer(typename T::endpoint endpoint) 10 | : socket_{context_, endpoint} {} 11 | void Start() { 12 | start_receiving(); 13 | runner = std::thread([this]() { context_.run(); }); 14 | } 15 | 16 | void Stop() { 17 | spectator::DefaultLogger()->info("Stopping test server"); 18 | context_.stop(); 19 | runner.join(); 20 | } 21 | 22 | ~TestServer() { 23 | if (runner.joinable()) { 24 | spectator::DefaultLogger()->info( 25 | "Test server runner was not stopped properly"); 26 | Stop(); 27 | } 28 | } 29 | 30 | void Reset() { msgs.clear(); } 31 | 32 | [[nodiscard]] std::vector GetMessages() const { return msgs; } 33 | 34 | protected: 35 | std::thread runner; 36 | asio::io_context context_{}; 37 | typename T::socket socket_; 38 | char buf[32768]; 39 | std::vector msgs; 40 | 41 | void start_receiving() { 42 | socket_.async_receive( 43 | asio::buffer(buf, sizeof buf), 44 | [this](const std::error_code& err, size_t bytes_transferred) { 45 | assert(!err); 46 | msgs.emplace_back(std::string(buf, bytes_transferred)); 47 | start_receiving(); 48 | }); 49 | } 50 | }; 51 | 52 | class TestUdpServer : public TestServer { 53 | public: 54 | TestUdpServer() 55 | : TestServer{asio::ip::udp::endpoint{asio::ip::udp::v6(), 0}}, 56 | port_{socket_.local_endpoint().port()} {} 57 | 58 | [[nodiscard]] int GetPort() const { return port_; } 59 | 60 | private: 61 | int port_; 62 | }; 63 | 64 | class TestUnixServer : public TestServer { 65 | public: 66 | explicit TestUnixServer(std::string_view path) 67 | : TestServer{asio::local::datagram_protocol::endpoint{path}} {} 68 | }; 69 | -------------------------------------------------------------------------------- /spectator/timer_test.cc: -------------------------------------------------------------------------------- 1 | #include "stateless_meters.h" 2 | #include "test_publisher.h" 3 | #include 4 | 5 | namespace { 6 | using spectator::Id; 7 | using spectator::Tags; 8 | using spectator::TestPublisher; 9 | using spectator::Timer; 10 | 11 | TEST(Timer, Record) { 12 | TestPublisher publisher; 13 | auto id = std::make_shared("t.name", Tags{}); 14 | auto id2 = std::make_shared("t2", Tags{{"key", "val"}}); 15 | Timer t{id, &publisher}; 16 | Timer t2{id2, &publisher}; 17 | t.Record(std::chrono::milliseconds(1)); 18 | t2.Record(absl::Seconds(0.1)); 19 | t2.Record(absl::Microseconds(500)); 20 | std::vector expected = {"t:t.name:0.001", "t:t2,key=val:0.1", "t:t2,key=val:0.0005"}; 21 | EXPECT_EQ(publisher.SentMessages(), expected); 22 | } 23 | 24 | TEST(Timer, InvalidTags) { 25 | TestPublisher publisher; 26 | // test with a single tag, because tags order is not guaranteed in a flat_hash_map 27 | auto id = std::make_shared("timer`!@#$%^&*()-=~_+[]{}\\|;:'\",<.>/?foo", 28 | Tags{{"tag1,:=", "value1,:="}}); 29 | Timer t{id, &publisher}; 30 | EXPECT_EQ("t:timer______^____-_~______________.___foo,tag1___=value1___:", t.GetPrefix()); 31 | } 32 | } // namespace 33 | -------------------------------------------------------------------------------- /spectator/util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace spectator { 4 | 5 | template 6 | T restrict(T amount, T min, T max) { 7 | auto r = amount; 8 | if (r > max) { 9 | r = max; 10 | } else if (r < min) { 11 | r = min; 12 | } 13 | return r; 14 | } 15 | 16 | } // namespace spectator 17 | -------------------------------------------------------------------------------- /tools/gen_valid_chars.cc: -------------------------------------------------------------------------------- 1 | // generate the atlas valid charsets 2 | 3 | #include 4 | #include 5 | 6 | void dump_array(std::ostream& os, const std::string& name, const std::array& chars) { 7 | os << "static constexpr std::array " << name << " = {{"; 8 | 9 | os << chars[0]; 10 | for (auto i = 1u; i < chars.size(); ++i) { 11 | os << ", " << chars[i]; 12 | } 13 | 14 | os << "}};\n"; 15 | } 16 | 17 | int main(int argc, char* argv[]) { 18 | std::ofstream of; 19 | if (argc > 1) { 20 | of.open(argv[1]); 21 | } else { 22 | of.open("/dev/stdout"); 23 | } 24 | 25 | // default false 26 | std::array charsAllowed{}; 27 | for (int i = 0; i < 256; ++i) { 28 | charsAllowed[i] = false; 29 | } 30 | 31 | // configure allowed characters 32 | charsAllowed['.'] = true; 33 | charsAllowed['-'] = true; 34 | 35 | for (auto ch = '0'; ch <= '9'; ++ch) { 36 | charsAllowed[ch] = true; 37 | } 38 | for (auto ch = 'a'; ch <= 'z'; ++ch) { 39 | charsAllowed[ch] = true; 40 | } 41 | for (auto ch = 'A'; ch <= 'Z'; ++ch) { 42 | charsAllowed[ch] = true; 43 | } 44 | charsAllowed['~'] = true; 45 | charsAllowed['^'] = true; 46 | 47 | dump_array(of, "kAtlasChars", charsAllowed); 48 | } 49 | --------------------------------------------------------------------------------