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/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..08335848 --- /dev/null +++ b/src/stats.rs @@ -0,0 +1,80 @@ +// 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; + +#[derive(Copy, Clone)] +pub struct Counter { + id: u32, +} + +impl Counter { + 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 } + } +} + +impl traits::Metric for Counter { + fn id(&self) -> u32 { + self.id + } +} + +impl traits::IncrementingMetric for Counter {} + +#[derive(Copy, Clone)] +pub struct Gauge { + id: u32, +} + +impl Gauge { + 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 } + } +} + +impl traits::Metric for Gauge { + fn id(&self) -> u32 { + self.id + } +} + +impl traits::RecordingMetric for Gauge {} + +#[derive(Copy, Clone)] +pub struct Histogram { + id: u32, +} + +impl Histogram { + 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 } + } +} + +impl traits::Metric for Histogram { + fn id(&self) -> u32 { + self.id + } +} + +impl traits::RecordingMetric for Histogram {} diff --git a/src/traits.rs b/src/traits.rs index 034f87ea..ab073a77 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -534,3 +534,35 @@ pub trait HttpContext: Context { fn on_log(&mut self) {} } + +pub trait Metric { + 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.id()), + Err(err) => panic!("unexpected status: {:?}", err), + } + } +} + +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.id()), + Err(err) => panic!("unexpected status: {:?}", err), + } + } +} + +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.id()), + Err(err) => panic!("unexpected status: {:?}", err), + } + } +}