Skip to content

Commit

Permalink
feat: add support for exporting metrics via wasitelmetrics.Exporter
Browse files Browse the repository at this point in the history
Signed-off-by: Joonas Bergius <[email protected]>
  • Loading branch information
joonas committed Nov 21, 2024
1 parent e801185 commit 1bcb4c3
Show file tree
Hide file tree
Showing 11 changed files with 928 additions and 15 deletions.
11 changes: 6 additions & 5 deletions x/wasitel/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ 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.opentelemetry.io/otel v1.32.0
go.opentelemetry.io/otel/sdk v1.32.0
go.opentelemetry.io/otel/sdk/metric v1.32.0
go.opentelemetry.io/otel/trace v1.32.0
go.wasmcloud.dev/component v0.0.5
)

Expand All @@ -14,8 +15,8 @@ require (
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
go.opentelemetry.io/otel/metric v1.32.0 // indirect
golang.org/x/sys v0.27.0 // indirect
)

replace go.wasmcloud.dev/component => ../../
22 changes: 12 additions & 10 deletions x/wasitel/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
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=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.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=
79 changes: 79 additions & 0 deletions x/wasitel/wasitelmetric/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package wasitelmetric

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

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

type clienti interface {
UploadMetrics(context.Context, *types.ResourceMetrics) error
Shutdown(context.Context) error
}

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

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

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

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

func (c *client) UploadMetrics(ctx context.Context, rm *types.ResourceMetrics) error {
export := &types.ExportMetricsServiceRequest{
ResourceMetrics: []*types.ResourceMetrics{rm},
}

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
}

// Shutdown shuts down the client, freeing all resources.
func (c *client) Shutdown(ctx context.Context) error {
c.httpClient = nil
return ctx.Err()
}

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/wasitelmetric/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package wasitelmetric

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 metrics.
DefaultPath string = "/v1/metrics"
)

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
})
}
110 changes: 110 additions & 0 deletions x/wasitel/wasitelmetric/exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package wasitelmetric

import (
"context"
"fmt"
"sync"

metric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
"go.wasmcloud.dev/component/x/wasitel/wasitelmetric/internal/convert"
)

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

type Exporter struct {
client *client

temporalitySelector metric.TemporalitySelector
aggregationSelector metric.AggregationSelector

stopped bool
stoppedMu sync.RWMutex
}

var _ metric.Exporter = (*Exporter)(nil)

// Temporality returns the Temporality to use for an instrument kind.
func (e *Exporter) Temporality(k metric.InstrumentKind) metricdata.Temporality {
return e.temporalitySelector(k)
}

// Aggregation returns the Aggregation to use for an instrument kind.
func (e *Exporter) Aggregation(k metric.InstrumentKind) metric.Aggregation {
return e.aggregationSelector(k)
}

func (e *Exporter) Export(ctx context.Context, data *metricdata.ResourceMetrics) 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
}

metrics, err := convert.ResourceMetrics(data)
if err != nil {
return err
}

err = e.client.UploadMetrics(ctx, metrics)
if err != nil {
return err
}

return nil
}

// ForceFlush flushes any metric data held by an exporter.
//
// This method returns an error if called after Shutdown.
// This method returns an error if the method is canceled by the passed context.
//
// This method is safe to call concurrently.
func (e *Exporter) ForceFlush(ctx context.Context) error {
// Check whether the exporter has been told to Shutdown
e.stoppedMu.RLock()
stopped := e.stopped
e.stoppedMu.RUnlock()
if stopped {
return errShutdown
}
return ctx.Err()
}

var errShutdown = fmt.Errorf("Exporter is shutdown")

// Shutdown flushes all metric data held by an exporter and releases any held
// computational resources.
//
// This method returns an error if called after Shutdown.
// This method returns an error if the method is canceled by the passed context.
//
// This method is safe to call concurrently.
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: "wasitelmetric",
}
}
10 changes: 10 additions & 0 deletions x/wasitel/wasitelmetric/internal/convert/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package convert

import (
"go.opentelemetry.io/otel/sdk/metric/metricdata"
"go.wasmcloud.dev/component/x/wasitel/wasitelmetric/internal/types"
)

func ResourceMetrics(data *metricdata.ResourceMetrics) (*types.ResourceMetrics, error) {
return nil, nil
}
66 changes: 66 additions & 0 deletions x/wasitel/wasitelmetric/internal/types/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Original source: https://github.com/open-telemetry/opentelemetry-proto-go/blob/v1.3.1/slim/otlp/common/v1/common.pb.go
package types

// AnyValue is used to represent any type of attribute value. AnyValue may contain a
// primitive value such as a string or integer or it may contain an arbitrary nested
// object containing arrays, key-value lists and primitives.
type AnyValue struct {
// The value is one of the listed fields. It is valid for all values to be unspecified
// in which case this AnyValue is considered to be "empty".
//
// Types that are assignable to Value:
// *AnyValue_StringValue
// *AnyValue_BoolValue
// *AnyValue_IntValue
// *AnyValue_DoubleValue
// *AnyValue_ArrayValue
// *AnyValue_KvlistValue
// *AnyValue_BytesValue
StringValue string `json:"stringValue,omitempty"`
BoolValue bool `json:"boolValue,omitempty"`
IntValue int64 `json:"intValue,omitempty"`
DoubleValue float64 `json:"doubleValue,omitempty"`
ArrayValue *ArrayValue `json:"arrayValue,omitempty"`
KvlistValue *KeyValueList `json:"kvlistValue,omitempty"`
BytesValue []byte `json:"bytesValue,omitempty"`
}

// ArrayValue is a list of AnyValue messages. We need ArrayValue as a message
// since oneof in AnyValue does not allow repeated fields.
type ArrayValue struct {
// Array of values. The array may be empty (contain 0 elements).
Values []*AnyValue `json:"values,omitempty"`
}

// KeyValueList is a list of KeyValue messages. We need KeyValueList as a message
// since `oneof` in AnyValue does not allow repeated fields. Everywhere else where we need
// a list of KeyValue messages (e.g. in Span) we use `repeated KeyValue` directly to
// avoid unnecessary extra wrapping (which slows down the protocol). The 2 approaches
// are semantically equivalent.
type KeyValueList struct {
// A collection of key/value pairs of key-value pairs. The list may be empty (may
// contain 0 elements).
// The keys MUST be unique (it is not allowed to have more than one
// value with the same key).
Values []*KeyValue `json:"values,omitempty"`
}

// KeyValue is a key-value pair that is used to store Span attributes, Link
// attributes, etc.
type KeyValue struct {
Key string `json:"key,omitempty"`
Value *AnyValue `json:"value,omitempty"`
}

// InstrumentationScope is a message representing the instrumentation scope information
// such as the fully qualified name and version.
type InstrumentationScope struct {
// An empty instrumentation scope name means the name is unknown.
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
// Additional attributes that describe the scope. [Optional].
// Attribute keys MUST be unique (it is not allowed to have more than one
// attribute with the same key).
Attributes []*KeyValue `json:"attributes,omitempty"`
DroppedAttributesCount uint32 `json:"dropped_attributes_count,omitempty"`
}
Loading

0 comments on commit 1bcb4c3

Please sign in to comment.