From 6543af3c7591309e1ff1e47b395057df17a8f318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ulises=20Ni=C3=B1o=20Rivera?= Date: Thu, 11 Jul 2024 11:29:02 -0700 Subject: [PATCH 1/2] Add simple stats API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Ulises Niño Rivera --- src/lib.rs | 1 + src/stats.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/traits.rs | 29 ++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 src/stats.rs diff --git a/src/lib.rs b/src/lib.rs index a8f42651..1005b750 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ // limitations under the License. pub mod hostcalls; +pub mod stats; pub mod traits; pub mod types; diff --git a/src/stats.rs b/src/stats.rs new file mode 100644 index 00000000..726a4d98 --- /dev/null +++ b/src/stats.rs @@ -0,0 +1,107 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::hostcalls; +use crate::traits; +use crate::types; + +pub struct Counter { + id: u32, + name: String, +} + +impl Counter { + pub fn counter(name: String) -> Counter { + let returned_id = hostcalls::define_metric(types::MetricType::Counter, &name).unwrap(); + Counter { + id: returned_id, + name, + } + } +} + +impl traits::Metric for Counter { + fn id(&self) -> u32 { + self.id + } + + fn name(&self) -> &str { + self.name.as_str() + } + + fn record(&self, _: u64) { + // A Counter can only be incremented. + return; + } +} + +pub struct Gauge { + id: u32, + name: String, +} + +impl Gauge { + pub fn gauge(name: String) -> Gauge { + let returned_id = hostcalls::define_metric(types::MetricType::Gauge, &name).unwrap(); + Gauge { + id: returned_id, + name, + } + } +} + +impl traits::Metric for Gauge { + fn id(&self) -> u32 { + self.id + } + + fn name(&self) -> &str { + self.name.as_str() + } + + fn increment(&self, _: i64) { + // A gauge can only be recorded. + return; + } +} + +pub struct Histogram { + id: u32, + name: String, +} + +impl Histogram { + pub fn histogram(name: String) -> Histogram { + let returned_id = hostcalls::define_metric(types::MetricType::Histogram, &name).unwrap(); + Histogram { + id: returned_id, + name, + } + } +} + +impl traits::Metric for Histogram { + fn id(&self) -> u32 { + self.id + } + + fn name(&self) -> &str { + self.name.as_str() + } + + fn increment(&self, _: i64) { + // A Histogram can only be recorded. + return; + } +} diff --git a/src/traits.rs b/src/traits.rs index 034f87ea..a4ed32b2 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -534,3 +534,32 @@ pub trait HttpContext: Context { fn on_log(&mut self) {} } + +pub trait Metric { + fn name(&self) -> &str; + fn id(&self) -> u32; + + fn value(&self) -> u64 { + match hostcalls::get_metric(self.id()) { + Ok(value) => value, + Err(Status::NotFound) => panic!("metric not found: {}", self.name()), + Err(err) => panic!("unexpected status: {:?}", err), + } + } + + fn record(&self, value: u64) { + match hostcalls::record_metric(self.id(), value) { + Ok(_) => return, + Err(Status::NotFound) => panic!("metric not found: {}", self.name()), + Err(err) => panic!("unexpected status: {:?}", err), + } + } + + fn increment(&self, offset: i64) { + match hostcalls::increment_metric(self.id(), offset) { + Ok(_) => return, + Err(Status::NotFound) => panic!("metric not found: {}", self.name()), + Err(err) => panic!("unexpected status: {:?}", err), + } + } +} From 5b19423bef6617f232dd0f2f773337bf8a3318af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ulises=20Ni=C3=B1o=20Rivera?= Date: Fri, 12 Jul 2024 17:58:49 -0700 Subject: [PATCH 2/2] comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Ulises Niño Rivera --- .github/workflows/rust.yml | 2 + README.md | 1 + examples/metrics/Cargo.toml | 22 +++++++ examples/metrics/README.md | 32 +++++++++++ examples/metrics/docker-compose.yaml | 28 +++++++++ examples/metrics/envoy.yaml | 61 ++++++++++++++++++++ examples/metrics/src/lib.rs | 86 ++++++++++++++++++++++++++++ src/stats.rs | 69 +++++++--------------- src/traits.rs | 19 +++--- 9 files changed, 264 insertions(+), 56 deletions(-) create mode 100644 examples/metrics/Cargo.toml create mode 100644 examples/metrics/README.md create mode 100644 examples/metrics/docker-compose.yaml create mode 100644 examples/metrics/envoy.yaml create mode 100644 examples/metrics/src/lib.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 49d5e0bd..fc90cdaf 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -238,6 +238,7 @@ jobs: - 'http_body' - 'http_config' - 'http_headers' + - 'metrics' defaults: run: @@ -301,6 +302,7 @@ jobs: - 'http_body' - 'http_config' - 'http_headers' + - 'metrics' defaults: run: diff --git a/README.md b/README.md index d43816d8..931c1ceb 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ - [HTTP Headers](./examples/http_headers/) - [HTTP Response body](./examples/http_body/) - [HTTP Configuration](./examples/http_config/) +- [Metrics](./examples/metrics/) ## Articles & blog posts from the community diff --git a/examples/metrics/Cargo.toml b/examples/metrics/Cargo.toml new file mode 100644 index 00000000..39852c08 --- /dev/null +++ b/examples/metrics/Cargo.toml @@ -0,0 +1,22 @@ +[package] +publish = false +name = "proxy-wasm-example-metrics" +version = "0.0.1" +authors = ["José Ulises Niño Rivera "] +description = "Proxy-Wasm plugin example: Metrics" +license = "Apache-2.0" +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +log = "0.4" +proxy-wasm = { path = "../../" } + +[profile.release] +lto = true +opt-level = 3 +codegen-units = 1 +panic = "abort" +strip = "debuginfo" diff --git a/examples/metrics/README.md b/examples/metrics/README.md new file mode 100644 index 00000000..ff15d06d --- /dev/null +++ b/examples/metrics/README.md @@ -0,0 +1,32 @@ +## Proxy-Wasm plugin example: HTTP headers + +Proxy-Wasm plugin that logs HTTP request/response headers. + +### Building + +```sh +$ cargo build --target wasm32-wasi --release +``` + +### Using in Envoy + +This example can be run with [`docker compose`](https://docs.docker.com/compose/install/) +and has a matching Envoy configuration. + +```sh +$ docker compose up +``` + +Send HTTP request to `localhost:10000/`: + +```sh +$ curl localhost:10000/ -H "x-envoy-wasm-metric-value: 100" -H "x-envoy-wasm-metric: gauge" +``` + +For instance that request will set the example gauge to 100. Which you can see using the stats endpoint + +```sh +& curl -s localhost:9001/stats | grep wasmcustom.wasm_gauge + +100 +``` diff --git a/examples/metrics/docker-compose.yaml b/examples/metrics/docker-compose.yaml new file mode 100644 index 00000000..ea2da1f1 --- /dev/null +++ b/examples/metrics/docker-compose.yaml @@ -0,0 +1,28 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +services: + envoy: + image: envoyproxy/envoy:v1.24-latest + hostname: envoy + ports: + - "10000:10000" + - "9001:9001" + volumes: + - ./envoy.yaml:/etc/envoy/envoy.yaml + - ./target/wasm32-wasi/release:/etc/envoy/proxy-wasm-plugins + networks: + - envoymesh +networks: + envoymesh: {} diff --git a/examples/metrics/envoy.yaml b/examples/metrics/envoy.yaml new file mode 100644 index 00000000..d7ff3233 --- /dev/null +++ b/examples/metrics/envoy.yaml @@ -0,0 +1,61 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +static_resources: + listeners: + address: + socket_address: + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + codec_type: AUTO + route_config: + name: local_routes + virtual_hosts: + - name: local_service + domains: + - "*" + routes: + - match: + prefix: "/" + direct_response: + status: 200 + body: + inline_string: "Request /hello and be welcomed!\n" + http_filters: + - name: envoy.filters.http.wasm + typed_config: + "@type": type.googleapis.com/udpa.type.v1.TypedStruct + type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm + value: + config: + name: "http_headers" + vm_config: + runtime: "envoy.wasm.runtime.v8" + code: + local: + filename: "/etc/envoy/proxy-wasm-plugins/proxy_wasm_example_metrics.wasm" + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + +admin: + profile_path: /tmp/envoy.prof + address: + socket_address: { address: 0.0.0.0, port_value: 9001 } diff --git a/examples/metrics/src/lib.rs b/examples/metrics/src/lib.rs new file mode 100644 index 00000000..348d8aa0 --- /dev/null +++ b/examples/metrics/src/lib.rs @@ -0,0 +1,86 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use proxy_wasm::stats; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +use std::convert::TryInto; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { Box::new( + MetricsRootContext { + metrics: WasmMetrics { + counter: stats::Counter::new(String::from("wasm_counter")), + gauge: stats::Gauge::new(String::from("wasm_gauge")), + histogram: stats::Histogram::new(String::from("wasm_histogram")), + } + } + )}); +}} + +#[derive(Copy, Clone)] +struct WasmMetrics { + counter: stats::Counter, + gauge: stats::Gauge, + histogram: stats::Histogram, +} + +struct MetricsRootContext { + metrics: WasmMetrics, +} + +impl Context for MetricsRootContext {} + +impl RootContext for MetricsRootContext { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(StreamContext { + metrics: self.metrics, + })) + } +} + +struct StreamContext { + metrics: WasmMetrics, +} + +impl Context for StreamContext {} + +impl HttpContext for StreamContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let value = match self.get_http_request_header("x-envoy-wasm-metric-value") { + Some(value) => value.parse::().unwrap(), + _ => 0, + }; + + let metric_type = match self.get_http_request_header("x-envoy-wasm-metric") { + Some(metric_type) => metric_type, + _ => return Action::Continue, + }; + + match metric_type.as_str() { + "counter" => self.metrics.counter.increment(value), + "gauge" => self.metrics.gauge.record(value.try_into().unwrap()), + "histogram" => self.metrics.histogram.record(value.try_into().unwrap()), + _ => return Action::Continue, + } + + Action::Continue + } +} diff --git a/src/stats.rs b/src/stats.rs index 726a4d98..08335848 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -16,18 +16,16 @@ use crate::hostcalls; use crate::traits; use crate::types; +#[derive(Copy, Clone)] pub struct Counter { id: u32, - name: String, } impl Counter { - pub fn counter(name: String) -> Counter { - let returned_id = hostcalls::define_metric(types::MetricType::Counter, &name).unwrap(); - Counter { - id: returned_id, - name, - } + pub fn new(name: String) -> Counter { + let returned_id = hostcalls::define_metric(types::MetricType::Counter, &name) + .expect("failed to define counter '{}', name"); + Counter { id: returned_id } } } @@ -35,29 +33,20 @@ impl traits::Metric for Counter { fn id(&self) -> u32 { self.id } - - fn name(&self) -> &str { - self.name.as_str() - } - - fn record(&self, _: u64) { - // A Counter can only be incremented. - return; - } } +impl traits::IncrementingMetric for Counter {} + +#[derive(Copy, Clone)] pub struct Gauge { id: u32, - name: String, } impl Gauge { - pub fn gauge(name: String) -> Gauge { - let returned_id = hostcalls::define_metric(types::MetricType::Gauge, &name).unwrap(); - Gauge { - id: returned_id, - name, - } + pub fn new(name: String) -> Gauge { + let returned_id = hostcalls::define_metric(types::MetricType::Gauge, &name) + .expect("failed to define gauge '{}', name"); + Gauge { id: returned_id } } } @@ -65,29 +54,20 @@ impl traits::Metric for Gauge { fn id(&self) -> u32 { self.id } - - fn name(&self) -> &str { - self.name.as_str() - } - - fn increment(&self, _: i64) { - // A gauge can only be recorded. - return; - } } +impl traits::RecordingMetric for Gauge {} + +#[derive(Copy, Clone)] pub struct Histogram { id: u32, - name: String, } impl Histogram { - pub fn histogram(name: String) -> Histogram { - let returned_id = hostcalls::define_metric(types::MetricType::Histogram, &name).unwrap(); - Histogram { - id: returned_id, - name, - } + pub fn new(name: String) -> Histogram { + let returned_id = hostcalls::define_metric(types::MetricType::Histogram, &name) + .expect("failed to define histogram '{}', name"); + Histogram { id: returned_id } } } @@ -95,13 +75,6 @@ impl traits::Metric for Histogram { fn id(&self) -> u32 { self.id } - - fn name(&self) -> &str { - self.name.as_str() - } - - fn increment(&self, _: i64) { - // A Histogram can only be recorded. - return; - } } + +impl traits::RecordingMetric for Histogram {} diff --git a/src/traits.rs b/src/traits.rs index a4ed32b2..ab073a77 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -536,29 +536,32 @@ pub trait HttpContext: Context { } pub trait Metric { - fn name(&self) -> &str; fn id(&self) -> u32; fn value(&self) -> u64 { match hostcalls::get_metric(self.id()) { Ok(value) => value, - Err(Status::NotFound) => panic!("metric not found: {}", self.name()), + Err(Status::NotFound) => panic!("metric not found: {}", self.id()), Err(err) => panic!("unexpected status: {:?}", err), } } +} - fn record(&self, value: u64) { - match hostcalls::record_metric(self.id(), value) { +pub trait IncrementingMetric: Metric { + fn increment(&self, offset: i64) { + match hostcalls::increment_metric(self.id(), offset) { Ok(_) => return, - Err(Status::NotFound) => panic!("metric not found: {}", self.name()), + Err(Status::NotFound) => panic!("metric not found: {}", self.id()), Err(err) => panic!("unexpected status: {:?}", err), } } +} - fn increment(&self, offset: i64) { - match hostcalls::increment_metric(self.id(), offset) { +pub trait RecordingMetric: Metric { + fn record(&self, value: u64) { + match hostcalls::record_metric(self.id(), value) { Ok(_) => return, - Err(Status::NotFound) => panic!("metric not found: {}", self.name()), + Err(Status::NotFound) => panic!("metric not found: {}", self.id()), Err(err) => panic!("unexpected status: {:?}", err), } }