promproto 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/LICENSE.md +21 -0
- data/README.md +50 -0
- data/exe/promproto +52 -0
- data/lib/promproto/cli.rb +305 -0
- data/lib/promproto/metrics.pb +0 -0
- data/lib/promproto/metrics_pb.rb +40 -0
- data/lib/promproto/version.rb +5 -0
- data/lib/promproto.rb +26 -0
- metadata +67 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 871334ffa068b6468e54037c4f9ec27afb7d0d58f876250664a5448a33024317
|
|
4
|
+
data.tar.gz: e09172bef789aaf9e87badebfde3b5e0ec807d137642c9029f01fd47a2c19087
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 64ec3528751a00e64f98540acd3847d750bd3566abe8b084ee6b9e9378e77e75d07639419202ca0feba596ee21da1cd8ac8689daddfb9a190c229f3fba4d94fc
|
|
7
|
+
data.tar.gz: 589923e4bafcac52eea6536a1d17fd510701195cb6df5e1b78326d787851265610dd64249191eceee14d222be5ecbb38bd569d8cdc028894c798172c484443a4
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Lewis Buckley
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Promproto
|
|
2
|
+
|
|
3
|
+
A command-line tool to fetch and display Prometheus metrics in protobuf format.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gem install promproto
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Fetch metrics from a target
|
|
15
|
+
promproto localhost:9394
|
|
16
|
+
|
|
17
|
+
# Watch mode - continuously refresh
|
|
18
|
+
promproto -w localhost:9394
|
|
19
|
+
|
|
20
|
+
# Custom refresh interval (in seconds)
|
|
21
|
+
promproto -w -n 5 localhost:9394
|
|
22
|
+
|
|
23
|
+
# Full URL
|
|
24
|
+
promproto http://localhost:9394/metrics
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- Fetches metrics using the Prometheus protobuf exposition format
|
|
30
|
+
- Color-coded output for easy reading
|
|
31
|
+
- Supports both classic and native histograms
|
|
32
|
+
- Watch mode for continuous monitoring
|
|
33
|
+
- Automatic URL normalization (adds http:// and /metrics if missing)
|
|
34
|
+
|
|
35
|
+
## Native Histograms
|
|
36
|
+
|
|
37
|
+
When the server exports native histograms, promproto displays:
|
|
38
|
+
- Schema and zero threshold
|
|
39
|
+
- Zero bucket count
|
|
40
|
+
- Positive and negative bucket spans with computed bounds
|
|
41
|
+
- Visual bar charts for bucket counts
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
|
|
45
|
+
- Ruby 3.1+
|
|
46
|
+
- google-protobuf gem
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
data/exe/promproto
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
require_relative "../lib/promproto"
|
|
6
|
+
|
|
7
|
+
options = { watch: false, interval: Promproto::CLI::DEFAULT_WATCH_INTERVAL }
|
|
8
|
+
|
|
9
|
+
parser = OptionParser.new do |opts|
|
|
10
|
+
opts.banner = "Usage: promproto [options] <target>"
|
|
11
|
+
opts.separator ""
|
|
12
|
+
opts.separator "Fetches Prometheus metrics in protobuf format and renders them."
|
|
13
|
+
opts.separator ""
|
|
14
|
+
opts.separator "Target can be:"
|
|
15
|
+
opts.separator " - Full URL: http://localhost:9090/metrics"
|
|
16
|
+
opts.separator " - Host:port: localhost:9090 (assumes http:// and /metrics)"
|
|
17
|
+
opts.separator " - Host only: localhost (assumes http://, port 80, and /metrics)"
|
|
18
|
+
opts.separator ""
|
|
19
|
+
opts.separator "Options:"
|
|
20
|
+
|
|
21
|
+
opts.on("-w", "--watch", "Continuously fetch and display metrics") do
|
|
22
|
+
options[:watch] = true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
opts.on("-n", "--interval SECONDS", Integer, "Refresh interval in seconds (default: #{Promproto::CLI::DEFAULT_WATCH_INTERVAL})") do |n|
|
|
26
|
+
options[:interval] = n
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
opts.on("-h", "--help", "Show this help message") do
|
|
30
|
+
puts opts
|
|
31
|
+
exit
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
opts.on("-v", "--version", "Show version") do
|
|
35
|
+
puts "promproto #{Promproto::VERSION}"
|
|
36
|
+
exit
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
parser.parse!
|
|
42
|
+
rescue OptionParser::InvalidOption => e
|
|
43
|
+
abort "#{e.message}\nUse --help for usage information."
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if ARGV.empty?
|
|
47
|
+
puts parser
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
url = Promproto.normalize_url(ARGV[0])
|
|
52
|
+
Promproto::CLI.new(url, watch: options[:watch], interval: options[:interval]).run
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Promproto
|
|
7
|
+
class CLI
|
|
8
|
+
ACCEPT_HEADER = "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited"
|
|
9
|
+
DEFAULT_WATCH_INTERVAL = 2
|
|
10
|
+
|
|
11
|
+
def initialize(url, watch: false, interval: DEFAULT_WATCH_INTERVAL)
|
|
12
|
+
@url = url
|
|
13
|
+
@watch = watch
|
|
14
|
+
@interval = interval
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
if @watch
|
|
19
|
+
run_watch
|
|
20
|
+
else
|
|
21
|
+
run_once
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def run_once
|
|
28
|
+
data = fetch_metrics
|
|
29
|
+
families = parse_delimited(data)
|
|
30
|
+
render(families)
|
|
31
|
+
rescue Errno::ECONNREFUSED => e
|
|
32
|
+
abort "Error: Connection refused to #{@url}\n\nMake sure the metrics server is running and accessible."
|
|
33
|
+
rescue Errno::ETIMEDOUT, Net::OpenTimeout
|
|
34
|
+
abort "Error: Connection timed out to #{@url}\n\nThe server may be unreachable or behind a firewall."
|
|
35
|
+
rescue SocketError => e
|
|
36
|
+
abort "Error: Could not resolve host for #{@url}\n\n#{e.message}"
|
|
37
|
+
rescue Net::ReadTimeout
|
|
38
|
+
abort "Error: Read timeout waiting for response from #{@url}"
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
abort "Error: #{e.message}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def run_watch
|
|
44
|
+
loop do
|
|
45
|
+
print "\e[2J\e[H" # Clear screen and move cursor to top
|
|
46
|
+
puts "\e[90m#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} - #{@url}\e[0m"
|
|
47
|
+
puts
|
|
48
|
+
|
|
49
|
+
begin
|
|
50
|
+
data = fetch_metrics
|
|
51
|
+
families = parse_delimited(data)
|
|
52
|
+
render(families)
|
|
53
|
+
rescue Errno::ECONNREFUSED
|
|
54
|
+
puts "\e[31mError: Connection refused\e[0m"
|
|
55
|
+
puts "\e[90mMake sure the metrics server is running and accessible.\e[0m"
|
|
56
|
+
rescue Errno::ETIMEDOUT, Net::OpenTimeout
|
|
57
|
+
puts "\e[31mError: Connection timed out\e[0m"
|
|
58
|
+
rescue SocketError => e
|
|
59
|
+
puts "\e[31mError: #{e.message}\e[0m"
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
puts "\e[31mError: #{e.message}\e[0m"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
sleep @interval
|
|
65
|
+
end
|
|
66
|
+
rescue Interrupt
|
|
67
|
+
puts "\nStopped."
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def fetch_metrics
|
|
71
|
+
uri = URI.parse(@url)
|
|
72
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
73
|
+
http.use_ssl = uri.scheme == "https"
|
|
74
|
+
http.open_timeout = 10
|
|
75
|
+
http.read_timeout = 30
|
|
76
|
+
|
|
77
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
78
|
+
request["Accept"] = ACCEPT_HEADER
|
|
79
|
+
|
|
80
|
+
response = http.request(request)
|
|
81
|
+
|
|
82
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
83
|
+
raise "HTTP #{response.code} #{response.message}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
content_type = response["Content-Type"] || ""
|
|
87
|
+
unless content_type.include?("application/vnd.google.protobuf")
|
|
88
|
+
warn "Warning: Server returned Content-Type: #{content_type}"
|
|
89
|
+
warn "Expected: #{ACCEPT_HEADER}"
|
|
90
|
+
warn ""
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
response.body
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def parse_delimited(data)
|
|
97
|
+
families = []
|
|
98
|
+
pos = 0
|
|
99
|
+
|
|
100
|
+
while pos < data.bytesize
|
|
101
|
+
# Read varint length
|
|
102
|
+
len, bytes_read = read_varint(data, pos)
|
|
103
|
+
break if len.zero? || bytes_read.zero?
|
|
104
|
+
|
|
105
|
+
pos += bytes_read
|
|
106
|
+
|
|
107
|
+
break if pos + len > data.bytesize
|
|
108
|
+
|
|
109
|
+
msg_data = data[pos, len]
|
|
110
|
+
begin
|
|
111
|
+
family = Io::Prometheus::Client::MetricFamily.decode(msg_data)
|
|
112
|
+
families << family
|
|
113
|
+
rescue Google::Protobuf::ParseError => e
|
|
114
|
+
warn "Warning: Failed to decode message at offset #{pos}: #{e.message}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
pos += len
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
families
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def read_varint(data, pos)
|
|
124
|
+
value = 0
|
|
125
|
+
shift = 0
|
|
126
|
+
bytes_read = 0
|
|
127
|
+
|
|
128
|
+
loop do
|
|
129
|
+
return [0, 0] if pos >= data.bytesize
|
|
130
|
+
|
|
131
|
+
byte = data.getbyte(pos)
|
|
132
|
+
pos += 1
|
|
133
|
+
bytes_read += 1
|
|
134
|
+
|
|
135
|
+
value |= (byte & 0x7F) << shift
|
|
136
|
+
|
|
137
|
+
break if (byte & 0x80).zero?
|
|
138
|
+
|
|
139
|
+
shift += 7
|
|
140
|
+
return [0, 0] if shift > 63 # Overflow protection
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
[value, bytes_read]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def render(families)
|
|
147
|
+
families.each do |family|
|
|
148
|
+
render_family(family)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def render_family(family)
|
|
153
|
+
puts "\e[1;36m#{family.name}\e[0m \e[33m(#{type_name(family.type)})\e[0m"
|
|
154
|
+
puts " \e[90m#{family.help}\e[0m" unless family.help.empty?
|
|
155
|
+
|
|
156
|
+
family.metric.each do |metric|
|
|
157
|
+
render_metric(metric, family.type)
|
|
158
|
+
end
|
|
159
|
+
puts
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def type_name(type)
|
|
163
|
+
case type
|
|
164
|
+
when :COUNTER then "counter"
|
|
165
|
+
when :GAUGE then "gauge"
|
|
166
|
+
when :SUMMARY then "summary"
|
|
167
|
+
when :HISTOGRAM then "histogram"
|
|
168
|
+
when :GAUGE_HISTOGRAM then "gauge_histogram"
|
|
169
|
+
when :UNTYPED then "untyped"
|
|
170
|
+
else type.to_s.downcase
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def render_metric(metric, type)
|
|
175
|
+
labels = format_labels(metric.label)
|
|
176
|
+
|
|
177
|
+
case type
|
|
178
|
+
when :COUNTER
|
|
179
|
+
puts " #{labels} \e[32m#{metric.counter.value}\e[0m"
|
|
180
|
+
when :GAUGE
|
|
181
|
+
puts " #{labels} \e[32m#{metric.gauge.value}\e[0m"
|
|
182
|
+
when :SUMMARY
|
|
183
|
+
render_summary(metric, labels)
|
|
184
|
+
when :HISTOGRAM
|
|
185
|
+
render_histogram(metric, labels)
|
|
186
|
+
when :UNTYPED
|
|
187
|
+
puts " #{labels} \e[32m#{metric.untyped.value}\e[0m"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def format_labels(labels)
|
|
192
|
+
return "" if labels.empty?
|
|
193
|
+
|
|
194
|
+
pairs = labels.map { |l| "\e[35m#{l.name}\e[0m=\"\e[34m#{l.value}\e[0m\"" }
|
|
195
|
+
"{#{pairs.join(", ")}}"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def render_summary(metric, labels)
|
|
199
|
+
summary = metric.summary
|
|
200
|
+
puts " #{labels}"
|
|
201
|
+
puts " count: \e[32m#{summary.sample_count}\e[0m sum: \e[32m#{summary.sample_sum}\e[0m"
|
|
202
|
+
summary.quantile.each do |q|
|
|
203
|
+
puts " p#{(q.quantile * 100).to_i}: \e[32m#{q.value}\e[0m"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def render_histogram(metric, labels)
|
|
208
|
+
histogram = metric.histogram
|
|
209
|
+
puts " #{labels}"
|
|
210
|
+
puts " count: \e[32m#{histogram.sample_count}\e[0m sum: \e[32m#{format_number(histogram.sample_sum)}\e[0m"
|
|
211
|
+
|
|
212
|
+
# Check if this is a native histogram (has schema) or classic
|
|
213
|
+
if histogram.schema != 0 || histogram.positive_span.any? || histogram.negative_span.any?
|
|
214
|
+
render_native_histogram(histogram)
|
|
215
|
+
elsif histogram.bucket.any?
|
|
216
|
+
render_classic_histogram(histogram)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def render_native_histogram(histogram)
|
|
221
|
+
puts " \e[90mschema: #{histogram.schema} zero_threshold: #{histogram.zero_threshold}\e[0m"
|
|
222
|
+
|
|
223
|
+
if histogram.zero_count > 0
|
|
224
|
+
puts " zero: \e[32m#{histogram.zero_count}\e[0m"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
if histogram.negative_span.any?
|
|
228
|
+
puts " \e[90mnegative buckets:\e[0m"
|
|
229
|
+
render_spans(histogram.negative_span, histogram.negative_delta, histogram.schema, negative: true)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
if histogram.positive_span.any?
|
|
233
|
+
puts " \e[90mpositive buckets:\e[0m"
|
|
234
|
+
render_spans(histogram.positive_span, histogram.positive_delta, histogram.schema, negative: false)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def render_spans(spans, deltas, schema, negative:)
|
|
239
|
+
bucket_idx = 0
|
|
240
|
+
count = 0
|
|
241
|
+
delta_idx = 0
|
|
242
|
+
|
|
243
|
+
spans.each do |span|
|
|
244
|
+
bucket_idx += span.offset
|
|
245
|
+
|
|
246
|
+
span.length.times do
|
|
247
|
+
break if delta_idx >= deltas.size
|
|
248
|
+
|
|
249
|
+
count += deltas[delta_idx]
|
|
250
|
+
delta_idx += 1
|
|
251
|
+
|
|
252
|
+
lower, upper = bucket_bounds(bucket_idx, schema)
|
|
253
|
+
if negative
|
|
254
|
+
lower, upper = -upper, -lower
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
bar = bar_chart(count, 20)
|
|
258
|
+
puts " [#{format_bound(lower)}, #{format_bound(upper)}): #{bar} \e[32m#{count}\e[0m"
|
|
259
|
+
|
|
260
|
+
bucket_idx += 1
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def bucket_bounds(index, schema)
|
|
266
|
+
base = 2.0 ** (2.0 ** -schema)
|
|
267
|
+
lower = base ** index
|
|
268
|
+
upper = base ** (index + 1)
|
|
269
|
+
[lower, upper]
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def render_classic_histogram(histogram)
|
|
273
|
+
puts " \e[90mclassic buckets:\e[0m"
|
|
274
|
+
histogram.bucket.each do |bucket|
|
|
275
|
+
bar = bar_chart(bucket.cumulative_count, 20)
|
|
276
|
+
puts " le=#{format_bound(bucket.upper_bound)}: #{bar} \e[32m#{bucket.cumulative_count}\e[0m"
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def bar_chart(value, max_width)
|
|
281
|
+
return "" if value <= 0
|
|
282
|
+
|
|
283
|
+
width = [Math.log10(value + 1) * max_width / 5, max_width].min.to_i
|
|
284
|
+
"\e[44m#{" " * width}\e[0m"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def format_bound(value)
|
|
288
|
+
if value.infinite?
|
|
289
|
+
value.positive? ? "+Inf" : "-Inf"
|
|
290
|
+
elsif value.abs >= 1000 || (value.abs < 0.001 && value != 0)
|
|
291
|
+
format("%.3e", value)
|
|
292
|
+
else
|
|
293
|
+
format("%.4g", value)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def format_number(value)
|
|
298
|
+
if value.abs >= 1000 || (value.abs < 0.001 && value != 0)
|
|
299
|
+
format("%.3e", value)
|
|
300
|
+
else
|
|
301
|
+
format("%.4g", value)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Prometheus client model protobuf definitions
|
|
4
|
+
# Loaded from compiled FileDescriptorSet
|
|
5
|
+
|
|
6
|
+
require "google/protobuf"
|
|
7
|
+
require "google/protobuf/descriptor_pb"
|
|
8
|
+
|
|
9
|
+
module Io
|
|
10
|
+
module Prometheus
|
|
11
|
+
module Client
|
|
12
|
+
# Load the compiled FileDescriptorSet
|
|
13
|
+
DESCRIPTOR_DATA = File.binread(
|
|
14
|
+
File.expand_path("metrics.pb", __dir__)
|
|
15
|
+
).freeze
|
|
16
|
+
|
|
17
|
+
# Parse the FileDescriptorSet
|
|
18
|
+
FILE_DESCRIPTOR_SET = Google::Protobuf::FileDescriptorSet.decode(DESCRIPTOR_DATA)
|
|
19
|
+
|
|
20
|
+
# Add each file descriptor to a pool
|
|
21
|
+
DESCRIPTOR_POOL = Google::Protobuf::DescriptorPool.new
|
|
22
|
+
FILE_DESCRIPTOR_SET.file.each do |file_proto|
|
|
23
|
+
DESCRIPTOR_POOL.add_serialized_file(file_proto.to_proto)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
LabelPair = DESCRIPTOR_POOL.lookup("io.prometheus.client.LabelPair").msgclass
|
|
27
|
+
Gauge = DESCRIPTOR_POOL.lookup("io.prometheus.client.Gauge").msgclass
|
|
28
|
+
Counter = DESCRIPTOR_POOL.lookup("io.prometheus.client.Counter").msgclass
|
|
29
|
+
Quantile = DESCRIPTOR_POOL.lookup("io.prometheus.client.Quantile").msgclass
|
|
30
|
+
Summary = DESCRIPTOR_POOL.lookup("io.prometheus.client.Summary").msgclass
|
|
31
|
+
Untyped = DESCRIPTOR_POOL.lookup("io.prometheus.client.Untyped").msgclass
|
|
32
|
+
BucketSpan = DESCRIPTOR_POOL.lookup("io.prometheus.client.BucketSpan").msgclass
|
|
33
|
+
Bucket = DESCRIPTOR_POOL.lookup("io.prometheus.client.Bucket").msgclass
|
|
34
|
+
Histogram = DESCRIPTOR_POOL.lookup("io.prometheus.client.Histogram").msgclass
|
|
35
|
+
Metric = DESCRIPTOR_POOL.lookup("io.prometheus.client.Metric").msgclass
|
|
36
|
+
MetricFamily = DESCRIPTOR_POOL.lookup("io.prometheus.client.MetricFamily").msgclass
|
|
37
|
+
MetricType = DESCRIPTOR_POOL.lookup("io.prometheus.client.MetricType").enummodule
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/promproto.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "promproto/version"
|
|
4
|
+
require_relative "promproto/metrics_pb"
|
|
5
|
+
require_relative "promproto/cli"
|
|
6
|
+
|
|
7
|
+
module Promproto
|
|
8
|
+
def self.normalize_url(input)
|
|
9
|
+
url = input.dup
|
|
10
|
+
|
|
11
|
+
# Add http:// if no scheme
|
|
12
|
+
unless url.match?(%r{^https?://})
|
|
13
|
+
url = "http://#{url}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Parse to check/add path
|
|
17
|
+
uri = URI.parse(url)
|
|
18
|
+
|
|
19
|
+
# Add /metrics if path is empty or just /
|
|
20
|
+
if uri.path.nil? || uri.path.empty? || uri.path == "/"
|
|
21
|
+
uri.path = "/metrics"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
uri.to_s
|
|
25
|
+
end
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: promproto
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Lewis Buckley
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2025-12-11 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: google-protobuf
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.21'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.21'
|
|
26
|
+
description: A command-line tool that fetches Prometheus metrics using the protobuf
|
|
27
|
+
exposition format and renders them with color-coded output. Supports both classic
|
|
28
|
+
and native histograms.
|
|
29
|
+
email:
|
|
30
|
+
- lewis@basecamp.com
|
|
31
|
+
executables:
|
|
32
|
+
- promproto
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- LICENSE.md
|
|
37
|
+
- README.md
|
|
38
|
+
- exe/promproto
|
|
39
|
+
- lib/promproto.rb
|
|
40
|
+
- lib/promproto/cli.rb
|
|
41
|
+
- lib/promproto/metrics.pb
|
|
42
|
+
- lib/promproto/metrics_pb.rb
|
|
43
|
+
- lib/promproto/version.rb
|
|
44
|
+
homepage: https://github.com/lewispb/promproto
|
|
45
|
+
licenses:
|
|
46
|
+
- MIT
|
|
47
|
+
metadata:
|
|
48
|
+
homepage_uri: https://github.com/lewispb/promproto
|
|
49
|
+
source_code_uri: https://github.com/lewispb/promproto
|
|
50
|
+
rdoc_options: []
|
|
51
|
+
require_paths:
|
|
52
|
+
- lib
|
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: 3.1.0
|
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '0'
|
|
63
|
+
requirements: []
|
|
64
|
+
rubygems_version: 3.6.2
|
|
65
|
+
specification_version: 4
|
|
66
|
+
summary: CLI tool to fetch and display Prometheus metrics in protobuf format
|
|
67
|
+
test_files: []
|