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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/AGENTS.md +38 -0
  3. data/CHANGELOG.md +59 -0
  4. data/README.md +109 -0
  5. data/lib/fast/prometheus/counter.rb +22 -0
  6. data/lib/fast/prometheus/errors.rb +11 -0
  7. data/lib/fast/prometheus/exposition.rb +46 -0
  8. data/lib/fast/prometheus/formats/metrics_pb.rb +33 -0
  9. data/lib/fast/prometheus/formats/protobuf.rb +154 -0
  10. data/lib/fast/prometheus/formats/text.rb +90 -0
  11. data/lib/fast/prometheus/gauge.rb +37 -0
  12. data/lib/fast/prometheus/histogram.rb +112 -0
  13. data/lib/fast/prometheus/metric.rb +144 -0
  14. data/lib/fast/prometheus/middleware/exporter.rb +39 -0
  15. data/lib/fast/prometheus/middleware/instrumentation.rb +34 -0
  16. data/lib/fast/prometheus/native_histogram.rb +145 -0
  17. data/lib/fast/prometheus/otlp/grpc_exporter.rb +39 -0
  18. data/lib/fast/prometheus/otlp/http_exporter.rb +43 -0
  19. data/lib/fast/prometheus/otlp/mapper.rb +207 -0
  20. data/lib/fast/prometheus/otlp/pb/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.rb +27 -0
  21. data/lib/fast/prometheus/otlp/pb/opentelemetry/proto/common/v1/common_pb.rb +26 -0
  22. data/lib/fast/prometheus/otlp/pb/opentelemetry/proto/metrics/v1/metrics_pb.rb +41 -0
  23. data/lib/fast/prometheus/otlp/pb/opentelemetry/proto/resource/v1/resource_pb.rb +23 -0
  24. data/lib/fast/prometheus/otlp/push.rb +39 -0
  25. data/lib/fast/prometheus/otlp/service_interface.rb +18 -0
  26. data/lib/fast/prometheus/rack/exporter.rb +32 -0
  27. data/lib/fast/prometheus/rack/instrumentation.rb +33 -0
  28. data/lib/fast/prometheus/registry.rb +111 -0
  29. data/lib/fast/prometheus/request_metrics.rb +45 -0
  30. data/lib/fast/prometheus/snapshot.rb +78 -0
  31. data/lib/fast/prometheus/store.rb +37 -0
  32. data/lib/fast/prometheus/summary.rb +50 -0
  33. data/lib/fast/prometheus/version.rb +7 -0
  34. data/lib/fast/prometheus.rb +17 -0
  35. metadata +158 -0
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fast/prometheus"
4
+
5
+ $LOAD_PATH.unshift(File.expand_path("pb", __dir__)) unless $LOAD_PATH.include?(File.expand_path("pb", __dir__))
6
+
7
+ require "opentelemetry/proto/collector/metrics/v1/metrics_service_pb"
8
+
9
+ module Fast
10
+ module Prometheus
11
+ module OTLP
12
+ # Maps a Snapshot to an ExportMetricsServiceRequest.
13
+ class Mapper
14
+ CUMULATIVE = Opentelemetry::Proto::Metrics::V1::AggregationTemporality::AGGREGATION_TEMPORALITY_CUMULATIVE
15
+ private_constant :CUMULATIVE
16
+
17
+ def initialize(resource_attributes: {}, start_time: Time.now)
18
+ @resource_attributes = resource_attributes.map { |k, v| kv(k, v) }
19
+ @start_time_unix_nano = (start_time.to_f * 1_000_000_000).to_i
20
+ end
21
+
22
+ def request(snapshot)
23
+ Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceRequest.new(
24
+ resource_metrics: [
25
+ Opentelemetry::Proto::Metrics::V1::ResourceMetrics.new(
26
+ resource: Opentelemetry::Proto::Resource::V1::Resource.new(
27
+ attributes: @resource_attributes
28
+ ),
29
+ scope_metrics: [
30
+ Opentelemetry::Proto::Metrics::V1::ScopeMetrics.new(
31
+ scope: Opentelemetry::Proto::Common::V1::InstrumentationScope.new(
32
+ name: "fast-prometheus",
33
+ version: Fast::Prometheus::VERSION
34
+ ),
35
+ metrics: snapshot.metrics.map { |ms| build_metric(ms, snapshot.taken_at) }
36
+ )
37
+ ]
38
+ )
39
+ ]
40
+ )
41
+ end
42
+
43
+ private
44
+
45
+ def build_metric(metric_snapshot, taken_at)
46
+ time_unix_nano = (taken_at.to_f * 1_000_000_000).to_i
47
+
48
+ Opentelemetry::Proto::Metrics::V1::Metric.new(
49
+ name: metric_snapshot.name.to_s,
50
+ description: metric_snapshot.docstring,
51
+ **build_data(metric_snapshot, time_unix_nano)
52
+ )
53
+ end
54
+
55
+ def build_data(metric_snapshot, time_unix_nano)
56
+ case metric_snapshot.type
57
+ when :counter
58
+ {
59
+ sum: Opentelemetry::Proto::Metrics::V1::Sum.new(
60
+ data_points: metric_snapshot.series.map do |series|
61
+ number_data_point(series, time_unix_nano)
62
+ end,
63
+ aggregation_temporality: CUMULATIVE,
64
+ is_monotonic: true
65
+ )
66
+ }
67
+ when :gauge
68
+ {
69
+ gauge: Opentelemetry::Proto::Metrics::V1::Gauge.new(
70
+ data_points: metric_snapshot.series.map do |series|
71
+ number_data_point(series, time_unix_nano)
72
+ end
73
+ )
74
+ }
75
+ when :histogram
76
+ {
77
+ histogram: Opentelemetry::Proto::Metrics::V1::Histogram.new(
78
+ data_points: metric_snapshot.series.map do |series|
79
+ histogram_data_point(series, time_unix_nano)
80
+ end,
81
+ aggregation_temporality: CUMULATIVE
82
+ )
83
+ }
84
+ when :summary
85
+ {
86
+ summary: Opentelemetry::Proto::Metrics::V1::Summary.new(
87
+ data_points: metric_snapshot.series.map do |series|
88
+ summary_data_point(series, time_unix_nano)
89
+ end
90
+ )
91
+ }
92
+ when :native_histogram
93
+ {
94
+ exponential_histogram: Opentelemetry::Proto::Metrics::V1::ExponentialHistogram.new(
95
+ data_points: metric_snapshot.series.map do |series|
96
+ exponential_histogram_data_point(series, time_unix_nano)
97
+ end,
98
+ aggregation_temporality: CUMULATIVE
99
+ )
100
+ }
101
+ end
102
+ end
103
+
104
+ def number_data_point(series, time_unix_nano)
105
+ Opentelemetry::Proto::Metrics::V1::NumberDataPoint.new(
106
+ attributes: build_attributes(series.labels),
107
+ start_time_unix_nano: @start_time_unix_nano,
108
+ time_unix_nano: time_unix_nano,
109
+ as_double: series.value.to_f
110
+ )
111
+ end
112
+
113
+ def histogram_data_point(series, time_unix_nano)
114
+ nv = series.value
115
+ bounds, counts = non_cumulative_buckets(nv.cumulative_buckets)
116
+
117
+ Opentelemetry::Proto::Metrics::V1::HistogramDataPoint.new(
118
+ attributes: build_attributes(series.labels),
119
+ start_time_unix_nano: @start_time_unix_nano,
120
+ time_unix_nano: time_unix_nano,
121
+ count: nv.count,
122
+ sum: nv.sum,
123
+ bucket_counts: counts,
124
+ explicit_bounds: bounds
125
+ )
126
+ end
127
+
128
+ def non_cumulative_buckets(cumulative_buckets)
129
+ # cumulative_buckets: [[bound1, cum1], [bound2, cum2], ..., [+Inf, cumN]]
130
+ # bounds = all boundaries except +Inf
131
+ # counts = non-cumulative per-bucket counts + overflow
132
+ bounds = []
133
+ counts = Array.new(cumulative_buckets.size)
134
+ prev = 0
135
+
136
+ cumulative_buckets.each_with_index do |(boundary, cumulative), i|
137
+ counts[i] = cumulative - prev
138
+ prev = cumulative
139
+ bounds << boundary unless boundary.infinite?
140
+ end
141
+
142
+ [bounds, counts]
143
+ end
144
+
145
+ def summary_data_point(series, time_unix_nano)
146
+ sv = series.value
147
+
148
+ Opentelemetry::Proto::Metrics::V1::SummaryDataPoint.new(
149
+ attributes: build_attributes(series.labels),
150
+ start_time_unix_nano: @start_time_unix_nano,
151
+ time_unix_nano: time_unix_nano,
152
+ count: sv.count,
153
+ sum: sv.sum
154
+ )
155
+ end
156
+
157
+ def exponential_histogram_data_point(series, time_unix_nano)
158
+ nv = series.value
159
+
160
+ Opentelemetry::Proto::Metrics::V1::ExponentialHistogramDataPoint.new(
161
+ attributes: build_attributes(series.labels),
162
+ start_time_unix_nano: @start_time_unix_nano,
163
+ time_unix_nano: time_unix_nano,
164
+ count: nv.count,
165
+ sum: nv.sum,
166
+ scale: nv.schema,
167
+ zero_count: nv.zero_count,
168
+ zero_threshold: nv.zero_threshold,
169
+ positive: dense_buckets(nv.positive_buckets),
170
+ negative: dense_buckets(nv.negative_buckets)
171
+ )
172
+ end
173
+
174
+ # Convert sparse [[prom_idx, count], ...] to OTLP Buckets{offset, bucket_counts}.
175
+ # OTLP index = prom index - 1. Offset = min OTLP index.
176
+ # Buckets are always sorted (from NativeHistogram::Slot#positive_buckets / #negative_buckets).
177
+ def dense_buckets(buckets)
178
+ return Opentelemetry::Proto::Metrics::V1::ExponentialHistogramDataPoint::Buckets.new if buckets.empty?
179
+
180
+ first_idx, = buckets.first
181
+ last_idx, = buckets.last
182
+ offset = first_idx - 1
183
+ length = last_idx - offset
184
+
185
+ counts = Array.new(length, 0)
186
+ buckets.each { |prom_idx, count| counts[prom_idx - offset - 1] = count }
187
+
188
+ Opentelemetry::Proto::Metrics::V1::ExponentialHistogramDataPoint::Buckets.new(
189
+ offset: offset,
190
+ bucket_counts: counts
191
+ )
192
+ end
193
+
194
+ def build_attributes(labels)
195
+ labels.map { |k, v| kv(k.to_s, v.to_s) }
196
+ end
197
+
198
+ def kv(key, value)
199
+ Opentelemetry::Proto::Common::V1::KeyValue.new(
200
+ key: key,
201
+ value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: value)
202
+ )
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: opentelemetry/proto/collector/metrics/v1/metrics_service.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'opentelemetry/proto/metrics/v1/metrics_pb'
8
+
9
+
10
+ descriptor_data = "\n>opentelemetry/proto/collector/metrics/v1/metrics_service.proto\x12(opentelemetry.proto.collector.metrics.v1\x1a,opentelemetry/proto/metrics/v1/metrics.proto\"h\n\x1b\x45xportMetricsServiceRequest\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"~\n\x1c\x45xportMetricsServiceResponse\x12^\n\x0fpartial_success\x18\x01 \x01(\x0b\x32\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsPartialSuccess\"R\n\x1b\x45xportMetricsPartialSuccess\x12\x1c\n\x14rejected_data_points\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\xac\x01\n\x0eMetricsService\x12\x99\x01\n\x06\x45xport\x12\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest\x1a\x46.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse\"\x00\x42\xa4\x01\n+io.opentelemetry.proto.collector.metrics.v1B\x13MetricsServiceProtoP\x01Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1\xaa\x02(OpenTelemetry.Proto.Collector.Metrics.V1b\x06proto3"
11
+
12
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
13
+ pool.add_serialized_file(descriptor_data)
14
+
15
+ module Opentelemetry
16
+ module Proto
17
+ module Collector
18
+ module Metrics
19
+ module V1
20
+ ExportMetricsServiceRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest").msgclass
21
+ ExportMetricsServiceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse").msgclass
22
+ ExportMetricsPartialSuccess = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.collector.metrics.v1.ExportMetricsPartialSuccess").msgclass
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: opentelemetry/proto/common/v1/common.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+
8
+ descriptor_data = "\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\xad\x02\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12@\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32).opentelemetry.proto.common.v1.ArrayValueH\x00\x12\x43\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32+.opentelemetry.proto.common.v1.KeyValueListH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x12\x1f\n\x15string_value_strindex\x18\x08 \x01(\x05H\x00\x42\x07\n\x05value\"E\n\nArrayValue\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"G\n\x0cKeyValueList\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\"e\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\x12\x14\n\x0ckey_strindex\x18\x03 \x01(\x05\"\x94\x01\n\x14InstrumentationScope\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\"X\n\tEntityRef\x12\x12\n\nschema_url\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0f\n\x07id_keys\x18\x03 \x03(\t\x12\x18\n\x10\x64\x65scription_keys\x18\x04 \x03(\tB{\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z(go.opentelemetry.io/proto/otlp/common/v1\xaa\x02\x1dOpenTelemetry.Proto.Common.V1b\x06proto3"
9
+
10
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
11
+ pool.add_serialized_file(descriptor_data)
12
+
13
+ module Opentelemetry
14
+ module Proto
15
+ module Common
16
+ module V1
17
+ AnyValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.AnyValue").msgclass
18
+ ArrayValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.ArrayValue").msgclass
19
+ KeyValueList = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.KeyValueList").msgclass
20
+ KeyValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.KeyValue").msgclass
21
+ InstrumentationScope = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.InstrumentationScope").msgclass
22
+ EntityRef = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.EntityRef").msgclass
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: opentelemetry/proto/metrics/v1/metrics.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'opentelemetry/proto/common/v1/common_pb'
8
+ require 'opentelemetry/proto/resource/v1/resource_pb'
9
+
10
+
11
+ descriptor_data = "\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"X\n\x0bMetricsData\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\xaf\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x43\n\rscope_metrics\x18\x02 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.ScopeMetrics\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\x9f\x01\n\x0cScopeMetrics\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xcd\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12U\n\x15\x65xponential_histogram\x18\n \x01(\x0b\x32\x34.opentelemetry.proto.metrics.v1.ExponentialHistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x12\x39\n\x08metadata\x18\x0c \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValueB\x06\n\x04\x64\x61taJ\x04\x08\x04\x10\x05J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\t\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xc3\x01\n\x14\x45xponentialHistogram\x12R\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\x86\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\x08 \x01(\rB\x07\n\x05valueJ\x04\x08\x01\x10\x02\"\xe6\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\n \x01(\r\x12\x10\n\x03min\x18\x0b \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\x0c \x01(\x01H\x02\x88\x01\x01\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_maxJ\x04\x08\x01\x10\x02\"\xda\x04\n\x1d\x45xponentialHistogramDataPoint\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\r\n\x05scale\x18\x06 \x01(\x11\x12\x12\n\nzero_count\x18\x07 \x01(\x06\x12W\n\x08positive\x18\x08 \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12W\n\x08negative\x18\t \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12\r\n\x05\x66lags\x18\n \x01(\r\x12;\n\texemplars\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\x10\n\x03min\x18\x0c \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\r \x01(\x01H\x02\x88\x01\x01\x12\x16\n\x0ezero_threshold\x18\x0e \x01(\x01\x1a\x30\n\x07\x42uckets\x12\x0e\n\x06offset\x18\x01 \x01(\x11\x12\x15\n\rbucket_counts\x18\x02 \x03(\x04\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_max\"\xc5\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01J\x04\x08\x01\x10\x02\"\xc1\x01\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05valueJ\x04\x08\x01\x10\x02*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02*^\n\x0e\x44\x61taPointFlags\x12\x1f\n\x1b\x44\x41TA_POINT_FLAGS_DO_NOT_USE\x10\x00\x12+\n\'DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK\x10\x01\x42\x7f\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z)go.opentelemetry.io/proto/otlp/metrics/v1\xaa\x02\x1eOpenTelemetry.Proto.Metrics.V1b\x06proto3"
12
+
13
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
14
+ pool.add_serialized_file(descriptor_data)
15
+
16
+ module Opentelemetry
17
+ module Proto
18
+ module Metrics
19
+ module V1
20
+ MetricsData = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.MetricsData").msgclass
21
+ ResourceMetrics = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.ResourceMetrics").msgclass
22
+ ScopeMetrics = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.ScopeMetrics").msgclass
23
+ Metric = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.Metric").msgclass
24
+ Gauge = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.Gauge").msgclass
25
+ Sum = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.Sum").msgclass
26
+ Histogram = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.Histogram").msgclass
27
+ ExponentialHistogram = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.ExponentialHistogram").msgclass
28
+ Summary = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.Summary").msgclass
29
+ NumberDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.NumberDataPoint").msgclass
30
+ HistogramDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.HistogramDataPoint").msgclass
31
+ ExponentialHistogramDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint").msgclass
32
+ ExponentialHistogramDataPoint::Buckets = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets").msgclass
33
+ SummaryDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.SummaryDataPoint").msgclass
34
+ SummaryDataPoint::ValueAtQuantile = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile").msgclass
35
+ Exemplar = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.Exemplar").msgclass
36
+ AggregationTemporality = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.AggregationTemporality").enummodule
37
+ DataPointFlags = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.DataPointFlags").enummodule
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: opentelemetry/proto/resource/v1/resource.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+ require 'opentelemetry/proto/common/v1/common_pb'
8
+
9
+
10
+ descriptor_data = "\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"\xa8\x01\n\x08Resource\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\r\x12=\n\x0b\x65ntity_refs\x18\x03 \x03(\x0b\x32(.opentelemetry.proto.common.v1.EntityRefB\x83\x01\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z*go.opentelemetry.io/proto/otlp/resource/v1\xaa\x02\x1fOpenTelemetry.Proto.Resource.V1b\x06proto3"
11
+
12
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
13
+ pool.add_serialized_file(descriptor_data)
14
+
15
+ module Opentelemetry
16
+ module Proto
17
+ module Resource
18
+ module V1
19
+ Resource = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.resource.v1.Resource").msgclass
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fast/prometheus"
4
+ require "async"
5
+ require "console"
6
+
7
+ module Fast
8
+ module Prometheus
9
+ module OTLP
10
+ # Periodically pushes metrics using any exporter that responds to #export.
11
+ class Push
12
+ def initialize(exporter:, interval: 15)
13
+ @exporter = exporter
14
+ @interval = interval
15
+ @task = nil
16
+ end
17
+
18
+ # Start the push loop. Returns the Async::Task running it.
19
+ def run(parent: Async::Task.current)
20
+ @task = parent.async do |task|
21
+ task.annotate("OTLP push loop")
22
+ loop do
23
+ sleep(@interval)
24
+ @exporter.export
25
+ rescue StandardError => e
26
+ Console.warn("OTLP push error: #{e}")
27
+ end
28
+ end
29
+ end
30
+
31
+ # Stop the push loop.
32
+ def stop
33
+ @task&.stop
34
+ @task = nil
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fast/prometheus"
4
+ require "protocol/grpc/interface"
5
+ require_relative "mapper"
6
+
7
+ module Fast
8
+ module Prometheus
9
+ module OTLP
10
+ # Protocol::GRPC::Interface for opentelemetry.proto.collector.metrics.v1.MetricsService.
11
+ class MetricsServiceInterface < Protocol::GRPC::Interface
12
+ rpc :Export,
13
+ request_class: Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceRequest,
14
+ response_class: Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceResponse
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fast/prometheus"
4
+ require "fast/prometheus/exposition"
5
+
6
+ module Fast
7
+ module Prometheus
8
+ module Rack
9
+ # Rack middleware that serves metrics at a configurable path.
10
+ # Handles content negotiation (text vs protobuf) and gzip compression.
11
+ class Exporter
12
+ def initialize(app, registry: Fast::Prometheus.registry, path: "/metrics")
13
+ @app = app
14
+ @registry = registry
15
+ @path = path
16
+ end
17
+
18
+ def call(env)
19
+ return @app.call(env) unless env["REQUEST_METHOD"] == "GET" && env["PATH_INFO"] == @path
20
+
21
+ body, headers = Exposition.render(
22
+ @registry,
23
+ accept: env["HTTP_ACCEPT"],
24
+ accept_encoding: env["HTTP_ACCEPT_ENCODING"]
25
+ )
26
+
27
+ [200, headers, [body]]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fast/prometheus"
4
+ require "fast/prometheus/request_metrics"
5
+
6
+ module Fast
7
+ module Prometheus
8
+ module Rack
9
+ # Rack middleware that records RED metrics for every request.
10
+ # Drop one line into config.ru and get request counts and durations.
11
+ class Instrumentation
12
+ def initialize(app, registry: Fast::Prometheus.registry, native: false, prefix: "http_server")
13
+ @app = app
14
+ @metrics = RequestMetrics.new(registry: registry, native: native, prefix: prefix)
15
+ end
16
+
17
+ def call(env)
18
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
19
+ status, headers, body = @app.call(env)
20
+ @metrics.record(env["REQUEST_METHOD"], status.to_s, start)
21
+ [status, headers, body]
22
+ rescue StandardError => e
23
+ begin
24
+ @metrics.record(env["REQUEST_METHOD"], "500", start)
25
+ rescue StandardError
26
+ nil
27
+ end
28
+ raise e
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "counter"
5
+ require_relative "gauge"
6
+ require_relative "histogram"
7
+ require_relative "summary"
8
+ require_relative "native_histogram"
9
+ require_relative "snapshot"
10
+
11
+ module Fast
12
+ module Prometheus
13
+ # Registry holds a collection of metrics and produces immutable snapshots.
14
+ class Registry
15
+ def initialize
16
+ @by_name = {}
17
+ @lock = Mutex.new
18
+ end
19
+
20
+ # Register a metric. Raises DuplicateMetric if a metric with the same name
21
+ # is already registered. Returns the metric.
22
+ def register(metric)
23
+ @lock.synchronize do
24
+ raise DuplicateMetric, "metric #{metric.name} already registered" if @by_name.key?(metric.name)
25
+
26
+ @by_name[metric.name] = metric
27
+ metric
28
+ end
29
+ end
30
+
31
+ # Remove a metric by name.
32
+ def unregister(name)
33
+ @lock.synchronize { @by_name.delete(name) }
34
+ end
35
+
36
+ # Look up a metric by name, or nil.
37
+ def get(name)
38
+ @lock.synchronize { @by_name[name] }
39
+ end
40
+
41
+ # Atomic fetch-or-register: returns the metric already registered under
42
+ # +name+, else registers and returns the block's metric. The block runs
43
+ # only when +name+ is absent, inside the same lock hold as the lookup,
44
+ # so concurrent callers for one absent name never race each other into
45
+ # DuplicateMetric. Raises ArgumentError if the block's metric is not
46
+ # named +name+.
47
+ def fetch_or_register(name)
48
+ @lock.synchronize do
49
+ next @by_name[name] if @by_name.key?(name)
50
+
51
+ metric = yield
52
+ unless metric.name == name
53
+ raise ArgumentError, "fetch_or_register(#{name.inspect}) block built #{metric.name.inspect}"
54
+ end
55
+
56
+ @by_name[name] = metric
57
+ end
58
+ end
59
+
60
+ # Return the registered metrics, insertion order.
61
+ def metrics
62
+ @lock.synchronize { @by_name.values }
63
+ end
64
+
65
+ # Convenience: build, register, and return a Counter.
66
+ def counter(name, **kwargs)
67
+ register(Counter.new(name, **kwargs))
68
+ end
69
+
70
+ # Convenience: build, register, and return a Gauge.
71
+ def gauge(name, **kwargs)
72
+ register(Gauge.new(name, **kwargs))
73
+ end
74
+
75
+ # Convenience: build, register, and return a Histogram.
76
+ def histogram(name, **kwargs)
77
+ register(Histogram.new(name, **kwargs))
78
+ end
79
+
80
+ # Convenience: build, register, and return a Summary.
81
+ def summary(name, **kwargs)
82
+ register(Summary.new(name, **kwargs))
83
+ end
84
+
85
+ # Convenience: build, register, and return a NativeHistogram.
86
+ def native_histogram(name, **kwargs)
87
+ register(NativeHistogram.new(name, **kwargs))
88
+ end
89
+
90
+ # Collect an immutable snapshot of all registered metrics. The registry
91
+ # lock only covers copying the metric list; each metric's own snapshot
92
+ # is built under its own store lock (see Snapshot.of).
93
+ def collect
94
+ Snapshot.of(metrics)
95
+ end
96
+ end
97
+
98
+ @registry_lock = Mutex.new
99
+
100
+ # Module-level default registry. Constructed at most once across threads:
101
+ # the fast path is an unsynchronized read (safe under the GVL), and only
102
+ # the first caller pays for the lock.
103
+ def self.registry
104
+ @registry || @registry_lock.synchronize { @registry ||= Registry.new }
105
+ end
106
+
107
+ def self.registry=(registry)
108
+ @registry = registry
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fast/prometheus"
4
+
5
+ module Fast
6
+ module Prometheus
7
+ # Owns the RED metrics (request count + duration) recorded for every HTTP
8
+ # request. Shared by Middleware::Instrumentation and Rack::Instrumentation
9
+ # so registration and recording exist exactly once.
10
+ class RequestMetrics
11
+ def initialize(registry:, native: false, prefix: "http_server")
12
+ @registry = registry
13
+ @counter = ensure_counter(prefix)
14
+ @histogram = ensure_histogram(prefix, native)
15
+ end
16
+
17
+ def record(method, status, started_at)
18
+ labels = { method: method, status: status }
19
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
20
+ @counter.increment(labels: labels)
21
+ @histogram.observe(duration, labels: labels)
22
+ end
23
+
24
+ private
25
+
26
+ def ensure_counter(prefix)
27
+ name = :"#{prefix}_requests_total"
28
+ @registry.fetch_or_register(name) do
29
+ Counter.new(name, docstring: "Total HTTP requests", labels: %i[method status])
30
+ end
31
+ end
32
+
33
+ def ensure_histogram(prefix, native)
34
+ name = :"#{prefix}_request_duration_seconds"
35
+ @registry.fetch_or_register(name) do
36
+ if native
37
+ NativeHistogram.new(name, docstring: "HTTP request duration in seconds", labels: %i[method status])
38
+ else
39
+ Histogram.new(name, docstring: "HTTP request duration in seconds", labels: %i[method status])
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end