1use std::{
4 collections::BTreeMap,
5 fmt::Write,
6 sync::{
7 Arc, Mutex,
8 atomic::{AtomicBool, AtomicU64, Ordering},
9 },
10};
11
12mod kubernetes;
13pub use kubernetes::{KubernetesMetrics, KubernetesRequestMetrics};
14
15const TYPES: &[(u16, &str)] = &[
17 (1, "A"),
18 (28, "AAAA"),
19 (5, "CNAME"),
20 (48, "DNSKEY"),
21 (43, "DS"),
22 (15, "MX"),
23 (50, "NSEC3"),
24 (47, "NSEC"),
25 (2, "NS"),
26 (12, "PTR"),
27 (46, "RRSIG"),
28 (6, "SOA"),
29 (33, "SRV"),
30 (16, "TXT"),
31 (65, "HTTPS"),
32 (64, "SVCB"),
33 (251, "IXFR"),
34 (252, "AXFR"),
35 (255, "ANY"),
36];
37const RESPONSE_CODES: usize = 4096;
38pub const WRITER_METADATA: &str = "prometheus/writer";
39const TIME_BUCKETS: &[f64] = &[
40 0.00025, 0.0005, 0.001, 0.002, 0.004, 0.008, 0.016, 0.032, 0.064, 0.128, 0.256, 0.512, 1.024,
41 2.048, 4.096, 8.192,
42];
43const HEALTH_BUCKETS: &[f64] = &[0.00025, 0.0025, 0.025, 0.25, 2.5];
44const SIZE_BUCKETS: &[f64] = &[
45 0., 100., 200., 300., 400., 511., 1023., 2047., 4095., 8291., 16000., 32000., 48000., 64000.,
46];
47
48type Key = (String, String, String);
49type StatusKey = (String, String, String);
50
51#[derive(Clone)]
52pub struct Registry(Arc<RegistryInner>);
53
54struct RegistryInner {
55 version: String,
56 revision: String,
57 rustc: String,
58 blocks: Mutex<BTreeMap<Key, Arc<Block>>>,
59 statuses: Mutex<BTreeMap<StatusKey, Arc<AtomicU64>>>,
60 proxies: Mutex<BTreeMap<(String, String), Arc<Proxy>>>,
61 caches: Mutex<BTreeMap<Key, Arc<Cache>>>,
62 hosts: Mutex<BTreeMap<String, Arc<HostsGauge>>>,
63 hosts_timestamp: Arc<AtomicU64>,
64 forward: Arc<ForwardCounters>,
65 forward_enabled: AtomicBool,
66 local: Arc<AtomicU64>,
67 local_enabled: AtomicBool,
68 process: AtomicBool,
69 health_enabled: AtomicBool,
70 health_duration: Histogram,
71 health_failures: AtomicU64,
72 reload_enabled: AtomicBool,
73 reload_failures: AtomicU64,
74 reload_hash: Mutex<Option<String>>,
75 kubernetes: KubernetesMetrics,
76}
77
78#[derive(Default)]
79struct ForwardCounters {
80 healthcheck_broken: AtomicU64,
81 concurrent_rejects: AtomicU64,
82}
83
84#[derive(Clone)]
86pub struct ForwardMetrics(Arc<ForwardCounters>);
87
88#[derive(Clone)]
90pub struct CacheMetrics(Arc<Cache>);
91
92#[derive(Debug, Default)]
93struct HostsGauge {
94 entries: AtomicU64,
95 initialized: AtomicBool,
96}
97
98#[derive(Clone, Debug)]
100pub struct HostsMetrics {
101 gauge: Arc<HostsGauge>,
102 timestamp: Arc<AtomicU64>,
103}
104
105impl HostsMetrics {
106 pub fn update(&self, entries: usize, timestamp: f64) {
107 self.gauge.entries.store(entries as u64, Ordering::Relaxed);
108 self.gauge.initialized.store(true, Ordering::Relaxed);
109 self.timestamp.store(timestamp.to_bits(), Ordering::Relaxed);
110 }
111}
112
113#[derive(Default)]
114struct Cache {
115 entries: [AtomicU64; 2],
116 hits: [AtomicU64; 2],
117 evictions: [AtomicU64; 2],
118 requests: AtomicU64,
119 misses: AtomicU64,
120 prefetches: AtomicU64,
121 drops: AtomicU64,
122 served_stale: AtomicU64,
123}
124
125impl CacheMetrics {
126 pub fn request(&self) {
127 self.0.requests.fetch_add(1, Ordering::Relaxed);
128 }
129
130 pub fn hit(&self, denial: bool) {
131 self.0.hits[usize::from(denial)].fetch_add(1, Ordering::Relaxed);
132 }
133
134 pub fn miss(&self) {
135 self.0.misses.fetch_add(1, Ordering::Relaxed);
136 }
137
138 pub fn prefetch(&self) {
139 self.0.prefetches.fetch_add(1, Ordering::Relaxed);
140 }
141
142 pub fn drop_response(&self) {
143 self.0.drops.fetch_add(1, Ordering::Relaxed);
144 }
145
146 pub fn served_stale(&self) {
147 self.0.served_stale.fetch_add(1, Ordering::Relaxed);
148 }
149
150 pub fn evict(&self, denial: bool) {
151 self.0.evictions[usize::from(denial)].fetch_add(1, Ordering::Relaxed);
152 }
153
154 pub fn entries(&self, denial: bool, count: usize) {
155 self.0.entries[usize::from(denial)].store(count as u64, Ordering::Relaxed);
156 }
157}
158
159impl ForwardMetrics {
160 pub fn healthcheck_broken(&self) {
161 self.0.healthcheck_broken.fetch_add(1, Ordering::Relaxed);
162 }
163
164 pub fn concurrent_reject(&self) {
165 self.0.concurrent_rejects.fetch_add(1, Ordering::Relaxed);
166 }
167}
168
169#[derive(Clone)]
170pub struct BlockMetrics(Arc<Block>);
171
172#[derive(Clone)]
173pub struct TransportStatus(Arc<AtomicU64>);
174
175#[derive(Clone)]
177pub struct ProxyMetrics(Arc<Proxy>);
178
179#[derive(Clone)]
181pub struct ProxyRequestDuration(Arc<Histogram>);
182
183#[derive(Clone)]
185pub struct ProxyConnectionCache(Arc<ProxyCache>);
186
187struct Proxy {
188 proxy_name: String,
189 to: String,
190 durations: Mutex<BTreeMap<String, Arc<Histogram>>>,
191 caches: Mutex<BTreeMap<String, Arc<ProxyCache>>>,
192 healthcheck_failures: AtomicU64,
193}
194
195#[derive(Default)]
196struct ProxyCache {
197 hits: AtomicU64,
198 misses: AtomicU64,
199}
200
201impl ProxyMetrics {
202 pub fn request_duration(&self, rcode: &str) -> ProxyRequestDuration {
204 ProxyRequestDuration(Arc::clone(
205 self.0
206 .durations
207 .lock()
208 .unwrap()
209 .entry(rcode.into())
210 .or_insert_with(|| Arc::new(Histogram::new(TIME_BUCKETS))),
211 ))
212 }
213
214 pub fn connection_cache(&self, proto: &str) -> ProxyConnectionCache {
216 ProxyConnectionCache(Arc::clone(
217 self.0
218 .caches
219 .lock()
220 .unwrap()
221 .entry(proto.into())
222 .or_default(),
223 ))
224 }
225
226 pub fn healthcheck_failure(&self) {
227 self.0.healthcheck_failures.fetch_add(1, Ordering::Relaxed);
228 }
229}
230
231impl ProxyRequestDuration {
232 pub fn observe(&self, seconds: f64) {
233 self.0.observe(TIME_BUCKETS, seconds);
234 }
235}
236
237impl ProxyConnectionCache {
238 pub fn hit(&self) {
239 self.0.hits.fetch_add(1, Ordering::Relaxed);
240 }
241
242 pub fn miss(&self) {
243 self.0.misses.fetch_add(1, Ordering::Relaxed);
244 }
245}
246
247impl TransportStatus {
248 pub fn increment(&self) {
249 self.0.fetch_add(1, Ordering::Relaxed);
250 }
251}
252
253#[derive(Clone)]
254pub struct PluginMetrics {
255 block: Arc<Block>,
256 responses: Arc<Vec<AtomicU64>>,
257}
258
259struct Block {
260 server: String,
261 zone: String,
262 view: String,
263 requests: Vec<AtomicU64>,
264 do_requests: AtomicU64,
265 duration: Histogram,
266 request_size: [Histogram; 2],
267 response_size: [Histogram; 2],
268 default_responses: Arc<Vec<AtomicU64>>,
269 plugins: Mutex<BTreeMap<String, Arc<Vec<AtomicU64>>>>,
270 enabled: Mutex<BTreeMap<String, bool>>,
271}
272
273struct Histogram {
274 buckets: Vec<AtomicU64>,
275 sum: AtomicU64,
276}
277
278impl Histogram {
279 fn new(bounds: &[f64]) -> Self {
280 Self {
281 buckets: counters(bounds.len() + 1),
282 sum: AtomicU64::new(0),
283 }
284 }
285
286 fn observe(&self, bounds: &[f64], value: f64) {
287 let index = bounds
288 .iter()
289 .position(|bound| value <= *bound)
290 .unwrap_or(bounds.len());
291 let mut bits = self.sum.load(Ordering::Relaxed);
292 loop {
293 let next = (f64::from_bits(bits) + value).to_bits();
294 match self
295 .sum
296 .compare_exchange_weak(bits, next, Ordering::Relaxed, Ordering::Relaxed)
297 {
298 Ok(_) => break,
299 Err(actual) => bits = actual,
300 }
301 }
302 self.buckets[index].fetch_add(1, Ordering::Relaxed);
303 }
304
305 fn count(&self) -> u64 {
306 self.buckets.iter().map(|n| n.load(Ordering::Relaxed)).sum()
308 }
309}
310
311fn counters(count: usize) -> Vec<AtomicU64> {
312 (0..count).map(|_| AtomicU64::new(0)).collect()
313}
314
315impl Registry {
316 pub fn new(version: &str, revision: &str, rustc: &str) -> Self {
317 Self(Arc::new(RegistryInner {
318 version: version.into(),
319 revision: revision.into(),
320 rustc: rustc.into(),
321 blocks: Mutex::new(BTreeMap::new()),
322 statuses: Mutex::new(BTreeMap::new()),
323 proxies: Mutex::new(BTreeMap::new()),
324 caches: Mutex::new(BTreeMap::new()),
325 hosts: Mutex::new(BTreeMap::new()),
326 hosts_timestamp: Arc::new(AtomicU64::new(0)),
327 forward: Arc::new(ForwardCounters::default()),
328 forward_enabled: AtomicBool::new(false),
329 local: Arc::new(AtomicU64::new(0)),
330 local_enabled: AtomicBool::new(false),
331 process: AtomicBool::new(false),
332 health_enabled: AtomicBool::new(false),
333 health_duration: Histogram::new(HEALTH_BUCKETS),
334 health_failures: AtomicU64::new(0),
335 reload_enabled: AtomicBool::new(false),
336 reload_failures: AtomicU64::new(0),
337 reload_hash: Mutex::new(None),
338 kubernetes: KubernetesMetrics::default(),
339 }))
340 }
341
342 pub fn block(&self, server: &str, zone: &str, view: &str) -> BlockMetrics {
343 let key = (server.to_owned(), zone.to_owned(), view.to_owned());
344 let mut blocks = self.0.blocks.lock().unwrap();
345 BlockMetrics(Arc::clone(blocks.entry(key).or_insert_with(|| {
346 let default_responses = Arc::new(counters(RESPONSE_CODES));
347 let mut plugins = BTreeMap::new();
348 plugins.insert(String::new(), Arc::clone(&default_responses));
349 Arc::new(Block {
350 server: server.into(),
351 zone: zone.into(),
352 view: view.into(),
353 requests: counters((TYPES.len() + 1) * 4),
354 do_requests: AtomicU64::new(0),
355 duration: Histogram::new(TIME_BUCKETS),
356 request_size: std::array::from_fn(|_| Histogram::new(SIZE_BUCKETS)),
357 response_size: std::array::from_fn(|_| Histogram::new(SIZE_BUCKETS)),
358 default_responses,
359 plugins: Mutex::new(plugins),
360 enabled: Mutex::new(BTreeMap::new()),
361 })
362 })))
363 }
364
365 pub fn cache(&self, server: &str, zones: &str, view: &str) -> CacheMetrics {
367 let mut caches = self.0.caches.lock().unwrap();
368 CacheMetrics(Arc::clone(
369 caches
370 .entry((server.into(), zones.into(), view.into()))
371 .or_default(),
372 ))
373 }
374
375 pub fn hosts(&self, path: &str) -> HostsMetrics {
376 let gauge = Arc::clone(self.0.hosts.lock().unwrap().entry(path.into()).or_default());
377 HostsMetrics {
378 gauge,
379 timestamp: Arc::clone(&self.0.hosts_timestamp),
380 }
381 }
382
383 pub fn kubernetes(&self) -> KubernetesMetrics {
384 self.0.kubernetes.clone()
385 }
386
387 pub fn proxy(&self, proxy_name: &str, to: &str) -> ProxyMetrics {
389 let mut proxies = self.0.proxies.lock().unwrap();
390 ProxyMetrics(Arc::clone(
391 proxies
392 .entry((proxy_name.into(), to.into()))
393 .or_insert_with(|| {
394 Arc::new(Proxy {
395 proxy_name: proxy_name.into(),
396 to: to.into(),
397 durations: Mutex::new(BTreeMap::new()),
398 caches: Mutex::new(BTreeMap::new()),
399 healthcheck_failures: AtomicU64::new(0),
400 })
401 }),
402 ))
403 }
404
405 pub fn forward(&self) -> ForwardMetrics {
407 self.0.forward_enabled.store(true, Ordering::Relaxed);
408 ForwardMetrics(Arc::clone(&self.0.forward))
409 }
410
411 pub fn local(&self) -> Arc<AtomicU64> {
413 self.0.local_enabled.store(true, Ordering::Relaxed);
414 Arc::clone(&self.0.local)
415 }
416
417 pub fn enable_process_metrics(&self) {
419 self.0.process.store(true, Ordering::Relaxed);
420 }
421
422 pub fn enable_health_metrics(&self) {
423 self.0.health_enabled.store(true, Ordering::Relaxed);
424 }
425
426 pub fn reload_failed(&self) {
427 self.0.reload_enabled.store(true, Ordering::Relaxed);
428 self.0.reload_failures.fetch_add(1, Ordering::Relaxed);
429 }
430
431 pub fn enable_reload_metrics(&self) {
432 self.0.reload_enabled.store(true, Ordering::Relaxed);
433 }
434
435 pub fn reload_version(&self, hash: String) {
436 self.0.reload_enabled.store(true, Ordering::Relaxed);
437 *self.0.reload_hash.lock().unwrap() = Some(hash);
438 }
439
440 pub fn observe_health(&self, seconds: f64, failed: bool) {
441 self.0.health_duration.observe(HEALTH_BUCKETS, seconds);
442 if failed {
443 self.0.health_failures.fetch_add(1, Ordering::Relaxed);
444 }
445 }
446
447 pub fn transport_status(&self, transport: &str, server: &str, status: &str) -> TransportStatus {
449 assert!(matches!(transport, "https" | "https3" | "quic"));
450 let counter = self
451 .0
452 .statuses
453 .lock()
454 .unwrap()
455 .entry((transport.into(), server.into(), status.into()))
456 .or_default()
457 .clone();
458 TransportStatus(counter)
459 }
460
461 pub fn encode(&self, out: &mut String) {
463 self.0.kubernetes.encode(out);
464 if self.0.local_enabled.load(Ordering::Relaxed) {
465 header(
466 out,
467 "local_localhost_requests_total",
468 "Counter of special localhost subdomain requests.",
469 "counter",
470 );
471 writeln!(
472 out,
473 "pharos_local_localhost_requests_total {}",
474 self.0.local.load(Ordering::Relaxed)
475 )
476 .unwrap();
477 }
478 if self.0.reload_enabled.load(Ordering::Relaxed) {
479 header(
480 out,
481 "reload_failed_total",
482 "Counter of the number of failed reload attempts.",
483 "counter",
484 );
485 writeln!(
486 out,
487 "pharos_reload_failed_total {}",
488 self.0.reload_failures.load(Ordering::Relaxed)
489 )
490 .unwrap();
491 if let Some(hash) = self.0.reload_hash.lock().unwrap().as_ref() {
492 header(
493 out,
494 "reload_version_info",
495 "A metric with a constant '1' value labeled by hash, and value which type of hash generated.",
496 "gauge",
497 );
498 writeln!(
499 out,
500 "pharos_reload_version_info{{hash=\"sha512\",value=\"{hash}\"}} 1"
501 )
502 .unwrap();
503 }
504 }
505 if self.0.health_enabled.load(Ordering::Relaxed) {
506 let name = "health_request_duration_seconds";
507 header(
508 out,
509 name,
510 "Histogram of the time (in seconds) each request took.",
511 "histogram",
512 );
513 let histogram = &self.0.health_duration;
514 let mut count = 0;
515 for (index, bucket) in histogram.buckets.iter().enumerate() {
516 count += bucket.load(Ordering::Relaxed);
517 let bound = HEALTH_BUCKETS
518 .get(index)
519 .map_or_else(|| "+Inf".into(), |v| float(*v));
520 writeln!(
521 out,
522 "pharos_{name}_bucket{{le=\"{bound}\"}} {}",
523 float(count as f64)
524 )
525 .unwrap();
526 }
527 writeln!(
528 out,
529 "pharos_{name}_sum {}",
530 float(f64::from_bits(histogram.sum.load(Ordering::Relaxed)))
531 )
532 .unwrap();
533 writeln!(out, "pharos_{name}_count {}", float(count as f64)).unwrap();
534 header(
535 out,
536 "health_request_failures_total",
537 "The number of times the health check failed.",
538 "counter",
539 );
540 writeln!(
541 out,
542 "pharos_health_request_failures_total {}",
543 float(self.0.health_failures.load(Ordering::Relaxed) as f64)
544 )
545 .unwrap();
546 }
547 header(
548 out,
549 "build_info",
550 "A metric with a constant '1' value labeled by version, revision, and rustc from which Pharos was built.",
551 "gauge",
552 );
553 out.push_str("pharos_build_info{");
554 labels(
555 out,
556 &[
557 ("revision", &self.0.revision),
558 ("rustc", &self.0.rustc),
559 ("version", &self.0.version),
560 ],
561 );
562 out.push_str("} 1\n");
563
564 self.encode_caches(out);
565 let hosts = self.0.hosts.lock().unwrap();
566 if !hosts.is_empty() {
567 if hosts
568 .values()
569 .any(|gauge| gauge.initialized.load(Ordering::Relaxed))
570 {
571 header(
572 out,
573 "hosts_entries",
574 "The combined number of entries in hosts and Corefile.",
575 "gauge",
576 );
577 for (path, gauge) in hosts
578 .iter()
579 .filter(|(_, gauge)| gauge.initialized.load(Ordering::Relaxed))
580 {
581 sample(
582 out,
583 "hosts_entries",
584 &[("hostsfile", path)],
585 gauge.entries.load(Ordering::Relaxed),
586 );
587 }
588 }
589 header(
590 out,
591 "hosts_reload_timestamp_seconds",
592 "The timestamp of the last reload of hosts file.",
593 "gauge",
594 );
595 writeln!(
596 out,
597 "pharos_hosts_reload_timestamp_seconds {}",
598 float(f64::from_bits(
599 self.0.hosts_timestamp.load(Ordering::Relaxed)
600 ))
601 )
602 .unwrap();
603 }
604 drop(hosts);
605
606 let blocks = self.0.blocks.lock().unwrap();
607 let status = self.0.statuses.lock().unwrap();
608 if blocks
609 .values()
610 .any(|b| b.do_requests.load(Ordering::Relaxed) != 0)
611 {
612 header(
613 out,
614 "dns_do_requests_total",
615 "Counter of DNS requests with DO bit set per zone.",
616 "counter",
617 );
618 let mut sorted = SortedSamples::default();
619 for b in blocks.values() {
620 let n = b.do_requests.load(Ordering::Relaxed);
621 if n != 0 {
622 sorted.sample("dns_do_requests_total", &b.labels(), n);
623 }
624 }
625 sorted.write(out);
626 }
627 for (transport, help) in [
628 (
629 "https3",
630 "Counter of DoH3 responses per server and http status code.",
631 ),
632 (
633 "https",
634 "Counter of DoH responses per server and http status code.",
635 ),
636 (
637 "quic",
638 "Counter of DoQ responses per server and QUIC application code.",
639 ),
640 ] {
641 if status.keys().any(|(kind, _, _)| kind == transport) {
642 let name = format!("dns_{transport}_responses_total");
643 header(out, &name, help, "counter");
644 for ((kind, server, code), n) in
645 status.iter().filter(|((kind, _, _), _)| kind == transport)
646 {
647 let _ = kind;
648 sample(
649 out,
650 &name,
651 &[("server", server), ("status", code)],
652 n.load(Ordering::Relaxed),
653 );
654 }
655 }
656 }
657 histogram_family(
658 out,
659 &blocks,
660 "dns_request_duration_seconds",
661 "Histogram of the time (in seconds) each request took per zone.",
662 TIME_BUCKETS,
663 |b| vec![(None, &b.duration)],
664 );
665 histogram_family(
666 out,
667 &blocks,
668 "dns_request_size_bytes",
669 "Size of the EDNS0 UDP buffer in bytes (64K for TCP) per zone and protocol.",
670 SIZE_BUCKETS,
671 |b| {
672 vec![
673 (Some("tcp"), &b.request_size[1]),
674 (Some("udp"), &b.request_size[0]),
675 ]
676 },
677 );
678 if blocks
679 .values()
680 .any(|b| b.requests.iter().any(|n| n.load(Ordering::Relaxed) != 0))
681 {
682 header(
683 out,
684 "dns_requests_total",
685 "Counter of DNS requests made per zone, protocol and family.",
686 "counter",
687 );
688 let mut sorted = SortedSamples::default();
689 for b in blocks.values() {
690 for family in 0..2 {
691 for proto in 0..2 {
692 for typ in 0..=TYPES.len() {
693 let n = b.requests[(family * 2 + proto) * (TYPES.len() + 1) + typ]
694 .load(Ordering::Relaxed);
695 if n == 0 {
696 continue;
697 }
698 let ty = TYPES.get(typ).map_or("other", |(_, name)| *name);
699 sorted.sample(
700 "dns_requests_total",
701 &[
702 ("family", if family == 0 { "1" } else { "2" }),
703 ("proto", if proto == 0 { "udp" } else { "tcp" }),
704 ("server", &b.server),
705 ("type", ty),
706 ("view", &b.view),
707 ("zone", &b.zone),
708 ],
709 n,
710 );
711 }
712 }
713 }
714 }
715 sorted.write(out);
716 }
717 histogram_family(
718 out,
719 &blocks,
720 "dns_response_size_bytes",
721 "Size of the returned response in bytes.",
722 SIZE_BUCKETS,
723 |b| {
724 vec![
725 (Some("tcp"), &b.response_size[1]),
726 (Some("udp"), &b.response_size[0]),
727 ]
728 },
729 );
730 if blocks.values().any(|b| {
731 b.plugins
732 .lock()
733 .unwrap()
734 .values()
735 .any(|v| v.iter().any(|n| n.load(Ordering::Relaxed) != 0))
736 }) {
737 header(
738 out,
739 "dns_responses_total",
740 "Counter of response status codes.",
741 "counter",
742 );
743 let mut sorted = SortedSamples::default();
744 for b in blocks.values() {
745 for (plugin, counts) in b.plugins.lock().unwrap().iter() {
746 for (code, counter) in counts.iter().enumerate() {
747 let n = counter.load(Ordering::Relaxed);
748 if n != 0 {
749 let rcode = rcode(code as u16);
750 sorted.sample(
751 "dns_responses_total",
752 &[
753 ("plugin", plugin),
754 ("rcode", &rcode),
755 ("server", &b.server),
756 ("view", &b.view),
757 ("zone", &b.zone),
758 ],
759 n,
760 );
761 }
762 }
763 }
764 }
765 sorted.write(out);
766 }
767 header(
768 out,
769 "panics_total",
770 "A metrics that counts the number of panics.",
771 "counter",
772 );
773 writeln!(
774 out,
775 "pharos_panics_total {}",
776 float(crate::panics_total() as f64)
777 )
778 .unwrap();
779 if blocks
780 .values()
781 .any(|b| !b.enabled.lock().unwrap().is_empty())
782 {
783 header(
784 out,
785 "plugin_enabled",
786 "A metric that indicates whether a plugin is enabled on per server and zone basis.",
787 "gauge",
788 );
789 let mut sorted = SortedSamples::default();
790 for b in blocks.values() {
791 for (name, enabled) in b.enabled.lock().unwrap().iter() {
792 sorted.sample(
793 "plugin_enabled",
794 &[
795 ("name", name),
796 ("server", &b.server),
797 ("view", &b.view),
798 ("zone", &b.zone),
799 ],
800 u64::from(*enabled),
801 );
802 }
803 }
804 sorted.write(out);
805 }
806 self.encode_proxies(out);
807 if self.0.forward_enabled.load(Ordering::Relaxed) {
808 for (name, help, counter) in [
809 (
810 "forward_healthcheck_broken_total",
811 "Counter of the number of complete failures of the healthchecks.",
812 &self.0.forward.healthcheck_broken,
813 ),
814 (
815 "forward_max_concurrent_rejects_total",
816 "Counter of the number of queries rejected because the concurrent queries were at maximum.",
817 &self.0.forward.concurrent_rejects,
818 ),
819 ] {
820 header(out, name, help, "counter");
821 writeln!(out, "pharos_{name} {}", counter.load(Ordering::Relaxed)).unwrap();
822 }
823 }
824 if self.0.process.load(Ordering::Relaxed) {
825 crate::process_metrics::encode_process(out);
826 }
827 }
828
829 fn encode_caches(&self, out: &mut String) {
830 let caches = self.0.caches.lock().unwrap();
831 if caches.is_empty() {
832 return;
833 }
834 for (name, help, kind, typed, get) in [
835 (
836 "cache_drops_total",
837 "The number responses that are not cached, because the reply is malformed.",
838 "counter",
839 false,
840 (|c: &Cache| std::slice::from_ref(&c.drops)) as fn(&Cache) -> &[AtomicU64],
841 ),
842 (
843 "cache_entries",
844 "The number of elements in the cache.",
845 "gauge",
846 true,
847 |c: &Cache| &c.entries,
848 ),
849 (
850 "cache_evictions_total",
851 "The count of cache evictions.",
852 "counter",
853 true,
854 |c: &Cache| &c.evictions,
855 ),
856 (
857 "cache_hits_total",
858 "The count of cache hits.",
859 "counter",
860 true,
861 |c: &Cache| &c.hits,
862 ),
863 (
864 "cache_misses_total",
865 "The count of cache misses. Deprecated, derive misses from cache hits/requests counters.",
866 "counter",
867 false,
868 |c: &Cache| std::slice::from_ref(&c.misses),
869 ),
870 (
871 "cache_prefetch_total",
872 "The number of times the cache has prefetched a cached item.",
873 "counter",
874 false,
875 |c: &Cache| std::slice::from_ref(&c.prefetches),
876 ),
877 (
878 "cache_requests_total",
879 "The count of cache requests.",
880 "counter",
881 false,
882 |c: &Cache| std::slice::from_ref(&c.requests),
883 ),
884 (
885 "cache_served_stale_total",
886 "The number of requests served from stale cache entries.",
887 "counter",
888 false,
889 |c: &Cache| std::slice::from_ref(&c.served_stale),
890 ),
891 ] {
892 header(out, name, help, kind);
893 let mut sorted = SortedSamples::default();
894 for ((server, zones, view), cache) in caches.iter() {
895 if typed {
896 for (index, typ) in ["success", "denial"].iter().enumerate() {
897 sorted.sample(
898 name,
899 &[
900 ("server", server),
901 ("type", typ),
902 ("view", view),
903 ("zones", zones),
904 ],
905 get(cache)[index].load(Ordering::Relaxed),
906 );
907 }
908 } else {
909 sorted.sample(
910 name,
911 &[("server", server), ("view", view), ("zones", zones)],
912 get(cache)[0].load(Ordering::Relaxed),
913 );
914 }
915 }
916 sorted.write(out);
917 }
918 }
919
920 fn encode_proxies(&self, out: &mut String) {
921 let proxies = self.0.proxies.lock().unwrap();
922 for (name, help, hits) in [
923 (
924 "proxy_conn_cache_hits_total",
925 "Counter of connection cache hits per upstream and protocol.",
926 true,
927 ),
928 (
929 "proxy_conn_cache_misses_total",
930 "Counter of connection cache misses per upstream and protocol.",
931 false,
932 ),
933 ] {
934 let mut sorted = SortedSamples::default();
935 for proxy in proxies.values() {
936 for (proto, cache) in proxy.caches.lock().unwrap().iter() {
937 let n = if hits { &cache.hits } else { &cache.misses }.load(Ordering::Relaxed);
938 if n != 0 {
939 sorted.sample(
940 name,
941 &[
942 ("proto", proto),
943 ("proxy_name", &proxy.proxy_name),
944 ("to", &proxy.to),
945 ],
946 n,
947 );
948 }
949 }
950 }
951 if !sorted.0.is_empty() {
952 header(out, name, help, "counter");
953 sorted.write(out);
954 }
955 }
956 let mut sorted = SortedSamples::default();
957 for proxy in proxies.values() {
958 let n = proxy.healthcheck_failures.load(Ordering::Relaxed);
959 if n != 0 {
960 sorted.sample(
961 "proxy_healthcheck_failures_total",
962 &[("proxy_name", &proxy.proxy_name), ("to", &proxy.to)],
963 n,
964 );
965 }
966 }
967 if !sorted.0.is_empty() {
968 header(
969 out,
970 "proxy_healthcheck_failures_total",
971 "Counter of the number of failed healthchecks.",
972 "counter",
973 );
974 sorted.write(out);
975 }
976 let mut sorted = SortedSamples::default();
977 for proxy in proxies.values() {
978 for (rcode, histogram) in proxy.durations.lock().unwrap().iter() {
979 if histogram.count() != 0 {
980 let values = [
981 ("proxy_name", proxy.proxy_name.as_str()),
982 ("rcode", rcode.as_str()),
983 ("to", proxy.to.as_str()),
984 ];
985 let mut text = String::new();
986 encode_histogram(
987 &mut text,
988 "proxy_request_duration_seconds",
989 &values,
990 TIME_BUCKETS,
991 histogram,
992 );
993 sorted.insert(&values, text);
994 }
995 }
996 }
997 if !sorted.0.is_empty() {
998 header(
999 out,
1000 "proxy_request_duration_seconds",
1001 "Histogram of the time each request took.",
1002 "histogram",
1003 );
1004 sorted.write(out);
1005 }
1006 }
1007}
1008
1009impl Default for Registry {
1010 fn default() -> Self {
1011 let registry = Self::new(
1012 env!("CARGO_PKG_VERSION"),
1013 env!("PHAROS_REVISION"),
1014 env!("PHAROS_RUSTC"),
1015 );
1016 registry.enable_process_metrics();
1017 registry
1018 }
1019}
1020
1021impl Block {
1022 fn labels(&self) -> [(&str, &str); 3] {
1023 [
1024 ("server", &self.server),
1025 ("view", &self.view),
1026 ("zone", &self.zone),
1027 ]
1028 }
1029}
1030
1031impl BlockMetrics {
1032 pub fn plugin(&self, name: &str) -> PluginMetrics {
1033 let mut plugins = self.0.plugins.lock().unwrap();
1034 let responses = Arc::clone(
1035 plugins
1036 .entry(name.into())
1037 .or_insert_with(|| Arc::new(counters(RESPONSE_CODES))),
1038 );
1039 PluginMetrics {
1040 block: Arc::clone(&self.0),
1041 responses,
1042 }
1043 }
1044
1045 pub fn plugin_enabled(&self, name: &str) {
1046 self.0.enabled.lock().unwrap().insert(name.into(), true);
1047 }
1048
1049 #[allow(clippy::too_many_arguments)]
1050 pub fn record(
1051 &self,
1052 qtype: u16,
1053 tcp: bool,
1054 ipv6: bool,
1055 dnssec_ok: bool,
1056 request_bytes: usize,
1057 response_bytes: usize,
1058 rcode: u16,
1059 duration: f64,
1060 ) {
1061 record(
1063 &self.0,
1064 &self.0.default_responses,
1065 qtype,
1066 tcp,
1067 ipv6,
1068 dnssec_ok,
1069 request_bytes,
1070 response_bytes,
1071 rcode,
1072 duration,
1073 );
1074 }
1075}
1076
1077impl PluginMetrics {
1078 #[allow(clippy::too_many_arguments)]
1079 pub fn record(
1080 &self,
1081 qtype: u16,
1082 tcp: bool,
1083 ipv6: bool,
1084 dnssec_ok: bool,
1085 request_bytes: usize,
1086 response_bytes: usize,
1087 rcode: u16,
1088 duration: f64,
1089 ) {
1090 record(
1091 &self.block,
1092 &self.responses,
1093 qtype,
1094 tcp,
1095 ipv6,
1096 dnssec_ok,
1097 request_bytes,
1098 response_bytes,
1099 rcode,
1100 duration,
1101 );
1102 }
1103}
1104
1105#[allow(clippy::too_many_arguments)]
1106fn record(
1107 block: &Block,
1108 responses: &[AtomicU64],
1109 qtype: u16,
1110 tcp: bool,
1111 ipv6: bool,
1112 dnssec_ok: bool,
1113 request_bytes: usize,
1114 response_bytes: usize,
1115 rcode: u16,
1116 duration: f64,
1117) {
1118 assert!(
1120 usize::from(rcode) < RESPONSE_CODES,
1121 "DNS RCODE must fit in 12 bits"
1122 );
1123 let typ = TYPES
1124 .iter()
1125 .position(|(code, _)| *code == qtype)
1126 .unwrap_or(TYPES.len());
1127 let proto = usize::from(tcp);
1128 block.requests[(usize::from(ipv6) * 2 + proto) * (TYPES.len() + 1) + typ]
1129 .fetch_add(1, Ordering::Relaxed);
1130 if dnssec_ok {
1131 block.do_requests.fetch_add(1, Ordering::Relaxed);
1132 }
1133 block.duration.observe(TIME_BUCKETS, duration);
1134 block.request_size[proto].observe(SIZE_BUCKETS, request_bytes as f64);
1135 block.response_size[proto].observe(SIZE_BUCKETS, response_bytes as f64);
1136 responses[usize::from(rcode)].fetch_add(1, Ordering::Relaxed);
1137}
1138
1139fn header(out: &mut String, name: &str, help: &str, kind: &str) {
1141 writeln!(
1142 out,
1143 "# HELP pharos_{name} {help}\n# TYPE pharos_{name} {kind}"
1144 )
1145 .unwrap();
1146}
1147
1148fn labels(out: &mut String, values: &[(&str, &str)]) {
1150 for (index, (name, value)) in values.iter().enumerate() {
1151 if index != 0 {
1152 out.push(',');
1153 }
1154 write!(out, "{name}=\"").unwrap();
1155 for c in value.chars() {
1156 match c {
1157 '\\' => out.push_str("\\\\"),
1158 '"' => out.push_str("\\\""),
1159 '\n' => out.push_str("\\n"),
1160 _ => out.push(c),
1161 }
1162 }
1163 out.push('"');
1164 }
1165}
1166
1167fn sample(out: &mut String, name: &str, values: &[(&str, &str)], n: u64) {
1168 write!(out, "pharos_{name}{{").unwrap();
1169 labels(out, values);
1170 writeln!(out, "}} {}", float(n as f64)).unwrap();
1171}
1172
1173#[derive(Default)]
1174struct SortedSamples(Vec<(Vec<String>, String)>);
1175
1176impl SortedSamples {
1177 fn insert(&mut self, values: &[(&str, &str)], text: String) {
1178 self.0.push((
1179 values.iter().map(|(_, value)| (*value).into()).collect(),
1180 text,
1181 ));
1182 }
1183
1184 fn sample(&mut self, name: &str, values: &[(&str, &str)], n: u64) {
1185 let mut text = String::new();
1186 sample(&mut text, name, values, n);
1187 self.insert(values, text);
1188 }
1189
1190 fn write(mut self, out: &mut String) {
1191 self.0.sort_unstable_by(|a, b| a.0.cmp(&b.0));
1192 for (_, text) in self.0 {
1193 out.push_str(&text);
1194 }
1195 }
1196}
1197
1198pub(crate) fn float(value: f64) -> String {
1199 if value.is_nan() {
1201 return "NaN".into();
1202 }
1203 if value == f64::INFINITY {
1204 return "+Inf".into();
1205 }
1206 if value == f64::NEG_INFINITY {
1207 return "-Inf".into();
1208 }
1209 if value != 0. && (value.abs() < 0.0001 || value.abs() >= 1_000_000.) {
1210 let text = format!("{value:e}");
1211 let (mantissa, exp) = text.split_once('e').unwrap();
1212 let exponent: i32 = exp.parse().unwrap();
1213 return format!(
1214 "{mantissa}e{}{abs:02}",
1215 if exponent < 0 { '-' } else { '+' },
1216 abs = exponent.abs()
1217 );
1218 }
1219 value.to_string()
1220}
1221
1222fn histogram_family<'a>(
1223 out: &mut String,
1224 blocks: &'a BTreeMap<Key, Arc<Block>>,
1225 name: &str,
1226 help: &str,
1227 bounds: &[f64],
1228 get: impl Fn(&'a Block) -> Vec<(Option<&'static str>, &'a Histogram)>,
1229) {
1230 if !blocks
1231 .values()
1232 .any(|b| get(b).iter().any(|(_, h)| h.count() != 0))
1233 {
1234 return;
1235 }
1236 header(out, name, help, "histogram");
1237 let mut sorted = SortedSamples::default();
1238 for b in blocks.values() {
1239 for (proto, histogram) in get(b) {
1240 if histogram.count() == 0 {
1241 continue;
1242 }
1243 let mut values = Vec::new();
1244 if let Some(proto) = proto {
1245 values.push(("proto", proto));
1246 }
1247 values.extend(b.labels());
1248 let mut text = String::new();
1249 encode_histogram(&mut text, name, &values, bounds, histogram);
1250 sorted.insert(&values, text);
1251 }
1252 }
1253 sorted.write(out);
1254}
1255
1256fn encode_histogram(
1258 out: &mut String,
1259 name: &str,
1260 values: &[(&str, &str)],
1261 bounds: &[f64],
1262 histogram: &Histogram,
1263) {
1264 let mut count = 0;
1265 for (i, bucket) in histogram.buckets.iter().enumerate() {
1266 count += bucket.load(Ordering::Relaxed);
1267 write!(out, "pharos_{name}_bucket{{").unwrap();
1268 labels(out, values);
1269 let bound = bounds
1270 .get(i)
1271 .map_or_else(|| "+Inf".into(), |value| float(*value));
1272 writeln!(out, ",le=\"{bound}\"}} {}", float(count as f64)).unwrap();
1273 }
1274 write!(out, "pharos_{name}_sum{{").unwrap();
1275 labels(out, values);
1276 writeln!(
1277 out,
1278 "}} {}",
1279 float(f64::from_bits(histogram.sum.load(Ordering::Relaxed)))
1280 )
1281 .unwrap();
1282 write!(out, "pharos_{name}_count{{").unwrap();
1283 labels(out, values);
1284 writeln!(out, "}} {}", float(count as f64)).unwrap();
1285}
1286
1287fn rcode(code: u16) -> String {
1288 match code {
1289 0 => "NOERROR",
1290 1 => "FORMERR",
1291 2 => "SERVFAIL",
1292 3 => "NXDOMAIN",
1293 4 => "NOTIMP",
1294 5 => "REFUSED",
1295 6 => "YXDOMAIN",
1296 7 => "YXRRSET",
1297 8 => "NXRRSET",
1298 9 => "NOTAUTH",
1299 10 => "NOTZONE",
1300 11 => "DSOTYPENI",
1301 16 => "BADSIG",
1302 17 => "BADKEY",
1303 18 => "BADTIME",
1304 19 => "BADMODE",
1305 20 => "BADNAME",
1306 21 => "BADALG",
1307 22 => "BADTRUNC",
1308 23 => "BADCOOKIE",
1309 _ => return format!("RCODE{code}"),
1310 }
1311 .into()
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316 #[test]
1317 fn kubernetes_metrics_export_coredns_labels_and_buckets() {
1318 let registry = super::Registry::new("test", "test", "test");
1319 let metrics = registry.kubernetes();
1320 let request = metrics.request("GET", "api:6443");
1321 request.observe(0.02, "200");
1322 request.rate_limiter(0.003);
1323 metrics.programming_duration("cluster_ip", 0.002);
1324 let mut out = String::new();
1325 registry.encode(&mut out);
1326 for line in [
1327 "pharos_kubernetes_rest_client_request_duration_seconds_count{host=\"api:6443\",verb=\"GET\"} 1",
1328 "pharos_kubernetes_rest_client_rate_limiter_duration_seconds_count{host=\"api:6443\",verb=\"GET\"} 1",
1329 "pharos_kubernetes_rest_client_requests_total{code=\"200\",host=\"api:6443\",method=\"GET\"} 1",
1330 "pharos_kubernetes_dns_programming_duration_seconds_bucket{service_kind=\"cluster_ip\",le=\"0.002\"} 1",
1331 ] {
1332 assert!(
1333 out.lines().any(|actual| actual == line),
1334 "missing {line}\n{out}"
1335 );
1336 }
1337 }
1338 #[test]
1339 fn proxy_series_share_labels_and_escape_values() {
1340 let registry = super::Registry::new("test", "test", "test");
1341 let upstream = registry.proxy("forward\"\n", "192.0.2.1:53");
1342 let same = registry.proxy("forward\"\n", "192.0.2.1:53");
1343 let other = registry.proxy("forward\"\n", "192.0.2.2:53");
1344 upstream.request_duration("SERVFAIL").observe(9.0);
1345 same.request_duration("SERVFAIL").observe(0.0005);
1346 other.request_duration("NOERROR").observe(0.0);
1347 upstream.connection_cache("tcp").hit();
1348 same.connection_cache("tcp").hit();
1349 other.connection_cache("udp").miss();
1350 same.healthcheck_failure();
1351
1352 let mut out = String::new();
1353 registry.encode(&mut out);
1354 for expected in [
1355 "pharos_proxy_request_duration_seconds_bucket{proxy_name=\"forward\\\"\\n\",rcode=\"SERVFAIL\",to=\"192.0.2.1:53\",le=\"0.00025\"} 0",
1356 "pharos_proxy_request_duration_seconds_bucket{proxy_name=\"forward\\\"\\n\",rcode=\"SERVFAIL\",to=\"192.0.2.1:53\",le=\"8.192\"} 1",
1357 "pharos_proxy_request_duration_seconds_bucket{proxy_name=\"forward\\\"\\n\",rcode=\"SERVFAIL\",to=\"192.0.2.1:53\",le=\"+Inf\"} 2",
1358 "pharos_proxy_request_duration_seconds_count{proxy_name=\"forward\\\"\\n\",rcode=\"NOERROR\",to=\"192.0.2.2:53\"} 1",
1359 "pharos_proxy_conn_cache_hits_total{proto=\"tcp\",proxy_name=\"forward\\\"\\n\",to=\"192.0.2.1:53\"} 2",
1360 "pharos_proxy_conn_cache_misses_total{proto=\"udp\",proxy_name=\"forward\\\"\\n\",to=\"192.0.2.2:53\"} 1",
1361 "pharos_proxy_healthcheck_failures_total{proxy_name=\"forward\\\"\\n\",to=\"192.0.2.1:53\"} 1",
1362 ] {
1363 assert!(
1364 out.lines().any(|line| line == expected),
1365 "missing {expected}\n{out}"
1366 );
1367 }
1368 }
1369
1370 #[test]
1371 fn unused_proxy_handles_do_not_emit_series() {
1372 let registry = super::Registry::new("test", "test", "test");
1373 let proxy = registry.proxy("forward", "192.0.2.1:53");
1374 let _ = proxy.request_duration("NOERROR");
1375 let _ = proxy.connection_cache("udp");
1376 let mut out = String::new();
1377 registry.encode(&mut out);
1378 assert!(!out.contains("pharos_proxy_"));
1379 }
1380
1381 #[test]
1382 fn forward_counters_are_shared_and_exported() {
1383 let registry = super::Registry::new("test", "test", "test");
1384 let counters = registry.forward();
1385 let same = registry.forward();
1386 counters.healthcheck_broken();
1387 same.concurrent_reject();
1388 let mut out = String::new();
1389 registry.encode(&mut out);
1390 assert!(out.contains("pharos_forward_healthcheck_broken_total 1\n"));
1391 assert!(out.contains("pharos_forward_max_concurrent_rejects_total 1\n"));
1392 }
1393
1394 #[test]
1395 fn proxy_metrics_export_upstream_series() {
1396 let registry = super::Registry::new("test", "test", "test");
1397 let upstream = registry.proxy("forward", "dns://192.0.2.1:53");
1398 let duration = upstream.request_duration("NOERROR");
1399 let cache = upstream.connection_cache("udp");
1400 duration.observe(0.00025);
1401 duration.observe(0.001);
1402 cache.hit();
1403 cache.hit();
1404 cache.miss();
1405 upstream.healthcheck_failure();
1406 let mut out = String::new();
1407 registry.encode(&mut out);
1408
1409 for expected in [
1410 "# HELP pharos_proxy_request_duration_seconds Histogram of the time each request took.",
1411 "# TYPE pharos_proxy_request_duration_seconds histogram",
1412 "# HELP pharos_proxy_healthcheck_failures_total Counter of the number of failed healthchecks.",
1413 "# TYPE pharos_proxy_healthcheck_failures_total counter",
1414 "# HELP pharos_proxy_conn_cache_hits_total Counter of connection cache hits per upstream and protocol.",
1415 "# TYPE pharos_proxy_conn_cache_hits_total counter",
1416 "# HELP pharos_proxy_conn_cache_misses_total Counter of connection cache misses per upstream and protocol.",
1417 "# TYPE pharos_proxy_conn_cache_misses_total counter",
1418 "pharos_proxy_request_duration_seconds_bucket{proxy_name=\"forward\",rcode=\"NOERROR\",to=\"dns://192.0.2.1:53\",le=\"0.00025\"} 1",
1419 "pharos_proxy_request_duration_seconds_bucket{proxy_name=\"forward\",rcode=\"NOERROR\",to=\"dns://192.0.2.1:53\",le=\"0.001\"} 2",
1420 "pharos_proxy_request_duration_seconds_bucket{proxy_name=\"forward\",rcode=\"NOERROR\",to=\"dns://192.0.2.1:53\",le=\"+Inf\"} 2",
1421 "pharos_proxy_request_duration_seconds_sum{proxy_name=\"forward\",rcode=\"NOERROR\",to=\"dns://192.0.2.1:53\"} 0.00125",
1422 "pharos_proxy_request_duration_seconds_count{proxy_name=\"forward\",rcode=\"NOERROR\",to=\"dns://192.0.2.1:53\"} 2",
1423 "pharos_proxy_healthcheck_failures_total{proxy_name=\"forward\",to=\"dns://192.0.2.1:53\"} 1",
1424 "pharos_proxy_conn_cache_hits_total{proto=\"udp\",proxy_name=\"forward\",to=\"dns://192.0.2.1:53\"} 2",
1425 "pharos_proxy_conn_cache_misses_total{proto=\"udp\",proxy_name=\"forward\",to=\"dns://192.0.2.1:53\"} 1",
1426 ] {
1427 assert!(
1428 out.lines().any(|line| line == expected),
1429 "missing {expected}\n{out}"
1430 );
1431 }
1432 }
1433
1434 #[test]
1435 fn go_float_formatting() {
1436 for (value, expected) in [
1437 (0.0001, "0.0001"),
1438 (0.00001, "1e-05"),
1439 (999999., "999999"),
1440 (1_000_000., "1e+06"),
1441 (-0.0, "-0"),
1442 (f64::INFINITY, "+Inf"),
1443 (f64::NEG_INFINITY, "-Inf"),
1444 (f64::NAN, "NaN"),
1445 ] {
1446 assert_eq!(super::float(value), expected);
1447 }
1448 }
1449}