Skip to content

Commit

Permalink
feat: add experimental opentelemetry exporter (#42)
Browse files Browse the repository at this point in the history
Signed-off-by: Joonas Bergius <[email protected]>
  • Loading branch information
joonas authored Nov 11, 2024
1 parent 5dd92cb commit 4976065
Show file tree
Hide file tree
Showing 15 changed files with 1,098 additions and 0 deletions.
16 changes: 16 additions & 0 deletions x/wasitel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# OpenTelemetry Exporter for WebAssembly Components

> [!IMPORTANT]
> This requires TinyGo `0.35.0` (current `dev` branch) in order to compile.
`wasitel` provides a `wasi:http`-based OpenTelemetry Go exporter implementation.

## Examples:

To be added.

### Acknowledgements

The `wasiteltrace/internal/convert` code has been adapted from [`opentelemetry-go`](https://github.com/open-telemetry/opentelemetry-go)'s internal packages, please see the code itself for the upstream soure references.

The `wasiteltrace/internal/types` code has been adapted from [`opentelemetry-proto-go`](https://github.com/open-telemetry/opentelemetry-proto-go)'s generated protobufs, please see the code itself for the upstream source references.
21 changes: 21 additions & 0 deletions x/wasitel/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module go.wasmcloud.dev/component/x/wasitel

go 1.23.2

require (
go.opentelemetry.io/otel v1.31.0
go.opentelemetry.io/otel/sdk v1.31.0
go.opentelemetry.io/otel/trace v1.31.0
go.wasmcloud.dev/component v0.0.4
)

require (
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
go.bytecodealliance.org v0.4.0 // indirect
go.opentelemetry.io/otel/metric v1.31.0 // indirect
golang.org/x/sys v0.26.0 // indirect
)

replace go.wasmcloud.dev/component => ../../
29 changes: 29 additions & 0 deletions x/wasitel/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.bytecodealliance.org v0.4.0 h1:SRwgZIcXR54AmbJg9Y3AMgDlZlvD8dffteBYW+nCD3k=
go.bytecodealliance.org v0.4.0/go.mod h1:hkdjfgQ/bFZYUucnm9cn0Q8/SHO3iT0rzskYlkV4Jy0=
go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
72 changes: 72 additions & 0 deletions x/wasitel/wasiteltrace/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package wasiteltrace

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"

"go.wasmcloud.dev/component/net/wasihttp"
"go.wasmcloud.dev/component/x/wasitel/wasiteltrace/internal/types"
)

type client struct {
config config
httpClient *http.Client
}

func newClient(opts ...Option) *client {
cfg := newConfig(opts...)

wasiTransport := &wasihttp.Transport{}
httpClient := &http.Client{Transport: wasiTransport}

return &client{
config: cfg,
httpClient: httpClient,
}
}

func (c *client) UploadTraces(ctx context.Context, spans []*types.ResourceSpans) error {
if len(spans) == 0 {
return nil
}

export := &types.ExportTraceServiceRequest{
ResourceSpans: spans,
}

body, err := json.Marshal(export)
if err != nil {
return fmt.Errorf("failed to serialize export request to JSON: %w", err)
}

u := c.getUrl()
req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewBuffer(body))
if err != nil {
return fmt.Errorf("failed to create request to %q: %w", u.String(), err)
}
req.Header.Set("Content-Type", "application/json")

_, err = c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to request %q: %w", u.String(), err)
}

return nil
}

func (c *client) getUrl() url.URL {
scheme := "http"
if !c.config.Insecure {
scheme = "https"
}
u := url.URL{
Scheme: scheme,
Host: c.config.Endpoint,
Path: c.config.Path,
}
return u
}
74 changes: 74 additions & 0 deletions x/wasitel/wasiteltrace/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package wasiteltrace

import (
"fmt"
"net/url"
)

const (
// DefaultPort is the default HTTP port of the collector.
DefaultPort uint16 = 4318
// DefaultHost is the host address the client will attempt
// connect to if no collector address is provided.
DefaultHost string = "localhost"
// DefaultPath is a default URL path for endpoint that receives spans.
DefaultPath string = "/v1/traces"
)

type config struct {
Endpoint string
Insecure bool
Path string
}

func newConfig(opts ...Option) config {
cfg := config{
Insecure: true,
Endpoint: fmt.Sprintf("%s:%d", DefaultHost, DefaultPort),
Path: DefaultPath,
}
for _, opt := range opts {
cfg = opt.apply(cfg)
}
return cfg
}

type Option interface {
apply(config) config
}

func newWrappedOption(fn func(config) config) Option {
return &wrappedOption{fn: fn}
}

type wrappedOption struct {
fn func(config) config
}

func (o *wrappedOption) apply(cfg config) config {
return o.fn(cfg)
}

func WithEndpoint(endpoint string) Option {
return newWrappedOption(func(cfg config) config {
cfg.Endpoint = endpoint
return cfg
})
}

func WithEndpointURL(eu string) Option {
return newWrappedOption(func(cfg config) config {
u, err := url.Parse(eu)
if err != nil {
return cfg
}

cfg.Endpoint = u.Host
cfg.Path = u.Path
if u.Scheme != "https" {
cfg.Insecure = true
}

return cfg
})
}
71 changes: 71 additions & 0 deletions x/wasitel/wasiteltrace/exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package wasiteltrace

import (
"context"
"fmt"
"sync"

"go.opentelemetry.io/otel/sdk/trace"
tracesdk "go.opentelemetry.io/otel/sdk/trace"
"go.wasmcloud.dev/component/x/wasitel/wasiteltrace/internal/convert"
)

func New(opts ...Option) (*Exporter, error) {
client := newClient(opts...)
return &Exporter{
client: client,
}, nil
}

var _ tracesdk.SpanExporter = (*Exporter)(nil)

type Exporter struct {
client *client
stopped bool
stoppedMu sync.RWMutex
}

func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error {
err := ctx.Err()
if err != nil {
return err
}

// Check whether the exporter has been told to Shutdown
e.stoppedMu.RLock()
stopped := e.stopped
e.stoppedMu.RUnlock()
if stopped {
return nil
}

// Check if there's nothing to export
converted := convert.ResourceSpans(spans)
if len(converted) == 0 {
return nil
}

err = e.client.UploadTraces(ctx, converted)
if err != nil {
return fmt.Errorf("failed to export spans: %w", err)
}
return nil
}

// Shutdown is called to stop the exporter, it performs no action.
func (e *Exporter) Shutdown(ctx context.Context) error {
e.stoppedMu.Lock()
e.stopped = true
e.stoppedMu.Unlock()

return nil
}

// MarshalLog is the marshaling function used by the logging system to represent this Exporter.
func (e *Exporter) MarshalLog() interface{} {
return struct {
Type string
}{
Type: "wasiteltrace",
}
}
Loading

0 comments on commit 4976065

Please sign in to comment.