fast-prometheus 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/AGENTS.md +38 -0
- data/CHANGELOG.md +59 -0
- data/README.md +109 -0
- data/lib/fast/prometheus/counter.rb +22 -0
- data/lib/fast/prometheus/errors.rb +11 -0
- data/lib/fast/prometheus/exposition.rb +46 -0
- data/lib/fast/prometheus/formats/metrics_pb.rb +33 -0
- data/lib/fast/prometheus/formats/protobuf.rb +154 -0
- data/lib/fast/prometheus/formats/text.rb +90 -0
- data/lib/fast/prometheus/gauge.rb +37 -0
- data/lib/fast/prometheus/histogram.rb +112 -0
- data/lib/fast/prometheus/metric.rb +144 -0
- data/lib/fast/prometheus/middleware/exporter.rb +39 -0
- data/lib/fast/prometheus/middleware/instrumentation.rb +34 -0
- data/lib/fast/prometheus/native_histogram.rb +145 -0
- data/lib/fast/prometheus/otlp/grpc_exporter.rb +39 -0
- data/lib/fast/prometheus/otlp/http_exporter.rb +43 -0
- data/lib/fast/prometheus/otlp/mapper.rb +207 -0
- data/lib/fast/prometheus/otlp/pb/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.rb +27 -0
- data/lib/fast/prometheus/otlp/pb/opentelemetry/proto/common/v1/common_pb.rb +26 -0
- data/lib/fast/prometheus/otlp/pb/opentelemetry/proto/metrics/v1/metrics_pb.rb +41 -0
- data/lib/fast/prometheus/otlp/pb/opentelemetry/proto/resource/v1/resource_pb.rb +23 -0
- data/lib/fast/prometheus/otlp/push.rb +39 -0
- data/lib/fast/prometheus/otlp/service_interface.rb +18 -0
- data/lib/fast/prometheus/rack/exporter.rb +32 -0
- data/lib/fast/prometheus/rack/instrumentation.rb +33 -0
- data/lib/fast/prometheus/registry.rb +111 -0
- data/lib/fast/prometheus/request_metrics.rb +45 -0
- data/lib/fast/prometheus/snapshot.rb +78 -0
- data/lib/fast/prometheus/store.rb +37 -0
- data/lib/fast/prometheus/summary.rb +50 -0
- data/lib/fast/prometheus/version.rb +7 -0
- data/lib/fast/prometheus.rb +17 -0
- metadata +158 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bf50aa0854cf98fd362294bd64ac2f07d04526428b6aa595c50c1058ffc82664
|
|
4
|
+
data.tar.gz: 66b21fc919d3913bdda5ae6b1aff7b661eb850981a43f54f5928d67aa0dbf16e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: cb355b90d7dd5dcc885ff0181d87611ef6c39120f613053ed9a95e15988cf04b33959c3289bec698e8c664720a85a567729c8547ad53c427aa88214ba0143868
|
|
7
|
+
data.tar.gz: 379b37ebbccda56a8c3c7975351f709e138c4928be30ff3a8ccdac991164285db35fe1ce0758630506e36bb68199a311273de47d477b9578dd643ab2a8bc0da7
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Standing context for agents working in this repository.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bundle install
|
|
9
|
+
bundle exec sus
|
|
10
|
+
bundle exec rubocop
|
|
11
|
+
bundle exec ruby script/e2e_prometheus_scrape.rb
|
|
12
|
+
BENCH_QUICK=1 bundle exec ruby benchmark/observe.rb
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Style Rules
|
|
16
|
+
|
|
17
|
+
- 2-space indentation
|
|
18
|
+
- `# frozen_string_literal: true` on every source file
|
|
19
|
+
- Double-quoted strings (`"..."`)
|
|
20
|
+
- Fibers remain the IO concurrency model. `Thread::Mutex`/`Monitor` are permitted only where shared mutable state is guarded — metric stores and the registry. `Thread` is permitted only in tests and scripts that verify cross-thread behavior.
|
|
21
|
+
|
|
22
|
+
## Test Rules
|
|
23
|
+
|
|
24
|
+
- Use `sus` as the test framework
|
|
25
|
+
- Tests are terse, exercising public interfaces only
|
|
26
|
+
- No mock/stub of the class under test
|
|
27
|
+
|
|
28
|
+
## Local Reference Sources
|
|
29
|
+
|
|
30
|
+
- `~/architect/src/github.com/socketry/async`
|
|
31
|
+
- `~/architect/src/github.com/socketry/async-http`
|
|
32
|
+
- `~/architect/src/github.com/socketry/async-grpc`
|
|
33
|
+
- `~/architect/src/github.com/socketry/protocol-grpc`
|
|
34
|
+
- `~/architect/src/github.com/socketry/protocol-http`
|
|
35
|
+
- `~/architect/src/github.com/socketry/sus`
|
|
36
|
+
- `~/architect/src/github.com/socketry/sus-fixtures-async`
|
|
37
|
+
- `/Users/eric/architect/spaces/20260715-fast-prometheus-client/repos/client_ruby`
|
|
38
|
+
- `/Users/eric/architect/spaces/20260715-fast-prometheus-client/repos/prometheus`
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-09-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `Registry#fetch_or_register(name, &block)` — an atomic fetch-or-register:
|
|
15
|
+
returns the metric already registered under `name`, else registers and
|
|
16
|
+
returns the block's metric, in one lock hold.
|
|
17
|
+
- `Metric#synchronize` — runs a block atomically with respect to every other
|
|
18
|
+
mutation or read of that metric's store, including its `with_labels`-bound
|
|
19
|
+
children.
|
|
20
|
+
- `Fast::Prometheus::Rack::Exporter` and `Fast::Prometheus::Rack::Instrumentation` —
|
|
21
|
+
plain Rack middleware for `config.ru`, Rails, Puma and any other Rack host,
|
|
22
|
+
built on the same content-negotiation and RED-recording core as the
|
|
23
|
+
Falcon-native `Middleware::Exporter`/`Middleware::Instrumentation`.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `Instrumentation` middleware: concurrent construction of the middleware
|
|
28
|
+
against one registry could race two `get`/register check-then-act calls
|
|
29
|
+
and raise `DuplicateMetric`; it now registers its counter and histogram
|
|
30
|
+
through `Registry#fetch_or_register`.
|
|
31
|
+
- `Instrumentation` middleware: when the delegate raised and recording the
|
|
32
|
+
failed request also raised, the recording exception replaced the
|
|
33
|
+
delegate's; the delegate's exception now always propagates.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Minimum Ruby lowered from 3.4 to 3.3 (`required_ruby_version >= 3.3`); the
|
|
38
|
+
test suite, RuboCop, the integration harness and the thread-safety stress
|
|
39
|
+
harness all pass on 3.3.12 and 4.0.5, and CI now runs the suite on both.
|
|
40
|
+
- `Registry` and every metric type are now safe to share across OS threads by
|
|
41
|
+
default: each metric's per-series store is guarded by its own lock (shared
|
|
42
|
+
by `with_labels`-bound metrics), snapshot construction observes every
|
|
43
|
+
series at one consistent point, and registry operations are serialized on
|
|
44
|
+
a registry-owned lock. This retires the zero-lock hot-path invariant —
|
|
45
|
+
cross-thread use is now in contract, not out of it.
|
|
46
|
+
- Documentation reorganized: `README.md` is now a front door, and the
|
|
47
|
+
tutorials, how-to guides, reference, and explanation live under `docs/`
|
|
48
|
+
per the Diátaxis framework.
|
|
49
|
+
- `Formats::Text` and `Formats::Protobuf` expose only `.render`; the
|
|
50
|
+
rendering helpers they used to leak as public singleton methods are private.
|
|
51
|
+
|
|
52
|
+
## [0.1.0.pre.1] - 2026-07-19
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- Fiber-native Prometheus client built on `async`.
|
|
57
|
+
- Native histograms via protobuf scrape.
|
|
58
|
+
- OTLP export over gRPC and HTTP.
|
|
59
|
+
- `Fast::Prometheus` namespace.
|
data/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# fast-prometheus
|
|
2
|
+
|
|
3
|
+
A fiber-native Prometheus client for modern Ruby, built on the socketry/async
|
|
4
|
+
ecosystem: HTTP middleware for `Protocol::HTTP` (Falcon-native) and Rack, safe
|
|
5
|
+
to share one `Registry` across OS threads and fibers by default, native
|
|
6
|
+
histograms as a first-class metric type, protobuf scrape exposition, and OTLP
|
|
7
|
+
export over gRPC (async-grpc) and HTTP.
|
|
8
|
+
|
|
9
|
+
Not a fork of `prometheus/client_ruby` — a new gem that uses it as the
|
|
10
|
+
reference for supported surface.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
gem install fast-prometheus
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or add to your `Gemfile`:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem "fast-prometheus"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`require "fast/prometheus"` loads only the core, with no IO dependencies. The
|
|
25
|
+
opt-in surfaces and what each pulls in are listed in
|
|
26
|
+
[the require-path reference](docs/reference/require-paths.md).
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
require "fast/prometheus"
|
|
32
|
+
require "fast/prometheus/formats/text"
|
|
33
|
+
|
|
34
|
+
registry = Fast::Prometheus::Registry.new
|
|
35
|
+
|
|
36
|
+
requests = registry.counter(:http_requests_total, docstring: "Total HTTP requests", labels: %i[method path])
|
|
37
|
+
duration = registry.histogram(:request_duration_seconds, docstring: "Request duration", labels: [:method])
|
|
38
|
+
|
|
39
|
+
requests.increment(labels: { method: "GET", path: "/" })
|
|
40
|
+
requests.increment(by: 5, labels: { method: "POST", path: "/api" })
|
|
41
|
+
duration.observe(0.042, labels: { method: "GET" })
|
|
42
|
+
duration.observe(0.018, labels: { method: "GET" })
|
|
43
|
+
|
|
44
|
+
puts Fast::Prometheus::Formats::Text.render(registry.collect)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Run with `bundle exec ruby -I lib` from the repo root. For running this
|
|
48
|
+
under Falcon and scraping it with a real Prometheus, see
|
|
49
|
+
[the Falcon tutorial](docs/tutorials/falcon-app.md).
|
|
50
|
+
|
|
51
|
+
## Documentation
|
|
52
|
+
|
|
53
|
+
### Tutorials
|
|
54
|
+
|
|
55
|
+
- [Tutorial: instrument a Falcon app](docs/tutorials/falcon-app.md) — build a Falcon app instrumented with fast-prometheus and scrape it with Prometheus.
|
|
56
|
+
|
|
57
|
+
### How-to guides
|
|
58
|
+
|
|
59
|
+
- [How to instrument a Rack app](docs/how-to/instrument-a-rack-app.md) — add the two Rack middleware to a `config.ru`, Rails app or Puma host.
|
|
60
|
+
- [How to instrument a falcon.rb service](docs/how-to/instrument-a-falcon-service.md) — run the Falcon-native middleware under `falcon host` or a threaded launcher.
|
|
61
|
+
- [How to serve metrics on a separate port](docs/how-to/serve-metrics-on-a-separate-port.md) — run `/metrics` on its own `Async::HTTP::Server`, off the app's port.
|
|
62
|
+
- [How to scrape native histograms](docs/how-to/scrape-native-histograms.md) — configure Prometheus to negotiate protobuf so native histograms aren't dropped.
|
|
63
|
+
- [How to share one registry across boot paths](docs/how-to/share-a-registry.md) — use `fetch_or_register` so multiple boot paths can construct the same metric safely.
|
|
64
|
+
- [How to export over OTLP](docs/how-to/export-otlp.md) — push metrics to an OTLP collector over gRPC or HTTP.
|
|
65
|
+
- [How to verify a release build](docs/how-to/verify-a-release.md) — run the integration harness against a packaged gem before tagging.
|
|
66
|
+
|
|
67
|
+
### Reference
|
|
68
|
+
|
|
69
|
+
- [Reference: registry and metrics](docs/reference/metrics.md) — every public class and method on `Registry` and the five metric types.
|
|
70
|
+
- [Reference: exposition formats and HTTP middleware](docs/reference/exposition.md) — `Formats::Text`, `Formats::Protobuf`, `Middleware::Exporter`, `Middleware::Instrumentation`.
|
|
71
|
+
- [Reference: OTLP export](docs/reference/otlp.md) — `OTLP::HTTPExporter`, `OTLP::GRPCExporter`, `OTLP::Push`, `OTLP::Mapper`.
|
|
72
|
+
- [Reference: require paths and dependencies](docs/reference/require-paths.md) — what each require path loads and what it pulls in.
|
|
73
|
+
|
|
74
|
+
### Explanation
|
|
75
|
+
|
|
76
|
+
- [Concurrency model](docs/explanation/concurrency.md) — the locking contract and what it guarantees under threads and fibers.
|
|
77
|
+
- [Native histograms](docs/explanation/native-histograms.md) — what a native histogram is and why it's protobuf-only.
|
|
78
|
+
- [Design: why a new gem](docs/explanation/design.md) — why fast-prometheus exists instead of a `client_ruby` fork.
|
|
79
|
+
- [Benchmarks](docs/explanation/benchmarks.md) — the full benchmark-ips run and how to reproduce it.
|
|
80
|
+
|
|
81
|
+
## Concurrency
|
|
82
|
+
|
|
83
|
+
A `Registry` is safe to share across every OS thread of a process, and
|
|
84
|
+
remains safe across fibers within a thread — correct under Falcon
|
|
85
|
+
`--threaded --count N`, where the N OS threads share one module-level
|
|
86
|
+
`Fast::Prometheus.registry`. Falcon `--forked` mode gives each process its
|
|
87
|
+
own registry, so a single scrape only sees the metrics of the process that
|
|
88
|
+
handled it; aggregating across processes is out of scope. See
|
|
89
|
+
[the concurrency model](docs/explanation/concurrency.md) for the full guarantee.
|
|
90
|
+
|
|
91
|
+
## Performance
|
|
92
|
+
|
|
93
|
+
Single process, locked, thread-safe by default (`benchmark-ips` comparisons
|
|
94
|
+
against `prometheus-client`; see [the benchmarks page](docs/explanation/benchmarks.md) for the
|
|
95
|
+
full run and conditions):
|
|
96
|
+
|
|
97
|
+
- Bound counter (fast) is on par with prometheus-client's bound counter (1.04x)
|
|
98
|
+
- Classic histogram observe is **3.0x** faster (1.71M vs 566K i/s)
|
|
99
|
+
- Native histogram observe runs at **1.35M i/s** with no prometheus-client equivalent
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
bundle exec sus
|
|
105
|
+
bundle exec rubocop
|
|
106
|
+
E2E_REQUIRED=1 ./integration/run
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
See [how to verify a release build](docs/how-to/verify-a-release.md) for the pre-release rule.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "metric"
|
|
4
|
+
|
|
5
|
+
module Fast
|
|
6
|
+
module Prometheus
|
|
7
|
+
# Counter — a monotonically increasing metric.
|
|
8
|
+
class Counter < Metric
|
|
9
|
+
def type
|
|
10
|
+
:counter
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def increment(by: 1, labels: {})
|
|
14
|
+
raise ArgumentError, "by must be a non-negative numeric" unless by.is_a?(Numeric) && by >= 0
|
|
15
|
+
return if by.zero?
|
|
16
|
+
|
|
17
|
+
key = resolve(labels)
|
|
18
|
+
store.synchronize { store[key] = (store[key] || 0.0) + by.to_f }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fast
|
|
4
|
+
module Prometheus
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
class InvalidMetricName < Error; end
|
|
7
|
+
class InvalidLabelName < Error; end
|
|
8
|
+
class InvalidLabelSet < Error; end
|
|
9
|
+
class DuplicateMetric < Error; end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
require "fast/prometheus"
|
|
6
|
+
require "fast/prometheus/formats/text"
|
|
7
|
+
require "fast/prometheus/formats/protobuf"
|
|
8
|
+
|
|
9
|
+
module Fast
|
|
10
|
+
module Prometheus
|
|
11
|
+
# Renders a registry snapshot into an exposition body and response headers,
|
|
12
|
+
# negotiating format (text vs protobuf) and gzip from Accept / Accept-Encoding
|
|
13
|
+
# header values. Shared by Middleware::Exporter and Rack::Exporter so
|
|
14
|
+
# negotiation and compression exist exactly once.
|
|
15
|
+
module Exposition
|
|
16
|
+
PROTOBUF_ACCEPT = "application/vnd.google.protobuf"
|
|
17
|
+
|
|
18
|
+
FORMAT_MAP = {
|
|
19
|
+
true => [Formats::Protobuf, Formats::Protobuf::CONTENT_TYPE],
|
|
20
|
+
false => [Formats::Text, Formats::Text::CONTENT_TYPE]
|
|
21
|
+
}.freeze
|
|
22
|
+
private_constant :FORMAT_MAP
|
|
23
|
+
|
|
24
|
+
def self.render(registry, accept:, accept_encoding:)
|
|
25
|
+
format, content_type = FORMAT_MAP[protobuf?(accept)]
|
|
26
|
+
body = format.render(registry.collect)
|
|
27
|
+
headers = { "content-type" => content_type }
|
|
28
|
+
|
|
29
|
+
if gzip?(accept_encoding)
|
|
30
|
+
body = Zlib.gzip(body)
|
|
31
|
+
headers["content-encoding"] = "gzip"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
[body, headers]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private_class_method def self.protobuf?(accept)
|
|
38
|
+
accept.to_s.include?(PROTOBUF_ACCEPT)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private_class_method def self.gzip?(accept_encoding)
|
|
42
|
+
accept_encoding.to_s.include?("gzip")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: metrics.proto
|
|
4
|
+
|
|
5
|
+
require 'google/protobuf'
|
|
6
|
+
|
|
7
|
+
require 'google/protobuf/timestamp_pb'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
descriptor_data = "\n\rmetrics.proto\x12\x14io.prometheus.client\x1a\x1fgoogle/protobuf/timestamp.proto\"(\n\tLabelPair\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\x16\n\x05Gauge\x12\r\n\x05value\x18\x01 \x01(\x01\"\x7f\n\x07\x43ounter\x12\r\n\x05value\x18\x01 \x01(\x01\x12\x30\n\x08\x65xemplar\x18\x02 \x01(\x0b\x32\x1e.io.prometheus.client.Exemplar\x12\x33\n\x0fstart_timestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"+\n\x08Quantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01\"\x9a\x01\n\x07Summary\x12\x14\n\x0csample_count\x18\x01 \x01(\x04\x12\x12\n\nsample_sum\x18\x02 \x01(\x01\x12\x30\n\x08quantile\x18\x03 \x03(\x0b\x32\x1e.io.prometheus.client.Quantile\x12\x33\n\x0fstart_timestamp\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\x18\n\x07Untyped\x12\r\n\x05value\x18\x01 \x01(\x01\"\x8f\x04\n\tHistogram\x12\x14\n\x0csample_count\x18\x01 \x01(\x04\x12\x1a\n\x12sample_count_float\x18\x04 \x01(\x01\x12\x12\n\nsample_sum\x18\x02 \x01(\x01\x12,\n\x06\x62ucket\x18\x03 \x03(\x0b\x32\x1c.io.prometheus.client.Bucket\x12\x33\n\x0fstart_timestamp\x18\x0f \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06schema\x18\x05 \x01(\x11\x12\x16\n\x0ezero_threshold\x18\x06 \x01(\x01\x12\x12\n\nzero_count\x18\x07 \x01(\x04\x12\x18\n\x10zero_count_float\x18\x08 \x01(\x01\x12\x37\n\rnegative_span\x18\t \x03(\x0b\x32 .io.prometheus.client.BucketSpan\x12\x16\n\x0enegative_delta\x18\n \x03(\x12\x12\x16\n\x0enegative_count\x18\x0b \x03(\x01\x12\x37\n\rpositive_span\x18\x0c \x03(\x0b\x32 .io.prometheus.client.BucketSpan\x12\x16\n\x0epositive_delta\x18\r \x03(\x12\x12\x16\n\x0epositive_count\x18\x0e \x03(\x01\x12\x31\n\texemplars\x18\x10 \x03(\x0b\x32\x1e.io.prometheus.client.Exemplar\"\x89\x01\n\x06\x42ucket\x12\x18\n\x10\x63umulative_count\x18\x01 \x01(\x04\x12\x1e\n\x16\x63umulative_count_float\x18\x04 \x01(\x01\x12\x13\n\x0bupper_bound\x18\x02 \x01(\x01\x12\x30\n\x08\x65xemplar\x18\x03 \x01(\x0b\x32\x1e.io.prometheus.client.Exemplar\",\n\nBucketSpan\x12\x0e\n\x06offset\x18\x01 \x01(\x11\x12\x0e\n\x06length\x18\x02 \x01(\r\"x\n\x08\x45xemplar\x12.\n\x05label\x18\x01 \x03(\x0b\x32\x1f.io.prometheus.client.LabelPair\x12\r\n\x05value\x18\x02 \x01(\x01\x12-\n\ttimestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xbe\x02\n\x06Metric\x12.\n\x05label\x18\x01 \x03(\x0b\x32\x1f.io.prometheus.client.LabelPair\x12*\n\x05gauge\x18\x02 \x01(\x0b\x32\x1b.io.prometheus.client.Gauge\x12.\n\x07\x63ounter\x18\x03 \x01(\x0b\x32\x1d.io.prometheus.client.Counter\x12.\n\x07summary\x18\x04 \x01(\x0b\x32\x1d.io.prometheus.client.Summary\x12.\n\x07untyped\x18\x05 \x01(\x0b\x32\x1d.io.prometheus.client.Untyped\x12\x32\n\thistogram\x18\x07 \x01(\x0b\x32\x1f.io.prometheus.client.Histogram\x12\x14\n\x0ctimestamp_ms\x18\x06 \x01(\x03\"\x96\x01\n\x0cMetricFamily\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04help\x18\x02 \x01(\t\x12.\n\x04type\x18\x03 \x01(\x0e\x32 .io.prometheus.client.MetricType\x12,\n\x06metric\x18\x04 \x03(\x0b\x32\x1c.io.prometheus.client.Metric\x12\x0c\n\x04unit\x18\x05 \x01(\t*b\n\nMetricType\x12\x0b\n\x07\x43OUNTER\x10\x00\x12\t\n\x05GAUGE\x10\x01\x12\x0b\n\x07SUMMARY\x10\x02\x12\x0b\n\x07UNTYPED\x10\x03\x12\r\n\tHISTOGRAM\x10\x04\x12\x13\n\x0fGAUGE_HISTOGRAM\x10\x05\x42\x16Z\x14io_prometheus_clientb\x06proto3"
|
|
11
|
+
|
|
12
|
+
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
13
|
+
pool.add_serialized_file(descriptor_data)
|
|
14
|
+
|
|
15
|
+
module Io
|
|
16
|
+
module Prometheus
|
|
17
|
+
module Client
|
|
18
|
+
LabelPair = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("io.prometheus.client.LabelPair").msgclass
|
|
19
|
+
Gauge = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("io.prometheus.client.Gauge").msgclass
|
|
20
|
+
Counter = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("io.prometheus.client.Counter").msgclass
|
|
21
|
+
Quantile = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("io.prometheus.client.Quantile").msgclass
|
|
22
|
+
Summary = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("io.prometheus.client.Summary").msgclass
|
|
23
|
+
Untyped = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("io.prometheus.client.Untyped").msgclass
|
|
24
|
+
Histogram = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("io.prometheus.client.Histogram").msgclass
|
|
25
|
+
Bucket = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("io.prometheus.client.Bucket").msgclass
|
|
26
|
+
BucketSpan = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("io.prometheus.client.BucketSpan").msgclass
|
|
27
|
+
Exemplar = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("io.prometheus.client.Exemplar").msgclass
|
|
28
|
+
Metric = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("io.prometheus.client.Metric").msgclass
|
|
29
|
+
MetricFamily = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("io.prometheus.client.MetricFamily").msgclass
|
|
30
|
+
MetricType = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("io.prometheus.client.MetricType").enummodule
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fast/prometheus"
|
|
4
|
+
require_relative "metrics_pb"
|
|
5
|
+
|
|
6
|
+
module Fast
|
|
7
|
+
module Prometheus
|
|
8
|
+
module Formats
|
|
9
|
+
module Protobuf
|
|
10
|
+
CONTENT_TYPE = "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited"
|
|
11
|
+
|
|
12
|
+
TYPE_MAP = {
|
|
13
|
+
counter: Io::Prometheus::Client::MetricType::COUNTER,
|
|
14
|
+
gauge: Io::Prometheus::Client::MetricType::GAUGE,
|
|
15
|
+
summary: Io::Prometheus::Client::MetricType::SUMMARY,
|
|
16
|
+
histogram: Io::Prometheus::Client::MetricType::HISTOGRAM,
|
|
17
|
+
native_histogram: Io::Prometheus::Client::MetricType::HISTOGRAM
|
|
18
|
+
}.freeze
|
|
19
|
+
private_constant :TYPE_MAP
|
|
20
|
+
|
|
21
|
+
def self.render(snapshot)
|
|
22
|
+
snapshot.metrics.each_with_object(String.new) do |ms, buffer|
|
|
23
|
+
buffer << encode_frame(build_metric_family(ms))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private_class_method def self.build_metric_family(metric_snapshot)
|
|
28
|
+
Io::Prometheus::Client::MetricFamily.new(
|
|
29
|
+
name: metric_snapshot.name.to_s,
|
|
30
|
+
help: metric_snapshot.docstring,
|
|
31
|
+
type: TYPE_MAP.fetch(metric_snapshot.type),
|
|
32
|
+
metric: metric_snapshot.series.map { |s| build_metric(s, metric_snapshot.type) }
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private_class_method def self.build_metric(series, type)
|
|
37
|
+
labels = series.labels.map do |name, value|
|
|
38
|
+
Io::Prometheus::Client::LabelPair.new(name: name.to_s, value: value)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
case type
|
|
42
|
+
when :counter
|
|
43
|
+
Io::Prometheus::Client::Metric.new(
|
|
44
|
+
label: labels,
|
|
45
|
+
counter: Io::Prometheus::Client::Counter.new(value: series.value)
|
|
46
|
+
)
|
|
47
|
+
when :gauge
|
|
48
|
+
Io::Prometheus::Client::Metric.new(
|
|
49
|
+
label: labels,
|
|
50
|
+
gauge: Io::Prometheus::Client::Gauge.new(value: series.value)
|
|
51
|
+
)
|
|
52
|
+
when :summary
|
|
53
|
+
Io::Prometheus::Client::Metric.new(
|
|
54
|
+
label: labels,
|
|
55
|
+
summary: Io::Prometheus::Client::Summary.new(
|
|
56
|
+
sample_count: series.value.count,
|
|
57
|
+
sample_sum: series.value.sum
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
when :histogram
|
|
61
|
+
buckets = series.value.cumulative_buckets.map do |bound, count|
|
|
62
|
+
Io::Prometheus::Client::Bucket.new(
|
|
63
|
+
upper_bound: bound,
|
|
64
|
+
cumulative_count: count
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
Io::Prometheus::Client::Metric.new(
|
|
68
|
+
label: labels,
|
|
69
|
+
histogram: Io::Prometheus::Client::Histogram.new(
|
|
70
|
+
sample_count: series.value.count,
|
|
71
|
+
sample_sum: series.value.sum,
|
|
72
|
+
bucket: buckets
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
when :native_histogram
|
|
76
|
+
nv = series.value
|
|
77
|
+
pos_spans, pos_deltas = build_spans_deltas(nv.positive_buckets)
|
|
78
|
+
neg_spans, neg_deltas = build_spans_deltas(nv.negative_buckets)
|
|
79
|
+
|
|
80
|
+
Io::Prometheus::Client::Metric.new(
|
|
81
|
+
label: labels,
|
|
82
|
+
histogram: Io::Prometheus::Client::Histogram.new(
|
|
83
|
+
sample_count: nv.count,
|
|
84
|
+
sample_sum: nv.sum,
|
|
85
|
+
schema: nv.schema,
|
|
86
|
+
zero_threshold: nv.zero_threshold,
|
|
87
|
+
zero_count: nv.zero_count,
|
|
88
|
+
positive_span: pos_spans,
|
|
89
|
+
positive_delta: pos_deltas,
|
|
90
|
+
negative_span: neg_spans,
|
|
91
|
+
negative_delta: neg_deltas
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Build BucketSpan and sint64 delta arrays from ascending [index, count] pairs.
|
|
98
|
+
# Worked example: [[1, 3], [2, 1], [5, 4]] ->
|
|
99
|
+
# spans: [{offset: 1, length: 2}, {offset: 2, length: 1}]
|
|
100
|
+
# deltas: [3, -2, 3]
|
|
101
|
+
private_class_method def self.build_spans_deltas(buckets)
|
|
102
|
+
return [], [] if buckets.empty?
|
|
103
|
+
|
|
104
|
+
spans = []
|
|
105
|
+
deltas = []
|
|
106
|
+
span_offset = nil
|
|
107
|
+
span_length = 0
|
|
108
|
+
prev_index = nil
|
|
109
|
+
prev_count = nil
|
|
110
|
+
|
|
111
|
+
buckets.each do |index, count|
|
|
112
|
+
if prev_index.nil?
|
|
113
|
+
span_offset = index
|
|
114
|
+
span_length = 1
|
|
115
|
+
deltas << count
|
|
116
|
+
else
|
|
117
|
+
gap = index - prev_index
|
|
118
|
+
if gap == 1
|
|
119
|
+
span_length += 1
|
|
120
|
+
else
|
|
121
|
+
spans << Io::Prometheus::Client::BucketSpan.new(offset: span_offset, length: span_length)
|
|
122
|
+
span_offset = gap - 1
|
|
123
|
+
span_length = 1
|
|
124
|
+
end
|
|
125
|
+
deltas << count - prev_count
|
|
126
|
+
end
|
|
127
|
+
prev_index = index
|
|
128
|
+
prev_count = count
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
spans << Io::Prometheus::Client::BucketSpan.new(offset: span_offset, length: span_length)
|
|
132
|
+
[spans, deltas]
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private_class_method def self.encode_frame(message)
|
|
136
|
+
encoded = message.class.encode(message)
|
|
137
|
+
encode_varint(encoded.bytesize) << encoded
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private_class_method def self.encode_varint(value)
|
|
141
|
+
buffer = String.new
|
|
142
|
+
loop do
|
|
143
|
+
byte = value & 0x7f
|
|
144
|
+
value >>= 7
|
|
145
|
+
byte |= 0x80 if value.positive?
|
|
146
|
+
buffer << byte
|
|
147
|
+
break if value.zero?
|
|
148
|
+
end
|
|
149
|
+
buffer
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fast/prometheus"
|
|
4
|
+
|
|
5
|
+
module Fast
|
|
6
|
+
module Prometheus
|
|
7
|
+
module Formats
|
|
8
|
+
# Prometheus text exposition format 0.0.4.
|
|
9
|
+
# Consumes Snapshot value objects; never reads live metrics.
|
|
10
|
+
module Text
|
|
11
|
+
CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8"
|
|
12
|
+
|
|
13
|
+
DOC_ESCAPE = /[\n\\]/
|
|
14
|
+
DOC_REPLACE = { "\n" => "\\n", "\\" => "\\\\" }.freeze
|
|
15
|
+
|
|
16
|
+
LABEL_ESCAPE = /[\n\\"]/
|
|
17
|
+
LABEL_REPLACE = { "\\" => "\\\\", '"' => '\\"', "\n" => "\\n" }.freeze
|
|
18
|
+
|
|
19
|
+
def self.render(snapshot)
|
|
20
|
+
snapshot.metrics.each_with_object(String.new) do |metric, output|
|
|
21
|
+
next if metric.type == :native_histogram
|
|
22
|
+
|
|
23
|
+
output << "# HELP #{metric.name} #{escape_doc(metric.docstring)}\n"
|
|
24
|
+
output << "# TYPE #{metric.name} #{metric.type}\n"
|
|
25
|
+
|
|
26
|
+
metric.series.each do |series|
|
|
27
|
+
render_series(output, metric.type, metric.name, series)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private_class_method def self.render_series(output, type, name, series)
|
|
33
|
+
case type
|
|
34
|
+
when :counter, :gauge
|
|
35
|
+
metric_line(output, name, series.labels, series.value)
|
|
36
|
+
when :histogram
|
|
37
|
+
histogram_lines(output, name, series.labels, series.value)
|
|
38
|
+
when :summary
|
|
39
|
+
summary_lines(output, name, series.labels, series.value)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private_class_method def self.escape_doc(string)
|
|
44
|
+
return string unless DOC_ESCAPE.match?(string)
|
|
45
|
+
|
|
46
|
+
string.gsub(DOC_ESCAPE, DOC_REPLACE)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private_class_method def self.escape_label(string)
|
|
50
|
+
string = string.to_s
|
|
51
|
+
return string unless LABEL_ESCAPE.match?(string)
|
|
52
|
+
|
|
53
|
+
string.gsub(LABEL_ESCAPE, LABEL_REPLACE)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private_class_method def self.format_labels(labels)
|
|
57
|
+
return "" if labels.empty?
|
|
58
|
+
|
|
59
|
+
output = String.new("{")
|
|
60
|
+
labels.each do |key, value|
|
|
61
|
+
output << "#{key}=\"#{escape_label(value)}\""
|
|
62
|
+
output << ","
|
|
63
|
+
end
|
|
64
|
+
output.chop!
|
|
65
|
+
output << "}"
|
|
66
|
+
output
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private_class_method def self.metric_line(output, name, labels, value)
|
|
70
|
+
output << "#{name}#{format_labels(labels)} #{value}\n"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private_class_method def self.histogram_lines(output, name, labels, value)
|
|
74
|
+
value.cumulative_buckets.each do |boundary, count|
|
|
75
|
+
le = boundary.infinite? ? "+Inf" : boundary.to_s
|
|
76
|
+
metric_line(output, "#{name}_bucket", labels.merge("le" => le), count)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
metric_line(output, "#{name}_sum", labels, value.sum)
|
|
80
|
+
metric_line(output, "#{name}_count", labels, value.count)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private_class_method def self.summary_lines(output, name, labels, value)
|
|
84
|
+
metric_line(output, "#{name}_sum", labels, value.sum)
|
|
85
|
+
metric_line(output, "#{name}_count", labels, value.count)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "metric"
|
|
4
|
+
|
|
5
|
+
module Fast
|
|
6
|
+
module Prometheus
|
|
7
|
+
# Gauge — an instantaneous value that can go up or down.
|
|
8
|
+
class Gauge < Metric
|
|
9
|
+
def type
|
|
10
|
+
:gauge
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def set(value, labels: {})
|
|
14
|
+
raise ArgumentError, "value must be a numeric" unless value.is_a?(Numeric)
|
|
15
|
+
|
|
16
|
+
key = resolve(labels)
|
|
17
|
+
store.synchronize { store[key] = value.to_f }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def increment(by: 1, labels: {})
|
|
21
|
+
raise ArgumentError, "by must be a numeric" unless by.is_a?(Numeric)
|
|
22
|
+
return if by.zero?
|
|
23
|
+
|
|
24
|
+
key = resolve(labels)
|
|
25
|
+
store.synchronize { store[key] = (store[key] || 0.0) + by.to_f }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def decrement(by: 1, labels: {})
|
|
29
|
+
raise ArgumentError, "by must be a numeric" unless by.is_a?(Numeric)
|
|
30
|
+
return if by.zero?
|
|
31
|
+
|
|
32
|
+
key = resolve(labels)
|
|
33
|
+
store.synchronize { store[key] = (store[key] || 0.0) - by.to_f }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|