diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dfd3b23ee..89d6ccc83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ jobs: name: cabal / ghc-${{ matrix.ghc }} / ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: - ubuntu-latest @@ -52,10 +53,48 @@ jobs: # run: | # cabal test package:prometheus --enable-tests + cabal-ghc-stats: + name: cabal / prometheus-ghc-stats / ghc-${{ matrix.ghc }} / ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + cabal: + - "latest" + # prometheus-ghc-stats is GHC2024 and uses RTSStats fields that only + # exist on newer compilers, so it requires GHC >= 9.10. + ghc: + - "9.10.3" + - "9.12.4" + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0 + id: setup-haskell-cabal + name: Setup Haskell + with: + ghc-version: ${{ matrix.ghc }} + cabal-version: ${{ matrix.cabal }} + + - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + name: Cache cabal-store + with: + path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }} + key: ${{ matrix.os }}-${{ matrix.ghc }}-cabal-ghc-stats + + - name: Build + run: | + cabal update + cabal build all --project-dir=prometheus-ghc-stats --enable-tests --enable-benchmarks --write-ghc-environment-files=always + stack: name: stack ${{ matrix.plan.resolver }} / ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: - ubuntu-latest diff --git a/.gitignore b/.gitignore index ede39fd7c..a1fce0828 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ .tix cabal.sandbox.config dist/ +dist-newstyle/ **/.stack-work/ diff --git a/prometheus-ghc-stats/CHANGELOG.md b/prometheus-ghc-stats/CHANGELOG.md new file mode 100644 index 000000000..5f150d296 --- /dev/null +++ b/prometheus-ghc-stats/CHANGELOG.md @@ -0,0 +1,5 @@ + +## 0.1.0 + +* Initial release: `registerGHCStats` exports GHC RTS metrics through the + `prometheus` registry. diff --git a/prometheus-ghc-stats/LICENSE b/prometheus-ghc-stats/LICENSE new file mode 100644 index 000000000..b93422f8c --- /dev/null +++ b/prometheus-ghc-stats/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2016-present, Bitnomial, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/prometheus-ghc-stats/README.md b/prometheus-ghc-stats/README.md new file mode 100644 index 000000000..0fdddb0b5 --- /dev/null +++ b/prometheus-ghc-stats/README.md @@ -0,0 +1,34 @@ +# prometheus-ghc-stats + +Export GHC RTS metrics (from [`GHC.Stats`](https://hackage.haskell.org/package/base/docs/GHC-Stats.html)) +through the [`prometheus`](http://github.com/bitnomial/prometheus) registry. + +`registerGHCStats` registers a metric handle for each `RTSStats` field in the +current `RegistryT` and returns an `IO ()` action that snapshots the RTS stats +and writes each field to its handle. Compose that updater with the registry +sample so every scrape sees fresh values: + +```haskell +import System.Metrics.Prometheus.Concurrent.RegistryT (runRegistryT, sample) +import System.Metrics.Prometheus.GHCStats (registerGHCStats) +import System.Metrics.Prometheus.Http.Scrape (serveMetrics) + +main :: IO () +main = do + (updater, regSample) <- runRegistryT $ (,) <$> registerGHCStats <*> sample + serveMetrics 8080 ["metrics"] (updater >> regSample) +``` + +## Requirements + +* The executable must be run with `+RTS -T` to enable the RTS statistics. + Otherwise `getRTSStatsEnabled` returns `False`, the updater is a no-op, and + every metric reports `0`. +* Requires GHC >= 9.10. + +## Notes + +Time fields are exposed in nanoseconds (the unit `GHC.Stats` uses); divide by +`1e9` in PromQL for seconds. The `Counter` type in this `prometheus` fork is +`Int`-backed, so fractional seconds cannot be represented losslessly through +it. diff --git a/prometheus-ghc-stats/Setup.hs b/prometheus-ghc-stats/Setup.hs new file mode 100644 index 000000000..9a994af67 --- /dev/null +++ b/prometheus-ghc-stats/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/prometheus-ghc-stats/cabal.project b/prometheus-ghc-stats/cabal.project new file mode 100644 index 000000000..4b60f6f9e --- /dev/null +++ b/prometheus-ghc-stats/cabal.project @@ -0,0 +1,10 @@ +-- Project for building prometheus-ghc-stats against the in-tree prometheus +-- library. prometheus-ghc-stats is GHC2024 and requires GHC >= 9.10, so it is +-- kept out of the repo-root build (which targets prometheus across a wider GHC +-- range). Build from the repo root with: +-- +-- cabal build all --project-dir=prometheus-ghc-stats +-- +packages: + . + ../ diff --git a/prometheus-ghc-stats/prometheus-ghc-stats.cabal b/prometheus-ghc-stats/prometheus-ghc-stats.cabal new file mode 100644 index 000000000..d609ffeba --- /dev/null +++ b/prometheus-ghc-stats/prometheus-ghc-stats.cabal @@ -0,0 +1,49 @@ +cabal-version: 3.0 +name: prometheus-ghc-stats +version: 0.1.0 +synopsis: Export GHC RTS metrics through the Prometheus registry +homepage: http://github.com/bitnomial/prometheus +bug-reports: http://github.com/bitnomial/prometheus/issues +license: BSD-3-Clause +license-file: LICENSE +author: Bitnomial +maintainer: opensource@bitnomial.com +copyright: Bitnomial, Inc. (c) 2026 +category: Metrics, Monitoring, System +build-type: Simple +extra-source-files: README.md + , CHANGELOG.md +tested-with: GHC == 9.10.3 + , GHC == 9.12.4 + +description: + Export GHC RTS metrics (from "GHC.Stats") through the @prometheus@ registry. + . + 'System.Metrics.Prometheus.GHCStats.registerGHCStats' registers a handle for + each 'GHC.Stats.RTSStats' field in the current @RegistryT@ and returns an + @IO ()@ action that snapshots the RTS stats and updates every handle. Run the + executable with @+RTS -T@ to enable the underlying counters. + +library + hs-source-dirs: src/lib + default-language: GHC2024 + + ghc-options: -Wall + -Wmissing-home-modules + -Widentities + -Wincomplete-uni-patterns + -Wincomplete-record-updates + -Wpartial-fields + -Wmissing-export-lists + -Wno-unused-do-bind + -Wunused-packages + -funbox-strict-fields + + exposed-modules: System.Metrics.Prometheus.GHCStats + + build-depends: base >= 4.16 && < 5 + , prometheus >= 2.3 && < 2.5 + +source-repository head + type: git + location: https://github.com/bitnomial/prometheus diff --git a/prometheus-ghc-stats/src/lib/System/Metrics/Prometheus/GHCStats.hs b/prometheus-ghc-stats/src/lib/System/Metrics/Prometheus/GHCStats.hs new file mode 100644 index 000000000..c6c6970be --- /dev/null +++ b/prometheus-ghc-stats/src/lib/System/Metrics/Prometheus/GHCStats.hs @@ -0,0 +1,153 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- GHC RTS metrics exported through the 'prometheus' registry. +-- +-- 'registerGHCStats' registers metric handles in the current 'RegistryT' and +-- returns an 'IO' action that snapshots 'GHC.Stats.getRTSStats' and writes +-- each field to its handle. Compose the updater with the registry sample so +-- every scrape sees fresh values: +-- +-- @ +-- (updater, regSample) <- runRegistryT $ (,) \<$\> registerGHCStats \<*\> sample +-- serveMetrics port path (updater >> regSample) +-- @ +-- +-- Requires the executable to be run with @+RTS -T@; otherwise +-- 'getRTSStatsEnabled' returns 'False', the updater is a no-op, and every +-- metric reports @0@. +-- +-- Time fields are exposed in nanoseconds (the unit @GHC.Stats@ uses); divide +-- by @1e9@ in PromQL for seconds. The 'Counter' type in this @prometheus@ +-- fork is @Int@-backed, so fractional seconds cannot be represented +-- losslessly through it. +module System.Metrics.Prometheus.GHCStats ( + registerGHCStats, +) where + +import Control.Monad (when) +import Control.Monad.IO.Class (MonadIO) +import GHC.Stats ( + GCDetails (..), + RTSStats (..), + getRTSStats, + getRTSStatsEnabled, + ) +import System.Metrics.Prometheus.Concurrent.RegistryT ( + RegistryT, + registerCounter, + registerGauge, + ) +import System.Metrics.Prometheus.Metric.Counter (Counter) +import System.Metrics.Prometheus.Metric.Counter qualified as Counter +import System.Metrics.Prometheus.Metric.Gauge (Gauge) +import System.Metrics.Prometheus.Metric.Gauge qualified as Gauge + + +registerGHCStats :: MonadIO m => RegistryT m (IO ()) +registerGHCStats = do + cGcs <- counter "ghc_gcs_total" + cMajorGcs <- counter "ghc_major_gcs_total" + cAllocated <- counter "ghc_allocated_bytes_total" + cCopied <- counter "ghc_copied_bytes_total" + cParCopied <- counter "ghc_par_copied_bytes_total" + cCumLive <- counter "ghc_cumulative_live_bytes_total" + cCumParBalanced <- counter "ghc_cumulative_par_balanced_copied_bytes_total" + cInitCpu <- counter "ghc_init_cpu_ns_total" + cInitElapsed <- counter "ghc_init_elapsed_ns_total" + cMutCpu <- counter "ghc_mutator_cpu_ns_total" + cMutElapsed <- counter "ghc_mutator_elapsed_ns_total" + cGcCpu <- counter "ghc_gc_cpu_ns_total" + cGcElapsed <- counter "ghc_gc_elapsed_ns_total" + cCpu <- counter "ghc_cpu_ns_total" + cElapsed <- counter "ghc_elapsed_ns_total" + cNonmovingCpu <- counter "ghc_nonmoving_gc_cpu_ns_total" + cNonmovingElapsed <- counter "ghc_nonmoving_gc_elapsed_ns_total" + cNonmovingSyncCpu <- counter "ghc_nonmoving_gc_sync_cpu_ns_total" + cNonmovingSyncElapsed <- counter "ghc_nonmoving_gc_sync_elapsed_ns_total" + + gMaxLive <- gauge "ghc_max_live_bytes" + gMaxLargeObjects <- gauge "ghc_max_large_objects_bytes" + gMaxCompact <- gauge "ghc_max_compact_bytes" + gMaxSlop <- gauge "ghc_max_slop_bytes" + gMaxMemInUse <- gauge "ghc_max_mem_in_use_bytes" + gNonmovingSyncMaxElapsed <- gauge "ghc_nonmoving_gc_sync_max_elapsed_ns" + gNonmovingMaxElapsed <- gauge "ghc_nonmoving_gc_max_elapsed_ns" + + gDetailsGen <- gauge "ghc_gcdetails_gen" + gDetailsThreads <- gauge "ghc_gcdetails_threads" + gDetailsAllocated <- gauge "ghc_gcdetails_allocated_bytes" + gDetailsLive <- gauge "ghc_gcdetails_live_bytes" + gDetailsLargeObjects <- gauge "ghc_gcdetails_large_objects_bytes" + gDetailsCompact <- gauge "ghc_gcdetails_compact_bytes" + gDetailsSlop <- gauge "ghc_gcdetails_slop_bytes" + gDetailsMemInUse <- gauge "ghc_gcdetails_mem_in_use_bytes" + gDetailsCopied <- gauge "ghc_gcdetails_copied_bytes" + gDetailsParMaxCopied <- gauge "ghc_gcdetails_par_max_copied_bytes" + gDetailsParBalancedCopied <- gauge "ghc_gcdetails_par_balanced_copied_bytes" + gDetailsSyncElapsed <- gauge "ghc_gcdetails_sync_elapsed_ns" + gDetailsCpu <- gauge "ghc_gcdetails_cpu_ns" + gDetailsElapsed <- gauge "ghc_gcdetails_elapsed_ns" + gDetailsNonmovingSyncCpu <- gauge "ghc_gcdetails_nonmoving_gc_sync_cpu_ns" + gDetailsNonmovingSyncElapsed <- gauge "ghc_gcdetails_nonmoving_gc_sync_elapsed_ns" + + pure $ do + enabled <- getRTSStatsEnabled + when enabled $ do + stats <- getRTSStats + setCounterFrom stats gcs cGcs + setCounterFrom stats major_gcs cMajorGcs + setCounterFrom stats allocated_bytes cAllocated + setCounterFrom stats copied_bytes cCopied + setCounterFrom stats par_copied_bytes cParCopied + setCounterFrom stats cumulative_live_bytes cCumLive + setCounterFrom stats cumulative_par_balanced_copied_bytes cCumParBalanced + setCounterFrom stats init_cpu_ns cInitCpu + setCounterFrom stats init_elapsed_ns cInitElapsed + setCounterFrom stats mutator_cpu_ns cMutCpu + setCounterFrom stats mutator_elapsed_ns cMutElapsed + setCounterFrom stats gc_cpu_ns cGcCpu + setCounterFrom stats gc_elapsed_ns cGcElapsed + setCounterFrom stats cpu_ns cCpu + setCounterFrom stats elapsed_ns cElapsed + setCounterFrom stats nonmoving_gc_cpu_ns cNonmovingCpu + setCounterFrom stats nonmoving_gc_elapsed_ns cNonmovingElapsed + setCounterFrom stats nonmoving_gc_sync_cpu_ns cNonmovingSyncCpu + setCounterFrom stats nonmoving_gc_sync_elapsed_ns cNonmovingSyncElapsed + + setGaugeFrom stats max_live_bytes gMaxLive + setGaugeFrom stats max_large_objects_bytes gMaxLargeObjects + setGaugeFrom stats max_compact_bytes gMaxCompact + setGaugeFrom stats max_slop_bytes gMaxSlop + setGaugeFrom stats max_mem_in_use_bytes gMaxMemInUse + setGaugeFrom stats nonmoving_gc_sync_max_elapsed_ns gNonmovingSyncMaxElapsed + setGaugeFrom stats nonmoving_gc_max_elapsed_ns gNonmovingMaxElapsed + + let details = gc stats + setGaugeFrom details gcdetails_gen gDetailsGen + setGaugeFrom details gcdetails_threads gDetailsThreads + setGaugeFrom details gcdetails_allocated_bytes gDetailsAllocated + setGaugeFrom details gcdetails_live_bytes gDetailsLive + setGaugeFrom details gcdetails_large_objects_bytes gDetailsLargeObjects + setGaugeFrom details gcdetails_compact_bytes gDetailsCompact + setGaugeFrom details gcdetails_slop_bytes gDetailsSlop + setGaugeFrom details gcdetails_mem_in_use_bytes gDetailsMemInUse + setGaugeFrom details gcdetails_copied_bytes gDetailsCopied + setGaugeFrom details gcdetails_par_max_copied_bytes gDetailsParMaxCopied + setGaugeFrom details gcdetails_par_balanced_copied_bytes gDetailsParBalancedCopied + setGaugeFrom details gcdetails_sync_elapsed_ns gDetailsSyncElapsed + setGaugeFrom details gcdetails_cpu_ns gDetailsCpu + setGaugeFrom details gcdetails_elapsed_ns gDetailsElapsed + setGaugeFrom details gcdetails_nonmoving_gc_sync_cpu_ns gDetailsNonmovingSyncCpu + setGaugeFrom details gcdetails_nonmoving_gc_sync_elapsed_ns gDetailsNonmovingSyncElapsed + where + counter n = registerCounter n mempty + gauge n = registerGauge n mempty + + +setCounterFrom :: Integral a => s -> (s -> a) -> Counter -> IO () +setCounterFrom s f = Counter.set (fromIntegral (f s)) + + +setGaugeFrom :: Integral a => s -> (s -> a) -> Gauge -> IO () +setGaugeFrom s f = Gauge.set (fromIntegral (f s)) diff --git a/stack-9.10.yaml b/stack-9.10.yaml index a56362639..bcd8626b3 100644 --- a/stack-9.10.yaml +++ b/stack-9.10.yaml @@ -3,4 +3,5 @@ resolver: lts-24.46 flags: {} packages: - "." + - prometheus-ghc-stats extra-deps: []