Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for exporting metrics via wasitelmetrics.Exporter #50

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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