From 122d220c015f886b7302ca09fcf2dc7f0f990ecc Mon Sep 17 00:00:00 2001 From: Muhammad Azeez Date: Mon, 11 Dec 2023 16:52:20 +0300 Subject: [PATCH 1/5] feat: integrate observe-sdk into Extism --- extism.go | 60 +++++++++++++++++++++++++++++++-------- extism_test.go | 51 +++++++++++++++++++++++++++++++++ go.mod | 14 +++++++-- go.sum | 17 +++++++++++ runtime.go | 2 +- wasm/nested.c.instr.wasm | Bin 0 -> 63468 bytes 6 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 wasm/nested.c.instr.wasm diff --git a/extism.go b/extism.go index 84cff91..b5e8f93 100644 --- a/extism.go +++ b/extism.go @@ -14,12 +14,18 @@ import ( "os" "time" + observe "github.com/dylibso/observe-sdk/go" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" "github.com/tetratelabs/wazero/sys" ) +type module struct { + module api.Module + wasm []byte +} + //go:embed extism-runtime.wasm var extismRuntimeWasm []byte @@ -34,10 +40,12 @@ type Runtime struct { // PluginConfig contains configuration options for the Extism plugin. type PluginConfig struct { - ModuleConfig wazero.ModuleConfig - RuntimeConfig wazero.RuntimeConfig - EnableWasi bool - LogLevel LogLevel + ModuleConfig wazero.ModuleConfig + RuntimeConfig wazero.RuntimeConfig + EnableWasi bool + LogLevel LogLevel + ObserveAdapter *observe.AdapterBase + ObserveOptions *observe.Options } // HttpRequest represents an HTTP request to be made by the plugin. @@ -80,8 +88,8 @@ func (l LogLevel) String() string { // Plugin is used to call WASM functions type Plugin struct { Runtime *Runtime - Modules map[string]api.Module - Main api.Module + Modules map[string]module + Main module Timeout time.Duration Config map[string]string // NOTE: maybe we can have some nice methods for getting/setting vars @@ -92,6 +100,8 @@ type Plugin struct { log func(LogLevel, string) logLevel LogLevel guestRuntime guestRuntime + Adapter *observe.AdapterBase + TraceCtx *observe.TraceCtx } func logStd(level LogLevel, message string) { @@ -276,6 +286,12 @@ func (m *Manifest) UnmarshalJSON(data []byte) error { // Close closes the plugin by freeing the underlying resources. func (p *Plugin) Close() error { + // TODO: Since we don't start the adapter in the ctor, should we stop it here? + if p.Adapter != nil { + a := *p.Adapter + a.Stop(true) + } + return p.Runtime.Wazero.Close(p.Runtime.ctx) } @@ -344,7 +360,7 @@ func NewPlugin( return nil, fmt.Errorf("Manifest can't be empty.") } - modules := map[string]api.Module{} + modules := map[string]module{} // NOTE: this is only necessary for guest modules because // host modules have the same access privileges as the host itself @@ -371,6 +387,7 @@ func NewPlugin( // - If there is only one module in the manifest then that is the main module by default // - Otherwise the last module listed is the main module + var trace *observe.TraceCtx for i, wasm := range manifest.Wasm { data, err := wasm.ToWasmData(ctx) if err != nil { @@ -382,6 +399,18 @@ func NewPlugin( data.Name = "main" } + if data.Name == "main" && config.ObserveAdapter != nil { + // TODO: Since we can extract the names out of a api.Module, + // maybe we can have a new overload that accepts that instead of []byte? + trace, err = config.ObserveAdapter.NewTraceCtx(ctx, c.Wazero, data.Data, config.ObserveOptions) + if err != nil { + return nil, fmt.Errorf("Failed to initialize Observe Adapter: %v", err) + } + + // TODO: Do we actually need to finish the trace? + trace.Finish() + } + _, okh := hostModules[data.Name] _, okm := modules[data.Name] @@ -401,7 +430,7 @@ func NewPlugin( return nil, err } - modules[data.Name] = m + modules[data.Name] = module{module: m, wasm: data.Data} } logLevel := LogLevelWarn @@ -411,7 +440,7 @@ func NewPlugin( i := 0 for _, m := range modules { - if m.Name() == "main" { + if m.module.Name() == "main" { p := &Plugin{ Runtime: &c, Modules: modules, @@ -423,7 +452,9 @@ func NewPlugin( LastStatusCode: 0, Timeout: time.Duration(manifest.Timeout) * time.Millisecond, log: logStd, - logLevel: logLevel} + logLevel: logLevel, + Adapter: config.ObserveAdapter, + TraceCtx: trace} p.guestRuntime = detectGuestRuntime(p) return p, nil @@ -499,7 +530,7 @@ func (plugin *Plugin) GetError() string { // FunctionExists returns true when the named function is present in the plugin's main module func (plugin *Plugin) FunctionExists(name string) bool { - return plugin.Main.ExportedFunction(name) != nil + return plugin.Main.module.ExportedFunction(name) != nil } // Call a function by name with the given input, returning the output @@ -521,7 +552,7 @@ func (plugin *Plugin) Call(name string, data []byte) (uint32, []byte, error) { ctx = context.WithValue(ctx, "inputOffset", intputOffset) - var f = plugin.Main.ExportedFunction(name) + var f = plugin.Main.module.ExportedFunction(name) if f == nil { return 1, []byte{}, errors.New(fmt.Sprintf("Unknown function: %s", name)) @@ -542,6 +573,11 @@ func (plugin *Plugin) Call(name string, data []byte) (uint32, []byte, error) { res, err := f.Call(ctx) + // TODO: Should we finish the trace now, or defer it? + if plugin.TraceCtx != nil { + defer plugin.TraceCtx.Finish() + } + // Try to extact WASI exit code if exitErr, ok := err.(*sys.ExitError); ok { exitCode := exitErr.ExitCode() diff --git a/extism_test.go b/extism_test.go index 6c0e8ba..2133e70 100644 --- a/extism_test.go +++ b/extism_test.go @@ -11,6 +11,8 @@ import ( "testing" "time" + observe "github.com/dylibso/observe-sdk/go" + "github.com/dylibso/observe-sdk/go/adapter/stdout" "github.com/stretchr/testify/assert" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/sys" @@ -701,6 +703,55 @@ func TestJsonManifest(t *testing.T) { } } +func TestObserve(t *testing.T) { + ctx := context.Background() + + var buf bytes.Buffer + log.SetOutput(&buf) + + adapter := stdout.NewStdoutAdapter() + adapter.Start(ctx) + + manifest := manifest("nested.c.instr.wasm") + + config := PluginConfig{ + ModuleConfig: wazero.NewModuleConfig().WithSysWalltime(), + EnableWasi: true, + ObserveAdapter: adapter.AdapterBase, + ObserveOptions: &observe.Options{ + SpanFilter: &observe.SpanFilter{MinDuration: 1 * time.Nanosecond}, + ChannelBufferSize: 1024, + }, + } + + plugin, err := NewPlugin(ctx, manifest, config, []HostFunction{}) + if err != nil { + panic(err) + } + + meta := map[string]string{ + "http.url": "https://example.com/my-endpoint", + "http.status_code": "200", + "http.client_ip": "192.168.1.0", + } + + plugin.TraceCtx.Metadata(meta) + + _, _, _ = plugin.Call("_start", []byte("hello world")) + plugin.Close() + + // HACK: make sure we give enough time for the events to get flushed + time.Sleep(100 * time.Millisecond) + + actual := buf.String() + assert.Contains(t, actual, " Call to _start took") + assert.Contains(t, actual, " Call to main took") + assert.Contains(t, actual, " Call to one took") + assert.Contains(t, actual, " Call to two took") + assert.Contains(t, actual, " Call to three took") + assert.Contains(t, actual, " Call to printf took") +} + func BenchmarkInitialize(b *testing.B) { ctx := context.Background() cache := wazero.NewCompilationCache() diff --git a/go.mod b/go.mod index c2d3e7f..9778004 100644 --- a/go.mod +++ b/go.mod @@ -2,12 +2,20 @@ module github.com/extism/go-sdk go 1.20 -require github.com/tetratelabs/wazero v1.3.0 +require ( + github.com/dylibso/observe-sdk/go v0.0.0-20231201014635-141351c24659 + github.com/gobwas/glob v0.2.3 + github.com/stretchr/testify v1.8.4 + github.com/tetratelabs/wazero v1.3.0 +) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gobwas/glob v0.2.3 // indirect + github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.8.4 // indirect + github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + ) diff --git a/go.sum b/go.sum index 0a3c95c..7de1778 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,30 @@ 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/dylibso/observe-sdk/go v0.0.0-20231201014635-141351c24659 h1:RGgHymaENttkVRf0YEzly0Cr2q8xB56WuDEsn8oFXHE= +github.com/dylibso/observe-sdk/go v0.0.0-20231201014635-141351c24659/go.mod h1:7h4vx/+0cUjKN2f+ynM4tcC8kIjJqP6W2cLcn7buXl4= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab h1:BA4a7pe6ZTd9F8kXETBoijjFJ/ntaa//1wiH9BZu4zU= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= 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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= +github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= github.com/tetratelabs/wazero v1.3.0 h1:nqw7zCldxE06B8zSZAY0ACrR9OH5QCcPwYmYlwtcwtE= github.com/tetratelabs/wazero v1.3.0/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/runtime.go b/runtime.go index c4b14c4..ddff253 100644 --- a/runtime.go +++ b/runtime.go @@ -21,7 +21,7 @@ type guestRuntime struct { } func detectGuestRuntime(p *Plugin) guestRuntime { - m := p.Main + m := p.Main.module runtime, ok := haskellRuntime(p, m) if ok { diff --git a/wasm/nested.c.instr.wasm b/wasm/nested.c.instr.wasm new file mode 100644 index 0000000000000000000000000000000000000000..7f74c7f74fc6729799cd6ea5e0f9d04ae2393dd5 GIT binary patch literal 63468 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 Date: Tue, 12 Dec 2023 16:20:14 +0300 Subject: [PATCH 2/5] clean things up --- extism.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/extism.go b/extism.go index b5e8f93..1dc9093 100644 --- a/extism.go +++ b/extism.go @@ -286,12 +286,6 @@ func (m *Manifest) UnmarshalJSON(data []byte) error { // Close closes the plugin by freeing the underlying resources. func (p *Plugin) Close() error { - // TODO: Since we don't start the adapter in the ctor, should we stop it here? - if p.Adapter != nil { - a := *p.Adapter - a.Stop(true) - } - return p.Runtime.Wazero.Close(p.Runtime.ctx) } @@ -407,7 +401,6 @@ func NewPlugin( return nil, fmt.Errorf("Failed to initialize Observe Adapter: %v", err) } - // TODO: Do we actually need to finish the trace? trace.Finish() } @@ -573,7 +566,6 @@ func (plugin *Plugin) Call(name string, data []byte) (uint32, []byte, error) { res, err := f.Call(ctx) - // TODO: Should we finish the trace now, or defer it? if plugin.TraceCtx != nil { defer plugin.TraceCtx.Finish() } From 6de27167c6ba6c760ca000cbbf224f44312d4f3e Mon Sep 17 00:00:00 2001 From: Muhammad Azeez Date: Wed, 13 Dec 2023 17:17:26 +0300 Subject: [PATCH 3/5] test multiple plugins using the same adapter --- extism_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/extism_test.go b/extism_test.go index 2133e70..b67ff3a 100644 --- a/extism_test.go +++ b/extism_test.go @@ -724,6 +724,7 @@ func TestObserve(t *testing.T) { }, } + // Plugin 1 plugin, err := NewPlugin(ctx, manifest, config, []HostFunction{}) if err != nil { panic(err) @@ -750,6 +751,29 @@ func TestObserve(t *testing.T) { assert.Contains(t, actual, " Call to two took") assert.Contains(t, actual, " Call to three took") assert.Contains(t, actual, " Call to printf took") + + // Reset underlying buffer + buf.Reset() + + // Plugin 2 + plugin2, err := NewPlugin(ctx, manifest, config, []HostFunction{}) + if err != nil { + panic(err) + } + + _, _, _ = plugin2.Call("_start", []byte("hello world")) + plugin2.Close() + + // HACK: make sure we give enough time for the events to get flushed + time.Sleep(100 * time.Millisecond) + + actual2 := buf.String() + assert.Contains(t, actual2, " Call to _start took") + assert.Contains(t, actual2, " Call to main took") + assert.Contains(t, actual2, " Call to one took") + assert.Contains(t, actual2, " Call to two took") + assert.Contains(t, actual2, " Call to three took") + assert.Contains(t, actual2, " Call to printf took") } func BenchmarkInitialize(b *testing.B) { From fc6383ec3f006b8b49ed8c2ce7fac33ba0e97c37 Mon Sep 17 00:00:00 2001 From: Muhammad Azeez Date: Tue, 9 Jan 2024 13:25:40 +0300 Subject: [PATCH 4/5] remove incorrect note --- extism.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/extism.go b/extism.go index c3326ea..d9f067f 100644 --- a/extism.go +++ b/extism.go @@ -399,8 +399,6 @@ func NewPlugin( } if data.Name == "main" && config.ObserveAdapter != nil { - // TODO: Since we can extract the names out of a api.Module, - // maybe we can have a new overload that accepts that instead of []byte? trace, err = config.ObserveAdapter.NewTraceCtx(ctx, c.Wazero, data.Data, config.ObserveOptions) if err != nil { return nil, fmt.Errorf("Failed to initialize Observe Adapter: %v", err) From 5fd0b6b764c1b81ef04c6abe0dbe8fa81db6595d Mon Sep 17 00:00:00 2001 From: Muhammad Azeez Date: Tue, 9 Jan 2024 14:21:44 +0300 Subject: [PATCH 5/5] add section in readme --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index 379caa4..0b76ee5 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,42 @@ config := PluginConfig{ _, err := NewPlugin(ctx, manifest, config, []HostFunction{}) ``` +### Integrate with Dylibso Observe SDK +Dylibso provides [observability SDKs](https://github.com/dylibso/observe-sdk) for WebAssembly (Wasm), enabling continuous monitoring of WebAssembly code as it executes within a runtime. It provides developers with the tools necessary to capture and emit telemetry data from Wasm code, including function execution and memory allocation traces, logs, and metrics. + +While Observe SDK has adapters for many popular observability platforms, it also ships with an stdout adapter: + +``` +ctx := context.Background() + +adapter := stdout.NewStdoutAdapter() +adapter.Start(ctx) + +manifest := manifest("nested.c.instr.wasm") + +config := PluginConfig{ + ModuleConfig: wazero.NewModuleConfig().WithSysWalltime(), + EnableWasi: true, + ObserveAdapter: adapter.AdapterBase, +} + +plugin, err := NewPlugin(ctx, manifest, config, []HostFunction{}) +if err != nil { + panic(err) +} + +meta := map[string]string{ + "http.url": "https://example.com/my-endpoint", + "http.status_code": "200", + "http.client_ip": "192.168.1.0", +} + +plugin.TraceCtx.Metadata(meta) + +_, _, _ = plugin.Call("_start", []byte("hello world")) +plugin.Close() +``` + ## Build example plugins Since our [example plugins](./plugins/) are also written in Go, for compiling them we use [TinyGo](https://tinygo.org/):