diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 35cc2fa9..7909c3e7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -193,6 +193,7 @@ jobs: - 'http_body' - 'http_config' - 'http_headers' + - 'metrics' defaults: run: @@ -247,6 +248,7 @@ jobs: - 'http_body' - 'http_config' - 'http_headers' + - 'metrics' defaults: run: diff --git a/README.md b/README.md index 89cb2021..d5269cfe 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..4bc8771a --- /dev/null +++ b/examples/metrics/Cargo.toml @@ -0,0 +1,22 @@ +[package] +publish = false +name = "proxy-wasm-example-metrics" +version = "0.0.1" +authors = ["Caio Ramos Casimiro "] +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..c2b1a223 --- /dev/null +++ b/examples/metrics/README.md @@ -0,0 +1,30 @@ +## Proxy-Wasm plugin example: Metrics + +Proxy-Wasm plugin that demonstrates how to define and update metrics. + +### 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 requests to `localhost:10000/headers`: + + +Retrieve metrics from `localhost:9901/stats`: + +```console +$ curl -s localhost:9901/stats | grep 'example' +wasmcustom.conter_example: 1 +wasmcustom.gauge_example: 10 +wasmcustom.histogram_example: P0(nan,8) P25(nan,8.075) P50(nan,9.05) P75(nan,10.25) P90(nan,10.7) P95(nan,10.85) P99(nan,10.969999999999999) P99.5(nan,10.985) P99.9(nan,10.997) P100(nan,11) +``` diff --git a/examples/metrics/docker-compose.yaml b/examples/metrics/docker-compose.yaml new file mode 100644 index 00000000..4abf6e09 --- /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" + - "9901:9901" + 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..a0b18f51 --- /dev/null +++ b/examples/metrics/envoy.yaml @@ -0,0 +1,73 @@ +# 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: "/" + route: + cluster: httpbin + 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: "metrics" + 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 + clusters: + - name: httpbin + connect_timeout: 5s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: httpbin + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: httpbin.org + port_value: 80 + hostname: "httpbin.org" +admin: + profile_path: /tmp/envoy.prof + address: + socket_address: { address: 0.0.0.0, port_value: 9901 } diff --git a/examples/metrics/src/lib.rs b/examples/metrics/src/lib.rs new file mode 100644 index 00000000..89181770 --- /dev/null +++ b/examples/metrics/src/lib.rs @@ -0,0 +1,113 @@ +// 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 log::info; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +#[derive(Copy, Clone)] +struct HttpAuthMetrics { + counter_example: u32, + gauge_example: u32, + histogram_example: u32, +} + +struct HttpAuthRoot { + metrics: HttpAuthMetrics, + monitored_path: String, +} + +struct HttpAuth { + metrics: HttpAuthMetrics, + monitored_path: String, +} + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { + Box::new(HttpAuthRoot { + metrics: HttpAuthMetrics { + counter_example: 0, + gauge_example: 0, + histogram_example: 0, + }, + monitored_path: "".to_string(), + }) + }); +}} + +impl Context for HttpAuth {} +impl HttpContext for HttpAuth { + fn on_http_request_headers(&mut self, num_headers: usize, _: bool) -> Action { + self.record_metric(self.metrics.histogram_example, num_headers as u64) + .unwrap(); + + match self.get_http_request_header(":path") { + Some(path) if path == self.monitored_path => { + info!("Monitored path {} accessed.", self.monitored_path); + + self.increment_metric(self.metrics.counter_example, 1) + .unwrap(); + + Action::Continue + } + _ => Action::Continue, + } + } + + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + let counter_value = self.get_metric(self.metrics.counter_example).unwrap(); + let gauge_value = self.get_metric(self.metrics.gauge_example).unwrap(); + // histogram retrieval isn't supported + + self.set_http_response_header("Powered-By", Some("proxy-wasm")); + self.set_http_response_header("My-Counter", Some(format!("{}", counter_value).as_str())); + self.set_http_response_header("My-Gauge", Some(format!("{}", gauge_value).as_str())); + + Action::Continue + } +} + +impl Context for HttpAuthRoot {} + +impl RootContext for HttpAuthRoot { + fn on_configure(&mut self, _: usize) -> bool { + self.metrics.counter_example = self + .define_metric(MetricType::Counter, "counter_example") + .expect("failed defining counter_example metric"); + self.metrics.gauge_example = self + .define_metric(MetricType::Gauge, "gauge_example") + .expect("failed defining gauge_example metric"); + self.metrics.histogram_example = self + .define_metric(MetricType::Histogram, "histogram_example") + .expect("failed defining histogram_example metric"); + + self.record_metric(self.metrics.gauge_example, 10).unwrap(); + + self.monitored_path = "/headers".to_string(); + + true + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(HttpAuth { + metrics: self.metrics, + monitored_path: self.monitored_path.clone(), + })) + } + + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } +} diff --git a/src/traits.rs b/src/traits.rs index 034f87ea..691f3b43 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -58,6 +58,37 @@ pub trait Context { hostcalls::enqueue_shared_queue(queue_id, value) } + fn define_metric( + &self, + metric_type: MetricType, + name: &str, + ) -> Result { + hostcalls::define_metric(metric_type, name) + } + + fn get_metric( + &self, + metric_id: u32, + ) -> Result { + hostcalls::get_metric(metric_id) + } + + fn record_metric( + &self, + metric_id: u32, + value: u64, + ) -> Result<(), Status> { + hostcalls::record_metric(metric_id, value) + } + + fn increment_metric( + &self, + metric_id: u32, + offset: i64, + ) -> Result<(), Status> { + hostcalls::increment_metric(metric_id, offset) + } + fn dispatch_http_call( &self, upstream: &str,