├── .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 | [](https://clojars.org/clj-commons/iapetos)
9 | [](https://cljdoc.org/d/clj-commons/iapetos/CURRENT)
10 | [](https://circleci.com/gh/clj-commons/iapetos)
11 | [](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 |
--------------------------------------------------------------------------------