{"id":50965023,"url":"https://github.com/aurimasniekis/cpp-prom","last_synced_at":"2026-06-18T19:01:24.704Z","repository":{"id":361178221,"uuid":"1253342978","full_name":"aurimasniekis/cpp-prom","owner":"aurimasniekis","description":"Client-independent C++23 Prometheus/OpenMetrics metric abstraction","archived":false,"fork":false,"pushed_at":"2026-05-29T12:50:39.000Z","size":92,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-29T14:17:40.317Z","etag":null,"topics":["cpp","cpp23","header-only","metrics","prometheus"],"latest_commit_sha":null,"homepage":"https://aurimasniekis.github.io/cpp-prom/","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aurimasniekis.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-29T11:16:15.000Z","updated_at":"2026-05-29T12:50:44.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/aurimasniekis/cpp-prom","commit_stats":null,"previous_names":["aurimasniekis/cpp-prom"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/aurimasniekis/cpp-prom","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-prom","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-prom/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-prom/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-prom/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aurimasniekis","download_url":"https://codeload.github.com/aurimasniekis/cpp-prom/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aurimasniekis%2Fcpp-prom/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34503511,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-18T02:00:06.871Z","response_time":128,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cpp","cpp23","header-only","metrics","prometheus"],"created_at":"2026-06-18T19:01:22.211Z","updated_at":"2026-06-18T19:01:24.697Z","avatar_url":"https://github.com/aurimasniekis.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# prom\n\n[![CI](https://github.com/aurimasniekis/cpp-prom/actions/workflows/ci.yml/badge.svg)](https://github.com/aurimasniekis/cpp-prom/actions/workflows/ci.yml)\n[![Docs](https://github.com/aurimasniekis/cpp-prom/actions/workflows/docs.yml/badge.svg)](https://github.com/aurimasniekis/cpp-prom/actions/workflows/docs.yml)\n\n**A client-independent Prometheus / OpenMetrics metric abstraction for C++23.**\n\n`prom` lets you declare and record Prometheus-style metrics (`Counter`, `Gauge`,\n`Histogram`, `Summary`, `Untyped`, `Info`, `StateSet`) **without committing to a\nconcrete metrics client**. Your code records samples through `prom`'s small,\nstable API. A separate *adapter* decides where those samples actually go. Until\nan application installs a real backend, every metric resolves to a built-in\n`NullAdapter` that turns each operation into a safe, logged no-op — so code that\nrecords metrics runs unchanged whether or not a backend is present.\n\nThis is the headline use case: a **reusable library** can ship metric\ninstrumentation without forcing a Prometheus client dependency on everyone who\nlinks it. The **host application** installs a backend once, at startup.\n\n```cpp\n#include \u003cprom/prom.hpp\u003e\n\n// In a reusable library — no backend dependency, no registry to thread around:\nauto requests = prom::counter({.name = \"mylib_requests_total\",\n                               .help = \"Requests handled by mylib\"});\nrequests.inc();   // safe no-op until the application installs a backend\n```\n\n---\n\n## Why use this library?\n\nA C++ library that wants to expose operational metrics usually faces a choice:\nhard-depend on a specific Prometheus client (and impose it on every downstream\nuser), or expose nothing. `prom` removes that choice.\n\n- **Good for** instrumenting a library that should not dictate its consumers'\n  metrics stack.\n- **Good for** application code that wants a one-line `prom::counter(...)` API\n  and the freedom to wire (or not wire) a backend later.\n- **Avoids** leaking any backend type across the API boundary — the core never\n  includes a prometheus-cpp header.\n- **Useful when** you want metrics that are always safe to call, even before\n  (or without) any exporter installed.\n- **Useful when** you want to record dimensional quantities (seconds, bytes/s,\n  hertz) and have the unit carried along automatically.\n- **Not ideal for** a standalone application that already commits to one client\n  and wants its native API directly — the indirection buys you nothing there.\n- **Not ideal for** scrape/exposition itself: `prom` records samples; serving\n  `/metrics` is the backend adapter's job (see [Enabling a real\n  backend](#enabling-a-real-backend)).\n\n## Quick example\n\nThe smallest useful program. It records a few samples; with no backend\ninstalled they go to the `NullAdapter`.\n\n```cpp\n#include \u003cprom/prom.hpp\u003e\n\n#include \u003ciostream\u003e\n\nint main() {\n    // The free helpers create metrics through the process-wide registry.\n    // No Registry object to construct or pass around.\n    auto requests  = prom::counter({.name = \"demo_requests_total\", .help = \"demo\"});\n    auto in_flight = prom::gauge({.name = \"demo_in_flight\", .help = \"demo\"});\n\n    requests.inc();          // +1\n    requests.inc(10);        // +10\n    in_flight.set(3);\n    in_flight.dec();         // -1\n\n    std::cout \u003c\u003c \"backend = \"\n              \u003c\u003c prom::Registry::global()-\u003eadapter()-\u003ebackend_name() \u003c\u003c \"\\n\";\n    return 0;\n}\n```\n\nWhy it works:\n\n- `prom::counter(...)` / `prom::gauge(...)` are free functions that delegate to\n  the process-wide `Registry::global()`. You never have to create or pass a\n  registry for the common case.\n- Each metric is a cheap, copyable value. It binds to whatever adapter is\n  installed on the global cell on first use — here, the default `NullAdapter`.\n- Every mutation (`inc`, `set`, `dec`, ...) is `noexcept`. Nothing here can\n  throw, and nothing is exported, because no real backend is installed.\n\nRunning it prints `backend = null`.\n\n## Installation\n\n`prom`'s **core is header-only** and requires C++23. Its three runtime\ndependencies are fetched automatically by CMake when you don't already have them\ninstalled:\n\n- [commons](https://github.com/aurimasniekis/cpp-commons) — display metadata\n  (`comms::DisplayInfo`).\n- [logman](https://github.com/aurimasniekis/cpp-logman) — logging (over spdlog).\n- [dimval](https://github.com/aurimasniekis/cpp-dimval) — dimensional value\n  types (the core only matches them *structurally* — see [Dimensional\n  values](#recording-dimensional-dimval-values)).\n\n### CMake FetchContent (recommended)\n\n```cmake\ncmake_minimum_required(VERSION 3.25)\nproject(example LANGUAGES CXX)\n\nset(CMAKE_CXX_STANDARD 23)\nset(CMAKE_CXX_STANDARD_REQUIRED ON)\n\ninclude(FetchContent)\nFetchContent_Declare(\n    prom\n    URL      https://github.com/aurimasniekis/cpp-prom/archive/refs/tags/v0.1.0.tar.gz\n    URL_HASH SHA256=7a2cb15120646c6cb004df38911ef275a2c90f68b0ad2484ed434c3c8478a0e7\n    DOWNLOAD_EXTRACT_TIMESTAMP TRUE\n)\nFetchContent_MakeAvailable(prom)\n\nadd_executable(example main.cpp)\ntarget_link_libraries(example PRIVATE prom::prom)\n```\n\n`prom`'s own dependencies are each declared with `FIND_PACKAGE_ARGS`, so an\ninstalled copy is preferred over fetching when present.\n\n### CMake find_package (installed copy)\n\n```cmake\nfind_package(prom 0.1 REQUIRED)        # pulls commons / logman / dimval transitively\ntarget_link_libraries(app PRIVATE prom::prom)\n```\n\n### Enabling the prometheus-cpp backend\n\nThe real backend is an **opt-in module** built from source, gated by a CMake\noption. It is a compiled static library (not header-only):\n\n```bash\ncmake -S . -B build -DPROM_WITH_PROMETHEUS_CPP=ON\n```\n\n```cmake\ntarget_link_libraries(app PRIVATE prom::prom prom::prometheus_cpp)\n```\n\nThis pulls in [prometheus-cpp](https://github.com/jupp0r/prometheus-cpp) via\nFetchContent and exposes the `prom::prometheus_cpp` target.\n\n## Requirements\n\n- **C++ standard:** C++23 (`cxx_std_23`; `CMAKE_CXX_EXTENSIONS OFF`).\n- **CMake:** 3.25 or newer.\n- **Dependencies (core):** commons, logman (+ spdlog), dimval — fetched\n  automatically if not installed.\n- **Optional:** prometheus-cpp, only when `-DPROM_WITH_PROMETHEUS_CPP=ON`.\n\n## Core concepts\n\n### Metrics are value types with lazy binding\n\nEvery metric (`Counter`, `Gauge`, ...) is a small, copyable object holding a\n`shared_ptr` to shared per-series state. **Copies refer to the same series.**\n\n```cpp\nprom::Counter a{\"shared_total\", \"h\"};   // standalone, unbound\nprom::Counter b = a;                    // a copy\na.inc(1);\nb.inc(2);                               // same series — total is 3\n```\n\nA *standalone* metric (constructed directly from `name`/`help` or a spec) is\n**unbound** until first use; on the first `inc`/`set`/`observe` it binds to\nwhatever adapter the process-wide cell currently holds and registers itself.\n\n### `Registry` — the front door for registered metrics\n\nA `Registry` owns the adapter its metrics record through and validates each spec\nup front. You rarely need to construct one: `Registry::global()` is a\nprocess-wide instance the free `prom::counter(...)` helpers delegate to.\n\n```cpp\nauto reg = prom::Registry::global();\nauto c   = reg-\u003ecounter({.name = \"requests_total\", .help = \"...\"});\n```\n\n`Registry` is non-copyable and `shared_ptr`-managed: obtain one with\n`Registry::create(...)` or `Registry::global()` and call it through `-\u003e`. Reach\nfor an explicit registry when you want an independent adapter (e.g. in a test):\n\n```cpp\nauto backend = std::make_shared\u003cmy::Backend\u003e();\nauto pinned  = prom::Registry::create(backend);   // its own adapter cell\nauto c       = pinned-\u003ecounter({.name = \"x_total\", .help = \"...\"});\n```\n\n#### Registry-level prefix and labels\n\nA registry can also **decorate** every metric it creates with a shared name\nprefix, default constant labels, and default display — the same thing a\n[`Scope`](#per-library-scopes-prefix--default-labels) does, but applied at the\nregistry level. Pass a `RegistryConfig` to `create`:\n\n```cpp\nauto reg = prom::Registry::create(backend, {.prefix       = \"svc_\",\n                                            .const_labels = prom::Labels{{\"region\", \"eu\"}}});\nauto c   = reg-\u003ecounter({.name = \"requests_total\"});   // -\u003e svc_requests_total{region=\"eu\"}\n```\n\nThe decoration is **live**: every metric the registry has created — *including\nones created before the change* — re-registers under the new name/labels on its\nnext use (a metric's own label still wins over a registry default on a name\ncollision). The setters work even on a registry created without a config (its\ndecoration simply starts empty):\n\n```cpp\nreg-\u003eset_prefix(\"api_\");          // c re-registers as api_requests_total\nreg-\u003eadd_const_label(\"az\", \"eu-1\");\n```\n\nA registry whose decoration is empty leaves metrics with their spec names\nverbatim and reports `scoped == false` from `metrics()`.\n\n#### A process-wide prefix on `Registry::global()`\n\n`Registry::global()`'s decoration is the **process-wide** one, shared by\nstandalone metrics (`prom::counter(...)` and direct constructors) and used as the\nchain parent of every [`Scope`](#per-library-scopes-prefix--default-labels). So a\nprefix or label installed there reaches *every* metric in the process:\n\n```cpp\nprom::Registry::global()-\u003eset_prefix(\"svc_\");      // every metric gains svc_*\nprom::Registry::global()-\u003eadd_const_label(\"region\", \"eu\");\n```\n\nWhen a scope sits underneath, the two compose with the global prefix outermost —\na scope `foo_` under a global `reg_` yields `reg_foo_meter`. Label precedence is\n**own → scope → global**.\n\n### `Adapter` — the backend boundary\n\nEverything funnels through one interface, `prom::Adapter`, in a\n**register-then-mutate** model: a family is registered once\n(`register_metric`), labeled children are obtained with `resolve`, and samples\nare pushed with `inc`/`dec`/`set`/`observe`/`set_info`/`set_state`. No\nbackend-specific type ever crosses this line. Every adapter method is `noexcept`\nand may be called concurrently.\n\nThe default `NullAdapter` records nothing: it logs registration at debug,\nmutations at trace, and is stateless and fully thread-safe.\n\n### Installing a backend on a cell\n\nThe adapter does **not** live on each metric; it lives on an `AdapterCell`\nshared by the metrics that read from it. `Registry::global()` and all standalone\n/ scoped metrics share the *process-wide* cell, so installing a backend there\nreconfigures everything at once:\n\n```cpp\nprom::Registry::global()-\u003eset_adapter(std::make_shared\u003cmy::Backend\u003e());\n// passing nullptr resets the cell back to a fresh NullAdapter\n```\n\n## Common usage patterns\n\n### Instrumenting a library with no backend dependency\n\nThis is what `prom` is for. The library declares metrics through the free\nhelpers and never mentions a backend:\n\n```cpp\n#include \u003cprom/prom.hpp\u003e\n\nnamespace mylib {\n\nclass Telemetry {\npublic:\n    static Telemetry\u0026 instance() {\n        static Telemetry t;\n        return t;\n    }\n\n    prom::Counter   requests = prom::counter(\n        {.name = \"mylib_requests_total\", .help = \"Total requests handled by mylib\"});\n    prom::Histogram latency  = prom::histogram(\n        {.name = \"mylib_request_seconds\", .help = \"Request latency in seconds\"});\n};\n\nvoid handle_request(double seconds) {\n    Telemetry::instance().requests.inc();\n    Telemetry::instance().latency.observe(seconds);\n}\n\n}  // namespace mylib\n```\n\nIf the application never installs a backend, `mylib` still runs — the metrics\nresolve to the `NullAdapter`. If it does, the same code starts exporting.\n\n### Every metric type\n\n```cpp\n#include \u003cprom/prom.hpp\u003e\n\nauto reg = prom::Registry::create();\n\nauto counter = reg-\u003ecounter({.name = \"events_total\", .help = \"events\"});\nauto gauge   = reg-\u003egauge({.name = \"queue_depth\", .help = \"queue depth\"});\nauto hist    = reg-\u003ehistogram({.name = \"op_seconds\", .help = \"op latency\",\n                               .buckets = {0.1, 0.5, 1.0}});\nauto summ    = reg-\u003esummary({.name = \"payload_bytes\", .help = \"payload sizes\"});\nauto unt     = reg-\u003euntyped({.name = \"external_value\", .help = \"raw\"});\nauto info    = reg-\u003einfo({.name = \"build_info\", .help = \"build metadata\"});\nauto state   = reg-\u003estateset({.name = \"service_state\", .help = \"lifecycle\",\n                              .states = {\"starting\", \"running\", \"stopped\"}});\n\ncounter.inc();                 // +1\ncounter.inc(7);                // +7\ngauge.set(5);\ngauge.dec();                   // -1\nhist.observe(0.3);\nsumm.observe(2048);\nunt.set(-1.5);                 // untyped: no sign constraints\ninfo.set({{\"version\", \"0.1.0\"}, {\"commit\", \"deadbeef\"}});\nstate.set(\"running\", true);    // one boolean member of the set\n```\n\nWhat each type does:\n\n| Type        | Mutators                                       | Notes                                                                        |\n|-------------|------------------------------------------------|------------------------------------------------------------------------------|\n| `Counter`   | `inc()`, `inc(v)`                              | Monotonic. Negative / non-finite increments are dropped + logged.            |\n| `Gauge`     | `set(v)`, `inc()`, `inc(v)`, `dec()`, `dec(v)` | Moves both directions.                                                       |\n| `Histogram` | `observe(v)`                                   | Default buckets `{.005,.01,.025,.05,.1,.25,.5,1,2.5,5,10}` when unspecified. |\n| `Summary`   | `observe(v)`                                   | Default quantiles `{0.5, 0.9, 0.99}` when unspecified.                       |\n| `Untyped`   | `set(v)`                                       | No semantic constraints.                                                     |\n| `Info`      | `set({{k, v}, ...})`, `set(Labels)`            | Static key/value metadata; rendered as `name_info{...} 1`.                   |\n| `StateSet`  | `set(state, bool)`                             | A set of related boolean states; one series per state.                       |\n\n### Recording raw arithmetic values\n\nThe simplest path. Any arithmetic type works — it routes through\n`prom::normalize()` to a `double`:\n\n```cpp\nauto bytes = reg-\u003ecounter({.name = \"bytes_processed_total\", .help = \"bytes\"});\nauto temp  = reg-\u003egauge({.name = \"cpu_celsius\", .help = \"temperature\"});\n\nbytes.inc(512);     // int\nbytes.inc(1024U);   // unsigned\ntemp.set(41.7);     // double\ntemp.set(39);       // int — fine\n```\n\n### Recording dimensional (dimval) values\n\nA dimensional value carries its unit and dimensional *kind* alongside the\nmagnitude. `prom` reduces it to a `{value, Unit}` pair and **infers the metric's\nunit from the first dimensional sample it sees**:\n\n```cpp\n#include \u003cprom/prom.hpp\u003e\n#include \u003cdimval/dimval.hpp\u003e\n\nauto latency    = reg-\u003ehistogram({.name = \"rpc_seconds\",     .help = \"RPC latency\"});\nauto throughput = reg-\u003egauge    ({.name = \"link_byte_rate\",  .help = \"throughput\"});\nauto tuned      = reg-\u003egauge    ({.name = \"radio_center_hz\", .help = \"tuned freq\"});\n\nlatency.observe(dimval::SecondValue{0.0123});\nthroughput.set(dimval::ByteRateValue{1.25e6});\ntuned.set(dimval::CenterFrequencyValue{100.3e6});\n```\n\n`prom` never includes a dimval header. The `DimensionalValue` concept matches\ndimval's value types *structurally* (it just needs `value_t`,\n`numeric_as_double()`, and a unit descriptor), so handing `prom` a dimensional\nvalue does not drag dimval's concrete types into `prom`'s own translation units.\n\n\u003e **Pitfall — unit-kind mismatch.** Once a metric has latched a unit (declared\n\u003e or inferred), a later dimensional sample whose *kind* disagrees (e.g. a length\n\u003e value into a metric that latched `time`) is **dropped and logged, never\n\u003e thrown**. A raw (unitless) sample always passes. See [Edge cases](#edge-cases-and-pitfalls).\n\n### Labeled child series\n\nConstant labels apply to the whole family; dynamic labels select a child series\nvia `.labels(...)`:\n\n```cpp\nauto requests = reg-\u003ecounter({.name   = \"http_requests_total\",\n                              .help   = \"HTTP requests\",\n                              .labels = prom::Labels{{\"service\", \"api\"}}});  // constant\n\nrequests.labels(prom::Labels{{\"method\", \"GET\"},  {\"code\", \"200\"}}).inc();\nrequests.labels(prom::Labels{{\"method\", \"GET\"},  {\"code\", \"200\"}}).inc();   // same child\nrequests.labels(prom::Labels{{\"method\", \"POST\"}, {\"code\", \"500\"}}).inc();\n```\n\n`prom::Labels` is kept sorted by name with duplicates collapsed (last write\nwins), and caches an FNV-1a hash so it can key the backend's child cache.\n\n\u003e **Pitfall — labeled children are pinned.** A child snapshots its adapter (and\n\u003e scope-decorated state) at the moment `labels()` is called and **never\n\u003e migrates** if the adapter or scope later changes. Resolve children *after* the\n\u003e backend is installed.\n\n### Per-library scopes (prefix + default labels)\n\nA `Scope` is to `prom` what a named logger is to a logging library: one instance\nper library, with a shared name prefix, default constant labels, and default\ndisplay metadata. It is registered process-wide by name.\n\n```cpp\n#include \u003cprom/scope.hpp\u003e\n\nauto lib = prom::scope(\"mylib\", {.prefix = \"mylib_\",\n                                 .const_labels = prom::Labels{{\"component\", \"io\"}}});\n\nauto c = lib-\u003ecounter({.name = \"requests_total\", .help = \"...\"});\nc.inc();   // exported as mylib_requests_total{component=\"io\"}\n```\n\nThe scope config is **live**, not copied at creation: changing the prefix or\ndefault labels reconfigures every metric already created from the scope, and\nsubsequent samples flow to the newly-derived series.\n\n```cpp\nlib-\u003eset_prefix(\"srv_\");          // existing metrics re-register under srv_*\nlib-\u003eadd_const_label(\"region\", \"eu\");\n```\n\nA *user* of the library can fetch the same scope by name and adjust it:\n`prom::scope(\"mylib\")` returns the existing instance (the config argument is\nignored once a scope exists — reconfigure through the setters instead).\n\n### Fanning out to several backends\n\n`CompositeAdapter` forwards every call to a fixed list of child adapters —\nuseful for teeing metrics to two exporters during a migration, or feeding a real\nbackend and a test recorder at once:\n\n```cpp\n#include \u003cprom/prom.hpp\u003e\n\nauto composite = std::make_shared\u003cprom::CompositeAdapter\u003e(\n    std::vector\u003cprom::AdapterPtr\u003e{backend_a, backend_b});\nprom::Registry::global()-\u003eset_adapter(composite);\n```\n\nNull entries in the list are dropped. The composite needs no locking of its own\n(the list is fixed at construction), assuming each child honours the adapter\nthreading contract.\n\n### Enumerating what has been declared\n\n```cpp\nfor (const prom::MetricInfo\u0026 m : prom::Registry::global()-\u003emetrics()) {\n    // m.type, m.name, m.help, m.const_labels, m.unit, m.scoped\n}\nfor (const auto\u0026 s : prom::scopes()) { /* s-\u003emetrics() ... */ }\nauto names = prom::scope_names();\n```\n\n`metrics()` returns read-only snapshots — including declared-but-not-yet-used\nmetrics — with a scope's effective name and labels computed live. The `Unit`\nstring views in a `MetricInfo` reference the live metric's storage, so use a\nsnapshot only while that metric is alive.\n\n## Enabling a real backend\n\nThe prometheus-cpp adapter (`prom::prometheus_cpp`, gated by\n`-DPROM_WITH_PROMETHEUS_CPP=ON`) is what a host installs to actually export:\n\n```cpp\n#include \u003cprom/prom.hpp\u003e\n#include \u003cprom/prometheus_cpp/adapter.hpp\u003e\n\n#include \u003cprometheus/registry.h\u003e\n#include \u003cprometheus/text_serializer.h\u003e\n\n#include \u003ciostream\u003e\n#include \u003cmemory\u003e\n\nint main() {\n    // 1. Install the backend once, at startup.\n    auto adapter = std::make_shared\u003cprom::prometheus_cpp::PrometheusCppAdapter\u003e();\n    prom::Registry::global()-\u003eset_adapter(adapter);\n\n    // 2. Library code records through prom exactly as before.\n    auto reg      = prom::Registry::global();\n    auto requests = reg-\u003ecounter({.name   = \"http_requests_total\",\n                                  .help   = \"Total HTTP requests\",\n                                  .labels = prom::Labels{{\"service\", \"api\"}}});\n    auto latency  = reg-\u003ehistogram({.name = \"http_request_seconds\", .help = \"Latency\",\n                                    .buckets = {0.05, 0.1, 0.25, 0.5, 1.0}});\n\n    requests.inc();\n    requests.labels(prom::Labels{{\"code\", \"200\"}}).inc(3);\n    latency.observe(0.08);\n    latency.observe(0.42);\n\n    // 3. Expose adapter-\u003eregistry() through an HTTP scrape endpoint, or render it:\n    std::cout \u003c\u003c prometheus::TextSerializer().Serialize(adapter-\u003eregistry().Collect());\n}\n```\n\nwhich yields standard Prometheus exposition text:\n\n```\n# HELP http_requests_total Total HTTP requests\n# TYPE http_requests_total counter\nhttp_requests_total{service=\"api\",code=\"200\"} 3\nhttp_requests_total{service=\"api\"} 1\n# HELP http_request_seconds Latency\n# TYPE http_request_seconds histogram\nhttp_request_seconds_count 2\nhttp_request_seconds_sum 0.5\nhttp_request_seconds_bucket{le=\"0.05\"} 0\nhttp_request_seconds_bucket{le=\"0.1\"} 1\n...\n```\n\n**Type mapping.** Counter / Gauge / Histogram / Summary map to their direct\nprometheus-cpp equivalents. `Untyped` maps to a Gauge. `Info` maps to a `\u003cname\u003e`\nGauge whose label set carries the payload at value `1`. `StateSet` maps to a\nGauge family with one series per state, each `0` or `1`.\n\nPass an existing registry to the adapter constructor\n(`PrometheusCppAdapter{my_registry}`) when you already have one wired to a\n`prometheus::Exposer`.\n\n## Error handling\n\n`prom` splits errors cleanly between *definition time* and *recording time*.\n\n**Definition / registration validates.** Each `Registry` factory checks its\nspec:\n\n- The throwing factories (`counter`, `gauge`, ...) raise `prom::Exception` on a\n  bad spec.\n- The `noexcept` mirrors (`try_counter`, `try_gauge`, ...) return\n  `prom::expected\u003cT\u003e` (an alias for `std::expected\u003cT, prom::Error\u003e`) instead.\n\n```cpp\n// Throwing form:\ntry {\n    auto bad = prom::counter({.name = \"9bad\", .help = \"h\"});  // invalid name\n} catch (const prom::Exception\u0026 e) {\n    std::cerr \u003c\u003c e.what() \u003c\u003c '\\n';                 // \"invalid metric name: 9bad\"\n}\n\n// noexcept form — no exceptions, inspect the result:\nprom::expected\u003cprom::Gauge\u003e g = prom::try_gauge({.name = \"9bad\", .help = \"h\"});\nif (!g) {\n    // g.error().code == prom::ErrorCode::InvalidMetricName\n    // g.error().message == \"9bad\"\n}\n```\n\nWhat is validated:\n\n- Metric name against `[a-zA-Z_][a-zA-Z0-9_]*` (`InvalidMetricName`).\n- Label names, which additionally reject the reserved `__` prefix\n  (`InvalidLabelName`).\n- Histogram buckets must be non-empty, finite, and strictly increasing\n  (`InvalidBuckets`).\n- Summary quantiles must lie in the open interval `(0, 1)` (`InvalidQuantiles`).\n- A state set must declare at least one state (`EmptyStateSet`).\n\n**All mutations are `noexcept`.** Once a metric exists, recording never throws.\nInvalid samples are *dropped and logged* (see below). The no-client path never\nthrows because a metric always resolves to at least the `NullAdapter`.\n\n\u003e Note: the `EmptyHelp` error code exists in the enum, but help text is **not**\n\u003e currently rejected by the factories — an empty `.help` is accepted (inferred\n\u003e from the validation code). Supply meaningful help text anyway; backends and\n\u003e dashboards rely on it.\n\n## Edge cases and pitfalls\n\n**Negative counter increment.** Dropped and logged; the counter stays\nmonotonic.\n\n```cpp\nauto c = prom::counter({.name = \"x_total\", .help = \"h\"});\nc.inc(-5);   // no-op, logs a warning. c.inc(5) is fine.\n```\n\n**Non-finite sample (NaN / Inf).** Dropped and logged on any\n`inc`/`set`/`dec`/`observe`.\n\n**Unit-kind mismatch.** The first dimensional sample latches the metric's unit\n(name, kind, symbol). A later dimensional sample of a *different kind* is dropped\nand logged — never thrown. Declare a unit in the spec if you need it fixed up\nfront.\n\n**Adapter swap orphans old series.** `set_adapter(...)` re-registers each metric\nagainst the new backend on its *next use*, but series already written to the\nprevious backend stay there (backends cannot move a registered series). **Install\nthe backend before the bulk of your samples flow.**\n\n**Labeled children do not migrate.** A child created with `labels()` pins its\nbinding at creation. If you swap the adapter (or reconfigure the scope)\nafterward, the existing child keeps recording to the old binding. Re-resolve\nchildren after the swap.\n\n**Standalone metric used before the backend is installed.** It binds to the\n`NullAdapter` on first use, then re-binds to the real backend on the next use\nafter `set_adapter(...)` — but anything recorded in between went to the\n`NullAdapter` and is lost. Again: install early.\n\n**`set_unit` is best-effort.** The prometheus-cpp backend cannot rename an\nalready-registered family, so late (inferred) units affect `prom`'s own\nbookkeeping and the sample-dropping kind reconciliation, **not** the exported\nseries name. If you want the unit in the name, declare it in the spec.\n\n**Duplicate labels collapse.** `prom::Labels{{\"k\",\"a\"},{\"k\",\"b\"}}` keeps only\n`k=\"b\"` (last write wins), and labels are always sorted by name.\n\n**`MetricInfo` lifetime.** The `Unit` string views in an enumeration snapshot\npoint into the live metric. Don't keep a snapshot past the metric's lifetime.\n\n## API overview\n\nA compact map of the public surface a typical user reaches for first.\n\n| API                                                                 | Purpose                                              | Notes                                       |\n|---------------------------------------------------------------------|------------------------------------------------------|---------------------------------------------|\n| `prom::counter/gauge/histogram/summary/untyped/info/stateset(spec)` | Create a metric via the global registry              | Throws `prom::Exception` on a bad spec.     |\n| `prom::try_counter(...)` (and the rest)                             | `noexcept` mirrors                                   | Return `prom::expected\u003cT\u003e`.                 |\n| `prom::Registry::global()`                                          | Process-wide registry                                | Shares the global adapter cell.             |\n| `prom::Registry::create(adapter)`                                   | An independent registry with its own cell            | For tests / embedders.                      |\n| `prom::Registry::create(adapter, config)`                           | A registry that decorates its metrics                | Live prefix / labels / display, like a scope. |\n| `Registry::set_adapter(ptr)`                                        | Install / swap the backend (or reset with `nullptr`) | Existing metrics re-register on next use.   |\n| `Registry::metrics()`                                               | Enumerate declared metrics as `MetricInfo`           | Includes unused ones.                       |\n| `prom::scope(name[, config])`                                       | Get-or-create a named per-library scope              | Live, reconfigurable prefix/labels/display. |\n| `prom::scopes()` / `scope_names()` / `find_scope(name)`             | Enumerate / look up scopes                           | `find_scope` returns `nullptr` if absent.   |\n| `Metric::inc/dec/set/observe(...)`                                  | Record a sample                                      | `noexcept`; raw or dimensional value.       |\n| `Metric::labels(Labels)`                                            | A same-type labeled child                            | Pinned binding (see pitfalls).              |\n| `prom::Labels`                                                      | Sorted, deduped, hashed label set                    | `{{ \"k\", \"v\" }, ...}`.                      |\n| `prom::Adapter`                                                     | The backend interface                                | Subclass to write a new backend.            |\n| `prom::NullAdapter`                                                 | The default no-op backend                            | Always available, thread-safe.              |\n| `prom::CompositeAdapter`                                            | Fan out to several backends                          | Fixed child list.                           |\n| `prom::prometheus_cpp::PrometheusCppAdapter`                        | The prometheus-cpp backend                           | Opt-in module.                              |\n\n## Examples\n\nAll examples live in `examples/` and build against the `NullAdapter` (no backend\nneeded), except the prometheus-cpp one.\n\n| Example                                                   | Demonstrates                                                              |\n|-----------------------------------------------------------|---------------------------------------------------------------------------|\n| `examples/null_only.cpp`                                  | The minimal program: metrics with only the `NullAdapter`. Start here.     |\n| `examples/library_metrics.cpp`                            | The headline use case: a library instrumented with no backend dependency. |\n| `examples/raw_values.cpp`                                 | Recording plain arithmetic values of various types.                       |\n| `examples/dimval_values.cpp`                              | Recording dimensional (dimval) values; unit inference.                    |\n| `examples/labeled.cpp`                                    | Labeled child series from one family.                                     |\n| `examples/all_metric_types.cpp`                           | Exercises every metric type once.                                         |\n| `adapters/prometheus_cpp/examples/prometheus_backend.cpp` | Installs the real backend and prints scrape text.                         |\n\n## Testing\n\nThe test suite uses GoogleTest (fetched automatically). The convenience\n`Makefile` wraps the common CMake/CTest invocations:\n\n```bash\nmake test          # configure + build + run core + NullAdapter tests\nmake prometheus    # same, with -DPROM_WITH_PROMETHEUS_CPP=ON (real backend + scrape tests)\nmake examples      # build and run every example, asserting exit 0\nmake sanitize      # ASan + UBSan\nmake release       # Release build + tests\nmake tidy          # clang-tidy\nmake format-check  # clang-format --dry-run --Werror\nmake ci            # the full pre-push gate (all of the above)\n```\n\nOr drive CMake directly:\n\n```bash\ncmake -S . -B build\ncmake --build build\nctest --test-dir build --output-on-failure\n```\n\n### CMake options\n\n| Option                     | Default   | Meaning                                   |\n|----------------------------|-----------|-------------------------------------------|\n| `PROM_BUILD_TESTS`         | top-level | Build the GoogleTest suite.               |\n| `PROM_BUILD_EXAMPLES`      | top-level | Build the examples.                       |\n| `PROM_BUILD_DOCS`          | `OFF`     | Build Doxygen HTML.                       |\n| `PROM_ENABLE_CLANG_TIDY`   | `OFF`     | Run clang-tidy during the build.          |\n| `PROM_ENABLE_SANITIZERS`   | `OFF`     | ASan + UBSan (Debug).                     |\n| `PROM_ENABLE_COVERAGE`     | `OFF`     | Clang source-based coverage.              |\n| `PROM_WARNINGS_AS_ERRORS`  | top-level | `-Werror` / `/WX`.                        |\n| `PROM_INSTALL`             | top-level | Generate install/export rules.            |\n| `PROM_WITH_PROMETHEUS_CPP` | `OFF`     | Build the prometheus-cpp backend adapter. |\n\n## FAQ\n\n**Do I need to link a library, or is it header-only?** The core is header-only —\nlink the `prom::prom` interface target (which carries the include paths and its\nheader-only dependencies). The optional prometheus-cpp adapter\n(`prom::prometheus_cpp`) is a compiled static library.\n\n**What happens if I record an invalid sample?** Mutations never throw. A negative\ncounter increment, a NaN/Inf value, or a unit-kind mismatch is dropped and\nlogged. Only *definition* (creating a metric with a bad spec) can fail — and you\nchoose throwing (`counter`) or `expected`-returning (`try_counter`) factories.\n\n**Can I use this from multiple threads?** Yes. Metric mutation and binding are\nsafe from multiple threads; adapter access on a cell is mutex-guarded and hands\nout a `shared_ptr` copy so a concurrent swap can't invalidate an in-flight\ncaller. Backends self-synchronize (`NullAdapter` is stateless; the\nprometheus-cpp adapter guards family creation and relies on prometheus-cpp's\natomic series).\n\n**Does a metric own its data or borrow it?** A metric owns its series state via a\n`shared_ptr`; copies share it. The transient `MetricMeta`/`MetricInfo` views and\n`Unit` string views *borrow* from that state and are only valid while it lives.\n\n**What if no backend is ever installed?** Everything works as a logged no-op\nthrough the `NullAdapter`. Nothing is exported, nothing throws.\n\n**How do I see the no-op logging?** It goes through `logman`/spdlog under the\n`prom`, `prom.null`, and `prom.composite` channels (registration at debug,\nmutations at trace). Configure your logman/spdlog level to surface it.\n\n**Which compiler versions work?** Not formally documented. The code requires\nC++23 and is built with GCC and Apple Clang/libc++ per the build files. Treat\nspecific versions as inferred.\n\n## Contributing\n\nContributions to the library are welcome! If you encounter any issues or have suggestions for\nimprovements,\nplease feel free to submit a pull request or open an issue on the project's repository.\n\n## License\n\nThis project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faurimasniekis%2Fcpp-prom","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faurimasniekis%2Fcpp-prom","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faurimasniekis%2Fcpp-prom/lists"}