packages feed

hs-opentelemetry-instrumentation-ghc-metrics (empty) → 1.0.0.0

raw patch · 7 files changed

+1075/−0 lines, 7 filesdep +basedep +bytestringdep +directory

Dependencies added: base, bytestring, directory, hs-opentelemetry-api, hs-opentelemetry-exporter-in-memory, hs-opentelemetry-instrumentation-ghc-metrics, hs-opentelemetry-sdk, hs-opentelemetry-semantic-conventions, hspec, text, vector

Files

+ ChangeLog.md view
@@ -0,0 +1,17 @@+# Changelog for hs-opentelemetry-instrumentation-ghc-metrics++## 1.0.0.0 - 2026-05-29++- Promoted to 1.0.0.0 for the hs-opentelemetry 1.0 release.++## 0.1.0.0++- Initial release.+- `OpenTelemetry.Instrumentation.GHCMetrics`: registers GHC RTS statistics as+  `process.runtime.ghc.*` metrics (requires `+RTS -T`).+- `OpenTelemetry.Instrumentation.ProcessMetrics`: registers OS-level process metrics+  following OTel semantic conventions (`process.cpu.time`, `process.memory.usage`,+  `process.disk.io`, etc.). Linux-only metrics (page faults, context switches,+  file descriptor count, thread count) require `/proc` filesystem.+- Attribute keys follow OTel semantic conventions: `cpu.mode`, `disk.io.direction`,+  `system.paging.fault.type`, `process.context_switch.type`.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Ian Duncan (c) 2021-2026++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 Ian Duncan nor the names of other+      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+OWNER 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.
+ cbits/process_metrics.c view
@@ -0,0 +1,15 @@+#if defined(__APPLE__)+#include <mach/mach.h>+#include <stdint.h>++int64_t hs_otel_get_rss(void) {+    struct mach_task_basic_info info;+    mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;+    kern_return_t kr = task_info(mach_task_self(),+                                MACH_TASK_BASIC_INFO,+                                (task_info_t)&info,+                                &count);+    if (kr != KERN_SUCCESS) return 0;+    return (int64_t)info.resident_size;+}+#endif
+ hs-opentelemetry-instrumentation-ghc-metrics.cabal view
@@ -0,0 +1,67 @@+cabal-version: 2.4+name:          hs-opentelemetry-instrumentation-ghc-metrics+version:       1.0.0.0+synopsis:      OpenTelemetry metrics for the GHC runtime (RTS statistics)+description:+  Registers observable instruments that report GHC RTS statistics as OpenTelemetry+  metrics.  Requires the program to be run with @+RTS -T@ (or @-t@, @-s@, etc.)+  so that 'GHC.Stats.getRTSStats' is enabled.+  .+  Metric names follow the @process.runtime.ghc.*@ namespace convention.++category:      OpenTelemetry, Telemetry, Monitoring, Observability+homepage:      https://github.com/iand675/hs-opentelemetry#readme+bug-reports:   https://github.com/iand675/hs-opentelemetry/issues+author:        Ian Duncan+maintainer:    ian@iankduncan.com+copyright:     2024-2026 Ian Duncan, Mercury Technologies+license:       BSD-3-Clause+license-file:  LICENSE+build-type:    Simple+extra-doc-files: ChangeLog.md++source-repository head+  type:     git+  location: https://github.com/iand675/hs-opentelemetry++library+  exposed-modules:+    OpenTelemetry.Instrumentation.GHCMetrics+    OpenTelemetry.Instrumentation.ProcessMetrics+  hs-source-dirs:   src+  default-language:  Haskell2010+  default-extensions:+    OverloadedStrings+  ghc-options:       -Wall+  build-depends:+      base >= 4.16 && < 5+    , hs-opentelemetry-api ^>= 1.0+    , hs-opentelemetry-semantic-conventions >= 1.40 && < 2+    , text++  if os(darwin)+    c-sources: cbits/process_metrics.c+    frameworks: CoreFoundation++  if os(linux)+    build-depends:+        bytestring+      , directory++test-suite hs-opentelemetry-instrumentation-ghc-metrics-test+  type:             exitcode-stdio-1.0+  main-is:          Spec.hs+  hs-source-dirs:   test+  default-language:  Haskell2010+  default-extensions:+    OverloadedStrings+  ghc-options:       -threaded -rtsopts "-with-rtsopts=-T -N"+  build-depends:+      base >= 4.16 && < 5+    , hs-opentelemetry-api ^>= 1.0+    , hs-opentelemetry-exporter-in-memory ^>= 1.0+    , hs-opentelemetry-instrumentation-ghc-metrics ^>= 1.0+    , hs-opentelemetry-sdk ^>= 1.0+    , hspec >= 2.0+    , text+    , vector
+ src/OpenTelemetry/Instrumentation/GHCMetrics.hs view
@@ -0,0 +1,284 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}++{- |+Module      : OpenTelemetry.Instrumentation.GHCMetrics+Copyright   : (c) Ian Duncan, 2021-2026+License     : BSD-3+Description : Export GHC runtime metrics as OpenTelemetry metrics+Stability   : experimental++= Overview++Registers observable instruments that report GHC runtime statistics+(memory usage, GC counters, mutator vs GC time, thread counts, etc.)+as OpenTelemetry metrics. These are collected automatically on each+metric export cycle.++The metric set covers every field in 'GHC.Stats.RTSStats' and+'GHC.Stats.GCDetails', matching or exceeding the coverage provided+by @ekg-core@'s @registerGcMetrics@.++= Quick example++@+import OpenTelemetry.Instrumentation.GHCMetrics (registerGHCMetrics)+import OpenTelemetry.Metric.Core (getGlobalMeterProvider, getMeter)+import OpenTelemetry.Trace.Core (instrumentationLibrary)++main :: IO ()+main = do+  mp <- getGlobalMeterProvider+  meter <- getMeter mp (instrumentationLibrary "ghc-metrics" "0.1.0")+  _handles <- registerGHCMetrics meter+  -- GHC metrics are now exported alongside your application metrics+@++= Exported metrics++All instrument names use the @process.runtime.ghc.@ prefix.++== Cumulative counters (from 'GHC.Stats.RTSStats')++* @process.runtime.ghc.allocated_bytes@ (By)+* @process.runtime.ghc.gc.count@ / @gc.major_count@+* @process.runtime.ghc.gc.cpu_time@ / @gc.elapsed_time@ (s)+* @process.runtime.ghc.gc.copied_bytes@ / @gc.par_copied_bytes@ (By)+* @process.runtime.ghc.gc.cumulative_live_bytes@ (By)+* @process.runtime.ghc.gc.cumulative_par_balanced_copied_bytes@ (By)+* @process.runtime.ghc.mutator.cpu_time@ / @mutator.elapsed_time@ (s)+* @process.runtime.ghc.init.cpu_time@ / @init.elapsed_time@ (s)+* @process.runtime.ghc.cpu_time@ / @elapsed_time@ (s)+* @process.runtime.ghc.nonmoving_gc.sync.cpu_time@ / @sync.elapsed_time@ (s)+* @process.runtime.ghc.nonmoving_gc.cpu_time@ / @nonmoving_gc.elapsed_time@ (s)++== Gauges: high-water marks (from 'GHC.Stats.RTSStats')++* @process.runtime.ghc.memory.max_live_bytes@ (By)+* @process.runtime.ghc.memory.max_large_objects_bytes@ (By)+* @process.runtime.ghc.memory.max_compact_bytes@ (By)+* @process.runtime.ghc.memory.max_slop_bytes@ (By)+* @process.runtime.ghc.memory.max_mem_in_use_bytes@ (By)+* @process.runtime.ghc.nonmoving_gc.sync.max_elapsed_time@ (s)+* @process.runtime.ghc.nonmoving_gc.max_elapsed_time@ (s)++== Gauges: last GC snapshot (from 'GHC.Stats.GCDetails')++* @process.runtime.ghc.memory.live_bytes@ / @memory.heap_size@ (By)+* @process.runtime.ghc.gc.last.*@ for generation, threads, allocated,+  large objects, compact, slop, copied, parallel copy stats, sync+  time, CPU\/elapsed time, and nonmoving GC sync time.+* @process.runtime.ghc.gc.last.block_fragmentation_bytes@ (GHC 9.6+)++Use 'unregisterObservableCallback' on the returned handles for optional cleanup.++__Note__: requires the program to be started with @+RTS -T@ (runtime+statistics enabled) or a GHC version that enables them by default.+If stats are disabled, 'registerGHCMetrics' returns an empty list.++@since 0.1.0.0+-}+module OpenTelemetry.Instrumentation.GHCMetrics (+  registerGHCMetrics,+) where++import Data.Int (Int64)+import Data.Text (Text)+import GHC.Stats (GCDetails (..), RTSStats (..), getRTSStats, getRTSStatsEnabled)+import OpenTelemetry.Attributes (emptyAttributes)+import OpenTelemetry.Metric.Core (+  AdvisoryParameters,+  Meter (..),+  ObservableCallbackHandle (..),+  ObservableCounter (..),+  ObservableGauge (..),+  ObservableResult (..),+  defaultAdvisoryParameters,+ )+++{- | Register all GHC runtime metric instruments on the given 'Meter'.++Returns a list of callback handles that can be used to unregister the+callbacks (e.g. during shutdown). If RTS stats are not enabled+(@+RTS -T@ was not passed), returns an empty list and no instruments+are created.++@since 0.1.0.0+-}+registerGHCMetrics :: Meter -> IO [ObservableCallbackHandle]+registerGHCMetrics m = do+  enabled <- getRTSStatsEnabled+  if not enabled+    then pure []+    else+      sequence $+        concat+          [ rtsStatsCounters m+          , rtsStatsGauges m+          , gcDetailsGauges m+          ]+++-- ── RTSStats cumulative counters ─────────────────────────────────────++rtsStatsCounters :: Meter -> [IO ObservableCallbackHandle]+rtsStatsCounters m =+  [ obsCounterI64 m "process.runtime.ghc.allocated_bytes" "By" "Total bytes allocated on the GHC heap" $+      \s -> fromIntegral (allocated_bytes s)+  , obsCounterI64 m "process.runtime.ghc.gc.count" "{gc}" "Total number of GCs" $+      \s -> fromIntegral (gcs s)+  , obsCounterI64 m "process.runtime.ghc.gc.major_count" "{gc}" "Total number of major (oldest generation) GCs" $+      \s -> fromIntegral (major_gcs s)+  , obsCounterDbl m "process.runtime.ghc.gc.cpu_time" "s" "Total CPU time spent in GC" $+      \s -> nsToSec (gc_cpu_ns s)+  , obsCounterDbl m "process.runtime.ghc.gc.elapsed_time" "s" "Total wall-clock time spent in GC" $+      \s -> nsToSec (gc_elapsed_ns s)+  , obsCounterI64 m "process.runtime.ghc.gc.copied_bytes" "By" "Total bytes copied during GC" $+      \s -> fromIntegral (copied_bytes s)+  , obsCounterI64 m "process.runtime.ghc.gc.par_copied_bytes" "By" "Total bytes copied during parallel GCs" $+      \s -> fromIntegral (par_copied_bytes s)+  , obsCounterI64 m "process.runtime.ghc.gc.cumulative_live_bytes" "By" "Sum of live bytes across all major GCs" $+      \s -> fromIntegral (cumulative_live_bytes s)+  , obsCounterI64 m "process.runtime.ghc.gc.cumulative_par_balanced_copied_bytes" "By" "Sum of balanced parallel-copied bytes across all GCs" $+      \s -> fromIntegral (cumulative_par_balanced_copied_bytes s)+  , obsCounterDbl m "process.runtime.ghc.mutator.cpu_time" "s" "Total CPU time spent in the mutator" $+      \s -> nsToSec (mutator_cpu_ns s)+  , obsCounterDbl m "process.runtime.ghc.mutator.elapsed_time" "s" "Total wall-clock time spent in the mutator" $+      \s -> nsToSec (mutator_elapsed_ns s)+  , obsCounterDbl m "process.runtime.ghc.init.cpu_time" "s" "Total CPU time used by the init phase" $+      \s -> nsToSec (init_cpu_ns s)+  , obsCounterDbl m "process.runtime.ghc.init.elapsed_time" "s" "Total wall-clock time used by the init phase" $+      \s -> nsToSec (init_elapsed_ns s)+  , obsCounterDbl m "process.runtime.ghc.cpu_time" "s" "Total CPU time elapsed since program start" $+      \s -> nsToSec (cpu_ns s)+  , obsCounterDbl m "process.runtime.ghc.elapsed_time" "s" "Total wall-clock time elapsed since program start" $+      \s -> nsToSec (elapsed_ns s)+  , obsCounterDbl m "process.runtime.ghc.nonmoving_gc.sync.cpu_time" "s" "CPU time spent in nonmoving GC sync phase" $+      \s -> nsToSec (nonmoving_gc_sync_cpu_ns s)+  , obsCounterDbl m "process.runtime.ghc.nonmoving_gc.sync.elapsed_time" "s" "Wall-clock time spent in nonmoving GC sync phase" $+      \s -> nsToSec (nonmoving_gc_sync_elapsed_ns s)+  , obsCounterDbl m "process.runtime.ghc.nonmoving_gc.cpu_time" "s" "Total CPU time used by the nonmoving GC" $+      \s -> nsToSec (nonmoving_gc_cpu_ns s)+  , obsCounterDbl m "process.runtime.ghc.nonmoving_gc.elapsed_time" "s" "Total wall-clock time with nonmoving GC active" $+      \s -> nsToSec (nonmoving_gc_elapsed_ns s)+  ]+++-- ── RTSStats gauges (high-water marks) ───────────────────────────────++rtsStatsGauges :: Meter -> [IO ObservableCallbackHandle]+rtsStatsGauges m =+  [ obsGaugeI64 m "process.runtime.ghc.memory.max_live_bytes" "By" "Maximum live data seen in the heap (high-water mark)" $+      \s -> fromIntegral (max_live_bytes s)+  , obsGaugeI64 m "process.runtime.ghc.memory.max_large_objects_bytes" "By" "Maximum live data in large objects" $+      \s -> fromIntegral (max_large_objects_bytes s)+  , obsGaugeI64 m "process.runtime.ghc.memory.max_compact_bytes" "By" "Maximum live data in compact regions" $+      \s -> fromIntegral (max_compact_bytes s)+  , obsGaugeI64 m "process.runtime.ghc.memory.max_slop_bytes" "By" "Maximum slop (wasted memory)" $+      \s -> fromIntegral (max_slop_bytes s)+  , obsGaugeI64 m "process.runtime.ghc.memory.max_mem_in_use_bytes" "By" "Maximum memory in use by the RTS" $+      \s -> fromIntegral (max_mem_in_use_bytes s)+  , obsGaugeDbl m "process.runtime.ghc.nonmoving_gc.sync.max_elapsed_time" "s" "Maximum elapsed time of any nonmoving GC sync phase" $+      \s -> nsToSec (nonmoving_gc_sync_max_elapsed_ns s)+  , obsGaugeDbl m "process.runtime.ghc.nonmoving_gc.max_elapsed_time" "s" "Maximum elapsed time of any nonmoving GC cycle" $+      \s -> nsToSec (nonmoving_gc_max_elapsed_ns s)+  ]+++-- ── GCDetails gauges (last GC snapshot) ──────────────────────────────++gcDetailsGauges :: Meter -> [IO ObservableCallbackHandle]+gcDetailsGauges m =+  [ obsGaugeI64 m "process.runtime.ghc.memory.live_bytes" "By" "Current live data in the heap (as of last major GC)" $+      \s -> fromIntegral (gcdetails_live_bytes (gc s))+  , obsGaugeI64 m "process.runtime.ghc.memory.heap_size" "By" "Current memory in use by the RTS" $+      \s -> fromIntegral (gcdetails_mem_in_use_bytes (gc s))+  , obsGaugeI64 m "process.runtime.ghc.gc.last.gen" "{gen}" "Generation number of the most recent GC" $+      \s -> fromIntegral (gcdetails_gen (gc s))+  , obsGaugeI64 m "process.runtime.ghc.gc.last.threads" "{thread}" "Number of threads used in the most recent GC" $+      \s -> fromIntegral (gcdetails_threads (gc s))+  , obsGaugeI64 m "process.runtime.ghc.gc.last.allocated_bytes" "By" "Bytes allocated since the previous GC" $+      \s -> fromIntegral (gcdetails_allocated_bytes (gc s))+  , obsGaugeI64 m "process.runtime.ghc.gc.last.large_objects_bytes" "By" "Live data in large objects (last GC)" $+      \s -> fromIntegral (gcdetails_large_objects_bytes (gc s))+  , obsGaugeI64 m "process.runtime.ghc.gc.last.compact_bytes" "By" "Live data in compact regions (last GC)" $+      \s -> fromIntegral (gcdetails_compact_bytes (gc s))+  , obsGaugeI64 m "process.runtime.ghc.gc.last.slop_bytes" "By" "Wasted memory (slop) in the last GC" $+      \s -> fromIntegral (gcdetails_slop_bytes (gc s))+  , obsGaugeI64 m "process.runtime.ghc.gc.last.copied_bytes" "By" "Bytes copied during the last GC" $+      \s -> fromIntegral (gcdetails_copied_bytes (gc s))+  , obsGaugeI64 m "process.runtime.ghc.gc.last.par_max_copied_bytes" "By" "Max bytes copied by any one thread (last GC)" $+      \s -> fromIntegral (gcdetails_par_max_copied_bytes (gc s))+  , obsGaugeI64 m "process.runtime.ghc.gc.last.par_balanced_copied_bytes" "By" "Balanced parallel-copied bytes (last GC)" $+      \s -> fromIntegral (gcdetails_par_balanced_copied_bytes (gc s))+  , obsGaugeDbl m "process.runtime.ghc.gc.last.sync_elapsed_time" "s" "Elapsed time for synchronisation before last GC" $+      \s -> nsToSec (gcdetails_sync_elapsed_ns (gc s))+  , obsGaugeDbl m "process.runtime.ghc.gc.last.cpu_time" "s" "CPU time used by the last GC" $+      \s -> nsToSec (gcdetails_cpu_ns (gc s))+  , obsGaugeDbl m "process.runtime.ghc.gc.last.elapsed_time" "s" "Wall-clock time of the last GC" $+      \s -> nsToSec (gcdetails_elapsed_ns (gc s))+  , obsGaugeDbl m "process.runtime.ghc.gc.last.nonmoving_gc_sync_cpu_time" "s" "Nonmoving GC sync CPU time (last GC)" $+      \s -> nsToSec (gcdetails_nonmoving_gc_sync_cpu_ns (gc s))+  , obsGaugeDbl m "process.runtime.ghc.gc.last.nonmoving_gc_sync_elapsed_time" "s" "Nonmoving GC sync elapsed time (last GC)" $+      \s -> nsToSec (gcdetails_nonmoving_gc_sync_elapsed_ns (gc s))+  ]+    ++ gcDetailsBlockFragGauge m+++gcDetailsBlockFragGauge :: Meter -> [IO ObservableCallbackHandle]+#if MIN_VERSION_base(4,18,0)+gcDetailsBlockFragGauge m =+  [ obsGaugeI64 m "process.runtime.ghc.gc.last.block_fragmentation_bytes" "By" "Memory lost to block fragmentation (last GC)" $+      \s -> fromIntegral (gcdetails_block_fragmentation_bytes (gc s))+  ]+#else+gcDetailsBlockFragGauge _ = []+#endif+++-- ── Helpers ──────────────────────────────────────────────────────────++noAdv :: AdvisoryParameters+noAdv = defaultAdvisoryParameters+++obsCounterI64+  :: Meter -> Text -> Text -> Text -> (RTSStats -> Int64) -> IO ObservableCallbackHandle+obsCounterI64 m name unit desc extract = do+  oc <- meterCreateObservableCounterInt64 m name (Just unit) (Just desc) noAdv []+  observableCounterRegisterCallback oc $ \res -> do+    s <- getRTSStats+    observe res (extract s) emptyAttributes+++obsCounterDbl+  :: Meter -> Text -> Text -> Text -> (RTSStats -> Double) -> IO ObservableCallbackHandle+obsCounterDbl m name unit desc extract = do+  oc <- meterCreateObservableCounterDouble m name (Just unit) (Just desc) noAdv []+  observableCounterRegisterCallback oc $ \res -> do+    s <- getRTSStats+    observe res (extract s) emptyAttributes+++obsGaugeI64+  :: Meter -> Text -> Text -> Text -> (RTSStats -> Int64) -> IO ObservableCallbackHandle+obsGaugeI64 m name unit desc extract = do+  og <- meterCreateObservableGaugeInt64 m name (Just unit) (Just desc) noAdv []+  observableGaugeRegisterCallback og $ \res -> do+    s <- getRTSStats+    observe res (extract s) emptyAttributes+++obsGaugeDbl+  :: Meter -> Text -> Text -> Text -> (RTSStats -> Double) -> IO ObservableCallbackHandle+obsGaugeDbl m name unit desc extract = do+  og <- meterCreateObservableGaugeDouble m name (Just unit) (Just desc) noAdv []+  observableGaugeRegisterCallback og $ \res -> do+    s <- getRTSStats+    observe res (extract s) emptyAttributes+++nsToSec :: (Integral a) => a -> Double+nsToSec ns = fromIntegral ns / 1000000000+{-# INLINE nsToSec #-}
+ src/OpenTelemetry/Instrumentation/ProcessMetrics.hs view
@@ -0,0 +1,454 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ForeignFunctionInterface #-}+{-# LANGUAGE OverloadedStrings #-}++{- |+Module      : OpenTelemetry.Instrumentation.ProcessMetrics+Copyright   : (c) Ian Duncan, 2021-2026+License     : BSD-3+Description : OS-level process metrics following OTel semantic conventions+Stability   : experimental++Registers observable instruments for standard OS-level process metrics+(@process.cpu.time@, @process.memory.usage@, @process.uptime@, etc.)+as defined by the+<https://opentelemetry.io/docs/specs/semconv/system/process-metrics/ OpenTelemetry semantic conventions for process metrics>.++These complement the GHC-specific runtime metrics in+"OpenTelemetry.Instrumentation.GHCMetrics" — those cover the Haskell+runtime internals, while these cover what the OS reports about the+process.++Also includes @process.runtime.ghc.capability.count@ (number of HEC+capabilities, analogous to @go.processor.limit@ in the Go SDK).++= Metrics registered++== Standard process metrics++* @process.cpu.time@ — counter (s), with @cpu.mode=user@ and @cpu.mode=system@+* @process.memory.usage@ — gauge (By), resident set size+* @process.memory.virtual@ — gauge (By), virtual memory size (Linux only)+* @process.thread.count@ — gauge, live thread count from @\/proc\/self\/status@ (Linux only)+* @process.unix.file_descriptor.count@ — gauge, open FD count via @\/proc\/self\/fd@ (Linux only)+* @process.disk.io@ — counter (By), with @disk.io.direction=read|write@ from @\/proc\/self\/io@ (Linux only)+* @process.uptime@ — gauge (s), wall-clock time since registration+* @process.paging.faults@ — counter, with @system.paging.fault.type=minor|major@+* @process.context_switches@ — counter, with @process.context_switch.type=voluntary|involuntary@++== Haskell-specific++* @process.runtime.ghc.capability.count@ — gauge, number of HEC capabilities++= Data sources++On POSIX (Linux, macOS): @getrusage(2)@ for CPU, page faults, context switches.+Linux: @\/proc\/self\/status@ for VmRSS, VmSize, and Threads; @\/proc\/self\/fd@ for FD count;+@\/proc\/self\/io@ for read\/write bytes.+macOS: @task_info(MACH_TASK_BASIC_INFO)@ for resident size.++@since 0.1.0.0+-}+module OpenTelemetry.Instrumentation.ProcessMetrics (+  registerProcessMetrics,+) where++import Data.Int (Int64)+import Data.Text (Text)+import Data.Word (Word64)+import Foreign.C.Types (CLong (..))+import Foreign.Marshal.Alloc (allocaBytes)+import Foreign.Ptr (Ptr)+import Foreign.Storable (peekByteOff)+import GHC.Clock (getMonotonicTimeNSec)+import GHC.Conc (getNumCapabilities)+import qualified GHC.Stats as Stats+import OpenTelemetry.Attributes (+  Attributes,+  emptyAttributes,+  toAttribute,+  unsafeAttributesFromListIgnoringLimits,+ )+import OpenTelemetry.Attributes.Key (unkey)+import OpenTelemetry.Metric.Core (+  AdvisoryParameters,+  Meter (..),+  ObservableCallbackHandle (..),+  ObservableCounter (..),+  ObservableGauge (..),+  ObservableResult (..),+  defaultAdvisoryParameters,+ )+import qualified OpenTelemetry.SemanticConventions as SC+import System.IO.Unsafe (unsafePerformIO)+++#if defined(linux_HOST_OS)+import Control.Exception (SomeException, catch)+import qualified Data.ByteString as BS+import System.Directory (listDirectory)+#endif+++{- | Register OS-level process metric instruments on the given 'Meter'.++Returns callback handles for optional cleanup via+@unregisterObservableCallback@.++@since 0.1.0.0+-}+registerProcessMetrics :: Meter -> IO [ObservableCallbackHandle]+registerProcessMetrics m =+  sequence $+    concat+      [ cpuTimeCounters m+      , memoryGauges m+      , uptimeGauge m+      , pagingFaultsCounters m+      , contextSwitchCounters m+      , capabilityGauge m+      , threadCountGauge m+      , fdCountGauge m+      , diskIoCounters m+      ]+++-- ── CPU time ────────────────────────────────────────────────────────++cpuTimeCounters :: Meter -> [IO ObservableCallbackHandle]+cpuTimeCounters m =+  [ do+      oc <- meterCreateObservableCounterDouble m "process.cpu.time" (Just "s") (Just "Total CPU seconds broken down by different states") noAdv []+      observableCounterRegisterCallback oc $ \res -> do+        ru <- getRUsage+        observe res (ruUserSec ru) userModeAttr+        observe res (ruSystemSec ru) systemModeAttr+  ]+++-- ── Memory ──────────────────────────────────────────────────────────++memoryGauges :: Meter -> [IO ObservableCallbackHandle]+memoryGauges m =+  rssGauge m ++ virtualGauge m+++rssGauge :: Meter -> [IO ObservableCallbackHandle]+rssGauge m =+  [ do+      og <- meterCreateObservableGaugeInt64 m "process.memory.usage" (Just "By") (Just "The amount of physical memory in use") noAdv []+      observableGaugeRegisterCallback og $ \res -> do+        rss <- getResidentBytes+        observe res rss emptyAttributes+  ]++#if defined(linux_HOST_OS)+virtualGauge :: Meter -> [IO ObservableCallbackHandle]+virtualGauge m =+  [ do+      og <- meterCreateObservableGaugeInt64 m "process.memory.virtual" (Just "By") (Just "The amount of committed virtual memory") noAdv []+      observableGaugeRegisterCallback og $ \res -> do+        vm <- getVirtualBytes+        observe res vm emptyAttributes+  ]+#else+virtualGauge :: Meter -> [IO ObservableCallbackHandle]+virtualGauge _ = []+#endif+++-- ── Uptime ──────────────────────────────────────────────────────────++startTimeNs :: Word64+startTimeNs = unsafePerformIO getMonotonicTimeNSec+{-# NOINLINE startTimeNs #-}+++getUptimeSeconds :: IO Double+getUptimeSeconds =+  ( do+      rtsEnabled <- Stats.getRTSStatsEnabled+      if rtsEnabled+        then do+          stats <- Stats.getRTSStats+          pure $ fromIntegral (Stats.elapsed_ns stats) / 1e9+        else monoFallback+  )+  where+    monoFallback = do+      now <- getMonotonicTimeNSec+      pure $ fromIntegral (now - startTimeNs) / 1e9+++uptimeGauge :: Meter -> [IO ObservableCallbackHandle]+uptimeGauge m =+  [ do+      og <- meterCreateObservableGaugeDouble m "process.uptime" (Just "s") (Just "The time the process has been running") noAdv []+      observableGaugeRegisterCallback og $ \res -> do+        uptimeSec <- getUptimeSeconds+        observe res uptimeSec emptyAttributes+  ]+++-- ── Paging faults ───────────────────────────────────────────────────++pagingFaultsCounters :: Meter -> [IO ObservableCallbackHandle]+pagingFaultsCounters m =+  [ do+      oc <- meterCreateObservableCounterInt64 m "process.paging.faults" (Just "{fault}") (Just "Number of page faults the process has made") noAdv []+      observableCounterRegisterCallback oc $ \res -> do+        ru <- getRUsage+        observe res (ruMinorFaults ru) minorFaultAttr+        observe res (ruMajorFaults ru) majorFaultAttr+  ]+++-- ── Context switches ────────────────────────────────────────────────++contextSwitchCounters :: Meter -> [IO ObservableCallbackHandle]+contextSwitchCounters m =+  [ do+      oc <- meterCreateObservableCounterInt64 m "process.context_switches" (Just "{context_switch}") (Just "Number of times the process has been context switched") noAdv []+      observableCounterRegisterCallback oc $ \res -> do+        ru <- getRUsage+        observe res (ruVoluntaryCSW ru) voluntaryCSWAttr+        observe res (ruInvoluntaryCSW ru) involuntaryCSWAttr+  ]+++-- ── GHC capabilities ───────────────────────────────────────────────++capabilityGauge :: Meter -> [IO ObservableCallbackHandle]+capabilityGauge m =+  [ do+      og <- meterCreateObservableGaugeInt64 m "process.runtime.ghc.capability.count" (Just "{capability}") (Just "Number of GHC HEC capabilities (green thread execution contexts)") noAdv []+      observableGaugeRegisterCallback og $ \res -> do+        n <- getNumCapabilities+        observe res (fromIntegral n) emptyAttributes+  ]++#if defined(linux_HOST_OS)+readDirAttr :: Attributes+readDirAttr = unsafeAttributesFromListIgnoringLimits [(unkey SC.disk_io_direction, toAttribute ("read" :: Text))]+++writeDirAttr :: Attributes+writeDirAttr = unsafeAttributesFromListIgnoringLimits [(unkey SC.disk_io_direction, toAttribute ("write" :: Text))]+++threadCountGauge :: Meter -> [IO ObservableCallbackHandle]+threadCountGauge m =+  [ do+      og <- meterCreateObservableGaugeInt64 m "process.thread.count" (Just "{thread}") (Just "Process threads count") noAdv []+      observableGaugeRegisterCallback og $ \res -> do+        r <- parseProcCountField "Threads:"+        observe res (maybe 0 id r) emptyAttributes+  ]++fdCountGauge :: Meter -> [IO ObservableCallbackHandle]+fdCountGauge m =+  [ do+      og <- meterCreateObservableGaugeInt64 m "process.unix.file_descriptor.count" (Just "{file_descriptor}") (Just "Number of unix file descriptors in use by the process") noAdv []+      observableGaugeRegisterCallback og $ \res -> do+        n <- countOpenFds+        observe res n emptyAttributes+  ]++diskIoCounters :: Meter -> [IO ObservableCallbackHandle]+diskIoCounters m =+  [ do+      oc <- meterCreateObservableCounterInt64 m "process.disk.io" (Just "By") (Just "Disk bytes transferred") noAdv []+      observableCounterRegisterCallback oc $ \res -> do+        (r, w) <- getDiskIo+        observe res r readDirAttr+        observe res w writeDirAttr+  ]+#else+threadCountGauge :: Meter -> [IO ObservableCallbackHandle]+threadCountGauge _ = []++fdCountGauge :: Meter -> [IO ObservableCallbackHandle]+fdCountGauge _ = []++diskIoCounters :: Meter -> [IO ObservableCallbackHandle]+diskIoCounters _ = []+#endif+++-- ── Attribute constants ─────────────────────────────────────────────++userModeAttr :: Attributes+userModeAttr = unsafeAttributesFromListIgnoringLimits [(unkey SC.cpu_mode, toAttribute ("user" :: Text))]+++systemModeAttr :: Attributes+systemModeAttr = unsafeAttributesFromListIgnoringLimits [(unkey SC.cpu_mode, toAttribute ("system" :: Text))]+++minorFaultAttr :: Attributes+minorFaultAttr = unsafeAttributesFromListIgnoringLimits [(unkey SC.system_paging_fault_type, toAttribute ("minor" :: Text))]+++majorFaultAttr :: Attributes+majorFaultAttr = unsafeAttributesFromListIgnoringLimits [(unkey SC.system_paging_fault_type, toAttribute ("major" :: Text))]+++voluntaryCSWAttr :: Attributes+voluntaryCSWAttr = unsafeAttributesFromListIgnoringLimits [(unkey SC.process_contextSwitch_type, toAttribute ("voluntary" :: Text))]+++involuntaryCSWAttr :: Attributes+involuntaryCSWAttr = unsafeAttributesFromListIgnoringLimits [(unkey SC.process_contextSwitch_type, toAttribute ("involuntary" :: Text))]+++-- ── getrusage FFI ───────────────────────────────────────────────────++data RUsage = RUsage+  { ruUserSec :: {-# UNPACK #-} !Double+  , ruSystemSec :: {-# UNPACK #-} !Double+  , ruMinorFaults :: {-# UNPACK #-} !Int64+  , ruMajorFaults :: {-# UNPACK #-} !Int64+  , ruVoluntaryCSW :: {-# UNPACK #-} !Int64+  , ruInvoluntaryCSW :: {-# UNPACK #-} !Int64+  }+++foreign import ccall unsafe "sys/resource.h getrusage"+  c_getrusage :: Int -> Ptr () -> IO Int+++-- struct rusage layout on 64-bit POSIX (Linux glibc, macOS):+--   ru_utime  @ offset 0   (struct timeval: long tv_sec, long tv_usec)+--   ru_stime  @ offset 16+--   remaining long fields at 4× long-size intervals+getRUsage :: IO RUsage+getRUsage = allocaBytes rusageSize $ \ptr -> do+  _ <- c_getrusage 0 ptr -- RUSAGE_SELF = 0+  uSec <- peekByteOff ptr 0 :: IO CLong+  uUsec <- peekByteOff ptr longSize :: IO CLong+  sSec <- peekByteOff ptr (2 * longSize) :: IO CLong+  sUsec <- peekByteOff ptr (3 * longSize) :: IO CLong+  minflt <- peekByteOff ptr (7 * longSize) :: IO CLong+  majflt <- peekByteOff ptr (8 * longSize) :: IO CLong+  nvcsw <- peekByteOff ptr (14 * longSize) :: IO CLong+  nivcsw <- peekByteOff ptr (15 * longSize) :: IO CLong+  pure $!+    RUsage+      { ruUserSec = timevalToSec uSec uUsec+      , ruSystemSec = timevalToSec sSec sUsec+      , ruMinorFaults = fromIntegral minflt+      , ruMajorFaults = fromIntegral majflt+      , ruVoluntaryCSW = fromIntegral nvcsw+      , ruInvoluntaryCSW = fromIntegral nivcsw+      }+  where+    longSize = 8 -- 64-bit platforms (aarch64, x86_64)+    rusageSize = 18 * longSize+    timevalToSec :: CLong -> CLong -> Double+    timevalToSec s us = fromIntegral s + fromIntegral us / 1e6+{-# INLINE getRUsage #-}++-- ── Memory reading (platform-specific) ──────────────────────────────++#if defined(linux_HOST_OS)++getResidentBytes :: IO Int64+getResidentBytes = do+  r <- parseProcField "VmRSS:"+  pure $ maybe 0 id r++getVirtualBytes :: IO Int64+getVirtualBytes = do+  r <- parseProcField "VmSize:"+  pure $ maybe 0 id r++parseProcField :: BS.ByteString -> IO (Maybe Int64)+parseProcField label =+  (do+    bs <- BS.readFile "/proc/self/status"+    pure $ extractKbField label bs+  )+    `catch` \(_ :: SomeException) -> pure Nothing++parseProcCountField :: BS.ByteString -> IO (Maybe Int64)+parseProcCountField label =+  (do+    bs <- BS.readFile "/proc/self/status"+    pure $ extractCountField label bs+  )+    `catch` \(_ :: SomeException) -> pure Nothing++extractCountField :: BS.ByteString -> BS.ByteString -> Maybe Int64+extractCountField label bs = go (BS.lines bs)+  where+    go [] = Nothing+    go (line : rest)+      | label `BS.isPrefixOf` line =+          let stripped = BS.dropWhile isSpace (BS.drop (BS.length label) line)+              digits = BS.takeWhile isDigit stripped+          in case parseDecimal digits of+              Just n -> Just n+              Nothing -> go rest+      | otherwise = go rest+    isSpace w = w == 0x20 || w == 0x09+    isDigit w = w >= 0x30 && w <= 0x39++extractKbField :: BS.ByteString -> BS.ByteString -> Maybe Int64+extractKbField label bs = go (BS.lines bs)+  where+    go [] = Nothing+    go (line : rest)+      | label `BS.isPrefixOf` line =+          let stripped = BS.dropWhile isSpace (BS.drop (BS.length label) line)+              digits = BS.takeWhile isDigit stripped+          in case parseDecimal digits of+              Just kb -> Just (kb * 1024) -- VmRSS/VmSize report in kB+              Nothing -> go rest+      | otherwise = go rest+    isSpace w = w == 0x20 || w == 0x09+    isDigit w = w >= 0x30 && w <= 0x39++parseDecimal :: BS.ByteString -> Maybe Int64+parseDecimal bs+  | BS.null bs = Nothing+  | otherwise = Just $ BS.foldl' (\acc w -> acc * 10 + fromIntegral (w - 0x30)) 0 bs++countOpenFds :: IO Int64+countOpenFds =+  ( do+      entries <- listDirectory "/proc/self/fd"+      pure $! fromIntegral (length entries)+  )+    `catch` \(_ :: SomeException) -> pure 0++getDiskIo :: IO (Int64, Int64)+getDiskIo =+  ( do+      bs <- BS.readFile "/proc/self/io"+      let rb = maybe 0 id $ extractCountField "read_bytes:" bs+          wb = maybe 0 id $ extractCountField "write_bytes:" bs+      pure (rb, wb)+  )+    `catch` \(_ :: SomeException) -> pure (0, 0)++#elif defined(darwin_HOST_OS)++foreign import ccall unsafe "hs_otel_get_rss"+  c_get_rss :: IO Int64++getResidentBytes :: IO Int64+getResidentBytes = c_get_rss++#else++-- Fallback for unsupported platforms+getResidentBytes :: IO Int64+getResidentBytes = pure 0++#endif+++-- ── Helpers ─────────────────────────────────────────────────────────++noAdv :: AdvisoryParameters+noAdv = defaultAdvisoryParameters
+ test/Spec.hs view
@@ -0,0 +1,208 @@+{- FOURMOLU_DISABLE -}+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import qualified Data.Text as T+import qualified Data.Vector as V+import OpenTelemetry.Exporter.Metric (+  MetricExport (..),+  ResourceMetricsExport (..),+  ScopeMetricsExport (..),+ )+import OpenTelemetry.Instrumentation.GHCMetrics (registerGHCMetrics)+import OpenTelemetry.Instrumentation.ProcessMetrics (registerProcessMetrics)+import OpenTelemetry.Internal.Common.Types (instrumentationLibrary)+import OpenTelemetry.MeterProvider (+  collectResourceMetrics,+  createMeterProvider,+  defaultSdkMeterProviderOptions,+ )+import OpenTelemetry.Metric.Core (getMeter)+import OpenTelemetry.Resource (emptyMaterializedResources)+import Test.Hspec+++expectedBaseCount :: Int+#if MIN_VERSION_base(4,18,0)+expectedBaseCount = 43+#else+expectedBaseCount = 42+#endif+++-- process.cpu.time, process.memory.usage, process.uptime,+-- process.paging.faults, process.context_switches,+-- process.runtime.ghc.capability.count = 6 callbacks+-- On Linux: +1 process.memory.virtual, +1 process.thread.count,+-- +1 process.unix.file_descriptor.count, +1 process.disk.io = 10+expectedProcessCount :: Int+#if defined(linux_HOST_OS)+expectedProcessCount = 10+#else+expectedProcessCount = 6+#endif+++main :: IO ()+main = hspec spec+++spec :: Spec+spec = do+  describe "GHCMetrics" $ do+    it "registers all observable instruments" $ do+      (provider, _env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions+      m <- getMeter provider (instrumentationLibrary "test.ghc-metrics" "0.1.0")+      handles <- registerGHCMetrics m+      length handles `shouldBe` expectedBaseCount++    it "produces metrics with process.runtime.ghc. prefix" $ do+      (provider, env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions+      m <- getMeter provider (instrumentationLibrary "test.ghc-metrics" "0.1.0")+      _ <- registerGHCMetrics m+      batches <- collectResourceMetrics env+      let names = concatMap extractMetricNames batches+      names `shouldSatisfy` all (T.isPrefixOf "process.runtime.ghc.")+      length names `shouldBe` expectedBaseCount++    it "reports allocated_bytes > 0" $ do+      (provider, env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions+      m <- getMeter provider (instrumentationLibrary "test.ghc-metrics" "0.1.0")+      _ <- registerGHCMetrics m+      batches <- collectResourceMetrics env+      let names = concatMap extractMetricNames batches+      names `shouldSatisfy` elem "process.runtime.ghc.allocated_bytes"++    it "reports expected counter and gauge names" $ do+      (provider, env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions+      m <- getMeter provider (instrumentationLibrary "test.ghc-metrics" "0.1.0")+      _ <- registerGHCMetrics m+      batches <- collectResourceMetrics env+      let names = concatMap extractMetricNames batches++      names `shouldSatisfy` elem "process.runtime.ghc.gc.count"+      names `shouldSatisfy` elem "process.runtime.ghc.gc.cpu_time"+      names `shouldSatisfy` elem "process.runtime.ghc.gc.par_copied_bytes"+      names `shouldSatisfy` elem "process.runtime.ghc.gc.cumulative_live_bytes"+      names `shouldSatisfy` elem "process.runtime.ghc.mutator.cpu_time"+      names `shouldSatisfy` elem "process.runtime.ghc.init.cpu_time"+      names `shouldSatisfy` elem "process.runtime.ghc.cpu_time"+      names `shouldSatisfy` elem "process.runtime.ghc.elapsed_time"+      names `shouldSatisfy` elem "process.runtime.ghc.nonmoving_gc.sync.cpu_time"+      names `shouldSatisfy` elem "process.runtime.ghc.nonmoving_gc.cpu_time"++      names `shouldSatisfy` elem "process.runtime.ghc.memory.max_live_bytes"+      names `shouldSatisfy` elem "process.runtime.ghc.memory.max_large_objects_bytes"+      names `shouldSatisfy` elem "process.runtime.ghc.memory.max_compact_bytes"+      names `shouldSatisfy` elem "process.runtime.ghc.memory.max_slop_bytes"++      names `shouldSatisfy` elem "process.runtime.ghc.memory.live_bytes"+      names `shouldSatisfy` elem "process.runtime.ghc.memory.heap_size"+      names `shouldSatisfy` elem "process.runtime.ghc.gc.last.gen"+      names `shouldSatisfy` elem "process.runtime.ghc.gc.last.threads"+      names `shouldSatisfy` elem "process.runtime.ghc.gc.last.allocated_bytes"+      names `shouldSatisfy` elem "process.runtime.ghc.gc.last.slop_bytes"+      names `shouldSatisfy` elem "process.runtime.ghc.gc.last.cpu_time"+      names `shouldSatisfy` elem "process.runtime.ghc.gc.last.nonmoving_gc_sync_cpu_time"++  describe "ProcessMetrics" $ do+    it "registers expected number of callbacks" $ do+      (provider, _env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions+      m <- getMeter provider (instrumentationLibrary "test.process-metrics" "0.1.0")+      handles <- registerProcessMetrics m+      length handles `shouldBe` expectedProcessCount++    it "produces process.cpu.time metric" $ do+      (provider, env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions+      m <- getMeter provider (instrumentationLibrary "test.process-metrics" "0.1.0")+      _ <- registerProcessMetrics m+      batches <- collectResourceMetrics env+      let names = concatMap extractMetricNames batches+      names `shouldSatisfy` elem "process.cpu.time"++    it "produces process.memory.usage metric" $ do+      (provider, env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions+      m <- getMeter provider (instrumentationLibrary "test.process-metrics" "0.1.0")+      _ <- registerProcessMetrics m+      batches <- collectResourceMetrics env+      let names = concatMap extractMetricNames batches+      names `shouldSatisfy` elem "process.memory.usage"++    it "produces process.uptime metric" $ do+      (provider, env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions+      m <- getMeter provider (instrumentationLibrary "test.process-metrics" "0.1.0")+      _ <- registerProcessMetrics m+      batches <- collectResourceMetrics env+      let names = concatMap extractMetricNames batches+      names `shouldSatisfy` elem "process.uptime"++    it "produces process.paging.faults metric" $ do+      (provider, env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions+      m <- getMeter provider (instrumentationLibrary "test.process-metrics" "0.1.0")+      _ <- registerProcessMetrics m+      batches <- collectResourceMetrics env+      let names = concatMap extractMetricNames batches+      names `shouldSatisfy` elem "process.paging.faults"++    it "produces process.context_switches metric" $ do+      (provider, env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions+      m <- getMeter provider (instrumentationLibrary "test.process-metrics" "0.1.0")+      _ <- registerProcessMetrics m+      batches <- collectResourceMetrics env+      let names = concatMap extractMetricNames batches+      names `shouldSatisfy` elem "process.context_switches"++    it "produces process.runtime.ghc.capability.count metric" $ do+      (provider, env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions+      m <- getMeter provider (instrumentationLibrary "test.process-metrics" "0.1.0")+      _ <- registerProcessMetrics m+      batches <- collectResourceMetrics env+      let names = concatMap extractMetricNames batches+      names `shouldSatisfy` elem "process.runtime.ghc.capability.count"++#if defined(linux_HOST_OS)+    it "produces process.thread.count metric on Linux" $ do+      (provider, env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions+      m <- getMeter provider (instrumentationLibrary "test.process-metrics" "0.1.0")+      _ <- registerProcessMetrics m+      batches <- collectResourceMetrics env+      let names = concatMap extractMetricNames batches+      names `shouldSatisfy` elem "process.thread.count"++    it "produces process.unix.file_descriptor.count metric on Linux" $ do+      (provider, env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions+      m <- getMeter provider (instrumentationLibrary "test.process-metrics" "0.1.0")+      _ <- registerProcessMetrics m+      batches <- collectResourceMetrics env+      let names = concatMap extractMetricNames batches+      names `shouldSatisfy` elem "process.unix.file_descriptor.count"++    it "produces process.disk.io metric on Linux" $ do+      (provider, env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions+      m <- getMeter provider (instrumentationLibrary "test.process-metrics" "0.1.0")+      _ <- registerProcessMetrics m+      batches <- collectResourceMetrics env+      let names = concatMap extractMetricNames batches+      names `shouldSatisfy` elem "process.disk.io"+#endif++    it "reports non-negative uptime" $ do+      (provider, env) <- createMeterProvider emptyMaterializedResources defaultSdkMeterProviderOptions+      m <- getMeter provider (instrumentationLibrary "test.process-metrics" "0.1.0")+      _ <- registerProcessMetrics m+      batches <- collectResourceMetrics env+      let names = concatMap extractMetricNames batches+      names `shouldSatisfy` elem "process.uptime"+++extractMetricNames :: ResourceMetricsExport -> [T.Text]+extractMetricNames rme =+  concatMap scopeNames (V.toList (resourceMetricsScopes rme))+  where+    scopeNames sme = fmap metricExportName (V.toList (scopeMetricsExports sme))+    metricExportName (MetricExportSum {mesName = n}) = n+    metricExportName (MetricExportGauge {megName = n}) = n+    metricExportName (MetricExportHistogram {mehName = n}) = n+    metricExportName (MetricExportExponentialHistogram {meehName = n}) = n