├── .circleci └── config.yml ├── .github └── CODEOWNERS ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── ORIGINATOR ├── README.md ├── deps.edn ├── dev └── bench.clj ├── project.clj ├── src └── iapetos │ ├── collector.clj │ ├── collector │ ├── exceptions.clj │ ├── fn.clj │ ├── jvm.clj │ └── ring.clj │ ├── core.clj │ ├── export.clj │ ├── metric.clj │ ├── operations.clj │ ├── registry.clj │ ├── registry │ ├── collectors.clj │ └── utils.clj │ └── standalone.clj └── test └── iapetos ├── collector ├── exceptions_test.clj ├── fn_test.clj ├── jvm_test.clj └── ring_test.clj ├── collector_test.clj ├── core ├── counter_macro_test.clj ├── counter_test.clj ├── duration_macro_test.clj ├── gauge_test.clj ├── histogram_test.clj ├── lazy_test.clj ├── subsystem_test.clj ├── summary_test.clj └── timestamp_macro_test.clj ├── export_test.clj ├── metric_test.clj ├── registry └── collectors_test.clj ├── registry_test.clj ├── standalone_test.clj └── test └── generators.clj /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Clojure CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-clojure/ for more details 4 | # 5 | version: 2.1 6 | 7 | workflows: 8 | build-deploy: 9 | jobs: 10 | - build: 11 | filters: 12 | tags: 13 | only: /.*/ 14 | 15 | - deploy: 16 | requires: 17 | - build 18 | filters: 19 | tags: 20 | only: /Release-.*/ 21 | context: 22 | - CLOJARS_DEPLOY 23 | 24 | jobs: 25 | build: 26 | docker: 27 | # specify the version you desire here 28 | - image: circleci/clojure:openjdk-8-lein-2.9.1 29 | 30 | # Specify service dependencies here if necessary 31 | # CircleCI maintains a library of pre-built images 32 | # documented at https://circleci.com/docs/2.0/circleci-images/ 33 | # - image: circleci/postgres:9.4 34 | 35 | working_directory: ~/repo 36 | 37 | environment: 38 | LEIN_ROOT: "true" 39 | # Customize the JVM maximum heap limit 40 | JVM_OPTS: -Xmx3200m 41 | 42 | steps: 43 | - checkout 44 | 45 | # Download and cache dependencies 46 | - restore_cache: 47 | keys: 48 | - v1-dependencies-{{ checksum "project.clj" }} 49 | # fallback to using the latest cache if no exact match is found 50 | - v1-dependencies- 51 | 52 | - run: lein deps 53 | 54 | - save_cache: 55 | paths: 56 | - ~/.m2 57 | key: v1-dependencies-{{ checksum "project.clj" }} 58 | 59 | # run tests! 60 | - run: lein test 61 | deploy: 62 | docker: 63 | # specify the version you desire here 64 | - image: circleci/clojure:openjdk-8-lein-2.9.1 65 | # Specify service dependencies here if necessary 66 | # CircleCI maintains a library of pre-built images 67 | # documented at https://circleci.com/docs/2.0/circleci-images/ 68 | # - image: circleci/postgres:9.4 69 | 70 | working_directory: ~/repo 71 | 72 | environment: 73 | LEIN_ROOT: "true" 74 | # Customize the JVM maximum heap limit 75 | JVM_OPTS: -Xmx3200m 76 | 77 | steps: 78 | - checkout 79 | 80 | # Download and cache dependencies 81 | - restore_cache: 82 | keys: 83 | - v1-dependencies-{{ checksum "project.clj" }} 84 | # fallback to using the latest cache if no exact match is found 85 | - v1-dependencies- 86 | 87 | # Download and cache dependencies 88 | - restore_cache: 89 | keys: 90 | - v1-dependencies-{{ checksum "project.clj" }} 91 | # fallback to using the latest cache if no exact match is found 92 | - v1-dependencies- 93 | 94 | - run: 95 | name: Install babashka 96 | command: | 97 | curl -s https://raw.githubusercontent.com/borkdude/babashka/master/install -o install.sh 98 | sudo bash install.sh 99 | rm install.sh 100 | - run: 101 | name: Install deployment-script 102 | command: | 103 | curl -s https://raw.githubusercontent.com/clj-commons/infra/main/deployment/circle-maybe-deploy.bb -o circle-maybe-deploy.bb 104 | chmod a+x circle-maybe-deploy.bb 105 | 106 | - run: lein deps 107 | 108 | - run: 109 | name: Setup GPG signing key 110 | command: | 111 | GNUPGHOME="$HOME/.gnupg" 112 | export GNUPGHOME 113 | mkdir -p "$GNUPGHOME" 114 | chmod 0700 "$GNUPGHOME" 115 | 116 | echo "$GPG_KEY" \ 117 | | base64 --decode --ignore-garbage \ 118 | | gpg --batch --allow-secret-key-import --import 119 | 120 | gpg --keyid-format LONG --list-secret-keys 121 | 122 | - save_cache: 123 | paths: 124 | - ~/.m2 125 | key: v1-dependencies-{{ checksum "project.clj" }} 126 | - run: 127 | name: Deploy 128 | command: | 129 | GPG_TTY=$(tty) 130 | export GPG_TTY 131 | echo $GPG_TTY 132 | ./circle-maybe-deploy.bb lein deploy clojars 133 | 134 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @slipset @MalloZup 2 | # Original creator and Author: Yannick Scherer 3 | * @xsc 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | .idea 13 | *.iml 14 | .cpcache/ 15 | .lsp/ 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | sudo: required 3 | jdk: 4 | - openjdk8 5 | after_success: 6 | - lein codecov 7 | - bash <(curl -s https://codecov.io/bash) -f target/coverage/codecov.json 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version 0.1.9: First clj-commons Release 2 | 3 | ## Breaking Changes 4 | 5 | - clojars group change: move from iapetos to `clj-commons/iapetos`(https://clojars.org/clj-commons/iapetos) 6 | 7 | This change was needed to continue publishing new clojars to `clj-commons` after project migration. 8 | 9 | ## Changes: 10 | 11 | - updates Prometheus java dependencies to 0.6.0 12 | - introduce CHANGELOG.md 13 | 14 | 15 | # Version 0.1.8: Unregister/Clear Collectors 16 | 17 | ## Breaking Changes 18 | 19 | None. 20 | 21 | ## Deprecation 22 | 23 | - The `:lazy?` flag on collectors is now deprecated, use `register-lazy` instead. 24 | 25 | ## Features 26 | 27 | - upgrades the Prometheus Java dependencies to version 0.2.0. 28 | - introduces `register-lazy` as a replacement for the `:lazy?` flag on collectors. 29 | - introduces `clear` and `unregister` functions to remove collectors from a registry they 30 | were previously added to (see #10). 31 | 32 | 33 | # Version 0.1.7: Default Registry 34 | 35 | ## Breaking Changes 36 | 37 | None. 38 | 39 | ## Features 40 | 41 | - upgrades the Prometheus Java dependencies to version 0.0.26. 42 | - allows access to the default registry using iapetos.core/default-registry (see #10). 43 | - allows "wrapping" of an existing registry using pushable-collector-registry to make it pushable. 44 | 45 | 46 | # Version 0.1.6: Summary Quantiles 47 | 48 | ## Breaking Changes 49 | 50 | None. 51 | 52 | ## Features 53 | 54 | - allows specification of :quantiles when creating a summary collector (see #6). 55 | 56 | 57 | # Version 0.1.5: Ring Latency Buckets 58 | 59 | ## Breaking Changes 60 | 61 | None. 62 | 63 | ## Bugfixes 64 | 65 | - no longer ignores :latency-histogram-buckets option in Ring collector (see #5). 66 | 67 | 68 | # Version 0.1.4: Ring Collector Labels 69 | 70 | ## Breaking Changes 71 | 72 | None. 73 | 74 | ## Features 75 | 76 | - allows adding of additional labels to the Ring collector (see #4, thanks to @psalaberria002). 77 | 78 | 79 | # Version 0.1.3: Java Simple Client Upgrade 80 | 81 | ## Breaking Changes 82 | 83 | None. 84 | 85 | ## Dependencies 86 | 87 | This release upgrades the Java client dependencies to the latest versions. 88 | 89 | 90 | # Version 0.1.2: Add Request Hook to Ring Middlewares 91 | 92 | ## Breaking Changes 93 | 94 | None. 95 | 96 | ## Features 97 | 98 | - adds a :on-request hook to the wrap-metrics and wrap-metrics-expose middleware (see #2). 99 | 100 | 101 | # Version 0.1.1: Fix 'wrap-metrics' Middleware 102 | 103 | ## Breaking Changes 104 | 105 | None. 106 | 107 | ## Bugfixes 108 | 109 | - fixes passing of options from wrap-metrics to wrap-instrumentation, allowing for setting of :path-fn. 110 | 111 | 112 | # Version 0.1.0: Initial Release 113 | 114 | This is the initial release of iapetos, a Clojure [Prometheus](https://prometheus.io/) client. 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Yannick Scherer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /ORIGINATOR: -------------------------------------------------------------------------------- 1 | @xsc 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iapetos 2 | 3 | __iapetos__ is a Clojure wrapper around the [Prometheus Java 4 | Client][java-client], providing idiomatic and simple access to commonly used 5 | functionality while retaining low-level flexibility for tackling more complex 6 | tasks. 7 | 8 | [![Clojars Project](https://img.shields.io/clojars/v/clj-commons/iapetos.svg)](https://clojars.org/clj-commons/iapetos) 9 | [![cljdoc badge](https://cljdoc.org/badge/clj-commons/iapetos)](https://cljdoc.org/d/clj-commons/iapetos/CURRENT) 10 | [![CircleCI](https://circleci.com/gh/clj-commons/iapetos.svg?style=svg)](https://circleci.com/gh/clj-commons/iapetos) 11 | [![codecov](https://codecov.io/gh/clj-commons/iapetos/branch/master/graph/badge.svg)](https://codecov.io/gh/clj-commons/iapetos) 12 | 13 | [java-client]: https://github.com/prometheus/client_java 14 | 15 | **N.B.** Since version 0.1.9, iapetos is released as `clj-commons/iapetos` on Clojars. Previously it was available as `xsc/iapetos`. 16 | 17 | ### Table of Contents 18 | - [Basic Usage](#basic-usage) 19 | - [Registering Metrics](#registering-metrics) 20 | - [Metric Export](#metric-export) 21 | - [Metric Push](#metric-push) 22 | - [Labels](#labels) 23 | - [Subsystems](#subsystems) 24 | - [Features](#fatures) 25 | - [Code Block Instrumentation](#code-block-instrumentation) 26 | - [JVM Metrics](#jvm-metrics) 27 | - [Function Instrumentation](#function-instrumentation) 28 | - [Ring](#ring) 29 | - [Exception Handling](#exception-handling) 30 | - [Standalone HTTP Server](#standalone-http-server) 31 | - [History](#history) 32 | - [License](#license) 33 | 34 | 35 | ## Basic Usage 36 | 37 | ### Registering Metrics 38 | 39 | [__Documentation__](https://clj-commons.github.io/iapetos/iapetos.core.html) 40 | 41 | All metrics have to be registered with a collector registry before being used: 42 | 43 | ```clojure 44 | (require '[iapetos.core :as prometheus]) 45 | 46 | (defonce registry 47 | (-> (prometheus/collector-registry) 48 | (prometheus/register 49 | (prometheus/histogram :app/duration-seconds) 50 | (prometheus/gauge :app/active-users-total) 51 | (prometheus/counter :app/runs-total)) 52 | (prometheus/register-lazy 53 | (prometheus/gauge :app/last-success-unixtime)))) 54 | ``` 55 | 56 | Now, they are ready to be set and changed: 57 | 58 | ```clojure 59 | (-> registry 60 | (prometheus/inc :app/runs-total) 61 | (prometheus/observe :app/duration-seconds 0.7) 62 | (prometheus/set :app/active-users-total 22)) 63 | ``` 64 | 65 | The registry itself implements `clojure.lang.IFn` to allow access to all 66 | registered metrics (plus setting of metric [labels](#labels)), e.g.: 67 | 68 | ```clojure 69 | (registry :app/duration-seconds) 70 | ;; => #object[io.prometheus.client.Histogram$Child ...] 71 | ``` 72 | 73 | All metric operations can be called directly on such a collector, i.e.: 74 | 75 | ```clojure 76 | (prometheus/inc (registry :app/runs-total)) 77 | (prometheus/observe (registry :app/duration-seconds) 0.7) 78 | (prometheus/set (registry :app/active-users-total) 22) 79 | ``` 80 | 81 | ### Metric Export 82 | 83 | [__Documentation__](https://clj-commons.github.io/iapetos/iapetos.export.html) 84 | 85 | Metrics can be transformed into a textual representation using 86 | `iapetos.export/text-format`: 87 | 88 | ```clojure 89 | (require '[iapetos.export :as export]) 90 | 91 | (print (export/text-format registry)) 92 | ;; # HELP app_active_users_total a gauge metric. 93 | ;; # TYPE app_active_users_total gauge 94 | ;; app_active_users_total 22.0 95 | ;; # HELP app_runs_total a counter metric. 96 | ;; # TYPE app_runs_total counter 97 | ;; app_runs_total 1.0 98 | ;; ... 99 | ``` 100 | 101 | This could now be exposed e.g. using an HTTP endpoint (see also iapetos' 102 | [Ring](#ring) integration or the [standalone server](#standalone-http-server) ). 103 | 104 | ### Metric Push 105 | 106 | [__Documentation__](https://clj-commons.github.io/iapetos/iapetos.export.html) 107 | 108 | Another way of communicating metrics to Prometheus is using push mechanics, 109 | intended to be used for e.g. batch jobs that might not live long enough to be 110 | scraped in time. Iapetos offers a special kind of registry for this: 111 | 112 | ```clojure 113 | (require '[iapetos.export :as export]) 114 | 115 | (defonce registry 116 | (-> (export/pushable-collector-registry 117 | {:push-gateway "push-gateway-host:12345" 118 | :job "my-batch-job"}) 119 | (prometheus/register ...))) 120 | ... 121 | (export/push! registry) 122 | ``` 123 | 124 | Note that you can reduce the amount of boilerplate in most cases down to 125 | something like: 126 | 127 | ```clojure 128 | (export/with-push-gateway [registry {:push-gateway "...", :job "..."}] 129 | (-> registry 130 | (prometheus/register 131 | (prometheus/counter :app/rows-inserted-total) 132 | ...) 133 | (run-job! ...))) 134 | ``` 135 | 136 | ### Labels 137 | 138 | Prometheus allows for labels to be associated with metrics which can be declared 139 | for each collector before it is registered: 140 | 141 | ```clojure 142 | (def job-latency-histogram 143 | (prometheus/histogram 144 | :app/job-latency-seconds 145 | {:description "job execution latency by job type" 146 | :labels [:job-type] 147 | :buckets [1.0 5.0 7.5 10.0 12.5 15.0]})) 148 | 149 | (defonce registry 150 | (-> (prometheus/collector-registry) 151 | (prometheus/register job-latency-histogram))) 152 | ``` 153 | 154 | Now, you can lookup a collector bound to a set of labels by calling the 155 | registry with a label/value-map: 156 | 157 | ```clojure 158 | (prometheus/observe (registry :app/job-latency-seconds {:job-type "pull"}) 14.2) 159 | (prometheus/observe (registry :app/job-latency-seconds {:job-type "push"}) 8.7) 160 | 161 | (print (export/text-format registry)) 162 | ;; # HELP app_job_latency_seconds job execution latency by job type 163 | ;; # TYPE app_job_latency_seconds histogram 164 | ;; app_job_latency_seconds_bucket{job_type="pull",le="1.0",} 0.0 165 | ;; app_job_latency_seconds_bucket{job_type="pull",le="5.0",} 0.0 166 | ;; ... 167 | ;; app_job_latency_seconds_bucket{job_type="push",le="1.0",} 0.0 168 | ;; app_job_latency_seconds_bucket{job_type="push",le="5.0",} 0.0 169 | ;; ... 170 | ``` 171 | 172 | ### Subsystems 173 | 174 | In addition to namespaces, you can create collector declarations belonging to a 175 | subsystem, i.e.: 176 | 177 | ```clojure 178 | (prometheus/counter 179 | :app/job-runs-total 180 | {:description "the total number of finished job executions." 181 | :subsystem "worker"}) 182 | ``` 183 | 184 | But this reduces its reusability - you might want to register the above counter 185 | twice in different subsystems without having to create it anew - which is why 186 | iapetos lets you specify the subsystem on the registry level: 187 | 188 | ```clojure 189 | (defonce registry 190 | (prometheus/collector-registry)) 191 | 192 | (defonce worker-registry 193 | (-> registry 194 | (prometheus/subsystem "worker") 195 | (prometheus/register ...))) 196 | 197 | (defonce httpd-registry 198 | (-> registry 199 | (prometheus/subsystem "httpd") 200 | (prometheus/register ...))) 201 | ``` 202 | 203 | Now, collectors added to `worker-registry` and `httpd-registry` will have the 204 | appropriate subsystem. And when `registry` is exported it will contain all 205 | metrics that were added to the subsystems. 206 | 207 | (Note, however, that the subsystem registries will not have access to the 208 | original registry's collectors, i.e. you have to reregister things like 209 | [function instrumentation](#function-instrumentation) or [Ring](#ring) 210 | collectors.) 211 | 212 | ## Features 213 | 214 | ### Code Block Instrumentation 215 | 216 | [__Documentation__](https://clj-commons.github.io/iapetos/iapetos.core.html) 217 | 218 | iapetos provides a number of macros that you can use to instrument parts of your 219 | code, e.g. `with-failure-timestamp` to record the last time a task has thrown an 220 | error or `with-duration` to track execution time: 221 | 222 | ```clojure 223 | (prometheus/with-failure-timestamp (registry :app/last-worker-failure-unixtime) 224 | (prometheus/with-duration (registry :app/worker-latency-seconds) 225 | (run-worker! task))) 226 | ``` 227 | 228 | See the auto-generated documentation for all available macros or the [function 229 | instrumentation](#function-instrumentation) section below on how to easily wrap 230 | them around existing functions. 231 | 232 | ### JVM Metrics 233 | 234 | [__Documentation__](https://clj-commons.github.io/iapetos/iapetos.collector.jvm.html) 235 | 236 | Some characteristics of your current JVM are always useful (e.g. memory 237 | usage, thread count, ...) and can be added to your registry using the 238 | `iapetos.collector.jvm` namespace: 239 | 240 | ```clojure 241 | (require '[iapetos.collector.jvm :as jvm]) 242 | 243 | (defonce registry 244 | (-> (prometheus/collector-registry) 245 | (jvm/initialize))) 246 | ``` 247 | 248 | Alternatively, you can selectively register the JVM collectors: 249 | 250 | ```clojure 251 | (defonce registry 252 | (-> (prometheus/collector-registry) 253 | (prometheus/register 254 | (jvm/standard) 255 | (jvm/gc) 256 | (jvm/memory-pools) 257 | (jvm/threads)))) 258 | ``` 259 | 260 | __Note:__ You need to include the artifact `io.prometheus/simpleclient_hotspot` 261 | explicitly in your project's dependencies. 262 | 263 | ### Function Instrumentation 264 | 265 | [__Documentation__](https://clj-commons.github.io/iapetos/iapetos.collector.fn.html) 266 | 267 | To collect metrics about specific functions, you can use the functionality 268 | provided in `iapetos.collector.fn`: 269 | 270 | ```clojure 271 | (require '[iapetos.collector.fn :as fn]) 272 | 273 | (defn- run-the-job! 274 | [job] 275 | ...) 276 | 277 | (defonce registry 278 | (-> (prometheus/collector-registry) 279 | ... 280 | (fn/initialize))) 281 | 282 | (fn/instrument! registry #'run-the-job!) 283 | ``` 284 | 285 | Now, every call to `run-the-job!` will update a series of duration, success and 286 | failure metrics. Note, however, that re-evaluation of the `run-the-job!` 287 | declaration will remove the instrumentation again. 288 | 289 | ### Ring 290 | 291 | [__Documentation__](https://clj-commons.github.io/iapetos/iapetos.collector.ring.html) 292 | 293 | `iapetos.collector.ring` offers middlewares to 294 | 295 | - expose a iapetos collector registry via a fixed HTTP endpoint, and 296 | - collect metrics for Ring handlers. 297 | 298 | First, you need to initialize the available collectors in the registry: 299 | 300 | ```clojure 301 | (require '[iapetos.collector.ring :as ring]) 302 | 303 | (defonce registry 304 | (-> (prometheus/collector-registry) 305 | (ring/initialize))) 306 | ``` 307 | 308 | Afterwards, you can add the middlewares to your Ring stack: 309 | 310 | ```clojure 311 | (def app 312 | (-> (constantly {:status 200}) 313 | (ring/wrap-metrics registry {:path "/metrics"}))) 314 | ``` 315 | 316 | The following metrics will now be collected and exposed via the `GET /metrics` 317 | endpoint: 318 | 319 | - `http_requests_total` 320 | - `http_request_latency_seconds` 321 | - `http_exceptions_total` 322 | 323 | These are, purposefully, compatible with the metrics produced by 324 | [prometheus-clj](https://github.com/soundcloud/prometheus-clj), as to allow a 325 | smooth migration. 326 | 327 | Ring supports sync and async [handlers](https://github.com/ring-clojure/ring/wiki/Concepts#handlers), metrics are collected for both by `iapetos.collector.ring` ns. 328 | 329 | #### Exception Handling 330 | 331 | By default, if your ring handler throws an exception, only the `http_exceptions_total` counter would be incremented. 332 | This means that if you respond with a 500 error code on exceptions: 333 | 334 | 1. These responses won't be counted on `http_requests_total` 335 | 2. Their latencies won't be observed on `http_request_latency_seconds` 336 | 337 | To overcome this, you can use the optional `:exception-status` to define a status code to be reported 338 | on both metrics, for example: 339 | 340 | ```clojure 341 | (def app 342 | (-> (fn [_] (throw (Exception.))) 343 | (ring/wrap-metrics registry {:path "/metrics" :exception-status 500}))) 344 | ``` 345 | 346 | will increment all 3 metrics, assuming a 500 response code for exceptions: 347 | 348 | - `http_requests_total` 349 | - `http_request_latency_seconds` 350 | - `http_exceptions_total` 351 | 352 | ### Standalone HTTP Server 353 | 354 | [__Documentation__](https://clj-commons.github.io/iapetos/iapetos.standalone.html) 355 | 356 | A zero-dependency standalone HTTP server is included in `iapetos.standalone` 357 | and can be run using: 358 | 359 | ```clojure 360 | (require '[iapetos.standalone :as standalone]) 361 | 362 | (defonce httpd 363 | (standalone/metrics-server registry {:port 8080})) 364 | ``` 365 | 366 | This is particularly useful for applications that do not expose an HTTP port 367 | themselves but shall still be scraped by Prometheus. By default, metrics will 368 | be exposed at `/metrics`. 369 | 370 | ## History 371 | 372 | iapetos was originally created by Yannick Scherer ([@xsc](https://github.com/xsc)). In July 2019 it was moved to CLJ Commons for continued maintenance. 373 | 374 | It could previously be found at [xsc/iapetos](https://github.com/xsc/iapetos). [clj-commons/iapetos](https://github.com/clj-commons/iapetos) is the canonical repository now. 375 | 376 | ## License 377 | 378 | ``` 379 | MIT License 380 | 381 | Copyright (c) 2016 Yannick Scherer 382 | 383 | Permission is hereby granted, free of charge, to any person obtaining a copy 384 | of this software and associated documentation files (the "Software"), to deal 385 | in the Software without restriction, including without limitation the rights 386 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 387 | copies of the Software, and to permit persons to whom the Software is 388 | furnished to do so, subject to the following conditions: 389 | 390 | The above copyright notice and this permission notice shall be included in all 391 | copies or substantial portions of the Software. 392 | 393 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 394 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 395 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 396 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 397 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 398 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 399 | SOFTWARE. 400 | ``` 401 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.10.3"} 2 | io.prometheus/simpleclient {:mvn/version "0.12.0"} 3 | io.prometheus/simpleclient_common {:mvn/version "0.12.0"} 4 | io.prometheus/simpleclient_pushgateway {:mvn/version "0.12.0"} 5 | io.prometheus/simpleclient_hotspot {:mvn/version "0.12.0"}}} 6 | -------------------------------------------------------------------------------- /dev/bench.clj: -------------------------------------------------------------------------------- 1 | (ns bench 2 | (:require [clojure.test.check.generators :as gen] 3 | [iapetos.core :as prometheus] 4 | [iapetos.registry :as r] 5 | [iapetos.registry.collectors :as c] 6 | [iapetos.test.generators :as g] 7 | [jmh.core :as jmh]) 8 | (:import [iapetos.registry IapetosRegistry])) 9 | 10 | (defn metrics 11 | [metric-count] 12 | (gen/sample g/metric metric-count)) 13 | 14 | (def dirty-string-metric 15 | (gen/let [first-char gen/char-alpha 16 | invalid-chars (gen/return (apply str (map char (range 33 45)))) 17 | last-char gen/char-alphanumeric 18 | rest-chars gen/string-alphanumeric] 19 | (gen/return 20 | (str 21 | (apply str first-char invalid-chars rest-chars) 22 | last-char)))) 23 | 24 | (defn dirty-metrics 25 | [metric-count] 26 | (gen/sample dirty-string-metric metric-count)) 27 | 28 | ;; JMH fns 29 | 30 | (defn collectors 31 | [^IapetosRegistry registry] 32 | (.-collectors registry)) 33 | 34 | (defn register-collectors 35 | [metrics] 36 | (reduce (fn [reg metric] 37 | (r/register reg metric (prometheus/counter metric))) 38 | (r/create) 39 | metrics)) 40 | 41 | (defn lookup 42 | [collectors metric] 43 | (c/lookup collectors metric {})) 44 | 45 | (def bench-env 46 | {:benchmarks [{:name :registry-lookup 47 | :fn `lookup 48 | :args [:state/collectors :state/metric]} 49 | 50 | {:name :dirty-registry-lookup 51 | :fn `lookup 52 | :args [:state/dirty-collectors :state/dirty-metric]}] 53 | 54 | :states {:dirty-metrics {:fn `dirty-metrics :args [:param/metric-count]} 55 | :dirty-metric {:fn `rand-nth :args [:state/dirty-metrics]} 56 | :dirty-registry {:fn `register-collectors :args [:state/dirty-metrics]} 57 | :dirty-collectors {:fn `collectors :args [:state/dirty-registry]} 58 | 59 | :metrics {:fn `metrics :args [:param/metric-count]} 60 | :metric {:fn `rand-nth :args [:state/metrics]} 61 | :registry {:fn `register-collectors :args [:state/metrics]} 62 | :collectors {:fn `collectors :args [:state/registry]}} 63 | 64 | :params {:metric-count 500} 65 | 66 | :options {:registry-lookup {:measurement {:iterations 1000}} 67 | :dirty-registry-lookup {:measurement {:iterations 1000}} 68 | :jmh/default {:mode :average 69 | :output-time-unit :us 70 | :measurement {:iterations 1000 71 | :count 1}}}}) 72 | 73 | (def bench-opts 74 | {:type :quick 75 | :params {:metric-count 500}}) 76 | 77 | (comment 78 | 79 | (map #(select-keys % [:fn :mode :name :score]) (jmh/run bench-env bench-opts)) 80 | 81 | ) 82 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clj-commons/iapetos (or (System/getenv "PROJECT_VERSION") "0.1.12") 2 | :description "A Clojure Prometheus Client" 3 | :url "https://github.com/clj-commons/iapetos" 4 | :license {:name "MIT License" 5 | :url "https://opensource.org/licenses/MIT" 6 | :year 2019 7 | :key "mit"} 8 | :deploy-repositories [["clojars" {:url "https://repo.clojars.org" 9 | :username :env/clojars_username 10 | :password :env/clojars_password 11 | :sign-releases true}]] 12 | 13 | :dependencies [[org.clojure/clojure "1.10.3" :scope "provided"] 14 | [io.prometheus/simpleclient "0.12.0"] 15 | [io.prometheus/simpleclient_common "0.12.0"] 16 | [io.prometheus/simpleclient_pushgateway "0.12.0"] 17 | [io.prometheus/simpleclient_hotspot "0.12.0" :scope "provided"]] 18 | :profiles {:dev 19 | {:dependencies [[org.clojure/test.check "1.1.0"] 20 | [aleph "0.4.6"] 21 | [jmh-clojure "0.4.1"]] 22 | :source-paths ["dev"] 23 | :global-vars {*warn-on-reflection* true}} 24 | :codox 25 | {:plugins [[lein-codox "0.10.0"]] 26 | :dependencies [[codox-theme-rdash "0.1.2"]] 27 | :codox {:project {:name "iapetos"} 28 | :metadata {:doc/format :markdown} 29 | :themes [:rdash] 30 | :source-uri "https://github.com/clj-commons/iapetos/blob/v{version}/{filepath}#L{line}" 31 | :namespaces [iapetos.core 32 | iapetos.export 33 | iapetos.standalone 34 | #"^iapetos\.collector\..+"]}} 35 | :coverage 36 | {:plugins [[lein-cloverage "1.0.9"]] 37 | :pedantic? :warn 38 | :dependencies [[org.clojure/tools.reader "1.3.5"] 39 | [riddley "0.2.0"]]}} 40 | :aliases {"codox" ["with-profile" "+codox" "codox"] 41 | "codecov" ["with-profile" "+coverage" "cloverage" "--codecov"]} 42 | :pedantic? :abort) 43 | -------------------------------------------------------------------------------- /src/iapetos/collector.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.collector 2 | (:require [iapetos.metric :as metric]) 3 | (:import [io.prometheus.client 4 | Collector$MetricFamilySamples 5 | CollectorRegistry 6 | SimpleCollector 7 | SimpleCollector$Builder])) 8 | 9 | ;; ## Protocol 10 | 11 | (defprotocol Collector 12 | "Protocol for Collectors to be registered with a iapetos registry." 13 | (instantiate [this registry-options] 14 | "Return a collector instance that can be registered with collector 15 | registries.") 16 | (metric [this] 17 | "Return a `iapetos.metric/Metric` for this collector.") 18 | (metric-id [this] 19 | "Return user supplied (unsanitized) identifier for this collector.") 20 | (label-instance [this instance values] 21 | "Add labels to the given collector instance produced by `instantiate`.")) 22 | 23 | ;; ## Labels 24 | 25 | (defn- label-array 26 | ^"[Ljava.lang.String;" 27 | [labels] 28 | (into-array String (map metric/sanitize labels))) 29 | 30 | (defn- label-names 31 | [labels] 32 | (map metric/dasherize labels)) 33 | 34 | (defn- set-labels 35 | "Attach labels to the given `SimpleCollector` instance." 36 | [^SimpleCollector instance labels values] 37 | (let [label->value (->> (for [[k v] values] 38 | [(-> k metric/dasherize) v]) 39 | (into {}) 40 | (comp str)) 41 | ordered-labels (->> labels (map label->value) (into-array String))] 42 | (.labels instance ordered-labels))) 43 | 44 | ;; ## Record 45 | 46 | (defn- check-subsystem 47 | [{subsystem :subsystem} {subsystem' :subsystem}] 48 | (when (and subsystem subsystem' (not= subsystem subsystem')) 49 | (throw 50 | (IllegalArgumentException. 51 | (format 52 | "collector subsystem (%s) is conflicting with registry subsystem (%s)." 53 | (pr-str subsystem') 54 | (pr-str subsystem))))) 55 | (or subsystem subsystem')) 56 | 57 | (defrecord SimpleCollectorImpl [type 58 | namespace 59 | name 60 | metric-id 61 | description 62 | subsystem 63 | labels 64 | builder-constructor 65 | lazy?] 66 | Collector 67 | (instantiate [this registry-options] 68 | (let [subsystem (check-subsystem this registry-options)] 69 | (-> ^SimpleCollector$Builder 70 | (builder-constructor) 71 | (.name name) 72 | (.namespace namespace) 73 | (.help description) 74 | (.labelNames (label-array labels)) 75 | (cond-> subsystem (.subsystem subsystem)) 76 | (.create)))) 77 | (metric [_] 78 | {:name name 79 | :namespace namespace}) 80 | (metric-id [_] 81 | metric-id) 82 | (label-instance [_ instance values] 83 | (set-labels instance labels values))) 84 | 85 | (defn make-simple-collector 86 | "Create a new simple collector representation to be instantiated and 87 | registered with a iapetos registry." 88 | [{:keys [^String name 89 | ^String namespace 90 | ^String subsystem 91 | ^String description 92 | labels 93 | lazy?] 94 | ::metric/keys [id]} 95 | collector-type 96 | builder-constructor] 97 | {:pre [type name namespace description]} 98 | (map->SimpleCollectorImpl 99 | {:type collector-type 100 | :namespace namespace 101 | :name name 102 | :metric-id id 103 | :description description 104 | :subsystem subsystem 105 | :labels (label-names labels) 106 | :builder-constructor builder-constructor 107 | :lazy? lazy?})) 108 | 109 | ;; ## Implementation for Raw Collectors 110 | 111 | (defn- raw-metric 112 | [^io.prometheus.client.Collector v] 113 | (if-let [n (some-> (.collect v) 114 | ^Collector$MetricFamilySamples (first) 115 | (.name))] 116 | (let [[a b] (.split n "_" 2)] 117 | (if b 118 | {:name b, :namespace a} 119 | {:name a, :namespace "raw"})) 120 | {:name (str (.getSimpleName (class v)) "_" (hash v)) 121 | :namespace "raw"})) 122 | 123 | (extend-protocol Collector 124 | io.prometheus.client.Collector 125 | (instantiate [this _] 126 | this) 127 | (metric [this] 128 | (raw-metric this)) 129 | (metric-id [this] 130 | (hash this)) 131 | (label-instance [_ instance values] 132 | (if-not (empty? values) 133 | (throw (UnsupportedOperationException.)) 134 | instance))) 135 | 136 | ;; ## Named Collector 137 | 138 | (defn named 139 | [metric collector] 140 | (reify Collector 141 | (instantiate [_ options] 142 | (instantiate collector options)) 143 | (metric [_] 144 | metric) 145 | (metric-id [_] 146 | metric) 147 | (label-instance [_ instance values] 148 | (label-instance collector instance values)))) 149 | -------------------------------------------------------------------------------- /src/iapetos/collector/exceptions.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.collector.exceptions 2 | (:require [iapetos.core :as prometheus] 3 | [iapetos.collector :as collector] 4 | [iapetos.operations :as ops])) 5 | 6 | ;; ## Exception Counter Child 7 | ;; 8 | ;; This will wrap the original collector, the counter instance to use and 9 | ;; desired labels. Eventually, we'll add the 'exceptionClass' label and 10 | ;; increment the counter from within 'with-exception'. 11 | 12 | (deftype ExceptionCounterChild [collector instance values] 13 | clojure.lang.IFn 14 | (invoke [_ exception-class] 15 | (->> (assoc values :exceptionClass exception-class) 16 | (collector/label-instance collector instance))) 17 | 18 | ops/ReadableCollector 19 | (read-value [_] 20 | (if (contains? values :exceptionClass) 21 | (ops/read-value (collector/label-instance collector instance values)) 22 | (throw 23 | (IllegalStateException. 24 | "cannot read exception counter without 'exceptionClass' label.")))) 25 | 26 | ops/IncrementableCollector 27 | (increment* [_ amount] 28 | (throw 29 | (IllegalStateException. 30 | "exception counters cannot be incremented directly.")))) 31 | 32 | (alter-meta! #'->ExceptionCounterChild assoc :private true) 33 | 34 | ;; ## Exception Counter 35 | ;; 36 | ;; This is a wrapper around a counter that defers labeling of the counter 37 | ;; instance so it can be done when 'exceptionClass' is known, i.e. from 38 | ;; within 'with-exception'. 39 | 40 | (deftype ExceptionCounter [counter] 41 | collector/Collector 42 | (instantiate [this registry-options] 43 | (collector/instantiate counter registry-options)) 44 | (metric [this] 45 | (collector/metric counter)) 46 | (metric-id [this] 47 | (collector/metric-id counter)) 48 | (label-instance [_ instance values] 49 | (->ExceptionCounterChild counter instance values))) 50 | 51 | (alter-meta! #'->ExceptionCounter assoc :private true) 52 | 53 | ;; ## Constructor 54 | 55 | (defn exception-counter 56 | "Create a new exception counter. 57 | 58 | Note that the label `exceptionClass` will be automatically added." 59 | [metric 60 | & [{:keys [description labels] 61 | :or {description "the number and class of encountered exceptions."}}]] 62 | (->ExceptionCounter 63 | (prometheus/counter 64 | metric 65 | {:description description 66 | :labels (conj (vec labels) :exceptionClass)}))) 67 | 68 | ;; ## Logic 69 | 70 | (defn ^:no-doc record-exception! 71 | [^ExceptionCounterChild child ^Throwable t] 72 | {:pre [(instance? ExceptionCounterChild child)]} 73 | (let [exception-class (.getName (class t)) 74 | instance (child exception-class)] 75 | (prometheus/inc instance))) 76 | 77 | (defmacro with-exceptions 78 | "Use the given [[exception-counter]] to collect any Exceptions thrown within 79 | the given piece of code. 80 | 81 | ``` 82 | (defonce registry 83 | (-> (prometheus/collector-registry) 84 | (prometheus/register 85 | (exeception-counter :app/exceptions-total)))) 86 | 87 | (with-exceptions (registry :app/exceptions-total) 88 | ...) 89 | ``` 90 | 91 | The exception class will be stored in the counter's `exceptionClass` label." 92 | [exception-counter & body] 93 | `(let [c# ~exception-counter] 94 | (try 95 | (do ~@body) 96 | (catch Throwable t# 97 | (record-exception! c# t#) 98 | (throw t#))))) 99 | -------------------------------------------------------------------------------- /src/iapetos/collector/fn.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.collector.fn 2 | (:require [iapetos.collector :as collector] 3 | [iapetos.core :as prometheus] 4 | [iapetos.metric :as metric] 5 | [iapetos.collector.exceptions :as ex]) 6 | (:import [io.prometheus.client CollectorRegistry])) 7 | 8 | ;; ## Instrumentation 9 | 10 | (defmacro ^:private wrap 11 | [body f] 12 | `(let [f# ~f] 13 | (fn [& args#] 14 | (->> (apply f# args#) ~body)))) 15 | 16 | (defmacro ^:private wrap->> 17 | [v & pairs] 18 | (->> (partition 2 pairs) 19 | (mapcat 20 | (fn [[condition body]] 21 | (list condition `(wrap ~body)))) 22 | (list* `cond->> v))) 23 | 24 | (defn wrap-instrumentation 25 | "Wrap the given function to write a series of execution metrics to the given 26 | registry. See [[initialize]]." 27 | [f registry fn-name 28 | {:keys [duration? 29 | exceptions? 30 | last-failure? 31 | run-count? 32 | labels] 33 | :or {duration? true 34 | exceptions? true 35 | last-failure? true 36 | run-count? true 37 | labels {}}}] 38 | (let [labels (into labels {:fn fn-name, :result "success"}) 39 | failure-labels (assoc labels :result "failure")] 40 | (wrap->> 41 | f 42 | duration? (prometheus/with-duration 43 | (registry :fn/duration-seconds labels)) 44 | exceptions? (ex/with-exceptions 45 | (registry :fn/exceptions-total labels)) 46 | last-failure? (prometheus/with-failure-timestamp 47 | (registry :fn/last-failure-unixtime labels)) 48 | run-count? (prometheus/with-failure-counter 49 | (registry :fn/runs-total failure-labels)) 50 | run-count? (prometheus/with-success-counter 51 | (registry :fn/runs-total labels))))) 52 | 53 | (defn- instrument-function! 54 | [registry fn-name fn-var options] 55 | (let [f' (-> fn-var 56 | (alter-meta! update ::original #(or % @fn-var)) 57 | (::original) 58 | (wrap-instrumentation registry fn-name options))] 59 | (alter-var-root fn-var (constantly f')))) 60 | 61 | ;; ## Collectors 62 | 63 | (defn initialize 64 | "Enable function instrumentalization by registering the metric collectors. 65 | Metrics include: 66 | 67 | - `fn_duration_seconds`: a histogram of execution duration, 68 | - `fn_last_failure_unixtime`: a gauge with the last failure timestamp, 69 | - `fn_runs_total`: a counter for fn runs, split by success/failure, 70 | - `fn_exceptions_total`: a counter for fn exceptions, split by class. 71 | " 72 | [registry & [{:keys [labels]}]] 73 | (->> (vector 74 | (prometheus/histogram 75 | :fn/duration-seconds 76 | {:description "the time elapsed during execution of the observed function." 77 | :labels (into [:fn] labels)}) 78 | (prometheus/gauge 79 | :fn/last-failure-unixtime 80 | {:description "the UNIX timestamp of the last time the observed function threw an exception." 81 | :labels (into [:fn] labels)}) 82 | (prometheus/counter 83 | :fn/runs-total 84 | {:description "the total number of finished runs of the observed function." 85 | :labels (into [:fn :result] labels)}) 86 | (ex/exception-counter 87 | :fn/exceptions-total 88 | {:description "the total number and type of exceptions for the observed function." 89 | :labels (into [:fn] labels)})) 90 | (reduce prometheus/register registry))) 91 | 92 | ;; ## Constructor 93 | 94 | (defn- instrument!* 95 | [registry fn-name fn-var options] 96 | {:pre [(string? fn-name) (var? fn-var)]} 97 | (instrument-function! registry fn-name fn-var options) 98 | registry) 99 | 100 | (defn instrument! 101 | ([registry fn-var] 102 | (instrument! registry fn-var {})) 103 | ([registry fn-var 104 | {:keys [fn-name 105 | exceptions? 106 | duration? 107 | last-failure? 108 | run-count? 109 | labels] 110 | :or {fn-name (subs (str fn-var) 2)} 111 | :as options}] 112 | (instrument!* registry fn-name fn-var options))) 113 | 114 | (defn instrument-namespace! 115 | ([registry namespace] (instrument-namespace! registry namespace {})) 116 | ([registry namespace options] 117 | (->> namespace 118 | ns-publics vals 119 | (filter #(fn? (var-get %))) 120 | (map #(instrument! registry % options))))) 121 | -------------------------------------------------------------------------------- /src/iapetos/collector/jvm.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.collector.jvm 2 | (:require [iapetos.collector :as collector] 3 | [iapetos.core :as prometheus]) 4 | (:import [io.prometheus.client 5 | Collector 6 | CollectorRegistry] 7 | [io.prometheus.client.hotspot 8 | StandardExports 9 | MemoryPoolsExports 10 | GarbageCollectorExports 11 | ThreadExports])) 12 | 13 | ;; ## Collectors 14 | 15 | (defn standard 16 | "A set of standard collectors for the JVM. 17 | Can be attached to a iapetos registry using `iapetos.core/register`." 18 | [] 19 | (collector/named 20 | {:namespace "iapetos_internal" 21 | :name "jvm_standard"} 22 | (StandardExports.))) 23 | 24 | (defn gc 25 | "A set of GC metric collectors for the JVM. 26 | Can be attached to a iapetos registry using `iapetos.core/register`." 27 | [] 28 | (collector/named 29 | {:namespace "iapetos_internal" 30 | :name "jvm_gc"} 31 | (GarbageCollectorExports.))) 32 | 33 | (defn memory-pools 34 | "A set of memory usage metric collectors for the JVM. 35 | Can be attached to a iapetos registry using `iapetos.core/register`." 36 | [] 37 | (collector/named 38 | {:namespace "iapetos_internal" 39 | :name "jvm_memory_pools"} 40 | (MemoryPoolsExports.))) 41 | 42 | (defn threads 43 | "A set of thread usage metric collectors for the JVM. 44 | Can be attached to a iapetos registry using `iapetos.core/register`." 45 | [] 46 | (collector/named 47 | {:namespace "iapetos_internal" 48 | :name "jvm_threads"} 49 | (ThreadExports.))) 50 | 51 | ;; ## Initialize 52 | 53 | (defn initialize 54 | "Attach all available JVM collectors to the given registry." 55 | [registry] 56 | (-> registry 57 | (prometheus/register 58 | (standard) 59 | (gc) 60 | (memory-pools) 61 | (threads)))) 62 | -------------------------------------------------------------------------------- /src/iapetos/collector/ring.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.collector.ring 2 | (:require [iapetos.core :as prometheus] 3 | [iapetos.export :as export] 4 | [iapetos.collector.exceptions :as ex] 5 | [clojure.string :as string]) 6 | (:import [io.prometheus.client.exporter.common TextFormat])) 7 | 8 | ;; ## Note 9 | ;; 10 | ;; This implementation stays purposefully close to the one in 11 | ;; 'soundcloud/prometheus-clj' in regard metric naming and histogram bucket 12 | ;; selection. prometheus-clj was published under the Apache License 2.0: 13 | ;; 14 | ;; Copyright 2014 SoundCloud, Inc. 15 | ;; 16 | ;; Licensed under the Apache License, Version 2.0 (the "License"); you may 17 | ;; not use this file except in compliance with the License. You may obtain 18 | ;; a copy of the License at 19 | ;; 20 | ;; http://www.apache.org/licenses/LICENSE-2.0 21 | ;; 22 | ;; Unless required by applicable law or agreed to in writing, software 23 | ;; distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 24 | ;; WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 25 | ;; License for the specific language governing permissions and limitations 26 | ;; under the License. 27 | 28 | ;; ## Initialization 29 | 30 | (defn- make-latency-collector 31 | [labels buckets] 32 | (prometheus/histogram 33 | :http/request-latency-seconds 34 | {:description "the response latency for HTTP requests." 35 | :labels (concat [:method :status :statusClass :path] labels) 36 | :buckets buckets})) 37 | 38 | (defn- make-count-collector 39 | [labels] 40 | (prometheus/counter 41 | :http/requests-total 42 | {:description "the total number of HTTP requests processed." 43 | :labels (concat [:method :status :statusClass :path] labels)})) 44 | 45 | (defn- make-exception-collector 46 | [labels] 47 | (ex/exception-counter 48 | :http/exceptions-total 49 | {:description "the total number of exceptions encountered during HTTP processing." 50 | :labels (concat [:method :path] labels)})) 51 | 52 | (defn initialize 53 | "Initialize all collectors for Ring handler instrumentation. This includes: 54 | 55 | - `http_request_latency_seconds` 56 | - `http_requests_total` 57 | - `http_exceptions_total` 58 | 59 | Additional `:labels` can be given which need to be supplied using a 60 | `:label-fn` in [[wrap-instrumentation]] or [[wrap-metrics]]. " 61 | [registry 62 | & [{:keys [latency-histogram-buckets labels] 63 | :or {latency-histogram-buckets [0.001 0.005 0.01 0.02 0.05 0.1 0.2 0.3 0.5 0.75 1 5]}}]] 64 | (prometheus/register 65 | registry 66 | (make-latency-collector labels latency-histogram-buckets) 67 | (make-count-collector labels) 68 | (make-exception-collector labels))) 69 | 70 | ;; ## Response 71 | 72 | (defn metrics-response 73 | "Create a Ring response map describing the given collector registry's contents 74 | using the text format (version 0.0.4)." 75 | [registry] 76 | {:status 200 77 | :headers {"Content-Type" TextFormat/CONTENT_TYPE_004} 78 | :body (export/text-format registry)}) 79 | 80 | ;; ## Middlewares 81 | 82 | ;; ### Latency/Count 83 | 84 | (defn- exception? [response] (instance? Exception response)) 85 | 86 | (defn- ensure-response-map 87 | [response exception-status] 88 | (cond (nil? response) {:status 404} 89 | (exception? response) {:status exception-status} 90 | (not (map? response)) {:status 200} 91 | (not (:status response)) (assoc response :status 200) 92 | :else response)) 93 | 94 | (defn- status-class 95 | [{:keys [status]}] 96 | (str (quot status 100) "XX")) 97 | 98 | (defn- status 99 | [{:keys [status]}] 100 | (str status)) 101 | 102 | (defn- labels-for 103 | ([options request] 104 | (labels-for options request nil)) 105 | ([{:keys [label-fn path-fn]} {:keys [request-method] :as request} response] 106 | (merge {:path (path-fn request) 107 | :method (-> request-method name string/upper-case)} 108 | (label-fn request response)))) 109 | 110 | (defn- record-metrics! 111 | [{:keys [registry] :as options} delta request response] 112 | (let [labels (merge 113 | {:status (status response) 114 | :statusClass (status-class response)} 115 | (labels-for options request response)) 116 | delta-in-seconds (/ delta 1e9)] 117 | (-> registry 118 | (prometheus/inc :http/requests-total labels) 119 | (prometheus/observe :http/request-latency-seconds labels delta-in-seconds)))) 120 | 121 | (defn- exception-counter-for 122 | [{:keys [registry] :as options} request] 123 | (->> (labels-for options request) 124 | (registry :http/exceptions-total))) 125 | 126 | (defn- safe [catch-exceptions? f] 127 | (if catch-exceptions? 128 | (try (f) (catch Exception e e)) 129 | (f))) 130 | 131 | (defn- run-instrumented 132 | ([{:keys [handler exception-status] :as options} request] 133 | (ex/with-exceptions (exception-counter-for options request) 134 | (let [start-time (System/nanoTime) 135 | response (safe exception-status #(handler request)) 136 | delta (- (System/nanoTime) start-time)] 137 | (->> exception-status 138 | (ensure-response-map response) 139 | (record-metrics! options delta request)) 140 | (if-not (exception? response) 141 | response 142 | (throw response))))) 143 | 144 | ([{:keys [handler exception-status] :as options} request respond raise] 145 | (let [start-time (System/nanoTime) 146 | ex-counter (exception-counter-for options request) 147 | respond-fn #(let [delta (- (System/nanoTime) start-time) 148 | _ (->> exception-status 149 | (ensure-response-map %) 150 | (record-metrics! options delta request))] 151 | (respond %)) 152 | raise-fn #(let [delta (- (System/nanoTime) start-time)] 153 | (when exception-status 154 | (->> exception-status 155 | (ensure-response-map %) 156 | (record-metrics! options delta request))) 157 | (ex/record-exception! ex-counter %) 158 | (raise %))] 159 | (try 160 | (handler request respond-fn raise-fn) 161 | (catch Throwable t 162 | (ex/record-exception! ex-counter t) 163 | (raise t)))))) 164 | 165 | (defn- run-expose 166 | ([{:keys [path on-request registry handler] :as options} 167 | {:keys [request-method uri] :as request}] 168 | (if (= uri path) 169 | (if (= request-method :get) 170 | (do 171 | (on-request registry) 172 | (metrics-response registry)) 173 | {:status 405}) 174 | (handler request))) 175 | 176 | ([{:keys [path on-request registry handler] :as options} 177 | {:keys [request-method uri] :as request} 178 | respond 179 | raise] 180 | (if (= uri path) 181 | (if (= request-method :get) 182 | (do 183 | (on-request registry) 184 | (respond (metrics-response registry))) 185 | (respond {:status 405})) 186 | (handler request respond raise)))) 187 | 188 | (defn ring-fn [f options] 189 | (fn 190 | ([request] (f options request)) 191 | ([request respond raise] (f options request respond raise)))) 192 | 193 | (defn wrap-instrumentation 194 | "Wrap the given Ring handler to write metrics to the given registry: 195 | 196 | - `http_requests_total` 197 | - `http_request_latency_seconds` 198 | - `http_exceptions_total` 199 | 200 | Note that you have to call [[initialize]] on your registry first, to register 201 | the necessary collectors. 202 | 203 | Be aware that you should implement `path-fn` (which generates the value for 204 | the `:path` label) if you have any kind of ID in your URIs – since otherwise 205 | there will be one timeseries created for each observed ID. 206 | 207 | For additional labels in the metrics use `label-fn`, which takes the request 208 | as a first argument and the response as the second argument. 209 | 210 | Since collectors, and thus their labels, have to be registered before they 211 | are ever used, you need to provide the list of `:labels` when calling 212 | [[initialize]]." 213 | [handler registry 214 | & [{:keys [path-fn label-fn] 215 | :or {path-fn :uri 216 | label-fn (constantly {})} 217 | :as options}]] 218 | (let [options (assoc options 219 | :path-fn path-fn 220 | :label-fn label-fn 221 | :registry registry 222 | :handler handler)] 223 | (ring-fn run-instrumented options))) 224 | 225 | ;; ### Metrics Endpoint 226 | 227 | (defn wrap-metrics-expose 228 | "Expose Prometheus metrics at the given constant URI using the text format. 229 | 230 | If `:on-request` is given, it will be called with the collector registry 231 | whenever a request comes in (the result will be ignored). This lets you use 232 | the Prometheus scraper as a trigger for metrics collection." 233 | [handler registry 234 | & [{:keys [path on-request] 235 | :or {path "/metrics" 236 | on-request identity} 237 | :as options}]] 238 | (let [options (assoc options 239 | :path path 240 | :on-request on-request 241 | :registry registry 242 | :handler handler)] 243 | (ring-fn run-expose options))) 244 | 245 | ;; ### Compound Middleware 246 | 247 | (defn wrap-metrics 248 | "A combination of [[wrap-instrumentation]] and [[wrap-metrics-expose]]. 249 | 250 | Note that you have to call [[initialize]] on your registry first, to register 251 | the necessary collectors. 252 | 253 | Be aware that you should implement `path-fn` (which generates the value for 254 | the `:path` label) if you have any kind of ID in your URIs – since otherwise 255 | there will be one timeseries created for each observed ID. 256 | 257 | For additional labels in the metrics use `label-fn`, which takes the request 258 | as a first argument and the response as the second argument. 259 | 260 | Since collectors, and thus their labels, have to be registered before they 261 | are ever used, you need to provide the list of `:labels` when calling 262 | [[initialize]]." 263 | [handler registry 264 | & [{:keys [path path-fn on-request label-fn] 265 | :or {path "/metrics" 266 | path-fn :uri 267 | label-fn (constantly {})} 268 | :as options}]] 269 | (-> handler 270 | (wrap-instrumentation registry options) 271 | (wrap-metrics-expose registry options))) 272 | -------------------------------------------------------------------------------- /src/iapetos/core.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.core 2 | (:require [iapetos.collector :as collector] 3 | [iapetos.metric :as metric] 4 | [iapetos.operations :as ops] 5 | [iapetos.registry :as registry]) 6 | (:refer-clojure :exclude [get inc dec set]) 7 | (:import [io.prometheus.client 8 | CollectorRegistry 9 | Counter 10 | Counter$Child 11 | Histogram 12 | Histogram$Child 13 | Histogram$Timer 14 | Gauge 15 | Gauge$Child 16 | Gauge$Timer 17 | Summary 18 | Summary$Builder 19 | Summary$Child])) 20 | 21 | ;; ## Registry 22 | 23 | (def ^{:added "0.1.7"} default-registry 24 | registry/default) 25 | 26 | (defn collector-registry 27 | "Create a fresh iapetos collector registry." 28 | ([] (registry/create)) 29 | ([registry-name] (registry/create registry-name))) 30 | 31 | (defn register 32 | "Register the given collectors." 33 | [registry & collectors] 34 | (reduce 35 | (fn [registry collector] 36 | (registry/register 37 | registry 38 | (collector/metric collector) 39 | collector)) 40 | registry collectors)) 41 | 42 | (defn ^{:added "0.1.8"} register-lazy 43 | "Prepare the given collectors but only actually register them on first use. 44 | This can be useful for metrics that should only be available conditionally, 45 | e.g. the failure timestamp for a batch job." 46 | [registry & collectors] 47 | (reduce 48 | (fn [registry collector] 49 | (registry/register-lazy 50 | registry 51 | (collector/metric collector) 52 | collector)) 53 | registry collectors)) 54 | 55 | (defn register-as 56 | "Register the given collector under the given metric name. This is useful 57 | for plain Prometheus collectors, i.e. those that are not provided by 58 | iapetos." 59 | [registry metric collector] 60 | (registry/register registry metric collector)) 61 | 62 | (defn ^{:added "0.1.8"} unregister 63 | "Unregister the given collectors." 64 | [registry & collector-names] 65 | (reduce registry/unregister registry collector-names)) 66 | 67 | (defn ^{:added "0.1.8"} clear 68 | "Unregister the given collectors." 69 | [registry] 70 | (registry/clear registry)) 71 | 72 | (defn subsystem 73 | "Create a new registry bound to the given subsystem. The resulting value will 74 | not have access to any of the original registry's collectors. 75 | 76 | Subsystems can be nested, resulting in joining the subsystem names with 77 | underscores." 78 | [registry subsystem-name] 79 | {:pre [subsystem-name]} 80 | (registry/subsystem registry (metric/sanitize subsystem-name))) 81 | 82 | ;; ## Collectors 83 | 84 | (defn counter 85 | "Create a new `Counter` collector: 86 | 87 | - `:description`: a description for the counter, 88 | - `:labels`: a seq of available labels for the counter. 89 | " 90 | [metric 91 | & [{:keys [description labels] 92 | :or {description "a counter metric."} 93 | :as options}]] 94 | (-> (merge 95 | {:description description} 96 | (metric/as-map metric options)) 97 | (collector/make-simple-collector :counter #(Counter/build)))) 98 | 99 | (defn gauge 100 | "Create a new `Gauge` collector: 101 | 102 | - `:description`: a description for the gauge, 103 | - `:labels`: a seq of available labels for the gauge. 104 | " 105 | [metric 106 | & [{:keys [description labels] 107 | :or {description "a gauge metric."} 108 | :as options}]] 109 | (-> (merge 110 | {:description description} 111 | (metric/as-map metric options)) 112 | (collector/make-simple-collector :gauge #(Gauge/build)))) 113 | 114 | (defn histogram 115 | "Create a new `Histogram` collector: 116 | 117 | - `:description`: a description for the histogram, 118 | - `:buckets`: a seq of double values describing the histogram buckets, 119 | - `:labels`: a seq of available labels for the histogram. 120 | " 121 | [metric 122 | & [{:keys [description buckets labels] 123 | :or {description "a histogram metric."} 124 | :as options}]] 125 | (-> (merge 126 | {:description description} 127 | (metric/as-map metric options)) 128 | (collector/make-simple-collector 129 | :histogram 130 | #(cond-> (Histogram/build) 131 | (seq buckets) (.buckets (double-array buckets)))))) 132 | 133 | (defn- add-quantile [^Summary$Builder builder [quantile error]] 134 | (.quantile builder quantile error)) 135 | 136 | (defn summary 137 | "Create a new `Summary` collector: 138 | 139 | - `:description`: a description for the summary, 140 | - `:quantiles`: a map of double [quantile error] entries 141 | - `:labels`: a seq of available labels for the summary. 142 | " 143 | [metric 144 | & [{:keys [description quantiles labels] 145 | :or {description "a summary metric."} 146 | :as options}]] 147 | (-> (merge 148 | {:description description} 149 | (metric/as-map metric options)) 150 | (collector/make-simple-collector 151 | :summary 152 | #(reduce add-quantile (Summary/build) quantiles)))) 153 | 154 | ;; ## Raw Operations 155 | 156 | (defmacro ^:private with-metric-exception 157 | [metric & body] 158 | `(try 159 | (do ~@body) 160 | (catch Exception ex# 161 | (throw 162 | (RuntimeException. 163 | (str "error when updating metric: " 164 | (pr-str ~metric)) 165 | ex#))))) 166 | 167 | (defmacro ^:private ?-> 168 | [value predicate then-branch _ else-branch] 169 | `(let [v# ~value] 170 | (if (~predicate v#) 171 | (-> v# ~then-branch) 172 | (-> v# ~else-branch)))) 173 | 174 | (defn- registry? 175 | [value] 176 | (instance? iapetos.registry.Registry value)) 177 | 178 | (defn observe 179 | "Observe the given amount for the desired metric. This can be either called 180 | using a registry and metric name or directly on a collector: 181 | 182 | ``` 183 | (-> registry (observe :app/active-users-total 10.0)) 184 | (-> registry :app/active-users-total (observe 10.0)) 185 | ``` 186 | 187 | The return value of this operation is either the collector or registry that 188 | was passed in." 189 | ([collector amount] 190 | (ops/observe collector amount) 191 | collector) 192 | ([registry metric amount] 193 | (observe registry metric {} amount)) 194 | ([registry metric labels amount] 195 | (with-metric-exception metric 196 | (observe (registry/get registry metric labels) amount)) 197 | registry)) 198 | 199 | (def 200 | ^{:arglists '([collector] 201 | [collector amount] 202 | [registry metric] 203 | [registry metric amount] 204 | [registry metric labels] 205 | [registry metric labels amount])} 206 | inc 207 | "Increment the given metric by the given amount. This can be either called 208 | using a registry and metric name or directly on a collector: 209 | 210 | ``` 211 | (-> registry (inc :app/active-users-total)) 212 | (-> registry :app/active-users-total (inc)) 213 | ``` 214 | 215 | The return value of this operation is either the collector or registry that 216 | was passed in." 217 | (fn 218 | ([collector] 219 | (ops/increment collector) 220 | collector) 221 | ([a b] 222 | (?-> a 223 | registry? (inc b {} 1.0) 224 | :else (ops/increment b)) 225 | a) 226 | ([registry metric amount-or-labels] 227 | (?-> amount-or-labels 228 | map? (as-> <> (inc registry metric <> 1.0)) 229 | :else (as-> <> (inc registry metric {} <>)))) 230 | ([registry metric labels amount] 231 | (with-metric-exception metric 232 | (inc (registry/get registry metric labels) amount)) 233 | registry))) 234 | 235 | (def 236 | ^{:arglists '([collector] 237 | [collector amount] 238 | [registry metric] 239 | [registry metric amount] 240 | [registry metric labels] 241 | [registry metric labels amount])} 242 | dec 243 | "Decrement the given metric by the given amount. This can be either called 244 | using a registry and metric name or directly on a collector: 245 | 246 | ``` 247 | (-> registry (inc :app/active-users-total)) 248 | (-> registry :app/active-users-total (inc)) 249 | ``` 250 | 251 | The return value of this operation is either the collector or registry that 252 | was passed in." 253 | (fn 254 | ([collector] 255 | (ops/decrement collector) 256 | collector) 257 | ([a b] 258 | (?-> a 259 | registry? (dec b {} 1.0) 260 | :else (ops/decrement b)) 261 | a) 262 | ([registry metric amount-or-labels] 263 | (?-> amount-or-labels 264 | map? (as-> <> (dec registry metric <> 1.0)) 265 | :else (as-> <> (dec registry metric {} <>)))) 266 | ([registry metric labels amount] 267 | (with-metric-exception metric 268 | (dec (registry/get registry metric labels) amount)) 269 | registry))) 270 | 271 | (defn set 272 | "Set the given metric to the given value. This can be either called 273 | using a registry and metric name or directly on a collector: 274 | 275 | ``` 276 | (-> registry (set :app/active-users-total 10.0)) 277 | (-> registry :app/active-users-total (set 10.0)) 278 | ``` 279 | 280 | The return value of this operation is either the collector or registry that 281 | was passed in." 282 | ([collector amount] 283 | (ops/set-value collector amount) 284 | collector) 285 | ([registry metric amount] 286 | (set registry metric {} amount)) 287 | ([registry metric labels amount] 288 | (with-metric-exception metric 289 | (set (registry/get registry metric labels) amount)) 290 | registry)) 291 | 292 | (defn set-to-current-time 293 | "Set the given metric to the current timestamp. This can be either called 294 | using a registry and metric name or directly on a collector: 295 | 296 | ``` 297 | (-> registry (set-to-current-time :app/last-success-unixtime)) 298 | (-> registry :app/last-success-unixtime set-to-current-time) 299 | ``` 300 | 301 | The return value of this operation is either the collector or registry that 302 | was passed in." 303 | ([collector] 304 | (ops/set-value-to-current-time collector) 305 | collector) 306 | ([registry metric] 307 | (set-to-current-time registry metric {})) 308 | ([registry metric labels] 309 | (with-metric-exception metric 310 | (set-to-current-time (registry/get registry metric labels))) 311 | registry)) 312 | 313 | (defn start-timer 314 | "Start a timer that, when stopped, will store the duration in the given 315 | metric. This can be either called using a registry and metric name or a 316 | collector: 317 | 318 | ``` 319 | (-> registry (start-timer :app/duration-seconds)) 320 | (-> registry :app/duration-seconds (start-timer)) 321 | ``` 322 | 323 | The return value will be a _function_ that should be called once the 324 | operation to time has run." 325 | ([collector] 326 | (ops/start-timer collector)) 327 | ([registry metric] 328 | (start-timer registry metric {})) 329 | ([registry metric labels] 330 | (with-metric-exception metric 331 | (start-timer (registry/get registry metric labels))))) 332 | 333 | (defn value 334 | "Read the current value of a metric. This can be either called using a 335 | registry and a metric name or directly on a collector: 336 | 337 | ``` 338 | (-> registry (value :app/duration-seconds)) 339 | (-> registry :app/duration-seconds (value)) 340 | ``` 341 | 342 | The return value depends on the type of collector." 343 | ([collector] 344 | (ops/read-value collector)) 345 | ([registry metric] 346 | (value registry metric {})) 347 | ([registry metric labels] 348 | (with-metric-exception metric 349 | (value (registry/get registry metric labels))))) 350 | 351 | ;; ## Compound Operations 352 | 353 | ;; ### Counters 354 | 355 | (defmacro with-counter 356 | "Wrap the given block to increment the given counter once it is done." 357 | [collector & body] 358 | `(let [c# ~collector] 359 | (try 360 | (do ~@body) 361 | (finally 362 | (inc ~collector))))) 363 | 364 | (defmacro with-success-counter 365 | "Wrap the given block to increment the given counter once it has run 366 | successfully." 367 | [collector & body] 368 | `(let [result# (do ~@body)] 369 | (inc ~collector) 370 | result#)) 371 | 372 | (defmacro with-failure-counter 373 | "Wrap the given block to increment the given counter if it throws." 374 | [collector & body] 375 | `(try 376 | (do ~@body) 377 | (catch Throwable t# 378 | (inc ~collector) 379 | (throw t#)))) 380 | 381 | (defmacro with-counters 382 | "Wrap the given block to increment the given counters: 383 | 384 | - `:total`: incremented when the block is left, 385 | - `:success`: incremented when the block has executed successfully, 386 | - `:failure`: incremented when the block has thrown an exception. 387 | " 388 | [{:keys [total success failure] :as counters} & body] 389 | {:pre [(map? counters)]} 390 | (cond->> `(do ~@body) 391 | failure (list `with-failure-counter failure) 392 | success (list `with-success-counter success) 393 | total (list `with-counter total))) 394 | 395 | (defmacro with-activity-counter 396 | "Wrap the given block to increment the given collector once it is entered 397 | and decrement it once execution is done. This needs a [[gauge]] collector 398 | (since [[counter]] ones cannot be decremented). 399 | 400 | Example: Counting the number of in-flight requests in an HTTP server." 401 | [collector & body] 402 | `(let [c# ~collector] 403 | (inc c#) 404 | (try 405 | (do ~@body) 406 | (finally 407 | (dec c#))))) 408 | 409 | ;; ## Timestamps 410 | 411 | (defmacro with-timestamp 412 | "Wrap the given block to store the current timestamp in the given collector 413 | once execution is done. 414 | 415 | Needs a [[gauge]] collector." 416 | [collector & body] 417 | `(try 418 | (do ~@body) 419 | (finally 420 | (set-to-current-time ~collector)))) 421 | 422 | (defmacro with-success-timestamp 423 | "Wrap the given block to store the current timestamp in the given collector 424 | once execution is done successfully. 425 | 426 | Needs a [[gauge]] collector." 427 | [collector & body] 428 | `(let [result# (do ~@body)] 429 | (set-to-current-time ~collector) 430 | result#)) 431 | 432 | (defmacro with-failure-timestamp 433 | "Wrap the given block to store the current timestamp in the given collector 434 | once execution has failed. 435 | 436 | Needs a [[gauge]] collector." 437 | [collector & body] 438 | `(try 439 | (do ~@body) 440 | (catch Throwable t# 441 | (set-to-current-time ~collector) 442 | (throw t#)))) 443 | 444 | (defmacro with-timestamps 445 | "Wrap the given block to set a number of timestamps depending on whether 446 | execution succeeds or fails: 447 | 448 | `:last-run`: the last time the block was run, 449 | `:last-success`: the last time the block was run successfully, 450 | `:last-failure`: the last time execution failed. 451 | 452 | All keys are optional but have to point at a [[gauge]] collector if given." 453 | [{:keys [last-run last-success last-failure]} & body] 454 | (cond->> `(do ~@body) 455 | last-failure (list `with-failure-timestamp last-failure) 456 | last-success (list `with-success-timestamp last-success) 457 | last-run (list `with-timestamp last-run))) 458 | 459 | ;; ### Durations 460 | 461 | (defmacro with-duration 462 | "Wrap the given block to write its execution time to the given collector. 463 | 464 | Works with [[gauge]], [[histogram]] and [[summary]] collectors." 465 | [collector & body] 466 | `(let [stop# (start-timer ~collector)] 467 | (try 468 | (do ~@body) 469 | (finally 470 | (stop#))))) 471 | -------------------------------------------------------------------------------- /src/iapetos/export.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.export 2 | (:require [iapetos.registry :as registry]) 3 | (:import [io.prometheus.client CollectorRegistry] 4 | [io.prometheus.client.exporter 5 | PushGateway] 6 | [io.prometheus.client.exporter.common 7 | TextFormat])) 8 | 9 | ;; ## TextFormat (v0.0.4) 10 | 11 | (defn write-text-format! 12 | "Dump the given registry to the given writer using the Prometheus text format 13 | (version 0.0.4)." 14 | [^java.io.Writer w registry] 15 | (TextFormat/write004 16 | w 17 | (.metricFamilySamples ^CollectorRegistry (registry/raw registry)))) 18 | 19 | (defn text-format 20 | "Dump the given registry using the Prometheus text format (version 0.0.4)." 21 | [registry] 22 | (with-open [out (java.io.StringWriter.)] 23 | (write-text-format! out registry) 24 | (str out))) 25 | 26 | ;; ## Push Gateway 27 | 28 | ;; ### Protocol 29 | 30 | (defprotocol ^:private Pushable 31 | (push! [registry] 32 | "Push all metrics of the given registry.")) 33 | 34 | ;; ### Implementation 35 | 36 | (declare call-on-internal) 37 | 38 | (deftype PushableRegistry [internal-registry job push-gateway grouping-key] 39 | registry/Registry 40 | (register [this metric collector] 41 | (call-on-internal this registry/register metric collector)) 42 | (register-lazy [this metric collector] 43 | (call-on-internal this registry/register-lazy metric collector)) 44 | (unregister [this metric] 45 | (call-on-internal this registry/unregister metric)) 46 | (clear [this] 47 | (call-on-internal this registry/clear)) 48 | (subsystem [this subsystem-name] 49 | (call-on-internal this registry/subsystem subsystem-name)) 50 | (get [_ metric labels] 51 | (registry/get internal-registry metric labels)) 52 | (raw [_] 53 | (registry/raw internal-registry)) 54 | (name [_] 55 | (registry/name internal-registry)) 56 | 57 | clojure.lang.IFn 58 | (invoke [this k] 59 | (registry/get internal-registry k {})) 60 | (invoke [this k labels] 61 | (registry/get internal-registry k labels)) 62 | 63 | Pushable 64 | (push! [this] 65 | (.pushAdd 66 | ^PushGateway push-gateway 67 | ^CollectorRegistry (registry/raw this) 68 | ^String job 69 | ^java.util.Map grouping-key) 70 | this)) 71 | 72 | (alter-meta! #'->PushableRegistry assoc :private true) 73 | 74 | (defn- call-on-internal 75 | [^PushableRegistry r f & args] 76 | (PushableRegistry. 77 | (apply f (.-internal-registry r) args) 78 | (.-job r) 79 | (.-push-gateway r) 80 | (.-grouping-key r))) 81 | 82 | ;; ### Constructor 83 | 84 | (defn- as-push-gateway 85 | ^io.prometheus.client.exporter.PushGateway 86 | [gateway] 87 | (if (instance? PushGateway gateway) 88 | gateway 89 | (PushGateway. ^String gateway))) 90 | 91 | (defn- as-grouping-key 92 | [grouping-key] 93 | (->> (for [[k v] grouping-key] 94 | [(name k) (str v)]) 95 | (into {}))) 96 | 97 | (defn pushable-collector-registry 98 | "Create a fresh iapetos collector registry whose metrics can be pushed to the 99 | specified gateway using [[push!]]. 100 | 101 | Alternatively, by supplying `:registry`, an existing one can be wrapped to be 102 | pushable, e.g. the [[default-registry]]." 103 | [{:keys [job registry push-gateway grouping-key]}] 104 | {:pre [(string? job) push-gateway]} 105 | (->PushableRegistry 106 | (or registry (registry/create job)) 107 | job 108 | (as-push-gateway push-gateway) 109 | (as-grouping-key grouping-key))) 110 | 111 | (defn push-registry! 112 | "Directly push all metrics of the given registry to the given push gateway. 113 | This can be used if you don't have control over registry creation, otherwise 114 | [[pushable-collector-registry]] and [[push!]] are recommended." 115 | [registry {:keys [push-gateway job grouping-key]}] 116 | {:pre [(string? job) push-gateway]} 117 | (.pushAdd 118 | (as-push-gateway push-gateway) 119 | ^CollectorRegistry (registry/raw registry) 120 | ^String job 121 | ^java.util.Map (as-grouping-key grouping-key)) 122 | registry) 123 | 124 | ;; ### Macros 125 | 126 | (defmacro with-push 127 | "Use the given [[pushable-collector-registry]] to push metrics after the given 128 | block of code has run successfully." 129 | [registry & body] 130 | `(let [r# ~registry 131 | result# (do ~@body)] 132 | (push! r#) 133 | result#)) 134 | 135 | (defmacro with-push-gateway 136 | "Create a [[pushable-collector-registry]], run the given block of code, then 137 | push all collected metrics. 138 | 139 | ``` 140 | (with-push-gateway [registry {:job \"my-job\", :push-gateway \"0:8080\"}] 141 | ...) 142 | ```" 143 | [[binding options] & body] 144 | {:pre [binding options]} 145 | `(let [r# (pushable-collector-registry ~options)] 146 | (with-push r# 147 | (let [~binding r#] 148 | ~@body)))) 149 | -------------------------------------------------------------------------------- /src/iapetos/metric.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.metric 2 | (:require [clojure.string :as string]) 3 | (:import [io.prometheus.client Collector])) 4 | 5 | ;; ## Protocol 6 | 7 | (defprotocol Metric 8 | (metric-name [metric])) 9 | 10 | ;; ## Helper 11 | 12 | (defn- assert-valid-name 13 | [s original-value] 14 | (assert 15 | (re-matches #"[a-zA-Z_:][a-zA-Z0-9_:]*" s) 16 | (format "invalid metric name: %s (sanitized: %s)" 17 | (pr-str original-value) 18 | (pr-str s))) 19 | s) 20 | 21 | (defn sanitize 22 | [v] 23 | (-> ^String (if (keyword? v) 24 | (name v) 25 | (str v)) 26 | (Collector/sanitizeMetricName) 27 | (string/replace #"__+" "_") 28 | (string/replace #"(^_+|_+$)" "") 29 | (assert-valid-name v))) 30 | 31 | (defn dasherize 32 | [v] 33 | (-> (sanitize v) 34 | (string/replace "_" "-"))) 35 | 36 | ;; ## Implementation 37 | 38 | (extend-protocol Metric 39 | clojure.lang.Keyword 40 | (metric-name [k] 41 | {:name (-> k name sanitize) 42 | :namespace (or (some->> k namespace sanitize) 43 | "default")}) 44 | 45 | clojure.lang.IPersistentVector 46 | (metric-name [[namespace name]] 47 | {:name (sanitize name) 48 | :namespace (sanitize namespace)}) 49 | 50 | clojure.lang.IPersistentMap 51 | (metric-name [{:keys [name namespace]}] 52 | {:pre [name]} 53 | {:name (sanitize name) 54 | :namespace (or (some-> namespace sanitize) "default")}) 55 | 56 | String 57 | (metric-name [s] 58 | {:name (sanitize s) 59 | :namespace "default"})) 60 | 61 | ;; ## Derived Function 62 | 63 | (defn as-map 64 | [metric additional-keys] 65 | (merge (metric-name metric) 66 | additional-keys 67 | ;; user supplied (unsanitized) identifier e.g., :app/runs-total 68 | {::id metric})) 69 | -------------------------------------------------------------------------------- /src/iapetos/operations.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.operations 2 | (:import [io.prometheus.client 3 | Counter$Child 4 | Histogram$Child 5 | Histogram$Timer 6 | Gauge$Child 7 | Gauge$Timer 8 | Summary$Child 9 | Summary$Timer])) 10 | 11 | ;; ## Operation Protocols 12 | 13 | (defprotocol ReadableCollector 14 | (read-value [this])) 15 | 16 | (defprotocol IncrementableCollector 17 | (increment* [this amount])) 18 | 19 | (defprotocol DecrementableCollector 20 | (decrement* [this amount])) 21 | 22 | (defprotocol SettableCollector 23 | (set-value [this value]) 24 | (set-value-to-current-time [this])) 25 | 26 | (defprotocol ObservableCollector 27 | (observe [this amount])) 28 | 29 | (defprotocol TimeableCollector 30 | (start-timer [this])) 31 | 32 | ;; ## Derived Functions 33 | 34 | (defn increment 35 | ([this] (increment this 1.0)) 36 | ([this amount] (increment* this amount))) 37 | 38 | (defn decrement 39 | ([this] (decrement this 1.0)) 40 | ([this amount] (decrement* this amount))) 41 | 42 | ;; ## Counter 43 | 44 | (extend-type Counter$Child 45 | ReadableCollector 46 | (read-value [this] 47 | (.get ^Counter$Child this)) 48 | IncrementableCollector 49 | (increment* [this amount] 50 | (.inc ^Counter$Child this (double amount)))) 51 | 52 | ;; ## Gauge 53 | 54 | (extend-type Gauge$Child 55 | ReadableCollector 56 | (read-value [this] 57 | (.get ^Gauge$Child this)) 58 | 59 | IncrementableCollector 60 | (increment* [this amount] 61 | (.inc ^Gauge$Child this (double amount))) 62 | 63 | DecrementableCollector 64 | (decrement* [this amount] 65 | (.dec ^Gauge$Child this (double amount))) 66 | 67 | ObservableCollector 68 | (observe [this amount] 69 | (.set ^Gauge$Child this (double amount))) 70 | 71 | SettableCollector 72 | (set-value [this value] 73 | (.set ^Gauge$Child this (double value))) 74 | (set-value-to-current-time [this] 75 | (.setToCurrentTime ^Gauge$Child this)) 76 | 77 | TimeableCollector 78 | (start-timer [this] 79 | (let [^Gauge$Timer t (.startTimer ^Gauge$Child this)] 80 | #(.setDuration t)))) 81 | 82 | ;; ## Histogram 83 | 84 | (extend-type Histogram$Child 85 | ReadableCollector 86 | (read-value [this] 87 | (let [^io.prometheus.client.Histogram$Child$Value value 88 | (.get ^Histogram$Child this) 89 | buckets (vec (.-buckets value))] 90 | {:sum (.-sum value) 91 | :count (last buckets) 92 | :buckets buckets})) 93 | 94 | ObservableCollector 95 | (observe [this amount] 96 | (.observe ^Histogram$Child this (double amount))) 97 | 98 | TimeableCollector 99 | (start-timer [this] 100 | (let [^Histogram$Timer t (.startTimer ^Histogram$Child this)] 101 | #(.observeDuration t)))) 102 | 103 | ;; ## Summary 104 | 105 | (extend-type Summary$Child 106 | ReadableCollector 107 | (read-value [this] 108 | (let [^io.prometheus.client.Summary$Child$Value value 109 | (.get ^Summary$Child this)] 110 | {:sum (.-sum value) 111 | :count (.-count value) 112 | :quantiles (into {} (.-quantiles value))})) 113 | 114 | ObservableCollector 115 | (observe [this amount] 116 | (.observe ^Summary$Child this (double amount))) 117 | 118 | TimeableCollector 119 | (start-timer [this] 120 | (let [^Summary$Timer t (.startTimer ^Summary$Child this)] 121 | #(.observeDuration t)))) 122 | -------------------------------------------------------------------------------- /src/iapetos/registry.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.registry 2 | (:refer-clojure :exclude [get name]) 3 | (:require [iapetos.registry 4 | [collectors :as collectors] 5 | [utils :as utils]]) 6 | (:import [io.prometheus.client Collector CollectorRegistry])) 7 | 8 | ;; ## Protocol 9 | 10 | (defprotocol Registry 11 | "Protocol for the iapetos collector registry." 12 | (subsystem [registry subsystem-name] 13 | "Create a new registry that is bound to the given subsystem.") 14 | (register [registry metric collector] 15 | "Add the given `iapetos.collector/Collector` to the registry using the 16 | given name.") 17 | (register-lazy [registry metric collector] 18 | "Add the given `iapetos.collector/Collector` to the registry using the 19 | given name, but only actually register it on first use.") 20 | (unregister [registry metric] 21 | "Unregister the collector under the given name from the registry.") 22 | (clear [registry] 23 | "Clear the registry, removing all collectors from it.") 24 | (get [registry metric labels] 25 | "Retrieve the collector instance associated with the given metric, 26 | setting the given labels.") 27 | (raw [registry] 28 | "Retrieve the underlying `CollectorRegistry`.") 29 | (name [registry] 30 | "Retrieve the registry name (for exporting).")) 31 | 32 | ;; ## Implementation 33 | 34 | (declare set-collectors) 35 | 36 | (deftype IapetosRegistry [registry-name registry options collectors] 37 | Registry 38 | (register [this metric collector] 39 | (->> (collectors/prepare registry metric collector options) 40 | (collectors/register collectors) 41 | (set-collectors this))) 42 | (register-lazy [this metric collector] 43 | (->> (collectors/prepare registry metric collector options) 44 | (collectors/insert collectors) 45 | (set-collectors this))) 46 | (unregister [this metric] 47 | (->> (collectors/lookup collectors metric options) 48 | (collectors/unregister collectors) 49 | (set-collectors this))) 50 | (clear [this] 51 | (->> (collectors/clear collectors) 52 | (set-collectors this))) 53 | (subsystem [_ subsystem-name] 54 | (assert (string? subsystem-name)) 55 | (IapetosRegistry. 56 | registry-name 57 | registry 58 | (update options :subsystem utils/join-subsystem subsystem-name) 59 | (collectors/initialize))) 60 | (get [_ metric labels] 61 | (collectors/by collectors metric labels options)) 62 | (raw [_] 63 | registry) 64 | (name [_] 65 | registry-name) 66 | 67 | clojure.lang.IFn 68 | (invoke [this k] 69 | (get this k {})) 70 | (invoke [this k labels] 71 | (get this k labels)) 72 | 73 | clojure.lang.ILookup 74 | (valAt [this k] 75 | (get this k {})) 76 | (valAt [this k default] 77 | (or (get this k {}) default))) 78 | 79 | (defn- set-collectors 80 | [^IapetosRegistry r collectors] 81 | (->IapetosRegistry 82 | (.-registry-name r) 83 | (.-registry r) 84 | (.-options r) 85 | collectors)) 86 | 87 | ;; ## Constructor 88 | 89 | (defn create 90 | ([] (create "iapetos_registry")) 91 | ([registry-name] 92 | (create registry-name (CollectorRegistry.))) 93 | ([registry-name ^CollectorRegistry registry] 94 | (->> (collectors/initialize) 95 | (IapetosRegistry. registry-name registry {})))) 96 | 97 | (def default 98 | (create 99 | "prometheus_default_registry" 100 | (CollectorRegistry/defaultRegistry))) 101 | -------------------------------------------------------------------------------- /src/iapetos/registry/collectors.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.registry.collectors 2 | (:require [iapetos.metric :as metric] 3 | [iapetos.registry.utils :as utils] 4 | [iapetos.collector :as collector]) 5 | (:import [io.prometheus.client Collector CollectorRegistry])) 6 | 7 | ;; ## Init 8 | 9 | (defn initialize 10 | "Initialize map for collectors. 11 | 12 | - ::path-cache meta used to prevent additional metric path computation 13 | requiring metric name sanitization." 14 | [] 15 | ^{::path-cache {}} {}) 16 | 17 | ;; ## Management 18 | 19 | (defn- register-collector-delay 20 | [^CollectorRegistry registry ^Collector instance] 21 | (delay 22 | (.register registry instance) 23 | instance)) 24 | 25 | (defn- unregister-collector-delay 26 | [^CollectorRegistry registry ^Collector instance] 27 | (delay 28 | (.unregister registry instance) 29 | instance)) 30 | 31 | (defn- warn-lazy-deprecation! 32 | [{:keys [collector instance] :as collector-map}] 33 | (let [lazy? (:lazy? collector)] 34 | (when (some? lazy?) 35 | (println "collector option ':lazy?' is deprecated, use 'register-lazy' instead.") 36 | (println "collector: " (pr-str collector)) 37 | (when-not lazy? 38 | @instance))) 39 | collector-map) 40 | 41 | (defn prepare 42 | [registry metric collector options] 43 | (let [path (utils/metric->path metric options) 44 | instance (collector/instantiate collector options)] 45 | (-> {:collector collector 46 | :metric metric 47 | :cache-key [(collector/metric-id collector) options] 48 | :path path 49 | :raw instance 50 | :register (register-collector-delay registry instance) 51 | :unregister (unregister-collector-delay registry instance)} 52 | (warn-lazy-deprecation!)))) 53 | 54 | (defn insert 55 | [collectors {:keys [path cache-key] :as collector}] 56 | (-> collectors 57 | (vary-meta update ::path-cache assoc cache-key path) 58 | (assoc-in path collector))) 59 | 60 | (defn delete 61 | [collectors {:keys [path cache-key] :as _collector}] 62 | (-> collectors 63 | (vary-meta update ::path-cache dissoc cache-key) 64 | (update-in (butlast path) 65 | dissoc 66 | (last path)))) 67 | 68 | (defn unregister 69 | [collectors {:keys [register unregister] :as collector}] 70 | (when (realized? register) 71 | @unregister) 72 | (delete collectors collector)) 73 | 74 | (defn register 75 | [collectors {:keys [register] :as collector}] 76 | @register 77 | (insert collectors collector)) 78 | 79 | (defn clear 80 | [collectors] 81 | (->> (for [[_namespace vs] collectors 82 | [_subsystem vs] vs 83 | [_name collector] vs] 84 | collector) 85 | (reduce unregister collectors))) 86 | 87 | ;; ## Read Access 88 | 89 | (defn- label-collector 90 | [labels {:keys [collector register]}] 91 | (collector/label-instance collector @register labels)) 92 | 93 | (defn lookup 94 | [collectors metric options] 95 | (some->> (or (get-in (meta collectors) 96 | [::path-cache [metric options]]) 97 | (utils/metric->path metric options)) 98 | (get-in collectors))) 99 | 100 | (defn by 101 | [collectors metric labels options] 102 | (some->> (lookup collectors metric options) 103 | (label-collector labels))) 104 | -------------------------------------------------------------------------------- /src/iapetos/registry/utils.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.registry.utils 2 | (:require [iapetos.metric :as metric])) 3 | 4 | (defn join-subsystem 5 | [old-subsystem subsystem] 6 | (if (seq old-subsystem) 7 | (metric/sanitize (str old-subsystem "_" subsystem)) 8 | subsystem)) 9 | 10 | (defn metric->path 11 | [metric {:keys [subsystem]}] 12 | (let [{:keys [name namespace]} (metric/metric-name metric)] 13 | [namespace subsystem name])) 14 | -------------------------------------------------------------------------------- /src/iapetos/standalone.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.standalone 2 | (:require [iapetos.collector.ring :as ring] 3 | [clojure.java.io :as io]) 4 | (:import [com.sun.net.httpserver 5 | HttpHandler 6 | HttpServer 7 | HttpExchange] 8 | [java.net InetSocketAddress])) 9 | 10 | ;; ## Handler 11 | 12 | (defn- write-headers! 13 | [^HttpExchange e {:keys [status headers ^bytes body]}] 14 | (let [h (.getResponseHeaders e) 15 | content-length (alength body)] 16 | (doseq [[header value] headers] 17 | (.set h (str header) (str value))) 18 | (.set h "Content-Length" (str content-length)) 19 | (.sendResponseHeaders e (int status) content-length))) 20 | 21 | (defn- write-body! 22 | [^HttpExchange e {:keys [^bytes body]}] 23 | (with-open [out (.getResponseBody e)] 24 | (.write out body) 25 | (.flush out))) 26 | 27 | (defn- plain-response 28 | [status & text] 29 | {:status status 30 | :headers {"Content-Type" "text/plain; charset=UTF-8"} 31 | :body (apply str text)}) 32 | 33 | (defn- write-response! 34 | [^HttpExchange e registry path] 35 | (with-open [_ e] 36 | (let [request-method (.getRequestMethod e) 37 | request-path (.getPath (.getRequestURI e)) 38 | response 39 | (-> (try 40 | (cond (not= request-path path) 41 | (plain-response 404 "Not found: " request-path) 42 | 43 | (not= request-method "GET") 44 | (plain-response 405 "Method not allowed: " request-method) 45 | 46 | :else 47 | (ring/metrics-response registry)) 48 | (catch Throwable t 49 | (plain-response 500 (pr-str t)))) 50 | (update :body #(.getBytes ^String % "UTF-8")))] 51 | (doto e 52 | (write-headers! response) 53 | (write-body! response))))) 54 | 55 | (defn- metrics-handler 56 | [registry path] 57 | (reify HttpHandler 58 | (handle [_ e] 59 | (write-response! e registry path)))) 60 | 61 | ;; ## Server 62 | 63 | (defn metrics-server 64 | "Expose the metrics contained within the given collector registry using 65 | the given port and path. 66 | 67 | Returns a handle on the standalone server, implementing `java.io.Closeable`." 68 | ^java.io.Closeable 69 | [registry & [{:keys [^long port 70 | ^String path 71 | ^long queue-size 72 | ^java.util.concurrent.ExecutorService executor] 73 | :or {port 8080, 74 | path "/metrics" 75 | queue-size 5}}]] 76 | (let [handler (metrics-handler registry path) 77 | server (doto (HttpServer/create) 78 | (.bind (InetSocketAddress. port) queue-size) 79 | (.createContext "/" handler) 80 | (.setExecutor executor) 81 | (.start)) 82 | address (.getAddress server) 83 | data {:address address 84 | :port (.getPort address) 85 | :host (.getHostString address)}] 86 | (reify Object 87 | clojure.lang.ILookup 88 | (valAt [_ k default] 89 | (get data k default)) 90 | (valAt [this k] 91 | (get data k)) 92 | 93 | java.util.Map 94 | (keySet [_] 95 | (.keySet ^java.util.Map data)) 96 | (entrySet [_] 97 | (.entrySet ^java.util.Map data)) 98 | (values [_] 99 | (vals data)) 100 | (isEmpty [_] 101 | false) 102 | (size [_] 103 | (count data)) 104 | (get [_ k] 105 | (get data k)) 106 | 107 | java.io.Closeable 108 | (close [_] 109 | (.stop server 0))))) 110 | -------------------------------------------------------------------------------- /test/iapetos/collector/exceptions_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.collector.exceptions-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.test.check 4 | [generators :as gen] 5 | [properties :as prop] 6 | [clojure-test :refer [defspec]]] 7 | [iapetos.test.generators :as g] 8 | [iapetos.core :as prometheus] 9 | [iapetos.collector.exceptions :as ex])) 10 | 11 | ;; ## Generator 12 | 13 | (def gen-exeception-counter 14 | (g/collector ex/exception-counter)) 15 | 16 | ;; ## Tests 17 | 18 | (defspec t-with-exceptions 100 19 | (prop/for-all 20 | [exception-counter gen-exeception-counter 21 | exception (gen/elements 22 | [nil 23 | (Exception.) 24 | (IllegalArgumentException.) 25 | (IllegalStateException.) 26 | (RuntimeException.)])] 27 | (let [exception-class (some-> exception class (.getName)) 28 | f #(or (some-> exception throw) :ok) 29 | result (try 30 | (ex/with-exceptions exception-counter 31 | (f)) 32 | (catch Throwable _ 33 | :error))] 34 | (and (if exception 35 | (= result :error) 36 | (= result :ok)) 37 | (if exception 38 | (= 1.0 (prometheus/value (exception-counter exception-class))) 39 | (= 0.0 (prometheus/value (exception-counter "java.lang.Exception")))))))) 40 | -------------------------------------------------------------------------------- /test/iapetos/collector/fn_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.collector.fn-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.test.check 4 | [generators :as gen] 5 | [properties :as prop] 6 | [clojure-test :refer [defspec]]] 7 | [iapetos.test.generators :as g] 8 | [iapetos.core :as prometheus] 9 | [iapetos.collector.fn :as fn])) 10 | 11 | ;; ## Generators 12 | 13 | (defn gen-fn-registry [label-keys] 14 | (g/registry-fn #(fn/initialize %1 {:labels label-keys}))) 15 | 16 | (def gen-labels 17 | (gen/not-empty 18 | (gen/map 19 | g/metric-string 20 | g/metric-string))) 21 | 22 | ;; ## Tests 23 | 24 | (defspec t-wrap-instrumentation 10 25 | (prop/for-all 26 | [[labels registry-fn] 27 | (gen/let [labels gen-labels 28 | registry-fn (gen-fn-registry (keys labels))] 29 | [labels registry-fn]) 30 | [type f] (gen/elements 31 | [[:success #(Thread/sleep 20)] 32 | [:failure #(do (Thread/sleep 20) (throw (Exception.)))]])] 33 | (let [registry (registry-fn) 34 | f' (fn/wrap-instrumentation f registry "f" {:labels labels}) 35 | start-time (System/currentTimeMillis) 36 | start (System/nanoTime) 37 | _ (dotimes [_ 5] (try (f') (catch Throwable _))) 38 | end-time (System/currentTimeMillis) 39 | delta (/ (- (System/nanoTime) start) 1e9) 40 | val-of #(prometheus/value registry %1 (into {} (list {:fn "f"} labels %2)))] 41 | (and (<= 0.1 (:sum (val-of :fn/duration-seconds {})) delta) 42 | (= 5.0 (:count (val-of :fn/duration-seconds {}))) 43 | (or (= type :success) 44 | (= 5.0 (val-of :fn/exceptions-total {:exceptionClass "java.lang.Exception"}))) 45 | (or (= type :failure) 46 | (= 5.0 (val-of :fn/runs-total {:result "success"}))) 47 | (or (= type :success) 48 | (= 5.0 (val-of :fn/runs-total {:result "failure"}))) 49 | (or (= type :success) 50 | (<= (- end-time 20) 51 | (* 1000 (val-of :fn/last-failure-unixtime {})) 52 | end-time)))))) 53 | 54 | (def test-fn nil) 55 | 56 | (defn- reset-test-fn! 57 | [f] 58 | (alter-var-root #'test-fn (constantly f)) 59 | (alter-meta! #'test-fn (constantly {}))) 60 | 61 | (defspec t-instrument! 10 62 | (prop/for-all 63 | [[labels registry-fn] 64 | (gen/let [labels gen-labels 65 | registry-fn (gen-fn-registry (keys labels))] 66 | [labels registry-fn]) 67 | [type f] (gen/elements 68 | [[:success #(Thread/sleep 20)] 69 | [:failure #(do (Thread/sleep 20) (throw (Exception.)))]])] 70 | (reset-test-fn! f) 71 | (let [registry (doto (registry-fn) 72 | (fn/instrument! #'test-fn {:labels labels})) 73 | start-time (System/currentTimeMillis) 74 | start (System/nanoTime) 75 | fn-name "iapetos.collector.fn-test/test-fn" 76 | _ (dotimes [_ 5] (try (test-fn) (catch Throwable _))) 77 | end-time (System/currentTimeMillis) 78 | delta (/ (- (System/nanoTime) start) 1e9) 79 | val-of #(prometheus/value registry %1 (into {} (list {:fn fn-name} labels %2)))] 80 | (and (<= 0.1 (:sum (val-of :fn/duration-seconds {})) delta) 81 | (= 5.0 (:count (val-of :fn/duration-seconds {}))) 82 | (or (= type :success) 83 | (= 5.0 (val-of :fn/exceptions-total {:exceptionClass "java.lang.Exception"}))) 84 | (or (= type :failure) 85 | (= 5.0 (val-of :fn/runs-total {:result "success"}))) 86 | (or (= type :success) 87 | (= 5.0 (val-of :fn/runs-total {:result "failure"}))) 88 | (or (= type :success) 89 | (<= (- end-time 20) 90 | (* 1000 (val-of :fn/last-failure-unixtime {})) 91 | end-time)))))) 92 | -------------------------------------------------------------------------------- /test/iapetos/collector/jvm_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.collector.jvm-test 2 | (:require [clojure.test.check 3 | [generators :as gen] 4 | [properties :as prop] 5 | [clojure-test :refer [defspec]]] 6 | [clojure.test :refer :all] 7 | [iapetos.test.generators :as g] 8 | [iapetos.core :as prometheus] 9 | [iapetos.collector.jvm :as jvm])) 10 | 11 | (defspec t-jvm-collectors 10 12 | (prop/for-all 13 | [registry-fn (g/registry-fn)] 14 | (let [registry (-> (registry-fn) 15 | (jvm/initialize))] 16 | (and (registry :iapetos-internal/jvm-standard) 17 | (registry :iapetos-internal/jvm-gc) 18 | (registry :iapetos-internal/jvm-memory-pools) 19 | (registry :iapetos-internal/jvm-threads))))) 20 | -------------------------------------------------------------------------------- /test/iapetos/collector/ring_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.collector.ring-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.test.check 4 | [generators :as gen] 5 | [properties :as prop] 6 | [clojure-test :refer [defspec]]] 7 | [iapetos.test.generators :as g] 8 | [iapetos.core :as prometheus] 9 | [iapetos.export :as export] 10 | [iapetos.collector.ring :as ring])) 11 | 12 | ;; ## Generators 13 | 14 | (defn- status-labels 15 | [status] 16 | {:status (str status) 17 | :statusClass (str (quot status 100) "XX")}) 18 | 19 | (def gen-handler 20 | (gen/let [status (gen/elements 21 | (concat 22 | (range 200 205) 23 | (range 300 308) 24 | (range 400 429) 25 | (range 500 505)))] 26 | (gen/one-of 27 | [(gen/return 28 | {:handler (constantly {:status status}) 29 | :async? false 30 | :exception? false 31 | :labels {:status (str status) 32 | :statusClass (str (quot status 100) "XX")}}) 33 | (gen/return 34 | {:handler (fn [_ respond _] (deliver respond {:status status})) 35 | :async? true 36 | :exception? false 37 | :labels {:status (str status) 38 | :statusClass (str (quot status 100) "XX")}}) 39 | (gen/return 40 | {:handler (fn [_ _ raise] (deliver raise (Exception.))) 41 | :async? true 42 | :exception? true}) 43 | (gen/return 44 | {:handler (fn [_] (throw (Exception.))) 45 | :async? false 46 | :exception? true})]))) 47 | 48 | (def gen-request 49 | (gen/let [path (gen/fmap #(str "/" %) gen/string-alpha-numeric) 50 | method (gen/elements [:get :post :put :delete :patch :options :head])] 51 | (gen/return 52 | {:request-method method 53 | :uri path 54 | :labels {:method (-> method name .toUpperCase) 55 | :path path}}))) 56 | 57 | (defn async-response [handler request] 58 | (let [result (promise)] 59 | (handler request #(result [:ok %]) #(result [:fail %])) 60 | (if-let [[status value] (deref result 1500 nil)] 61 | (if (= status :ok) 62 | value 63 | ::error)))) 64 | 65 | (defn sync-response [handler request] 66 | (try 67 | (handler request) 68 | (catch Throwable t 69 | ::error))) 70 | 71 | ;; ## Tests 72 | 73 | (defspec t-wrap-instrumentation 200 74 | (prop/for-all 75 | [registry-fn (g/registry-fn ring/initialize) 76 | {:keys [handler async? exception? labels]} gen-handler 77 | {labels' :labels, :as request} gen-request 78 | wrap (gen/elements [ring/wrap-instrumentation ring/wrap-metrics])] 79 | (let [registry (registry-fn) 80 | handler' (wrap handler registry) 81 | start-time (System/nanoTime) 82 | response (if async? 83 | (async-response handler' request) 84 | (sync-response handler' request)) 85 | delta (/ (- (System/nanoTime) start-time) 1e9) 86 | labels (merge labels labels') 87 | ex-labels (assoc labels' :exceptionClass "java.lang.Exception") 88 | counter (registry :http/requests-total labels) 89 | histogram (registry :http/request-latency-seconds labels) 90 | ex-counter (registry :http/exceptions-total ex-labels)] 91 | (if exception? 92 | (and (= response ::error) 93 | (= 0.0 (prometheus/value counter)) 94 | (= 0.0 (:count (prometheus/value histogram))) 95 | (= 1.0 (prometheus/value ex-counter))) 96 | (and (map? response) 97 | (= 0.0 (prometheus/value ex-counter)) 98 | (< 0.0 (:sum (prometheus/value histogram)) delta) 99 | (= 1.0 (prometheus/value counter))))))) 100 | 101 | (defspec t-wrap-instrumentation-with-exception-status 10 102 | (prop/for-all 103 | [registry-fn (g/registry-fn ring/initialize) 104 | {:keys [handler exception? async? labels]} gen-handler 105 | {labels' :labels, :as request} gen-request 106 | wrap (gen/elements [ring/wrap-instrumentation ring/wrap-metrics])] 107 | (let [registry (registry-fn) 108 | ex-status 500 109 | handler' (wrap handler registry {:exception-status ex-status}) 110 | start-time (System/nanoTime) 111 | response (if async? 112 | (async-response handler' request) 113 | (sync-response handler' request)) 114 | delta (/ (- (System/nanoTime) start-time) 1e9) 115 | labels (merge (or labels (status-labels ex-status)) labels') 116 | ex-labels (assoc labels' :exceptionClass "java.lang.Exception") 117 | counter (registry :http/requests-total labels) 118 | histogram (registry :http/request-latency-seconds labels) 119 | ex-counter (registry :http/exceptions-total ex-labels)] 120 | (and 121 | (< 0.0 (:sum (prometheus/value histogram)) delta) 122 | (= 1.0 (prometheus/value counter)) 123 | (if exception? 124 | (and (= response ::error) 125 | (= 1.0 (prometheus/value ex-counter))) 126 | (and (map? response) 127 | (= 0.0 (prometheus/value ex-counter)))))))) 128 | 129 | (defspec t-wrap-metrics-expose 50 130 | (prop/for-all 131 | [registry-fn (g/registry-fn ring/initialize) 132 | path (gen/fmap #(str "/" %) gen/string-alpha-numeric) 133 | [handler-fn async?] (gen/elements [[(constantly {:status 200}) false] 134 | [(fn [_ respond _] (deliver respond {:status 200})) true]]) 135 | wrap (gen/elements [ring/wrap-metrics-expose ring/wrap-metrics])] 136 | (let [registry (registry-fn) 137 | response-fn (if async? async-response sync-response) 138 | handler (-> handler-fn 139 | (wrap registry {:path path}))] 140 | (and (= {:status 200} 141 | (response-fn handler {:request-method :get, :uri (str path "__/health")})) 142 | (= {:status 405} 143 | (response-fn handler {:request-method :post, :uri path})) 144 | (let [{:keys [status headers body]} 145 | (response-fn handler {:request-method :get, :uri path})] 146 | (and (= 200 status) 147 | (contains? headers "Content-Type") 148 | (re-matches #"text/plain(;.*)?" (headers "Content-Type")) 149 | (= (export/text-format registry) body))))))) 150 | 151 | (defspec t-wrap-metrics-expose-with-on-request-hook 50 152 | (prop/for-all 153 | [registry-fn (g/registry-fn ring/initialize) 154 | path (gen/fmap #(str "/" %) gen/string-alpha-numeric) 155 | [handler-fn async?] (gen/elements [[(constantly {:status 200}) false] 156 | [(fn [_ respond _] (deliver respond {:status 200})) true]]) 157 | wrap (gen/elements [ring/wrap-metrics-expose ring/wrap-metrics])] 158 | (let [registry (-> (registry-fn) 159 | (prometheus/register 160 | (prometheus/counter :http/scrape-requests-total))) 161 | response-fn (if async? async-response sync-response) 162 | on-request-fn #(prometheus/inc % :http/scrape-requests-total) 163 | handler (-> handler-fn 164 | (wrap registry 165 | {:path path 166 | :on-request on-request-fn}))] 167 | (and (zero? (prometheus/value (registry :http/scrape-requests-total))) 168 | (= 200 (:status (response-fn handler {:request-method :get, :uri path}))) 169 | (= 1.0 (prometheus/value (registry :http/scrape-requests-total))))))) 170 | 171 | (defspec t-wrap-metrics-with-labels 50 172 | (prop/for-all 173 | [registry-fn (g/registry-fn 174 | #(ring/initialize % {:labels [:extraReq :extraResp]})) 175 | [handler-fn async?] (gen/elements [[#(constantly %) false] 176 | [#(fn [_ respond _] (deliver respond %)) true]]) 177 | request-label (gen/not-empty gen/string-alpha-numeric) 178 | response-label (gen/not-empty gen/string-alpha-numeric) 179 | wrap (gen/elements [ring/wrap-metrics ring/wrap-instrumentation])] 180 | (let [registry (registry-fn) 181 | response-fn (if async? async-response sync-response) 182 | response {:status 200 183 | :extra-labels {:extraResp response-label}} 184 | request {:request-method :get 185 | :uri "/" 186 | :extra-labels {:extraReq request-label}} 187 | handler (-> (handler-fn response) 188 | (wrap 189 | registry 190 | {:label-fn (fn [request response] 191 | (merge 192 | (:extra-labels request) 193 | (:extra-labels response)))})) 194 | labels {:extraReq request-label 195 | :extraResp response-label 196 | :status "200" 197 | :statusClass "2XX" 198 | :method "GET" 199 | :path "/"}] 200 | (and (zero? (prometheus/value (registry :http/requests-total labels))) 201 | (= 200 (:status (response-fn handler request))) 202 | (= 1.0 (prometheus/value (registry :http/requests-total labels))))))) 203 | -------------------------------------------------------------------------------- /test/iapetos/collector_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.collector-test 2 | (:require [clojure.test.check 3 | [generators :as gen] 4 | [properties :as prop] 5 | [clojure-test :refer [defspec]]] 6 | [clojure.test :refer :all] 7 | [iapetos.test.generators :as g] 8 | [iapetos.collector :as c]) 9 | (:import [io.prometheus.client 10 | Counter 11 | Histogram 12 | Gauge 13 | SimpleCollector$Builder 14 | Summary])) 15 | 16 | (def gen-raw-collector 17 | (gen/let [builder (gen/elements 18 | [(Counter/build) 19 | (Histogram/build) 20 | (Gauge/build) 21 | (Summary/build)]) 22 | collector-namespace g/metric-string 23 | collector-name g/metric-string 24 | help-string (gen/not-empty gen/string-ascii)] 25 | (gen/return 26 | {:collector (-> ^SimpleCollector$Builder 27 | builder 28 | (.name collector-name) 29 | (.namespace collector-namespace) 30 | (.help help-string) 31 | (.create)) 32 | :collector-namespace collector-namespace 33 | :collector-name collector-name}))) 34 | 35 | (defspec t-raw-collectors 20 36 | (prop/for-all 37 | [{:keys [collector collector-name collector-namespace]} 38 | gen-raw-collector] 39 | (and (is (= collector 40 | (c/instantiate collector {}))) 41 | (is (= {:name collector-name 42 | :namespace collector-namespace} 43 | (c/metric collector))) 44 | (is (thrown? 45 | UnsupportedOperationException 46 | (c/label-instance collector collector {:label "value"}))) 47 | (is (= collector 48 | (c/label-instance collector collector {}))) ))) 49 | -------------------------------------------------------------------------------- /test/iapetos/core/counter_macro_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.core.counter-macro-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.test.check 4 | [generators :as gen] 5 | [properties :as prop] 6 | [clojure-test :refer [defspec]]] 7 | [iapetos.test.generators :as g] 8 | [iapetos.core :as prometheus])) 9 | 10 | ;; ## Helpers 11 | 12 | (defmacro run-success! 13 | [n f counter] 14 | `(dotimes [_# ~n] 15 | (~f ~counter 16 | (comment 'do-something)))) 17 | 18 | (defmacro run-failure! 19 | [n f counter] 20 | `(dotimes [_# ~n] 21 | (try 22 | (~f ~counter 23 | (throw (Exception.))) 24 | (catch Throwable _#)))) 25 | 26 | (defmacro run-test! 27 | ([n f counter] 28 | `(run-test! ~n ~n ~f ~counter)) 29 | ([n m f counter] 30 | `(do 31 | (run-success! ~n ~f ~counter) 32 | (run-failure! ~m ~f ~counter)))) 33 | 34 | ;; ## Generator 35 | 36 | (def gen-countable 37 | (gen/let [metric g/metric 38 | countable (gen/elements 39 | [(prometheus/counter metric) 40 | (prometheus/gauge metric)]) 41 | registry-fn (g/registry-fn)] 42 | (let [registry (-> (registry-fn) 43 | (prometheus/register countable))] 44 | (gen/return (registry metric))))) 45 | 46 | ;; ## Tests 47 | 48 | (defspec t-with-counter 100 49 | (prop/for-all 50 | [counter gen-countable] 51 | (run-test! 5 prometheus/with-counter counter) 52 | (= 10.0 (prometheus/value counter)))) 53 | 54 | (defspec t-with-success-counter 100 55 | (prop/for-all 56 | [counter gen-countable] 57 | (run-test! 7 3 prometheus/with-success-counter counter) 58 | (= 7.0 (prometheus/value counter)))) 59 | 60 | (defspec t-with-failure-counter 100 61 | (prop/for-all 62 | [counter gen-countable] 63 | (run-test! 7 3 prometheus/with-failure-counter counter) 64 | (= 3.0 (prometheus/value counter)))) 65 | 66 | (defspec t-with-counters 67 | (prop/for-all 68 | [total-counter gen-countable 69 | success-counter gen-countable 70 | failure-counter gen-countable] 71 | (dotimes [_ 7] 72 | (prometheus/with-counters 73 | {:success success-counter 74 | :failure failure-counter 75 | :total total-counter} 76 | (comment 'do-something))) 77 | (dotimes [_ 3] 78 | (try 79 | (prometheus/with-counters 80 | {:success success-counter 81 | :failure failure-counter 82 | :total total-counter} 83 | (throw (Exception.))) 84 | (catch Throwable _))) 85 | (and (= 10.0 (prometheus/value total-counter)) 86 | (= 7.0 (prometheus/value success-counter)) 87 | (= 3.0 (prometheus/value failure-counter))))) 88 | 89 | (defspec t-with-activity-counter 5 90 | (prop/for-all 91 | [registry-fn (g/registry-fn)] 92 | (let [metric :app/activity-total 93 | registry (-> (registry-fn) 94 | (prometheus/register 95 | (prometheus/gauge metric))) 96 | counter (registry metric) 97 | start-promise (promise) 98 | started-promise (promise) 99 | finish-promise (promise) 100 | job (future 101 | @start-promise 102 | (prometheus/with-activity-counter counter 103 | (deliver started-promise true) 104 | @finish-promise))] 105 | (and (= 0.0 (prometheus/value counter)) 106 | (deliver start-promise true) 107 | @started-promise 108 | (= 1.0 (prometheus/value counter)) 109 | (deliver finish-promise true) 110 | @job 111 | (= 0.0 (prometheus/value counter)))))) 112 | -------------------------------------------------------------------------------- /test/iapetos/core/counter_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.core.counter-test 2 | (:require [clojure.test.check 3 | [generators :as gen] 4 | [properties :as prop] 5 | [clojure-test :refer [defspec]]] 6 | [iapetos.test.generators :as g] 7 | [iapetos.core :as prometheus])) 8 | 9 | ;; ## Counter w/o Labels 10 | 11 | (def gen-incrementers 12 | (gen/vector 13 | (gen/let [amount (gen/fmap #(Math/abs ^double %) 14 | (gen/double* {:infinite? false, :NaN? false}))] 15 | (gen/elements 16 | [{:f #(prometheus/inc %1 %2 amount) 17 | :form '(inc registry metric ~amount) 18 | :amount amount} 19 | {:f #(prometheus/inc (%1 %2) amount) 20 | :form '(inc (registry metric) ~amount) 21 | :amount amount} 22 | {:f #(prometheus/inc %1 %2) 23 | :form '(inc registry metric) 24 | :amount 1.0} 25 | {:f #(prometheus/inc (%1 %2)) 26 | :form '(inc (registry metric)) 27 | :amount 1.0}])))) 28 | 29 | (defspec t-counter 100 30 | (prop/for-all 31 | [metric g/metric 32 | incrementers gen-incrementers 33 | registry-fn (g/registry-fn)] 34 | (let [registry (-> (registry-fn) 35 | (prometheus/register 36 | (prometheus/counter metric))) 37 | expected-value (double (reduce + (map :amount incrementers)))] 38 | (doseq [{:keys [f]} incrementers] 39 | (f registry metric)) 40 | (= expected-value (prometheus/value (registry metric)))))) 41 | 42 | ;; ## Counter w/ Labels 43 | 44 | (def labels 45 | {:label "x"}) 46 | 47 | (def gen-incrementers-with-labels 48 | (gen/vector 49 | (gen/let [amount (gen/fmap #(Math/abs ^double %) 50 | (gen/double* {:infinite? false, :NaN? false}))] 51 | (gen/elements 52 | [{:f #(prometheus/inc %1 %2 labels amount) 53 | :form '(inc registry metric labels ~amount) 54 | :amount amount} 55 | {:f #(prometheus/inc (%1 %2 labels) amount) 56 | :form '(inc (registry metric labels) ~amount) 57 | :amount amount} 58 | {:f #(prometheus/inc %1 %2 labels) 59 | :form '(inc registry metric labels) 60 | :amount 1.0} 61 | {:f #(prometheus/inc (%1 %2 labels)) 62 | :form '(inc (registry metric labels)) 63 | :amount 1.0}])))) 64 | 65 | (defspec t-counter-with-labels 100 66 | (prop/for-all 67 | [metric g/metric 68 | incrementers gen-incrementers-with-labels 69 | registry-fn (g/registry-fn)] 70 | (let [registry (-> (registry-fn) 71 | (prometheus/register 72 | (prometheus/counter metric {:labels (keys labels)}))) 73 | expected-value (double (reduce + (map :amount incrementers)))] 74 | (doseq [{:keys [f]} incrementers] 75 | (f registry metric)) 76 | (= expected-value (prometheus/value (registry metric labels)))))) 77 | -------------------------------------------------------------------------------- /test/iapetos/core/duration_macro_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.core.duration-macro-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.test.check 4 | [generators :as gen] 5 | [properties :as prop] 6 | [clojure-test :refer [defspec]]] 7 | [iapetos.test.generators :as g] 8 | [iapetos.core :as prometheus])) 9 | 10 | ;; ## Generator 11 | 12 | (def gen-timeable 13 | (gen/let [metric g/metric 14 | countable (gen/elements 15 | [(prometheus/gauge metric) 16 | (prometheus/histogram metric) 17 | (prometheus/summary metric)]) 18 | registry-fn (g/registry-fn)] 19 | (let [registry (-> (registry-fn) 20 | (prometheus/register countable))] 21 | (gen/return 22 | (vector 23 | (if (= (:type countable) :gauge) 24 | prometheus/value 25 | (comp :sum prometheus/value)) 26 | (registry metric)))))) 27 | 28 | ;; ## Tests 29 | 30 | (defspec t-with-duration 25 31 | (prop/for-all 32 | [[get-fn timeable] gen-timeable] 33 | (let [start (System/nanoTime) 34 | _ (prometheus/with-duration timeable 35 | (Thread/sleep 10)) 36 | delta (/ (- (System/nanoTime) start) 1e9) 37 | value (get-fn timeable)] 38 | (<= 0.01 value delta)))) 39 | -------------------------------------------------------------------------------- /test/iapetos/core/gauge_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.core.gauge-test 2 | (:require [clojure.test.check 3 | [generators :as gen] 4 | [properties :as prop] 5 | [clojure-test :refer [defspec]]] 6 | [clojure.test :refer :all] 7 | [iapetos.test.generators :as g] 8 | [iapetos.core :as prometheus])) 9 | 10 | ;; ## Gauge w/o Labels 11 | 12 | (def gen-ops 13 | (gen/vector 14 | (gen/let [v (gen/double* {:infinite? false, :NaN? false})] 15 | (let [amount (Math/abs ^double v)] 16 | (gen/elements 17 | [{:f #(prometheus/inc %1 %2 amount) 18 | :form '(inc registry metric ~amount) 19 | :effect #(+ % amount)} 20 | {:f #(prometheus/inc (%1 %2) amount) 21 | :form '(inc (registry metric) ~amount) 22 | :effect #(+ % amount)} 23 | {:f #(prometheus/inc %1 %2) 24 | :form '(inc registry metric) 25 | :effect #(+ % 1.0)} 26 | {:f #(prometheus/inc (%1 %2)) 27 | :form '(inc (registry metric)) 28 | :effect #(+ % 1.0)} 29 | {:f #(prometheus/dec %1 %2 amount) 30 | :form '(dec registry metric ~amount) 31 | :effect #(- % amount)} 32 | {:f #(prometheus/dec (%1 %2) amount) 33 | :form '(dec (registry metric) ~amount) 34 | :effect #(- % amount)} 35 | {:f #(prometheus/dec %1 %2) 36 | :form '(dec registry metric) 37 | :effect #(- % 1.0)} 38 | {:f #(prometheus/dec (%1 %2)) 39 | :form '(dec (registry metric)) 40 | :effect #(- % 1.0)} 41 | {:f #(prometheus/observe %1 %2 amount) 42 | :form '(observe registry metric ~amount) 43 | :effect (constantly amount)} 44 | {:f #(prometheus/observe (%1 %2) amount) 45 | :form '(observe (registry metric) ~amount) 46 | :effect (constantly amount)} 47 | {:f #(prometheus/set %1 %2 amount) 48 | :form '(set registry metric ~amount) 49 | :effect (constantly amount)} 50 | {:f #(prometheus/set (%1 %2) amount) 51 | :form '(set (registry metric) ~amount) 52 | :effect (constantly amount)}]))))) 53 | 54 | (defspec t-gauge 100 55 | (prop/for-all 56 | [metric g/metric 57 | ops gen-ops 58 | registry-fn (g/registry-fn)] 59 | (let [registry (-> (registry-fn) 60 | (prometheus/register 61 | (prometheus/gauge metric))) 62 | expected-value (double (reduce #((:effect %2) %1) 0.0 ops))] 63 | (doseq [{:keys [f]} ops] 64 | (f registry metric)) 65 | (= expected-value (prometheus/value (registry metric)))))) 66 | 67 | ;; ## Gauge w/ Labels 68 | 69 | (def labels 70 | {:label "x"}) 71 | 72 | (def gen-ops-with-labels 73 | (gen/vector 74 | (gen/let [v (gen/double* {:infinite? false, :NaN? false})] 75 | (let [amount (Math/abs ^double v)] 76 | (gen/elements 77 | [{:f #(prometheus/inc %1 %2 labels amount) 78 | :form '(inc registry metric labels ~amount) 79 | :effect #(+ % amount)} 80 | {:f #(prometheus/inc (%1 %2 labels) amount) 81 | :form '(inc (registry metric labels) ~amount) 82 | :effect #(+ % amount)} 83 | {:f #(prometheus/inc %1 %2 labels) 84 | :form '(inc registry metric labels) 85 | :effect #(+ % 1.0)} 86 | {:f #(prometheus/inc (%1 %2 labels)) 87 | :form '(inc (registry metric labels)) 88 | :effect #(+ % 1.0)} 89 | {:f #(prometheus/dec %1 %2 labels amount) 90 | :form '(dec registry metric labels ~amount) 91 | :effect #(- % amount)} 92 | {:f #(prometheus/dec (%1 %2 labels) amount) 93 | :form '(dec (registry metric labels) ~amount) 94 | :effect #(- % amount)} 95 | {:f #(prometheus/dec %1 %2 labels) 96 | :form '(dec registry metric labels) 97 | :effect #(- % 1.0)} 98 | {:f #(prometheus/dec (%1 %2 labels)) 99 | :form '(dec (registry metric labels)) 100 | :effect #(- % 1.0)} 101 | {:f #(prometheus/observe %1 %2 labels amount) 102 | :form '(observe registry metric labels ~amount) 103 | :effect (constantly amount)} 104 | {:f #(prometheus/observe (%1 %2 labels) amount) 105 | :form '(observe (registry metric labels) ~amount) 106 | :effect (constantly amount)} 107 | {:f #(prometheus/set %1 %2 labels amount) 108 | :form '(set registry metric labels ~amount) 109 | :effect (constantly amount)} 110 | {:f #(prometheus/set (%1 %2 labels) amount) 111 | :form '(set (registry metric labels) ~amount) 112 | :effect (constantly amount)}]))))) 113 | 114 | (defspec t-gauge-with-labels 100 115 | (prop/for-all 116 | [metric g/metric 117 | ops gen-ops-with-labels 118 | registry-fn (g/registry-fn)] 119 | (let [registry (-> (registry-fn) 120 | (prometheus/register 121 | (prometheus/gauge metric {:labels (keys labels)}))) 122 | expected-value (double (reduce #((:effect %2) %1) 0.0 ops))] 123 | (doseq [{:keys [f]} ops] 124 | (f registry metric)) 125 | (= expected-value (prometheus/value (registry metric labels)))))) 126 | 127 | ;; ## Setting to Current Time 128 | 129 | (def gen-set-to-current-time-op 130 | (gen/elements 131 | [{:f #(prometheus/set-to-current-time %1 %2) 132 | :form '(set-to-current-time registry metric)} 133 | {:f #(prometheus/set-to-current-time (%1 %2)) 134 | :form '(set-to-current-time (registry metric))}])) 135 | 136 | (defspec t-gauge-set-to-current-time 100 137 | (prop/for-all 138 | [metric g/metric 139 | op gen-set-to-current-time-op 140 | registry-fn (g/registry-fn)] 141 | (let [registry (-> (registry-fn) 142 | (prometheus/register 143 | (prometheus/gauge metric))) 144 | before (System/currentTimeMillis) 145 | _ ((:f op) registry metric) 146 | after (System/currentTimeMillis)] 147 | (<= before (* (prometheus/value (registry metric)) 1e3) after)))) 148 | 149 | ;; ## Timer 150 | 151 | (defspec t-gauge-timer 5 152 | (prop/for-all 153 | [registry-fn (g/registry-fn)] 154 | (let [metric :app/duration-seconds 155 | registry (-> (registry-fn) 156 | (prometheus/register 157 | (prometheus/gauge metric))) 158 | start (System/nanoTime) 159 | stop-timer (prometheus/start-timer registry metric) 160 | _ (do (Thread/sleep 50) (stop-timer)) 161 | delta (/ (- (System/nanoTime) start) 1e9)] 162 | (is (<= 0.05 (prometheus/value (registry metric)) delta))))) 163 | -------------------------------------------------------------------------------- /test/iapetos/core/histogram_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.core.histogram-test 2 | (:require [clojure.test.check 3 | [generators :as gen] 4 | [properties :as prop] 5 | [clojure-test :refer [defspec]]] 6 | [clojure.test :refer :all] 7 | [iapetos.test.generators :as g] 8 | [iapetos.core :as prometheus])) 9 | 10 | ;; ## Helpers 11 | 12 | (def buckets 13 | [1 2 3]) 14 | 15 | (defn- ->effect 16 | [amount] 17 | #(-> % 18 | (cond-> 19 | (<= amount 1.0) (update-in [:buckets 0] inc) 20 | (<= amount 2.0) (update-in [:buckets 1] inc) 21 | (<= amount 3.0) (update-in [:buckets 2] inc) 22 | (<= amount 4.0) (update-in [:buckets 3] inc)) 23 | (update :count inc) 24 | (update :sum + amount))) 25 | 26 | ;; ## Summary w/o Labels 27 | 28 | (def gen-ops 29 | (gen/vector 30 | (gen/let [amount (gen/double* 31 | {:infinite? false, :NaN? false, :min 0.0, :max 4.0})] 32 | (let [effect (->effect amount)] 33 | (gen/elements 34 | [{:f #(prometheus/observe %1 %2 amount) 35 | :form '(observe registry metric ~amount) 36 | :effect effect} 37 | {:f #(prometheus/observe (%1 %2) amount) 38 | :form '(observe (registry metric) ~amount) 39 | :effect effect}]))))) 40 | 41 | (defspec t-histogram 100 42 | (prop/for-all 43 | [metric g/metric 44 | ops gen-ops 45 | registry-fn (g/registry-fn)] 46 | (let [registry (-> (registry-fn) 47 | (prometheus/register 48 | (prometheus/histogram metric {:buckets buckets}))) 49 | expected-value (reduce 50 | #((:effect %2) %1) 51 | {:buckets [0.0 0.0 0.0 0.0], :count 0.0, :sum 0.0} 52 | ops)] 53 | (doseq [{:keys [f]} ops] 54 | (f registry metric)) 55 | (= expected-value (prometheus/value (registry metric)))))) 56 | 57 | ;; ## Summary w/ Labels 58 | 59 | (def labels 60 | {:label "x"}) 61 | 62 | (def gen-ops 63 | (gen/vector 64 | (gen/let [amount (gen/double* 65 | {:infinite? false, :NaN? false, :min 0.0, :max 4.0})] 66 | (let [effect (->effect amount)] 67 | (gen/elements 68 | [{:f #(prometheus/observe %1 %2 labels amount) 69 | :form '(observe registry metric labels ~amount) 70 | :effect effect} 71 | {:f #(prometheus/observe (%1 %2 labels) amount) 72 | :form '(observe (registry metric labels) ~amount) 73 | :effect effect}]))))) 74 | 75 | (defspec t-histogram-with-labels 100 76 | (prop/for-all 77 | [metric g/metric 78 | ops gen-ops 79 | registry-fn (g/registry-fn)] 80 | (let [registry (-> (registry-fn) 81 | (prometheus/register 82 | (prometheus/histogram 83 | metric 84 | {:labels (keys labels), :buckets buckets}))) 85 | expected-value (reduce 86 | #((:effect %2) %1) 87 | {:buckets [0.0 0.0 0.0 0.0], :count 0.0, :sum 0.0} 88 | ops)] 89 | (doseq [{:keys [f]} ops] 90 | (f registry metric)) 91 | (= expected-value (prometheus/value (registry metric labels)))))) 92 | 93 | ;; ## Histogram Timer 94 | 95 | (defspec t-histogram-timer 5 96 | (prop/for-all 97 | [registry-fn (g/registry-fn)] 98 | (let [metric :app/duration-seconds 99 | registry (-> (registry-fn) 100 | (prometheus/register 101 | (prometheus/histogram 102 | metric 103 | {:buckets [0.01 0.05 0.5]}))) 104 | start (System/nanoTime) 105 | stop-timer (prometheus/start-timer registry metric) 106 | _ (do (Thread/sleep 50) (stop-timer)) 107 | delta (/ (- (System/nanoTime) start) 1e9) 108 | {:keys [count sum buckets]} (prometheus/value (registry metric))] 109 | (and (= 1.0 count) 110 | (= [0.0 0.0 1.0 1.0] buckets) 111 | (<= 0.05 sum delta))))) 112 | -------------------------------------------------------------------------------- /test/iapetos/core/lazy_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.core.lazy-test 2 | (:require [clojure.test.check 3 | [generators :as gen] 4 | [properties :as prop] 5 | [clojure-test :refer [defspec]]] 6 | [clojure.test :refer :all] 7 | [iapetos.test.generators :as g] 8 | [iapetos.core :as prometheus] 9 | [iapetos.export :as export])) 10 | 11 | (def gen-collector-constructor 12 | (gen/elements 13 | [prometheus/gauge 14 | prometheus/counter 15 | prometheus/summary])) 16 | 17 | (defspec t-lazy-deprecation 20 18 | (prop/for-all 19 | [registry-fn (g/registry-fn) 20 | collector-fn g/collector-constructor 21 | collector-name g/metric] 22 | (let [collector (collector-fn collector-name {:lazy? true}) 23 | registry (registry-fn) 24 | output (with-out-str 25 | (prometheus/register registry collector))] 26 | (.startsWith 27 | ^String output 28 | "collector option ':lazy?' is deprecated, use 'register-lazy' instead.")))) 29 | 30 | (defspec t-register-lazy 100 31 | (prop/for-all 32 | [registry-fn (g/registry-fn) 33 | collector-fn g/collector-constructor 34 | collector-name g/metric] 35 | (let [collector (collector-fn collector-name) 36 | registry (-> (registry-fn) 37 | (prometheus/register-lazy collector))] 38 | (and (is (= "" (export/text-format registry))) 39 | (is (registry collector-name)) 40 | (is (not= "" (export/text-format registry))))))) 41 | -------------------------------------------------------------------------------- /test/iapetos/core/subsystem_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.core.subsystem-test 2 | (:require [clojure.test.check 3 | [generators :as gen] 4 | [properties :as prop] 5 | [clojure-test :refer [defspec]]] 6 | [clojure.string :as string] 7 | [iapetos.test.generators :as g] 8 | [iapetos.core :as prometheus] 9 | [iapetos.export :as export])) 10 | 11 | ;; ## Generators 12 | 13 | (def gen-metric-fn 14 | (gen/elements 15 | [prometheus/counter 16 | prometheus/gauge 17 | prometheus/histogram 18 | prometheus/summary])) 19 | 20 | ;; ## Helpers 21 | 22 | (defn- parse-subsystems 23 | [registry] 24 | (->> (export/text-format registry) 25 | (re-seq #"TYPE app_(.+)_runs_total ") 26 | (keep second) 27 | (sort))) 28 | 29 | ;; ## Test 30 | 31 | (defspec t-subsystem 25 32 | (prop/for-all 33 | [registry-fn (g/registry-fn) 34 | subsystem->metric-fn (gen/bind 35 | (->> (gen/vector g/valid-name) 36 | (gen/not-empty) 37 | (gen/fmap (comp sort distinct))) 38 | (fn [subsystems] 39 | (gen/fmap 40 | #(map vector subsystems %) 41 | (gen/vector gen-metric-fn (count subsystems)))))] 42 | (let [registry (registry-fn) 43 | subsystems (map first subsystem->metric-fn)] 44 | (doseq [[subsystem metric-fn] subsystem->metric-fn] 45 | (-> registry 46 | (prometheus/subsystem subsystem) 47 | (prometheus/register 48 | (metric-fn :app/runs-total)))) 49 | (= subsystems (parse-subsystems registry))))) 50 | 51 | (defspec t-explicit-subsystem 25 52 | (prop/for-all 53 | [registry-fn (g/registry-fn) 54 | subsystem-name g/valid-name 55 | metric-fn gen-metric-fn] 56 | (let [registry (-> (registry-fn) 57 | (prometheus/register 58 | (metric-fn :app/runs-total {:subsystem subsystem-name})))] 59 | (= [subsystem-name] (parse-subsystems registry))))) 60 | 61 | (defspec t-nested-subsystems 25 62 | (prop/for-all 63 | [registry-fn (g/registry-fn) 64 | subsystem-names (gen/not-empty (gen/vector g/valid-name)) 65 | metric-fn gen-metric-fn] 66 | (let [registry (registry-fn) 67 | subsystem-registry (-> (reduce prometheus/subsystem registry subsystem-names) 68 | (prometheus/register 69 | (metric-fn :app/runs-total))) 70 | expected-name (string/join "_" subsystem-names)] 71 | (= [expected-name] (parse-subsystems registry))))) 72 | 73 | (defspec t-subsystem-conflict 10 74 | (prop/for-all 75 | [registry-fn (g/registry-fn) 76 | subsystem-name g/valid-name 77 | metric-fn gen-metric-fn] 78 | (= (try 79 | (-> (registry-fn) 80 | (prometheus/subsystem subsystem-name) 81 | (prometheus/register 82 | (metric-fn :app/runs-total {:subsystem (str subsystem-name "x")}))) 83 | (catch IllegalArgumentException _ 84 | ::error)) 85 | ::error))) 86 | -------------------------------------------------------------------------------- /test/iapetos/core/summary_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.core.summary-test 2 | (:require [clojure.test.check 3 | [generators :as gen] 4 | [properties :as prop] 5 | [clojure-test :refer [defspec]]] 6 | [clojure.test :refer :all] 7 | [iapetos.test.generators :as g] 8 | [iapetos.core :as prometheus])) 9 | 10 | ;; ## Summary w/o Labels 11 | 12 | (def gen-ops 13 | (gen/vector 14 | (gen/let [v (gen/double* {:infinite? false, :NaN? false})] 15 | (let [amount (Math/abs ^double v) 16 | effect #(-> % 17 | (update :count inc) 18 | (update :sum + amount))] 19 | (gen/elements 20 | [{:f #(prometheus/observe %1 %2 amount) 21 | :form '(observe registry metric ~amount) 22 | :effect effect} 23 | {:f #(prometheus/observe (%1 %2) amount) 24 | :form '(observe (registry metric) ~amount) 25 | :effect effect}]))))) 26 | 27 | (defspec t-summary 100 28 | (prop/for-all 29 | [metric g/metric 30 | ops gen-ops 31 | registry-fn (g/registry-fn)] 32 | (let [registry (-> (registry-fn) 33 | (prometheus/register 34 | (prometheus/summary metric))) 35 | expected-value (reduce 36 | #((:effect %2) %1) 37 | {:count 0.0, :sum 0.0, :quantiles {}} 38 | ops)] 39 | (doseq [{:keys [f]} ops] 40 | (f registry metric)) 41 | (= expected-value (prometheus/value (registry metric)))))) 42 | 43 | ;; ## Summary w/ Labels 44 | 45 | (def labels 46 | {:label "x"}) 47 | 48 | (def gen-ops 49 | (gen/vector 50 | (gen/let [v (gen/double* {:infinite? false, :NaN? false})] 51 | (let [amount (Math/abs ^double v) 52 | effect #(-> % 53 | (update :count inc) 54 | (update :sum + amount))] 55 | (gen/elements 56 | [{:f #(prometheus/observe %1 %2 labels amount) 57 | :form '(observe registry metric labels ~amount) 58 | :effect effect} 59 | {:f #(prometheus/observe (%1 %2 labels) amount) 60 | :form '(observe (registry metric labels) ~amount) 61 | :effect effect}]))))) 62 | 63 | (defspec t-summary-with-labels 100 64 | (prop/for-all 65 | [metric g/metric 66 | ops gen-ops 67 | registry-fn (g/registry-fn)] 68 | (let [registry (-> (registry-fn) 69 | (prometheus/register 70 | (prometheus/summary metric {:labels (keys labels)}))) 71 | expected-value (reduce 72 | #((:effect %2) %1) 73 | {:count 0.0, :sum 0.0, :quantiles {}} 74 | ops)] 75 | (doseq [{:keys [f]} ops] 76 | (f registry metric)) 77 | (= expected-value (prometheus/value (registry metric labels)))))) 78 | 79 | ;; ## Summary w/ Labels and Quantiles 80 | 81 | (defspec t-summary-with-labels-and-quantiles 100 82 | (prop/for-all 83 | [metric g/metric 84 | ops gen-ops 85 | registry-fn (g/registry-fn)] 86 | (let [quantiles {0.5 0.05, 0.9 0.1, 0.99 0.001} 87 | registry (-> (registry-fn) 88 | (prometheus/register 89 | (prometheus/summary metric {:labels (keys labels), :quantiles quantiles})))] 90 | (doseq [{:keys [f]} ops] 91 | (f registry metric)) 92 | (= (keys quantiles) (keys (:quantiles (prometheus/value (registry metric labels)))))))) 93 | 94 | ;; ## Summary Timer 95 | 96 | (defspec t-summary-timer 5 97 | (prop/for-all 98 | [registry-fn (g/registry-fn)] 99 | (let [metric :app/duration-seconds 100 | registry (-> (registry-fn) 101 | (prometheus/register-lazy 102 | (prometheus/summary metric))) 103 | start (System/nanoTime) 104 | stop-timer (prometheus/start-timer registry metric) 105 | _ (do (Thread/sleep 50) (stop-timer)) 106 | delta (/ (- (System/nanoTime) start) 1e9) 107 | {:keys [sum count]} (prometheus/value (registry metric))] 108 | (and (= count 1.0) 109 | (<= 0.05 sum delta))))) 110 | -------------------------------------------------------------------------------- /test/iapetos/core/timestamp_macro_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.core.timestamp-macro-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.test.check 4 | [generators :as gen] 5 | [properties :as prop] 6 | [clojure-test :refer [defspec]]] 7 | [iapetos.test.generators :as g] 8 | [iapetos.core :as prometheus])) 9 | 10 | ;; ## Generator 11 | 12 | (def gen-gauge 13 | (gen/let [metric g/metric 14 | labels (gen/map g/metric-string gen/string-alpha-numeric) 15 | registry-fn (g/registry-fn)] 16 | (let [registry (-> (registry-fn) 17 | (prometheus/register 18 | (prometheus/gauge metric {:labels (keys labels)})))] 19 | (gen/return (registry metric labels))))) 20 | 21 | ;; ## Helpers 22 | 23 | (defn run-and-get! 24 | [f gauge] 25 | (let [start (System/currentTimeMillis) 26 | _ (try (f) (catch Throwable _)) 27 | end (System/currentTimeMillis)] 28 | {:value (* (prometheus/value gauge) 1000) 29 | :start start 30 | :end end})) 31 | 32 | ;; ## Tests 33 | 34 | (defspec t-with-timestamp 25 35 | (prop/for-all 36 | [gauge gen-gauge] 37 | (let [{:keys [value start end]} 38 | (run-and-get! 39 | #(prometheus/with-timestamp gauge 40 | (comment 'do-something)) 41 | gauge)] 42 | (<= start value end)))) 43 | 44 | (defspec t-with-failure-timestamp 25 45 | (prop/for-all 46 | [gauge gen-gauge] 47 | (let [{:keys [value start end]} 48 | (run-and-get! 49 | #(prometheus/with-failure-timestamp gauge 50 | (throw (Exception.))) 51 | gauge)] 52 | (<= start value end)))) 53 | 54 | (defspec t-with-failure-timestamp-but-success 25 55 | (prop/for-all 56 | [gauge gen-gauge] 57 | (let [{:keys [value]} 58 | (run-and-get! 59 | #(prometheus/with-failure-timestamp gauge 60 | (comment 'do-something)) 61 | gauge)] 62 | (= value 0.0)))) 63 | 64 | (defspec t-with-success-timestamp 25 65 | (prop/for-all 66 | [gauge gen-gauge] 67 | (let [{:keys [value start end]} 68 | (run-and-get! 69 | #(prometheus/with-success-timestamp gauge 70 | (comment 'do-something)) 71 | gauge)] 72 | (<= start value end)))) 73 | 74 | (defspec t-with-success-timestamp-but-failure 25 75 | (prop/for-all 76 | [gauge gen-gauge] 77 | (let [{:keys [value]} 78 | (run-and-get! 79 | #(prometheus/with-success-timestamp gauge 80 | (throw (Exception.))) 81 | gauge)] 82 | (= value 0.0)))) 83 | 84 | (defspec t-with-timestamps-and-success 25 85 | (prop/for-all 86 | [success-gauge gen-gauge 87 | failure-gauge gen-gauge 88 | run-gauge gen-gauge] 89 | (let [start (System/currentTimeMillis) 90 | _ (prometheus/with-timestamps 91 | {:last-run run-gauge 92 | :last-success success-gauge 93 | :last-failure failure-gauge} 94 | (comment 'do-something)) 95 | end (System/currentTimeMillis) 96 | val-of #(* 1000 (prometheus/value %))] 97 | (and (<= start (val-of run-gauge) end) 98 | (<= start (val-of success-gauge) end) 99 | (= (val-of failure-gauge) 0.0))))) 100 | 101 | (defspec t-with-timestamps-and-failure 25 102 | (prop/for-all 103 | [success-gauge gen-gauge 104 | failure-gauge gen-gauge 105 | run-gauge gen-gauge] 106 | (let [start (System/currentTimeMillis) 107 | _ (try 108 | (prometheus/with-timestamps 109 | {:last-run run-gauge 110 | :last-success success-gauge 111 | :last-failure failure-gauge} 112 | (throw (Exception.))) 113 | (catch Throwable _)) 114 | end (System/currentTimeMillis) 115 | val-of #(* 1000 (prometheus/value %))] 116 | (and (<= start (val-of run-gauge) end) 117 | (<= start (val-of failure-gauge) end) 118 | (= (val-of success-gauge) 0.0))))) 119 | -------------------------------------------------------------------------------- /test/iapetos/export_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.export-test 2 | (:require [clojure.test.check 3 | [generators :as gen] 4 | [properties :as prop] 5 | [clojure-test :refer [defspec]]] 6 | [clojure.test :refer :all] 7 | [iapetos.test.generators :as g] 8 | [iapetos.export :as export] 9 | [iapetos.core :as prometheus] 10 | [aleph.http :as http]) 11 | (:import [io.prometheus.client.exporter.common TextFormat])) 12 | 13 | ;; ## Helpers 14 | 15 | (def ^:private port 65433) 16 | (def ^:private push-gateway (str "0:" port)) 17 | 18 | (defn- start-server-for-promise 19 | ^java.io.Closeable 20 | [promise] 21 | (http/start-server 22 | (fn [request] 23 | (->> (-> request 24 | (update :body #(some-> % slurp)) 25 | (update :body str)) 26 | (deliver promise)) 27 | {:status 202}) 28 | {:port port})) 29 | 30 | (defmacro with-push-gateway 31 | [& body] 32 | `(let [p# (promise) 33 | result# (with-open [s# (start-server-for-promise p#)] 34 | (let [f# (future ~@body) 35 | v# (deref p# 500 ::timeout)] 36 | @f# 37 | v#))] 38 | (when (= result# ::timeout) 39 | (throw 40 | (Exception. 41 | "push gateway did not receive request within 500ms."))) 42 | result#)) 43 | 44 | (defn- matches-registry? 45 | [{:keys [request-method headers body] :as x} registry] 46 | (and (= :post request-method) 47 | (= TextFormat/CONTENT_TYPE_004 (get headers "content-type")) 48 | (= (export/text-format registry) body))) 49 | 50 | ;; ## Generators 51 | 52 | (def gen-push-spec-grouping-key 53 | (gen/map gen/string-alpha-numeric gen/string-alpha-numeric)) 54 | 55 | (def gen-push-spec 56 | (gen/let [registry-fn (gen/one-of 57 | [(gen/return (constantly nil)) 58 | (g/registry-fn)])] 59 | (gen/hash-map 60 | :job gen/string-alpha-numeric 61 | :registry (gen/return (registry-fn)) 62 | :push-gateway (gen/return push-gateway) 63 | :grouping-key gen-push-spec-grouping-key))) 64 | 65 | ;; ## Tests 66 | 67 | (defspec t-pushable-collector-registry 10 68 | (prop/for-all 69 | [push-spec gen-push-spec] 70 | (let [registry (export/pushable-collector-registry push-spec)] 71 | (matches-registry? 72 | (with-push-gateway 73 | (export/push! registry)) 74 | registry)))) 75 | 76 | (defspec t-push-registry! 10 77 | (prop/for-all 78 | [push-spec gen-push-spec 79 | registry-fn (g/registry-fn)] 80 | (let [registry (registry-fn)] 81 | (matches-registry? 82 | (with-push-gateway 83 | (export/push-registry! 84 | registry 85 | (dissoc push-spec :registry))) 86 | registry)))) 87 | 88 | (defspec t-with-push 10 89 | (prop/for-all 90 | [push-spec gen-push-spec] 91 | (let [registry (export/pushable-collector-registry push-spec)] 92 | (matches-registry? 93 | (with-push-gateway 94 | (export/with-push registry 95 | (comment 'do-something))) 96 | registry)))) 97 | 98 | (defspec t-with-push-gateway 10 99 | (prop/for-all 100 | [push-spec gen-push-spec] 101 | (let [registry-promise (promise)] 102 | (matches-registry? 103 | (with-push-gateway 104 | (export/with-push-gateway 105 | [registry push-spec] 106 | (deliver registry-promise registry))) 107 | @registry-promise)))) 108 | -------------------------------------------------------------------------------- /test/iapetos/metric_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.metric-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.test.check 4 | [generators :as gen] 5 | [properties :as prop] 6 | [clojure-test :refer [defspec]]] 7 | [iapetos.test.generators :as g] 8 | [iapetos.metric :as metric])) 9 | 10 | (defspec t-dasherize 100 11 | (prop/for-all 12 | [s g/metric-string] 13 | (let [result (metric/dasherize s)] 14 | (and (<= (count result) (count s)) 15 | (re-matches #"[a-zA-Z0-9\-]*" result))))) 16 | 17 | (defspec t-sanitize 100 18 | (prop/for-all 19 | [s g/metric-string] 20 | (let [result (metric/sanitize s)] 21 | (and (<= (count result) (count s)) 22 | (re-matches #"[a-zA-Z0-9_]*" result))))) 23 | 24 | (defspec t-sanitize-exception 100 25 | (prop/for-all 26 | [s g/invalid-metric-string] 27 | (= (try 28 | (metric/sanitize s) 29 | (catch AssertionError e 30 | :error)) 31 | :error))) 32 | 33 | (defspec t-metric-name 100 34 | (prop/for-all 35 | [metric g/metric] 36 | (let [{:keys [name namespace]} (metric/metric-name metric)] 37 | (and (re-matches #"[a-zA-Z0-9_]+" name) 38 | (re-matches #"[a-zA-Z0-9_]+" namespace))))) 39 | 40 | (defspec t-metric-as-map 100 41 | (prop/for-all 42 | [metric g/metric 43 | additional-keys (gen/map gen/string-ascii gen/string-ascii)] 44 | (let [{:keys [name namespace] :as r} (metric/as-map metric additional-keys)] 45 | (and (re-matches #"[a-zA-Z0-9_]+" name) 46 | (re-matches #"[a-zA-Z0-9_]+" namespace) 47 | (= additional-keys (dissoc r :name :namespace ::metric/id)) 48 | (= metric (::metric/id r)))))) 49 | -------------------------------------------------------------------------------- /test/iapetos/registry/collectors_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.registry.collectors-test 2 | (:require [clojure.test.check 3 | [generators :as gen] 4 | [properties :as prop] 5 | [clojure-test :refer [defspec]]] 6 | [clojure.test :refer :all] 7 | [iapetos.test.generators :as g] 8 | [iapetos.core :as prometheus] 9 | [iapetos.collector :as collector] 10 | [iapetos.registry.collectors :as c]) 11 | (:import [iapetos.registry IapetosRegistry])) 12 | 13 | (defn- all-collectors 14 | [^IapetosRegistry registry] 15 | (for [[_namespace vs] (.-collectors registry) 16 | [_subsystem vs] vs 17 | [_name collector] vs] 18 | collector)) 19 | 20 | (defn- path-cache 21 | [^IapetosRegistry registry] 22 | (::c/path-cache (meta (.-collectors registry)))) 23 | 24 | (defspec t-registry-should-cache-the-path-of-registered-collectors 50 25 | (prop/for-all 26 | [registry (gen/return (prometheus/collector-registry)) 27 | collector-constructor g/collector-constructor 28 | collector-name g/metric] 29 | (let [collector (collector-constructor collector-name) 30 | registry (prometheus/register registry collector) 31 | cache (path-cache registry)] 32 | (is (every? (fn [{:keys [path cache-key] :as _collector}] 33 | (= path (get cache cache-key))) 34 | (all-collectors registry)))))) 35 | 36 | (defspec t-registry-should-evict-path-from-cache-for-unregistered-collectors 50 37 | (prop/for-all 38 | [registry (gen/return (prometheus/collector-registry)) 39 | collector-constructor g/collector-constructor 40 | collector-name g/metric] 41 | (let [collector (collector-constructor collector-name) 42 | registry (-> registry 43 | (prometheus/register collector) 44 | (prometheus/unregister collector))] 45 | (is (empty? (path-cache registry)))))) 46 | -------------------------------------------------------------------------------- /test/iapetos/registry_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.registry-test 2 | (:require [clojure.test.check 3 | [generators :as gen] 4 | [properties :as prop] 5 | [clojure-test :refer [defspec]]] 6 | [clojure.test :refer :all] 7 | [iapetos.test.generators :as g] 8 | [iapetos.core :as prometheus] 9 | [iapetos.collector :as c] 10 | [iapetos.export :as export])) 11 | 12 | (defspec t-registry-should-return-nil-for-unknown-collectors 50 13 | (prop/for-all 14 | [registry-fn (g/registry-fn) 15 | collector-name g/metric] 16 | (let [registry (registry-fn)] 17 | (nil? (registry collector-name))))) 18 | 19 | (defspec t-registry-should-return-a-registered-collector 50 20 | (prop/for-all 21 | [registry-fn (g/registry-fn) 22 | collector-constructor g/collector-constructor 23 | collector-name g/metric] 24 | (let [collector (collector-constructor collector-name) 25 | registry (-> (registry-fn) 26 | (prometheus/register collector))] 27 | (some? (registry collector-name))))) 28 | 29 | (defspec t-registry-should-return-a-registered-collector-with-explicit-name 50 30 | (prop/for-all 31 | [registry-fn (g/registry-fn) 32 | collector-constructor g/collector-constructor 33 | collector-name g/metric 34 | register-name g/metric] 35 | (let [collector (collector-constructor collector-name) 36 | registry (-> (registry-fn) 37 | (prometheus/register-as register-name collector))] 38 | (some? (registry register-name))))) 39 | 40 | (defspec t-registry-should-return-nil-for-unregistered-collectors 50 41 | (prop/for-all 42 | [registry-fn (g/registry-fn) 43 | collector-constructor g/collector-constructor 44 | collector-name g/metric] 45 | (let [collector (collector-constructor collector-name) 46 | registry (-> (registry-fn) 47 | (prometheus/register collector) 48 | (prometheus/unregister collector-name))] 49 | (nil? (registry collector-name))))) 50 | 51 | (defspec t-registry-should-clear-all-collectors 50 52 | (prop/for-all 53 | [registry-fn (g/registry-fn) 54 | collectors (gen/not-empty g/collectors)] 55 | (let [registry (apply prometheus/register (registry-fn) collectors)] 56 | (and (is (not= "" (export/text-format registry))) 57 | (let [cleared-registry (prometheus/clear registry)] 58 | (is (= "" (export/text-format cleared-registry)))))))) 59 | 60 | (defspec t-subsystem=registry-should-only-clear-own-collectors 50 61 | (prop/for-all 62 | [registry-fn (g/registry-fn) 63 | collectors g/collectors] 64 | (let [[h & rst] collectors 65 | registry (-> (registry-fn) 66 | (cond-> h (prometheus/register h))) 67 | export-before (export/text-format registry) 68 | subregistry (apply prometheus/register 69 | (prometheus/subsystem registry "sub") 70 | (rest collectors)) 71 | export-with-sub (export/text-format registry) 72 | subregistry' (prometheus/clear subregistry) 73 | export-after (export/text-format registry)] 74 | (and (= export-before export-after) 75 | (or (not (seq rst)) 76 | (not= export-with-sub export-before)))))) 77 | -------------------------------------------------------------------------------- /test/iapetos/standalone_test.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.standalone-test 2 | (:require [clojure.test.check 3 | [generators :as gen] 4 | [properties :as prop] 5 | [clojure-test :refer [defspec]]] 6 | [clojure.test :refer :all] 7 | [iapetos.test.generators :as g] 8 | [iapetos.core :as prometheus] 9 | [iapetos.export :as export] 10 | [iapetos.standalone :as standalone] 11 | [aleph.http :as http])) 12 | 13 | (def ^:private path "/prometheus-metrics") 14 | 15 | (defn- fetch 16 | [{:keys [port]} request-method path] 17 | (try 18 | (-> (http/request 19 | {:method request-method 20 | :throw-exceptions? false 21 | :url (str "http://localhost:" port path)}) 22 | (deref) 23 | ((juxt :status (comp slurp :body)))) 24 | (catch Throwable t 25 | (println "exception when querying server:" t) 26 | t))) 27 | 28 | (defspec t-standalone-server 5 29 | (prop/for-all 30 | [registry-fn (g/registry-fn)] 31 | (let [registry (-> (registry-fn) 32 | (prometheus/register 33 | (prometheus/counter :app/runs-total)) 34 | (prometheus/inc :app/runs-total))] 35 | (with-open [server (standalone/metrics-server 36 | registry 37 | {:port 0 38 | :path "/prometheus-metrics"})] 39 | (and (= [200 (export/text-format registry)] 40 | (fetch server :get path)) 41 | (= [405 "Method not allowed: POST"] 42 | (fetch server :post path)) 43 | (= [404 (str "Not found: " path "/_info")] 44 | (fetch server :get (str path "/_info"))) 45 | (= [404 "Not found: /"] 46 | (fetch server :get ""))))))) 47 | -------------------------------------------------------------------------------- /test/iapetos/test/generators.clj: -------------------------------------------------------------------------------- 1 | (ns iapetos.test.generators 2 | (:require [clojure.test.check.generators :as gen] 3 | [iapetos.core :as prometheus] 4 | [iapetos.export :refer [pushable-collector-registry]] 5 | [clojure.string :as string]) 6 | (:import [io.prometheus.client CollectorRegistry])) 7 | 8 | ;; ## Metric 9 | 10 | (def separator 11 | (->> (gen/elements [\- \_ \.]) 12 | (gen/vector) 13 | (gen/not-empty) 14 | (gen/fmap #(apply str %)))) 15 | 16 | (def metric-string 17 | (gen/let [first-char gen/char-alpha 18 | last-char gen/char-alpha-numeric 19 | rest-chars gen/string-alpha-numeric] 20 | (gen/return 21 | (str 22 | (apply str first-char rest-chars) 23 | last-char)))) 24 | 25 | (def invalid-metric-string 26 | (->> (gen/tuple gen/nat (gen/vector separator)) 27 | (gen/fmap #(apply str (first %) (rest %))))) 28 | 29 | (def metric-namespace 30 | metric-string) 31 | 32 | (def metric-keyword 33 | (gen/let [namespace metric-namespace 34 | name metric-string] 35 | (gen/return (keyword namespace name)))) 36 | 37 | (def metric-vector 38 | (gen/tuple metric-namespace metric-string)) 39 | 40 | (def metric-map 41 | (gen/hash-map 42 | :namespace metric-namespace 43 | :name metric-string)) 44 | 45 | (def metric 46 | (gen/one-of 47 | [metric-keyword 48 | metric-map 49 | metric-string 50 | metric-vector])) 51 | 52 | ;; ## Name 53 | 54 | (def valid-name 55 | (gen/let [first-char gen/char-alpha 56 | parts (gen/vector (gen/not-empty gen/string-alpha-numeric))] 57 | (apply str first-char (string/join "_" parts)))) 58 | 59 | ;; ## Registry 60 | 61 | (defn registry-fn 62 | [& initializers] 63 | (gen/let [registry-name valid-name 64 | base-fn (gen/elements 65 | [#(prometheus/collector-registry %) 66 | #(do % (prometheus/collector-registry)) 67 | #(pushable-collector-registry 68 | {:job % 69 | :push-gateway "0:8080"}) 70 | (constantly prometheus/default-registry)])] 71 | (gen/return 72 | (fn [] 73 | (let [registry (base-fn registry-name)] 74 | (.clear ^CollectorRegistry (iapetos.registry/raw registry)) 75 | (reduce 76 | (fn [r f] 77 | (f r)) 78 | registry 79 | initializers)))))) 80 | 81 | ;; ## Collector 82 | 83 | (defn collector 84 | ([collector-fn] 85 | (collector collector-fn (registry-fn))) 86 | ([collector-fn registry-fn-gen] 87 | (gen/let [metric metric 88 | registry-fn registry-fn-gen] 89 | (let [collector (collector-fn metric) 90 | registry (prometheus/register (registry-fn) collector)] 91 | (gen/return (registry metric)))))) 92 | 93 | (def collector-constructor 94 | (gen/elements 95 | [prometheus/counter 96 | prometheus/gauge 97 | prometheus/histogram 98 | prometheus/summary])) 99 | 100 | (def collectors 101 | (gen/let [metrics (gen/vector-distinct metric)] 102 | (->> (gen/vector collector-constructor (count metrics)) 103 | (gen/fmap 104 | (fn [fs] 105 | (mapv #(%1 %2) fs metrics)))))) 106 | --------------------------------------------------------------------------------