1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
use prometheus::{
CounterVec, Gauge, Histogram, HistogramVec,
Opts, Registry, TextEncoder, Encoder,
histogram_opts,
};
use std::sync::OnceLock;
use std::thread;
pub const STATE_IDLE: f64 = 0.0;
pub const STATE_ACTIVE: f64 = 1.0;
pub const STATE_THROTTLED: f64 = 2.0;
pub static METRICS: OnceLock<Metrics> = OnceLock::new();
pub fn get() -> &'static Metrics {
METRICS.get().unwrap()
}
pub struct Metrics {
pub state: Gauge,
pub images_checked: CounterVec,
pub images_updated: CounterVec,
pub http_request: CounterVec,
pub http_request_duration: HistogramVec,
pub rate_limits: CounterVec,
pub auth_attempts: CounterVec,
pub run_duration: Gauge,
pub git_operations: CounterVec,
pub db_query_duration: Histogram,
registry: Registry,
}
impl Metrics {
pub fn new() -> Self {
let registry = Registry::new();
let state = Gauge::with_opts(
Opts::new("vbump_state", "Current state: 0=idle, 1=active, 2=throttled"),
).unwrap();
registry.register(Box::new(state.clone())).unwrap();
let images_checked = CounterVec::new(
Opts::new("vbump_images_checked_total", "Total images checked"),
&["registry"],
).unwrap();
registry.register(Box::new(images_checked.clone())).unwrap();
let images_updated = CounterVec::new(
Opts::new("vbump_images_updated_total", "Total images with newer versions found"),
&["registry"],
).unwrap();
registry.register(Box::new(images_updated.clone())).unwrap();
let http_request = CounterVec::new(
Opts::new("vbump_http_request_total", "Total HTTP requests to registries"),
&["registry", "outcome"],
).unwrap();
registry.register(Box::new(http_request.clone())).unwrap();
let http_request_duration = HistogramVec::new(
histogram_opts!(
"vbump_http_request_duration_seconds",
"Request latency distribution",
vec![0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
),
&["registry"],
).unwrap();
registry.register(Box::new(http_request_duration.clone())).unwrap();
let rate_limits = CounterVec::new(
Opts::new("vbump_rate_limits_total", "Rate limit (429) responses received"),
&["registry"],
).unwrap();
registry.register(Box::new(rate_limits.clone())).unwrap();
let auth_attempts = CounterVec::new(
Opts::new("vbump_auth_attempts_total", "Auth attempts (success/failure)"),
&["registry", "outcome"],
).unwrap();
registry.register(Box::new(auth_attempts.clone())).unwrap();
let run_duration = Gauge::with_opts(
Opts::new("vbump_run_duration_seconds", "Duration of last complete scan"),
).unwrap();
registry.register(Box::new(run_duration.clone())).unwrap();
let git_operations = CounterVec::new(
Opts::new("vbump_git_operations_total", "Git commits/pushes (success/failure)"),
&["operation", "outcome"],
).unwrap();
registry.register(Box::new(git_operations.clone())).unwrap();
let db_query_duration = Histogram::with_opts(
histogram_opts!(
"vbump_query_duration_seconds",
"Database query latency distribution",
vec![0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5]
),
).unwrap();
registry.register(Box::new(db_query_duration.clone())).unwrap();
return Self {
state,
images_checked,
images_updated,
http_request,
http_request_duration,
rate_limits,
auth_attempts,
run_duration,
git_operations,
db_query_duration,
registry,
};
}
}
pub fn init() {
METRICS.get_or_init(Metrics::new);
}
pub fn serve(port: u16) {
let registry = get().registry.clone();
thread::spawn(move || {
let addr = format!("0.0.0.0:{}", port);
let server = match tiny_http::Server::http(&addr) {
Ok(s) => s,
Err(e) => {
eprintln!("Failed to start metrics server on {}: {}", addr, e);
return;
}
};
println!("Metrics server listening on {}", addr);
for request in server.incoming_requests() {
let response = if request.url() == "/metrics" {
let encoder = TextEncoder::new();
let metric_families = registry.gather();
let mut buffer = Vec::new();
encoder.encode(&metric_families, &mut buffer).unwrap();
tiny_http::Response::from_data(buffer)
.with_header(
tiny_http::Header::from_bytes(
&b"Content-Type"[..],
&b"text/plain; version=0.0.4; charset=utf-8"[..],
).unwrap()
)
} else {
tiny_http::Response::from_string("Not Found")
.with_status_code(404)
};
let _ = request.respond(response);
}
});
}
|