diff --git a/.github/workflows/bazel-ci.yml b/.github/workflows/bazel-ci.yml new file mode 100644 index 00000000..841d9b3a --- /dev/null +++ b/.github/workflows/bazel-ci.yml @@ -0,0 +1,47 @@ +name: Bazel CI +on: [push, pull_request] + +jobs: + build: + name: Bazel on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, ubuntu-20.04, windows-2019] + steps: + - name: Checkout source + uses: actions/checkout@v2 + + - name: Generate German locale on Ubuntu + if: runner.os == 'Linux' + run: | + sudo apt-get remove -y --purge man-db # avoid time-consuming trigger + sudo apt-get update + sudo apt-get install -y locales + sudo locale-gen de_DE.UTF-8 # used by SerializerTest + + - name: Install telegraf on Ubuntu + if: runner.os == 'Linux' + run: | + curl -sL https://repos.influxdata.com/influxdb.key | sudo apt-key add - + source /etc/lsb-release + echo "deb https://repos.influxdata.com/${DISTRIB_ID,,} ${DISTRIB_CODENAME} stable" | sudo tee /etc/apt/sources.list.d/influxdb.list + sudo apt-get update + sudo apt-get install -y telegraf + + - name: Install telegraf on macOS + if: runner.os == 'macOS' + run: brew install telegraf + + - name: Build + run: bazel build //... + + - name: Test + run: bazel test --test_output=all //core/... //pull/... + + - name: Scraping Test + if: runner.os != 'Windows' + run: bazel test --test_output=all //pull/tests/integration:scrape-test + + - name: Benchmark + run: bazel run -c opt //core/benchmarks diff --git a/.github/workflows/cmake-ci.yml b/.github/workflows/cmake-ci.yml new file mode 100644 index 00000000..562fae52 --- /dev/null +++ b/.github/workflows/cmake-ci.yml @@ -0,0 +1,133 @@ +name: CMake CI +on: [push, pull_request] + +jobs: + build: + name: CMake on ${{ matrix.os }} with ${{ matrix.dependencies }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macOS-latest, ubuntu-18.04, windows-2016] + dependencies: [submodule, vcpkg] + steps: + - name: Checkout source + uses: actions/checkout@v2 + + - name: Checkout submodules + if: matrix.dependencies == 'submodule' + shell: bash + run: | + auth_header="$(git config --local --get http.https://github.com/.extraheader)" + git submodule sync --recursive + git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1 + + - name: Mount vcpkg cache + if: matrix.dependencies == 'vcpkg' + uses: actions/cache@v2 + with: + path: "~/.cache/vcpkg/archives" + key: vcpkg-${{ matrix.os }} + + - name: Install vcpkg dependencies + if: matrix.dependencies == 'vcpkg' + run: vcpkg install benchmark civetweb curl[core] gtest zlib + + - name: Generate German locale on Ubuntu + if: runner.os == 'Linux' + run: | + sudo apt-get remove -y --purge man-db # avoid time-consuming trigger + sudo apt-get update + sudo apt-get install -y locales + sudo locale-gen de_DE.UTF-8 # used by SerializerTest + + - name: Install ninja on Ubuntu + if: runner.os == 'Linux' + run: | + sudo apt-get install -y ninja-build + + - name: Install ninja on macOS + if: runner.os == 'macOS' + run: brew install ninja + + - name: "Configure for Unix with internal dependencies" + if: matrix.dependencies == 'submodule' && runner.os != 'Windows' + run: cmake -DUSE_THIRDPARTY_LIBRARIES=ON -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/_install -DENABLE_WARNINGS_AS_ERRORS=ON -DENABLE_COMPRESSION=OFF -DENABLE_PUSH=OFF -DCMAKE_DEBUG_POSTFIX=_d -DCMAKE_CONFIGURATION_TYPES='Release;Debug' -G"Ninja Multi-Config" -S ${{ github.workspace }} -B ${{ github.workspace }}/_build + + - name: "Configure for Windows with internal dependencies" + if: matrix.dependencies == 'submodule' && runner.os == 'Windows' + run: cmake -DUSE_THIRDPARTY_LIBRARIES=ON -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/_install -DENABLE_WARNINGS_AS_ERRORS=ON -DENABLE_COMPRESSION=OFF -DENABLE_PUSH=OFF -DCMAKE_DEBUG_POSTFIX=_d -S ${{ github.workspace }} -B ${{ github.workspace }}/_build + + - name: "Configure for Unix with vcpkg dependencies" + if: matrix.dependencies == 'vcpkg' && runner.os != 'Windows' + run: cmake -DUSE_THIRDPARTY_LIBRARIES=OFF -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/_install "-DCMAKE_TOOLCHAIN_FILE=${VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake" -DCMAKE_DEBUG_POSTFIX=_d -DCMAKE_CONFIGURATION_TYPES='Release;Debug' -G"Ninja Multi-Config" -S ${{ github.workspace }} -B ${{ github.workspace }}/_build + + - name: "Configure for Windows with vcpkg dependencies" + if: matrix.dependencies == 'vcpkg' && runner.os == 'Windows' + run: cmake -DUSE_THIRDPARTY_LIBRARIES=OFF -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/_install "-DCMAKE_TOOLCHAIN_FILE=${Env:VCPKG_INSTALLATION_ROOT}\scripts\buildsystems\vcpkg.cmake" -DCMAKE_DEBUG_POSTFIX=_d -S ${{ github.workspace }} -B ${{ github.workspace }}/_build + + - name: "Build Debug" + run: cmake --build ${{ github.workspace }}/_build --config Debug + + - name: "Build Release" + run: cmake --build ${{ github.workspace }}/_build --config Release + + - name: "Test Debug" + run: ctest -C Debug -V -LE Benchmark + working-directory: "${{ github.workspace }}/_build" + + - name: "Test Release" + run: ctest -C Release -V -LE Benchmark + working-directory: "${{ github.workspace }}/_build" + + - name: "Run Benchmark" + run: ctest -C Release -V -L Benchmark + working-directory: "${{ github.workspace }}/_build" + + - name: "Install Debug" + run: cmake --install ${{ github.workspace }}/_build --config Debug + + - name: "Install Release" + run: cmake --install ${{ github.workspace }}/_build --config Release + + - name: "Configure CMake import for Unix with internal dependencies" + if: matrix.dependencies == 'submodule' && runner.os != 'Windows' + run: cmake -Dprometheus-cpp_DIR=${{ github.workspace }}/_install/lib/cmake/prometheus-cpp -DCMAKE_CONFIGURATION_TYPES='Release;Debug' -G"Ninja Multi-Config" -S ${{ github.workspace }}/cmake/project-import-cmake -B ${{ github.workspace }}/_import_cmake + + - name: "Configure CMake import for Windows with internal dependencies" + if: matrix.dependencies == 'submodule' && runner.os == 'Windows' + run: cmake -Dprometheus-cpp_DIR=${{ github.workspace }}/_install/lib/cmake/prometheus-cpp -S ${{ github.workspace }}/cmake/project-import-cmake -B ${{ github.workspace }}/_import_cmake + + - name: "Configure CMake import for Unix with vcpkg dependencies" + if: matrix.dependencies == 'vcpkg' && runner.os != 'Windows' + run: cmake -Dprometheus-cpp_DIR=${{ github.workspace }}/_install/lib/cmake/prometheus-cpp "-DCMAKE_TOOLCHAIN_FILE=${VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake" -DCMAKE_CONFIGURATION_TYPES='Release;Debug' -G"Ninja Multi-Config" -S ${{ github.workspace }}/cmake/project-import-cmake -B ${{ github.workspace }}/_import_cmake + + - name: "Configure CMake import for Windows with vcpkg dependencies" + if: matrix.dependencies == 'vcpkg' && runner.os == 'Windows' + run: cmake -Dprometheus-cpp_DIR=${{ github.workspace }}/_install/lib/cmake/prometheus-cpp "-DCMAKE_TOOLCHAIN_FILE=${Env:VCPKG_INSTALLATION_ROOT}\scripts\buildsystems\vcpkg.cmake" -S ${{ github.workspace }}/cmake/project-import-cmake -B ${{ github.workspace }}/_import_cmake + + - name: "Build CMake import Debug" + run: cmake --build ${{ github.workspace }}/_import_cmake --config Debug + + - name: "Build CMake import Release" + run: cmake --build ${{ github.workspace }}/_import_cmake --config Release + + - name: "Configure for Unix Shared Libs with internal dependencies" + if: matrix.dependencies == 'submodule' && runner.os != 'Windows' + run: cmake -DUSE_THIRDPARTY_LIBRARIES=ON -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/_install_shared -DENABLE_WARNINGS_AS_ERRORS=ON -DENABLE_COMPRESSION=OFF -DENABLE_PUSH=OFF -DCMAKE_DEBUG_POSTFIX=_d -GNinja -S ${{ github.workspace }} -B ${{ github.workspace }}/_build_shared + + - name: "Build for Unix Shared Libs" + if: matrix.dependencies == 'submodule' && runner.os != 'Windows' + run: cmake --build ${{ github.workspace }}/_build_shared + + - name: "Install for Unix Shared Libs" + if: matrix.dependencies == 'submodule' && runner.os != 'Windows' + run: cmake --install ${{ github.workspace }}/_build_shared + + - name: "Configure pkg-config import for Unix" + if: matrix.dependencies == 'submodule' && runner.os != 'Windows' + run: cmake -DCMAKE_PREFIX_PATH=${{ github.workspace }}/_install_shared -GNinja -S ${{ github.workspace }}/cmake/project-import-pkgconfig -B ${{ github.workspace }}/_import_pkgconfig + + - name: "Build pkg-config import for Unix" + if: matrix.dependencies == 'submodule' && runner.os != 'Windows' + run: cmake --build ${{ github.workspace }}/_import_pkgconfig diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..6cfb1c7d --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,60 @@ +name: Coverage +on: [push, pull_request] + +jobs: + build: + name: Code Coverage + runs-on: ubuntu-20.04 + steps: + - name: Checkout source + uses: actions/checkout@v2 + + - name: Mount vcpkg cache + uses: actions/cache@v2 + with: + path: "~/.cache/vcpkg/archives" + key: vcpkg-${{ runner.os }} + + - name: Install vcpkg dependencies + run: vcpkg install benchmark civetweb curl[core] gtest zlib + + - name: Generate German locale on Ubuntu + if: runner.os == 'Linux' + run: | + sudo apt-get remove -y --purge man-db # avoid time-consuming trigger + sudo apt-get update + sudo apt-get install -y locales + sudo locale-gen de_DE.UTF-8 # used by SerializerTest + + - name: Install ninja on Ubuntu + if: runner.os == 'Linux' + run: | + sudo apt-get install -y ninja-build + + - name: Install lcov + if: runner.os == 'Linux' + run: | + sudo apt-get install -y lcov + + - name: "CMake Configure for Unix with vcpkg dependencies" + env: + CFLAGS: "--coverage" + CXXFLAGS: "--coverage" + LDFLAGS: "--coverage" + run: cmake -DUSE_THIRDPARTY_LIBRARIES=OFF "-DCMAKE_TOOLCHAIN_FILE=${VCPKG_INSTALLATION_ROOT}/scripts/buildsystems/vcpkg.cmake" -GNinja -S ${{ github.workspace }} -B ${{ github.workspace }}/_build + + - name: Build + run: cmake --build ${{ github.workspace }}/_build + + - name: Test + run: ctest -V -LE Benchmark + working-directory: "${{ github.workspace }}/_build" + + - name: Run lcov + run: lcov --capture --directory "${{ github.workspace }}/_build" --output-file coverage.info --no-external --directory "${{ github.workspace }}" --exclude '*/tests/*' + + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + path-to-lcov: coverage.info diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml new file mode 100644 index 00000000..5edbd7a9 --- /dev/null +++ b/.github/workflows/doxygen.yml @@ -0,0 +1,30 @@ +name: Doxygen +on: + push: + branches: + - master + +jobs: + build: + name: Code Coverage + runs-on: ubuntu-20.04 + steps: + - name: Checkout source + uses: actions/checkout@v2 + + - name: Install doxygen + run: | + sudo apt-get remove -y --purge man-db # avoid time-consuming trigger + sudo apt-get update + sudo apt-get install -y doxygen graphviz + + - name: Generate doxygen + run: doxygen + working-directory: "${{ github.workspace }}/doc" + + - name: Deploy documentation + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + force_orphan: true + publish_dir: ./doc/html diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 00000000..99d82503 --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,58 @@ +name: Linting +on: [push, pull_request] + +jobs: + iwyu: + name: Include What You Use + runs-on: ubuntu-latest + container: + image: debian:sid-slim + steps: + - name: Install dependencies + run: | + apt-get update + apt-get install -y --no-install-recommends ca-certificates clang-12 cmake git iwyu libbenchmark-dev libcurl4-openssl-dev ninja-build zlib1g-dev + + - name: Checkout source + uses: actions/checkout@v2 + + - name: Checkout submodules + shell: bash + run: | + auth_header="$(git config --local --get http.https://github.com/.extraheader)" + git submodule sync --recursive + git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1 + + - name: "CMake Configure" + run: cmake -GNinja -DRUN_IWYU=ON -DCMAKE_C_COMPILER=clang-12 -DCMAKE_CXX_COMPILER=clang++-12 -S ${GITHUB_WORKSPACE} -B ${GITHUB_WORKSPACE}/_build + + - name: Build + run: cmake --build ${GITHUB_WORKSPACE}/_build 2>&1 | tee ${GITHUB_WORKSPACE}/output.txt + + - name: Check build output + run: if egrep -q 'should (add|remove) these lines' ${GITHUB_WORKSPACE}/output.txt; then exit 1; fi + + #- name: "CMake Configure" + # run: cmake -GNinja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -S ${GITHUB_WORKSPACE} -B ${GITHUB_WORKSPACE}/_build + + #- name: "Run IWYU" + # run: iwyu_tool -p ${GITHUB_WORKSPACE}/_build core push pull -- -Xiwyu --mapping_file=${GITHUB_WORKSPACE}/cmake/googletest.imp -Xiwyu --no_fwd_decls 2>&1 | tee ${{ github.workspace }}/output.txt + + format: + name: Clang Format + runs-on: ubuntu-20.04 + steps: + - name: Checkout source + uses: actions/checkout@v2 + + - name: Install dependencies + run: | + sudo apt-get remove -y --purge man-db # avoid time-consuming trigger + sudo apt-get update + sudo apt-get install -y clang-format-11 + + - name: Run clang-format + run: find . -type f \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.cxx' -o -name '*.o' -o -name '*.h' -o -name '*.hpp' -o -name '*.hxx' \) -exec clang-format-11 -style=file -i {} \; + + - name: Check for changes + run: git diff --exit-code diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..342d3f68 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +on: + release: + types: [created] + +name: Handle Release + +jobs: + build: + name: Upload Release Asset + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Checkout submodules + shell: bash + run: | + auth_header="$(git config --local --get http.https://github.com/.extraheader)" + git submodule sync --recursive + git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1 + + - name: Create tarball including submodules + shell: bash + env: + PREFIX: prometheus-cpp-with-submodules + run: | + git archive --prefix "${PREFIX}/" -o "${PREFIX}.tar" HEAD + git submodule foreach --recursive "git archive --prefix=${PREFIX}/\$path/ --output=\$sha1.tar HEAD && tar --concatenate --file=$(pwd)/${PREFIX}.tar \$sha1.tar && rm \$sha1.tar" + gzip "${PREFIX}.tar" + + # using the official actions/upload-release-asset action would be preferred but is blocked by + # https://github.com/actions/upload-release-asset/pull/41 + - name: Upload the artifacts + uses: skx/github-action-publish-binaries@75ce5546020fc1848da842f40240f9fa03e7a3a8 # release-0.14 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + args: prometheus-cpp-with-submodules.tar.gz diff --git a/.gitignore b/.gitignore index 3406309c..5267e3b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -bazel-* +/bazel-* cmake-build-*/ _*/ .idea/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a3c68891..00000000 --- a/.travis.yml +++ /dev/null @@ -1,88 +0,0 @@ -sudo: required -dist: trusty -language: c++ - -before_install: - - echo -n | openssl s_client -connect scan.coverity.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sudo tee -a /etc/ssl/certs/ca- - -env: - global: - - secure: "DwlWRs05m1pw3/NprZdVRy3Vog7k+KYIW4O1YQczbKk0t64vYLJMxMh9D8HrngnUv1UDOOU2orEO/hYYjlmiNRqoMA0Me+q9ndqEGjdCvFketH7zYpPXMxV/Uk1E7yYK60CJYbCFK7NossBWlBwSIETUNXXz1MhqF4c7SwDCTQk6ybPUThJVI3/nZLC4Wn9DB4pxPkEZaXJLL3HU0vXH1r0vKRFsjBU0OtDx1KQgtFYVmWbmd7tpxAlDfI11HJjpaoFD6HHcTlwvM5Ogj4lWL8Ze3glJsSsAYntEqgm7GQa4tYWsYmvGC5554WwIQ/0cDsSTLZjk2+FtSGmJpxdwXwmfOzzVjt7Ise7KG2Zg+CZ/IwF9VpwP2xmH/ug926sJDIjMmehZx2eetDzwY3oB3g3AF+JJIoaDF14Skt6QXEFWm/s/PiSQwkenMF80xzUufB7CRCVLR054HfJsQ0m5O8bNtUjyH7byZwOjvz8t/VdlnfFn5Ccs4tniOK6iiwvKmGAaakv6pfA7xKpRbExFkgPoTmejeQX83Ee1/A4JySMPTxkHPsJbMhimpMdbLVJTh9mKJxo2kleCC0MKB7OwTyNwUdR8+nqoZpUknGX6BrGY+R6ou3xlNYRUM9LCxxBKBJB05CswbhRWroun9fbpDTzBz3XPwBjiVTlxUc/YnXA=" - - secure: "fg0cACBBm7NAjad4Pxhp9DeTGDbcLnD0U9uxclsioTcB5X+88sTsgKRr5gG0hajPG6QF0L8iNt7zp87eZNcSU7JlTWnCBHPAAADD2apFnPUhioth+vPUBsUiKdOksoEG1q/hrjMYQ6yAf3XXd0+/ZaUA1X5v0OA2rjMTpNpNvMaQEN7M3XvcLRr1ydaAD/Wc1PAbuK8owWU3Hyo7+GdxoUWOtYiH44OuHSlSlnVQw2/yRR53yM1StKLZ2xarsWviXr76e1PutqoeeTbBbpAU6xboiyyDVey4Ae83HfOgPiADtsE4UjE2pqX2oqponY2q+0j8kI7sZVODR20nLCKcq9RHJR+yi0JEpsrqC4SE0lPKxG8HHlDaH+NdRVHTdNYuCfENR0R4YX/K59I83kLEnLNpC+j1BzXiRBjYzAMl5UtEDCUQGJcixq6BxE7i1uwskPckmYi2K63TaIxj5nvVm4Um8aHPrWHtbAf4stTDQHcFGcfQeBbX7PswKJAyIljaHn5T7kwAatRuWLHGsVTuxTkhkYohZy+/SDhFakI+6jfz8XZtL8gOIGMnDuvDWT2Di1JPZkBLcKKWpCIXdDaJnTogNid9xBpsX5IMVmlS51FxCOkoIT62gc9Lo+rxwgqFvEe+QIQh2zd8OjgS5m5HsftxCKVCbcPr+RjsNsCyOnQ=" - - secure: "ijrdtLO7y18oJTcISP1Zl0+O8dbCmo8DB4+3N6kZ9JjL1rVF5NM9nCsaCQ6vmHFOsUjEehvGJR6ZYFOYOMBU11rCPqd2FrEPewtQ0qEYI8eOV062oa/DOxHKyzGPde8eHcopw6b44EjPXx4iZTabcU8U00azNyWinVxKtP8lHJiVH+waNejuoAuMKypM9Lz+RCz/TLg4+DpLvQ5kWfpcFsuBCBAMbCK6Ujmv5mscvKxmWLr2Z79Wl3i5MbBe3IuaDAKXTz0ponkvZCssPr/USD9AA04EEn/Eg95JXOtRUi6Ah/fUBDAF4Ez/7yOHsXD1y8xt332eE8nJqjX3eLqEdplT19M/hBsbXxNCL0iZSZ8LL0JHKYG5beDGvfZmrk4/Nj/qd3Es1NGT6q2kbkrbxWSEFdkniQuwfr+yvAoGv4XFdRMTPH321WrL8wxfud0b2OPMMJo6obDOgRZfHe6c+4Eo9i/G98eL7xOz0kUUILiex8IQKNWnYflH1CqRKBbNs0APAMMlFKQ8FVwLiu/OzA8mjI+CuPGxOdfilgsJfCvKMBvz12y2AIBHdVBf1T6Ph8NZpwKivNYt70QOu5J/rg5K6E6XY1faMgWuDzQkMaeof1esU1IVlXqKBWa9c9rMNHQHTvhZ2KZ1EZvNrCX1fnY/X8fuwnhJ1Aq1nzhJTrM=" -os: - - linux - - osx - -addons: - apt: - sources: - - sourceline: 'deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8' - key_url: 'https://storage.googleapis.com/bazel-apt/doc/apt-key.pub.gpg' - - sourceline: 'deb https://repos.influxdata.com/ubuntu trusty stable' - key_url: 'https://repos.influxdata.com/influxdb.key' - packages: - - bazel - - cmake3 - - curl - - doxygen - - g++-4.8 - - gcc-4.8 - - git - - lcov - - libcurl4-openssl-dev - - python-pip - - telegraf - homebrew: - packages: - - cmake - - bazelbuild/tap/bazel - - prometheus - - telegraf - taps: - - bazelbuild/tap - -script: - - bazel build //... - - bazel test --test_output=all //core/... //pull/... -# - bazel test --test_output=all //pull/tests/integration:scrape-test -# - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then bazel test --test_output=all //pull/tests/integration:lint-test; fi - - bazel run -c opt //core/benchmarks - - - pushd . - - mkdir _build_internal_deps - - cd _build_internal_deps - - cmake .. -DUSE_THIRDPARTY_LIBRARIES=ON -DENABLE_WARNINGS_AS_ERRORS=ON - - make -j 4 - - ctest -V - - mkdir -p deploy - - make DESTDIR=`pwd`/deploy install - - popd - - - 3rdparty/build_for_travis.sh - - rm -rf 3rdparty/* - - - pushd . - - mkdir _build_coverage - - cd _build_coverage - - CFLAGS="--coverage" CXXFLAGS="--coverage" LDFLAGS="--coverage" cmake .. -DCMAKE_INSTALL_PREFIX=../_opt -DUSE_THIRDPARTY_LIBRARIES=OFF - - make -j 4 - - ctest -V -LE Benchmark - - mkdir -p deploy - - make DESTDIR=`pwd`/deploy install - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then pip install --user cpp-coveralls && coveralls --root .. --build-root . -E ".*/3rdparty/.*" -E ".*/_.*" -E ".*/tests/.*" -E ".*/benchmarks/.*"; fi - - popd - - - pushd . - - cd doc - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then doxygen && touch html/.nojekyll; fi - - popd - -deploy: - provider: pages - local-dir: doc/html - github-token: $GITHUB_TOKEN - skip-cleanup: true - on: - branch: master - condition: $TRAVIS_OS_NAME == linux diff --git a/3rdparty/build_for_travis.sh b/3rdparty/build_for_travis.sh deleted file mode 100755 index da895bb7..00000000 --- a/3rdparty/build_for_travis.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -THIRDPARTY_ROOT=$(cd $(dirname "${BASH_SOURCE[0]}") && /bin/pwd -P) -INSTALL_PREFIX="${TRAVIS_BUILD_DIR:?}/_opt" - -mkdir "${THIRDPARTY_ROOT}/civetweb/_build" -cd "${THIRDPARTY_ROOT}/civetweb/_build" -cmake .. -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" -DCIVETWEB_ENABLE_CXX=ON -DCIVETWEB_ENABLE_SSL=OFF -DCIVETWEB_BUILD_TESTING=OFF -make -j4 -make install - -mkdir "${THIRDPARTY_ROOT}/googletest/_build" -cd "${THIRDPARTY_ROOT}/googletest/_build" -cmake .. -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" -make -j4 -make install diff --git a/3rdparty/civetweb b/3rdparty/civetweb index ce8f6d38..eefb26f8 160000 --- a/3rdparty/civetweb +++ b/3rdparty/civetweb @@ -1 +1 @@ -Subproject commit ce8f6d38a60eb16c996afee1e5340f76ef4d0923 +Subproject commit eefb26f82b233268fc98577d265352720d477ba4 diff --git a/3rdparty/googletest b/3rdparty/googletest index 2fe3bd99..e2239ee6 160000 --- a/3rdparty/googletest +++ b/3rdparty/googletest @@ -1 +1 @@ -Subproject commit 2fe3bd994b3189899d93f1d5a881e725e046fdc2 +Subproject commit e2239ee6043f73722e7aa812a459f54a28552929 diff --git a/CMakeLists.txt b/CMakeLists.txt index 94dc1e12..4ee6812c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,41 +1,99 @@ -cmake_minimum_required(VERSION 3.5 FATAL_ERROR) +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) +if(POLICY CMP0091) + cmake_policy(SET CMP0091 NEW) # recognize CMAKE_MSVC_RUNTIME_LIBRARY +endif() -project(prometheus-cpp) +project(prometheus-cpp + VERSION 1.0.0 + DESCRIPTION "Prometheus Client Library for Modern C++" + HOMEPAGE_URL "https://github.com/jupp0r/prometheus-cpp" +) +include(GenerateExportHeader) include(GNUInstallDirs) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +option(BUILD_SHARED_LIBS "Build libraries as shared ones" OFF) option(ENABLE_PULL "Build prometheus-cpp pull library" ON) option(ENABLE_PUSH "Build prometheus-cpp push library" ON) option(ENABLE_COMPRESSION "Enable gzip compression" ON) option(ENABLE_TESTING "Build tests" ON) option(USE_THIRDPARTY_LIBRARIES "Use 3rdParty submodules" ON) +option(THIRDPARTY_CIVETWEB_WITH_SSL "Enable SSL support for embedded civetweb source code") option(OVERRIDE_CXX_STANDARD_FLAGS "Force building with -std=c++11 even if the CXXLFAGS are configured differently" ON) +option(GENERATE_PKGCONFIG "Generate and install pkg-config files" ${UNIX}) +option(RUN_IWYU "Run include-what-you-use" OFF) if(OVERRIDE_CXX_STANDARD_FLAGS) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS Off) endif() +# Set default directory permissions until +# https://gitlab.kitware.com/cmake/cmake/issues/15163 +# is fixed +set(CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS + OWNER_READ + OWNER_WRITE + OWNER_EXECUTE + GROUP_READ + GROUP_EXECUTE + WORLD_READ + WORLD_EXECUTE) + +# Put DLLs and binaries into same directory +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin) + +# Hide things by default for shared libraries +if(BUILD_SHARED_LIBS) + set(CMAKE_C_VISIBILITY_PRESET hidden) + set(CMAKE_CXX_VISIBILITY_PRESET hidden) + set(CMAKE_VISIBILITY_INLINES_HIDDEN YES) +endif() + set(CMAKE_THREAD_PREFER_PTHREAD TRUE) find_package(Threads) +# include-what-you-use + +if(RUN_IWYU) + find_program(IWYU_EXECUTABLE NAMES include-what-you-use iwyu) + if(NOT IWYU_EXECUTABLE) + message(FATAL_ERROR "Include-what-you-use not found") + endif() + + set(IWYU_ARGS + "${IWYU_EXECUTABLE}" + "-Xiwyu" "--no_fwd_decls" + "-Xiwyu" "--mapping_file=${CMAKE_CURRENT_SOURCE_DIR}/cmake/googletest.imp" + ) + + set(CMAKE_C_INCLUDE_WHAT_YOU_USE ${IWYU_ARGS}) + set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE ${IWYU_ARGS}) +endif() + +# check for required libatomic + +include(CheckAtomic) + if(ENABLE_TESTING) if(USE_THIRDPARTY_LIBRARIES) find_package(googlemock-3rdparty CONFIG REQUIRED) else() find_package(GTest 1.8.1 CONFIG REQUIRED) endif() - find_package(GoogleBenchmark) + find_package(benchmark CONFIG) enable_testing() endif() # build flags for CI system -if(ENABLE_WARNINGS_AS_ERRORS) +if(ENABLE_WARNINGS_AS_ERRORS AND NOT MSVC) add_compile_options( $<$,CXX>,$>:-Werror> $<$,CXX>,$>:-Wall> @@ -82,3 +140,37 @@ install( FILES "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config.cmake" DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}" ) + +# packaging + +if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + set(CPACK_PACKAGE_CONTACT "prometheus-cpp@@noreply.github.com") + set(CPACK_PACKAGE_DESCRIPTION "${PROJECT_DESCRIPTION}") + set(CPACK_PACKAGE_RELOCATABLE OFF) + set(CPACK_PACKAGE_VENDOR "The prometheus-cpp authors") + + if(CMAKE_VERSION VERSION_LESS "3.12") + set(CPACK_PACKAGE_VERSION_MAJOR "${PROJECT_VERSION_MAJOR}") + set(CPACK_PACKAGE_VERSION_MINOR "${PROJECT_VERSION_MINOR}") + set(CPACK_PACKAGE_VERSION_PATCH "${PROJECT_VERSION_PATCH}") + endif() + + set(CPACK_DEBIAN_PACKAGE_GENERATE_SHLIBS ON) + set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) + set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) + + set(CPACK_RPM_PACKAGE_AUTOREQPROV ON) + set(CPACK_RPM_FILE_NAME RPM-DEFAULT) + + include(CPack) +endif() + +# summary + +include(FeatureSummary) +add_feature_info("Pull" "${ENABLE_PULL}" "support for pulling metrics") +add_feature_info("Push" "${ENABLE_PUSH}" "support for pushing metrics to a push-gateway") +add_feature_info("Compression" "${ENABLE_COMPRESSION}" "support for zlib compression of metrics") +add_feature_info("pkg-config" "${GENERATE_PKGCONFIG}" "generate pkg-config files") +add_feature_info("IYWU" "${RUN_IWYU}" "include-what-you-use") +feature_summary(WHAT ALL) diff --git a/LICENSE b/LICENSE index 8aa26455..0e08d61d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,10 @@ MIT License -Copyright (c) [year] [fullname] +Copyright (c) 2016-2019 Jupp Mueller +Copyright (c) 2017-2019 Gregor Jasny + +And many contributors, see +https://github.com/jupp0r/prometheus-cpp/graphs/contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8395359f..3647861b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -# Prometheus Client Library for Modern C++ [![Build Status](https://travis-ci.org/jupp0r/prometheus-cpp.svg?branch=master)](https://travis-ci.org/jupp0r/prometheus-cpp)[![Coverage Status](https://coveralls.io/repos/github/jupp0r/prometheus-cpp/badge.svg?branch=master)](https://coveralls.io/github/jupp0r/prometheus-cpp?branch=master)[![Coverity Scan](https://scan.coverity.com/projects/10567/badge.svg)](https://scan.coverity.com/projects/jupp0r-prometheus-cpp) +# Prometheus Client Library for Modern C++ + +[![CI Status](https://github.com/jupp0r/prometheus-cpp/workflows/Continuous%20Integration/badge.svg)](https://github.com/jupp0r/prometheus-cpp/actions?workflow=Continuous+Integration) +[![Travis Status](https://travis-ci.org/jupp0r/prometheus-cpp.svg?branch=master)](https://travis-ci.org/jupp0r/prometheus-cpp) +[![Coverage Status](https://coveralls.io/repos/github/jupp0r/prometheus-cpp/badge.svg?branch=master)](https://coveralls.io/github/jupp0r/prometheus-cpp?branch=master) +[![Coverity Scan](https://scan.coverity.com/projects/10567/badge.svg)](https://scan.coverity.com/projects/jupp0r-prometheus-cpp) This library aims to enable [Metrics-Driven Development](https://sookocheff.com/post/mdd/mdd/) for @@ -10,53 +15,84 @@ other push/pull collections can be added as plugins. ## Usage +See https://jupp0r.github.io/prometheus-cpp for more detailed interface documentation. + ``` c++ +#include +#include +#include + +#include #include -#include +#include #include #include #include -#include -#include - -int main(int argc, char** argv) { +int main() { using namespace prometheus; // create an http server running on port 8080 Exposer exposer{"127.0.0.1:8080"}; - // create a metrics registry with component=main labels applied to all its - // metrics + // create a metrics registry + // @note it's the users responsibility to keep the object alive auto registry = std::make_shared(); // add a new counter family to the registry (families combine values with the // same name, but distinct label dimensions) - auto& counter_family = BuildCounter() - .Name("time_running_seconds_total") - .Help("How many seconds is this server running?") - .Labels({{"label", "value"}}) + // + // @note please follow the metric-naming best-practices: + // https://prometheus.io/docs/practices/naming/ + auto& packet_counter = BuildCounter() + .Name("observed_packets_total") + .Help("Number of observed packets") .Register(*registry); - // add a counter to the metric family - auto& second_counter = counter_family.Add( - {{"another_label", "value"}, {"yet_another_label", "value"}}); - - // ask the exposer to scrape the registry on incoming scrapes + // add and remember dimensional data, incrementing those is very cheap + auto& tcp_rx_counter = + packet_counter.Add({{"protocol", "tcp"}, {"direction", "rx"}}); + auto& tcp_tx_counter = + packet_counter.Add({{"protocol", "tcp"}, {"direction", "tx"}}); + auto& udp_rx_counter = + packet_counter.Add({{"protocol", "udp"}, {"direction", "rx"}}); + auto& udp_tx_counter = + packet_counter.Add({{"protocol", "udp"}, {"direction", "tx"}}); + + // add a counter whose dimensional data is not known at compile time + // nevertheless dimensional values should only occur in low cardinality: + // https://prometheus.io/docs/practices/naming/#labels + auto& http_requests_counter = BuildCounter() + .Name("http_requests_total") + .Help("Number of HTTP requests") + .Register(*registry); + + // ask the exposer to scrape the registry on incoming HTTP requests exposer.RegisterCollectable(registry); for (;;) { std::this_thread::sleep_for(std::chrono::seconds(1)); - // increment the counter by one (second) - second_counter.Increment(); + const auto random_value = std::rand(); + + if (random_value & 1) tcp_rx_counter.Increment(); + if (random_value & 2) tcp_tx_counter.Increment(); + if (random_value & 4) udp_rx_counter.Increment(); + if (random_value & 8) udp_tx_counter.Increment(); + + const std::array methods = {"GET", "PUT", "POST", "HEAD"}; + auto method = methods.at(random_value % methods.size()); + // dynamically calling Family.Add() works but is slow and should be + // avoided + http_requests_counter.Add({{"method", method}}).Increment(); } return 0; } + ``` ## Requirements -Using `prometheus-cpp` requires a C++11 compliant compiler. It has been successfully tested with GNU GCC 4.8 on Ubuntu Trusty and Visual Studio 2017 (but Visual Studio 2015 should work, too). +Using `prometheus-cpp` requires a C++11 compliant compiler. It has been successfully tested with GNU GCC 7.4 on Ubuntu Bionic (18.04) and Visual Studio 2017 (but Visual Studio 2015 should work, too). ## Building @@ -66,11 +102,15 @@ and [bazel](https://bazel.io). Both are tested in CI and should work on master and for all releases. In case these instructions don't work for you, looking at -the [travis build script](.travis.yml) might help. +the [GitHub Workflows](.github/workflows) might help. ### via CMake -For CMake builds don't forget to fetch the submodules first. Then build as usual. +For CMake builds don't forget to fetch the submodules first. Please note that +[zlib](https://zlib.net/) and [libcurl](https://curl.se/) are not provided by +the included submodules. In the example below their usage is disabled. + +Then build as usual. ``` shell # fetch third-party dependencies @@ -81,17 +121,16 @@ mkdir _build cd _build # run cmake -cmake .. -DBUILD_SHARED_LIBS=ON # or OFF for static libraries +cmake .. -DBUILD_SHARED_LIBS=ON -DENABLE_PUSH=OFF -DENABLE_COMPRESSION=OFF # build -make -j 4 +cmake --build . --parallel 4 # run tests ctest -V # install the libraries and headers -mkdir -p deploy -make DESTDIR=`pwd`/deploy install +cmake --install . ``` ### via Bazel @@ -101,13 +140,14 @@ this repo to your project as a dependency. Just add the following to your `WORKSPACE`: ```python +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file") http_archive( name = "com_github_jupp0r_prometheus_cpp", strip_prefix = "prometheus-cpp-master", urls = ["https://github.com/jupp0r/prometheus-cpp/archive/master.zip"], ) -load("@com_github_jupp0r_prometheus_cpp//:repositories.bzl", "prometheus_cpp_repositories") +load("@com_github_jupp0r_prometheus_cpp//bazel:repositories.bzl", "prometheus_cpp_repositories") prometheus_cpp_repositories() ``` @@ -119,21 +159,83 @@ demonstrated with the sample server included in this repository: cc_binary( name = "sample_server", srcs = ["sample_server.cc"], - deps = ["@com_github_jupp0r_prometheus_cpp//:prometheus_cpp"], + deps = ["@com_github_jupp0r_prometheus_cpp//pull"], ) ``` When you call `prometheus_cpp_repositories()` in your `WORKSPACE` file, -you introduce the following dependencies, if they do not exist yet, to your project: +you load the following dependencies, if they do not exist yet, into your project: + +* `civetweb` for [Civetweb](https://github.com/civetweb/civetweb) +* `com_google_googletest` for [Google Test](https://github.com/google/googletest) +* `com_github_google_benchmark` for [Google Benchmark](https://github.com/google/benchmark) +* `com_github_curl` for [curl](https://curl.haxx.se/) +* `net_zlib_zlib` for [zlib](http://www.zlib.net/) + +The list of dependencies is also available from file [repositories.bzl](bazel/repositories.bzl). + +## Packaging + +By configuring CPack you can generate an installer like a +Debian package (.deb) or RPM (.rpm) for the static or dynamic +libraries so they can be easily installed on +other systems. + +Please refer to the [CPack](https://cmake.org/cmake/help/latest/module/CPack.html) +documentation for all available generators and their +configuration options. + +To generate a Debian package you could follow these steps: + +``` shell +# fetch third-party dependencies +git submodule update --init -* `load_civetweb()` to load `civetweb` rules for Civetweb -* `load_com_google_googletest()` to load `com_google_googletest` rules for Google gtest -* `load_com_google_googlebenchmark()` to load `com_github_google_benchmark` rules for Googlebenchmark -* `load_com_github_curl()` to load `com_github_curl` rules for curl -* `load_net_zlib_zlib()` to load `net_zlib_zlib` rules for zlib +# run cmake +cmake -B_build -DCPACK_GENERATOR=DEB -DBUILD_SHARED_LIBS=ON # or OFF for static libraries + +# build and package +cmake --build _build --target package --parallel $(nproc) +``` + +This will place an appropriately named .deb in the +`_build` folder. To build a RPM package set the `CPACK_GENERATOR` +variable to `RPM`. + +## Consuming the installed project + +### CMake + +Consuming prometheus-cpp via CMake is the preferred way because all the dependencies +between the three prometheus-cpp libraries are handled correctly. + +The `cmake/project-import` directory contains an +example project and minimal [CMakeLists.txt](cmake/project-import-cmake/CMakeLists.txt). + +### vcpkg + +The [vcpkg](https://github.com/microsoft/vcpkg) package manager contains a +prometheus-cpp port which has been tested on Linux, macOS, and Windows. -The list of dependencies is also available from file `repositories.bzl`. +### Conan +[Conan](https://conan.io/) package manager contains prometheus-cpp package as well +in [ConanCenter](https://conan.io/center/prometheus-cpp) repository + +### Plain Makefiles + +When manually linking prometheus-cpp the library order matters. The needed +libraries depend on the individual use case but the following should work for the pull metrics approach: + +``` +-lprometheus-cpp-pull -lprometheus-cpp-core -lz +``` + +For the push-workflow please try: + +``` +-lprometheus-cpp-push -lprometheus-cpp-core -lcurl -lz +``` ## Contributing @@ -259,9 +361,9 @@ BM_Summary_Collect_Common/262144 128723 ns 126987 ns 55 ``` ## Project Status -Beta, getting ready for 1.0. The library is pretty stable and used in -production. There are some small breaking API changes that might -happen before 1.0 Parts of the library are instrumented by itself +Stable and used in production. + +Parts of the library are instrumented by itself (bytes scraped, number of scrapes, scrape request latencies). There is a working [example](pull/tests/integration/sample_server.cc) that's scraped by telegraf as part of integration tests. diff --git a/WORKSPACE b/WORKSPACE index ee0a24fa..a7b4172e 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -1,5 +1,5 @@ workspace(name = "com_github_jupp0r_prometheus_cpp") -load(":repositories.bzl", "prometheus_cpp_repositories") +load("//bazel:repositories.bzl", "prometheus_cpp_repositories") prometheus_cpp_repositories() diff --git a/bazel/BUILD.bazel b/bazel/BUILD.bazel index a9f760ac..271eb33a 100644 --- a/bazel/BUILD.bazel +++ b/bazel/BUILD.bazel @@ -1,4 +1,7 @@ exports_files( - glob(["*.BUILD"]), + glob([ + "*.BUILD", + "*.tpl", + ]), visibility = ["//visibility:public"], ) diff --git a/bazel/civetweb.BUILD b/bazel/civetweb.BUILD index e8d60241..31b57aea 100644 --- a/bazel/civetweb.BUILD +++ b/bazel/civetweb.BUILD @@ -1,28 +1,24 @@ licenses(["notice"]) # MIT license config_setting( - name = "darwin", - values = {"cpu": "darwin"},) - -config_setting( - name = "darwin_x86_64", - values = {"cpu": "darwin_x86_64"}, + name = "osx", + constraint_values = [ + "@bazel_tools//platforms:osx", + ], ) config_setting( name = "windows", - values = { "cpu": "x64_windows" }, -) - -config_setting( - name = "windows_msvc", - values = {"cpu": "x64_windows_msvc"}, + constraint_values = [ + "@bazel_tools//platforms:windows", + ], ) cc_library( name = "libcivetweb", srcs = [ "src/civetweb.c", + "src/response.inl", ], hdrs = [ "include/civetweb.h", @@ -41,13 +37,10 @@ cc_library( ], linkopts = select({ ":windows": [], - ":windows_msvc": [], "//conditions:default": ["-lpthread"], }) + select({ - ":darwin": [], - ":darwin_x86_64": [], + ":osx": [], ":windows": [], - ":windows_msvc": [], "//conditions:default": ["-lrt"], }), textual_hdrs = [ @@ -65,9 +58,6 @@ cc_library( hdrs = [ "include/CivetServer.h", ], - deps = [ - ":libcivetweb", - ], copts = [ "-DUSE_IPV6", "-DNDEBUG", @@ -81,14 +71,14 @@ cc_library( ], linkopts = select({ ":windows": [], - ":windows_msvc": [], "//conditions:default": ["-lpthread"], }) + select({ - ":darwin": [], - ":darwin_x86_64": [], + ":osx": [], ":windows": [], - ":windows_msvc": [], "//conditions:default": ["-lrt"], }), visibility = ["//visibility:public"], -) \ No newline at end of file + deps = [ + ":libcivetweb", + ], +) diff --git a/bazel/curl.BUILD b/bazel/curl.BUILD index 38301b50..eb045de2 100644 --- a/bazel/curl.BUILD +++ b/bazel/curl.BUILD @@ -18,18 +18,22 @@ licenses(["notice"]) # MIT/X derivative license load("@com_github_jupp0r_prometheus_cpp//bazel:curl.bzl", "CURL_COPTS") -package(features = ['no_copts_tokenization']) +package(features = ["no_copts_tokenization"]) config_setting( name = "windows", - values = {"cpu": "x64_windows"}, - visibility = [ "//visibility:private" ], + constraint_values = [ + "@bazel_tools//platforms:windows", + ], + visibility = ["//visibility:private"], ) config_setting( name = "osx", - values = {"cpu": "darwin"}, - visibility = [ "//visibility:private" ], + constraint_values = [ + "@bazel_tools//platforms:osx", + ], + visibility = ["//visibility:private"], ) cc_library( @@ -41,21 +45,28 @@ cc_library( "include/curl/*.h", "lib/**/*.h", ]), + copts = CURL_COPTS + [ + '-DOS="os"', + ], defines = ["CURL_STATICLIB"], - includes = ["include/", "lib/"], - linkopts = select({ + includes = [ + "include/", + "lib/", + ], + linkopts = select({ "//:windows": [ "-DEFAULTLIB:ws2_32.lib", "-DEFAULTLIB:advapi32.lib", "-DEFAULTLIB:crypt32.lib", "-DEFAULTLIB:Normaliz.lib", ], + "//:osx": [ + "-framework SystemConfiguration", + "-lpthread", + ], "//conditions:default": [ "-lpthread", ], }), - copts = CURL_COPTS + [ - '-DOS="os"', - ], visibility = ["//visibility:public"], ) diff --git a/bazel/curl.bzl b/bazel/curl.bzl index f1cbf10f..796fe41e 100644 --- a/bazel/curl.bzl +++ b/bazel/curl.bzl @@ -55,7 +55,6 @@ BASE_CURL_COPTS = [ "-DHAVE_GETHOSTBYNAME_R_6=1", "-DHAVE_GETHOSTNAME=1", "-DHAVE_GETIFADDRS=1", - "-DHAVE_GETNAMEINFO=1", "-DHAVE_GETPPID=1", "-DHAVE_GETPWUID=1", "-DHAVE_GETPWUID_R=1", @@ -90,6 +89,7 @@ BASE_CURL_COPTS = [ "-DHAVE_PTHREAD_H=1", "-DHAVE_PWD_H=1", "-DHAVE_RECV=1", + "-DHAVE_SA_FAMILY_T=1", "-DHAVE_SELECT=1", "-DHAVE_SEND=1", "-DHAVE_SETJMP_H=1", @@ -150,7 +150,6 @@ BASE_CURL_COPTS = [ "-DRECV_TYPE_ARG3=size_t", "-DRECV_TYPE_ARG4=int", "-DRECV_TYPE_RETV=ssize_t", - "-DRETSIGTYPE=void", "-DSELECT_QUAL_ARG5=", "-DSELECT_TYPE_ARG1=int", "-DSELECT_TYPE_ARG234=fd_set*", @@ -182,7 +181,6 @@ BASE_CURL_COPTS = [ LINUX_CURL_COPTS = [ "-DHAVE_LINUX_TCP_H=1", "-DHAVE_MSG_NOSIGNAL=1", - "-DHAVE_STROPTS_H=1", ] CURL_COPTS = select({ diff --git a/bazel/dummy_export.h.tpl b/bazel/dummy_export.h.tpl new file mode 100644 index 00000000..84c44eff --- /dev/null +++ b/bazel/dummy_export.h.tpl @@ -0,0 +1,4 @@ +#pragma once + +#define {BASE_NAME}_EXPORT +#define {BASE_NAME}_NO_EXPORT diff --git a/bazel/export_header.bzl b/bazel/export_header.bzl new file mode 100644 index 00000000..4637562a --- /dev/null +++ b/bazel/export_header.bzl @@ -0,0 +1,21 @@ +def _generate_dummy_export_header_impl(ctx): + ctx.actions.expand_template( + template = ctx.file._template, + output = ctx.outputs.header_file, + substitutions = { + "{BASE_NAME}": ctx.attr.basename, + }, + ) + +generate_dummy_export_header = rule( + attrs = { + "basename": attr.string(mandatory = True), + "header": attr.string(mandatory = True), + "_template": attr.label( + allow_single_file = True, + default = Label("@com_github_jupp0r_prometheus_cpp//bazel:dummy_export.h.tpl"), + ), + }, + implementation = _generate_dummy_export_header_impl, + outputs = {"header_file": "%{header}"}, +) diff --git a/bazel/repositories.bzl b/bazel/repositories.bzl new file mode 100644 index 00000000..0fe8e252 --- /dev/null +++ b/bazel/repositories.bzl @@ -0,0 +1,58 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") + +def prometheus_cpp_repositories(): + maybe( + http_archive, + name = "civetweb", + strip_prefix = "civetweb-1.15", + sha256 = "90a533422944ab327a4fbb9969f0845d0dba05354f9cacce3a5005fa59f593b9", + urls = [ + "https://github.com/civetweb/civetweb/archive/v1.15.tar.gz", + ], + build_file = "@com_github_jupp0r_prometheus_cpp//bazel:civetweb.BUILD", + ) + + maybe( + http_archive, + name = "com_google_googletest", + sha256 = "b4870bf121ff7795ba20d20bcdd8627b8e088f2d1dab299a031c1034eddc93d5", + strip_prefix = "googletest-release-1.11.0", + urls = [ + "https://github.com/google/googletest/archive/release-1.11.0.tar.gz", + ], + ) + + maybe( + http_archive, + name = "com_github_curl", + sha256 = "910cc5fe279dc36e2cca534172c94364cf3fcf7d6494ba56e6c61a390881ddce", + strip_prefix = "curl-7.82.0", + urls = [ + "https://github.com/curl/curl/releases/download/curl-7_82_0/curl-7.82.0.tar.gz", + "https://curl.haxx.se/download/curl-7.82.0.tar.gz", + ], + build_file = "@com_github_jupp0r_prometheus_cpp//bazel:curl.BUILD", + ) + + maybe( + http_archive, + name = "com_github_google_benchmark", + sha256 = "6132883bc8c9b0df5375b16ab520fac1a85dc9e4cf5be59480448ece74b278d4", + strip_prefix = "benchmark-1.6.1", + urls = [ + "https://github.com/google/benchmark/archive/v1.6.1.tar.gz", + ], + ) + + maybe( + http_archive, + name = "net_zlib_zlib", + sha256 = "c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1", + strip_prefix = "zlib-1.2.11", + urls = [ + "https://mirror.bazel.build/zlib.net/zlib-1.2.11.tar.gz", + "https://zlib.net/zlib-1.2.11.tar.gz", + ], + build_file = "@com_github_jupp0r_prometheus_cpp//bazel:zlib.BUILD", + ) diff --git a/bazel/zlib.BUILD b/bazel/zlib.BUILD index 9091e8eb..34cd232c 100644 --- a/bazel/zlib.BUILD +++ b/bazel/zlib.BUILD @@ -7,7 +7,10 @@ cc_library( srcs = glob(["*.c"]), hdrs = glob(["*.h"]), # Use -Dverbose=-1 to turn off zlib's trace logging. (bazelbuild/bazel#3280) - copts = ["-w", "-Dverbose=-1"], + copts = [ + "-w", + "-Dverbose=-1", + ], includes = ["."], visibility = ["//visibility:public"], -) \ No newline at end of file +) diff --git a/cmake/CheckAtomic.cmake b/cmake/CheckAtomic.cmake new file mode 100644 index 00000000..bbb6e474 --- /dev/null +++ b/cmake/CheckAtomic.cmake @@ -0,0 +1,37 @@ +# Inspired by CheckAtomic.cmake from LLVM project: +# https://github.com/llvm/llvm-project/blob/master/llvm/cmake/modules/CheckAtomic.cmake +# +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +include(CheckCXXSourceCompiles) +include(CheckLibraryExists) + +function(check_working_cxx_atomics varname) + check_cxx_source_compiles(" +#include +#include +std::atomic x(0); +int main() { + std::uint64_t i = x.load(std::memory_order_relaxed); + return static_cast(i); +} +" ${varname}) +endfunction() + +if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + # First check if atomics work without the library. + check_working_cxx_atomics(HAVE_CXX_ATOMICS_WITHOUT_LIB) + # If not, check if the library exists, and atomics work with it. + if(NOT HAVE_CXX_ATOMICS_WITHOUT_LIB) + check_library_exists(atomic __atomic_load_8 "" HAVE_CXX_LIBATOMIC) + if(HAVE_CXX_LIBATOMIC) + list(APPEND CMAKE_REQUIRED_LIBRARIES "atomic") + check_working_cxx_atomics(HAVE_CXX_ATOMICS_WITH_LIB) + if(NOT HAVE_CXX_ATOMICS_WITH_LIB) + message(FATAL_ERROR "Host compiler must support 64-bit std::atomic!") + endif() + else() + message(FATAL_ERROR "Host compiler appears to require libatomic for 64-bit operations, but cannot find it.") + endif() + endif() +endif() diff --git a/cmake/FindGoogleBenchmark.cmake b/cmake/FindGoogleBenchmark.cmake deleted file mode 100644 index 6b50152c..00000000 --- a/cmake/FindGoogleBenchmark.cmake +++ /dev/null @@ -1,25 +0,0 @@ -find_library(GoogleBenchmark_LIBRARY NAMES benchmark) -find_path(GoogleBenchmark_INCLUDE_DIR benchmark/benchmark.h) -mark_as_advanced(GoogleBenchmark_LIBRARY GoogleBenchmark_INCLUDE_DIR) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(GoogleBenchmark - FOUND_VAR GoogleBenchmark_FOUND - REQUIRED_VARS - GoogleBenchmark_LIBRARY - GoogleBenchmark_INCLUDE_DIR -) - -if(GoogleBenchmark_FOUND) - set(GoogleBenchmark_LIBRARIES ${GoogleBenchmark_LIBRARY}) - set(GoogleBenchmark_INCLUDE_DIRS ${GoogleBenchmark_INCLUDE_DIR}) - - if(NOT TARGET Google::Benchmark) - add_library(Google::Benchmark UNKNOWN IMPORTED) - set_target_properties(Google::Benchmark PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${GoogleBenchmark_INCLUDE_DIR}" - IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" - IMPORTED_LOCATION "${GoogleBenchmark_LIBRARY}" - ) - endif() -endif() diff --git a/cmake/Findcivetweb.cmake b/cmake/Findcivetweb.cmake deleted file mode 100644 index 8a170269..00000000 --- a/cmake/Findcivetweb.cmake +++ /dev/null @@ -1,56 +0,0 @@ -find_path(CIVETWEB_INCLUDE_DIR - NAMES civetweb.h - DOC "The CivetWeb include directory" -) - -find_path(CIVETWEB_CXX_INCLUDE_DIR - NAMES CivetServer.h - DOC "The CivetWeb C++ include directory" -) - -find_library(CIVETWEB_LIBRARY - NAMES civetweb - DOC "The CivetWeb library" -) - -find_library(CIVETWEB_CXX_LIBRARY - NAMES civetweb-cpp - DOC "The CivetWeb C++ library" -) - -mark_as_advanced(CIVETWEB_LIBRARY CIVETWEB_CXX_LIBRARY CIVETWEB_INCLUDE_DIR CIVETWEB_CXX_INCLUDE_DIR) - -if(CIVETWEB_INCLUDE_DIR AND EXISTS "${CIVETWEB_INCLUDE_DIR}/civetweb.h") - file(STRINGS "${CIVETWEB_INCLUDE_DIR}/civetweb.h" civetweb_version_str REGEX "^#define[\t ]+CIVETWEB_VERSION[\t ]+\".*\"") - - string(REGEX REPLACE "^.*CIVETWEB_VERSION[\t ]+\"([^\"]*)\".*$" "\\1" CIVETWEB_VERSION_STRING "${civetweb_version_str}") - unset(civetweb_version_str) -endif() - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(civetweb - FOUND_VAR civetweb_FOUND - REQUIRED_VARS CIVETWEB_LIBRARY CIVETWEB_CXX_LIBRARY CIVETWEB_INCLUDE_DIR CIVETWEB_CXX_INCLUDE_DIR - VERSION_VAR CIVETWEB_VERSION_STRING) - -if(civetweb_FOUND) - set(CIVETWEB_LIBRARIES civetweb::civetweb civetweb::civetweb-cpp) - set(CIVETWEB_INCLUDE_DIRS "${CIVETWEB_INCLUDE_DIR}" "${CIVETWEB_CXX_INCLUDE_DIR}") - if(NOT TARGET civetweb::civetweb) - add_library(civetweb::civetweb UNKNOWN IMPORTED) - set_target_properties(civetweb::civetweb PROPERTIES - IMPORTED_LOCATION "${CIVETWEB_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${CIVETWEB_INCLUDE_DIR}" - IMPORTED_LINK_INTERFACE_LANGUAGES "C" - ) - endif() - if(NOT TARGET civetweb::civetweb-cpp) - add_library(civetweb::civetweb-cpp UNKNOWN IMPORTED) - set_target_properties(civetweb::civetweb-cpp PROPERTIES - IMPORTED_LOCATION "${CIVETWEB_CXX_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${CIVETWEB_CXX_INCLUDE_DIR}" - IMPORTED_LINK_INTERFACE_LIBRARIES "civetweb::civetweb" - IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" - ) - endif() -endif() diff --git a/cmake/civetweb-3rdparty-config.cmake b/cmake/civetweb-3rdparty-config.cmake index 8e20fef1..8cc73cf9 100644 --- a/cmake/civetweb-3rdparty-config.cmake +++ b/cmake/civetweb-3rdparty-config.cmake @@ -19,6 +19,11 @@ add_library(civetweb OBJECT ${_IMPORT_PREFIX}/src/md5.inl ) +set_property(TARGET civetweb PROPERTY PUBLIC_HEADER + ${_IMPORT_PREFIX}/include/CivetServer.h + ${_IMPORT_PREFIX}/include/civetweb.h +) + target_compile_definitions(civetweb PRIVATE CIVETWEB_API= @@ -26,8 +31,8 @@ target_compile_definitions(civetweb NDEBUG NO_CGI NO_CACHING - NO_SSL NO_FILES + SOCKET_TIMEOUT_QUANTUM=200 ) target_compile_options(civetweb @@ -41,6 +46,18 @@ target_include_directories(civetweb ${CIVETWEB_INCLUDE_DIRS} ) +if(THIRDPARTY_CIVETWEB_WITH_SSL) + include(CMakeFindDependencyMacro) + find_dependency(OpenSSL) + if(OPENSSL_VERSION VERSION_GREATER_EQUAL 1.1) + target_compile_definitions(civetweb PRIVATE OPENSSL_API_1_1) + endif() + target_compile_definitions(civetweb PRIVATE NO_SSL_DL) + target_link_libraries(civetweb PUBLIC OpenSSL::SSL) +else() + target_compile_definitions(civetweb PRIVATE NO_SSL) +endif() + if(BUILD_SHARED_LIBS) set_target_properties(civetweb PROPERTIES POSITION_INDEPENDENT_CODE ON @@ -49,3 +66,8 @@ if(BUILD_SHARED_LIBS) VISIBILITY_INLINES_HIDDEN ON ) endif() + +set_target_properties(civetweb PROPERTIES + C_INCLUDE_WHAT_YOU_USE "" + CXX_INCLUDE_WHAT_YOU_USE "" +) diff --git a/cmake/googlemock-3rdparty-config.cmake b/cmake/googlemock-3rdparty-config.cmake index c7bc4428..66defd2f 100644 --- a/cmake/googlemock-3rdparty-config.cmake +++ b/cmake/googlemock-3rdparty-config.cmake @@ -8,7 +8,7 @@ add_library(gmock_main STATIC EXCLUDE_FROM_ALL ${_IMPORT_PREFIX}/googlemock/src/gmock_main.cc ) -target_include_directories(gmock_main +target_include_directories(gmock_main SYSTEM PUBLIC ${_IMPORT_PREFIX}/googletest/include ${_IMPORT_PREFIX}/googlemock/include @@ -22,3 +22,8 @@ target_link_libraries(gmock_main Threads::Threads ) add_library(GTest::gmock_main ALIAS gmock_main) + +set_target_properties(gmock_main PROPERTIES + C_INCLUDE_WHAT_YOU_USE "" + CXX_INCLUDE_WHAT_YOU_USE "" +) diff --git a/cmake/googletest.imp b/cmake/googletest.imp new file mode 100644 index 00000000..c23b9f06 --- /dev/null +++ b/cmake/googletest.imp @@ -0,0 +1,5 @@ +[ + { include: [ "@", private, "", public ] }, + { include: [ "@", private, "", public ] }, + { include: [ "@", private, "", public ]} +] diff --git a/cmake/project-import-cmake/CMakeLists.txt b/cmake/project-import-cmake/CMakeLists.txt new file mode 100644 index 00000000..6fb0c6c9 --- /dev/null +++ b/cmake/project-import-cmake/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.12 FATAL_ERROR) + +project(prometheus-cpp-import) + +set(CMAKE_CXX_STANDARD 11) + +find_package(prometheus-cpp CONFIG REQUIRED) + +if(PROMETHEUS_CPP_ENABLE_PUSH) + add_executable(sample-client sample_client.cc) + target_link_libraries(sample-client PRIVATE prometheus-cpp::push) +endif() + +if(PROMETHEUS_CPP_ENABLE_PULL) + add_executable(sample-server sample_server.cc) + target_link_libraries(sample-server PRIVATE prometheus-cpp::pull) +endif() diff --git a/cmake/project-import-cmake/sample_client.cc b/cmake/project-import-cmake/sample_client.cc new file mode 120000 index 00000000..4c67af80 --- /dev/null +++ b/cmake/project-import-cmake/sample_client.cc @@ -0,0 +1 @@ +../../push/tests/integration/sample_client.cc \ No newline at end of file diff --git a/cmake/project-import-cmake/sample_server.cc b/cmake/project-import-cmake/sample_server.cc new file mode 120000 index 00000000..89f9e5c0 --- /dev/null +++ b/cmake/project-import-cmake/sample_server.cc @@ -0,0 +1 @@ +../../pull/tests/integration/sample_server.cc \ No newline at end of file diff --git a/cmake/project-import-pkgconfig/CMakeLists.txt b/cmake/project-import-pkgconfig/CMakeLists.txt new file mode 100644 index 00000000..bcd1d8e1 --- /dev/null +++ b/cmake/project-import-pkgconfig/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.12 FATAL_ERROR) + +project(prometheus-cpp-import) + +set(CMAKE_CXX_STANDARD 11) + +find_package(PkgConfig REQUIRED) + +pkg_check_modules(PROMETHEUS_CPP_CORE REQUIRED prometheus-cpp-core) +pkg_check_modules(PROMETHEUS_CPP_PUSH IMPORTED_TARGET prometheus-cpp-push) +pkg_check_modules(PROMETHEUS_CPP_PULL IMPORTED_TARGET prometheus-cpp-pull) + +if(PROMETHEUS_CPP_PUSH_FOUND) + add_executable(sample-client sample_client.cc) + target_link_libraries(sample-client PRIVATE PkgConfig::PROMETHEUS_CPP_PUSH) +endif() + +if(PROMETHEUS_CPP_PULL_FOUND) + add_executable(sample-server sample_server.cc) + target_link_libraries(sample-server PRIVATE PkgConfig::PROMETHEUS_CPP_PULL) +endif() diff --git a/cmake/project-import-pkgconfig/sample_client.cc b/cmake/project-import-pkgconfig/sample_client.cc new file mode 120000 index 00000000..4c67af80 --- /dev/null +++ b/cmake/project-import-pkgconfig/sample_client.cc @@ -0,0 +1 @@ +../../push/tests/integration/sample_client.cc \ No newline at end of file diff --git a/cmake/project-import-pkgconfig/sample_server.cc b/cmake/project-import-pkgconfig/sample_server.cc new file mode 120000 index 00000000..89f9e5c0 --- /dev/null +++ b/cmake/project-import-pkgconfig/sample_server.cc @@ -0,0 +1 @@ +../../pull/tests/integration/sample_server.cc \ No newline at end of file diff --git a/cmake/prometheus-cpp-config.cmake.in b/cmake/prometheus-cpp-config.cmake.in index 9c64914e..fe9606e0 100644 --- a/cmake/prometheus-cpp-config.cmake.in +++ b/cmake/prometheus-cpp-config.cmake.in @@ -6,11 +6,23 @@ set_and_check(prometheus-cpp_INCLUDE_DIR "@PACKAGE_CMAKE_INSTALL_INCLUDEDIR@") set(PROMETHEUS_CPP_ENABLE_PULL @ENABLE_PULL@) set(PROMETHEUS_CPP_ENABLE_PUSH @ENABLE_PUSH@) set(PROMETHEUS_CPP_USE_COMPRESSION @ENABLE_COMPRESSION@) +set(PROMETHEUS_CPP_USE_THIRDPARTY_LIBRARIES @USE_THIRDPARTY_LIBRARIES@) +set(PROMETHEUS_CPP_THIRDPARTY_CIVETWEB_WITH_SSL @THIRDPARTY_CIVETWEB_WITH_SSL@) set(CMAKE_THREAD_PREFER_PTHREAD TRUE) find_dependency(Threads) unset(CMAKE_THREAD_PREFER_PTHREAD) +if(PROMETHEUS_CPP_ENABLE_PULL) + if(PROMETHEUS_CPP_USE_THIRDPARTY_LIBRARIES) + if(PROMETHEUS_CPP_THIRDPARTY_CIVETWEB_WITH_SSL) + find_dependency(OpenSSL) + endif() + else() + find_dependency(civetweb) + endif() +endif() + if(PROMETHEUS_CPP_ENABLE_PULL AND PROMETHEUS_CPP_USE_COMPRESSION) find_dependency(ZLIB) endif() diff --git a/cmake/prometheus-cpp-core.pc.in b/cmake/prometheus-cpp-core.pc.in new file mode 100644 index 00000000..8a24ced8 --- /dev/null +++ b/cmake/prometheus-cpp-core.pc.in @@ -0,0 +1,14 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ +libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ + +Name: @PROJECT_NAME@-core +Description: @PROJECT_DESCRIPTION@ +URL: @PROJECT_HOMEPAGE_URL@ +Version: @PROJECT_VERSION@ +Requires: +Requires.private: @PKGCONFIG_REQUIRES@ +Cflags: -I${includedir} +Libs: -L${libdir} -l@PROJECT_NAME@-core +Libs.private: @CMAKE_THREAD_LIBS_INIT@ @PKGCONFIG_LIBS@ \ No newline at end of file diff --git a/cmake/prometheus-cpp-pull.pc.in b/cmake/prometheus-cpp-pull.pc.in new file mode 100644 index 00000000..652848f6 --- /dev/null +++ b/cmake/prometheus-cpp-pull.pc.in @@ -0,0 +1,14 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ +libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ + +Name: @PROJECT_NAME@-pull +Description: @PROJECT_DESCRIPTION@ +URL: @PROJECT_HOMEPAGE_URL@ +Version: @PROJECT_VERSION@ +Requires: @PROJECT_NAME@-core +Requires.private: @PKGCONFIG_REQUIRES@ +Cflags: -I${includedir} +Libs: -L${libdir} -l@PROJECT_NAME@-pull +Libs.private: @CMAKE_THREAD_LIBS_INIT@ @PKGCONFIG_LIBS@ diff --git a/cmake/prometheus-cpp-push.pc.in b/cmake/prometheus-cpp-push.pc.in new file mode 100644 index 00000000..a94484d1 --- /dev/null +++ b/cmake/prometheus-cpp-push.pc.in @@ -0,0 +1,14 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ +libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ + +Name: @PROJECT_NAME@-push +Description: @PROJECT_DESCRIPTION@ +URL: @PROJECT_HOMEPAGE_URL@ +Version: @PROJECT_VERSION@ +Requires: @PROJECT_NAME@-core +Requires.private: @PKGCONFIG_REQUIRES@ +Cflags: -I${includedir} +Libs: -L${libdir} -l@PROJECT_NAME@-push +Libs.private: @CMAKE_THREAD_LIBS_INIT@ @PKGCONFIG_LIBS@ diff --git a/core/BUILD.bazel b/core/BUILD.bazel index 263c37b8..a302f195 100644 --- a/core/BUILD.bazel +++ b/core/BUILD.bazel @@ -1,3 +1,11 @@ +load("//bazel:export_header.bzl", "generate_dummy_export_header") + +generate_dummy_export_header( + name = "export_header", + basename = "PROMETHEUS_CPP_CORE", + header = "include/prometheus/detail/core_export.h", +) + cc_library( name = "core", srcs = glob([ @@ -6,7 +14,7 @@ cc_library( ]), hdrs = glob( ["include/**/*.h"], - ), + ) + [":export_header"], strip_include_prefix = "include", visibility = ["//visibility:public"], ) diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 219cc82c..13b27624 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -2,13 +2,11 @@ add_library(core src/check_names.cc src/counter.cc + src/detail/builder.cc src/detail/ckms_quantiles.cc - src/detail/counter_builder.cc - src/detail/gauge_builder.cc - src/detail/histogram_builder.cc - src/detail/summary_builder.cc src/detail/time_window_quantiles.cc src/detail/utils.cc + src/family.cc src/gauge.cc src/histogram.cc src/registry.cc @@ -25,12 +23,30 @@ target_link_libraries(core $<$,$>>:rt> ) +if(HAVE_CXX_LIBATOMIC) + # the exported library config must use libatomic unconditionally + # (the HAVE_CXX_LIBATOMIC variable should not leak into the target config) + target_link_libraries(core PUBLIC atomic) +endif() + target_include_directories(core PUBLIC $ + $ ) -set_target_properties(core PROPERTIES OUTPUT_NAME ${PROJECT_NAME}-core) +set_target_properties(core + PROPERTIES + OUTPUT_NAME ${PROJECT_NAME}-core + DEFINE_SYMBOL PROMETHEUS_CPP_CORE_EXPORTS + VERSION "${PROJECT_VERSION}" + SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}" +) + +generate_export_header(core + BASE_NAME ${PROJECT_NAME}-core + EXPORT_FILE_NAME include/prometheus/detail/core_export.h +) install( TARGETS core @@ -42,14 +58,34 @@ install( ) install( - DIRECTORY include/ + DIRECTORY include/ ${CMAKE_CURRENT_BINARY_DIR}/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) +if(GENERATE_PKGCONFIG) + set(PKGCONFIG_LIBS) + set(PKGCONFIG_REQUIRES) + + if(HAVE_CXX_LIBATOMIC) + string(APPEND PKGCONFIG_LIBS " -latomic") + endif() + + configure_file( + ${PROJECT_SOURCE_DIR}/cmake/prometheus-cpp-core.pc.in + ${CMAKE_CURRENT_BINARY_DIR}/prometheus-cpp-core.pc + @ONLY + ) + + install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/prometheus-cpp-core.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig + ) +endif() + if(ENABLE_TESTING) add_subdirectory(tests) endif() -if(GoogleBenchmark_FOUND) +if(benchmark_FOUND) add_subdirectory(benchmarks) endif() diff --git a/core/benchmarks/BUILD.bazel b/core/benchmarks/BUILD.bazel index bd48104e..3efb7be3 100644 --- a/core/benchmarks/BUILD.bazel +++ b/core/benchmarks/BUILD.bazel @@ -4,7 +4,7 @@ cc_binary( "*.cc", "*.h", ]), - linkstatic = 1, + linkstatic = True, deps = [ "//core", "@com_github_google_benchmark//:benchmark", diff --git a/core/benchmarks/CMakeLists.txt b/core/benchmarks/CMakeLists.txt index dd9f2230..c0d2dd09 100644 --- a/core/benchmarks/CMakeLists.txt +++ b/core/benchmarks/CMakeLists.txt @@ -13,7 +13,7 @@ add_executable(benchmarks target_link_libraries(benchmarks PRIVATE ${PROJECT_NAME}::core - Google::Benchmark + benchmark::benchmark ) add_test( diff --git a/core/benchmarks/benchmark_helpers.cc b/core/benchmarks/benchmark_helpers.cc index 12cb5a20..78f719c2 100644 --- a/core/benchmarks/benchmark_helpers.cc +++ b/core/benchmarks/benchmark_helpers.cc @@ -1,12 +1,13 @@ +#include "benchmark_helpers.h" + #include #include +#include -#include "benchmark_helpers.h" - -std::string GenerateRandomString(size_t length) { +std::string GenerateRandomString(std::size_t length) { auto randchar = []() -> char { const char charset[] = "abcdefghijklmnopqrstuvwxyz"; - const size_t max_index = (sizeof(charset) - 1); + const std::size_t max_index = (sizeof(charset) - 1); return charset[rand() % max_index]; }; std::string str(length, 0); @@ -14,10 +15,9 @@ std::string GenerateRandomString(size_t length) { return str; } -std::map GenerateRandomLabels( - std::size_t number_of_pairs) { +prometheus::Labels GenerateRandomLabels(std::size_t number_of_pairs) { const auto label_character_count = 10; - auto label_pairs = std::map{}; + auto label_pairs = prometheus::Labels{}; for (std::size_t i = 0; i < number_of_pairs; i++) { label_pairs.insert({GenerateRandomString(label_character_count), GenerateRandomString(label_character_count)}); diff --git a/core/benchmarks/benchmark_helpers.h b/core/benchmarks/benchmark_helpers.h index 0b449a67..51866764 100644 --- a/core/benchmarks/benchmark_helpers.h +++ b/core/benchmarks/benchmark_helpers.h @@ -1,8 +1,9 @@ #pragma once -#include +#include #include -std::string GenerateRandomString(size_t length); -std::map GenerateRandomLabels( - std::size_t number_of_labels); +#include "prometheus/labels.h" + +std::string GenerateRandomString(std::size_t length); +prometheus::Labels GenerateRandomLabels(std::size_t number_of_labels); diff --git a/core/benchmarks/counter_bench.cc b/core/benchmarks/counter_bench.cc index 34707ac5..be060386 100644 --- a/core/benchmarks/counter_bench.cc +++ b/core/benchmarks/counter_bench.cc @@ -1,5 +1,8 @@ #include -#include + +#include "prometheus/counter.h" +#include "prometheus/family.h" +#include "prometheus/registry.h" static void BM_Counter_Increment(benchmark::State& state) { using prometheus::BuildCounter; diff --git a/core/benchmarks/gauge_bench.cc b/core/benchmarks/gauge_bench.cc index a7aaeb2c..87bcec56 100644 --- a/core/benchmarks/gauge_bench.cc +++ b/core/benchmarks/gauge_bench.cc @@ -1,5 +1,8 @@ #include -#include + +#include "prometheus/family.h" +#include "prometheus/gauge.h" +#include "prometheus/registry.h" static void BM_Gauge_Increment(benchmark::State& state) { using prometheus::BuildGauge; diff --git a/core/benchmarks/histogram_bench.cc b/core/benchmarks/histogram_bench.cc index 4926075d..9bcbb6c0 100644 --- a/core/benchmarks/histogram_bench.cc +++ b/core/benchmarks/histogram_bench.cc @@ -1,13 +1,19 @@ +#include + #include +#include #include +#include -#include -#include +#include "prometheus/family.h" +#include "prometheus/histogram.h" +#include "prometheus/registry.h" using prometheus::Histogram; -static Histogram::BucketBoundaries CreateLinearBuckets(double start, double end, - double step) { +static Histogram::BucketBoundaries CreateLinearBuckets(std::int64_t start, + std::int64_t end, + std::int64_t step) { auto bucket_boundaries = Histogram::BucketBoundaries{}; for (auto i = start; i < end; i += step) { bucket_boundaries.push_back(i); diff --git a/core/benchmarks/registry_bench.cc b/core/benchmarks/registry_bench.cc index ad81432d..b33ddc4d 100644 --- a/core/benchmarks/registry_bench.cc +++ b/core/benchmarks/registry_bench.cc @@ -1,9 +1,11 @@ -#include - #include -#include + +#include #include "benchmark_helpers.h" +#include "prometheus/counter.h" +#include "prometheus/family.h" +#include "prometheus/registry.h" static void BM_Registry_CreateFamily(benchmark::State& state) { using prometheus::BuildCounter; diff --git a/core/benchmarks/summary_bench.cc b/core/benchmarks/summary_bench.cc index 40917eee..1a73807c 100644 --- a/core/benchmarks/summary_bench.cc +++ b/core/benchmarks/summary_bench.cc @@ -1,8 +1,14 @@ +#include + +#include #include +#include #include +#include -#include -#include +#include "prometheus/family.h" +#include "prometheus/registry.h" +#include "prometheus/summary.h" using prometheus::Summary; diff --git a/core/include/prometheus/check_names.h b/core/include/prometheus/check_names.h index 91acf239..2d73ab6c 100644 --- a/core/include/prometheus/check_names.h +++ b/core/include/prometheus/check_names.h @@ -2,8 +2,10 @@ #include +#include "prometheus/detail/core_export.h" + namespace prometheus { -bool CheckMetricName(const std::string& name); -bool CheckLabelName(const std::string& name); +PROMETHEUS_CPP_CORE_EXPORT bool CheckMetricName(const std::string& name); +PROMETHEUS_CPP_CORE_EXPORT bool CheckLabelName(const std::string& name); } // namespace prometheus diff --git a/core/include/prometheus/client_metric.h b/core/include/prometheus/client_metric.h index 4f04281a..b224b80c 100644 --- a/core/include/prometheus/client_metric.h +++ b/core/include/prometheus/client_metric.h @@ -5,9 +5,11 @@ #include #include +#include "prometheus/detail/core_export.h" + namespace prometheus { -struct ClientMetric { +struct PROMETHEUS_CPP_CORE_EXPORT ClientMetric { // Label struct Label { diff --git a/core/include/prometheus/collectable.h b/core/include/prometheus/collectable.h index 3838492b..4d1489a2 100644 --- a/core/include/prometheus/collectable.h +++ b/core/include/prometheus/collectable.h @@ -3,6 +3,8 @@ #include #include +#include "prometheus/detail/core_export.h" + namespace prometheus { struct MetricFamily; } @@ -13,13 +15,13 @@ namespace prometheus { /// collect metrics. /// /// A Collectable has to be registered for collection. See Registry. -class Collectable { +class PROMETHEUS_CPP_CORE_EXPORT Collectable { public: virtual ~Collectable() = default; /// \brief Returns a list of metrics and their samples. - virtual std::vector Collect() = 0; - virtual std::vector Collect(std::time_t) = 0; + virtual std::vector Collect() const = 0; + virtual std::vector Collect(std::time_t) const = 0; }; } // namespace prometheus diff --git a/core/include/prometheus/counter.h b/core/include/prometheus/counter.h index a0ecbc6b..9c61654b 100644 --- a/core/include/prometheus/counter.h +++ b/core/include/prometheus/counter.h @@ -3,7 +3,8 @@ #include #include "prometheus/client_metric.h" -#include "prometheus/detail/counter_builder.h" +#include "prometheus/detail/builder.h" // IWYU pragma: export +#include "prometheus/detail/core_export.h" #include "prometheus/gauge.h" #include "prometheus/metric_type.h" @@ -24,7 +25,7 @@ namespace prometheus { /// /// The class is thread-safe. No concurrent call to any API of this type causes /// a data race. -class Counter { +class PROMETHEUS_CPP_CORE_EXPORT Counter { public: static const MetricType metric_type{MetricType::Counter}; @@ -74,11 +75,11 @@ class Counter { /// /// - Name(const std::string&) to set the metric name, /// - Help(const std::string&) to set an additional description. -/// - Label(const std::map&) to assign a set of +/// - Labels(const Labels&) to assign a set of /// key-value pairs (= labels) to the metric. /// /// To finish the configuration of the Counter metric, register it with /// Register(Registry&). -detail::CounterBuilder BuildCounter(); +PROMETHEUS_CPP_CORE_EXPORT detail::Builder BuildCounter(); } // namespace prometheus diff --git a/core/include/prometheus/detail/builder.h b/core/include/prometheus/detail/builder.h new file mode 100644 index 00000000..c145718f --- /dev/null +++ b/core/include/prometheus/detail/builder.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "prometheus/labels.h" + +// IWYU pragma: private +// IWYU pragma: no_include "prometheus/family.h" +// IWYU pragma: no_include "prometheus/registry.h" + +namespace prometheus { + +template +class Family; // IWYU pragma: keep +class Registry; // IWYU pragma: keep + +namespace detail { + +template +class Builder { + public: + Builder& Labels(const ::prometheus::Labels& labels); + Builder& Name(const std::string&); + Builder& Help(const std::string&); + Builder& Seconds(double); + Family& Register(Registry&); + + private: + ::prometheus::Labels labels_; + std::string name_; + std::string help_; + double seconds_; +}; + +} // namespace detail +} // namespace prometheus diff --git a/core/include/prometheus/detail/ckms_quantiles.h b/core/include/prometheus/detail/ckms_quantiles.h index 2fe6fe22..771e816f 100644 --- a/core/include/prometheus/detail/ckms_quantiles.h +++ b/core/include/prometheus/detail/ckms_quantiles.h @@ -5,27 +5,31 @@ #include #include +#include "prometheus/detail/core_export.h" + +// IWYU pragma: private, include "prometheus/summary.h" + namespace prometheus { namespace detail { -class CKMSQuantiles { +class PROMETHEUS_CPP_CORE_EXPORT CKMSQuantiles { public: - struct Quantile { - const double quantile; - const double error; - const double u; - const double v; - + struct PROMETHEUS_CPP_CORE_EXPORT Quantile { Quantile(double quantile, double error); + + double quantile; + double error; + double u; + double v; }; private: struct Item { - /*const*/ double value; + double value; int g; - /*const*/ int delta; + int delta; - explicit Item(double value, int lower_delta, int delta); + Item(double value, int lower_delta, int delta); }; public: diff --git a/core/include/prometheus/detail/counter_builder.h b/core/include/prometheus/detail/counter_builder.h deleted file mode 100644 index 96748c36..00000000 --- a/core/include/prometheus/detail/counter_builder.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include - -namespace prometheus { - -template -class Family; -class Counter; -class Registry; - -namespace detail { - -class CounterBuilder { - public: - CounterBuilder& Labels(const std::map& labels); - CounterBuilder& Name(const std::string&); - CounterBuilder& Help(const std::string&); - CounterBuilder& Seconds(double); - Family& Register(Registry&); - - private: - std::map labels_; - std::string name_; - std::string help_; - double seconds_; -}; - -} // namespace detail -} // namespace prometheus diff --git a/core/include/prometheus/detail/gauge_builder.h b/core/include/prometheus/detail/gauge_builder.h deleted file mode 100644 index 7bd500f0..00000000 --- a/core/include/prometheus/detail/gauge_builder.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include - -namespace prometheus { - -template -class Family; -class Gauge; -class Registry; - -namespace detail { - -class GaugeBuilder { - public: - GaugeBuilder& Labels(const std::map& labels); - GaugeBuilder& Name(const std::string&); - GaugeBuilder& Help(const std::string&); - GaugeBuilder& Seconds(double); - Family& Register(Registry&); - - private: - std::map labels_; - std::string name_; - std::string help_; - double seconds_; -}; - -} // namespace detail -} // namespace prometheus diff --git a/core/include/prometheus/detail/histogram_builder.h b/core/include/prometheus/detail/histogram_builder.h deleted file mode 100644 index 5b3412d1..00000000 --- a/core/include/prometheus/detail/histogram_builder.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include - -namespace prometheus { - -template -class Family; -class Histogram; -class Registry; - -namespace detail { - -class HistogramBuilder { - public: - HistogramBuilder& Labels(const std::map& labels); - HistogramBuilder& Name(const std::string&); - HistogramBuilder& Help(const std::string&); - HistogramBuilder& Seconds(double); - Family& Register(Registry&); - - private: - std::map labels_; - std::string name_; - std::string help_; - double seconds_; -}; - -} // namespace detail -} // namespace prometheus diff --git a/core/include/prometheus/detail/summary_builder.h b/core/include/prometheus/detail/summary_builder.h deleted file mode 100644 index 75aab1cb..00000000 --- a/core/include/prometheus/detail/summary_builder.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include -#include - -namespace prometheus { - -template -class Family; -class Summary; -class Registry; - -namespace detail { - -class SummaryBuilder { - public: - SummaryBuilder& Labels(const std::map& labels); - SummaryBuilder& Name(const std::string&); - SummaryBuilder& Help(const std::string&); - SummaryBuilder& Seconds(double); - Family& Register(Registry&); - - private: - std::map labels_; - std::string name_; - std::string help_; - double seconds_; -}; - -} // namespace detail -} // namespace prometheus diff --git a/core/include/prometheus/detail/time_window_quantiles.h b/core/include/prometheus/detail/time_window_quantiles.h index aeb42eeb..498baed2 100644 --- a/core/include/prometheus/detail/time_window_quantiles.h +++ b/core/include/prometheus/detail/time_window_quantiles.h @@ -4,29 +4,32 @@ #include #include -#include "prometheus/detail/ckms_quantiles.h" +#include "prometheus/detail/ckms_quantiles.h" // IWYU pragma: export +#include "prometheus/detail/core_export.h" + +// IWYU pragma: private, include "prometheus/summary.h" namespace prometheus { namespace detail { -class TimeWindowQuantiles { +class PROMETHEUS_CPP_CORE_EXPORT TimeWindowQuantiles { using Clock = std::chrono::steady_clock; public: TimeWindowQuantiles(const std::vector& quantiles, Clock::duration max_age_seconds, int age_buckets); - double get(double q); + double get(double q) const; void insert(double value); private: - CKMSQuantiles& rotate(); + CKMSQuantiles& rotate() const; const std::vector& quantiles_; - std::vector ckms_quantiles_; - std::size_t current_bucket_; + mutable std::vector ckms_quantiles_; + mutable std::size_t current_bucket_; - Clock::time_point last_rotation_; + mutable Clock::time_point last_rotation_; const Clock::duration rotation_interval_; }; diff --git a/core/include/prometheus/detail/utils.h b/core/include/prometheus/detail/utils.h index 503c1b20..5cea6bda 100644 --- a/core/include/prometheus/detail/utils.h +++ b/core/include/prometheus/detail/utils.h @@ -1,20 +1,22 @@ #pragma once #include -#include -#include -namespace prometheus { +#include "prometheus/detail/core_export.h" +#include "prometheus/labels.h" +namespace prometheus { namespace detail { -/// \brief Compute the hash value of a map of labels. -/// -/// \param labels The map that will be computed the hash value. -/// -/// \returns The hash value of the given labels. -std::size_t hash_labels(const std::map& labels); +/// \brief Label hasher for use in STL containers. +struct PROMETHEUS_CPP_CORE_EXPORT LabelHasher { + /// \brief Compute the hash value of a map of labels. + /// + /// \param labels The map that will be computed the hash value. + /// + /// \returns The hash value of the given labels. + std::size_t operator()(const Labels& labels) const; +}; } // namespace detail - } // namespace prometheus diff --git a/core/include/prometheus/family.h b/core/include/prometheus/family.h index 81f5a26c..968cead0 100644 --- a/core/include/prometheus/family.h +++ b/core/include/prometheus/family.h @@ -1,25 +1,25 @@ #pragma once -#include -#include #include -#include -#include #include #include -#include #include #include -#include #include -#include "prometheus/check_names.h" #include "prometheus/client_metric.h" #include "prometheus/collectable.h" +#include "prometheus/detail/core_export.h" #include "prometheus/detail/future_std.h" #include "prometheus/detail/utils.h" +#include "prometheus/labels.h" #include "prometheus/metric_family.h" +// IWYU pragma: no_include "prometheus/counter.h" +// IWYU pragma: no_include "prometheus/gauge.h" +// IWYU pragma: no_include "prometheus/histogram.h" +// IWYU pragma: no_include "prometheus/summary.h" + namespace prometheus { /// \brief A metric of type T with a set of labeled dimensions. @@ -59,7 +59,7 @@ namespace prometheus { /// /// \tparam T One of the metric types Counter, Gauge, Histogram or Summary. template -class Family : public Collectable { +class PROMETHEUS_CPP_CORE_EXPORT Family : public Collectable { public: /// \brief Create a new metric. /// @@ -88,9 +88,9 @@ class Family : public Collectable { /// \param constant_labels Assign a set of key-value pairs (= labels) to the /// metric. All these labels are propagated to each time series within the /// metric. + /// \throw std::runtime_exception on invalid metric or label names. Family(const std::string& name, const std::string& help, - const std::map& constant_labels, - double seconds); + const Labels& constant_labels, double seconds); /// \brief Add a new dimensional data. /// @@ -102,14 +102,17 @@ class Family : public Collectable { /// http_requests_total{job= "prometheus",method= "POST"} /// /// \param labels Assign a set of key-value pairs (= labels) to the - /// dimensional data. The function does nothing, if the same set of lables + /// dimensional data. The function does nothing, if the same set of labels /// already exists. /// \param args Arguments are passed to the constructor of metric type T. See /// Counter, Gauge, Histogram or Summary for required constructor arguments. /// \return Return the newly created dimensional data or - if a same set of - /// lables already exists - the already existing dimensional data. + /// labels already exists - the already existing dimensional data. + /// \throw std::runtime_exception on invalid label names. template - T& Add(const std::map& labels, Args&&... args); + T& Add(const Labels& labels, Args&&... args) { + return Add(labels, detail::make_unique(args...)); + } /// \brief Remove the given dimensional data. /// @@ -117,122 +120,40 @@ class Family : public Collectable { /// if the given metric was not returned by Add(). void Remove(T* metric); + /// \brief Returns true if the dimensional data with the given labels exist + /// + /// \param labels A set of key-value pairs (= labels) of the dimensional data. + bool Has(const Labels& labels) const; + + /// \brief Returns the name for this family. + /// + /// \return The family name. + const std::string& GetName() const; + + /// \brief Returns the constant labels for this family. + /// + /// \return All constant labels as key-value pairs. + const Labels GetConstantLabels() const; + /// \brief Returns the current value of each dimensional data. /// /// Collect is called by the Registry when collecting metrics. /// /// \return Zero or more samples for each dimensional data. - std::vector Collect() override; - std::vector Collect(std::time_t) override; + std::vector Collect() const override; + std::vector Collect(std::time_t) const override; private: - std::unordered_map> metrics_; - std::unordered_map> labels_; - std::unordered_map labels_reverse_lookup_; + std::unordered_map, detail::LabelHasher> metrics_; const std::string name_; const std::string help_; - const std::map constant_labels_; + const Labels constant_labels_; double seconds_; - std::mutex mutex_; + mutable std::mutex mutex_; - ClientMetric CollectMetric(std::size_t hash, T* metric); + ClientMetric CollectMetric(const Labels& labels, T* metric) const; + T& Add(const Labels& labels, std::unique_ptr object); }; -template -Family::Family(const std::string& name, const std::string& help, - const std::map& constant_labels, - double seconds) - : name_(name), help_(help), constant_labels_(constant_labels), seconds_(seconds) { - assert(CheckMetricName(name_)); -} - -template -template -T& Family::Add(const std::map& labels, - Args&&... args) { -#ifndef NDEBUG - for (auto& label_pair : labels) { - auto& label_name = label_pair.first; - assert(CheckLabelName(label_name)); - } -#endif - - auto hash = detail::hash_labels(labels); - std::lock_guard lock{mutex_}; - auto metrics_iter = metrics_.find(hash); - - if (metrics_iter != metrics_.end()) { -#ifndef NDEBUG - auto labels_iter = labels_.find(hash); - assert(labels_iter != labels_.end()); - const auto& old_labels = labels_iter->second; - assert(labels == old_labels); -#endif - return *metrics_iter->second; - } else { - auto metric = - metrics_.insert(std::make_pair(hash, detail::make_unique(args...))); - assert(metric.second); - labels_.insert({hash, labels}); - labels_reverse_lookup_.insert({metric.first->second.get(), hash}); - return *(metric.first->second); - } -} - -template -void Family::Remove(T* metric) { - std::lock_guard lock{mutex_}; - if (labels_reverse_lookup_.count(metric) == 0) { - return; - } - - auto hash = labels_reverse_lookup_.at(metric); - metrics_.erase(hash); - labels_.erase(hash); - labels_reverse_lookup_.erase(metric); -} - -template -std::vector Family::Collect() { - const auto time = std::time(nullptr); - return Collect(time); -} - -template -std::vector Family::Collect(std::time_t time) { - std::lock_guard lock{mutex_}; - auto family = MetricFamily{}; - family.name = name_; - family.help = help_; - family.type = T::metric_type; - for (const auto& m : metrics_) { - if (!m.second.get()->Expired(time, seconds_)) { - family.metric.push_back(std::move(CollectMetric(m.first, m.second.get()))); - } - } - return {family}; -} - - -template -ClientMetric Family::CollectMetric(std::size_t hash, T* metric) { - auto collected = metric->Collect(); - auto add_label = - [&collected](const std::pair& label_pair) { - auto label = ClientMetric::Label{}; - label.name = label_pair.first; - label.value = label_pair.second; - collected.label.push_back(std::move(label)); - }; - std::for_each(constant_labels_.cbegin(), constant_labels_.cend(), add_label); - const auto& metric_labels = labels_.at(hash); - for (auto const &label : metric_labels) { - if (constant_labels_.find(label.first) == constant_labels_.end()) { - add_label(label); - } - } - return collected; -} - } // namespace prometheus diff --git a/core/include/prometheus/gauge.h b/core/include/prometheus/gauge.h index f58948b4..13665682 100644 --- a/core/include/prometheus/gauge.h +++ b/core/include/prometheus/gauge.h @@ -4,7 +4,8 @@ #include #include "prometheus/client_metric.h" -#include "prometheus/detail/gauge_builder.h" +#include "prometheus/detail/builder.h" // IWYU pragma: export +#include "prometheus/detail/core_export.h" #include "prometheus/metric_type.h" namespace prometheus { @@ -21,7 +22,7 @@ namespace prometheus { /// /// The class is thread-safe. No concurrent call to any API of this type causes /// a data race. -class Gauge { +class PROMETHEUS_CPP_CORE_EXPORT Gauge { public: static const MetricType metric_type{MetricType::Gauge}; @@ -86,11 +87,11 @@ class Gauge { /// /// - Name(const std::string&) to set the metric name, /// - Help(const std::string&) to set an additional description. -/// - Label(const std::map&) to assign a set of +/// - Labels(const Labels&) to assign a set of /// key-value pairs (= labels) to the metric. /// /// To finish the configuration of the Gauge metric register it with /// Register(Registry&). -detail::GaugeBuilder BuildGauge(); +PROMETHEUS_CPP_CORE_EXPORT detail::Builder BuildGauge(); } // namespace prometheus diff --git a/core/include/prometheus/histogram.h b/core/include/prometheus/histogram.h index b5012241..fb1ba759 100644 --- a/core/include/prometheus/histogram.h +++ b/core/include/prometheus/histogram.h @@ -1,11 +1,14 @@ #pragma once #include +#include #include #include "prometheus/client_metric.h" #include "prometheus/counter.h" -#include "prometheus/detail/histogram_builder.h" +#include "prometheus/detail/builder.h" // IWYU pragma: export +#include "prometheus/detail/core_export.h" +#include "prometheus/gauge.h" #include "prometheus/metric_type.h" namespace prometheus { @@ -19,20 +22,20 @@ namespace prometheus { /// values, allowing to calculate the average of the observed values. /// /// At its core a histogram has a counter per bucket. The sum of observations -/// also behaves like a counter. +/// also behaves like a counter as long as there are no negative observations. /// /// See https://prometheus.io/docs/practices/histograms/ for detailed /// explanations of histogram usage and differences to summaries. /// /// The class is thread-safe. No concurrent call to any API of this type causes /// a data race. -class Histogram { +class PROMETHEUS_CPP_CORE_EXPORT Histogram { public: using BucketBoundaries = std::vector; static const MetricType metric_type{MetricType::Histogram}; - /// \brief Create a histogram with manually choosen buckets. + /// \brief Create a histogram with manually chosen buckets. /// /// The BucketBoundaries are a list of monotonically increasing values /// representing the bucket boundaries. Each consecutive pair of values is @@ -52,6 +55,14 @@ class Histogram { /// sum of all observations is incremented. void Observe(double value); + /// \brief Observe multiple data points. + /// + /// Increments counters given a count for each bucket. (i.e. the caller of + /// this function must have already sorted the values into buckets). + /// Also increments the total sum of all observations by the given value. + void ObserveMultiple(const std::vector& bucket_increments, + const double sum_of_values); + /// \brief Get the current value of the counter. /// /// Collect is called by the Registry when collecting metrics. @@ -60,8 +71,9 @@ class Histogram { private: const BucketBoundaries bucket_boundaries_; + mutable std::mutex mutex_; std::vector bucket_counts_; - Counter sum_; + Gauge sum_; }; /// \brief Return a builder to configure and register a Histogram metric. @@ -86,11 +98,11 @@ class Histogram { /// /// - Name(const std::string&) to set the metric name, /// - Help(const std::string&) to set an additional description. -/// - Label(const std::map&) to assign a set of +/// - Labels(const Labels&) to assign a set of /// key-value pairs (= labels) to the metric. /// /// To finish the configuration of the Histogram metric register it with /// Register(Registry&). -detail::HistogramBuilder BuildHistogram(); +PROMETHEUS_CPP_CORE_EXPORT detail::Builder BuildHistogram(); } // namespace prometheus diff --git a/core/include/prometheus/labels.h b/core/include/prometheus/labels.h new file mode 100644 index 00000000..919625b7 --- /dev/null +++ b/core/include/prometheus/labels.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +namespace prometheus { + +/// \brief Multiple labels and their value. +using Labels = std::map; + +} // namespace prometheus diff --git a/core/include/prometheus/metric_family.h b/core/include/prometheus/metric_family.h index da913a45..1ba3f5a3 100644 --- a/core/include/prometheus/metric_family.h +++ b/core/include/prometheus/metric_family.h @@ -4,11 +4,12 @@ #include #include "prometheus/client_metric.h" +#include "prometheus/detail/core_export.h" #include "prometheus/metric_type.h" namespace prometheus { -struct MetricFamily { +struct PROMETHEUS_CPP_CORE_EXPORT MetricFamily { std::string name; std::string help; MetricType type = MetricType::Untyped; diff --git a/core/include/prometheus/registry.h b/core/include/prometheus/registry.h index 67d2ba0a..9176aefa 100644 --- a/core/include/prometheus/registry.h +++ b/core/include/prometheus/registry.h @@ -1,26 +1,34 @@ #pragma once -#include +#include #include #include #include #include #include "prometheus/collectable.h" -#include "prometheus/counter.h" -#include "prometheus/detail/counter_builder.h" -#include "prometheus/detail/future_std.h" -#include "prometheus/detail/gauge_builder.h" -#include "prometheus/detail/histogram_builder.h" -#include "prometheus/detail/summary_builder.h" +#include "prometheus/detail/core_export.h" #include "prometheus/family.h" -#include "prometheus/gauge.h" -#include "prometheus/histogram.h" +#include "prometheus/labels.h" #include "prometheus/metric_family.h" -#include "prometheus/summary.h" +// IWYU pragma: no_include "prometheus/counter.h" +// IWYU pragma: no_include "prometheus/gauge.h" +// IWYU pragma: no_include "prometheus/histogram.h" +// IWYU pragma: no_include "prometheus/summary.h" namespace prometheus { +class Counter; // IWYU pragma: keep +class Gauge; // IWYU pragma: keep +class Histogram; // IWYU pragma: keep +class Summary; // IWYU pragma: keep + +namespace detail { + +template +class Builder; // IWYU pragma: keep + +} /// \brief Manages the collection of a number of metrics. /// /// The Registry is responsible to expose data to a class/method/function @@ -34,41 +42,83 @@ namespace prometheus { /// /// The class is thread-safe. No concurrent call to any API of this type causes /// a data race. -class Registry : public Collectable { +class PROMETHEUS_CPP_CORE_EXPORT Registry : public Collectable { public: + /// \brief How to deal with repeatedly added family names for a type. + /// + /// Adding a family with the same name but different types is always an error + /// and will lead to an exception. + enum class InsertBehavior { + /// \brief If a family with the same name and labels already exists return + /// the existing one. If no family with that name exists create it. + /// Otherwise throw. + Merge, + /// \brief Throws if a family with the same name already exists. + Throw, + }; + + /// \brief name Create a new registry. + /// + /// \param insert_behavior How to handle families with the same name. + explicit Registry(InsertBehavior insert_behavior = InsertBehavior::Merge); + + /// \brief Deleted copy constructor. + Registry(const Registry&) = delete; + + /// \brief Deleted copy assignment. + Registry& operator=(const Registry&) = delete; + + /// \brief Deleted move constructor. + Registry(Registry&&) = delete; + + /// \brief Deleted move assignment. + Registry& operator=(Registry&&) = delete; + + /// \brief name Destroys a registry. + ~Registry() override; + /// \brief Returns a list of metrics and their samples. /// /// Every time the Registry is scraped it calls each of the metrics Collect /// function. /// /// \return Zero or more metrics and their samples. - std::vector Collect() override; - std::vector Collect(std::time_t) override; + std::vector Collect() const override; + std::vector Collect(std::time_t) const override; + + /// \brief Removes a metrics family from the registry. + /// + /// Please note that this operation invalidates the previously + /// returned reference to the Family and all of their added + /// metric objects. + /// + /// \tparam T One of the metric types Counter, Gauge, Histogram or Summary. + /// \param family The family to remove + /// + /// \return True if the family was found and removed. + template + bool Remove(const Family& family); private: - friend class detail::CounterBuilder; - friend class detail::GaugeBuilder; - friend class detail::HistogramBuilder; - friend class detail::SummaryBuilder; + template + friend class detail::Builder; + + template + std::vector>>& GetFamilies(); + + template + bool NameExistsInOtherType(const std::string& name) const; template Family& Add(const std::string& name, const std::string& help, - const std::map& labels, - double seconds); + const Labels& labels, double seconds); - std::vector> collectables_; - std::mutex mutex_; + const InsertBehavior insert_behavior_; + std::vector>> counters_; + std::vector>> gauges_; + std::vector>> histograms_; + std::vector>> summaries_; + mutable std::mutex mutex_; }; -template -Family& Registry::Add(const std::string& name, const std::string& help, - const std::map& labels, - double seconds) { - std::lock_guard lock{mutex_}; - auto family = detail::make_unique>(name, help, labels, seconds); - auto& ref = *family; - collectables_.push_back(std::move(family)); - return ref; -} - } // namespace prometheus diff --git a/core/include/prometheus/serializer.h b/core/include/prometheus/serializer.h index 83bbd6e4..c55ae096 100644 --- a/core/include/prometheus/serializer.h +++ b/core/include/prometheus/serializer.h @@ -4,11 +4,12 @@ #include #include +#include "prometheus/detail/core_export.h" #include "prometheus/metric_family.h" namespace prometheus { -class Serializer { +class PROMETHEUS_CPP_CORE_EXPORT Serializer { public: virtual ~Serializer() = default; virtual std::string Serialize(const std::vector&) const; diff --git a/core/include/prometheus/summary.h b/core/include/prometheus/summary.h index 12794a60..0bebf4bd 100644 --- a/core/include/prometheus/summary.h +++ b/core/include/prometheus/summary.h @@ -7,8 +7,9 @@ #include #include "prometheus/client_metric.h" +#include "prometheus/detail/builder.h" // IWYU pragma: export #include "prometheus/detail/ckms_quantiles.h" -#include "prometheus/detail/summary_builder.h" +#include "prometheus/detail/core_export.h" #include "prometheus/detail/time_window_quantiles.h" #include "prometheus/metric_type.h" @@ -38,7 +39,7 @@ namespace prometheus { /// /// The class is thread-safe. No concurrent call to any API of this type causes /// a data race. -class Summary { +class PROMETHEUS_CPP_CORE_EXPORT Summary { public: using Quantiles = std::vector; @@ -81,12 +82,12 @@ class Summary { /// \brief Get the current value of the summary. /// /// Collect is called by the Registry when collecting metrics. - ClientMetric Collect(); + ClientMetric Collect() const; bool Expired(std::time_t, double) const; private: const Quantiles quantiles_; - std::mutex mutex_; + mutable std::mutex mutex_; std::uint64_t count_; double sum_; detail::TimeWindowQuantiles quantile_values_; @@ -114,11 +115,11 @@ class Summary { /// /// - Name(const std::string&) to set the metric name, /// - Help(const std::string&) to set an additional description. -/// - Label(const std::map&) to assign a set of +/// - Labels(const Labels&) to assign a set of /// key-value pairs (= labels) to the metric. /// /// To finish the configuration of the Summary metric register it with /// Register(Registry&). -detail::SummaryBuilder BuildSummary(); +PROMETHEUS_CPP_CORE_EXPORT detail::Builder BuildSummary(); } // namespace prometheus diff --git a/core/include/prometheus/text_serializer.h b/core/include/prometheus/text_serializer.h index a12f0ec1..315a164d 100644 --- a/core/include/prometheus/text_serializer.h +++ b/core/include/prometheus/text_serializer.h @@ -1,15 +1,15 @@ #pragma once #include -#include #include +#include "prometheus/detail/core_export.h" #include "prometheus/metric_family.h" #include "prometheus/serializer.h" namespace prometheus { -class TextSerializer : public Serializer { +class PROMETHEUS_CPP_CORE_EXPORT TextSerializer : public Serializer { public: using Serializer::Serialize; void Serialize(std::ostream& out, diff --git a/core/src/check_names.cc b/core/src/check_names.cc index 5a1443ae..8ce018af 100644 --- a/core/src/check_names.cc +++ b/core/src/check_names.cc @@ -1,37 +1,79 @@ #include "prometheus/check_names.h" -#include - -#if defined(__GLIBCXX__) && __GLIBCXX__ <= 20150623 -#define STD_REGEX_IS_BROKEN -#endif -#if defined(_MSC_VER) && _MSC_VER < 1900 -#define STD_REGEX_IS_BROKEN -#endif +#include +#include namespace prometheus { -bool CheckMetricName(const std::string& name) { - // see https://prometheus.io/docs/concepts/data_model/ + +namespace { +bool isLocaleIndependentAlphaNumeric(char c) { + return ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || + ('A' <= c && c <= 'Z'); +} + +bool isLocaleIndependentDigit(char c) { return '0' <= c && c <= '9'; } + +bool nameStartsValid(const std::string& name) { + // must not be empty + if (name.empty()) { + return false; + } + + // must not start with a digit + if (isLocaleIndependentDigit(name.front())) { + return false; + } + + // must not start with "__" auto reserved_for_internal_purposes = name.compare(0, 2, "__") == 0; if (reserved_for_internal_purposes) return false; -#ifdef STD_REGEX_IS_BROKEN - return !name.empty(); -#else - static const std::regex metric_name_regex("[a-zA-Z_:][a-zA-Z0-9_:]*"); - return std::regex_match(name, metric_name_regex); -#endif + + return true; } +} // anonymous namespace +/// \brief Check if the metric name is valid +/// +/// The metric name regex is "[a-zA-Z_:][a-zA-Z0-9_:]*" +/// +/// \see https://prometheus.io/docs/concepts/data_model/ +/// +/// \param name metric name +/// \return true is valid, false otherwise +bool CheckMetricName(const std::string& name) { + if (!nameStartsValid(name)) { + return false; + } + + auto validMetricCharacters = [](char c) { + return isLocaleIndependentAlphaNumeric(c) || c == '_' || c == ':'; + }; + + auto mismatch = + std::find_if_not(std::begin(name), std::end(name), validMetricCharacters); + return mismatch == std::end(name); +} + +/// \brief Check if the label name is valid +/// +/// The label name regex is "[a-zA-Z_][a-zA-Z0-9_]*" +/// +/// \see https://prometheus.io/docs/concepts/data_model/ +/// +/// \param name label name +/// \return true is valid, false otherwise bool CheckLabelName(const std::string& name) { - // see https://prometheus.io/docs/concepts/data_model/ - auto reserved_for_internal_purposes = name.compare(0, 2, "__") == 0; - if (reserved_for_internal_purposes) return false; -#ifdef STD_REGEX_IS_BROKEN - return !name.empty(); -#else - static const std::regex label_name_regex("[a-zA-Z_][a-zA-Z0-9_]*"); - return std::regex_match(name, label_name_regex); -#endif + if (!nameStartsValid(name)) { + return false; + } + + auto validLabelCharacters = [](char c) { + return isLocaleIndependentAlphaNumeric(c) || c == '_'; + }; + + auto mismatch = + std::find_if_not(std::begin(name), std::end(name), validLabelCharacters); + return mismatch == std::end(name); } } // namespace prometheus diff --git a/core/src/counter.cc b/core/src/counter.cc index e1d66f23..02d720c1 100644 --- a/core/src/counter.cc +++ b/core/src/counter.cc @@ -4,7 +4,12 @@ namespace prometheus { void Counter::Increment() { gauge_.Increment(); } -void Counter::Increment(const double val) { gauge_.Increment(val); } +void Counter::Increment(const double val) { + if (val < 0.0) { + return; + } + gauge_.Increment(val); +} double Counter::Value() const { return gauge_.Value(); } @@ -15,9 +20,9 @@ ClientMetric Counter::Collect() const { } bool Counter::Expired(std::time_t time, double seconds) const { + (void)time; + (void)seconds; return false; } -detail::CounterBuilder BuildCounter() { return {}; } - } // namespace prometheus diff --git a/core/src/detail/builder.cc b/core/src/detail/builder.cc new file mode 100644 index 00000000..924a8737 --- /dev/null +++ b/core/src/detail/builder.cc @@ -0,0 +1,56 @@ +#include "prometheus/detail/builder.h" + +#include "prometheus/counter.h" +#include "prometheus/detail/core_export.h" +#include "prometheus/family.h" +#include "prometheus/gauge.h" +#include "prometheus/histogram.h" +#include "prometheus/registry.h" +#include "prometheus/summary.h" + +namespace prometheus { + +namespace detail { + +template +Builder& Builder::Labels(const ::prometheus::Labels& labels) { + labels_ = labels; + return *this; +} + +template +Builder& Builder::Name(const std::string& name) { + name_ = name; + return *this; +} + +template +Builder& Builder::Help(const std::string& help) { + help_ = help; + return *this; +} + +template +Builder& Builder::Seconds(double seconds) { + seconds_ = seconds; + return *this; +} + +template +Family& Builder::Register(Registry& registry) { + return registry.Add(name_, help_, labels_, seconds_); +} + +template class PROMETHEUS_CPP_CORE_EXPORT Builder; +template class PROMETHEUS_CPP_CORE_EXPORT Builder; +template class PROMETHEUS_CPP_CORE_EXPORT Builder; +template class PROMETHEUS_CPP_CORE_EXPORT Builder; + +} // namespace detail + +detail::Builder BuildCounter() { return {}; } +detail::Builder BuildGauge() { return {}; } +detail::Builder BuildHistogram() { return {}; } +detail::Builder BuildSummary() { return {}; } + +} // namespace prometheus diff --git a/core/src/detail/ckms_quantiles.cc b/core/src/detail/ckms_quantiles.cc index 63205dbc..7ab6f1f2 100644 --- a/core/src/detail/ckms_quantiles.cc +++ b/core/src/detail/ckms_quantiles.cc @@ -1,8 +1,9 @@ -#include "prometheus/detail/ckms_quantiles.h" +#include "prometheus/detail/ckms_quantiles.h" // IWYU pragma: export #include #include #include +#include namespace prometheus { namespace detail { diff --git a/core/src/detail/counter_builder.cc b/core/src/detail/counter_builder.cc deleted file mode 100644 index 732a94ae..00000000 --- a/core/src/detail/counter_builder.cc +++ /dev/null @@ -1,34 +0,0 @@ -#include "prometheus/detail/counter_builder.h" - -#include "prometheus/registry.h" - -namespace prometheus { -namespace detail { - -CounterBuilder& CounterBuilder::Labels( - const std::map& labels) { - labels_ = labels; - return *this; -} - -CounterBuilder& CounterBuilder::Name(const std::string& name) { - name_ = name; - return *this; -} - -CounterBuilder& CounterBuilder::Help(const std::string& help) { - help_ = help; - return *this; -} - -CounterBuilder& CounterBuilder::Seconds(double seconds) { - seconds_ = seconds; - return *this; -} - -Family& CounterBuilder::Register(Registry& registry) { - return registry.Add(name_, help_, labels_, seconds_); -} - -} // namespace detail -} // namespace prometheus diff --git a/core/src/detail/gauge_builder.cc b/core/src/detail/gauge_builder.cc deleted file mode 100644 index 45f61b5d..00000000 --- a/core/src/detail/gauge_builder.cc +++ /dev/null @@ -1,34 +0,0 @@ -#include "prometheus/detail/gauge_builder.h" - -#include "prometheus/registry.h" - -namespace prometheus { -namespace detail { - -GaugeBuilder& GaugeBuilder::Labels( - const std::map& labels) { - labels_ = labels; - return *this; -} - -GaugeBuilder& GaugeBuilder::Name(const std::string& name) { - name_ = name; - return *this; -} - -GaugeBuilder& GaugeBuilder::Help(const std::string& help) { - help_ = help; - return *this; -} - -GaugeBuilder& GaugeBuilder::Seconds(double seconds) { - seconds_ = seconds; - return *this; -} - -Family& GaugeBuilder::Register(Registry& registry) { - return registry.Add(name_, help_, labels_, seconds_); -} - -} // namespace detail -} // namespace prometheus diff --git a/core/src/detail/hash.h b/core/src/detail/hash.h index bfa54483..da415dbd 100644 --- a/core/src/detail/hash.h +++ b/core/src/detail/hash.h @@ -29,7 +29,7 @@ inline void hash_combine(std::size_t *seed, const T &value) { /// \param args The objects that will be combined with the given hash value. template inline void hash_combine(std::size_t *seed, const T &value, - const Types &... args) { + const Types &...args) { hash_combine(seed, value); hash_combine(seed, args...); } @@ -39,7 +39,7 @@ inline void hash_combine(std::size_t *seed, const T &value, /// \param args The arguments that will be computed hash value. /// \return The hash value of the given args. template -inline std::size_t hash_value(const Types &... args) { +inline std::size_t hash_value(const Types &...args) { std::size_t seed = 0; hash_combine(&seed, args...); return seed; diff --git a/core/src/detail/histogram_builder.cc b/core/src/detail/histogram_builder.cc deleted file mode 100644 index fa80fcc5..00000000 --- a/core/src/detail/histogram_builder.cc +++ /dev/null @@ -1,34 +0,0 @@ -#include "prometheus/detail/histogram_builder.h" - -#include "prometheus/registry.h" - -namespace prometheus { -namespace detail { - -HistogramBuilder& HistogramBuilder::Labels( - const std::map& labels) { - labels_ = labels; - return *this; -} - -HistogramBuilder& HistogramBuilder::Name(const std::string& name) { - name_ = name; - return *this; -} - -HistogramBuilder& HistogramBuilder::Help(const std::string& help) { - help_ = help; - return *this; -} - -HistogramBuilder& HistogramBuilder::Seconds(double seconds) { - seconds_ = seconds; - return *this; -} - -Family& HistogramBuilder::Register(Registry& registry) { - return registry.Add(name_, help_, labels_, seconds_); -} - -} // namespace detail -} // namespace prometheus diff --git a/core/src/detail/summary_builder.cc b/core/src/detail/summary_builder.cc deleted file mode 100644 index 43cb000d..00000000 --- a/core/src/detail/summary_builder.cc +++ /dev/null @@ -1,34 +0,0 @@ -#include "prometheus/detail/summary_builder.h" - -#include "prometheus/registry.h" - -namespace prometheus { -namespace detail { - -SummaryBuilder& SummaryBuilder::Labels( - const std::map& labels) { - labels_ = labels; - return *this; -} - -SummaryBuilder& SummaryBuilder::Name(const std::string& name) { - name_ = name; - return *this; -} - -SummaryBuilder& SummaryBuilder::Help(const std::string& help) { - help_ = help; - return *this; -} - -SummaryBuilder& SummaryBuilder::Seconds(double seconds) { - seconds_ = seconds; - return *this; -} - -Family& SummaryBuilder::Register(Registry& registry) { - return registry.Add(name_, help_, labels_, seconds_); -} - -} // namespace detail -} // namespace prometheus diff --git a/core/src/detail/time_window_quantiles.cc b/core/src/detail/time_window_quantiles.cc index eec6f2d3..55e48564 100644 --- a/core/src/detail/time_window_quantiles.cc +++ b/core/src/detail/time_window_quantiles.cc @@ -1,4 +1,6 @@ -#include "prometheus/detail/time_window_quantiles.h" +#include "prometheus/detail/time_window_quantiles.h" // IWYU pragma: export + +#include namespace prometheus { namespace detail { @@ -12,7 +14,7 @@ TimeWindowQuantiles::TimeWindowQuantiles( last_rotation_(Clock::now()), rotation_interval_(max_age / age_buckets) {} -double TimeWindowQuantiles::get(double q) { +double TimeWindowQuantiles::get(double q) const { CKMSQuantiles& current_bucket = rotate(); return current_bucket.get(q); } @@ -24,7 +26,7 @@ void TimeWindowQuantiles::insert(double value) { } } -CKMSQuantiles& TimeWindowQuantiles::rotate() { +CKMSQuantiles& TimeWindowQuantiles::rotate() const { auto delta = Clock::now() - last_rotation_; while (delta > rotation_interval_) { ckms_quantiles_[current_bucket_].reset(); diff --git a/core/src/detail/utils.cc b/core/src/detail/utils.cc index 80f557ef..f605f7c7 100644 --- a/core/src/detail/utils.cc +++ b/core/src/detail/utils.cc @@ -1,13 +1,12 @@ #include "prometheus/detail/utils.h" -#include "hash.h" -#include +#include "hash.h" namespace prometheus { namespace detail { -std::size_t hash_labels(const std::map& labels) { +std::size_t LabelHasher::operator()(const Labels& labels) const { size_t seed = 0; for (auto& label : labels) { hash_combine(&seed, label.first, label.second); diff --git a/core/src/family.cc b/core/src/family.cc new file mode 100644 index 00000000..e4a4145d --- /dev/null +++ b/core/src/family.cc @@ -0,0 +1,140 @@ +#include "prometheus/family.h" + +#include +#include +#include +#include +#include + +#include "prometheus/check_names.h" +#include "prometheus/counter.h" +#include "prometheus/gauge.h" +#include "prometheus/histogram.h" +#include "prometheus/summary.h" + +namespace prometheus { + +template +Family::Family(const std::string& name, const std::string& help, + const Labels& constant_labels, double seconds) + : name_(name), + help_(help), + constant_labels_(constant_labels), + seconds_(seconds) { + if (!CheckMetricName(name_)) { + throw std::invalid_argument("Invalid metric name"); + } + for (auto& label_pair : constant_labels_) { + auto& label_name = label_pair.first; + if (!CheckLabelName(label_name)) { + throw std::invalid_argument("Invalid label name"); + } + } +} + +template +T& Family::Add(const Labels& labels, std::unique_ptr object) { + std::lock_guard lock{mutex_}; + + auto insert_result = + metrics_.insert(std::make_pair(labels, std::move(object))); + + if (insert_result.second) { + // insertion took place, retroactively check for unlikely issues + for (auto& label_pair : labels) { + const auto& label_name = label_pair.first; + if (!CheckLabelName(label_name)) { + metrics_.erase(insert_result.first); + throw std::invalid_argument("Invalid label name"); + } + if (constant_labels_.count(label_name)) { + metrics_.erase(insert_result.first); + throw std::invalid_argument("Duplicate label name"); + } + } + } + + auto& stored_object = insert_result.first->second; + assert(stored_object); + return *stored_object; +} + +template +void Family::Remove(T* metric) { + std::lock_guard lock{mutex_}; + + for (auto it = metrics_.begin(); it != metrics_.end(); ++it) { + if (it->second.get() == metric) { + metrics_.erase(it); + break; + } + } +} + +template +bool Family::Has(const Labels& labels) const { + std::lock_guard lock{mutex_}; + return metrics_.count(labels) != 0u; +} + +template +const std::string& Family::GetName() const { + return name_; +} + +template +const Labels Family::GetConstantLabels() const { + return constant_labels_; +} + +template +std::vector Family::Collect() const { + const auto time = std::time(nullptr); + return Collect(time); +} + +template +std::vector Family::Collect(std::time_t time) const { + std::lock_guard lock{mutex_}; + + if (metrics_.empty()) { + return {}; + } + + auto family = MetricFamily{}; + family.name = name_; + family.help = help_; + family.type = T::metric_type; + family.metric.reserve(metrics_.size()); + for (const auto& m : metrics_) { + if (!m.second.get()->Expired(time, seconds_)) { + family.metric.push_back( + std::move(CollectMetric(m.first, m.second.get()))); + } + } + return {family}; +} + +template +ClientMetric Family::CollectMetric(const Labels& metric_labels, + T* metric) const { + auto collected = metric->Collect(); + collected.label.reserve(constant_labels_.size() + metric_labels.size()); + const auto add_label = + [&collected](const std::pair& label_pair) { + auto label = ClientMetric::Label{}; + label.name = label_pair.first; + label.value = label_pair.second; + collected.label.push_back(std::move(label)); + }; + std::for_each(constant_labels_.cbegin(), constant_labels_.cend(), add_label); + std::for_each(metric_labels.cbegin(), metric_labels.cend(), add_label); + return collected; +} + +template class PROMETHEUS_CPP_CORE_EXPORT Family; +template class PROMETHEUS_CPP_CORE_EXPORT Family; +template class PROMETHEUS_CPP_CORE_EXPORT Family; +template class PROMETHEUS_CPP_CORE_EXPORT Family; + +} // namespace prometheus diff --git a/core/src/gauge.cc b/core/src/gauge.cc index 559c4df7..d687e506 100644 --- a/core/src/gauge.cc +++ b/core/src/gauge.cc @@ -6,21 +6,11 @@ Gauge::Gauge(const double value) : value_{value} {} void Gauge::Increment() { Increment(1.0); } -void Gauge::Increment(const double value) { - if (value < 0.0) { - return; - } - Change(value); -} +void Gauge::Increment(const double value) { Change(value); } void Gauge::Decrement() { Decrement(1.0); } -void Gauge::Decrement(const double value) { - if (value < 0.0) { - return; - } - Change(-1.0 * value); -} +void Gauge::Decrement(const double value) { Change(-1.0 * value); } void Gauge::Set(const double value) { value_.store(value); @@ -28,9 +18,11 @@ void Gauge::Set(const double value) { } void Gauge::Change(const double value) { + // C++ 20 will add std::atomic::fetch_add support for floating point types auto current = value_.load(); - while (!value_.compare_exchange_weak(current, current + value)) - ; + while (!value_.compare_exchange_weak(current, current + value)) { + // intentionally empty block + } time_.store(std::time(nullptr)); } @@ -51,6 +43,4 @@ bool Gauge::Expired(std::time_t time, double seconds) const { return std::difftime(time, time_) > seconds; } -detail::GaugeBuilder BuildGauge() { return {}; } - } // namespace prometheus diff --git a/core/src/histogram.cc b/core/src/histogram.cc index 5b687b54..89e3d738 100644 --- a/core/src/histogram.cc +++ b/core/src/histogram.cc @@ -2,8 +2,12 @@ #include #include +#include #include -#include +#include +#include +#include +#include namespace prometheus { @@ -20,14 +24,35 @@ void Histogram::Observe(const double value) { std::find_if( std::begin(bucket_boundaries_), std::end(bucket_boundaries_), [value](const double boundary) { return boundary >= value; }))); + + std::lock_guard lock(mutex_); sum_.Increment(value); bucket_counts_[bucket_index].Increment(); } +void Histogram::ObserveMultiple(const std::vector& bucket_increments, + const double sum_of_values) { + if (bucket_increments.size() != bucket_counts_.size()) { + throw std::length_error( + "The size of bucket_increments was not equal to" + "the number of buckets in the histogram."); + } + + std::lock_guard lock(mutex_); + sum_.Increment(sum_of_values); + + for (std::size_t i{0}; i < bucket_counts_.size(); ++i) { + bucket_counts_[i].Increment(bucket_increments[i]); + } +} + ClientMetric Histogram::Collect() const { + std::lock_guard lock(mutex_); + auto metric = ClientMetric{}; auto cumulative_count = 0ULL; + metric.histogram.bucket.reserve(bucket_counts_.size()); for (std::size_t i{0}; i < bucket_counts_.size(); ++i) { cumulative_count += bucket_counts_[i].Value(); auto bucket = ClientMetric::Bucket{}; @@ -44,9 +69,9 @@ ClientMetric Histogram::Collect() const { } bool Histogram::Expired(std::time_t time, double seconds) const { + (void)time; + (void)seconds; return false; } -detail::HistogramBuilder BuildHistogram() { return {}; } - } // namespace prometheus diff --git a/core/src/registry.cc b/core/src/registry.cc index 9d01eb47..30f1e4e9 100644 --- a/core/src/registry.cc +++ b/core/src/registry.cc @@ -1,21 +1,190 @@ #include "prometheus/registry.h" +#include +#include +#include +#include + +#include "prometheus/counter.h" +#include "prometheus/detail/future_std.h" +#include "prometheus/gauge.h" +#include "prometheus/histogram.h" +#include "prometheus/summary.h" + namespace prometheus { -std::vector Registry::Collect() { +namespace { +template +void CollectAll(std::vector& results, const T& families, + std::time_t time) { + for (auto&& collectable : families) { + auto metrics = collectable->Collect(time); + results.insert(results.end(), std::make_move_iterator(metrics.begin()), + std::make_move_iterator(metrics.end())); + } +} + +bool FamilyNameExists(const std::string& /* name */) { return false; } + +template +bool FamilyNameExists(const std::string& name, const T& families, + Args&&... args) { + auto sameName = [&name](const typename T::value_type& entry) { + return name == entry->GetName(); + }; + auto exists = std::find_if(std::begin(families), std::end(families), + sameName) != std::end(families); + return exists || FamilyNameExists(name, args...); +} +} // namespace + +Registry::Registry(InsertBehavior insert_behavior) + : insert_behavior_{insert_behavior} {} + +Registry::~Registry() = default; + +std::vector Registry::Collect() const { const auto time = std::time(nullptr); return Collect(time); } -std::vector Registry::Collect(std::time_t time) { +std::vector Registry::Collect(std::time_t time) const { std::lock_guard lock{mutex_}; auto results = std::vector{}; - for (auto&& collectable : collectables_) { - auto metrics = collectable->Collect(time); - results.insert(results.end(), metrics.begin(), metrics.end()); - } + + CollectAll(results, counters_, time); + CollectAll(results, gauges_, time); + CollectAll(results, histograms_, time); + CollectAll(results, summaries_, time); return results; } +template <> +std::vector>>& Registry::GetFamilies() { + return counters_; +} + +template <> +std::vector>>& Registry::GetFamilies() { + return gauges_; +} + +template <> +std::vector>>& Registry::GetFamilies() { + return histograms_; +} + +template <> +std::vector>>& Registry::GetFamilies() { + return summaries_; +} + +template <> +bool Registry::NameExistsInOtherType(const std::string& name) const { + return FamilyNameExists(name, gauges_, histograms_, summaries_); +} + +template <> +bool Registry::NameExistsInOtherType(const std::string& name) const { + return FamilyNameExists(name, counters_, histograms_, summaries_); +} + +template <> +bool Registry::NameExistsInOtherType(const std::string& name) const { + return FamilyNameExists(name, counters_, gauges_, summaries_); +} + +template <> +bool Registry::NameExistsInOtherType(const std::string& name) const { + return FamilyNameExists(name, counters_, gauges_, histograms_); +} + +template +Family& Registry::Add(const std::string& name, const std::string& help, + const Labels& labels, double seconds) { + std::lock_guard lock{mutex_}; + + if (NameExistsInOtherType(name)) { + throw std::invalid_argument( + "Family name already exists with different type"); + } + + auto& families = GetFamilies(); + + if (insert_behavior_ == InsertBehavior::Merge) { + auto same_name_and_labels = + [&name, &labels](const std::unique_ptr>& family) { + return std::tie(name, labels) == + std::tie(family->GetName(), family->GetConstantLabels()); + }; + + auto it = + std::find_if(families.begin(), families.end(), same_name_and_labels); + if (it != families.end()) { + return **it; + } + } + + auto same_name = [&name](const std::unique_ptr>& family) { + return name == family->GetName(); + }; + + auto it = std::find_if(families.begin(), families.end(), same_name); + if (it != families.end()) { + throw std::invalid_argument("Family name already exists"); + } + + auto family = detail::make_unique>(name, help, labels, seconds); + auto& ref = *family; + families.push_back(std::move(family)); + return ref; +} + +template Family& Registry::Add(const std::string& name, + const std::string& help, + const Labels& labels, double seconds); + +template Family& Registry::Add(const std::string& name, + const std::string& help, + const Labels& labels, double seconds); + +template Family& Registry::Add(const std::string& name, + const std::string& help, + const Labels& labels, double seconds); + +template Family& Registry::Add(const std::string& name, + const std::string& help, + const Labels& labels, double seconds); + +template +bool Registry::Remove(const Family& family) { + std::lock_guard lock{mutex_}; + + auto& families = GetFamilies(); + auto same_family = [&family](const std::unique_ptr>& in) { + return &family == in.get(); + }; + + auto it = std::find_if(families.begin(), families.end(), same_family); + if (it == families.end()) { + return false; + } + + families.erase(it); + return true; +} + +template bool PROMETHEUS_CPP_CORE_EXPORT +Registry::Remove(const Family& family); + +template bool PROMETHEUS_CPP_CORE_EXPORT +Registry::Remove(const Family& family); + +template bool PROMETHEUS_CPP_CORE_EXPORT +Registry::Remove(const Family& family); + +template bool PROMETHEUS_CPP_CORE_EXPORT +Registry::Remove(const Family& family); + } // namespace prometheus diff --git a/core/src/serializer.cc b/core/src/serializer.cc index 4f86c455..69e6a580 100644 --- a/core/src/serializer.cc +++ b/core/src/serializer.cc @@ -1,6 +1,6 @@ #include "prometheus/serializer.h" -#include +#include // IWYU pragma: keep namespace prometheus { diff --git a/core/src/summary.cc b/core/src/summary.cc index e7bf4c18..2582d90e 100644 --- a/core/src/summary.cc +++ b/core/src/summary.cc @@ -1,5 +1,7 @@ #include "prometheus/summary.h" +#include + namespace prometheus { Summary::Summary(const Quantiles& quantiles, @@ -17,11 +19,12 @@ void Summary::Observe(const double value) { quantile_values_.insert(value); } -ClientMetric Summary::Collect() { +ClientMetric Summary::Collect() const { auto metric = ClientMetric{}; std::lock_guard lock(mutex_); + metric.summary.quantile.reserve(quantiles_.size()); for (const auto& quantile : quantiles_) { auto metricQuantile = ClientMetric::Quantile{}; metricQuantile.quantile = quantile.quantile; @@ -35,9 +38,9 @@ ClientMetric Summary::Collect() { } bool Summary::Expired(std::time_t time, double seconds) const { + (void)time; + (void)seconds; return false; } -detail::SummaryBuilder BuildSummary() { return {}; } - } // namespace prometheus diff --git a/core/src/text_serializer.cc b/core/src/text_serializer.cc index 432379b8..d26d3e58 100644 --- a/core/src/text_serializer.cc +++ b/core/src/text_serializer.cc @@ -2,66 +2,72 @@ #include #include +#include #include +#include + +#include "prometheus/client_metric.h" +#include "prometheus/metric_family.h" +#include "prometheus/metric_type.h" namespace prometheus { namespace { // Write a double as a string, with proper formatting for infinity and NaN -std::string ToString(double v) { - if (std::isnan(v)) { - return "Nan"; - } - if (std::isinf(v)) { - return (v < 0 ? "-Inf" : "+Inf"); +void WriteValue(std::ostream& out, double value) { + if (std::isnan(value)) { + out << "Nan"; + } else if (std::isinf(value)) { + out << (value < 0 ? "-Inf" : "+Inf"); + } else { + out << value; } - return std::to_string(v); } -const std::string& EscapeLabelValue(const std::string& value, - std::string* tmp) { - bool copy = false; - for (size_t i = 0; i < value.size(); ++i) { - auto c = value[i]; - if (c == '\\' || c == '"' || c == '\n') { - if (!copy) { - tmp->reserve(value.size() + 1); - tmp->assign(value, 0, i); - copy = true; - } - if (c == '\\') { - tmp->append("\\\\"); - } else if (c == '"') { - tmp->append("\\\""); - } else { - tmp->append("\\\n"); - } - } else if (copy) { - tmp->push_back(c); +void WriteValue(std::ostream& out, const std::string& value) { + for (auto c : value) { + switch (c) { + case '\n': + out << '\\' << 'n'; + break; + + case '\\': + out << '\\' << c; + break; + + case '"': + out << '\\' << c; + break; + + default: + out << c; + break; } } - return copy ? *tmp : value; } // Write a line header: metric name and labels +template void WriteHead(std::ostream& out, const MetricFamily& family, const ClientMetric& metric, const std::string& suffix = "", const std::string& extraLabelName = "", - const std::string& extraLabelValue = "") { + const T& extraLabelValue = T()) { out << family.name << suffix; if (!metric.label.empty() || !extraLabelName.empty()) { out << "{"; const char* prefix = ""; - std::string tmp; + for (auto& lp : metric.label) { - out << prefix << lp.name << "=\"" << EscapeLabelValue(lp.value, &tmp) - << "\""; + out << prefix << lp.name << "=\""; + WriteValue(out, lp.value); + out << "\""; prefix = ","; } if (!extraLabelName.empty()) { - out << prefix << extraLabelName << "=\"" - << EscapeLabelValue(extraLabelValue, &tmp) << "\""; + out << prefix << extraLabelName << "=\""; + WriteValue(out, extraLabelValue); + out << "\""; } out << "}"; } @@ -79,14 +85,14 @@ void WriteTail(std::ostream& out, const ClientMetric& metric) { void SerializeCounter(std::ostream& out, const MetricFamily& family, const ClientMetric& metric) { WriteHead(out, family, metric); - out << ToString(metric.counter.value); + WriteValue(out, metric.counter.value); WriteTail(out, metric); } void SerializeGauge(std::ostream& out, const MetricFamily& family, const ClientMetric& metric) { WriteHead(out, family, metric); - out << ToString(metric.gauge.value); + WriteValue(out, metric.gauge.value); WriteTail(out, metric); } @@ -98,12 +104,12 @@ void SerializeSummary(std::ostream& out, const MetricFamily& family, WriteTail(out, metric); WriteHead(out, family, metric, "_sum"); - out << ToString(sum.sample_sum); + WriteValue(out, sum.sample_sum); WriteTail(out, metric); for (auto& q : sum.quantile) { - WriteHead(out, family, metric, "", "quantile", ToString(q.quantile)); - out << ToString(q.value); + WriteHead(out, family, metric, "", "quantile", q.quantile); + WriteValue(out, q.value); WriteTail(out, metric); } } @@ -111,7 +117,7 @@ void SerializeSummary(std::ostream& out, const MetricFamily& family, void SerializeUntyped(std::ostream& out, const MetricFamily& family, const ClientMetric& metric) { WriteHead(out, family, metric); - out << ToString(metric.untyped.value); + WriteValue(out, metric.untyped.value); WriteTail(out, metric); } @@ -123,12 +129,12 @@ void SerializeHistogram(std::ostream& out, const MetricFamily& family, WriteTail(out, metric); WriteHead(out, family, metric, "_sum"); - out << ToString(hist.sample_sum); + WriteValue(out, hist.sample_sum); WriteTail(out, metric); double last = -std::numeric_limits::infinity(); for (auto& b : hist.bucket) { - WriteHead(out, family, metric, "_bucket", "le", ToString(b.upper_bound)); + WriteHead(out, family, metric, "_bucket", "le", b.upper_bound); last = b.upper_bound; out << b.cumulative_count; WriteTail(out, metric); @@ -176,16 +182,23 @@ void SerializeFamily(std::ostream& out, const MetricFamily& family) { SerializeHistogram(out, family, metric); } break; - default: - break; } } } // namespace void TextSerializer::Serialize(std::ostream& out, const std::vector& metrics) const { + auto saved_locale = out.getloc(); + auto saved_precision = out.precision(); + + out.imbue(std::locale::classic()); + out.precision(std::numeric_limits::max_digits10 - 1); + for (auto& family : metrics) { SerializeFamily(out, family); } + + out.imbue(saved_locale); + out.precision(saved_precision); } } // namespace prometheus diff --git a/core/tests/BUILD.bazel b/core/tests/BUILD.bazel index f82aa5ae..c00fb2bf 100644 --- a/core/tests/BUILD.bazel +++ b/core/tests/BUILD.bazel @@ -5,7 +5,7 @@ cc_test( "*.h", ]), copts = ["-Iexternal/googletest/include"], - linkstatic = 1, + linkstatic = True, deps = [ "//core", "@com_google_googletest//:gtest_main", diff --git a/core/tests/CMakeLists.txt b/core/tests/CMakeLists.txt index 881695ba..d641a852 100644 --- a/core/tests/CMakeLists.txt +++ b/core/tests/CMakeLists.txt @@ -1,22 +1,25 @@ -add_executable(prometheus_test +add_executable(prometheus_core_test + builder_test.cc check_names_test.cc counter_test.cc family_test.cc gauge_test.cc histogram_test.cc registry_test.cc + serializer_test.cc summary_test.cc + text_serializer_test.cc utils_test.cc ) -target_link_libraries(prometheus_test +target_link_libraries(prometheus_core_test PRIVATE ${PROJECT_NAME}::core GTest::gmock_main ) add_test( - NAME prometheus_test - COMMAND prometheus_test + NAME prometheus_core_test + COMMAND prometheus_core_test ) diff --git a/core/tests/builder_test.cc b/core/tests/builder_test.cc new file mode 100644 index 00000000..e3764f04 --- /dev/null +++ b/core/tests/builder_test.cc @@ -0,0 +1,107 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "prometheus/client_metric.h" +#include "prometheus/counter.h" +#include "prometheus/family.h" +#include "prometheus/gauge.h" +#include "prometheus/histogram.h" +#include "prometheus/labels.h" +#include "prometheus/registry.h" +#include "prometheus/summary.h" + +namespace prometheus { +namespace { + +class BuilderTest : public testing::Test { + protected: + std::vector getExpectedLabels() { + std::vector labels; + + auto gen = [](std::pair p) { + return ClientMetric::Label{p.first, p.second}; + }; + + std::transform(std::begin(const_labels), std::end(const_labels), + std::back_inserter(labels), gen); + std::transform(std::begin(more_labels), std::end(more_labels), + std::back_inserter(labels), gen); + + return labels; + } + + void verifyCollectedLabels() { + const auto collected = registry.Collect(); + + ASSERT_EQ(1U, collected.size()); + EXPECT_EQ(name, collected.at(0).name); + EXPECT_EQ(help, collected.at(0).help); + ASSERT_EQ(1U, collected.at(0).metric.size()); + + EXPECT_THAT(collected.at(0).metric.at(0).label, + testing::UnorderedElementsAreArray(expected_labels)); + } + + Registry registry; + + const std::string name = "some_name"; + const std::string help = "Additional description."; + const Labels const_labels = {{"key", "value"}}; + const Labels more_labels = {{"name", "test"}}; + const std::vector expected_labels = getExpectedLabels(); +}; + +TEST_F(BuilderTest, build_counter) { + auto& family = BuildCounter() + .Name(name) + .Help(help) + .Labels(const_labels) + .Register(registry); + family.Add(more_labels); + + verifyCollectedLabels(); +} + +TEST_F(BuilderTest, build_gauge) { + auto& family = BuildGauge() + .Name(name) + .Help(help) + .Labels(const_labels) + .Register(registry); + family.Add(more_labels); + + verifyCollectedLabels(); +} + +TEST_F(BuilderTest, build_histogram) { + auto& family = BuildHistogram() + .Name(name) + .Help(help) + .Labels(const_labels) + .Register(registry); + family.Add(more_labels, Histogram::BucketBoundaries{1, 2}); + + verifyCollectedLabels(); +} + +TEST_F(BuilderTest, build_summary) { + auto& family = BuildSummary() + .Name(name) + .Help(help) + .Labels(const_labels) + .Register(registry); + family.Add(more_labels, Summary::Quantiles{}); + + verifyCollectedLabels(); +} + +} // namespace +} // namespace prometheus diff --git a/core/tests/check_names_test.cc b/core/tests/check_names_test.cc index 8d694507..d4f1848b 100644 --- a/core/tests/check_names_test.cc +++ b/core/tests/check_names_test.cc @@ -1,6 +1,6 @@ #include "prometheus/check_names.h" -#include +#include namespace prometheus { namespace { @@ -12,8 +12,19 @@ TEST(CheckNamesTest, good_metric_name) { TEST(CheckNamesTest, reserved_metric_name) { EXPECT_FALSE(CheckMetricName("__some_reserved_metric")); } - +TEST(CheckNamesTest, malformed_metric_name) { + EXPECT_FALSE(CheckMetricName("fa mi ly with space in name or |")); +} TEST(CheckNamesTest, empty_label_name) { EXPECT_FALSE(CheckLabelName("")); } +TEST(CheckNamesTest, invalid_label_name) { + EXPECT_FALSE(CheckLabelName("log-level")); +} +TEST(CheckNamesTest, leading_invalid_label_name) { + EXPECT_FALSE(CheckLabelName("-abcd")); +} +TEST(CheckNamesTest, trailing_invalid_label_name) { + EXPECT_FALSE(CheckLabelName("abcd-")); +} TEST(CheckNamesTest, good_label_name) { EXPECT_TRUE(CheckLabelName("type")); } TEST(CheckNamesTest, reserved_label_name) { EXPECT_FALSE(CheckMetricName("__some_reserved_label")); diff --git a/core/tests/counter_test.cc b/core/tests/counter_test.cc index 1c5566cb..9087e9bd 100644 --- a/core/tests/counter_test.cc +++ b/core/tests/counter_test.cc @@ -1,6 +1,6 @@ #include "prometheus/counter.h" -#include +#include namespace prometheus { namespace { diff --git a/core/tests/family_test.cc b/core/tests/family_test.cc index a80522bc..cf18d3ac 100644 --- a/core/tests/family_test.cc +++ b/core/tests/family_test.cc @@ -1,13 +1,16 @@ #include "prometheus/family.h" -#include -#include - #include +#include + +#include +#include #include "prometheus/client_metric.h" +#include "prometheus/counter.h" #include "prometheus/detail/future_std.h" #include "prometheus/histogram.h" +#include "prometheus/labels.h" namespace prometheus { namespace { @@ -28,8 +31,18 @@ TEST(FamilyTest, labels) { ::testing::ElementsAre(const_label, dynamic_label)); } +TEST(FamilyTest, reject_same_label_keys) { + auto labels = Labels{{"component", "test"}}; + + Family family{"total_requests", "Counts all requests", labels, + std::numeric_limits::max()}; + EXPECT_ANY_THROW(family.Add(labels)); +} + TEST(FamilyTest, counter_value) { - Family family{"total_requests", "Counts all requests", {}, + Family family{"total_requests", + "Counts all requests", + {}, std::numeric_limits::max()}; auto& counter = family.Add({}); counter.Increment(); @@ -40,7 +53,9 @@ TEST(FamilyTest, counter_value) { } TEST(FamilyTest, remove) { - Family family{"total_requests", "Counts all requests", {}, + Family family{"total_requests", + "Counts all requests", + {}, std::numeric_limits::max()}; auto& counter1 = family.Add({{"name", "counter1"}}); family.Add({{"name", "counter2"}}); @@ -50,8 +65,18 @@ TEST(FamilyTest, remove) { EXPECT_EQ(collected[0].metric.size(), 1U); } +TEST(FamilyTest, removeUnknownMetricMustNotCrash) { + Family family{"total_requests", + "Counts all requests", + {}, + std::numeric_limits::max()}; + family.Remove(nullptr); +} + TEST(FamilyTest, Histogram) { - Family family{"request_latency", "Latency Histogram", {}, + Family family{"request_latency", + "Latency Histogram", + {}, std::numeric_limits::max()}; auto& histogram1 = family.Add({{"name", "histogram1"}}, Histogram::BucketBoundaries{0, 1, 2}); @@ -63,31 +88,60 @@ TEST(FamilyTest, Histogram) { } TEST(FamilyTest, add_twice) { - Family family{"total_requests", "Counts all requests", {}, + Family family{"total_requests", + "Counts all requests", + {}, std::numeric_limits::max()}; auto& counter = family.Add({{"name", "counter1"}}); auto& counter1 = family.Add({{"name", "counter1"}}); ASSERT_EQ(&counter, &counter1); } -TEST(FamilyTest, should_assert_on_invalid_metric_name) { +TEST(FamilyTest, throw_on_invalid_metric_name) { auto create_family_with_invalid_name = []() { return detail::make_unique>( - "", "empty name", std::map{}, - std::numeric_limits::max()); + "", "empty name", Labels{}, std::numeric_limits::max()); }; - EXPECT_DEBUG_DEATH(create_family_with_invalid_name(), - ".*Assertion .*CheckMetricName.*"); + EXPECT_ANY_THROW(create_family_with_invalid_name()); } -TEST(FamilyTest, should_assert_on_invalid_labels) { - Family family{"total_requests", "Counts all requests", {}, +TEST(FamilyTest, throw_on_invalid_constant_label_name) { + auto create_family_with_invalid_labels = []() { + return detail::make_unique>( + "total_requests", "Counts all requests", + Labels{{"__inavlid", "counter1"}}, std::numeric_limits::max()); + }; + EXPECT_ANY_THROW(create_family_with_invalid_labels()); +} + +TEST(FamilyTest, should_throw_on_invalid_labels) { + Family family{"total_requests", + "Counts all requests", + {}, std::numeric_limits::max()}; auto add_metric_with_invalid_label_name = [&family]() { family.Add({{"__invalid", "counter1"}}); }; - EXPECT_DEBUG_DEATH(add_metric_with_invalid_label_name(), - ".*Assertion .*CheckLabelName.*"); + EXPECT_ANY_THROW(add_metric_with_invalid_label_name()); +} + +TEST(FamilyTest, should_not_collect_empty_metrics) { + Family family{"total_requests", + "Counts all requests", + {}, + std::numeric_limits::max()}; + auto collected = family.Collect(); + EXPECT_TRUE(collected.empty()); +} + +TEST(FamilyTest, query_family_if_metric_already_exists) { + Family family{"total_rquests", + "Counts all requests", + {}, + std::numeric_limits::max()}; + family.Add({{"name", "counter1"}}); + EXPECT_TRUE(family.Has({{"name", "counter1"}})); + EXPECT_FALSE(family.Has({{"name", "couner2"}})); } } // namespace diff --git a/core/tests/gauge_test.cc b/core/tests/gauge_test.cc index fdcd34bf..690754b0 100644 --- a/core/tests/gauge_test.cc +++ b/core/tests/gauge_test.cc @@ -1,6 +1,6 @@ #include "prometheus/gauge.h" -#include +#include namespace prometheus { namespace { @@ -30,6 +30,12 @@ TEST(GaugeTest, inc_multiple) { EXPECT_EQ(gauge.Value(), 7.0); } +TEST(GaugeTest, inc_negative_value) { + Gauge gauge; + gauge.Increment(-1.0); + EXPECT_EQ(gauge.Value(), -1.0); +} + TEST(GaugeTest, dec) { Gauge gauge; gauge.Set(5.0); @@ -37,6 +43,12 @@ TEST(GaugeTest, dec) { EXPECT_EQ(gauge.Value(), 4.0); } +TEST(GaugeTest, dec_negative_value) { + Gauge gauge; + gauge.Decrement(-1.0); + EXPECT_EQ(gauge.Value(), 1.0); +} + TEST(GaugeTest, dec_number) { Gauge gauge; gauge.Set(5.0); diff --git a/core/tests/histogram_test.cc b/core/tests/histogram_test.cc index 60aa75fb..86cc66a8 100644 --- a/core/tests/histogram_test.cc +++ b/core/tests/histogram_test.cc @@ -1,8 +1,10 @@ #include "prometheus/histogram.h" -#include +#include -#include +#include +#include +#include namespace prometheus { namespace { @@ -79,5 +81,42 @@ TEST(HistogramTest, cumulative_bucket_count) { EXPECT_EQ(h.bucket.at(2).cumulative_count, 7U); } +TEST(HistogramTest, observe_multiple_test_bucket_counts) { + Histogram histogram{{1, 2}}; + histogram.ObserveMultiple({5, 9, 3}, 20); + histogram.ObserveMultiple({0, 20, 6}, 34); + auto metric = histogram.Collect(); + auto h = metric.histogram; + ASSERT_EQ(h.bucket.size(), 3U); + EXPECT_EQ(h.bucket.at(0).cumulative_count, 5U); + EXPECT_EQ(h.bucket.at(1).cumulative_count, 34U); + EXPECT_EQ(h.bucket.at(2).cumulative_count, 43U); +} + +TEST(HistogramTest, observe_multiple_test_total_sum) { + Histogram histogram{{1, 2}}; + histogram.ObserveMultiple({5, 9, 3}, 20); + histogram.ObserveMultiple({0, 20, 6}, 34); + auto metric = histogram.Collect(); + auto h = metric.histogram; + EXPECT_EQ(h.sample_count, 43U); + EXPECT_EQ(h.sample_sum, 54); +} + +TEST(HistogramTest, observe_multiple_test_length_error) { + Histogram histogram{{1, 2}}; + // 2 bucket boundaries means there are 3 buckets, so giving just 2 bucket + // increments should result in a length_error. + ASSERT_THROW(histogram.ObserveMultiple({5, 9}, 20), std::length_error); +} + +TEST(HistogramTest, sum_can_go_down) { + Histogram histogram{{1}}; + auto metric1 = histogram.Collect(); + histogram.Observe(-10); + auto metric2 = histogram.Collect(); + EXPECT_LT(metric2.histogram.sample_sum, metric1.histogram.sample_sum); +} + } // namespace } // namespace prometheus diff --git a/core/tests/raii_locale.h b/core/tests/raii_locale.h new file mode 100644 index 00000000..481cd846 --- /dev/null +++ b/core/tests/raii_locale.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +class RAIILocale { + public: + RAIILocale(const char* name) : savedLocale_(std::locale::classic()) { + std::locale::global(std::locale(name)); + } + + ~RAIILocale() { std::locale::global(savedLocale_); } + + RAIILocale(const RAIILocale&) = delete; + RAIILocale(RAIILocale&&) = delete; + RAIILocale& operator=(const RAIILocale&) = delete; + RAIILocale& operator=(RAIILocale&&) = delete; + + private: + const std::locale savedLocale_; +}; diff --git a/core/tests/registry_test.cc b/core/tests/registry_test.cc index 6c6ecc68..9e9b8f9c 100644 --- a/core/tests/registry_test.cc +++ b/core/tests/registry_test.cc @@ -1,10 +1,15 @@ #include "prometheus/registry.h" -#include +#include -#include +#include +#include +#include -#include "prometheus/collectable.h" +#include "prometheus/counter.h" +#include "prometheus/gauge.h" +#include "prometheus/histogram.h" +#include "prometheus/summary.h" namespace prometheus { namespace { @@ -37,5 +42,101 @@ TEST(RegistryTest, build_histogram_family) { ASSERT_EQ(collected.size(), 1U); } +TEST(RegistryTest, unable_to_remove_family) { + Family family{ + "name", "help", {}, std::numeric_limits::max()}; + Registry registry{}; + EXPECT_FALSE(registry.Remove(family)); +} + +TEST(RegistryTest, remove_and_readd_family) { + Registry registry{Registry::InsertBehavior::Throw}; + + auto& counter = BuildCounter().Name("name").Register(registry); + EXPECT_TRUE(registry.Remove(counter)); + EXPECT_NO_THROW(BuildCounter().Name("name").Register(registry)); +} + +TEST(RegistryTest, reject_different_type_than_counter) { + const auto same_name = std::string{"same_name"}; + Registry registry{}; + + EXPECT_NO_THROW(BuildCounter().Name(same_name).Register(registry)); + EXPECT_ANY_THROW(BuildGauge().Name(same_name).Register(registry)); + EXPECT_ANY_THROW(BuildHistogram().Name(same_name).Register(registry)); + EXPECT_ANY_THROW(BuildSummary().Name(same_name).Register(registry)); +} + +TEST(RegistryTest, reject_different_type_than_gauge) { + const auto same_name = std::string{"same_name"}; + Registry registry{}; + + EXPECT_NO_THROW(BuildGauge().Name(same_name).Register(registry)); + EXPECT_ANY_THROW(BuildCounter().Name(same_name).Register(registry)); + EXPECT_ANY_THROW(BuildHistogram().Name(same_name).Register(registry)); + EXPECT_ANY_THROW(BuildSummary().Name(same_name).Register(registry)); +} + +TEST(RegistryTest, reject_different_type_than_histogram) { + const auto same_name = std::string{"same_name"}; + Registry registry{}; + + EXPECT_NO_THROW(BuildHistogram().Name(same_name).Register(registry)); + EXPECT_ANY_THROW(BuildCounter().Name(same_name).Register(registry)); + EXPECT_ANY_THROW(BuildGauge().Name(same_name).Register(registry)); + EXPECT_ANY_THROW(BuildSummary().Name(same_name).Register(registry)); +} + +TEST(RegistryTest, reject_different_type_than_summary) { + const auto same_name = std::string{"same_name"}; + Registry registry{}; + + EXPECT_NO_THROW(BuildSummary().Name(same_name).Register(registry)); + EXPECT_ANY_THROW(BuildCounter().Name(same_name).Register(registry)); + EXPECT_ANY_THROW(BuildGauge().Name(same_name).Register(registry)); + EXPECT_ANY_THROW(BuildHistogram().Name(same_name).Register(registry)); +} + +TEST(RegistryTest, throw_for_same_family_name) { + const auto same_name = std::string{"same_name"}; + Registry registry{Registry::InsertBehavior::Throw}; + + EXPECT_NO_THROW(BuildCounter().Name(same_name).Register(registry)); + EXPECT_ANY_THROW(BuildCounter().Name(same_name).Register(registry)); +} + +TEST(RegistryTest, merge_same_families) { + Registry registry{Registry::InsertBehavior::Merge}; + + std::size_t loops = 4; + + while (loops-- > 0) { + BuildCounter() + .Name("counter") + .Help("Test Counter") + .Register(registry) + .Add({{"name", "test_counter"}}); + } + + auto collected = registry.Collect(); + EXPECT_EQ(1U, collected.size()); +} + +TEST(RegistryTest, do_not_merge_families_with_different_labels) { + Registry registry{Registry::InsertBehavior::Merge}; + + EXPECT_NO_THROW(BuildCounter() + .Name("counter") + .Help("Test Counter") + .Labels({{"a", "A"}}) + .Register(registry)); + + EXPECT_ANY_THROW(BuildCounter() + .Name("counter") + .Help("Test Counter") + .Labels({{"b", "B"}}) + .Register(registry)); +} + } // namespace } // namespace prometheus diff --git a/core/tests/serializer_test.cc b/core/tests/serializer_test.cc new file mode 100644 index 00000000..d9a10761 --- /dev/null +++ b/core/tests/serializer_test.cc @@ -0,0 +1,78 @@ +#include +#include + +#include +#include +#include +#include +#include + +#include "prometheus/counter.h" +#include "prometheus/detail/future_std.h" +#include "prometheus/family.h" +#include "prometheus/metric_family.h" +#include "prometheus/text_serializer.h" +#include "raii_locale.h" + +namespace prometheus { +namespace { + +class SerializerTest : public testing::Test { + public: + void SetUp() override { + Family family{ + "requests_total", "", {}, std::numeric_limits::max()}; + auto& counter = family.Add({}); + counter.Increment(); + + collected = family.Collect(); + } + + std::vector collected; + TextSerializer textSerializer; +}; + +#ifndef _WIN32 +// This test expects a working German locale to test that floating +// point numbers do not use , but . as a delimiter. +// +// On Debian systems they can be generated by "locale-gen de_DE.UTF-8" +TEST_F(SerializerTest, shouldSerializeLocaleIndependent) { + std::unique_ptr localeWithCommaDecimalSeparator; + + // ignore missing locale and skip test if setup fails + try { + localeWithCommaDecimalSeparator = + detail::make_unique("de_DE.UTF-8"); + } catch (std::runtime_error&) { + GTEST_SKIP(); + } + + const auto serialized = textSerializer.Serialize(collected); + EXPECT_THAT(serialized, testing::HasSubstr(" 1\n")); +} +#endif + +TEST_F(SerializerTest, shouldRestoreStreamState) { + std::ostringstream os; + + // save stream state + auto saved_flags = os.flags(); + auto saved_precision = os.precision(); + auto saved_width = os.width(); + auto saved_fill = os.fill(); + auto saved_locale = os.getloc(); + + // serialize + textSerializer.Serialize(os, collected); + + // check for expected flags + EXPECT_EQ(os.flags(), saved_flags); + EXPECT_EQ(os.precision(), saved_precision); + EXPECT_EQ(os.width(), saved_width); + EXPECT_EQ(os.fill(), saved_fill); + EXPECT_EQ(os.getloc(), saved_locale); +} + +} // namespace +} // namespace prometheus diff --git a/core/tests/summary_test.cc b/core/tests/summary_test.cc index 243a4dee..32b1e804 100644 --- a/core/tests/summary_test.cc +++ b/core/tests/summary_test.cc @@ -1,10 +1,13 @@ #include "prometheus/summary.h" +#include + +#include #include +#include +#include #include -#include - namespace prometheus { namespace { @@ -72,7 +75,7 @@ TEST(SummaryTest, max_age) { 2}; summary.Observe(8.0); - static const auto test_value = [&summary](double ref) { + const auto test_value = [&summary](double ref) { auto metric = summary.Collect(); auto s = metric.summary; ASSERT_EQ(s.quantile.size(), 1U); @@ -90,5 +93,13 @@ TEST(SummaryTest, max_age) { test_value(std::numeric_limits::quiet_NaN()); } +TEST(SummaryTest, construction_with_dynamic_quantile_vector) { + auto quantiles = Summary::Quantiles{{0.99, 0.001}}; + quantiles.push_back({0.5, 0.05}); + + Summary summary{quantiles, std::chrono::seconds(1), 2}; + summary.Observe(8.0); +} + } // namespace } // namespace prometheus diff --git a/core/tests/text_serializer_test.cc b/core/tests/text_serializer_test.cc new file mode 100644 index 00000000..0ef68a0d --- /dev/null +++ b/core/tests/text_serializer_test.cc @@ -0,0 +1,123 @@ +#include "prometheus/text_serializer.h" + +#include +#include + +#include +#include +#include + +#include "prometheus/client_metric.h" +#include "prometheus/histogram.h" +#include "prometheus/metric_family.h" +#include "prometheus/metric_type.h" +#include "prometheus/summary.h" + +namespace prometheus { +namespace { + +class TextSerializerTest : public testing::Test { + public: + std::string Serialize(MetricType type) const { + MetricFamily metricFamily; + metricFamily.name = name; + metricFamily.help = "my metric help text"; + metricFamily.type = type; + metricFamily.metric = std::vector{metric}; + + std::vector families{metricFamily}; + + return textSerializer.Serialize(families); + } + + const std::string name = "my_metric"; + ClientMetric metric; + TextSerializer textSerializer; +}; + +TEST_F(TextSerializerTest, shouldSerializeNotANumber) { + metric.gauge.value = std::nan(""); + EXPECT_THAT(Serialize(MetricType::Gauge), testing::HasSubstr(name + " Nan")); +} + +TEST_F(TextSerializerTest, shouldSerializeNegativeInfinity) { + metric.gauge.value = -std::numeric_limits::infinity(); + EXPECT_THAT(Serialize(MetricType::Gauge), testing::HasSubstr(name + " -Inf")); +} + +TEST_F(TextSerializerTest, shouldSerializePositiveInfinity) { + metric.gauge.value = std::numeric_limits::infinity(); + EXPECT_THAT(Serialize(MetricType::Gauge), testing::HasSubstr(name + " +Inf")); +} + +TEST_F(TextSerializerTest, shouldEscapeBackslash) { + metric.label.resize(1, ClientMetric::Label{"k", "v\\v"}); + EXPECT_THAT(Serialize(MetricType::Gauge), + testing::HasSubstr(name + "{k=\"v\\\\v\"}")); +} + +TEST_F(TextSerializerTest, shouldEscapeNewline) { + metric.label.resize(1, ClientMetric::Label{"k", "v\nv"}); + EXPECT_THAT(Serialize(MetricType::Gauge), + testing::HasSubstr(name + "{k=\"v\\nv\"}")); +} + +TEST_F(TextSerializerTest, shouldEscapeDoubleQuote) { + metric.label.resize(1, ClientMetric::Label{"k", "v\"v"}); + EXPECT_THAT(Serialize(MetricType::Gauge), + testing::HasSubstr(name + "{k=\"v\\\"v\"}")); +} + +TEST_F(TextSerializerTest, shouldSerializeUntyped) { + metric.untyped.value = 64.0; + + const auto serialized = Serialize(MetricType::Untyped); + EXPECT_THAT(serialized, testing::HasSubstr(name + " 64\n")); +} + +TEST_F(TextSerializerTest, shouldSerializeTimestamp) { + metric.counter.value = 64.0; + metric.timestamp_ms = 1234; + + const auto serialized = Serialize(MetricType::Counter); + EXPECT_THAT(serialized, testing::HasSubstr(name + " 64 1234\n")); +} + +TEST_F(TextSerializerTest, shouldSerializeHistogramWithNoBuckets) { + metric.histogram.sample_count = 2; + metric.histogram.sample_sum = 32.0; + + const auto serialized = Serialize(MetricType::Histogram); + EXPECT_THAT(serialized, testing::HasSubstr(name + "_count 2")); + EXPECT_THAT(serialized, testing::HasSubstr(name + "_sum 32\n")); + EXPECT_THAT(serialized, testing::HasSubstr(name + "_bucket{le=\"+Inf\"} 2")); +} + +TEST_F(TextSerializerTest, shouldSerializeHistogram) { + Histogram histogram{{1}}; + histogram.Observe(0); + histogram.Observe(200); + metric = histogram.Collect(); + + const auto serialized = Serialize(MetricType::Histogram); + EXPECT_THAT(serialized, testing::HasSubstr(name + "_count 2\n")); + EXPECT_THAT(serialized, testing::HasSubstr(name + "_sum 200\n")); + EXPECT_THAT(serialized, testing::HasSubstr(name + "_bucket{le=\"1\"} 1\n")); + EXPECT_THAT(serialized, + testing::HasSubstr(name + "_bucket{le=\"+Inf\"} 2\n")); +} + +TEST_F(TextSerializerTest, shouldSerializeSummary) { + Summary summary{Summary::Quantiles{{0.5, 0.05}}}; + summary.Observe(0); + summary.Observe(200); + metric = summary.Collect(); + + const auto serialized = Serialize(MetricType::Summary); + EXPECT_THAT(serialized, testing::HasSubstr(name + "_count 2")); + EXPECT_THAT(serialized, testing::HasSubstr(name + "_sum 200\n")); + EXPECT_THAT(serialized, testing::HasSubstr(name + "{quantile=\"0.5\"} 0\n")); +} + +} // namespace +} // namespace prometheus diff --git a/core/tests/utils_test.cc b/core/tests/utils_test.cc index a8933293..b3c1f4f7 100644 --- a/core/tests/utils_test.cc +++ b/core/tests/utils_test.cc @@ -1,32 +1,31 @@ #include "prometheus/detail/utils.h" -#include -#include +#include namespace prometheus { namespace { -TEST(UtilsTest, hash_labels_1) { - std::map labels; - labels.insert(std::make_pair("key1", "value1")); - labels.insert(std::make_pair("key2", "vaule2")); - auto value1 = detail::hash_labels(labels); - auto value2 = detail::hash_labels(labels); +class UtilsTest : public testing::Test { + public: + detail::LabelHasher hasher; +}; - EXPECT_EQ(value1, value2); +TEST_F(UtilsTest, hash_labels_1) { + Labels labels{{"key1", "value1"}, {"key2", "vaule2"}}; + EXPECT_EQ(hasher(labels), hasher(labels)); } -TEST(UtilsTest, hash_labels_2) { - std::map labels1{{"aa", "bb"}}; - std::map labels2{{"a", "abb"}}; - EXPECT_NE(detail::hash_labels(labels1), detail::hash_labels(labels2)); +TEST_F(UtilsTest, hash_labels_2) { + Labels labels1{{"aa", "bb"}}; + Labels labels2{{"a", "abb"}}; + EXPECT_NE(hasher(labels1), hasher(labels2)); } -TEST(UtilsTest, hash_label_3) { - std::map labels1{{"a", "a"}}; - std::map labels2{{"aa", ""}}; - EXPECT_NE(detail::hash_labels(labels1), detail::hash_labels(labels2)); +TEST_F(UtilsTest, hash_label_3) { + Labels labels1{{"a", "a"}}; + Labels labels2{{"aa", ""}}; + EXPECT_NE(hasher(labels1), hasher(labels2)); } } // namespace diff --git a/doc/Doxyfile b/doc/Doxyfile index d616c2a6..14a657e4 100644 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -5,8 +5,9 @@ EXCLUDE_SYMBOLS = prometheus::detail::* FILE_PATTERNS = *.h *.cc GENERATE_LATEX = NO GRAPHICAL_HIERARCHY = NO -INPUT = ../core/include ../core/src ../pull/include ../pull/src ../push/include ../push/src +INPUT = ../README.md ../core/include ../core/src ../pull/include ../pull/src ../push/include ../push/src RECURSIVE = YES SHOW_FILES = NO SHOW_INCLUDE_FILES = NO SHOW_USED_FILES = NO +USE_MDFILE_AS_MAINPAGE = ../README.md diff --git a/pull/BUILD.bazel b/pull/BUILD.bazel index a5ae5df7..1e6e9250 100644 --- a/pull/BUILD.bazel +++ b/pull/BUILD.bazel @@ -1,3 +1,11 @@ +load("//bazel:export_header.bzl", "generate_dummy_export_header") + +generate_dummy_export_header( + name = "export_header", + basename = "PROMETHEUS_CPP_PULL", + header = "include/prometheus/detail/pull_export.h", +) + cc_library( name = "pull", srcs = glob([ @@ -6,9 +14,9 @@ cc_library( ]), hdrs = glob( ["include/**/*.h"], - ), - copts = [ - "-DHAVE_ZLIB", + ) + [":export_header"], + local_defines = [ + "HAVE_ZLIB", ], strip_include_prefix = "include", visibility = ["//visibility:public"], @@ -18,3 +26,15 @@ cc_library( "@net_zlib_zlib//:z", ], ) + +cc_library( + name = "pull_internal_headers", + hdrs = glob( + ["src/detail/*.h"], + ), + strip_include_prefix = "src", + visibility = ["//pull/tests:__subpackages__"], + deps = [ + "//core", + ], +) diff --git a/pull/CMakeLists.txt b/pull/CMakeLists.txt index edca4033..250128d7 100644 --- a/pull/CMakeLists.txt +++ b/pull/CMakeLists.txt @@ -1,8 +1,20 @@ - if(USE_THIRDPARTY_LIBRARIES) find_package(civetweb-3rdparty CONFIG REQUIRED) + add_library(${PROJECT_NAME}::civetweb ALIAS civetweb) + install( + TARGETS civetweb + EXPORT ${PROJECT_NAME}-targets + # keep embedded civetweb headers scoped to prometheus(-cpp) + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/prometheus + ) else() - find_package(civetweb REQUIRED) + find_package(civetweb CONFIG REQUIRED) + + # work-around https://github.com/civetweb/civetweb/pull/918 + if(WIN32 AND NOT TARGET WINSOCK::WINSOCK) + add_library(WINSOCK::WINSOCK INTERFACE IMPORTED) + target_link_libraries(WINSOCK::WINSOCK INTERFACE ws2_32) + endif() endif() if(ENABLE_COMPRESSION) @@ -10,10 +22,17 @@ if(ENABLE_COMPRESSION) endif() add_library(pull + src/basic_auth.cc + src/basic_auth.h + src/endpoint.cc + src/endpoint.h src/exposer.cc src/handler.cc src/handler.h - $<$:$> + src/metrics_collector.cc + src/metrics_collector.h + + src/detail/base64.h ) add_library(${PROJECT_NAME}::pull ALIAS pull) @@ -23,7 +42,7 @@ target_link_libraries(pull ${PROJECT_NAME}::core PRIVATE Threads::Threads - ${CIVETWEB_LIBRARIES} + $,${PROJECT_NAME}::civetweb,civetweb::civetweb-cpp> $<$,$>>:rt> $<$:ZLIB::ZLIB> ) @@ -31,6 +50,7 @@ target_link_libraries(pull target_include_directories(pull PUBLIC $ + $ PRIVATE ${CIVETWEB_INCLUDE_DIRS} ) @@ -40,7 +60,18 @@ target_compile_definitions(pull $<$:HAVE_ZLIB> ) -set_target_properties(pull PROPERTIES OUTPUT_NAME ${PROJECT_NAME}-pull) +set_target_properties(pull + PROPERTIES + OUTPUT_NAME ${PROJECT_NAME}-pull + DEFINE_SYMBOL PROMETHEUS_CPP_PULL_EXPORTS + VERSION "${PROJECT_VERSION}" + SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}" +) + +generate_export_header(pull + BASE_NAME ${PROJECT_NAME}-pull + EXPORT_FILE_NAME include/prometheus/detail/pull_export.h +) install( TARGETS pull @@ -52,10 +83,38 @@ install( ) install( - DIRECTORY include/ + DIRECTORY include/ ${CMAKE_CURRENT_BINARY_DIR}/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) +if(GENERATE_PKGCONFIG) + set(PKGCONFIG_LIBS) + set(PKGCONFIG_REQUIRES) + + if(NOT USE_THIRDPARTY_LIBRARIES) + string(APPEND PKGCONFIG_LIBS " -lcivetweb-cpp -lcivetweb") + endif() + + if(ENABLE_COMPRESSION) + string(APPEND PKGCONFIG_REQUIRES " zlib") + endif() + + configure_file( + ${PROJECT_SOURCE_DIR}/cmake/prometheus-cpp-pull.pc.in + ${CMAKE_CURRENT_BINARY_DIR}/prometheus-cpp-pull.pc + @ONLY + ) + + install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/prometheus-cpp-pull.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig + ) +endif() + if(ENABLE_TESTING) + add_library(pull_internal_headers INTERFACE) + add_library(${PROJECT_NAME}::pull_internal_headers ALIAS pull_internal_headers) + target_include_directories(pull_internal_headers INTERFACE src) + add_subdirectory(tests) endif() diff --git a/pull/include/prometheus/exposer.h b/pull/include/prometheus/exposer.h index 611004ba..601cffb8 100644 --- a/pull/include/prometheus/exposer.h +++ b/pull/include/prometheus/exposer.h @@ -1,36 +1,58 @@ #pragma once -#include -#include +#include +#include #include +#include #include #include #include "prometheus/collectable.h" -#include "prometheus/registry.h" +#include "prometheus/detail/pull_export.h" +// IWYU pragma: no_include "CivetServer.h" -class CivetServer; +class CivetServer; // IWYU pragma: keep +struct CivetCallbacks; // IWYU pragma: keep namespace prometheus { namespace detail { -class MetricsHandler; +class Endpoint; // IWYU pragma: keep } // namespace detail -class Exposer { +class PROMETHEUS_CPP_PULL_EXPORT Exposer { public: explicit Exposer(const std::string& bind_address, - const std::string& uri = std::string("/metrics"), - const std::size_t num_threads = 2); + const std::size_t num_threads = 2, + const CivetCallbacks* callbacks = nullptr); + explicit Exposer(std::vector options, + const CivetCallbacks* callbacks = nullptr); ~Exposer(); - void RegisterCollectable(const std::weak_ptr& collectable); + + Exposer(const Exposer&) = delete; + Exposer(Exposer&&) = delete; + Exposer& operator=(const Exposer&) = delete; + Exposer& operator=(Exposer&&) = delete; + + void RegisterCollectable(const std::weak_ptr& collectable, + const std::string& uri = std::string("/metrics")); + + void RegisterAuth( + std::function authCB, + const std::string& realm = "Prometheus-cpp Exporter", + const std::string& uri = std::string("/metrics")); + + void RemoveCollectable(const std::weak_ptr& collectable, + const std::string& uri = std::string("/metrics")); + + std::vector GetListeningPorts() const; private: + detail::Endpoint& GetEndpointForUri(const std::string& uri); + std::unique_ptr server_; - std::vector> collectables_; - std::shared_ptr exposer_registry_; - std::unique_ptr metrics_handler_; - std::string uri_; + std::vector> endpoints_; + std::mutex mutex_; }; } // namespace prometheus diff --git a/pull/src/basic_auth.cc b/pull/src/basic_auth.cc new file mode 100644 index 00000000..a669c586 --- /dev/null +++ b/pull/src/basic_auth.cc @@ -0,0 +1,74 @@ +#include "basic_auth.h" + +#include + +#include "CivetServer.h" +#include "detail/base64.h" + +namespace prometheus { + +BasicAuthHandler::BasicAuthHandler(AuthFunc callback, std::string realm) + : callback_(std::move(callback)), realm_(std::move(realm)) {} + +bool BasicAuthHandler::authorize(CivetServer* server, mg_connection* conn) { + if (!AuthorizeInner(server, conn)) { + WriteUnauthorizedResponse(conn); + return false; + } + return true; +} + +bool BasicAuthHandler::AuthorizeInner(CivetServer*, mg_connection* conn) { + const char* authHeader = mg_get_header(conn, "Authorization"); + + if (authHeader == nullptr) { + // No auth header was provided. + return false; + } + std::string authHeaderStr = authHeader; + + // Basic auth header is expected to be of the form: + // "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + + const std::string prefix = "Basic "; + if (authHeaderStr.compare(0, prefix.size(), prefix) != 0) { + return false; + } + + // Strip the "Basic " prefix leaving the base64 encoded auth string + auto b64Auth = authHeaderStr.substr(prefix.size()); + + std::string decoded; + try { + decoded = detail::base64_decode(b64Auth); + } catch (...) { + return false; + } + + // decoded auth string is expected to be of the form: + // "username:password" + // colons may not appear in the username. + auto splitPos = decoded.find(':'); + if (splitPos == std::string::npos) { + return false; + } + + auto username = decoded.substr(0, splitPos); + auto password = decoded.substr(splitPos + 1); + + // TODO: bool does not permit a distinction between 401 Unauthorized + // and 403 Forbidden. Authentication may succeed, but the user still + // not be authorized to perform the request. + return callback_(username, password); +} + +void BasicAuthHandler::WriteUnauthorizedResponse(mg_connection* conn) { + mg_printf(conn, "HTTP/1.1 401 Unauthorized\r\n"); + mg_printf(conn, "WWW-Authenticate: Basic realm=\"%s\"\r\n", realm_.c_str()); + mg_printf(conn, "Connection: close\r\n"); + mg_printf(conn, "Content-Length: 0\r\n"); + // end headers + mg_printf(conn, "\r\n"); +} + +} // namespace prometheus diff --git a/pull/src/basic_auth.h b/pull/src/basic_auth.h new file mode 100644 index 00000000..3047b951 --- /dev/null +++ b/pull/src/basic_auth.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +#include "CivetServer.h" +#include "civetweb.h" + +namespace prometheus { + +/** + * Handler for HTTP Basic authentication for Endpoints. + */ +class BasicAuthHandler : public CivetAuthHandler { + public: + using AuthFunc = std::function; + explicit BasicAuthHandler(AuthFunc callback, std::string realm); + + /** + * Implements civetweb authorization interface. + * + * Attempts to extract a username and password from the Authorization header + * to pass to the owning AuthHandler, `this->handler`. + * If handler returns true, permits the request to proceed. + * If handler returns false, or the Auth header is absent, + * rejects the request with 401 Unauthorized. + */ + bool authorize(CivetServer* server, mg_connection* conn) override; + + private: + bool AuthorizeInner(CivetServer* server, mg_connection* conn); + void WriteUnauthorizedResponse(mg_connection* conn); + + AuthFunc callback_; + std::string realm_; +}; + +} // namespace prometheus diff --git a/pull/src/detail/base64.h b/pull/src/detail/base64.h new file mode 100644 index 00000000..36233252 --- /dev/null +++ b/pull/src/detail/base64.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include +#include + +namespace prometheus { +namespace detail { + +/* +Copyright (C) 2019-2020 by Martin Vorbrodt + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +https://github.com/mvorbrodt/blog/blob/master/src/base64.hpp +*/ + +inline std::string base64_decode(const std::string& input) { + const char kPadCharacter = '='; + + if (input.length() % 4) { + throw std::runtime_error("Invalid base64 length!"); + } + + std::size_t padding = 0; + + if (!input.empty()) { + if (input[input.length() - 1] == kPadCharacter) padding++; + if (input[input.length() - 2] == kPadCharacter) padding++; + } + + std::string decoded; + decoded.reserve(((input.length() / 4) * 3) - padding); + + std::uint32_t temp = 0; + auto it = input.begin(); + + while (it < input.end()) { + for (std::size_t i = 0; i < 4; ++i) { + temp <<= 6; + if (*it >= 0x41 && *it <= 0x5A) { + temp |= *it - 0x41; + } else if (*it >= 0x61 && *it <= 0x7A) { + temp |= *it - 0x47; + } else if (*it >= 0x30 && *it <= 0x39) { + temp |= *it + 0x04; + } else if (*it == 0x2B) { + temp |= 0x3E; + } else if (*it == 0x2F) { + temp |= 0x3F; + } else if (*it == kPadCharacter) { + switch (input.end() - it) { + case 1: + decoded.push_back((temp >> 16) & 0x000000FF); + decoded.push_back((temp >> 8) & 0x000000FF); + return decoded; + case 2: + decoded.push_back((temp >> 10) & 0x000000FF); + return decoded; + default: + throw std::runtime_error("Invalid padding in base64!"); + } + } else { + throw std::runtime_error("Invalid character in base64!"); + } + + ++it; + } + + decoded.push_back((temp >> 16) & 0x000000FF); + decoded.push_back((temp >> 8) & 0x000000FF); + decoded.push_back((temp)&0x000000FF); + } + + return decoded; +} + +} // namespace detail +} // namespace prometheus diff --git a/pull/src/endpoint.cc b/pull/src/endpoint.cc new file mode 100644 index 00000000..3e8ab3fb --- /dev/null +++ b/pull/src/endpoint.cc @@ -0,0 +1,63 @@ +#include "endpoint.h" + +#include + +#include "basic_auth.h" +#include "handler.h" +#include "prometheus/detail/future_std.h" + +namespace prometheus { +namespace detail { + +namespace { +class AlwaysAllowAccessHandler : public CivetAuthHandler { + bool authorize(CivetServer*, struct mg_connection*) override { return true; } +}; + +AlwaysAllowAccessHandler alwaysAllowAccessHandler; +} // namespace + +Endpoint::Endpoint(CivetServer& server, std::string uri) + : server_(server), + uri_(std::move(uri)), + endpoint_registry_(std::make_shared()), + metrics_handler_( + detail::make_unique(*endpoint_registry_)) { + RegisterCollectable(endpoint_registry_); + server_.addHandler(uri_, metrics_handler_.get()); +} + +Endpoint::~Endpoint() { + server_.removeHandler(uri_); + if (auth_handler_) { + // work-around https://github.com/civetweb/civetweb/issues/941 + // server_.removeAuthHandler(uri_); + server_.addAuthHandler(uri_, alwaysAllowAccessHandler); + } +} + +void Endpoint::RegisterCollectable( + const std::weak_ptr& collectable) { + metrics_handler_->RegisterCollectable(collectable); +} + +void Endpoint::RegisterAuth( + std::function authCB, + const std::string& realm) { + // split creating, assigning, and storing to avoid a race-condition when + // being called the second time and the handler is replaced + auto new_handler = + detail::make_unique(std::move(authCB), realm); + server_.addAuthHandler(uri_, new_handler.get()); + auth_handler_ = std::move(new_handler); +} + +void Endpoint::RemoveCollectable( + const std::weak_ptr& collectable) { + metrics_handler_->RemoveCollectable(collectable); +} + +const std::string& Endpoint::GetURI() const { return uri_; } + +} // namespace detail +} // namespace prometheus diff --git a/pull/src/endpoint.h b/pull/src/endpoint.h new file mode 100644 index 00000000..5c4312ae --- /dev/null +++ b/pull/src/endpoint.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +#include "CivetServer.h" +#include "basic_auth.h" +#include "prometheus/collectable.h" +#include "prometheus/registry.h" + +// IWYU pragma: no_include "handler.h" + +namespace prometheus { +namespace detail { +class MetricsHandler; // IWYU pragma: keep + +class Endpoint { + public: + explicit Endpoint(CivetServer& server, std::string uri); + ~Endpoint(); + + Endpoint(const Endpoint&) = delete; + Endpoint(Endpoint&&) = delete; + Endpoint& operator=(const Endpoint&) = delete; + Endpoint& operator=(Endpoint&&) = delete; + + void RegisterCollectable(const std::weak_ptr& collectable); + void RegisterAuth( + std::function authCB, + const std::string& realm); + void RemoveCollectable(const std::weak_ptr& collectable); + + const std::string& GetURI() const; + + private: + CivetServer& server_; + const std::string uri_; + // registry for "meta" metrics about the endpoint itself + std::shared_ptr endpoint_registry_; + std::unique_ptr metrics_handler_; + std::unique_ptr auth_handler_; +}; + +} // namespace detail +} // namespace prometheus diff --git a/pull/src/exposer.cc b/pull/src/exposer.cc index 6287c830..5ee3adea 100644 --- a/pull/src/exposer.cc +++ b/pull/src/exposer.cc @@ -1,33 +1,67 @@ #include "prometheus/exposer.h" -#include +#include +#include #include -#include - -#include "prometheus/client_metric.h" +#include #include "CivetServer.h" -#include "handler.h" +#include "endpoint.h" +#include "prometheus/detail/future_std.h" namespace prometheus { -Exposer::Exposer(const std::string& bind_address, const std::string& uri, const std::size_t num_threads) - : server_(new CivetServer{ - std::vector{ - "listening_ports", bind_address, - "num_threads", std::to_string(num_threads)}}), - exposer_registry_(std::make_shared()), - metrics_handler_( - new detail::MetricsHandler{collectables_, *exposer_registry_}), - uri_(uri) { - RegisterCollectable(exposer_registry_); - server_->addHandler(uri, metrics_handler_.get()); +Exposer::Exposer(const std::string& bind_address, const std::size_t num_threads, + const CivetCallbacks* callbacks) + : Exposer( + std::vector{"listening_ports", bind_address, + "num_threads", std::to_string(num_threads)}, + callbacks) {} + +Exposer::Exposer(std::vector options, + const CivetCallbacks* callbacks) + : server_(detail::make_unique(std::move(options), callbacks)) { +} + +Exposer::~Exposer() = default; + +void Exposer::RegisterCollectable(const std::weak_ptr& collectable, + const std::string& uri) { + std::lock_guard lock{mutex_}; + auto& endpoint = GetEndpointForUri(uri); + endpoint.RegisterCollectable(collectable); +} + +void Exposer::RegisterAuth( + std::function authCB, + const std::string& realm, const std::string& uri) { + std::lock_guard lock{mutex_}; + auto& endpoint = GetEndpointForUri(uri); + endpoint.RegisterAuth(std::move(authCB), realm); +} + +void Exposer::RemoveCollectable(const std::weak_ptr& collectable, + const std::string& uri) { + std::lock_guard lock{mutex_}; + auto& endpoint = GetEndpointForUri(uri); + endpoint.RemoveCollectable(collectable); } -Exposer::~Exposer() { server_->removeHandler(uri_); } +std::vector Exposer::GetListeningPorts() const { + return server_->getListeningPorts(); +} + +detail::Endpoint& Exposer::GetEndpointForUri(const std::string& uri) { + auto sameUri = [uri](const std::unique_ptr& endpoint) { + return endpoint->GetURI() == uri; + }; + auto it = std::find_if(std::begin(endpoints_), std::end(endpoints_), sameUri); + if (it != std::end(endpoints_)) { + return *it->get(); + } -void Exposer::RegisterCollectable( - const std::weak_ptr& collectable) { - collectables_.push_back(collectable); + endpoints_.emplace_back(detail::make_unique(*server_, uri)); + return *endpoints_.back().get(); } + } // namespace prometheus diff --git a/pull/src/handler.cc b/pull/src/handler.cc index d2efca8a..da8f7055 100644 --- a/pull/src/handler.cc +++ b/pull/src/handler.cc @@ -1,23 +1,29 @@ #include "handler.h" +#include +#include +#include #include #include +#include #ifdef HAVE_ZLIB +#include #include #endif -#include "prometheus/serializer.h" +#include "civetweb.h" +#include "metrics_collector.h" +#include "prometheus/counter.h" +#include "prometheus/metric_family.h" +#include "prometheus/summary.h" #include "prometheus/text_serializer.h" namespace prometheus { namespace detail { -MetricsHandler::MetricsHandler( - const std::vector>& collectables, - Registry& registry) - : collectables_(collectables), - bytes_transferred_family_( +MetricsHandler::MetricsHandler(Registry& registry) + : bytes_transferred_family_( BuildCounter() .Name("exposer_transferred_bytes_total") .Help("Transferred bytes to metrics services") @@ -89,7 +95,7 @@ static std::size_t WriteResponse(struct mg_connection* conn, const std::string& body) { mg_printf(conn, "HTTP/1.1 200 OK\r\n" - "Content-Type: text/plain\r\n"); + "Content-Type: text/plain; charset=utf-8\r\n"); #ifdef HAVE_ZLIB auto acceptsGzip = IsEncodingAccepted(conn, "gzip"); @@ -113,14 +119,40 @@ static std::size_t WriteResponse(struct mg_connection* conn, return body.size(); } +void MetricsHandler::RegisterCollectable( + const std::weak_ptr& collectable) { + std::lock_guard lock{collectables_mutex_}; + CleanupStalePointers(collectables_); + collectables_.push_back(collectable); +} + +void MetricsHandler::RemoveCollectable( + const std::weak_ptr& collectable) { + std::lock_guard lock{collectables_mutex_}; + + auto locked = collectable.lock(); + auto same_pointer = [&locked](const std::weak_ptr& candidate) { + return locked == candidate.lock(); + }; + + collectables_.erase(std::remove_if(std::begin(collectables_), + std::end(collectables_), same_pointer), + std::end(collectables_)); +} + bool MetricsHandler::handleGet(CivetServer*, struct mg_connection* conn) { auto start_time_of_request = std::chrono::steady_clock::now(); - auto metrics = CollectMetrics(); + std::vector metrics; - auto serializer = std::unique_ptr{new TextSerializer()}; + { + std::lock_guard lock{collectables_mutex_}; + metrics = CollectMetrics(collectables_); + } + + const TextSerializer serializer; - auto bodySize = WriteResponse(conn, serializer->Serialize(metrics)); + auto bodySize = WriteResponse(conn, serializer.Serialize(metrics)); auto stop_time_of_request = std::chrono::steady_clock::now(); auto duration = std::chrono::duration_cast( @@ -131,22 +163,15 @@ bool MetricsHandler::handleGet(CivetServer*, struct mg_connection* conn) { num_scrapes_.Increment(); return true; } -std::vector MetricsHandler::CollectMetrics() const { - auto collected_metrics = std::vector{}; - - for (auto&& wcollectable : collectables_) { - auto collectable = wcollectable.lock(); - if (!collectable) { - continue; - } - - auto&& metrics = collectable->Collect(); - collected_metrics.insert(collected_metrics.end(), - std::make_move_iterator(metrics.begin()), - std::make_move_iterator(metrics.end())); - } - return collected_metrics; +void MetricsHandler::CleanupStalePointers( + std::vector>& collectables) { + collectables.erase( + std::remove_if(std::begin(collectables), std::end(collectables), + [](const std::weak_ptr& candidate) { + return candidate.expired(); + }), + std::end(collectables)); } } // namespace detail } // namespace prometheus diff --git a/pull/src/handler.h b/pull/src/handler.h index 70c29a24..10c90f9f 100644 --- a/pull/src/handler.h +++ b/pull/src/handler.h @@ -1,24 +1,33 @@ #pragma once #include +#include #include #include "CivetServer.h" +#include "prometheus/collectable.h" +#include "prometheus/counter.h" +#include "prometheus/family.h" #include "prometheus/registry.h" +#include "prometheus/summary.h" namespace prometheus { namespace detail { class MetricsHandler : public CivetHandler { public: - MetricsHandler(const std::vector>& collectables, - Registry& registry); + explicit MetricsHandler(Registry& registry); + + void RegisterCollectable(const std::weak_ptr& collectable); + void RemoveCollectable(const std::weak_ptr& collectable); bool handleGet(CivetServer* server, struct mg_connection* conn) override; private: - std::vector CollectMetrics() const; + static void CleanupStalePointers( + std::vector>& collectables); - const std::vector>& collectables_; + std::mutex collectables_mutex_; + std::vector> collectables_; Family& bytes_transferred_family_; Counter& bytes_transferred_; Family& num_scrapes_family_; diff --git a/pull/src/metrics_collector.cc b/pull/src/metrics_collector.cc new file mode 100644 index 00000000..0372d693 --- /dev/null +++ b/pull/src/metrics_collector.cc @@ -0,0 +1,30 @@ +#include "metrics_collector.h" + +#include + +#include "prometheus/collectable.h" + +namespace prometheus { +namespace detail { + +std::vector CollectMetrics( + const std::vector>& collectables) { + auto collected_metrics = std::vector{}; + + for (auto&& wcollectable : collectables) { + auto collectable = wcollectable.lock(); + if (!collectable) { + continue; + } + + auto&& metrics = collectable->Collect(); + collected_metrics.insert(collected_metrics.end(), + std::make_move_iterator(metrics.begin()), + std::make_move_iterator(metrics.end())); + } + + return collected_metrics; +} + +} // namespace detail +} // namespace prometheus diff --git a/pull/src/metrics_collector.h b/pull/src/metrics_collector.h new file mode 100644 index 00000000..a6d89a27 --- /dev/null +++ b/pull/src/metrics_collector.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +#include "prometheus/metric_family.h" + +// IWYU pragma: no_include "prometheus/collectable.h" + +namespace prometheus { +class Collectable; // IWYU pragma: keep +namespace detail { +std::vector CollectMetrics( + const std::vector>& collectables); +} // namespace detail +} // namespace prometheus diff --git a/pull/tests/CMakeLists.txt b/pull/tests/CMakeLists.txt index 26e40e61..bb92342e 100644 --- a/pull/tests/CMakeLists.txt +++ b/pull/tests/CMakeLists.txt @@ -1 +1,3 @@ add_subdirectory(integration) +add_subdirectory(internal) +add_subdirectory(unit) diff --git a/pull/tests/integration/BUILD.bazel b/pull/tests/integration/BUILD.bazel index 84fdb4b6..283679d6 100644 --- a/pull/tests/integration/BUILD.bazel +++ b/pull/tests/integration/BUILD.bazel @@ -4,6 +4,18 @@ cc_binary( deps = ["//pull"], ) +cc_binary( + name = "sample-server_multi", + srcs = ["sample_server_multi.cc"], + deps = ["//pull"], +) + +cc_binary( + name = "sample-server_auth", + srcs = ["sample_server_auth.cc"], + deps = ["//pull"], +) + sh_test( name = "scrape-test", size = "small", @@ -23,4 +35,16 @@ sh_test( "sample-server", ], tags = ["manual"], -) \ No newline at end of file +) + +cc_test( + name = "integration_test", + srcs = ["integration_test.cc"], + copts = ["-Iexternal/googletest/include"], + linkstatic = True, + deps = [ + "//pull", + "@com_github_curl//:curl", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/pull/tests/integration/CMakeLists.txt b/pull/tests/integration/CMakeLists.txt index 1ba22cdc..4478d92e 100644 --- a/pull/tests/integration/CMakeLists.txt +++ b/pull/tests/integration/CMakeLists.txt @@ -7,3 +7,41 @@ target_link_libraries(sample_server PRIVATE ${PROJECT_NAME}::pull ) + +add_executable(sample_server_multi + sample_server_multi.cc +) + +target_link_libraries(sample_server_multi + PRIVATE + ${PROJECT_NAME}::pull +) + +add_executable(sample_server_auth + sample_server_auth.cc +) + +target_link_libraries(sample_server_auth + PRIVATE + ${PROJECT_NAME}::pull +) + +find_package(CURL) + +if(CURL_FOUND) + add_executable(prometheus_pull_integration_test + integration_test.cc + ) + + target_link_libraries(prometheus_pull_integration_test + PRIVATE + ${PROJECT_NAME}::pull + CURL::libcurl + GTest::gmock_main + ) + + add_test( + NAME prometheus_pull_integration_test + COMMAND prometheus_pull_integration_test + ) +endif() diff --git a/pull/tests/integration/integration_test.cc b/pull/tests/integration/integration_test.cc new file mode 100644 index 00000000..0ac4fd99 --- /dev/null +++ b/pull/tests/integration/integration_test.cc @@ -0,0 +1,248 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "prometheus/counter.h" +#include "prometheus/detail/future_std.h" +#include "prometheus/exposer.h" +#include "prometheus/family.h" +#include "prometheus/registry.h" + +namespace prometheus { +namespace { + +using namespace testing; + +class IntegrationTest : public testing::Test { + public: + void SetUp() override { + exposer_ = detail::make_unique("127.0.0.1:0"); + auto ports = exposer_->GetListeningPorts(); + base_url_ = std::string("http://127.0.0.1:") + std::to_string(ports.at(0)); + } + + struct Resonse { + long code = 0; + std::string body; + std::string contentType; + }; + + std::function fetchPrePerform_; + + Resonse FetchMetrics(std::string metrics_path) { + auto curl = std::shared_ptr(curl_easy_init(), curl_easy_cleanup); + if (!curl) { + throw std::runtime_error("failed to initialize libcurl"); + } + + const auto url = base_url_ + metrics_path; + Resonse response; + + curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response.body); + curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, WriteCallback); + + if (fetchPrePerform_) { + fetchPrePerform_(curl.get()); + } + + CURLcode curl_error = curl_easy_perform(curl.get()); + if (curl_error != CURLE_OK) { + throw std::runtime_error("failed to perform HTTP request"); + } + + curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &response.code); + + char* ct = nullptr; + curl_easy_getinfo(curl.get(), CURLINFO_CONTENT_TYPE, &ct); + if (ct) { + response.contentType = ct; + } + + return response; + } + + std::shared_ptr RegisterSomeCounter(const std::string& name, + const std::string& path) { + const auto registry = std::make_shared(); + + BuildCounter().Name(name).Register(*registry).Add({}).Increment(); + + exposer_->RegisterCollectable(registry, path); + + return registry; + }; + + std::unique_ptr exposer_; + + std::string base_url_; + std::string default_metrics_path_ = "/metrics"; + + private: + static size_t WriteCallback(void* contents, size_t size, size_t nmemb, + void* userp) { + auto response = reinterpret_cast(userp); + + size_t realsize = size * nmemb; + response->append(reinterpret_cast(contents), realsize); + return realsize; + } +}; + +TEST_F(IntegrationTest, doesNotExposeAnythingOnDefaultPath) { + const auto metrics = FetchMetrics(default_metrics_path_); + + EXPECT_GE(metrics.code, 400); +} + +TEST_F(IntegrationTest, exposeSingleCounter) { + const std::string counter_name = "example_total"; + auto registry = RegisterSomeCounter(counter_name, default_metrics_path_); + + const auto metrics = FetchMetrics(default_metrics_path_); + + ASSERT_EQ(metrics.code, 200); + EXPECT_THAT(metrics.body, HasSubstr(counter_name)); +} + +TEST_F(IntegrationTest, exposesCountersOnDifferentUrls) { + const std::string first_metrics_path = "/first"; + const std::string second_metrics_path = "/second"; + + const std::string first_counter_name = "first_total"; + const std::string second_counter_name = "second_total"; + + const auto first_registry = + RegisterSomeCounter(first_counter_name, first_metrics_path); + const auto second_registry = + RegisterSomeCounter(second_counter_name, second_metrics_path); + + // all set-up + + const auto first_metrics = FetchMetrics(first_metrics_path); + const auto second_metrics = FetchMetrics(second_metrics_path); + + // check results + + ASSERT_EQ(first_metrics.code, 200); + ASSERT_EQ(second_metrics.code, 200); + + EXPECT_THAT(first_metrics.body, HasSubstr(first_counter_name)); + EXPECT_THAT(second_metrics.body, HasSubstr(second_counter_name)); + + EXPECT_THAT(first_metrics.body, Not(HasSubstr(second_counter_name))); + EXPECT_THAT(second_metrics.body, Not(HasSubstr(first_counter_name))); +} + +TEST_F(IntegrationTest, unexposeRegistry) { + const std::string counter_name = "some_counter_total"; + const auto registry = + RegisterSomeCounter(counter_name, default_metrics_path_); + + exposer_->RemoveCollectable(registry, default_metrics_path_); + + const auto metrics = FetchMetrics(default_metrics_path_); + ASSERT_EQ(metrics.code, 200); + EXPECT_THAT(metrics.body, Not(HasSubstr(counter_name))); +} + +TEST_F(IntegrationTest, acceptOptionalCompression) { + const std::string counter_name = "example_total"; + auto registry = RegisterSomeCounter(counter_name, default_metrics_path_); + + fetchPrePerform_ = [](CURL* curl) { + curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, ""); + }; + const auto metrics = FetchMetrics(default_metrics_path_); + + ASSERT_EQ(metrics.code, 200); + EXPECT_THAT(metrics.body, HasSubstr(counter_name)); +} + +#if 0 // https://github.com/civetweb/civetweb/issues/954 +TEST_F(IntegrationTest, shouldRejectRequestWithoutAuthorization) { + const std::string counter_name = "example_total"; + auto registry = RegisterSomeCounter(counter_name, default_metrics_path_); + + exposer_->RegisterAuth( + [](const std::string& user, const std::string& password) { + return user == "test_user" && password == "test_password"; + }, + "Some Auth Realm", default_metrics_path_); + + const auto metrics = FetchMetrics(default_metrics_path_); + + ASSERT_EQ(metrics.code, 401); +} +#endif + +TEST_F(IntegrationTest, shouldPerformProperAuthentication) { + const std::string counter_name = "example_total"; + auto registry = RegisterSomeCounter(counter_name, default_metrics_path_); + + const auto my_username = "test_user"; + const auto my_password = "test_password"; + + fetchPrePerform_ = [my_username, my_password](CURL* curl) { + curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_easy_setopt(curl, CURLOPT_USERNAME, my_username); + curl_easy_setopt(curl, CURLOPT_PASSWORD, my_password); + }; + + exposer_->RegisterAuth( + [my_username, my_password](const std::string& user, + const std::string& password) { + return user == my_username && password == my_password; + }, + "Some Auth Realm", default_metrics_path_); + + const auto metrics = FetchMetrics(default_metrics_path_); + + ASSERT_EQ(metrics.code, 200); + EXPECT_THAT(metrics.body, HasSubstr(counter_name)); +} + +TEST_F(IntegrationTest, shouldDealWithExpiredCollectables) { + const std::string first_counter_name = "first_total"; + const std::string second_counter_name = "second_total"; + + const auto registry = + RegisterSomeCounter(first_counter_name, default_metrics_path_); + auto disappearing_registry = + RegisterSomeCounter(second_counter_name, default_metrics_path_); + + disappearing_registry.reset(); + + // all set-up + + const auto metrics = FetchMetrics(default_metrics_path_); + + // check results + + ASSERT_EQ(metrics.code, 200); + + EXPECT_THAT(metrics.body, HasSubstr(first_counter_name)); + EXPECT_THAT(metrics.body, Not(HasSubstr(second_counter_name))); +} + +TEST_F(IntegrationTest, shouldSendBodyAsUtf8) { + const std::string counter_name = "example_total"; + auto registry = RegisterSomeCounter(counter_name, default_metrics_path_); + + const auto metrics = FetchMetrics(default_metrics_path_); + + // check content type + + ASSERT_EQ(metrics.code, 200); + EXPECT_THAT(metrics.contentType, HasSubstr("utf-8")); +} + +} // namespace +} // namespace prometheus diff --git a/pull/tests/integration/sample_server.cc b/pull/tests/integration/sample_server.cc index 0ca20ebf..742a365c 100644 --- a/pull/tests/integration/sample_server.cc +++ b/pull/tests/integration/sample_server.cc @@ -1,41 +1,71 @@ +#include #include -#include +#include #include #include #include -#include -#include +#include "prometheus/client_metric.h" +#include "prometheus/counter.h" +#include "prometheus/exposer.h" +#include "prometheus/family.h" +#include "prometheus/registry.h" int main() { using namespace prometheus; // create an http server running on port 8080 - Exposer exposer{"127.0.0.1:8080", "/metrics", 1}; + Exposer exposer{"127.0.0.1:8080"}; - // create a metrics registry with component=main labels applied to all its - // metrics + // create a metrics registry + // @note it's the users responsibility to keep the object alive auto registry = std::make_shared(); // add a new counter family to the registry (families combine values with the // same name, but distinct label dimensions) - auto& counter_family = BuildCounter() - .Name("time_running_seconds_total") - .Help("How many seconds is this server running?") - .Labels({{"label", "value"}}) + // + // @note please follow the metric-naming best-practices: + // https://prometheus.io/docs/practices/naming/ + auto& packet_counter = BuildCounter() + .Name("observed_packets_total") + .Help("Number of observed packets") .Register(*registry); - // add a counter to the metric family - auto& second_counter = counter_family.Add( - {{"another_label", "value"}, {"yet_another_label", "value"}}); + // add and remember dimensional data, incrementing those is very cheap + auto& tcp_rx_counter = + packet_counter.Add({{"protocol", "tcp"}, {"direction", "rx"}}); + auto& tcp_tx_counter = + packet_counter.Add({{"protocol", "tcp"}, {"direction", "tx"}}); + auto& udp_rx_counter = + packet_counter.Add({{"protocol", "udp"}, {"direction", "rx"}}); + auto& udp_tx_counter = + packet_counter.Add({{"protocol", "udp"}, {"direction", "tx"}}); - // ask the exposer to scrape the registry on incoming scrapes + // add a counter whose dimensional data is not known at compile time + // nevertheless dimensional values should only occur in low cardinality: + // https://prometheus.io/docs/practices/naming/#labels + auto& http_requests_counter = BuildCounter() + .Name("http_requests_total") + .Help("Number of HTTP requests") + .Register(*registry); + + // ask the exposer to scrape the registry on incoming HTTP requests exposer.RegisterCollectable(registry); for (;;) { std::this_thread::sleep_for(std::chrono::seconds(1)); - // increment the counter by one (second) - second_counter.Increment(); + const auto random_value = std::rand(); + + if (random_value & 1) tcp_rx_counter.Increment(); + if (random_value & 2) tcp_tx_counter.Increment(); + if (random_value & 4) udp_rx_counter.Increment(); + if (random_value & 8) udp_tx_counter.Increment(); + + const std::array methods = {"GET", "PUT", "POST", "HEAD"}; + auto method = methods.at(random_value % methods.size()); + // dynamically calling Family.Add() works but is slow and should be + // avoided + http_requests_counter.Add({{"method", method}}).Increment(); } return 0; } diff --git a/pull/tests/integration/sample_server_auth.cc b/pull/tests/integration/sample_server_auth.cc new file mode 100644 index 00000000..b8ff99dd --- /dev/null +++ b/pull/tests/integration/sample_server_auth.cc @@ -0,0 +1,45 @@ +#include +#include +#include +#include + +#include "prometheus/client_metric.h" +#include "prometheus/counter.h" +#include "prometheus/exposer.h" +#include "prometheus/family.h" +#include "prometheus/registry.h" + +int main() { + using namespace prometheus; + + // create an http server running on port 8080 + Exposer exposer{"127.0.0.1:8080", 1}; + + auto registry = std::make_shared(); + + // add a new counter family to the registry (families combine values with the + // same name, but distinct label dimensions) + auto& counter_family = BuildCounter() + .Name("time_running_seconds_total") + .Help("How many seconds is this server running?") + .Register(*registry); + + // add a counter to the metric family + auto& seconds_counter = counter_family.Add( + {{"another_label", "bar"}, {"yet_another_label", "baz"}}); + + // ask the exposer to scrape registry on incoming scrapes for "/metrics" + exposer.RegisterCollectable(registry, "/metrics"); + exposer.RegisterAuth( + [](const std::string& user, const std::string& password) { + return user == "test_user" && password == "test_password"; + }, + "Some Auth Realm"); + + for (;;) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + // increment the counters by one (second) + seconds_counter.Increment(1.0); + } + return 0; +} diff --git a/pull/tests/integration/sample_server_multi.cc b/pull/tests/integration/sample_server_multi.cc new file mode 100644 index 00000000..801f1b9d --- /dev/null +++ b/pull/tests/integration/sample_server_multi.cc @@ -0,0 +1,54 @@ +#include +#include +#include + +#include "prometheus/client_metric.h" +#include "prometheus/counter.h" +#include "prometheus/exposer.h" +#include "prometheus/family.h" +#include "prometheus/registry.h" + +int main() { + using namespace prometheus; + + // create an http server running on port 8080 + Exposer exposer{"127.0.0.1:8080", 1}; + + auto registryA = std::make_shared(); + + // add a new counter family to the registry (families combine values with the + // same name, but distinct label dimensions) + auto& counter_familyA = BuildCounter() + .Name("time_running_seconds_total") + .Help("How many seconds is this server running?") + .Register(*registryA); + + // add a counter to the metric family + auto& seconds_counterA = counter_familyA.Add( + {{"another_label", "bar"}, {"yet_another_label", "baz"}}); + + // ask the exposer to scrape registryA on incoming scrapes for "/metricsA" + exposer.RegisterCollectable(registryA, "/metricsA"); + + auto registryB = std::make_shared(); + + auto& counter_familyB = + BuildCounter() + .Name("other_time_running_seconds_total") + .Help("How many seconds has something else been running?") + .Register(*registryB); + + auto& seconds_counterB = counter_familyB.Add( + {{"another_label", "not_bar"}, {"yet_another_label", "not_baz"}}); + + // This endpoint exposes registryB. + exposer.RegisterCollectable(registryB, "/metricsB"); + + for (;;) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + // increment the counters by one (second) + seconds_counterA.Increment(1.0); + seconds_counterB.Increment(1.5); + } + return 0; +} diff --git a/pull/tests/integration/scrape.conf b/pull/tests/integration/scrape.conf index 2acc7be5..a9581256 100644 --- a/pull/tests/integration/scrape.conf +++ b/pull/tests/integration/scrape.conf @@ -1,5 +1,7 @@ [[inputs.prometheus]] # An array of urls to scrape metrics from. urls = ["http://localhost:8080/metrics"] + interval = '1s' [[outputs.file]] files = ["stdout"] + flush_interval = "1s" diff --git a/pull/tests/integration/scrape.sh b/pull/tests/integration/scrape.sh index 800b2513..5b1297f1 100755 --- a/pull/tests/integration/scrape.sh +++ b/pull/tests/integration/scrape.sh @@ -6,24 +6,14 @@ if [ ! -x "$telegraf" ] ; then exit 1 fi -pull/tests/integration/sample-server& -sample_server_pid=$! -sleep 1 -telegraf_output="$(telegraf -test -config pull/tests/integration/scrape.conf)" -telegraf_run_result=$? -kill -9 $sample_server_pid +function timeout_after +{ + ( echo failing after $1 seconds for execution && sleep $1 && kill $$ ) & +} -if [ $telegraf_run_result -ne 0 ] ; then - exit $telegraf_run_result -fi - -if [[ ! $telegraf_output == *"time_running_seconds_total"* ]] ; then - echo "Could not find time_running_seconds_total in exposed metrics:" - echo "${telegraf_run_output}" - exit 1 -fi +trap 'kill $(jobs -p)' EXIT -echo "Success:" -echo "${telegraf_output}" +timeout_after 10 -exit 0 +pull/tests/integration/sample-server & +telegraf --config pull/tests/integration/scrape.conf --quiet | grep -m1 http_requests_total diff --git a/pull/tests/internal/BUILD.bazel b/pull/tests/internal/BUILD.bazel new file mode 100644 index 00000000..f6967b17 --- /dev/null +++ b/pull/tests/internal/BUILD.bazel @@ -0,0 +1,13 @@ +cc_test( + name = "internal", + srcs = glob([ + "*.cc", + "*.h", + ]), + copts = ["-Iexternal/googletest/include"], + linkstatic = True, + deps = [ + "//pull:pull_internal_headers", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/pull/tests/internal/CMakeLists.txt b/pull/tests/internal/CMakeLists.txt new file mode 100644 index 00000000..87489021 --- /dev/null +++ b/pull/tests/internal/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable(prometheus_pull_internal_test + base64_test.cc +) + +target_link_libraries(prometheus_pull_internal_test + PRIVATE + ${PROJECT_NAME}::pull_internal_headers + GTest::gmock_main +) + +add_test( + NAME prometheus_pull_internal_test + COMMAND prometheus_pull_internal_test +) diff --git a/pull/tests/internal/base64_test.cc b/pull/tests/internal/base64_test.cc new file mode 100644 index 00000000..980e8c96 --- /dev/null +++ b/pull/tests/internal/base64_test.cc @@ -0,0 +1,49 @@ +#include "detail/base64.h" + +#include + +#include + +namespace prometheus { +namespace { + +struct TestVector { + const std::string decoded; + const std::string encoded; +}; + +const TestVector testVector[] = { + {"", ""}, + {"f", "Zg=="}, + {"fo", "Zm8="}, + {"foo", "Zm9v"}, + {"foob", "Zm9vYg=="}, + {"fooba", "Zm9vYmE="}, + {"foobar", "Zm9vYmFy"}, +}; + +const unsigned nVectors = sizeof(testVector) / sizeof(testVector[0]); + +using namespace testing; + +TEST(Base64Test, decodeTest) { + for (unsigned i = 0; i < nVectors; ++i) { + std::string decoded = detail::base64_decode(testVector[i].encoded); + EXPECT_EQ(testVector[i].decoded, decoded); + } +} + +TEST(Base64Test, rejectInvalidSymbols) { + EXPECT_ANY_THROW(detail::base64_decode("....")); +} + +TEST(Base64Test, rejectInvalidInputSize) { + EXPECT_ANY_THROW(detail::base64_decode("ABC")); +} + +TEST(Base64Test, rejectInvalidPadding) { + EXPECT_ANY_THROW(detail::base64_decode("A===")); +} + +} // namespace +} // namespace prometheus diff --git a/pull/tests/unit/BUILD.bazel b/pull/tests/unit/BUILD.bazel new file mode 100644 index 00000000..fc174146 --- /dev/null +++ b/pull/tests/unit/BUILD.bazel @@ -0,0 +1,13 @@ +cc_test( + name = "unit", + srcs = glob([ + "*.cc", + "*.h", + ]), + copts = ["-Iexternal/googletest/include"], + linkstatic = True, + deps = [ + "//pull", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/pull/tests/unit/CMakeLists.txt b/pull/tests/unit/CMakeLists.txt new file mode 100644 index 00000000..5c5c4857 --- /dev/null +++ b/pull/tests/unit/CMakeLists.txt @@ -0,0 +1,15 @@ + +add_executable(prometheus_pull_test + exposer_test.cc +) + +target_link_libraries(prometheus_pull_test + PRIVATE + ${PROJECT_NAME}::pull + GTest::gmock_main +) + +add_test( + NAME prometheus_pull_test + COMMAND prometheus_pull_test +) diff --git a/pull/tests/unit/exposer_test.cc b/pull/tests/unit/exposer_test.cc new file mode 100644 index 00000000..94c2f32d --- /dev/null +++ b/pull/tests/unit/exposer_test.cc @@ -0,0 +1,25 @@ +#include "prometheus/exposer.h" + +#include + +namespace prometheus { +namespace { + +using namespace testing; + +TEST(ExposerTest, listenOnDistinctPorts) { + Exposer firstExposer{"0.0.0.0:0"}; + auto firstExposerPorts = firstExposer.GetListeningPorts(); + ASSERT_EQ(1u, firstExposerPorts.size()); + EXPECT_NE(0, firstExposerPorts.front()); + + Exposer secondExposer{"0.0.0.0:0"}; + auto secondExposerPorts = secondExposer.GetListeningPorts(); + ASSERT_EQ(1u, secondExposerPorts.size()); + EXPECT_NE(0, secondExposerPorts.front()); + + EXPECT_NE(firstExposerPorts, secondExposerPorts); +} + +} // namespace +} // namespace prometheus diff --git a/push/BUILD.bazel b/push/BUILD.bazel index 15d44c09..c7cb3f91 100644 --- a/push/BUILD.bazel +++ b/push/BUILD.bazel @@ -1,3 +1,11 @@ +load("//bazel:export_header.bzl", "generate_dummy_export_header") + +generate_dummy_export_header( + name = "export_header", + basename = "PROMETHEUS_CPP_PUSH", + header = "include/prometheus/detail/push_export.h", +) + cc_library( name = "push", srcs = glob([ @@ -6,7 +14,7 @@ cc_library( ]), hdrs = glob( ["include/**/*.h"], - ), + ) + [":export_header"], linkopts = select({ "//:windows": [], "//:windows_msvc": [], diff --git a/push/CMakeLists.txt b/push/CMakeLists.txt index 4e4002e2..191504a8 100644 --- a/push/CMakeLists.txt +++ b/push/CMakeLists.txt @@ -2,6 +2,8 @@ find_package(CURL REQUIRED) add_library(push + src/curl_wrapper.cc + src/curl_wrapper.h src/gateway.cc ) @@ -12,18 +14,28 @@ target_link_libraries(push ${PROJECT_NAME}::core PRIVATE Threads::Threads - ${CURL_LIBRARIES} + CURL::libcurl $<$,$>>:rt> ) target_include_directories(push PUBLIC $ - PRIVATE - ${CURL_INCLUDE_DIRS} + $ ) -set_target_properties(push PROPERTIES OUTPUT_NAME ${PROJECT_NAME}-push) +set_target_properties(push + PROPERTIES + OUTPUT_NAME ${PROJECT_NAME}-push + DEFINE_SYMBOL PROMETHEUS_CPP_PUSH_EXPORTS + VERSION "${PROJECT_VERSION}" + SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}" +) + +generate_export_header(push + BASE_NAME ${PROJECT_NAME}-push + EXPORT_FILE_NAME include/prometheus/detail/push_export.h +) install( TARGETS push @@ -35,10 +47,28 @@ install( ) install( - DIRECTORY include/ + DIRECTORY include/ ${CMAKE_CURRENT_BINARY_DIR}/include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) +if(GENERATE_PKGCONFIG) + set(PKGCONFIG_LIBS) + set(PKGCONFIG_REQUIRES) + + string(APPEND PKGCONFIG_REQUIRES " libcurl") + + configure_file( + ${PROJECT_SOURCE_DIR}/cmake/prometheus-cpp-push.pc.in + ${CMAKE_CURRENT_BINARY_DIR}/prometheus-cpp-push.pc + @ONLY + ) + + install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/prometheus-cpp-push.pc + DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig + ) +endif() + if(ENABLE_TESTING) add_subdirectory(tests) endif() diff --git a/push/include/prometheus/detail/http_method.h b/push/include/prometheus/detail/http_method.h new file mode 100644 index 00000000..21eb1820 --- /dev/null +++ b/push/include/prometheus/detail/http_method.h @@ -0,0 +1,12 @@ +#pragma once + +namespace prometheus { +namespace detail { +enum class HttpMethod { + Post, + Put, + Delete, +}; + +} // namespace detail +} // namespace prometheus diff --git a/push/include/prometheus/gateway.h b/push/include/prometheus/gateway.h index 1456d99c..2bdb0de9 100644 --- a/push/include/prometheus/gateway.h +++ b/push/include/prometheus/gateway.h @@ -1,29 +1,40 @@ #pragma once #include -#include -#include #include +#include #include +#include #include -#include "prometheus/registry.h" +#include "prometheus/collectable.h" +#include "prometheus/detail/http_method.h" +#include "prometheus/detail/push_export.h" +#include "prometheus/labels.h" namespace prometheus { -class Gateway { +namespace detail { +class CurlWrapper; // IWYU pragma: keep +} + +class PROMETHEUS_CPP_PUSH_EXPORT Gateway { public: - using Labels = std::map; + Gateway(const std::string& host, const std::string& port, + const std::string& jobname, const Labels& labels = {}, + const std::string& username = {}, const std::string& password = {}); + + Gateway(const Gateway&) = delete; + Gateway(Gateway&&) = delete; + Gateway& operator=(const Gateway&) = delete; + Gateway& operator=(Gateway&&) = delete; - Gateway(const std::string host, const std::string port, - const std::string jobname, const Labels& labels = {}, - const std::string username = {}, const std::string password = {}); ~Gateway(); void RegisterCollectable(const std::weak_ptr& collectable, const Labels* labels = nullptr); - static const Labels GetInstanceLabel(std::string hostname); + static Labels GetInstanceLabel(std::string hostname); // Push metrics to the given pushgateway. int Push(); @@ -41,28 +52,28 @@ class Gateway { // Delete metrics from the given pushgateway. std::future AsyncDelete(); + // Delete metrics from the given pushgateway (for configured instance labels). + int DeleteForInstance(); + + // Delete metrics from the given pushgateway (for configured instance labels). + std::future AsyncDeleteForInstance(); + private: std::string jobUri_; std::string labels_; - std::string auth_; + std::unique_ptr curlWrapper_; + std::mutex mutex_; using CollectableEntry = std::pair, std::string>; std::vector collectables_; std::string getUri(const CollectableEntry& collectable) const; - enum class HttpMethod { - Post, - Put, - Delete, - }; - - int performHttpRequest(HttpMethod method, const std::string& uri, - const std::string& body) const; + int push(detail::HttpMethod method); - int push(HttpMethod method); + std::future async_push(detail::HttpMethod method); - std::future async_push(HttpMethod method); + static void CleanupStalePointers(std::vector& collectables); }; } // namespace prometheus diff --git a/push/src/curl_wrapper.cc b/push/src/curl_wrapper.cc new file mode 100644 index 00000000..767e10f1 --- /dev/null +++ b/push/src/curl_wrapper.cc @@ -0,0 +1,90 @@ +#include "curl_wrapper.h" + +#include + +namespace prometheus { +namespace detail { + +static const char CONTENT_TYPE[] = + "Content-Type: text/plain; version=0.0.4; charset=utf-8"; + +CurlWrapper::CurlWrapper(const std::string& username, + const std::string& password) { + /* In windows, this will init the winsock stuff */ + auto error = curl_global_init(CURL_GLOBAL_ALL); + if (error) { + throw std::runtime_error("Cannot initialize global curl!"); + } + + curl_ = curl_easy_init(); + if (!curl_) { + curl_global_cleanup(); + throw std::runtime_error("Cannot initialize easy curl!"); + } + + if (!username.empty()) { + auth_ = username + ":" + password; + } +} + +CurlWrapper::~CurlWrapper() { + curl_easy_cleanup(curl_); + curl_global_cleanup(); +} + +int CurlWrapper::performHttpRequest(HttpMethod method, const std::string& uri, + const std::string& body) { + std::lock_guard l(mutex_); + + curl_easy_reset(curl_); + curl_easy_setopt(curl_, CURLOPT_URL, uri.c_str()); + + curl_slist* header_chunk = nullptr; + header_chunk = curl_slist_append(header_chunk, CONTENT_TYPE); + curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, header_chunk); + + if (!body.empty()) { + curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, body.size()); + curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, body.data()); + } else { + curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, 0L); + } + + if (!auth_.empty()) { + curl_easy_setopt(curl_, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_easy_setopt(curl_, CURLOPT_USERPWD, auth_.c_str()); + } + + switch (method) { + case HttpMethod::Post: + curl_easy_setopt(curl_, CURLOPT_POST, 1L); + break; + + case HttpMethod::Put: + curl_easy_setopt(curl_, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "PUT"); + break; + + case HttpMethod::Delete: + curl_easy_setopt(curl_, CURLOPT_HTTPGET, 0L); + curl_easy_setopt(curl_, CURLOPT_NOBODY, 0L); + curl_easy_setopt(curl_, CURLOPT_CUSTOMREQUEST, "DELETE"); + break; + } + + auto curl_error = curl_easy_perform(curl_); + + long response_code; + curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &response_code); + + curl_slist_free_all(header_chunk); + + if (curl_error != CURLE_OK) { + return -curl_error; + } + + return response_code; +} + +} // namespace detail +} // namespace prometheus diff --git a/push/src/curl_wrapper.h b/push/src/curl_wrapper.h new file mode 100644 index 00000000..df20d6bb --- /dev/null +++ b/push/src/curl_wrapper.h @@ -0,0 +1,32 @@ +#include + +#include +#include + +#include "prometheus/detail/http_method.h" + +namespace prometheus { +namespace detail { + +class CurlWrapper { + public: + CurlWrapper(const std::string& username, const std::string& password); + + CurlWrapper(const CurlWrapper&) = delete; + CurlWrapper(CurlWrapper&&) = delete; + CurlWrapper& operator=(const CurlWrapper&) = delete; + CurlWrapper& operator=(CurlWrapper&&) = delete; + + ~CurlWrapper(); + + int performHttpRequest(HttpMethod method, const std::string& uri, + const std::string& body); + + private: + CURL* curl_; + std::string auth_; + std::mutex mutex_; +}; + +} // namespace detail +} // namespace prometheus diff --git a/push/src/gateway.cc b/push/src/gateway.cc index bc137ec4..15227ec8 100644 --- a/push/src/gateway.cc +++ b/push/src/gateway.cc @@ -1,33 +1,31 @@ #include "prometheus/gateway.h" +#include +#include +#include +#include #include -#include "prometheus/client_metric.h" -#include "prometheus/serializer.h" +#include "curl_wrapper.h" +#include "prometheus/detail/future_std.h" +#include "prometheus/metric_family.h" // IWYU pragma: keep #include "prometheus/text_serializer.h" -#include +// IWYU pragma: no_include +// IWYU pragma: no_include namespace prometheus { -static const char CONTENT_TYPE[] = - "Content-Type: text/plain; version=0.0.4; charset=utf-8"; - -Gateway::Gateway(const std::string host, const std::string port, - const std::string jobname, const Labels& labels, - const std::string username, const std::string password) { - /* In windows, this will init the winsock stuff */ - curl_global_init(CURL_GLOBAL_ALL); +Gateway::Gateway(const std::string& host, const std::string& port, + const std::string& jobname, const Labels& labels, + const std::string& username, const std::string& password) { + curlWrapper_ = detail::make_unique(username, password); std::stringstream jobUriStream; jobUriStream << host << ':' << port << "/metrics/job/" << jobname; jobUri_ = jobUriStream.str(); - if (!username.empty()) { - auth_ = username + ":" + password; - } - std::stringstream labelStream; for (auto& label : labels) { labelStream << "/" << label.first << "/" << label.second; @@ -35,13 +33,13 @@ Gateway::Gateway(const std::string host, const std::string port, labels_ = labelStream.str(); } -Gateway::~Gateway() { curl_global_cleanup(); } +Gateway::~Gateway() = default; -const Gateway::Labels Gateway::GetInstanceLabel(std::string hostname) { +Labels Gateway::GetInstanceLabel(std::string hostname) { if (hostname.empty()) { - return Gateway::Labels{}; + return Labels{}; } - return Gateway::Labels{{"instance", hostname}}; + return Labels{{"instance", hostname}}; } void Gateway::RegisterCollectable(const std::weak_ptr& collectable, @@ -54,66 +52,11 @@ void Gateway::RegisterCollectable(const std::weak_ptr& collectable, } } + std::lock_guard lock{mutex_}; + CleanupStalePointers(collectables_); collectables_.push_back(std::make_pair(collectable, ss.str())); } -int Gateway::performHttpRequest(HttpMethod method, const std::string& uri, - const std::string& body) const { - auto curl = curl_easy_init(); - if (!curl) { - return -CURLE_FAILED_INIT; - } - - curl_easy_setopt(curl, CURLOPT_URL, uri.c_str()); - - curl_slist* header_chunk = nullptr; - - if (!body.empty()) { - header_chunk = curl_slist_append(nullptr, CONTENT_TYPE); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_chunk); - - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body.size()); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.data()); - } - - if (!auth_.empty()) { - curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); - curl_easy_setopt(curl, CURLOPT_USERPWD, auth_.c_str()); - } - - switch (method) { - case HttpMethod::Post: - curl_easy_setopt(curl, CURLOPT_HTTPGET, 0L); - curl_easy_setopt(curl, CURLOPT_NOBODY, 0L); - break; - - case HttpMethod::Put: - curl_easy_setopt(curl, CURLOPT_NOBODY, 0L); - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); - break; - - case HttpMethod::Delete: - curl_easy_setopt(curl, CURLOPT_HTTPGET, 0L); - curl_easy_setopt(curl, CURLOPT_NOBODY, 0L); - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); - break; - } - - auto curl_error = curl_easy_perform(curl); - - long response_code; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); - - curl_easy_cleanup(curl); - curl_slist_free_all(header_chunk); - - if (curl_error != CURLE_OK) { - return -curl_error; - } - - return response_code; -} - std::string Gateway::getUri(const CollectableEntry& collectable) const { std::stringstream uri; uri << jobUri_ << labels_ << collectable.second; @@ -121,13 +64,14 @@ std::string Gateway::getUri(const CollectableEntry& collectable) const { return uri.str(); } -int Gateway::Push() { return push(HttpMethod::Post); } +int Gateway::Push() { return push(detail::HttpMethod::Post); } -int Gateway::PushAdd() { return push(HttpMethod::Put); } +int Gateway::PushAdd() { return push(detail::HttpMethod::Put); } -int Gateway::push(HttpMethod method) { +int Gateway::push(detail::HttpMethod method) { const auto serializer = TextSerializer{}; + std::lock_guard lock{mutex_}; for (auto& wcollectable : collectables_) { auto collectable = wcollectable.first.lock(); if (!collectable) { @@ -137,9 +81,9 @@ int Gateway::push(HttpMethod method) { auto metrics = collectable->Collect(); auto body = serializer.Serialize(metrics); auto uri = getUri(wcollectable); - auto status_code = performHttpRequest(method, uri, body); + auto status_code = curlWrapper_->performHttpRequest(method, uri, body); - if (status_code >= 400) { + if (status_code < 100 || status_code >= 400) { return status_code; } } @@ -147,14 +91,19 @@ int Gateway::push(HttpMethod method) { return 200; } -std::future Gateway::AsyncPush() { return async_push(HttpMethod::Post); } +std::future Gateway::AsyncPush() { + return async_push(detail::HttpMethod::Post); +} -std::future Gateway::AsyncPushAdd() { return async_push(HttpMethod::Put); } +std::future Gateway::AsyncPushAdd() { + return async_push(detail::HttpMethod::Put); +} -std::future Gateway::async_push(HttpMethod method) { +std::future Gateway::async_push(detail::HttpMethod method) { const auto serializer = TextSerializer{}; std::vector> futures; + std::lock_guard lock{mutex_}; for (auto& wcollectable : collectables_) { auto collectable = wcollectable.first.lock(); if (!collectable) { @@ -162,11 +111,11 @@ std::future Gateway::async_push(HttpMethod method) { } auto metrics = collectable->Collect(); - auto body = serializer.Serialize(metrics); + auto body = std::make_shared(serializer.Serialize(metrics)); auto uri = getUri(wcollectable); - futures.push_back(std::async(std::launch::async, [&] { - return performHttpRequest(method, uri, body); + futures.push_back(std::async(std::launch::async, [method, uri, body, this] { + return curlWrapper_->performHttpRequest(method, uri, *body); })); } @@ -176,7 +125,7 @@ std::future Gateway::async_push(HttpMethod method) { for (auto& future : lfutures) { auto status_code = future.get(); - if (status_code >= 400) { + if (status_code < 100 || status_code >= 400) { final_status_code = status_code; } } @@ -188,11 +137,31 @@ std::future Gateway::async_push(HttpMethod method) { } int Gateway::Delete() { - return performHttpRequest(HttpMethod::Delete, jobUri_, {}); + return curlWrapper_->performHttpRequest(detail::HttpMethod::Delete, jobUri_, + {}); } std::future Gateway::AsyncDelete() { return std::async(std::launch::async, [&] { return Delete(); }); } +int Gateway::DeleteForInstance() { + return curlWrapper_->performHttpRequest(detail::HttpMethod::Delete, + jobUri_ + labels_, {}); +} + +std::future Gateway::AsyncDeleteForInstance() { + return std::async(std::launch::async, [&] { return DeleteForInstance(); }); +} + +void Gateway::CleanupStalePointers( + std::vector& collectables) { + collectables.erase( + std::remove_if(std::begin(collectables), std::end(collectables), + [](const CollectableEntry& candidate) { + return candidate.first.expired(); + }), + std::end(collectables)); +} + } // namespace prometheus diff --git a/push/tests/integration/sample_client.cc b/push/tests/integration/sample_client.cc index 7a0c283c..d2c5f8bf 100644 --- a/push/tests/integration/sample_client.cc +++ b/push/tests/integration/sample_client.cc @@ -1,16 +1,18 @@ #include -#include +#include #include #include #include -#include -#include +#include "prometheus/client_metric.h" +#include "prometheus/counter.h" +#include "prometheus/family.h" +#include "prometheus/gateway.h" +#include "prometheus/registry.h" #ifdef _WIN32 #include #else -#include #include #endif @@ -56,7 +58,8 @@ int main() { second_counter.Increment(); // push metrics - gateway.Push(); + auto returnCode = gateway.Push(); + std::cout << "returnCode is " << returnCode << std::endl; } return 0; } diff --git a/repositories.bzl b/repositories.bzl deleted file mode 100644 index 8696657b..00000000 --- a/repositories.bzl +++ /dev/null @@ -1,63 +0,0 @@ -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") - -def load_civetweb(): - http_archive( - name = "civetweb", - strip_prefix = "civetweb-1.11", - sha256 = "de7d5e7a2d9551d325898c71e41d437d5f7b51e754b242af897f7be96e713a42", - urls = [ - "https://github.com/civetweb/civetweb/archive/v1.11.tar.gz", - ], - build_file = "@com_github_jupp0r_prometheus_cpp//bazel:civetweb.BUILD", - ) - -def load_com_google_googletest(): - http_archive( - name = "com_google_googletest", - sha256 = "9bf1fe5182a604b4135edc1a425ae356c9ad15e9b23f9f12a02e80184c3a249c", - strip_prefix = "googletest-release-1.8.1", - urls = [ - "https://github.com/google/googletest/archive/release-1.8.1.tar.gz", - ], - ) - -def load_com_github_curl(): - http_archive( - name = "com_github_curl", - sha256 = "e9c37986337743f37fd14fe8737f246e97aec94b39d1b71e8a5973f72a9fc4f5", - strip_prefix = "curl-7.60.0", - urls = [ - "https://mirror.bazel.build/curl.haxx.se/download/curl-7.60.0.tar.gz", - "https://curl.haxx.se/download/curl-7.60.0.tar.gz", - ], - build_file = "@com_github_jupp0r_prometheus_cpp//bazel:curl.BUILD", - ) - -def load_com_github_google_benchmark(): - http_archive( - name = "com_github_google_benchmark", - sha256 = "f8e525db3c42efc9c7f3bc5176a8fa893a9a9920bbd08cef30fb56a51854d60d", - strip_prefix = "benchmark-1.4.1", - urls = [ - "https://github.com/google/benchmark/archive/v1.4.1.tar.gz", - ], - ) - -def load_net_zlib_zlib(): - http_archive( - name = "net_zlib_zlib", - sha256 = "c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1", - strip_prefix = "zlib-1.2.11", - urls = [ - "https://mirror.bazel.build/zlib.net/zlib-1.2.11.tar.gz", - "https://zlib.net/zlib-1.2.11.tar.gz", - ], - build_file = "@com_github_jupp0r_prometheus_cpp//bazel:zlib.BUILD", - ) - -def prometheus_cpp_repositories(): - if "civetweb" not in native.existing_rules(): load_civetweb() - if "com_google_googletest" not in native.existing_rules(): load_com_google_googletest() - if "com_github_google_benchmark" not in native.existing_rules(): load_com_github_google_benchmark() - if "com_github_curl" not in native.existing_rules(): load_com_github_curl() - if "net_zlib_zlib" not in native.existing_rules(): load_net_zlib_zlib()