diff --git a/x/wasitel/go.mod b/x/wasitel/go.mod index 8fc5d97..0d0a1eb 100644 --- a/x/wasitel/go.mod +++ b/x/wasitel/go.mod @@ -3,9 +3,11 @@ 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/log v0.8.0 + go.opentelemetry.io/otel/sdk v1.32.0 + go.opentelemetry.io/otel/sdk/log v0.8.0 + go.opentelemetry.io/otel/trace v1.32.0 go.wasmcloud.dev/component v0.0.5 ) @@ -14,8 +16,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 => ../../ diff --git a/x/wasitel/go.sum b/x/wasitel/go.sum index 542344e..6ebb848 100644 --- a/x/wasitel/go.sum +++ b/x/wasitel/go.sum @@ -15,15 +15,19 @@ 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/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= +go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= +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/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= +go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= +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= diff --git a/x/wasitel/wasitellog/client.go b/x/wasitel/wasitellog/client.go new file mode 100644 index 0000000..ce7c595 --- /dev/null +++ b/x/wasitel/wasitellog/client.go @@ -0,0 +1,72 @@ +package wasitellog + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "go.wasmcloud.dev/component/net/wasihttp" + "go.wasmcloud.dev/component/x/wasitel/wasitellog/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) UploadLogs(ctx context.Context, logs []*types.ResourceLogs) error { + if len(logs) == 0 { + return nil + } + + export := &types.ExportLogsServiceRequest{ + ResourceLogs: logs, + } + + 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 +} diff --git a/x/wasitel/wasitellog/config.go b/x/wasitel/wasitellog/config.go new file mode 100644 index 0000000..5db0699 --- /dev/null +++ b/x/wasitel/wasitellog/config.go @@ -0,0 +1,74 @@ +package wasitellog + +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 logs + DefaultPath string = "/v1/logs" +) + +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 + }) +} diff --git a/x/wasitel/wasitellog/exporter.go b/x/wasitel/wasitellog/exporter.go new file mode 100644 index 0000000..44a0af2 --- /dev/null +++ b/x/wasitel/wasitellog/exporter.go @@ -0,0 +1,64 @@ +package wasitellog + +import ( + "context" + "fmt" + "sync" + + logsdk "go.opentelemetry.io/otel/sdk/log" + "go.wasmcloud.dev/component/x/wasitel/wasitellog/internal/convert" +) + +func New(opts ...Option) (*Exporter, error) { + client := newClient(opts...) + return &Exporter{ + client: client, + }, nil +} + +var _ logsdk.Exporter = (*Exporter)(nil) + +type Exporter struct { + client *client + stopped bool + stoppedMu sync.RWMutex +} + +func (e *Exporter) Export(ctx context.Context, logs []logsdk.Record) 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 whether there's anything to export + converted := convert.ResourceLogs(logs) + if len(converted) == 0 { + return nil + } + + err = e.client.UploadLogs(ctx, converted) + if err != nil { + return fmt.Errorf("failed to export spans: %w", err) + } + return nil +} + +func (e *Exporter) Shutdown(ctx context.Context) error { + e.stoppedMu.Lock() + e.stopped = true + e.stoppedMu.Unlock() + + return nil +} + +func (e *Exporter) ForceFlush(ctx context.Context) error { + return nil +} diff --git a/x/wasitel/wasitellog/internal/convert/log.go b/x/wasitel/wasitellog/internal/convert/log.go new file mode 100644 index 0000000..d85eacd --- /dev/null +++ b/x/wasitel/wasitellog/internal/convert/log.go @@ -0,0 +1,342 @@ +// Original source: https://github.com/open-telemetry/opentelemetry-go/blob/v1.31.0/exporters/otlp/otlplog/otlploghttp/internal/transform/log.go + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +package convert + +import ( + "time" + + "go.wasmcloud.dev/component/x/wasitel/wasitellog/internal/types" + + "go.opentelemetry.io/otel/attribute" + api "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/sdk/instrumentation" + logsdk "go.opentelemetry.io/otel/sdk/log" +) + +// ResourceLogs returns an slice of OTLP ResourceLogs generated from records. +func ResourceLogs(records []logsdk.Record) []*types.ResourceLogs { + if len(records) == 0 { + return nil + } + + resMap := make(map[attribute.Distinct]*types.ResourceLogs) + + type key struct { + r attribute.Distinct + is instrumentation.Scope + } + scopeMap := make(map[key]*types.ScopeLogs) + + var resources int + for _, r := range records { + res := r.Resource() + rKey := res.Equivalent() + scope := r.InstrumentationScope() + k := key{ + r: rKey, + is: scope, + } + sl, iOk := scopeMap[k] + if !iOk { + sl = new(types.ScopeLogs) + var emptyScope instrumentation.Scope + if scope != emptyScope { + sl.Scope = &types.InstrumentationScope{ + Name: scope.Name, + Version: scope.Version, + Attributes: AttrIter(scope.Attributes.Iter()), + } + sl.SchemaUrl = scope.SchemaURL + } + scopeMap[k] = sl + } + + sl.LogRecords = append(sl.LogRecords, LogRecord(r)) + rl, rOk := resMap[rKey] + if !rOk { + resources++ + rl = new(types.ResourceLogs) + if res.Len() > 0 { + rl.Resource = &types.Resource{ + Attributes: AttrIter(res.Iter()), + } + } + rl.SchemaUrl = res.SchemaURL() + resMap[rKey] = rl + } + if !iOk { + rl.ScopeLogs = append(rl.ScopeLogs, sl) + } + } + + // Transform the categorized map into a slice + resLogs := make([]*types.ResourceLogs, 0, resources) + for _, rl := range resMap { + resLogs = append(resLogs, rl) + } + + return resLogs +} + +// LogRecord returns an OTLP LogRecord generated from record. +func LogRecord(record logsdk.Record) *types.LogRecord { + r := &types.LogRecord{ + TimeUnixNano: timeUnixNano(record.Timestamp()), + ObservedTimeUnixNano: timeUnixNano(record.ObservedTimestamp()), + SeverityNumber: SeverityNumber(record.Severity()), + SeverityText: record.SeverityText(), + Body: LogAttrValue(record.Body()), + Attributes: make([]*types.KeyValue, 0, record.AttributesLen()), + Flags: uint32(record.TraceFlags()), + // TODO: DroppedAttributesCount: /* ... */, + } + record.WalkAttributes(func(kv api.KeyValue) bool { + r.Attributes = append(r.Attributes, LogAttr(kv)) + return true + }) + if tID := record.TraceID(); tID.IsValid() { + r.TraceId = types.NewTraceID(tID) + } + if sID := record.SpanID(); sID.IsValid() { + r.SpanId = types.NewSpanID(sID) + } + return r +} + +// timeUnixNano returns t as a Unix time, the number of nanoseconds elapsed +// since January 1, 1970 UTC as uint64. The result is undefined if the Unix +// time in nanoseconds cannot be represented by an int64 (a date before the +// year 1678 or after 2262). timeUnixNano on the zero Time returns 0. The +// result does not depend on the location associated with t. +func timeUnixNano(t time.Time) uint64 { + nano := t.UnixNano() + if nano < 0 { + return 0 + } + return uint64(nano) // nolint:gosec // Overflow checked. +} + +// AttrIter transforms an [attribute.Iterator] into OTLP key-values. +func AttrIter(iter attribute.Iterator) []*types.KeyValue { + l := iter.Len() + if l == 0 { + return nil + } + + out := make([]*types.KeyValue, 0, l) + for iter.Next() { + out = append(out, Attr(iter.Attribute())) + } + return out +} + +// Attrs transforms a slice of [attribute.KeyValue] into OTLP key-values. +func Attrs(attrs []attribute.KeyValue) []*types.KeyValue { + if len(attrs) == 0 { + return nil + } + + out := make([]*types.KeyValue, 0, len(attrs)) + for _, kv := range attrs { + out = append(out, Attr(kv)) + } + return out +} + +// Attr transforms an [attribute.KeyValue] into an OTLP key-value. +func Attr(kv attribute.KeyValue) *types.KeyValue { + return &types.KeyValue{Key: string(kv.Key), Value: AttrValue(kv.Value)} +} + +// AttrValue transforms an [attribute.Value] into an OTLP AnyValue. +func AttrValue(v attribute.Value) *types.AnyValue { + av := new(types.AnyValue) + switch v.Type() { + case attribute.BOOL: + av.BoolValue = v.AsBool() + case attribute.BOOLSLICE: + av.ArrayValue = &types.ArrayValue{ + Values: boolSliceValues(v.AsBoolSlice()), + } + case attribute.INT64: + av.IntValue = v.AsInt64() + case attribute.INT64SLICE: + av.ArrayValue = &types.ArrayValue{ + Values: int64SliceValues(v.AsInt64Slice()), + } + case attribute.FLOAT64: + av.DoubleValue = v.AsFloat64() + case attribute.FLOAT64SLICE: + av.ArrayValue = &types.ArrayValue{ + Values: float64SliceValues(v.AsFloat64Slice()), + } + case attribute.STRING: + av.StringValue = v.AsString() + case attribute.STRINGSLICE: + av.ArrayValue = &types.ArrayValue{ + Values: stringSliceValues(v.AsStringSlice()), + } + default: + av.StringValue = "INVALID" + } + return av +} + +func boolSliceValues(vals []bool) []*types.AnyValue { + converted := make([]*types.AnyValue, len(vals)) + for i, v := range vals { + converted[i] = &types.AnyValue{ + BoolValue: v, + } + } + return converted +} + +func int64SliceValues(vals []int64) []*types.AnyValue { + converted := make([]*types.AnyValue, len(vals)) + for i, v := range vals { + converted[i] = &types.AnyValue{ + IntValue: v, + } + } + return converted +} + +func float64SliceValues(vals []float64) []*types.AnyValue { + converted := make([]*types.AnyValue, len(vals)) + for i, v := range vals { + converted[i] = &types.AnyValue{ + DoubleValue: v, + } + } + return converted +} + +func stringSliceValues(vals []string) []*types.AnyValue { + converted := make([]*types.AnyValue, len(vals)) + for i, v := range vals { + converted[i] = &types.AnyValue{ + StringValue: v, + } + } + return converted +} + +// Attrs transforms a slice of [api.KeyValue] into OTLP key-values. +func LogAttrs(attrs []api.KeyValue) []*types.KeyValue { + if len(attrs) == 0 { + return nil + } + + out := make([]*types.KeyValue, 0, len(attrs)) + for _, kv := range attrs { + out = append(out, LogAttr(kv)) + } + return out +} + +// LogAttr transforms an [api.KeyValue] into an OTLP key-value. +func LogAttr(attr api.KeyValue) *types.KeyValue { + return &types.KeyValue{ + Key: attr.Key, + Value: LogAttrValue(attr.Value), + } +} + +// LogAttrValues transforms a slice of [api.Value] into an OTLP []AnyValue. +func LogAttrValues(vals []api.Value) []*types.AnyValue { + if len(vals) == 0 { + return nil + } + + out := make([]*types.AnyValue, 0, len(vals)) + for _, v := range vals { + out = append(out, LogAttrValue(v)) + } + return out +} + +// LogAttrValue transforms an [api.Value] into an OTLP AnyValue. +func LogAttrValue(v api.Value) *types.AnyValue { + av := new(types.AnyValue) + switch v.Kind() { + case api.KindBool: + av.BoolValue = v.AsBool() + case api.KindInt64: + av.IntValue = v.AsInt64() + case api.KindFloat64: + av.DoubleValue = v.AsFloat64() + case api.KindString: + av.StringValue = v.AsString() + case api.KindBytes: + av.BytesValue = v.AsBytes() + case api.KindSlice: + av.ArrayValue = &types.ArrayValue{ + Values: LogAttrValues(v.AsSlice()), + } + case api.KindMap: + av.KvlistValue = &types.KeyValueList{ + Values: LogAttrs(v.AsMap()), + } + default: + av.StringValue = "INVALID" + } + return av +} + +// SeverityNumber transforms a [log.Severity] into an OTLP SeverityNumber. +func SeverityNumber(s api.Severity) types.SeverityNumber { + switch s { + case api.SeverityTrace: + return types.SeverityNumber_SEVERITY_NUMBER_TRACE + case api.SeverityTrace2: + return types.SeverityNumber_SEVERITY_NUMBER_TRACE2 + case api.SeverityTrace3: + return types.SeverityNumber_SEVERITY_NUMBER_TRACE3 + case api.SeverityTrace4: + return types.SeverityNumber_SEVERITY_NUMBER_TRACE4 + case api.SeverityDebug: + return types.SeverityNumber_SEVERITY_NUMBER_DEBUG + case api.SeverityDebug2: + return types.SeverityNumber_SEVERITY_NUMBER_DEBUG2 + case api.SeverityDebug3: + return types.SeverityNumber_SEVERITY_NUMBER_DEBUG3 + case api.SeverityDebug4: + return types.SeverityNumber_SEVERITY_NUMBER_DEBUG4 + case api.SeverityInfo: + return types.SeverityNumber_SEVERITY_NUMBER_INFO + case api.SeverityInfo2: + return types.SeverityNumber_SEVERITY_NUMBER_INFO2 + case api.SeverityInfo3: + return types.SeverityNumber_SEVERITY_NUMBER_INFO3 + case api.SeverityInfo4: + return types.SeverityNumber_SEVERITY_NUMBER_INFO4 + case api.SeverityWarn: + return types.SeverityNumber_SEVERITY_NUMBER_WARN + case api.SeverityWarn2: + return types.SeverityNumber_SEVERITY_NUMBER_WARN2 + case api.SeverityWarn3: + return types.SeverityNumber_SEVERITY_NUMBER_WARN3 + case api.SeverityWarn4: + return types.SeverityNumber_SEVERITY_NUMBER_WARN4 + case api.SeverityError: + return types.SeverityNumber_SEVERITY_NUMBER_ERROR + case api.SeverityError2: + return types.SeverityNumber_SEVERITY_NUMBER_ERROR2 + case api.SeverityError3: + return types.SeverityNumber_SEVERITY_NUMBER_ERROR3 + case api.SeverityError4: + return types.SeverityNumber_SEVERITY_NUMBER_ERROR4 + case api.SeverityFatal: + return types.SeverityNumber_SEVERITY_NUMBER_FATAL + case api.SeverityFatal2: + return types.SeverityNumber_SEVERITY_NUMBER_FATAL2 + case api.SeverityFatal3: + return types.SeverityNumber_SEVERITY_NUMBER_FATAL3 + case api.SeverityFatal4: + return types.SeverityNumber_SEVERITY_NUMBER_FATAL4 + } + return types.SeverityNumber_SEVERITY_NUMBER_UNSPECIFIED +} diff --git a/x/wasitel/wasitellog/internal/types/common.go b/x/wasitel/wasitellog/internal/types/common.go new file mode 100644 index 0000000..068d061 --- /dev/null +++ b/x/wasitel/wasitellog/internal/types/common.go @@ -0,0 +1,64 @@ +// Original source: https://github.com/open-telemetry/opentelemetry-proto-go/blob/v1.3.1/slim/otlp/common/v1/common.pb.go +package types + +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"` +} + +// 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"` +} diff --git a/x/wasitel/wasitellog/internal/types/context.go b/x/wasitel/wasitellog/internal/types/context.go new file mode 100644 index 0000000..0ae3d34 --- /dev/null +++ b/x/wasitel/wasitellog/internal/types/context.go @@ -0,0 +1,29 @@ +package types + +import ( + "encoding/json" + + "go.opentelemetry.io/otel/trace" +) + +type TraceID []byte + +func NewTraceID(tid trace.TraceID) *TraceID { + id := TraceID(tid.String()) + return &id +} + +func (tid *TraceID) MarshalJSON() ([]byte, error) { + return json.Marshal(string(*tid)) +} + +func NewSpanID(sid trace.SpanID) *SpanID { + id := SpanID(sid.String()) + return &id +} + +type SpanID []byte + +func (sid *SpanID) MarshalJSON() ([]byte, error) { + return json.Marshal(string(*sid)) +} diff --git a/x/wasitel/wasitellog/internal/types/logs.go b/x/wasitel/wasitellog/internal/types/logs.go new file mode 100644 index 0000000..fd7cd64 --- /dev/null +++ b/x/wasitel/wasitellog/internal/types/logs.go @@ -0,0 +1,192 @@ +package types + +// Possible values for LogRecord.SeverityNumber. +type SeverityNumber int32 + +const ( + // UNSPECIFIED is the default SeverityNumber, it MUST NOT be used. + SeverityNumber_SEVERITY_NUMBER_UNSPECIFIED SeverityNumber = 0 + SeverityNumber_SEVERITY_NUMBER_TRACE SeverityNumber = 1 + SeverityNumber_SEVERITY_NUMBER_TRACE2 SeverityNumber = 2 + SeverityNumber_SEVERITY_NUMBER_TRACE3 SeverityNumber = 3 + SeverityNumber_SEVERITY_NUMBER_TRACE4 SeverityNumber = 4 + SeverityNumber_SEVERITY_NUMBER_DEBUG SeverityNumber = 5 + SeverityNumber_SEVERITY_NUMBER_DEBUG2 SeverityNumber = 6 + SeverityNumber_SEVERITY_NUMBER_DEBUG3 SeverityNumber = 7 + SeverityNumber_SEVERITY_NUMBER_DEBUG4 SeverityNumber = 8 + SeverityNumber_SEVERITY_NUMBER_INFO SeverityNumber = 9 + SeverityNumber_SEVERITY_NUMBER_INFO2 SeverityNumber = 10 + SeverityNumber_SEVERITY_NUMBER_INFO3 SeverityNumber = 11 + SeverityNumber_SEVERITY_NUMBER_INFO4 SeverityNumber = 12 + SeverityNumber_SEVERITY_NUMBER_WARN SeverityNumber = 13 + SeverityNumber_SEVERITY_NUMBER_WARN2 SeverityNumber = 14 + SeverityNumber_SEVERITY_NUMBER_WARN3 SeverityNumber = 15 + SeverityNumber_SEVERITY_NUMBER_WARN4 SeverityNumber = 16 + SeverityNumber_SEVERITY_NUMBER_ERROR SeverityNumber = 17 + SeverityNumber_SEVERITY_NUMBER_ERROR2 SeverityNumber = 18 + SeverityNumber_SEVERITY_NUMBER_ERROR3 SeverityNumber = 19 + SeverityNumber_SEVERITY_NUMBER_ERROR4 SeverityNumber = 20 + SeverityNumber_SEVERITY_NUMBER_FATAL SeverityNumber = 21 + SeverityNumber_SEVERITY_NUMBER_FATAL2 SeverityNumber = 22 + SeverityNumber_SEVERITY_NUMBER_FATAL3 SeverityNumber = 23 + SeverityNumber_SEVERITY_NUMBER_FATAL4 SeverityNumber = 24 +) + +// Enum value maps for SeverityNumber. +var ( + SeverityNumber_name = map[int32]string{ + 0: "SEVERITY_NUMBER_UNSPECIFIED", + 1: "SEVERITY_NUMBER_TRACE", + 2: "SEVERITY_NUMBER_TRACE2", + 3: "SEVERITY_NUMBER_TRACE3", + 4: "SEVERITY_NUMBER_TRACE4", + 5: "SEVERITY_NUMBER_DEBUG", + 6: "SEVERITY_NUMBER_DEBUG2", + 7: "SEVERITY_NUMBER_DEBUG3", + 8: "SEVERITY_NUMBER_DEBUG4", + 9: "SEVERITY_NUMBER_INFO", + 10: "SEVERITY_NUMBER_INFO2", + 11: "SEVERITY_NUMBER_INFO3", + 12: "SEVERITY_NUMBER_INFO4", + 13: "SEVERITY_NUMBER_WARN", + 14: "SEVERITY_NUMBER_WARN2", + 15: "SEVERITY_NUMBER_WARN3", + 16: "SEVERITY_NUMBER_WARN4", + 17: "SEVERITY_NUMBER_ERROR", + 18: "SEVERITY_NUMBER_ERROR2", + 19: "SEVERITY_NUMBER_ERROR3", + 20: "SEVERITY_NUMBER_ERROR4", + 21: "SEVERITY_NUMBER_FATAL", + 22: "SEVERITY_NUMBER_FATAL2", + 23: "SEVERITY_NUMBER_FATAL3", + 24: "SEVERITY_NUMBER_FATAL4", + } + SeverityNumber_value = map[string]int32{ + "SEVERITY_NUMBER_UNSPECIFIED": 0, + "SEVERITY_NUMBER_TRACE": 1, + "SEVERITY_NUMBER_TRACE2": 2, + "SEVERITY_NUMBER_TRACE3": 3, + "SEVERITY_NUMBER_TRACE4": 4, + "SEVERITY_NUMBER_DEBUG": 5, + "SEVERITY_NUMBER_DEBUG2": 6, + "SEVERITY_NUMBER_DEBUG3": 7, + "SEVERITY_NUMBER_DEBUG4": 8, + "SEVERITY_NUMBER_INFO": 9, + "SEVERITY_NUMBER_INFO2": 10, + "SEVERITY_NUMBER_INFO3": 11, + "SEVERITY_NUMBER_INFO4": 12, + "SEVERITY_NUMBER_WARN": 13, + "SEVERITY_NUMBER_WARN2": 14, + "SEVERITY_NUMBER_WARN3": 15, + "SEVERITY_NUMBER_WARN4": 16, + "SEVERITY_NUMBER_ERROR": 17, + "SEVERITY_NUMBER_ERROR2": 18, + "SEVERITY_NUMBER_ERROR3": 19, + "SEVERITY_NUMBER_ERROR4": 20, + "SEVERITY_NUMBER_FATAL": 21, + "SEVERITY_NUMBER_FATAL2": 22, + "SEVERITY_NUMBER_FATAL3": 23, + "SEVERITY_NUMBER_FATAL4": 24, + } +) + +// A collection of ScopeLogs from a Resource. +type ResourceLogs struct { + // The resource for the logs in this message. + // If this field is not set then resource info is unknown. + Resource *Resource `json:"resource,omitempty"` + // A list of ScopeLogs that originate from a resource. + ScopeLogs []*ScopeLogs `json:"scope_logs,omitempty"` + // The Schema URL, if known. This is the identifier of the Schema that the resource data + // is recorded in. To learn more about Schema URL see + // https://opentelemetry.io/docs/specs/otel/schemas/#schema-url + // This schema_url applies to the data in the "resource" field. It does not apply + // to the data in the "scope_logs" field which have their own schema_url field. + SchemaUrl string `json:"schema_url,omitempty"` +} + +// A collection of Logs produced by a Scope. +type ScopeLogs struct { + // The instrumentation scope information for the logs in this message. + // Semantically when InstrumentationScope isn't set, it is equivalent with + // an empty instrumentation scope name (unknown). + Scope *InstrumentationScope `json:"scope,omitempty"` + // A list of log records. + LogRecords []*LogRecord `json:"log_records,omitempty"` + // The Schema URL, if known. This is the identifier of the Schema that the log data + // is recorded in. To learn more about Schema URL see + // https://opentelemetry.io/docs/specs/otel/schemas/#schema-url + // This schema_url applies to all logs in the "logs" field. + SchemaUrl string `json:"schema_url,omitempty"` +} + +// A log record according to OpenTelemetry Log Data Model: +// https://github.com/open-telemetry/oteps/blob/main/text/logs/0097-log-data-model.md +type LogRecord struct { + // time_unix_nano is the time when the event occurred. + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + // Value of 0 indicates unknown or missing timestamp. + TimeUnixNano uint64 `json:"time_unix_nano,omitempty"` + // Time when the event was observed by the collection system. + // For events that originate in OpenTelemetry (e.g. using OpenTelemetry Logging SDK) + // this timestamp is typically set at the generation time and is equal to Timestamp. + // For events originating externally and collected by OpenTelemetry (e.g. using + // Collector) this is the time when OpenTelemetry's code observed the event measured + // by the clock of the OpenTelemetry code. This field MUST be set once the event is + // observed by OpenTelemetry. + // + // For converting OpenTelemetry log data to formats that support only one timestamp or + // when receiving OpenTelemetry log data by recipients that support only one timestamp + // internally the following logic is recommended: + // - Use time_unix_nano if it is present, otherwise use observed_time_unix_nano. + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + // Value of 0 indicates unknown or missing timestamp. + ObservedTimeUnixNano uint64 `json:"observed_time_unix_nano,omitempty"` + // Numerical value of the severity, normalized to values described in Log Data Model. + // [Optional]. + SeverityNumber SeverityNumber `json:"severity_number,omitempty"` + // The severity text (also known as log level). The original string representation as + // it is known at the source. [Optional]. + SeverityText string `json:"severity_text,omitempty"` + // A value containing the body of the log record. Can be for example a human-readable + // string message (including multi-line) describing the event in a free form or it can + // be a structured data composed of arrays and maps of other values. [Optional]. + Body *AnyValue `json:"body,omitempty"` + // Additional attributes that describe the specific event occurrence. [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"` + // Flags, a bit field. 8 least significant bits are the trace flags as + // defined in W3C Trace Context specification. 24 most significant bits are reserved + // and must be set to 0. Readers must not assume that 24 most significant bits + // will be zero and must correctly mask the bits when reading 8-bit trace flag (use + // flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK). [Optional]. + Flags uint32 `json:"flags,omitempty"` + // A unique identifier for a trace. All logs from the same trace share + // the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes OR + // of length other than 16 bytes is considered invalid (empty string in OTLP/JSON + // is zero-length and thus is also invalid). + // + // This field is optional. + // + // The receivers SHOULD assume that the log record is not associated with a + // trace if any of the following is true: + // - the field is not present, + // - the field contains an invalid value. + TraceId *TraceID `json:"trace_id,omitempty"` + // A unique identifier for a span within a trace, assigned when the span + // is created. The ID is an 8-byte array. An ID with all zeroes OR of length + // other than 8 bytes is considered invalid (empty string in OTLP/JSON + // is zero-length and thus is also invalid). + // + // This field is optional. If the sender specifies a valid span_id then it SHOULD also + // specify a valid trace_id. + // + // The receivers SHOULD assume that the log record is not associated with a + // span if any of the following is true: + // - the field is not present, + // - the field contains an invalid value. + SpanId *SpanID `json:"span_id,omitempty"` +} diff --git a/x/wasitel/wasitellog/internal/types/logs_service.go b/x/wasitel/wasitellog/internal/types/logs_service.go new file mode 100644 index 0000000..9aa8ef8 --- /dev/null +++ b/x/wasitel/wasitellog/internal/types/logs_service.go @@ -0,0 +1,10 @@ +package types + +type ExportLogsServiceRequest struct { + // An array of ResourceLogs. + // For data coming from a single resource this array will typically contain one + // element. Intermediary nodes (such as OpenTelemetry Collector) that receive + // data from multiple origins typically batch the data before forwarding further and + // in that case this array will contain multiple elements. + ResourceLogs []*ResourceLogs `json:"resource_logs,omitempty"` +} diff --git a/x/wasitel/wasitellog/internal/types/resource.go b/x/wasitel/wasitellog/internal/types/resource.go new file mode 100644 index 0000000..12cfffa --- /dev/null +++ b/x/wasitel/wasitellog/internal/types/resource.go @@ -0,0 +1,13 @@ +// Original source: https://github.com/open-telemetry/opentelemetry-proto-go/blob/v1.3.1/slim/otlp/resource/v1/resource.pb.go +package types + +// Resource information. +type Resource struct { + // Set of attributes that describe the resource. + // Attribute keys MUST be unique (it is not allowed to have more than one + // attribute with the same key). + Attributes []*KeyValue `json:"attributes,omitempty"` + // dropped_attributes_count is the number of dropped attributes. If the value is 0, then + // no attributes were dropped. + DroppedAttributesCount uint32 `json:"dropped_attributes_count,omitempty"` +}