diff --git a/src/main/java/io/roastedroot/proxywasm/impl/Imports.java b/src/main/java/io/roastedroot/proxywasm/impl/Imports.java index 61c6027..7c677ba 100644 --- a/src/main/java/io/roastedroot/proxywasm/impl/Imports.java +++ b/src/main/java/io/roastedroot/proxywasm/impl/Imports.java @@ -10,6 +10,7 @@ import io.roastedroot.proxywasm.v1.Handler; import io.roastedroot.proxywasm.v1.LogLevel; import io.roastedroot.proxywasm.v1.MapType; +import io.roastedroot.proxywasm.v1.MetricType; import io.roastedroot.proxywasm.v1.StreamType; import io.roastedroot.proxywasm.v1.WasmException; import io.roastedroot.proxywasm.v1.WasmResult; @@ -889,4 +890,50 @@ int proxyCallForeignFunction( return e.result().getValue(); } } + + @WasmExport + int proxyDefineMetric(int metricType, int nameDataPtr, int nameSize, int returnMetricId) { + try { + MetricType type = MetricType.fromInt(metricType); + if (type == null) { + return WasmResult.BAD_ARGUMENT.getValue(); + } + + var name = string(readMemory(nameDataPtr, nameSize)); + int metricId = handler.defineMetric(type, name); + putUint32(returnMetricId, metricId); + return WasmResult.OK.getValue(); + } catch (WasmException e) { + return e.result().getValue(); + } + } + + @WasmExport + int proxyRecordMetric(int metricId, long value) { + WasmResult result = handler.recordMetric(metricId, value); + return result.getValue(); + } + + @WasmExport + int proxyRemoveMetric(int metricId) { + WasmResult result = handler.removeMetric(metricId); + return result.getValue(); + } + + @WasmExport + int proxyIncrementMetric(int metricId, long value) { + WasmResult result = handler.incrementMetric(metricId, value); + return result.getValue(); + } + + @WasmExport + int proxyGetMetric(int metricId, int returnValuePtr) { + try { + var result = handler.getMetric(metricId); + putUint32(returnValuePtr, (int) result); + return WasmResult.OK.getValue(); + } catch (WasmException e) { + return e.result().getValue(); + } + } } diff --git a/src/main/java/io/roastedroot/proxywasm/v1/ChainedHandler.java b/src/main/java/io/roastedroot/proxywasm/v1/ChainedHandler.java index 98aaf7a..cd1f906 100644 --- a/src/main/java/io/roastedroot/proxywasm/v1/ChainedHandler.java +++ b/src/main/java/io/roastedroot/proxywasm/v1/ChainedHandler.java @@ -281,4 +281,29 @@ public int dispatchHttpCall( public byte[] callForeignFunction(String name, byte[] bytes) throws WasmException { return next().callForeignFunction(name, bytes); } + + @Override + public int defineMetric(MetricType metricType, String name) throws WasmException { + return next().defineMetric(metricType, name); + } + + @Override + public WasmResult removeMetric(int metricId) { + return next().removeMetric(metricId); + } + + @Override + public WasmResult recordMetric(int metricId, long value) { + return next().recordMetric(metricId, value); + } + + @Override + public WasmResult incrementMetric(int metricId, long value) { + return next().incrementMetric(metricId, value); + } + + @Override + public long getMetric(int metricId) throws WasmException { + return next().getMetric(metricId); + } } diff --git a/src/main/java/io/roastedroot/proxywasm/v1/Handler.java b/src/main/java/io/roastedroot/proxywasm/v1/Handler.java index 429b5ef..42fee6f 100644 --- a/src/main/java/io/roastedroot/proxywasm/v1/Handler.java +++ b/src/main/java/io/roastedroot/proxywasm/v1/Handler.java @@ -410,4 +410,24 @@ default int dispatchHttpCall( default byte[] callForeignFunction(String name, byte[] bytes) throws WasmException { throw new WasmException(WasmResult.NOT_FOUND); } + + default int defineMetric(MetricType metricType, String name) throws WasmException { + throw new WasmException(WasmResult.UNIMPLEMENTED); + } + + default WasmResult removeMetric(int metricId) { + return WasmResult.UNIMPLEMENTED; + } + + default WasmResult recordMetric(int metricId, long value) { + return WasmResult.UNIMPLEMENTED; + } + + default WasmResult incrementMetric(int metricId, long value) { + return WasmResult.UNIMPLEMENTED; + } + + default long getMetric(int metricId) throws WasmException { + throw new WasmException(WasmResult.UNIMPLEMENTED); + } } diff --git a/src/main/java/io/roastedroot/proxywasm/v1/MetricType.java b/src/main/java/io/roastedroot/proxywasm/v1/MetricType.java new file mode 100644 index 0000000..5e354d6 --- /dev/null +++ b/src/main/java/io/roastedroot/proxywasm/v1/MetricType.java @@ -0,0 +1,45 @@ +package io.roastedroot.proxywasm.v1; + +/** + * Represents the type of metric in proxy WASM. + */ +public enum MetricType { + COUNTER(0), + GAUGE(1), + HISTOGRAM(2); + + private final int value; + + /** + * Constructor for MetricType enum. + * + * @param value The integer value of the metric type + */ + MetricType(int value) { + this.value = value; + } + + /** + * Get the integer value of this metric type. + * + * @return The integer value + */ + public int getValue() { + return value; + } + + /** + * Convert an integer value to a MetricType. + * + * @param value The integer value to convert + * @return The corresponding MetricType or null if the value doesn't match any MetricType + */ + public static MetricType fromInt(int value) { + for (MetricType type : values()) { + if (type.value == value) { + return type; + } + } + return null; + } +} diff --git a/src/test/go-examples/metrics/README.md b/src/test/go-examples/metrics/README.md new file mode 100644 index 0000000..dd8b11d --- /dev/null +++ b/src/test/go-examples/metrics/README.md @@ -0,0 +1,5 @@ +## Attribution + +This example originally came from: +https://github.com/proxy-wasm/proxy-wasm-go-sdk/blob/ab4161dcf9246a828008b539a82a1556cf0f2e24/examples/metrics +``` diff --git a/src/test/go-examples/metrics/go.mod b/src/test/go-examples/metrics/go.mod new file mode 100644 index 0000000..8a1b608 --- /dev/null +++ b/src/test/go-examples/metrics/go.mod @@ -0,0 +1,5 @@ +module github.com/proxy-wasm/proxy-wasm-go-sdk/examples/metrics + +go 1.24 + +require github.com/proxy-wasm/proxy-wasm-go-sdk v0.0.0-20250212164326-ab4161dcf924 diff --git a/src/test/go-examples/metrics/go.sum b/src/test/go-examples/metrics/go.sum new file mode 100644 index 0000000..3ddb896 --- /dev/null +++ b/src/test/go-examples/metrics/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/proxy-wasm/proxy-wasm-go-sdk v0.0.0-20250212164326-ab4161dcf924 h1:wTcK6gcyTKJMeDka69AMjZYvisdI8CBXzTEfZ+2pOxI= +github.com/proxy-wasm/proxy-wasm-go-sdk v0.0.0-20250212164326-ab4161dcf924/go.mod h1:9mBRvh8I6Td6sg3CwEY+zGFE4DKaIoieCaca1kQnDBE= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/test/go-examples/metrics/main.go b/src/test/go-examples/metrics/main.go new file mode 100644 index 0000000..c05ebd9 --- /dev/null +++ b/src/test/go-examples/metrics/main.go @@ -0,0 +1,85 @@ +// Copyright 2020-2024 Tetrate +// +// 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. + +package main + +import ( + "fmt" + + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm" + "github.com/proxy-wasm/proxy-wasm-go-sdk/proxywasm/types" +) + +func main() {} +func init() { + proxywasm.SetVMContext(&vmContext{}) +} + +// vmContext implements types.VMContext. +type vmContext struct { + // Embed the default VM context here, + // so that we don't need to reimplement all the methods. + types.DefaultVMContext +} + +// NewPluginContext implements types.VMContext. +func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { + return &metricPluginContext{} +} + +// metricPluginContext implements types.PluginContext. +type metricPluginContext struct { + // Embed the default plugin context here, + // so that we don't need to reimplement all the methods. + types.DefaultPluginContext +} + +// NewHttpContext implements types.PluginContext. +func (ctx *metricPluginContext) NewHttpContext(contextID uint32) types.HttpContext { + return &metricHttpContext{} +} + +// metricHttpContext implements types.HttpContext. +type metricHttpContext struct { + // Embed the default http context here, + // so that we don't need to reimplement all the methods. + types.DefaultHttpContext +} + +const ( + customHeaderKey = "my-custom-header" + customHeaderValueTagKey = "value" +) + +// counters is a map from custom header value to a counter metric. +// Note that Proxy-Wasm plugins are single threaded, so no need to use a lock. +var counters = map[string]proxywasm.MetricCounter{} + +// OnHttpRequestHeaders implements types.HttpContext. +func (ctx *metricHttpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action { + customHeaderValue, err := proxywasm.GetHttpRequestHeader(customHeaderKey) + if err == nil { + counter, ok := counters[customHeaderValue] + if !ok { + // This metric is processed as: custom_header_value_counts{value="foo",reporter="wasmgosdk"} n. + // The extraction rule is defined in envoy.yaml as a bootstrap configuration. + // See https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/metrics/v3/stats.proto#config-metrics-v3-statsconfig. + fqn := fmt.Sprintf("custom_header_value_counts_%s=%s_reporter=wasmgosdk", customHeaderValueTagKey, customHeaderValue) + counter = proxywasm.DefineCounterMetric(fqn) + counters[customHeaderValue] = counter + } + counter.Increment(1) + } + return types.ActionContinue +} diff --git a/src/test/go-examples/metrics/main.wasm b/src/test/go-examples/metrics/main.wasm new file mode 100644 index 0000000..416f492 Binary files /dev/null and b/src/test/go-examples/metrics/main.wasm differ diff --git a/src/test/java/io/roastedroot/proxywasm/MetricsTest.java b/src/test/java/io/roastedroot/proxywasm/MetricsTest.java new file mode 100644 index 0000000..99a1d02 --- /dev/null +++ b/src/test/java/io/roastedroot/proxywasm/MetricsTest.java @@ -0,0 +1,49 @@ +package io.roastedroot.proxywasm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.dylibso.chicory.wasm.Parser; +import io.roastedroot.proxywasm.v1.Action; +import io.roastedroot.proxywasm.v1.MetricType; +import io.roastedroot.proxywasm.v1.ProxyWasm; +import io.roastedroot.proxywasm.v1.StartException; +import java.nio.file.Path; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Java port of https://github.com/proxy-wasm/proxy-wasm-go-sdk/blob/ab4161dcf9246a828008b539a82a1556cf0f2e24/examples/metrics/main_test.go + */ +public class MetricsTest { + + @Test + public void testMetric() throws StartException { + var handler = new MockHandler(); + var module = Parser.parse(Path.of("./src/test/go-examples/metrics/main.wasm")); + ProxyWasm.Builder builder = ProxyWasm.builder().withPluginHandler(handler); + + try (var host = builder.build(module)) { + try (var context = host.createHttpContext(handler)) { + // Create headers with custom header + Map headers = Map.of("my-custom-header", "foo"); + + // Call OnRequestHeaders multiple times + long expectedCount = 3; + for (int i = 0; i < expectedCount; i++) { + handler.setHttpRequestHeaders(headers); + Action action = context.callOnRequestHeaders(false); + assertEquals(Action.CONTINUE, action); + } + + // Check metrics + var metric = + handler.getMetric( + "custom_header_value_counts_value=foo_reporter=wasmgosdk"); + assertNotNull(metric); + assertEquals(MetricType.COUNTER, metric.type); + assertEquals(expectedCount, metric.value); + } + } + } +} diff --git a/src/test/java/io/roastedroot/proxywasm/MockHandler.java b/src/test/java/io/roastedroot/proxywasm/MockHandler.java index 235dfb9..0677e1a 100644 --- a/src/test/java/io/roastedroot/proxywasm/MockHandler.java +++ b/src/test/java/io/roastedroot/proxywasm/MockHandler.java @@ -7,6 +7,7 @@ import io.roastedroot.proxywasm.v1.Handler; import io.roastedroot.proxywasm.v1.Helpers; import io.roastedroot.proxywasm.v1.LogLevel; +import io.roastedroot.proxywasm.v1.MetricType; import io.roastedroot.proxywasm.v1.WasmException; import io.roastedroot.proxywasm.v1.WasmResult; import java.util.ArrayList; @@ -350,4 +351,74 @@ public int dispatchHttpCall( httpCalls.put(id, value); return id; } + + public static class Metric { + + public final int id; + public final MetricType type; + public final String name; + public long value; + + public Metric(int id, MetricType type, String name) { + this.id = id; + this.type = type; + this.name = name; + } + } + + private final AtomicInteger lastMetricId = new AtomicInteger(0); + private HashMap metrics = new HashMap<>(); + private HashMap metricsByName = new HashMap<>(); + + @Override + public int defineMetric(MetricType type, String name) throws WasmException { + var id = lastMetricId.incrementAndGet(); + Metric value = new Metric(id, type, name); + metrics.put(id, value); + metricsByName.put(name, value); + return id; + } + + @Override + public long getMetric(int metricId) throws WasmException { + var metric = metrics.get(metricId); + if (metric == null) { + throw new WasmException(WasmResult.NOT_FOUND); + } + return metric.value; + } + + public Metric getMetric(String name) { + return metricsByName.get(name); + } + + @Override + public WasmResult incrementMetric(int metricId, long value) { + var metric = metrics.get(metricId); + if (metric == null) { + return WasmResult.NOT_FOUND; + } + metric.value += value; + return WasmResult.OK; + } + + @Override + public WasmResult recordMetric(int metricId, long value) { + var metric = metrics.get(metricId); + if (metric == null) { + return WasmResult.NOT_FOUND; + } + metric.value = value; + return WasmResult.OK; + } + + @Override + public WasmResult removeMetric(int metricId) { + Metric metric = metrics.remove(metricId); + if (metric == null) { + return WasmResult.NOT_FOUND; + } + metricsByName.remove(metric.name); + return WasmResult.OK; + } }