From 8bac6614b138955df11ba069c746eb46c2053a81 Mon Sep 17 00:00:00 2001 From: Stig Bakken Date: Mon, 29 Jun 2026 15:52:18 +0200 Subject: [PATCH 1/6] chore: adopt AGENTS.md with CLAUDE.md as a symlink Move the repo guidance into AGENTS.md (read by agent tooling generally) and point CLAUDE.md at it via symlink so both resolve to the same content. --- AGENTS.md | 21 +++++++++++++++++++++ CLAUDE.md | 22 +--------------------- 2 files changed, 22 insertions(+), 21 deletions(-) create mode 100644 AGENTS.md mode change 100644 => 120000 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9c323a1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,21 @@ +# prometheus-cpp + +High-performance header-only C++23 Prometheus client library (stigsb/prometheus-cpp). + +- Values are `int64_t` atomics, not `double` — single `LOCK XADD` on x86, no CAS loops +- Labels are compile-time typed structs via `PROMETHEUS_DEFINE_LABELS` macro +- Zero allocation on the hot path; `get()` handles should be cached + +## Building + +```bash +cmake -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build +# Tests: +cmake -B build -DCMAKE_BUILD_TYPE=Debug -DPROMETHEUS_BUILD_TESTS=ON && cmake --build build && ctest --test-dir build --output-on-failure +``` + +## Available skills + +- `/prometheus-metrics` — Add, audit, and maintain Prometheus metrics in apps using this library. + Install in your project: `cp -r .claude/skills/prometheus-metrics/ your-project/.claude/skills/` diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 9c323a1..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,21 +0,0 @@ -# prometheus-cpp - -High-performance header-only C++23 Prometheus client library (stigsb/prometheus-cpp). - -- Values are `int64_t` atomics, not `double` — single `LOCK XADD` on x86, no CAS loops -- Labels are compile-time typed structs via `PROMETHEUS_DEFINE_LABELS` macro -- Zero allocation on the hot path; `get()` handles should be cached - -## Building - -```bash -cmake -B build -DCMAKE_BUILD_TYPE=Release -cmake --build build -# Tests: -cmake -B build -DCMAKE_BUILD_TYPE=Debug -DPROMETHEUS_BUILD_TESTS=ON && cmake --build build && ctest --test-dir build --output-on-failure -``` - -## Available skills - -- `/prometheus-metrics` — Add, audit, and maintain Prometheus metrics in apps using this library. - Install in your project: `cp -r .claude/skills/prometheus-metrics/ your-project/.claude/skills/` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file From ef7fa106755f17b2c9db472dfe64f531eb829ae0 Mon Sep 17 00:00:00 2001 From: Stig Bakken Date: Mon, 29 Jun 2026 15:52:32 +0200 Subject: [PATCH 2/6] build: add clang-format/clang-tidy, pre-commit, and lint-check Stop hook - .clang-format: style matched to the codebase (clang-format 22). - .clang-tidy: broad bugprone/performance/modernize/readability/portability/ clang-analyzer families, with the checks the existing code does not yet satisfy disabled so the tree is green today; WarningsAsErrors keeps new violations from sneaking in. Design-driven disables (#pragma once, terse hot-path names, label macros) are grouped separately from the candidates to fix-and-re-enable later. - .pre-commit-config.yaml: clang-format via the pinned mirror; clang-tidy via scripts/run-clang-tidy-precommit.sh, which lints only translation units the compile DB knows about and skips gracefully when no build dir exists. - .claude/hooks/lint-check.sh + settings.json: a Stop hook that blocks turn completion until files changed vs HEAD are clang-format clean and clang-tidy clean. Both wrappers add the macOS SDK isysroot so clang-tidy can read an AppleClang compile database. --- .clang-format | 66 +++++++++++++ .clang-tidy | 65 +++++++++++++ .claude/hooks/lint-check.sh | 141 ++++++++++++++++++++++++++++ .claude/settings.json | 17 ++++ .gitignore | 1 + .pre-commit-config.yaml | 30 ++++++ scripts/run-clang-tidy-precommit.sh | 69 ++++++++++++++ 7 files changed, 389 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100755 .claude/hooks/lint-check.sh create mode 100644 .claude/settings.json create mode 100644 .pre-commit-config.yaml create mode 100755 scripts/run-clang-tidy-precommit.sh diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..44a2a7b --- /dev/null +++ b/.clang-format @@ -0,0 +1,66 @@ +# Derived from the existing prometheus-cpp source style. +# Base: Google, with 4-space indent / 100-col / aligned-assignment overrides. +--- +Language: Cpp +BasedOnStyle: Google + +# Indentation +IndentWidth: 4 +ContinuationIndentWidth: 4 +ConstructorInitializerIndentWidth: 4 +AccessModifierOffset: -4 +NamespaceIndentation: None +TabWidth: 4 +UseTab: Never + +# Line width +ColumnLimit: 100 + +# Pointers / references bind to the type (int64_t& x, std::ostream& out) +PointerAlignment: Left +DerivePointerAlignment: false + +# Braces stay attached (K&R) +BreakBeforeBraces: Attach + +# Constructor initializer lists: colon/comma lead the line +BreakConstructorInitializers: BeforeComma +PackConstructorInitializers: Never + +# Functions are written out across multiple lines, even when short. +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: true +AllowShortLambdasOnASingleLine: All + +# case X: return Y; rows are kept on one line and aligned. +AlignConsecutiveShortCaseStatements: + Enabled: true + AcrossEmptyLines: false + AcrossComments: false + AlignCaseColons: false + +# Do not collapse hand-broken parameter/argument lists. +BinPackParameters: false +BinPackArguments: false + +# Aligned consecutive assignments are used intentionally throughout. +AlignConsecutiveAssignments: + Enabled: true + AcrossEmptyLines: false + AcrossComments: false + AlignCompound: false + PadOperators: true + +# Includes are grouped by hand and not alphabetised. +SortIncludes: Never +IncludeBlocks: Preserve + +# Misc +Standard: c++20 +SpacesBeforeTrailingComments: 1 +AlignAfterOpenBracket: Align +AlwaysBreakTemplateDeclarations: Yes +EmptyLineBeforeAccessModifier: LogicalBlock diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..279fc8a --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,65 @@ +# clang-tidy configuration for prometheus-cpp. +# +# Strategy: enable broad, high-value check families and disable the specific +# checks that the existing code does not currently satisfy, so the tree is +# GREEN today and any *new* violation of a still-enabled check fails the build +# (WarningsAsErrors below). The disabled-because-firing checks are grouped +# separately from the disabled-by-design ones; the former are candidates to +# fix-and-re-enable later, the latter conflict with deliberate design choices +# (preprocessor label macros, #pragma once, int64_t atomics, terse hot-path +# names) and should stay off. +# +# Run it: +# cmake -B build -DPROMETHEUS_BUILD_TESTS=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +# clang-tidy -p build +# On macOS, clang-tidy needs the SDK; the repo's lint wrappers add it for you +# (scripts/run-clang-tidy-precommit.sh and .claude/hooks/lint-check.sh). +--- +Checks: > + bugprone-*, + performance-*, + modernize-*, + readability-*, + portability-*, + clang-analyzer-*, + + -bugprone-easily-swappable-parameters, + -modernize-use-trailing-return-type, + -modernize-macro-to-enum, + -modernize-use-designated-initializers, + -readability-identifier-length, + -readability-magic-numbers, + -readability-braces-around-statements, + -readability-implicit-bool-conversion, + -readability-function-cognitive-complexity, + -readability-math-missing-parentheses, + -misc-include-cleaner, + -misc-non-private-member-variables-in-classes, + -misc-use-anonymous-namespace, + -portability-avoid-pragma-once, + + -bugprone-exception-escape, + -bugprone-implicit-widening-of-multiplication-result, + -readability-uppercase-literal-suffix, + -clang-diagnostic-unused-lambda-capture, + -clang-diagnostic-comment, + -bugprone-suspicious-include, + -bugprone-unused-local-non-trivial-variable, + -clang-analyzer-optin.core.EnumCastOutOfRange, + -clang-analyzer-optin.performance.Padding, + -modernize-avoid-c-arrays, + -modernize-return-braced-init-list, + -modernize-use-default-member-init, + -modernize-use-nodiscard, + -modernize-use-ranges, + -modernize-use-std-print, + -performance-enum-size, + -performance-faster-string-find, + -readability-container-contains, + -readability-inconsistent-ifelse-braces, + -readability-redundant-member-init, + -readability-redundant-typename, + +HeaderFilterRegex: 'prometheus/.*\.hpp$' +WarningsAsErrors: '*' +FormatStyle: file diff --git a/.claude/hooks/lint-check.sh b/.claude/hooks/lint-check.sh new file mode 100755 index 0000000..ed36a32 --- /dev/null +++ b/.claude/hooks/lint-check.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# +# Stop hook: ensure the C/C++ code Claude touched this turn is lint-free before +# the turn is allowed to finish. Runs clang-format (always) and clang-tidy (when +# a compile database is available, or directly on headers) over the files changed +# relative to HEAD. If anything is non-conformant it blocks the stop and feeds the +# diagnostics back to Claude so it fixes them and re-checks — by design this lives +# at "post-prompt" (Stop) rather than post-tool, so Claude can make several edits +# and only needs the tree clean once it is done. +# +# Output contract (Stop hook): print {"decision":"block","reason":...} on stdout +# to keep Claude working; print nothing and exit 0 to let it stop. + +set -uo pipefail + +repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "$repo_root" || exit 0 + +# --- locate tools (PATH first, then a Homebrew LLVM install) ----------------- +find_tool() { + local name="$1" p + if command -v "$name" >/dev/null 2>&1; then command -v "$name"; return 0; fi + for p in /opt/homebrew/opt/llvm/bin /usr/local/opt/llvm/bin; do + [ -x "$p/$name" ] && { echo "$p/$name"; return 0; } + done + return 1 +} + +CLANG_FORMAT="$(find_tool clang-format || true)" +CLANG_TIDY="$(find_tool clang-tidy || true)" + +# On macOS, clang-tidy (Homebrew LLVM) must be pointed at the SDK or it cannot +# find the C++ standard library headers when reading an AppleClang compile DB. +TIDY_EXTRA=() +if [ "$(uname -s)" = "Darwin" ] && command -v xcrun >/dev/null 2>&1; then + _sdk="$(xcrun --show-sdk-path 2>/dev/null || true)" + [ -n "$_sdk" ] && TIDY_EXTRA=(--extra-arg=-isysroot --extra-arg="$_sdk") +fi + +# Nothing to enforce with — don't block the turn over missing local tooling. +[ -z "$CLANG_FORMAT" ] && [ -z "$CLANG_TIDY" ] && exit 0 + +# --- collect changed C/C++ files (modified, staged, untracked) --------------- +mapfile -t changed < <( + { + git diff --name-only HEAD 2>/dev/null + git diff --name-only --cached 2>/dev/null + git ls-files --others --exclude-standard 2>/dev/null + } | sort -u +) + +files=() +for f in "${changed[@]}"; do + [ -f "$f" ] || continue + case "$f" in + build/*|build-*/*|build_*/*) continue ;; + esac + case "$f" in + include/*|tests/*|examples/*|bench/*) ;; + *) continue ;; + esac + case "$f" in + *.hpp|*.cpp|*.h|*.hh|*.cc|*.cxx|*.hxx) files+=("$f") ;; + esac +done + +[ "${#files[@]}" -eq 0 ] && exit 0 + +# --- find a compile database for clang-tidy ---------------------------------- +compile_db_dir="" +for d in build-tidy build build-example build_bench; do + [ -f "$d/compile_commands.json" ] && { compile_db_dir="$d"; break; } +done + +report="" + +# --- clang-format: report any file that is not already formatted ------------- +if [ -n "$CLANG_FORMAT" ]; then + unformatted=() + for f in "${files[@]}"; do + if ! "$CLANG_FORMAT" --dry-run --Werror "$f" >/dev/null 2>&1; then + unformatted+=("$f") + fi + done + if [ "${#unformatted[@]}" -gt 0 ]; then + report+="clang-format: the following files are not formatted. Run \`clang-format -i \` (or fix manually):"$'\n' + for f in "${unformatted[@]}"; do report+=" - $f"$'\n'; done + report+=$'\n' + fi +fi + +# --- clang-tidy: lint only translation units present in the compile DB -------- +# Only enforce when the project ships a tuned .clang-tidy AND a compile database +# exists. Linting a file that is not in the DB makes clang-tidy fall back to a +# guessed command line; with missing external headers the parse breaks and the +# broken AST yields garbage findings. So we lint strictly the TUs the DB knows. +# Headers are covered transitively (HeaderFilterRegex): if a header changed we +# lint every DB TU, since we cannot tell which one exercises it. +if [ -n "$CLANG_TIDY" ] && [ -f "$repo_root/.clang-tidy" ] && [ -n "$compile_db_dir" ]; then + mapfile -t db_files < <( + python3 -c "import json,sys,os +for e in json.load(open(sys.argv[1])): + print(os.path.realpath(e['file']))" "$compile_db_dir/compile_commands.json" 2>/dev/null | sort -u + ) + + header_changed=0 + declare -A want=() + for f in "${files[@]}"; do + case "$f" in include/*.hpp|include/*.h|include/*/*.hpp|include/*/*.h) header_changed=1 ;; esac + abs="$(cd "$repo_root" && python3 -c "import os,sys;print(os.path.realpath(sys.argv[1]))" "$f")" + for t in "${db_files[@]}"; do [ "$t" = "$abs" ] && want["$t"]=1; done + done + if [ "$header_changed" -eq 1 ]; then + for t in "${db_files[@]}"; do want["$t"]=1; done + fi + + tidy_out="" + for f in "${!want[@]}"; do + out="$("$CLANG_TIDY" -p "$compile_db_dir" "${TIDY_EXTRA[@]}" "$f" 2>/dev/null)" + if printf '%s' "$out" | grep -Eq 'warning:|error:'; then + tidy_out+="### ${f#"$repo_root"/}"$'\n'"$out"$'\n\n' + fi + done + if [ -n "$tidy_out" ]; then + report+="clang-tidy reported issues:"$'\n'"$tidy_out" + fi +fi + +[ -z "$report" ] && exit 0 + +# Keep the fed-back context bounded. +report="$(printf '%s' "$report" | head -c 6000)" + +reason="Lint is not clean. Fix these before finishing (clang-format / clang-tidy, see .clang-format and .clang-tidy):"$'\n\n'"$report" + +# Emit the Stop-hook block decision as JSON. +python3 - "$reason" <<'PY' 2>/dev/null || printf '{"decision":"block","reason":%s}\n' "\"lint not clean; run clang-format/clang-tidy on changed files\"" +import json, sys +print(json.dumps({"decision": "block", "reason": sys.argv[1]})) +PY +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..3c6aa5d --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/lint-check.sh\"", + "timeout": 120, + "statusMessage": "Checking clang-format / clang-tidy on changed files" + } + ] + } + ] + } +} diff --git a/.gitignore b/.gitignore index 1d4a7ac..e7354ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build/ build_bench/ +build-*/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..165f8e8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +# Pre-commit hooks for prometheus-cpp. +# +# pip install pre-commit # or: brew install pre-commit +# pre-commit install # enable on every commit +# pre-commit run --all-files # run across the whole tree +# +# clang-format runs from a pinned, self-contained mirror (no system install +# needed). clang-tidy runs the system binary against the CMake compile +# database; it is skipped gracefully if the project hasn't been configured +# (see scripts/run-clang-tidy-precommit.sh). + +minimum_pre_commit_version: "3.0.0" + +repos: + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v22.1.8 + hooks: + - id: clang-format + files: ^(include|tests|examples|bench)/.*\.(c|cc|cpp|cxx|h|hh|hpp|hxx)$ + + - repo: local + hooks: + - id: clang-tidy + name: clang-tidy + language: system + entry: bash scripts/run-clang-tidy-precommit.sh + # Only translation units; headers are covered transitively via the + # .clang-tidy HeaderFilterRegex when their including TU is checked. + files: ^(tests|examples|bench)/.*\.(c|cc|cpp|cxx)$ + require_serial: true diff --git a/scripts/run-clang-tidy-precommit.sh b/scripts/run-clang-tidy-precommit.sh new file mode 100755 index 0000000..8e971bf --- /dev/null +++ b/scripts/run-clang-tidy-precommit.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# +# pre-commit wrapper for clang-tidy. +# +# clang-tidy needs a CMake compile database. Generate one first with: +# cmake -B build -DCMAKE_BUILD_TYPE=Debug -DPROMETHEUS_BUILD_TESTS=ON \ +# -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +# +# If no compile database is found this hook prints a notice and passes, so +# commits are never blocked solely because the project hasn't been configured. +# Receives the staged translation-unit paths as arguments. + +set -uo pipefail + +repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "$repo_root" || exit 0 + +find_tool() { + local name="$1" p + if command -v "$name" >/dev/null 2>&1; then command -v "$name"; return 0; fi + for p in /opt/homebrew/opt/llvm/bin /usr/local/opt/llvm/bin; do + [ -x "$p/$name" ] && { echo "$p/$name"; return 0; } + done + return 1 +} + +CLANG_TIDY="$(find_tool clang-tidy || true)" +if [ -z "$CLANG_TIDY" ]; then + echo "clang-tidy not found; skipping (install LLVM to enable)." >&2 + exit 0 +fi + +db_dir="" +for d in build build-tidy build-debug; do + [ -f "$d/compile_commands.json" ] && { db_dir="$d"; break; } +done +if [ -z "$db_dir" ]; then + echo "No compile_commands.json found; skipping clang-tidy." >&2 + echo "Run: cmake -B build -DPROMETHEUS_BUILD_TESTS=ON -DCMAKE_EXPORT_COMPILE_COMMANDS=ON" >&2 + exit 0 +fi + +# macOS: point clang-tidy at the SDK so it finds the C++ standard library +# headers when reading an AppleClang compile database. +extra=() +if [ "$(uname -s)" = "Darwin" ] && command -v xcrun >/dev/null 2>&1; then + sdk="$(xcrun --show-sdk-path 2>/dev/null || true)" + [ -n "$sdk" ] && extra=(--extra-arg=-isysroot --extra-arg="$sdk") +fi + +# Only lint TUs the compile DB actually knows about. A file absent from the DB +# makes clang-tidy guess a command line; with missing external headers the parse +# breaks and the broken AST emits spurious findings. Skip those quietly. +mapfile -t db_files < <( + python3 -c "import json,sys,os +for e in json.load(open(sys.argv[1])): + print(os.path.realpath(e['file']))" "$db_dir/compile_commands.json" 2>/dev/null | sort -u +) +in_db() { local t a; a="$(python3 -c "import os,sys;print(os.path.realpath(sys.argv[1]))" "$1")"; for t in "${db_files[@]}"; do [ "$t" = "$a" ] && return 0; done; return 1; } + +status=0 +for f in "$@"; do + [ -f "$f" ] || continue + in_db "$f" || { echo "skipping $f (not in compile database)" >&2; continue; } + if ! "$CLANG_TIDY" -p "$db_dir" "${extra[@]}" "$f"; then + status=1 + fi +done +exit "$status" From dd2e4cce7a9b75b751f5f5df3ab09713eff7a376 Mon Sep 17 00:00:00 2001 From: Stig Bakken Date: Mon, 29 Jun 2026 15:52:47 +0200 Subject: [PATCH 3/6] style: apply clang-format across the codebase Mechanical reformat to the new .clang-format. No behavioural changes. label_def.hpp gains clang-format on/off guards around the preprocessor metaprogramming, whose hand-alignment clang-format would otherwise mangle. --- bench/bench.cpp | 118 +++++++++---------- bench/bench_jupp0r.cpp | 43 +++---- bench/bench_stigsb.cpp | 45 ++++--- examples/basic_usage.cpp | 46 ++++---- include/prometheus/detail/assert.hpp | 20 ++-- include/prometheus/detail/check_names.hpp | 39 ++++-- include/prometheus/detail/label_key.hpp | 7 +- include/prometheus/detail/metric_store.hpp | 7 +- include/prometheus/gauge.hpp | 4 +- include/prometheus/histogram.hpp | 12 +- include/prometheus/label_def.hpp | 8 +- include/prometheus/label_mask.hpp | 3 +- include/prometheus/metric_family.hpp | 59 ++++++---- include/prometheus/metric_family_builder.hpp | 27 ++--- include/prometheus/registry.hpp | 39 +++--- include/prometheus/text_serializer.hpp | 31 +++-- include/prometheus/unit.hpp | 53 +++++---- tests/test_check_names.cpp | 2 +- tests/test_concurrency.cpp | 27 ++--- tests/test_counter.cpp | 10 +- tests/test_gauge.cpp | 21 +++- tests/test_histogram.cpp | 20 ++-- tests/test_label_def.cpp | 37 +++--- tests/test_metric_family.cpp | 84 ++++++------- tests/test_registry.cpp | 55 ++++----- tests/test_text_serializer.cpp | 112 ++++++++---------- tests/test_unit.cpp | 69 ++++++----- 27 files changed, 492 insertions(+), 506 deletions(-) diff --git a/bench/bench.cpp b/bench/bench.cpp index 1eebd97..a7375e5 100644 --- a/bench/bench.cpp +++ b/bench/bench.cpp @@ -4,10 +4,9 @@ // Define labels for benchmarks PROMETHEUS_DEFINE_LABELS(BenchLabels, - (service, std::string_view), - (method, std::string_view), - (code, uint32_t) -); + (service, std::string_view), + (method, std::string_view), + (code, uint32_t)); // ============================================================ // Counter/Gauge increment with EXISTING labels (hot path) @@ -17,9 +16,9 @@ PROMETHEUS_DEFINE_LABELS(BenchLabels, static void BM_CounterInc(benchmark::State& state) { prometheus::Registry reg; auto& fam = reg.counter("bench_counter", "bench") - .required(BenchLabels::Key::service, BenchLabels::Key::method) - .build(); - auto& c = fam.get({.service = "api", .method = "GET"}); + .required(BenchLabels::Key::service, BenchLabels::Key::method) + .build(); + auto& c = fam.get({.service = "api", .method = "GET"}); for (auto _ : state) { c.inc(); } @@ -30,15 +29,15 @@ BENCHMARK(BM_CounterInc); // Multi-threaded counter increment — same metric handle, N threads static void BM_CounterInc_MT(benchmark::State& state) { // Use a static registry so all threads share the same metric - static prometheus::Registry* reg = nullptr; + static prometheus::Registry* reg = nullptr; static prometheus::MetricFamily* fam = nullptr; - static prometheus::Counter* ctr = nullptr; + static prometheus::Counter* ctr = nullptr; if (state.thread_index() == 0) { reg = new prometheus::Registry(); fam = ®->counter("bench_counter_mt", "bench") - .required(BenchLabels::Key::service, BenchLabels::Key::method) - .build(); + .required(BenchLabels::Key::service, BenchLabels::Key::method) + .build(); ctr = &fam->get({.service = "api", .method = "GET"}); } @@ -57,10 +56,10 @@ BENCHMARK(BM_CounterInc_MT)->ThreadRange(1, 16)->UseRealTime(); // Counter increment with different delta sizes static void BM_CounterIncDelta(benchmark::State& state) { prometheus::Registry reg; - auto& fam = reg.counter("bench_counter_delta", "bench") - .required(BenchLabels::Key::service) - .build(); - auto& c = fam.get({.service = "api"}); + auto& fam = reg.counter("bench_counter_delta", "bench") + .required(BenchLabels::Key::service) + .build(); + auto& c = fam.get({.service = "api"}); const int64_t delta = state.range(0); for (auto _ : state) { c.inc(delta); @@ -72,10 +71,9 @@ BENCHMARK(BM_CounterIncDelta)->Arg(1)->Arg(1000)->Arg(1000000); // Gauge set static void BM_GaugeSet(benchmark::State& state) { prometheus::Registry reg; - auto& fam = reg.gauge("bench_gauge", "bench") - .required(BenchLabels::Key::service) - .build(); - auto& g = fam.get({.service = "api"}); + auto& fam = + reg.gauge("bench_gauge", "bench").required(BenchLabels::Key::service).build(); + auto& g = fam.get({.service = "api"}); int64_t v = 0; for (auto _ : state) { g.set(v++); @@ -87,14 +85,14 @@ BENCHMARK(BM_GaugeSet); // Gauge inc (multi-threaded) static void BM_GaugeInc_MT(benchmark::State& state) { static prometheus::Registry* reg = nullptr; - static prometheus::Gauge* gauge = nullptr; + static prometheus::Gauge* gauge = nullptr; if (state.thread_index() == 0) { - reg = new prometheus::Registry(); + reg = new prometheus::Registry(); auto& fam = reg->gauge("bench_gauge_mt", "bench") - .required(BenchLabels::Key::service) - .build(); - gauge = &fam.get({.service = "api"}); + .required(BenchLabels::Key::service) + .build(); + gauge = &fam.get({.service = "api"}); } for (auto _ : state) { @@ -117,10 +115,10 @@ BENCHMARK(BM_GaugeInc_MT)->ThreadRange(1, 16)->UseRealTime(); static void BM_HistogramObserve_SmallBuckets(benchmark::State& state) { prometheus::Registry reg; auto& fam = reg.histogram("bench_hist_small", "bench") - .required(BenchLabels::Key::service) - .buckets(100, 4) - .build(); - auto& h = fam.get({.service = "api"}); + .required(BenchLabels::Key::service) + .buckets(100, 4) + .build(); + auto& h = fam.get({.service = "api"}); int64_t v = 0; for (auto _ : state) { h.observe(v % 500); @@ -134,10 +132,10 @@ BENCHMARK(BM_HistogramObserve_SmallBuckets); static void BM_HistogramObserve_LargeBuckets(benchmark::State& state) { prometheus::Registry reg; auto& fam = reg.histogram("bench_hist_large", "bench") - .required(BenchLabels::Key::service) - .buckets(1, 20) // 1, 2, 4, 8, ... 2^18, +Inf - .build(); - auto& h = fam.get({.service = "api"}); + .required(BenchLabels::Key::service) + .buckets(1, 20) // 1, 2, 4, 8, ... 2^18, +Inf + .build(); + auto& h = fam.get({.service = "api"}); int64_t v = 0; for (auto _ : state) { h.observe(v % 300000); @@ -149,16 +147,16 @@ BENCHMARK(BM_HistogramObserve_LargeBuckets); // Histogram observe multi-threaded (small buckets) static void BM_HistogramObserve_SmallBuckets_MT(benchmark::State& state) { - static prometheus::Registry* reg = nullptr; + static prometheus::Registry* reg = nullptr; static prometheus::Histogram* hist = nullptr; if (state.thread_index() == 0) { - reg = new prometheus::Registry(); + reg = new prometheus::Registry(); auto& fam = reg->histogram("bench_hist_small_mt", "bench") - .required(BenchLabels::Key::service) - .buckets(100, 4) - .build(); - hist = &fam.get({.service = "api"}); + .required(BenchLabels::Key::service) + .buckets(100, 4) + .build(); + hist = &fam.get({.service = "api"}); } int64_t v = state.thread_index(); @@ -177,16 +175,16 @@ BENCHMARK(BM_HistogramObserve_SmallBuckets_MT)->ThreadRange(1, 16)->UseRealTime( // Histogram observe multi-threaded (large buckets) static void BM_HistogramObserve_LargeBuckets_MT(benchmark::State& state) { - static prometheus::Registry* reg = nullptr; + static prometheus::Registry* reg = nullptr; static prometheus::Histogram* hist = nullptr; if (state.thread_index() == 0) { - reg = new prometheus::Registry(); + reg = new prometheus::Registry(); auto& fam = reg->histogram("bench_hist_large_mt", "bench") - .required(BenchLabels::Key::service) - .buckets(1, 20) - .build(); - hist = &fam.get({.service = "api"}); + .required(BenchLabels::Key::service) + .buckets(1, 20) + .build(); + hist = &fam.get({.service = "api"}); } int64_t v = state.thread_index(); @@ -210,12 +208,12 @@ BENCHMARK(BM_HistogramObserve_LargeBuckets_MT)->ThreadRange(1, 16)->UseRealTime( static void BM_MetricFamilyGet_NewLabels(benchmark::State& state) { prometheus::Registry reg; auto& fam = reg.counter("bench_new_labels", "bench") - .required(BenchLabels::Key::service, BenchLabels::Key::method) - .build(); + .required(BenchLabels::Key::service, BenchLabels::Key::method) + .build(); int i = 0; for (auto _ : state) { - auto svc = "svc_" + std::to_string(i); + auto svc = "svc_" + std::to_string(i); auto method = "m_" + std::to_string(i); fam.get({.service = svc, .method = method}); i++; @@ -226,19 +224,19 @@ BENCHMARK(BM_MetricFamilyGet_NewLabels); // Get with new labels, multi-threaded static void BM_MetricFamilyGet_NewLabels_MT(benchmark::State& state) { - static prometheus::Registry* reg = nullptr; + static prometheus::Registry* reg = nullptr; static prometheus::MetricFamily* fam = nullptr; if (state.thread_index() == 0) { reg = new prometheus::Registry(); fam = ®->counter("bench_new_labels_mt", "bench") - .required(BenchLabels::Key::service, BenchLabels::Key::method) - .build(); + .required(BenchLabels::Key::service, BenchLabels::Key::method) + .build(); } int i = state.thread_index() * 1000000; for (auto _ : state) { - auto svc = "svc_" + std::to_string(i); + auto svc = "svc_" + std::to_string(i); auto method = "m_" + std::to_string(i); fam->get({.service = svc, .method = method}); i++; @@ -259,8 +257,8 @@ BENCHMARK(BM_MetricFamilyGet_NewLabels_MT)->ThreadRange(1, 16)->UseRealTime(); static void BM_MetricFamilyGet_ExistingLabels(benchmark::State& state) { prometheus::Registry reg; auto& fam = reg.counter("bench_existing_labels", "bench") - .required(BenchLabels::Key::service, BenchLabels::Key::method) - .build(); + .required(BenchLabels::Key::service, BenchLabels::Key::method) + .build(); // Pre-create the metric fam.get({.service = "api", .method = "GET"}); @@ -274,13 +272,14 @@ BENCHMARK(BM_MetricFamilyGet_ExistingLabels); // Serialization benchmark static void BM_Serialize(benchmark::State& state) { prometheus::Registry reg; - auto& counters = reg.counter("bench_ser_counter", "bench") - .required(BenchLabels::Key::service, BenchLabels::Key::method, BenchLabels::Key::code) - .build(); + auto& counters = + reg.counter("bench_ser_counter", "bench") + .required(BenchLabels::Key::service, BenchLabels::Key::method, BenchLabels::Key::code) + .build(); // Create 100 label combinations for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { - auto svc = "svc_" + std::to_string(i); + auto svc = "svc_" + std::to_string(i); auto method = "m_" + std::to_string(j); counters.get({.service = svc, .method = method, .code = 200u}).inc(1000); } @@ -322,10 +321,11 @@ static void BM_LocalHistogramBatchObserve_MT(benchmark::State& state) { // Google Benchmark only synchronizes threads at the start of the for-loop. // Constructing it before the loop races with thread 0's setup of `hist`. prometheus::LocalHistogram* local = nullptr; - constexpr int kBatch = 1000; + constexpr int kBatch = 1000; for (auto _ : state) { - if (!local) local = new prometheus::LocalHistogram(*hist); + if (!local) + local = new prometheus::LocalHistogram(*hist); for (int i = 0; i < kBatch; ++i) local->observe(i % 500); local->merge_into(*hist); diff --git a/bench/bench_jupp0r.cpp b/bench/bench_jupp0r.cpp index c184092..7a9d970 100644 --- a/bench/bench_jupp0r.cpp +++ b/bench/bench_jupp0r.cpp @@ -11,10 +11,8 @@ // 1. Single-threaded counter increment (hot path, handle pre-obtained) static void BM_Jupp0r_CounterInc(benchmark::State& state) { auto registry = std::make_shared(); - auto& family = prometheus::BuildCounter() - .Name("jupp0r_counter") - .Help("bench") - .Register(*registry); + auto& family = + prometheus::BuildCounter().Name("jupp0r_counter").Help("bench").Register(*registry); auto& counter = family.Add({{"service", "api"}, {"method", "GET"}}); for (auto _ : state) { @@ -31,10 +29,8 @@ static void BM_Jupp0r_CounterInc_MT(benchmark::State& state) { if (state.thread_index() == 0) { registry = std::make_shared(); - auto& family = prometheus::BuildCounter() - .Name("jupp0r_counter_mt") - .Help("bench") - .Register(*registry); + auto& family = + prometheus::BuildCounter().Name("jupp0r_counter_mt").Help("bench").Register(*registry); ctr = &family.Add({{"service", "api"}, {"method", "GET"}}); } @@ -53,13 +49,12 @@ BENCHMARK(BM_Jupp0r_CounterInc_MT)->ThreadRange(1, 8)->UseRealTime(); // 3. Single-threaded histogram observe static void BM_Jupp0r_HistogramObserve(benchmark::State& state) { auto registry = std::make_shared(); - auto& family = prometheus::BuildHistogram() - .Name("jupp0r_hist") - .Help("bench") - .Register(*registry); + auto& family = + prometheus::BuildHistogram().Name("jupp0r_hist").Help("bench").Register(*registry); // Equivalent bucket boundaries: 1, 2, 4, 8, ..., 512 (10 buckets) - auto& hist = family.Add({{"service", "api"}}, - prometheus::Histogram::BucketBoundaries{1, 2, 4, 8, 16, 32, 64, 128, 256, 512}); + auto& hist = + family.Add({{"service", "api"}}, + prometheus::Histogram::BucketBoundaries{1, 2, 4, 8, 16, 32, 64, 128, 256, 512}); double v = 0; for (auto _ : state) { @@ -73,10 +68,8 @@ BENCHMARK(BM_Jupp0r_HistogramObserve); // 4. Add(labels) + Increment() combined (map lookup + atomic) static void BM_Jupp0r_AddAndInc(benchmark::State& state) { auto registry = std::make_shared(); - auto& family = prometheus::BuildCounter() - .Name("jupp0r_add_inc") - .Help("bench") - .Register(*registry); + auto& family = + prometheus::BuildCounter().Name("jupp0r_add_inc").Help("bench").Register(*registry); // Pre-create family.Add({{"service", "api"}, {"method", "GET"}}); @@ -90,20 +83,18 @@ BENCHMARK(BM_Jupp0r_AddAndInc); // 5. Serialize (1 counter family, 4 label combos) static void BM_Jupp0r_Serialize(benchmark::State& state) { auto registry = std::make_shared(); - auto& family = prometheus::BuildCounter() - .Name("jupp0r_ser_counter") - .Help("bench") - .Register(*registry); - family.Add({{"service", "web"}, {"method", "GET"}, {"code", "200"}}).Increment(100); + auto& family = + prometheus::BuildCounter().Name("jupp0r_ser_counter").Help("bench").Register(*registry); + family.Add({{"service", "web"}, {"method", "GET"}, {"code", "200"}}).Increment(100); family.Add({{"service", "web"}, {"method", "POST"}, {"code", "200"}}).Increment(50); - family.Add({{"service", "api"}, {"method", "GET"}, {"code", "200"}}).Increment(200); - family.Add({{"service", "api"}, {"method", "GET"}, {"code", "500"}}).Increment(3); + family.Add({{"service", "api"}, {"method", "GET"}, {"code", "200"}}).Increment(200); + family.Add({{"service", "api"}, {"method", "GET"}, {"code", "500"}}).Increment(3); prometheus::TextSerializer serializer; for (auto _ : state) { auto collected = registry->Collect(); - auto out = serializer.Serialize(collected); + auto out = serializer.Serialize(collected); benchmark::DoNotOptimize(out); } state.SetItemsProcessed(state.iterations()); diff --git a/bench/bench_stigsb.cpp b/bench/bench_stigsb.cpp index 1d32335..c3df0ea 100644 --- a/bench/bench_stigsb.cpp +++ b/bench/bench_stigsb.cpp @@ -4,18 +4,17 @@ // Define labels for comparison benchmarks PROMETHEUS_DEFINE_LABELS(CmpLabels, - (service, std::string), - (method, std::string), - (code, uint32_t) -); + (service, std::string), + (method, std::string), + (code, uint32_t)); // 1. Single-threaded counter increment (hot path, handle pre-obtained) static void BM_Stigsb_CounterInc(benchmark::State& state) { prometheus::Registry reg; auto& fam = reg.counter("stigsb_counter", "bench") - .required(CmpLabels::Key::service, CmpLabels::Key::method) - .build(); - auto& c = fam.get({.service = "api", .method = "GET"}); + .required(CmpLabels::Key::service, CmpLabels::Key::method) + .build(); + auto& c = fam.get({.service = "api", .method = "GET"}); for (auto _ : state) { c.inc(); } @@ -26,14 +25,14 @@ BENCHMARK(BM_Stigsb_CounterInc); // 2. Multi-threaded counter increment static void BM_Stigsb_CounterInc_MT(benchmark::State& state) { static prometheus::Registry* reg = nullptr; - static prometheus::Counter* ctr = nullptr; + static prometheus::Counter* ctr = nullptr; if (state.thread_index() == 0) { - reg = new prometheus::Registry(); + reg = new prometheus::Registry(); auto& fam = reg->counter("stigsb_counter_mt", "bench") - .required(CmpLabels::Key::service, CmpLabels::Key::method) - .build(); - ctr = &fam.get({.service = "api", .method = "GET"}); + .required(CmpLabels::Key::service, CmpLabels::Key::method) + .build(); + ctr = &fam.get({.service = "api", .method = "GET"}); } for (auto _ : state) { @@ -52,10 +51,10 @@ BENCHMARK(BM_Stigsb_CounterInc_MT)->ThreadRange(1, 8)->UseRealTime(); static void BM_Stigsb_HistogramObserve(benchmark::State& state) { prometheus::Registry reg; auto& fam = reg.histogram("stigsb_hist", "bench") - .required(CmpLabels::Key::service) - .buckets(1, 10) // 1, 2, 4, ..., 512, +Inf - .build(); - auto& h = fam.get({.service = "api"}); + .required(CmpLabels::Key::service) + .buckets(1, 10) // 1, 2, 4, ..., 512, +Inf + .build(); + auto& h = fam.get({.service = "api"}); int64_t v = 0; for (auto _ : state) { h.observe(v % 600); @@ -69,8 +68,8 @@ BENCHMARK(BM_Stigsb_HistogramObserve); static void BM_Stigsb_GetAndInc(benchmark::State& state) { prometheus::Registry reg; auto& fam = reg.counter("stigsb_get_inc", "bench") - .required(CmpLabels::Key::service, CmpLabels::Key::method) - .build(); + .required(CmpLabels::Key::service, CmpLabels::Key::method) + .build(); // Pre-create fam.get({.service = "api", .method = "GET"}); @@ -85,12 +84,12 @@ BENCHMARK(BM_Stigsb_GetAndInc); static void BM_Stigsb_Serialize(benchmark::State& state) { prometheus::Registry reg; auto& fam = reg.counter("stigsb_ser_counter", "bench") - .required(CmpLabels::Key::service, CmpLabels::Key::method, CmpLabels::Key::code) - .build(); - fam.get({.service = "web", .method = "GET", .code = 200u}).inc(100); + .required(CmpLabels::Key::service, CmpLabels::Key::method, CmpLabels::Key::code) + .build(); + fam.get({.service = "web", .method = "GET", .code = 200u}).inc(100); fam.get({.service = "web", .method = "POST", .code = 200u}).inc(50); - fam.get({.service = "api", .method = "GET", .code = 200u}).inc(200); - fam.get({.service = "api", .method = "GET", .code = 500u}).inc(3); + fam.get({.service = "api", .method = "GET", .code = 200u}).inc(200); + fam.get({.service = "api", .method = "GET", .code = 500u}).inc(3); for (auto _ : state) { auto out = reg.serialize(); diff --git a/examples/basic_usage.cpp b/examples/basic_usage.cpp index ffd796a..628d8e6 100644 --- a/examples/basic_usage.cpp +++ b/examples/basic_usage.cpp @@ -3,40 +3,36 @@ // 1. Define your application's labels PROMETHEUS_DEFINE_LABELS(AppLabels, - (service, std::string_view), - (method, std::string_view), - (status_code, uint32_t) -); + (service, std::string_view), + (method, std::string_view), + (status_code, uint32_t)); int main() { // 2. Create a registry prometheus::Registry registry; // 3. Register metric families - auto& requests = registry.counter( - "http_requests_total", "Total HTTP requests") - .required(AppLabels::Key::service, - AppLabels::Key::method, - AppLabels::Key::status_code) - .const_label("env", "production") - .build(); - - auto& latency = registry.histogram( - "http_request_duration_seconds", "Request latency") - .required(AppLabels::Key::service, AppLabels::Key::method) - .unit(prometheus::units::microseconds) - .buckets(/*min=*/100, /*count=*/10) // 100, 200, 400, 800 ... µs → scaled to seconds - .build(); - - auto& active = registry.gauge( - "active_connections", "Currently open connections") - .required(AppLabels::Key::service) - .build(); + auto& requests = + registry.counter("http_requests_total", "Total HTTP requests") + .required(AppLabels::Key::service, AppLabels::Key::method, AppLabels::Key::status_code) + .const_label("env", "production") + .build(); + + auto& latency = + registry.histogram("http_request_duration_seconds", "Request latency") + .required(AppLabels::Key::service, AppLabels::Key::method) + .unit(prometheus::units::microseconds) + .buckets(/*min=*/100, /*count=*/10) // 100, 200, 400, 800 ... µs → scaled to seconds + .build(); + + auto& active = registry.gauge("active_connections", "Currently open connections") + .required(AppLabels::Key::service) + .build(); // 4. Update metrics (the hot path — single atomic op each) - requests.get({.service = "api", .method = "GET", .status_code = 200u}).inc(1500); + requests.get({.service = "api", .method = "GET", .status_code = 200u}).inc(1500); requests.get({.service = "api", .method = "POST", .status_code = 201u}).inc(342); - requests.get({.service = "api", .method = "GET", .status_code = 404u}).inc(12); + requests.get({.service = "api", .method = "GET", .status_code = 404u}).inc(12); latency.get({.service = "api", .method = "GET"}).observe(150); latency.get({.service = "api", .method = "GET"}).observe(250); diff --git a/include/prometheus/detail/assert.hpp b/include/prometheus/detail/assert.hpp index 8faca27..e07349b 100644 --- a/include/prometheus/detail/assert.hpp +++ b/include/prometheus/detail/assert.hpp @@ -4,15 +4,17 @@ #include #include -#define PROMETHEUS_ASSERT(cond) \ - do { \ - if (!(cond)) { \ - std::fprintf(stderr, \ - "prometheus assertion failed: %s at %s:%d\n", \ - #cond, __FILE__, __LINE__); \ - std::abort(); \ - } \ +#define PROMETHEUS_ASSERT(cond) \ + do { \ + if (!(cond)) { \ + std::fprintf( \ + stderr, "prometheus assertion failed: %s at %s:%d\n", #cond, __FILE__, __LINE__); \ + std::abort(); \ + } \ } while (false) #else -#define PROMETHEUS_ASSERT(cond) do { (void)(cond); } while (false) +#define PROMETHEUS_ASSERT(cond) \ + do { \ + (void)(cond); \ + } while (false) #endif diff --git a/include/prometheus/detail/check_names.hpp b/include/prometheus/detail/check_names.hpp index 33e6b34..eea9393 100644 --- a/include/prometheus/detail/check_names.hpp +++ b/include/prometheus/detail/check_names.hpp @@ -5,24 +5,41 @@ namespace prometheus::detail { // Metric names: [a-zA-Z_:][a-zA-Z0-9_:]* constexpr bool check_metric_name(std::string_view name) noexcept { - if (name.empty()) return false; - auto first_ok = [](char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == ':'; }; - auto rest_ok = [](char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == ':'; }; - if (!first_ok(name[0])) return false; + if (name.empty()) + return false; + auto first_ok = [](char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == ':'; + }; + auto rest_ok = [](char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || + c == '_' || c == ':'; + }; + if (!first_ok(name[0])) + return false; for (std::size_t i = 1; i < name.size(); ++i) - if (!rest_ok(name[i])) return false; + if (!rest_ok(name[i])) + return false; return true; } // Label names: [a-zA-Z_][a-zA-Z0-9_]*, must not start with __ constexpr bool check_label_name(std::string_view name) noexcept { - if (name.empty()) return false; - if (name.size() >= 2 && name[0] == '_' && name[1] == '_') return false; - auto first_ok = [](char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; }; - auto rest_ok = [](char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; }; - if (!first_ok(name[0])) return false; + if (name.empty()) + return false; + if (name.size() >= 2 && name[0] == '_' && name[1] == '_') + return false; + auto first_ok = [](char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; + }; + auto rest_ok = [](char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || + c == '_'; + }; + if (!first_ok(name[0])) + return false; for (std::size_t i = 1; i < name.size(); ++i) - if (!rest_ok(name[i])) return false; + if (!rest_ok(name[i])) + return false; return true; } diff --git a/include/prometheus/detail/label_key.hpp b/include/prometheus/detail/label_key.hpp index 336c6a0..1262260 100644 --- a/include/prometheus/detail/label_key.hpp +++ b/include/prometheus/detail/label_key.hpp @@ -13,8 +13,8 @@ constexpr std::string escape_label_value(std::string_view sv) { switch (c) { case '\\': out += "\\\\"; break; case '"': out += "\\\""; break; - case '\n': out += "\\n"; break; - default: out += c; break; + case '\n': out += "\\n"; break; + default: out += c; break; } } return out; @@ -28,7 +28,8 @@ std::string make_label_display(const typename LabelTraits::LabelSet& ls, for (auto k : LabelTraits::all_keys()) { auto bit = static_cast(k); if ((allowed_mask & bit) && (LabelTraits::populated_mask(ls) & bit)) { - if (!result.empty()) result += ','; + if (!result.empty()) + result += ','; result += LabelTraits::key_name(k); result += "=\""; result += escape_label_value(LabelTraits::format_value(k, ls)); diff --git a/include/prometheus/detail/metric_store.hpp b/include/prometheus/detail/metric_store.hpp index 89b5d4a..e460f8e 100644 --- a/include/prometheus/detail/metric_store.hpp +++ b/include/prometheus/detail/metric_store.hpp @@ -25,14 +25,13 @@ class MetricStore { // make_display – callable() -> std::string (only called on first insert) // factory – callable() -> unique_ptr template - MetricT& get_or_create(std::size_t hash, - DisplayFn&& make_display, - Factory&& factory) { + MetricT& get_or_create(std::size_t hash, DisplayFn&& make_display, Factory&& factory) { // Fast path: shared lock, hash-only lookup — zero allocation { std::shared_lock lock(mutex_); auto it = instances_.find(hash); - if (it != instances_.end()) return *it->second.metric; + if (it != instances_.end()) + return *it->second.metric; } // Slow path: exclusive lock + double-check + insert std::unique_lock lock(mutex_); diff --git a/include/prometheus/gauge.hpp b/include/prometheus/gauge.hpp index 2f5b5a0..a99f682 100644 --- a/include/prometheus/gauge.hpp +++ b/include/prometheus/gauge.hpp @@ -31,8 +31,8 @@ class alignas(detail::cache_line_size) Gauge { void set_to_current_time() noexcept { auto now = std::chrono::system_clock::now(); - auto epoch = std::chrono::duration_cast( - now.time_since_epoch()).count(); + auto epoch = + std::chrono::duration_cast(now.time_since_epoch()).count(); value_.store(static_cast(epoch), std::memory_order_relaxed); } diff --git a/include/prometheus/histogram.hpp b/include/prometheus/histogram.hpp index 1838e82..7b9e6a9 100644 --- a/include/prometheus/histogram.hpp +++ b/include/prometheus/histogram.hpp @@ -16,14 +16,13 @@ class Histogram { public: explicit Histogram(std::vector upper_bounds) : upper_bounds_(std::move(upper_bounds)) - , bucket_counts_(upper_bounds_.size()) - {} + , bucket_counts_(upper_bounds_.size()) {} // Hot path: binary search + 2 atomic ops. // Since the last bound is always INT64_MAX and int64_t <= INT64_MAX, // lower_bound always finds a valid bucket — no end() check needed. void observe(int64_t value) noexcept { - auto it = std::lower_bound(upper_bounds_.begin(), upper_bounds_.end(), value); + auto it = std::lower_bound(upper_bounds_.begin(), upper_bounds_.end(), value); auto idx = static_cast(it - upper_bounds_.begin()); bucket_counts_[idx].fetch_add(1, std::memory_order_relaxed); sum_.fetch_add(value, std::memory_order_relaxed); @@ -112,12 +111,11 @@ class LocalHistogram { explicit LocalHistogram(const Histogram& target) : upper_bounds_(target.bounds()) , counts_(target.num_buckets(), 0) - , sum_{0} - {} + , sum_{0} {} // Hot path: pure local writes, no atomics, no cache line bouncing void observe(int64_t value) noexcept { - auto it = std::lower_bound(upper_bounds_.begin(), upper_bounds_.end(), value); + auto it = std::lower_bound(upper_bounds_.begin(), upper_bounds_.end(), value); auto idx = static_cast(it - upper_bounds_.begin()); counts_[idx]++; sum_ += value; @@ -144,7 +142,7 @@ class LocalHistogram { } private: - const std::vector& upper_bounds_; // borrows from histogram + const std::vector& upper_bounds_; // borrows from histogram std::vector counts_; int64_t sum_; }; diff --git a/include/prometheus/label_def.hpp b/include/prometheus/label_def.hpp index 82d9cc0..daa10bb 100644 --- a/include/prometheus/label_def.hpp +++ b/include/prometheus/label_def.hpp @@ -33,7 +33,8 @@ inline void hash_mix_byte(std::size_t& h, uint8_t b) noexcept { } inline void hash_label_field(std::size_t& h, std::string_view v) noexcept { - for (char c : v) hash_mix_byte(h, static_cast(c)); + for (char c : v) + hash_mix_byte(h, static_cast(c)); } inline void hash_label_field(std::size_t& h, const std::string& v) noexcept { @@ -50,6 +51,10 @@ inline void hash_label_field(std::size_t& h, T v) noexcept { } // namespace prometheus::detail +// clang-format off +// The preprocessor metaprogramming below is hand-aligned for readability; +// clang-format would mangle the macro continuations, so it is left untouched. + // ── Preprocessor helpers ──────────────────────────────────────────────────── #define PROMETHEUS_PP_CAT(a, b) PROMETHEUS_PP_CAT_I(a, b) @@ -266,3 +271,4 @@ struct Name { \ return h; \ } \ } +// clang-format on diff --git a/include/prometheus/label_mask.hpp b/include/prometheus/label_mask.hpp index 1d160c0..d7882dd 100644 --- a/include/prometheus/label_mask.hpp +++ b/include/prometheus/label_mask.hpp @@ -10,8 +10,7 @@ using LabelMask = typename LabelTraits::Mask; template consteval LabelMask make_mask( - std::same_as auto... keys -) noexcept { + std::same_as auto... keys) noexcept { if constexpr (sizeof...(keys) == 0) { return LabelMask{0}; } else { diff --git a/include/prometheus/metric_family.hpp b/include/prometheus/metric_family.hpp index ae24c6f..b71ef2d 100644 --- a/include/prometheus/metric_family.hpp +++ b/include/prometheus/metric_family.hpp @@ -24,11 +24,11 @@ namespace prometheus { // Type-erased interface implemented by every MetricFamily. class Collectable { public: - virtual ~Collectable() = default; + virtual ~Collectable() = default; virtual void collect(TextSerializer& out) const = 0; - virtual std::string_view name() const noexcept = 0; - virtual std::string_view help() const noexcept = 0; - virtual MetricType type() const noexcept = 0; + virtual std::string_view name() const noexcept = 0; + virtual std::string_view help() const noexcept = 0; + virtual MetricType type() const noexcept = 0; }; // A named, typed group of metric instances sharing HELP/TYPE but differing @@ -40,7 +40,7 @@ class MetricFamily : public Collectable { std::string help, uint64_t required_mask, uint64_t optional_mask, - std::vector> const_labels, + std::vector> const_labels, double scale, std::function()> factory) : name_(std::move(name)) @@ -49,36 +49,39 @@ class MetricFamily : public Collectable { , optional_mask_(optional_mask) , const_labels_(std::move(const_labels)) , scale_(scale) - , factory_(std::move(factory)) - {} + , factory_(std::move(factory)) {} // Obtain (or create) the metric instance for the given label set. // In debug builds, asserts required labels are present and no // forbidden labels are supplied. MetricT& get(const typename LabelTraits::LabelSet& ls) { - const uint64_t allowed = required_mask_ | optional_mask_; - const uint64_t populated = LabelTraits::populated_mask(ls); + const uint64_t allowed = required_mask_ | optional_mask_; + const uint64_t populated = LabelTraits::populated_mask(ls); PROMETHEUS_ASSERT((populated & required_mask_) == required_mask_); PROMETHEUS_ASSERT((populated & ~allowed) == 0u); const auto hash = detail::make_label_hash(ls, allowed); return store_.get_or_create( - hash, - [&]{ return detail::make_label_display(ls, allowed); }, - factory_ - ); + hash, [&] { return detail::make_label_display(ls, allowed); }, factory_); } // --- Collectable interface --- - std::string_view name() const noexcept override { return name_; } - std::string_view help() const noexcept override { return help_; } + std::string_view name() const noexcept override { + return name_; + } + std::string_view help() const noexcept override { + return help_; + } MetricType type() const noexcept override { - if constexpr (std::is_same_v) return MetricType::Counter; - else if constexpr (std::is_same_v) return MetricType::Gauge; - else if constexpr (std::is_same_v) return MetricType::Histogram; + if constexpr (std::is_same_v) + return MetricType::Counter; + else if constexpr (std::is_same_v) + return MetricType::Gauge; + else if constexpr (std::is_same_v) + return MetricType::Histogram; else { static_assert(sizeof(MetricT) == 0, "Unknown metric type"); return MetricType::Counter; // unreachable @@ -91,7 +94,7 @@ class MetricFamily : public Collectable { if constexpr (std::is_same_v) { store_.for_each([&](const std::string& dyn, const MetricT& h) { const std::size_t n = h.num_buckets(); - int64_t cum = 0; + int64_t cum = 0; for (std::size_t i = 0; i < n; ++i) { cum += h.bucket_count(i); std::string le_val; @@ -101,13 +104,17 @@ class MetricFamily : public Collectable { le_val = TextSerializer::format_double( static_cast(h.upper_bound(i)) * scale_); } - ser.write_sample(name_ + "_bucket", dyn, const_labels_, - static_cast(cum), "le", le_val); + ser.write_sample(name_ + "_bucket", + dyn, + const_labels_, + static_cast(cum), + "le", + le_val); } - ser.write_sample(name_ + "_sum", dyn, const_labels_, - static_cast(h.sum()) * scale_); - ser.write_sample(name_ + "_count", dyn, const_labels_, - static_cast(h.total_count())); + ser.write_sample( + name_ + "_sum", dyn, const_labels_, static_cast(h.sum()) * scale_); + ser.write_sample( + name_ + "_count", dyn, const_labels_, static_cast(h.total_count())); }); } else { store_.for_each([&](const std::string& dyn, const MetricT& m) { @@ -123,7 +130,7 @@ class MetricFamily : public Collectable { std::string help_; uint64_t required_mask_{}; uint64_t optional_mask_{}; - std::vector> const_labels_; + std::vector> const_labels_; double scale_{1.0}; std::function()> factory_; diff --git a/include/prometheus/metric_family_builder.hpp b/include/prometheus/metric_family_builder.hpp index a02dce6..ad7f5be 100644 --- a/include/prometheus/metric_family_builder.hpp +++ b/include/prometheus/metric_family_builder.hpp @@ -28,16 +28,13 @@ class MetricFamilyBuilder { MetricFamilyBuilder(std::string name, std::string help, Registry& reg) : name_(std::move(name)) , help_(std::move(help)) - , registry_(reg) - { + , registry_(reg) { PROMETHEUS_ASSERT(detail::check_metric_name(name_)); if constexpr (!std::is_same_v) { - factory_ = []{ return std::make_unique(); }; + factory_ = [] { return std::make_unique(); }; } else { // Default: 8 power-of-two buckets starting at 100 - factory_ = []{ - return std::make_unique(Histogram::make_bounds(100, 8)); - }; + factory_ = [] { return std::make_unique(Histogram::make_bounds(100, 8)); }; } } @@ -63,7 +60,7 @@ class MetricFamilyBuilder { auto& buckets(int64_t min, std::size_t count) requires std::same_as { - factory_ = [min, count]{ + factory_ = [min, count] { return std::make_unique(Histogram::make_bounds(min, count)); }; return *this; @@ -73,10 +70,8 @@ class MetricFamilyBuilder { auto& buckets(std::vector boundaries) requires std::same_as { - auto b = Histogram::make_bounds(std::move(boundaries)); - factory_ = [b = std::move(b)]{ - return std::make_unique(b); - }; + auto b = Histogram::make_bounds(std::move(boundaries)); + factory_ = [b = std::move(b)] { return std::make_unique(b); }; return *this; } @@ -98,11 +93,11 @@ class MetricFamilyBuilder { private: std::string name_; std::string help_; - Registry& registry_; - uint64_t required_mask_{}; - uint64_t optional_mask_{}; - std::vector> const_labels_; - double scale_{1.0}; + Registry& registry_; + uint64_t required_mask_{}; + uint64_t optional_mask_{}; + std::vector> const_labels_; + double scale_{1.0}; std::function()> factory_; }; diff --git a/include/prometheus/registry.hpp b/include/prometheus/registry.hpp index 8a68c59..1b58f4f 100644 --- a/include/prometheus/registry.hpp +++ b/include/prometheus/registry.hpp @@ -24,35 +24,32 @@ class Registry { // --- Builder entry points --- template - MetricFamilyBuilder - counter(std::string name, std::string help) { + MetricFamilyBuilder counter(std::string name, std::string help) { return {std::move(name), std::move(help), *this}; } template - MetricFamilyBuilder - gauge(std::string name, std::string help) { + MetricFamilyBuilder gauge(std::string name, std::string help) { return {std::move(name), std::move(help), *this}; } template - MetricFamilyBuilder - histogram(std::string name, std::string help) { + MetricFamilyBuilder histogram(std::string name, std::string help) { return {std::move(name), std::move(help), *this}; } // Called by MetricFamilyBuilder::build(). // Takes ownership of the family and returns a stable reference. template - MetricFamily& - register_family(std::unique_ptr> family) { + MetricFamily& register_family( + std::unique_ptr> family) { auto& ref = *family; std::unique_lock lock(mutex_); - auto [it, inserted] = registered_names_.try_emplace( - std::string(family->name()), family->type()); + auto [it, inserted] = + registered_names_.try_emplace(std::string(family->name()), family->type()); if (!inserted) { - PROMETHEUS_ASSERT(it->second == family->type() - && "metric family registered with conflicting type"); + PROMETHEUS_ASSERT(it->second == family->type() && + "metric family registered with conflicting type"); PROMETHEUS_ASSERT(false && "duplicate metric family name"); } families_.push_back(std::move(family)); @@ -84,16 +81,14 @@ class Registry { // --- MetricFamilyBuilder::build() --- defined here so Registry is complete --- template -MetricFamily& -MetricFamilyBuilder::build() { - auto family = std::make_unique>( - std::move(name_), - std::move(help_), - required_mask_, - optional_mask_, - std::move(const_labels_), - scale_, - std::move(factory_)); +MetricFamily& MetricFamilyBuilder::build() { + auto family = std::make_unique>(std::move(name_), + std::move(help_), + required_mask_, + optional_mask_, + std::move(const_labels_), + scale_, + std::move(factory_)); return registry_.register_family(std::move(family)); } diff --git a/include/prometheus/text_serializer.hpp b/include/prometheus/text_serializer.hpp index 7d5b608..87ec2b4 100644 --- a/include/prometheus/text_serializer.hpp +++ b/include/prometheus/text_serializer.hpp @@ -24,7 +24,8 @@ constexpr std::string_view metric_type_name(MetricType t) noexcept { // Writes Prometheus text exposition format (version 0.0.4) to an ostream. class TextSerializer { public: - explicit TextSerializer(std::ostream& out) : out_(out) {} + explicit TextSerializer(std::ostream& out) + : out_(out) {} static constexpr std::string escape_help(std::string_view help) { std::string out; @@ -32,8 +33,8 @@ class TextSerializer { for (char c : help) { switch (c) { case '\\': out += "\\\\"; break; - case '\n': out += "\\n"; break; - default: out += c; break; + case '\n': out += "\\n"; break; + default: out += c; break; } } return out; @@ -49,14 +50,12 @@ class TextSerializer { // const_labels: family-level fixed labels, appended after dynamic + extra void write_sample(std::string_view metric_name, std::string_view dynamic_labels, - const std::vector>& const_labels, + const std::vector>& const_labels, double value, - std::string_view extra_label_name = {}, + std::string_view extra_label_name = {}, std::string_view extra_label_value = {}) { out_ << metric_name; - bool any = !dynamic_labels.empty() - || !extra_label_name.empty() - || !const_labels.empty(); + bool any = !dynamic_labels.empty() || !extra_label_name.empty() || !const_labels.empty(); if (any) { out_ << '{'; bool first = true; @@ -65,12 +64,14 @@ class TextSerializer { first = false; } if (!extra_label_name.empty()) { - if (!first) out_ << ','; + if (!first) + out_ << ','; out_ << extra_label_name << "=\"" << extra_label_value << '"'; first = false; } for (const auto& [k, v] : const_labels) { - if (!first) out_ << ','; + if (!first) + out_ << ','; out_ << k << "=\"" << v << '"'; first = false; } @@ -79,13 +80,17 @@ class TextSerializer { out_ << ' ' << format_double(value) << '\n'; } - void write_newline() { out_ << '\n'; } + void write_newline() { + out_ << '\n'; + } // Compact floating-point formatting: integers without decimal point, // +Inf/-Inf/NaN handled, up to 15 significant digits otherwise. static std::string format_double(double v) { - if (std::isinf(v)) return v > 0.0 ? "+Inf" : "-Inf"; - if (std::isnan(v)) return "NaN"; + if (std::isinf(v)) + return v > 0.0 ? "+Inf" : "-Inf"; + if (std::isnan(v)) + return "NaN"; char buf[64]; auto [ptr, ec] = std::to_chars(buf, buf + sizeof(buf), v); return std::string(buf, ptr); diff --git a/include/prometheus/unit.hpp b/include/prometheus/unit.hpp index c32d734..f44583a 100644 --- a/include/prometheus/unit.hpp +++ b/include/prometheus/unit.hpp @@ -5,49 +5,50 @@ namespace prometheus { struct Unit { - std::string_view name; // human label: "microseconds" - double scale; // multiply stored int64 value by this to get base unit - std::string_view base_name; // "seconds", "bytes", "joules" - std::string_view base_suffix; // "_seconds", "_bytes" (for reference, not auto-applied) + std::string_view name; // human label: "microseconds" + double scale; // multiply stored int64 value by this to get base unit + std::string_view base_name; // "seconds", "bytes", "joules" + std::string_view base_suffix; // "_seconds", "_bytes" (for reference, not auto-applied) }; namespace units { // ── Duration ────────────────────────────────────────── -inline constexpr Unit nanoseconds {"nanoseconds", 1e-9, "seconds", "_seconds"}; -inline constexpr Unit microseconds {"microseconds", 1e-6, "seconds", "_seconds"}; -inline constexpr Unit milliseconds {"milliseconds", 1e-3, "seconds", "_seconds"}; -inline constexpr Unit seconds {"seconds", 1.0, "seconds", "_seconds"}; +inline constexpr Unit nanoseconds{"nanoseconds", 1e-9, "seconds", "_seconds"}; +inline constexpr Unit microseconds{"microseconds", 1e-6, "seconds", "_seconds"}; +inline constexpr Unit milliseconds{"milliseconds", 1e-3, "seconds", "_seconds"}; +inline constexpr Unit seconds{"seconds", 1.0, "seconds", "_seconds"}; // ── Data size ───────────────────────────────────────── -inline constexpr Unit bytes {"bytes", 1.0, "bytes", "_bytes"}; -inline constexpr Unit kilobytes {"kilobytes", 1e3, "bytes", "_bytes"}; -inline constexpr Unit megabytes {"megabytes", 1e6, "bytes", "_bytes"}; -inline constexpr Unit gigabytes {"gigabytes", 1e9, "bytes", "_bytes"}; -inline constexpr Unit kibibytes {"kibibytes", 1024.0, "bytes", "_bytes"}; -inline constexpr Unit mebibytes {"mebibytes", 1048576.0, "bytes", "_bytes"}; -inline constexpr Unit gibibytes {"gibibytes", 1073741824.0, "bytes", "_bytes"}; +inline constexpr Unit bytes{"bytes", 1.0, "bytes", "_bytes"}; +inline constexpr Unit kilobytes{"kilobytes", 1e3, "bytes", "_bytes"}; +inline constexpr Unit megabytes{"megabytes", 1e6, "bytes", "_bytes"}; +inline constexpr Unit gigabytes{"gigabytes", 1e9, "bytes", "_bytes"}; +inline constexpr Unit kibibytes{"kibibytes", 1024.0, "bytes", "_bytes"}; +inline constexpr Unit mebibytes{"mebibytes", 1048576.0, "bytes", "_bytes"}; +inline constexpr Unit gibibytes{"gibibytes", 1073741824.0, "bytes", "_bytes"}; // ── Energy ──────────────────────────────────────────── -inline constexpr Unit joules {"joules", 1.0, "joules", "_joules"}; -inline constexpr Unit kilojoules {"kilojoules", 1e3, "joules", "_joules"}; -inline constexpr Unit megajoules {"megajoules", 1e6, "joules", "_joules"}; +inline constexpr Unit joules{"joules", 1.0, "joules", "_joules"}; +inline constexpr Unit kilojoules{"kilojoules", 1e3, "joules", "_joules"}; +inline constexpr Unit megajoules{"megajoules", 1e6, "joules", "_joules"}; // ── Temperature ─────────────────────────────────────── -inline constexpr Unit celsius {"celsius", 1.0, "celsius", "_celsius"}; -inline constexpr Unit fahrenheit {"fahrenheit", 1.0, "fahrenheit", "_fahrenheit"}; -inline constexpr Unit kelvin {"kelvin", 1.0, "kelvin", "_kelvin"}; +inline constexpr Unit celsius{"celsius", 1.0, "celsius", "_celsius"}; +inline constexpr Unit fahrenheit{"fahrenheit", 1.0, "fahrenheit", "_fahrenheit"}; +inline constexpr Unit kelvin{"kelvin", 1.0, "kelvin", "_kelvin"}; // ── Ratios / rates ──────────────────────────────────── -inline constexpr Unit ratio {"ratio", 1.0, "ratio", "_ratio"}; -inline constexpr Unit percent {"percent", 0.01, "ratio", "_ratio"}; +inline constexpr Unit ratio{"ratio", 1.0, "ratio", "_ratio"}; +inline constexpr Unit percent{"percent", 0.01, "ratio", "_ratio"}; // ── Dimensionless (no unit, scale=1) ────────────────── -inline constexpr Unit none {"", 1.0, "", ""}; +inline constexpr Unit none{"", 1.0, "", ""}; // ── Custom unit builder ─────────────────────────────── -consteval Unit custom(std::string_view name, double scale = 1.0, - std::string_view base_name = "", +consteval Unit custom(std::string_view name, + double scale = 1.0, + std::string_view base_name = "", std::string_view base_suffix = "") { return {name, scale, base_name, base_suffix}; } diff --git a/tests/test_check_names.cpp b/tests/test_check_names.cpp index 50f86aa..bdc14da 100644 --- a/tests/test_check_names.cpp +++ b/tests/test_check_names.cpp @@ -1,8 +1,8 @@ #include #include -using prometheus::detail::check_metric_name; using prometheus::detail::check_label_name; +using prometheus::detail::check_metric_name; // Metric names TEST(CheckNamesTest, ValidMetricNames) { diff --git a/tests/test_concurrency.cpp b/tests/test_concurrency.cpp index 82125b9..ddc9036 100644 --- a/tests/test_concurrency.cpp +++ b/tests/test_concurrency.cpp @@ -3,20 +3,17 @@ #include #include -PROMETHEUS_DEFINE_LABELS(ConcLabels, - (service, std::string_view), - (method, std::string_view) -); +PROMETHEUS_DEFINE_LABELS(ConcLabels, (service, std::string_view), (method, std::string_view)); TEST(ConcurrencyTest, ConcurrentGetAndInc) { // Multiple threads getting the same metric handle and incrementing prometheus::Registry reg; auto& fam = reg.counter("conc_counter", "Concurrent counter") - .required(ConcLabels::Key::service, ConcLabels::Key::method) - .build(); + .required(ConcLabels::Key::service, ConcLabels::Key::method) + .build(); constexpr int num_threads = 8; - constexpr int iters = 100'000; + constexpr int iters = 100'000; std::vector workers; workers.reserve(num_threads); @@ -38,10 +35,10 @@ TEST(ConcurrencyTest, ConcurrentNewLabelCombinations) { // Multiple threads creating different label combinations simultaneously prometheus::Registry reg; auto& fam = reg.counter("conc_new_labels", "Concurrent label creation") - .required(ConcLabels::Key::service, ConcLabels::Key::method) - .build(); + .required(ConcLabels::Key::service, ConcLabels::Key::method) + .build(); - constexpr int num_threads = 8; + constexpr int num_threads = 8; constexpr int combos_per_thread = 100; // Each thread creates unique label combos and increments each once @@ -50,7 +47,7 @@ TEST(ConcurrencyTest, ConcurrentNewLabelCombinations) { for (int t = 0; t < num_threads; ++t) { workers.emplace_back([&, t] { for (int c = 0; c < combos_per_thread; ++c) { - auto svc = "svc_" + std::to_string(t); + auto svc = "svc_" + std::to_string(t); auto method = "m_" + std::to_string(c); // string_view must outlive the call fam.get({.service = svc, .method = method}).inc(); @@ -67,12 +64,12 @@ TEST(ConcurrencyTest, ConcurrentNewLabelCombinations) { TEST(ConcurrencyTest, ConcurrentHistogramObserve) { prometheus::Registry reg; auto& fam = reg.histogram("conc_hist", "Concurrent histogram") - .required(ConcLabels::Key::service) - .buckets(100, 6) - .build(); + .required(ConcLabels::Key::service) + .buckets(100, 6) + .build(); constexpr int num_threads = 8; - constexpr int iters = 100'000; + constexpr int iters = 100'000; std::vector workers; workers.reserve(num_threads); diff --git a/tests/test_counter.cpp b/tests/test_counter.cpp index d390616..1f48d8e 100644 --- a/tests/test_counter.cpp +++ b/tests/test_counter.cpp @@ -71,7 +71,10 @@ TEST(CounterTest, ConcurrentIncrements) { std::vector workers; workers.reserve(kThreads); for (int i = 0; i < kThreads; ++i) - workers.emplace_back([&]{ for (int j = 0; j < kIters; ++j) c.inc(); }); + workers.emplace_back([&] { + for (int j = 0; j < kIters; ++j) + c.inc(); + }); workers.clear(); // joins all jthreads EXPECT_EQ(c.load(), static_cast(kThreads) * kIters); @@ -102,8 +105,9 @@ TEST(CounterTest, ConcurrentIncrementsByDelta) { std::vector workers; workers.reserve(kThreads); for (int i = 0; i < kThreads; ++i) - workers.emplace_back([&]{ - for (int j = 0; j < kIters; ++j) c.inc(kDelta); + workers.emplace_back([&] { + for (int j = 0; j < kIters; ++j) + c.inc(kDelta); }); workers.clear(); diff --git a/tests/test_gauge.cpp b/tests/test_gauge.cpp index c95e133..78f285c 100644 --- a/tests/test_gauge.cpp +++ b/tests/test_gauge.cpp @@ -91,10 +91,12 @@ TEST(GaugeTest, ToDoubleNegative) { TEST(GaugeTest, SetToCurrentTime) { prometheus::Gauge g; auto before = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count(); + std::chrono::system_clock::now().time_since_epoch()) + .count(); g.set_to_current_time(); auto after = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count(); + std::chrono::system_clock::now().time_since_epoch()) + .count(); EXPECT_GE(g.load(), before); EXPECT_LE(g.load(), after); } @@ -107,7 +109,10 @@ TEST(GaugeTest, ConcurrentIncrements) { std::vector workers; workers.reserve(kThreads); for (int i = 0; i < kThreads; ++i) - workers.emplace_back([&]{ for (int j = 0; j < kIters; ++j) g.inc(); }); + workers.emplace_back([&] { + for (int j = 0; j < kIters; ++j) + g.inc(); + }); workers.clear(); EXPECT_EQ(g.load(), static_cast(kThreads) * kIters); @@ -122,9 +127,15 @@ TEST(GaugeTest, ConcurrentMixed) { std::vector workers; workers.reserve(kHalf * 2); for (int i = 0; i < kHalf; ++i) - workers.emplace_back([&]{ for (int j = 0; j < kIters; ++j) g.inc(); }); + workers.emplace_back([&] { + for (int j = 0; j < kIters; ++j) + g.inc(); + }); for (int i = 0; i < kHalf; ++i) - workers.emplace_back([&]{ for (int j = 0; j < kIters; ++j) g.dec(); }); + workers.emplace_back([&] { + for (int j = 0; j < kIters; ++j) + g.dec(); + }); workers.clear(); EXPECT_EQ(g.load(), 0); diff --git a/tests/test_histogram.cpp b/tests/test_histogram.cpp index 045b02b..a731f32 100644 --- a/tests/test_histogram.cpp +++ b/tests/test_histogram.cpp @@ -133,7 +133,10 @@ TEST(HistogramTest, ConcurrentObserveSameBucket) { std::vector workers; workers.reserve(kThreads); for (int i = 0; i < kThreads; ++i) - workers.emplace_back([&]{ for (int j = 0; j < kIters; ++j) h.observe(50); }); + workers.emplace_back([&] { + for (int j = 0; j < kIters; ++j) + h.observe(50); + }); workers.clear(); // join EXPECT_EQ(h.total_count(), static_cast(kThreads) * kIters); @@ -184,8 +187,9 @@ TEST(HistogramTest, ConcurrentObserveDifferentBuckets) { workers.reserve(kThreads); for (int i = 0; i < kThreads; ++i) { int64_t val = (i % 2 == 0) ? 50 : 150; // alternates bucket 0 and 1 - workers.emplace_back([&h, val, kIters]{ - for (int j = 0; j < kIters; ++j) h.observe(val); + workers.emplace_back([&h, val, kIters] { + for (int j = 0; j < kIters; ++j) + h.observe(val); }); } workers.clear(); @@ -200,9 +204,9 @@ TEST(LocalHistogramTest, BasicAccumulate) { LocalHistogram local(hist); // Accumulate locally (no atomics) - local.observe(50); // bucket 0 - local.observe(150); // bucket 1 - local.observe(500); // bucket 3 (+Inf) + local.observe(50); // bucket 0 + local.observe(150); // bucket 1 + local.observe(500); // bucket 3 (+Inf) // Not yet merged — histogram should be empty EXPECT_EQ(hist.total_count(), 0); @@ -257,7 +261,7 @@ TEST(LocalHistogramTest, Reset) { local.observe(50); local.observe(150); - local.reset(); // discard without merging + local.reset(); // discard without merging local.merge_into(hist); EXPECT_EQ(hist.total_count(), 0); @@ -268,7 +272,7 @@ TEST(LocalHistogramTest, ConcurrentLocalMerge) { // Each thread has its own LocalHistogram, merges into shared Histogram Histogram hist(Histogram::make_bounds(100, 4)); - constexpr int kThreads = 8; + constexpr int kThreads = 8; constexpr int kBatchSize = 100'000; std::vector workers; diff --git a/tests/test_label_def.cpp b/tests/test_label_def.cpp index d84a9a1..26bff50 100644 --- a/tests/test_label_def.cpp +++ b/tests/test_label_def.cpp @@ -3,17 +3,16 @@ #include PROMETHEUS_DEFINE_LABELS(TestLabels, - (service, std::string_view), - (method, std::string_view), - (code, uint32_t), - (region, std::string_view) -); + (service, std::string_view), + (method, std::string_view), + (code, uint32_t), + (region, std::string_view)); TEST(LabelDefTest, KeyValuesArePowersOfTwo) { EXPECT_EQ(static_cast(TestLabels::Key::service), 1ULL); - EXPECT_EQ(static_cast(TestLabels::Key::method), 2ULL); - EXPECT_EQ(static_cast(TestLabels::Key::code), 4ULL); - EXPECT_EQ(static_cast(TestLabels::Key::region), 8ULL); + EXPECT_EQ(static_cast(TestLabels::Key::method), 2ULL); + EXPECT_EQ(static_cast(TestLabels::Key::code), 4ULL); + EXPECT_EQ(static_cast(TestLabels::Key::region), 8ULL); } TEST(LabelDefTest, Count) { @@ -22,9 +21,9 @@ TEST(LabelDefTest, Count) { TEST(LabelDefTest, KeyName) { EXPECT_EQ(TestLabels::key_name(TestLabels::Key::service), "service"); - EXPECT_EQ(TestLabels::key_name(TestLabels::Key::method), "method"); - EXPECT_EQ(TestLabels::key_name(TestLabels::Key::code), "code"); - EXPECT_EQ(TestLabels::key_name(TestLabels::Key::region), "region"); + EXPECT_EQ(TestLabels::key_name(TestLabels::Key::method), "method"); + EXPECT_EQ(TestLabels::key_name(TestLabels::Key::code), "code"); + EXPECT_EQ(TestLabels::key_name(TestLabels::Key::region), "region"); } TEST(LabelDefTest, KeyNameUnknown) { @@ -36,7 +35,7 @@ TEST(LabelDefTest, KeyNameUnknown) { TEST(LabelDefTest, PopulatedMaskServiceMethod) { TestLabels::LabelSet ls{.service = "api", .method = "GET"}; auto mask = TestLabels::populated_mask(ls); - EXPECT_EQ(mask, 1ULL | 2ULL); // service | method + EXPECT_EQ(mask, 1ULL | 2ULL); // service | method } TEST(LabelDefTest, PopulatedMaskNone) { @@ -77,7 +76,7 @@ TEST(LabelDefTest, FormatValueArithmetic) { TEST(LabelDefTest, FormatValueAbsent) { TestLabels::LabelSet ls{}; EXPECT_EQ(TestLabels::format_value(TestLabels::Key::service, ls), ""); - EXPECT_EQ(TestLabels::format_value(TestLabels::Key::code, ls), ""); + EXPECT_EQ(TestLabels::format_value(TestLabels::Key::code, ls), ""); } TEST(LabelDefTest, FormatValueUnknownKey) { @@ -96,8 +95,7 @@ TEST(LabelDefTest, AllKeys) { } TEST(LabelDefTest, MakeMask) { - auto mask = prometheus::make_mask( - TestLabels::Key::service, TestLabels::Key::code); + auto mask = prometheus::make_mask(TestLabels::Key::service, TestLabels::Key::code); EXPECT_EQ(mask, 1ULL | 4ULL); } @@ -107,11 +105,10 @@ TEST(LabelDefTest, MakeMaskEmpty) { } TEST(LabelDefTest, MakeMaskAll) { - auto mask = prometheus::make_mask( - TestLabels::Key::service, - TestLabels::Key::method, - TestLabels::Key::code, - TestLabels::Key::region); + auto mask = prometheus::make_mask(TestLabels::Key::service, + TestLabels::Key::method, + TestLabels::Key::code, + TestLabels::Key::region); EXPECT_EQ(mask, 1ULL | 2ULL | 4ULL | 8ULL); } diff --git a/tests/test_metric_family.cpp b/tests/test_metric_family.cpp index f0fdd8c..6fb3f6d 100644 --- a/tests/test_metric_family.cpp +++ b/tests/test_metric_family.cpp @@ -1,21 +1,16 @@ #include #include - - PROMETHEUS_DEFINE_LABELS(FamLabels, - (service, std::string_view), - (method, std::string_view), - (code, uint32_t) -); + (service, std::string_view), + (method, std::string_view), + (code, uint32_t)); // --- Counter family basics --- TEST(MetricFamilyTest, GetReturnsMetric) { prometheus::Registry reg; - auto& fam = reg.counter("reqs", "help") - .required(FamLabels::Key::service) - .build(); + auto& fam = reg.counter("reqs", "help").required(FamLabels::Key::service).build(); auto& c = fam.get({.service = "api"}); c.inc(10); @@ -24,9 +19,7 @@ TEST(MetricFamilyTest, GetReturnsMetric) { TEST(MetricFamilyTest, SameLabelsSameRef) { prometheus::Registry reg; - auto& fam = reg.counter("reqs", "help") - .required(FamLabels::Key::service) - .build(); + auto& fam = reg.counter("reqs", "help").required(FamLabels::Key::service).build(); auto& c1 = fam.get({.service = "api"}); auto& c2 = fam.get({.service = "api"}); @@ -35,9 +28,7 @@ TEST(MetricFamilyTest, SameLabelsSameRef) { TEST(MetricFamilyTest, DifferentLabelsDifferentRefs) { prometheus::Registry reg; - auto& fam = reg.counter("reqs", "help") - .required(FamLabels::Key::service) - .build(); + auto& fam = reg.counter("reqs", "help").required(FamLabels::Key::service).build(); auto& c1 = fam.get({.service = "api"}); auto& c2 = fam.get({.service = "web"}); @@ -50,9 +41,9 @@ TEST(MetricFamilyTest, DifferentLabelsDifferentRefs) { TEST(MetricFamilyTest, OptionalLabelDistinguishesCombinations) { prometheus::Registry reg; auto& fam = reg.counter("reqs", "help") - .required(FamLabels::Key::service) - .optional(FamLabels::Key::method) - .build(); + .required(FamLabels::Key::service) + .optional(FamLabels::Key::method) + .build(); auto& c1 = fam.get({.service = "api", .method = "GET"}); auto& c2 = fam.get({.service = "api", .method = "POST"}); @@ -63,9 +54,7 @@ TEST(MetricFamilyTest, OptionalLabelDistinguishesCombinations) { TEST(MetricFamilyTest, HandleStableAcrossCalls) { prometheus::Registry reg; - auto& fam = reg.counter("reqs", "help") - .required(FamLabels::Key::service) - .build(); + auto& fam = reg.counter("reqs", "help").required(FamLabels::Key::service).build(); auto& c = fam.get({.service = "api"}); c.inc(1); @@ -79,8 +68,8 @@ TEST(MetricFamilyTest, HandleStableAcrossCalls) { TEST(MetricFamilyTest, GaugeFamily) { prometheus::Registry reg; auto& fam = reg.gauge("active", "Active connections") - .required(FamLabels::Key::service) - .build(); + .required(FamLabels::Key::service) + .build(); auto& g = fam.get({.service = "api"}); g.set(42); @@ -94,9 +83,9 @@ TEST(MetricFamilyTest, GaugeFamily) { TEST(MetricFamilyTest, HistogramFamilyWithBuckets) { prometheus::Registry reg; auto& fam = reg.histogram("latency", "Latency us") - .required(FamLabels::Key::service) - .buckets(100, 4) - .build(); + .required(FamLabels::Key::service) + .buckets(100, 4) + .build(); auto& h = fam.get({.service = "svc"}); h.observe(50); @@ -108,10 +97,10 @@ TEST(MetricFamilyTest, HistogramFamilyWithBuckets) { TEST(MetricFamilyTest, HistogramCustomBuckets) { prometheus::Registry reg; auto& fam = reg.histogram("latency", "Latency") - .required(FamLabels::Key::service) - .buckets(std::vector{100, 250, 500}) - .build(); - auto& h = fam.get({.service = "svc"}); + .required(FamLabels::Key::service) + .buckets(std::vector{100, 250, 500}) + .build(); + auto& h = fam.get({.service = "svc"}); h.observe(150); EXPECT_EQ(h.num_buckets(), 4u); // 3 custom + +Inf } @@ -121,28 +110,28 @@ TEST(MetricFamilyTest, HistogramCustomBuckets) { TEST(MetricFamilyTest, ConstLabelsInOutput) { prometheus::Registry reg; auto& fam = reg.counter("total", "Total things") - .required(FamLabels::Key::service) - .const_label("env", "test") - .build(); + .required(FamLabels::Key::service) + .const_label("env", "test") + .build(); fam.get({.service = "api"}).inc(5); const std::string out = reg.serialize(); - EXPECT_NE(out.find("env=\"test\""), std::string::npos); + EXPECT_NE(out.find("env=\"test\""), std::string::npos); EXPECT_NE(out.find("service=\"api\""), std::string::npos); } TEST(MetricFamilyTest, MultipleConstLabels) { prometheus::Registry reg; auto& fam = reg.counter("c", "c") - .required(FamLabels::Key::service) - .const_label("env", "prod") - .const_label("version", "2.0") - .build(); + .required(FamLabels::Key::service) + .const_label("env", "prod") + .const_label("version", "2.0") + .build(); fam.get({.service = "api"}).inc(1); const std::string out = reg.serialize(); - EXPECT_NE(out.find("env=\"prod\""), std::string::npos); + EXPECT_NE(out.find("env=\"prod\""), std::string::npos); EXPECT_NE(out.find("version=\"2.0\""), std::string::npos); } @@ -151,16 +140,15 @@ TEST(MetricFamilyTest, MultipleConstLabels) { TEST(MetricFamilyTest, HelpAccessor) { prometheus::Registry reg; auto& fam = reg.counter("test_help_ctr", "My help text") - .required(FamLabels::Key::service) - .build(); + .required(FamLabels::Key::service) + .build(); EXPECT_EQ(fam.help(), "My help text"); } TEST(MetricFamilyTest, NameAccessor) { prometheus::Registry reg; - auto& fam = reg.counter("test_name_ctr", "help") - .required(FamLabels::Key::service) - .build(); + auto& fam = + reg.counter("test_name_ctr", "help").required(FamLabels::Key::service).build(); EXPECT_EQ(fam.name(), "test_name_ctr"); } @@ -170,17 +158,13 @@ TEST(MetricFamilyTest, NameAccessor) { TEST(MetricFamilyTest, RequiredLabelMissingAborts) { prometheus::Registry reg; - auto& fam = reg.counter("reqs", "help") - .required(FamLabels::Key::service) - .build(); + auto& fam = reg.counter("reqs", "help").required(FamLabels::Key::service).build(); EXPECT_DEATH(fam.get({}), "assertion failed"); } TEST(MetricFamilyTest, ForbiddenLabelAborts) { prometheus::Registry reg; - auto& fam = reg.counter("reqs", "help") - .required(FamLabels::Key::service) - .build(); + auto& fam = reg.counter("reqs", "help").required(FamLabels::Key::service).build(); // method is not required or optional -> forbidden EXPECT_DEATH(fam.get({.service = "api", .method = "GET"}), "assertion failed"); } diff --git a/tests/test_registry.cpp b/tests/test_registry.cpp index 3c93fd3..2f06fa9 100644 --- a/tests/test_registry.cpp +++ b/tests/test_registry.cpp @@ -3,26 +3,25 @@ #include PROMETHEUS_DEFINE_LABELS(RegLabels, - (service, std::string_view), - (method, std::string_view), - (code, uint32_t) -); + (service, std::string_view), + (method, std::string_view), + (code, uint32_t)); TEST(RegistryTest, RegisterCounterGaugeHistogram) { prometheus::Registry reg; auto& counter = reg.counter("reqs", "Total requests") - .required(RegLabels::Key::service, RegLabels::Key::method) - .build(); + .required(RegLabels::Key::service, RegLabels::Key::method) + .build(); auto& gauge = reg.gauge("conns", "Active connections") - .required(RegLabels::Key::service) - .build(); + .required(RegLabels::Key::service) + .build(); auto& hist = reg.histogram("latency", "Latency") - .required(RegLabels::Key::service, RegLabels::Key::method) - .buckets(100, 5) - .build(); + .required(RegLabels::Key::service, RegLabels::Key::method) + .buckets(100, 5) + .build(); counter.get({.service = "api", .method = "GET"}).inc(100); gauge.get({.service = "api"}).set(42); @@ -37,15 +36,16 @@ TEST(RegistryTest, RegisterCounterGaugeHistogram) { TEST(RegistryTest, EndToEnd) { prometheus::Registry reg; - auto& counter = reg.counter("http_total", "HTTP requests") - .required(RegLabels::Key::service, RegLabels::Key::method, RegLabels::Key::code) - .const_label("env", "test") - .build(); + auto& counter = + reg.counter("http_total", "HTTP requests") + .required(RegLabels::Key::service, RegLabels::Key::method, RegLabels::Key::code) + .const_label("env", "test") + .build(); // Simulate some traffic - counter.get({.service = "api", .method = "GET", .code = 200u}).inc(1000); + counter.get({.service = "api", .method = "GET", .code = 200u}).inc(1000); counter.get({.service = "api", .method = "POST", .code = 201u}).inc(50); - counter.get({.service = "api", .method = "GET", .code = 404u}).inc(3); + counter.get({.service = "api", .method = "GET", .code = 404u}).inc(3); auto out = reg.serialize(); @@ -60,9 +60,8 @@ TEST(RegistryTest, EndToEnd) { TEST(RegistryTest, SerializeToStream) { prometheus::Registry reg; - auto& fam = reg.counter("count", "A counter") - .required(RegLabels::Key::service) - .build(); + auto& fam = + reg.counter("count", "A counter").required(RegLabels::Key::service).build(); fam.get({.service = "svc"}).inc(7); std::ostringstream oss; @@ -81,25 +80,17 @@ TEST(RegistryTest, EmptySerialize) { #ifndef NDEBUG TEST(RegistryTest, DuplicateNameAborts) { prometheus::Registry reg; - reg.counter("dup_name", "first") - .required(RegLabels::Key::service) - .build(); + reg.counter("dup_name", "first").required(RegLabels::Key::service).build(); EXPECT_DEATH( - reg.counter("dup_name", "second") - .required(RegLabels::Key::service) - .build(), + reg.counter("dup_name", "second").required(RegLabels::Key::service).build(), "duplicate metric family name"); } TEST(RegistryTest, DifferentTypeConflictAborts) { prometheus::Registry reg; - reg.counter("conflict_name", "as counter") - .required(RegLabels::Key::service) - .build(); + reg.counter("conflict_name", "as counter").required(RegLabels::Key::service).build(); EXPECT_DEATH( - reg.gauge("conflict_name", "as gauge") - .required(RegLabels::Key::service) - .build(), + reg.gauge("conflict_name", "as gauge").required(RegLabels::Key::service).build(), "conflicting type"); } #endif diff --git a/tests/test_text_serializer.cpp b/tests/test_text_serializer.cpp index 21a3b70..4b1a7d1 100644 --- a/tests/test_text_serializer.cpp +++ b/tests/test_text_serializer.cpp @@ -3,16 +3,13 @@ #include #include -PROMETHEUS_DEFINE_LABELS(SerLabels, - (service, std::string_view), - (method, std::string_view) -); +PROMETHEUS_DEFINE_LABELS(SerLabels, (service, std::string_view), (method, std::string_view)); TEST(TextSerializerTest, CounterFormat) { prometheus::Registry reg; auto& fam = reg.counter("http_requests_total", "Total HTTP requests") - .required(SerLabels::Key::service, SerLabels::Key::method) - .build(); + .required(SerLabels::Key::service, SerLabels::Key::method) + .build(); fam.get({.service = "api", .method = "GET"}).inc(42); @@ -26,8 +23,8 @@ TEST(TextSerializerTest, CounterFormat) { TEST(TextSerializerTest, GaugeFormat) { prometheus::Registry reg; auto& fam = reg.gauge("active_connections", "Open connections") - .required(SerLabels::Key::service) - .build(); + .required(SerLabels::Key::service) + .build(); fam.get({.service = "api"}).set(37); @@ -39,14 +36,14 @@ TEST(TextSerializerTest, GaugeFormat) { TEST(TextSerializerTest, HistogramFormat) { prometheus::Registry reg; auto& fam = reg.histogram("latency_us", "Request latency") - .required(SerLabels::Key::service) - .buckets(100, 4) // 100, 200, 400, +Inf - .build(); + .required(SerLabels::Key::service) + .buckets(100, 4) // 100, 200, 400, +Inf + .build(); auto& h = fam.get({.service = "api"}); - h.observe(50); // bucket 0 (le=100) - h.observe(150); // bucket 1 (le=200) - h.observe(500); // bucket 3 (+Inf) + h.observe(50); // bucket 0 (le=100) + h.observe(150); // bucket 1 (le=200) + h.observe(500); // bucket 3 (+Inf) auto out = reg.serialize(); EXPECT_NE(out.find("# TYPE latency_us histogram"), std::string::npos); @@ -59,9 +56,9 @@ TEST(TextSerializerTest, HistogramFormat) { TEST(TextSerializerTest, ConstLabels) { prometheus::Registry reg; auto& fam = reg.counter("req_total", "Requests") - .required(SerLabels::Key::service) - .const_label("env", "prod") - .build(); + .required(SerLabels::Key::service) + .const_label("env", "prod") + .build(); fam.get({.service = "api"}).inc(10); @@ -72,10 +69,10 @@ TEST(TextSerializerTest, ConstLabels) { TEST(TextSerializerTest, ScaleFactor) { prometheus::Registry reg; auto& fam = reg.histogram("latency_s", "Request latency in seconds") - .required(SerLabels::Key::service) - .buckets(1000, 3) // 1000, 2000, +Inf (microseconds) - .scale(1e-6) // display as seconds - .build(); + .required(SerLabels::Key::service) + .buckets(1000, 3) // 1000, 2000, +Inf (microseconds) + .scale(1e-6) // display as seconds + .build(); auto& h = fam.get({.service = "api"}); h.observe(500); @@ -90,12 +87,9 @@ TEST(TextSerializerTest, ScaleFactor) { TEST(TextSerializerTest, MultipleFamilies) { prometheus::Registry reg; - auto& c = reg.counter("counter_a", "Counter A") - .required(SerLabels::Key::service) - .build(); - auto& g = reg.gauge("gauge_b", "Gauge B") - .required(SerLabels::Key::service) - .build(); + auto& c = + reg.counter("counter_a", "Counter A").required(SerLabels::Key::service).build(); + auto& g = reg.gauge("gauge_b", "Gauge B").required(SerLabels::Key::service).build(); c.get({.service = "svc"}).inc(5); g.get({.service = "svc"}).set(99); @@ -110,12 +104,12 @@ TEST(TextSerializerTest, MultipleFamilies) { TEST(TextSerializerTest, FormatDouble) { EXPECT_EQ(prometheus::TextSerializer::format_double(42.0), "42"); EXPECT_EQ(prometheus::TextSerializer::format_double(3.14), "3.14"); - EXPECT_EQ(prometheus::TextSerializer::format_double( - std::numeric_limits::infinity()), "+Inf"); - EXPECT_EQ(prometheus::TextSerializer::format_double( - -std::numeric_limits::infinity()), "-Inf"); - EXPECT_EQ(prometheus::TextSerializer::format_double( - std::numeric_limits::quiet_NaN()), "NaN"); + EXPECT_EQ(prometheus::TextSerializer::format_double(std::numeric_limits::infinity()), + "+Inf"); + EXPECT_EQ(prometheus::TextSerializer::format_double(-std::numeric_limits::infinity()), + "-Inf"); + EXPECT_EQ(prometheus::TextSerializer::format_double(std::numeric_limits::quiet_NaN()), + "NaN"); } TEST(TextSerializerTest, FormatDoubleLocaleIndependent) { @@ -128,8 +122,8 @@ TEST(TextSerializerTest, FormatDoubleLocaleIndependent) { TEST(TextSerializerTest, LabelValueEscaping) { prometheus::Registry reg; auto& fam = reg.counter("escaped_metric", "Test escaping") - .required(SerLabels::Key::service) - .build(); + .required(SerLabels::Key::service) + .build(); fam.get({.service = "has\"quote"}).inc(1); auto out = reg.serialize(); @@ -140,8 +134,8 @@ TEST(TextSerializerTest, LabelValueEscaping) { TEST(TextSerializerTest, LabelValueBackslashEscaping) { prometheus::Registry reg; auto& fam = reg.counter("bs_metric", "Test backslash") - .required(SerLabels::Key::service) - .build(); + .required(SerLabels::Key::service) + .build(); fam.get({.service = "path\\to\\thing"}).inc(1); auto out = reg.serialize(); @@ -152,9 +146,9 @@ TEST(TextSerializerTest, LabelValueBackslashEscaping) { TEST(TextSerializerTest, EmptyHistogramSerialization) { prometheus::Registry reg; auto& fam = reg.histogram("empty_hist", "Empty histogram") - .required(SerLabels::Key::service) - .buckets(100, 4) // 100, 200, 400, +Inf - .build(); + .required(SerLabels::Key::service) + .buckets(100, 4) // 100, 200, 400, +Inf + .build(); // Create the metric instance but don't observe anything fam.get({.service = "api"}); @@ -173,9 +167,9 @@ TEST(TextSerializerTest, EmptyHistogramSerialization) { TEST(TextSerializerTest, EmptyCounterSerialization) { prometheus::Registry reg; auto& fam = reg.counter("empty_counter", "Empty counter") - .required(SerLabels::Key::service) - .build(); - fam.get({.service = "api"}); // create but don't increment + .required(SerLabels::Key::service) + .build(); + fam.get({.service = "api"}); // create but don't increment auto out = reg.serialize(); EXPECT_NE(out.find("# TYPE empty_counter counter"), std::string::npos); @@ -197,10 +191,9 @@ TEST(TextSerializerTest, NoInstancesSerialization) { TEST(TextSerializerTest, GaugeZeroValue) { prometheus::Registry reg; - auto& fam = reg.gauge("zero_gauge", "Zero gauge") - .required(SerLabels::Key::service) - .build(); - fam.get({.service = "api"}); // default 0 + auto& fam = + reg.gauge("zero_gauge", "Zero gauge").required(SerLabels::Key::service).build(); + fam.get({.service = "api"}); // default 0 auto out = reg.serialize(); EXPECT_NE(out.find("} 0\n"), std::string::npos); } @@ -208,8 +201,8 @@ TEST(TextSerializerTest, GaugeZeroValue) { TEST(TextSerializerTest, GaugeNegativeValue) { prometheus::Registry reg; auto& fam = reg.gauge("neg_gauge", "Negative gauge") - .required(SerLabels::Key::service) - .build(); + .required(SerLabels::Key::service) + .build(); fam.get({.service = "api"}).set(-42); auto out = reg.serialize(); EXPECT_NE(out.find("-42"), std::string::npos); @@ -218,22 +211,21 @@ TEST(TextSerializerTest, GaugeNegativeValue) { TEST(TextSerializerTest, GaugeLargeValue) { prometheus::Registry reg; auto& fam = reg.gauge("large_gauge", "Large gauge") - .required(SerLabels::Key::service) - .build(); + .required(SerLabels::Key::service) + .build(); fam.get({.service = "api"}).set(1'000'000'000'000LL); auto out = reg.serialize(); // to_chars may produce "1000000000000" or "1e+12" or "1e12" - bool found = out.find("1000000000000") != std::string::npos - || out.find("1e+12") != std::string::npos - || out.find("1e12") != std::string::npos; + bool found = out.find("1000000000000") != std::string::npos || + out.find("1e+12") != std::string::npos || out.find("1e12") != std::string::npos; EXPECT_TRUE(found) << "Output: " << out; } TEST(TextSerializerTest, LabelValueNewlineEscaping) { prometheus::Registry reg; auto& fam = reg.counter("nl_metric", "Test newline") - .required(SerLabels::Key::service) - .build(); + .required(SerLabels::Key::service) + .build(); fam.get({.service = "api\nv2"}).inc(1); auto out = reg.serialize(); @@ -244,9 +236,7 @@ PROMETHEUS_DEFINE_LABELS(NoLabels, (dummy, std::string_view)); TEST(TextSerializerTest, HistogramNoDynamicLabelsHasLeBucket) { prometheus::Registry reg; - auto& fam = reg.histogram("no_dyn_hist", "No dynamic labels") - .buckets(100, 3) - .build(); + auto& fam = reg.histogram("no_dyn_hist", "No dynamic labels").buckets(100, 3).build(); fam.get({}).observe(50); @@ -259,8 +249,8 @@ TEST(TextSerializerTest, HistogramNoDynamicLabelsHasLeBucket) { TEST(TextSerializerTest, HelpTextEscaping) { prometheus::Registry reg; auto& fam = reg.counter("help_escape", "Help with \\backslash and\nnewline") - .required(SerLabels::Key::service) - .build(); + .required(SerLabels::Key::service) + .build(); fam.get({.service = "api"}).inc(1); auto out = reg.serialize(); // Backslash should be escaped, newline should be escaped diff --git a/tests/test_unit.cpp b/tests/test_unit.cpp index 80698bb..8950aaa 100644 --- a/tests/test_unit.cpp +++ b/tests/test_unit.cpp @@ -1,20 +1,18 @@ #include #include -PROMETHEUS_DEFINE_LABELS(UnitLabels, - (service, std::string_view) -); +PROMETHEUS_DEFINE_LABELS(UnitLabels, (service, std::string_view)); TEST(UnitTest, MicrosecondsScale) { prometheus::Registry reg; auto& fam = reg.histogram("request_duration_seconds", "Duration") - .required(UnitLabels::Key::service) - .unit(prometheus::units::microseconds) - .buckets(1000, 4) // 1000us, 2000us, 4000us, +Inf - .build(); + .required(UnitLabels::Key::service) + .unit(prometheus::units::microseconds) + .buckets(1000, 4) // 1000us, 2000us, 4000us, +Inf + .build(); auto& h = fam.get({.service = "api"}); - h.observe(1500); // 1500 microseconds + h.observe(1500); // 1500 microseconds auto out = reg.serialize(); // le values should be scaled to seconds: 0.001, 0.002, 0.004 @@ -26,29 +24,28 @@ TEST(UnitTest, MicrosecondsScale) { TEST(UnitTest, GigabytesScale) { prometheus::Registry reg; auto& fam = reg.gauge("disk_usage_bytes", "Disk usage") - .required(UnitLabels::Key::service) - .unit(prometheus::units::gigabytes) - .build(); + .required(UnitLabels::Key::service) + .unit(prometheus::units::gigabytes) + .build(); - fam.get({.service = "db"}).set(2); // 2 gigabytes + fam.get({.service = "db"}).set(2); // 2 gigabytes auto out = reg.serialize(); // 2 * 1e9 = 2000000000 (to_chars may use scientific notation) - bool found = out.find("2000000000") != std::string::npos - || out.find("2e+09") != std::string::npos - || out.find("2e+9") != std::string::npos - || out.find("2e9") != std::string::npos; + bool found = out.find("2000000000") != std::string::npos || + out.find("2e+09") != std::string::npos || out.find("2e+9") != std::string::npos || + out.find("2e9") != std::string::npos; EXPECT_TRUE(found) << "Output: " << out; } TEST(UnitTest, PercentToRatio) { prometheus::Registry reg; auto& fam = reg.gauge("cpu_usage_ratio", "CPU usage") - .required(UnitLabels::Key::service) - .unit(prometheus::units::percent) - .build(); + .required(UnitLabels::Key::service) + .unit(prometheus::units::percent) + .build(); - fam.get({.service = "web"}).set(75); // 75% + fam.get({.service = "web"}).set(75); // 75% auto out = reg.serialize(); // 75 * 0.01 = 0.75 @@ -58,13 +55,13 @@ TEST(UnitTest, PercentToRatio) { TEST(UnitTest, MillisecondsScale) { prometheus::Registry reg; auto& fam = reg.histogram("latency_seconds", "Latency") - .required(UnitLabels::Key::service) - .unit(prometheus::units::milliseconds) - .buckets(100, 3) // 100ms, 200ms, +Inf - .build(); + .required(UnitLabels::Key::service) + .unit(prometheus::units::milliseconds) + .buckets(100, 3) // 100ms, 200ms, +Inf + .build(); auto& h = fam.get({.service = "api"}); - h.observe(150); // 150ms + h.observe(150); // 150ms auto out = reg.serialize(); // le=0.1 (100ms), le=0.2 (200ms) @@ -75,9 +72,9 @@ TEST(UnitTest, MillisecondsScale) { TEST(UnitTest, NoneUnitNoScaling) { prometheus::Registry reg; auto& fam = reg.counter("http_requests_total", "Requests") - .required(UnitLabels::Key::service) - .unit(prometheus::units::none) - .build(); + .required(UnitLabels::Key::service) + .unit(prometheus::units::none) + .build(); fam.get({.service = "api"}).inc(42); @@ -92,11 +89,11 @@ TEST(UnitTest, CustomUnit) { prometheus::Registry reg; auto& fam = reg.gauge("battery_volts", "Battery voltage") - .required(UnitLabels::Key::service) - .unit(millivolts) - .build(); + .required(UnitLabels::Key::service) + .unit(millivolts) + .build(); - fam.get({.service = "sensor"}).set(3300); // 3300mV + fam.get({.service = "sensor"}).set(3300); // 3300mV auto out = reg.serialize(); // 3300 * 0.001 = 3.3 @@ -117,12 +114,12 @@ TEST(UnitTest, UnitConstexpr) { TEST(UnitTest, RawScaleStillWorks) { prometheus::Registry reg; auto& fam = reg.gauge("custom_metric", "Custom") - .required(UnitLabels::Key::service) - .scale(0.001) - .build(); + .required(UnitLabels::Key::service) + .scale(0.001) + .build(); fam.get({.service = "api"}).set(5000); auto out = reg.serialize(); - EXPECT_NE(out.find("5"), std::string::npos); // 5000 * 0.001 = 5 + EXPECT_NE(out.find("5"), std::string::npos); // 5000 * 0.001 = 5 } From 7ff4afe0e8b79190b92b343e2e1ed97eb20e88df Mon Sep 17 00:00:00 2001 From: Stig Bakken Date: Mon, 29 Jun 2026 15:52:48 +0200 Subject: [PATCH 4/6] feat: add http_server_example; fix cache_line.hpp missing include examples/http_server_example.cpp exposes a Prometheus /metrics endpoint over cpp-httplib (closes the remaining example item in #1). Building it surfaced a pre-existing bug: detail/cache_line.hpp used std::hardware_destructive_interference_size without including , which broke every example build on AppleClang; add the include. --- examples/CMakeLists.txt | 5 ++ examples/http_server_example.cpp | 61 ++++++++++++++++++++++++ include/prometheus/detail/cache_line.hpp | 5 +- 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 examples/http_server_example.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 70af4df..f826c75 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -2,3 +2,8 @@ add_executable(basic_usage basic_usage.cpp) target_link_libraries(basic_usage PRIVATE prometheus::client) target_compile_features(basic_usage PRIVATE cxx_std_23) prometheus_set_warnings(basic_usage) + +add_executable(http_server_example http_server_example.cpp) +target_link_libraries(http_server_example PRIVATE prometheus::client httplib::httplib) +target_compile_features(http_server_example PRIVATE cxx_std_23) +prometheus_set_warnings(http_server_example) diff --git a/examples/http_server_example.cpp b/examples/http_server_example.cpp new file mode 100644 index 0000000..b3d0cde --- /dev/null +++ b/examples/http_server_example.cpp @@ -0,0 +1,61 @@ +#include +#include + +#include +#include + +// 1. Define your application's labels +PROMETHEUS_DEFINE_LABELS(HttpLabels, + (method, std::string_view), + (path, std::string_view), + (status_code, uint32_t)); + +// Listen address for the demo server. +static constexpr const char* kHost = "0.0.0.0"; +static constexpr int kPort = 8080; + +int main() { + // 2. Create a registry + prometheus::Registry registry; + + // 3. Register metric families + auto& requests = + registry.counter("http_requests_total", "Total HTTP requests") + .required(HttpLabels::Key::method, HttpLabels::Key::path, HttpLabels::Key::status_code) + .build(); + + auto& in_flight = + registry.gauge("http_requests_in_flight", "Requests currently being served") + .required(HttpLabels::Key::path) + .build(); + + // 4. Wire up the HTTP server + httplib::Server server; + + // Demo route — increments the request counter. + server.Get("/hello", [&](const httplib::Request&, httplib::Response& res) { + in_flight.get({.path = "/hello"}).inc(1); + res.set_content("Hello, world!\n", "text/plain"); + requests.get({.method = "GET", .path = "/hello", .status_code = 200u}).inc(); + in_flight.get({.path = "/hello"}).dec(1); + }); + + // Demo route — always returns 404 to show a different status_code label. + server.Get("/missing", [&](const httplib::Request&, httplib::Response& res) { + res.status = 404; + res.set_content("Not found\n", "text/plain"); + requests.get({.method = "GET", .path = "/missing", .status_code = 404u}).inc(); + }); + + // 5. The Prometheus scrape endpoint — serialize the registry to text format. + server.Get("/metrics", [&](const httplib::Request&, httplib::Response& res) { + res.set_content(registry.serialize(), "text/plain; version=0.0.4"); + requests.get({.method = "GET", .path = "/metrics", .status_code = 200u}).inc(); + }); + + std::cout << "Serving metrics on http://" << kHost << ':' << kPort << "/metrics\n" + << "Try: curl http://localhost:" << kPort << "/hello\n"; + + server.listen(kHost, kPort); + return 0; +} diff --git a/include/prometheus/detail/cache_line.hpp b/include/prometheus/detail/cache_line.hpp index b8d2d4f..96f1964 100644 --- a/include/prometheus/detail/cache_line.hpp +++ b/include/prometheus/detail/cache_line.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include // std::hardware_destructive_interference_size namespace prometheus::detail { @@ -9,9 +10,9 @@ namespace prometheus::detail { // but some implementations don't provide it or warn about ABI instability. // Fall back to 64, which is correct for x86-64, ARM64, and POWER. #if defined(__cpp_lib_hardware_interference_size) && __cpp_lib_hardware_interference_size >= 201703L - inline constexpr std::size_t cache_line_size = std::hardware_destructive_interference_size; +inline constexpr std::size_t cache_line_size = std::hardware_destructive_interference_size; #else - inline constexpr std::size_t cache_line_size = 64; +inline constexpr std::size_t cache_line_size = 64; #endif } // namespace prometheus::detail From f5dc8963f80522b128ac09334c7fabd94812fb27 Mon Sep 17 00:00:00 2001 From: Stig Bakken Date: Tue, 30 Jun 2026 11:02:04 +0200 Subject: [PATCH 5/6] ci: make the PR benchmark gate robust The Bench jobs failed on this PR for two infra reasons, neither a real regression (the changes are a reformat plus a header include): - The alert fired only on per-benchmark _stddev/_cv aggregates, which measure run-to-run noise on shared CI runners, not performance. They swing >15% routinely and would fail every PR. Strip them with jq before the compare so the gate is on the mean/median timings only. - comment-on-alert hit "Resource not accessible by integration" (403): the workflow granted contents/deployments write but not pull-requests write. Add pull-requests: write. --- .github/workflows/ci.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4eb8078..2290fec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ on: permissions: contents: write deployments: write + pull-requests: write # github-action-benchmark posts comment-on-alert on PRs jobs: build: @@ -188,13 +189,24 @@ jobs: comment-on-alert: true fail-on-alert: false + # The PR gate compares against the mean/median means only. Google + # Benchmark also emits per-benchmark _stddev and _cv aggregates, which + # measure run-to-run noise on a shared CI runner, not performance — they + # routinely swing >15% and would fail every PR. Drop them before compare. + - name: Strip noise aggregates for PR comparison + if: github.event_name == 'pull_request' + run: | + jq '.benchmarks |= map(select((.aggregate_name // "") as $a + | $a != "stddev" and $a != "cv"))' \ + bench_results.json > bench_results_pr.json + - name: Compare benchmarks (PR) if: github.event_name == 'pull_request' uses: benchmark-action/github-action-benchmark@v1 with: name: "Benchmarks (${{ matrix.arch }})" tool: googlecpp - output-file-path: bench_results.json + output-file-path: bench_results_pr.json github-token: ${{ secrets.GITHUB_TOKEN }} auto-push: false gh-pages-branch: gh-pages From b7006d2704adb56b10050d0a605be8a6c1030ecd Mon Sep 17 00:00:00 2001 From: Stig Bakken Date: Tue, 30 Jun 2026 11:09:28 +0200 Subject: [PATCH 6/6] perf: hard-code cache_line_size to 64 instead of the std constant The earlier include fixed the AppleClang build but had a side effect: with guaranteed visible, libstdc++/aarch64 takes the std::hardware_destructive_interference_size branch, which is 256 there. That over-pads the per-counter and per-bucket structures and slowed the multithreaded histogram/counter benchmarks ~1.2-1.25x on arm64 CI (only the _MT means regressed; single-threaded ones did not). Hard-code 64 instead: it is the destructive-interference size on the targets this library supports (the header comment already said so), it compiles everywhere without , and it removes the include-order hazard where the value could differ between translation units. --- include/prometheus/detail/cache_line.hpp | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/include/prometheus/detail/cache_line.hpp b/include/prometheus/detail/cache_line.hpp index 96f1964..e42699d 100644 --- a/include/prometheus/detail/cache_line.hpp +++ b/include/prometheus/detail/cache_line.hpp @@ -1,18 +1,24 @@ #pragma once #include -#include // std::hardware_destructive_interference_size namespace prometheus::detail { // Cache line size for avoiding false sharing. -// std::hardware_destructive_interference_size is the standard way (C++17) -// but some implementations don't provide it or warn about ABI instability. -// Fall back to 64, which is correct for x86-64, ARM64, and POWER. -#if defined(__cpp_lib_hardware_interference_size) && __cpp_lib_hardware_interference_size >= 201703L -inline constexpr std::size_t cache_line_size = std::hardware_destructive_interference_size; -#else +// +// Deliberately hard-coded to 64 rather than using +// std::hardware_destructive_interference_size: +// - 64 is the destructive-interference size on x86-64, ARM64 and POWER, the +// targets this library supports; +// - the standard constant is not provided by every standard library and warns +// about ABI instability on some; and +// - on libstdc++/aarch64 it evaluates to 256, which over-pads the per-counter +// and per-histogram-bucket structures, bloating the working set and +// measurably slowing multithreaded workloads. +// Hard-coding it also removes an include-order hazard: whether the standard +// constant was visible depended on whether / had been included by +// the time this header was processed, so the value could silently differ +// between translation units. inline constexpr std::size_t cache_line_size = 64; -#endif } // namespace prometheus::detail