├── .gitignore ├── metrics-scope ├── .gitignore ├── README.md ├── Cargo.toml ├── src │ ├── client.rs │ ├── args.rs │ └── main.rs └── assets │ └── bma.svg ├── scope.gif ├── examples ├── client.rs ├── with-fallback.rs └── simple.rs ├── Cargo.toml ├── .github └── workflows │ └── ci.yml ├── proto.md ├── README.md ├── LICENSE ├── src └── lib.rs └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /metrics-scope/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /metrics-scope/README.md: -------------------------------------------------------------------------------- 1 | work in progress 2 | -------------------------------------------------------------------------------- /scope.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roboplc/metrics-exporter-scope/HEAD/scope.gif -------------------------------------------------------------------------------- /examples/client.rs: -------------------------------------------------------------------------------- 1 | use std::{net::TcpStream, time::Duration}; 2 | 3 | use metrics_exporter_scope::{protocol, ClientSettings}; 4 | 5 | fn main() -> Result<(), Box> { 6 | let mut client = TcpStream::connect("127.0.0.1:5001")?; 7 | let version = protocol::read_version(&client)?; 8 | if version != protocol::VERSION { 9 | return Err("Incompatible version".into()); 10 | } 11 | let settings = ClientSettings::new(Duration::from_millis(100)); 12 | protocol::write_client_settings(&mut client, &settings)?; 13 | loop { 14 | match protocol::read_packet(&mut client) { 15 | Ok(packet) => { 16 | dbg!(&packet); 17 | } 18 | Err(e) => { 19 | return Err(e.into()); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /metrics-scope/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "metrics-scope" 3 | version = "0.2.0" 4 | edition = "2021" 5 | authors = ["Serhij S. "] 6 | license = "Apache-2.0" 7 | repository = "https://github.com/roboplc/metrics-exporter-scope" 8 | description = "Metrics scope UI" 9 | readme = "README.md" 10 | keywords = ["metrics", "scope", "ui"] 11 | 12 | [dependencies] 13 | atomic_float = "1.0.0" 14 | bma-ts = "0.1.14" 15 | clap = { version = "4.5.16", features = ["derive"] } 16 | eframe = "0.32" 17 | egui = "0.32" 18 | egui_extras = { version = "0.32", features = ["default", "all_loaders"] } 19 | egui_plot = "0.33" 20 | metrics-exporter-scope = { version = "0.2.0" } 21 | once_cell = "1.19.0" 22 | parking_lot = "0.12.3" 23 | rtsc = { version = "0.3", features = ["parking_lot"] } 24 | 25 | [profile.release] 26 | strip = true 27 | -------------------------------------------------------------------------------- /examples/with-fallback.rs: -------------------------------------------------------------------------------- 1 | use metrics::gauge; 2 | use metrics_exporter_prometheus::PrometheusBuilder; 3 | use metrics_exporter_scope::ScopeBuilder; 4 | use rtsc::time::interval; 5 | use std::{thread, time::Duration}; 6 | 7 | #[allow(clippy::cast_precision_loss)] 8 | fn main() { 9 | // build runtime for exporter prometheus 10 | let runtime = tokio::runtime::Builder::new_current_thread() 11 | .enable_all() 12 | .build() 13 | .unwrap(); 14 | let (prometheus_exporter, prometheus_exporter_fut) = { 15 | let _g = runtime.enter(); 16 | PrometheusBuilder::new().build().unwrap() 17 | }; 18 | // build scope recorder with fallback to prometheus exporter 19 | ScopeBuilder::new() 20 | .with_fallback(Box::new(prometheus_exporter)) 21 | .install() 22 | .unwrap(); 23 | // start prometheus exporter 24 | thread::spawn(move || runtime.block_on(prometheus_exporter_fut)); 25 | // generate some metrics 26 | for (i, _) in interval(Duration::from_millis(10)).enumerate() { 27 | gauge!("~test", "plot" => "1", "color" => "blue").set((i % 1000) as f64); // to scope 28 | gauge!("~test2", "plot" => "2", "color" => "red").set((i % 50) as f64); // to scope 29 | gauge!("test3").set(i as f64); // to fallback 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "metrics-exporter-scope" 3 | version = "0.2.0" 4 | edition = "2021" 5 | authors = ["Serhij S. "] 6 | license = "Apache-2.0" 7 | repository = "https://github.com/roboplc/metrics-exporter-scope" 8 | description = "Metrics scope exporter" 9 | readme = "README.md" 10 | keywords = ["metrics", "scope", "exporter"] 11 | 12 | [dependencies] 13 | bma-ts = { version = "0.1.14", features = ["serde"] } 14 | rmp-serde = "1.3" 15 | rtsc = "0.3" 16 | serde = { version = "1.0", features = ["derive", "rc"] } 17 | thiserror = "1.0.63" 18 | tracing = "0.1.40" 19 | 20 | # MSRV 21 | metrics-legacy = { package = "metrics", version = "0.22", optional = true } 22 | metrics-util-legacy = { package = "metrics-util", version = "0.16", default-features = false, features = ["recency", "registry"], optional = true } 23 | 24 | # LATEST 25 | metrics = { version = "0.24", optional = true } 26 | metrics-util = { version = "0.18", default-features = false, features = ["recency", "registry"], optional = true } 27 | 28 | [features] 29 | default = ["latest"] 30 | msrv = ["metrics-legacy", "metrics-util-legacy"] 31 | latest = ["metrics", "metrics-util"] 32 | 33 | [dev-dependencies] 34 | tracing-subscriber = "0.3.18" 35 | metrics-exporter-prometheus = { version = "0.16", default-features = false, features = ["http-listener"] } 36 | tokio = { version = "1.39", features = ["rt"] } 37 | -------------------------------------------------------------------------------- /metrics-scope/src/client.rs: -------------------------------------------------------------------------------- 1 | use std::net::ToSocketAddrs; 2 | use std::time::Duration; 3 | use std::{net::TcpStream, thread}; 4 | 5 | use metrics_exporter_scope::{protocol, ClientSettings}; 6 | 7 | use crate::{Event, EventSender}; 8 | 9 | fn read_remote( 10 | addr: &str, 11 | tx: &EventSender, 12 | sampling_interval: Duration, 13 | timeout: Duration, 14 | ) -> Result<(), Box> { 15 | let addr = addr.to_socket_addrs()?.next().ok_or("Invalid address")?; 16 | let mut client = TcpStream::connect_timeout(&addr, timeout)?; 17 | client.set_nodelay(true)?; 18 | client.set_read_timeout(Some(timeout))?; 19 | let version = protocol::read_version(&client)?; 20 | if version != protocol::VERSION { 21 | return Err(format!("Unsupported version: {}", version).into()); 22 | } 23 | let settings = ClientSettings::new(sampling_interval); 24 | protocol::write_client_settings(&mut client, &settings)?; 25 | println!("Client connected: {}", addr); 26 | tx.send(Event::Connect).unwrap(); 27 | loop { 28 | let packet = protocol::read_packet(&mut client)?; 29 | tx.send(Event::Packet(packet)).ok(); 30 | } 31 | } 32 | 33 | pub fn reader(addr: &str, tx: EventSender, sampling_interval: Duration, timeout: Duration) { 34 | loop { 35 | if let Err(e) = read_remote(addr, &tx, sampling_interval, timeout) { 36 | tx.send(Event::Disconnect).ok(); 37 | eprintln!("Error: {:?}", e); 38 | } 39 | thread::sleep(Duration::from_secs(1)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use metrics::gauge; 2 | use metrics_exporter_scope::ScopeBuilder; 3 | use rtsc::time::interval; 4 | use std::time::Duration; 5 | 6 | /// Trait to round a float to a given number of digits (just for the demo) 7 | trait RoundTo { 8 | fn round_to(self, digits: u32) -> Self; 9 | } 10 | 11 | impl RoundTo for f64 { 12 | #[allow(clippy::cast_precision_loss)] 13 | fn round_to(self, digits: u32) -> Self { 14 | let factor = 10u64.pow(digits) as f64; 15 | (self * factor).round() / factor 16 | } 17 | } 18 | 19 | #[allow(clippy::cast_precision_loss)] 20 | fn main() { 21 | // build scope recorder 22 | ScopeBuilder::new().install().unwrap(); 23 | // generate some metrics 24 | for (i, _) in interval(Duration::from_millis(10)).enumerate() { 25 | gauge!("~i%1000").set((i % 1000) as f64); // to scope, default plot, default color 26 | gauge!("~i_sin", "plot" => "trig", "color" => "orange") 27 | .set((i as f64 / 90.0).sin().round_to(3)); // to scope 28 | gauge!("~i_cos", "plot" => "trig", "color" => "#9cf") 29 | .set((i as f64 / 90.0).cos().round_to(3)); // to scope 30 | gauge!("~i_sin2", "plot" => "trig2", "color" => "yellow") 31 | .set((i as f64 / 180.0).sin().round_to(3)); // to scope 32 | gauge!("~i_cos2", "plot" => "trig2", "color" => "cyan") 33 | .set((i as f64 / 180.0).cos().round_to(3)); // to scope 34 | gauge!("~i%100", "plot" => "counts", "color" => "#336699").set((i % 100) as f64); // to scope 35 | gauge!("iteration").set(i as f64); // ignored 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "*" ] 6 | pull_request: 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: cargo test default 21 | run: cargo test --all-targets 22 | fmt: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: cargo fmt 27 | run: cargo fmt --check 28 | clippy: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v3 32 | - name: cargo clippy 33 | run: | 34 | cargo clippy --all-targets -- -W clippy::all -W clippy::pedantic \ 35 | -A clippy::used-underscore-binding \ 36 | -A clippy::doc_markdown \ 37 | -A clippy::needless_pass_by_value \ 38 | -A clippy::must_use_candidate \ 39 | -A clippy::return_self_not_must_use \ 40 | -A clippy::missing_errors_doc \ 41 | -A clippy::single_match \ 42 | -A clippy::uninlined_format_args \ 43 | -A clippy::no_effect_underscore_binding 44 | client-test: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v3 48 | - name: cargo test 49 | run: cd metrics-scope && cargo test --verbose --all-features --all-targets 50 | client-fmt: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v3 54 | - name: cargo fmt 55 | run: cd metrics-scope && cargo fmt --check 56 | client-clippy: 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v3 60 | - name: cargo clippy 61 | run: | 62 | cd metrics-scope && cargo clippy --all-targets -- -W clippy::all -W clippy::pedantic \ 63 | -A clippy::used-underscore-binding \ 64 | -A clippy::doc_markdown \ 65 | -A clippy::needless_pass_by_value \ 66 | -A clippy::must_use_candidate \ 67 | -A clippy::return_self_not_must_use \ 68 | -A clippy::missing_errors_doc \ 69 | -A clippy::single_match \ 70 | -A clippy::uninlined_format_args \ 71 | -A clippy::no_effect_underscore_binding 72 | -------------------------------------------------------------------------------- /proto.md: -------------------------------------------------------------------------------- 1 | # metrics-exporter-scope communication protocol 2 | 3 | The metrics-exporter-scope communication protocol is a TCP protocol to exchange 4 | metrics between the server process and clients. 5 | 6 | ## Defaults 7 | 8 | The default port is `5001`. 9 | 10 | ## Data serialization 11 | 12 | The serialization format is a MessagePack. 13 | 14 | ## Chat 15 | 16 | * After the connection is established, the server writes 2-byte VERSION packet, 17 | which contains the protocol version, encoded in little-endian. If the client 18 | does not support the protocol version, it should close the connection. 19 | 20 | * The client sends serialized `ClientSettings` structure: 21 | ```json 22 | { 23 | "sampling_interval": 1000000 24 | } 25 | ``` 26 | 27 | where 28 | 29 | * `sampling_interval` is the interval the server thread should sample the 30 | metrics and send them to the client. The interval is specified in 31 | nanoseconds. 32 | 33 | ## Communication 34 | 35 | The server sends serialized metrics snapshot packets as well as information 36 | ones to the client. The first packet is always an information one. The client 37 | should determine the packet type according to its structure. 38 | 39 | ### Information packets 40 | 41 | The information packets are used to send metrics metadata to the client. The 42 | server sends such packets every 5 seconds (by default). 43 | 44 | ```json 45 | { 46 | "metrics": { 47 | "metric_name": { 48 | "labels": { 49 | "label_name": "label_value", 50 | "label_name2": "label_value2" 51 | } 52 | }, 53 | "metric_name2": { 54 | "labels": { 55 | "label_name": "label_value", 56 | "label_name2": "label_value2" 57 | } 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | The client may use metrics labels as hints for displaying the data. The default 64 | labels are: 65 | 66 | * `plot` group several metrics into a single plot 67 | 68 | * `color` specify the color of the line in the plot 69 | 70 | ### Snapshot packets 71 | 72 | The snapshot packets contain the actual metrics data. The server sends such 73 | using the sampling interval, specified during `Chat` phase. 74 | 75 | ```json 76 | { 77 | "t": 1234567890, 78 | "d": { 79 | "metric_name": 123.4, 80 | "metric_name2": 456.7 81 | } 82 | } 83 | ``` 84 | 85 | where 86 | 87 | * `t` is the timestamp of the snapshot, in nanoseconds. The timestamp is monotonic 88 | and relative to the time point the `Communication` phase started at. 89 | 90 | * `d` is the dictionary of metrics. The keys are metric names, and the values 91 | are float numbers. 92 | 93 | The payload always contains state of all metrics at the moment of the snapshot, 94 | despite the metrics have been changed or not. 95 | -------------------------------------------------------------------------------- /metrics-scope/assets/bma.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | metrics-exporter-scope 3 | crates.io page 4 | docs.rs page 5 |

6 | 7 | An oscilloscope for the Rust [metrics 8 | ecosystem](https://github.com/metrics-rs/metrics). 9 | 10 | # Introduction 11 | 12 | `metrics-exporter-scope` is an exporter for 13 | [metrics](https://crates.io/crates/metrics) which is designed to output 14 | frequently changed metrics as snapshots. The functionality pretty is similar to 15 | a classic oscilloscope and use cases are similar as well: the crate is 16 | developed to sample metrics with high (1Hz+) frequencies and is mostly used to 17 | display real-time data from embedded systems, industrial automation 18 | controllers, robotics, network devices, etc. 19 | 20 | 22 | 23 | `metrics-exporter-scope` is a part of the [RoboPLC](https://roboplc.com) 24 | project. 25 | 26 | ## Usage 27 | 28 | ### Setup 29 | 30 | Installing the exporter with the default settings (binds to `0.0.0.0:5001`): 31 | 32 | ```rust,no_run 33 | metrics_exporter_scope::ScopeBuilder::new().install().unwrap(); 34 | ``` 35 | 36 | ### Defining metrics 37 | 38 | **The exporter works with `Gauge` metrics only**. 39 | 40 | The crate is designed as a secondary metrics exporter, all scope-metrics, must 41 | be prefixed with `~` char. Metrics without the prefix are either ignored or 42 | exported by the primary program exporter. 43 | 44 | ```rust,no_run 45 | use metrics::gauge; 46 | 47 | gauge!("~my_metric").set(42.0); 48 | ``` 49 | 50 | ### Metric labels 51 | 52 | Metrics can have additional labels, some are used by the client program to 53 | configure plots using `plot` label key. 54 | 55 | ```rust,no_run 56 | use metrics::gauge; 57 | 58 | gauge!("~my_metric", "plot" => "plot1").set(42.0); 59 | gauge!("~my_metric2", "plot" => "plot1").set(42.0); 60 | ``` 61 | 62 | The above example groups two metrics into the same plot. 63 | 64 | ### Metric colors 65 | 66 | `color` label key is used as a hint for the client program to set the color of 67 | a plot line the metric is associated with. 68 | 69 | ```rust,no_run 70 | use metrics::gauge; 71 | 72 | gauge!("~my_metric", "color" => "blue").set(42.0); 73 | gauge!("~my_metric2", "color" => "#99ccff").set(42.0); 74 | ``` 75 | 76 | Colors, supported by the client program are: `red`, `green`, `blue`, `yellow`, 77 | `cyan`, `magenta`, `orange`, `white`, `black`. A color also can be set as a 78 | RGB, using either `#RRGGBB` or `#RGB` format. 79 | 80 | ### Falling back to the primary exporter 81 | 82 | If a metric is not prefixed with `~`, it is processed by the primary exporter. 83 | 84 | ```rust,ignore 85 | let primary_recorder = SomePrimaryMetricsRecorder::new().build(); 86 | 87 | metrics_exporter_scope::ScopeBuilder::new() 88 | .with_fallback(Box::new(primary_recorder)) 89 | .install() 90 | .unwrap(); 91 | ``` 92 | 93 | A fall-back example can be found in 94 | [examples/with-fallback.rs](https://github.com/roboplc/metrics-exporter-scope/blob/main/examples/with-fallback.rs). 95 | 96 | ## Client installation 97 | 98 | The repository contains a client implementation for the oscilloscope, which is 99 | available for all major desktop platforms: 100 | 101 | ```shell 102 | cargo install metrics-scope 103 | ``` 104 | 105 | Client features: 106 | 107 | * Real-time data visualization 108 | 109 | * Multiple metrics support 110 | 111 | * Simple moving averages 112 | 113 | * Triggers 114 | 115 | Navigation: 116 | 117 | * `L` - toggle chart legends 118 | 119 | * `F5` - reset chart views and clear active trigger events 120 | 121 | * `P` - pause/resume chart updates 122 | 123 | * `Mouse click + drag` - move chart view (X-axis is moved for all charts) 124 | 125 | * `Ctrl + mouse wheel` - zoom charts in/out 126 | 127 | * `Mouse double click` - reset chart view 128 | 129 | ## Real-time safety 130 | 131 | The exporter does not contain any locks and is safe to be used in real-time 132 | programs. It is recommended to install the server in a dedicated thread. 133 | 134 | ## MSRV 135 | 136 | By default, the crate supports the latest `metrics` version, follow the 137 | [metrics README](https://github.com/metrics-rs/metrics) for the actual minimum 138 | supported Rust version details. 139 | 140 | The crate also can be built to support MSRV 1.68.0, by disabling the default 141 | features and enabling the `msrv` feature: 142 | 143 | ```toml 144 | [dependencies] 145 | metrics-exporter-scope = { version = "0.1", default-features = false, features = ["msrv"] } 146 | ``` 147 | 148 | If set, `metrics` version 0.22 is used. 149 | -------------------------------------------------------------------------------- /metrics-scope/src/args.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use clap::{ 4 | builder::{TypedValueParser, ValueParserFactory}, 5 | Parser, ValueEnum, 6 | }; 7 | 8 | #[derive(Parser)] 9 | pub struct Args { 10 | #[clap(help = "HOST[:PORT], the default port is 5001")] 11 | pub source: String, 12 | #[clap( 13 | short = 's', 14 | long, 15 | help = "Sampling interval in seconds", 16 | default_value = "0.1" 17 | )] 18 | pub sampling_interval: f64, 19 | #[clap( 20 | short = 't', 21 | long, 22 | help = "Network timeout in seconds", 23 | default_value = "10" 24 | )] 25 | pub timeout: u64, 26 | #[clap(long, help = "Hide legend")] 27 | pub hide_legend: bool, 28 | #[clap( 29 | short = 'w', 30 | long, 31 | help = "Time window in seconds", 32 | default_value = "10" 33 | )] 34 | pub time_window: f32, 35 | #[clap(long, help = "Chart columns", default_value = "2")] 36 | pub chart_cols: f32, 37 | #[clap(long, help = "Chart aspect ratio", default_value = "2")] 38 | pub chart_aspect: f32, 39 | #[clap(long, help = "Override system colors")] 40 | pub theme: Option, 41 | #[clap( 42 | long = "y-range", 43 | value_name = "RANGE", 44 | help = "Predefined Y-range (plot=[min],[max])" 45 | )] 46 | pub predefined_y_range: Vec, 47 | #[clap( 48 | long = "sma", 49 | value_name = "WINDOW", 50 | help = "Predefined SMA (plot/metric=window or metric=window)" 51 | )] 52 | pub predefined_sma: Vec, 53 | #[clap( 54 | long = "trigger", 55 | value_name = "TRIGGER", 56 | help = "Predefined Trigger (plot/metric=[below],[above] or metric=[below],[above])" 57 | )] 58 | pub predefined_trigger: Vec, 59 | } 60 | 61 | pub trait ToPlotConfigMap { 62 | fn to_plot_config_map(&self) -> BTreeMap; 63 | } 64 | 65 | impl ToPlotConfigMap for Vec { 66 | fn to_plot_config_map(&self) -> BTreeMap { 67 | let mut map = BTreeMap::new(); 68 | for PredefinedYRange { key, min, max } in self { 69 | map.insert( 70 | key.to_owned(), 71 | PlotConfig { 72 | min: *min, 73 | max: *max, 74 | }, 75 | ); 76 | } 77 | map 78 | } 79 | } 80 | 81 | pub trait ToSmaMap { 82 | fn to_sma_map(&self) -> BTreeMap; 83 | } 84 | 85 | impl ToSmaMap for Vec { 86 | fn to_sma_map(&self) -> BTreeMap { 87 | let mut map = BTreeMap::new(); 88 | for PredefinedSma { key, value } in self { 89 | map.insert(key.to_owned(), *value); 90 | } 91 | map 92 | } 93 | } 94 | 95 | pub trait ToTriggerMap { 96 | fn to_trigger_map(&self) -> BTreeMap; 97 | } 98 | 99 | impl ToTriggerMap for Vec { 100 | fn to_trigger_map(&self) -> BTreeMap { 101 | let mut map = BTreeMap::new(); 102 | for PredefinedTrigger { key, below, above } in self { 103 | map.insert( 104 | key.to_owned(), 105 | TriggerConfig { 106 | below: *below, 107 | above: *above, 108 | }, 109 | ); 110 | } 111 | map 112 | } 113 | } 114 | 115 | #[derive(Clone)] 116 | pub struct PredefinedYRange { 117 | key: String, 118 | min: Option, 119 | max: Option, 120 | } 121 | 122 | impl ValueParserFactory for PredefinedYRange { 123 | type Parser = PredefinedYRangeParser; 124 | fn value_parser() -> Self::Parser { 125 | PredefinedYRangeParser 126 | } 127 | } 128 | 129 | #[derive(Clone)] 130 | pub struct PredefinedYRangeParser; 131 | 132 | impl TypedValueParser for PredefinedYRangeParser { 133 | type Value = PredefinedYRange; 134 | 135 | fn parse_ref( 136 | &self, 137 | _cmd: &clap::Command, 138 | _arg: Option<&clap::Arg>, 139 | value: &std::ffi::OsStr, 140 | ) -> Result { 141 | let v = value.to_str().ok_or_else(|| { 142 | clap::error::Error::raw( 143 | clap::error::ErrorKind::ValueValidation, 144 | "Invalid Y-range string", 145 | ) 146 | })?; 147 | let mut sp = v.splitn(2, '='); 148 | let key = sp.next().unwrap(); 149 | let value_str = sp.next().ok_or_else(|| { 150 | clap::error::Error::raw( 151 | clap::error::ErrorKind::ValueValidation, 152 | "Invalid Y-range - no value", 153 | ) 154 | })?; 155 | let mut value_sp = value_str.splitn(2, ','); 156 | let min_str = value_sp.next().unwrap(); 157 | let max_str = value_sp.next().ok_or_else(|| { 158 | clap::error::Error::raw( 159 | clap::error::ErrorKind::ValueValidation, 160 | "Invalid Y-range - no max value", 161 | ) 162 | })?; 163 | let min = if min_str.is_empty() { 164 | None 165 | } else { 166 | Some(min_str.parse().map_err(|_| { 167 | clap::error::Error::raw( 168 | clap::error::ErrorKind::ValueValidation, 169 | "Invalid Y-range - min must be a float", 170 | ) 171 | })?) 172 | }; 173 | let max = if max_str.is_empty() { 174 | None 175 | } else { 176 | Some(max_str.parse().map_err(|_| { 177 | clap::error::Error::raw( 178 | clap::error::ErrorKind::ValueValidation, 179 | "Invalid Y-range - max must be a float", 180 | ) 181 | })?) 182 | }; 183 | Ok(PredefinedYRange { 184 | key: key.to_owned(), 185 | min, 186 | max, 187 | }) 188 | } 189 | } 190 | 191 | #[derive(Clone)] 192 | pub struct PredefinedSma { 193 | key: String, 194 | value: usize, 195 | } 196 | 197 | impl ValueParserFactory for PredefinedSma { 198 | type Parser = PredefinedSmaParser; 199 | fn value_parser() -> Self::Parser { 200 | PredefinedSmaParser 201 | } 202 | } 203 | 204 | #[derive(Clone)] 205 | pub struct PredefinedSmaParser; 206 | 207 | impl TypedValueParser for PredefinedSmaParser { 208 | type Value = PredefinedSma; 209 | 210 | fn parse_ref( 211 | &self, 212 | _cmd: &clap::Command, 213 | _arg: Option<&clap::Arg>, 214 | value: &std::ffi::OsStr, 215 | ) -> Result { 216 | let v = value.to_str().ok_or_else(|| { 217 | clap::error::Error::raw( 218 | clap::error::ErrorKind::ValueValidation, 219 | "Invalid SMA string", 220 | ) 221 | })?; 222 | let mut sp = v.splitn(2, '='); 223 | let key = sp.next().unwrap(); 224 | let value_str = sp.next().ok_or_else(|| { 225 | clap::error::Error::raw( 226 | clap::error::ErrorKind::ValueValidation, 227 | "Invalid SMA - no value", 228 | ) 229 | })?; 230 | let value: usize = value_str.parse().map_err(|_| { 231 | clap::error::Error::raw( 232 | clap::error::ErrorKind::ValueValidation, 233 | "Invalid SMA - window must be an unsigned integer", 234 | ) 235 | })?; 236 | Ok(PredefinedSma { 237 | key: key.to_owned(), 238 | value, 239 | }) 240 | } 241 | } 242 | 243 | #[derive(Clone)] 244 | pub struct PredefinedTrigger { 245 | key: String, 246 | below: Option, 247 | above: Option, 248 | } 249 | 250 | impl ValueParserFactory for PredefinedTrigger { 251 | type Parser = PredefinedTriggerParser; 252 | fn value_parser() -> Self::Parser { 253 | PredefinedTriggerParser 254 | } 255 | } 256 | 257 | #[derive(Clone)] 258 | pub struct PredefinedTriggerParser; 259 | 260 | impl TypedValueParser for PredefinedTriggerParser { 261 | type Value = PredefinedTrigger; 262 | 263 | fn parse_ref( 264 | &self, 265 | _cmd: &clap::Command, 266 | _arg: Option<&clap::Arg>, 267 | value: &std::ffi::OsStr, 268 | ) -> Result { 269 | let v = value.to_str().ok_or_else(|| { 270 | clap::error::Error::raw( 271 | clap::error::ErrorKind::ValueValidation, 272 | "Invalid Trigger string", 273 | ) 274 | })?; 275 | let mut sp = v.splitn(2, '='); 276 | let key = sp.next().unwrap(); 277 | let value_str = sp.next().ok_or_else(|| { 278 | clap::error::Error::raw( 279 | clap::error::ErrorKind::ValueValidation, 280 | "Invalid Trigger - no value", 281 | ) 282 | })?; 283 | let mut value_sp = value_str.splitn(2, ','); 284 | let below_str = value_sp.next().unwrap(); 285 | let above_str = value_sp.next().ok_or_else(|| { 286 | clap::error::Error::raw( 287 | clap::error::ErrorKind::ValueValidation, 288 | "Invalid Trigger - no above value", 289 | ) 290 | })?; 291 | let below = if below_str.is_empty() { 292 | None 293 | } else { 294 | Some(below_str.parse().map_err(|_| { 295 | clap::error::Error::raw( 296 | clap::error::ErrorKind::ValueValidation, 297 | "Invalid Trigger - below must be a float", 298 | ) 299 | })?) 300 | }; 301 | let above = if above_str.is_empty() { 302 | None 303 | } else { 304 | Some(above_str.parse().map_err(|_| { 305 | clap::error::Error::raw( 306 | clap::error::ErrorKind::ValueValidation, 307 | "Invalid Trigger - above must be a float", 308 | ) 309 | })?) 310 | }; 311 | Ok(PredefinedTrigger { 312 | key: key.to_owned(), 313 | below, 314 | above, 315 | }) 316 | } 317 | } 318 | 319 | #[derive(ValueEnum, Clone)] 320 | pub enum Theme { 321 | #[clap(name = "dark")] 322 | Dark, 323 | #[clap(name = "light")] 324 | Light, 325 | } 326 | 327 | pub struct TriggerConfig { 328 | pub below: Option, 329 | pub above: Option, 330 | } 331 | 332 | pub struct PlotConfig { 333 | pub min: Option, 334 | pub max: Option, 335 | } 336 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![ doc = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/", "README.md" ) ) ] 2 | #![deny(missing_docs)] 3 | #[cfg(feature = "msrv")] 4 | extern crate metrics_legacy as metrics; 5 | #[cfg(feature = "msrv")] 6 | extern crate metrics_util_legacy as metrics_util; 7 | 8 | use std::{ 9 | collections::BTreeMap, 10 | net::{SocketAddr, TcpListener, TcpStream}, 11 | num::TryFromIntError, 12 | sync::{atomic::Ordering, Arc}, 13 | thread, 14 | time::Duration, 15 | }; 16 | 17 | use bma_ts::Monotonic; 18 | use metrics::{Key, Recorder}; 19 | use metrics_util::registry::{AtomicStorage, GenerationalStorage, Registry}; 20 | use rtsc::time::interval; 21 | use serde::{Deserialize, Serialize}; 22 | use tracing::{error, info}; 23 | 24 | /// Crate error type 25 | #[derive(thiserror::Error, Debug)] 26 | pub enum Error { 27 | /// I/O errors 28 | #[error("io error: {0}")] 29 | Io(#[from] std::io::Error), 30 | /// Data serialization errors 31 | #[error("encode error: {0}")] 32 | Encode(#[from] rmp_serde::encode::Error), 33 | /// Data deserialization errors 34 | #[error("decode error: {0}")] 35 | Decode(#[from] rmp_serde::decode::Error), 36 | /// Recorder setup errors 37 | #[error("set recorder error: {0}")] 38 | SetRecorder(#[from] metrics::SetRecorderError), 39 | /// Other errors 40 | #[error("{0}")] 41 | Other(String), 42 | } 43 | 44 | impl From for Error { 45 | fn from(error: TryFromIntError) -> Self { 46 | Self::Other(error.to_string()) 47 | } 48 | } 49 | 50 | const CLIENT_CHAT_TIMEOUT: Duration = Duration::from_secs(60); 51 | 52 | const SEND_INFO_INTERVAL: Duration = Duration::from_secs(5); 53 | 54 | const SERVER_THREAD_NAME: &str = "MScopeSrv"; 55 | 56 | /// Communication protocol 57 | pub mod protocol { 58 | 59 | /// Current protocol version 60 | pub const VERSION: u16 = 1; 61 | 62 | use std::io::{Read, Write}; 63 | 64 | use crate::{ClientSettings, Error, Packet}; 65 | use serde::{Deserialize, Serialize}; 66 | 67 | /// Read protocol version from a stream 68 | pub fn read_version(mut stream: R) -> Result 69 | where 70 | R: Read, 71 | { 72 | let buf = &mut [0u8; 2]; 73 | stream.read_exact(buf)?; 74 | Ok(u16::from_le_bytes(*buf)) 75 | } 76 | 77 | /// Write protocol version to a stream 78 | pub fn write_version(mut stream: W) -> Result<(), Error> 79 | where 80 | W: Write, 81 | { 82 | stream.write_all(&VERSION.to_le_bytes())?; 83 | Ok(()) 84 | } 85 | 86 | /// Read a packet from a stream 87 | pub fn read_packet(stream: R) -> Result 88 | where 89 | R: Read, 90 | { 91 | read(stream) 92 | } 93 | 94 | /// Write a packet to a stream 95 | pub fn write_packet(stream: W, packet: &Packet) -> Result<(), Error> 96 | where 97 | W: Write, 98 | { 99 | write(stream, packet) 100 | } 101 | 102 | /// Read client settings from a stream 103 | pub fn read_client_settings(stream: R) -> Result 104 | where 105 | R: Read, 106 | { 107 | read(stream) 108 | } 109 | 110 | /// Write client settings to a stream 111 | pub fn write_client_settings(stream: W, settings: &ClientSettings) -> Result<(), Error> 112 | where 113 | W: Write, 114 | { 115 | write(stream, settings) 116 | } 117 | 118 | fn write(mut stream: W, data: D) -> Result<(), Error> 119 | where 120 | W: Write, 121 | D: Serialize, 122 | { 123 | let data = rmp_serde::to_vec_named(&data)?; 124 | stream.write_all(&u32::try_from(data.len())?.to_le_bytes())?; 125 | stream.write_all(&data)?; 126 | Ok(()) 127 | } 128 | 129 | fn read(mut stream: R) -> Result 130 | where 131 | R: Read, 132 | D: for<'de> Deserialize<'de>, 133 | { 134 | let buf = &mut [0u8; 4]; 135 | stream.read_exact(buf)?; 136 | let len = usize::try_from(u32::from_le_bytes(*buf))?; 137 | let mut buf = vec![0u8; len]; 138 | stream.read_exact(&mut buf)?; 139 | Ok(rmp_serde::from_slice(&buf)?) 140 | } 141 | } 142 | 143 | /// Communication packets 144 | #[derive(Clone, Serialize, Deserialize, Debug)] 145 | #[serde(untagged)] 146 | pub enum Packet { 147 | /// Information packet (metrics metadata) 148 | Info(Info), 149 | /// Snapshot packet (metrics data) 150 | Snapshot(Snapshot), 151 | } 152 | 153 | /// Client settings 154 | #[derive(Clone, Serialize, Deserialize, Debug)] 155 | pub struct ClientSettings { 156 | sampling_interval: u64, 157 | } 158 | 159 | impl ClientSettings { 160 | /// # Panics 161 | /// 162 | /// Panics if the duration is too large to fit into a u64. 163 | pub fn new(sampling_interval: Duration) -> Self { 164 | Self { 165 | sampling_interval: u64::try_from(sampling_interval.as_nanos()).unwrap(), 166 | } 167 | } 168 | } 169 | 170 | /// Information packet 171 | #[derive(Clone, Serialize, Deserialize, Debug)] 172 | pub struct Info { 173 | metrics: BTreeMap, 174 | } 175 | 176 | impl Info { 177 | /// Get metrics metadata map 178 | pub fn metrics(&self) -> &BTreeMap { 179 | &self.metrics 180 | } 181 | } 182 | 183 | /// Metrics metadata 184 | #[derive(Clone, Serialize, Deserialize, Debug)] 185 | pub struct MetricInfo { 186 | labels: BTreeMap, 187 | } 188 | 189 | impl MetricInfo { 190 | /// Metric labels map 191 | pub fn labels(&self) -> &BTreeMap { 192 | &self.labels 193 | } 194 | } 195 | 196 | /// Snapshot packet 197 | #[derive(Clone, Serialize, Deserialize, Debug)] 198 | pub struct Snapshot { 199 | t: Monotonic, 200 | d: BTreeMap, 201 | } 202 | 203 | impl Snapshot { 204 | /// Snapshot timestamp (monotonic, relative to the communication start) 205 | pub fn ts(&self) -> Monotonic { 206 | self.t 207 | } 208 | /// Snapshot data map (metric name -> value) 209 | pub fn data(&self) -> &BTreeMap { 210 | &self.d 211 | } 212 | /// Snapshot data map mutable (metric name -> value) 213 | pub fn data_mut(&mut self) -> &mut BTreeMap { 214 | &mut self.d 215 | } 216 | /// Take snapshot data map 217 | pub fn take_data(&mut self) -> BTreeMap { 218 | std::mem::take(&mut self.d) 219 | } 220 | } 221 | 222 | /// Exporter builder 223 | pub struct ScopeBuilder { 224 | addr: SocketAddr, 225 | fallback: Option>, 226 | } 227 | 228 | impl Default for ScopeBuilder { 229 | fn default() -> Self { 230 | Self::new() 231 | } 232 | } 233 | 234 | impl ScopeBuilder { 235 | /// Create a new exporter builder 236 | pub fn new() -> Self { 237 | Self { 238 | addr: (std::net::Ipv4Addr::UNSPECIFIED, 5001).into(), 239 | fallback: None, 240 | } 241 | } 242 | /// Set the server listening address and port 243 | pub fn with_addr>(mut self, addr: A) -> Self { 244 | self.addr = addr.into(); 245 | self 246 | } 247 | /// Set the fallback recorder 248 | pub fn with_fallback(mut self, fallback: Box) -> Self { 249 | self.fallback = Some(fallback); 250 | self 251 | } 252 | /// Build the exporter's recorder 253 | pub fn build(self) -> ScopeRecorder { 254 | ScopeRecorder::build(self.addr, self.fallback) 255 | } 256 | /// Build the exporter's recorder and install it as the global recorder 257 | pub fn install(self) -> Result<(), Error> { 258 | self.build().install() 259 | } 260 | } 261 | 262 | /// Scope recorder 263 | #[derive(Clone)] 264 | pub struct ScopeRecorder { 265 | inner: Arc, 266 | fallback: Arc>>, 267 | } 268 | 269 | impl ScopeRecorder { 270 | fn build>( 271 | addr: A, 272 | fallback: Option>, 273 | ) -> Self { 274 | Self { 275 | inner: Arc::new(Inner::new(addr.into())), 276 | fallback: fallback.into(), 277 | } 278 | } 279 | /// # Panics 280 | /// 281 | /// Panics if the global recorder has already been set. 282 | pub fn install(self) -> Result<(), Error> { 283 | self.spawn_tasks()?; 284 | metrics::set_global_recorder(self).map_err(Into::into) 285 | } 286 | fn spawn_tasks(&self) -> Result<(), std::io::Error> { 287 | self.inner.spawn_server(self.inner.addr)?; 288 | Ok(()) 289 | } 290 | } 291 | 292 | struct Inner { 293 | registry: Registry>, 294 | addr: SocketAddr, 295 | } 296 | 297 | impl Inner { 298 | fn new(addr: SocketAddr) -> Self { 299 | let registry = Registry::new(GenerationalStorage::new(AtomicStorage)); 300 | Self { registry, addr } 301 | } 302 | fn snapshot(&self, t: Monotonic) -> Snapshot { 303 | let handles = self.registry.get_gauge_handles(); 304 | let mut map = BTreeMap::new(); 305 | for (key, gauge) in handles { 306 | let name = key.name(); 307 | let value = f64::from_bits(gauge.get_inner().load(Ordering::Acquire)); 308 | map.insert(name[1..].to_string(), value); 309 | } 310 | Snapshot { t, d: map } 311 | } 312 | fn info(&self) -> Info { 313 | let info = self 314 | .registry 315 | .get_gauge_handles() 316 | .iter() 317 | .map(|(key, _)| { 318 | let labels = key 319 | .labels() 320 | .map(|label| (label.key().to_owned(), label.value().to_owned())); 321 | ( 322 | key.name()[1..].to_string(), 323 | MetricInfo { 324 | labels: labels.collect(), 325 | }, 326 | ) 327 | }) 328 | .collect(); 329 | Info { metrics: info } 330 | } 331 | fn spawn_server(self: &Arc, addr: SocketAddr) -> Result<(), std::io::Error> { 332 | let listener = TcpListener::bind(addr)?; 333 | let metrics_scope = self.clone(); 334 | thread::Builder::new() 335 | .name(SERVER_THREAD_NAME.to_owned()) 336 | .spawn(move || { 337 | while let Ok((stream, addr)) = listener.accept() { 338 | info!(?addr, "client connected"); 339 | let metrics_scope = metrics_scope.clone(); 340 | thread::spawn(move || { 341 | if let Err(error) = handle_client(stream, metrics_scope) { 342 | error!(?addr, ?error, "client error, disconnected"); 343 | } else { 344 | info!(?addr, "client disconnected"); 345 | } 346 | }); 347 | } 348 | })?; 349 | Ok(()) 350 | } 351 | } 352 | fn handle_client(mut stream: TcpStream, metrics_scope: Arc) -> Result<(), Error> { 353 | stream.set_read_timeout(Some(CLIENT_CHAT_TIMEOUT))?; 354 | stream.set_write_timeout(Some(CLIENT_CHAT_TIMEOUT))?; 355 | stream.set_nodelay(true)?; 356 | protocol::write_version(&mut stream)?; 357 | let clients_settings = protocol::read_client_settings(&mut stream)?; 358 | stream.set_read_timeout(None)?; 359 | stream.set_write_timeout(None)?; 360 | protocol::write_packet(&mut stream, &Packet::Info(metrics_scope.info()))?; 361 | let mut last_info_sent = Monotonic::now(); 362 | let int_ns = u128::from(clients_settings.sampling_interval); 363 | let start = Monotonic::now(); 364 | for _ in interval(Duration::from_nanos(clients_settings.sampling_interval)) { 365 | let ts = Monotonic::from_nanos( 366 | (start.elapsed().as_nanos() / int_ns * int_ns) 367 | .try_into() 368 | .unwrap(), 369 | ); 370 | let packet = Packet::Snapshot(metrics_scope.snapshot(ts)); 371 | if protocol::write_packet(&mut stream, &packet).is_err() { 372 | break; 373 | } 374 | if last_info_sent.elapsed() >= SEND_INFO_INTERVAL { 375 | let packet = Packet::Info(metrics_scope.info()); 376 | if protocol::write_packet(&mut stream, &packet).is_err() { 377 | break; 378 | } 379 | last_info_sent = Monotonic::now(); 380 | } 381 | } 382 | Ok(()) 383 | } 384 | 385 | impl Recorder for ScopeRecorder { 386 | fn describe_counter( 387 | &self, 388 | key: metrics::KeyName, 389 | unit: Option, 390 | description: metrics::SharedString, 391 | ) { 392 | if let Some(fallback) = self.fallback.as_ref() { 393 | fallback.describe_counter(key, unit, description); 394 | } 395 | } 396 | 397 | fn describe_gauge( 398 | &self, 399 | key: metrics::KeyName, 400 | unit: Option, 401 | description: metrics::SharedString, 402 | ) { 403 | if let Some(fallback) = self.fallback.as_ref() { 404 | fallback.describe_gauge(key, unit, description); 405 | } 406 | } 407 | 408 | fn describe_histogram( 409 | &self, 410 | key: metrics::KeyName, 411 | unit: Option, 412 | description: metrics::SharedString, 413 | ) { 414 | if let Some(fallback) = self.fallback.as_ref() { 415 | fallback.describe_histogram(key, unit, description); 416 | } 417 | } 418 | 419 | fn register_counter( 420 | &self, 421 | key: &metrics::Key, 422 | metadata: &metrics::Metadata<'_>, 423 | ) -> metrics::Counter { 424 | if let Some(fallback) = self.fallback.as_ref() { 425 | fallback.register_counter(key, metadata) 426 | } else { 427 | metrics::Counter::noop() 428 | } 429 | } 430 | 431 | fn register_gauge( 432 | &self, 433 | key: &metrics::Key, 434 | metadata: &metrics::Metadata<'_>, 435 | ) -> metrics::Gauge { 436 | if key.name().starts_with('~') { 437 | self.inner 438 | .registry 439 | .get_or_create_gauge(key, |c| c.clone().into()) 440 | } else if let Some(fallback) = self.fallback.as_ref() { 441 | fallback.register_gauge(key, metadata) 442 | } else { 443 | metrics::Gauge::noop() 444 | } 445 | } 446 | 447 | fn register_histogram( 448 | &self, 449 | key: &metrics::Key, 450 | metadata: &metrics::Metadata<'_>, 451 | ) -> metrics::Histogram { 452 | if let Some(fallback) = self.fallback.as_ref() { 453 | fallback.register_histogram(key, metadata) 454 | } else { 455 | metrics::Histogram::noop() 456 | } 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 25 | dependencies = [ 26 | "cfg-if", 27 | "once_cell", 28 | "version_check", 29 | "zerocopy", 30 | ] 31 | 32 | [[package]] 33 | name = "atomic-waker" 34 | version = "1.1.2" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 37 | 38 | [[package]] 39 | name = "autocfg" 40 | version = "1.3.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 43 | 44 | [[package]] 45 | name = "backtrace" 46 | version = "0.3.73" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 49 | dependencies = [ 50 | "addr2line", 51 | "cc", 52 | "cfg-if", 53 | "libc", 54 | "miniz_oxide", 55 | "object", 56 | "rustc-demangle", 57 | ] 58 | 59 | [[package]] 60 | name = "base64" 61 | version = "0.22.1" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 64 | 65 | [[package]] 66 | name = "bitflags" 67 | version = "2.6.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 70 | 71 | [[package]] 72 | name = "bma-ts" 73 | version = "0.1.14" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "1c0cd34a34174c74d6984fdf7dee88fce5befb990562c1ff0bebbfdc79bd7bd1" 76 | dependencies = [ 77 | "nix", 78 | "once_cell", 79 | "serde", 80 | "thiserror", 81 | ] 82 | 83 | [[package]] 84 | name = "bumpalo" 85 | version = "3.16.0" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 88 | 89 | [[package]] 90 | name = "byteorder" 91 | version = "1.5.0" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 94 | 95 | [[package]] 96 | name = "bytes" 97 | version = "1.7.1" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" 100 | 101 | [[package]] 102 | name = "cc" 103 | version = "1.1.14" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "50d2eb3cd3d1bf4529e31c215ee6f93ec5a3d536d9f578f93d9d33ee19562932" 106 | dependencies = [ 107 | "shlex", 108 | ] 109 | 110 | [[package]] 111 | name = "cfg-if" 112 | version = "1.0.0" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 115 | 116 | [[package]] 117 | name = "crossbeam-epoch" 118 | version = "0.9.18" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 121 | dependencies = [ 122 | "crossbeam-utils", 123 | ] 124 | 125 | [[package]] 126 | name = "crossbeam-utils" 127 | version = "0.8.20" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 130 | 131 | [[package]] 132 | name = "darling" 133 | version = "0.13.4" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" 136 | dependencies = [ 137 | "darling_core", 138 | "darling_macro", 139 | ] 140 | 141 | [[package]] 142 | name = "darling_core" 143 | version = "0.13.4" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" 146 | dependencies = [ 147 | "fnv", 148 | "ident_case", 149 | "proc-macro2", 150 | "quote", 151 | "strsim", 152 | "syn 1.0.109", 153 | ] 154 | 155 | [[package]] 156 | name = "darling_macro" 157 | version = "0.13.4" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" 160 | dependencies = [ 161 | "darling_core", 162 | "quote", 163 | "syn 1.0.109", 164 | ] 165 | 166 | [[package]] 167 | name = "equivalent" 168 | version = "1.0.1" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 171 | 172 | [[package]] 173 | name = "fnv" 174 | version = "1.0.7" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 177 | 178 | [[package]] 179 | name = "foldhash" 180 | version = "0.1.3" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" 183 | 184 | [[package]] 185 | name = "futures-channel" 186 | version = "0.3.30" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 189 | dependencies = [ 190 | "futures-core", 191 | ] 192 | 193 | [[package]] 194 | name = "futures-core" 195 | version = "0.3.30" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 198 | 199 | [[package]] 200 | name = "futures-sink" 201 | version = "0.3.30" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 204 | 205 | [[package]] 206 | name = "futures-task" 207 | version = "0.3.30" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 210 | 211 | [[package]] 212 | name = "futures-util" 213 | version = "0.3.30" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 216 | dependencies = [ 217 | "futures-core", 218 | "futures-task", 219 | "pin-project-lite", 220 | "pin-utils", 221 | ] 222 | 223 | [[package]] 224 | name = "gimli" 225 | version = "0.29.0" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 228 | 229 | [[package]] 230 | name = "h2" 231 | version = "0.4.6" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" 234 | dependencies = [ 235 | "atomic-waker", 236 | "bytes", 237 | "fnv", 238 | "futures-core", 239 | "futures-sink", 240 | "http", 241 | "indexmap", 242 | "slab", 243 | "tokio", 244 | "tokio-util", 245 | "tracing", 246 | ] 247 | 248 | [[package]] 249 | name = "hashbrown" 250 | version = "0.14.5" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 253 | dependencies = [ 254 | "ahash", 255 | ] 256 | 257 | [[package]] 258 | name = "hashbrown" 259 | version = "0.15.1" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" 262 | dependencies = [ 263 | "foldhash", 264 | ] 265 | 266 | [[package]] 267 | name = "hermit-abi" 268 | version = "0.3.9" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 271 | 272 | [[package]] 273 | name = "http" 274 | version = "1.1.0" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 277 | dependencies = [ 278 | "bytes", 279 | "fnv", 280 | "itoa", 281 | ] 282 | 283 | [[package]] 284 | name = "http-body" 285 | version = "1.0.1" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 288 | dependencies = [ 289 | "bytes", 290 | "http", 291 | ] 292 | 293 | [[package]] 294 | name = "http-body-util" 295 | version = "0.1.2" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 298 | dependencies = [ 299 | "bytes", 300 | "futures-util", 301 | "http", 302 | "http-body", 303 | "pin-project-lite", 304 | ] 305 | 306 | [[package]] 307 | name = "httparse" 308 | version = "1.9.4" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" 311 | 312 | [[package]] 313 | name = "httpdate" 314 | version = "1.0.3" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 317 | 318 | [[package]] 319 | name = "hyper" 320 | version = "1.4.1" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" 323 | dependencies = [ 324 | "bytes", 325 | "futures-channel", 326 | "futures-util", 327 | "h2", 328 | "http", 329 | "http-body", 330 | "httparse", 331 | "httpdate", 332 | "itoa", 333 | "pin-project-lite", 334 | "smallvec", 335 | "tokio", 336 | "want", 337 | ] 338 | 339 | [[package]] 340 | name = "hyper-util" 341 | version = "0.1.7" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" 344 | dependencies = [ 345 | "bytes", 346 | "futures-channel", 347 | "futures-util", 348 | "http", 349 | "http-body", 350 | "hyper", 351 | "pin-project-lite", 352 | "socket2", 353 | "tokio", 354 | "tower", 355 | "tower-service", 356 | "tracing", 357 | ] 358 | 359 | [[package]] 360 | name = "ident_case" 361 | version = "1.0.1" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 364 | 365 | [[package]] 366 | name = "indexmap" 367 | version = "2.6.0" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" 370 | dependencies = [ 371 | "equivalent", 372 | "hashbrown 0.15.1", 373 | ] 374 | 375 | [[package]] 376 | name = "ipnet" 377 | version = "2.9.0" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" 380 | 381 | [[package]] 382 | name = "itoa" 383 | version = "1.0.11" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 386 | 387 | [[package]] 388 | name = "js-sys" 389 | version = "0.3.70" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" 392 | dependencies = [ 393 | "wasm-bindgen", 394 | ] 395 | 396 | [[package]] 397 | name = "lazy_static" 398 | version = "1.5.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 401 | 402 | [[package]] 403 | name = "libc" 404 | version = "0.2.158" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 407 | 408 | [[package]] 409 | name = "linux-futex" 410 | version = "1.0.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "ca13551457f48de808024473c481e3978eb6ea5e7f345433eb0f9c3b325fc1ca" 413 | dependencies = [ 414 | "libc", 415 | ] 416 | 417 | [[package]] 418 | name = "lock_api" 419 | version = "0.4.12" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 422 | dependencies = [ 423 | "autocfg", 424 | "scopeguard", 425 | ] 426 | 427 | [[package]] 428 | name = "log" 429 | version = "0.4.22" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 432 | 433 | [[package]] 434 | name = "memchr" 435 | version = "2.7.4" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 438 | 439 | [[package]] 440 | name = "metrics" 441 | version = "0.22.3" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "2be3cbd384d4e955b231c895ce10685e3d8260c5ccffae898c96c723b0772835" 444 | dependencies = [ 445 | "ahash", 446 | "portable-atomic", 447 | ] 448 | 449 | [[package]] 450 | name = "metrics" 451 | version = "0.24.0" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "8ae428771d17306715c5091d446327d1cfdedc82185c65ba8423ab404e45bf10" 454 | dependencies = [ 455 | "ahash", 456 | "portable-atomic", 457 | ] 458 | 459 | [[package]] 460 | name = "metrics-exporter-prometheus" 461 | version = "0.16.0" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "85b6f8152da6d7892ff1b7a1c0fa3f435e92b5918ad67035c3bb432111d9a29b" 464 | dependencies = [ 465 | "base64", 466 | "http-body-util", 467 | "hyper", 468 | "hyper-util", 469 | "indexmap", 470 | "ipnet", 471 | "metrics 0.24.0", 472 | "metrics-util 0.18.0", 473 | "quanta", 474 | "thiserror", 475 | "tokio", 476 | "tracing", 477 | ] 478 | 479 | [[package]] 480 | name = "metrics-exporter-scope" 481 | version = "0.2.0" 482 | dependencies = [ 483 | "bma-ts", 484 | "metrics 0.22.3", 485 | "metrics 0.24.0", 486 | "metrics-exporter-prometheus", 487 | "metrics-util 0.16.3", 488 | "metrics-util 0.18.0", 489 | "rmp-serde", 490 | "rtsc", 491 | "serde", 492 | "thiserror", 493 | "tokio", 494 | "tracing", 495 | "tracing-subscriber", 496 | ] 497 | 498 | [[package]] 499 | name = "metrics-util" 500 | version = "0.16.3" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "8b07a5eb561b8cbc16be2d216faf7757f9baf3bfb94dbb0fae3df8387a5bb47f" 503 | dependencies = [ 504 | "crossbeam-epoch", 505 | "crossbeam-utils", 506 | "hashbrown 0.14.5", 507 | "metrics 0.22.3", 508 | "num_cpus", 509 | "quanta", 510 | ] 511 | 512 | [[package]] 513 | name = "metrics-util" 514 | version = "0.18.0" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "15b482df36c13dd1869d73d14d28cd4855fbd6cfc32294bee109908a9f4a4ed7" 517 | dependencies = [ 518 | "crossbeam-epoch", 519 | "crossbeam-utils", 520 | "hashbrown 0.15.1", 521 | "metrics 0.24.0", 522 | "quanta", 523 | "sketches-ddsketch", 524 | ] 525 | 526 | [[package]] 527 | name = "miniz_oxide" 528 | version = "0.7.4" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 531 | dependencies = [ 532 | "adler", 533 | ] 534 | 535 | [[package]] 536 | name = "mio" 537 | version = "1.0.2" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 540 | dependencies = [ 541 | "hermit-abi", 542 | "libc", 543 | "wasi", 544 | "windows-sys", 545 | ] 546 | 547 | [[package]] 548 | name = "nix" 549 | version = "0.27.1" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" 552 | dependencies = [ 553 | "bitflags", 554 | "cfg-if", 555 | "libc", 556 | ] 557 | 558 | [[package]] 559 | name = "nu-ansi-term" 560 | version = "0.46.0" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 563 | dependencies = [ 564 | "overload", 565 | "winapi", 566 | ] 567 | 568 | [[package]] 569 | name = "num-traits" 570 | version = "0.2.19" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 573 | dependencies = [ 574 | "autocfg", 575 | ] 576 | 577 | [[package]] 578 | name = "num_cpus" 579 | version = "1.16.0" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 582 | dependencies = [ 583 | "hermit-abi", 584 | "libc", 585 | ] 586 | 587 | [[package]] 588 | name = "object" 589 | version = "0.36.3" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" 592 | dependencies = [ 593 | "memchr", 594 | ] 595 | 596 | [[package]] 597 | name = "object-id" 598 | version = "0.1.4" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "c587bd1cd63959a8520442afc0f92a875d83deea175c7b48dd9f104a2c5070a9" 601 | 602 | [[package]] 603 | name = "once_cell" 604 | version = "1.19.0" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 607 | 608 | [[package]] 609 | name = "overload" 610 | version = "0.1.1" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 613 | 614 | [[package]] 615 | name = "parking_lot_core" 616 | version = "0.9.10" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 619 | dependencies = [ 620 | "cfg-if", 621 | "libc", 622 | "redox_syscall", 623 | "smallvec", 624 | "windows-targets", 625 | ] 626 | 627 | [[package]] 628 | name = "parking_lot_rt" 629 | version = "0.12.1" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "d7f9d3dba7751cf4b5ac6f3e9423cb573df1c927bb41808118ff1eb132dc9a50" 632 | dependencies = [ 633 | "lock_api", 634 | "parking_lot_core", 635 | ] 636 | 637 | [[package]] 638 | name = "paste" 639 | version = "1.0.15" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 642 | 643 | [[package]] 644 | name = "pin-project" 645 | version = "1.1.5" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" 648 | dependencies = [ 649 | "pin-project-internal", 650 | ] 651 | 652 | [[package]] 653 | name = "pin-project-internal" 654 | version = "1.1.5" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" 657 | dependencies = [ 658 | "proc-macro2", 659 | "quote", 660 | "syn 2.0.76", 661 | ] 662 | 663 | [[package]] 664 | name = "pin-project-lite" 665 | version = "0.2.14" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 668 | 669 | [[package]] 670 | name = "pin-utils" 671 | version = "0.1.0" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 674 | 675 | [[package]] 676 | name = "portable-atomic" 677 | version = "1.7.0" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" 680 | 681 | [[package]] 682 | name = "proc-macro2" 683 | version = "1.0.86" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 686 | dependencies = [ 687 | "unicode-ident", 688 | ] 689 | 690 | [[package]] 691 | name = "quanta" 692 | version = "0.12.3" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" 695 | dependencies = [ 696 | "crossbeam-utils", 697 | "libc", 698 | "once_cell", 699 | "raw-cpuid", 700 | "wasi", 701 | "web-sys", 702 | "winapi", 703 | ] 704 | 705 | [[package]] 706 | name = "quote" 707 | version = "1.0.37" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 710 | dependencies = [ 711 | "proc-macro2", 712 | ] 713 | 714 | [[package]] 715 | name = "raw-cpuid" 716 | version = "11.1.0" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d" 719 | dependencies = [ 720 | "bitflags", 721 | ] 722 | 723 | [[package]] 724 | name = "redox_syscall" 725 | version = "0.5.3" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" 728 | dependencies = [ 729 | "bitflags", 730 | ] 731 | 732 | [[package]] 733 | name = "rmp" 734 | version = "0.8.14" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" 737 | dependencies = [ 738 | "byteorder", 739 | "num-traits", 740 | "paste", 741 | ] 742 | 743 | [[package]] 744 | name = "rmp-serde" 745 | version = "1.3.0" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" 748 | dependencies = [ 749 | "byteorder", 750 | "rmp", 751 | "serde", 752 | ] 753 | 754 | [[package]] 755 | name = "rtsc" 756 | version = "0.3.2" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "679c8b184bde821ebc2136ba8e598f7e2f349e8c93b96f5f551e8581e11ba453" 759 | dependencies = [ 760 | "bma-ts", 761 | "libc", 762 | "linux-futex", 763 | "lock_api", 764 | "object-id", 765 | "parking_lot_rt", 766 | "pin-project", 767 | "rtsc-derive", 768 | "thiserror", 769 | ] 770 | 771 | [[package]] 772 | name = "rtsc-derive" 773 | version = "0.1.0" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "478d8d87ce09eb1fb06a75170e56123af8bd9ed5ec1b5b2ad8d0d9dac2fc6f52" 776 | dependencies = [ 777 | "darling", 778 | "proc-macro2", 779 | "quote", 780 | "syn 1.0.109", 781 | ] 782 | 783 | [[package]] 784 | name = "rustc-demangle" 785 | version = "0.1.24" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 788 | 789 | [[package]] 790 | name = "scopeguard" 791 | version = "1.2.0" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 794 | 795 | [[package]] 796 | name = "serde" 797 | version = "1.0.209" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" 800 | dependencies = [ 801 | "serde_derive", 802 | ] 803 | 804 | [[package]] 805 | name = "serde_derive" 806 | version = "1.0.209" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" 809 | dependencies = [ 810 | "proc-macro2", 811 | "quote", 812 | "syn 2.0.76", 813 | ] 814 | 815 | [[package]] 816 | name = "sharded-slab" 817 | version = "0.1.7" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 820 | dependencies = [ 821 | "lazy_static", 822 | ] 823 | 824 | [[package]] 825 | name = "shlex" 826 | version = "1.3.0" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 829 | 830 | [[package]] 831 | name = "sketches-ddsketch" 832 | version = "0.3.0" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" 835 | 836 | [[package]] 837 | name = "slab" 838 | version = "0.4.9" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 841 | dependencies = [ 842 | "autocfg", 843 | ] 844 | 845 | [[package]] 846 | name = "smallvec" 847 | version = "1.13.2" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 850 | 851 | [[package]] 852 | name = "socket2" 853 | version = "0.5.7" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 856 | dependencies = [ 857 | "libc", 858 | "windows-sys", 859 | ] 860 | 861 | [[package]] 862 | name = "strsim" 863 | version = "0.10.0" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 866 | 867 | [[package]] 868 | name = "syn" 869 | version = "1.0.109" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 872 | dependencies = [ 873 | "proc-macro2", 874 | "quote", 875 | "unicode-ident", 876 | ] 877 | 878 | [[package]] 879 | name = "syn" 880 | version = "2.0.76" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" 883 | dependencies = [ 884 | "proc-macro2", 885 | "quote", 886 | "unicode-ident", 887 | ] 888 | 889 | [[package]] 890 | name = "thiserror" 891 | version = "1.0.63" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 894 | dependencies = [ 895 | "thiserror-impl", 896 | ] 897 | 898 | [[package]] 899 | name = "thiserror-impl" 900 | version = "1.0.63" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 903 | dependencies = [ 904 | "proc-macro2", 905 | "quote", 906 | "syn 2.0.76", 907 | ] 908 | 909 | [[package]] 910 | name = "thread_local" 911 | version = "1.1.8" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 914 | dependencies = [ 915 | "cfg-if", 916 | "once_cell", 917 | ] 918 | 919 | [[package]] 920 | name = "tokio" 921 | version = "1.39.3" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" 924 | dependencies = [ 925 | "backtrace", 926 | "bytes", 927 | "libc", 928 | "mio", 929 | "pin-project-lite", 930 | "socket2", 931 | "windows-sys", 932 | ] 933 | 934 | [[package]] 935 | name = "tokio-util" 936 | version = "0.7.11" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" 939 | dependencies = [ 940 | "bytes", 941 | "futures-core", 942 | "futures-sink", 943 | "pin-project-lite", 944 | "tokio", 945 | ] 946 | 947 | [[package]] 948 | name = "tower" 949 | version = "0.4.13" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 952 | dependencies = [ 953 | "futures-core", 954 | "futures-util", 955 | "pin-project", 956 | "pin-project-lite", 957 | "tokio", 958 | "tower-layer", 959 | "tower-service", 960 | ] 961 | 962 | [[package]] 963 | name = "tower-layer" 964 | version = "0.3.3" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 967 | 968 | [[package]] 969 | name = "tower-service" 970 | version = "0.3.3" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 973 | 974 | [[package]] 975 | name = "tracing" 976 | version = "0.1.40" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 979 | dependencies = [ 980 | "pin-project-lite", 981 | "tracing-attributes", 982 | "tracing-core", 983 | ] 984 | 985 | [[package]] 986 | name = "tracing-attributes" 987 | version = "0.1.27" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 990 | dependencies = [ 991 | "proc-macro2", 992 | "quote", 993 | "syn 2.0.76", 994 | ] 995 | 996 | [[package]] 997 | name = "tracing-core" 998 | version = "0.1.32" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1001 | dependencies = [ 1002 | "once_cell", 1003 | "valuable", 1004 | ] 1005 | 1006 | [[package]] 1007 | name = "tracing-log" 1008 | version = "0.2.0" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 1011 | dependencies = [ 1012 | "log", 1013 | "once_cell", 1014 | "tracing-core", 1015 | ] 1016 | 1017 | [[package]] 1018 | name = "tracing-subscriber" 1019 | version = "0.3.18" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 1022 | dependencies = [ 1023 | "nu-ansi-term", 1024 | "sharded-slab", 1025 | "smallvec", 1026 | "thread_local", 1027 | "tracing-core", 1028 | "tracing-log", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "try-lock" 1033 | version = "0.2.5" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1036 | 1037 | [[package]] 1038 | name = "unicode-ident" 1039 | version = "1.0.12" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1042 | 1043 | [[package]] 1044 | name = "valuable" 1045 | version = "0.1.0" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1048 | 1049 | [[package]] 1050 | name = "version_check" 1051 | version = "0.9.5" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1054 | 1055 | [[package]] 1056 | name = "want" 1057 | version = "0.3.1" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1060 | dependencies = [ 1061 | "try-lock", 1062 | ] 1063 | 1064 | [[package]] 1065 | name = "wasi" 1066 | version = "0.11.0+wasi-snapshot-preview1" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1069 | 1070 | [[package]] 1071 | name = "wasm-bindgen" 1072 | version = "0.2.93" 1073 | source = "registry+https://github.com/rust-lang/crates.io-index" 1074 | checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" 1075 | dependencies = [ 1076 | "cfg-if", 1077 | "once_cell", 1078 | "wasm-bindgen-macro", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "wasm-bindgen-backend" 1083 | version = "0.2.93" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" 1086 | dependencies = [ 1087 | "bumpalo", 1088 | "log", 1089 | "once_cell", 1090 | "proc-macro2", 1091 | "quote", 1092 | "syn 2.0.76", 1093 | "wasm-bindgen-shared", 1094 | ] 1095 | 1096 | [[package]] 1097 | name = "wasm-bindgen-macro" 1098 | version = "0.2.93" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" 1101 | dependencies = [ 1102 | "quote", 1103 | "wasm-bindgen-macro-support", 1104 | ] 1105 | 1106 | [[package]] 1107 | name = "wasm-bindgen-macro-support" 1108 | version = "0.2.93" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" 1111 | dependencies = [ 1112 | "proc-macro2", 1113 | "quote", 1114 | "syn 2.0.76", 1115 | "wasm-bindgen-backend", 1116 | "wasm-bindgen-shared", 1117 | ] 1118 | 1119 | [[package]] 1120 | name = "wasm-bindgen-shared" 1121 | version = "0.2.93" 1122 | source = "registry+https://github.com/rust-lang/crates.io-index" 1123 | checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" 1124 | 1125 | [[package]] 1126 | name = "web-sys" 1127 | version = "0.3.70" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" 1130 | dependencies = [ 1131 | "js-sys", 1132 | "wasm-bindgen", 1133 | ] 1134 | 1135 | [[package]] 1136 | name = "winapi" 1137 | version = "0.3.9" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1140 | dependencies = [ 1141 | "winapi-i686-pc-windows-gnu", 1142 | "winapi-x86_64-pc-windows-gnu", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "winapi-i686-pc-windows-gnu" 1147 | version = "0.4.0" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1150 | 1151 | [[package]] 1152 | name = "winapi-x86_64-pc-windows-gnu" 1153 | version = "0.4.0" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1156 | 1157 | [[package]] 1158 | name = "windows-sys" 1159 | version = "0.52.0" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1162 | dependencies = [ 1163 | "windows-targets", 1164 | ] 1165 | 1166 | [[package]] 1167 | name = "windows-targets" 1168 | version = "0.52.6" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1171 | dependencies = [ 1172 | "windows_aarch64_gnullvm", 1173 | "windows_aarch64_msvc", 1174 | "windows_i686_gnu", 1175 | "windows_i686_gnullvm", 1176 | "windows_i686_msvc", 1177 | "windows_x86_64_gnu", 1178 | "windows_x86_64_gnullvm", 1179 | "windows_x86_64_msvc", 1180 | ] 1181 | 1182 | [[package]] 1183 | name = "windows_aarch64_gnullvm" 1184 | version = "0.52.6" 1185 | source = "registry+https://github.com/rust-lang/crates.io-index" 1186 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1187 | 1188 | [[package]] 1189 | name = "windows_aarch64_msvc" 1190 | version = "0.52.6" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1193 | 1194 | [[package]] 1195 | name = "windows_i686_gnu" 1196 | version = "0.52.6" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1199 | 1200 | [[package]] 1201 | name = "windows_i686_gnullvm" 1202 | version = "0.52.6" 1203 | source = "registry+https://github.com/rust-lang/crates.io-index" 1204 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1205 | 1206 | [[package]] 1207 | name = "windows_i686_msvc" 1208 | version = "0.52.6" 1209 | source = "registry+https://github.com/rust-lang/crates.io-index" 1210 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1211 | 1212 | [[package]] 1213 | name = "windows_x86_64_gnu" 1214 | version = "0.52.6" 1215 | source = "registry+https://github.com/rust-lang/crates.io-index" 1216 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1217 | 1218 | [[package]] 1219 | name = "windows_x86_64_gnullvm" 1220 | version = "0.52.6" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1223 | 1224 | [[package]] 1225 | name = "windows_x86_64_msvc" 1226 | version = "0.52.6" 1227 | source = "registry+https://github.com/rust-lang/crates.io-index" 1228 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1229 | 1230 | [[package]] 1231 | name = "zerocopy" 1232 | version = "0.7.35" 1233 | source = "registry+https://github.com/rust-lang/crates.io-index" 1234 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1235 | dependencies = [ 1236 | "zerocopy-derive", 1237 | ] 1238 | 1239 | [[package]] 1240 | name = "zerocopy-derive" 1241 | version = "0.7.35" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1244 | dependencies = [ 1245 | "proc-macro2", 1246 | "quote", 1247 | "syn 2.0.76", 1248 | ] 1249 | -------------------------------------------------------------------------------- /metrics-scope/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::collections::{BTreeMap, BTreeSet}; 3 | use std::sync::atomic::{AtomicUsize, Ordering}; 4 | use std::sync::Arc; 5 | use std::thread; 6 | use std::time::Duration; 7 | 8 | use args::{ 9 | Args, PlotConfig, ToPlotConfigMap as _, ToSmaMap as _, ToTriggerMap as _, TriggerConfig, 10 | }; 11 | use atomic_float::AtomicF64; 12 | use clap::Parser; 13 | use egui::{Button, Color32, RichText, Ui}; 14 | use egui_plot::{Legend, Line, Plot, PlotPoint, PlotPoints}; 15 | use metrics_exporter_scope::Packet; 16 | use once_cell::sync::Lazy; 17 | use rtsc::data_policy::{DataDeliveryPolicy, DeliveryPolicy}; 18 | 19 | mod args; 20 | mod client; 21 | 22 | type EventSender = rtsc::policy_channel::Sender; 23 | type EventReceiver = 24 | rtsc::policy_channel::Receiver; 25 | 26 | const DATA_BUF_SIZE: usize = 100_000; 27 | const UI_DELAY: Duration = Duration::from_millis(50); 28 | 29 | const MAX_TIME_WINDOW: f32 = 600.0; 30 | 31 | enum Event { 32 | Connect, 33 | Disconnect, 34 | Packet(Packet), 35 | } 36 | 37 | impl DataDeliveryPolicy for Event { 38 | fn delivery_policy(&self) -> DeliveryPolicy { 39 | match self { 40 | Event::Connect | Event::Disconnect => DeliveryPolicy::Always, 41 | Event::Packet(_) => DeliveryPolicy::Latest, 42 | } 43 | } 44 | } 45 | 46 | static COLORS: Lazy> = Lazy::new(|| { 47 | let mut colors = BTreeMap::new(); 48 | colors.insert("red".to_owned(), Color32::RED); 49 | colors.insert("green".to_owned(), Color32::GREEN); 50 | colors.insert("blue".to_owned(), Color32::BLUE); 51 | colors.insert("yellow".to_owned(), Color32::YELLOW); 52 | colors.insert("cyan".to_owned(), Color32::from_rgb(0, 255, 255)); 53 | colors.insert("magenta".to_owned(), Color32::from_rgb(255, 0, 255)); 54 | colors.insert("orange".to_owned(), Color32::from_rgb(255, 105, 0)); 55 | colors.insert("white".to_owned(), Color32::WHITE); 56 | colors.insert("black".to_owned(), Color32::from_rgb(0, 0, 0)); 57 | colors 58 | }); 59 | 60 | fn parse_color(color: &str) -> Option { 61 | if let Some(color) = COLORS.get(color) { 62 | Some(*color) 63 | } else if let Some(c) = color.strip_prefix('#') { 64 | match c.len() { 65 | 3 => { 66 | let r = u8::from_str_radix(&c[0..1].repeat(2), 16).ok()?; 67 | let g = u8::from_str_radix(&c[1..2].repeat(2), 16).ok()?; 68 | let b = u8::from_str_radix(&c[2..3].repeat(2), 16).ok()?; 69 | Some(Color32::from_rgb(r, g, b)) 70 | } 71 | 6 => { 72 | let r = u8::from_str_radix(&c[0..2], 16).ok()?; 73 | let g = u8::from_str_radix(&c[2..4], 16).ok()?; 74 | let b = u8::from_str_radix(&c[4..6], 16).ok()?; 75 | Some(Color32::from_rgb(r, g, b)) 76 | } 77 | _ => None, 78 | } 79 | } else { 80 | None 81 | } 82 | } 83 | 84 | fn main() { 85 | let args = Args::parse(); 86 | let mut source = args.source.clone(); 87 | if !source.contains(':') { 88 | source = format!("{}:5001", source); 89 | } 90 | let options = eframe::NativeOptions { 91 | viewport: egui::ViewportBuilder::default().with_inner_size([640.0, 480.0]), 92 | ..Default::default() 93 | }; 94 | let (tx, rx) = 95 | rtsc::policy_channel::bounded::( 96 | DATA_BUF_SIZE, 97 | ); 98 | let source_c = source.clone(); 99 | let timeout = Duration::from_secs(args.timeout); 100 | let sampling_interval = Duration::from_secs_f64(args.sampling_interval); 101 | thread::spawn(move || { 102 | client::reader(&source_c, tx, sampling_interval, timeout); 103 | }); 104 | // make args static 105 | let args = Box::leak(Box::new(args)); 106 | eframe::run_native( 107 | &format!("{} - metrics-scope", source), 108 | options, 109 | Box::new(|cc| { 110 | egui_extras::install_image_loaders(&cc.egui_ctx); 111 | if let Some(theme) = args.theme.as_ref() { 112 | match theme { 113 | args::Theme::Dark => cc.egui_ctx.set_visuals(egui::Visuals::dark()), 114 | args::Theme::Light => cc.egui_ctx.set_visuals(egui::Visuals::light()), 115 | } 116 | } 117 | Ok(Box::new(Scope { 118 | rx, 119 | data: <_>::default(), 120 | plots: <_>::default(), 121 | plot_settings: <_>::default(), 122 | colors: <_>::default(), 123 | paused: false, 124 | need_reset: false, 125 | show_legend: !args.hide_legend, 126 | time_window: args.time_window, 127 | chart_cols: args.chart_cols, 128 | aspect: args.chart_aspect, 129 | sma_selected_plot: None, 130 | sma_selected_metric: None, 131 | sma_selected_value: String::new(), 132 | trigger_selected_plot: None, 133 | trigger_selected_metric: None, 134 | trigger_selected_value_below: String::new(), 135 | trigger_selected_value_above: String::new(), 136 | range_selected_plot: None, 137 | range_selected_value_min: String::new(), 138 | range_selected_value_max: String::new(), 139 | triggered: None, 140 | sampling_interval_ns: Duration::from_secs_f64(args.sampling_interval) 141 | .as_nanos() 142 | .try_into() 143 | .unwrap(), 144 | connected: false, 145 | source: args.source.clone(), 146 | predefined_smas: args.predefined_sma.to_sma_map(), 147 | predefined_triggers: args.predefined_trigger.to_trigger_map(), 148 | predefined_plots: args.predefined_y_range.to_plot_config_map(), 149 | })) 150 | }), 151 | ) 152 | .expect("Failed to run UI"); 153 | } 154 | 155 | #[allow(clippy::struct_excessive_bools)] 156 | struct Scope { 157 | rx: EventReceiver, 158 | data: BTreeMap>, 159 | plots: BTreeMap>>, 160 | plot_settings: BTreeMap, 161 | colors: BTreeMap, 162 | paused: bool, 163 | need_reset: bool, 164 | show_legend: bool, 165 | time_window: f32, 166 | chart_cols: f32, 167 | aspect: f32, 168 | sma_selected_plot: Option, 169 | sma_selected_metric: Option>, 170 | sma_selected_value: String, 171 | trigger_selected_plot: Option, 172 | trigger_selected_metric: Option>, 173 | trigger_selected_value_below: String, 174 | trigger_selected_value_above: String, 175 | range_selected_plot: Option, 176 | range_selected_value_min: String, 177 | range_selected_value_max: String, 178 | triggered: Option, 179 | sampling_interval_ns: u64, 180 | connected: bool, 181 | source: String, 182 | predefined_smas: BTreeMap, 183 | predefined_triggers: BTreeMap, 184 | predefined_plots: BTreeMap, 185 | } 186 | 187 | struct PlotSettings { 188 | min_y: AtomicF64, 189 | max_y: AtomicF64, 190 | } 191 | 192 | impl PlotSettings { 193 | fn new() -> Self { 194 | Self { 195 | min_y: AtomicF64::new(f64::NAN), 196 | max_y: AtomicF64::new(f64::NAN), 197 | } 198 | } 199 | fn get_min_y(&self) -> Option { 200 | let val = self.min_y.load(Ordering::Relaxed); 201 | if val.is_nan() { 202 | None 203 | } else { 204 | Some(val) 205 | } 206 | } 207 | fn get_max_y(&self) -> Option { 208 | let val = self.max_y.load(Ordering::Relaxed); 209 | if val.is_nan() { 210 | None 211 | } else { 212 | Some(val) 213 | } 214 | } 215 | fn set_min_y(&self, value: Option) { 216 | if let Some(value) = value { 217 | self.min_y.store(value, Ordering::Relaxed); 218 | } else { 219 | self.min_y.store(f64::NAN, Ordering::Relaxed); 220 | } 221 | } 222 | fn set_max_y(&self, value: Option) { 223 | if let Some(value) = value { 224 | self.max_y.store(value, Ordering::Relaxed); 225 | } else { 226 | self.max_y.store(f64::NAN, Ordering::Relaxed); 227 | } 228 | } 229 | } 230 | 231 | struct Triggered { 232 | at: f64, 233 | by: String, 234 | below_above: TriggeredKind, 235 | } 236 | 237 | impl Triggered { 238 | fn below(at: f64, by: impl AsRef) -> Self { 239 | Self { 240 | at, 241 | by: by.as_ref().to_owned(), 242 | below_above: TriggeredKind::Below, 243 | } 244 | } 245 | fn above(at: f64, by: impl AsRef) -> Self { 246 | Self { 247 | at, 248 | by: by.as_ref().to_owned(), 249 | below_above: TriggeredKind::Above, 250 | } 251 | } 252 | } 253 | 254 | #[derive(Eq, PartialEq, Clone, Copy)] 255 | enum TriggeredKind { 256 | Below, 257 | Above, 258 | } 259 | 260 | struct Metric { 261 | name: String, 262 | sma_window: AtomicUsize, 263 | trigger_below: AtomicF64, 264 | trigger_above: AtomicF64, 265 | } 266 | 267 | impl Metric { 268 | fn new(name: &str) -> Self { 269 | Self { 270 | name: name.to_owned(), 271 | sma_window: AtomicUsize::new(0), 272 | trigger_below: AtomicF64::new(f64::NAN), 273 | trigger_above: AtomicF64::new(f64::NAN), 274 | } 275 | } 276 | fn get_sma(&self) -> usize { 277 | self.sma_window.load(Ordering::Relaxed) 278 | } 279 | fn set_sma(&self, value: usize) { 280 | self.sma_window.store(value, Ordering::Relaxed); 281 | } 282 | fn get_trigger_below(&self) -> Option { 283 | let val = self.trigger_below.load(Ordering::Relaxed); 284 | if val.is_nan() { 285 | None 286 | } else { 287 | Some(val) 288 | } 289 | } 290 | fn get_trigger_above(&self) -> Option { 291 | let val = self.trigger_above.load(Ordering::Relaxed); 292 | if val.is_nan() { 293 | None 294 | } else { 295 | Some(val) 296 | } 297 | } 298 | fn set_trigger_below(&self, value: Option) { 299 | if let Some(value) = value { 300 | self.trigger_below.store(value, Ordering::Relaxed); 301 | } else { 302 | self.trigger_below.store(f64::NAN, Ordering::Relaxed); 303 | } 304 | } 305 | fn set_trigger_above(&self, value: Option) { 306 | if let Some(value) = value { 307 | self.trigger_above.store(value, Ordering::Relaxed); 308 | } else { 309 | self.trigger_above.store(f64::NAN, Ordering::Relaxed); 310 | } 311 | } 312 | } 313 | 314 | impl PartialOrd for Metric { 315 | fn partial_cmp(&self, other: &Self) -> Option { 316 | Some(self.name.cmp(&other.name)) 317 | } 318 | } 319 | 320 | impl Ord for Metric { 321 | fn cmp(&self, other: &Self) -> cmp::Ordering { 322 | self.name.cmp(&other.name) 323 | } 324 | } 325 | 326 | impl Eq for Metric {} 327 | 328 | impl PartialEq for Metric { 329 | fn eq(&self, other: &Self) -> bool { 330 | self.name == other.name 331 | } 332 | } 333 | 334 | impl Scope { 335 | fn handle_event(&mut self, event: Event) { 336 | match event { 337 | Event::Connect => { 338 | self.data.clear(); 339 | //self.plots.clear(); 340 | self.colors.clear(); 341 | self.connected = true; 342 | } 343 | Event::Disconnect => { 344 | self.connected = false; 345 | } 346 | Event::Packet(Packet::Snapshot(mut snapshot)) => { 347 | let max_time_window = Duration::from_secs_f32(MAX_TIME_WINDOW); 348 | let max_data_ponts = usize::try_from( 349 | u64::try_from(max_time_window.as_nanos()).unwrap() / self.sampling_interval_ns, 350 | ) 351 | .unwrap(); 352 | let ts_vec = self.data.entry(String::new()).or_default(); 353 | ts_vec.push(snapshot.ts().as_secs_f64()); 354 | if ts_vec.len() > max_data_ponts { 355 | ts_vec.drain(0..(ts_vec.len() - max_data_ponts)); 356 | } 357 | for (n, v) in snapshot.take_data() { 358 | let data_vec = self.data.entry(n).or_default(); 359 | data_vec.push(v); 360 | if data_vec.len() > max_data_ponts { 361 | data_vec.drain(0..(data_vec.len() - max_data_ponts)); 362 | } 363 | } 364 | } 365 | Event::Packet(Packet::Info(info)) => { 366 | for (name, m) in info.metrics() { 367 | let metric = Arc::new(Metric::new(name)); 368 | let (plot, tag) = if let Some(plot) = m.labels().get("plot") { 369 | if self 370 | .plots 371 | .entry(plot.to_owned()) 372 | .or_default() 373 | .insert(metric.clone()) 374 | { 375 | (Some(plot.to_owned()), Some(format!("{}/{}", plot, name))) 376 | } else { 377 | (None, None) 378 | } 379 | } else if self 380 | .plots 381 | .entry(name.to_owned()) 382 | .or_default() 383 | .insert(metric.clone()) 384 | { 385 | (Some(name.to_owned()), Some(name.to_owned())) 386 | } else { 387 | (None, None) 388 | }; 389 | if let Some(plot) = plot { 390 | let plot_settings = 391 | if let Some(plot_config) = self.predefined_plots.get(&plot) { 392 | let settings = PlotSettings::new(); 393 | settings.set_min_y(plot_config.min); 394 | settings.set_max_y(plot_config.max); 395 | settings 396 | } else { 397 | PlotSettings::new() 398 | }; 399 | self.plot_settings.insert(plot, plot_settings); 400 | } 401 | if let Some(tag) = tag { 402 | if let Some(sma) = self.predefined_smas.get(&tag) { 403 | metric.set_sma(*sma); 404 | } 405 | if let Some(triggers) = self.predefined_triggers.get(&tag) { 406 | if let Some(below) = triggers.below { 407 | metric.set_trigger_below(Some(below)); 408 | } 409 | if let Some(above) = triggers.above { 410 | metric.set_trigger_above(Some(above)); 411 | } 412 | } 413 | } 414 | if let Some(color) = m.labels().get("color") { 415 | if let Some(color) = parse_color(color) { 416 | self.colors.insert(name.to_owned(), color); 417 | } else { 418 | eprintln!("Invalid color: {}", color); 419 | } 420 | } 421 | } 422 | } 423 | } 424 | } 425 | 426 | fn process_global_keys(&mut self, ui: &mut Ui) { 427 | if ui.input(|i| i.key_pressed(egui::Key::L)) { 428 | self.show_legend = !self.show_legend; 429 | } 430 | if ui.input(|i| i.key_pressed(egui::Key::F5)) { 431 | self.need_reset = true; 432 | self.triggered = None; 433 | } 434 | if ui.input(|i| i.key_pressed(egui::Key::P)) { 435 | self.paused = !self.paused; 436 | self.triggered = None; 437 | } 438 | } 439 | 440 | fn show_sma_toolbar(&mut self, ui: &mut Ui) { 441 | egui::ComboBox::from_label("SMA") 442 | .selected_text(self.sma_selected_plot.as_deref().unwrap_or("-")) 443 | .show_ui(ui, |ui| { 444 | if self.sma_selected_plot.is_some() && ui.selectable_label(false, "-").clicked() { 445 | self.sma_selected_plot = None; 446 | self.sma_selected_metric = None; 447 | } 448 | for plot in self.plots.keys() { 449 | if ui.selectable_label(false, plot).clicked() { 450 | self.sma_selected_plot = Some(plot.clone()); 451 | self.sma_selected_metric = None; 452 | } 453 | } 454 | }); 455 | if let Some(plot) = self.sma_selected_plot.as_ref() { 456 | if let Some(metrics) = self.plots.get(plot).as_mut() { 457 | egui::ComboBox::from_label("SMA for metric") 458 | .selected_text( 459 | self.sma_selected_metric 460 | .as_ref() 461 | .map(|m| m.name.clone()) 462 | .unwrap_or_default(), 463 | ) 464 | .show_ui(ui, |ui| { 465 | for metric in *metrics { 466 | if ui.selectable_label(false, &metric.name).clicked() { 467 | self.sma_selected_metric = Some(metric.clone()); 468 | self.sma_selected_value = metric.get_sma().to_string(); 469 | } 470 | } 471 | }); 472 | } 473 | if let Some(metric) = self.sma_selected_metric.as_ref() { 474 | if ui 475 | .add(egui::widgets::TextEdit::singleline( 476 | &mut self.sma_selected_value, 477 | )) 478 | .changed() 479 | { 480 | metric.set_sma(self.sma_selected_value.parse().unwrap_or_default()); 481 | } 482 | } 483 | } 484 | ui.end_row(); 485 | } 486 | 487 | fn show_trigger_toolbar(&mut self, ui: &mut Ui) { 488 | egui::ComboBox::from_label("Trigger") 489 | .selected_text(self.trigger_selected_plot.as_deref().unwrap_or("-")) 490 | .show_ui(ui, |ui| { 491 | if self.trigger_selected_plot.is_some() && ui.selectable_label(false, "-").clicked() 492 | { 493 | self.trigger_selected_plot = None; 494 | self.trigger_selected_metric = None; 495 | } 496 | for plot in self.plots.keys() { 497 | if ui.selectable_label(false, plot).clicked() { 498 | self.trigger_selected_plot = Some(plot.clone()); 499 | self.trigger_selected_metric = None; 500 | } 501 | } 502 | }); 503 | if let Some(plot) = self.trigger_selected_plot.as_ref() { 504 | if let Some(metrics) = self.plots.get(plot).as_mut() { 505 | egui::ComboBox::from_label("Trigger for metric") 506 | .selected_text( 507 | self.trigger_selected_metric 508 | .as_ref() 509 | .map(|m| m.name.clone()) 510 | .unwrap_or_default(), 511 | ) 512 | .show_ui(ui, |ui| { 513 | for metric in *metrics { 514 | if ui.selectable_label(false, &metric.name).clicked() { 515 | self.trigger_selected_metric = Some(metric.clone()); 516 | self.trigger_selected_value_below = metric 517 | .get_trigger_below() 518 | .map(|v| v.to_string()) 519 | .unwrap_or_default(); 520 | self.trigger_selected_value_above = metric 521 | .get_trigger_above() 522 | .map(|v| v.to_string()) 523 | .unwrap_or_default(); 524 | } 525 | } 526 | }); 527 | } 528 | if let Some(metric) = self.trigger_selected_metric.as_ref() { 529 | ui.label("below"); 530 | if ui 531 | .add(egui::widgets::TextEdit::singleline( 532 | &mut self.trigger_selected_value_below, 533 | )) 534 | .changed() 535 | { 536 | metric.set_trigger_below(self.trigger_selected_value_below.parse().ok()); 537 | if let Some(ref tr) = self.triggered { 538 | if tr.by == metric.name && tr.below_above == TriggeredKind::Below { 539 | self.triggered = None; 540 | } 541 | } 542 | } 543 | ui.label("above"); 544 | if ui 545 | .add(egui::widgets::TextEdit::singleline( 546 | &mut self.trigger_selected_value_above, 547 | )) 548 | .changed() 549 | { 550 | metric.set_trigger_above(self.trigger_selected_value_above.parse().ok()); 551 | if let Some(ref tr) = self.triggered { 552 | if tr.by == metric.name && tr.below_above == TriggeredKind::Above { 553 | self.triggered = None; 554 | } 555 | } 556 | } 557 | } 558 | } 559 | if let Some(ref tr) = self.triggered { 560 | let mut text = RichText::new(format!("TRIG {}", tr.by)).color(Color32::BLACK); 561 | if self.paused { 562 | text = text.background_color(Color32::GRAY); 563 | } else { 564 | text = text.background_color(Color32::YELLOW); 565 | } 566 | ui.label(text); 567 | } 568 | ui.end_row(); 569 | } 570 | 571 | fn show_range_toolbar(&mut self, ui: &mut Ui) { 572 | egui::ComboBox::from_label("Y-Range") 573 | .selected_text(self.range_selected_plot.as_deref().unwrap_or("-")) 574 | .show_ui(ui, |ui| { 575 | if self.range_selected_plot.is_some() && ui.selectable_label(false, "-").clicked() { 576 | self.range_selected_plot = None; 577 | } 578 | for plot in self.plots.keys() { 579 | if ui.selectable_label(false, plot).clicked() { 580 | let plot_settings = self.plot_settings.get(plot).unwrap(); 581 | self.range_selected_plot = Some(plot.clone()); 582 | self.range_selected_value_min = plot_settings 583 | .get_min_y() 584 | .map(|v| v.to_string()) 585 | .unwrap_or_default(); 586 | self.range_selected_value_max = plot_settings 587 | .get_max_y() 588 | .map(|v| v.to_string()) 589 | .unwrap_or_default(); 590 | } 591 | } 592 | }); 593 | if let Some(plot) = self.range_selected_plot.as_ref() { 594 | ui.label("min"); 595 | if ui 596 | .add(egui::widgets::TextEdit::singleline( 597 | &mut self.range_selected_value_min, 598 | )) 599 | .changed() 600 | { 601 | self.plot_settings 602 | .get(plot) 603 | .unwrap() 604 | .set_min_y(self.range_selected_value_min.parse().ok()); 605 | } 606 | ui.label("max"); 607 | if ui 608 | .add(egui::widgets::TextEdit::singleline( 609 | &mut self.range_selected_value_max, 610 | )) 611 | .changed() 612 | { 613 | self.plot_settings 614 | .get(plot) 615 | .unwrap() 616 | .set_max_y(self.range_selected_value_max.parse().ok()); 617 | } 618 | } 619 | ui.end_row(); 620 | } 621 | 622 | fn show_common_controls(&mut self, ui: &mut Ui) { 623 | ui.add( 624 | egui::Slider::new(&mut self.time_window, 1.0..=MAX_TIME_WINDOW) 625 | .text("Time window") 626 | .step_by(1.0) 627 | .integer() 628 | .logarithmic(true), 629 | ); 630 | ui.checkbox(&mut self.show_legend, "Legend (L)"); 631 | if ui.add(Button::new("Reset (F5)")).clicked() { 632 | self.need_reset = true; 633 | self.triggered = None; 634 | } 635 | if self.paused { 636 | if ui.add(Button::new("Resume (P)")).clicked() { 637 | self.paused = false; 638 | self.triggered = None; 639 | } 640 | } else if ui.add(Button::new("Pause (P)")).clicked() { 641 | self.paused = true; 642 | self.triggered = None; 643 | } 644 | ui.end_row(); 645 | ui.add( 646 | egui::Slider::new(&mut self.chart_cols, 1.0..=10.0) 647 | .text("Cols") 648 | .integer(), 649 | ); 650 | ui.add( 651 | egui::Slider::new(&mut self.aspect, 1.0..=5.0) 652 | .text("Aspect") 653 | .step_by(0.1), 654 | ); 655 | ui.end_row(); 656 | } 657 | 658 | #[allow(clippy::too_many_lines, clippy::cast_precision_loss)] 659 | fn show_charts(&mut self, ui: &mut Ui, ts_vec: Vec, data_points: usize) { 660 | let chart_width = ui.available_width() / self.chart_cols - 10.0; 661 | let plots: Vec<_> = self.plots.iter().filter(|(_, v)| !v.is_empty()).collect(); 662 | let mut ts_vec_axis = vec![]; 663 | for i in (0..data_points).rev() { 664 | ts_vec_axis.push(-(i as f64 * self.sampling_interval_ns as f64 / 1_000_000_000.0)); 665 | } 666 | #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] 667 | for plot_chunk in plots.chunks(self.chart_cols as usize) { 668 | ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { 669 | for (plot, metrics) in plot_chunk { 670 | let mut plot_name = String::new(); 671 | for metric in *metrics { 672 | if plot_name.is_empty() && metric.name != **plot { 673 | plot_name.push_str(&format!("{} ", plot)); 674 | } 675 | if let Some(data) = self.data.get(&metric.name) { 676 | if let Some(last) = data.last() { 677 | plot_name.push_str(&format!("{}={} ", metric.name, last)); 678 | } 679 | } 680 | } 681 | let mut chart_plot = Plot::new(plot) 682 | .view_aspect(self.aspect) 683 | .x_axis_label(plot_name) 684 | .label_formatter(|name, value| { 685 | if name.is_empty() { 686 | format!("t={}\n{}", value.x, value.y) 687 | } else { 688 | format!("t={}\n{}={}", value.x, name, value.y) 689 | } 690 | }) 691 | .width(chart_width) 692 | .link_axis("scope", [true, false]) 693 | .link_cursor("scope", [true, false]); 694 | if self.need_reset { 695 | chart_plot = chart_plot.reset(); 696 | } 697 | if self.show_legend { 698 | let legend = Legend::default(); 699 | chart_plot = chart_plot.legend(legend); 700 | }; 701 | let plot_settings = self.plot_settings.get(*plot).unwrap(); 702 | if let Some(min_y) = plot_settings.get_min_y() { 703 | chart_plot = chart_plot.include_y(min_y); 704 | } 705 | if let Some(max_y) = plot_settings.get_max_y() { 706 | chart_plot = chart_plot.include_y(max_y); 707 | } 708 | chart_plot.show(ui, |plot_ui| { 709 | for metric in *metrics { 710 | let mut data = if let Some(d) = self.data.get(&metric.name) { 711 | if self.triggered.is_none() { 712 | if let Some(last) = d.last() { 713 | if let Some(min) = metric.get_trigger_below() { 714 | if *last <= min { 715 | self.triggered = Some(Triggered::below( 716 | *ts_vec.last().unwrap(), 717 | &metric.name, 718 | )); 719 | } 720 | } 721 | if let Some(max) = metric.get_trigger_above() { 722 | if *last >= max { 723 | self.triggered = Some(Triggered::above( 724 | *ts_vec.last().unwrap(), 725 | &metric.name, 726 | )); 727 | } 728 | } 729 | } 730 | } 731 | match d.len().cmp(&data_points) { 732 | cmp::Ordering::Less => { 733 | let to_insert = data_points - d.len(); 734 | let mut data = Vec::with_capacity(data_points); 735 | data.resize(to_insert, f64::NAN); 736 | data.extend(d); 737 | data 738 | } 739 | cmp::Ordering::Equal => d.clone(), 740 | cmp::Ordering::Greater => d[d.len() - data_points..].to_vec(), 741 | } 742 | } else { 743 | vec![f64::NAN; data_points] 744 | }; 745 | if let Some(min_y) = plot_settings.get_min_y() { 746 | for entry in &mut data { 747 | if *entry < min_y { 748 | *entry = f64::NAN; 749 | } 750 | } 751 | } 752 | if let Some(max_y) = plot_settings.get_max_y() { 753 | for entry in &mut data { 754 | if *entry > max_y { 755 | *entry = f64::NAN; 756 | } 757 | } 758 | } 759 | let sma_window = metric.get_sma(); 760 | if sma_window > 0 { 761 | let sma = data 762 | .windows(sma_window) 763 | .map(|w| w.iter().sum::() / w.len() as f64) 764 | .collect::>(); 765 | let pp = PlotPoints::Owned( 766 | sma.into_iter() 767 | .zip(ts_vec_axis.clone()) 768 | .skip(sma_window - 1) 769 | .map(|(d, ts)| PlotPoint::new(ts, d)) 770 | .collect(), 771 | ); 772 | plot_ui.line( 773 | Line::new(format!("SMA {}", metric.name), pp) 774 | .style(egui_plot::LineStyle::Dotted { spacing: 5.0 }), 775 | ); 776 | } 777 | let pp = PlotPoints::Owned( 778 | data.into_iter() 779 | .zip(ts_vec_axis.clone()) 780 | .map(|(d, ts)| PlotPoint::new(ts, d)) 781 | .collect(), 782 | ); 783 | let mut line = Line::new(&metric.name, pp); 784 | if let Some(color) = self.colors.get(&metric.name) { 785 | line = line.color(*color); 786 | } 787 | plot_ui.line(line); 788 | if let Some(trigger_min) = metric.get_trigger_below() { 789 | plot_ui.line( 790 | Line::new( 791 | format!("TrB {}", metric.name), 792 | PlotPoints::Owned(vec![ 793 | PlotPoint::new( 794 | ts_vec_axis.first().copied().unwrap_or_default(), 795 | trigger_min, 796 | ), 797 | PlotPoint::new( 798 | ts_vec_axis.last().copied().unwrap_or_default(), 799 | trigger_min, 800 | ), 801 | ]), 802 | ) 803 | .color(Color32::from_rgba_premultiplied(149, 80, 45, 20)) 804 | .style(egui_plot::LineStyle::Dashed { length: 10.0 }), 805 | ); 806 | } 807 | if let Some(trigger_max) = metric.get_trigger_above() { 808 | plot_ui.line( 809 | Line::new( 810 | format!("TrA {}", metric.name), 811 | PlotPoints::Owned(vec![ 812 | PlotPoint::new( 813 | ts_vec_axis.first().copied().unwrap_or_default(), 814 | trigger_max, 815 | ), 816 | PlotPoint::new( 817 | ts_vec_axis.last().copied().unwrap_or_default(), 818 | trigger_max, 819 | ), 820 | ]), 821 | ) 822 | .color(Color32::from_rgba_premultiplied(149, 40, 45, 20)) 823 | .style(egui_plot::LineStyle::Dashed { length: 10.0 }), 824 | ); 825 | } 826 | } 827 | }); 828 | } 829 | }); 830 | } 831 | } 832 | } 833 | 834 | impl eframe::App for Scope { 835 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 836 | let time_window = Duration::from_secs_f32(self.time_window); 837 | if self.paused { 838 | thread::sleep(UI_DELAY); 839 | } else { 840 | let mut received = false; 841 | while let Ok(event) = self.rx.try_recv() { 842 | received = true; 843 | self.handle_event(event); 844 | } 845 | if !received { 846 | thread::sleep(UI_DELAY); 847 | } 848 | } 849 | let Some(full_ts_vec) = self.data.get("") else { 850 | egui::CentralPanel::default().show(ctx, |ui| { 851 | ui.label("Connecting..."); 852 | }); 853 | thread::sleep(UI_DELAY); 854 | ctx.request_repaint(); 855 | return; 856 | }; 857 | let data_points = usize::try_from( 858 | u64::try_from(time_window.as_nanos()).unwrap() / self.sampling_interval_ns, 859 | ) 860 | .unwrap(); 861 | let mut ts_vec; 862 | match full_ts_vec.len().cmp(&data_points) { 863 | cmp::Ordering::Less => { 864 | let ts = full_ts_vec.first().copied().unwrap_or_default(); 865 | let to_insert = data_points - full_ts_vec.len(); 866 | ts_vec = Vec::with_capacity(data_points); 867 | #[allow(clippy::cast_precision_loss)] 868 | for i in (0..to_insert).rev() { 869 | ts_vec.push( 870 | ts - (i as f64) * (self.sampling_interval_ns as f64) / 1_000_000_000.0, 871 | ); 872 | } 873 | ts_vec.extend(full_ts_vec); 874 | } 875 | cmp::Ordering::Equal => { 876 | ts_vec = full_ts_vec.clone(); 877 | } 878 | cmp::Ordering::Greater => { 879 | ts_vec = full_ts_vec[full_ts_vec.len() - data_points..].to_vec(); 880 | } 881 | } 882 | if let Some(ref tr) = self.triggered { 883 | let ts_half = ts_vec.len() / 2; 884 | if let Some(ts) = ts_vec.get(ts_half) { 885 | if tr.at <= *ts { 886 | self.paused = true; 887 | } 888 | } 889 | } 890 | egui::CentralPanel::default().show(ctx, |ui| { 891 | self.process_global_keys(ui); 892 | ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { 893 | ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { 894 | ui.add( 895 | egui::Image::new(egui::include_image!("../assets/bma.svg")) 896 | .corner_radius(5.0) 897 | .max_width(48.0) 898 | .shrink_to_fit(), 899 | ); 900 | egui::Grid::new("status").show(ui, |ui| { 901 | ui.label(&self.source); 902 | ui.end_row(); 903 | let text = if self.connected { 904 | RichText::new("ONLINE") 905 | .color(Color32::WHITE) 906 | .background_color(Color32::DARK_GREEN) 907 | } else { 908 | RichText::new("OFFLINE") 909 | .color(Color32::WHITE) 910 | .background_color(Color32::DARK_RED) 911 | }; 912 | ui.label(text); 913 | }); 914 | }); 915 | egui::Grid::new("toolbar").show(ui, |ui| { 916 | self.show_sma_toolbar(ui); 917 | self.show_trigger_toolbar(ui); 918 | self.show_range_toolbar(ui); 919 | }); 920 | }); 921 | egui::Grid::new("common_controls").show(ui, |ui| { 922 | self.show_common_controls(ui); 923 | }); 924 | egui::ScrollArea::both().show(ui, |ui| { 925 | self.show_charts(ui, ts_vec, data_points); 926 | ui.with_layout(egui::Layout::right_to_left(egui::Align::RIGHT), |ui| { 927 | let text = RichText::new("RoboPLC Metrics Scope © Bohemia Automation") 928 | .color(Color32::DARK_GRAY); 929 | ui.label(text); 930 | }); 931 | }); 932 | }); 933 | self.need_reset = false; 934 | ctx.request_repaint(); 935 | } 936 | } 937 | --------------------------------------------------------------------------------