vendor: update buildkit to f449174742bf

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2026-06-10 16:41:09 +02:00
parent 2fcfce0e71
commit 1916210ddc
248 changed files with 29779 additions and 2300 deletions
@@ -47,7 +47,6 @@ type config struct {
TracerProvider trace.TracerProvider
MeterProvider metric.MeterProvider
SpanKind trace.SpanKind
SpanStartOptions []trace.SpanStartOption
SpanAttributes []attribute.KeyValue
MetricAttributes []attribute.KeyValue
MetricAttributesFn func(ctx context.Context) []attribute.KeyValue
@@ -204,16 +203,6 @@ func WithMessageEvents(events ...Event) Option {
})
}
// WithSpanOptions configures an additional set of
// trace.SpanOptions, which are applied to each new span.
//
// Deprecated: It is only used by the deprecated interceptor, and is unused by [NewClientHandler] and [NewServerHandler].
func WithSpanOptions(opts ...trace.SpanStartOption) Option {
return optionFunc(func(c *config) {
c.SpanStartOptions = append(c.SpanStartOptions, opts...)
})
}
// WithSpanKind returns an Option to set the span kind for spans created by
// the handler.
//
@@ -11,7 +11,7 @@ import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
grpc_codes "google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
@@ -9,7 +9,7 @@ import (
"go.opentelemetry.io/otel/attribute"
oldsemconv "go.opentelemetry.io/otel/semconv/v1.37.0" //nolint:depguard // Use of v1.37.0 is required for backward compatibility stability opt-in.
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
)
// ParseFullMethod returns a span name following the OpenTelemetry semantic
@@ -13,8 +13,8 @@ import (
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
oldrpcconv "go.opentelemetry.io/otel/semconv/v1.37.0/rpcconv" //nolint:depguard // Use of v1.37.0 is required for backward compatibility stability opt-in.
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.opentelemetry.io/otel/semconv/v1.40.0/rpcconv"
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
"go.opentelemetry.io/otel/semconv/v1.41.0/rpcconv"
"go.opentelemetry.io/otel/trace"
grpc_codes "google.golang.org/grpc/codes"
@@ -367,27 +367,39 @@ func (*config) handleRPC(
span.End()
}
var metricAttrs []attribute.KeyValue
if gctx != nil {
// Don't use gctx.metricAttrSet here, because it requires passing
// multiple RecordOptions, which would call metric.mergeSets and
// allocate a new set for each Record call.
metricAttrs = make([]attribute.KeyValue, 0, len(gctx.metricAttrs)+1)
metricAttrs = append(metricAttrs, gctx.metricAttrs...)
}
metricAttrs = append(metricAttrs, rpcStatusAttr)
// Allocate vararg slice once.
recordOpts := []metric.RecordOption{metric.WithAttributeSet(attribute.NewSet(metricAttrs...))}
// Use floating point division here for higher precision (instead of Millisecond method).
// Measure right before calling Record() to capture as much elapsed time as possible.
elapsedTime := float64(rs.EndTime.Sub(rs.BeginTime)) / float64(time.Second)
var durationEnabled bool
var oldDurationEnabled bool
if duration != nil {
duration.Record(ctx, elapsedTime, recordOpts...)
durationEnabled = duration.Enabled(ctx)
}
if oldDuration != nil {
oldDuration.Record(ctx, elapsedTime*1000.0, recordOpts...)
oldDurationEnabled = oldDuration.Enabled(ctx)
}
if durationEnabled || oldDurationEnabled {
var metricAttrs []attribute.KeyValue
if gctx != nil {
// Don't use gctx.metricAttrSet here, because it requires passing
// multiple RecordOptions, which would call metric.mergeSets and
// allocate a new set for each Record call.
metricAttrs = make([]attribute.KeyValue, 0, len(gctx.metricAttrs)+1)
metricAttrs = append(metricAttrs, gctx.metricAttrs...)
}
metricAttrs = append(metricAttrs, rpcStatusAttr)
// Allocate vararg slice once.
recordOpts := []metric.RecordOption{metric.WithAttributeSet(attribute.NewSet(metricAttrs...))}
// Use floating point division here for higher precision (instead of Millisecond method).
// Measure right before calling Record() to capture as much elapsed time as possible.
elapsedTime := float64(rs.EndTime.Sub(rs.BeginTime)) / float64(time.Second)
if durationEnabled {
duration.Record(ctx, elapsedTime, recordOpts...)
}
if oldDurationEnabled {
oldDuration.Record(ctx, elapsedTime*1000.0, recordOpts...)
}
}
default:
@@ -4,4 +4,4 @@
package otelgrpc // import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
// Version is the current release version of the gRPC instrumentation.
const Version = "0.68.0"
const Version = "0.69.0"
@@ -63,8 +63,20 @@ func (fn clientTraceOptionFunc) apply(c *clientTracer) {
}
// WithoutSubSpans will modify the httptrace.ClientTrace to only collect data
// as Events and Attributes on a span found in the context. By default
// as Events and Attributes on a span found in the context. By default
// sub-spans will be generated.
//
// This option is recommended for services that make a large number of
// outbound HTTP requests per incoming request (e.g., API gateways,
// fan-out proxies, or GraphQL servers). Each outbound request creates
// up to 7 sub-spans (http.getconn, http.dns, http.connect, http.tls,
// http.headers, http.send, http.receive) as separate heap-allocated
// trace.Span objects. In high fan-out services this multiplied span
// volume can overwhelm the span processor queue and increase GC
// pressure, resulting in elevated and continuously growing memory usage.
// Using WithoutSubSpans replaces those spans with lightweight events on
// the parent span, preserving the diagnostic information at a fraction
// of the cost.
func WithoutSubSpans() ClientTraceOption {
return clientTraceOptionFunc(func(ct *clientTracer) {
ct.useSpans = false
@@ -189,10 +201,11 @@ func NewClientTrace(ctx context.Context, opts ...ClientTraceOption) *httptrace.C
}
func (ct *clientTracer) start(hook, spanName string, attrs ...attribute.KeyValue) {
if ct.root == nil {
ct.root = trace.SpanFromContext(ct.Context)
}
if !ct.useSpans {
if ct.root == nil {
ct.root = trace.SpanFromContext(ct.Context)
}
ct.root.AddEvent(hook+".start", trace.WithAttributes(attrs...))
return
}
@@ -201,11 +214,7 @@ func (ct *clientTracer) start(hook, spanName string, attrs ...attribute.KeyValue
defer ct.mtx.Unlock()
if hookCtx, found := ct.activeHooks[hook]; !found {
var sp trace.Span
ct.activeHooks[hook], sp = ct.tr.Start(ct.getParentContext(hook), spanName, trace.WithAttributes(attrs...), trace.WithSpanKind(trace.SpanKindClient))
if ct.root == nil {
ct.root = sp
}
ct.activeHooks[hook], _ = ct.tr.Start(ct.getParentContext(hook), spanName, trace.WithAttributes(attrs...), trace.WithSpanKind(trace.SpanKindClient))
} else {
// end was called before start finished, add the start attributes and end the span here
span := trace.SpanFromContext(hookCtx)
@@ -306,14 +315,16 @@ func (ct *clientTracer) dnsDone(info httptrace.DNSDoneInfo) {
}
func (ct *clientTracer) connectStart(network, addr string) {
ct.start("http.connect."+addr, "http.connect",
ct.start(
"http.connect."+addr, "http.connect",
HTTPRemoteAddr.String(addr),
HTTPConnectionStartNetwork.String(network),
)
}
func (ct *clientTracer) connectDone(network, addr string, err error) {
ct.end("http.connect."+addr, err,
ct.end(
"http.connect."+addr, err,
HTTPConnectionDoneAddr.String(addr),
HTTPConnectionDoneNetwork.String(network),
)
@@ -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",
@@ -4,4 +4,4 @@
package otelhttptrace // import "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
// Version is the current release version of the httptrace instrumentation.
const Version = "0.68.0"
const Version = "0.69.0"
@@ -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"
}
@@ -23,16 +23,21 @@ import (
"google.golang.org/protobuf/proto"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/counter"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/observ"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/oconf"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/retry"
)
type client struct {
// req is cloned for every upload the client makes.
req *http.Request
compression Compression
requestFunc retry.RequestFunc
httpClient *http.Client
req *http.Request
compression Compression
maxRequestSize int
requestFunc retry.RequestFunc
httpClient *http.Client
inst *observ.Instrumentation
}
// Keep it in sync with golang's DefaultTransport from net/http! We
@@ -111,12 +116,17 @@ func newClient(cfg oconf.Config) (*client, error) {
}
req.Header.Set("Content-Type", "application/x-protobuf")
// Initialize the instrumentation.
inst, err := observ.NewInstrumentation(counter.NextExporterID(), cfg.Metrics.Endpoint)
return &client{
compression: Compression(cfg.Metrics.Compression),
req: req,
requestFunc: cfg.RetryConfig.RequestFunc(evaluate),
httpClient: httpClient,
}, nil
compression: Compression(cfg.Metrics.Compression),
maxRequestSize: cfg.Metrics.MaxRequestSize,
req: req,
requestFunc: cfg.RetryConfig.RequestFunc(evaluate),
httpClient: httpClient,
inst: inst,
}, err
}
// Shutdown shuts down the client, freeing all resources.
@@ -146,11 +156,20 @@ func (c *client) UploadMetrics(ctx context.Context, protoMetrics *metricpb.Resou
if err != nil {
return err
}
if maxSize := c.maxRequestSize; maxSize > 0 && len(body) > maxSize {
return fmt.Errorf("request body too large: exceeded %d bytes", maxSize)
}
request, err := c.newRequest(ctx, body)
if err != nil {
return err
}
var statusCode int
if c.inst != nil {
op := c.inst.ExportMetrics(ctx, protoMetrics)
defer func() { op.End(uploadErr, statusCode) }()
}
return errors.Join(uploadErr, c.requestFunc(ctx, func(iCtx context.Context) error {
select {
case <-iCtx.Done():
@@ -158,6 +177,7 @@ func (c *client) UploadMetrics(ctx context.Context, protoMetrics *metricpb.Resou
default:
}
statusCode = 0
request.reset(iCtx)
// nolint:gosec // URL is constructed from validated OTLP endpoint configuration
resp, err := c.httpClient.Do(request.Request)
@@ -168,15 +188,18 @@ func (c *client) UploadMetrics(ctx context.Context, protoMetrics *metricpb.Resou
if err != nil {
return err
}
if resp != nil && resp.Body != nil {
defer func() {
if err := resp.Body.Close(); err != nil {
uploadErr = errors.Join(uploadErr, err)
}
}()
if resp != nil {
statusCode = resp.StatusCode
if resp.Body != nil {
defer func() {
if err := resp.Body.Close(); err != nil {
uploadErr = errors.Join(uploadErr, err)
}
}()
}
}
if sc := resp.StatusCode; sc >= 200 && sc <= 299 {
if statusCode >= 200 && statusCode <= 299 {
// Success, do not retry.
// Read the partial success message, if any.
@@ -265,7 +288,10 @@ func (c *client) newRequest(ctx context.Context, body []byte) (request, error) {
r.Header.Set("Content-Encoding", "gzip")
gz := gzPool.Get().(*gzip.Writer)
defer gzPool.Put(gz)
defer func() {
gz.Reset(io.Discard)
gzPool.Put(gz)
}()
var b bytes.Buffer
gz.Reset(&b)
@@ -279,7 +305,7 @@ func (c *client) newRequest(ctx context.Context, body []byte) (request, error) {
}
req.bodyReader = bodyReader(b.Bytes())
req.GetBody = bodyReaderErr(body)
req.GetBody = bodyReaderErr(b.Bytes())
}
return req, nil
@@ -185,6 +185,16 @@ func WithTimeout(duration time.Duration) Option {
return wrappedOption{oconf.WithTimeout(duration)}
}
// WithMaxRequestSize sets the maximum size, in bytes, of a serialized export
// request, before compression, that the exporter will send.
//
// If size is less than or equal to zero, no request-size limit is applied.
// Disabling the limit is not recommended because it can lead to excessive
// resource consumption or abuse.
func WithMaxRequestSize(size int) Option {
return wrappedOption{oconf.WithMaxRequestSize(size)}
}
// WithRetry sets the retry policy for transient retryable errors that are
// returned by the target endpoint.
//
@@ -0,0 +1,31 @@
// Code generated by gotmpl. DO NOT MODIFY.
// source: internal/shared/counter/counter.go.tmpl
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package counter provides a simple counter for generating unique IDs.
//
// This package is used to generate unique IDs while allowing testing packages
// to reset the counter.
package counter // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/counter"
import "sync/atomic"
// exporterN is a global 0-based count of the number of exporters created.
var exporterN atomic.Int64
// NextExporterID returns the next unique ID for an exporter.
func NextExporterID() int64 {
const inc = 1
return exporterN.Add(inc) - inc
}
// SetExporterID sets the exporter ID counter to v and returns the previous
// value.
//
// This function is useful for testing purposes, allowing you to reset the
// counter. It should not be used in production code.
func SetExporterID(v int64) int64 {
return exporterN.Swap(v)
}
@@ -30,3 +30,9 @@ package internal // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/o
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/error_test.go.tmpl "--data={}" --out=transform/error_test.go
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/metricdata.go.tmpl "--data={}" --out=transform/metricdata.go
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/transform/metricdata_test.go.tmpl "--data={}" --out=transform/metricdata_test.go
//go:generate gotmpl --body=../../../../../internal/shared/counter/counter.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/counter\" }" --out=counter/counter.go
//go:generate gotmpl --body=../../../../../internal/shared/counter/counter_test.go.tmpl "--data={}" --out=counter/counter_test.go
//go:generate gotmpl --body=../../../../../internal/shared/x/x.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/x\" }" --out=x/x.go
//go:generate gotmpl --body=../../../../../internal/shared/x/x_test.go.tmpl "--data={}" --out=x/x_test.go
@@ -0,0 +1,42 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package observ // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/observ"
import metricpb "go.opentelemetry.io/proto/otlp/metrics/v1"
// countDataPoints counts the total number of data points in a ResourceMetrics.
func countDataPoints(rm *metricpb.ResourceMetrics) int64 {
if rm == nil {
return 0
}
var total int64
for _, sm := range rm.ScopeMetrics {
for _, m := range sm.Metrics {
switch data := m.Data.(type) {
case *metricpb.Metric_Gauge:
if data.Gauge != nil {
total += int64(len(data.Gauge.DataPoints))
}
case *metricpb.Metric_Sum:
if data.Sum != nil {
total += int64(len(data.Sum.DataPoints))
}
case *metricpb.Metric_Histogram:
if data.Histogram != nil {
total += int64(len(data.Histogram.DataPoints))
}
case *metricpb.Metric_ExponentialHistogram:
if data.ExponentialHistogram != nil {
total += int64(len(data.ExponentialHistogram.DataPoints))
}
case *metricpb.Metric_Summary:
if data.Summary != nil {
total += int64(len(data.Summary.DataPoints))
}
}
}
}
return total
}
@@ -0,0 +1,411 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package observ provides experimental observability instrumentation for the
// otlpmetrichttp exporter.
package observ // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/observ"
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
"strconv"
"strings"
"sync"
"time"
metricpb "go.opentelemetry.io/proto/otlp/metrics/v1"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/x"
"go.opentelemetry.io/otel/internal/global"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
"go.opentelemetry.io/otel/semconv/v1.41.0/otelconv"
)
const (
// ScopeName is the unique name of the meter used for instrumentation.
ScopeName = "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/observ"
// SchemaURL is the schema URL of the metrics produced by this
// instrumentation.
SchemaURL = semconv.SchemaURL
// Version is the current version of this instrumentation.
//
// This matches the version of the exporter.
Version = internal.Version
)
var (
measureAttrsPool = &sync.Pool{
New: func() any {
const n = 1 + // component.name
1 + // component.type
1 + // server.addr
1 + // server.port
1 + // error.type
1 // http.response.status_code
s := make([]attribute.KeyValue, 0, n)
// Return a pointer to a slice instead of a slice itself
// to avoid allocations on every call.
return &s
},
}
addOptPool = &sync.Pool{
New: func() any {
const n = 1 // WithAttributeSet
o := make([]metric.AddOption, 0, n)
return &o
},
}
recordOptPool = &sync.Pool{
New: func() any {
const n = 1 // WithAttributeSet
o := make([]metric.RecordOption, 0, n)
return &o
},
}
)
func get[T any](p *sync.Pool) *[]T { return p.Get().(*[]T) }
func put[T any](p *sync.Pool, s *[]T) {
*s = (*s)[:0] // Reset.
p.Put(s)
}
// ComponentName returns the component name for the exporter with the
// provided ID.
func ComponentName(id int64) string {
t := semconv.OTelComponentTypeOtlpHTTPMetricExporter.Value.AsString()
return fmt.Sprintf("%s/%d", t, id)
}
// Instrumentation is experimental instrumentation for the exporter.
type Instrumentation struct {
inflightMetric metric.Int64UpDownCounter
exportedMetric metric.Int64Counter
opDuration metric.Float64Histogram
attrs []attribute.KeyValue
addOpt metric.AddOption
recOpt metric.RecordOption
}
// NewInstrumentation returns instrumentation for an OTLP over HTTP metric
// exporter with the provided ID and endpoint. It uses the global
// MeterProvider to create the instrumentation.
//
// The id should be the unique exporter instance ID. It is used
// to set the "component.name" attribute.
//
// The endpoint is the HTTP endpoint the exporter is exporting to.
//
// If the experimental observability is disabled, nil is returned.
func NewInstrumentation(id int64, endpoint string) (*Instrumentation, error) {
if !x.Observability.Enabled() {
return nil, nil
}
attrs := BaseAttrs(id, endpoint)
i := &Instrumentation{
attrs: attrs,
addOpt: metric.WithAttributeSet(attribute.NewSet(attrs...)),
// Do not modify attrs (NewSet sorts in-place), make a new slice.
recOpt: metric.WithAttributeSet(attribute.NewSet(append(
// Default to OK status code (200).
[]attribute.KeyValue{semconv.HTTPResponseStatusCode(http.StatusOK)},
attrs...,
)...)),
}
mp := otel.GetMeterProvider()
m := mp.Meter(
ScopeName,
metric.WithInstrumentationVersion(Version),
metric.WithSchemaURL(SchemaURL),
)
var err error
inflightMetric, e := otelconv.NewSDKExporterMetricDataPointInflight(m)
if e != nil {
e = fmt.Errorf("failed to create inflight metric: %w", e)
err = errors.Join(err, e)
}
i.inflightMetric = inflightMetric.Inst()
exportedMetric, e := otelconv.NewSDKExporterMetricDataPointExported(m)
if e != nil {
e = fmt.Errorf("failed to create exported metric: %w", e)
err = errors.Join(err, e)
}
i.exportedMetric = exportedMetric.Inst()
opDuration, e := otelconv.NewSDKExporterOperationDuration(m)
if e != nil {
e = fmt.Errorf("failed to create operation duration metric: %w", e)
err = errors.Join(err, e)
}
i.opDuration = opDuration.Inst()
return i, err
}
// BaseAttrs returns the base attributes for the exporter with the provided ID
// and endpoint.
//
// The id should be the unique exporter instance ID. It is used
// to set the "component.name" attribute.
//
// The endpoint is the HTTP endpoint the exporter is exporting to. It should be
// in the format "host[:port]".
func BaseAttrs(id int64, endpoint string) []attribute.KeyValue {
host, port, err := parseEndpoint(endpoint)
if err != nil || (host == "" && port < 0) {
if err != nil {
global.Debug("failed to parse endpoint", "endpoint", endpoint, "error", err)
}
return []attribute.KeyValue{
semconv.OTelComponentName(ComponentName(id)),
semconv.OTelComponentTypeOtlpHTTPMetricExporter,
}
}
// Do not use append so the slice is exactly allocated.
if port < 0 {
return []attribute.KeyValue{
semconv.OTelComponentName(ComponentName(id)),
semconv.OTelComponentTypeOtlpHTTPMetricExporter,
semconv.ServerAddress(host),
}
}
if host == "" {
return []attribute.KeyValue{
semconv.OTelComponentName(ComponentName(id)),
semconv.OTelComponentTypeOtlpHTTPMetricExporter,
semconv.ServerPort(port),
}
}
return []attribute.KeyValue{
semconv.OTelComponentName(ComponentName(id)),
semconv.OTelComponentTypeOtlpHTTPMetricExporter,
semconv.ServerAddress(host),
semconv.ServerPort(port),
}
}
// parseEndpoint parses the host and port from endpoint that has the form
// "host[:port]", or it returns an error if the endpoint is not parsable.
//
// If no port is specified, -1 is returned.
//
// If no host is specified, an empty string is returned.
func parseEndpoint(endpoint string) (string, int, error) {
// First check if the endpoint is just an IP address.
if ip := parseIP(endpoint); ip != "" {
return ip, -1, nil
}
// If there's no colon, there is no port (IPv6 with no port checked above).
if !strings.Contains(endpoint, ":") {
return endpoint, -1, nil
}
// Otherwise, parse as host:port.
host, portStr, err := net.SplitHostPort(endpoint)
if err != nil {
return "", -1, fmt.Errorf("invalid host:port %q: %w", endpoint, err)
}
const base, bitSize = 10, 16
port16, err := strconv.ParseUint(portStr, base, bitSize)
if err != nil {
return "", -1, fmt.Errorf("invalid port %q: %w", portStr, err)
}
port := int(port16) // port is guaranteed to be in the range [0, 65535].
return host, port, nil
}
// parseIP attempts to parse the entire endpoint as an IP address.
// It returns the normalized string form of the IP if successful,
// or an empty string if parsing fails.
func parseIP(ip string) string {
// Strip leading and trailing brackets for IPv6 addresses.
if len(ip) >= 2 && ip[0] == '[' && ip[len(ip)-1] == ']' {
ip = ip[1 : len(ip)-1]
}
addr, err := netip.ParseAddr(ip)
if err != nil {
return ""
}
// Return the normalized string form of the IP.
return addr.String()
}
// ExportMetrics instruments the UploadMetrics method of the client. It returns an
// [ExportOp] that must have its [ExportOp.End] method called when the
// operation ends.
func (i *Instrumentation) ExportMetrics(ctx context.Context, rm *metricpb.ResourceMetrics) ExportOp {
start := time.Now()
nMetrics := countDataPoints(rm)
if i.inflightMetric.Enabled(ctx) {
addOpt := get[metric.AddOption](addOptPool)
defer put(addOptPool, addOpt)
*addOpt = append(*addOpt, i.addOpt)
i.inflightMetric.Add(ctx, nMetrics, *addOpt...)
}
return ExportOp{
ctx: ctx,
start: start,
nMetrics: nMetrics,
inst: i,
}
}
// ExportOp tracks the export operation being observed by
// [Instrumentation.ExportMetrics].
type ExportOp struct {
ctx context.Context
start time.Time
nMetrics int64
inst *Instrumentation
}
// End completes the observation of the operation being observed by a call to
// [Instrumentation.ExportMetrics].
//
// Any error that is encountered is provided as err.
// The HTTP status code from the response is provided as status.
//
// If err is not nil, all metrics will be recorded as failures unless error is of
// type [internal.PartialSuccess]. In the case of a PartialSuccess, the number
// of successfully exported metrics will be determined by inspecting the
// RejectedItems field of the PartialSuccess.
func (e ExportOp) End(err error, status int) {
addOpt := get[metric.AddOption](addOptPool)
defer put(addOptPool, addOpt)
*addOpt = append(*addOpt, e.inst.addOpt)
if e.inst.inflightMetric.Enabled(e.ctx) {
e.inst.inflightMetric.Add(e.ctx, -e.nMetrics, *addOpt...)
}
success := successful(e.nMetrics, err)
// Record successfully exported metrics, even if the value is 0 which are
// meaningful to distribution aggregations.
if e.inst.exportedMetric.Enabled(e.ctx) {
e.inst.exportedMetric.Add(e.ctx, success, *addOpt...)
}
if err != nil && e.inst.exportedMetric.Enabled(e.ctx) {
attrs := get[attribute.KeyValue](measureAttrsPool)
defer put(measureAttrsPool, attrs)
*attrs = append(*attrs, e.inst.attrs...)
*attrs = append(*attrs, semconv.ErrorType(err))
// Do not inefficiently make a copy of attrs by using
// WithAttributes instead of WithAttributeSet.
o := metric.WithAttributeSet(attribute.NewSet(*attrs...))
// Reset addOpt with new attribute set.
*addOpt = append((*addOpt)[:0], o)
e.inst.exportedMetric.Add(e.ctx, e.nMetrics-success, *addOpt...)
}
if e.inst.opDuration.Enabled(e.ctx) {
recOpt := get[metric.RecordOption](recordOptPool)
defer put(recordOptPool, recOpt)
*recOpt = append(*recOpt, e.inst.recordOption(err, status))
d := time.Since(e.start).Seconds()
e.inst.opDuration.Record(e.ctx, d, *recOpt...)
}
}
// recordOption returns a RecordOption with attributes representing the
// outcome of the operation being recorded.
//
// If err is nil and status is 200, the default recOpt of the
// Instrumentation is returned.
//
// Otherwise, a new RecordOption is returned with the base attributes of the
// Instrumentation plus the http.response.status_code attribute set to the
// provided status (if non-zero), and if err is not nil, the error.type attribute set
// to the type of the error.
func (i *Instrumentation) recordOption(err error, status int) metric.RecordOption {
if err == nil && status == http.StatusOK {
return i.recOpt
}
attrs := get[attribute.KeyValue](measureAttrsPool)
defer put(measureAttrsPool, attrs)
*attrs = append(*attrs, i.attrs...)
if status != 0 {
*attrs = append(*attrs, semconv.HTTPResponseStatusCode(status))
}
if err != nil {
*attrs = append(*attrs, semconv.ErrorType(err))
}
// Do not inefficiently make a copy of attrs by using WithAttributes
// instead of WithAttributeSet.
return metric.WithAttributeSet(attribute.NewSet(*attrs...))
}
// successful returns the number of successfully exported metrics out of the n
// that were exported based on the provided error.
//
// If err is nil, n is returned. All metrics were successfully exported.
//
// If err is not nil and not an [internal.PartialSuccess] error, 0 is returned.
// It is assumed all metrics failed to be exported.
//
// If err is an [internal.PartialSuccess] error, the number of successfully
// exported metrics is computed by subtracting the RejectedItems field from n. If
// RejectedItems is negative, n is returned. If RejectedItems is greater than
// n, 0 is returned.
func successful(n int64, err error) int64 {
if err == nil {
return n // All metrics successfully exported.
}
// Split rejection calculation so successful is inlinable.
return n - rejected(n, err)
}
var errPartialPool = &sync.Pool{
New: func() any { return new(internal.PartialSuccess) },
}
// rejected returns how many out of the n metrics were rejected based on the
// provided non-nil err.
func rejected(n int64, err error) int64 {
ps := errPartialPool.Get().(*internal.PartialSuccess)
defer errPartialPool.Put(ps)
// Check for partial success.
if errors.As(err, ps) {
// Bound RejectedItems to [0, n]. This should not be needed,
// but be defensive as this is from an external source.
return min(max(ps.RejectedItems, 0), n)
}
return n // All metrics rejected.
}
@@ -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,8 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package internal // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal"
// Version is the current release version of the OpenTelemetry OTLP HTTP metric
// exporter in use.
const Version = "1.44.0"
@@ -0,0 +1,36 @@
# Experimental Features
The `otlpmetrichttp` exporter contains features that have not yet stabilized in the OpenTelemetry specification.
These features are added to the `otlpmetrichttp` exporter prior to stabilization in the specification so that users can start experimenting with them and provide feedback.
These features may change in backwards incompatible ways as feedback is applied.
See the [Compatibility and Stability](#compatibility-and-stability) section for more information.
## Features
- [Observability](#observability)
### Observability
The `otlpmetrichttp` exporter can be configured to provide observability about itself using OpenTelemetry metrics.
To opt-in, set the environment variable `OTEL_GO_X_OBSERVABILITY` to `true`.
When enabled, the exporter will create the following metrics using the global `MeterProvider`:
- `otel.sdk.exporter.metric_data_point.inflight`
- `otel.sdk.exporter.metric_data_point.exported`
- `otel.sdk.exporter.operation.duration`
Please see the [Semantic conventions for OpenTelemetry SDK metrics] documentation for more details on these metrics.
[Semantic conventions for OpenTelemetry SDK metrics]: https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/otel/sdk-metrics.md
## Compatibility and Stability
Experimental features do not fall within the scope of the OpenTelemetry Go versioning and stability [policy](../../../../../../VERSIONING.md).
These features may be removed or modified in successive version releases, including patch versions.
When an experimental feature is promoted to a stable feature, a migration path will be included in the changelog entry of the release.
There is no guarantee that any environment variable feature flags that enabled the experimental feature will be supported by the stable version.
If they are supported, they may be accompanied with a deprecation notice stating a timeline for the removal of that support.
@@ -0,0 +1,22 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package x // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/x"
import "strings"
// Observability is an experimental feature flag that determines if exporter
// observability metrics are enabled.
//
// To enable this feature set the OTEL_GO_X_OBSERVABILITY environment variable
// to the case-insensitive string value of "true" (i.e. "True" and "TRUE"
// will also enable this).
var Observability = newFeature(
[]string{"OBSERVABILITY"},
func(v string) (string, bool) {
if strings.EqualFold(v, "true") {
return v, true
}
return "", false
},
)
@@ -0,0 +1,58 @@
// Code generated by gotmpl. DO NOT MODIFY.
// source: internal/shared/x/x.go.tmpl
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
// Package x documents experimental features for [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/x].
package x // import "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp/internal/x"
import (
"os"
)
// Feature is an experimental feature control flag. It provides a uniform way
// to interact with these feature flags and parse their values.
type Feature[T any] struct {
keys []string
parse func(v string) (T, bool)
}
func newFeature[T any](suffix []string, parse func(string) (T, bool)) Feature[T] {
const envKeyRoot = "OTEL_GO_X_"
keys := make([]string, 0, len(suffix))
for _, s := range suffix {
keys = append(keys, envKeyRoot+s)
}
return Feature[T]{
keys: keys,
parse: parse,
}
}
// Keys returns the environment variable keys that can be set to enable the
// feature.
func (f Feature[T]) Keys() []string { return f.keys }
// Lookup returns the user configured value for the feature and true if the
// user has enabled the feature. Otherwise, if the feature is not enabled, a
// zero-value and false are returned.
func (f Feature[T]) Lookup() (v T, ok bool) {
// https://github.com/open-telemetry/opentelemetry-specification/blob/62effed618589a0bec416a87e559c0a9d96289bb/specification/configuration/sdk-environment-variables.md#parsing-empty-value
//
// > The SDK MUST interpret an empty value of an environment variable the
// > same way as when the variable is unset.
for _, key := range f.keys {
vRaw := os.Getenv(key)
if vRaw != "" {
return f.parse(vRaw)
}
}
return v, ok
}
// Enabled reports whether the feature is enabled.
func (f Feature[T]) Enabled() bool {
_, ok := f.Lookup()
return ok
}
@@ -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.
//
@@ -143,9 +143,9 @@ func (c *client) Start(ctx context.Context) error {
}
// Stop shuts down the client and interrupt any in-flight request.
func (d *client) Stop(ctx context.Context) error {
d.stopOnce.Do(func() {
close(d.stopCh)
func (c *client) Stop(ctx context.Context) error {
c.stopOnce.Do(func() {
close(c.stopCh)
})
select {
case <-ctx.Done():
@@ -156,7 +156,7 @@ func (d *client) Stop(ctx context.Context) error {
}
// UploadTraces sends a batch of spans to the collector.
func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.ResourceSpans) (uploadErr error) {
func (c *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.ResourceSpans) (uploadErr error) {
pbRequest := &coltracepb.ExportTraceServiceRequest{
ResourceSpans: protoSpans,
}
@@ -165,30 +165,41 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
return err
}
ctx, cancel := d.contextWithStop(ctx)
ctx, cancel := c.contextWithStop(ctx)
defer cancel()
request, err := d.newRequest(rawRequest)
if maxSize := c.cfg.MaxRequestSize; maxSize > 0 && len(rawRequest) > maxSize {
return fmt.Errorf("request body too large: exceeded %d bytes", maxSize)
}
request, err := c.newRequest(rawRequest)
if err != nil {
return err
}
var statusCode int
if d.inst != nil {
op := d.inst.ExportSpans(ctx, len(protoSpans))
if c.inst != nil {
var spanCount int
for _, rs := range protoSpans {
for _, ss := range rs.ScopeSpans {
spanCount += len(ss.Spans)
}
}
op := c.inst.ExportSpans(ctx, spanCount)
defer func() { op.End(uploadErr, statusCode) }()
}
return errors.Join(uploadErr, d.requestFunc(ctx, func(ctx context.Context) error {
return errors.Join(uploadErr, c.requestFunc(ctx, func(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
statusCode = 0
request.reset(ctx)
// nolint:gosec // URL is constructed from validated OTLP endpoint configuration
resp, err := d.client.Do(request.Request)
resp, err := c.client.Do(request.Request)
var urlErr *url.Error
if errors.As(err, &urlErr) && urlErr.Temporary() {
return newResponseError(http.Header{}, err)
@@ -272,8 +283,8 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
}))
}
func (d *client) newRequest(body []byte) (request, error) {
u := url.URL{Scheme: d.getScheme(), Host: d.cfg.Endpoint, Path: d.cfg.URLPath}
func (c *client) newRequest(body []byte) (request, error) {
u := url.URL{Scheme: c.getScheme(), Host: c.cfg.Endpoint, Path: c.cfg.URLPath}
r, err := http.NewRequestWithContext(context.Background(), http.MethodPost, u.String(), http.NoBody)
if err != nil {
return request{Request: r}, err
@@ -282,13 +293,13 @@ func (d *client) newRequest(body []byte) (request, error) {
userAgent := "OTel OTLP Exporter Go/" + otlptrace.Version()
r.Header.Set("User-Agent", userAgent)
for k, v := range d.cfg.Headers {
for k, v := range c.cfg.Headers {
r.Header.Set(k, v)
}
r.Header.Set("Content-Type", contentTypeProto)
req := request{Request: r}
switch Compression(d.cfg.Compression) {
switch Compression(c.cfg.Compression) {
case NoCompression:
r.ContentLength = int64(len(body))
req.bodyReader = bodyReader(body)
@@ -299,7 +310,10 @@ func (d *client) newRequest(body []byte) (request, error) {
r.Header.Set("Content-Encoding", "gzip")
gz := gzPool.Get().(*gzip.Writer)
defer gzPool.Put(gz)
defer func() {
gz.Reset(io.Discard)
gzPool.Put(gz)
}()
var b bytes.Buffer
gz.Reset(&b)
@@ -320,15 +334,15 @@ func (d *client) newRequest(body []byte) (request, error) {
}
// MarshalLog is the marshaling function used by the logging system to represent this Client.
func (d *client) MarshalLog() any {
func (c *client) MarshalLog() any {
return struct {
Type string
Endpoint string
Insecure bool
}{
Type: "otlptracehttp",
Endpoint: d.cfg.Endpoint,
Insecure: d.cfg.Insecure,
Endpoint: c.cfg.Endpoint,
Insecure: c.cfg.Insecure,
}
}
@@ -425,14 +439,14 @@ func evaluate(err error) (bool, time.Duration) {
return true, time.Duration(rErr.throttle)
}
func (d *client) getScheme() string {
if d.cfg.Insecure {
func (c *client) getScheme() string {
if c.cfg.Insecure {
return "http"
}
return "https"
}
func (d *client) contextWithStop(ctx context.Context) (context.Context, context.CancelFunc) {
func (c *client) contextWithStop(ctx context.Context) (context.Context, context.CancelFunc) {
// Unify the parent context Done signal with the client's stop
// channel.
ctx, cancel := context.WithCancel(ctx)
@@ -441,7 +455,7 @@ func (d *client) contextWithStop(ctx context.Context) (context.Context, context.
case <-ctx.Done():
// Nothing to do, either cancelled or deadline
// happened.
case <-d.stopCh:
case <-c.stopCh:
cancel()
}
}(ctx, cancel)
@@ -23,8 +23,8 @@ import (
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp/internal/x"
"go.opentelemetry.io/otel/internal/global"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"go.opentelemetry.io/otel/semconv/v1.40.0/otelconv"
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
"go.opentelemetry.io/otel/semconv/v1.41.0/otelconv"
)
const (
@@ -77,6 +77,7 @@ var (
func get[T any](p *sync.Pool) *[]T { return p.Get().(*[]T) }
func put[T any](p *sync.Pool, s *[]T) {
clear(*s) // erase elements to allow GC to collect what they refer to.
*s = (*s)[:0] // Reset.
p.Put(s)
}
@@ -167,7 +168,7 @@ func NewInstrumentation(id int64, endpoint string) (*Instrumentation, error) {
// to set the "component.name" attribute.
//
// The endpoint is the HTTP endpoint the exporter is exporting to. It should be
// in the format "host:port" or a full URL.
// in the format "host[:port]".
func BaseAttrs(id int64, endpoint string) []attribute.KeyValue {
host, port, err := parseEndpoint(endpoint)
if err != nil || (host == "" && port < 0) {
@@ -345,7 +346,7 @@ func (e ExportOp) End(err error, status int) {
//
// Otherwise, a new RecordOption is returned with the base attributes of the
// Instrumentation plus the http.response.status_code attribute set to the
// provided status, and if err is not nil, the error.type attribute set
// provided status (if non-zero), and if err is not nil, the error.type attribute set
// to the type of the error.
func (i *Instrumentation) recordOption(err error, status int) metric.RecordOption {
if err == nil && status == http.StatusOK {
@@ -356,7 +357,9 @@ func (i *Instrumentation) recordOption(err error, status int) metric.RecordOptio
defer put(measureAttrsPool, attrs)
*attrs = append(*attrs, i.attrs...)
*attrs = append(*attrs, semconv.HTTPResponseStatusCode(status))
if status != 0 {
*attrs = append(*attrs, semconv.HTTPResponseStatusCode(status))
}
if err != nil {
*attrs = append(*attrs, semconv.ErrorType(err))
}
@@ -394,7 +397,10 @@ var errPartialPool = &sync.Pool{
// the provided non-nil err.
func rejected(n int64, err error) int64 {
ps := errPartialPool.Get().(*internal.PartialSuccess)
defer errPartialPool.Put(ps)
defer func() {
*ps = internal.PartialSuccess{} // erase fields to allow GC to collect them.
errPartialPool.Put(ps)
}()
// Check for partial success.
if errors.As(err, ps) {
// Bound RejectedItems to [0, n]. This should not be needed,
@@ -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 HTTP trace
// exporter in use.
const Version = "1.43.0"
const Version = "1.44.0"
@@ -138,6 +138,16 @@ func WithTimeout(duration time.Duration) Option {
return wrappedOption{otlpconfig.WithTimeout(duration)}
}
// WithMaxRequestSize sets the maximum size, in bytes, of a serialized export
// request, before compression, that the exporter will send.
//
// If size is less than or equal to zero, no request-size limit is applied.
// Disabling the limit is not recommended because it can lead to excessive
// resource consumption or abuse.
func WithMaxRequestSize(size int) Option {
return wrappedOption{otlpconfig.WithMaxRequestSize(size)}
}
// WithRetry configures the retry policy for transient errors that may occurs
// when exporting traces. An exponential back-off algorithm is used to ensure
// endpoints are not overwhelmed with retries. If unset, the default retry
+1 -1
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

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