vendor: update buildkit to f449174742bf
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
Generated
Vendored
-11
@@ -47,7 +47,6 @@ type config struct {
|
||||
TracerProvider trace.TracerProvider
|
||||
MeterProvider metric.MeterProvider
|
||||
SpanKind trace.SpanKind
|
||||
SpanStartOptions []trace.SpanStartOption
|
||||
SpanAttributes []attribute.KeyValue
|
||||
MetricAttributes []attribute.KeyValue
|
||||
MetricAttributesFn func(ctx context.Context) []attribute.KeyValue
|
||||
@@ -204,16 +203,6 @@ func WithMessageEvents(events ...Event) Option {
|
||||
})
|
||||
}
|
||||
|
||||
// WithSpanOptions configures an additional set of
|
||||
// trace.SpanOptions, which are applied to each new span.
|
||||
//
|
||||
// Deprecated: It is only used by the deprecated interceptor, and is unused by [NewClientHandler] and [NewServerHandler].
|
||||
func WithSpanOptions(opts ...trace.SpanStartOption) Option {
|
||||
return optionFunc(func(c *config) {
|
||||
c.SpanStartOptions = append(c.SpanStartOptions, opts...)
|
||||
})
|
||||
}
|
||||
|
||||
// WithSpanKind returns an Option to set the span kind for spans created by
|
||||
// the handler.
|
||||
//
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
grpc_codes "google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
vendor/go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/internal/parse.go
Generated
Vendored
+1
-1
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
oldsemconv "go.opentelemetry.io/otel/semconv/v1.37.0" //nolint:depguard // Use of v1.37.0 is required for backward compatibility stability opt-in.
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
)
|
||||
|
||||
// ParseFullMethod returns a span name following the OpenTelemetry semantic
|
||||
|
||||
Generated
Vendored
+31
-19
@@ -13,8 +13,8 @@ import (
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
oldrpcconv "go.opentelemetry.io/otel/semconv/v1.37.0/rpcconv" //nolint:depguard // Use of v1.37.0 is required for backward compatibility stability opt-in.
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.40.0/rpcconv"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.41.0/rpcconv"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
grpc_codes "google.golang.org/grpc/codes"
|
||||
@@ -367,27 +367,39 @@ func (*config) handleRPC(
|
||||
span.End()
|
||||
}
|
||||
|
||||
var metricAttrs []attribute.KeyValue
|
||||
if gctx != nil {
|
||||
// Don't use gctx.metricAttrSet here, because it requires passing
|
||||
// multiple RecordOptions, which would call metric.mergeSets and
|
||||
// allocate a new set for each Record call.
|
||||
metricAttrs = make([]attribute.KeyValue, 0, len(gctx.metricAttrs)+1)
|
||||
metricAttrs = append(metricAttrs, gctx.metricAttrs...)
|
||||
}
|
||||
metricAttrs = append(metricAttrs, rpcStatusAttr)
|
||||
// Allocate vararg slice once.
|
||||
recordOpts := []metric.RecordOption{metric.WithAttributeSet(attribute.NewSet(metricAttrs...))}
|
||||
|
||||
// Use floating point division here for higher precision (instead of Millisecond method).
|
||||
// Measure right before calling Record() to capture as much elapsed time as possible.
|
||||
elapsedTime := float64(rs.EndTime.Sub(rs.BeginTime)) / float64(time.Second)
|
||||
var durationEnabled bool
|
||||
var oldDurationEnabled bool
|
||||
|
||||
if duration != nil {
|
||||
duration.Record(ctx, elapsedTime, recordOpts...)
|
||||
durationEnabled = duration.Enabled(ctx)
|
||||
}
|
||||
if oldDuration != nil {
|
||||
oldDuration.Record(ctx, elapsedTime*1000.0, recordOpts...)
|
||||
oldDurationEnabled = oldDuration.Enabled(ctx)
|
||||
}
|
||||
|
||||
if durationEnabled || oldDurationEnabled {
|
||||
var metricAttrs []attribute.KeyValue
|
||||
if gctx != nil {
|
||||
// Don't use gctx.metricAttrSet here, because it requires passing
|
||||
// multiple RecordOptions, which would call metric.mergeSets and
|
||||
// allocate a new set for each Record call.
|
||||
metricAttrs = make([]attribute.KeyValue, 0, len(gctx.metricAttrs)+1)
|
||||
metricAttrs = append(metricAttrs, gctx.metricAttrs...)
|
||||
}
|
||||
metricAttrs = append(metricAttrs, rpcStatusAttr)
|
||||
// Allocate vararg slice once.
|
||||
recordOpts := []metric.RecordOption{metric.WithAttributeSet(attribute.NewSet(metricAttrs...))}
|
||||
|
||||
// Use floating point division here for higher precision (instead of Millisecond method).
|
||||
// Measure right before calling Record() to capture as much elapsed time as possible.
|
||||
elapsedTime := float64(rs.EndTime.Sub(rs.BeginTime)) / float64(time.Second)
|
||||
|
||||
if durationEnabled {
|
||||
duration.Record(ctx, elapsedTime, recordOpts...)
|
||||
}
|
||||
if oldDurationEnabled {
|
||||
oldDuration.Record(ctx, elapsedTime*1000.0, recordOpts...)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -4,4 +4,4 @@
|
||||
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||
|
||||
// Version is the current release version of the gRPC instrumentation.
|
||||
const Version = "0.68.0"
|
||||
const Version = "0.69.0"
|
||||
|
||||
Generated
Vendored
+22
-11
@@ -63,8 +63,20 @@ func (fn clientTraceOptionFunc) apply(c *clientTracer) {
|
||||
}
|
||||
|
||||
// WithoutSubSpans will modify the httptrace.ClientTrace to only collect data
|
||||
// as Events and Attributes on a span found in the context. By default
|
||||
// as Events and Attributes on a span found in the context. By default
|
||||
// sub-spans will be generated.
|
||||
//
|
||||
// This option is recommended for services that make a large number of
|
||||
// outbound HTTP requests per incoming request (e.g., API gateways,
|
||||
// fan-out proxies, or GraphQL servers). Each outbound request creates
|
||||
// up to 7 sub-spans (http.getconn, http.dns, http.connect, http.tls,
|
||||
// http.headers, http.send, http.receive) as separate heap-allocated
|
||||
// trace.Span objects. In high fan-out services this multiplied span
|
||||
// volume can overwhelm the span processor queue and increase GC
|
||||
// pressure, resulting in elevated and continuously growing memory usage.
|
||||
// Using WithoutSubSpans replaces those spans with lightweight events on
|
||||
// the parent span, preserving the diagnostic information at a fraction
|
||||
// of the cost.
|
||||
func WithoutSubSpans() ClientTraceOption {
|
||||
return clientTraceOptionFunc(func(ct *clientTracer) {
|
||||
ct.useSpans = false
|
||||
@@ -189,10 +201,11 @@ func NewClientTrace(ctx context.Context, opts ...ClientTraceOption) *httptrace.C
|
||||
}
|
||||
|
||||
func (ct *clientTracer) start(hook, spanName string, attrs ...attribute.KeyValue) {
|
||||
if ct.root == nil {
|
||||
ct.root = trace.SpanFromContext(ct.Context)
|
||||
}
|
||||
|
||||
if !ct.useSpans {
|
||||
if ct.root == nil {
|
||||
ct.root = trace.SpanFromContext(ct.Context)
|
||||
}
|
||||
ct.root.AddEvent(hook+".start", trace.WithAttributes(attrs...))
|
||||
return
|
||||
}
|
||||
@@ -201,11 +214,7 @@ func (ct *clientTracer) start(hook, spanName string, attrs ...attribute.KeyValue
|
||||
defer ct.mtx.Unlock()
|
||||
|
||||
if hookCtx, found := ct.activeHooks[hook]; !found {
|
||||
var sp trace.Span
|
||||
ct.activeHooks[hook], sp = ct.tr.Start(ct.getParentContext(hook), spanName, trace.WithAttributes(attrs...), trace.WithSpanKind(trace.SpanKindClient))
|
||||
if ct.root == nil {
|
||||
ct.root = sp
|
||||
}
|
||||
ct.activeHooks[hook], _ = ct.tr.Start(ct.getParentContext(hook), spanName, trace.WithAttributes(attrs...), trace.WithSpanKind(trace.SpanKindClient))
|
||||
} else {
|
||||
// end was called before start finished, add the start attributes and end the span here
|
||||
span := trace.SpanFromContext(hookCtx)
|
||||
@@ -306,14 +315,16 @@ func (ct *clientTracer) dnsDone(info httptrace.DNSDoneInfo) {
|
||||
}
|
||||
|
||||
func (ct *clientTracer) connectStart(network, addr string) {
|
||||
ct.start("http.connect."+addr, "http.connect",
|
||||
ct.start(
|
||||
"http.connect."+addr, "http.connect",
|
||||
HTTPRemoteAddr.String(addr),
|
||||
HTTPConnectionStartNetwork.String(network),
|
||||
)
|
||||
}
|
||||
|
||||
func (ct *clientTracer) connectDone(network, addr string, err error) {
|
||||
ct.end("http.connect."+addr, err,
|
||||
ct.end(
|
||||
"http.connect."+addr, err,
|
||||
HTTPConnectionDoneAddr.String(addr),
|
||||
HTTPConnectionDoneNetwork.String(network),
|
||||
)
|
||||
|
||||
Generated
Vendored
+41
-4
@@ -12,6 +12,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -19,8 +20,8 @@ import (
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.40.0/httpconv"
|
||||
"go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.41.0/httpconv"
|
||||
)
|
||||
|
||||
type HTTPClient struct {
|
||||
@@ -166,7 +167,7 @@ func (n HTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyValue
|
||||
|
||||
func (n HTTPClient) method(method string) (attribute.KeyValue, attribute.KeyValue) {
|
||||
if method == "" {
|
||||
return semconv.HTTPRequestMethodGet, attribute.KeyValue{}
|
||||
return semconv.HTTPRequestMethodOther, attribute.KeyValue{}
|
||||
}
|
||||
if attr, ok := methodLookup[method]; ok {
|
||||
return attr, attribute.KeyValue{}
|
||||
@@ -176,7 +177,7 @@ func (n HTTPClient) method(method string) (attribute.KeyValue, attribute.KeyValu
|
||||
if attr, ok := methodLookup[strings.ToUpper(method)]; ok {
|
||||
return attr, orig
|
||||
}
|
||||
return semconv.HTTPRequestMethodGet, orig
|
||||
return semconv.HTTPRequestMethodOther, orig
|
||||
}
|
||||
|
||||
func (n HTTPClient) MetricAttributes(req *http.Request, statusCode int, additionalAttributes []attribute.KeyValue) []attribute.KeyValue {
|
||||
@@ -249,6 +250,9 @@ func (o MetricOpts) AddOptions() metric.AddOption {
|
||||
|
||||
func (n HTTPClient) MetricOptions(ma MetricAttributes) MetricOpts {
|
||||
attributes := n.MetricAttributes(ma.Req, ma.StatusCode, ma.AdditionalAttributes)
|
||||
if ma.StatusCode == 0 && ma.Err != nil {
|
||||
attributes = append(attributes, n.ErrorType(ma.Err))
|
||||
}
|
||||
set := metric.WithAttributeSet(attribute.NewSet(attributes...))
|
||||
|
||||
return MetricOpts{
|
||||
@@ -257,6 +261,39 @@ func (n HTTPClient) MetricOptions(ma MetricAttributes) MetricOpts {
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorType returns an error.type attribute for the given error. The otelhttp
|
||||
// Transport calls the underlying RoundTripper directly, so transport failures
|
||||
// arrive as *net.OpError (connection refused, timeout, etc.) rather than
|
||||
// *url.Error (which http.Client.Do adds above the Transport layer). This
|
||||
// function intentionally does not unwrap further: reporting a single concrete
|
||||
// type per failure keeps attribute cardinality bounded, as required by the
|
||||
// OTel spec (error.type SHOULD have low cardinality). Callers that need
|
||||
// finer-grained error distinctions should inspect the error themselves.
|
||||
func (n HTTPClient) ErrorType(err error) attribute.KeyValue {
|
||||
t := reflect.TypeOf(err)
|
||||
if t == nil {
|
||||
return semconv.ErrorTypeOther
|
||||
}
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
var value string
|
||||
if t.PkgPath() == "" || t.Name() == "" {
|
||||
// t.PkgPath() == "" covers builtin and unnamed types.
|
||||
// t.Name() == "" covers anonymous struct types that implement error,
|
||||
// which are uncommon but possible. Fall back to t.String() for both.
|
||||
value = t.String()
|
||||
} else {
|
||||
value = fmt.Sprintf("%s.%s", t.PkgPath(), t.Name())
|
||||
}
|
||||
|
||||
if value == "" {
|
||||
return semconv.ErrorTypeOther
|
||||
}
|
||||
|
||||
return semconv.ErrorTypeKey.String(value)
|
||||
}
|
||||
|
||||
func (n HTTPClient) RecordMetrics(ctx context.Context, md MetricData, opts MetricOpts) {
|
||||
recordOpts := metricRecordOptionPool.Get().(*[]metric.RecordOption)
|
||||
defer func() {
|
||||
|
||||
Generated
Vendored
+28
-11
@@ -20,8 +20,8 @@ import (
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.40.0/httpconv"
|
||||
"go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.41.0/httpconv"
|
||||
)
|
||||
|
||||
type RequestTraceAttrsOpts struct {
|
||||
@@ -243,6 +243,7 @@ type MetricAttributes struct {
|
||||
StatusCode int
|
||||
Route string
|
||||
AdditionalAttributes []attribute.KeyValue
|
||||
Err error
|
||||
}
|
||||
|
||||
type MetricData struct {
|
||||
@@ -250,13 +251,11 @@ type MetricData struct {
|
||||
RequestDuration time.Duration
|
||||
}
|
||||
|
||||
var (
|
||||
metricRecordOptionPool = &sync.Pool{
|
||||
New: func() any {
|
||||
return &[]metric.RecordOption{}
|
||||
},
|
||||
}
|
||||
)
|
||||
var metricRecordOptionPool = &sync.Pool{
|
||||
New: func() any {
|
||||
return &[]metric.RecordOption{}
|
||||
},
|
||||
}
|
||||
|
||||
func (n HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) {
|
||||
attributes := n.MetricAttributes(md.ServerName, md.Req, md.StatusCode, md.Route, md.AdditionalAttributes)
|
||||
@@ -270,9 +269,27 @@ func (n HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) {
|
||||
metricRecordOptionPool.Put(recordOpts)
|
||||
}
|
||||
|
||||
// SpanName returns the span name for an HTTP request following the
|
||||
// OpenTelemetry HTTP semantic conventions.
|
||||
// It returns "{method} {route}" when the request has a pattern,
|
||||
// or just "{method}" when no route is available.
|
||||
// Non-standard HTTP methods are replaced by "HTTP".
|
||||
func (n HTTPServer) SpanName(r *http.Request) string {
|
||||
method := strings.ToUpper(r.Method)
|
||||
if _, ok := methodLookup[method]; !ok {
|
||||
method = "HTTP"
|
||||
}
|
||||
|
||||
route := httpRoute(r.Pattern)
|
||||
if route != "" {
|
||||
return method + " " + route
|
||||
}
|
||||
return method
|
||||
}
|
||||
|
||||
func (n HTTPServer) method(method string) (attribute.KeyValue, attribute.KeyValue) {
|
||||
if method == "" {
|
||||
return semconv.HTTPRequestMethodGet, attribute.KeyValue{}
|
||||
return semconv.HTTPRequestMethodOther, attribute.KeyValue{}
|
||||
}
|
||||
if attr, ok := methodLookup[method]; ok {
|
||||
return attr, attribute.KeyValue{}
|
||||
@@ -282,7 +299,7 @@ func (n HTTPServer) method(method string) (attribute.KeyValue, attribute.KeyValu
|
||||
if attr, ok := methodLookup[strings.ToUpper(method)]; ok {
|
||||
return attr, orig
|
||||
}
|
||||
return semconv.HTTPRequestMethodGet, orig
|
||||
return semconv.HTTPRequestMethodOther, orig
|
||||
}
|
||||
|
||||
func (n HTTPServer) scheme(https bool) attribute.KeyValue { //nolint:revive // ignore linter
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
semconvNew "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
semconvNew "go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
)
|
||||
|
||||
// SplitHostPort splits a network address hostport of the form "host",
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -4,4 +4,4 @@
|
||||
package otelhttptrace // import "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
|
||||
|
||||
// Version is the current release version of the httptrace instrumentation.
|
||||
const Version = "0.68.0"
|
||||
const Version = "0.69.0"
|
||||
|
||||
+12
-5
@@ -35,10 +35,6 @@ type middleware struct {
|
||||
semconv semconv.HTTPServer
|
||||
}
|
||||
|
||||
func defaultHandlerFormatter(operation string, _ *http.Request) string {
|
||||
return operation
|
||||
}
|
||||
|
||||
// NewHandler wraps the passed handler in a span named after the operation and
|
||||
// enriches it with metrics.
|
||||
func NewHandler(handler http.Handler, operation string, opts ...Option) http.Handler {
|
||||
@@ -55,12 +51,17 @@ func NewMiddleware(operation string, opts ...Option) func(http.Handler) http.Han
|
||||
|
||||
defaultOpts := []Option{
|
||||
WithSpanOptions(trace.WithSpanKind(trace.SpanKindServer)),
|
||||
WithSpanNameFormatter(defaultHandlerFormatter),
|
||||
}
|
||||
|
||||
c := newConfig(append(defaultOpts, opts...)...)
|
||||
h.configure(c)
|
||||
|
||||
if h.spanNameFormatter == nil {
|
||||
h.spanNameFormatter = func(_ string, r *http.Request) string {
|
||||
return h.semconv.SpanName(r)
|
||||
}
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.serveHTTP(w, r, next)
|
||||
@@ -138,7 +139,13 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http
|
||||
// ReadCloser fulfills a certain interface and it is indeed nil or NoBody.
|
||||
bw := request.NewBodyWrapper(r.Body, readRecordFunc)
|
||||
if r.Body != nil && r.Body != http.NoBody {
|
||||
origReq := r
|
||||
prevBody := r.Body
|
||||
r.Body = bw
|
||||
|
||||
// Restore the original body after the request is processed to avoid issues
|
||||
// with extra wrapper since `http/server.go` later checks type of `r.Body`.
|
||||
defer func() { origReq.Body = prevBody }()
|
||||
}
|
||||
|
||||
writeRecordFunc := func(int64) {}
|
||||
|
||||
Generated
Vendored
+41
-4
@@ -12,6 +12,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -19,8 +20,8 @@ import (
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.40.0/httpconv"
|
||||
"go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.41.0/httpconv"
|
||||
)
|
||||
|
||||
type HTTPClient struct {
|
||||
@@ -166,7 +167,7 @@ func (n HTTPClient) ResponseTraceAttrs(resp *http.Response) []attribute.KeyValue
|
||||
|
||||
func (n HTTPClient) method(method string) (attribute.KeyValue, attribute.KeyValue) {
|
||||
if method == "" {
|
||||
return semconv.HTTPRequestMethodGet, attribute.KeyValue{}
|
||||
return semconv.HTTPRequestMethodOther, attribute.KeyValue{}
|
||||
}
|
||||
if attr, ok := methodLookup[method]; ok {
|
||||
return attr, attribute.KeyValue{}
|
||||
@@ -176,7 +177,7 @@ func (n HTTPClient) method(method string) (attribute.KeyValue, attribute.KeyValu
|
||||
if attr, ok := methodLookup[strings.ToUpper(method)]; ok {
|
||||
return attr, orig
|
||||
}
|
||||
return semconv.HTTPRequestMethodGet, orig
|
||||
return semconv.HTTPRequestMethodOther, orig
|
||||
}
|
||||
|
||||
func (n HTTPClient) MetricAttributes(req *http.Request, statusCode int, additionalAttributes []attribute.KeyValue) []attribute.KeyValue {
|
||||
@@ -249,6 +250,9 @@ func (o MetricOpts) AddOptions() metric.AddOption {
|
||||
|
||||
func (n HTTPClient) MetricOptions(ma MetricAttributes) MetricOpts {
|
||||
attributes := n.MetricAttributes(ma.Req, ma.StatusCode, ma.AdditionalAttributes)
|
||||
if ma.StatusCode == 0 && ma.Err != nil {
|
||||
attributes = append(attributes, n.ErrorType(ma.Err))
|
||||
}
|
||||
set := metric.WithAttributeSet(attribute.NewSet(attributes...))
|
||||
|
||||
return MetricOpts{
|
||||
@@ -257,6 +261,39 @@ func (n HTTPClient) MetricOptions(ma MetricAttributes) MetricOpts {
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorType returns an error.type attribute for the given error. The otelhttp
|
||||
// Transport calls the underlying RoundTripper directly, so transport failures
|
||||
// arrive as *net.OpError (connection refused, timeout, etc.) rather than
|
||||
// *url.Error (which http.Client.Do adds above the Transport layer). This
|
||||
// function intentionally does not unwrap further: reporting a single concrete
|
||||
// type per failure keeps attribute cardinality bounded, as required by the
|
||||
// OTel spec (error.type SHOULD have low cardinality). Callers that need
|
||||
// finer-grained error distinctions should inspect the error themselves.
|
||||
func (n HTTPClient) ErrorType(err error) attribute.KeyValue {
|
||||
t := reflect.TypeOf(err)
|
||||
if t == nil {
|
||||
return semconv.ErrorTypeOther
|
||||
}
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
var value string
|
||||
if t.PkgPath() == "" || t.Name() == "" {
|
||||
// t.PkgPath() == "" covers builtin and unnamed types.
|
||||
// t.Name() == "" covers anonymous struct types that implement error,
|
||||
// which are uncommon but possible. Fall back to t.String() for both.
|
||||
value = t.String()
|
||||
} else {
|
||||
value = fmt.Sprintf("%s.%s", t.PkgPath(), t.Name())
|
||||
}
|
||||
|
||||
if value == "" {
|
||||
return semconv.ErrorTypeOther
|
||||
}
|
||||
|
||||
return semconv.ErrorTypeKey.String(value)
|
||||
}
|
||||
|
||||
func (n HTTPClient) RecordMetrics(ctx context.Context, md MetricData, opts MetricOpts) {
|
||||
recordOpts := metricRecordOptionPool.Get().(*[]metric.RecordOption)
|
||||
defer func() {
|
||||
|
||||
Generated
Vendored
+28
-11
@@ -20,8 +20,8 @@ import (
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.40.0/httpconv"
|
||||
"go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.41.0/httpconv"
|
||||
)
|
||||
|
||||
type RequestTraceAttrsOpts struct {
|
||||
@@ -243,6 +243,7 @@ type MetricAttributes struct {
|
||||
StatusCode int
|
||||
Route string
|
||||
AdditionalAttributes []attribute.KeyValue
|
||||
Err error
|
||||
}
|
||||
|
||||
type MetricData struct {
|
||||
@@ -250,13 +251,11 @@ type MetricData struct {
|
||||
RequestDuration time.Duration
|
||||
}
|
||||
|
||||
var (
|
||||
metricRecordOptionPool = &sync.Pool{
|
||||
New: func() any {
|
||||
return &[]metric.RecordOption{}
|
||||
},
|
||||
}
|
||||
)
|
||||
var metricRecordOptionPool = &sync.Pool{
|
||||
New: func() any {
|
||||
return &[]metric.RecordOption{}
|
||||
},
|
||||
}
|
||||
|
||||
func (n HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) {
|
||||
attributes := n.MetricAttributes(md.ServerName, md.Req, md.StatusCode, md.Route, md.AdditionalAttributes)
|
||||
@@ -270,9 +269,27 @@ func (n HTTPServer) RecordMetrics(ctx context.Context, md ServerMetricData) {
|
||||
metricRecordOptionPool.Put(recordOpts)
|
||||
}
|
||||
|
||||
// SpanName returns the span name for an HTTP request following the
|
||||
// OpenTelemetry HTTP semantic conventions.
|
||||
// It returns "{method} {route}" when the request has a pattern,
|
||||
// or just "{method}" when no route is available.
|
||||
// Non-standard HTTP methods are replaced by "HTTP".
|
||||
func (n HTTPServer) SpanName(r *http.Request) string {
|
||||
method := strings.ToUpper(r.Method)
|
||||
if _, ok := methodLookup[method]; !ok {
|
||||
method = "HTTP"
|
||||
}
|
||||
|
||||
route := httpRoute(r.Pattern)
|
||||
if route != "" {
|
||||
return method + " " + route
|
||||
}
|
||||
return method
|
||||
}
|
||||
|
||||
func (n HTTPServer) method(method string) (attribute.KeyValue, attribute.KeyValue) {
|
||||
if method == "" {
|
||||
return semconv.HTTPRequestMethodGet, attribute.KeyValue{}
|
||||
return semconv.HTTPRequestMethodOther, attribute.KeyValue{}
|
||||
}
|
||||
if attr, ok := methodLookup[method]; ok {
|
||||
return attr, attribute.KeyValue{}
|
||||
@@ -282,7 +299,7 @@ func (n HTTPServer) method(method string) (attribute.KeyValue, attribute.KeyValu
|
||||
if attr, ok := methodLookup[strings.ToUpper(method)]; ok {
|
||||
return attr, orig
|
||||
}
|
||||
return semconv.HTTPRequestMethodGet, orig
|
||||
return semconv.HTTPRequestMethodOther, orig
|
||||
}
|
||||
|
||||
func (n HTTPServer) scheme(https bool) attribute.KeyValue { //nolint:revive // ignore linter
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
semconvNew "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
semconvNew "go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
)
|
||||
|
||||
// SplitHostPort splits a network address hostport of the form "host",
|
||||
|
||||
Generated
Vendored
+2
-1
@@ -15,7 +15,7 @@ import (
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
otelsemconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
otelsemconv "go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request"
|
||||
@@ -161,6 +161,7 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
t.semconv.MetricOptions(semconv.MetricAttributes{
|
||||
Req: r,
|
||||
StatusCode: statusCode,
|
||||
Err: err,
|
||||
AdditionalAttributes: append(labeler.Get(), t.metricAttributesFromRequest(r)...),
|
||||
}),
|
||||
)
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
|
||||
// Version is the current release version of the otelhttp instrumentation.
|
||||
const Version = "0.68.0"
|
||||
const Version = "0.69.0"
|
||||
|
||||
+11
-7
@@ -96,9 +96,9 @@ linters:
|
||||
- "!**/exporters/zipkin/**"
|
||||
deny:
|
||||
- pkg: go.opentelemetry.io/otel/semconv
|
||||
desc: "Use go.opentelemetry.io/otel/semconv/v1.40.0 instead. If a newer semconv version has been released, update the depguard rule."
|
||||
desc: "Use go.opentelemetry.io/otel/semconv/v1.41.0 instead. If a newer semconv version has been released, update the depguard rule."
|
||||
allow:
|
||||
- go.opentelemetry.io/otel/semconv/v1.40.0
|
||||
- go.opentelemetry.io/otel/semconv/v1.41.0
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- appendAssign
|
||||
@@ -134,13 +134,16 @@ linters:
|
||||
strconcat: true
|
||||
revive:
|
||||
confidence: 0.01
|
||||
enable-all-rules: false
|
||||
enable-default-rules: true
|
||||
max-open-files: 2048
|
||||
rules:
|
||||
- name: blank-imports
|
||||
- name: bool-literal-in-expr
|
||||
- name: constant-logical-expr
|
||||
- name: context-as-argument
|
||||
arguments:
|
||||
- allowTypesBefore: '*testing.T'
|
||||
- allow-types-before: '*testing.T'
|
||||
disabled: true
|
||||
- name: context-keys-type
|
||||
- name: deep-exit
|
||||
@@ -152,7 +155,7 @@ linters:
|
||||
- name: duplicated-imports
|
||||
- name: early-return
|
||||
arguments:
|
||||
- preserveScope
|
||||
- preserve-scope
|
||||
- name: empty-block
|
||||
- name: empty-lines
|
||||
- name: error-naming
|
||||
@@ -161,7 +164,7 @@ linters:
|
||||
- name: errorf
|
||||
- name: exported
|
||||
arguments:
|
||||
- sayRepetitiveInsteadOfStutters
|
||||
- say-repetitive-instead-of-stutters
|
||||
- name: flag-parameter
|
||||
- name: identical-branches
|
||||
- name: if-return
|
||||
@@ -169,11 +172,12 @@ linters:
|
||||
- name: increment-decrement
|
||||
- name: indent-error-flow
|
||||
arguments:
|
||||
- preserveScope
|
||||
- preserve-scope
|
||||
- name: package-comments
|
||||
- name: range
|
||||
- name: range-val-in-closure
|
||||
- name: range-val-address
|
||||
- name: receiver-naming
|
||||
- name: redefines-builtin-id
|
||||
- name: string-format
|
||||
arguments:
|
||||
@@ -183,7 +187,7 @@ linters:
|
||||
- name: struct-tag
|
||||
- name: superfluous-else
|
||||
arguments:
|
||||
- preserveScope
|
||||
- preserve-scope
|
||||
- name: time-equal
|
||||
- name: unconditional-recursion
|
||||
- name: unexported-return
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
# Agent Guide for opentelemetry-go
|
||||
|
||||
This file contains active, task-oriented instructions for autonomous and semi-autonomous coding agents working in this repository.
|
||||
|
||||
Before starting any task, read `.github/copilot-instructions.md`, `CONTRIBUTING.md`, and this file.
|
||||
Treat `.github/copilot-instructions.md` as global passive guidance for every task, including docs-only and review-only work.
|
||||
|
||||
## Core expectations
|
||||
|
||||
- Preserve OpenTelemetry specification compliance, API stability, and idiomatic Go.
|
||||
- Prefer minimal, surgical changes over broad refactors or speculative cleanup.
|
||||
- Read the package you are editing and match its existing naming, option types, error handling, comments, tests, and concurrency patterns.
|
||||
- Keep public APIs backward compatible unless the task explicitly requires a breaking change.
|
||||
- Keep telemetry resilient and loosely coupled. Do not introduce behavior that can unexpectedly interfere with host applications.
|
||||
- Inspect boundaries carefully: input validation, resource limits, cancellation, shutdown, error propagation, concurrency, and memory growth.
|
||||
- Prefer fail-safe behavior and explicit invariants over implicit assumptions.
|
||||
- Keep dependencies minimal and justified.
|
||||
- Preserve host-application safety: telemetry should not panic, block indefinitely, or amplify attacker-controlled input.
|
||||
- Be conservative on hot paths. Avoid unnecessary allocations, reflection, interface churn, blocking, global state, and high-cardinality telemetry.
|
||||
- Write comments only for intent, invariants, and non-obvious constraints. Do not add comments that restate the code.
|
||||
|
||||
## Default workflow
|
||||
|
||||
For new features and behavior changes, use this order unless the task explicitly says otherwise:
|
||||
|
||||
1. Read the relevant package, its tests, and any package docs or `README.md`.
|
||||
2. Add or update a failing unit test that captures the required behavior or regression.
|
||||
3. Implement the smallest change that makes the test pass.
|
||||
4. Refactor only after the behavior is locked in, and only if the refactor keeps the diff focused.
|
||||
5. If the changed code is on a hot path or performance-sensitive, inspect existing benchmarks and run them. Add a benchmark if coverage is missing.
|
||||
6. Update documentation artifacts as needed while the context is fresh. Follow the documentation and changelog conventions below for the specific updates required.
|
||||
7. Run `make precommit` each time before considering the work complete.
|
||||
|
||||
For docs-only, test-only, or review-only tasks, still start with the required repository guidance above, then skip the workflow steps that do not apply while keeping the same discipline around scope, verification, and repository conventions.
|
||||
|
||||
## Verification
|
||||
|
||||
- Use `make` as the canonical repository verification command. The default target is `precommit`.
|
||||
- `make precommit` is the expected final verification step for linting, generation, README checks, module checks, and tests.
|
||||
- During iteration, targeted commands are fine for fast feedback, but do not stop there if the task changes code.
|
||||
- If you touch performance-sensitive code, run focused benchmarks and compare the results using `benchstat` in addition to `make`.
|
||||
|
||||
## Documentation and changelog
|
||||
|
||||
- Non-internal, non-test packages should have Go doc comments, usually in `doc.go`.
|
||||
- Non-internal, non-test, non-documentation packages should also have a `README.md` with at least a title and a `pkg.go.dev` badge.
|
||||
- Prefer examples over long code snippets in GoDoc when practical.
|
||||
- Keep docs aligned with actual behavior. Do not leave stale comments, stale examples, or stale package documentation behind.
|
||||
- For user-visible changes, update `CHANGELOG.md` under the appropriate `Added`, `Changed`, `Deprecated`, `Fixed`, or `Removed` section within `## [Unreleased]`.
|
||||
|
||||
## Repository habits
|
||||
|
||||
- Prefer focused diffs. Avoid drive-by cleanup.
|
||||
- Follow existing option patterns and exported API conventions instead of inventing new abstractions.
|
||||
- Generated files are checked in. If your change affects generation, keep generated output up to date.
|
||||
- Prefer fast local search tools such as `rg` when exploring the repository.
|
||||
- When changing behavior, make the invariants explicit in tests.
|
||||
|
||||
## Personas
|
||||
|
||||
### Feature Agent
|
||||
|
||||
Use this persona for new behavior, new API surface, or spec-driven feature work.
|
||||
|
||||
- Start with a failing unit test.
|
||||
- Confirm the expected behavior against the spec, existing package behavior, and public API compatibility.
|
||||
- Implement the smallest viable change.
|
||||
- Update GoDoc, examples, `README.md`, and `CHANGELOG.md` when the change is user-visible.
|
||||
- If the feature touches a hot path, check benchmarks and add one if the coverage is missing.
|
||||
|
||||
### Refactoring Agent
|
||||
|
||||
Use this persona when improving structure without intentionally changing behavior.
|
||||
|
||||
- Treat behavior preservation as the default contract.
|
||||
- Add or tighten tests before moving code if current behavior is not already pinned down.
|
||||
- Avoid broad rewrites, clever abstractions, or package-wide cleanup unless explicitly requested.
|
||||
- If a refactor touches a hot path, benchmark before and after.
|
||||
- Keep API shape, semantics, concurrency guarantees, and failure modes unchanged unless the task says otherwise.
|
||||
|
||||
### Test Agent
|
||||
|
||||
Use this persona when adding missing coverage, reproducing bugs, or hardening regressions.
|
||||
|
||||
- Reproduce the bug or missing behavior with the smallest failing test you can.
|
||||
- Prefer testing public behavior and externally visible invariants.
|
||||
- Add targeted regression tests before changing production code.
|
||||
- Only change production code when it is required to make the tested behavior correct or testable.
|
||||
- Keep tests deterministic, readable, and aligned with package patterns.
|
||||
|
||||
### Performance Agent
|
||||
|
||||
Use this persona for hot-path work, allocation reduction, or throughput and latency improvements.
|
||||
|
||||
- Benchmark first to establish a baseline.
|
||||
- Prefer changes that reduce allocations, copying, interface churn, and unnecessary synchronization.
|
||||
- Do not trade away correctness, spec compliance, or API stability for micro-optimizations.
|
||||
- Add or update benchmarks when performance-sensitive coverage is missing.
|
||||
- If you materially change a hot path, capture before-and-after results, preferably with `benchstat`.
|
||||
|
||||
### Review Agent
|
||||
|
||||
Use this persona when asked to review code, patches, or pull requests.
|
||||
|
||||
- Lead with findings, not summaries.
|
||||
- Order findings by severity and include precise file and line references when available.
|
||||
- Focus on correctness, spec compliance, API compatibility, concurrency safety, resilience, performance regressions, missing tests, missing benchmarks, documentation gaps, and changelog gaps.
|
||||
- Call out when a diff is broader than necessary.
|
||||
- If you find no issues, say that explicitly and note any residual risks or verification gaps.
|
||||
+96
-1
@@ -11,6 +11,100 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||
<!-- Released section -->
|
||||
<!-- Don't change this section unless doing release -->
|
||||
|
||||
## [1.44.0/0.66.0/0.20.0/0.0.17] 2026-05-27
|
||||
|
||||
### Added
|
||||
|
||||
- Add `ByteSlice` and `ByteSliceValue` functions for new `BYTESLICE` attribute type in `go.opentelemetry.io/otel/attribute`. (#7948)
|
||||
- Apply attribute value limit to the `KindBytes` attribute type in `go.opentelemetry.io/otel/sdk/log`. (#7990)
|
||||
- Apply attribute value limit to the `BYTESLICE` attribute type in `go.opentelemetry.io/otel/sdk/trace`. (#7990)
|
||||
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/trace`. (#8153)
|
||||
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlptrace`. (#8153)
|
||||
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlplog`. (#8153)
|
||||
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric`. (#8153)
|
||||
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/zipkin`. (#8153)
|
||||
- Add `String` method for `Value` type in `go.opentelemetry.io/otel/attribute`. (#8142)
|
||||
- Add `Slice` and `SliceValue` functions for new `SLICE` attribute type in `go.opentelemetry.io/otel/attribute`. (#8166)
|
||||
- Support `SLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlptrace`. (#8216)
|
||||
- Support `SLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlplog`. (#8216)
|
||||
- Support `SLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric`. (#8216)
|
||||
- Support `SLICE` attributes in `go.opentelemetry.io/otel/exporters/zipkin`. (#8216)
|
||||
- Apply `AttributeValueLengthLimit` to `attribute.SLICE` type attribute values in `go.opentelemetry.io/otel/sdk/trace`, recursively truncating contained string values. (#8217)
|
||||
- Add `Error` field on `Record` type in `go.opentelemetry.io/otel/log/logtest`. (#8148)
|
||||
- Add `WithMaxRequestSize` option in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. (#8157)
|
||||
- Add `WithMaxRequestSize` option in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#8157)
|
||||
- Add `WithMaxRequestSize` option in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc`. (#8157)
|
||||
- Add `WithMaxRequestSize` option in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#8157)
|
||||
- Add `WithMaxRequestSize` option in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. (#8157)
|
||||
- Add `WithMaxRequestSize` option in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8157)
|
||||
- Add `Settable` to `go.opentelemetry.io/otel/metric/x` to allow reusing attribute options. (#8178)
|
||||
- Add experimental support for splitting metric data across multiple batches in `go.opentelemetry.io/otel/sdk/metric`.
|
||||
Set `OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE=<max_size>` to enable for all periodic readers.
|
||||
See `go.opentelemetry.io/otel/sdk/metric/internal/x` for feature documentation. (#8071)
|
||||
- Add experimental self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc`.
|
||||
Enable with `OTEL_GO_X_SELF_OBSERVABILITY=true` environment variable.
|
||||
See `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/x` for feature documentation. (#8192)
|
||||
- Add experimental self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`.
|
||||
Enable with `OTEL_GO_X_SELF_OBSERVABILITY=true` environment variable.
|
||||
See `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/x` for feature documentation. (#8194)
|
||||
- Add experimental self-observability metrics in `go.opentelemetry.io/otel/exporters/stdout/stdoutlog`.
|
||||
Enable with `OTEL_GO_X_SELF_OBSERVABILITY=true` environment variable.
|
||||
See `go.opentelemetry.io/otel/stdout/stdoutlog/internal/x` for feature documentation. (#8263)
|
||||
- Add `WithDefaultAttributes` to `go.opentelemetry.io/otel/metric/x` to support setting default attributes on instruments. (#8135)
|
||||
- Add `go.opentelemetry.io/otel/semconv/v1.41.0` package.
|
||||
The package contains semantic conventions from the `v1.41.0` version of the OpenTelemetry Semantic Conventions.
|
||||
See the [migration documentation](./semconv/v1.41.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.40.0`. (#8324)
|
||||
- Add Observable variants of instruments to `go.opentelemetry.io/otel/semconv/v1.41.0` package. (#8350)
|
||||
- Generate explicit histogram bucket boundaries from weaver configuration for HTTP and RPC duration instruments in `go.opentelemetry.io/otel/semconv/v1.41.0`. (#8002)
|
||||
|
||||
### Changed
|
||||
|
||||
- ⚠️ **Breaking Change:** `go.opentelemetry.io/otel/sdk/metric` now applies a default cardinality limit of 2000 to comply with the Metrics SDK specification recommendation.
|
||||
New attribute sets are dropped when the cardinality limit is reached. The measurement of these sets are aggregated into a special attribute set containing `attribute.Bool("otel.metric.overflow", true)`.
|
||||
This can break users who relied on the previous unlimited default.
|
||||
Set `WithCardinalityLimit(0)` or the deprecated `OTEL_GO_X_CARDINALITY_LIMIT=0` environment variable to preserve unlimited cardinality.
|
||||
Note that support for `OTEL_GO_X_CARDINALITY_LIMIT` may be removed in a future release. (#8247)
|
||||
- `ErrorType` in `go.opentelemetry.io/otel/semconv` now unwraps errors created with `fmt.Errorf` when deriving the `error.type` attribute. (#8133)
|
||||
- `go.opentelemetry.io/otel/sdk/log` now unwraps error chains created with `fmt.Errorf` when deriving the `error.type` attribute from errors on log records. (#8133)
|
||||
- `Set.MarshalLog` method in `go.opentelemetry.io/otel/attribute` now uses `Value.String` formatting following the [OpenTelemetry AnyValue representation for non-OTLP protocols](https://opentelemetry.io/docs/specs/otel/common/#anyvalue). (#8169)
|
||||
- Optimize `go.opentelemetry.io/otel/sdk/metric` to return a drop reservoir and short-circuit `Offer` calls to the exemplar reservoir when `exemplar.AlwaysOffFilter` is configured. (#8211) (#8267)
|
||||
- Optimize `go.opentelemetry.io/otel/sdk/metric` to return a drop reservoir for asynchronous instruments when `exemplar.TraceBasedFilter` is configured. (#8286)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecate `Value.Emit` method in `go.opentelemetry.io/otel/attribute`.
|
||||
Use `Value.String` instead. (#8176)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Limit OTLP request size to 64 MiB by default in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`.
|
||||
The limit applies before compression, oversized requests are treated as non-retryable errors, and the limit can be configured with the new `WithMaxRequestSize` option. (#8157, #8365)
|
||||
- Limit OTLP request size to 64 MiB by default in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`.
|
||||
The limit applies before compression, oversized requests are treated as non-retryable errors, and the limit can be configured with the new `WithMaxRequestSize` option. (#8157, #8365)
|
||||
- Limit OTLP request size to 64 MiB by default in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc`.
|
||||
The limit applies before compression, oversized requests are treated as non-retryable errors, and the limit can be configured with the new `WithMaxRequestSize` option. (#8157, #8365)
|
||||
- Limit OTLP request size to 64 MiB by default in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`.
|
||||
The limit applies before compression, oversized requests are treated as non-retryable errors, and the limit can be configured with the new `WithMaxRequestSize` option. (#8157, #8365)
|
||||
- Limit OTLP request size to 64 MiB by default in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`.
|
||||
The limit applies before compression, oversized requests are treated as non-retryable errors, and the limit can be configured with the new `WithMaxRequestSize` option. (#8157, #8365)
|
||||
- Limit OTLP request size to 64 MiB by default in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`.
|
||||
The limit applies before compression, oversized requests are treated as non-retryable errors, and the limit can be configured with the new `WithMaxRequestSize` option. (#8157, #8365)
|
||||
- Fix gzipped request body replay on redirect in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#8135)
|
||||
- Fix gzipped request body replay on redirect in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8152)
|
||||
- `go.opentelemetry.io/otel/exporters/prometheus` now uses `Value.String` formatting for label values following the [OpenTelemetry AnyValue representation for non-OTLP protocols](https://opentelemetry.io/docs/specs/otel/common/#anyvalue). (#8170)
|
||||
- Propagate errors from the exporter when calling `Shutdown` on `BatchSpanProcessor` in `go.opentelemetry.io/otel/sdk/trace`. (#8197)
|
||||
- Fix stale status code reporting on self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp` and `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8226)
|
||||
- Fix a concurrent `Collect` data race and potential panic in `go.opentelemetry.io/otel/exporters/prometheus` when `WithResourceAsConstantLabels` option is used. (#8227)
|
||||
- Fix race condition in `FixedSizeReservoir` in `go.opentelemetry.io/otel/sdk/metric/exemplar` by reverting #7447. (#8249)
|
||||
- Fix `FixedSizeReservoir` in `go.opentelemetry.io/otel/sdk/metric/exemplar` to safely handle zero size.
|
||||
A capacity check in the constructor initializes the reservoir safely and skips initialization for zero-cap; early returns in `Offer()` and `Collect()` ensure no-op behavior. (#8295)
|
||||
- Fix counting of spans and logs in self-observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`, `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`, `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`, and `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8254)
|
||||
- Drop conflicting scope attributes named `name`, `version`, or `schema_url` from metric labels in `go.opentelemetry.io/otel/exporters/prometheus`, preserving the dedicated `otel_scope_name`, `otel_scope_version`, and `otel_scope_schema_url` labels. (#8264)
|
||||
- Close schema files opened by `ParseFile` in `go.opentelemetry.io/otel/schema/v1.0` and `go.opentelemetry.io/otel/schema/v1.1`. ([GHSA-995v-fvrw-c78m](https://github.com/open-telemetry/opentelemetry-go/security/advisories/GHSA-995v-fvrw-c78m))
|
||||
- Enforce the 8192-byte baggage size limit during extraction/parsing, changing behavior when the limit is exceeded in `go.opentelemetry.io/otel/baggage` and `go.opentelemetry.io/otel/propagation`. (#8222)
|
||||
- Fix `go.opentelemetry.io/otel/semconv/v1.41.0` to include `Attr*` helper methods for required attributes on observable instruments. (#8361)
|
||||
- Limit baggage extraction error reporting in `go.opentelemetry.io/otel/propagation` to prevent malformed or oversized baggage headers from flooding logs. ([GHSA-5wrp-cwcj-q835](https://github.com/open-telemetry/opentelemetry-go/security/advisories/GHSA-5wrp-cwcj-q835))
|
||||
|
||||
## [1.43.0/0.65.0/0.19.0] 2026-04-02
|
||||
|
||||
### Added
|
||||
@@ -3619,7 +3713,8 @@ It contains api and sdk for trace and meter.
|
||||
- CircleCI build CI manifest files.
|
||||
- CODEOWNERS file to track owners of this project.
|
||||
|
||||
[Unreleased]: https://github.com/open-telemetry/opentelemetry-go/compare/v1.43.0...HEAD
|
||||
[Unreleased]: https://github.com/open-telemetry/opentelemetry-go/compare/v1.44.0...HEAD
|
||||
[1.44.0/0.66.0/0.20.0/0.0.17]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.44.0
|
||||
[1.43.0/0.65.0/0.19.0]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.43.0
|
||||
[1.42.0/0.64.0/0.18.0/0.0.16]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.42.0
|
||||
[1.41.0/0.63.0/0.17.0/0.0.15]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.41.0
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# Instructions for Claude Code
|
||||
|
||||
@AGENTS.md
|
||||
+89
-12
@@ -11,6 +11,12 @@ for a summary description of past meetings. To request edit access,
|
||||
join the meeting or get in touch on
|
||||
[Slack](https://cloud-native.slack.com/archives/C01NPAXACKT).
|
||||
|
||||
The meeting is open for all to join. We invite everyone to join our
|
||||
meeting, regardless of your experience level. Whether you're a
|
||||
seasoned OpenTelemetry developer, just starting your journey, or
|
||||
simply curious about the work we do, you're more than welcome to
|
||||
participate!
|
||||
|
||||
## Development
|
||||
|
||||
You can view and edit the source code by cloning this repository:
|
||||
@@ -746,8 +752,8 @@ Encapsulate setup in constructor functions, ensuring clear ownership and scope:
|
||||
import (
|
||||
"errors"
|
||||
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.40.0/otelconv"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.41.0/otelconv"
|
||||
)
|
||||
|
||||
type SDKComponent struct {
|
||||
@@ -808,11 +814,11 @@ func (c *Component) initObservability() {
|
||||
|
||||
#### Performance
|
||||
|
||||
When observability is disabled there should be little to no overhead.
|
||||
When observability is disabled or the instrument is not `Enabled`, there should be little to no overhead.
|
||||
|
||||
```go
|
||||
func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error {
|
||||
if e.inst != nil {
|
||||
if e.inst != nil && e.inst.Enabled(ctx) {
|
||||
attrs := expensiveOperation()
|
||||
e.inst.recordSpanInflight(ctx, int64(len(spans)), attrs...)
|
||||
}
|
||||
@@ -829,7 +835,7 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
|
||||
}
|
||||
|
||||
func (i *instrumentation) recordSpanInflight(ctx context.Context, count int64, attrs ...attribute.KeyValue) {
|
||||
if i == nil || i.inflight == nil {
|
||||
if i == nil || i.inflight == nil || !i.inflight.Enabled(ctx) {
|
||||
return
|
||||
}
|
||||
i.inflight.Add(ctx, count, metric.WithAttributes(attrs...))
|
||||
@@ -865,8 +871,12 @@ var (
|
||||
)
|
||||
|
||||
func (i *instrumentation) record(ctx context.Context, value int64, baseAttrs ...attribute.KeyValue) {
|
||||
if !i.counter.Enabled(ctx) {
|
||||
return
|
||||
}
|
||||
attrs := attrPool.Get().(*[]attribute.KeyValue)
|
||||
defer func() {
|
||||
clear(*attrs) // Clear references to strings/etc to let GC collect them.
|
||||
*attrs = (*attrs)[:0] // Reset.
|
||||
attrPool.Put(attrs)
|
||||
}()
|
||||
@@ -877,6 +887,7 @@ func (i *instrumentation) record(ctx context.Context, value int64, baseAttrs ...
|
||||
|
||||
addOpt := addOptPool.Get().(*[]metric.AddOption)
|
||||
defer func() {
|
||||
clear(*addOpt)
|
||||
*addOpt = (*addOpt)[:0]
|
||||
addOptPool.Put(addOpt)
|
||||
}()
|
||||
@@ -1007,16 +1018,20 @@ Ensure observability measurements receive the correct context, especially for tr
|
||||
```go
|
||||
func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error {
|
||||
// Use the provided context for observability measurements
|
||||
e.inst.recordSpanExportStarted(ctx, len(spans))
|
||||
if e.inst.Enabled(ctx) {
|
||||
e.inst.recordSpanExportStarted(ctx, len(spans))
|
||||
}
|
||||
|
||||
err := e.doExport(ctx, spans)
|
||||
|
||||
if err != nil {
|
||||
e.inst.recordSpanExportFailed(ctx, len(spans), err)
|
||||
} else {
|
||||
e.inst.recordSpanExportSucceeded(ctx, len(spans))
|
||||
if e.inst.Enabled(ctx) {
|
||||
if err != nil {
|
||||
e.inst.recordSpanExportFailed(ctx, len(spans), err)
|
||||
} else {
|
||||
e.inst.recordSpanExportSucceeded(ctx, len(spans))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return err
|
||||
}
|
||||
```
|
||||
@@ -1039,7 +1054,7 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
|
||||
|
||||
All observability metrics should follow the [OpenTelemetry Semantic Conventions for SDK metrics](https://github.com/open-telemetry/semantic-conventions/blob/1cf2476ae5e518225a766990a28a6d5602bd5a30/docs/otel/sdk-metrics.md).
|
||||
|
||||
Use the metric semantic conventions convenience package [otelconv](./semconv/v1.40.0/otelconv/metric.go).
|
||||
Use the metric semantic conventions convenience package [otelconv](./semconv/v1.41.0/otelconv/metric.go).
|
||||
|
||||
##### Component Identification
|
||||
|
||||
@@ -1109,6 +1124,68 @@ func TestObservability(t *testing.T) {
|
||||
Test order should not affect results.
|
||||
Ensure that any global state (e.g. component ID counters) is reset between tests.
|
||||
|
||||
### Experimental Features
|
||||
|
||||
To support the development of new features in the specification, we use the following patterns to implement in-development features without adding new public artifacts in stable modules.
|
||||
|
||||
#### Experimental behavior with no API artifacts
|
||||
|
||||
Features that change behavior without changing the API (e.g., exemplar collection, auto-generation of identifiers) are implemented behind a feature gate.
|
||||
The implementation resides in an `/internal/x` package and is activated through environment variables with the `OTEL_GO_X_` prefix (e.g., `OTEL_GO_X_OBSERVABILITY`).
|
||||
The feature must be documented in a `README.md` file in the `/internal/x` package.
|
||||
|
||||
#### Experimental methods on SDK-only interfaces
|
||||
|
||||
Features that require new methods on SDK interfaces are defined as a new interface in an experimental module (e.g., `go.opentelemetry.io/otel/sdk/x`).
|
||||
The SDK uses type assertions (without importing the unstable package) to check if passing types implement these experimental interfaces.
|
||||
The SDK must not depend on the experimental module.
|
||||
|
||||
#### Experimental structs, functions, or interfaces
|
||||
|
||||
Features that don't need any changes to the existing stable package are implemented in an experimental module (e.g., `go.opentelemetry.io/otel/sdk/x`).
|
||||
|
||||
#### Experimental signals and components
|
||||
|
||||
New telemetry signals (e.g., Logs before stabilization) and components (e.g. bridges) are hosted in new, unstable modules (e.g., `go.opentelemetry.io/otel/log` before 1.0.0).
|
||||
The package should have the final name it will use once stabilized (i.e. not `/x`), and is released at a v0.x.y version to indicate it is not stable.
|
||||
Most new components are hosted in [opentelemetry-go-contrib](https://github.com/open-telemetry/opentelemetry-go-contrib).
|
||||
|
||||
#### Experimental options for API or SDK functions
|
||||
|
||||
Experimental Options functions are implemented in an experimental module (e.g., `go.opentelemetry.io/otel/sdk/x`).
|
||||
The return type of the Option function must embed the option's type (e.g. `metric.InstrumentOption`), and have an `Experimental()` method to prevent the API from panicking when the option is used.
|
||||
The SDK uses type assertions (without importing the unstable package) to check if passing types implement these experimental interfaces.
|
||||
The SDK must not depend on the experimental module.
|
||||
|
||||
For example:
|
||||
|
||||
```go
|
||||
type myOption struct {
|
||||
// Embed the stable option type.
|
||||
metric.InstrumentOption
|
||||
value string
|
||||
}
|
||||
|
||||
// Experimental prevents the API from panicking when the option is used.
|
||||
func (o myOption) Experimental() {}
|
||||
|
||||
// The SDK can use type assertions to use this function.
|
||||
func (o myOption) Value() string { return o.value }
|
||||
|
||||
func WithMyOption(value string) metric.InstrumentOption {
|
||||
return myOption{value: value}
|
||||
}
|
||||
```
|
||||
|
||||
#### Not Supported
|
||||
|
||||
The following kinds of experimental features are **not currently supported** on stable interfaces:
|
||||
|
||||
- Experimental methods on API interfaces
|
||||
- Experimental fields for API or SDK exported structs
|
||||
|
||||
In some cases forks or long-lived branches may be used for prototyping these features.
|
||||
|
||||
## Approvers and Maintainers
|
||||
|
||||
### Maintainers
|
||||
|
||||
+9
-1
@@ -191,8 +191,16 @@ benchmark: $(OTEL_GO_MOD_DIRS:%=benchmark/%)
|
||||
benchmark/%:
|
||||
cd $* && $(GO) test -run='^$$' -bench=. $(ARGS) ./...
|
||||
|
||||
# sdk/metric is split into two shards to work around CodSpeed limitations.
|
||||
# See https://github.com/CodSpeedHQ/codspeed-go/issues/56
|
||||
BENCHMARK_SHARDS := $(filter-out ./sdk/metric,$(OTEL_GO_MOD_DIRS)) ./sdk/metric/root ./sdk/metric/internal
|
||||
benchmark/./sdk/metric/root:
|
||||
cd ./sdk/metric && $(GO) test -run='^$$' -bench=. $(ARGS) . ./exemplar/...
|
||||
benchmark/./sdk/metric/internal:
|
||||
cd ./sdk/metric && $(GO) test -run='^$$' -bench=. $(ARGS) ./internal/...
|
||||
|
||||
print-sharded-benchmarks:
|
||||
@echo $(OTEL_GO_MOD_DIRS) | jq -cR 'split(" ")'
|
||||
@echo $(BENCHMARK_SHARDS) | jq -cR 'split(" ")'
|
||||
|
||||
.PHONY: golangci-lint golangci-lint-fix
|
||||
golangci-lint-fix: ARGS=--fix
|
||||
|
||||
+3
-1
@@ -105,7 +105,9 @@ func (d *defaultAttrEncoder) Encode(iter Iterator) string {
|
||||
if keyValue.Value.Type() == STRING {
|
||||
copyAndEscape(buf, keyValue.Value.AsString())
|
||||
} else {
|
||||
_, _ = buf.WriteString(keyValue.Value.Emit())
|
||||
_, _ = buf.WriteString(
|
||||
keyValue.Value.Emit(),
|
||||
) //nolint:staticcheck // Preserve the existing default encoder output.
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
|
||||
+47
-11
@@ -27,6 +27,8 @@ const (
|
||||
int64SliceID uint64 = 3762322556277578591 // "_[]int64" (little endian)
|
||||
float64SliceID uint64 = 7308324551835016539 // "[]double" (little endian)
|
||||
stringSliceID uint64 = 7453010373645655387 // "[]string" (little endian)
|
||||
byteSliceID uint64 = 6874028470941080415 // "_[]byte_" (little endian)
|
||||
sliceID uint64 = 7883494272577650031 // "__slice_" (little endian)
|
||||
emptyID uint64 = 7305809155345288421 // "__empty_" (little endian)
|
||||
)
|
||||
|
||||
@@ -42,53 +44,87 @@ func hashKVs(kvs []KeyValue) uint64 {
|
||||
// hashKV returns the xxHash64 hash of kv with h as the base.
|
||||
func hashKV(h xxhash.Hash, kv KeyValue) xxhash.Hash {
|
||||
h = h.String(string(kv.Key))
|
||||
return hashValue(h, kv.Value)
|
||||
}
|
||||
|
||||
switch kv.Value.Type() {
|
||||
func hashValue(h xxhash.Hash, v Value) xxhash.Hash {
|
||||
switch v.Type() {
|
||||
case BOOL:
|
||||
h = h.Uint64(boolID)
|
||||
h = h.Uint64(kv.Value.numeric)
|
||||
h = h.Uint64(v.numeric)
|
||||
case INT64:
|
||||
h = h.Uint64(int64ID)
|
||||
h = h.Uint64(kv.Value.numeric)
|
||||
h = h.Uint64(v.numeric)
|
||||
case FLOAT64:
|
||||
h = h.Uint64(float64ID)
|
||||
// Assumes numeric stored with math.Float64bits.
|
||||
h = h.Uint64(kv.Value.numeric)
|
||||
h = h.Uint64(v.numeric)
|
||||
case STRING:
|
||||
h = h.Uint64(stringID)
|
||||
h = h.String(kv.Value.stringly)
|
||||
h = h.String(v.stringly)
|
||||
case BOOLSLICE:
|
||||
h = h.Uint64(boolSliceID)
|
||||
rv := reflect.ValueOf(kv.Value.slice)
|
||||
rv := reflect.ValueOf(v.slice)
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
h = h.Bool(rv.Index(i).Bool())
|
||||
}
|
||||
case INT64SLICE:
|
||||
h = h.Uint64(int64SliceID)
|
||||
rv := reflect.ValueOf(kv.Value.slice)
|
||||
rv := reflect.ValueOf(v.slice)
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
h = h.Int64(rv.Index(i).Int())
|
||||
}
|
||||
case FLOAT64SLICE:
|
||||
h = h.Uint64(float64SliceID)
|
||||
rv := reflect.ValueOf(kv.Value.slice)
|
||||
rv := reflect.ValueOf(v.slice)
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
h = h.Float64(rv.Index(i).Float())
|
||||
}
|
||||
case STRINGSLICE:
|
||||
h = h.Uint64(stringSliceID)
|
||||
rv := reflect.ValueOf(kv.Value.slice)
|
||||
rv := reflect.ValueOf(v.slice)
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
h = h.String(rv.Index(i).String())
|
||||
}
|
||||
case BYTESLICE:
|
||||
h = h.Uint64(byteSliceID)
|
||||
h = h.String(v.stringly)
|
||||
case SLICE:
|
||||
h = h.Uint64(sliceID)
|
||||
switch vals := v.slice.(type) {
|
||||
case [0]Value:
|
||||
// No values to hash, but the type identifier is still hashed above.
|
||||
case [1]Value:
|
||||
h = hashValueSlice(h, vals[:])
|
||||
case [2]Value:
|
||||
h = hashValueSlice(h, vals[:])
|
||||
case [3]Value:
|
||||
h = hashValueSlice(h, vals[:])
|
||||
case [4]Value:
|
||||
h = hashValueSlice(h, vals[:])
|
||||
case [5]Value:
|
||||
h = hashValueSlice(h, vals[:])
|
||||
default:
|
||||
rv := reflect.ValueOf(v.slice)
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
h = hashValue(h, rv.Index(i).Interface().(Value))
|
||||
}
|
||||
}
|
||||
case EMPTY:
|
||||
h = h.Uint64(emptyID)
|
||||
default:
|
||||
// Logging is an alternative, but using the internal logger here
|
||||
// causes an import cycle so it is not done.
|
||||
v := kv.Value.AsInterface()
|
||||
msg := fmt.Sprintf("unknown value type: %[1]v (%[1]T)", v)
|
||||
val := v.AsInterface()
|
||||
msg := fmt.Sprintf("unknown value type: %[1]v (%[1]T)", val)
|
||||
panic(msg)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func hashValueSlice(h xxhash.Hash, vals []Value) xxhash.Hash {
|
||||
for _, v := range vals {
|
||||
h = hashValue(h, v)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
+22
@@ -117,6 +117,28 @@ func (k Key) StringSlice(v []string) KeyValue {
|
||||
}
|
||||
}
|
||||
|
||||
// ByteSlice creates a KeyValue instance with a BYTESLICE Value.
|
||||
//
|
||||
// If creating both a key and value at the same time, use the provided
|
||||
// convenience function instead -- ByteSlice(name, value).
|
||||
func (k Key) ByteSlice(v []byte) KeyValue {
|
||||
return KeyValue{
|
||||
Key: k,
|
||||
Value: ByteSliceValue(v),
|
||||
}
|
||||
}
|
||||
|
||||
// Slice creates a KeyValue instance with a SLICE Value.
|
||||
//
|
||||
// If creating both a key and value at the same time, use the provided
|
||||
// convenience function instead -- Slice(name, values...).
|
||||
func (k Key) Slice(v ...Value) KeyValue {
|
||||
return KeyValue{
|
||||
Key: k,
|
||||
Value: SliceValue(v...),
|
||||
}
|
||||
}
|
||||
|
||||
// Defined reports whether the key is not empty.
|
||||
func (k Key) Defined() bool {
|
||||
return len(k) != 0
|
||||
|
||||
+10
@@ -68,6 +68,16 @@ func StringSlice(k string, v []string) KeyValue {
|
||||
return Key(k).StringSlice(v)
|
||||
}
|
||||
|
||||
// ByteSlice creates a KeyValue with a BYTESLICE Value type.
|
||||
func ByteSlice(k string, v []byte) KeyValue {
|
||||
return Key(k).ByteSlice(v)
|
||||
}
|
||||
|
||||
// Slice creates a KeyValue with a SLICE Value type.
|
||||
func Slice(k string, v ...Value) KeyValue {
|
||||
return Key(k).Slice(v...)
|
||||
}
|
||||
|
||||
// Stringer creates a new key-value pair with a passed name and a string
|
||||
// value generated by the passed Stringer interface.
|
||||
func Stringer(k string, v fmt.Stringer) KeyValue {
|
||||
|
||||
+2
-2
@@ -401,7 +401,7 @@ func computeDataFixed(kvs []KeyValue) any {
|
||||
func computeDataReflect(kvs []KeyValue) any {
|
||||
at := reflect.New(reflect.ArrayOf(len(kvs), keyValueType)).Elem()
|
||||
for i, keyValue := range kvs {
|
||||
*(at.Index(i).Addr().Interface().(*KeyValue)) = keyValue
|
||||
*at.Index(i).Addr().Interface().(*KeyValue) = keyValue
|
||||
}
|
||||
return at.Interface()
|
||||
}
|
||||
@@ -415,7 +415,7 @@ func (l *Set) MarshalJSON() ([]byte, error) {
|
||||
func (l Set) MarshalLog() any {
|
||||
kvs := make(map[string]string)
|
||||
for _, kv := range l.ToSlice() {
|
||||
kvs[string(kv.Key)] = kv.Value.Emit()
|
||||
kvs[string(kv.Key)] = kv.Value.String()
|
||||
}
|
||||
return kvs
|
||||
}
|
||||
|
||||
+4
-2
@@ -17,11 +17,13 @@ func _() {
|
||||
_ = x[INT64SLICE-6]
|
||||
_ = x[FLOAT64SLICE-7]
|
||||
_ = x[STRINGSLICE-8]
|
||||
_ = x[BYTESLICE-9]
|
||||
_ = x[SLICE-10]
|
||||
}
|
||||
|
||||
const _Type_name = "EMPTYBOOLINT64FLOAT64STRINGBOOLSLICEINT64SLICEFLOAT64SLICESTRINGSLICE"
|
||||
const _Type_name = "EMPTYBOOLINT64FLOAT64STRINGBOOLSLICEINT64SLICEFLOAT64SLICESTRINGSLICEBYTESLICESLICE"
|
||||
|
||||
var _Type_index = [...]uint8{0, 5, 9, 14, 21, 27, 36, 46, 58, 69}
|
||||
var _Type_index = [...]uint8{0, 5, 9, 14, 21, 27, 36, 46, 58, 69, 78, 83}
|
||||
|
||||
func (i Type) String() string {
|
||||
idx := int(i) - 0
|
||||
|
||||
+742
@@ -4,9 +4,14 @@
|
||||
package attribute // import "go.opentelemetry.io/otel/attribute"
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
attribute "go.opentelemetry.io/otel/attribute/internal"
|
||||
)
|
||||
@@ -45,6 +50,10 @@ const (
|
||||
FLOAT64SLICE
|
||||
// STRINGSLICE is a slice of strings Type Value.
|
||||
STRINGSLICE
|
||||
// BYTESLICE is a slice of bytes Type Value.
|
||||
BYTESLICE
|
||||
// SLICE is a slice of Value Type values.
|
||||
SLICE
|
||||
// INVALID is used for a Value with no value set.
|
||||
//
|
||||
// Deprecated: Use EMPTY instead as an empty value is a valid value.
|
||||
@@ -134,6 +143,19 @@ func StringSliceValue(v []string) Value {
|
||||
return Value{vtype: STRINGSLICE, slice: attribute.SliceValue(v)}
|
||||
}
|
||||
|
||||
// ByteSliceValue creates a BYTESLICE Value.
|
||||
func ByteSliceValue(v []byte) Value {
|
||||
return Value{
|
||||
vtype: BYTESLICE,
|
||||
stringly: string(v),
|
||||
}
|
||||
}
|
||||
|
||||
// SliceValue creates a SLICE Value.
|
||||
func SliceValue(v ...Value) Value {
|
||||
return Value{vtype: SLICE, slice: sliceValue(v)}
|
||||
}
|
||||
|
||||
// Type returns a type of the Value.
|
||||
func (v Value) Type() Type {
|
||||
return v.vtype
|
||||
@@ -215,6 +237,59 @@ func (v Value) asStringSlice() []string {
|
||||
return attribute.AsSlice[string](v.slice)
|
||||
}
|
||||
|
||||
// AsSlice returns the []Value value. Make sure that the Value's type is
|
||||
// SLICE.
|
||||
func (v Value) AsSlice() []Value {
|
||||
if v.vtype != SLICE {
|
||||
return nil
|
||||
}
|
||||
return v.asSlice()
|
||||
}
|
||||
|
||||
func (v Value) asSlice() []Value {
|
||||
switch vals := v.slice.(type) {
|
||||
case [0]Value:
|
||||
return []Value{}
|
||||
case [1]Value:
|
||||
return []Value{vals[0]}
|
||||
case [2]Value:
|
||||
return []Value{vals[0], vals[1]}
|
||||
case [3]Value:
|
||||
return []Value{vals[0], vals[1], vals[2]}
|
||||
case [4]Value:
|
||||
return []Value{vals[0], vals[1], vals[2], vals[3]}
|
||||
case [5]Value:
|
||||
return []Value{vals[0], vals[1], vals[2], vals[3], vals[4]}
|
||||
default:
|
||||
return asValueSliceReflect(v.slice)
|
||||
}
|
||||
}
|
||||
|
||||
func asValueSliceReflect(v any) []Value {
|
||||
rv := reflect.ValueOf(v)
|
||||
if !rv.IsValid() || rv.Kind() != reflect.Array || rv.Type().Elem() != reflect.TypeFor[Value]() {
|
||||
return nil
|
||||
}
|
||||
cpy := make([]Value, rv.Len())
|
||||
if len(cpy) > 0 {
|
||||
_ = reflect.Copy(reflect.ValueOf(cpy), rv)
|
||||
}
|
||||
return cpy
|
||||
}
|
||||
|
||||
// AsByteSlice returns the bytes value. Make sure that the Value's type
|
||||
// is BYTESLICE.
|
||||
func (v Value) AsByteSlice() []byte {
|
||||
if v.vtype != BYTESLICE {
|
||||
return nil
|
||||
}
|
||||
return v.asByteSlice()
|
||||
}
|
||||
|
||||
func (v Value) asByteSlice() []byte {
|
||||
return []byte(v.stringly)
|
||||
}
|
||||
|
||||
type unknownValueType struct{}
|
||||
|
||||
// AsInterface returns Value's data as any.
|
||||
@@ -236,13 +311,60 @@ func (v Value) AsInterface() any {
|
||||
return v.stringly
|
||||
case STRINGSLICE:
|
||||
return v.asStringSlice()
|
||||
case BYTESLICE:
|
||||
return v.asByteSlice()
|
||||
case SLICE:
|
||||
return v.asSlice()
|
||||
case EMPTY:
|
||||
return nil
|
||||
}
|
||||
return unknownValueType{}
|
||||
}
|
||||
|
||||
// String returns a string representation of Value using the
|
||||
// [OpenTelemetry AnyValue representation for non-OTLP protocols] rules.
|
||||
//
|
||||
// Strings are returned as-is without JSON quoting, booleans and integers use
|
||||
// JSON literals, floating-point values use JSON numbers except that NaN and
|
||||
// ±Inf are rendered as NaN, Infinity, and -Infinity, byte slices are
|
||||
// base64-encoded, empty values are the empty string, and slices are encoded as
|
||||
// JSON arrays. String, byte, and special floating-point values inside arrays
|
||||
// are encoded as JSON strings, and empty values inside arrays are encoded as
|
||||
// null.
|
||||
//
|
||||
// [OpenTelemetry AnyValue representation for non-OTLP protocols]: https://opentelemetry.io/docs/specs/otel/common/#anyvalue-representation-for-non-otlp-protocols
|
||||
func (v Value) String() string {
|
||||
switch v.Type() {
|
||||
case BOOL:
|
||||
return strconv.FormatBool(v.AsBool())
|
||||
case BOOLSLICE:
|
||||
return formatBoolSliceValue(v.slice)
|
||||
case INT64:
|
||||
return strconv.FormatInt(v.AsInt64(), 10)
|
||||
case INT64SLICE:
|
||||
return formatInt64SliceValue(v.slice)
|
||||
case FLOAT64:
|
||||
return formatFloat64(v.AsFloat64())
|
||||
case FLOAT64SLICE:
|
||||
return formatFloat64SliceValue(v.slice)
|
||||
case STRING:
|
||||
return v.stringly
|
||||
case STRINGSLICE:
|
||||
return formatStringSliceValue(v.slice)
|
||||
case BYTESLICE:
|
||||
return formatByteSlice(v.stringly)
|
||||
case SLICE:
|
||||
return formatValueSliceValue(v.slice)
|
||||
case EMPTY:
|
||||
return ""
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Emit returns a string representation of Value's data.
|
||||
//
|
||||
// Deprecated: Use [Value.String] instead.
|
||||
func (v Value) Emit() string {
|
||||
switch v.Type() {
|
||||
case BOOLSLICE:
|
||||
@@ -273,6 +395,10 @@ func (v Value) Emit() string {
|
||||
return string(j)
|
||||
case STRING:
|
||||
return v.stringly
|
||||
case BYTESLICE:
|
||||
return formatByteSlice(v.stringly)
|
||||
case SLICE:
|
||||
return formatValueSliceValue(v.slice)
|
||||
case EMPTY:
|
||||
return ""
|
||||
default:
|
||||
@@ -280,6 +406,622 @@ func (v Value) Emit() string {
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
jsonArrayBracketsLen = len("[]")
|
||||
boolArrayElemMaxLen = len("false")
|
||||
int64ArrayElemMaxLen = len("-9223372036854775808")
|
||||
float64ArrayElemMaxLen = len("-1.7976931348623157e+308")
|
||||
commaLen = len(",")
|
||||
)
|
||||
|
||||
func sliceValue(v []Value) any {
|
||||
switch len(v) {
|
||||
case 0:
|
||||
return [0]Value{}
|
||||
case 1:
|
||||
return [1]Value{v[0]}
|
||||
case 2:
|
||||
return [2]Value{v[0], v[1]}
|
||||
case 3:
|
||||
return [3]Value{v[0], v[1], v[2]}
|
||||
case 4:
|
||||
return [4]Value{v[0], v[1], v[2], v[3]}
|
||||
case 5:
|
||||
return [5]Value{v[0], v[1], v[2], v[3], v[4]}
|
||||
default:
|
||||
return sliceValueReflect(v)
|
||||
}
|
||||
}
|
||||
|
||||
func sliceValueReflect(v []Value) any {
|
||||
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[Value]())).Elem()
|
||||
reflect.Copy(cp, reflect.ValueOf(v))
|
||||
return cp.Interface()
|
||||
}
|
||||
|
||||
func formatBoolSliceValue(v any) string {
|
||||
switch vals := v.(type) {
|
||||
case [0]bool:
|
||||
return "[]"
|
||||
case [1]bool:
|
||||
return formatBoolSlice(vals[:])
|
||||
case [2]bool:
|
||||
return formatBoolSlice(vals[:])
|
||||
case [3]bool:
|
||||
return formatBoolSlice(vals[:])
|
||||
default:
|
||||
return formatBoolSliceReflect(v)
|
||||
}
|
||||
}
|
||||
|
||||
func formatBoolSlice(vals []bool) string {
|
||||
var b strings.Builder
|
||||
appendBoolSlice(&b, vals)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatBoolSliceReflect(v any) string {
|
||||
var b strings.Builder
|
||||
appendBoolSliceReflect(&b, reflect.ValueOf(v))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func appendBoolSliceValue(dst *strings.Builder, v any) {
|
||||
switch vals := v.(type) {
|
||||
case [0]bool:
|
||||
_, _ = dst.WriteString("[]")
|
||||
case [1]bool:
|
||||
appendBoolSlice(dst, vals[:])
|
||||
case [2]bool:
|
||||
appendBoolSlice(dst, vals[:])
|
||||
case [3]bool:
|
||||
appendBoolSlice(dst, vals[:])
|
||||
default:
|
||||
appendBoolSliceReflect(dst, reflect.ValueOf(v))
|
||||
}
|
||||
}
|
||||
|
||||
func appendBoolSlice(dst *strings.Builder, vals []bool) {
|
||||
dst.Grow(jsonArrayBracketsLen + len(vals)*(boolArrayElemMaxLen+commaLen))
|
||||
_ = dst.WriteByte('[')
|
||||
for i, val := range vals {
|
||||
if i > 0 {
|
||||
_ = dst.WriteByte(',')
|
||||
}
|
||||
if val {
|
||||
_, _ = dst.WriteString("true")
|
||||
} else {
|
||||
_, _ = dst.WriteString("false")
|
||||
}
|
||||
}
|
||||
_ = dst.WriteByte(']')
|
||||
}
|
||||
|
||||
func appendBoolSliceReflect(dst *strings.Builder, rv reflect.Value) {
|
||||
dst.Grow(jsonArrayBracketsLen + rv.Len()*(boolArrayElemMaxLen+commaLen))
|
||||
_ = dst.WriteByte('[')
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
if i > 0 {
|
||||
_ = dst.WriteByte(',')
|
||||
}
|
||||
if rv.Index(i).Bool() {
|
||||
_, _ = dst.WriteString("true")
|
||||
} else {
|
||||
_, _ = dst.WriteString("false")
|
||||
}
|
||||
}
|
||||
_ = dst.WriteByte(']')
|
||||
}
|
||||
|
||||
func formatInt64SliceValue(v any) string {
|
||||
switch vals := v.(type) {
|
||||
case [0]int64:
|
||||
return "[]"
|
||||
case [1]int64:
|
||||
return formatInt64Slice(vals[:])
|
||||
case [2]int64:
|
||||
return formatInt64Slice(vals[:])
|
||||
case [3]int64:
|
||||
return formatInt64Slice(vals[:])
|
||||
default:
|
||||
return formatInt64SliceReflect(v)
|
||||
}
|
||||
}
|
||||
|
||||
func formatInt64Slice(vals []int64) string {
|
||||
var b strings.Builder
|
||||
appendInt64Slice(&b, vals)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatInt64SliceReflect(v any) string {
|
||||
var b strings.Builder
|
||||
appendInt64SliceReflect(&b, reflect.ValueOf(v))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func appendInt64SliceValue(dst *strings.Builder, v any) {
|
||||
switch vals := v.(type) {
|
||||
case [0]int64:
|
||||
_, _ = dst.WriteString("[]")
|
||||
case [1]int64:
|
||||
appendInt64Slice(dst, vals[:])
|
||||
case [2]int64:
|
||||
appendInt64Slice(dst, vals[:])
|
||||
case [3]int64:
|
||||
appendInt64Slice(dst, vals[:])
|
||||
default:
|
||||
appendInt64SliceReflect(dst, reflect.ValueOf(v))
|
||||
}
|
||||
}
|
||||
|
||||
func appendInt64Slice(dst *strings.Builder, vals []int64) {
|
||||
dst.Grow(jsonArrayBracketsLen + len(vals)*(int64ArrayElemMaxLen+commaLen))
|
||||
_ = dst.WriteByte('[')
|
||||
|
||||
var buf [int64ArrayElemMaxLen]byte
|
||||
for i, val := range vals {
|
||||
if i > 0 {
|
||||
_ = dst.WriteByte(',')
|
||||
}
|
||||
out := strconv.AppendInt(buf[:0], val, 10)
|
||||
_, _ = dst.Write(out)
|
||||
}
|
||||
|
||||
_ = dst.WriteByte(']')
|
||||
}
|
||||
|
||||
func appendInt64SliceReflect(dst *strings.Builder, rv reflect.Value) {
|
||||
dst.Grow(jsonArrayBracketsLen + rv.Len()*(int64ArrayElemMaxLen+commaLen))
|
||||
_ = dst.WriteByte('[')
|
||||
|
||||
var scratch [int64ArrayElemMaxLen]byte
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
if i > 0 {
|
||||
_ = dst.WriteByte(',')
|
||||
}
|
||||
out := strconv.AppendInt(scratch[:0], rv.Index(i).Int(), 10)
|
||||
_, _ = dst.Write(out)
|
||||
}
|
||||
|
||||
_ = dst.WriteByte(']')
|
||||
}
|
||||
|
||||
func formatFloat64(v float64) string {
|
||||
switch {
|
||||
case math.IsNaN(v):
|
||||
return "NaN"
|
||||
case math.IsInf(v, 1):
|
||||
return "Infinity"
|
||||
case math.IsInf(v, -1):
|
||||
return "-Infinity"
|
||||
default:
|
||||
return strconv.FormatFloat(v, 'g', -1, 64)
|
||||
}
|
||||
}
|
||||
|
||||
func formatFloat64SliceValue(v any) string {
|
||||
switch vals := v.(type) {
|
||||
case [0]float64:
|
||||
return "[]"
|
||||
case [1]float64:
|
||||
return formatFloat64Slice(vals[:])
|
||||
case [2]float64:
|
||||
return formatFloat64Slice(vals[:])
|
||||
case [3]float64:
|
||||
return formatFloat64Slice(vals[:])
|
||||
default:
|
||||
return formatFloat64SliceReflect(v)
|
||||
}
|
||||
}
|
||||
|
||||
func formatFloat64Slice(vals []float64) string {
|
||||
var b strings.Builder
|
||||
appendFloat64Slice(&b, vals)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatFloat64SliceReflect(v any) string {
|
||||
var b strings.Builder
|
||||
appendFloat64SliceReflect(&b, reflect.ValueOf(v))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func appendFloat64SliceValue(dst *strings.Builder, v any) {
|
||||
switch vals := v.(type) {
|
||||
case [0]float64:
|
||||
_, _ = dst.WriteString("[]")
|
||||
case [1]float64:
|
||||
appendFloat64Slice(dst, vals[:])
|
||||
case [2]float64:
|
||||
appendFloat64Slice(dst, vals[:])
|
||||
case [3]float64:
|
||||
appendFloat64Slice(dst, vals[:])
|
||||
default:
|
||||
appendFloat64SliceReflect(dst, reflect.ValueOf(v))
|
||||
}
|
||||
}
|
||||
|
||||
func appendFloat64Slice(dst *strings.Builder, vals []float64) {
|
||||
dst.Grow(jsonArrayBracketsLen + len(vals)*(float64ArrayElemMaxLen+commaLen))
|
||||
_ = dst.WriteByte('[')
|
||||
|
||||
var buf [float64ArrayElemMaxLen]byte
|
||||
for i, val := range vals {
|
||||
if i > 0 {
|
||||
_ = dst.WriteByte(',')
|
||||
}
|
||||
|
||||
switch {
|
||||
case math.IsNaN(val):
|
||||
_, _ = dst.WriteString(`"NaN"`)
|
||||
case math.IsInf(val, 1):
|
||||
_, _ = dst.WriteString(`"Infinity"`)
|
||||
case math.IsInf(val, -1):
|
||||
_, _ = dst.WriteString(`"-Infinity"`)
|
||||
default:
|
||||
out := strconv.AppendFloat(buf[:0], val, 'g', -1, 64)
|
||||
_, _ = dst.Write(out)
|
||||
}
|
||||
}
|
||||
|
||||
_ = dst.WriteByte(']')
|
||||
}
|
||||
|
||||
func appendFloat64SliceReflect(dst *strings.Builder, rv reflect.Value) {
|
||||
dst.Grow(jsonArrayBracketsLen + rv.Len()*(float64ArrayElemMaxLen+commaLen))
|
||||
_ = dst.WriteByte('[')
|
||||
|
||||
var scratch [float64ArrayElemMaxLen]byte
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
if i > 0 {
|
||||
_ = dst.WriteByte(',')
|
||||
}
|
||||
val := rv.Index(i).Float()
|
||||
switch {
|
||||
case math.IsNaN(val):
|
||||
_, _ = dst.WriteString(`"NaN"`)
|
||||
case math.IsInf(val, 1):
|
||||
_, _ = dst.WriteString(`"Infinity"`)
|
||||
case math.IsInf(val, -1):
|
||||
_, _ = dst.WriteString(`"-Infinity"`)
|
||||
default:
|
||||
out := strconv.AppendFloat(scratch[:0], val, 'g', -1, 64)
|
||||
_, _ = dst.Write(out)
|
||||
}
|
||||
}
|
||||
|
||||
_ = dst.WriteByte(']')
|
||||
}
|
||||
|
||||
func formatStringSliceValue(v any) string {
|
||||
switch vals := v.(type) {
|
||||
case [0]string:
|
||||
return "[]"
|
||||
case [1]string:
|
||||
return formatStringSlice(vals[:])
|
||||
case [2]string:
|
||||
return formatStringSlice(vals[:])
|
||||
case [3]string:
|
||||
return formatStringSlice(vals[:])
|
||||
default:
|
||||
return formatStringSliceReflect(v)
|
||||
}
|
||||
}
|
||||
|
||||
func formatStringSlice(vals []string) string {
|
||||
var b strings.Builder
|
||||
appendStringSlice(&b, vals)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatStringSliceReflect(v any) string {
|
||||
var b strings.Builder
|
||||
appendStringSliceReflect(&b, reflect.ValueOf(v))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func appendStringSliceValue(dst *strings.Builder, v any) {
|
||||
switch vals := v.(type) {
|
||||
case [0]string:
|
||||
_, _ = dst.WriteString("[]")
|
||||
case [1]string:
|
||||
appendStringSlice(dst, vals[:])
|
||||
case [2]string:
|
||||
appendStringSlice(dst, vals[:])
|
||||
case [3]string:
|
||||
appendStringSlice(dst, vals[:])
|
||||
default:
|
||||
appendStringSliceReflect(dst, reflect.ValueOf(v))
|
||||
}
|
||||
}
|
||||
|
||||
func appendStringSlice(dst *strings.Builder, vals []string) {
|
||||
size := jsonArrayBracketsLen
|
||||
for _, val := range vals {
|
||||
size += len(val) + commaLen + 2 // Account for JSON string quotes and comma.
|
||||
}
|
||||
|
||||
dst.Grow(size)
|
||||
_ = dst.WriteByte('[')
|
||||
for i, val := range vals {
|
||||
if i > 0 {
|
||||
_ = dst.WriteByte(',')
|
||||
}
|
||||
appendJSONString(dst, val)
|
||||
}
|
||||
_ = dst.WriteByte(']')
|
||||
}
|
||||
|
||||
func appendStringSliceReflect(dst *strings.Builder, rv reflect.Value) {
|
||||
size := jsonArrayBracketsLen
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
size += len(rv.Index(i).String()) + commaLen + 2 // Account for JSON string quotes and comma.
|
||||
}
|
||||
|
||||
dst.Grow(size)
|
||||
_ = dst.WriteByte('[')
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
if i > 0 {
|
||||
_ = dst.WriteByte(',')
|
||||
}
|
||||
appendJSONString(dst, rv.Index(i).String())
|
||||
}
|
||||
_ = dst.WriteByte(']')
|
||||
}
|
||||
|
||||
func formatByteSlice(v string) string {
|
||||
var b strings.Builder
|
||||
appendBase64(&b, v)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatValueSliceValue(v any) string {
|
||||
switch vals := v.(type) {
|
||||
case [0]Value:
|
||||
return "[]"
|
||||
case [1]Value:
|
||||
return formatValueSlice(vals[:])
|
||||
case [2]Value:
|
||||
return formatValueSlice(vals[:])
|
||||
case [3]Value:
|
||||
return formatValueSlice(vals[:])
|
||||
case [4]Value:
|
||||
return formatValueSlice(vals[:])
|
||||
case [5]Value:
|
||||
return formatValueSlice(vals[:])
|
||||
default:
|
||||
return formatValueSliceReflect(v)
|
||||
}
|
||||
}
|
||||
|
||||
func formatValueSlice(vals []Value) string {
|
||||
var b strings.Builder
|
||||
appendValueSlice(&b, vals)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatValueSliceReflect(v any) string {
|
||||
var b strings.Builder
|
||||
appendValueSliceReflect(&b, reflect.ValueOf(v))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func appendValueSliceValue(dst *strings.Builder, v any) {
|
||||
switch vals := v.(type) {
|
||||
case [0]Value:
|
||||
_, _ = dst.WriteString("[]")
|
||||
case [1]Value:
|
||||
appendValueSlice(dst, vals[:])
|
||||
case [2]Value:
|
||||
appendValueSlice(dst, vals[:])
|
||||
case [3]Value:
|
||||
appendValueSlice(dst, vals[:])
|
||||
case [4]Value:
|
||||
appendValueSlice(dst, vals[:])
|
||||
case [5]Value:
|
||||
appendValueSlice(dst, vals[:])
|
||||
default:
|
||||
appendValueSliceReflect(dst, reflect.ValueOf(v))
|
||||
}
|
||||
}
|
||||
|
||||
func appendValueSlice(dst *strings.Builder, vals []Value) {
|
||||
// Estimate 10 bytes per value for small values and commas.
|
||||
dst.Grow(jsonArrayBracketsLen + len(vals)*commaLen + len(vals)*10)
|
||||
_ = dst.WriteByte('[')
|
||||
for i, val := range vals {
|
||||
if i > 0 {
|
||||
_ = dst.WriteByte(',')
|
||||
}
|
||||
appendJSONValue(dst, val)
|
||||
}
|
||||
_ = dst.WriteByte(']')
|
||||
}
|
||||
|
||||
func appendValueSliceReflect(dst *strings.Builder, rv reflect.Value) {
|
||||
// Estimate 10 bytes per value for small values and commas.
|
||||
dst.Grow(jsonArrayBracketsLen + rv.Len()*commaLen + rv.Len()*10)
|
||||
_ = dst.WriteByte('[')
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
if i > 0 {
|
||||
_ = dst.WriteByte(',')
|
||||
}
|
||||
appendJSONValue(dst, rv.Index(i).Interface().(Value))
|
||||
}
|
||||
_ = dst.WriteByte(']')
|
||||
}
|
||||
|
||||
func appendJSONValue(dst *strings.Builder, v Value) {
|
||||
switch v.Type() {
|
||||
case BOOL:
|
||||
if v.AsBool() {
|
||||
_, _ = dst.WriteString("true")
|
||||
} else {
|
||||
_, _ = dst.WriteString("false")
|
||||
}
|
||||
case BOOLSLICE:
|
||||
appendBoolSliceValue(dst, v.slice)
|
||||
case INT64:
|
||||
var buf [int64ArrayElemMaxLen]byte
|
||||
out := strconv.AppendInt(buf[:0], v.AsInt64(), 10)
|
||||
_, _ = dst.Write(out)
|
||||
case INT64SLICE:
|
||||
appendInt64SliceValue(dst, v.slice)
|
||||
case FLOAT64:
|
||||
val := v.AsFloat64()
|
||||
switch {
|
||||
case math.IsNaN(val):
|
||||
appendJSONString(dst, "NaN")
|
||||
case math.IsInf(val, 1):
|
||||
appendJSONString(dst, "Infinity")
|
||||
case math.IsInf(val, -1):
|
||||
appendJSONString(dst, "-Infinity")
|
||||
default:
|
||||
var buf [float64ArrayElemMaxLen]byte
|
||||
out := strconv.AppendFloat(buf[:0], val, 'g', -1, 64)
|
||||
_, _ = dst.Write(out)
|
||||
}
|
||||
case FLOAT64SLICE:
|
||||
appendFloat64SliceValue(dst, v.slice)
|
||||
case STRING:
|
||||
appendJSONString(dst, v.stringly)
|
||||
case STRINGSLICE:
|
||||
appendStringSliceValue(dst, v.slice)
|
||||
case BYTESLICE:
|
||||
_ = dst.WriteByte('"')
|
||||
appendBase64(dst, v.stringly)
|
||||
_ = dst.WriteByte('"')
|
||||
case SLICE:
|
||||
appendValueSliceValue(dst, v.slice)
|
||||
case EMPTY:
|
||||
_, _ = dst.WriteString("null")
|
||||
default:
|
||||
appendJSONString(dst, "unknown")
|
||||
}
|
||||
}
|
||||
|
||||
// appendJSONString appends s to dst as a JSON string literal.
|
||||
//
|
||||
// This is adapted from the Go standard library's encoding/json
|
||||
// [appendString implementation]. It keeps the same escaping behavior we need
|
||||
// here, but writes directly into a strings.Builder and intentionally does not
|
||||
// apply HTML escaping because the OpenTelemetry non-OTLP AnyValue representation
|
||||
// only requires JSON array string encoding. We inline this instead of using
|
||||
// encoding/json so slice formatting avoids allocations and reflection.
|
||||
//
|
||||
// [appendString implementation]: https://github.com/golang/go/blob/3b5954c6349d31465dca409b45ab6597e0942d9f/src/encoding/json/encode.go#L998-L1064
|
||||
func appendJSONString(dst *strings.Builder, s string) {
|
||||
const hex = "0123456789abcdef" // For escaping bytes to hex.
|
||||
|
||||
_ = dst.WriteByte('"')
|
||||
start := 0
|
||||
|
||||
for i := 0; i < len(s); {
|
||||
if c := s[i]; c < utf8.RuneSelf {
|
||||
if c >= 0x20 && c != '\\' && c != '"' {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if start < i {
|
||||
_, _ = dst.WriteString(s[start:i])
|
||||
}
|
||||
|
||||
switch c {
|
||||
case '\\', '"':
|
||||
_ = dst.WriteByte('\\')
|
||||
_ = dst.WriteByte(c)
|
||||
case '\b':
|
||||
_, _ = dst.WriteString(`\b`)
|
||||
case '\f':
|
||||
_, _ = dst.WriteString(`\f`)
|
||||
case '\n':
|
||||
_, _ = dst.WriteString(`\n`)
|
||||
case '\r':
|
||||
_, _ = dst.WriteString(`\r`)
|
||||
case '\t':
|
||||
_, _ = dst.WriteString(`\t`)
|
||||
default:
|
||||
_, _ = dst.WriteString(`\u00`)
|
||||
_ = dst.WriteByte(hex[c>>4])
|
||||
_ = dst.WriteByte(hex[c&0x0f])
|
||||
}
|
||||
|
||||
i++
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
|
||||
r, size := utf8.DecodeRuneInString(s[i:])
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
if start < i {
|
||||
_, _ = dst.WriteString(s[start:i])
|
||||
}
|
||||
// Match encoding/json by replacing invalid UTF-8 with U+FFFD.
|
||||
_, _ = dst.WriteString(`\ufffd`)
|
||||
i++
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
|
||||
if r == '\u2028' || r == '\u2029' {
|
||||
if start < i {
|
||||
_, _ = dst.WriteString(s[start:i])
|
||||
}
|
||||
// Escape JSONP-sensitive separators unconditionally, like encoding/json.
|
||||
_, _ = dst.WriteString(`\u202`)
|
||||
_ = dst.WriteByte(hex[r&0x0f])
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
|
||||
i += size
|
||||
}
|
||||
|
||||
if start < len(s) {
|
||||
_, _ = dst.WriteString(s[start:])
|
||||
}
|
||||
_ = dst.WriteByte('"')
|
||||
}
|
||||
|
||||
// This is adapted from the Go standard library's encoding/base64
|
||||
// [Encoding.Encode implementation]. It keeps the same encoding behavior we need
|
||||
// here, but writes directly into a strings.Builder. We inline this instead of using
|
||||
// encoding/base64 to avoid allocations.
|
||||
//
|
||||
// [Encoding.Encode implementation]: https://github.com/golang/go/blob/3b5954c6349d31465dca409b45ab6597e0942d9f/src/encoding/base64/base64.go#L139-L189
|
||||
func appendBase64(dst *strings.Builder, s string) {
|
||||
const encode = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
|
||||
dst.Grow(base64.StdEncoding.EncodedLen(len(s)))
|
||||
|
||||
i := 0
|
||||
for ; i+2 < len(s); i += 3 {
|
||||
n := uint32(s[i])<<16 | uint32(s[i+1])<<8 | uint32(s[i+2])
|
||||
_ = dst.WriteByte(encode[n>>18&0x3f])
|
||||
_ = dst.WriteByte(encode[n>>12&0x3f])
|
||||
_ = dst.WriteByte(encode[n>>6&0x3f])
|
||||
_ = dst.WriteByte(encode[n&0x3f])
|
||||
}
|
||||
|
||||
switch len(s) - i {
|
||||
case 1:
|
||||
n := uint32(s[i]) << 16
|
||||
_ = dst.WriteByte(encode[n>>18&0x3f])
|
||||
_ = dst.WriteByte(encode[n>>12&0x3f])
|
||||
_ = dst.WriteByte('=')
|
||||
_ = dst.WriteByte('=')
|
||||
case 2:
|
||||
n := uint32(s[i])<<16 | uint32(s[i+1])<<8
|
||||
_ = dst.WriteByte(encode[n>>18&0x3f])
|
||||
_ = dst.WriteByte(encode[n>>12&0x3f])
|
||||
_ = dst.WriteByte(encode[n>>6&0x3f])
|
||||
_ = dst.WriteByte('=')
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of the Value.
|
||||
func (v Value) MarshalJSON() ([]byte, error) {
|
||||
var jsonVal struct {
|
||||
|
||||
+26
-4
@@ -14,6 +14,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
maxParseErrors = 5
|
||||
|
||||
// W3C Baggage specification limits.
|
||||
// https://www.w3.org/TR/baggage/#limits
|
||||
maxMembers = 64
|
||||
maxBytesPerBaggageString = 8192
|
||||
|
||||
@@ -493,9 +497,15 @@ func New(members ...Member) (Baggage, error) {
|
||||
// from the W3C Baggage specification which allows duplicate list-members, but
|
||||
// conforms to the OpenTelemetry Baggage specification.
|
||||
//
|
||||
// If the baggage-string exceeds the maximum allowed members (64) or bytes
|
||||
// (8192), members are dropped until the limits are satisfied and an error is
|
||||
// returned along with the partial result.
|
||||
// If the raw baggage-string exceeds the maximum allowed bytes (8192), an
|
||||
// empty Baggage and an error are returned.
|
||||
//
|
||||
// Otherwise, members are parsed left-to-right and accumulated until one of
|
||||
// the following conditions is reached, at which point parsing stops and an
|
||||
// error is returned alongside the partial result:
|
||||
// - accepting the next member would cause the encoded baggage to exceed
|
||||
// 8192 bytes, or
|
||||
// - the baggage already contains 64 distinct keys.
|
||||
//
|
||||
// Invalid members are skipped and the error is returned along with the
|
||||
// partial result containing the valid members.
|
||||
@@ -504,9 +514,14 @@ func Parse(bStr string) (Baggage, error) {
|
||||
return Baggage{}, nil
|
||||
}
|
||||
|
||||
if n := len(bStr); n > maxBytesPerBaggageString {
|
||||
return Baggage{}, fmt.Errorf("%w: %d", errBaggageBytes, n)
|
||||
}
|
||||
|
||||
b := make(baggage.List)
|
||||
sizes := make(map[string]int) // Track per-key byte sizes
|
||||
var totalBytes int
|
||||
var parseErrors int
|
||||
var truncateErr error
|
||||
for memberStr := range strings.SplitSeq(bStr, listDelimiter) {
|
||||
// Check member count limit.
|
||||
@@ -517,7 +532,10 @@ func Parse(bStr string) (Baggage, error) {
|
||||
|
||||
m, err := parseMember(memberStr)
|
||||
if err != nil {
|
||||
truncateErr = errors.Join(truncateErr, err)
|
||||
parseErrors++
|
||||
if parseErrors <= maxParseErrors {
|
||||
truncateErr = errors.Join(truncateErr, err)
|
||||
}
|
||||
continue // skip invalid member, keep processing
|
||||
}
|
||||
|
||||
@@ -553,6 +571,10 @@ func Parse(bStr string) (Baggage, error) {
|
||||
totalBytes = newTotalBytes
|
||||
}
|
||||
|
||||
if dropped := parseErrors - maxParseErrors; dropped > 0 {
|
||||
truncateErr = errors.Join(truncateErr, fmt.Errorf("and %d more invalid member(s)", dropped))
|
||||
}
|
||||
|
||||
if len(b) == 0 {
|
||||
return Baggage{}, truncateErr
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# This is a renovate-friendly source of Docker images.
|
||||
FROM python:3.13.6-slim-bullseye@sha256:e98b521460ee75bca92175c16247bdf7275637a8faaeb2bcfa19d879ae5c4b9a AS python
|
||||
FROM otel/weaver:v0.22.1@sha256:33ae522ae4b71c1c562563c1d81f46aa0f79f088a0873199143a1f11ac30e5c9 AS weaver
|
||||
FROM otel/weaver:v0.23.0@sha256:7984ecb55b859eb3034ae9d836c4eeda137e2bdd0873b7ba2bb6c3d24d6ff457 AS weaver
|
||||
FROM avtodev/markdown-lint:v1@sha256:6aeedc2f49138ce7a1cd0adffc1b1c0321b841dc2102408967d9301c031949ee AS markdown
|
||||
|
||||
Generated
Vendored
+18
-9
@@ -6,6 +6,7 @@ package otlpmetricgrpc // import "go.opentelemetry.io/otel/exporters/otlp/otlpme
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
colmetricpb "go.opentelemetry.io/proto/otlp/collector/metrics/v1"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf"
|
||||
@@ -22,9 +24,10 @@ import (
|
||||
)
|
||||
|
||||
type client struct {
|
||||
metadata metadata.MD
|
||||
exportTimeout time.Duration
|
||||
requestFunc retry.RequestFunc
|
||||
metadata metadata.MD
|
||||
exportTimeout time.Duration
|
||||
maxRequestSize int
|
||||
requestFunc retry.RequestFunc
|
||||
|
||||
// ourConn keeps track of where conn was created: true if created here in
|
||||
// NewClient, or false if passed with an option. This is important on
|
||||
@@ -38,9 +41,10 @@ type client struct {
|
||||
// newClient creates a new gRPC metric client.
|
||||
func newClient(_ context.Context, cfg oconf.Config) (*client, error) {
|
||||
c := &client{
|
||||
exportTimeout: cfg.Metrics.Timeout,
|
||||
requestFunc: cfg.RetryConfig.RequestFunc(retryable),
|
||||
conn: cfg.GRPCConn,
|
||||
exportTimeout: cfg.Metrics.Timeout,
|
||||
maxRequestSize: cfg.Metrics.MaxRequestSize,
|
||||
requestFunc: cfg.RetryConfig.RequestFunc(retryable),
|
||||
conn: cfg.GRPCConn,
|
||||
}
|
||||
|
||||
if len(cfg.Metrics.Headers) > 0 {
|
||||
@@ -115,10 +119,15 @@ func (c *client) UploadMetrics(ctx context.Context, protoMetrics *metricpb.Resou
|
||||
ctx, cancel := c.exportContext(ctx)
|
||||
defer cancel()
|
||||
|
||||
pbRequest := &colmetricpb.ExportMetricsServiceRequest{
|
||||
ResourceMetrics: []*metricpb.ResourceMetrics{protoMetrics},
|
||||
}
|
||||
if maxSize := c.maxRequestSize; maxSize > 0 && proto.Size(pbRequest) > maxSize {
|
||||
return fmt.Errorf("request message too large: exceeded %d bytes", maxSize)
|
||||
}
|
||||
|
||||
return errors.Join(uploadErr, c.requestFunc(ctx, func(iCtx context.Context) error {
|
||||
resp, err := c.msc.Export(iCtx, &colmetricpb.ExportMetricsServiceRequest{
|
||||
ResourceMetrics: []*metricpb.ResourceMetrics{protoMetrics},
|
||||
})
|
||||
resp, err := c.msc.Export(iCtx, pbRequest)
|
||||
if resp != nil && resp.PartialSuccess != nil {
|
||||
msg := resp.PartialSuccess.GetErrorMessage()
|
||||
n := resp.PartialSuccess.GetRejectedDataPoints()
|
||||
|
||||
Generated
Vendored
+10
@@ -231,6 +231,16 @@ func WithTimeout(duration time.Duration) Option {
|
||||
return wrappedOption{oconf.WithTimeout(duration)}
|
||||
}
|
||||
|
||||
// WithMaxRequestSize sets the maximum size, in bytes, of a serialized export
|
||||
// request, before compression, that the exporter will send.
|
||||
//
|
||||
// If size is less than or equal to zero, no request-size limit is applied.
|
||||
// Disabling the limit is not recommended because it can lead to excessive
|
||||
// resource consumption or abuse.
|
||||
func WithMaxRequestSize(size int) Option {
|
||||
return wrappedOption{oconf.WithMaxRequestSize(size)}
|
||||
}
|
||||
|
||||
// WithRetry sets the retry policy for transient retryable errors that are
|
||||
// returned by the target endpoint.
|
||||
//
|
||||
|
||||
+3
@@ -77,6 +77,9 @@ default aggregation to use for histogram instruments. Supported values:
|
||||
|
||||
The configuration can be overridden by [WithAggregationSelector] option.
|
||||
|
||||
See [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/x] for information about
|
||||
the experimental features.
|
||||
|
||||
[W3C Baggage HTTP Header Content Format]: https://www.w3.org/TR/baggage/#header-content
|
||||
[Explicit Bucket Histogram Aggregation]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.26.0/specification/metrics/sdk.md#explicit-bucket-histogram-aggregation
|
||||
[Base2 Exponential Bucket Histogram Aggregation]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.26.0/specification/metrics/sdk.md#base2-exponential-bucket-histogram-aggregation
|
||||
|
||||
Generated
Vendored
+31
-2
@@ -11,8 +11,11 @@ import (
|
||||
|
||||
metricpb "go.opentelemetry.io/proto/otlp/metrics/v1"
|
||||
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/counter"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/observ"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/transform"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/x"
|
||||
"go.opentelemetry.io/otel/internal/global"
|
||||
"go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
||||
@@ -31,6 +34,9 @@ type Exporter struct {
|
||||
aggregationSelector metric.AggregationSelector
|
||||
|
||||
shutdownOnce sync.Once
|
||||
|
||||
// Self-observability metrics
|
||||
inst *observ.Instrumentation
|
||||
}
|
||||
|
||||
func newExporter(c *client, cfg oconf.Config) (*Exporter, error) {
|
||||
@@ -46,12 +52,27 @@ func newExporter(c *client, cfg oconf.Config) (*Exporter, error) {
|
||||
as = metric.DefaultAggregationSelector
|
||||
}
|
||||
|
||||
var inst *observ.Instrumentation
|
||||
var initErr error
|
||||
if x.Observability.Enabled() {
|
||||
var err error
|
||||
inst, err = observ.NewInstrumentation(
|
||||
counter.NextExporterID(),
|
||||
c.conn.CanonicalTarget(),
|
||||
)
|
||||
if err != nil {
|
||||
initErr = err
|
||||
}
|
||||
}
|
||||
|
||||
return &Exporter{
|
||||
client: c,
|
||||
|
||||
temporalitySelector: ts,
|
||||
aggregationSelector: as,
|
||||
}, nil
|
||||
|
||||
inst: inst,
|
||||
}, initErr
|
||||
}
|
||||
|
||||
// Temporality returns the Temporality to use for an instrument kind.
|
||||
@@ -72,10 +93,18 @@ func (e *Exporter) Export(ctx context.Context, rm *metricdata.ResourceMetrics) e
|
||||
defer global.Debug("OTLP/gRPC exporter export", "Data", rm)
|
||||
|
||||
otlpRm, err := transform.ResourceMetrics(rm)
|
||||
|
||||
// Track export operation for self-observability
|
||||
op := e.inst.TrackExport(ctx, otlpRm)
|
||||
|
||||
var upErr error
|
||||
defer func() { op.End(upErr) }()
|
||||
|
||||
// Best effort upload of transformable metrics.
|
||||
e.clientMu.Lock()
|
||||
upErr := e.client.UploadMetrics(ctx, otlpRm)
|
||||
upErr = e.client.UploadMetrics(ctx, otlpRm)
|
||||
e.clientMu.Unlock()
|
||||
|
||||
if upErr != nil {
|
||||
if err == nil {
|
||||
return fmt.Errorf("failed to upload metrics: %w", upErr)
|
||||
|
||||
vendor/go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/counter/counter.go
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
// Code generated by gotmpl. DO NOT MODIFY.
|
||||
// source: internal/shared/counter/counter.go.tmpl
|
||||
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package counter provides a simple counter for generating unique IDs.
|
||||
//
|
||||
// This package is used to generate unique IDs while allowing testing packages
|
||||
// to reset the counter.
|
||||
package counter // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/counter"
|
||||
|
||||
import "sync/atomic"
|
||||
|
||||
// exporterN is a global 0-based count of the number of exporters created.
|
||||
var exporterN atomic.Int64
|
||||
|
||||
// NextExporterID returns the next unique ID for an exporter.
|
||||
func NextExporterID() int64 {
|
||||
const inc = 1
|
||||
return exporterN.Add(inc) - inc
|
||||
}
|
||||
|
||||
// SetExporterID sets the exporter ID counter to v and returns the previous
|
||||
// value.
|
||||
//
|
||||
// This function is useful for testing purposes, allowing you to reset the
|
||||
// counter. It should not be used in production code.
|
||||
func SetExporterID(v int64) int64 {
|
||||
return exporterN.Swap(v)
|
||||
}
|
||||
Generated
Vendored
+9
@@ -7,6 +7,9 @@ package internal // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/o
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/partialsuccess.go.tmpl "--data={}" --out=partialsuccess.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/partialsuccess_test.go.tmpl "--data={}" --out=partialsuccess_test.go
|
||||
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/observ/target.go.tmpl "--data={ \"pkg\": \"observ\", \"pkg_path\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/observ\" }" --out=observ/target.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/observ/target_test.go.tmpl "--data={ \"pkg\": \"observ\" }" --out=observ/target_test.go
|
||||
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/retry/retry.go.tmpl "--data={}" --out=retry/retry.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/retry/retry_test.go.tmpl "--data={}" --out=retry/retry_test.go
|
||||
|
||||
@@ -30,3 +33,9 @@ package internal // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/o
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/error_test.go.tmpl "--data={}" --out=transform/error_test.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/metricdata.go.tmpl "--data={}" --out=transform/metricdata.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/metricdata_test.go.tmpl "--data={}" --out=transform/metricdata_test.go
|
||||
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/counter/counter.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/counter\" }" --out=counter/counter.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/counter/counter_test.go.tmpl "--data={}" --out=counter/counter_test.go
|
||||
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/x/x.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc\" }" --out=x/x.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/x/x_test.go.tmpl "--data={}" --out=x/x_test.go
|
||||
|
||||
Generated
Vendored
+343
@@ -0,0 +1,343 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package observ provides self-observability metrics for OTLP metric exporters.
|
||||
// This is an experimental feature controlled by the x.Observability feature flag.
|
||||
package observ // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/observ"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/internal/global"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/sdk"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.41.0/otelconv"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
metricpb "go.opentelemetry.io/proto/otlp/metrics/v1"
|
||||
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/x"
|
||||
)
|
||||
|
||||
var (
|
||||
attrPool = sync.Pool{
|
||||
New: func() any {
|
||||
// Pre-allocate for common attributes and dynamic error attributes.
|
||||
const n = 1 /* otel.component.type */ +
|
||||
1 /* otel.component.name */ +
|
||||
1 /* server.address */ +
|
||||
1 /* server.port */ +
|
||||
1 /* error.type */ +
|
||||
1 /* rpc.grpc.status_code */
|
||||
s := make([]attribute.KeyValue, 0, n)
|
||||
return &s
|
||||
},
|
||||
}
|
||||
|
||||
recOptPool = sync.Pool{
|
||||
New: func() any {
|
||||
o := make([]metric.RecordOption, 0, 1)
|
||||
return &o
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Instrumentation holds the self-observability metric instruments for an OTLP metric exporter.
|
||||
type Instrumentation struct {
|
||||
exported otelconv.SDKExporterMetricDataPointExported
|
||||
inflight otelconv.SDKExporterMetricDataPointInflight
|
||||
duration otelconv.SDKExporterOperationDuration
|
||||
attrs []attribute.KeyValue
|
||||
addOpt metric.AddOption
|
||||
recOpt metric.RecordOption
|
||||
}
|
||||
|
||||
// NewInstrumentation returns instrumentation for an OTLP over gRPC metric
|
||||
// exporter with the provided ID using the global MeterProvider.
|
||||
//
|
||||
// The id should be the unique exporter instance ID. It is used
|
||||
// to set the "component.name" attribute.
|
||||
//
|
||||
// The target is the endpoint the exporter is exporting to.
|
||||
//
|
||||
// If the experimental observability is disabled, nil is returned.
|
||||
func NewInstrumentation(id int64, target string) (*Instrumentation, error) {
|
||||
if !x.Observability.Enabled() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
em := &Instrumentation{}
|
||||
|
||||
meter := otel.GetMeterProvider().Meter(
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc",
|
||||
metric.WithInstrumentationVersion(sdk.Version()),
|
||||
metric.WithSchemaURL(semconv.SchemaURL),
|
||||
)
|
||||
|
||||
var err error
|
||||
var instrumentErr error
|
||||
|
||||
em.exported, instrumentErr = otelconv.NewSDKExporterMetricDataPointExported(meter)
|
||||
if instrumentErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("failed to create exported metric: %w", instrumentErr))
|
||||
}
|
||||
|
||||
em.inflight, instrumentErr = otelconv.NewSDKExporterMetricDataPointInflight(meter)
|
||||
if instrumentErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("failed to create inflight metric: %w", instrumentErr))
|
||||
}
|
||||
|
||||
em.duration, instrumentErr = otelconv.NewSDKExporterOperationDuration(meter)
|
||||
if instrumentErr != nil {
|
||||
err = errors.Join(err, fmt.Errorf("failed to create duration metric: %w", instrumentErr))
|
||||
}
|
||||
|
||||
em.attrs = BaseAttrs(id, target)
|
||||
|
||||
attrSet := attribute.NewSet(em.attrs...)
|
||||
em.addOpt = metric.WithAttributeSet(attrSet)
|
||||
em.recOpt = metric.WithAttributeSet(attribute.NewSet(append(
|
||||
[]attribute.KeyValue{semconv.RPCResponseStatusCode(codes.OK.String())},
|
||||
em.attrs...,
|
||||
)...))
|
||||
|
||||
return em, err
|
||||
}
|
||||
|
||||
// ComponentName returns the component name for the exporter with the
|
||||
// provided ID.
|
||||
func ComponentName(id int64) string {
|
||||
t := string(otelconv.ComponentTypeOtlpGRPCMetricExporter)
|
||||
return fmt.Sprintf("%s/%d", t, id)
|
||||
}
|
||||
|
||||
// BaseAttrs returns the base attributes for the exporter with the provided ID
|
||||
// and target.
|
||||
//
|
||||
// The id should be the unique exporter instance ID. It is used
|
||||
// to set the "component.name" attribute.
|
||||
//
|
||||
// The target is the gRPC target the exporter is exporting to. It is expected
|
||||
// to be the output of the Client's CanonicalTarget method.
|
||||
func BaseAttrs(id int64, target string) []attribute.KeyValue {
|
||||
host, port, err := ParseCanonicalTarget(target)
|
||||
if err != nil || (host == "" && port < 0) {
|
||||
if err != nil {
|
||||
global.Debug("failed to parse target", "target", target, "error", err)
|
||||
}
|
||||
return []attribute.KeyValue{
|
||||
semconv.OTelComponentName(ComponentName(id)),
|
||||
semconv.OTelComponentTypeKey.String(string(otelconv.ComponentTypeOtlpGRPCMetricExporter)),
|
||||
}
|
||||
}
|
||||
|
||||
// Do not use append so the slice is exactly allocated.
|
||||
|
||||
if port < 0 {
|
||||
return []attribute.KeyValue{
|
||||
semconv.OTelComponentName(ComponentName(id)),
|
||||
semconv.OTelComponentTypeKey.String(string(otelconv.ComponentTypeOtlpGRPCMetricExporter)),
|
||||
semconv.ServerAddress(host),
|
||||
}
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
return []attribute.KeyValue{
|
||||
semconv.OTelComponentName(ComponentName(id)),
|
||||
semconv.OTelComponentTypeKey.String(string(otelconv.ComponentTypeOtlpGRPCMetricExporter)),
|
||||
semconv.ServerPort(port),
|
||||
}
|
||||
}
|
||||
|
||||
return []attribute.KeyValue{
|
||||
semconv.OTelComponentName(ComponentName(id)),
|
||||
semconv.OTelComponentTypeKey.String(string(otelconv.ComponentTypeOtlpGRPCMetricExporter)),
|
||||
semconv.ServerAddress(host),
|
||||
semconv.ServerPort(port),
|
||||
}
|
||||
}
|
||||
|
||||
// TrackExport tracks an export operation and returns an ExportOp to complete the tracking.
|
||||
func (em *Instrumentation) TrackExport(ctx context.Context, rm *metricpb.ResourceMetrics) ExportOp {
|
||||
if em == nil {
|
||||
return ExportOp{}
|
||||
}
|
||||
start := time.Now()
|
||||
|
||||
var dataPointCount int64
|
||||
inflightEnabled := em.inflight.Enabled(ctx)
|
||||
exportedEnabled := em.exported.Enabled(ctx)
|
||||
|
||||
if inflightEnabled || exportedEnabled {
|
||||
dataPointCount = countProtoDataPoints(rm)
|
||||
}
|
||||
|
||||
if inflightEnabled {
|
||||
em.inflight.Inst().Add(ctx, dataPointCount, em.addOpt)
|
||||
}
|
||||
|
||||
return ExportOp{
|
||||
ctx: ctx,
|
||||
start: start,
|
||||
dataPointCount: dataPointCount,
|
||||
inst: em,
|
||||
}
|
||||
}
|
||||
|
||||
// ExportOp tracks the operation being observed by [Instrumentation.TrackExport].
|
||||
type ExportOp struct {
|
||||
ctx context.Context
|
||||
start time.Time
|
||||
dataPointCount int64
|
||||
|
||||
inst *Instrumentation
|
||||
}
|
||||
|
||||
// End completes the observation of the operation being observed by a call to
|
||||
// [Instrumentation.TrackExport].
|
||||
//
|
||||
// Any error that is encountered is provided as err.
|
||||
func (e ExportOp) End(err error) {
|
||||
if e.inst == nil {
|
||||
return
|
||||
}
|
||||
if e.inst.inflight.Enabled(e.ctx) {
|
||||
e.inst.inflight.Inst().Add(e.ctx, -e.dataPointCount, e.inst.addOpt)
|
||||
}
|
||||
|
||||
success := successful(e.dataPointCount, err)
|
||||
// Record successfully exported data points, even if the value is 0 which are
|
||||
// meaningful to distribution aggregations.
|
||||
if e.inst.exported.Enabled(e.ctx) {
|
||||
e.inst.exported.Inst().Add(e.ctx, success, e.inst.addOpt)
|
||||
}
|
||||
|
||||
if err != nil && e.inst.exported.Enabled(e.ctx) {
|
||||
attrsPtr := attrPool.Get().(*[]attribute.KeyValue)
|
||||
defer func() {
|
||||
*attrsPtr = (*attrsPtr)[:0]
|
||||
attrPool.Put(attrsPtr)
|
||||
}()
|
||||
|
||||
*attrsPtr = append(*attrsPtr, e.inst.attrs...)
|
||||
*attrsPtr = append(*attrsPtr, semconv.ErrorType(err))
|
||||
|
||||
set := attribute.NewSet(*attrsPtr...)
|
||||
e.inst.exported.Inst().Add(e.ctx, e.dataPointCount-success, metric.WithAttributeSet(set))
|
||||
}
|
||||
|
||||
if e.inst.duration.Enabled(e.ctx) {
|
||||
d := time.Since(e.start).Seconds()
|
||||
if err != nil {
|
||||
recOptPtr := recOptPool.Get().(*[]metric.RecordOption)
|
||||
defer func() {
|
||||
*recOptPtr = (*recOptPtr)[:0]
|
||||
recOptPool.Put(recOptPtr)
|
||||
}()
|
||||
|
||||
attrsPtr := attrPool.Get().(*[]attribute.KeyValue)
|
||||
defer func() {
|
||||
*attrsPtr = (*attrsPtr)[:0]
|
||||
attrPool.Put(attrsPtr)
|
||||
}()
|
||||
|
||||
*attrsPtr = append(*attrsPtr, e.inst.attrs...)
|
||||
*attrsPtr = append(
|
||||
*attrsPtr,
|
||||
semconv.ErrorType(err),
|
||||
semconv.RPCResponseStatusCode(status.Code(err).String()),
|
||||
)
|
||||
|
||||
set := attribute.NewSet(*attrsPtr...)
|
||||
*recOptPtr = append(*recOptPtr, metric.WithAttributeSet(set))
|
||||
|
||||
e.inst.duration.Inst().Record(e.ctx, d, *recOptPtr...)
|
||||
} else {
|
||||
e.inst.duration.Inst().Record(e.ctx, d, e.inst.recOpt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// countProtoDataPoints counts the total number of data points in a ResourceMetrics.
|
||||
func countProtoDataPoints(rm *metricpb.ResourceMetrics) int64 {
|
||||
if rm == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
var total int64
|
||||
for _, sm := range rm.ScopeMetrics {
|
||||
for _, m := range sm.Metrics {
|
||||
switch data := m.Data.(type) {
|
||||
case *metricpb.Metric_Gauge:
|
||||
if data.Gauge != nil {
|
||||
total += int64(len(data.Gauge.DataPoints))
|
||||
}
|
||||
case *metricpb.Metric_Sum:
|
||||
if data.Sum != nil {
|
||||
total += int64(len(data.Sum.DataPoints))
|
||||
}
|
||||
case *metricpb.Metric_Histogram:
|
||||
if data.Histogram != nil {
|
||||
total += int64(len(data.Histogram.DataPoints))
|
||||
}
|
||||
case *metricpb.Metric_ExponentialHistogram:
|
||||
if data.ExponentialHistogram != nil {
|
||||
total += int64(len(data.ExponentialHistogram.DataPoints))
|
||||
}
|
||||
case *metricpb.Metric_Summary:
|
||||
if data.Summary != nil {
|
||||
total += int64(len(data.Summary.DataPoints))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// successful returns the number of successfully exported data points out of the n
|
||||
// that were exported based on the provided error.
|
||||
//
|
||||
// If err is nil, n is returned. All data points were successfully exported.
|
||||
//
|
||||
// If err is not nil and not an [internal.PartialSuccess] error, 0 is returned.
|
||||
// It is assumed all data points failed to be exported.
|
||||
//
|
||||
// If err is an [internal.PartialSuccess] error, the number of successfully
|
||||
// exported data points is computed by subtracting the RejectedItems field from n. If
|
||||
// RejectedItems is negative, n is returned. If RejectedItems is greater than
|
||||
// n, 0 is returned.
|
||||
func successful(n int64, err error) int64 {
|
||||
if err == nil {
|
||||
return n // All data points successfully exported.
|
||||
}
|
||||
// Split rejection calculation so successful is inlinable.
|
||||
return n - rejected(n, err)
|
||||
}
|
||||
|
||||
var errPartialPool = &sync.Pool{
|
||||
New: func() any { return new(internal.PartialSuccess) },
|
||||
}
|
||||
|
||||
// rejected returns how many out of the n data points exporter were rejected based on
|
||||
// the provided non-nil err.
|
||||
func rejected(n int64, err error) int64 {
|
||||
ps := errPartialPool.Get().(*internal.PartialSuccess)
|
||||
defer errPartialPool.Put(ps)
|
||||
// Check for partial success.
|
||||
if errors.As(err, ps) {
|
||||
// Bound RejectedItems to [0, n]. This should not be needed,
|
||||
// but be defensive as this is from an external source.
|
||||
return min(max(ps.RejectedItems, 0), n)
|
||||
}
|
||||
return n // All data points rejected.
|
||||
}
|
||||
Generated
Vendored
+143
@@ -0,0 +1,143 @@
|
||||
// Code generated by gotmpl. DO NOT MODIFY.
|
||||
// source: internal/shared/otlp/observ/target.go.tmpl
|
||||
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package observ // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/observ"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
schemeUnix = "unix"
|
||||
schemeUnixAbstract = "unix-abstract"
|
||||
)
|
||||
|
||||
// ParseCanonicalTarget parses a target string and returns the extracted host
|
||||
// (domain address or IP), the target port, or an error.
|
||||
//
|
||||
// If no port is specified, -1 is returned.
|
||||
//
|
||||
// If no host is specified, an empty string is returned.
|
||||
//
|
||||
// The target string is expected to always have the form
|
||||
// "<scheme>://[authority]/<endpoint>". For example:
|
||||
// - "dns:///example.com:42"
|
||||
// - "dns://8.8.8.8/example.com:42"
|
||||
// - "unix:///path/to/socket"
|
||||
// - "unix-abstract:///socket-name"
|
||||
// - "passthrough:///192.34.2.1:42"
|
||||
//
|
||||
// The target is expected to come from the CanonicalTarget method of a gRPC
|
||||
// Client.
|
||||
func ParseCanonicalTarget(target string) (string, int, error) {
|
||||
const sep = "://"
|
||||
|
||||
// Find scheme. Do not allocate the string by using url.Parse.
|
||||
idx := strings.Index(target, sep)
|
||||
if idx == -1 {
|
||||
return "", -1, fmt.Errorf("invalid target %q: missing scheme", target)
|
||||
}
|
||||
scheme, endpoint := target[:idx], target[idx+len(sep):]
|
||||
|
||||
// Check for unix schemes.
|
||||
if scheme == schemeUnix || scheme == schemeUnixAbstract {
|
||||
return parseUnix(endpoint)
|
||||
}
|
||||
|
||||
// Strip leading slash and any authority.
|
||||
if i := strings.Index(endpoint, "/"); i != -1 {
|
||||
endpoint = endpoint[i+1:]
|
||||
}
|
||||
|
||||
// DNS, passthrough, and custom resolvers.
|
||||
return parseEndpoint(endpoint)
|
||||
}
|
||||
|
||||
// parseUnix parses unix socket targets.
|
||||
func parseUnix(endpoint string) (string, int, error) {
|
||||
// Format: unix[-abstract]://path
|
||||
//
|
||||
// We should have "/path" (empty authority) if valid.
|
||||
if len(endpoint) >= 1 && endpoint[0] == '/' {
|
||||
// Return the full path including leading slash.
|
||||
return endpoint, -1, nil
|
||||
}
|
||||
|
||||
// If there's no leading slash, it means there might be an authority
|
||||
// Check for authority case (should error): "authority/path"
|
||||
if slashIdx := strings.Index(endpoint, "/"); slashIdx > 0 {
|
||||
return "", -1, fmt.Errorf("invalid (non-empty) authority: %s", endpoint[:slashIdx])
|
||||
}
|
||||
|
||||
return "", -1, errors.New("invalid unix target format")
|
||||
}
|
||||
|
||||
// parseEndpoint parses an endpoint from a gRPC target.
|
||||
//
|
||||
// It supports the following formats:
|
||||
// - "host"
|
||||
// - "host%zone"
|
||||
// - "host:port"
|
||||
// - "host%zone:port"
|
||||
// - "ipv4"
|
||||
// - "ipv4%zone"
|
||||
// - "ipv4:port"
|
||||
// - "ipv4%zone:port"
|
||||
// - "ipv6"
|
||||
// - "ipv6%zone"
|
||||
// - "[ipv6]"
|
||||
// - "[ipv6%zone]"
|
||||
// - "[ipv6]:port"
|
||||
// - "[ipv6%zone]:port"
|
||||
//
|
||||
// It returns the host or host%zone (domain address or IP), the port (or -1 if
|
||||
// not specified), or an error if the input is not a valid.
|
||||
func parseEndpoint(endpoint string) (string, int, error) {
|
||||
// First check if the endpoint is just an IP address.
|
||||
if ip := parseIP(endpoint); ip != "" {
|
||||
return ip, -1, nil
|
||||
}
|
||||
|
||||
// If there's no colon, there is no port (IPv6 with no port checked above).
|
||||
if !strings.Contains(endpoint, ":") {
|
||||
return endpoint, -1, nil
|
||||
}
|
||||
|
||||
host, portStr, err := net.SplitHostPort(endpoint)
|
||||
if err != nil {
|
||||
return "", -1, fmt.Errorf("invalid host:port %q: %w", endpoint, err)
|
||||
}
|
||||
|
||||
const base, bitSize = 10, 16
|
||||
port16, err := strconv.ParseUint(portStr, base, bitSize)
|
||||
if err != nil {
|
||||
return "", -1, fmt.Errorf("invalid port %q: %w", portStr, err)
|
||||
}
|
||||
port := int(port16) // port is guaranteed to be in the range [0, 65535].
|
||||
|
||||
return host, port, nil
|
||||
}
|
||||
|
||||
// parseIP attempts to parse the entire endpoint as an IP address.
|
||||
// It returns the normalized string form of the IP if successful,
|
||||
// or an empty string if parsing fails.
|
||||
func parseIP(ip string) string {
|
||||
// Strip leading and trailing brackets for IPv6 addresses.
|
||||
if len(ip) >= 2 && ip[0] == '[' && ip[len(ip)-1] == ']' {
|
||||
ip = ip[1 : len(ip)-1]
|
||||
}
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// Return the normalized string form of the IP.
|
||||
return addr.String()
|
||||
}
|
||||
Generated
Vendored
+28
-15
@@ -35,6 +35,9 @@ const (
|
||||
// DefaultMetricsPath is a default URL path for endpoint that
|
||||
// receives metrics.
|
||||
DefaultMetricsPath string = "/v1/metrics"
|
||||
// DefaultMaxRequestSize is the default maximum size of a serialized export
|
||||
// request, before compression.
|
||||
DefaultMaxRequestSize int = 64 * 1024 * 1024
|
||||
// DefaultBackoff is a default base backoff time used in the
|
||||
// exponential backoff strategy.
|
||||
DefaultBackoff time.Duration = 300 * time.Millisecond
|
||||
@@ -49,13 +52,14 @@ type (
|
||||
HTTPTransportProxyFunc func(*http.Request) (*url.URL, error)
|
||||
|
||||
SignalConfig struct {
|
||||
Endpoint string
|
||||
Insecure bool
|
||||
TLSCfg *tls.Config
|
||||
Headers map[string]string
|
||||
Compression Compression
|
||||
Timeout time.Duration
|
||||
URLPath string
|
||||
Endpoint string
|
||||
Insecure bool
|
||||
TLSCfg *tls.Config
|
||||
Headers map[string]string
|
||||
Compression Compression
|
||||
MaxRequestSize int
|
||||
Timeout time.Duration
|
||||
URLPath string
|
||||
|
||||
TemporalitySelector metric.TemporalitySelector
|
||||
AggregationSelector metric.AggregationSelector
|
||||
@@ -87,10 +91,11 @@ type (
|
||||
func NewHTTPConfig(opts ...HTTPOption) Config {
|
||||
cfg := Config{
|
||||
Metrics: SignalConfig{
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorHTTPPort),
|
||||
URLPath: DefaultMetricsPath,
|
||||
Compression: NoCompression,
|
||||
Timeout: DefaultTimeout,
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorHTTPPort),
|
||||
URLPath: DefaultMetricsPath,
|
||||
Compression: NoCompression,
|
||||
MaxRequestSize: DefaultMaxRequestSize,
|
||||
Timeout: DefaultTimeout,
|
||||
|
||||
TemporalitySelector: metric.DefaultTemporalitySelector,
|
||||
AggregationSelector: metric.DefaultAggregationSelector,
|
||||
@@ -123,10 +128,11 @@ func cleanPath(urlPath string, defaultPath string) string {
|
||||
func NewGRPCConfig(opts ...GRPCOption) Config {
|
||||
cfg := Config{
|
||||
Metrics: SignalConfig{
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorGRPCPort),
|
||||
URLPath: DefaultMetricsPath,
|
||||
Compression: NoCompression,
|
||||
Timeout: DefaultTimeout,
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorGRPCPort),
|
||||
URLPath: DefaultMetricsPath,
|
||||
Compression: NoCompression,
|
||||
MaxRequestSize: DefaultMaxRequestSize,
|
||||
Timeout: DefaultTimeout,
|
||||
|
||||
TemporalitySelector: metric.DefaultTemporalitySelector,
|
||||
AggregationSelector: metric.DefaultAggregationSelector,
|
||||
@@ -354,6 +360,13 @@ func WithTimeout(duration time.Duration) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
func WithMaxRequestSize(size int) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.MaxRequestSize = size
|
||||
return cfg
|
||||
})
|
||||
}
|
||||
|
||||
func WithTemporalitySelector(selector metric.TemporalitySelector) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.TemporalitySelector = selector
|
||||
|
||||
Generated
Vendored
+18
@@ -81,6 +81,16 @@ func Value(v attribute.Value) *cpb.AnyValue {
|
||||
av.Value = &cpb.AnyValue_StringValue{
|
||||
StringValue: v.AsString(),
|
||||
}
|
||||
case attribute.BYTESLICE:
|
||||
av.Value = &cpb.AnyValue_BytesValue{
|
||||
BytesValue: v.AsByteSlice(),
|
||||
}
|
||||
case attribute.SLICE:
|
||||
av.Value = &cpb.AnyValue_ArrayValue{
|
||||
ArrayValue: &cpb.ArrayValue{
|
||||
Values: attrValues(v.AsSlice()),
|
||||
},
|
||||
}
|
||||
case attribute.STRINGSLICE:
|
||||
av.Value = &cpb.AnyValue_ArrayValue{
|
||||
ArrayValue: &cpb.ArrayValue{
|
||||
@@ -143,3 +153,11 @@ func stringSliceValues(vals []string) []*cpb.AnyValue {
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func attrValues(vals []attribute.Value) []*cpb.AnyValue {
|
||||
converted := make([]*cpb.AnyValue, len(vals))
|
||||
for i, v := range vals {
|
||||
converted[i] = Value(v)
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
Generated
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
# Experimental Features
|
||||
|
||||
The OTLP gRPC metric exporter contains features that have not yet stabilized in the OpenTelemetry specification.
|
||||
These features are added to the OpenTelemetry Go OTLP exporters prior to stabilization in the specification so that users can start experimenting with them and provide feedback.
|
||||
|
||||
These features may change in backwards incompatible ways as feedback is applied.
|
||||
See the [Compatibility and Stability](#compatibility-and-stability) section for more information.
|
||||
|
||||
## Features
|
||||
|
||||
- [Self-Observability](#self-observability)
|
||||
|
||||
### Self-Observability
|
||||
|
||||
The OTLP gRPC metric exporter can emit self-observability metrics to track its own operation.
|
||||
|
||||
This experimental feature can be enabled by setting the `OTEL_GO_X_OBSERVABILITY` environment variable.
|
||||
The value must be the case-insensitive string of `"true"` to enable the feature.
|
||||
All other values are ignored.
|
||||
|
||||
When enabled, the exporter will emit the following metrics using the global MeterProvider:
|
||||
|
||||
- `otel.sdk.exporter.metric_data_point.exported`: Counter tracking successfully exported data points
|
||||
- `otel.sdk.exporter.metric_data_point.inflight`: UpDownCounter tracking data points currently being exported
|
||||
- `otel.sdk.exporter.operation.duration`: Histogram tracking export operation duration in seconds
|
||||
|
||||
All metrics include attributes identifying the exporter component and destination server:
|
||||
|
||||
- `otel.component.type`: Type of component (e.g., "otlp_grpc_metric_exporter")
|
||||
- `otel.component.name`: Unique component instance name (e.g., "otlp_grpc_metric_exporter/0")
|
||||
- `server.address`: Server hostname or address
|
||||
- `server.port`: Server port number
|
||||
|
||||
#### Examples
|
||||
|
||||
Enable self-observability metrics.
|
||||
|
||||
```console
|
||||
export OTEL_GO_X_OBSERVABILITY=true
|
||||
```
|
||||
|
||||
Disable self-observability metrics.
|
||||
|
||||
```console
|
||||
unset OTEL_GO_X_OBSERVABILITY
|
||||
```
|
||||
|
||||
## Compatibility and Stability
|
||||
|
||||
Experimental features do not fall within the scope of the OpenTelemetry Go versioning and stability [policy](../../../../../../VERSIONING.md).
|
||||
These features may be removed or modified in successive version releases, including patch versions.
|
||||
|
||||
When an experimental feature is promoted to a stable feature, a migration path will be included in the changelog entry of the release.
|
||||
There is no guarantee that any environment variable feature flags that enabled the experimental feature will be supported by the stable version.
|
||||
If they are supported, they may be accompanied with a deprecation notice stating a timeline for the removal of that support.
|
||||
Generated
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package x // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/x"
|
||||
|
||||
import "strings"
|
||||
|
||||
// Observability is an experimental feature flag that defines if OTLP
|
||||
// gRPC metric exporter should include self-observability metrics.
|
||||
//
|
||||
// To enable this feature set the OTEL_GO_X_OBSERVABILITY environment variable
|
||||
// to the case-insensitive string value of "true" (i.e. "True" and "TRUE"
|
||||
// will also enable this).
|
||||
var Observability = newFeature(
|
||||
[]string{"OBSERVABILITY"},
|
||||
func(v string) (string, bool) {
|
||||
if strings.EqualFold(v, "true") {
|
||||
return v, true
|
||||
}
|
||||
return "", false
|
||||
},
|
||||
)
|
||||
Generated
Vendored
+58
@@ -0,0 +1,58 @@
|
||||
// Code generated by gotmpl. DO NOT MODIFY.
|
||||
// source: internal/shared/x/x.go.tmpl
|
||||
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package x documents experimental features for [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc].
|
||||
package x // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/x"
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Feature is an experimental feature control flag. It provides a uniform way
|
||||
// to interact with these feature flags and parse their values.
|
||||
type Feature[T any] struct {
|
||||
keys []string
|
||||
parse func(v string) (T, bool)
|
||||
}
|
||||
|
||||
func newFeature[T any](suffix []string, parse func(string) (T, bool)) Feature[T] {
|
||||
const envKeyRoot = "OTEL_GO_X_"
|
||||
keys := make([]string, 0, len(suffix))
|
||||
for _, s := range suffix {
|
||||
keys = append(keys, envKeyRoot+s)
|
||||
}
|
||||
return Feature[T]{
|
||||
keys: keys,
|
||||
parse: parse,
|
||||
}
|
||||
}
|
||||
|
||||
// Keys returns the environment variable keys that can be set to enable the
|
||||
// feature.
|
||||
func (f Feature[T]) Keys() []string { return f.keys }
|
||||
|
||||
// Lookup returns the user configured value for the feature and true if the
|
||||
// user has enabled the feature. Otherwise, if the feature is not enabled, a
|
||||
// zero-value and false are returned.
|
||||
func (f Feature[T]) Lookup() (v T, ok bool) {
|
||||
// https://github.com/open-telemetry/opentelemetry-specification/blob/62effed618589a0bec416a87e559c0a9d96289bb/specification/configuration/sdk-environment-variables.md#parsing-empty-value
|
||||
//
|
||||
// > The SDK MUST interpret an empty value of an environment variable the
|
||||
// > same way as when the variable is unset.
|
||||
for _, key := range f.keys {
|
||||
vRaw := os.Getenv(key)
|
||||
if vRaw != "" {
|
||||
return f.parse(vRaw)
|
||||
}
|
||||
}
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// Enabled reports whether the feature is enabled.
|
||||
func (f Feature[T]) Enabled() bool {
|
||||
_, ok := f.Lookup()
|
||||
return ok
|
||||
}
|
||||
Generated
Vendored
+1
-1
@@ -5,5 +5,5 @@ package otlpmetricgrpc // import "go.opentelemetry.io/otel/exporters/otlp/otlpme
|
||||
|
||||
// Version is the current release version of the OpenTelemetry OTLP over gRPC metrics exporter in use.
|
||||
func Version() string {
|
||||
return "1.43.0"
|
||||
return "1.44.0"
|
||||
}
|
||||
|
||||
Generated
Vendored
+44
-18
@@ -23,16 +23,21 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/counter"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/observ"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/retry"
|
||||
)
|
||||
|
||||
type client struct {
|
||||
// req is cloned for every upload the client makes.
|
||||
req *http.Request
|
||||
compression Compression
|
||||
requestFunc retry.RequestFunc
|
||||
httpClient *http.Client
|
||||
req *http.Request
|
||||
compression Compression
|
||||
maxRequestSize int
|
||||
requestFunc retry.RequestFunc
|
||||
httpClient *http.Client
|
||||
|
||||
inst *observ.Instrumentation
|
||||
}
|
||||
|
||||
// Keep it in sync with golang's DefaultTransport from net/http! We
|
||||
@@ -111,12 +116,17 @@ func newClient(cfg oconf.Config) (*client, error) {
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-protobuf")
|
||||
|
||||
// Initialize the instrumentation.
|
||||
inst, err := observ.NewInstrumentation(counter.NextExporterID(), cfg.Metrics.Endpoint)
|
||||
|
||||
return &client{
|
||||
compression: Compression(cfg.Metrics.Compression),
|
||||
req: req,
|
||||
requestFunc: cfg.RetryConfig.RequestFunc(evaluate),
|
||||
httpClient: httpClient,
|
||||
}, nil
|
||||
compression: Compression(cfg.Metrics.Compression),
|
||||
maxRequestSize: cfg.Metrics.MaxRequestSize,
|
||||
req: req,
|
||||
requestFunc: cfg.RetryConfig.RequestFunc(evaluate),
|
||||
httpClient: httpClient,
|
||||
inst: inst,
|
||||
}, err
|
||||
}
|
||||
|
||||
// Shutdown shuts down the client, freeing all resources.
|
||||
@@ -146,11 +156,20 @@ func (c *client) UploadMetrics(ctx context.Context, protoMetrics *metricpb.Resou
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if maxSize := c.maxRequestSize; maxSize > 0 && len(body) > maxSize {
|
||||
return fmt.Errorf("request body too large: exceeded %d bytes", maxSize)
|
||||
}
|
||||
request, err := c.newRequest(ctx, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var statusCode int
|
||||
if c.inst != nil {
|
||||
op := c.inst.ExportMetrics(ctx, protoMetrics)
|
||||
defer func() { op.End(uploadErr, statusCode) }()
|
||||
}
|
||||
|
||||
return errors.Join(uploadErr, c.requestFunc(ctx, func(iCtx context.Context) error {
|
||||
select {
|
||||
case <-iCtx.Done():
|
||||
@@ -158,6 +177,7 @@ func (c *client) UploadMetrics(ctx context.Context, protoMetrics *metricpb.Resou
|
||||
default:
|
||||
}
|
||||
|
||||
statusCode = 0
|
||||
request.reset(iCtx)
|
||||
// nolint:gosec // URL is constructed from validated OTLP endpoint configuration
|
||||
resp, err := c.httpClient.Do(request.Request)
|
||||
@@ -168,15 +188,18 @@ func (c *client) UploadMetrics(ctx context.Context, protoMetrics *metricpb.Resou
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp != nil && resp.Body != nil {
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
uploadErr = errors.Join(uploadErr, err)
|
||||
}
|
||||
}()
|
||||
if resp != nil {
|
||||
statusCode = resp.StatusCode
|
||||
if resp.Body != nil {
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
uploadErr = errors.Join(uploadErr, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
if sc := resp.StatusCode; sc >= 200 && sc <= 299 {
|
||||
if statusCode >= 200 && statusCode <= 299 {
|
||||
// Success, do not retry.
|
||||
|
||||
// Read the partial success message, if any.
|
||||
@@ -265,7 +288,10 @@ func (c *client) newRequest(ctx context.Context, body []byte) (request, error) {
|
||||
r.Header.Set("Content-Encoding", "gzip")
|
||||
|
||||
gz := gzPool.Get().(*gzip.Writer)
|
||||
defer gzPool.Put(gz)
|
||||
defer func() {
|
||||
gz.Reset(io.Discard)
|
||||
gzPool.Put(gz)
|
||||
}()
|
||||
|
||||
var b bytes.Buffer
|
||||
gz.Reset(&b)
|
||||
@@ -279,7 +305,7 @@ func (c *client) newRequest(ctx context.Context, body []byte) (request, error) {
|
||||
}
|
||||
|
||||
req.bodyReader = bodyReader(b.Bytes())
|
||||
req.GetBody = bodyReaderErr(body)
|
||||
req.GetBody = bodyReaderErr(b.Bytes())
|
||||
}
|
||||
|
||||
return req, nil
|
||||
|
||||
Generated
Vendored
+10
@@ -185,6 +185,16 @@ func WithTimeout(duration time.Duration) Option {
|
||||
return wrappedOption{oconf.WithTimeout(duration)}
|
||||
}
|
||||
|
||||
// WithMaxRequestSize sets the maximum size, in bytes, of a serialized export
|
||||
// request, before compression, that the exporter will send.
|
||||
//
|
||||
// If size is less than or equal to zero, no request-size limit is applied.
|
||||
// Disabling the limit is not recommended because it can lead to excessive
|
||||
// resource consumption or abuse.
|
||||
func WithMaxRequestSize(size int) Option {
|
||||
return wrappedOption{oconf.WithMaxRequestSize(size)}
|
||||
}
|
||||
|
||||
// WithRetry sets the retry policy for transient retryable errors that are
|
||||
// returned by the target endpoint.
|
||||
//
|
||||
|
||||
vendor/go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/counter/counter.go
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
// Code generated by gotmpl. DO NOT MODIFY.
|
||||
// source: internal/shared/counter/counter.go.tmpl
|
||||
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package counter provides a simple counter for generating unique IDs.
|
||||
//
|
||||
// This package is used to generate unique IDs while allowing testing packages
|
||||
// to reset the counter.
|
||||
package counter // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/counter"
|
||||
|
||||
import "sync/atomic"
|
||||
|
||||
// exporterN is a global 0-based count of the number of exporters created.
|
||||
var exporterN atomic.Int64
|
||||
|
||||
// NextExporterID returns the next unique ID for an exporter.
|
||||
func NextExporterID() int64 {
|
||||
const inc = 1
|
||||
return exporterN.Add(inc) - inc
|
||||
}
|
||||
|
||||
// SetExporterID sets the exporter ID counter to v and returns the previous
|
||||
// value.
|
||||
//
|
||||
// This function is useful for testing purposes, allowing you to reset the
|
||||
// counter. It should not be used in production code.
|
||||
func SetExporterID(v int64) int64 {
|
||||
return exporterN.Swap(v)
|
||||
}
|
||||
Generated
Vendored
+6
@@ -30,3 +30,9 @@ package internal // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/o
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/error_test.go.tmpl "--data={}" --out=transform/error_test.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/metricdata.go.tmpl "--data={}" --out=transform/metricdata.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/metricdata_test.go.tmpl "--data={}" --out=transform/metricdata_test.go
|
||||
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/counter/counter.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/counter\" }" --out=counter/counter.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/counter/counter_test.go.tmpl "--data={}" --out=counter/counter_test.go
|
||||
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/x/x.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/x\" }" --out=x/x.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/x/x_test.go.tmpl "--data={}" --out=x/x_test.go
|
||||
|
||||
Generated
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package observ // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/observ"
|
||||
|
||||
import metricpb "go.opentelemetry.io/proto/otlp/metrics/v1"
|
||||
|
||||
// countDataPoints counts the total number of data points in a ResourceMetrics.
|
||||
func countDataPoints(rm *metricpb.ResourceMetrics) int64 {
|
||||
if rm == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
var total int64
|
||||
for _, sm := range rm.ScopeMetrics {
|
||||
for _, m := range sm.Metrics {
|
||||
switch data := m.Data.(type) {
|
||||
case *metricpb.Metric_Gauge:
|
||||
if data.Gauge != nil {
|
||||
total += int64(len(data.Gauge.DataPoints))
|
||||
}
|
||||
case *metricpb.Metric_Sum:
|
||||
if data.Sum != nil {
|
||||
total += int64(len(data.Sum.DataPoints))
|
||||
}
|
||||
case *metricpb.Metric_Histogram:
|
||||
if data.Histogram != nil {
|
||||
total += int64(len(data.Histogram.DataPoints))
|
||||
}
|
||||
case *metricpb.Metric_ExponentialHistogram:
|
||||
if data.ExponentialHistogram != nil {
|
||||
total += int64(len(data.ExponentialHistogram.DataPoints))
|
||||
}
|
||||
case *metricpb.Metric_Summary:
|
||||
if data.Summary != nil {
|
||||
total += int64(len(data.Summary.DataPoints))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
Generated
Vendored
+411
@@ -0,0 +1,411 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package observ provides experimental observability instrumentation for the
|
||||
// otlpmetrichttp exporter.
|
||||
package observ // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/observ"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
metricpb "go.opentelemetry.io/proto/otlp/metrics/v1"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/x"
|
||||
"go.opentelemetry.io/otel/internal/global"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.41.0/otelconv"
|
||||
)
|
||||
|
||||
const (
|
||||
// ScopeName is the unique name of the meter used for instrumentation.
|
||||
ScopeName = "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/observ"
|
||||
|
||||
// SchemaURL is the schema URL of the metrics produced by this
|
||||
// instrumentation.
|
||||
SchemaURL = semconv.SchemaURL
|
||||
|
||||
// Version is the current version of this instrumentation.
|
||||
//
|
||||
// This matches the version of the exporter.
|
||||
Version = internal.Version
|
||||
)
|
||||
|
||||
var (
|
||||
measureAttrsPool = &sync.Pool{
|
||||
New: func() any {
|
||||
const n = 1 + // component.name
|
||||
1 + // component.type
|
||||
1 + // server.addr
|
||||
1 + // server.port
|
||||
1 + // error.type
|
||||
1 // http.response.status_code
|
||||
s := make([]attribute.KeyValue, 0, n)
|
||||
// Return a pointer to a slice instead of a slice itself
|
||||
// to avoid allocations on every call.
|
||||
return &s
|
||||
},
|
||||
}
|
||||
|
||||
addOptPool = &sync.Pool{
|
||||
New: func() any {
|
||||
const n = 1 // WithAttributeSet
|
||||
o := make([]metric.AddOption, 0, n)
|
||||
return &o
|
||||
},
|
||||
}
|
||||
|
||||
recordOptPool = &sync.Pool{
|
||||
New: func() any {
|
||||
const n = 1 // WithAttributeSet
|
||||
o := make([]metric.RecordOption, 0, n)
|
||||
return &o
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func get[T any](p *sync.Pool) *[]T { return p.Get().(*[]T) }
|
||||
|
||||
func put[T any](p *sync.Pool, s *[]T) {
|
||||
*s = (*s)[:0] // Reset.
|
||||
p.Put(s)
|
||||
}
|
||||
|
||||
// ComponentName returns the component name for the exporter with the
|
||||
// provided ID.
|
||||
func ComponentName(id int64) string {
|
||||
t := semconv.OTelComponentTypeOtlpHTTPMetricExporter.Value.AsString()
|
||||
return fmt.Sprintf("%s/%d", t, id)
|
||||
}
|
||||
|
||||
// Instrumentation is experimental instrumentation for the exporter.
|
||||
type Instrumentation struct {
|
||||
inflightMetric metric.Int64UpDownCounter
|
||||
exportedMetric metric.Int64Counter
|
||||
opDuration metric.Float64Histogram
|
||||
|
||||
attrs []attribute.KeyValue
|
||||
addOpt metric.AddOption
|
||||
recOpt metric.RecordOption
|
||||
}
|
||||
|
||||
// NewInstrumentation returns instrumentation for an OTLP over HTTP metric
|
||||
// exporter with the provided ID and endpoint. It uses the global
|
||||
// MeterProvider to create the instrumentation.
|
||||
//
|
||||
// The id should be the unique exporter instance ID. It is used
|
||||
// to set the "component.name" attribute.
|
||||
//
|
||||
// The endpoint is the HTTP endpoint the exporter is exporting to.
|
||||
//
|
||||
// If the experimental observability is disabled, nil is returned.
|
||||
func NewInstrumentation(id int64, endpoint string) (*Instrumentation, error) {
|
||||
if !x.Observability.Enabled() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
attrs := BaseAttrs(id, endpoint)
|
||||
i := &Instrumentation{
|
||||
attrs: attrs,
|
||||
addOpt: metric.WithAttributeSet(attribute.NewSet(attrs...)),
|
||||
|
||||
// Do not modify attrs (NewSet sorts in-place), make a new slice.
|
||||
recOpt: metric.WithAttributeSet(attribute.NewSet(append(
|
||||
// Default to OK status code (200).
|
||||
[]attribute.KeyValue{semconv.HTTPResponseStatusCode(http.StatusOK)},
|
||||
attrs...,
|
||||
)...)),
|
||||
}
|
||||
|
||||
mp := otel.GetMeterProvider()
|
||||
m := mp.Meter(
|
||||
ScopeName,
|
||||
metric.WithInstrumentationVersion(Version),
|
||||
metric.WithSchemaURL(SchemaURL),
|
||||
)
|
||||
|
||||
var err error
|
||||
|
||||
inflightMetric, e := otelconv.NewSDKExporterMetricDataPointInflight(m)
|
||||
if e != nil {
|
||||
e = fmt.Errorf("failed to create inflight metric: %w", e)
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
i.inflightMetric = inflightMetric.Inst()
|
||||
|
||||
exportedMetric, e := otelconv.NewSDKExporterMetricDataPointExported(m)
|
||||
if e != nil {
|
||||
e = fmt.Errorf("failed to create exported metric: %w", e)
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
i.exportedMetric = exportedMetric.Inst()
|
||||
|
||||
opDuration, e := otelconv.NewSDKExporterOperationDuration(m)
|
||||
if e != nil {
|
||||
e = fmt.Errorf("failed to create operation duration metric: %w", e)
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
i.opDuration = opDuration.Inst()
|
||||
|
||||
return i, err
|
||||
}
|
||||
|
||||
// BaseAttrs returns the base attributes for the exporter with the provided ID
|
||||
// and endpoint.
|
||||
//
|
||||
// The id should be the unique exporter instance ID. It is used
|
||||
// to set the "component.name" attribute.
|
||||
//
|
||||
// The endpoint is the HTTP endpoint the exporter is exporting to. It should be
|
||||
// in the format "host[:port]".
|
||||
func BaseAttrs(id int64, endpoint string) []attribute.KeyValue {
|
||||
host, port, err := parseEndpoint(endpoint)
|
||||
if err != nil || (host == "" && port < 0) {
|
||||
if err != nil {
|
||||
global.Debug("failed to parse endpoint", "endpoint", endpoint, "error", err)
|
||||
}
|
||||
return []attribute.KeyValue{
|
||||
semconv.OTelComponentName(ComponentName(id)),
|
||||
semconv.OTelComponentTypeOtlpHTTPMetricExporter,
|
||||
}
|
||||
}
|
||||
|
||||
// Do not use append so the slice is exactly allocated.
|
||||
|
||||
if port < 0 {
|
||||
return []attribute.KeyValue{
|
||||
semconv.OTelComponentName(ComponentName(id)),
|
||||
semconv.OTelComponentTypeOtlpHTTPMetricExporter,
|
||||
semconv.ServerAddress(host),
|
||||
}
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
return []attribute.KeyValue{
|
||||
semconv.OTelComponentName(ComponentName(id)),
|
||||
semconv.OTelComponentTypeOtlpHTTPMetricExporter,
|
||||
semconv.ServerPort(port),
|
||||
}
|
||||
}
|
||||
|
||||
return []attribute.KeyValue{
|
||||
semconv.OTelComponentName(ComponentName(id)),
|
||||
semconv.OTelComponentTypeOtlpHTTPMetricExporter,
|
||||
semconv.ServerAddress(host),
|
||||
semconv.ServerPort(port),
|
||||
}
|
||||
}
|
||||
|
||||
// parseEndpoint parses the host and port from endpoint that has the form
|
||||
// "host[:port]", or it returns an error if the endpoint is not parsable.
|
||||
//
|
||||
// If no port is specified, -1 is returned.
|
||||
//
|
||||
// If no host is specified, an empty string is returned.
|
||||
func parseEndpoint(endpoint string) (string, int, error) {
|
||||
// First check if the endpoint is just an IP address.
|
||||
if ip := parseIP(endpoint); ip != "" {
|
||||
return ip, -1, nil
|
||||
}
|
||||
|
||||
// If there's no colon, there is no port (IPv6 with no port checked above).
|
||||
if !strings.Contains(endpoint, ":") {
|
||||
return endpoint, -1, nil
|
||||
}
|
||||
|
||||
// Otherwise, parse as host:port.
|
||||
host, portStr, err := net.SplitHostPort(endpoint)
|
||||
if err != nil {
|
||||
return "", -1, fmt.Errorf("invalid host:port %q: %w", endpoint, err)
|
||||
}
|
||||
|
||||
const base, bitSize = 10, 16
|
||||
port16, err := strconv.ParseUint(portStr, base, bitSize)
|
||||
if err != nil {
|
||||
return "", -1, fmt.Errorf("invalid port %q: %w", portStr, err)
|
||||
}
|
||||
port := int(port16) // port is guaranteed to be in the range [0, 65535].
|
||||
|
||||
return host, port, nil
|
||||
}
|
||||
|
||||
// parseIP attempts to parse the entire endpoint as an IP address.
|
||||
// It returns the normalized string form of the IP if successful,
|
||||
// or an empty string if parsing fails.
|
||||
func parseIP(ip string) string {
|
||||
// Strip leading and trailing brackets for IPv6 addresses.
|
||||
if len(ip) >= 2 && ip[0] == '[' && ip[len(ip)-1] == ']' {
|
||||
ip = ip[1 : len(ip)-1]
|
||||
}
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// Return the normalized string form of the IP.
|
||||
return addr.String()
|
||||
}
|
||||
|
||||
// ExportMetrics instruments the UploadMetrics method of the client. It returns an
|
||||
// [ExportOp] that must have its [ExportOp.End] method called when the
|
||||
// operation ends.
|
||||
func (i *Instrumentation) ExportMetrics(ctx context.Context, rm *metricpb.ResourceMetrics) ExportOp {
|
||||
start := time.Now()
|
||||
|
||||
nMetrics := countDataPoints(rm)
|
||||
|
||||
if i.inflightMetric.Enabled(ctx) {
|
||||
addOpt := get[metric.AddOption](addOptPool)
|
||||
defer put(addOptPool, addOpt)
|
||||
*addOpt = append(*addOpt, i.addOpt)
|
||||
i.inflightMetric.Add(ctx, nMetrics, *addOpt...)
|
||||
}
|
||||
|
||||
return ExportOp{
|
||||
ctx: ctx,
|
||||
start: start,
|
||||
nMetrics: nMetrics,
|
||||
inst: i,
|
||||
}
|
||||
}
|
||||
|
||||
// ExportOp tracks the export operation being observed by
|
||||
// [Instrumentation.ExportMetrics].
|
||||
type ExportOp struct {
|
||||
ctx context.Context
|
||||
start time.Time
|
||||
nMetrics int64
|
||||
|
||||
inst *Instrumentation
|
||||
}
|
||||
|
||||
// End completes the observation of the operation being observed by a call to
|
||||
// [Instrumentation.ExportMetrics].
|
||||
//
|
||||
// Any error that is encountered is provided as err.
|
||||
// The HTTP status code from the response is provided as status.
|
||||
//
|
||||
// If err is not nil, all metrics will be recorded as failures unless error is of
|
||||
// type [internal.PartialSuccess]. In the case of a PartialSuccess, the number
|
||||
// of successfully exported metrics will be determined by inspecting the
|
||||
// RejectedItems field of the PartialSuccess.
|
||||
func (e ExportOp) End(err error, status int) {
|
||||
addOpt := get[metric.AddOption](addOptPool)
|
||||
defer put(addOptPool, addOpt)
|
||||
*addOpt = append(*addOpt, e.inst.addOpt)
|
||||
|
||||
if e.inst.inflightMetric.Enabled(e.ctx) {
|
||||
e.inst.inflightMetric.Add(e.ctx, -e.nMetrics, *addOpt...)
|
||||
}
|
||||
|
||||
success := successful(e.nMetrics, err)
|
||||
// Record successfully exported metrics, even if the value is 0 which are
|
||||
// meaningful to distribution aggregations.
|
||||
if e.inst.exportedMetric.Enabled(e.ctx) {
|
||||
e.inst.exportedMetric.Add(e.ctx, success, *addOpt...)
|
||||
}
|
||||
|
||||
if err != nil && e.inst.exportedMetric.Enabled(e.ctx) {
|
||||
attrs := get[attribute.KeyValue](measureAttrsPool)
|
||||
defer put(measureAttrsPool, attrs)
|
||||
*attrs = append(*attrs, e.inst.attrs...)
|
||||
*attrs = append(*attrs, semconv.ErrorType(err))
|
||||
|
||||
// Do not inefficiently make a copy of attrs by using
|
||||
// WithAttributes instead of WithAttributeSet.
|
||||
o := metric.WithAttributeSet(attribute.NewSet(*attrs...))
|
||||
// Reset addOpt with new attribute set.
|
||||
*addOpt = append((*addOpt)[:0], o)
|
||||
|
||||
e.inst.exportedMetric.Add(e.ctx, e.nMetrics-success, *addOpt...)
|
||||
}
|
||||
|
||||
if e.inst.opDuration.Enabled(e.ctx) {
|
||||
recOpt := get[metric.RecordOption](recordOptPool)
|
||||
defer put(recordOptPool, recOpt)
|
||||
*recOpt = append(*recOpt, e.inst.recordOption(err, status))
|
||||
|
||||
d := time.Since(e.start).Seconds()
|
||||
e.inst.opDuration.Record(e.ctx, d, *recOpt...)
|
||||
}
|
||||
}
|
||||
|
||||
// recordOption returns a RecordOption with attributes representing the
|
||||
// outcome of the operation being recorded.
|
||||
//
|
||||
// If err is nil and status is 200, the default recOpt of the
|
||||
// Instrumentation is returned.
|
||||
//
|
||||
// Otherwise, a new RecordOption is returned with the base attributes of the
|
||||
// Instrumentation plus the http.response.status_code attribute set to the
|
||||
// provided status (if non-zero), and if err is not nil, the error.type attribute set
|
||||
// to the type of the error.
|
||||
func (i *Instrumentation) recordOption(err error, status int) metric.RecordOption {
|
||||
if err == nil && status == http.StatusOK {
|
||||
return i.recOpt
|
||||
}
|
||||
|
||||
attrs := get[attribute.KeyValue](measureAttrsPool)
|
||||
defer put(measureAttrsPool, attrs)
|
||||
*attrs = append(*attrs, i.attrs...)
|
||||
|
||||
if status != 0 {
|
||||
*attrs = append(*attrs, semconv.HTTPResponseStatusCode(status))
|
||||
}
|
||||
if err != nil {
|
||||
*attrs = append(*attrs, semconv.ErrorType(err))
|
||||
}
|
||||
|
||||
// Do not inefficiently make a copy of attrs by using WithAttributes
|
||||
// instead of WithAttributeSet.
|
||||
return metric.WithAttributeSet(attribute.NewSet(*attrs...))
|
||||
}
|
||||
|
||||
// successful returns the number of successfully exported metrics out of the n
|
||||
// that were exported based on the provided error.
|
||||
//
|
||||
// If err is nil, n is returned. All metrics were successfully exported.
|
||||
//
|
||||
// If err is not nil and not an [internal.PartialSuccess] error, 0 is returned.
|
||||
// It is assumed all metrics failed to be exported.
|
||||
//
|
||||
// If err is an [internal.PartialSuccess] error, the number of successfully
|
||||
// exported metrics is computed by subtracting the RejectedItems field from n. If
|
||||
// RejectedItems is negative, n is returned. If RejectedItems is greater than
|
||||
// n, 0 is returned.
|
||||
func successful(n int64, err error) int64 {
|
||||
if err == nil {
|
||||
return n // All metrics successfully exported.
|
||||
}
|
||||
// Split rejection calculation so successful is inlinable.
|
||||
return n - rejected(n, err)
|
||||
}
|
||||
|
||||
var errPartialPool = &sync.Pool{
|
||||
New: func() any { return new(internal.PartialSuccess) },
|
||||
}
|
||||
|
||||
// rejected returns how many out of the n metrics were rejected based on the
|
||||
// provided non-nil err.
|
||||
func rejected(n int64, err error) int64 {
|
||||
ps := errPartialPool.Get().(*internal.PartialSuccess)
|
||||
defer errPartialPool.Put(ps)
|
||||
// Check for partial success.
|
||||
if errors.As(err, ps) {
|
||||
// Bound RejectedItems to [0, n]. This should not be needed,
|
||||
// but be defensive as this is from an external source.
|
||||
return min(max(ps.RejectedItems, 0), n)
|
||||
}
|
||||
return n // All metrics rejected.
|
||||
}
|
||||
Generated
Vendored
+28
-15
@@ -35,6 +35,9 @@ const (
|
||||
// DefaultMetricsPath is a default URL path for endpoint that
|
||||
// receives metrics.
|
||||
DefaultMetricsPath string = "/v1/metrics"
|
||||
// DefaultMaxRequestSize is the default maximum size of a serialized export
|
||||
// request, before compression.
|
||||
DefaultMaxRequestSize int = 64 * 1024 * 1024
|
||||
// DefaultBackoff is a default base backoff time used in the
|
||||
// exponential backoff strategy.
|
||||
DefaultBackoff time.Duration = 300 * time.Millisecond
|
||||
@@ -49,13 +52,14 @@ type (
|
||||
HTTPTransportProxyFunc func(*http.Request) (*url.URL, error)
|
||||
|
||||
SignalConfig struct {
|
||||
Endpoint string
|
||||
Insecure bool
|
||||
TLSCfg *tls.Config
|
||||
Headers map[string]string
|
||||
Compression Compression
|
||||
Timeout time.Duration
|
||||
URLPath string
|
||||
Endpoint string
|
||||
Insecure bool
|
||||
TLSCfg *tls.Config
|
||||
Headers map[string]string
|
||||
Compression Compression
|
||||
MaxRequestSize int
|
||||
Timeout time.Duration
|
||||
URLPath string
|
||||
|
||||
TemporalitySelector metric.TemporalitySelector
|
||||
AggregationSelector metric.AggregationSelector
|
||||
@@ -87,10 +91,11 @@ type (
|
||||
func NewHTTPConfig(opts ...HTTPOption) Config {
|
||||
cfg := Config{
|
||||
Metrics: SignalConfig{
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorHTTPPort),
|
||||
URLPath: DefaultMetricsPath,
|
||||
Compression: NoCompression,
|
||||
Timeout: DefaultTimeout,
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorHTTPPort),
|
||||
URLPath: DefaultMetricsPath,
|
||||
Compression: NoCompression,
|
||||
MaxRequestSize: DefaultMaxRequestSize,
|
||||
Timeout: DefaultTimeout,
|
||||
|
||||
TemporalitySelector: metric.DefaultTemporalitySelector,
|
||||
AggregationSelector: metric.DefaultAggregationSelector,
|
||||
@@ -123,10 +128,11 @@ func cleanPath(urlPath string, defaultPath string) string {
|
||||
func NewGRPCConfig(opts ...GRPCOption) Config {
|
||||
cfg := Config{
|
||||
Metrics: SignalConfig{
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorGRPCPort),
|
||||
URLPath: DefaultMetricsPath,
|
||||
Compression: NoCompression,
|
||||
Timeout: DefaultTimeout,
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorGRPCPort),
|
||||
URLPath: DefaultMetricsPath,
|
||||
Compression: NoCompression,
|
||||
MaxRequestSize: DefaultMaxRequestSize,
|
||||
Timeout: DefaultTimeout,
|
||||
|
||||
TemporalitySelector: metric.DefaultTemporalitySelector,
|
||||
AggregationSelector: metric.DefaultAggregationSelector,
|
||||
@@ -354,6 +360,13 @@ func WithTimeout(duration time.Duration) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
func WithMaxRequestSize(size int) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.MaxRequestSize = size
|
||||
return cfg
|
||||
})
|
||||
}
|
||||
|
||||
func WithTemporalitySelector(selector metric.TemporalitySelector) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.TemporalitySelector = selector
|
||||
|
||||
Generated
Vendored
+18
@@ -81,6 +81,16 @@ func Value(v attribute.Value) *cpb.AnyValue {
|
||||
av.Value = &cpb.AnyValue_StringValue{
|
||||
StringValue: v.AsString(),
|
||||
}
|
||||
case attribute.BYTESLICE:
|
||||
av.Value = &cpb.AnyValue_BytesValue{
|
||||
BytesValue: v.AsByteSlice(),
|
||||
}
|
||||
case attribute.SLICE:
|
||||
av.Value = &cpb.AnyValue_ArrayValue{
|
||||
ArrayValue: &cpb.ArrayValue{
|
||||
Values: attrValues(v.AsSlice()),
|
||||
},
|
||||
}
|
||||
case attribute.STRINGSLICE:
|
||||
av.Value = &cpb.AnyValue_ArrayValue{
|
||||
ArrayValue: &cpb.ArrayValue{
|
||||
@@ -143,3 +153,11 @@ func stringSliceValues(vals []string) []*cpb.AnyValue {
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func attrValues(vals []attribute.Value) []*cpb.AnyValue {
|
||||
converted := make([]*cpb.AnyValue, len(vals))
|
||||
for i, v := range vals {
|
||||
converted[i] = Value(v)
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package internal // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal"
|
||||
|
||||
// Version is the current release version of the OpenTelemetry OTLP HTTP metric
|
||||
// exporter in use.
|
||||
const Version = "1.44.0"
|
||||
Generated
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
# Experimental Features
|
||||
|
||||
The `otlpmetrichttp` exporter contains features that have not yet stabilized in the OpenTelemetry specification.
|
||||
These features are added to the `otlpmetrichttp` exporter prior to stabilization in the specification so that users can start experimenting with them and provide feedback.
|
||||
|
||||
These features may change in backwards incompatible ways as feedback is applied.
|
||||
See the [Compatibility and Stability](#compatibility-and-stability) section for more information.
|
||||
|
||||
## Features
|
||||
|
||||
- [Observability](#observability)
|
||||
|
||||
### Observability
|
||||
|
||||
The `otlpmetrichttp` exporter can be configured to provide observability about itself using OpenTelemetry metrics.
|
||||
|
||||
To opt-in, set the environment variable `OTEL_GO_X_OBSERVABILITY` to `true`.
|
||||
|
||||
When enabled, the exporter will create the following metrics using the global `MeterProvider`:
|
||||
|
||||
- `otel.sdk.exporter.metric_data_point.inflight`
|
||||
- `otel.sdk.exporter.metric_data_point.exported`
|
||||
- `otel.sdk.exporter.operation.duration`
|
||||
|
||||
Please see the [Semantic conventions for OpenTelemetry SDK metrics] documentation for more details on these metrics.
|
||||
|
||||
[Semantic conventions for OpenTelemetry SDK metrics]: https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/otel/sdk-metrics.md
|
||||
|
||||
## Compatibility and Stability
|
||||
|
||||
Experimental features do not fall within the scope of the OpenTelemetry Go versioning and stability [policy](../../../../../../VERSIONING.md).
|
||||
These features may be removed or modified in successive version releases, including patch versions.
|
||||
|
||||
When an experimental feature is promoted to a stable feature, a migration path will be included in the changelog entry of the release.
|
||||
There is no guarantee that any environment variable feature flags that enabled the experimental feature will be supported by the stable version.
|
||||
If they are supported, they may be accompanied with a deprecation notice stating a timeline for the removal of that support.
|
||||
Generated
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package x // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/x"
|
||||
|
||||
import "strings"
|
||||
|
||||
// Observability is an experimental feature flag that determines if exporter
|
||||
// observability metrics are enabled.
|
||||
//
|
||||
// To enable this feature set the OTEL_GO_X_OBSERVABILITY environment variable
|
||||
// to the case-insensitive string value of "true" (i.e. "True" and "TRUE"
|
||||
// will also enable this).
|
||||
var Observability = newFeature(
|
||||
[]string{"OBSERVABILITY"},
|
||||
func(v string) (string, bool) {
|
||||
if strings.EqualFold(v, "true") {
|
||||
return v, true
|
||||
}
|
||||
return "", false
|
||||
},
|
||||
)
|
||||
Generated
Vendored
+58
@@ -0,0 +1,58 @@
|
||||
// Code generated by gotmpl. DO NOT MODIFY.
|
||||
// source: internal/shared/x/x.go.tmpl
|
||||
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package x documents experimental features for [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/x].
|
||||
package x // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/x"
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Feature is an experimental feature control flag. It provides a uniform way
|
||||
// to interact with these feature flags and parse their values.
|
||||
type Feature[T any] struct {
|
||||
keys []string
|
||||
parse func(v string) (T, bool)
|
||||
}
|
||||
|
||||
func newFeature[T any](suffix []string, parse func(string) (T, bool)) Feature[T] {
|
||||
const envKeyRoot = "OTEL_GO_X_"
|
||||
keys := make([]string, 0, len(suffix))
|
||||
for _, s := range suffix {
|
||||
keys = append(keys, envKeyRoot+s)
|
||||
}
|
||||
return Feature[T]{
|
||||
keys: keys,
|
||||
parse: parse,
|
||||
}
|
||||
}
|
||||
|
||||
// Keys returns the environment variable keys that can be set to enable the
|
||||
// feature.
|
||||
func (f Feature[T]) Keys() []string { return f.keys }
|
||||
|
||||
// Lookup returns the user configured value for the feature and true if the
|
||||
// user has enabled the feature. Otherwise, if the feature is not enabled, a
|
||||
// zero-value and false are returned.
|
||||
func (f Feature[T]) Lookup() (v T, ok bool) {
|
||||
// https://github.com/open-telemetry/opentelemetry-specification/blob/62effed618589a0bec416a87e559c0a9d96289bb/specification/configuration/sdk-environment-variables.md#parsing-empty-value
|
||||
//
|
||||
// > The SDK MUST interpret an empty value of an environment variable the
|
||||
// > same way as when the variable is unset.
|
||||
for _, key := range f.keys {
|
||||
vRaw := os.Getenv(key)
|
||||
if vRaw != "" {
|
||||
return f.parse(vRaw)
|
||||
}
|
||||
}
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// Enabled reports whether the feature is enabled.
|
||||
func (f Feature[T]) Enabled() bool {
|
||||
_, ok := f.Lookup()
|
||||
return ok
|
||||
}
|
||||
Generated
Vendored
+18
@@ -87,6 +87,16 @@ func Value(v attribute.Value) *commonpb.AnyValue {
|
||||
av.Value = &commonpb.AnyValue_StringValue{
|
||||
StringValue: v.AsString(),
|
||||
}
|
||||
case attribute.BYTESLICE:
|
||||
av.Value = &commonpb.AnyValue_BytesValue{
|
||||
BytesValue: v.AsByteSlice(),
|
||||
}
|
||||
case attribute.SLICE:
|
||||
av.Value = &commonpb.AnyValue_ArrayValue{
|
||||
ArrayValue: &commonpb.ArrayValue{
|
||||
Values: values(v.AsSlice()),
|
||||
},
|
||||
}
|
||||
case attribute.STRINGSLICE:
|
||||
av.Value = &commonpb.AnyValue_ArrayValue{
|
||||
ArrayValue: &commonpb.ArrayValue{
|
||||
@@ -149,3 +159,11 @@ func stringSliceValues(vals []string) []*commonpb.AnyValue {
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func values(vals []attribute.Value) []*commonpb.AnyValue {
|
||||
converted := make([]*commonpb.AnyValue, len(vals))
|
||||
for i, v := range vals {
|
||||
converted[i] = Value(v)
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
+34
-18
@@ -6,6 +6,7 @@ package otlptracegrpc // import "go.opentelemetry.io/otel/exporters/otlp/otlptra
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal"
|
||||
@@ -26,11 +28,12 @@ import (
|
||||
)
|
||||
|
||||
type client struct {
|
||||
endpoint string
|
||||
dialOpts []grpc.DialOption
|
||||
metadata metadata.MD
|
||||
exportTimeout time.Duration
|
||||
requestFunc retry.RequestFunc
|
||||
endpoint string
|
||||
dialOpts []grpc.DialOption
|
||||
metadata metadata.MD
|
||||
exportTimeout time.Duration
|
||||
maxRequestSize int
|
||||
requestFunc retry.RequestFunc
|
||||
|
||||
// stopCtx is used as a parent context for all exports. Therefore, when it
|
||||
// is canceled with the stopFunc all exports are canceled.
|
||||
@@ -65,14 +68,15 @@ func newClient(opts ...Option) *client {
|
||||
ctx, cancel := context.WithCancel(context.Background()) //nolint:gosec // cancel called in client shutdown.
|
||||
|
||||
c := &client{
|
||||
endpoint: cfg.Traces.Endpoint,
|
||||
exportTimeout: cfg.Traces.Timeout,
|
||||
requestFunc: cfg.RetryConfig.RequestFunc(retryable),
|
||||
dialOpts: cfg.DialOptions,
|
||||
stopCtx: ctx,
|
||||
stopFunc: cancel,
|
||||
conn: cfg.GRPCConn,
|
||||
instID: counter.NextExporterID(),
|
||||
endpoint: cfg.Traces.Endpoint,
|
||||
exportTimeout: cfg.Traces.Timeout,
|
||||
maxRequestSize: cfg.Traces.MaxRequestSize,
|
||||
requestFunc: cfg.RetryConfig.RequestFunc(retryable),
|
||||
dialOpts: cfg.DialOptions,
|
||||
stopCtx: ctx,
|
||||
stopFunc: cancel,
|
||||
conn: cfg.GRPCConn,
|
||||
instID: counter.NextExporterID(),
|
||||
}
|
||||
|
||||
if len(cfg.Traces.Headers) > 0 {
|
||||
@@ -205,16 +209,28 @@ func (c *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
|
||||
ctx, cancel := c.exportContext(ctx)
|
||||
defer cancel()
|
||||
|
||||
var code codes.Code
|
||||
pbRequest := &coltracepb.ExportTraceServiceRequest{
|
||||
ResourceSpans: protoSpans,
|
||||
}
|
||||
|
||||
code := codes.Unknown
|
||||
if c.inst != nil {
|
||||
op := c.inst.ExportSpans(ctx, len(protoSpans))
|
||||
var spanCount int
|
||||
for _, rs := range protoSpans {
|
||||
for _, ss := range rs.ScopeSpans {
|
||||
spanCount += len(ss.Spans)
|
||||
}
|
||||
}
|
||||
op := c.inst.ExportSpans(ctx, spanCount)
|
||||
defer func() { op.End(uploadErr, code) }()
|
||||
}
|
||||
|
||||
if maxSize := c.maxRequestSize; maxSize > 0 && proto.Size(pbRequest) > maxSize {
|
||||
return fmt.Errorf("request message too large: exceeded %d bytes", maxSize)
|
||||
}
|
||||
|
||||
return c.requestFunc(ctx, func(iCtx context.Context) error {
|
||||
resp, err := c.tsc.Export(iCtx, &coltracepb.ExportTraceServiceRequest{
|
||||
ResourceSpans: protoSpans,
|
||||
})
|
||||
resp, err := c.tsc.Export(iCtx, pbRequest)
|
||||
if resp != nil && resp.PartialSuccess != nil {
|
||||
msg := resp.PartialSuccess.GetErrorMessage()
|
||||
n := resp.PartialSuccess.GetRejectedSpans()
|
||||
|
||||
Generated
Vendored
+7
-3
@@ -18,8 +18,8 @@ import (
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/x"
|
||||
"go.opentelemetry.io/otel/internal/global"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.40.0/otelconv"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.41.0/otelconv"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -72,6 +72,7 @@ var (
|
||||
func get[T any](p *sync.Pool) *[]T { return p.Get().(*[]T) }
|
||||
|
||||
func put[T any](p *sync.Pool, s *[]T) {
|
||||
clear(*s) // erase elements to allow GC to collect what they refer to.
|
||||
*s = (*s)[:0] // Reset.
|
||||
p.Put(s)
|
||||
}
|
||||
@@ -339,7 +340,10 @@ var errPartialPool = &sync.Pool{
|
||||
// the provided non-nil err.
|
||||
func rejected(n int64, err error) int64 {
|
||||
ps := errPartialPool.Get().(*internal.PartialSuccess)
|
||||
defer errPartialPool.Put(ps)
|
||||
defer func() {
|
||||
*ps = internal.PartialSuccess{} // erase fields to allow GC to collect them.
|
||||
errPartialPool.Put(ps)
|
||||
}()
|
||||
// Check for partial success.
|
||||
if errors.As(err, ps) {
|
||||
// Bound RejectedItems to [0, n]. This should not be needed,
|
||||
|
||||
Generated
Vendored
+28
-15
@@ -31,6 +31,9 @@ const (
|
||||
// DefaultTracesPath is a default URL path for endpoint that
|
||||
// receives spans.
|
||||
DefaultTracesPath string = "/v1/traces"
|
||||
// DefaultMaxRequestSize is the default maximum size of a serialized export
|
||||
// request, before compression.
|
||||
DefaultMaxRequestSize int = 64 * 1024 * 1024
|
||||
// DefaultTimeout is a default max waiting time for the backend to process
|
||||
// each span batch.
|
||||
DefaultTimeout time.Duration = 10 * time.Second
|
||||
@@ -42,13 +45,14 @@ type (
|
||||
HTTPTransportProxyFunc func(*http.Request) (*url.URL, error)
|
||||
|
||||
SignalConfig struct {
|
||||
Endpoint string
|
||||
Insecure bool
|
||||
TLSCfg *tls.Config
|
||||
Headers map[string]string
|
||||
Compression Compression
|
||||
Timeout time.Duration
|
||||
URLPath string
|
||||
Endpoint string
|
||||
Insecure bool
|
||||
TLSCfg *tls.Config
|
||||
Headers map[string]string
|
||||
Compression Compression
|
||||
MaxRequestSize int
|
||||
Timeout time.Duration
|
||||
URLPath string
|
||||
|
||||
// gRPC configurations
|
||||
GRPCCredentials credentials.TransportCredentials
|
||||
@@ -77,10 +81,11 @@ type (
|
||||
func NewHTTPConfig(opts ...HTTPOption) Config {
|
||||
cfg := Config{
|
||||
Traces: SignalConfig{
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorHTTPPort),
|
||||
URLPath: DefaultTracesPath,
|
||||
Compression: NoCompression,
|
||||
Timeout: DefaultTimeout,
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorHTTPPort),
|
||||
URLPath: DefaultTracesPath,
|
||||
Compression: NoCompression,
|
||||
MaxRequestSize: DefaultMaxRequestSize,
|
||||
Timeout: DefaultTimeout,
|
||||
},
|
||||
RetryConfig: retry.DefaultConfig,
|
||||
}
|
||||
@@ -111,10 +116,11 @@ func NewGRPCConfig(opts ...GRPCOption) Config {
|
||||
userAgent := "OTel OTLP Exporter Go/" + otlptrace.Version()
|
||||
cfg := Config{
|
||||
Traces: SignalConfig{
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorGRPCPort),
|
||||
URLPath: DefaultTracesPath,
|
||||
Compression: NoCompression,
|
||||
Timeout: DefaultTimeout,
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorGRPCPort),
|
||||
URLPath: DefaultTracesPath,
|
||||
Compression: NoCompression,
|
||||
MaxRequestSize: DefaultMaxRequestSize,
|
||||
Timeout: DefaultTimeout,
|
||||
},
|
||||
RetryConfig: retry.DefaultConfig,
|
||||
DialOptions: []grpc.DialOption{grpc.WithUserAgent(userAgent)},
|
||||
@@ -345,6 +351,13 @@ func WithTimeout(duration time.Duration) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
func WithMaxRequestSize(size int) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Traces.MaxRequestSize = size
|
||||
return cfg
|
||||
})
|
||||
}
|
||||
|
||||
func WithProxy(pf HTTPTransportProxyFunc) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Traces.Proxy = pf
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -5,4 +5,4 @@ package internal // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/ot
|
||||
|
||||
// Version is the current release version of the OpenTelemetry OTLP gRPC trace
|
||||
// exporter in use.
|
||||
const Version = "1.43.0"
|
||||
const Version = "1.44.0"
|
||||
|
||||
Generated
Vendored
+10
@@ -192,6 +192,16 @@ func WithTimeout(duration time.Duration) Option {
|
||||
return wrappedOption{otlpconfig.WithTimeout(duration)}
|
||||
}
|
||||
|
||||
// WithMaxRequestSize sets the maximum size, in bytes, of a serialized export
|
||||
// request, before compression, that the exporter will send.
|
||||
//
|
||||
// If size is less than or equal to zero, no request-size limit is applied.
|
||||
// Disabling the limit is not recommended because it can lead to excessive
|
||||
// resource consumption or abuse.
|
||||
func WithMaxRequestSize(size int) Option {
|
||||
return wrappedOption{otlpconfig.WithMaxRequestSize(size)}
|
||||
}
|
||||
|
||||
// WithRetry sets the retry policy for transient retryable errors that may be
|
||||
// returned by the target endpoint when exporting a batch of spans.
|
||||
//
|
||||
|
||||
+36
-22
@@ -143,9 +143,9 @@ func (c *client) Start(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Stop shuts down the client and interrupt any in-flight request.
|
||||
func (d *client) Stop(ctx context.Context) error {
|
||||
d.stopOnce.Do(func() {
|
||||
close(d.stopCh)
|
||||
func (c *client) Stop(ctx context.Context) error {
|
||||
c.stopOnce.Do(func() {
|
||||
close(c.stopCh)
|
||||
})
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -156,7 +156,7 @@ func (d *client) Stop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// UploadTraces sends a batch of spans to the collector.
|
||||
func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.ResourceSpans) (uploadErr error) {
|
||||
func (c *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.ResourceSpans) (uploadErr error) {
|
||||
pbRequest := &coltracepb.ExportTraceServiceRequest{
|
||||
ResourceSpans: protoSpans,
|
||||
}
|
||||
@@ -165,30 +165,41 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := d.contextWithStop(ctx)
|
||||
ctx, cancel := c.contextWithStop(ctx)
|
||||
defer cancel()
|
||||
|
||||
request, err := d.newRequest(rawRequest)
|
||||
if maxSize := c.cfg.MaxRequestSize; maxSize > 0 && len(rawRequest) > maxSize {
|
||||
return fmt.Errorf("request body too large: exceeded %d bytes", maxSize)
|
||||
}
|
||||
|
||||
request, err := c.newRequest(rawRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var statusCode int
|
||||
if d.inst != nil {
|
||||
op := d.inst.ExportSpans(ctx, len(protoSpans))
|
||||
if c.inst != nil {
|
||||
var spanCount int
|
||||
for _, rs := range protoSpans {
|
||||
for _, ss := range rs.ScopeSpans {
|
||||
spanCount += len(ss.Spans)
|
||||
}
|
||||
}
|
||||
op := c.inst.ExportSpans(ctx, spanCount)
|
||||
defer func() { op.End(uploadErr, statusCode) }()
|
||||
}
|
||||
|
||||
return errors.Join(uploadErr, d.requestFunc(ctx, func(ctx context.Context) error {
|
||||
return errors.Join(uploadErr, c.requestFunc(ctx, func(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
statusCode = 0
|
||||
request.reset(ctx)
|
||||
// nolint:gosec // URL is constructed from validated OTLP endpoint configuration
|
||||
resp, err := d.client.Do(request.Request)
|
||||
resp, err := c.client.Do(request.Request)
|
||||
var urlErr *url.Error
|
||||
if errors.As(err, &urlErr) && urlErr.Temporary() {
|
||||
return newResponseError(http.Header{}, err)
|
||||
@@ -272,8 +283,8 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
|
||||
}))
|
||||
}
|
||||
|
||||
func (d *client) newRequest(body []byte) (request, error) {
|
||||
u := url.URL{Scheme: d.getScheme(), Host: d.cfg.Endpoint, Path: d.cfg.URLPath}
|
||||
func (c *client) newRequest(body []byte) (request, error) {
|
||||
u := url.URL{Scheme: c.getScheme(), Host: c.cfg.Endpoint, Path: c.cfg.URLPath}
|
||||
r, err := http.NewRequestWithContext(context.Background(), http.MethodPost, u.String(), http.NoBody)
|
||||
if err != nil {
|
||||
return request{Request: r}, err
|
||||
@@ -282,13 +293,13 @@ func (d *client) newRequest(body []byte) (request, error) {
|
||||
userAgent := "OTel OTLP Exporter Go/" + otlptrace.Version()
|
||||
r.Header.Set("User-Agent", userAgent)
|
||||
|
||||
for k, v := range d.cfg.Headers {
|
||||
for k, v := range c.cfg.Headers {
|
||||
r.Header.Set(k, v)
|
||||
}
|
||||
r.Header.Set("Content-Type", contentTypeProto)
|
||||
|
||||
req := request{Request: r}
|
||||
switch Compression(d.cfg.Compression) {
|
||||
switch Compression(c.cfg.Compression) {
|
||||
case NoCompression:
|
||||
r.ContentLength = int64(len(body))
|
||||
req.bodyReader = bodyReader(body)
|
||||
@@ -299,7 +310,10 @@ func (d *client) newRequest(body []byte) (request, error) {
|
||||
r.Header.Set("Content-Encoding", "gzip")
|
||||
|
||||
gz := gzPool.Get().(*gzip.Writer)
|
||||
defer gzPool.Put(gz)
|
||||
defer func() {
|
||||
gz.Reset(io.Discard)
|
||||
gzPool.Put(gz)
|
||||
}()
|
||||
|
||||
var b bytes.Buffer
|
||||
gz.Reset(&b)
|
||||
@@ -320,15 +334,15 @@ func (d *client) newRequest(body []byte) (request, error) {
|
||||
}
|
||||
|
||||
// MarshalLog is the marshaling function used by the logging system to represent this Client.
|
||||
func (d *client) MarshalLog() any {
|
||||
func (c *client) MarshalLog() any {
|
||||
return struct {
|
||||
Type string
|
||||
Endpoint string
|
||||
Insecure bool
|
||||
}{
|
||||
Type: "otlptracehttp",
|
||||
Endpoint: d.cfg.Endpoint,
|
||||
Insecure: d.cfg.Insecure,
|
||||
Endpoint: c.cfg.Endpoint,
|
||||
Insecure: c.cfg.Insecure,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,14 +439,14 @@ func evaluate(err error) (bool, time.Duration) {
|
||||
return true, time.Duration(rErr.throttle)
|
||||
}
|
||||
|
||||
func (d *client) getScheme() string {
|
||||
if d.cfg.Insecure {
|
||||
func (c *client) getScheme() string {
|
||||
if c.cfg.Insecure {
|
||||
return "http"
|
||||
}
|
||||
return "https"
|
||||
}
|
||||
|
||||
func (d *client) contextWithStop(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
func (c *client) contextWithStop(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||
// Unify the parent context Done signal with the client's stop
|
||||
// channel.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
@@ -441,7 +455,7 @@ func (d *client) contextWithStop(ctx context.Context) (context.Context, context.
|
||||
case <-ctx.Done():
|
||||
// Nothing to do, either cancelled or deadline
|
||||
// happened.
|
||||
case <-d.stopCh:
|
||||
case <-c.stopCh:
|
||||
cancel()
|
||||
}
|
||||
}(ctx, cancel)
|
||||
|
||||
Generated
Vendored
+12
-6
@@ -23,8 +23,8 @@ import (
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/x"
|
||||
"go.opentelemetry.io/otel/internal/global"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.40.0/otelconv"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.41.0/otelconv"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -77,6 +77,7 @@ var (
|
||||
func get[T any](p *sync.Pool) *[]T { return p.Get().(*[]T) }
|
||||
|
||||
func put[T any](p *sync.Pool, s *[]T) {
|
||||
clear(*s) // erase elements to allow GC to collect what they refer to.
|
||||
*s = (*s)[:0] // Reset.
|
||||
p.Put(s)
|
||||
}
|
||||
@@ -167,7 +168,7 @@ func NewInstrumentation(id int64, endpoint string) (*Instrumentation, error) {
|
||||
// to set the "component.name" attribute.
|
||||
//
|
||||
// The endpoint is the HTTP endpoint the exporter is exporting to. It should be
|
||||
// in the format "host:port" or a full URL.
|
||||
// in the format "host[:port]".
|
||||
func BaseAttrs(id int64, endpoint string) []attribute.KeyValue {
|
||||
host, port, err := parseEndpoint(endpoint)
|
||||
if err != nil || (host == "" && port < 0) {
|
||||
@@ -345,7 +346,7 @@ func (e ExportOp) End(err error, status int) {
|
||||
//
|
||||
// Otherwise, a new RecordOption is returned with the base attributes of the
|
||||
// Instrumentation plus the http.response.status_code attribute set to the
|
||||
// provided status, and if err is not nil, the error.type attribute set
|
||||
// provided status (if non-zero), and if err is not nil, the error.type attribute set
|
||||
// to the type of the error.
|
||||
func (i *Instrumentation) recordOption(err error, status int) metric.RecordOption {
|
||||
if err == nil && status == http.StatusOK {
|
||||
@@ -356,7 +357,9 @@ func (i *Instrumentation) recordOption(err error, status int) metric.RecordOptio
|
||||
defer put(measureAttrsPool, attrs)
|
||||
*attrs = append(*attrs, i.attrs...)
|
||||
|
||||
*attrs = append(*attrs, semconv.HTTPResponseStatusCode(status))
|
||||
if status != 0 {
|
||||
*attrs = append(*attrs, semconv.HTTPResponseStatusCode(status))
|
||||
}
|
||||
if err != nil {
|
||||
*attrs = append(*attrs, semconv.ErrorType(err))
|
||||
}
|
||||
@@ -394,7 +397,10 @@ var errPartialPool = &sync.Pool{
|
||||
// the provided non-nil err.
|
||||
func rejected(n int64, err error) int64 {
|
||||
ps := errPartialPool.Get().(*internal.PartialSuccess)
|
||||
defer errPartialPool.Put(ps)
|
||||
defer func() {
|
||||
*ps = internal.PartialSuccess{} // erase fields to allow GC to collect them.
|
||||
errPartialPool.Put(ps)
|
||||
}()
|
||||
// Check for partial success.
|
||||
if errors.As(err, ps) {
|
||||
// Bound RejectedItems to [0, n]. This should not be needed,
|
||||
|
||||
Generated
Vendored
+28
-15
@@ -31,6 +31,9 @@ const (
|
||||
// DefaultTracesPath is a default URL path for endpoint that
|
||||
// receives spans.
|
||||
DefaultTracesPath string = "/v1/traces"
|
||||
// DefaultMaxRequestSize is the default maximum size of a serialized export
|
||||
// request, before compression.
|
||||
DefaultMaxRequestSize int = 64 * 1024 * 1024
|
||||
// DefaultTimeout is a default max waiting time for the backend to process
|
||||
// each span batch.
|
||||
DefaultTimeout time.Duration = 10 * time.Second
|
||||
@@ -42,13 +45,14 @@ type (
|
||||
HTTPTransportProxyFunc func(*http.Request) (*url.URL, error)
|
||||
|
||||
SignalConfig struct {
|
||||
Endpoint string
|
||||
Insecure bool
|
||||
TLSCfg *tls.Config
|
||||
Headers map[string]string
|
||||
Compression Compression
|
||||
Timeout time.Duration
|
||||
URLPath string
|
||||
Endpoint string
|
||||
Insecure bool
|
||||
TLSCfg *tls.Config
|
||||
Headers map[string]string
|
||||
Compression Compression
|
||||
MaxRequestSize int
|
||||
Timeout time.Duration
|
||||
URLPath string
|
||||
|
||||
// gRPC configurations
|
||||
GRPCCredentials credentials.TransportCredentials
|
||||
@@ -77,10 +81,11 @@ type (
|
||||
func NewHTTPConfig(opts ...HTTPOption) Config {
|
||||
cfg := Config{
|
||||
Traces: SignalConfig{
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorHTTPPort),
|
||||
URLPath: DefaultTracesPath,
|
||||
Compression: NoCompression,
|
||||
Timeout: DefaultTimeout,
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorHTTPPort),
|
||||
URLPath: DefaultTracesPath,
|
||||
Compression: NoCompression,
|
||||
MaxRequestSize: DefaultMaxRequestSize,
|
||||
Timeout: DefaultTimeout,
|
||||
},
|
||||
RetryConfig: retry.DefaultConfig,
|
||||
}
|
||||
@@ -111,10 +116,11 @@ func NewGRPCConfig(opts ...GRPCOption) Config {
|
||||
userAgent := "OTel OTLP Exporter Go/" + otlptrace.Version()
|
||||
cfg := Config{
|
||||
Traces: SignalConfig{
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorGRPCPort),
|
||||
URLPath: DefaultTracesPath,
|
||||
Compression: NoCompression,
|
||||
Timeout: DefaultTimeout,
|
||||
Endpoint: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorGRPCPort),
|
||||
URLPath: DefaultTracesPath,
|
||||
Compression: NoCompression,
|
||||
MaxRequestSize: DefaultMaxRequestSize,
|
||||
Timeout: DefaultTimeout,
|
||||
},
|
||||
RetryConfig: retry.DefaultConfig,
|
||||
DialOptions: []grpc.DialOption{grpc.WithUserAgent(userAgent)},
|
||||
@@ -345,6 +351,13 @@ func WithTimeout(duration time.Duration) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
func WithMaxRequestSize(size int) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Traces.MaxRequestSize = size
|
||||
return cfg
|
||||
})
|
||||
}
|
||||
|
||||
func WithProxy(pf HTTPTransportProxyFunc) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Traces.Proxy = pf
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -5,4 +5,4 @@ package internal // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/ot
|
||||
|
||||
// Version is the current release version of the OpenTelemetry OTLP HTTP trace
|
||||
// exporter in use.
|
||||
const Version = "1.43.0"
|
||||
const Version = "1.44.0"
|
||||
|
||||
Generated
Vendored
+10
@@ -138,6 +138,16 @@ func WithTimeout(duration time.Duration) Option {
|
||||
return wrappedOption{otlpconfig.WithTimeout(duration)}
|
||||
}
|
||||
|
||||
// WithMaxRequestSize sets the maximum size, in bytes, of a serialized export
|
||||
// request, before compression, that the exporter will send.
|
||||
//
|
||||
// If size is less than or equal to zero, no request-size limit is applied.
|
||||
// Disabling the limit is not recommended because it can lead to excessive
|
||||
// resource consumption or abuse.
|
||||
func WithMaxRequestSize(size int) Option {
|
||||
return wrappedOption{otlpconfig.WithMaxRequestSize(size)}
|
||||
}
|
||||
|
||||
// WithRetry configures the retry policy for transient errors that may occurs
|
||||
// when exporting traces. An exponential back-off algorithm is used to ensure
|
||||
// endpoints are not overwhelmed with retries. If unset, the default retry
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@ package otlptrace // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||||
|
||||
// Version is the current release version of the OpenTelemetry OTLP trace exporter in use.
|
||||
func Version() string {
|
||||
return "1.43.0"
|
||||
return "1.44.0"
|
||||
}
|
||||
|
||||
+9
@@ -51,6 +51,9 @@ type Float64ObservableCounterConfig struct {
|
||||
func NewFloat64ObservableCounterConfig(opts ...Float64ObservableCounterOption) Float64ObservableCounterConfig {
|
||||
var config Float64ObservableCounterConfig
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyFloat64ObservableCounter(config)
|
||||
}
|
||||
return config
|
||||
@@ -111,6 +114,9 @@ func NewFloat64ObservableUpDownCounterConfig(
|
||||
) Float64ObservableUpDownCounterConfig {
|
||||
var config Float64ObservableUpDownCounterConfig
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyFloat64ObservableUpDownCounter(config)
|
||||
}
|
||||
return config
|
||||
@@ -168,6 +174,9 @@ type Float64ObservableGaugeConfig struct {
|
||||
func NewFloat64ObservableGaugeConfig(opts ...Float64ObservableGaugeOption) Float64ObservableGaugeConfig {
|
||||
var config Float64ObservableGaugeConfig
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyFloat64ObservableGauge(config)
|
||||
}
|
||||
return config
|
||||
|
||||
+9
@@ -50,6 +50,9 @@ type Int64ObservableCounterConfig struct {
|
||||
func NewInt64ObservableCounterConfig(opts ...Int64ObservableCounterOption) Int64ObservableCounterConfig {
|
||||
var config Int64ObservableCounterConfig
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyInt64ObservableCounter(config)
|
||||
}
|
||||
return config
|
||||
@@ -110,6 +113,9 @@ func NewInt64ObservableUpDownCounterConfig(
|
||||
) Int64ObservableUpDownCounterConfig {
|
||||
var config Int64ObservableUpDownCounterConfig
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyInt64ObservableUpDownCounter(config)
|
||||
}
|
||||
return config
|
||||
@@ -167,6 +173,9 @@ type Int64ObservableGaugeConfig struct {
|
||||
func NewInt64ObservableGaugeConfig(opts ...Int64ObservableGaugeOption) Int64ObservableGaugeConfig {
|
||||
var config Int64ObservableGaugeConfig
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyInt64ObservableGauge(config)
|
||||
}
|
||||
return config
|
||||
|
||||
+7
@@ -42,11 +42,18 @@ type MeterOption interface {
|
||||
applyMeter(MeterConfig) MeterConfig
|
||||
}
|
||||
|
||||
type experimentalOption interface {
|
||||
Experimental()
|
||||
}
|
||||
|
||||
// NewMeterConfig creates a new MeterConfig and applies
|
||||
// all the given options.
|
||||
func NewMeterConfig(opts ...MeterOption) MeterConfig {
|
||||
var config MeterConfig
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyMeter(config)
|
||||
}
|
||||
return config
|
||||
|
||||
+39
-12
@@ -24,10 +24,10 @@ all instruments fall into two overlapping logical categories: asynchronous or
|
||||
synchronous, and int64 or float64.
|
||||
|
||||
All synchronous instruments ([Int64Counter], [Int64UpDownCounter],
|
||||
[Int64Histogram], [Float64Counter], [Float64UpDownCounter], and
|
||||
[Float64Histogram]) are used to measure the operation and performance of source
|
||||
code during the source code execution. These instruments only make measurements
|
||||
when the source code they instrument is run.
|
||||
[Int64Histogram], [Int64Gauge], [Float64Counter], [Float64UpDownCounter],
|
||||
[Float64Histogram], and [Float64Gauge]) are used to measure the operation and
|
||||
performance of source code during the source code execution. These instruments
|
||||
only make measurements when the source code they instrument is run.
|
||||
|
||||
All asynchronous instruments ([Int64ObservableCounter],
|
||||
[Int64ObservableUpDownCounter], [Int64ObservableGauge],
|
||||
@@ -50,9 +50,11 @@ incrementally increase in value. UpDownCounters ([Int64UpDownCounter],
|
||||
values that can increase and decrease. When more information needs to be
|
||||
conveyed about all the synchronous measurements made during a collection cycle,
|
||||
a Histogram ([Int64Histogram] and [Float64Histogram]) should be used. Finally,
|
||||
when just the most recent measurement needs to be conveyed about an
|
||||
asynchronous measurement, a Gauge ([Int64ObservableGauge] and
|
||||
[Float64ObservableGauge]) should be used.
|
||||
when just the most recent measurement needs to be conveyed, a Gauge
|
||||
([Int64Gauge], [Float64Gauge], [Int64ObservableGauge], and
|
||||
[Float64ObservableGauge]) should be used: the synchronous variants record an
|
||||
instantaneous value at a specific point in code, while the observable variants
|
||||
sample the value via a callback once per collection cycle.
|
||||
|
||||
See the [OpenTelemetry documentation] for more information about instruments
|
||||
and their intended use.
|
||||
@@ -80,11 +82,11 @@ Measurements are made by recording values and information about the values with
|
||||
an instrument. How these measurements are recorded depends on the instrument.
|
||||
|
||||
Measurements for synchronous instruments ([Int64Counter], [Int64UpDownCounter],
|
||||
[Int64Histogram], [Float64Counter], [Float64UpDownCounter], and
|
||||
[Float64Histogram]) are recorded using the instrument methods directly. All
|
||||
counter instruments have an Add method that is used to measure an increment
|
||||
value, and all histogram instruments have a Record method to measure a data
|
||||
point.
|
||||
[Int64Histogram], [Int64Gauge], [Float64Counter], [Float64UpDownCounter],
|
||||
[Float64Histogram], and [Float64Gauge]) are recorded using the instrument
|
||||
methods directly. All counter instruments have an Add method that is used to
|
||||
measure an increment value, and all histogram and synchronous gauge
|
||||
instruments have a Record method to measure a data point.
|
||||
|
||||
Asynchronous instruments ([Int64ObservableCounter],
|
||||
[Int64ObservableUpDownCounter], [Int64ObservableGauge],
|
||||
@@ -107,6 +109,31 @@ respectively):
|
||||
If the criteria are not met, use the RegisterCallback method of the [Meter] that
|
||||
created the instrument to register a [Callback].
|
||||
|
||||
# Avoiding Expensive Computations
|
||||
|
||||
All synchronous instruments provide an Enabled method that reports whether the
|
||||
instrument will process measurements for the given context. When no SDK is
|
||||
registered or the instrument is otherwise disabled, Enabled returns false. This
|
||||
can be used to avoid expensive measurement work when a measurement will not be
|
||||
recorded:
|
||||
|
||||
if counter.Enabled(ctx) {
|
||||
counter.Add(ctx, 1, metric.WithAttributes(expensiveAttributes()...))
|
||||
}
|
||||
|
||||
This is especially valuable when computing attributes is expensive.
|
||||
[WithAttributes] performs non-trivial work on every call to build an
|
||||
[attribute.Set] from the provided attributes, and that work is wasted if the
|
||||
measurement is not recorded.
|
||||
|
||||
For performance sensitive code where the same attribute set is used repeatedly,
|
||||
prefer [WithAttributeSet]. It accepts a pre-built [attribute.Set], letting you
|
||||
pay the construction cost once and reuse it across many measurements:
|
||||
|
||||
attrs := attribute.NewSet(attribute.String("key", "val"))
|
||||
// ... later, on each call:
|
||||
counter.Add(ctx, 1, metric.WithAttributeSet(attrs))
|
||||
|
||||
# API Implementations
|
||||
|
||||
This package does not conform to the standard Go versioning policy, all of its
|
||||
|
||||
+33
-6
@@ -3,7 +3,9 @@
|
||||
|
||||
package metric // import "go.opentelemetry.io/otel/metric"
|
||||
|
||||
import "go.opentelemetry.io/otel/attribute"
|
||||
import (
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
// Observable is used as a grouping mechanism for all instruments that are
|
||||
// updated within a Callback.
|
||||
@@ -228,6 +230,9 @@ type AddConfig struct {
|
||||
func NewAddConfig(opts []AddOption) AddConfig {
|
||||
config := AddConfig{attrs: *attribute.EmptySet()}
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyAdd(config)
|
||||
}
|
||||
return config
|
||||
@@ -253,6 +258,9 @@ type RecordConfig struct {
|
||||
func NewRecordConfig(opts []RecordOption) RecordConfig {
|
||||
config := RecordConfig{attrs: *attribute.EmptySet()}
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyRecord(config)
|
||||
}
|
||||
return config
|
||||
@@ -278,6 +286,9 @@ type ObserveConfig struct {
|
||||
func NewObserveConfig(opts []ObserveOption) ObserveConfig {
|
||||
config := ObserveConfig{attrs: *attribute.EmptySet()}
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyObserve(config)
|
||||
}
|
||||
return config
|
||||
@@ -299,6 +310,10 @@ type attrOpt struct {
|
||||
set attribute.Set
|
||||
}
|
||||
|
||||
func (o *attrOpt) Set(set attribute.Set) {
|
||||
o.set = set
|
||||
}
|
||||
|
||||
// mergeSets returns the union of keys between a and b. Any duplicate keys will
|
||||
// use the value associated with b.
|
||||
func mergeSets(a, b attribute.Set) attribute.Set {
|
||||
@@ -311,7 +326,7 @@ func mergeSets(a, b attribute.Set) attribute.Set {
|
||||
return attribute.NewSet(merged...)
|
||||
}
|
||||
|
||||
func (o attrOpt) applyAdd(c AddConfig) AddConfig {
|
||||
func (o *attrOpt) applyAdd(c AddConfig) AddConfig {
|
||||
switch {
|
||||
case o.set.Len() == 0:
|
||||
case c.attrs.Len() == 0:
|
||||
@@ -322,7 +337,7 @@ func (o attrOpt) applyAdd(c AddConfig) AddConfig {
|
||||
return c
|
||||
}
|
||||
|
||||
func (o attrOpt) applyRecord(c RecordConfig) RecordConfig {
|
||||
func (o *attrOpt) applyRecord(c RecordConfig) RecordConfig {
|
||||
switch {
|
||||
case o.set.Len() == 0:
|
||||
case c.attrs.Len() == 0:
|
||||
@@ -333,7 +348,7 @@ func (o attrOpt) applyRecord(c RecordConfig) RecordConfig {
|
||||
return c
|
||||
}
|
||||
|
||||
func (o attrOpt) applyObserve(c ObserveConfig) ObserveConfig {
|
||||
func (o *attrOpt) applyObserve(c ObserveConfig) ObserveConfig {
|
||||
switch {
|
||||
case o.set.Len() == 0:
|
||||
case c.attrs.Len() == 0:
|
||||
@@ -350,8 +365,14 @@ func (o attrOpt) applyObserve(c ObserveConfig) ObserveConfig {
|
||||
// If multiple WithAttributeSet or WithAttributes options are passed the
|
||||
// attributes will be merged together in the order they are passed. Attributes
|
||||
// with duplicate keys will use the last value passed.
|
||||
//
|
||||
// Experimental: The returned option may implement
|
||||
// [go.opentelemetry.io/otel/metric/x.Settable][attribute.Set], which can be
|
||||
// used to replace the option's attribute set and reuse the option without
|
||||
// additional allocations. This behavior is experimental and may be changed or
|
||||
// removed in a future release without notice.
|
||||
func WithAttributeSet(attributes attribute.Set) MeasurementOption {
|
||||
return attrOpt{set: attributes}
|
||||
return &attrOpt{set: attributes}
|
||||
}
|
||||
|
||||
// WithAttributes converts attributes into an attribute Set and sets the Set to
|
||||
@@ -369,8 +390,14 @@ func WithAttributeSet(attributes attribute.Set) MeasurementOption {
|
||||
//
|
||||
// See [WithAttributeSet] for information about how multiple WithAttributes are
|
||||
// merged.
|
||||
//
|
||||
// Experimental: The returned option may implement
|
||||
// [go.opentelemetry.io/otel/metric/x.Settable][[]attribute.KeyValue], which can be
|
||||
// used to replace the option's attributes and reuse the option without
|
||||
// additional allocations. This behavior is experimental and may be changed or
|
||||
// removed in a future release without notice.
|
||||
func WithAttributes(attributes ...attribute.KeyValue) MeasurementOption {
|
||||
cp := make([]attribute.KeyValue, len(attributes))
|
||||
copy(cp, attributes)
|
||||
return attrOpt{set: attribute.NewSet(cp...)}
|
||||
return &attrOpt{set: attribute.NewSet(cp...)}
|
||||
}
|
||||
|
||||
+12
@@ -51,6 +51,9 @@ type Float64CounterConfig struct {
|
||||
func NewFloat64CounterConfig(opts ...Float64CounterOption) Float64CounterConfig {
|
||||
var config Float64CounterConfig
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyFloat64Counter(config)
|
||||
}
|
||||
return config
|
||||
@@ -116,6 +119,9 @@ type Float64UpDownCounterConfig struct {
|
||||
func NewFloat64UpDownCounterConfig(opts ...Float64UpDownCounterOption) Float64UpDownCounterConfig {
|
||||
var config Float64UpDownCounterConfig
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyFloat64UpDownCounter(config)
|
||||
}
|
||||
return config
|
||||
@@ -182,6 +188,9 @@ type Float64HistogramConfig struct {
|
||||
func NewFloat64HistogramConfig(opts ...Float64HistogramOption) Float64HistogramConfig {
|
||||
var config Float64HistogramConfig
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyFloat64Histogram(config)
|
||||
}
|
||||
return config
|
||||
@@ -251,6 +260,9 @@ type Float64GaugeConfig struct {
|
||||
func NewFloat64GaugeConfig(opts ...Float64GaugeOption) Float64GaugeConfig {
|
||||
var config Float64GaugeConfig
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyFloat64Gauge(config)
|
||||
}
|
||||
return config
|
||||
|
||||
+12
@@ -51,6 +51,9 @@ type Int64CounterConfig struct {
|
||||
func NewInt64CounterConfig(opts ...Int64CounterOption) Int64CounterConfig {
|
||||
var config Int64CounterConfig
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyInt64Counter(config)
|
||||
}
|
||||
return config
|
||||
@@ -116,6 +119,9 @@ type Int64UpDownCounterConfig struct {
|
||||
func NewInt64UpDownCounterConfig(opts ...Int64UpDownCounterOption) Int64UpDownCounterConfig {
|
||||
var config Int64UpDownCounterConfig
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyInt64UpDownCounter(config)
|
||||
}
|
||||
return config
|
||||
@@ -182,6 +188,9 @@ type Int64HistogramConfig struct {
|
||||
func NewInt64HistogramConfig(opts ...Int64HistogramOption) Int64HistogramConfig {
|
||||
var config Int64HistogramConfig
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyInt64Histogram(config)
|
||||
}
|
||||
return config
|
||||
@@ -251,6 +260,9 @@ type Int64GaugeConfig struct {
|
||||
func NewInt64GaugeConfig(opts ...Int64GaugeOption) Int64GaugeConfig {
|
||||
var config Int64GaugeConfig
|
||||
for _, o := range opts {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
config = o.applyInt64Gauge(config)
|
||||
}
|
||||
return config
|
||||
|
||||
+60
-12
@@ -5,6 +5,9 @@ package propagation // import "go.opentelemetry.io/otel/propagation"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"go.opentelemetry.io/otel/baggage"
|
||||
"go.opentelemetry.io/otel/internal/errorhandler"
|
||||
@@ -13,11 +16,18 @@ import (
|
||||
const (
|
||||
baggageHeader = "baggage"
|
||||
|
||||
maxParseErrors = 5
|
||||
|
||||
// W3C Baggage specification limits.
|
||||
// https://www.w3.org/TR/baggage/#limits
|
||||
maxMembers = 64
|
||||
maxMembers = 64
|
||||
maxBytesPerBaggageString = 8192
|
||||
)
|
||||
|
||||
// handleExtractErrOnce limits error reporting for attacker-controlled baggage headers
|
||||
// to one process-wide emission, preventing repeated extraction from flooding logs.
|
||||
var handleExtractErrOnce sync.Once
|
||||
|
||||
// Baggage is a propagator that supports the W3C Baggage format.
|
||||
//
|
||||
// This propagates user-defined baggage associated with a trace. The complete
|
||||
@@ -57,7 +67,9 @@ func extractSingleBaggage(parent context.Context, carrier TextMapCarrier) contex
|
||||
|
||||
bag, err := baggage.Parse(bStr)
|
||||
if err != nil {
|
||||
errorhandler.GetErrorHandler().Handle(err)
|
||||
handleExtractErrOnce.Do(func() {
|
||||
errorhandler.GetErrorHandler().Handle(err)
|
||||
})
|
||||
}
|
||||
if bag.Len() == 0 {
|
||||
return parent
|
||||
@@ -72,24 +84,60 @@ func extractMultiBaggage(parent context.Context, carrier ValuesGetter) context.C
|
||||
}
|
||||
|
||||
var members []baggage.Member
|
||||
for _, bStr := range bVals {
|
||||
currBag, err := baggage.Parse(bStr)
|
||||
if err != nil {
|
||||
errorhandler.GetErrorHandler().Handle(err)
|
||||
var totalBytes int
|
||||
var parseErrors int
|
||||
var truncateErr error
|
||||
for i, bStr := range bVals {
|
||||
if i > 0 {
|
||||
totalBytes++ // comma separator between combined header values
|
||||
}
|
||||
if currBag.Len() == 0 {
|
||||
continue
|
||||
totalBytes += len(bStr)
|
||||
if totalBytes > maxBytesPerBaggageString {
|
||||
// Per the W3C Baggage spec, the byte limit applies to the
|
||||
// combination of all baggage headers, not each header
|
||||
// individually. Mirror the single-header behavior of
|
||||
// reporting the error and returning the parent context
|
||||
// with no baggage attached.
|
||||
handleExtractErrOnce.Do(func() {
|
||||
errorhandler.GetErrorHandler().Handle(fmt.Errorf(
|
||||
"baggage: aggregate header size %d exceeds %d byte limit",
|
||||
totalBytes,
|
||||
maxBytesPerBaggageString,
|
||||
))
|
||||
})
|
||||
return parent
|
||||
}
|
||||
members = append(members, currBag.Members()...)
|
||||
if len(members) >= maxMembers {
|
||||
break
|
||||
|
||||
// If members exceed the limit, stop parsing baggage.
|
||||
if len(members) <= maxMembers {
|
||||
currBag, err := baggage.Parse(bStr)
|
||||
if err != nil {
|
||||
parseErrors++
|
||||
if parseErrors <= maxParseErrors {
|
||||
truncateErr = errors.Join(truncateErr, err)
|
||||
}
|
||||
}
|
||||
if currBag.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
members = append(members, currBag.Members()...)
|
||||
}
|
||||
}
|
||||
|
||||
if dropped := parseErrors - maxParseErrors; dropped > 0 {
|
||||
truncateErr = errors.Join(truncateErr, fmt.Errorf("and %d more error(s)", dropped))
|
||||
}
|
||||
|
||||
b, err := baggage.New(members...)
|
||||
if err != nil {
|
||||
errorhandler.GetErrorHandler().Handle(err)
|
||||
truncateErr = errors.Join(truncateErr, err)
|
||||
}
|
||||
if truncateErr != nil {
|
||||
handleExtractErrOnce.Do(func() {
|
||||
errorhandler.GetErrorHandler().Handle(truncateErr)
|
||||
})
|
||||
}
|
||||
|
||||
if b.Len() == 0 {
|
||||
return parent
|
||||
}
|
||||
|
||||
+13
-3
@@ -25,7 +25,7 @@ type config struct {
|
||||
cardinalityLimit int
|
||||
}
|
||||
|
||||
const defaultCardinalityLimit = 0
|
||||
const defaultCardinalityLimit = 2000
|
||||
|
||||
// readerSignals returns a force-flush and shutdown function for a
|
||||
// MeterProvider to call in their respective options. All Readers c contains
|
||||
@@ -70,6 +70,10 @@ func unifyShutdown(funcs []func(context.Context) error) func(context.Context) er
|
||||
}
|
||||
}
|
||||
|
||||
type experimentalOption interface {
|
||||
Experimental()
|
||||
}
|
||||
|
||||
// newConfig returns a config configured with options.
|
||||
func newConfig(options []Option) config {
|
||||
conf := config{
|
||||
@@ -81,6 +85,9 @@ func newConfig(options []Option) config {
|
||||
conf = o.apply(conf)
|
||||
}
|
||||
for _, o := range options {
|
||||
if _, ok := o.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
conf = o.apply(conf)
|
||||
}
|
||||
return conf
|
||||
@@ -150,7 +157,7 @@ func WithView(views ...View) Option {
|
||||
// exemplar reservoir, but the exemplar reservoir makes the final decision of
|
||||
// whether to store an exemplar.
|
||||
//
|
||||
// By default, the [exemplar.SampledFilter]
|
||||
// By default, the [exemplar.TraceBasedFilter]
|
||||
// is used. Exemplars can be entirely disabled by providing the
|
||||
// [exemplar.AlwaysOffFilter].
|
||||
func WithExemplarFilter(filter exemplar.Filter) Option {
|
||||
@@ -165,7 +172,10 @@ func WithExemplarFilter(filter exemplar.Filter) Option {
|
||||
// The cardinality limit is the hard limit on the number of metric datapoints
|
||||
// that can be collected for a single instrument in a single collect cycle.
|
||||
//
|
||||
// Setting this to a zero or negative value means no limit is applied.
|
||||
// By default, if this option is not used, a limit of
|
||||
// 2000 is applied.
|
||||
//
|
||||
// Setting this to a zero or negative means no limit is applied.
|
||||
// This value applies to all instrument kinds, but can be overridden per kind by
|
||||
// the reader's cardinality limit selector (see [WithCardinalityLimitSelector]).
|
||||
func WithCardinalityLimit(limit int) Option {
|
||||
|
||||
+7
-5
@@ -24,6 +24,10 @@
|
||||
// View. Views allow users that run OpenTelemetry instrumented code to modify
|
||||
// the generated data of that instrumentation.
|
||||
//
|
||||
// Note that attributes filtered out by a View may still appear on Exemplars,
|
||||
// because Exemplars are recorded with the dropped measurement attributes
|
||||
// when View attribute filtering is applied.
|
||||
//
|
||||
// The data generated by a MeterProvider needs to include information about its
|
||||
// origin. A MeterProvider needs to be configured with a Resource, using the
|
||||
// WithResource MeterProviderOption, to include this information. This Resource
|
||||
@@ -44,9 +48,7 @@
|
||||
// Cardinality refers to the number of unique attributes collected. High cardinality can lead to
|
||||
// excessive memory usage, increased storage costs, and backend performance issues.
|
||||
//
|
||||
// Currently, the OpenTelemetry Go Metric SDK does not enforce a cardinality limit by default
|
||||
// (note that this may change in a future release). Use [WithCardinalityLimit] to set the
|
||||
// cardinality limit as desired.
|
||||
// By default, the OpenTelemetry Go Metric SDK enforces a cardinality limit of 2000.
|
||||
//
|
||||
// New attribute sets are dropped when the cardinality limit is reached. The measurement of
|
||||
// these sets are aggregated into
|
||||
@@ -57,8 +59,8 @@
|
||||
//
|
||||
// Recommendations:
|
||||
//
|
||||
// - Set the limit based on the theoretical maximum combinations or expected
|
||||
// active combinations. The OpenTelemetry Specification recommends a default of 2000.
|
||||
// - Tune the limit based on the theoretical maximum combinations or expected
|
||||
// active combinations. The SDK default is 2000.
|
||||
// - A too high of a limit increases worst-case memory overhead in the SDK and may cause downstream
|
||||
// issues for databases that cannot handle high cardinality.
|
||||
// - A too low of a limit causes loss of attribute detail as more data falls into overflow.
|
||||
|
||||
+11
@@ -4,6 +4,7 @@
|
||||
package metric // import "go.opentelemetry.io/otel/sdk/metric"
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
@@ -19,9 +20,19 @@ type ExemplarReservoirProviderSelector func(Aggregation) exemplar.ReservoirProvi
|
||||
// reservoirFunc returns the appropriately configured exemplar reservoir
|
||||
// creation func based on the passed InstrumentKind and filter configuration.
|
||||
func reservoirFunc[N int64 | float64](
|
||||
kind InstrumentKind,
|
||||
provider exemplar.ReservoirProvider,
|
||||
filter exemplar.Filter,
|
||||
) func(attribute.Set) aggregate.FilteredExemplarReservoir[N] {
|
||||
if reflect.ValueOf(filter).Pointer() == reflect.ValueOf(exemplar.AlwaysOffFilter).Pointer() {
|
||||
return aggregate.DropReservoir[N]
|
||||
}
|
||||
if (kind == InstrumentKindObservableCounter || kind == InstrumentKindObservableUpDownCounter || kind == InstrumentKindObservableGauge) &&
|
||||
reflect.ValueOf(filter).Pointer() == reflect.ValueOf(exemplar.TraceBasedFilter).Pointer() {
|
||||
// Asynchronous instruments do not accept context, so TraceBasedFilter
|
||||
// will never record any exemplars.
|
||||
return aggregate.DropReservoir[N]
|
||||
}
|
||||
return func(attrs attribute.Set) aggregate.FilteredExemplarReservoir[N] {
|
||||
return aggregate.NewFilteredExemplarReservoir[N](filter, provider(attrs))
|
||||
}
|
||||
|
||||
+70
-108
@@ -8,7 +8,6 @@ import (
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
@@ -27,19 +26,7 @@ func FixedSizeReservoirProvider(k int) ReservoirProvider {
|
||||
// sample each one. If there are more than k, the Reservoir will then randomly
|
||||
// sample all additional measurement with a decreasing probability.
|
||||
func NewFixedSizeReservoir(k int) *FixedSizeReservoir {
|
||||
if k < 0 {
|
||||
k = 0
|
||||
}
|
||||
// Use math.MaxInt32 instead of math.MaxUint32 to prevent overflowing int
|
||||
// on 32-bit systems.
|
||||
if k > math.MaxInt32 {
|
||||
k = math.MaxInt32
|
||||
}
|
||||
return &FixedSizeReservoir{
|
||||
storage: newStorage(k),
|
||||
// Above we ensure k is positive, and less than MaxInt32.
|
||||
nextTracker: newNextTracker(uint32(k)), // nolint: gosec
|
||||
}
|
||||
return newFixedSizeReservoir(newStorage(k))
|
||||
}
|
||||
|
||||
var _ Reservoir = &FixedSizeReservoir{}
|
||||
@@ -51,7 +38,42 @@ var _ Reservoir = &FixedSizeReservoir{}
|
||||
type FixedSizeReservoir struct {
|
||||
reservoir.ConcurrentSafe
|
||||
*storage
|
||||
*nextTracker
|
||||
mu sync.Mutex
|
||||
|
||||
// count is the number of measurement seen.
|
||||
count int64
|
||||
// next is the next count that will store a measurement at a random index
|
||||
// once the reservoir has been filled.
|
||||
next int64
|
||||
// w is the largest random number in a distribution that is used to compute
|
||||
// the next next.
|
||||
w float64
|
||||
}
|
||||
|
||||
func newFixedSizeReservoir(s *storage) *FixedSizeReservoir {
|
||||
r := &FixedSizeReservoir{
|
||||
storage: s,
|
||||
}
|
||||
if cap(r.measurements) > 0 {
|
||||
r.reset()
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// randomFloat64 returns, as a float64, a uniform pseudo-random number in the
|
||||
// open interval (0.0,1.0).
|
||||
func (*FixedSizeReservoir) randomFloat64() float64 {
|
||||
// TODO: Use an algorithm that avoids rejection sampling. For example:
|
||||
//
|
||||
// const precision = 1 << 53 // 2^53
|
||||
// // Generate an integer in [1, 2^53 - 1]
|
||||
// v := rand.Uint64() % (precision - 1) + 1
|
||||
// return float64(v) / float64(precision)
|
||||
f := rand.Float64()
|
||||
for f == 0 {
|
||||
f = rand.Float64()
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// Offer accepts the parameters associated with a measurement. The
|
||||
@@ -66,6 +88,10 @@ type FixedSizeReservoir struct {
|
||||
// parameters are the value and dropped (filtered) attributes of the
|
||||
// measurement respectively.
|
||||
func (r *FixedSizeReservoir) Offer(ctx context.Context, t time.Time, n Value, a []attribute.KeyValue) {
|
||||
if cap(r.measurements) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// The following algorithm is "Algorithm L" from Li, Kim-Hung (4 December
|
||||
// 1994). "Reservoir-Sampling Algorithms of Time Complexity
|
||||
// O(n(1+log(N/n)))". ACM Transactions on Mathematical Software. 20 (4):
|
||||
@@ -107,65 +133,25 @@ func (r *FixedSizeReservoir) Offer(ctx context.Context, t time.Time, n Value, a
|
||||
// https://github.com/MrAlias/reservoir-sampling for a performance
|
||||
// comparison of reservoir sampling algorithms.
|
||||
|
||||
count, next := r.incrementCount()
|
||||
if count < r.k {
|
||||
r.store(ctx, int(count), t, n, a)
|
||||
} else if count == next {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if int(r.count) < cap(r.measurements) {
|
||||
r.store(ctx, int(r.count), t, n, a)
|
||||
} else if r.count == r.next {
|
||||
// Overwrite a random existing measurement with the one offered.
|
||||
idx := rand.IntN(int(r.k))
|
||||
idx := int(rand.Int64N(int64(cap(r.measurements))))
|
||||
r.store(ctx, idx, t, n, a)
|
||||
r.wMu.Lock()
|
||||
defer r.wMu.Unlock()
|
||||
newCount, newNext := r.loadCountAndNext()
|
||||
if newNext < next || newCount < count {
|
||||
// This Observe() raced with Collect(), and r.reset() has been
|
||||
// called since r.incrementCount(). Skip the call to advance in
|
||||
// this case because our exemplar may have been collected in the
|
||||
// previous interval.
|
||||
return
|
||||
}
|
||||
r.advance()
|
||||
}
|
||||
}
|
||||
|
||||
// Collect returns all the held exemplars.
|
||||
//
|
||||
// The Reservoir state is preserved after this call.
|
||||
func (r *FixedSizeReservoir) Collect(dest *[]Exemplar) {
|
||||
r.storage.Collect(dest)
|
||||
// Call reset here even though it will reset r.count and restart the random
|
||||
// number series. This will persist any old exemplars as long as no new
|
||||
// measurements are offered, but it will also prioritize those new
|
||||
// measurements that are made over the older collection cycle ones.
|
||||
r.reset()
|
||||
}
|
||||
|
||||
func newNextTracker(k uint32) *nextTracker {
|
||||
nt := &nextTracker{k: k}
|
||||
nt.reset()
|
||||
return nt
|
||||
}
|
||||
|
||||
type nextTracker struct {
|
||||
// countAndNext holds the current counts in the lower 32 bits and the next
|
||||
// value in the upper 32 bits.
|
||||
countAndNext atomic.Uint64
|
||||
// w is the largest random number in a distribution that is used to compute
|
||||
// the next next.
|
||||
w float64
|
||||
// wMu ensures w is kept consistent with next during advance and reset.
|
||||
wMu sync.Mutex
|
||||
// k is the number of measurements that can be stored in the reservoir.
|
||||
k uint32
|
||||
r.count++
|
||||
}
|
||||
|
||||
// reset resets r to the initial state.
|
||||
func (r *nextTracker) reset() {
|
||||
r.wMu.Lock()
|
||||
defer r.wMu.Unlock()
|
||||
func (r *FixedSizeReservoir) reset() {
|
||||
// This resets the number of exemplars known.
|
||||
r.count = 0
|
||||
// Random index inserts should only happen after the storage is full.
|
||||
r.setCountAndNext(0, r.k)
|
||||
r.next = int64(cap(r.measurements))
|
||||
|
||||
// Initial random number in the series used to generate r.next.
|
||||
//
|
||||
@@ -176,40 +162,14 @@ func (r *nextTracker) reset() {
|
||||
// This maps the uniform random number in (0,1) to a geometric distribution
|
||||
// over the same interval. The mean of the distribution is inversely
|
||||
// proportional to the storage capacity.
|
||||
r.w = math.Exp(math.Log(randomFloat64()) / float64(r.k))
|
||||
r.w = math.Exp(math.Log(r.randomFloat64()) / float64(cap(r.measurements)))
|
||||
|
||||
r.advance()
|
||||
}
|
||||
|
||||
// incrementCount increments the count. It returns the count before the
|
||||
// increment and the current next value.
|
||||
func (r *nextTracker) incrementCount() (uint32, uint32) {
|
||||
n := r.countAndNext.Add(1)
|
||||
// Both count and next are stored in the upper and lower 32 bits, and thus
|
||||
// can't overflow.
|
||||
return uint32(n&((1<<32)-1) - 1), uint32(n >> 32) // nolint: gosec
|
||||
}
|
||||
|
||||
// incrementNext increments the next value.
|
||||
func (r *nextTracker) incrementNext(inc uint32) {
|
||||
r.countAndNext.Add(uint64(inc) << 32)
|
||||
}
|
||||
|
||||
// setCountAndNext sets the count and next values.
|
||||
func (r *nextTracker) setCountAndNext(count, next uint32) {
|
||||
r.countAndNext.Store(uint64(next)<<32 + uint64(count))
|
||||
}
|
||||
|
||||
func (r *nextTracker) loadCountAndNext() (uint32, uint32) {
|
||||
n := r.countAndNext.Load()
|
||||
// Both count and next are stored in the upper and lower 32 bits, and thus
|
||||
// can't overflow.
|
||||
return uint32(n&((1<<32)-1) - 1), uint32(n >> 32) // nolint: gosec
|
||||
}
|
||||
|
||||
// advance updates the count at which the offered measurement will overwrite an
|
||||
// existing exemplar.
|
||||
func (r *nextTracker) advance() {
|
||||
func (r *FixedSizeReservoir) advance() {
|
||||
// Calculate the next value in the random number series.
|
||||
//
|
||||
// The current value of r.w is based on the max of a distribution of random
|
||||
@@ -222,7 +182,7 @@ func (r *nextTracker) advance() {
|
||||
// therefore the next r.w will be based on the same distribution (i.e.
|
||||
// `max(u_1,u_2,...,u_k)`). Therefore, we can sample the next r.w by
|
||||
// computing the next random number `u` and take r.w as `w * u^(1/k)`.
|
||||
r.w *= math.Exp(math.Log(randomFloat64()) / float64(r.k))
|
||||
r.w *= math.Exp(math.Log(r.randomFloat64()) / float64(cap(r.measurements)))
|
||||
// Use the new random number in the series to calculate the count of the
|
||||
// next measurement that will be stored.
|
||||
//
|
||||
@@ -233,21 +193,23 @@ func (r *nextTracker) advance() {
|
||||
//
|
||||
// Important to note, the new r.next will always be at least 1 more than
|
||||
// the last r.next.
|
||||
r.incrementNext(uint32(math.Log(randomFloat64())/math.Log(1-r.w)) + 1)
|
||||
r.next += int64(math.Log(r.randomFloat64())/math.Log(1-r.w)) + 1
|
||||
}
|
||||
|
||||
// randomFloat64 returns, as a float64, a uniform pseudo-random number in the
|
||||
// open interval (0.0,1.0).
|
||||
func randomFloat64() float64 {
|
||||
// TODO: Use an algorithm that avoids rejection sampling. For example:
|
||||
//
|
||||
// const precision = 1 << 53 // 2^53
|
||||
// // Generate an integer in [1, 2^53 - 1]
|
||||
// v := rand.Uint64() % (precision - 1) + 1
|
||||
// return float64(v) / float64(precision)
|
||||
f := rand.Float64()
|
||||
for f == 0 {
|
||||
f = rand.Float64()
|
||||
// Collect returns all the held exemplars.
|
||||
//
|
||||
// The Reservoir state is preserved after this call.
|
||||
func (r *FixedSizeReservoir) Collect(dest *[]Exemplar) {
|
||||
if cap(r.measurements) == 0 {
|
||||
*dest = (*dest)[:0]
|
||||
return
|
||||
}
|
||||
return f
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.storage.Collect(dest)
|
||||
// Call reset here even though it will reset r.count and restart the random
|
||||
// number series. This will persist any old exemplars as long as no new
|
||||
// measurements are offered, but it will also prioritize those new
|
||||
// measurements that are made over the older collection cycle ones.
|
||||
r.reset()
|
||||
}
|
||||
|
||||
+4
@@ -141,6 +141,10 @@ type Stream struct {
|
||||
// the attribute will not be recorded, otherwise, if it returns true, it
|
||||
// will record the attribute.
|
||||
//
|
||||
// Note that attributes filtered out by a View may still appear on Exemplars,
|
||||
// because Exemplars are recorded with the dropped measurement attributes
|
||||
// when View attribute filtering is applied.
|
||||
//
|
||||
// Use NewAllowKeysFilter from "go.opentelemetry.io/otel/attribute" to
|
||||
// provide an allow-list of attribute keys here.
|
||||
AttributeFilter attribute.Filter
|
||||
|
||||
+3
-3
@@ -36,8 +36,8 @@ type Builder[N int64 | float64] struct {
|
||||
// ReservoirFunc is the factory function used by aggregate functions to
|
||||
// create new exemplar reservoirs for a new seen attribute set.
|
||||
//
|
||||
// If this is not provided a default factory function that returns an
|
||||
// dropReservoir reservoir will be used.
|
||||
// If this is not provided a default factory function that returns a
|
||||
// DropReservoir reservoir will be used.
|
||||
ReservoirFunc func(attribute.Set) FilteredExemplarReservoir[N]
|
||||
// AggregationLimit is the cardinality limit of measurement attributes. Any
|
||||
// measurement for new attributes once the limit has been reached will be
|
||||
@@ -54,7 +54,7 @@ func (b Builder[N]) resFunc() func(attribute.Set) FilteredExemplarReservoir[N] {
|
||||
return b.ReservoirFunc
|
||||
}
|
||||
|
||||
return dropReservoir
|
||||
return DropReservoir
|
||||
}
|
||||
|
||||
type fltrMeasure[N int64 | float64] func(ctx context.Context, value N, fltrAttr attribute.Set, droppedAttr []attribute.KeyValue)
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@ import (
|
||||
"go.opentelemetry.io/otel/sdk/metric/exemplar"
|
||||
)
|
||||
|
||||
// dropReservoir returns a [FilteredReservoir] that drops all measurements it is offered.
|
||||
func dropReservoir[N int64 | float64](attribute.Set) FilteredExemplarReservoir[N] {
|
||||
// DropReservoir returns a [FilteredExemplarReservoir] that drops all measurements it is offered.
|
||||
func DropReservoir[N int64 | float64](attribute.Set) FilteredExemplarReservoir[N] {
|
||||
return &dropRes[N]{}
|
||||
}
|
||||
|
||||
|
||||
Generated
Vendored
+10
-4
@@ -26,8 +26,9 @@ const (
|
||||
|
||||
// expoHistogramDataPoint is a single data point in an exponential histogram.
|
||||
type expoHistogramDataPoint[N int64 | float64] struct {
|
||||
attrs attribute.Set
|
||||
res FilteredExemplarReservoir[N]
|
||||
attrs attribute.Set
|
||||
res FilteredExemplarReservoir[N]
|
||||
dropExemplars bool
|
||||
|
||||
minMax atomicMinMax[N]
|
||||
sum atomicCounter[N]
|
||||
@@ -349,13 +350,18 @@ func (e *expoHistogram[N]) measure(
|
||||
v, ok = e.values[fltrAttr.Equivalent()]
|
||||
if !ok {
|
||||
v = newExpoHistogramDataPoint[N](fltrAttr, e.maxSize, e.maxScale, e.noMinMax, e.noSum)
|
||||
v.res = e.newRes(fltrAttr)
|
||||
r := e.newRes(fltrAttr)
|
||||
_, isDrop := r.(*dropRes[N])
|
||||
v.res = r
|
||||
v.dropExemplars = isDrop
|
||||
|
||||
e.values[fltrAttr.Equivalent()] = v
|
||||
}
|
||||
}
|
||||
v.record(value)
|
||||
v.res.Offer(ctx, value, droppedAttr)
|
||||
if !v.dropExemplars {
|
||||
v.res.Offer(ctx, value, droppedAttr)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *expoHistogram[N]) delta(
|
||||
|
||||
+24
-12
@@ -17,8 +17,9 @@ import (
|
||||
|
||||
// histogramPoint is a single histogram point, used in delta aggregations.
|
||||
type histogramPoint[N int64 | float64] struct {
|
||||
attrs attribute.Set
|
||||
res FilteredExemplarReservoir[N]
|
||||
attrs attribute.Set
|
||||
res FilteredExemplarReservoir[N]
|
||||
dropExemplars bool
|
||||
histogramPointCounters[N]
|
||||
}
|
||||
|
||||
@@ -28,9 +29,10 @@ type hotColdHistogramPoint[N int64 | float64] struct {
|
||||
hcwg hotColdWaitGroup
|
||||
hotColdPoint [2]histogramPointCounters[N]
|
||||
|
||||
attrs attribute.Set
|
||||
res FilteredExemplarReservoir[N]
|
||||
startTime time.Time
|
||||
attrs attribute.Set
|
||||
res FilteredExemplarReservoir[N]
|
||||
startTime time.Time
|
||||
dropExemplars bool
|
||||
}
|
||||
|
||||
// histogramPointCounters contains only the atomic counter data, and is used by
|
||||
@@ -113,9 +115,12 @@ func (s *deltaHistogram[N]) measure(
|
||||
hotIdx := s.hcwg.start()
|
||||
defer s.hcwg.done(hotIdx)
|
||||
h := s.hotColdValMap[hotIdx].LoadOrStoreAttr(fltrAttr, func(attr attribute.Set) any {
|
||||
r := s.newRes(attr)
|
||||
_, isDrop := r.(*dropRes[N])
|
||||
hPt := &histogramPoint[N]{
|
||||
res: s.newRes(attr),
|
||||
attrs: attr,
|
||||
res: r,
|
||||
attrs: attr,
|
||||
dropExemplars: isDrop,
|
||||
// N+1 buckets. For example:
|
||||
//
|
||||
// bounds = [0, 5, 10]
|
||||
@@ -141,7 +146,9 @@ func (s *deltaHistogram[N]) measure(
|
||||
if !s.noSum {
|
||||
h.total.add(value)
|
||||
}
|
||||
h.res.Offer(ctx, value, droppedAttr)
|
||||
if !h.dropExemplars {
|
||||
h.res.Offer(ctx, value, droppedAttr)
|
||||
}
|
||||
}
|
||||
|
||||
// newDeltaHistogram returns a histogram that is reset each time it is
|
||||
@@ -282,9 +289,13 @@ func (s *cumulativeHistogram[N]) measure(
|
||||
droppedAttr []attribute.KeyValue,
|
||||
) {
|
||||
h := s.values.LoadOrStoreAttr(fltrAttr, func(attr attribute.Set) any {
|
||||
r := s.newRes(attr)
|
||||
_, isDrop := r.(*dropRes[N])
|
||||
hPt := &hotColdHistogramPoint[N]{
|
||||
res: s.newRes(attr),
|
||||
attrs: attr,
|
||||
res: r,
|
||||
attrs: attr,
|
||||
startTime: now(),
|
||||
dropExemplars: isDrop,
|
||||
// N+1 buckets. For example:
|
||||
//
|
||||
// bounds = [0, 5, 10]
|
||||
@@ -300,7 +311,6 @@ func (s *cumulativeHistogram[N]) measure(
|
||||
counts: make([]atomic.Uint64, len(s.bounds)+1),
|
||||
},
|
||||
},
|
||||
startTime: now(),
|
||||
}
|
||||
return hPt
|
||||
}).(*hotColdHistogramPoint[N])
|
||||
@@ -322,7 +332,9 @@ func (s *cumulativeHistogram[N]) measure(
|
||||
if !s.noSum {
|
||||
h.hotColdPoint[hotIdx].total.add(value)
|
||||
}
|
||||
h.res.Offer(ctx, value, droppedAttr)
|
||||
if !h.dropExemplars {
|
||||
h.res.Offer(ctx, value, droppedAttr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *cumulativeHistogram[N]) collect(
|
||||
|
||||
+14
-8
@@ -14,10 +14,11 @@ import (
|
||||
|
||||
// lastValuePoint is timestamped measurement data.
|
||||
type lastValuePoint[N int64 | float64] struct {
|
||||
attrs attribute.Set
|
||||
value atomicN[N]
|
||||
res FilteredExemplarReservoir[N]
|
||||
startTime time.Time
|
||||
attrs attribute.Set
|
||||
value atomicN[N]
|
||||
res FilteredExemplarReservoir[N]
|
||||
startTime time.Time
|
||||
dropExemplars bool
|
||||
}
|
||||
|
||||
// lastValueMap summarizes a set of measurements as the last one made.
|
||||
@@ -33,17 +34,22 @@ func (s *lastValueMap[N]) measure(
|
||||
droppedAttr []attribute.KeyValue,
|
||||
) {
|
||||
lv := s.values.LoadOrStoreAttr(fltrAttr, func(attr attribute.Set) any {
|
||||
r := s.newRes(attr)
|
||||
_, isDrop := r.(*dropRes[N])
|
||||
p := &lastValuePoint[N]{
|
||||
res: s.newRes(attr),
|
||||
attrs: attr,
|
||||
startTime: now(),
|
||||
res: r,
|
||||
attrs: attr,
|
||||
startTime: now(),
|
||||
dropExemplars: isDrop,
|
||||
}
|
||||
p.value.Store(value)
|
||||
return p
|
||||
}).(*lastValuePoint[N])
|
||||
|
||||
lv.value.Store(value)
|
||||
lv.res.Offer(ctx, value, droppedAttr)
|
||||
if !lv.dropExemplars {
|
||||
lv.res.Offer(ctx, value, droppedAttr)
|
||||
}
|
||||
}
|
||||
|
||||
func newDeltaLastValue[N int64 | float64](
|
||||
|
||||
+14
-8
@@ -13,10 +13,11 @@ import (
|
||||
)
|
||||
|
||||
type sumValue[N int64 | float64] struct {
|
||||
n atomicCounter[N]
|
||||
res FilteredExemplarReservoir[N]
|
||||
attrs attribute.Set
|
||||
startTime time.Time
|
||||
n atomicCounter[N]
|
||||
res FilteredExemplarReservoir[N]
|
||||
attrs attribute.Set
|
||||
startTime time.Time
|
||||
dropExemplars bool
|
||||
}
|
||||
|
||||
type sumValueMap[N int64 | float64] struct {
|
||||
@@ -31,17 +32,22 @@ func (s *sumValueMap[N]) measure(
|
||||
droppedAttr []attribute.KeyValue,
|
||||
) {
|
||||
sv := s.values.LoadOrStoreAttr(fltrAttr, func(attr attribute.Set) any {
|
||||
r := s.newRes(attr)
|
||||
_, isDrop := r.(*dropRes[N])
|
||||
return &sumValue[N]{
|
||||
res: s.newRes(attr),
|
||||
attrs: attr,
|
||||
startTime: now(),
|
||||
res: r,
|
||||
attrs: attr,
|
||||
startTime: now(),
|
||||
dropExemplars: isDrop,
|
||||
}
|
||||
}).(*sumValue[N])
|
||||
sv.n.add(value)
|
||||
// It is possible for collection to race with measurement and observe the
|
||||
// exemplar in the batch of metrics after the add() for cumulative sums.
|
||||
// This is an accepted tradeoff to avoid locking during measurement.
|
||||
sv.res.Offer(ctx, value, droppedAttr)
|
||||
if !sv.dropExemplars {
|
||||
sv.res.Offer(ctx, value, droppedAttr)
|
||||
}
|
||||
}
|
||||
|
||||
// newDeltaSum returns an aggregator that summarizes a set of measurements as
|
||||
|
||||
+3
-2
@@ -16,8 +16,8 @@ import (
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/sdk"
|
||||
"go.opentelemetry.io/otel/sdk/internal/x"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.40.0/otelconv"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
"go.opentelemetry.io/otel/semconv/v1.41.0/otelconv"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -54,6 +54,7 @@ var (
|
||||
func get[T any](p *sync.Pool) *[]T { return p.Get().(*[]T) }
|
||||
|
||||
func put[T any](p *sync.Pool, s *[]T) {
|
||||
clear(*s)
|
||||
*s = (*s)[:0] // Reset.
|
||||
p.Put(s)
|
||||
}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
# Experimental Features
|
||||
|
||||
The Metric SDK contains features that have not yet stabilized in the OpenTelemetry specification.
|
||||
These features are added to the OpenTelemetry Go Metric SDK prior to stabilization in the specification so that users can start experimenting with them and provide feedback.
|
||||
|
||||
These feature may change in backwards incompatible ways as feedback is applied.
|
||||
See the [Compatibility and Stability](#compatibility-and-stability) section for more information.
|
||||
|
||||
## Features
|
||||
|
||||
- [Metric Export Batch Size](#metric-export-batch-size)
|
||||
|
||||
### Metric Export Batch Size
|
||||
|
||||
The metric export can be split into batches before exporting by specifying a maximum number of data points per batch.
|
||||
|
||||
This experimental feature can be enabled by setting the `OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE` environment variable.
|
||||
The value MUST be a positive integer.
|
||||
All other values or an empty value will result in the default behavior of not batching.
|
||||
|
||||
#### Examples
|
||||
|
||||
Enable metrics to be batched by maximum export batch size of 200.
|
||||
|
||||
```console
|
||||
export OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE=200
|
||||
```
|
||||
|
||||
Disable metric export batching.
|
||||
|
||||
```console
|
||||
unset OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE
|
||||
```
|
||||
|
||||
## Compatibility and Stability
|
||||
|
||||
Experimental features do not fall within the scope of the OpenTelemetry Go versioning and stability [policy](../../../../VERSIONING.md).
|
||||
These features may be removed or modified in successive version releases, including patch versions.
|
||||
|
||||
When an experimental feature is promoted to a stable feature, a migration path will be included in the changelog entry of the release.
|
||||
There is no guarantee that any environment variable feature flags that enabled the experimental feature will be supported by the stable version.
|
||||
If they are supported, they may be accompanied with a deprecation notice stating a timeline for the removal of that support.
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package x contains support for OTel metric SDK experimental features.
|
||||
//
|
||||
// This package should only be used for features defined in the specification.
|
||||
// It should not be used for experiments or new project ideas.
|
||||
package x // import "go.opentelemetry.io/otel/sdk/metric/internal/x"
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Feature is an experimental feature control flag. It provides a uniform way
|
||||
// to interact with these feature flags and parse their values.
|
||||
type Feature[T any] struct {
|
||||
key string
|
||||
parse func(v string) (T, bool)
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
func newFeature[T any](suffix string, parse func(string) (T, bool)) Feature[T] {
|
||||
const envKeyRoot = "OTEL_GO_X_"
|
||||
return Feature[T]{
|
||||
key: envKeyRoot + suffix,
|
||||
parse: parse,
|
||||
}
|
||||
}
|
||||
|
||||
// Key returns the environment variable key that needs to be set to enable the
|
||||
// feature.
|
||||
func (f Feature[T]) Key() string { return f.key }
|
||||
|
||||
// Lookup returns the user configured value for the feature and true if the
|
||||
// user has enabled the feature. Otherwise, if the feature is not enabled, a
|
||||
// zero-value and false are returned.
|
||||
func (f Feature[T]) Lookup() (v T, ok bool) {
|
||||
// https://github.com/open-telemetry/opentelemetry-specification/blob/62effed618589a0bec416a87e559c0a9d96289bb/specification/configuration/sdk-environment-variables.md#parsing-empty-value
|
||||
//
|
||||
// > The SDK MUST interpret an empty value of an environment variable the
|
||||
// > same way as when the variable is unset.
|
||||
vRaw := os.Getenv(f.key)
|
||||
if vRaw == "" {
|
||||
return v, ok
|
||||
}
|
||||
return f.parse(vRaw)
|
||||
}
|
||||
|
||||
// Enabled reports whether the feature is enabled.
|
||||
func (f Feature[T]) Enabled() bool {
|
||||
_, ok := f.Lookup()
|
||||
return ok
|
||||
}
|
||||
|
||||
// MetricExportBatchSize is an experimental feature flag that controls the
|
||||
// max export batch size for metric data.
|
||||
//
|
||||
// To enable this feature set the OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE environment
|
||||
// variable to a positive integer value.
|
||||
var MetricExportBatchSize = newFeature(
|
||||
"METRIC_EXPORT_BATCH_SIZE",
|
||||
func(v string) (int, bool) {
|
||||
val, err := strconv.Atoi(v)
|
||||
if err == nil && val > 0 {
|
||||
return val, true
|
||||
}
|
||||
return 0, false
|
||||
},
|
||||
)
|
||||
+5
-5
@@ -169,17 +169,17 @@ func (mr *ManualReader) Collect(ctx context.Context, rm *metricdata.ResourceMetr
|
||||
}
|
||||
|
||||
// MarshalLog returns logging data about the ManualReader.
|
||||
func (r *ManualReader) MarshalLog() any {
|
||||
r.mu.Lock()
|
||||
down := r.isShutdown
|
||||
r.mu.Unlock()
|
||||
func (mr *ManualReader) MarshalLog() any {
|
||||
mr.mu.Lock()
|
||||
down := mr.isShutdown
|
||||
mr.mu.Unlock()
|
||||
return struct {
|
||||
Type string
|
||||
Registered bool
|
||||
Shutdown bool
|
||||
}{
|
||||
Type: "ManualReader",
|
||||
Registered: r.sdkProducer.Load() != nil,
|
||||
Registered: mr.sdkProducer.Load() != nil,
|
||||
Shutdown: down,
|
||||
}
|
||||
}
|
||||
|
||||
+83
-34
@@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/internal/global"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/metric/embedded"
|
||||
@@ -70,7 +71,7 @@ func (m *meter) Int64Counter(name string, options ...metric.Int64CounterOption)
|
||||
cfg := metric.NewInt64CounterConfig(options...)
|
||||
const kind = InstrumentKindCounter
|
||||
p := int64InstProvider{m}
|
||||
i, err := p.lookup(kind, name, cfg.Description(), cfg.Unit())
|
||||
i, err := p.lookup(kind, name, cfg.Description(), cfg.Unit(), defaultAttributes(options))
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
@@ -88,7 +89,7 @@ func (m *meter) Int64UpDownCounter(
|
||||
cfg := metric.NewInt64UpDownCounterConfig(options...)
|
||||
const kind = InstrumentKindUpDownCounter
|
||||
p := int64InstProvider{m}
|
||||
i, err := p.lookup(kind, name, cfg.Description(), cfg.Unit())
|
||||
i, err := p.lookup(kind, name, cfg.Description(), cfg.Unit(), defaultAttributes(options))
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
@@ -102,7 +103,7 @@ func (m *meter) Int64UpDownCounter(
|
||||
func (m *meter) Int64Histogram(name string, options ...metric.Int64HistogramOption) (metric.Int64Histogram, error) {
|
||||
cfg := metric.NewInt64HistogramConfig(options...)
|
||||
p := int64InstProvider{m}
|
||||
i, err := p.lookupHistogram(name, cfg)
|
||||
i, err := p.lookupHistogram(name, cfg, defaultAttributes(options))
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
@@ -117,7 +118,7 @@ func (m *meter) Int64Gauge(name string, options ...metric.Int64GaugeOption) (met
|
||||
cfg := metric.NewInt64GaugeConfig(options...)
|
||||
const kind = InstrumentKindGauge
|
||||
p := int64InstProvider{m}
|
||||
i, err := p.lookup(kind, name, cfg.Description(), cfg.Unit())
|
||||
i, err := p.lookup(kind, name, cfg.Description(), cfg.Unit(), defaultAttributes(options))
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
@@ -127,7 +128,11 @@ func (m *meter) Int64Gauge(name string, options ...metric.Int64GaugeOption) (met
|
||||
|
||||
// int64ObservableInstrument returns a new observable identified by the Instrument.
|
||||
// It registers callbacks for each reader's pipeline.
|
||||
func (m *meter) int64ObservableInstrument(id Instrument, callbacks []metric.Int64Callback) (int64Observable, error) {
|
||||
func (m *meter) int64ObservableInstrument(
|
||||
id Instrument,
|
||||
allowedKeys []attribute.Key,
|
||||
callbacks []metric.Int64Callback,
|
||||
) (int64Observable, error) {
|
||||
key := instID{
|
||||
Name: id.Name,
|
||||
Description: id.Description,
|
||||
@@ -142,7 +147,7 @@ func (m *meter) int64ObservableInstrument(id Instrument, callbacks []metric.Int6
|
||||
for _, insert := range m.int64Resolver.inserters {
|
||||
// Connect the measure functions for instruments in this pipeline with the
|
||||
// callbacks for this pipeline.
|
||||
in, err := insert.Instrument(id, insert.readerDefaultAggregation(id.Kind))
|
||||
in, err := insert.Instrument(id, allowedKeys, insert.readerDefaultAggregation(id.Kind))
|
||||
if err != nil {
|
||||
return inst, err
|
||||
}
|
||||
@@ -188,7 +193,7 @@ func (m *meter) Int64ObservableCounter(
|
||||
Kind: InstrumentKindObservableCounter,
|
||||
Scope: m.scope,
|
||||
}
|
||||
return m.int64ObservableInstrument(id, cfg.Callbacks())
|
||||
return m.int64ObservableInstrument(id, defaultAttributes(options), cfg.Callbacks())
|
||||
}
|
||||
|
||||
// Int64ObservableUpDownCounter returns a new instrument identified by name and
|
||||
@@ -212,7 +217,7 @@ func (m *meter) Int64ObservableUpDownCounter(
|
||||
Kind: InstrumentKindObservableUpDownCounter,
|
||||
Scope: m.scope,
|
||||
}
|
||||
return m.int64ObservableInstrument(id, cfg.Callbacks())
|
||||
return m.int64ObservableInstrument(id, defaultAttributes(options), cfg.Callbacks())
|
||||
}
|
||||
|
||||
// Int64ObservableGauge returns a new instrument identified by name and
|
||||
@@ -236,7 +241,7 @@ func (m *meter) Int64ObservableGauge(
|
||||
Kind: InstrumentKindObservableGauge,
|
||||
Scope: m.scope,
|
||||
}
|
||||
return m.int64ObservableInstrument(id, cfg.Callbacks())
|
||||
return m.int64ObservableInstrument(id, defaultAttributes(options), cfg.Callbacks())
|
||||
}
|
||||
|
||||
// Float64Counter returns a new instrument identified by name and configured
|
||||
@@ -246,7 +251,7 @@ func (m *meter) Float64Counter(name string, options ...metric.Float64CounterOpti
|
||||
cfg := metric.NewFloat64CounterConfig(options...)
|
||||
const kind = InstrumentKindCounter
|
||||
p := float64InstProvider{m}
|
||||
i, err := p.lookup(kind, name, cfg.Description(), cfg.Unit())
|
||||
i, err := p.lookup(kind, name, cfg.Description(), cfg.Unit(), defaultAttributes(options))
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
@@ -264,7 +269,7 @@ func (m *meter) Float64UpDownCounter(
|
||||
cfg := metric.NewFloat64UpDownCounterConfig(options...)
|
||||
const kind = InstrumentKindUpDownCounter
|
||||
p := float64InstProvider{m}
|
||||
i, err := p.lookup(kind, name, cfg.Description(), cfg.Unit())
|
||||
i, err := p.lookup(kind, name, cfg.Description(), cfg.Unit(), defaultAttributes(options))
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
@@ -281,7 +286,7 @@ func (m *meter) Float64Histogram(
|
||||
) (metric.Float64Histogram, error) {
|
||||
cfg := metric.NewFloat64HistogramConfig(options...)
|
||||
p := float64InstProvider{m}
|
||||
i, err := p.lookupHistogram(name, cfg)
|
||||
i, err := p.lookupHistogram(name, cfg, defaultAttributes(options))
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
@@ -296,7 +301,7 @@ func (m *meter) Float64Gauge(name string, options ...metric.Float64GaugeOption)
|
||||
cfg := metric.NewFloat64GaugeConfig(options...)
|
||||
const kind = InstrumentKindGauge
|
||||
p := float64InstProvider{m}
|
||||
i, err := p.lookup(kind, name, cfg.Description(), cfg.Unit())
|
||||
i, err := p.lookup(kind, name, cfg.Description(), cfg.Unit(), defaultAttributes(options))
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
@@ -308,6 +313,7 @@ func (m *meter) Float64Gauge(name string, options ...metric.Float64GaugeOption)
|
||||
// It registers callbacks for each reader's pipeline.
|
||||
func (m *meter) float64ObservableInstrument(
|
||||
id Instrument,
|
||||
allowedKeys []attribute.Key,
|
||||
callbacks []metric.Float64Callback,
|
||||
) (float64Observable, error) {
|
||||
key := instID{
|
||||
@@ -316,7 +322,7 @@ func (m *meter) float64ObservableInstrument(
|
||||
Unit: id.Unit,
|
||||
Kind: id.Kind,
|
||||
}
|
||||
if m.int64ObservableInsts.HasKey(key) && len(callbacks) > 0 {
|
||||
if m.float64ObservableInsts.HasKey(key) && len(callbacks) > 0 {
|
||||
warnRepeatedObservableCallbacks(id)
|
||||
}
|
||||
return m.float64ObservableInsts.Lookup(key, func() (float64Observable, error) {
|
||||
@@ -324,7 +330,7 @@ func (m *meter) float64ObservableInstrument(
|
||||
for _, insert := range m.float64Resolver.inserters {
|
||||
// Connect the measure functions for instruments in this pipeline with the
|
||||
// callbacks for this pipeline.
|
||||
in, err := insert.Instrument(id, insert.readerDefaultAggregation(id.Kind))
|
||||
in, err := insert.Instrument(id, allowedKeys, insert.readerDefaultAggregation(id.Kind))
|
||||
if err != nil {
|
||||
return inst, err
|
||||
}
|
||||
@@ -370,7 +376,7 @@ func (m *meter) Float64ObservableCounter(
|
||||
Kind: InstrumentKindObservableCounter,
|
||||
Scope: m.scope,
|
||||
}
|
||||
return m.float64ObservableInstrument(id, cfg.Callbacks())
|
||||
return m.float64ObservableInstrument(id, defaultAttributes(options), cfg.Callbacks())
|
||||
}
|
||||
|
||||
// Float64ObservableUpDownCounter returns a new instrument identified by name
|
||||
@@ -394,7 +400,7 @@ func (m *meter) Float64ObservableUpDownCounter(
|
||||
Kind: InstrumentKindObservableUpDownCounter,
|
||||
Scope: m.scope,
|
||||
}
|
||||
return m.float64ObservableInstrument(id, cfg.Callbacks())
|
||||
return m.float64ObservableInstrument(id, defaultAttributes(options), cfg.Callbacks())
|
||||
}
|
||||
|
||||
// Float64ObservableGauge returns a new instrument identified by name and
|
||||
@@ -418,7 +424,7 @@ func (m *meter) Float64ObservableGauge(
|
||||
Kind: InstrumentKindObservableGauge,
|
||||
Scope: m.scope,
|
||||
}
|
||||
return m.float64ObservableInstrument(id, cfg.Callbacks())
|
||||
return m.float64ObservableInstrument(id, defaultAttributes(options), cfg.Callbacks())
|
||||
}
|
||||
|
||||
func validateInstrumentName(name string) error {
|
||||
@@ -576,7 +582,8 @@ func (r observer) ObserveFloat64(o metric.Float64Observable, v float64, opts ...
|
||||
|
||||
if _, registered := r.float64[oImpl.observableID]; !registered {
|
||||
if !oImpl.dropAggregation {
|
||||
global.Error(errUnregObserver, "failed to record",
|
||||
global.Error(
|
||||
errUnregObserver, "failed to record",
|
||||
"name", oImpl.name,
|
||||
"description", oImpl.description,
|
||||
"unit", oImpl.unit,
|
||||
@@ -606,7 +613,8 @@ func (r observer) ObserveInt64(o metric.Int64Observable, v int64, opts ...metric
|
||||
|
||||
if _, registered := r.int64[oImpl.observableID]; !registered {
|
||||
if !oImpl.dropAggregation {
|
||||
global.Error(errUnregObserver, "failed to record",
|
||||
global.Error(
|
||||
errUnregObserver, "failed to record",
|
||||
"name", oImpl.name,
|
||||
"description", oImpl.description,
|
||||
"unit", oImpl.unit,
|
||||
@@ -633,7 +641,11 @@ func (noopRegister) Unregister() error {
|
||||
// int64InstProvider provides int64 OpenTelemetry instruments.
|
||||
type int64InstProvider struct{ *meter }
|
||||
|
||||
func (p int64InstProvider) aggs(kind InstrumentKind, name, desc, u string) ([]aggregate.Measure[int64], error) {
|
||||
func (p int64InstProvider) aggs(
|
||||
kind InstrumentKind,
|
||||
name, desc, u string,
|
||||
allowedKeys []attribute.Key,
|
||||
) ([]aggregate.Measure[int64], error) {
|
||||
inst := Instrument{
|
||||
Name: name,
|
||||
Description: desc,
|
||||
@@ -641,12 +653,13 @@ func (p int64InstProvider) aggs(kind InstrumentKind, name, desc, u string) ([]ag
|
||||
Kind: kind,
|
||||
Scope: p.scope,
|
||||
}
|
||||
return p.int64Resolver.Aggregators(inst)
|
||||
return p.int64Resolver.Aggregators(inst, allowedKeys)
|
||||
}
|
||||
|
||||
func (p int64InstProvider) histogramAggs(
|
||||
name string,
|
||||
cfg metric.Int64HistogramConfig,
|
||||
allowedKeys []attribute.Key,
|
||||
) ([]aggregate.Measure[int64], error) {
|
||||
boundaries := cfg.ExplicitBucketBoundaries()
|
||||
aggError := AggregationExplicitBucketHistogram{Boundaries: boundaries}.err()
|
||||
@@ -661,32 +674,40 @@ func (p int64InstProvider) histogramAggs(
|
||||
Kind: InstrumentKindHistogram,
|
||||
Scope: p.scope,
|
||||
}
|
||||
measures, err := p.int64Resolver.HistogramAggregators(inst, boundaries)
|
||||
measures, err := p.int64Resolver.HistogramAggregators(inst, allowedKeys, boundaries)
|
||||
return measures, errors.Join(aggError, err)
|
||||
}
|
||||
|
||||
// lookup returns the resolved instrumentImpl.
|
||||
func (p int64InstProvider) lookup(kind InstrumentKind, name, desc, u string) (*int64Inst, error) {
|
||||
func (p int64InstProvider) lookup(
|
||||
kind InstrumentKind,
|
||||
name, desc, u string,
|
||||
allowedKeys []attribute.Key,
|
||||
) (*int64Inst, error) {
|
||||
return p.int64Insts.Lookup(instID{
|
||||
Name: name,
|
||||
Description: desc,
|
||||
Unit: u,
|
||||
Kind: kind,
|
||||
}, func() (*int64Inst, error) {
|
||||
aggs, err := p.aggs(kind, name, desc, u)
|
||||
aggs, err := p.aggs(kind, name, desc, u, allowedKeys)
|
||||
return &int64Inst{measures: aggs}, err
|
||||
})
|
||||
}
|
||||
|
||||
// lookupHistogram returns the resolved instrumentImpl.
|
||||
func (p int64InstProvider) lookupHistogram(name string, cfg metric.Int64HistogramConfig) (*int64Inst, error) {
|
||||
func (p int64InstProvider) lookupHistogram(
|
||||
name string,
|
||||
cfg metric.Int64HistogramConfig,
|
||||
allowedKeys []attribute.Key,
|
||||
) (*int64Inst, error) {
|
||||
return p.int64Insts.Lookup(instID{
|
||||
Name: name,
|
||||
Description: cfg.Description(),
|
||||
Unit: cfg.Unit(),
|
||||
Kind: InstrumentKindHistogram,
|
||||
}, func() (*int64Inst, error) {
|
||||
aggs, err := p.histogramAggs(name, cfg)
|
||||
aggs, err := p.histogramAggs(name, cfg, allowedKeys)
|
||||
return &int64Inst{measures: aggs}, err
|
||||
})
|
||||
}
|
||||
@@ -694,7 +715,11 @@ func (p int64InstProvider) lookupHistogram(name string, cfg metric.Int64Histogra
|
||||
// float64InstProvider provides float64 OpenTelemetry instruments.
|
||||
type float64InstProvider struct{ *meter }
|
||||
|
||||
func (p float64InstProvider) aggs(kind InstrumentKind, name, desc, u string) ([]aggregate.Measure[float64], error) {
|
||||
func (p float64InstProvider) aggs(
|
||||
kind InstrumentKind,
|
||||
name, desc, u string,
|
||||
allowedKeys []attribute.Key,
|
||||
) ([]aggregate.Measure[float64], error) {
|
||||
inst := Instrument{
|
||||
Name: name,
|
||||
Description: desc,
|
||||
@@ -702,12 +727,13 @@ func (p float64InstProvider) aggs(kind InstrumentKind, name, desc, u string) ([]
|
||||
Kind: kind,
|
||||
Scope: p.scope,
|
||||
}
|
||||
return p.float64Resolver.Aggregators(inst)
|
||||
return p.float64Resolver.Aggregators(inst, allowedKeys)
|
||||
}
|
||||
|
||||
func (p float64InstProvider) histogramAggs(
|
||||
name string,
|
||||
cfg metric.Float64HistogramConfig,
|
||||
allowedKeys []attribute.Key,
|
||||
) ([]aggregate.Measure[float64], error) {
|
||||
boundaries := cfg.ExplicitBucketBoundaries()
|
||||
aggError := AggregationExplicitBucketHistogram{Boundaries: boundaries}.err()
|
||||
@@ -722,32 +748,40 @@ func (p float64InstProvider) histogramAggs(
|
||||
Kind: InstrumentKindHistogram,
|
||||
Scope: p.scope,
|
||||
}
|
||||
measures, err := p.float64Resolver.HistogramAggregators(inst, boundaries)
|
||||
measures, err := p.float64Resolver.HistogramAggregators(inst, allowedKeys, boundaries)
|
||||
return measures, errors.Join(aggError, err)
|
||||
}
|
||||
|
||||
// lookup returns the resolved instrumentImpl.
|
||||
func (p float64InstProvider) lookup(kind InstrumentKind, name, desc, u string) (*float64Inst, error) {
|
||||
func (p float64InstProvider) lookup(
|
||||
kind InstrumentKind,
|
||||
name, desc, u string,
|
||||
allowedKeys []attribute.Key,
|
||||
) (*float64Inst, error) {
|
||||
return p.float64Insts.Lookup(instID{
|
||||
Name: name,
|
||||
Description: desc,
|
||||
Unit: u,
|
||||
Kind: kind,
|
||||
}, func() (*float64Inst, error) {
|
||||
aggs, err := p.aggs(kind, name, desc, u)
|
||||
aggs, err := p.aggs(kind, name, desc, u, allowedKeys)
|
||||
return &float64Inst{measures: aggs}, err
|
||||
})
|
||||
}
|
||||
|
||||
// lookupHistogram returns the resolved instrumentImpl.
|
||||
func (p float64InstProvider) lookupHistogram(name string, cfg metric.Float64HistogramConfig) (*float64Inst, error) {
|
||||
func (p float64InstProvider) lookupHistogram(
|
||||
name string,
|
||||
cfg metric.Float64HistogramConfig,
|
||||
allowedKeys []attribute.Key,
|
||||
) (*float64Inst, error) {
|
||||
return p.float64Insts.Lookup(instID{
|
||||
Name: name,
|
||||
Description: cfg.Description(),
|
||||
Unit: cfg.Unit(),
|
||||
Kind: InstrumentKindHistogram,
|
||||
}, func() (*float64Inst, error) {
|
||||
aggs, err := p.histogramAggs(name, cfg)
|
||||
aggs, err := p.histogramAggs(name, cfg, allowedKeys)
|
||||
return &float64Inst{measures: aggs}, err
|
||||
})
|
||||
}
|
||||
@@ -771,3 +805,18 @@ func (o float64Observer) Observe(val float64, opts ...metric.ObserveOption) {
|
||||
c := metric.NewObserveConfig(opts)
|
||||
o.observe(val, c.Attributes())
|
||||
}
|
||||
|
||||
func defaultAttributes[T any](opts []T) []attribute.Key {
|
||||
var keys []attribute.Key
|
||||
var found bool
|
||||
for _, o := range opts {
|
||||
if exp, ok := any(o).(interface{ AllowedKeys() []attribute.Key }); ok {
|
||||
found = true
|
||||
keys = append(keys, exp.AllowedKeys()...)
|
||||
}
|
||||
}
|
||||
if found && keys == nil {
|
||||
return []attribute.Key{}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
+46
-7
@@ -14,8 +14,9 @@ import (
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/internal/global"
|
||||
"go.opentelemetry.io/otel/sdk/metric/internal/observ"
|
||||
"go.opentelemetry.io/otel/sdk/metric/internal/x"
|
||||
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
)
|
||||
|
||||
// Default periodic reader timing.
|
||||
@@ -126,6 +127,9 @@ func NewPeriodicReader(exporter Exporter, options ...PeriodicReaderOption) *Peri
|
||||
},
|
||||
},
|
||||
}
|
||||
if val, ok := x.MetricExportBatchSize.Lookup(); ok {
|
||||
r.batcher = batcher{size: val}
|
||||
}
|
||||
r.externalProducers.Store(conf.producers)
|
||||
|
||||
go func() {
|
||||
@@ -164,6 +168,7 @@ type PeriodicReader struct {
|
||||
|
||||
interval time.Duration
|
||||
timeout time.Duration
|
||||
batcher batcher
|
||||
exporter Exporter
|
||||
flushCh chan chan error
|
||||
|
||||
@@ -235,16 +240,28 @@ func (r *PeriodicReader) cardinalityLimit(kind InstrumentKind) (int, bool) {
|
||||
// collectAndExport gather all metric data related to the periodicReader r from
|
||||
// the SDK and exports it with r's exporter.
|
||||
func (r *PeriodicReader) collectAndExport(ctx context.Context) error {
|
||||
originalCtx := ctx
|
||||
ctx, cancel := context.WithTimeoutCause(ctx, r.timeout, errors.New("reader collect and export timeout"))
|
||||
defer cancel()
|
||||
|
||||
// TODO (#3047): Use a sync.Pool or persistent pointer instead of allocating rm every Collect.
|
||||
rm := r.rmPool.Get().(*metricdata.ResourceMetrics)
|
||||
defer func() {
|
||||
*rm = metricdata.ResourceMetrics{} // erase fields to allow GC to collect them.
|
||||
r.rmPool.Put(rm)
|
||||
}()
|
||||
err := r.Collect(ctx, rm)
|
||||
if err == nil {
|
||||
err = r.export(ctx, rm)
|
||||
if r.batcher.size > 0 {
|
||||
batches := r.batcher.splitResourceMetrics(rm)
|
||||
for _, batch := range batches {
|
||||
// The export timeout is applied individually to each batch by using
|
||||
// the original context.
|
||||
err = errors.Join(err, r.exportWithTimeout(originalCtx, batch))
|
||||
}
|
||||
} else {
|
||||
err = r.exporter.Export(ctx, rm)
|
||||
}
|
||||
}
|
||||
r.rmPool.Put(rm)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -307,7 +324,10 @@ func (r *PeriodicReader) collect(ctx context.Context, p any, rm *metricdata.Reso
|
||||
}
|
||||
|
||||
// export exports metric data m using r's exporter.
|
||||
func (r *PeriodicReader) export(ctx context.Context, m *metricdata.ResourceMetrics) error {
|
||||
func (r *PeriodicReader) exportWithTimeout(ctx context.Context, m *metricdata.ResourceMetrics) error {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeoutCause(ctx, r.timeout, errors.New("reader export timeout"))
|
||||
defer cancel()
|
||||
return r.exporter.Export(ctx, m)
|
||||
}
|
||||
|
||||
@@ -349,7 +369,9 @@ func (r *PeriodicReader) Shutdown(ctx context.Context) error {
|
||||
err := ErrReaderShutdown
|
||||
r.shutdownOnce.Do(func() {
|
||||
// Prioritize the ctx timeout if it is set.
|
||||
if _, ok := ctx.Deadline(); !ok {
|
||||
originalCtx := ctx
|
||||
_, userProvidedContext := ctx.Deadline()
|
||||
if !userProvidedContext {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeoutCause(ctx, r.timeout, errors.New("reader shutdown timeout"))
|
||||
defer cancel()
|
||||
@@ -369,7 +391,24 @@ func (r *PeriodicReader) Shutdown(ctx context.Context) error {
|
||||
m := r.rmPool.Get().(*metricdata.ResourceMetrics)
|
||||
err = r.collect(ctx, ph, m)
|
||||
if err == nil {
|
||||
err = r.export(ctx, m)
|
||||
if r.batcher.size > 0 {
|
||||
batches := r.batcher.splitResourceMetrics(m)
|
||||
for _, batch := range batches {
|
||||
if userProvidedContext {
|
||||
// Do not apply the export timeout if the user passed a timeout to
|
||||
// Shutdown().
|
||||
err = errors.Join(err, r.exporter.Export(ctx, batch))
|
||||
} else {
|
||||
// The export timeout is applied individually to each batch by using
|
||||
// the original context.
|
||||
err = errors.Join(err, r.exportWithTimeout(originalCtx, batch))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Do not apply the export timeout if the user passed a timeout to
|
||||
// Shutdown().
|
||||
err = r.exporter.Export(ctx, m)
|
||||
}
|
||||
}
|
||||
r.rmPool.Put(m)
|
||||
}
|
||||
|
||||
+21
-5
@@ -11,6 +11,7 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/internal/global"
|
||||
"go.opentelemetry.io/otel/metric/embedded"
|
||||
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||
@@ -236,7 +237,11 @@ func newInserter[N int64 | float64](p *pipeline, vc *cache[string, instID]) *ins
|
||||
//
|
||||
// If an instrument is determined to use a Drop aggregation, that instrument is
|
||||
// not inserted nor returned.
|
||||
func (i *inserter[N]) Instrument(inst Instrument, readerAggregation Aggregation) ([]aggregate.Measure[N], error) {
|
||||
func (i *inserter[N]) Instrument(
|
||||
inst Instrument,
|
||||
allowedKeys []attribute.Key,
|
||||
readerAggregation Aggregation,
|
||||
) ([]aggregate.Measure[N], error) {
|
||||
var (
|
||||
matched bool
|
||||
measures []aggregate.Measure[N]
|
||||
@@ -279,6 +284,12 @@ func (i *inserter[N]) Instrument(inst Instrument, readerAggregation Aggregation)
|
||||
Description: inst.Description,
|
||||
Unit: inst.Unit,
|
||||
}
|
||||
// allowedKeys == nil indicates that the WithDefaultAttributes option was not passed,
|
||||
// and all keys are allowed. An empty (non-nil) slice indicates that the option was passed
|
||||
// with an empty set of keys, and no keys are allowed.
|
||||
if allowedKeys != nil {
|
||||
stream.AttributeFilter = attribute.NewAllowKeysFilter(allowedKeys...)
|
||||
}
|
||||
in, _, e := i.cachedAggregator(inst.Scope, inst.Kind, stream, readerAggregation)
|
||||
if e != nil {
|
||||
if err == nil {
|
||||
@@ -388,6 +399,7 @@ func (i *inserter[N]) cachedAggregator(
|
||||
b := aggregate.Builder[N]{
|
||||
Temporality: i.pipeline.reader.temporality(kind),
|
||||
ReservoirFunc: reservoirFunc[N](
|
||||
kind,
|
||||
stream.ExemplarReservoirProviderSelector(stream.Aggregation),
|
||||
i.pipeline.exemplarFilter,
|
||||
),
|
||||
@@ -661,12 +673,12 @@ func newResolver[N int64 | float64](p pipelines, vc *cache[string, instID]) reso
|
||||
|
||||
// Aggregators returns the Aggregators that must be updated by the instrument
|
||||
// defined by key.
|
||||
func (r resolver[N]) Aggregators(id Instrument) ([]aggregate.Measure[N], error) {
|
||||
func (r resolver[N]) Aggregators(id Instrument, allowedKeys []attribute.Key) ([]aggregate.Measure[N], error) {
|
||||
var measures []aggregate.Measure[N]
|
||||
|
||||
var err error
|
||||
for _, i := range r.inserters {
|
||||
in, e := i.Instrument(id, i.readerDefaultAggregation(id.Kind))
|
||||
in, e := i.Instrument(id, allowedKeys, i.readerDefaultAggregation(id.Kind))
|
||||
if e != nil {
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
@@ -678,7 +690,11 @@ func (r resolver[N]) Aggregators(id Instrument) ([]aggregate.Measure[N], error)
|
||||
// HistogramAggregators returns the histogram Aggregators that must be updated by the instrument
|
||||
// defined by key. If boundaries were provided on instrument instantiation, those take precedence
|
||||
// over boundaries provided by the reader.
|
||||
func (r resolver[N]) HistogramAggregators(id Instrument, boundaries []float64) ([]aggregate.Measure[N], error) {
|
||||
func (r resolver[N]) HistogramAggregators(
|
||||
id Instrument,
|
||||
allowedKeys []attribute.Key,
|
||||
boundaries []float64,
|
||||
) ([]aggregate.Measure[N], error) {
|
||||
var measures []aggregate.Measure[N]
|
||||
|
||||
var err error
|
||||
@@ -688,7 +704,7 @@ func (r resolver[N]) HistogramAggregators(id Instrument, boundaries []float64) (
|
||||
histAgg.Boundaries = boundaries
|
||||
agg = histAgg
|
||||
}
|
||||
in, e := i.Instrument(id, agg)
|
||||
in, e := i.Instrument(id, allowedKeys, agg)
|
||||
if e != nil {
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
|
||||
+4
-2
@@ -47,7 +47,8 @@ func NewMeterProvider(options ...Option) *MeterProvider {
|
||||
shutdown: sdown,
|
||||
}
|
||||
// Log after creation so all readers show correctly they are registered.
|
||||
global.Info("MeterProvider created",
|
||||
global.Info(
|
||||
"MeterProvider created",
|
||||
"Resource", conf.res,
|
||||
"Readers", conf.readers,
|
||||
"Views", len(conf.views),
|
||||
@@ -82,7 +83,8 @@ func (mp *MeterProvider) Meter(name string, options ...metric.MeterOption) metri
|
||||
Attributes: c.InstrumentationAttributes(),
|
||||
}
|
||||
|
||||
global.Info("Meter created",
|
||||
global.Info(
|
||||
"Meter created",
|
||||
"Name", s.Name,
|
||||
"Version", s.Version,
|
||||
"SchemaURL", s.SchemaURL,
|
||||
|
||||
+9
@@ -183,6 +183,15 @@ type AggregationSelector func(InstrumentKind) Aggregation
|
||||
// mapping: Counter ⇨ Sum, Observable Counter ⇨ Sum, UpDownCounter ⇨ Sum,
|
||||
// Observable UpDownCounter ⇨ Sum, Observable Gauge ⇨ LastValue,
|
||||
// Histogram ⇨ ExplicitBucketHistogram.
|
||||
//
|
||||
// The default ExplicitBucketHistogram boundaries are designed for
|
||||
// millisecond-scale latency values. Boundaries are interpreted relative to the
|
||||
// values recorded for the instrument and are not rescaled when an instrument is
|
||||
// created with a different unit (e.g. via
|
||||
// [go.opentelemetry.io/otel/metric.WithUnit]). Instrumentation authors should
|
||||
// supply appropriate boundaries per instrument via
|
||||
// [go.opentelemetry.io/otel/metric.WithExplicitBucketBoundaries]; end users
|
||||
// can also override boundaries for a specific instrument with a [View].
|
||||
func DefaultAggregationSelector(ik InstrumentKind) Aggregation {
|
||||
switch ik {
|
||||
case InstrumentKindCounter,
|
||||
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package metric // import "go.opentelemetry.io/otel/sdk/metric"
|
||||
|
||||
import (
|
||||
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
||||
)
|
||||
|
||||
// batcher splits metrics into batches.
|
||||
type batcher struct {
|
||||
size int
|
||||
}
|
||||
|
||||
// splitResourceMetrics splits a metricdata.ResourceMetrics into multiple ResourceMetrics, sequentially,
|
||||
// ensuring no ResourceMetrics has more than `size` data points. It does not mutate the `src` object.
|
||||
func (b batcher) splitResourceMetrics(src *metricdata.ResourceMetrics) []*metricdata.ResourceMetrics {
|
||||
if b.size <= 0 || len(src.ScopeMetrics) == 0 {
|
||||
return []*metricdata.ResourceMetrics{src}
|
||||
}
|
||||
var batches []*metricdata.ResourceMetrics
|
||||
var currentBatch *metricdata.ResourceMetrics
|
||||
currentPoints := 0
|
||||
|
||||
for i := 0; i < len(src.ScopeMetrics); i++ {
|
||||
sm := src.ScopeMetrics[i]
|
||||
|
||||
take := b.size - currentPoints
|
||||
smChunks := b.splitScopeMetrics(sm, take)
|
||||
|
||||
for _, chunk := range smChunks {
|
||||
if currentBatch == nil {
|
||||
currentBatch = &metricdata.ResourceMetrics{Resource: src.Resource}
|
||||
batches = append(batches, currentBatch)
|
||||
}
|
||||
currentBatch.ScopeMetrics = append(currentBatch.ScopeMetrics, chunk)
|
||||
currentPoints += scopeMetricsDPC(chunk)
|
||||
|
||||
if currentPoints == b.size {
|
||||
currentBatch = nil
|
||||
currentPoints = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
return batches
|
||||
}
|
||||
|
||||
// splitScopeMetrics splits a metricdata.ScopeMetrics into chunks. The first chunk will have at most firstSize data points.
|
||||
func (b batcher) splitScopeMetrics(sm metricdata.ScopeMetrics, firstSize int) []metricdata.ScopeMetrics {
|
||||
smPoints := scopeMetricsDPC(sm)
|
||||
if smPoints <= firstSize {
|
||||
return []metricdata.ScopeMetrics{sm}
|
||||
}
|
||||
|
||||
var chunks []metricdata.ScopeMetrics
|
||||
var currentChunk *metricdata.ScopeMetrics
|
||||
currentPoints := 0
|
||||
targetSize := firstSize
|
||||
|
||||
for i := 0; i < len(sm.Metrics); i++ {
|
||||
m := sm.Metrics[i]
|
||||
|
||||
take := targetSize - currentPoints
|
||||
mChunks := b.splitMetric(m, take)
|
||||
|
||||
for _, mc := range mChunks {
|
||||
if currentChunk == nil {
|
||||
chunks = append(chunks, metricdata.ScopeMetrics{Scope: sm.Scope})
|
||||
currentChunk = &chunks[len(chunks)-1]
|
||||
}
|
||||
currentChunk.Metrics = append(currentChunk.Metrics, mc)
|
||||
currentPoints += metricDPC(mc)
|
||||
|
||||
if currentPoints == targetSize {
|
||||
currentChunk = nil
|
||||
currentPoints = 0
|
||||
targetSize = b.size
|
||||
}
|
||||
}
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
// splitMetric splits a metricdata.Metrics into chunks. The first chunk will have at most firstSize data points.
|
||||
func (b batcher) splitMetric(m metricdata.Metrics, firstSize int) []metricdata.Metrics {
|
||||
mPoints := metricDPC(m)
|
||||
if mPoints <= firstSize {
|
||||
return []metricdata.Metrics{m}
|
||||
}
|
||||
|
||||
var chunks []metricdata.Metrics
|
||||
mRemaining := mPoints
|
||||
mOffset := 0
|
||||
take := firstSize
|
||||
|
||||
for mRemaining > 0 {
|
||||
if take > mRemaining {
|
||||
take = mRemaining
|
||||
}
|
||||
chunks = append(chunks, copyMetricData(m, mOffset, take))
|
||||
mRemaining -= take
|
||||
mOffset += take
|
||||
take = b.size
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
// copyMetricData creates a copy of the metricdata.Metrics with the specified offset and number of datapoints to take.
|
||||
func copyMetricData(m metricdata.Metrics, offset, take int) metricdata.Metrics {
|
||||
dest := metricdata.Metrics{
|
||||
Name: m.Name,
|
||||
Description: m.Description,
|
||||
Unit: m.Unit,
|
||||
}
|
||||
switch a := m.Data.(type) {
|
||||
case metricdata.Gauge[int64]:
|
||||
dest.Data = metricdata.Gauge[int64]{DataPoints: a.DataPoints[offset : offset+take]}
|
||||
case metricdata.Gauge[float64]:
|
||||
dest.Data = metricdata.Gauge[float64]{DataPoints: a.DataPoints[offset : offset+take]}
|
||||
case metricdata.Sum[int64]:
|
||||
dest.Data = metricdata.Sum[int64]{
|
||||
DataPoints: a.DataPoints[offset : offset+take],
|
||||
Temporality: a.Temporality,
|
||||
IsMonotonic: a.IsMonotonic,
|
||||
}
|
||||
case metricdata.Sum[float64]:
|
||||
dest.Data = metricdata.Sum[float64]{
|
||||
DataPoints: a.DataPoints[offset : offset+take],
|
||||
Temporality: a.Temporality,
|
||||
IsMonotonic: a.IsMonotonic,
|
||||
}
|
||||
case metricdata.Histogram[int64]:
|
||||
dest.Data = metricdata.Histogram[int64]{
|
||||
DataPoints: a.DataPoints[offset : offset+take],
|
||||
Temporality: a.Temporality,
|
||||
}
|
||||
case metricdata.Histogram[float64]:
|
||||
dest.Data = metricdata.Histogram[float64]{
|
||||
DataPoints: a.DataPoints[offset : offset+take],
|
||||
Temporality: a.Temporality,
|
||||
}
|
||||
case metricdata.ExponentialHistogram[int64]:
|
||||
dest.Data = metricdata.ExponentialHistogram[int64]{
|
||||
DataPoints: a.DataPoints[offset : offset+take],
|
||||
Temporality: a.Temporality,
|
||||
}
|
||||
case metricdata.ExponentialHistogram[float64]:
|
||||
dest.Data = metricdata.ExponentialHistogram[float64]{
|
||||
DataPoints: a.DataPoints[offset : offset+take],
|
||||
Temporality: a.Temporality,
|
||||
}
|
||||
case metricdata.Summary:
|
||||
dest.Data = metricdata.Summary{DataPoints: a.DataPoints[offset : offset+take]}
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
// scopeMetricsDPC calculates the total number of data points in the metricdata.ScopeMetrics.
|
||||
func scopeMetricsDPC(sm metricdata.ScopeMetrics) int {
|
||||
dataPointCount := 0
|
||||
ms := sm.Metrics
|
||||
for k := range ms {
|
||||
dataPointCount += metricDPC(ms[k])
|
||||
}
|
||||
return dataPointCount
|
||||
}
|
||||
|
||||
// metricDPC calculates the total number of data points in the metricdata.Metrics.
|
||||
func metricDPC(m metricdata.Metrics) int {
|
||||
switch a := m.Data.(type) {
|
||||
case metricdata.Gauge[int64]:
|
||||
return len(a.DataPoints)
|
||||
case metricdata.Gauge[float64]:
|
||||
return len(a.DataPoints)
|
||||
case metricdata.Sum[int64]:
|
||||
return len(a.DataPoints)
|
||||
case metricdata.Sum[float64]:
|
||||
return len(a.DataPoints)
|
||||
case metricdata.Histogram[int64]:
|
||||
return len(a.DataPoints)
|
||||
case metricdata.Histogram[float64]:
|
||||
return len(a.DataPoints)
|
||||
case metricdata.ExponentialHistogram[int64]:
|
||||
return len(a.DataPoints)
|
||||
case metricdata.ExponentialHistogram[float64]:
|
||||
return len(a.DataPoints)
|
||||
case metricdata.Summary:
|
||||
return len(a.DataPoints)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
+1
-1
@@ -5,5 +5,5 @@ package metric // import "go.opentelemetry.io/otel/sdk/metric"
|
||||
|
||||
// version is the current release version of the metric SDK in use.
|
||||
func version() string {
|
||||
return "1.43.0"
|
||||
return "1.44.0"
|
||||
}
|
||||
|
||||
+4
@@ -22,6 +22,10 @@ var (
|
||||
// should be collected for certain instruments. It returns true and the exact
|
||||
// Stream to use for matching Instruments. Otherwise, if the view does not
|
||||
// match, false is returned.
|
||||
//
|
||||
// Note that attributes filtered out by a View may still appear on Exemplars,
|
||||
// because Exemplars are recorded with the dropped measurement attributes
|
||||
// when View attribute filtering is applied.
|
||||
type View func(Instrument) (Stream, bool)
|
||||
|
||||
// NewView returns a View that applies the Stream mask for all instruments that
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user