A Rust-based Proxy-Wasm filter that injects OpenAPI operations (e.g., GET /users/{id}) into request headers, which can be mapped as Istio metric labels via the Telemetry API to enable endpoint-level observability without cardinality explosion.
- OpenAPI Endpoint Identification: Adds the endpoint corresponding to the request path as the
x-api-endpointrequest header (e.g.,GET /foo/{foo-id}), as well as thex-path-templateheader (e.g.,/foo/{foo-id}) for the path template.- Map headers to Istio metric labels: Use
tagOverridesin the IstioTelemetryAPI; the WASM only creates headers and does not alter metrics directly. Refer to./resources/telemetry.yaml.
- Map headers to Istio metric labels: Use
- High-Performance Path Matching: Utilizes the Radix tree-based path matching algorithm via the
matchitcrate, whose own benchmarks report it as the fastest. - Uses OpenAPI Path Syntax: There is no need to transform OpenAPI documents. You can insert them directly into the config as-is (although removing items other than path templates is recommended for readability). Refer to
./resources/wasmplugin.yaml. - Support for Multiple OpenAPI Documents: The
namefield represents the service name of the respective OpenAPI document and is added as thex-service-namerequest header, which can be used in the same manner asx-api-endpointfor Istio metric labels. Refer to./resources/wasmplugin.yaml.
Request β Istio Proxy (Sidecar or Ingress Gateway or Waypoint)
β openapi-endpoint-filter (Proxy-Wasm)
β x-api-endpoint header injection
β request_endpoint label mapping (via Istio Telemetry API)
- OpenAPI 3.x specification file
Create an Istio WasmPlugin manifest by referring to resources/wasmplugin.yaml and apply it.
Create or update an Istio Telemetry manifest by referring to resources/telemetry.yaml and apply it.
Run a curl command to test the endpoint:
curl <SERVICE_URL>/users/123Verify that the injected headers are present:
# Expected output:
x-api-endpoint: GET /users/{id}
x-path-template: /users/{id}
x-service-name: userserviceThen, verify the metrics (e.g., in Prometheus):
# Query:
sum by (request_service, request_endpoint, request_path_template) (istio_requests_total)
# Expected output:
request_service="userservice", request_endpoint="GET /users/{id}", request_path_template="/users/{id}"- The smallest unit for traffic identification in Istio metrics is the workload. However, identification at the endpoint (path + method) level is highly useful in real-world scenarios.
- While the Istio
TelemetryAPI allows adding method and path labels for identification, it cannot map a path to its corresponding template that includes path parameters, making endpoint-level identification impossible. - Classifying Metrics Based on Request or Response discusses endpoint-level identification using the
attributegenplugin, but the plugin's path matching algorithm relies on regex and scan operations, resulting in performance issues. Additionally, converting OpenAPI path templates into regex-based patterns is required.
- Matching key: Requests are matched using host (and basePath from OpenAPI
servers), HTTP method, and normalized path template. - Host matching toggle: If
useHostInMatchisfalse, host is ignored and only basePath/method/path are used (basePath matching still applies). - Header preservation:
preserveExistingHeadersdefault:true. When enabled, if the request already includesx-api-endpoint,x-path-template, orx-service-name, the WASM does not recompute or replace them. - Matching fallback: If no route matches, the plugin sets
unknownvalues (e.g.,x-api-endpoint: <METHOD> unknown,x-path-template: unknown,x-service-name: unknown). - Config errors: On config parse errors, the filter fails open and injects
config-errorinto all three headers for observability. - Host/method rules:
- Host is read from
:authorityorhost, lowercased, and port-stripped. - If a path item has no HTTP methods, all methods are allowed for that path.
- Host is read from
- OpenAPI servers:
servers.urlandvariablesare expanded for host/basePath matching (max 100 expansions).
wasmplugin.yaml: Register OpenAPI path templates and service names. You can specify multiple services and their paths at once.useHostInMatch: Whether to match request host against servers.url host (default:true)preserveExistingHeaders: Preserve existingx-*headers from upstream (default:true)services: List of service names and their OpenAPI path templates
telemetry.yaml: Maps the headers added by the plugin (x-api-endpoint,x-path-template,x-service-name) to Istio metric labels usingtagOverrides. ThetagOverrideskeys are the metric label names (e.g.,request_endpoint,request_path_template,request_service) and the values read from request headers.
Minimal example showing how a request maps to injected headers and metric labels (when Telemetry API tagOverrides is enabled).
# Config (partial)
pluginConfig:
services:
- name: userservice
servers:
- url: https://api.example.com/v1
paths:
/users/{id}: {}
# Request
:method: GET
:authority: api.example.com
:path: /v1/users/42
# Output headers
x-api-endpoint: GET /users/{id}
x-path-template: /users/{id}
x-service-name: userservice
# Output labels on Istio metrics (e.g., istio_requests_total) via Telemetry tagOverrides.
istio_requests_total{
request_endpoint="GET /users/{id}",
request_path_template="/users/{id}",
request_service="userservice",
request_method="GET", # Telemetry API (not from openapi-endpoint-filter)
request_host="api.example.com" # Telemetry API (not from openapi-endpoint-filter)
}Shows host matching across multiple server variable expansions.
# Config (partial)
pluginConfig:
services:
- name: userservice
servers:
- url: https://{env}.example.com/v1
variables:
env:
default: api
enum: [api, staging]
paths:
/users/{id}: {}
# Requests
- method: GET
host: api.example.com
path: /v1/users/42
- method: GET
host: staging.example.com
path: /v1/users/42
# Output headers
x-api-endpoint: GET /users/{id}
x-path-template: /users/{id}
x-service-name: userservice
# Output labels on Istio metrics (e.g., istio_requests_total) via Telemetry tagOverrides.
istio_requests_total{
request_endpoint="GET /users/{id}",
request_path_template="/users/{id}",
request_service="userservice",
request_method="GET", # Telemetry API (not from openapi-endpoint-filter)
request_host="<api.example.com | staging.example.com>" # per request host; Telemetry API (not from openapi-endpoint-filter)
}For detailed examples, refer to each config file in resources directory
You can use the published image anyflow/openapi-endpoint-filter:<version> without building locally. Refer to resources/wasmplugin.yaml for configuration.
# Install Rust. Note: Installing via brew on macOS may not compile correctly. Follow the official Rust installation path instead.
> curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install cargo-make (build tool; refer to Makefile.toml).
> cargo install cargo-make
# Install wasm-opt (binaryen) (for macOS; other OSes require a different method. If installation fails, you can skip this step by removing the optimize-wasm task from Makefile.toml).
> brew install binaryen
# Create a .env file at the root and set DOCKER_IMAGE_PATH. Example below:
DOCKER_IMAGE_PATH=anyflow/openapi-endpoint-filter
# Run tests -> Rust build -> Image optimization -> Docker build -> Docker push
> cargo make deploy# Change the WASM log level of the target pod to debug.
> istioctl pc log -n <namespace name> <pod name> --level wasm:debug
# Filter logs to show only openapi-endpoint-filter.
> k logs -n <namespace name> <pod name> -f | grep -F '[oef]'
# Apply resources/telemetry.yaml: To use the x-path-template, x-service-name headers and the method as metric labels `request_path`, `request_service` and `request_method`.
> kubectl apply -f telemetry.yaml
# Apply resources/wasmplugin.yaml: Check the logs to confirm successful loading, e.g., "[oef] Router configured successfully".
> kubectl apply -f wasmplugin.yaml
# Make a curl request and verify if the matching success log appears, e.g., "[oef] /dockebi/v1/stuff matched with dockebi, /dockebi/v1/stuff".This is used for log grepping. Grepping with just openapi-endpoint-filter isn't feasible because, as shown below, it's automatically included in some cases but not in others.
2025-02-23T20:30:59.970615Z debug envoy wasm external/envoy/source/extensions/common/wasm/context.cc:1192 wasm log cluster.openapi-endpoint-filter: [oef] Creating HTTP context thread=292025-02-23T20:28:39.632084Z info envoy wasm external/envoy/source/extensions/common/wasm/context.cc:1195 wasm log: [oef] Router configured successfully thread=20
Even after running kubectl delete -f wasmplugin.yaml, the WASM isn't immediately removed from Envoy; it seems to take 30β60 seconds. You can confirm this with logs like the one below. If you need to test new WASM behavior, remove the existing WASM, wait for the message below, and then load the new WASM.
2025-02-23T19:35:58.014282Z info envoy wasm external/envoy/source/extensions/common/wasm/context.cc:1195 wasm log: openapi-endpoint-filter terminated thread=20
openapi-endpoint-filter is released under the Apache License, version 2.0.