vendor: go.opentelemetry.io/otel v1.44.0, go.opentelemetry.io/contrib v0.69.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2026-06-09 16:35:02 +02:00
parent a721bd651b
commit 22d7ca46a3
127 changed files with 5676 additions and 606 deletions
+11 -11
View File
@@ -49,14 +49,14 @@ require (
github.com/spf13/pflag v1.0.10
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
github.com/xeipuuv/gojsonschema v1.2.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0
go.opentelemetry.io/otel/metric v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/sdk/metric v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
go.opentelemetry.io/otel/metric v1.44.0
go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/sdk/metric v1.44.0
go.opentelemetry.io/otel/trace v1.44.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/sync v0.20.0
golang.org/x/sys v0.45.0
@@ -82,7 +82,7 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
@@ -98,13 +98,13 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
go.etcd.io/raft/v3 v3.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/grpc v1.81.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)
+24 -22
View File
@@ -87,8 +87,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -206,24 +206,26 @@ go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 h1:SUplec5dp06reu1zaXmOXdvqH398taqrDXqUl99jxSc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0/go.mod h1:ho2g4N+ane+swq5I/VBkKWnRDY4kUINH3FuqyZqX/Ug=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -285,8 +287,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+17 -1
View File
@@ -71,6 +71,7 @@ type ServeMux struct {
streamErrorHandler StreamErrorHandlerFunc
routingErrorHandler RoutingErrorHandlerFunc
disablePathLengthFallback bool
disableHTTPMethodOverride bool
unescapingMode UnescapingMode
writeContentLength bool
disableChunkedEncoding bool
@@ -271,6 +272,19 @@ func WithDisablePathLengthFallback() ServeMuxOption {
}
}
// WithDisableHTTPMethodOverride returns a ServeMuxOption that disables the
// X-HTTP-Method-Override header handling.
//
// When this option is used, the mux will no longer allow POST requests with
// the X-HTTP-Method-Override header to override the HTTP method. The path
// length fallback (POST with application/x-www-form-urlencoded falling back
// to a matching GET handler) is not affected by this option.
func WithDisableHTTPMethodOverride() ServeMuxOption {
return func(serveMux *ServeMux) {
serveMux.disableHTTPMethodOverride = true
}
}
// WithWriteContentLength returns a ServeMuxOption to enable writing content length on non-streaming responses
func WithWriteContentLength() ServeMuxOption {
return func(serveMux *ServeMux) {
@@ -405,7 +419,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path = r.URL.RawPath
}
if override := r.Header.Get("X-HTTP-Method-Override"); override != "" && s.isPathLengthFallback(r) {
if override := r.Header.Get("X-HTTP-Method-Override"); override != "" && !s.disableHTTPMethodOverride && s.isPathLengthFallback(r) {
if err := r.ParseForm(); err != nil {
_, outboundMarshaler := MarshalerForRequest(s, r)
sterr := status.Error(codes.InvalidArgument, err.Error())
@@ -467,6 +481,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
HTTPStatus: http.StatusBadRequest,
Err: mse,
})
return
}
continue
}
@@ -509,6 +524,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
HTTPStatus: http.StatusBadRequest,
Err: mse,
})
return
}
continue
}
@@ -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) {}
@@ -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() {
@@ -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
@@ -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",
@@ -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)...),
}),
)
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,3 @@
# Instructions for Claude Code
@AGENTS.md
+89 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
@@ -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()
@@ -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.
//
@@ -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
@@ -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)
@@ -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)
}
@@ -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
@@ -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.
}
@@ -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()
}
@@ -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
@@ -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
}
@@ -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.
@@ -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
},
)
@@ -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
}
@@ -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"
}
@@ -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
}
@@ -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()
@@ -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,
@@ -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
@@ -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"
@@ -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.
//
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
@@ -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
View File
@@ -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]{}
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -13,7 +13,7 @@ import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
)
type (
@@ -79,7 +79,7 @@ func (sd stringDetector) Detect(context.Context) (*Resource, error) {
}
a := sd.K.String(value)
if !a.Valid() {
return nil, fmt.Errorf("invalid attribute: %q -> %q", a.Key, a.Value.Emit())
return nil, fmt.Errorf("invalid attribute: %q -> %q", a.Key, a.Value.String())
}
return NewWithAttributes(sd.schemaURL, sd.K.String(value)), nil
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"os"
"regexp"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
)
type containerIDProvider func() (string, error)
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
)
const (
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"errors"
"strings"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
)
type hostIDProvider func() (string, error)
+5 -2
View File
@@ -5,10 +5,13 @@
package resource // import "go.opentelemetry.io/otel/sdk/resource"
import "os/exec"
import (
"context"
"os/exec"
)
func execCommand(name string, arg ...string) (string, error) {
cmd := exec.Command(name, arg...)
cmd := exec.CommandContext(context.Background(), name, arg...)
b, err := cmd.Output()
if err != nil {
return "", err
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"strings"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
)
type osDescriptionProvider func() (string, error)
+2 -1
View File
@@ -55,7 +55,8 @@ func uname() (string, error) {
return "", err
}
return fmt.Sprintf("%s %s %s %s %s",
return fmt.Sprintf(
"%s %s %s %s %s",
unix.ByteSliceToString(utsName.Sysname[:]),
unix.ByteSliceToString(utsName.Nodename[:]),
unix.ByteSliceToString(utsName.Release[:]),
+4 -2
View File
@@ -16,7 +16,8 @@ import (
// resembles the one displayed by the Version Reporter Applet (winver.exe).
func platformOSDescription() (string, error) {
k, err := registry.OpenKey(
registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE,
)
if err != nil {
return "", err
}
@@ -37,7 +38,8 @@ func platformOSDescription() (string, error) {
displayVersion += " "
}
return fmt.Sprintf("%s %s(%s) [Version %s.%s.%s.%s]",
return fmt.Sprintf(
"%s %s(%s) [Version %s.%s.%s.%s]",
productName,
displayVersion,
releaseID,
+3 -2
View File
@@ -11,7 +11,7 @@ import (
"path/filepath"
"runtime"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
)
type (
@@ -164,7 +164,8 @@ func (processRuntimeVersionDetector) Detect(context.Context) (*Resource, error)
// Detect returns a *Resource that describes the runtime of this process.
func (processRuntimeDescriptionDetector) Detect(context.Context) (*Resource, error) {
runtimeDescription := fmt.Sprintf(
"go version %s %s/%s", runtimeVersion(), runtimeOS(), runtimeArch())
"go version %s %s/%s", runtimeVersion(), runtimeOS(), runtimeArch(),
)
return NewWithAttributes(
semconv.SchemaURL,
+6 -4
View File
@@ -163,19 +163,21 @@ func (bsp *batchSpanProcessor) Shutdown(ctx context.Context) error {
bsp.stopOnce.Do(func() {
bsp.stopped.Store(true)
wait := make(chan struct{})
// exportErr is written by the goroutine before closing wait.
// It is only read in the <-wait case, so there is no race.
var exportErr error
go func() {
close(bsp.stopCh)
bsp.stopWait.Wait()
if bsp.e != nil {
if err := bsp.e.Shutdown(ctx); err != nil {
otel.Handle(err)
}
exportErr = bsp.e.Shutdown(ctx)
}
close(wait)
}()
// Wait until the wait group is done or the context is cancelled
// Wait until the channel is ready or the context is canceled.
select {
case <-wait:
err = exportErr
case <-ctx.Done():
err = ctx.Err()
}
@@ -13,8 +13,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 (
@@ -13,8 +13,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"
)
var measureAttrsPool = sync.Pool{
@@ -86,6 +86,7 @@ func (ssp *SSP) addOption(err error) []metric.AddOption {
}
attrs := measureAttrsPool.Get().(*[]attribute.KeyValue)
defer func() {
clear(*attrs)
*attrs = (*attrs)[:0] // reset the slice for reuse
measureAttrsPool.Put(attrs)
}()
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/sdk"
"go.opentelemetry.io/otel/sdk/internal/x"
"go.opentelemetry.io/otel/semconv/v1.40.0/otelconv"
"go.opentelemetry.io/otel/semconv/v1.41.0/otelconv"
"go.opentelemetry.io/otel/trace"
)
+8 -1
View File
@@ -82,6 +82,10 @@ type TracerProvider struct {
var _ trace.TracerProvider = &TracerProvider{}
type experimentalOption interface {
Experimental()
}
// NewTracerProvider returns a new and configured TracerProvider.
//
// By default the returned TracerProvider is configured with:
@@ -99,6 +103,9 @@ func NewTracerProvider(opts ...TracerProviderOption) *TracerProvider {
o = applyTracerProviderEnvConfigs(o)
for _, opt := range opts {
if _, ok := opt.(experimentalOption); ok {
continue
}
o = opt.apply(o)
}
@@ -310,7 +317,7 @@ func (p *TracerProvider) Shutdown(ctx context.Context) error {
}
func (p *TracerProvider) getSpanProcessors() spanProcessorStates {
return *(p.spanProcessors.Load())
return *p.spanProcessors.Load()
}
// TracerProviderOption configures a TracerProvider.
+3 -2
View File
@@ -297,8 +297,9 @@ func (pb parentBased) ShouldSample(p SamplingParameters) SamplingResult {
}
func (pb parentBased) Description() string {
return fmt.Sprintf("ParentBased{root:%s,remoteParentSampled:%s,"+
"remoteParentNotSampled:%s,localParentSampled:%s,localParentNotSampled:%s}",
return fmt.Sprintf(
"ParentBased{root:%s,remoteParentSampled:%s,"+
"remoteParentNotSampled:%s,localParentSampled:%s,localParentNotSampled:%s}",
pb.root.Description(),
pb.config.remoteParentSampled.Description(),
pb.config.remoteParentNotSampled.Description(),
+92 -5
View File
@@ -20,7 +20,7 @@ import (
"go.opentelemetry.io/otel/internal/global"
"go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/embedded"
)
@@ -346,10 +346,12 @@ func (s *recordingSpan) addOverCapAttrs(limit int, attrs []attribute.KeyValue) {
}
}
// truncateAttr returns a truncated version of attr. Only string and string
// slice attribute values are truncated. String values are truncated to at
// most a length of limit. Each string slice value is truncated in this fashion
// (the slice length itself is unaffected).
// truncateAttr returns a truncated version of attr. Only string, string
// slice, byte slice, and slice attribute values are truncated. String values are truncated
// to at most a length of limit. Each string slice value is truncated in this
// fashion (the slice length itself is unaffected), and byte slice values are truncated to at most
// limit bytes. For slice attribute values, the limit is applied to each
// element recursively.
//
// No truncation is performed for a negative limit.
func truncateAttr(limit int, attr attribute.KeyValue) attribute.KeyValue {
@@ -366,10 +368,95 @@ func truncateAttr(limit int, attr attribute.KeyValue) attribute.KeyValue {
v[i] = truncate(limit, v[i])
}
return attr.Key.StringSlice(v)
case attribute.BYTESLICE:
v := attr.Value.AsString()
if len(v) > limit {
return attr.Key.ByteSlice([]byte(v[:limit]))
}
return attr
case attribute.SLICE:
v := attr.Value.AsSlice()
if !slices.ContainsFunc(v, func(e attribute.Value) bool { return needsTruncation(limit, e) }) {
return attr
}
newV := make([]attribute.Value, len(v))
for i, elem := range v {
newV[i] = truncateValue(limit, elem)
}
return attr.Key.Slice(newV...)
}
return attr
}
// truncateValue returns a truncated version of v. Only string, string slice,
// byte slice, and (recursively) slice values are modified.
//
// No truncation is performed for a negative limit.
func truncateValue(limit int, v attribute.Value) attribute.Value {
switch v.Type() {
case attribute.STRING:
return attribute.StringValue(truncate(limit, v.AsString()))
case attribute.STRINGSLICE:
ss := v.AsStringSlice()
for i := range ss {
ss[i] = truncate(limit, ss[i])
}
return attribute.StringSliceValue(ss)
case attribute.BYTESLICE:
// len(v.AsString()) is identical to len(v.AsByteSlice()) but
// avoids allocating the full slice before truncation.
s := v.AsString()
if limit >= 0 && len(s) > limit {
return attribute.ByteSliceValue([]byte(s[:limit]))
}
case attribute.SLICE:
sl := v.AsSlice()
if !slices.ContainsFunc(sl, func(e attribute.Value) bool { return needsTruncation(limit, e) }) {
return v
}
newSl := make([]attribute.Value, len(sl))
for i, elem := range sl {
newSl[i] = truncateValue(limit, elem)
}
return attribute.SliceValue(newSl...)
}
return v
}
// stringNeedsTruncation reports whether s would be modified by truncate for the
// given limit.
func stringNeedsTruncation(limit int, s string) bool {
if limit < 0 || len(s) <= limit {
return false
}
return utf8.RuneCountInString(s) > limit || !utf8.ValidString(s)
}
// needsTruncation reports whether v would be modified by truncateValue for the
// given limit.
func needsTruncation(limit int, v attribute.Value) bool {
switch v.Type() {
case attribute.STRING:
return stringNeedsTruncation(limit, v.AsString())
case attribute.BYTESLICE:
// len(v.AsString()) is identical to len(v.AsByteSlice()) but
// avoids memory allocation.
if limit >= 0 && len(v.AsString()) > limit {
return true
}
case attribute.STRINGSLICE:
for _, s := range v.AsStringSlice() {
if stringNeedsTruncation(limit, s) {
return true
}
}
case attribute.SLICE:
return slices.ContainsFunc(v.AsSlice(), func(e attribute.Value) bool { return needsTruncation(limit, e) })
}
return false
}
// truncate returns a truncated version of s such that it contains less than
// the limit number of characters. Truncation is applied by returning the limit
// number of valid characters contained in s.
+4 -2
View File
@@ -35,8 +35,10 @@ const (
type SpanLimits struct {
// AttributeValueLengthLimit is the maximum allowed attribute value length.
//
// This limit only applies to string and string slice attribute values.
// Any string longer than this value will be truncated to this length.
// This limit only applies to string, string slice, byte slice, and slice attribute
// values. Any string and byte slice longer than this value will be truncated to this
// length. For slice attribute values, the limit is applied to each string and byte slice
// element recursively.
//
// Setting this to a negative value means no limit is applied.
AttributeValueLengthLimit int
+1 -1
View File
@@ -6,5 +6,5 @@ package sdk // import "go.opentelemetry.io/otel/sdk"
// Version is the current release version of the OpenTelemetry SDK in use.
func Version() string {
return "1.43.0"
return "1.44.0"
}
+8 -4
View File
@@ -1447,9 +1447,11 @@ func AWSExtendedRequestID(val string) attribute.KeyValue {
// AWSKinesisStreamName returns an attribute KeyValue conforming to the
// "aws.kinesis.stream_name" semantic conventions. It represents the name of the
// AWS Kinesis [stream] the request refers to. Corresponds to the `--stream-name`
// parameter of the Kinesis [describe-stream] operation.
//
// parameter of the Kinesis [describe-stream] operation.
//
// [stream]: https://docs.aws.amazon.com/streams/latest/dev/introduction.html
//
// [describe-stream]: https://docs.aws.amazon.com/cli/latest/reference/kinesis/describe-stream.html
func AWSKinesisStreamName(val string) attribute.KeyValue {
return AWSKinesisStreamNameKey.String(val)
@@ -1459,7 +1461,8 @@ func AWSKinesisStreamName(val string) attribute.KeyValue {
// "aws.lambda.invoked_arn" semantic conventions. It represents the full invoked
// ARN as provided on the `Context` passed to the function (
// `Lambda-Runtime-Invoked-Function-Arn` header on the `/runtime/invocation/next`
// applicable).
//
// applicable).
func AWSLambdaInvokedARN(val string) attribute.KeyValue {
return AWSLambdaInvokedARNKey.String(val)
}
@@ -2635,7 +2638,8 @@ func CloudRegion(val string) attribute.KeyValue {
// "cloud.resource_id" semantic conventions. It represents the cloud
// provider-specific native identifier of the monitored cloud resource (e.g. an
// [ARN] on AWS, a [fully qualified resource ID] on Azure, a [full resource name]
// on GCP).
//
// on GCP).
//
// [ARN]: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
// [fully qualified resource ID]: https://learn.microsoft.com/rest/api/resources/resources/get-by-id
@@ -15190,4 +15194,4 @@ func ZOSSmfID(val string) attribute.KeyValue {
// to which the z/OS system belongs too.
func ZOSSysplexName(val string) attribute.KeyValue {
return ZOSSysplexNameKey.String(val)
}
}
-3
View File
@@ -1,3 +0,0 @@
# Semconv v1.40.0
[![PkgGoDev](https://pkg.go.dev/badge/go.opentelemetry.io/otel/semconv/v1.40.0)](https://pkg.go.dev/go.opentelemetry.io/otel/semconv/v1.40.0)
@@ -1,7 +1,7 @@
<!-- Generated. DO NOT MODIFY. -->
# Migration from v1.39.0 to v1.40.0
# Migration from v1.40.0 to v1.41.0
The `go.opentelemetry.io/otel/semconv/v1.40.0` package should be a drop-in replacement for `go.opentelemetry.io/otel/semconv/v1.39.0` with the following exceptions.
The `go.opentelemetry.io/otel/semconv/v1.41.0` package should be a drop-in replacement for `go.opentelemetry.io/otel/semconv/v1.40.0` with the following exceptions.
## Removed
@@ -11,17 +11,7 @@ Refer to the [OpenTelemetry Semantic Conventions documentation] for deprecation
If the type is not listed in the documentation as deprecated, it has been removed in this version due to lack of applicability or use.
If you use any of these non-deprecated declarations in your Go application, please [open an issue] describing your use-case.
- `ErrorMessage`
- `ErrorMessageKey`
- `RPCMessageCompressedSize`
- `RPCMessageCompressedSizeKey`
- `RPCMessageID`
- `RPCMessageIDKey`
- `RPCMessageTypeKey`
- `RPCMessageTypeReceived`
- `RPCMessageTypeSent`
- `RPCMessageUncompressedSize`
- `RPCMessageUncompressedSizeKey`
- `DeploymentEnvironmentName`
[OpenTelemetry Semantic Conventions documentation]: https://github.com/open-telemetry/semantic-conventions
[open an issue]: https://github.com/open-telemetry/opentelemetry-go/issues/new?template=Blank+issue
+3
View File
@@ -0,0 +1,3 @@
# Semconv v1.41.0
[![PkgGoDev](https://pkg.go.dev/badge/go.opentelemetry.io/otel/semconv/v1.41.0)](https://pkg.go.dev/go.opentelemetry.io/otel/semconv/v1.41.0)
@@ -3,7 +3,7 @@
// Code generated from semantic convention specification. DO NOT EDIT.
package semconv // import "go.opentelemetry.io/otel/semconv/v1.40.0"
package semconv // import "go.opentelemetry.io/otel/semconv/v1.41.0"
import "go.opentelemetry.io/otel/attribute"
@@ -950,7 +950,7 @@ const (
// of the [AWS Lambda EvenSource Mapping]. An event source is mapped to a lambda
// function. It's contents are read by Lambda and used to trigger a function.
// This isn't available in the lambda execution context or the lambda runtime
// environtment. This is going to be populated by the AWS SDK for each language
// environment. This is going to be populated by the AWS SDK for each language
// when that UUID is present. Some of these operations are
// Create/Delete/Get/List/Update EventSourceMapping.
//
@@ -1186,7 +1186,7 @@ const (
// AWSSecretsmanagerSecretARNKey is the attribute Key conforming to the
// "aws.secretsmanager.secret.arn" semantic conventions. It represents the ARN
// of the Secret stored in the Secrets Mangger.
// of the Secret stored in the Secrets Manager.
//
// Type: string
// RequirementLevel: Recommended
@@ -1515,7 +1515,7 @@ func AWSLambdaInvokedARN(val string) attribute.KeyValue {
// of the [AWS Lambda EvenSource Mapping]. An event source is mapped to a lambda
// function. It's contents are read by Lambda and used to trigger a function.
// This isn't available in the lambda execution context or the lambda runtime
// environtment. This is going to be populated by the AWS SDK for each language
// environment. This is going to be populated by the AWS SDK for each language
// when that UUID is present. Some of these operations are
// Create/Delete/Get/List/Update EventSourceMapping.
//
@@ -1609,7 +1609,7 @@ func AWSS3UploadID(val string) attribute.KeyValue {
// AWSSecretsmanagerSecretARN returns an attribute KeyValue conforming to the
// "aws.secretsmanager.secret.arn" semantic conventions. It represents the ARN of
// the Secret stored in the Secrets Mangger.
// the Secret stored in the Secrets Manager.
func AWSSecretsmanagerSecretARN(val string) attribute.KeyValue {
return AWSSecretsmanagerSecretARNKey.String(val)
}
@@ -2196,6 +2196,11 @@ const (
// Stability: Development
//
// Examples: "12097"
// Note: For a given pipeline run and task, the `cicd.pipeline.task.run.id` MUST
// be unique within that run. For the same task across different runs of the
// same pipeline, the `cicd.pipeline.task.run.id` MAY remain the same, enabling
// correlation of `cicd.pipeline.task.run.result` values across multiple
// pipeline runs.
CICDPipelineTaskRunIDKey = attribute.Key("cicd.pipeline.task.run.id")
// CICDPipelineTaskRunResultKey is the attribute Key conforming to the
@@ -3431,7 +3436,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "a3bf90e006b2"
//
@@ -3467,7 +3472,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "gcr.io/opentelemetry/operator"
ContainerImageNameKey = attribute.Key("container.image.name")
@@ -3478,7 +3483,7 @@ const (
//
// Type: string[]
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples:
// "example@sha256:afcc7f1ac1b49db317a7196c902e61c6c3c4607d63599ee1a82d702d249a0ccb",
@@ -3497,7 +3502,7 @@ const (
//
// Type: string[]
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "v1.27.1", "3.5.7-0"
//
@@ -3518,7 +3523,7 @@ const (
// ContainerRuntimeDescriptionKey is the attribute Key conforming to the
// "container.runtime.description" semantic conventions. It represents a
// description about the runtime which could include, for example details about
// the CRI/API version being used or other customisations.
// the CRI/API version being used or other customizations.
//
// Type: string
// RequirementLevel: Recommended
@@ -3649,7 +3654,7 @@ func ContainerName(val string) attribute.KeyValue {
// ContainerRuntimeDescription returns an attribute KeyValue conforming to the
// "container.runtime.description" semantic conventions. It represents a
// description about the runtime which could include, for example details about
// the CRI/API version being used or other customisations.
// the CRI/API version being used or other customizations.
func ContainerRuntimeDescription(val string) attribute.KeyValue {
return ContainerRuntimeDescriptionKey.String(val)
}
@@ -4260,9 +4265,9 @@ const (
// "deployment.environment.name" semantic conventions. It represents the name of
// the [deployment environment] (aka deployment tier).
//
// Type: string
// Type: Enum
// RequirementLevel: Recommended
// Stability: Development
// Stability: Stable
//
// Examples: "staging", "production"
// Note: `deployment.environment.name` does not affect the uniqueness
@@ -4312,15 +4317,6 @@ const (
DeploymentStatusKey = attribute.Key("deployment.status")
)
// DeploymentEnvironmentName returns an attribute KeyValue conforming to the
// "deployment.environment.name" semantic conventions. It represents the name of
// the [deployment environment] (aka deployment tier).
//
// [deployment environment]: https://wikipedia.org/wiki/Deployment_environment
func DeploymentEnvironmentName(val string) attribute.KeyValue {
return DeploymentEnvironmentNameKey.String(val)
}
// DeploymentID returns an attribute KeyValue conforming to the "deployment.id"
// semantic conventions. It represents the id of the deployment.
func DeploymentID(val string) attribute.KeyValue {
@@ -4334,6 +4330,22 @@ func DeploymentName(val string) attribute.KeyValue {
return DeploymentNameKey.String(val)
}
// Enum values for deployment.environment.name
var (
// Production environment
// Stability: stable
DeploymentEnvironmentNameProduction = DeploymentEnvironmentNameKey.String("production")
// Staging environment
// Stability: stable
DeploymentEnvironmentNameStaging = DeploymentEnvironmentNameKey.String("staging")
// Testing environment
// Stability: stable
DeploymentEnvironmentNameTest = DeploymentEnvironmentNameKey.String("test")
// Development environment
// Stability: stable
DeploymentEnvironmentNameDevelopment = DeploymentEnvironmentNameKey.String("development")
)
// Enum values for deployment.status
var (
// failed
@@ -4645,6 +4657,12 @@ const (
// When `error.type` is set to a type (e.g., an exception type), its
// canonical class name identifying the type within the artifact SHOULD be used.
//
// If the recorded error type is a wrapper that is not meaningful for
// failure classification, instrumentation MAY use the type of the inner
// error instead. For example, in Go, errors created with `fmt.Errorf`
// using `%w` MAY be unwrapped when the wrapper type does not help
// classify the failure.
//
// Instrumentations SHOULD document the list of errors they report.
//
// The cardinality of `error.type` within one instrumentation library SHOULD be
@@ -4718,6 +4736,11 @@ const (
// Stability: Stable
//
// Examples: "java.net.ConnectException", "OSError"
// Note: If the recorded exception type is a wrapper that is not meaningful for
// failure classification, instrumentation MAY use the type of the inner
// exception instead. For example, in Go, errors created with `fmt.Errorf`
// using `%w` MAY be unwrapped when the wrapper type does not help
// classify the failure.
ExceptionTypeKey = attribute.Key("exception.type")
)
@@ -6667,6 +6690,17 @@ const (
// Examples: "forest", "lived"
GenAIRequestStopSequencesKey = attribute.Key("gen_ai.request.stop_sequences")
// GenAIRequestStreamKey is the attribute Key conforming to the
// "gen_ai.request.stream" semantic conventions. It represents the indicates
// whether the GenAI request was made in streaming mode.
//
// Type: boolean
// RequirementLevel: Recommended
// Stability: Development
//
// Examples:
GenAIRequestStreamKey = attribute.Key("gen_ai.request.stream")
// GenAIRequestTemperatureKey is the attribute Key conforming to the
// "gen_ai.request.temperature" semantic conventions. It represents the
// temperature setting for the GenAI request.
@@ -6734,6 +6768,19 @@ const (
// Examples: "gpt-4-0613"
GenAIResponseModelKey = attribute.Key("gen_ai.response.model")
// GenAIResponseTimeToFirstChunkKey is the attribute Key conforming to the
// "gen_ai.response.time_to_first_chunk" semantic conventions. It represents the
// time to first chunk in a streaming response, measured from request issuance,
// in seconds. The value is measured from when the client issues the generation
// request to when the first chunk is received in the response stream.
//
// Type: double
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: 0.5, 1.2
GenAIResponseTimeToFirstChunkKey = attribute.Key("gen_ai.response.time_to_first_chunk")
// GenAIRetrievalDocumentsKey is the attribute Key conforming to the
// "gen_ai.retrieval.documents" semantic conventions. It represents the
// documents retrieved.
@@ -6875,7 +6922,7 @@ const (
// GenAIToolDefinitionsKey is the attribute Key conforming to the
// "gen_ai.tool.definitions" semantic conventions. It represents the list of
// source system tool definitions available to the GenAI agent or model.
// tool definitions available to the GenAI agent or model.
//
// Type: any
// RequirementLevel: Recommended
@@ -6887,19 +6934,18 @@ const (
// "description": "The city and state, e.g. San Francisco, CA"\n },\n "unit":
// {\n "type": "string",\n "enum": [\n "celsius",\n "fahrenheit"\n ]\n }\n },\n
// "required": [\n "location",\n "unit"\n ]\n }\n }\n]\n"
// Note: The value of this attribute matches source system tool definition
// format.
// Note: Instrumentations MUST follow [Tool Definitions JSON Schema].
//
// It's expected to be an array of objects where each object represents a tool
// definition. In case a serialized string is available
// to the instrumentation, the instrumentation SHOULD do the best effort to
// deserialize it to an array. When recorded on spans, it MAY be recorded as a
// JSON string if structured format is not supported and SHOULD be recorded in
// structured form otherwise.
// When the attribute is recorded on events, it MUST be recorded in structured
// form. When recorded on spans, it MAY be recorded as a JSON string if
// structured
// format is not supported and SHOULD be recorded in structured form otherwise.
//
// Since this attribute could be large, it's NOT RECOMMENDED to populate
// it by default. Instrumentations MAY provide a way to enable
// populating this attribute.
// non-required properties by default. Instrumentations MAY provide a way
// to enable populating optional properties.
//
// [Tool Definitions JSON Schema]: /docs/gen-ai/gen-ai-tool-definitions.json
GenAIToolDefinitionsKey = attribute.Key("gen_ai.tool.definitions")
// GenAIToolDescriptionKey is the attribute Key conforming to the
@@ -6997,6 +7043,32 @@ const (
//
// Examples: 180
GenAIUsageOutputTokensKey = attribute.Key("gen_ai.usage.output_tokens")
// GenAIUsageReasoningOutputTokensKey is the attribute Key conforming to the
// "gen_ai.usage.reasoning.output_tokens" semantic conventions. It represents
// the number of output tokens used for reasoning (e.g. chain-of-thought,
// extended thinking).
//
// Type: int
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: 50
// Note: The value SHOULD be included in `gen_ai.usage.output_tokens`.
GenAIUsageReasoningOutputTokensKey = attribute.Key("gen_ai.usage.reasoning.output_tokens")
// GenAIWorkflowNameKey is the attribute Key conforming to the
// "gen_ai.workflow.name" semantic conventions. It represents the human-readable
// name of the GenAI workflow provided by the application.
//
// Type: string
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: "multi_agent_rag", "customer_support_pipeline"
// Note: This attribute can be populated in different frameworks eg: name of the
// first chain in LangChain OR name of the crew in CrewAI.
GenAIWorkflowNameKey = attribute.Key("gen_ai.workflow.name")
)
// GenAIAgentDescription returns an attribute KeyValue conforming to the
@@ -7139,6 +7211,13 @@ func GenAIRequestStopSequences(val ...string) attribute.KeyValue {
return GenAIRequestStopSequencesKey.StringSlice(val)
}
// GenAIRequestStream returns an attribute KeyValue conforming to the
// "gen_ai.request.stream" semantic conventions. It represents the indicates
// whether the GenAI request was made in streaming mode.
func GenAIRequestStream(val bool) attribute.KeyValue {
return GenAIRequestStreamKey.Bool(val)
}
// GenAIRequestTemperature returns an attribute KeyValue conforming to the
// "gen_ai.request.temperature" semantic conventions. It represents the
// temperature setting for the GenAI request.
@@ -7182,6 +7261,15 @@ func GenAIResponseModel(val string) attribute.KeyValue {
return GenAIResponseModelKey.String(val)
}
// GenAIResponseTimeToFirstChunk returns an attribute KeyValue conforming to the
// "gen_ai.response.time_to_first_chunk" semantic conventions. It represents the
// time to first chunk in a streaming response, measured from request issuance,
// in seconds. The value is measured from when the client issues the generation
// request to when the first chunk is received in the response stream.
func GenAIResponseTimeToFirstChunk(val float64) attribute.KeyValue {
return GenAIResponseTimeToFirstChunkKey.Float64(val)
}
// GenAIRetrievalQueryText returns an attribute KeyValue conforming to the
// "gen_ai.retrieval.query.text" semantic conventions. It represents the query
// text used for retrieval.
@@ -7245,6 +7333,21 @@ func GenAIUsageOutputTokens(val int) attribute.KeyValue {
return GenAIUsageOutputTokensKey.Int(val)
}
// GenAIUsageReasoningOutputTokens returns an attribute KeyValue conforming to
// the "gen_ai.usage.reasoning.output_tokens" semantic conventions. It represents
// the number of output tokens used for reasoning (e.g. chain-of-thought,
// extended thinking).
func GenAIUsageReasoningOutputTokens(val int) attribute.KeyValue {
return GenAIUsageReasoningOutputTokensKey.Int(val)
}
// GenAIWorkflowName returns an attribute KeyValue conforming to the
// "gen_ai.workflow.name" semantic conventions. It represents the human-readable
// name of the GenAI workflow provided by the application.
func GenAIWorkflowName(val string) attribute.KeyValue {
return GenAIWorkflowNameKey.String(val)
}
// Enum values for gen_ai.operation.name
var (
// Chat completion operation such as [OpenAI Chat API]
@@ -7281,6 +7384,9 @@ var (
// Execute a tool
// Stability: development
GenAIOperationNameExecuteTool = GenAIOperationNameKey.String("execute_tool")
// Invoke GenAI workflow
// Stability: development
GenAIOperationNameInvokeWorkflow = GenAIOperationNameKey.String("invoke_workflow")
)
// Enum values for gen_ai.output.type
@@ -7335,7 +7441,7 @@ var (
// [Azure OpenAI]
// Stability: development
//
// [Azure OpenAI]: https://azure.microsoft.com/products/ai-services/openai-service/
// [Azure OpenAI]: https://learn.microsoft.com/en-us/azure/ai-services/openai/overview
GenAIProviderNameAzureAIOpenAI = GenAIProviderNameKey.String("azure.ai.openai")
// [IBM Watsonx AI]
// Stability: development
@@ -7551,6 +7657,44 @@ var (
// Namespace: go
const (
// GoCPUDetailedStateKey is the attribute Key conforming to the
// "go.cpu.detailed_state" semantic conventions. It represents the detailed
// state of the CPU.
//
// Type: string
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: "gc/pause", "gc/mark/assist"
// Note: Value SHOULD match the specific CPU class reported by the Go runtime
// under `/cpu/classes/...`. The list of possible values is subject to change
// with the Go version used.
GoCPUDetailedStateKey = attribute.Key("go.cpu.detailed_state")
// GoCPUStateKey is the attribute Key conforming to the "go.cpu.state" semantic
// conventions. It represents the state of the CPU.
//
// Type: Enum
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: "user", "gc"
GoCPUStateKey = attribute.Key("go.cpu.state")
// GoMemoryDetailedTypeKey is the attribute Key conforming to the
// "go.memory.detailed_type" semantic conventions. It represents the detailed
// type of memory.
//
// Type: string
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: "heap/objects", "heap/free"
// Note: Value SHOULD match the specific memory class reported by the Go runtime
// under `/memory/classes/...`. The list of possible values is subject to change
// with the Go version used.
GoMemoryDetailedTypeKey = attribute.Key("go.memory.detailed_type")
// GoMemoryTypeKey is the attribute Key conforming to the "go.memory.type"
// semantic conventions. It represents the type of memory.
//
@@ -7562,6 +7706,36 @@ const (
GoMemoryTypeKey = attribute.Key("go.memory.type")
)
// GoCPUDetailedState returns an attribute KeyValue conforming to the
// "go.cpu.detailed_state" semantic conventions. It represents the detailed state
// of the CPU.
func GoCPUDetailedState(val string) attribute.KeyValue {
return GoCPUDetailedStateKey.String(val)
}
// GoMemoryDetailedType returns an attribute KeyValue conforming to the
// "go.memory.detailed_type" semantic conventions. It represents the detailed
// type of memory.
func GoMemoryDetailedType(val string) attribute.KeyValue {
return GoMemoryDetailedTypeKey.String(val)
}
// Enum values for go.cpu.state
var (
// CPU time spent running user Go code.
// Stability: development
GoCPUStateUser = GoCPUStateKey.String("user")
// CPU time spent performing garbage collection tasks.
// Stability: development
GoCPUStateGC = GoCPUStateKey.String("gc")
// CPU time spent returning unused memory to the underlying platform.
// Stability: development
GoCPUStateScavenge = GoCPUStateKey.String("scavenge")
// Available CPU time not spent executing any Go or Go runtime code.
// Stability: development
GoCPUStateIdle = GoCPUStateKey.String("idle")
)
// Enum values for go.memory.type
var (
// Memory allocated from the heap that is reserved for stack space, whether or
@@ -7584,7 +7758,8 @@ const (
// Stability: Development
//
// Examples: query findBookById { bookById(id: ?) { name } }
// Note: The value may be sanitized to exclude sensitive information.
// Note: If instrumentation can reliably identify and redact sensitive
// information it SHOULD do it.
GraphQLDocumentKey = attribute.Key("graphql.document")
// GraphQLOperationNameKey is the attribute Key conforming to the
@@ -8335,7 +8510,7 @@ var (
const (
// HwBatteryCapacityKey is the attribute Key conforming to the
// "hw.battery.capacity" semantic conventions. It represents the design capacity
// in Watts-hours or Amper-hours.
// in Watts-hours or Ampere-hours.
//
// Type: string
// RequirementLevel: Recommended
@@ -8637,7 +8812,7 @@ const (
// HwBatteryCapacity returns an attribute KeyValue conforming to the
// "hw.battery.capacity" semantic conventions. It represents the design capacity
// in Watts-hours or Amper-hours.
// in Watts-hours or Ampere-hours.
func HwBatteryCapacity(val string) attribute.KeyValue {
return HwBatteryCapacityKey.String(val)
}
@@ -9026,7 +9201,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "opentelemetry-cluster"
K8SClusterNameKey = attribute.Key("k8s.cluster.name")
@@ -9037,7 +9212,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "218fc5a9-a5f1-4b54-aa05-46717d0ab26d"
// Note: K8s doesn't have support for obtaining a cluster ID. If this is ever
@@ -9073,7 +9248,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "redis"
K8SContainerNameKey = attribute.Key("k8s.container.name")
@@ -9085,7 +9260,7 @@ const (
//
// Type: int
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples:
K8SContainerRestartCountKey = attribute.Key("k8s.container.restart_count")
@@ -9136,7 +9311,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "opentelemetry"
K8SCronJobNameKey = attribute.Key("k8s.cronjob.name")
@@ -9146,7 +9321,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"
K8SCronJobUIDKey = attribute.Key("k8s.cronjob.uid")
@@ -9157,7 +9332,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "opentelemetry"
K8SDaemonSetNameKey = attribute.Key("k8s.daemonset.name")
@@ -9167,7 +9342,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"
K8SDaemonSetUIDKey = attribute.Key("k8s.daemonset.uid")
@@ -9178,7 +9353,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "opentelemetry"
K8SDeploymentNameKey = attribute.Key("k8s.deployment.name")
@@ -9189,7 +9364,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"
K8SDeploymentUIDKey = attribute.Key("k8s.deployment.uid")
@@ -9279,7 +9454,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "opentelemetry"
K8SJobNameKey = attribute.Key("k8s.job.name")
@@ -9289,7 +9464,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"
K8SJobUIDKey = attribute.Key("k8s.job.uid")
@@ -9300,7 +9475,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "default"
K8SNamespaceNameKey = attribute.Key("k8s.namespace.name")
@@ -9365,27 +9540,128 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "node-1"
K8SNodeNameKey = attribute.Key("k8s.node.name")
// K8SNodeSystemContainerNameKey is the attribute Key conforming to the
// "k8s.node.system_container.name" semantic conventions. It represents the name
// of the system container running on the K8s Node.
//
// Type: string
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: "kubelet", "runtime", "pods", "misc"
K8SNodeSystemContainerNameKey = attribute.Key("k8s.node.system_container.name")
// K8SNodeUIDKey is the attribute Key conforming to the "k8s.node.uid" semantic
// conventions. It represents the UID of the Node.
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "1eb3a0c6-0477-4080-a9cb-0cb7db65c6a2"
K8SNodeUIDKey = attribute.Key("k8s.node.uid")
// K8SPersistentvolumeNameKey is the attribute Key conforming to the
// "k8s.persistentvolume.name" semantic conventions. It represents the name of
// the PersistentVolume.
//
// Type: string
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: "pv-data-01"
K8SPersistentvolumeNameKey = attribute.Key("k8s.persistentvolume.name")
// K8SPersistentvolumeReclaimPolicyKey is the attribute Key conforming to the
// "k8s.persistentvolume.reclaim_policy" semantic conventions. It represents the
// reclaim policy of the PersistentVolume.
//
// Type: Enum
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: "Delete", "Retain", "Recycle"
// Note: This attribute aligns with the `persistentVolumeReclaimPolicy` field of
// the
// [K8s PersistentVolumeSpec].
//
// [K8s PersistentVolumeSpec]: https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-v1/#PersistentVolumeSpec
K8SPersistentvolumeReclaimPolicyKey = attribute.Key("k8s.persistentvolume.reclaim_policy")
// K8SPersistentvolumeStatusPhaseKey is the attribute Key conforming to the
// "k8s.persistentvolume.status.phase" semantic conventions. It represents the
// phase of the PersistentVolume.
//
// Type: Enum
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: "Pending", "Available", "Bound", "Released", "Failed"
// Note: This attribute aligns with the `phase` field of the
// [K8s PersistentVolumeStatus].
//
// [K8s PersistentVolumeStatus]: https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-v1/#PersistentVolumeStatus
K8SPersistentvolumeStatusPhaseKey = attribute.Key("k8s.persistentvolume.status.phase")
// K8SPersistentvolumeUIDKey is the attribute Key conforming to the
// "k8s.persistentvolume.uid" semantic conventions. It represents the UID of the
// PersistentVolume.
//
// Type: string
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"
K8SPersistentvolumeUIDKey = attribute.Key("k8s.persistentvolume.uid")
// K8SPersistentvolumeclaimNameKey is the attribute Key conforming to the
// "k8s.persistentvolumeclaim.name" semantic conventions. It represents the name
// of the PersistentVolumeClaim.
//
// Type: string
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: "pvc-data-01"
K8SPersistentvolumeclaimNameKey = attribute.Key("k8s.persistentvolumeclaim.name")
// K8SPersistentvolumeclaimStatusPhaseKey is the attribute Key conforming to the
// "k8s.persistentvolumeclaim.status.phase" semantic conventions. It represents
// the phase of the PersistentVolumeClaim.
//
// Type: Enum
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: "Pending", "Bound", "Lost"
// Note: This attribute aligns with the `phase` field of the
// [K8s PersistentVolumeClaimStatus].
//
// [K8s PersistentVolumeClaimStatus]: https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-claim-v1/#PersistentVolumeClaimStatus
K8SPersistentvolumeclaimStatusPhaseKey = attribute.Key("k8s.persistentvolumeclaim.status.phase")
// K8SPersistentvolumeclaimUIDKey is the attribute Key conforming to the
// "k8s.persistentvolumeclaim.uid" semantic conventions. It represents the UID
// of the PersistentVolumeClaim.
//
// Type: string
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"
K8SPersistentvolumeclaimUIDKey = attribute.Key("k8s.persistentvolumeclaim.uid")
// K8SPodHostnameKey is the attribute Key conforming to the "k8s.pod.hostname"
// semantic conventions. It represents the specifies the hostname of the Pod.
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "collector-gateway"
// Note: The K8s Pod spec has an optional hostname field, which can be used to
@@ -9405,7 +9681,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "172.18.0.2"
// Note: This attribute aligns with the `podIP` field of the
@@ -9419,7 +9695,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "opentelemetry-pod-autoconf"
K8SPodNameKey = attribute.Key("k8s.pod.name")
@@ -9430,7 +9706,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "2025-12-04T08:41:03Z"
// Note: Date and time at which the object was acknowledged by the Kubelet.
@@ -9474,7 +9750,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"
K8SPodUIDKey = attribute.Key("k8s.pod.uid")
@@ -9485,7 +9761,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "opentelemetry"
K8SReplicaSetNameKey = attribute.Key("k8s.replicaset.name")
@@ -9496,7 +9772,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"
K8SReplicaSetUIDKey = attribute.Key("k8s.replicaset.uid")
@@ -9709,7 +9985,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "opentelemetry"
K8SStatefulSetNameKey = attribute.Key("k8s.statefulset.name")
@@ -9720,7 +9996,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Beta
// Stability: Release_Candidate
//
// Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"
K8SStatefulSetUIDKey = attribute.Key("k8s.statefulset.uid")
@@ -10005,12 +10281,80 @@ func K8SNodeName(val string) attribute.KeyValue {
return K8SNodeNameKey.String(val)
}
// K8SNodeSystemContainerName returns an attribute KeyValue conforming to the
// "k8s.node.system_container.name" semantic conventions. It represents the name
// of the system container running on the K8s Node.
func K8SNodeSystemContainerName(val string) attribute.KeyValue {
return K8SNodeSystemContainerNameKey.String(val)
}
// K8SNodeUID returns an attribute KeyValue conforming to the "k8s.node.uid"
// semantic conventions. It represents the UID of the Node.
func K8SNodeUID(val string) attribute.KeyValue {
return K8SNodeUIDKey.String(val)
}
// K8SPersistentvolumeAnnotation returns an attribute KeyValue conforming to the
// "k8s.persistentvolume.annotation" semantic conventions. It represents the
// annotation placed on the PersistentVolume, the `<key>` being the annotation
// name, the value being the annotation value, even if the value is empty.
func K8SPersistentvolumeAnnotation(key string, val string) attribute.KeyValue {
return attribute.String("k8s.persistentvolume.annotation."+key, val)
}
// K8SPersistentvolumeLabel returns an attribute KeyValue conforming to the
// "k8s.persistentvolume.label" semantic conventions. It represents the label
// placed on the PersistentVolume, the `<key>` being the label name, the value
// being the label value, even if the value is empty.
func K8SPersistentvolumeLabel(key string, val string) attribute.KeyValue {
return attribute.String("k8s.persistentvolume.label."+key, val)
}
// K8SPersistentvolumeName returns an attribute KeyValue conforming to the
// "k8s.persistentvolume.name" semantic conventions. It represents the name of
// the PersistentVolume.
func K8SPersistentvolumeName(val string) attribute.KeyValue {
return K8SPersistentvolumeNameKey.String(val)
}
// K8SPersistentvolumeUID returns an attribute KeyValue conforming to the
// "k8s.persistentvolume.uid" semantic conventions. It represents the UID of the
// PersistentVolume.
func K8SPersistentvolumeUID(val string) attribute.KeyValue {
return K8SPersistentvolumeUIDKey.String(val)
}
// K8SPersistentvolumeclaimAnnotation returns an attribute KeyValue conforming to
// the "k8s.persistentvolumeclaim.annotation" semantic conventions. It represents
// the annotation placed on the PersistentVolumeClaim, the `<key>` being the
// annotation name, the value being the annotation value, even if the value is
// empty.
func K8SPersistentvolumeclaimAnnotation(key string, val string) attribute.KeyValue {
return attribute.String("k8s.persistentvolumeclaim.annotation."+key, val)
}
// K8SPersistentvolumeclaimLabel returns an attribute KeyValue conforming to the
// "k8s.persistentvolumeclaim.label" semantic conventions. It represents the
// label placed on the PersistentVolumeClaim, the `<key>` being the label name,
// the value being the label value, even if the value is empty.
func K8SPersistentvolumeclaimLabel(key string, val string) attribute.KeyValue {
return attribute.String("k8s.persistentvolumeclaim.label."+key, val)
}
// K8SPersistentvolumeclaimName returns an attribute KeyValue conforming to the
// "k8s.persistentvolumeclaim.name" semantic conventions. It represents the name
// of the PersistentVolumeClaim.
func K8SPersistentvolumeclaimName(val string) attribute.KeyValue {
return K8SPersistentvolumeclaimNameKey.String(val)
}
// K8SPersistentvolumeclaimUID returns an attribute KeyValue conforming to the
// "k8s.persistentvolumeclaim.uid" semantic conventions. It represents the UID of
// the PersistentVolumeClaim.
func K8SPersistentvolumeclaimUID(val string) attribute.KeyValue {
return K8SPersistentvolumeclaimUIDKey.String(val)
}
// K8SPodAnnotation returns an attribute KeyValue conforming to the
// "k8s.pod.annotation" semantic conventions. It represents the annotation placed
// on the Pod, the `<key>` being the annotation name, the value being the
@@ -10318,6 +10662,51 @@ var (
K8SNodeConditionTypeNetworkUnavailable = K8SNodeConditionTypeKey.String("NetworkUnavailable")
)
// Enum values for k8s.persistentvolume.reclaim_policy
var (
// The volume will be deleted when released from its claim.
// Stability: development
K8SPersistentvolumeReclaimPolicyDelete = K8SPersistentvolumeReclaimPolicyKey.String("Delete")
// The volume will be recycled (basic scrub) when released from its claim.
// Stability: development
K8SPersistentvolumeReclaimPolicyRecycle = K8SPersistentvolumeReclaimPolicyKey.String("Recycle")
// The volume will be retained when released from its claim.
// Stability: development
K8SPersistentvolumeReclaimPolicyRetain = K8SPersistentvolumeReclaimPolicyKey.String("Retain")
)
// Enum values for k8s.persistentvolume.status.phase
var (
// The volume is available and not yet bound to a claim.
// Stability: development
K8SPersistentvolumeStatusPhaseAvailable = K8SPersistentvolumeStatusPhaseKey.String("Available")
// The volume is bound to a claim.
// Stability: development
K8SPersistentvolumeStatusPhaseBound = K8SPersistentvolumeStatusPhaseKey.String("Bound")
// The volume has failed its automatic reclamation.
// Stability: development
K8SPersistentvolumeStatusPhaseFailed = K8SPersistentvolumeStatusPhaseKey.String("Failed")
// The volume is being provisioned.
// Stability: development
K8SPersistentvolumeStatusPhasePending = K8SPersistentvolumeStatusPhaseKey.String("Pending")
// The claim has been deleted but the volume is not yet available.
// Stability: development
K8SPersistentvolumeStatusPhaseReleased = K8SPersistentvolumeStatusPhaseKey.String("Released")
)
// Enum values for k8s.persistentvolumeclaim.status.phase
var (
// The claim is bound to a volume.
// Stability: development
K8SPersistentvolumeclaimStatusPhaseBound = K8SPersistentvolumeclaimStatusPhaseKey.String("Bound")
// The claim has lost its underlying volume (the volume does not exist anymore).
// Stability: development
K8SPersistentvolumeclaimStatusPhaseLost = K8SPersistentvolumeclaimStatusPhaseKey.String("Lost")
// The claim has not yet been bound to a volume.
// Stability: development
K8SPersistentvolumeclaimStatusPhasePending = K8SPersistentvolumeclaimStatusPhaseKey.String("Pending")
)
// Enum values for k8s.pod.status.phase
var (
// The pod has been accepted by the system, but one or more of the containers
@@ -12669,7 +13058,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Development
// Stability: Stable
//
// Examples: "browser.mouse.click", "device.app.lifecycle"
// Note: This attribute SHOULD be used by non-OTLP exporters when destination
@@ -13240,14 +13629,27 @@ const (
// ProcessExecutableBuildIDHtlhashKey is the attribute Key conforming to the
// "process.executable.build_id.htlhash" semantic conventions. It represents the
// profiling specific build ID for executables. See the OTel specification for
// Profiles for more information.
// deterministic build ID for executables.
//
// Type: string
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: "600DCAFE4A110000F2BF38C493F5FB92"
// Note: GNU and Go build IDs may be stripped or unavailable in some
// environments
// (e.g., Alpine Linux, Docker images). This attribute provides a deterministic
// build ID computed by hashing the first and last 4096 bytes of the file
// along with its length:
//
// ```
// Input ← Concat(File[:4096], File[-4096:], BigEndianUInt64(Len(File)))
// Digest ← SHA256(Input)
// BuildID ← Digest[:16]
// ```
//
// The result is the first 16 bytes (128 bits) of the SHA256 digest,
// represented as a hex string.
ProcessExecutableBuildIDHtlhashKey = attribute.Key("process.executable.build_id.htlhash")
// ProcessExecutableNameKey is the attribute Key conforming to the
@@ -13603,8 +14005,7 @@ func ProcessExecutableBuildIDGo(val string) attribute.KeyValue {
// ProcessExecutableBuildIDHtlhash returns an attribute KeyValue conforming to
// the "process.executable.build_id.htlhash" semantic conventions. It represents
// the profiling specific build ID for executables. See the OTel specification
// for Profiles for more information.
// the deterministic build ID for executables.
func ProcessExecutableBuildIDHtlhash(val string) attribute.KeyValue {
return ProcessExecutableBuildIDHtlhashKey.String(val)
}
@@ -14317,9 +14718,11 @@ const (
// Examples: "shoppingcart"
// Note: MUST be the same for all instances of horizontally scaled services. If
// the value was not specified, SDKs MUST fallback to `unknown_service:`
// concatenated with [`process.executable.name`], e.g. `unknown_service:bash`.
// If `process.executable.name` is not available, the value MUST be set to
// concatenated with the process executable name, e.g. `unknown_service:bash`.
// If the process executable name is not available, the value MUST be set to
// `unknown_service`.
// The process executable name is the name of the process executable, the same
// value as described by the [`process.executable.name`] resource attribute.
//
// [`process.executable.name`]: process.md
ServiceNameKey = attribute.Key("service.name")
@@ -14643,6 +15046,17 @@ const (
// Examples: "ext4"
SystemFilesystemTypeKey = attribute.Key("system.filesystem.type")
// SystemMemoryLinuxHugepagesStateKey is the attribute Key conforming to the
// "system.memory.linux.hugepages.state" semantic conventions. It represents the
// Linux HugePages memory state.
//
// Type: Enum
// RequirementLevel: Recommended
// Stability: Development
//
// Examples: "free", "used"
SystemMemoryLinuxHugepagesStateKey = attribute.Key("system.memory.linux.hugepages.state")
// SystemMemoryLinuxSlabStateKey is the attribute Key conforming to the
// "system.memory.linux.slab.state" semantic conventions. It represents the
// Linux Slab memory state.
@@ -14753,6 +15167,16 @@ var (
SystemFilesystemTypeExt4 = SystemFilesystemTypeKey.String("ext4")
)
// Enum values for system.memory.linux.hugepages.state
var (
// free
// Stability: development
SystemMemoryLinuxHugepagesStateFree = SystemMemoryLinuxHugepagesStateKey.String("free")
// used
// Stability: development
SystemMemoryLinuxHugepagesStateUsed = SystemMemoryLinuxHugepagesStateKey.String("used")
)
// Enum values for system.memory.linux.slab.state
var (
// reclaimable
@@ -14817,7 +15241,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Development
// Stability: Stable
//
// Examples: "parts-unlimited-java"
// Note: Official auto instrumentation agents and distributions SHOULD set the
@@ -14832,7 +15256,7 @@ const (
//
// Type: string
// RequirementLevel: Recommended
// Stability: Development
// Stability: Stable
//
// Examples: "1.2.3"
TelemetryDistroVersionKey = attribute.Key("telemetry.distro.version")
@@ -1,9 +1,11 @@
// Code generated from semantic convention specification. DO NOT EDIT.
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package semconv implements OpenTelemetry semantic conventions.
//
// OpenTelemetry semantic conventions are agreed standardized naming
// patterns for OpenTelemetry things. This package represents the v1.40.0
// patterns for OpenTelemetry things. This package represents the v1.41.0
// version of the OpenTelemetry semantic conventions.
package semconv // import "go.opentelemetry.io/otel/semconv/v1.40.0"
package semconv // import "go.opentelemetry.io/otel/semconv/v1.41.0"

Some files were not shown because too many files have changed in this diff Show More