mirror of
https://github.com/docker/cli.git
synced 2026-11-06 13:10:41 -04:00
Merge pull request #7468 from thaJeztah/bump_otels
vendor: go.opentelemetry.io/otel v1.47.0, contrib v0.72.0
This commit is contained in:
+12
-11
@@ -52,14 +52,14 @@ require (
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0
|
||||
go.opentelemetry.io/otel v1.46.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0
|
||||
go.opentelemetry.io/otel/metric v1.46.0
|
||||
go.opentelemetry.io/otel/sdk v1.46.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0
|
||||
go.opentelemetry.io/otel/trace v1.46.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0
|
||||
go.opentelemetry.io/otel v1.47.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0
|
||||
go.opentelemetry.io/otel/metric v1.47.0
|
||||
go.opentelemetry.io/otel/sdk v1.47.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.47.0
|
||||
go.opentelemetry.io/otel/trace v1.47.0
|
||||
go.yaml.in/yaml/v3 v3.0.6
|
||||
golang.org/x/sync v0.24.0
|
||||
golang.org/x/sys v0.49.0
|
||||
@@ -99,13 +99,14 @@ require (
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
go.etcd.io/raft/v3 v3.6.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect
|
||||
go.opentelemetry.io/otel/log v1.47.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||
golang.org/x/mod v0.42.0 // indirect
|
||||
golang.org/x/net v0.62.0 // indirect
|
||||
golang.org/x/time v0.16.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect
|
||||
google.golang.org/grpc v1.83.2 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
)
|
||||
|
||||
+26
-24
@@ -155,26 +155,28 @@ go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
|
||||
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU=
|
||||
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
|
||||
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 h1:qkDYCAFiZXLcs1L4aY+tP2wguQ4kURANqHOQMA2et2s=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0/go.mod h1:tkipS4DRzmpAmvg+Gw4++O1IdDq6TVDnvnYU6cmbQVs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 h1:w53CDeOA/Kurp7yRsegSr6pbbr759dOvJ+yNmWM6Hxs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0/go.mod h1:BOmGMCbAtvcJiSJ+hLuhgPLdDbimnraSl8irz3iY8sY=
|
||||
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
|
||||
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
|
||||
go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU=
|
||||
go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4=
|
||||
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
|
||||
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
|
||||
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
|
||||
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 h1:LxwW/9ctSCv+QkE/cLR7M91ZIkXNMqJtEMi1vCw9U8s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0/go.mod h1:tOsftB4SslBwwErVEPaenU2RpThXWPIU8DoJHEC4dyw=
|
||||
go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
|
||||
go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0 h1:LaLVGJtIzHcvHjVs2OftmKcc+M9mdOqS8vZ1fBD1Yes=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0/go.mod h1:vF6H5sNDuQBJWgSX00iZgbIqk2MZZoVMH0ar5t5bOpw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 h1:UFpxOpYPmMNUtOWhdb+nC1WELIgVf8z9higURrbzYU4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0/go.mod h1:YBGjxe3lt0jtXoEXxGrGhv/jM7oUBUDVK7zhAvNyEjc=
|
||||
go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI=
|
||||
go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw=
|
||||
go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY=
|
||||
go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU=
|
||||
go.opentelemetry.io/otel/metric/x v0.69.0 h1:DjRLr15H83v+hCW7JA9NoJvOkYTtmq5YoDRbe9deYpM=
|
||||
go.opentelemetry.io/otel/metric/x v0.69.0/go.mod h1:uVvsMPMFFyj/HUQfrUnH3JjnOQ1dwFDorgFLRBasM0k=
|
||||
go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I=
|
||||
go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw=
|
||||
go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM=
|
||||
go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -224,10 +226,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 h1:izFU9hz7aeLI/Mi1J0991ae+xcwRLr7hTqWnB/9aIIU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5/go.mod h1:3LhxRw4YYkf+ylAfgaY9JlVLFKhokkCV8duhLLe7+t0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc=
|
||||
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
|
||||
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
|
||||
+24
-5
@@ -10,12 +10,31 @@ import (
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Attribute keys that can be added to a span.
|
||||
// Legacy HTTP attribute keys retained for compatibility.
|
||||
const (
|
||||
ReadBytesKey = attribute.Key("http.read_bytes") // if anything was read from the request body, the total number of bytes read
|
||||
ReadErrorKey = attribute.Key("http.read_error") // If an error occurred while reading a request, the string of the error (io.EOF is not recorded)
|
||||
WroteBytesKey = attribute.Key("http.wrote_bytes") // if anything was written to the response writer, the total number of bytes written
|
||||
WriteErrorKey = attribute.Key("http.write_error") // if an error occurred while writing a reply, the string of the error (io.EOF is not recorded)
|
||||
// ReadBytesKey is the attribute key for the number of bytes returned by an
|
||||
// individual read from an HTTP request body.
|
||||
//
|
||||
// Deprecated: there is no direct semantic-convention replacement.
|
||||
ReadBytesKey = attribute.Key("http.read_bytes")
|
||||
|
||||
// ReadErrorKey is the attribute key for the string form of a non-EOF error
|
||||
// returned while reading an HTTP request body.
|
||||
//
|
||||
// Deprecated: there is no direct semantic-convention replacement.
|
||||
ReadErrorKey = attribute.Key("http.read_error")
|
||||
|
||||
// WroteBytesKey is the attribute key for the number of bytes returned by an
|
||||
// individual write to an HTTP response body.
|
||||
//
|
||||
// Deprecated: there is no direct semantic-convention replacement.
|
||||
WroteBytesKey = attribute.Key("http.wrote_bytes")
|
||||
|
||||
// WriteErrorKey is the attribute key for the string form of a non-EOF error
|
||||
// returned while writing an HTTP response body.
|
||||
//
|
||||
// Deprecated: there is no direct semantic-convention replacement.
|
||||
WriteErrorKey = attribute.Key("http.write_error")
|
||||
)
|
||||
|
||||
// Filter is a predicate used to determine whether a given http.request should
|
||||
|
||||
+7
-6
@@ -143,14 +143,15 @@ const (
|
||||
)
|
||||
|
||||
// WithMessageEvents configures the Handler to record the specified events
|
||||
// (span.AddEvent) on spans. By default only summary attributes are added at the
|
||||
// end of the request.
|
||||
// (span.AddEvent) on spans. By default, no read or write events are recorded.
|
||||
//
|
||||
// Valid events are:
|
||||
// - ReadEvents: Record the number of bytes read after every http.Request.Body.Read
|
||||
// using the ReadBytesKey
|
||||
// - WriteEvents: Record the number of bytes written after every http.ResponeWriter.Write
|
||||
// using the WriteBytesKey
|
||||
// - ReadEvents: Record a "read" span event after every http.Request.Body.Read.
|
||||
// - WriteEvents: Record a "write" span event after every http.ResponseWriter.Write.
|
||||
//
|
||||
// The events do not include per-call byte-count attributes. Total request and
|
||||
// response body sizes are recorded on the span according to HTTP semantic
|
||||
// conventions.
|
||||
func WithMessageEvents(events ...Event) Option {
|
||||
return optionFunc(func(c *config) {
|
||||
for _, e := range events {
|
||||
|
||||
+20
-11
@@ -129,8 +129,8 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http
|
||||
|
||||
readRecordFunc := func(int64) {}
|
||||
if h.readEvent {
|
||||
readRecordFunc = func(n int64) {
|
||||
span.AddEvent("read", trace.WithAttributes(ReadBytesKey.Int64(n)))
|
||||
readRecordFunc = func(int64) {
|
||||
span.AddEvent("read")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,8 +150,8 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http
|
||||
|
||||
writeRecordFunc := func(int64) {}
|
||||
if h.writeEvent {
|
||||
writeRecordFunc = func(n int64) {
|
||||
span.AddEvent("write", trace.WithAttributes(WroteBytesKey.Int64(n)))
|
||||
writeRecordFunc = func(int64) {
|
||||
span.AddEvent("write")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,12 +180,21 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http
|
||||
if !found {
|
||||
ctx = ContextWithLabeler(ctx, labeler)
|
||||
}
|
||||
rCtx := r.WithContext(ctx)
|
||||
defer func() {
|
||||
// Copy MultipartForm back to the original request so net/http can
|
||||
// find and remove any temp files ParseMultipartForm created on the
|
||||
// copy. Deferred so the copy-back also runs during panic unwinding,
|
||||
// letting net/http cleanup paths that still run (HTTP/2 handler
|
||||
// recovery, outer recovery middleware) find the form.
|
||||
if rCtx.MultipartForm != nil {
|
||||
r.MultipartForm = rCtx.MultipartForm
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, rCtx)
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
if r.Pattern != "" {
|
||||
span.SetName(h.spanNameFormatter(h.operation, r))
|
||||
if rCtx.Pattern != "" {
|
||||
span.SetName(h.spanNameFormatter(h.operation, rCtx))
|
||||
}
|
||||
|
||||
statusCode := rww.StatusCode()
|
||||
@@ -204,9 +213,9 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http
|
||||
ServerName: h.server,
|
||||
ResponseSize: bytesWritten,
|
||||
MetricAttributes: semconv.MetricAttributes{
|
||||
Req: r,
|
||||
Req: rCtx,
|
||||
StatusCode: statusCode,
|
||||
AdditionalAttributes: append(labeler.Get(), h.metricAttributesFromRequest(r)...),
|
||||
AdditionalAttributes: append(labeler.Get(), h.metricAttributesFromRequest(rCtx)...),
|
||||
},
|
||||
MetricData: semconv.MetricData{
|
||||
RequestSize: bytesRead,
|
||||
|
||||
Generated
Vendored
+3
-5
@@ -38,11 +38,9 @@ func SplitHostPort(hostport string) (host string, port int) {
|
||||
host = hostport[1:addrEnd]
|
||||
return host, port
|
||||
}
|
||||
} else {
|
||||
if i := strings.LastIndexByte(hostport, ':'); i < 0 {
|
||||
host = hostport
|
||||
return host, port
|
||||
}
|
||||
} else if !strings.Contains(hostport, ":") {
|
||||
host = hostport
|
||||
return host, port
|
||||
}
|
||||
|
||||
host, pStr, err := net.SplitHostPort(hostport)
|
||||
|
||||
+24
@@ -56,3 +56,27 @@ func LabelerFromContext(ctx context.Context) (*Labeler, bool) {
|
||||
}
|
||||
return l, ok
|
||||
}
|
||||
|
||||
// clientLabelerContextKey is a separate context key for the client-side Labeler.
|
||||
// This prevents server-side Labeler attributes (e.g., http.route) from leaking
|
||||
// into client-side metrics when a server handler propagates its request context
|
||||
// into an outbound HTTP client request.
|
||||
const clientLabelerContextKey labelerContextKeyType = 1
|
||||
|
||||
// ContextWithClientLabeler returns a new context with the provided Labeler instance
|
||||
// for use with client-side HTTP instrumentation (otelhttp.Transport).
|
||||
// Attributes added to this labeler will be attached to client-side metrics.
|
||||
func ContextWithClientLabeler(parent context.Context, l *Labeler) context.Context {
|
||||
return context.WithValue(parent, clientLabelerContextKey, l)
|
||||
}
|
||||
|
||||
// ClientLabelerFromContext retrieves a Labeler instance from the provided context
|
||||
// for use with client-side HTTP instrumentation. If no Labeler was found, a new,
|
||||
// empty Labeler is returned and the second return value is false.
|
||||
func ClientLabelerFromContext(ctx context.Context) (*Labeler, bool) {
|
||||
l, ok := ctx.Value(clientLabelerContextKey).(*Labeler)
|
||||
if !ok {
|
||||
l = &Labeler{}
|
||||
}
|
||||
return l, ok
|
||||
}
|
||||
|
||||
Generated
Vendored
+180
-52
@@ -9,6 +9,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -19,7 +20,6 @@ import (
|
||||
otelsemconv "go.opentelemetry.io/otel/semconv/v1.43.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv"
|
||||
)
|
||||
|
||||
@@ -82,6 +82,142 @@ func defaultTransportFormatter(_ string, r *http.Request) string {
|
||||
return "HTTP " + r.Method
|
||||
}
|
||||
|
||||
// requestTracker tracks the request body lifecycle of a single RoundTrip and
|
||||
// records the client metrics exactly once, after the request body reaches a
|
||||
// final state.
|
||||
//
|
||||
// A single tracker is allocated per request with a body, replacing the
|
||||
// body-wrapper allocation of the untracked implementation: the first request
|
||||
// body is wrapped by the embedded first field, and the lifecycle state is
|
||||
// held in explicit fields with methods instead of escaping closures. Only
|
||||
// GetBody retries allocate an additional trackedRequestBody.
|
||||
type requestTracker struct {
|
||||
transport *Transport
|
||||
first trackedRequestBody
|
||||
|
||||
originalGetBody func() (io.ReadCloser, error)
|
||||
|
||||
// mu protects body, published, and recorded. Body close callbacks may
|
||||
// fire on a different goroutine (e.g. during transport retries or
|
||||
// asynchronous closes) while the main goroutine publishes the metric
|
||||
// data after RoundTrip returns.
|
||||
mu sync.Mutex
|
||||
body *trackedRequestBody // Last wrapped request body; nil records no data.
|
||||
published bool
|
||||
recorded bool
|
||||
|
||||
// Metric data published when RoundTrip returns; immutable afterwards.
|
||||
ctx context.Context
|
||||
duration time.Duration
|
||||
opts semconv.MetricOpts
|
||||
}
|
||||
|
||||
func newRequestTracker(t *Transport, r *http.Request) *requestTracker {
|
||||
rt := &requestTracker{transport: t}
|
||||
rt.first = trackedRequestBody{body: r.Body, tracker: rt}
|
||||
rt.body = &rt.first
|
||||
r.Body = &rt.first
|
||||
if r.GetBody != nil {
|
||||
rt.originalGetBody = r.GetBody
|
||||
r.GetBody = rt.getBody
|
||||
}
|
||||
return rt
|
||||
}
|
||||
|
||||
func (rt *requestTracker) getBody() (io.ReadCloser, error) {
|
||||
b, err := rt.originalGetBody()
|
||||
if err != nil {
|
||||
// The underlying transport will fail to make a retry request,
|
||||
// hence, record no data.
|
||||
rt.setBody(nil)
|
||||
return nil, err
|
||||
}
|
||||
if b == nil || b == http.NoBody {
|
||||
return b, nil
|
||||
}
|
||||
tb := &trackedRequestBody{body: b, tracker: rt}
|
||||
rt.setBody(tb)
|
||||
return tb, nil
|
||||
}
|
||||
|
||||
func (rt *requestTracker) setBody(b *trackedRequestBody) {
|
||||
rt.mu.Lock()
|
||||
rt.body = b
|
||||
rt.mu.Unlock()
|
||||
}
|
||||
|
||||
// publish stores the metric data gathered when RoundTrip returns and records
|
||||
// the metrics if the request body already reached a final state.
|
||||
func (rt *requestTracker) publish(ctx context.Context, d time.Duration, opts semconv.MetricOpts) {
|
||||
rt.mu.Lock()
|
||||
rt.ctx = ctx
|
||||
rt.duration = d
|
||||
rt.opts = opts
|
||||
rt.published = true
|
||||
rt.mu.Unlock()
|
||||
rt.maybeRecord()
|
||||
}
|
||||
|
||||
// maybeRecord records the client metrics once the metric data has been
|
||||
// published and the request body has reached a final state. Close is the
|
||||
// only authoritative completion signal for a non-nil body: a body may yield
|
||||
// more bytes than its declared ContentLength, so a read count is never proof
|
||||
// of completion.
|
||||
func (rt *requestTracker) maybeRecord() {
|
||||
rt.mu.Lock()
|
||||
body := rt.body
|
||||
record := rt.published && !rt.recorded && (body == nil || body.closed.Load())
|
||||
if record {
|
||||
rt.recorded = true
|
||||
}
|
||||
rt.mu.Unlock()
|
||||
if !record {
|
||||
return
|
||||
}
|
||||
|
||||
// The winning caller evaluates the request size here so the recorded
|
||||
// value reflects the latest byte count.
|
||||
var requestSize int64
|
||||
if body != nil {
|
||||
requestSize = body.read.Load()
|
||||
}
|
||||
rt.transport.semconv.RecordMetrics(
|
||||
rt.ctx,
|
||||
semconv.MetricData{
|
||||
RequestSize: requestSize,
|
||||
RequestDuration: rt.duration,
|
||||
},
|
||||
rt.opts,
|
||||
)
|
||||
}
|
||||
|
||||
// trackedRequestBody wraps a request body to count read bytes and signal the
|
||||
// tracker when the body is closed.
|
||||
type trackedRequestBody struct {
|
||||
body io.ReadCloser
|
||||
tracker *requestTracker
|
||||
|
||||
read atomic.Int64
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
var _ io.ReadCloser = (*trackedRequestBody)(nil)
|
||||
|
||||
func (b *trackedRequestBody) Read(p []byte) (int, error) {
|
||||
n, err := b.body.Read(p)
|
||||
b.read.Add(int64(n))
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (b *trackedRequestBody) Close() error {
|
||||
// Finalize telemetry before delegating to the underlying Close, which
|
||||
// may block (mirroring wrappedBody.Close). Otherwise a blocking close
|
||||
// could suppress client metrics indefinitely.
|
||||
b.closed.Store(true)
|
||||
b.tracker.maybeRecord()
|
||||
return b.body.Close()
|
||||
}
|
||||
|
||||
// RoundTrip creates a Span and propagates its context via the provided request's headers
|
||||
// before handing the request to the configured base RoundTripper. The created span will
|
||||
// end when the response body is closed or when a read from the body returns io.EOF.
|
||||
@@ -110,33 +246,20 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
ctx = httptrace.WithClientTrace(ctx, t.clientTrace(ctx))
|
||||
}
|
||||
|
||||
labeler, found := LabelerFromContext(ctx)
|
||||
labeler, found := ClientLabelerFromContext(ctx)
|
||||
if !found {
|
||||
ctx = ContextWithLabeler(ctx, labeler)
|
||||
ctx = ContextWithClientLabeler(ctx, labeler)
|
||||
}
|
||||
|
||||
r = r.Clone(ctx) // According to RoundTripper spec, we shouldn't modify the origin request.
|
||||
|
||||
var lastBW *request.BodyWrapper // Records the last body wrapper. Can be nil.
|
||||
maybeWrapBody := func(body io.ReadCloser) io.ReadCloser {
|
||||
if body == nil || body == http.NoBody {
|
||||
return body
|
||||
}
|
||||
bw := request.NewBodyWrapper(body, func(int64) {})
|
||||
lastBW = bw
|
||||
return bw
|
||||
}
|
||||
r.Body = maybeWrapBody(r.Body)
|
||||
if r.GetBody != nil {
|
||||
originalGetBody := r.GetBody
|
||||
r.GetBody = func() (io.ReadCloser, error) {
|
||||
b, err := originalGetBody()
|
||||
if err != nil {
|
||||
lastBW = nil // The underlying transport will fail to make a retry request, hence, record no data.
|
||||
return nil, err
|
||||
}
|
||||
return maybeWrapBody(b), nil
|
||||
}
|
||||
// Track the request body lifecycle so metrics report the final read
|
||||
// size even when the transport keeps reading the body after RoundTrip
|
||||
// returns. Bodyless requests allocate no tracker and record
|
||||
// synchronously below.
|
||||
var tracker *requestTracker
|
||||
if r.Body != nil && r.Body != http.NoBody {
|
||||
tracker = newRequestTracker(t, r)
|
||||
}
|
||||
|
||||
span.SetAttributes(t.semconv.RequestTraceAttrs(r)...)
|
||||
@@ -147,29 +270,32 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
res, err = ensureResponseBody(t.rt, r, res)
|
||||
}
|
||||
|
||||
// Record the metrics on error or no error.
|
||||
requestDuration := time.Since(requestStartTime)
|
||||
statusCode := 0
|
||||
if err == nil {
|
||||
statusCode = res.StatusCode
|
||||
}
|
||||
var requestSize int64
|
||||
if lastBW != nil {
|
||||
requestSize = lastBW.BytesRead()
|
||||
metricOpts := t.semconv.MetricOptions(semconv.MetricAttributes{
|
||||
Req: r,
|
||||
Resp: res,
|
||||
StatusCode: statusCode,
|
||||
Err: err,
|
||||
AdditionalAttributes: append(labeler.Get(), t.metricAttributesFromRequest(r)...),
|
||||
})
|
||||
|
||||
if tracker != nil {
|
||||
// Delay metric recording until the request body reaches a final
|
||||
// state. The transport can continue reading the body after
|
||||
// RoundTrip returns, including on errors, since RoundTrippers may
|
||||
// close the request body asynchronously.
|
||||
tracker.publish(ctx, requestDuration, metricOpts)
|
||||
} else {
|
||||
t.semconv.RecordMetrics(
|
||||
ctx,
|
||||
semconv.MetricData{RequestDuration: requestDuration},
|
||||
metricOpts,
|
||||
)
|
||||
}
|
||||
t.semconv.RecordMetrics(
|
||||
ctx,
|
||||
semconv.MetricData{
|
||||
RequestSize: requestSize,
|
||||
RequestDuration: time.Since(requestStartTime),
|
||||
},
|
||||
t.semconv.MetricOptions(semconv.MetricAttributes{
|
||||
Req: r,
|
||||
Resp: res,
|
||||
StatusCode: statusCode,
|
||||
Err: err,
|
||||
AdditionalAttributes: append(labeler.Get(), t.metricAttributesFromRequest(r)...),
|
||||
}),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
span.SetAttributes(otelsemconv.ErrorType(err))
|
||||
@@ -263,8 +389,7 @@ func (wb *wrappedBody) Read(b []byte) (int, error) {
|
||||
case nil:
|
||||
// nothing to do here but fall through to the return
|
||||
case io.EOF:
|
||||
wb.recordBytesRead()
|
||||
wb.span.End()
|
||||
wb.recordMetricsOnce()
|
||||
default:
|
||||
wb.span.SetAttributes(otelsemconv.ErrorType(err))
|
||||
wb.span.SetStatus(codes.Error, err.Error())
|
||||
@@ -272,21 +397,24 @@ func (wb *wrappedBody) Read(b []byte) (int, error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// recordBytesRead is a function that ensures the number of bytes read is recorded once and only once.
|
||||
func (wb *wrappedBody) recordBytesRead() {
|
||||
// note: it is more performant (and equally correct) to use atomic.Bool over sync.Once here. In the event that
|
||||
// two goroutines are racing to call this method, the number of bytes read will no longer increase. Using
|
||||
// CompareAndSwap allows later goroutines to return quickly and not block waiting for the race winner to finish
|
||||
// calling wb.record(wb.read.Load()).
|
||||
// recordMetricsOnce ensures the final number of bytes read is recorded once.
|
||||
func (wb *wrappedBody) recordMetricsOnce() {
|
||||
// note: it is more performant (and equally correct) to use atomic.Bool
|
||||
// over sync.Once here. In the event that two goroutines are racing to
|
||||
// call this method, the number of bytes read will no longer increase.
|
||||
// Using CompareAndSwap allows later goroutines to return quickly and not
|
||||
// block waiting for the race winner to finish calling
|
||||
// wb.record(wb.read.Load()).
|
||||
if wb.recorded.CompareAndSwap(false, true) {
|
||||
// Record the total number of bytes read
|
||||
wb.record(wb.read.Load())
|
||||
wb.span.End()
|
||||
}
|
||||
}
|
||||
|
||||
func (wb *wrappedBody) Close() error {
|
||||
wb.recordBytesRead()
|
||||
wb.span.End()
|
||||
// Finalize telemetry before delegating to the underlying Close, which
|
||||
// may block (e.g. upgraded streams or custom RoundTripper bodies).
|
||||
wb.recordMetricsOnce()
|
||||
if wb.body != nil {
|
||||
return wb.body.Close()
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
package otelhttp
|
||||
|
||||
// Version is the current release version of the otelhttp instrumentation.
|
||||
const Version = "0.71.0"
|
||||
const Version = "0.72.0"
|
||||
|
||||
+4
-8
@@ -230,13 +230,6 @@ linters:
|
||||
- revive
|
||||
path: schema/v.*/types/.*
|
||||
text: avoid meaningless package names
|
||||
# TODO: Having appropriate comments for exported objects helps development,
|
||||
# even for objects in internal packages. Appropriate comments for all
|
||||
# exported objects should be added and this exclusion removed.
|
||||
- linters:
|
||||
- revive
|
||||
path: .*internal/.*
|
||||
text: exported (method|function|type|const) (.+) should have comment or be unexported
|
||||
# Yes, they are, but it's okay in a test.
|
||||
- linters:
|
||||
- revive
|
||||
@@ -272,7 +265,10 @@ formatters:
|
||||
- golines
|
||||
settings:
|
||||
gofumpt:
|
||||
extra-rules: true
|
||||
extra:
|
||||
group-params: true
|
||||
clothe-returns: true
|
||||
balance-calls: true
|
||||
goimports:
|
||||
local-prefixes:
|
||||
- go.opentelemetry.io/otel
|
||||
|
||||
+90
-1
@@ -11,6 +11,93 @@ 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.47.0/0.69.0/0.23.0/0.1.0] - 2026-10-02
|
||||
|
||||
This release contains the first stable release of the OpenTelemetry Go Logs API and SDK.
|
||||
Our project stability guarantees now apply to the following modules:
|
||||
|
||||
- `go.opentelemetry.io/otel/log`
|
||||
- `go.opentelemetry.io/otel/sdk/log`
|
||||
|
||||
See our [versioning policy](VERSIONING.md) for more information about these stability guarantees.
|
||||
|
||||
### Added
|
||||
|
||||
- Add `String` method for `KeyValue` type in `go.opentelemetry.io/otel/attribute`. (#8205)
|
||||
- Add `String` method for `Set` type in `go.opentelemetry.io/otel/attribute`. (#8347)
|
||||
- Add `WithMaxExportBatchSize` to `go.opentelemetry.io/otel/sdk/metric` to configure the maximum export batch size for `PeriodicReader`. (#8960)
|
||||
- Add `WithAttributeValueDepthLimit`, `DefaultAttributeValueDepthLimit`, and `SpanLimits.AttributeValueDepthLimit` in `go.opentelemetry.io/otel/sdk/trace` to configure the maximum depth of span, event, link, and instrumentation scope attribute values. (#8938)
|
||||
- Add `WithAttributeValueDepthLimit` in `go.opentelemetry.io/otel/sdk/log` to configure the maximum depth of log record and instrumentation scope attribute values. (#8938)
|
||||
- Add `WithMaxResponseSize` to `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`,
|
||||
`go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`, and
|
||||
`go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8941)
|
||||
- Add experimental `ProbabilitySampler` in `go.opentelemetry.io/otel/sdk/trace/x` that conforms to the [OpenTelemetry specification's threshold-based sampling algorithm](https://opentelemetry.io/docs/specs/otel/trace/sdk/#probabilitysampler). (#8123)
|
||||
- Add experimental `*Binder` extension interfaces in `go.opentelemetry.io/otel/metric/x` for instruments that support binding attributes ahead of time. (#8760)
|
||||
- Add the experimental `Finisher` synchronous metric instrument extension interface to `go.opentelemetry.io/otel/metric/x`. (#8906)
|
||||
|
||||
### Changed
|
||||
|
||||
- Use the [OpenTelemetry Attribute Collection representation for non-OTLP protocols](https://opentelemetry.io/docs/specs/otel/common/#attribute-collection-representation-for-non-otlp) for metric data diffs in `go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest`. (#8993)
|
||||
- Apply a maximum depth of 64 by default to span, event, link, log record, and instrumentation scope attribute values in `go.opentelemetry.io/otel/sdk/trace` and `go.opentelemetry.io/otel/sdk/log`. (#8938)
|
||||
- Reduce allocations when applying attribute value length limits in `go.opentelemetry.io/otel/sdk/trace` and `go.opentelemetry.io/otel/sdk/log` by rebuilding composite values only when truncation is needed. (#8912)
|
||||
- Decode `traceparent` using a hex lookup table in `go.opentelemetry.io/otel/propagation`, which rejects the specification-disallowed upper-case characters without a separate scan of the header. (#8739)
|
||||
- Decode all characters at constant indices in `TraceIDFromHex` and `SpanIDFromHex` in `go.opentelemetry.io/otel/trace` to eliminate bounds checks. (#8740)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Count valid U+FFFD replacement characters toward attribute value length limits in `go.opentelemetry.io/otel/trace`, `go.opentelemetry.io/otel/sdk/trace`, and `go.opentelemetry.io/otel/sdk/log`.
|
||||
- Truncate string attribute values to empty strings when the configured length limit is zero, including malformed UTF-8, in `go.opentelemetry.io/otel/trace`, `go.opentelemetry.io/otel/sdk/trace`, and `go.opentelemetry.io/otel/sdk/log`. (#9054)
|
||||
- Fix a data race in `NewPeriodicReader` where `r.inst` was assigned after the background goroutine was launched in `go.opentelemetry.io/otel/sdk/metric`. (#9028)
|
||||
- Prevent precision loss for large `int64` values in `Sum`, `Histogram`, and `ExponentialHistogram` aggregations in `go.opentelemetry.io/otel/sdk/metric`. (#8981)
|
||||
- Ensure `grpc.DialOption` values passed via `WithDialOption` take precedence over conflicting internally-computed defaults in
|
||||
`go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`,
|
||||
`go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc`, and
|
||||
`go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. (#8805, #8834, #8836)
|
||||
- Treat overflowing `OTEL_METRIC_EXPORT_INTERVAL` and `OTEL_METRIC_EXPORT_TIMEOUT` values as invalid in `go.opentelemetry.io/otel/sdk/metric`. (#8800)
|
||||
- Prevent a panic in `NewFixedSizeReservoir` and `FixedSizeReservoirProvider` when given a negative size in `go.opentelemetry.io/otel/sdk/metric/exemplar`; negative sizes are now clamped to zero, consistent with a size of zero. (#8832)
|
||||
- Preserve exponential histogram `ZeroThreshold` during OTLP metric export in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric`. (#8801)
|
||||
- Ensure metric helpers in `go.opentelemetry.io/otel/semconv/v1.32.0`, `go.opentelemetry.io/otel/semconv/v1.33.0`, and `go.opentelemetry.io/otel/semconv/v1.34.0` record measurements only once when no attributes are provided. (#8849)
|
||||
- Treat empty `OTEL_TRACES_SAMPLER` and `OTEL_TRACES_SAMPLER_ARG` values as unset in `go.opentelemetry.io/otel/sdk/trace`. (#8873)
|
||||
- Normalize global `OTEL_EXPORTER_OTLP_ENDPOINT` path joining in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp` so trailing-slash base URLs do not produce double-slash log export paths. (#8864)
|
||||
- Prevent a deadlock when formatting an error passed to `RecordError` calls back into the span in `go.opentelemetry.io/otel/sdk/trace`. (#8815)
|
||||
- Prevent `SimpleSpanProcessor.Shutdown` from panicking when constructed with a nil exporter in `go.opentelemetry.io/otel/sdk/trace`. (#8844)
|
||||
- Propagate invalid exponential histogram scale errors to Prometheus exporter self-observability metrics in `go.opentelemetry.io/otel/exporters/prometheus`. (#8839)
|
||||
- Retain instrument advisory attributes configured with `go.opentelemetry.io/otel/metric/x.WithDefaultAttributes` when a matching View in `go.opentelemetry.io/otel/sdk/metric` does not specify an attribute filter. (#8859)
|
||||
- Ignore HTTP URL paths when building OTLP/gRPC metric exporter targets from `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`, while preserving `unix://` and `unix-abstract://` targets; treat `unix-abstract://` endpoints as insecure in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc` and `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#8862)
|
||||
- Ignore HTTP(S) paths when deriving gRPC trace exporter endpoints from `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. (#8852)
|
||||
- Bound memory used while parsing dense W3C property metadata by storing it compactly and materializing `Property` values on demand in `go.opentelemetry.io/otel/baggage` and `go.opentelemetry.io/otel/propagation`.
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove the experimental `OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE` environment variable in `go.opentelemetry.io/otel/sdk/metric`.
|
||||
Use the `WithMaxExportBatchSize` option instead. (#8960)
|
||||
|
||||
## [1.47.0-rc.1] - 2026-08-28
|
||||
|
||||
This is a release candidate for the v1.47.0 release.
|
||||
That release is expected to include the `v1` release of the OpenTelemetry Go Logs API and SDK and will provide stability guarantees for the following modules:
|
||||
|
||||
- `go.opentelemetry.io/otel/log`
|
||||
- `go.opentelemetry.io/otel/sdk/log`
|
||||
|
||||
See our [versioning policy](VERSIONING.md) for more information about these stability guarantees.
|
||||
|
||||
### Added
|
||||
|
||||
- Add `Logger`, `GetLoggerProvider`, and `SetLoggerProvider` to `go.opentelemetry.io/otel`. (#8874)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecate `go.opentelemetry.io/otel/log/global`; use the equivalent APIs in `go.opentelemetry.io/otel`. (#8874)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ignore attempts to unregister unknown span processors in `go.opentelemetry.io/otel/sdk/trace`. (#8840)
|
||||
|
||||
### Removed
|
||||
|
||||
- Drop support for [Go 1.25]. (#8876)
|
||||
|
||||
## [1.46.0/0.68.0/0.22.0/0.0.19] - 2026-08-25
|
||||
|
||||
This release is the last to support [Go 1.25].
|
||||
@@ -3810,7 +3897,9 @@ 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.46.0...HEAD
|
||||
[Unreleased]: https://github.com/open-telemetry/opentelemetry-go/compare/v1.47.0...HEAD
|
||||
[1.47.0/0.69.0/0.23.0/0.1.0]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.47.0
|
||||
[1.47.0-rc.1]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.47.0-rc.1
|
||||
[1.46.0/0.68.0/0.22.0/0.0.19]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.46.0
|
||||
[1.45.0/0.67.0/0.21.0/0.0.18]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.45.0
|
||||
[1.44.0/0.66.0/0.20.0/0.0.17]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.44.0
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
# Instructions for Claude Code
|
||||
|
||||
@AGENTS.md
|
||||
+1
-1
@@ -12,6 +12,6 @@
|
||||
# https://help.github.com/en/articles/about-code-owners
|
||||
#
|
||||
|
||||
* @MrAlias @XSAM @dashpole @pellared @dmathieu @flc1125
|
||||
* @MrAlias @XSAM @dashpole @pellared @dmathieu @flc1125 @ps-mir
|
||||
|
||||
CODEOWNERS @MrAlias @pellared @dashpole @XSAM @dmathieu
|
||||
|
||||
+11
-9
@@ -779,7 +779,7 @@ func newInstrumentation() (*instrumentation, error) {
|
||||
if !x.Observability.Enabled() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
meter := otel.GetMeterProvider().Meter(
|
||||
"<component-package-name>",
|
||||
metric.WithInstrumentationVersion(sdk.Version()),
|
||||
@@ -949,7 +949,7 @@ func BenchmarkExportSpans(b *testing.B) {
|
||||
{"ObsDisabled", false},
|
||||
{"ObsEnabled", true},
|
||||
}
|
||||
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
b.Run(scenario.name, func(b *testing.B) {
|
||||
b.Setenv(
|
||||
@@ -980,7 +980,7 @@ func newInstrumentation() (*instrumentation, error) {
|
||||
if !x.Observability.Enabled() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
m := otel.GetMeterProvider().Meter(/* initialize meter */)
|
||||
counter, err := otelconv.NewSDKComponentCounter(m)
|
||||
// Use the partially initialized counter if available.
|
||||
@@ -996,7 +996,7 @@ func newInstrumentation() *instrumentation {
|
||||
if !x.Observability.Enabled() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
m := otel.GetMeterProvider().Meter(/* initialize meter */)
|
||||
counter, err := otelconv.NewSDKComponentCounter(m)
|
||||
if err != nil {
|
||||
@@ -1023,7 +1023,7 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
|
||||
if e.inst.Enabled(ctx) {
|
||||
e.inst.recordSpanExportStarted(ctx, len(spans))
|
||||
}
|
||||
|
||||
|
||||
err := e.doExport(ctx, spans)
|
||||
|
||||
if e.inst.Enabled(ctx) {
|
||||
@@ -1043,11 +1043,11 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan)
|
||||
func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) error {
|
||||
// ❌ Do not break the context propagation.
|
||||
e.inst.recordSpanExportStarted(context.Background(), len(spans))
|
||||
|
||||
|
||||
err := e.doExport(ctx, spans)
|
||||
|
||||
/* ... */
|
||||
|
||||
|
||||
return err
|
||||
}
|
||||
```
|
||||
@@ -1118,7 +1118,7 @@ func TestObservability(t *testing.T) {
|
||||
|
||||
// Reset component ID counter to ensure deterministic component names.
|
||||
componentIDCounter.Store(0)
|
||||
|
||||
|
||||
/* ... test code ... */
|
||||
}
|
||||
```
|
||||
@@ -1203,18 +1203,20 @@ For more information about the maintainer role, see the [community repository](h
|
||||
### Approvers
|
||||
|
||||
- [Flc](https://github.com/flc1125), Independent
|
||||
- [Puneet Singh](https://github.com/ps-mir), Independent
|
||||
|
||||
For more information about the approver role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#approver).
|
||||
|
||||
### Triagers
|
||||
|
||||
- [Alex Kats](https://github.com/akats7), Capital One
|
||||
- [Marc Schäfer](https://github.com/marcschaeferger), T&A SYSTEME GmbH
|
||||
|
||||
For more information about the triager role, see the [community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#triager).
|
||||
|
||||
### Emeritus
|
||||
|
||||
- [Aaron Clawson](https://github.com/MadVikingGod)
|
||||
- [Alex Kats](https://github.com/akats7)
|
||||
- [Anthony Mirabella](https://github.com/Aneurysm9)
|
||||
- [Cheng-Zhen Yang](https://github.com/scorpionknifes)
|
||||
- [Chester Cheung](https://github.com/hanyuancheung)
|
||||
|
||||
+2
-19
@@ -3,12 +3,11 @@
|
||||
[](https://github.com/open-telemetry/opentelemetry-go/actions/workflows/ci.yml)
|
||||
[](https://app.codecov.io/gh/open-telemetry/opentelemetry-go?branch=main)
|
||||
[](https://pkg.go.dev/go.opentelemetry.io/otel)
|
||||
[](https://goreportcard.com/report/go.opentelemetry.io/otel)
|
||||
[](https://scorecard.dev/viewer/?uri=github.com/open-telemetry/opentelemetry-go)
|
||||
[](https://www.bestpractices.dev/projects/9996)
|
||||
[](https://issues.oss-fuzz.com/issues?q=project:opentelemetry-go)
|
||||
[](https://app.fossa.com/projects/custom%2B162%2Fgithub.com%2Fopen-telemetry%2Fopentelemetry-go?ref=badge_shield&issueType=license)
|
||||
[](https://cloud-native.slack.com/archives/C01NPAXACKT)
|
||||
[](https://slack.cncf.io/)
|
||||
|
||||
OpenTelemetry-Go is the [Go](https://golang.org/) implementation of [OpenTelemetry](https://opentelemetry.io/).
|
||||
It provides a set of APIs to directly measure performance and behavior of your software and send this data to observability platforms.
|
||||
@@ -19,7 +18,7 @@ It provides a set of APIs to directly measure performance and behavior of your s
|
||||
|---------|--------------------|
|
||||
| Traces | Stable |
|
||||
| Metrics | Stable |
|
||||
| Logs | Beta[^1] |
|
||||
| Logs | Stable |
|
||||
|
||||
Progress and status specific to this repository is tracked in our
|
||||
[project boards](https://github.com/open-telemetry/opentelemetry-go/projects)
|
||||
@@ -29,8 +28,6 @@ and
|
||||
Project versioning information and stability guarantees can be found in the
|
||||
[versioning documentation](VERSIONING.md).
|
||||
|
||||
[^1]: https://github.com/orgs/open-telemetry/projects/43
|
||||
|
||||
### Compatibility
|
||||
|
||||
OpenTelemetry-Go ensures compatibility with the current supported versions of
|
||||
@@ -55,25 +52,18 @@ Currently, this project supports the following environments.
|
||||
|----------|------------|--------------|
|
||||
| Ubuntu | 1.27 | amd64 |
|
||||
| Ubuntu | 1.26 | amd64 |
|
||||
| Ubuntu | 1.25 | amd64 |
|
||||
| Ubuntu | 1.27 | 386 |
|
||||
| Ubuntu | 1.26 | 386 |
|
||||
| Ubuntu | 1.25 | 386 |
|
||||
| Ubuntu | 1.27 | arm64 |
|
||||
| Ubuntu | 1.26 | arm64 |
|
||||
| Ubuntu | 1.25 | arm64 |
|
||||
| macOS | 1.27 | amd64 |
|
||||
| macOS | 1.26 | amd64 |
|
||||
| macOS | 1.25 | amd64 |
|
||||
| macOS | 1.27 | arm64 |
|
||||
| macOS | 1.26 | arm64 |
|
||||
| macOS | 1.25 | arm64 |
|
||||
| Windows | 1.27 | amd64 |
|
||||
| Windows | 1.26 | amd64 |
|
||||
| Windows | 1.25 | amd64 |
|
||||
| Windows | 1.27 | 386 |
|
||||
| Windows | 1.26 | 386 |
|
||||
| Windows | 1.25 | 386 |
|
||||
|
||||
While this project should work for other systems, no compatibility guarantees
|
||||
are made for those systems currently.
|
||||
@@ -120,10 +110,3 @@ All officially supported exporters for the OpenTelemetry project are contained i
|
||||
## Contributing
|
||||
|
||||
See the [contributing documentation](CONTRIBUTING.md).
|
||||
|
||||
### Emeritus
|
||||
|
||||
- [Alex Kats](https://github.com/akats7), Triager
|
||||
|
||||
For more information about the emeritus role, see the
|
||||
[community repository](https://github.com/open-telemetry/community/blob/main/guides/contributor/membership.md#emeritus-maintainerapprovertriager).
|
||||
|
||||
+3
@@ -21,6 +21,7 @@ func New() Hash {
|
||||
return Hash{d: xxhash.New()}
|
||||
}
|
||||
|
||||
// Uint64 hashes val into h, returning the updated Hash.
|
||||
func (h Hash) Uint64(val uint64) Hash {
|
||||
var buf [8]byte
|
||||
binary.LittleEndian.PutUint64(buf[:], val)
|
||||
@@ -40,10 +41,12 @@ func (h Hash) Bool(val bool) Hash { // nolint:revive // This is a hashing functi
|
||||
return h.Uint64(0)
|
||||
}
|
||||
|
||||
// Float64 hashes val into h, returning the updated Hash.
|
||||
func (h Hash) Float64(val float64) Hash {
|
||||
return h.Uint64(math.Float64bits(val))
|
||||
}
|
||||
|
||||
// Int64 hashes val into h, returning the updated Hash.
|
||||
func (h Hash) Int64(val int64) Hash {
|
||||
return h.Uint64(uint64(val)) // nolint:gosec // Overflow doesn't matter since we are hashing.
|
||||
}
|
||||
|
||||
+10
@@ -13,6 +13,16 @@ type KeyValue struct {
|
||||
Value Value
|
||||
}
|
||||
|
||||
// String returns a string representation of KeyValue using the
|
||||
// [OpenTelemetry Attribute representation for non-OTLP protocols] rules.
|
||||
//
|
||||
// The KeyValue is encoded as a JSON object containing a single key-value pair.
|
||||
//
|
||||
// [OpenTelemetry Attribute representation for non-OTLP protocols]: https://opentelemetry.io/docs/specs/otel/common/#attribute-representation-for-non-otlp
|
||||
func (kv KeyValue) String() string {
|
||||
return formatMap([]KeyValue{kv})
|
||||
}
|
||||
|
||||
// Valid reports whether kv is a valid OpenTelemetry attribute.
|
||||
func (kv KeyValue) Valid() bool {
|
||||
return kv.Key.Defined()
|
||||
|
||||
+15
@@ -409,6 +409,21 @@ func computeDataReflect(kvs []KeyValue) any {
|
||||
return at.Interface()
|
||||
}
|
||||
|
||||
// String returns a string representation of the Set using the
|
||||
// [OpenTelemetry Attribute Collection representation for non-OTLP protocols]
|
||||
// rules.
|
||||
//
|
||||
// The Set is encoded as a JSON object. A nil or empty Set is encoded as an
|
||||
// empty JSON object.
|
||||
//
|
||||
// [OpenTelemetry Attribute Collection representation for non-OTLP protocols]: https://opentelemetry.io/docs/specs/otel/common/#attribute-collection-representation-for-non-otlp
|
||||
func (l *Set) String() string {
|
||||
if l == nil || l.hash == 0 {
|
||||
return "{}"
|
||||
}
|
||||
return formatMapValue(l.data)
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of the Set.
|
||||
func (l *Set) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(l.data)
|
||||
|
||||
+167
-68
@@ -247,10 +247,78 @@ func (p properties) String() string {
|
||||
return strings.Join(props, propertyDelimiter)
|
||||
}
|
||||
|
||||
// validateMetadata validates non-empty W3C property elements without decoding
|
||||
// or materializing Property values. Empty elements are accepted to preserve
|
||||
// existing parsing behavior.
|
||||
func validateMetadata(raw string) (string, bool) {
|
||||
start := 0
|
||||
for end := 0; end <= len(raw); end++ {
|
||||
if end != len(raw) && raw[end] != propertyDelimiter[0] {
|
||||
continue
|
||||
}
|
||||
|
||||
if start != end {
|
||||
if _, ok := parsePropertyFields(raw[start:end]); !ok {
|
||||
return raw[start:end], false
|
||||
}
|
||||
}
|
||||
start = end + 1
|
||||
}
|
||||
return "", true
|
||||
}
|
||||
|
||||
// normalizeMetadata removes empty property elements while preserving the raw
|
||||
// representation of all non-empty elements.
|
||||
func normalizeMetadata(metadata string) string {
|
||||
if metadata == "" ||
|
||||
(metadata[0] != propertyDelimiter[0] &&
|
||||
metadata[len(metadata)-1] != propertyDelimiter[0] &&
|
||||
!strings.Contains(metadata, propertyDelimiter+propertyDelimiter)) {
|
||||
return metadata
|
||||
}
|
||||
|
||||
var normalized strings.Builder
|
||||
normalized.Grow(len(metadata))
|
||||
for property := range strings.SplitSeq(metadata, propertyDelimiter) {
|
||||
if property == "" {
|
||||
continue
|
||||
}
|
||||
if normalized.Len() > 0 {
|
||||
_, _ = normalized.WriteString(propertyDelimiter)
|
||||
}
|
||||
_, _ = normalized.WriteString(property)
|
||||
}
|
||||
return normalized.String()
|
||||
}
|
||||
|
||||
func propertiesFromMetadata(metadata string) properties {
|
||||
if metadata == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
props := make(properties, 0, strings.Count(metadata, propertyDelimiter)+1)
|
||||
for pStr := range strings.SplitSeq(metadata, propertyDelimiter) {
|
||||
if pStr == "" {
|
||||
continue
|
||||
}
|
||||
p, err := parseProperty(pStr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
props = append(props, p)
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
// Member is a list-member of a baggage-string as defined by the W3C Baggage
|
||||
// specification.
|
||||
type Member struct {
|
||||
key, value string
|
||||
|
||||
// metadata contains the opaque W3C property suffix extracted from the wire
|
||||
// without the leading semicolon. properties contains API-created
|
||||
// properties. The fields are mutually exclusive.
|
||||
metadata string
|
||||
properties properties
|
||||
|
||||
// hasData indicates whether the created property contains data or not.
|
||||
@@ -307,21 +375,16 @@ func newInvalidMember() Member {
|
||||
return Member{}
|
||||
}
|
||||
|
||||
// parseMember attempts to decode a Member from the passed string. It returns
|
||||
// an error if the input is invalid according to the W3C Baggage
|
||||
// specification.
|
||||
// parseMember attempts to decode a Member from the passed string. Property
|
||||
// metadata grammar is validated without decoding or materializing properties.
|
||||
func parseMember(member string) (Member, error) {
|
||||
var props properties
|
||||
keyValue, properties, found := strings.Cut(member, propertyDelimiter)
|
||||
var metadata string
|
||||
keyValue, rawMetadata, found := strings.Cut(member, propertyDelimiter)
|
||||
if found {
|
||||
// Parse the member properties.
|
||||
for pStr := range strings.SplitSeq(properties, propertyDelimiter) {
|
||||
p, err := parseProperty(pStr)
|
||||
if err != nil {
|
||||
return newInvalidMember(), err
|
||||
}
|
||||
props = append(props, p)
|
||||
if invalid, ok := validateMetadata(rawMetadata); !ok {
|
||||
return newInvalidMember(), fmt.Errorf("%w: %q", errInvalidProperty, invalid)
|
||||
}
|
||||
metadata = normalizeMetadata(rawMetadata)
|
||||
}
|
||||
// Parse the member key/value pair.
|
||||
|
||||
@@ -349,7 +412,7 @@ func parseMember(member string) (Member, error) {
|
||||
}
|
||||
|
||||
value := replaceInvalidUTF8Sequences(len(rawVal), unescapeVal)
|
||||
return Member{key: key, value: value, properties: props, hasData: true}, nil
|
||||
return Member{key: key, value: value, metadata: metadata, hasData: true}, nil
|
||||
}
|
||||
|
||||
// replaceInvalidUTF8Sequences replaces invalid UTF-8 sequences with '�'.
|
||||
@@ -399,10 +462,14 @@ func (m Member) Key() string { return m.key }
|
||||
func (m Member) Value() string { return m.value }
|
||||
|
||||
// Properties returns a copy of the Member properties.
|
||||
func (m Member) Properties() []Property { return m.properties.Copy() }
|
||||
func (m Member) Properties() []Property {
|
||||
if m.metadata != "" {
|
||||
return propertiesFromMetadata(m.metadata)
|
||||
}
|
||||
return m.properties.Copy()
|
||||
}
|
||||
|
||||
// String encodes Member into a header string compliant with the W3C Baggage
|
||||
// specification.
|
||||
// String encodes Member into a baggage header string.
|
||||
// It would return empty string if the key is invalid with the W3C Baggage
|
||||
// specification. This could happen for a UTF-8 key, as it may contain
|
||||
// invalid characters.
|
||||
@@ -413,12 +480,31 @@ func (m Member) String() string {
|
||||
}
|
||||
|
||||
s := m.key + keyValueDelimiter + valueEscape(m.value)
|
||||
if len(m.properties) > 0 {
|
||||
if m.metadata != "" {
|
||||
s += propertyDelimiter + m.metadata
|
||||
} else if len(m.properties) > 0 {
|
||||
s += propertyDelimiter + m.properties.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func memberFromItem(key string, item baggage.Item) Member {
|
||||
return Member{
|
||||
key: key,
|
||||
value: item.Value(),
|
||||
metadata: item.Metadata(),
|
||||
properties: fromInternalProperties(item.Properties()),
|
||||
hasData: true,
|
||||
}
|
||||
}
|
||||
|
||||
func itemFromMember(member Member) baggage.Item {
|
||||
if member.metadata != "" {
|
||||
return baggage.NewItemWithMetadata(member.value, member.metadata)
|
||||
}
|
||||
return baggage.NewItemWithProperties(member.value, member.properties.asInternal())
|
||||
}
|
||||
|
||||
// Baggage is a list of baggage members representing the baggage-string as
|
||||
// defined by the W3C Baggage specification.
|
||||
type Baggage struct { //nolint:golint
|
||||
@@ -444,10 +530,7 @@ func New(members ...Member) (Baggage, error) {
|
||||
return Baggage{}, errInvalidMember
|
||||
}
|
||||
// OpenTelemetry resolves duplicates by last-one-wins.
|
||||
b[m.key] = baggage.Item{
|
||||
Value: m.value,
|
||||
Properties: m.properties.asInternal(),
|
||||
}
|
||||
b[m.key] = itemFromMember(m)
|
||||
}
|
||||
|
||||
var truncateErr error
|
||||
@@ -467,11 +550,7 @@ func New(members ...Member) (Baggage, error) {
|
||||
totalBytes := 0
|
||||
first := true
|
||||
for k := range b {
|
||||
m := Member{
|
||||
key: k,
|
||||
value: b[k].Value,
|
||||
properties: fromInternalProperties(b[k].Properties),
|
||||
}
|
||||
m := memberFromItem(k, b[k])
|
||||
memberSize := len(m.String())
|
||||
if !first {
|
||||
memberSize++ // comma separator
|
||||
@@ -563,10 +642,7 @@ func Parse(bStr string) (Baggage, error) {
|
||||
}
|
||||
|
||||
// OpenTelemetry resolves duplicates by last-one-wins.
|
||||
b[m.key] = baggage.Item{
|
||||
Value: m.value,
|
||||
Properties: m.properties.asInternal(),
|
||||
}
|
||||
b[m.key] = itemFromMember(m)
|
||||
sizes[m.key] = memberBytes
|
||||
totalBytes = newTotalBytes
|
||||
}
|
||||
@@ -597,12 +673,7 @@ func (b Baggage) Member(key string) Member {
|
||||
return newInvalidMember()
|
||||
}
|
||||
|
||||
return Member{
|
||||
key: key,
|
||||
value: v.Value,
|
||||
properties: fromInternalProperties(v.Properties),
|
||||
hasData: true,
|
||||
}
|
||||
return memberFromItem(key, v)
|
||||
}
|
||||
|
||||
// Members returns all the baggage list-members.
|
||||
@@ -617,12 +688,7 @@ func (b Baggage) Members() []Member {
|
||||
|
||||
members := make([]Member, 0, len(b.list))
|
||||
for k, v := range b.list {
|
||||
members = append(members, Member{
|
||||
key: k,
|
||||
value: v.Value,
|
||||
properties: fromInternalProperties(v.Properties),
|
||||
hasData: true,
|
||||
})
|
||||
members = append(members, memberFromItem(k, v))
|
||||
}
|
||||
return members
|
||||
}
|
||||
@@ -652,10 +718,7 @@ func (b Baggage) SetMember(member Member) (Baggage, error) {
|
||||
list[k] = v
|
||||
}
|
||||
|
||||
list[member.key] = baggage.Item{
|
||||
Value: member.value,
|
||||
Properties: member.properties.asInternal(),
|
||||
}
|
||||
list[member.key] = itemFromMember(member)
|
||||
|
||||
return Baggage{list: list}, nil
|
||||
}
|
||||
@@ -692,11 +755,7 @@ func (b Baggage) Len() int {
|
||||
func (b Baggage) String() string {
|
||||
members := make([]string, 0, len(b.list))
|
||||
for k, v := range b.list {
|
||||
s := Member{
|
||||
key: k,
|
||||
value: v.Value,
|
||||
properties: fromInternalProperties(v.Properties),
|
||||
}.String()
|
||||
s := memberFromItem(k, v).String()
|
||||
|
||||
// Ignored empty members.
|
||||
if s != "" {
|
||||
@@ -706,9 +765,14 @@ func (b Baggage) String() string {
|
||||
return strings.Join(members, listDelimiter)
|
||||
}
|
||||
|
||||
// parsePropertyInternal attempts to decode a Property from the passed string.
|
||||
// It follows the spec at https://www.w3.org/TR/baggage/#definition.
|
||||
func parsePropertyInternal(s string) (p Property, ok bool) {
|
||||
type propertyFields struct {
|
||||
key, rawValue string
|
||||
hasValue bool
|
||||
}
|
||||
|
||||
// parsePropertyFields validates and locates a property's fields without
|
||||
// decoding or allocating them.
|
||||
func parsePropertyFields(s string) (fields propertyFields, ok bool) {
|
||||
// For the entire function we will use " key = value " as an example.
|
||||
// Attempting to parse the key.
|
||||
// First skip spaces at the beginning "< >key = value " (they could be empty).
|
||||
@@ -727,7 +791,7 @@ func parsePropertyInternal(s string) (p Property, ok bool) {
|
||||
// If we couldn't find any valid key character,
|
||||
// it means the key is either empty or invalid.
|
||||
if keyStart == keyEnd {
|
||||
return p, ok
|
||||
return fields, ok
|
||||
}
|
||||
|
||||
// Skip spaces after the key: " key< >= value ".
|
||||
@@ -735,15 +799,14 @@ func parsePropertyInternal(s string) (p Property, ok bool) {
|
||||
|
||||
if index == len(s) {
|
||||
// A key can have no value, like: " key ".
|
||||
ok = true
|
||||
p.key = s[keyStart:keyEnd]
|
||||
return p, ok
|
||||
fields.key = s[keyStart:keyEnd]
|
||||
return fields, true
|
||||
}
|
||||
|
||||
// If we have not reached the end and we can't find the '=' delimiter,
|
||||
// it means the property is invalid.
|
||||
if s[index] != keyValueDelimiter[0] {
|
||||
return p, ok
|
||||
return fields, ok
|
||||
}
|
||||
|
||||
// Attempting to parse the value.
|
||||
@@ -769,23 +832,59 @@ func parsePropertyInternal(s string) (p Property, ok bool) {
|
||||
// we have not reached the end, it means the property is
|
||||
// invalid, something like: " key = value value1".
|
||||
if index != len(s) {
|
||||
return p, ok
|
||||
return fields, ok
|
||||
}
|
||||
rawVal := s[valueStart:valueEnd]
|
||||
if !validPercentEncoding(rawVal) {
|
||||
return fields, ok
|
||||
}
|
||||
|
||||
fields.key = s[keyStart:keyEnd]
|
||||
fields.rawValue = rawVal
|
||||
fields.hasValue = true
|
||||
return fields, true
|
||||
}
|
||||
|
||||
func validPercentEncoding(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] != '%' {
|
||||
continue
|
||||
}
|
||||
if i+2 >= len(s) || !isHex(s[i+1]) || !isHex(s[i+2]) {
|
||||
return false
|
||||
}
|
||||
i += 2
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isHex(c byte) bool {
|
||||
return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F'
|
||||
}
|
||||
|
||||
// parsePropertyInternal attempts to decode a Property from the passed string.
|
||||
// It follows the spec at https://www.w3.org/TR/baggage/#definition.
|
||||
func parsePropertyInternal(s string) (p Property, ok bool) {
|
||||
fields, ok := parsePropertyFields(s)
|
||||
if !ok {
|
||||
return p, false
|
||||
}
|
||||
if !fields.hasValue {
|
||||
p.key = fields.key
|
||||
return p, true
|
||||
}
|
||||
|
||||
// Decode a percent-encoded value.
|
||||
rawVal := s[valueStart:valueEnd]
|
||||
unescapeVal, err := url.PathUnescape(rawVal)
|
||||
unescapeVal, err := url.PathUnescape(fields.rawValue)
|
||||
if err != nil {
|
||||
return p, ok
|
||||
return p, false
|
||||
}
|
||||
value := replaceInvalidUTF8Sequences(len(rawVal), unescapeVal)
|
||||
value := replaceInvalidUTF8Sequences(len(fields.rawValue), unescapeVal)
|
||||
|
||||
ok = true
|
||||
p.key = s[keyStart:keyEnd]
|
||||
p.key = fields.key
|
||||
p.hasValue = true
|
||||
|
||||
p.value = value
|
||||
return p, ok
|
||||
return p, true
|
||||
}
|
||||
|
||||
func skipSpace(s string, offset int) int {
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ const (
|
||||
maxCode = 3
|
||||
)
|
||||
|
||||
// Code is an 32-bit representation of a status state.
|
||||
// Code is a 32-bit representation of a status state.
|
||||
type Code uint32
|
||||
|
||||
var codeToStr = map[Code]string{
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# This is a renovate-friendly source of Docker images.
|
||||
FROM python:3.13.6-slim-bullseye@sha256:e98b521460ee75bca92175c16247bdf7275637a8faaeb2bcfa19d879ae5c4b9a AS python
|
||||
FROM otel/weaver:v0.25.1@sha256:9ad46ca9cd4fa5974b121f886aa3e9946a8ef8ea905001a96c018d21f9db87ca AS weaver
|
||||
FROM otel/weaver:v0.26.1@sha256:9094862c0ab261bdbcb079bb981f9a573b3659b130a6d2ab8616eca6ba37aaec AS weaver
|
||||
FROM avtodev/markdown-lint:v1@sha256:6aeedc2f49138ce7a1cd0adffc1b1c0321b841dc2102408967d9301c031949ee AS markdown
|
||||
|
||||
Generated
Vendored
+2
-2
@@ -18,8 +18,8 @@ package internal
|
||||
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/envconfig.go.tmpl "--data={\"envconfigImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/envconfig\"}" --out=oconf/envconfig.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/envconfig_test.go.tmpl "--data={}" --out=oconf/envconfig_test.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/options.go.tmpl "--data={\"retryImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/retry\"}" --out=oconf/options.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/options_test.go.tmpl "--data={\"envconfigImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/envconfig\"}" --out=oconf/options_test.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/options.go.tmpl "--data={\"isHTTP\": false, \"retryImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/retry\"}" --out=oconf/options.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/options_test.go.tmpl "--data={\"isHTTP\": false, \"envconfigImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/envconfig\"}" --out=oconf/options_test.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/optiontypes.go.tmpl "--data={}" --out=oconf/optiontypes.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlpmetric/oconf/tls.go.tmpl "--data={}" --out=oconf/tls.go
|
||||
|
||||
|
||||
vendor/go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf/envconfig.go
Generated
Vendored
+13
-2
@@ -121,11 +121,22 @@ func withEndpointForGRPC(u *url.URL) func(cfg Config) Config {
|
||||
return func(cfg Config) Config {
|
||||
// For OTLP/gRPC endpoints, this is the target to which the
|
||||
// exporter is going to send telemetry.
|
||||
cfg.Metrics.Endpoint = path.Join(u.Host, u.Path)
|
||||
cfg.Metrics.Endpoint = grpcEndpoint(u)
|
||||
return cfg
|
||||
}
|
||||
}
|
||||
|
||||
func grpcEndpoint(u *url.URL) string {
|
||||
switch strings.ToLower(u.Scheme) {
|
||||
case "http", "https":
|
||||
return u.Host
|
||||
case "":
|
||||
return path.Join(u.Host, u.Path)
|
||||
default:
|
||||
return u.String()
|
||||
}
|
||||
}
|
||||
|
||||
// WithEnvCompression retrieves the specified config and passes it to ConfigFn as a Compression.
|
||||
func WithEnvCompression(n string, fn func(Compression)) func(e *envconfig.EnvOptionsReader) {
|
||||
return func(e *envconfig.EnvOptionsReader) {
|
||||
@@ -142,7 +153,7 @@ func WithEnvCompression(n string, fn func(Compression)) func(e *envconfig.EnvOpt
|
||||
|
||||
func withEndpointScheme(u *url.URL) GenericOption {
|
||||
switch strings.ToLower(u.Scheme) {
|
||||
case "http", "unix":
|
||||
case "http", "unix", "unix-abstract":
|
||||
return WithInsecure()
|
||||
default:
|
||||
return WithSecure()
|
||||
|
||||
Generated
Vendored
+42
-6
@@ -51,6 +51,7 @@ type (
|
||||
// This type is compatible with `http.Transport.Proxy` and can be used to set a custom proxy function to the OTLP HTTP client.
|
||||
HTTPTransportProxyFunc func(*http.Request) (*url.URL, error)
|
||||
|
||||
// SignalConfig holds the configuration for exporting a single signal.
|
||||
SignalConfig struct {
|
||||
Endpoint string
|
||||
Insecure bool
|
||||
@@ -72,6 +73,7 @@ type (
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// Config holds the configuration for an otlpmetric exporter.
|
||||
Config struct {
|
||||
// Signal specific configurations
|
||||
Metrics SignalConfig
|
||||
@@ -144,30 +146,36 @@ func NewGRPCConfig(opts ...GRPCOption) Config {
|
||||
cfg = opt.ApplyGRPCOption(cfg)
|
||||
}
|
||||
|
||||
// dialOptsPrefix holds the internally computed defaults. It is prepended
|
||||
// to cfg.DialOptions so that a raw grpc.DialOption supplied via WithDialOption
|
||||
// always takes precedence: grpc.DialOption values are opaque closures, so this code has no way to
|
||||
// detect a conflicting user-supplied option and defer to it instead.
|
||||
var dialOptsPrefix []grpc.DialOption
|
||||
if cfg.ServiceConfig != "" {
|
||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultServiceConfig(cfg.ServiceConfig))
|
||||
dialOptsPrefix = append(dialOptsPrefix, grpc.WithDefaultServiceConfig(cfg.ServiceConfig))
|
||||
}
|
||||
// Prioritize GRPCCredentials over Insecure (passing both is an error).
|
||||
if cfg.Metrics.GRPCCredentials != nil { //nolint:gocritic // if-else is clearer than switch
|
||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(cfg.Metrics.GRPCCredentials))
|
||||
dialOptsPrefix = append(dialOptsPrefix, grpc.WithTransportCredentials(cfg.Metrics.GRPCCredentials))
|
||||
} else if cfg.Metrics.Insecure {
|
||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
dialOptsPrefix = append(dialOptsPrefix, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
} else {
|
||||
// Default to using the host's root CA.
|
||||
creds := credentials.NewTLS(nil)
|
||||
cfg.Metrics.GRPCCredentials = creds
|
||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(creds))
|
||||
dialOptsPrefix = append(dialOptsPrefix, grpc.WithTransportCredentials(creds))
|
||||
}
|
||||
if cfg.Metrics.Compression == GzipCompression {
|
||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)))
|
||||
dialOptsPrefix = append(dialOptsPrefix, grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)))
|
||||
}
|
||||
if cfg.ReconnectionPeriod != 0 {
|
||||
p := grpc.ConnectParams{
|
||||
Backoff: backoff.DefaultConfig,
|
||||
MinConnectTimeout: cfg.ReconnectionPeriod,
|
||||
}
|
||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithConnectParams(p))
|
||||
dialOptsPrefix = append(dialOptsPrefix, grpc.WithConnectParams(p))
|
||||
}
|
||||
cfg.DialOptions = append(dialOptsPrefix, cfg.DialOptions...)
|
||||
|
||||
return cfg
|
||||
}
|
||||
@@ -257,6 +265,7 @@ func (h *httpOption) ApplyHTTPOption(cfg Config) Config {
|
||||
|
||||
func (httpOption) private() {}
|
||||
|
||||
// NewHTTPOption creates an option that is only applied to the HTTP driver.
|
||||
func NewHTTPOption(fn func(cfg Config) Config) HTTPOption {
|
||||
return &httpOption{fn: fn}
|
||||
}
|
||||
@@ -272,12 +281,16 @@ func (h *grpcOption) ApplyGRPCOption(cfg Config) Config {
|
||||
|
||||
func (grpcOption) private() {}
|
||||
|
||||
// NewGRPCOption creates an option that is only applied to the gRPC driver.
|
||||
func NewGRPCOption(fn func(cfg Config) Config) GRPCOption {
|
||||
return &grpcOption{fn: fn}
|
||||
}
|
||||
|
||||
// Generic Options
|
||||
|
||||
// WithEndpoint configures the metrics host and port only; endpoint should
|
||||
// resemble "example.com" or "localhost:4317". To configure the scheme and
|
||||
// path, use WithEndpointURL.
|
||||
func WithEndpoint(endpoint string) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.Endpoint = endpoint
|
||||
@@ -285,6 +298,8 @@ func WithEndpoint(endpoint string) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithEndpointURL configures the metrics scheme, host, port, and path; the
|
||||
// provided value should resemble "https://example.com:4318/v1/metrics".
|
||||
func WithEndpointURL(v string) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
u, err := url.Parse(v)
|
||||
@@ -306,6 +321,7 @@ func WithEndpointURL(v string) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithCompression configures the compression used for exports.
|
||||
func WithCompression(compression Compression) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.Compression = compression
|
||||
@@ -313,6 +329,7 @@ func WithCompression(compression Compression) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithURLPath configures the URL path the exporter sends requests to.
|
||||
func WithURLPath(urlPath string) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.URLPath = urlPath
|
||||
@@ -320,6 +337,7 @@ func WithURLPath(urlPath string) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithRetry configures the retry policy used on failed exports.
|
||||
func WithRetry(rc retry.Config) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.RetryConfig = rc
|
||||
@@ -327,6 +345,8 @@ func WithRetry(rc retry.Config) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithTLSClientConfig configures the TLS configuration used by the
|
||||
// exporter's client.
|
||||
func WithTLSClientConfig(tlsCfg *tls.Config) GenericOption {
|
||||
return newSplitOption(func(cfg Config) Config {
|
||||
cfg.Metrics.TLSCfg = tlsCfg.Clone()
|
||||
@@ -337,6 +357,8 @@ func WithTLSClientConfig(tlsCfg *tls.Config) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithInsecure disables client transport security for the exporter's
|
||||
// connection.
|
||||
func WithInsecure() GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.Insecure = true
|
||||
@@ -344,6 +366,8 @@ func WithInsecure() GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithSecure enables client transport security for the exporter's
|
||||
// connection.
|
||||
func WithSecure() GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.Insecure = false
|
||||
@@ -351,6 +375,7 @@ func WithSecure() GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithHeaders configures headers sent with every export request.
|
||||
func WithHeaders(headers map[string]string) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.Headers = headers
|
||||
@@ -358,6 +383,8 @@ func WithHeaders(headers map[string]string) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithTimeout configures the max waiting time for the backend to process
|
||||
// each export batch.
|
||||
func WithTimeout(duration time.Duration) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.Timeout = duration
|
||||
@@ -365,6 +392,8 @@ func WithTimeout(duration time.Duration) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithMaxRequestSize configures the maximum size, in bytes, of a serialized
|
||||
// export request, before compression.
|
||||
func WithMaxRequestSize(size int) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.MaxRequestSize = size
|
||||
@@ -372,6 +401,8 @@ func WithMaxRequestSize(size int) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithTemporalitySelector configures the TemporalitySelector used to
|
||||
// determine the temporality of exported metrics.
|
||||
func WithTemporalitySelector(selector metric.TemporalitySelector) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.TemporalitySelector = selector
|
||||
@@ -379,6 +410,8 @@ func WithTemporalitySelector(selector metric.TemporalitySelector) GenericOption
|
||||
})
|
||||
}
|
||||
|
||||
// WithAggregationSelector configures the AggregationSelector used to
|
||||
// determine the aggregation of exported metrics.
|
||||
func WithAggregationSelector(selector metric.AggregationSelector) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.AggregationSelector = selector
|
||||
@@ -386,6 +419,8 @@ func WithAggregationSelector(selector metric.AggregationSelector) GenericOption
|
||||
})
|
||||
}
|
||||
|
||||
// WithProxy configures the proxy function used by the exporter's HTTP
|
||||
// client.
|
||||
func WithProxy(pf HTTPTransportProxyFunc) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.Proxy = pf
|
||||
@@ -393,6 +428,7 @@ func WithProxy(pf HTTPTransportProxyFunc) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithHTTPClient configures the HTTP client used to make requests.
|
||||
func WithHTTPClient(c *http.Client) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Metrics.HTTPClient = c
|
||||
|
||||
Generated
Vendored
+1
@@ -235,6 +235,7 @@ func ExponentialHistogramDataPoints[N int64 | float64](
|
||||
Sum: &sum,
|
||||
Scale: dPt.Scale,
|
||||
ZeroCount: dPt.ZeroCount,
|
||||
ZeroThreshold: dPt.ZeroThreshold,
|
||||
Exemplars: Exemplars(dPt.Exemplars),
|
||||
|
||||
Positive: ExponentialHistogramDataPointBuckets(dPt.PositiveBucket),
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -5,5 +5,5 @@ package otlpmetricgrpc
|
||||
|
||||
// Version is the current release version of the OpenTelemetry OTLP over gRPC metrics exporter in use.
|
||||
func Version() string {
|
||||
return "1.46.0"
|
||||
return "1.47.0"
|
||||
}
|
||||
|
||||
Generated
Vendored
+2
@@ -9,6 +9,8 @@ import (
|
||||
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||
)
|
||||
|
||||
// InstrumentationScope transforms il into an OTLP InstrumentationScope,
|
||||
// returning nil if il is the zero value.
|
||||
func InstrumentationScope(il instrumentation.Scope) *commonpb.InstrumentationScope {
|
||||
if il == (instrumentation.Scope{}) {
|
||||
return nil
|
||||
|
||||
+3
-2
@@ -12,8 +12,9 @@ The environment variables described below can be used for configuration.
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT (default: "https://localhost:4317") -
|
||||
target to which the exporter sends telemetry.
|
||||
The target syntax is defined in https://github.com/grpc/grpc/blob/master/doc/naming.md.
|
||||
The value must contain a scheme ("http" or "https") and host.
|
||||
The value may additionally contain a port, and a path.
|
||||
If the value uses the "http" or "https" scheme, the host and optional port are
|
||||
used as the gRPC target and any path is ignored.
|
||||
Values with other schemes are passed to gRPC as-is.
|
||||
The value should not contain a query string or fragment.
|
||||
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT takes precedence over OTEL_EXPORTER_OTLP_ENDPOINT.
|
||||
The configuration can be overridden by [WithEndpoint], [WithEndpointURL], [WithInsecure], and [WithGRPCConn] options.
|
||||
|
||||
Generated
Vendored
+2
-2
@@ -14,8 +14,8 @@ package internal
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/envconfig/envconfig_test.go.tmpl "--data={}" --out=envconfig/envconfig_test.go
|
||||
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlptrace/otlpconfig/envconfig.go.tmpl "--data={\"envconfigImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/envconfig\"}" --out=otlpconfig/envconfig.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlptrace/otlpconfig/options.go.tmpl "--data={\"retryImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/retry\"}" --out=otlpconfig/options.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlptrace/otlpconfig/options_test.go.tmpl "--data={\"envconfigImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/envconfig\"}" --out=otlpconfig/options_test.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlptrace/otlpconfig/options.go.tmpl "--data={\"isHTTP\": false, \"retryImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/retry\"}" --out=otlpconfig/options.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlptrace/otlpconfig/options_test.go.tmpl "--data={\"isHTTP\": false, \"envconfigImportPath\": \"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/envconfig\"}" --out=otlpconfig/options_test.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlptrace/otlpconfig/optiontypes.go.tmpl "--data={}" --out=otlpconfig/optiontypes.go
|
||||
//go:generate gotmpl --body=../../../../../internal/shared/otlp/otlptrace/otlpconfig/tls.go.tmpl "--data={}" --out=otlpconfig/tls.go
|
||||
|
||||
|
||||
Generated
Vendored
+11
-1
@@ -119,10 +119,20 @@ func withEndpointScheme(u *url.URL) GenericOption {
|
||||
}
|
||||
|
||||
func withEndpointForGRPC(u *url.URL) func(cfg Config) Config {
|
||||
target := u.String()
|
||||
switch strings.ToLower(u.Scheme) {
|
||||
case "http", "https":
|
||||
target = u.Host
|
||||
case "":
|
||||
if u.Host != "" {
|
||||
target = path.Join(u.Host, u.Path)
|
||||
}
|
||||
}
|
||||
|
||||
return func(cfg Config) Config {
|
||||
// For OTLP/gRPC endpoints, this is the target to which the
|
||||
// exporter is going to send telemetry.
|
||||
cfg.Traces.Endpoint = path.Join(u.Host, u.Path)
|
||||
cfg.Traces.Endpoint = target
|
||||
return cfg
|
||||
}
|
||||
}
|
||||
|
||||
Generated
Vendored
+34
-7
@@ -44,6 +44,7 @@ type (
|
||||
// This type is compatible with `http.Transport.Proxy` and can be used to set a custom proxy function to the OTLP HTTP client.
|
||||
HTTPTransportProxyFunc func(*http.Request) (*url.URL, error)
|
||||
|
||||
// SignalConfig holds the configuration for exporting a single signal.
|
||||
SignalConfig struct {
|
||||
Endpoint string
|
||||
Insecure bool
|
||||
@@ -63,6 +64,7 @@ type (
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// Config holds the configuration for an otlptrace exporter.
|
||||
Config struct {
|
||||
// Signal specific configurations
|
||||
Traces SignalConfig
|
||||
@@ -126,37 +128,42 @@ func NewGRPCConfig(opts ...GRPCOption) Config {
|
||||
Timeout: DefaultTimeout,
|
||||
},
|
||||
RetryConfig: retry.DefaultConfig,
|
||||
DialOptions: []grpc.DialOption{grpc.WithUserAgent(userAgent)},
|
||||
}
|
||||
cfg = ApplyGRPCEnvConfigs(cfg)
|
||||
for _, opt := range opts {
|
||||
cfg = opt.ApplyGRPCOption(cfg)
|
||||
}
|
||||
|
||||
// dialOptsPrefix holds the internally computed defaults. It is prepended
|
||||
// to cfg.DialOptions so that a raw grpc.DialOption supplied via WithDialOption
|
||||
// always takes precedence: grpc.DialOption values are opaque closures, so this code has no way to
|
||||
// detect a conflicting user-supplied option and defer to it instead.
|
||||
dialOptsPrefix := []grpc.DialOption{grpc.WithUserAgent(userAgent)}
|
||||
if cfg.ServiceConfig != "" {
|
||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultServiceConfig(cfg.ServiceConfig))
|
||||
dialOptsPrefix = append(dialOptsPrefix, grpc.WithDefaultServiceConfig(cfg.ServiceConfig))
|
||||
}
|
||||
// Prioritize GRPCCredentials over Insecure (passing both is an error).
|
||||
if cfg.Traces.GRPCCredentials != nil { //nolint:gocritic // if-else is clearer than switch
|
||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(cfg.Traces.GRPCCredentials))
|
||||
dialOptsPrefix = append(dialOptsPrefix, grpc.WithTransportCredentials(cfg.Traces.GRPCCredentials))
|
||||
} else if cfg.Traces.Insecure {
|
||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
dialOptsPrefix = append(dialOptsPrefix, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
} else {
|
||||
// Default to using the host's root CA.
|
||||
creds := credentials.NewTLS(nil)
|
||||
cfg.Traces.GRPCCredentials = creds
|
||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(creds))
|
||||
dialOptsPrefix = append(dialOptsPrefix, grpc.WithTransportCredentials(creds))
|
||||
}
|
||||
if cfg.Traces.Compression == GzipCompression {
|
||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)))
|
||||
dialOptsPrefix = append(dialOptsPrefix, grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)))
|
||||
}
|
||||
if cfg.ReconnectionPeriod != 0 {
|
||||
p := grpc.ConnectParams{
|
||||
Backoff: backoff.DefaultConfig,
|
||||
MinConnectTimeout: cfg.ReconnectionPeriod,
|
||||
}
|
||||
cfg.DialOptions = append(cfg.DialOptions, grpc.WithConnectParams(p))
|
||||
dialOptsPrefix = append(dialOptsPrefix, grpc.WithConnectParams(p))
|
||||
}
|
||||
cfg.DialOptions = append(dialOptsPrefix, cfg.DialOptions...)
|
||||
|
||||
return cfg
|
||||
}
|
||||
@@ -246,6 +253,7 @@ func (h *httpOption) ApplyHTTPOption(cfg Config) Config {
|
||||
|
||||
func (httpOption) private() {}
|
||||
|
||||
// NewHTTPOption creates an option that is only applied to the HTTP driver.
|
||||
func NewHTTPOption(fn func(cfg Config) Config) HTTPOption {
|
||||
return &httpOption{fn: fn}
|
||||
}
|
||||
@@ -261,6 +269,7 @@ func (h *grpcOption) ApplyGRPCOption(cfg Config) Config {
|
||||
|
||||
func (grpcOption) private() {}
|
||||
|
||||
// NewGRPCOption creates an option that is only applied to the gRPC driver.
|
||||
func NewGRPCOption(fn func(cfg Config) Config) GRPCOption {
|
||||
return &grpcOption{fn: fn}
|
||||
}
|
||||
@@ -300,6 +309,7 @@ func WithEndpointURL(v string) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithCompression configures the compression used for exports.
|
||||
func WithCompression(compression Compression) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Traces.Compression = compression
|
||||
@@ -307,6 +317,7 @@ func WithCompression(compression Compression) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithURLPath configures the URL path the exporter sends requests to.
|
||||
func WithURLPath(urlPath string) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Traces.URLPath = urlPath
|
||||
@@ -314,6 +325,7 @@ func WithURLPath(urlPath string) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithRetry configures the retry policy used on failed exports.
|
||||
func WithRetry(rc retry.Config) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.RetryConfig = rc
|
||||
@@ -321,6 +333,8 @@ func WithRetry(rc retry.Config) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithTLSClientConfig configures the TLS configuration used by the
|
||||
// exporter's client.
|
||||
func WithTLSClientConfig(tlsCfg *tls.Config) GenericOption {
|
||||
return newSplitOption(func(cfg Config) Config {
|
||||
cfg.Traces.TLSCfg = tlsCfg.Clone()
|
||||
@@ -331,6 +345,8 @@ func WithTLSClientConfig(tlsCfg *tls.Config) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithInsecure disables client transport security for the exporter's
|
||||
// connection.
|
||||
func WithInsecure() GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Traces.Insecure = true
|
||||
@@ -338,6 +354,8 @@ func WithInsecure() GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithSecure enables client transport security for the exporter's
|
||||
// connection.
|
||||
func WithSecure() GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Traces.Insecure = false
|
||||
@@ -345,6 +363,7 @@ func WithSecure() GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithHeaders configures headers sent with every export request.
|
||||
func WithHeaders(headers map[string]string) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Traces.Headers = headers
|
||||
@@ -352,6 +371,8 @@ func WithHeaders(headers map[string]string) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithTimeout configures the max waiting time for the backend to process
|
||||
// each export batch.
|
||||
func WithTimeout(duration time.Duration) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Traces.Timeout = duration
|
||||
@@ -359,6 +380,8 @@ func WithTimeout(duration time.Duration) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithMaxRequestSize configures the maximum size, in bytes, of a serialized
|
||||
// export request, before compression.
|
||||
func WithMaxRequestSize(size int) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Traces.MaxRequestSize = size
|
||||
@@ -366,6 +389,8 @@ func WithMaxRequestSize(size int) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithProxy configures the proxy function used by the exporter's HTTP
|
||||
// client.
|
||||
func WithProxy(pf HTTPTransportProxyFunc) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Traces.Proxy = pf
|
||||
@@ -373,6 +398,7 @@ func WithProxy(pf HTTPTransportProxyFunc) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithHTTPClient configures the HTTP client used to make requests.
|
||||
func WithHTTPClient(c *http.Client) GenericOption {
|
||||
return newGenericOption(func(cfg Config) Config {
|
||||
cfg.Traces.HTTPClient = c
|
||||
@@ -380,6 +406,7 @@ func WithHTTPClient(c *http.Client) GenericOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithProtocol configures the protocol used to encode and send telemetry.
|
||||
func WithProtocol(protocol Protocol) GenericOption {
|
||||
return newSplitOption(
|
||||
// For OTLP/HTTP endpoints, this is the encoding format of the payloads sent to the collector.
|
||||
|
||||
Generated
Vendored
+1
-1
@@ -5,4 +5,4 @@ package internal
|
||||
|
||||
// Version is the current release version of the OpenTelemetry OTLP gRPC trace
|
||||
// exporter in use.
|
||||
const Version = "1.46.0"
|
||||
const Version = "1.47.0"
|
||||
|
||||
Generated
Vendored
+5
-4
@@ -74,9 +74,10 @@ func WithEndpoint(endpoint string) Option {
|
||||
return wrappedOption{otlpconfig.WithEndpoint(endpoint)}
|
||||
}
|
||||
|
||||
// WithEndpointURL sets the target endpoint URL (scheme, host, port, path)
|
||||
// the Exporter will connect to. The provided endpoint URL should resemble
|
||||
// "https://example.com:4318/v1/traces".
|
||||
// WithEndpointURL sets the target endpoint URL (scheme, host, port) the
|
||||
// Exporter will connect to. The provided endpoint URL should resemble
|
||||
// "https://example.com:4317". If the URL contains a path, it is ignored by
|
||||
// the gRPC client.
|
||||
//
|
||||
// If the OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
|
||||
// environment variable is set, and this option is not passed, that variable
|
||||
@@ -90,7 +91,7 @@ func WithEndpoint(endpoint string) Option {
|
||||
// If an invalid URL is provided, the default value will be kept.
|
||||
//
|
||||
// By default, if an environment variable is not set, and this option is not
|
||||
// passed, "https://localhost:4317/v1/traces" will be used.
|
||||
// passed, "https://localhost:4317" will be used.
|
||||
//
|
||||
// This option has no effect if WithGRPCConn is used.
|
||||
func WithEndpointURL(u string) Option {
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@ package otlptrace
|
||||
|
||||
// Version is the current release version of the OpenTelemetry OTLP trace exporter in use.
|
||||
func Version() string {
|
||||
return "1.46.0"
|
||||
return "1.47.0"
|
||||
}
|
||||
|
||||
+26
-3
@@ -16,12 +16,35 @@ package baggage
|
||||
// but OpenTelemetry does not, therefore, this is represented as a map.
|
||||
type List map[string]Item
|
||||
|
||||
// Item is the value and metadata properties part of a list-member.
|
||||
// Item is the value and metadata or properties part of a list-member.
|
||||
// Metadata and properties are mutually exclusive representations.
|
||||
type Item struct {
|
||||
Value string
|
||||
Properties []Property
|
||||
value string
|
||||
metadata string
|
||||
properties []Property
|
||||
}
|
||||
|
||||
// NewItemWithMetadata returns an Item with opaque W3C metadata.
|
||||
func NewItemWithMetadata(value, metadata string) Item {
|
||||
return Item{value: value, metadata: metadata}
|
||||
}
|
||||
|
||||
// NewItemWithProperties returns an Item with materialized properties.
|
||||
func NewItemWithProperties(value string, properties []Property) Item {
|
||||
return Item{value: value, properties: properties}
|
||||
}
|
||||
|
||||
// Value returns the value of i.
|
||||
func (i Item) Value() string { return i.value }
|
||||
|
||||
// Metadata returns the opaque W3C metadata of i. It is empty for an Item
|
||||
// created with properties.
|
||||
func (i Item) Metadata() string { return i.metadata }
|
||||
|
||||
// Properties returns the materialized properties of i. It is nil for an Item
|
||||
// created with metadata.
|
||||
func (i Item) Properties() []Property { return i.properties }
|
||||
|
||||
// Property is a metadata entry for a list-member.
|
||||
type Property struct {
|
||||
Key, Value string
|
||||
|
||||
+5
@@ -21,6 +21,9 @@ type ErrorHandler interface {
|
||||
Handle(error)
|
||||
}
|
||||
|
||||
// ErrDelegator is an ErrorHandler that forwards errors to a delegate
|
||||
// ErrorHandler set with setDelegate. Before a delegate is set, it logs
|
||||
// errors to STDERR.
|
||||
type ErrDelegator struct {
|
||||
delegate atomic.Pointer[ErrorHandler]
|
||||
}
|
||||
@@ -28,6 +31,8 @@ type ErrDelegator struct {
|
||||
// Compile-time check that delegator implements ErrorHandler.
|
||||
var _ ErrorHandler = (*ErrDelegator)(nil)
|
||||
|
||||
// Handle handles err by forwarding it to the configured delegate, or
|
||||
// logging it to STDERR if no delegate is set.
|
||||
func (d *ErrDelegator) Handle(err error) {
|
||||
if eh := d.delegate.Load(); eh != nil {
|
||||
(*eh).Handle(err)
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package global
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"go.opentelemetry.io/otel/log"
|
||||
"go.opentelemetry.io/otel/log/embedded"
|
||||
)
|
||||
|
||||
type loggerProvider struct {
|
||||
embedded.LoggerProvider
|
||||
|
||||
mu sync.Mutex
|
||||
loggers map[il]*logger
|
||||
delegate log.LoggerProvider
|
||||
}
|
||||
|
||||
// This is a compile-time guarantee that loggerProvider implements LoggerProvider.
|
||||
var _ log.LoggerProvider = (*loggerProvider)(nil)
|
||||
|
||||
func (p *loggerProvider) Logger(name string, options ...log.LoggerOption) log.Logger {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.delegate != nil {
|
||||
return p.delegate.Logger(name, options...)
|
||||
}
|
||||
|
||||
cfg := log.NewLoggerConfig(options...)
|
||||
key := il{
|
||||
name: name,
|
||||
version: cfg.InstrumentationVersion(),
|
||||
schema: cfg.SchemaURL(),
|
||||
attrs: cfg.InstrumentationAttributes(),
|
||||
}
|
||||
|
||||
if p.loggers == nil {
|
||||
l := &logger{name: name, options: options}
|
||||
p.loggers = map[il]*logger{key: l}
|
||||
return l
|
||||
}
|
||||
|
||||
if l, ok := p.loggers[key]; ok {
|
||||
return l
|
||||
}
|
||||
|
||||
l := &logger{name: name, options: options}
|
||||
p.loggers[key] = l
|
||||
return l
|
||||
}
|
||||
|
||||
func (p *loggerProvider) setDelegate(provider log.LoggerProvider) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
p.delegate = provider
|
||||
for _, l := range p.loggers {
|
||||
l.setDelegate(provider)
|
||||
}
|
||||
p.loggers = nil // Only set logger delegates once.
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
embedded.Logger
|
||||
|
||||
name string
|
||||
options []log.LoggerOption
|
||||
|
||||
delegate atomic.Value // log.Logger
|
||||
}
|
||||
|
||||
// This is a compile-time guarantee that logger implements Logger.
|
||||
var _ log.Logger = (*logger)(nil)
|
||||
|
||||
func (l *logger) Emit(ctx context.Context, r log.Record) {
|
||||
if del, ok := l.delegate.Load().(log.Logger); ok {
|
||||
del.Emit(ctx, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *logger) Enabled(ctx context.Context, param log.EnabledParameters) bool {
|
||||
var enabled bool
|
||||
if del, ok := l.delegate.Load().(log.Logger); ok {
|
||||
enabled = del.Enabled(ctx, param)
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
func (l *logger) setDelegate(provider log.LoggerProvider) {
|
||||
l.delegate.Store(provider.Logger(l.name, l.options...))
|
||||
}
|
||||
+42
-3
@@ -9,6 +9,7 @@ import (
|
||||
"sync/atomic"
|
||||
|
||||
"go.opentelemetry.io/otel/internal/errorhandler"
|
||||
"go.opentelemetry.io/otel/log"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -26,16 +27,22 @@ type (
|
||||
meterProviderHolder struct {
|
||||
mp metric.MeterProvider
|
||||
}
|
||||
|
||||
loggerProviderHolder struct {
|
||||
provider log.LoggerProvider
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
globalTracer = defaultTracerValue()
|
||||
globalPropagators = defaultPropagatorsValue()
|
||||
globalMeterProvider = defaultMeterProvider()
|
||||
globalTracer = defaultTracerValue()
|
||||
globalPropagators = defaultPropagatorsValue()
|
||||
globalMeterProvider = defaultMeterProvider()
|
||||
globalLoggerProvider = defaultLoggerProvider()
|
||||
|
||||
delegateTraceOnce sync.Once
|
||||
delegateTextMapPropagatorOnce sync.Once
|
||||
delegateMeterOnce sync.Once
|
||||
delegateLoggerOnce sync.Once
|
||||
)
|
||||
|
||||
// GetErrorHandler returns the global ErrorHandler instance.
|
||||
@@ -150,6 +157,32 @@ func SetMeterProvider(mp metric.MeterProvider) {
|
||||
globalMeterProvider.Store(meterProviderHolder{mp: mp})
|
||||
}
|
||||
|
||||
// LoggerProvider is the internal implementation for global.LoggerProvider.
|
||||
func LoggerProvider() log.LoggerProvider {
|
||||
return globalLoggerProvider.Load().(loggerProviderHolder).provider
|
||||
}
|
||||
|
||||
// SetLoggerProvider is the internal implementation for global.SetLoggerProvider.
|
||||
func SetLoggerProvider(provider log.LoggerProvider) {
|
||||
current := LoggerProvider()
|
||||
if _, cOk := current.(*loggerProvider); cOk {
|
||||
if _, lpOk := provider.(*loggerProvider); lpOk && current == provider {
|
||||
Error(
|
||||
errors.New("no delegate configured in logger provider"),
|
||||
"Setting logger provider to its current value. No delegate will be configured",
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
delegateLoggerOnce.Do(func() {
|
||||
if def, ok := current.(*loggerProvider); ok {
|
||||
def.setDelegate(provider)
|
||||
}
|
||||
})
|
||||
globalLoggerProvider.Store(loggerProviderHolder{provider: provider})
|
||||
}
|
||||
|
||||
func defaultTracerValue() *atomic.Value {
|
||||
v := &atomic.Value{}
|
||||
v.Store(tracerProviderHolder{tp: &tracerProvider{}})
|
||||
@@ -167,3 +200,9 @@ func defaultMeterProvider() *atomic.Value {
|
||||
v.Store(meterProviderHolder{mp: &meterProvider{}})
|
||||
return v
|
||||
}
|
||||
|
||||
func defaultLoggerProvider() *atomic.Value {
|
||||
v := &atomic.Value{}
|
||||
v.Store(loggerProviderHolder{provider: &loggerProvider{}})
|
||||
return v
|
||||
}
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package otel
|
||||
|
||||
import (
|
||||
"go.opentelemetry.io/otel/internal/global"
|
||||
"go.opentelemetry.io/otel/log"
|
||||
)
|
||||
|
||||
// Logger returns a [log.Logger] configured with the provided name and options
|
||||
// from the globally configured [log.LoggerProvider].
|
||||
//
|
||||
// If this is called before a global LoggerProvider is configured, the returned
|
||||
// Logger will be a No-Op implementation of a Logger. When a global
|
||||
// LoggerProvider is registered for the first time, the returned Logger is
|
||||
// updated in place to report to this new LoggerProvider. There is no need to
|
||||
// call this function again for an updated instance.
|
||||
//
|
||||
// This is a convenience function. It is equivalent to:
|
||||
//
|
||||
// GetLoggerProvider().Logger(name, options...)
|
||||
func Logger(name string, options ...log.LoggerOption) log.Logger {
|
||||
return GetLoggerProvider().Logger(name, options...)
|
||||
}
|
||||
|
||||
// GetLoggerProvider returns the globally configured [log.LoggerProvider].
|
||||
//
|
||||
// If a global LoggerProvider has not been configured with [SetLoggerProvider],
|
||||
// the returned LoggerProvider will be a No-Op implementation. When
|
||||
// a global LoggerProvider is registered for the first time, the returned
|
||||
// LoggerProvider and all Loggers it has created are updated in place. There is
|
||||
// no need to call this function again for an updated instance.
|
||||
func GetLoggerProvider() log.LoggerProvider {
|
||||
return global.LoggerProvider()
|
||||
}
|
||||
|
||||
// SetLoggerProvider configures provider as the global [log.LoggerProvider].
|
||||
func SetLoggerProvider(provider log.LoggerProvider) {
|
||||
global.SetLoggerProvider(provider)
|
||||
}
|
||||
+566
@@ -0,0 +1,566 @@
|
||||
# Logs API
|
||||
|
||||
## Abstract
|
||||
|
||||
`go.opentelemetry.io/otel/log` provides
|
||||
[Logs API](https://opentelemetry.io/docs/specs/otel/logs/api/).
|
||||
|
||||
The prototype was created in
|
||||
[#4725](https://github.com/open-telemetry/opentelemetry-go/pull/4725).
|
||||
|
||||
## Background
|
||||
|
||||
The key challenge is to create a performant API compliant with the [specification](https://opentelemetry.io/docs/specs/otel/logs/api/)
|
||||
with an intuitive and user friendly design.
|
||||
Performance is seen as one of the most important characteristics of logging libraries in Go.
|
||||
|
||||
## Design
|
||||
|
||||
This proposed design aims to:
|
||||
|
||||
- be specification compliant,
|
||||
- be similar to Trace and Metrics API,
|
||||
- take advantage of both OpenTelemetry and `slog` experience to achieve acceptable performance.
|
||||
|
||||
### Module structure
|
||||
|
||||
The API is published as a single `go.opentelemetry.io/otel/log` Go module.
|
||||
|
||||
The package structure is similar to Trace API and Metrics API.
|
||||
The Go module consists of the following packages:
|
||||
|
||||
- `go.opentelemetry.io/otel/log`
|
||||
- `go.opentelemetry.io/otel/log/embedded`
|
||||
- `go.opentelemetry.io/otel/log/logtest`
|
||||
- `go.opentelemetry.io/otel/log/noop`
|
||||
|
||||
Rejected alternative:
|
||||
|
||||
- [Reuse slog](#reuse-slog)
|
||||
|
||||
### LoggerProvider
|
||||
|
||||
The [`LoggerProvider` abstraction](https://opentelemetry.io/docs/specs/otel/logs/api/#loggerprovider)
|
||||
is defined as `LoggerProvider` interface in [provider.go](provider.go).
|
||||
|
||||
The specification may add new operations to `LoggerProvider`.
|
||||
The interface may have methods added without a package major version bump.
|
||||
This embeds `embedded.LoggerProvider` to help inform an API implementation
|
||||
author about this non-standard API evolution.
|
||||
This approach is already used in Trace API and Metrics API.
|
||||
|
||||
#### LoggerProvider.Logger
|
||||
|
||||
The `Logger` method implements the [`Get a Logger` operation](https://opentelemetry.io/docs/specs/otel/logs/api/#get-a-logger).
|
||||
|
||||
The required `name` parameter is accepted as a `string` method argument.
|
||||
|
||||
The `LoggerOption` options are defined to support optional parameters.
|
||||
|
||||
Implementation requirements:
|
||||
|
||||
- The [specification requires](https://opentelemetry.io/docs/specs/otel/logs/api/#concurrency-requirements)
|
||||
the method to be safe to be called concurrently.
|
||||
|
||||
- If the passed name is empty, this should be reported as invalid as specified
|
||||
by the [Logs SDK specification](https://opentelemetry.io/docs/specs/otel/logs/sdk/#logger-creation).
|
||||
The method should still use it as the instrumentation scope name and return
|
||||
a working logger.
|
||||
|
||||
`Logger` can be extended by adding new `LoggerOption` options
|
||||
and adding new exported fields to the `LoggerConfig` struct.
|
||||
This design is already used in Trace API for getting tracers
|
||||
and in Metrics API for getting meters.
|
||||
|
||||
Rejected alternative:
|
||||
|
||||
- [Passing struct as parameter to LoggerProvider.Logger](#passing-struct-as-parameter-to-loggerproviderlogger).
|
||||
|
||||
### Logger
|
||||
|
||||
The [`Logger` abstraction](https://opentelemetry.io/docs/specs/otel/logs/api/#logger)
|
||||
is defined as `Logger` interface in [logger.go](logger.go).
|
||||
|
||||
The specification may add new operations to `Logger`.
|
||||
The interface may have methods added without a package major version bump.
|
||||
This embeds `embedded.Logger` to help inform an API implementation
|
||||
author about this non-standard API evolution.
|
||||
This approach is already used in Trace API and Metrics API.
|
||||
|
||||
### Logger.Emit
|
||||
|
||||
The `Emit` method implements the [`Emit a LogRecord` operation](https://opentelemetry.io/docs/specs/otel/logs/api/#emit-a-logrecord).
|
||||
|
||||
[`Context` associated with the `LogRecord`](https://opentelemetry.io/docs/specs/otel/context/)
|
||||
is accepted as a `context.Context` method argument.
|
||||
|
||||
Calls to `Emit` are supposed to be on the hot path.
|
||||
Therefore, in order to reduce the number of heap allocations,
|
||||
the [`LogRecord` abstraction](https://opentelemetry.io/docs/specs/otel/logs/api/#emit-a-logrecord),
|
||||
is defined as `Record` struct in [record.go](record.go).
|
||||
|
||||
[`Timestamp`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-timestamp)
|
||||
is accessed using following methods:
|
||||
|
||||
```go
|
||||
func (r *Record) Timestamp() time.Time
|
||||
func (r *Record) SetTimestamp(t time.Time)
|
||||
```
|
||||
|
||||
[`ObservedTimestamp`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-observedtimestamp)
|
||||
is accessed using following methods:
|
||||
|
||||
```go
|
||||
func (r *Record) ObservedTimestamp() time.Time
|
||||
func (r *Record) SetObservedTimestamp(t time.Time)
|
||||
```
|
||||
|
||||
[`EventName`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-eventname)
|
||||
is accessed using following methods:
|
||||
|
||||
```go
|
||||
func (r *Record) EventName() string
|
||||
func (r *Record) SetEventName(s string)
|
||||
```
|
||||
|
||||
[`SeverityNumber`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber)
|
||||
is accessed using following methods:
|
||||
|
||||
```go
|
||||
func (r *Record) Severity() Severity
|
||||
func (r *Record) SetSeverity(s Severity)
|
||||
```
|
||||
|
||||
`Severity` type is defined in [severity.go](severity.go).
|
||||
The constants are are based on
|
||||
[Displaying Severity recommendation](https://opentelemetry.io/docs/specs/otel/logs/data-model/#displaying-severity).
|
||||
Additionally, `Severity[Level]` constants are defined to make the API more readable and user friendly.
|
||||
|
||||
[`SeverityText`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitytext)
|
||||
is accessed using following methods:
|
||||
|
||||
```go
|
||||
func (r *Record) SeverityText() string
|
||||
func (r *Record) SetSeverityText(s string)
|
||||
```
|
||||
|
||||
[`Body`](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-body)
|
||||
is accessed using following methods:
|
||||
|
||||
```go
|
||||
func (r *Record) Body() attribute.Value
|
||||
func (r *Record) SetBody(v attribute.Value)
|
||||
```
|
||||
|
||||
[Log record attributes](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-attributes)
|
||||
are accessed using following methods:
|
||||
|
||||
```go
|
||||
func (r *Record) WalkAttributes(f func(attribute.KeyValue) bool)
|
||||
func (r *Record) AddAttributes(attrs ...attribute.KeyValue)
|
||||
```
|
||||
|
||||
`Record` has a `AttributesLen` method that returns
|
||||
the number of attributes to allow slice preallocation
|
||||
when converting records to a different representation:
|
||||
|
||||
```go
|
||||
func (r *Record) AttributesLen() int
|
||||
```
|
||||
|
||||
The records attributes design and implementation is based on
|
||||
[`slog.Record`](https://pkg.go.dev/log/slog#Record).
|
||||
It allows achieving high-performance access and manipulation of the attributes
|
||||
while keeping the API user friendly.
|
||||
It relieves the user from making his own improvements
|
||||
for reducing the number of allocations when passing attributes.
|
||||
|
||||
Log body and attributes use the common
|
||||
[`attribute.Value`](https://pkg.go.dev/go.opentelemetry.io/otel/attribute#Value)
|
||||
and
|
||||
[`attribute.KeyValue`](https://pkg.go.dev/go.opentelemetry.io/otel/attribute#KeyValue)
|
||||
types.
|
||||
These types cover the Logs Data Model `any` value shape, including empty
|
||||
values, byte slices, generic slices, and maps.
|
||||
Reusing the attribute package keeps the API consistent across signals and
|
||||
avoids maintaining a second value model with conversion helpers.
|
||||
|
||||
The zero value of `attribute.Value` represents an empty body.
|
||||
Log maps use `attribute.MAP`, which may contain duplicate keys when callers
|
||||
construct one that way. Duplicate-key normalization is an SDK policy, not an
|
||||
API behavior.
|
||||
|
||||
The caller must not subsequently mutate the record passed to `Emit`.
|
||||
This would allow the implementation to not clone the record,
|
||||
but simply retain, modify or discard it.
|
||||
The implementation may still choose to clone the record or copy its attributes
|
||||
if it needs to retain or modify it,
|
||||
e.g. in case of asynchronous processing to eliminate the possibility of data races,
|
||||
because the user can technically reuse the record and add new attributes
|
||||
after the call (even when the documentation says that the caller must not do it).
|
||||
|
||||
Implementation requirements:
|
||||
|
||||
- The [specification requires](https://opentelemetry.io/docs/specs/otel/logs/api/#concurrency-requirements)
|
||||
the method to be safe to be called concurrently.
|
||||
|
||||
- The method must not interrupt the record processing if the context is canceled
|
||||
per ["ignoring context cancellation" guideline](../CONTRIBUTING.md#ignoring-context-cancellation).
|
||||
|
||||
- The [specification requires](https://opentelemetry.io/docs/specs/otel/logs/api/#emit-a-logrecord)
|
||||
use the current time as observed timestamp if the passed is empty.
|
||||
|
||||
- The method should handle the trace context passed via `ctx` argument in order to meet the
|
||||
[specification's SDK requirement](https://opentelemetry.io/docs/specs/otel/logs/sdk/#readablelogrecord)
|
||||
to populate the trace context fields from the resolved context.
|
||||
|
||||
`Emit` can be extended by adding new exported fields to the `Record` struct.
|
||||
|
||||
Rejected alternatives:
|
||||
|
||||
- [Record as interface](#record-as-interface)
|
||||
- [Options as parameter to Logger.Emit](#options-as-parameter-to-loggeremit)
|
||||
- [Passing record as pointer to Logger.Emit](#passing-record-as-pointer-to-loggeremit)
|
||||
- [Logger.WithAttributes](#loggerwithattributes)
|
||||
- [Record attributes as slice](#record-attributes-as-slice)
|
||||
- [Use any instead of attribute.Value](#use-any-instead-of-attributevalue)
|
||||
- [Severity type encapsulating number and text](#severity-type-encapsulating-number-and-text)
|
||||
- [Define log-specific value types](#define-log-specific-value-types)
|
||||
- [Mix receiver types for Record](#mix-receiver-types-for-record)
|
||||
- [Add XYZ method to Logger](#add-xyz-method-to-logger)
|
||||
|
||||
### Logger.Enabled
|
||||
|
||||
The `Enabled` method implements the [`Enabled` operation](https://opentelemetry.io/docs/specs/otel/logs/api/#enabled).
|
||||
|
||||
[`Context` associated with the `LogRecord`](https://opentelemetry.io/docs/specs/otel/context/)
|
||||
is accepted as a `context.Context` method argument.
|
||||
|
||||
Calls to `Enabled` are supposed to be on the hot path and the list of arguments
|
||||
can be extendend in future. Therefore, in order to reduce the number of heap
|
||||
allocations and make it possible to handle new arguments, `Enabled` accepts
|
||||
a `EnabledParameters` struct, defined in [logger.go](logger.go), as the second
|
||||
method argument.
|
||||
|
||||
The `EnabledParameters` uses fields, instead of getters and setters, to allow
|
||||
simpler usage which allows configuring the `EnabledParameters` in the same line
|
||||
where `Enabled` is called.
|
||||
|
||||
### noop package
|
||||
|
||||
The `go.opentelemetry.io/otel/log/noop` package provides
|
||||
[Logs API No-Op Implementation](https://opentelemetry.io/docs/specs/otel/logs/noop/).
|
||||
|
||||
### Trace context correlation
|
||||
|
||||
The bridge implementation should do its best to pass
|
||||
the `ctx` containing the trace context from the caller
|
||||
so it can later be passed via `Logger.Emit`.
|
||||
|
||||
It is not expected that users (caller or bridge implementation) reconstruct
|
||||
a `context.Context`. Reconstructing a `context.Context` with
|
||||
[`trace.ContextWithSpanContext`](https://pkg.go.dev/go.opentelemetry.io/otel/trace#ContextWithSpanContext)
|
||||
and [`trace.NewSpanContext`](https://pkg.go.dev/go.opentelemetry.io/otel/trace#NewSpanContext)
|
||||
would usually involve more memory allocations.
|
||||
|
||||
The logging libraries which have recording methods that accepts `context.Context`,
|
||||
such us [`slog`](https://pkg.go.dev/log/slog),
|
||||
[`logrus`](https://pkg.go.dev/github.com/sirupsen/logrus),
|
||||
[`zerolog`](https://pkg.go.dev/github.com/rs/zerolog),
|
||||
makes passing the trace context trivial.
|
||||
|
||||
However, some libraries do not accept a `context.Context` in their recording methods.
|
||||
Structured logging libraries,
|
||||
such as [`logr`](https://pkg.go.dev/github.com/go-logr/logr)
|
||||
and [`zap`](https://pkg.go.dev/go.uber.org/zap),
|
||||
offer passing `any` type as a log attribute/field.
|
||||
Therefore, their bridge implementations can define a "special" log attributes/field
|
||||
that will be used to capture the trace context.
|
||||
|
||||
[The prototype](https://github.com/open-telemetry/opentelemetry-go/pull/4725)
|
||||
has bridge implementations that handle trace context correlation efficiently.
|
||||
|
||||
## Benchmarking
|
||||
|
||||
The benchmarks take inspiration from [`slog`](https://pkg.go.dev/log/slog),
|
||||
because for the Go team it was also critical to create API that would be fast
|
||||
and interoperable with existing logging packages.[^2][^3]
|
||||
|
||||
The benchmark results can be found in [the prototype](https://github.com/open-telemetry/opentelemetry-go/pull/4725).
|
||||
|
||||
## Rejected alternatives
|
||||
|
||||
### Reuse slog
|
||||
|
||||
The API must not be coupled to [`slog`](https://pkg.go.dev/log/slog),
|
||||
nor any other logging library.
|
||||
|
||||
The API needs to evolve orthogonally to `slog`.
|
||||
|
||||
`slog` is not compliant with the [Logs API](https://opentelemetry.io/docs/specs/otel/logs/api/).
|
||||
and we cannot expect the Go team to make `slog` compliant with it.
|
||||
|
||||
The interoperability can be achieved using [a log bridge](https://opentelemetry.io/docs/specs/otel/glossary/#log-appender--bridge).
|
||||
|
||||
You can read more about OpenTelemetry Logs design on [opentelemetry.io](https://opentelemetry.io/docs/concepts/signals/logs/).
|
||||
|
||||
### Record as interface
|
||||
|
||||
`Record` is defined as a `struct` because of the following reasons.
|
||||
|
||||
Log record is a value object without any behavior.
|
||||
It is used as data input for Logger methods.
|
||||
|
||||
The log record resembles the instrument config structs like [metric.Float64CounterConfig](https://pkg.go.dev/go.opentelemetry.io/otel/metric#Float64CounterConfig).
|
||||
|
||||
Using `struct` instead of `interface` improves the performance as e.g.
|
||||
indirect calls are less optimized,
|
||||
usage of interfaces tend to increase heap allocations.[^3]
|
||||
|
||||
### Options as parameter to Logger.Emit
|
||||
|
||||
One of the initial ideas was to have:
|
||||
|
||||
```go
|
||||
type Logger interface{
|
||||
embedded.Logger
|
||||
Emit(ctx context.Context, options ...RecordOption)
|
||||
}
|
||||
```
|
||||
|
||||
The main reason was that design would be similar
|
||||
to the [Meter API](https://pkg.go.dev/go.opentelemetry.io/otel/metric#Meter)
|
||||
for creating instruments.
|
||||
|
||||
However, passing `Record` directly, instead of using options,
|
||||
is more performant as it reduces heap allocations.[^4]
|
||||
|
||||
Another advantage of passing `Record` is that API would not have functions like `NewRecord(options...)`,
|
||||
which would be used by the SDK and not by the users.
|
||||
|
||||
Finally, the definition would be similar to [`slog.Handler.Handle`](https://pkg.go.dev/log/slog#Handler)
|
||||
that was designed to provide optimization opportunities.[^2]
|
||||
|
||||
### Passing record as pointer to Logger.Emit
|
||||
|
||||
So far the benchmarks do not show differences that would
|
||||
favor passing the record via pointer (and vice versa).
|
||||
|
||||
Passing via value feels safer because of the following reasons.
|
||||
|
||||
The user would not be able to pass `nil`.
|
||||
Therefore, it reduces the possibility to have a nil pointer dereference.
|
||||
|
||||
It should reduce the possibility of a heap allocation.
|
||||
|
||||
It follows the design of [`slog.Handler`](https://pkg.go.dev/log/slog#Handler).
|
||||
|
||||
If follows one of Google's Go Style Decisions
|
||||
to prefer [passing values](https://google.github.io/styleguide/go/decisions#pass-values).
|
||||
|
||||
### Passing struct as parameter to LoggerProvider.Logger
|
||||
|
||||
Similarly to `Logger.Emit`, we could have something like:
|
||||
|
||||
```go
|
||||
type LoggerProvider interface{
|
||||
embedded.LoggerProvider
|
||||
Logger(name string, config LoggerConfig)
|
||||
}
|
||||
```
|
||||
|
||||
The drawback of this idea would be that this would be
|
||||
a different design from Trace and Metrics API.
|
||||
|
||||
The performance of acquiring a logger is not as critical
|
||||
as the performance of emitting a log record. While a single
|
||||
HTTP/RPC handler could write hundreds of logs, it should not
|
||||
create a new logger for each log entry.
|
||||
The bridge implementation should reuse loggers whenever possible.
|
||||
|
||||
### Logger.WithAttributes
|
||||
|
||||
We could add `WithAttributes` to the `Logger` interface.
|
||||
Then `Record` could be a simple struct with only exported fields.
|
||||
The idea was that the SDK would implement the performance improvements
|
||||
instead of doing it in the API.
|
||||
This would allow having different optimization strategies.
|
||||
|
||||
During the analysis[^5], it occurred that the main problem of this proposal
|
||||
is that the variadic slice passed to an interface method is always heap allocated.
|
||||
|
||||
Moreover, the logger returned by `WithAttribute` was allocated on the heap.
|
||||
|
||||
Lastly, the proposal was not specification compliant.
|
||||
|
||||
### Record attributes as slice
|
||||
|
||||
One of the proposals[^6] was to have `Record` as a simple struct:
|
||||
|
||||
```go
|
||||
type Record struct {
|
||||
Timestamp time.Time
|
||||
ObservedTimestamp time.Time
|
||||
EventName string
|
||||
Severity Severity
|
||||
SeverityText string
|
||||
Body Value
|
||||
Attributes []KeyValue
|
||||
}
|
||||
```
|
||||
|
||||
The bridge implementations could use [`sync.Pool`](https://pkg.go.dev/sync#Pool)
|
||||
for reducing the number of allocations when passing attributes.
|
||||
|
||||
The benchmarks results were better.
|
||||
|
||||
In such a design, most bridges would have a `sync.Pool`
|
||||
to reduce the number of heap allocations.
|
||||
However, the `sync.Pool` will not work correctly with API implementations
|
||||
that would take ownership of the record
|
||||
(e.g. implementations that do not copy records for asynchronous processing).
|
||||
The current design, even in case of improper API implementation,
|
||||
has lower chances of encountering a bug as most bridges would
|
||||
create a record, pass it, and forget about it.
|
||||
|
||||
For reference, here is the reason why `slog` does not use `sync.Pool`[^3]
|
||||
as well:
|
||||
|
||||
> We can use a sync pool for records though we decided not to.
|
||||
You can but it's a bad idea for us. Why?
|
||||
Because users have control of Records.
|
||||
Handler writers can get their hands on a record
|
||||
and we'd have to ask them to free it
|
||||
or try to free it magically at some some point.
|
||||
But either way, they could get themselves in trouble by freeing it twice
|
||||
or holding on to one after they free it.
|
||||
That's a use after free bug and that's why `zerolog` was problematic for us.
|
||||
`zerolog` as as part of its speed exposes a pool allocated value to users
|
||||
if you use `zerolog` the normal way, that you'll see in all the examples,
|
||||
you will never encounter a problem.
|
||||
But if you do something a little out of the ordinary you can get
|
||||
use after free bugs and we just didn't want to put that in the standard library.
|
||||
|
||||
Therefore, we decided to not follow the proposal as it is
|
||||
less user friendly (users and bridges would use e.g. a `sync.Pool` to reduce
|
||||
the number of heap allocation), less safe (more prone to use after free bugs
|
||||
and race conditions), and the benchmark differences were not significant.
|
||||
|
||||
### Use any instead of attribute.Value
|
||||
|
||||
[Logs Data Model](https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-body)
|
||||
defines Body to be `any`.
|
||||
One could propose to define `Body` (and attribute values) as `any`
|
||||
instead of using `attribute.Value`.
|
||||
|
||||
First of all, [`any` type defined in the specification](https://opentelemetry.io/docs/specs/otel/logs/data-model/#type-any)
|
||||
is not the same as `any` (`interface{}`) in Go.
|
||||
|
||||
Moreover, using `any` as a field would decrease the performance.[^7]
|
||||
|
||||
Using `attribute.Value` preserves a typed, allocation-conscious representation
|
||||
of the Logs Data Model `any` values while avoiding unconstrained `interface{}`
|
||||
handling in bridge implementations.
|
||||
|
||||
### Severity type encapsulating number and text
|
||||
|
||||
We could combine severity into a single field defining a type:
|
||||
|
||||
```go
|
||||
type Severity struct {
|
||||
Number SeverityNumber
|
||||
Text string
|
||||
}
|
||||
```
|
||||
|
||||
However, the [Logs Data Model](https://opentelemetry.io/docs/specs/otel/logs/data-model/#log-and-event-record-definition)
|
||||
define it as independent fields.
|
||||
It should be more user friendly to have them separated.
|
||||
Especially when having getter and setter methods, setting one value
|
||||
when the other is already set would be unpleasant.
|
||||
|
||||
### Define log-specific value types
|
||||
|
||||
The original design defined `Kind`, `Value`, and `KeyValue` in
|
||||
`go.opentelemetry.io/otel/log`.
|
||||
That avoided coupling log bodies to the common attribute package while logs were
|
||||
still exploring structured value support.
|
||||
|
||||
The Logs Data Model and common attribute value model now share the structured
|
||||
`any` shapes needed by Go: empty, bool, int64, float64, string, byte slice,
|
||||
homogeneous slices, generic slices, and maps.
|
||||
The specification direction is to reuse these value shapes across signals.
|
||||
Keeping log-specific types would duplicate API surface, require conversion
|
||||
helpers, and make bridge code choose between two equivalent value models.
|
||||
|
||||
Therefore log records now use `attribute.Value` for body values and
|
||||
`attribute.KeyValue` for attributes and map entries.
|
||||
|
||||
### Mix receiver types for Record
|
||||
|
||||
Methods of [`slog.Record`](https://pkg.go.dev/log/slog#Record)
|
||||
have different receiver types.
|
||||
|
||||
In `log/slog` GitHub issue we can only find that the reason is:[^8]
|
||||
|
||||
>> some receiver of Record struct is by value
|
||||
> Passing Records by value means they incur no heap allocation.
|
||||
> That improves performance overall, even though they are copied.
|
||||
|
||||
However, the benchmarks do not show any noticeable differences.[^9]
|
||||
|
||||
The compiler is smart-enough to not make a heap allocation for any of these methods.
|
||||
The use of a pointer receiver does not cause any heap allocation.
|
||||
From Go FAQ:[^10]
|
||||
|
||||
> In the current compilers, if a variable has its address taken,
|
||||
> that variable is a candidate for allocation on the heap.
|
||||
> However, a basic escape analysis recognizes some cases
|
||||
> when such variables will not live past the return from the function
|
||||
> and can reside on the stack.
|
||||
|
||||
The [Understanding Allocations: the Stack and the Heap](https://www.youtube.com/watch?v=ZMZpH4yT7M0)
|
||||
presentation by Jacob Walker describes the escape analysis with details.
|
||||
|
||||
Moreover, also from Go FAQ:[^10]
|
||||
|
||||
> Also, if a local variable is very large,
|
||||
> it might make more sense to store it on the heap rather than the stack.
|
||||
|
||||
Therefore, even if we use a value receiver and the value is very large
|
||||
it may be heap allocated.
|
||||
|
||||
Both [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments#receiver-type)
|
||||
and [Google's Go Style Decisions](https://google.github.io/styleguide/go/decisions#receiver-type)
|
||||
highly recommend making the methods for a type either all pointer methods
|
||||
or all value methods. Google's Go Style Decisions even goes further and says:
|
||||
|
||||
> There is a lot of misinformation about whether passing a value or a pointer
|
||||
> to a function can affect performance.
|
||||
> The compiler can choose to pass pointers to values on the stack
|
||||
> as well as copying values on the stack,
|
||||
> but these considerations should not outweigh the readability
|
||||
> and correctness of the code in most circumstances.
|
||||
> When the performance does matter, it is important to profile both approaches
|
||||
> with a realistic benchmark before deciding that one approach outperforms the other.
|
||||
|
||||
Because, the benchmarks[^9] do not proof any performance difference
|
||||
and the general recommendation is to not mix receiver types,
|
||||
we decided to use pointer receivers for all `Record` methods.
|
||||
|
||||
### Add XYZ method to Logger
|
||||
|
||||
The `Logger` does not have methods like `SetSeverity`, etc.
|
||||
as the Logs API needs to follow (be compliant with)
|
||||
the [specification](https://opentelemetry.io/docs/specs/otel/logs/api/)
|
||||
|
||||
[^1]: [Handle structured body and attributes](https://github.com/pellared/opentelemetry-go/pull/7)
|
||||
[^2]: Jonathan Amsterdam, [The Go Blog: Structured Logging with slog](https://go.dev/blog/slog)
|
||||
[^3]: Jonathan Amsterdam, [GopherCon Europe 2023: A Fast Structured Logging Package](https://www.youtube.com/watch?v=tC4Jt3i62ns)
|
||||
[^4]: [Emit definition discussion with benchmarks](https://github.com/open-telemetry/opentelemetry-go/pull/4725#discussion_r1400869566)
|
||||
[^5]: [Logger.WithAttributes analysis](https://github.com/pellared/opentelemetry-go/pull/3)
|
||||
[^6]: [Record attributes as field and use sync.Pool for reducing allocations](https://github.com/pellared/opentelemetry-go/pull/4) and [Record attributes based on slog.Record](https://github.com/pellared/opentelemetry-go/pull/6)
|
||||
[^7]: [Record.Body as any](https://github.com/pellared/opentelemetry-go/pull/5)
|
||||
[^8]: [log/slog: structured, leveled logging](https://github.com/golang/go/issues/56345#issuecomment-1302563756)
|
||||
[^9]: [Record with pointer receivers only](https://github.com/pellared/opentelemetry-go/pull/8)
|
||||
[^10]: [Go FAQ: Stack or heap](https://go.dev/doc/faq#stack_or_heap)
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# Log API
|
||||
|
||||
[](https://pkg.go.dev/go.opentelemetry.io/otel/log)
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/*
|
||||
Package log provides the OpenTelemetry Logs API.
|
||||
|
||||
This API is separate from its implementation, so the instrumentation built
|
||||
from it is reusable. See [go.opentelemetry.io/otel/sdk/log] for the official
|
||||
OpenTelemetry implementation of this API.
|
||||
|
||||
The log package provides the OpenTelemetry Logs API, which serves as a standard
|
||||
interface for generating and managing log records within the OpenTelemetry ecosystem.
|
||||
This package allows users to emit LogRecords, enabling structured, context-rich logging
|
||||
that can be easily integrated with observability tools. It ensures that log data is captured
|
||||
in a way that is consistent with OpenTelemetry's data model.
|
||||
|
||||
This package can be used to create bridges between existing logging libraries and OpenTelemetry.
|
||||
Log bridges allow existing logging setups to be integrated with OpenTelemetry.
|
||||
Log bridges can be found in the [registry].
|
||||
|
||||
# API Implementations
|
||||
|
||||
This package does not conform to the standard Go versioning policy; all of its
|
||||
interfaces may have methods added to them without a package major version bump.
|
||||
This non-standard API evolution could surprise an uninformed implementation
|
||||
author. They could unknowingly build their implementation in a way that would
|
||||
result in a runtime panic for their users who update to the new API.
|
||||
|
||||
The API is designed to help inform an instrumentation author about this
|
||||
non-standard API evolution. It requires them to choose a default behavior for
|
||||
unimplemented interface methods. There are three behavior choices they can
|
||||
make:
|
||||
|
||||
- Compilation failure
|
||||
- Panic
|
||||
- Default to another implementation
|
||||
|
||||
All interfaces in this API embed a corresponding interface from
|
||||
[go.opentelemetry.io/otel/log/embedded]. If an author wants the default
|
||||
behavior of their implementations to be a compilation failure, signaling to
|
||||
their users that they need to update to the latest version of that implementation,
|
||||
they need to embed the corresponding interface from
|
||||
[go.opentelemetry.io/otel/log/embedded] in their implementation. For example,
|
||||
|
||||
import "go.opentelemetry.io/otel/log/embedded"
|
||||
|
||||
type LoggerProvider struct {
|
||||
embedded.LoggerProvider
|
||||
// ...
|
||||
}
|
||||
|
||||
If an author wants the default behavior of their implementations to panic,
|
||||
they need to embed the API interface directly.
|
||||
|
||||
import "go.opentelemetry.io/otel/log"
|
||||
|
||||
type LoggerProvider struct {
|
||||
log.LoggerProvider
|
||||
// ...
|
||||
}
|
||||
|
||||
This is not a recommended behavior as it could lead to publishing packages that
|
||||
contain runtime panics when users update other packages that use newer versions
|
||||
of [go.opentelemetry.io/otel/log].
|
||||
|
||||
Finally, an author can embed another implementation in theirs. The embedded
|
||||
implementation will be used for methods not defined by the author. For example,
|
||||
an author who wants to default to silently dropping the call can use
|
||||
[go.opentelemetry.io/otel/log/noop]:
|
||||
|
||||
import "go.opentelemetry.io/otel/log/noop"
|
||||
|
||||
type LoggerProvider struct {
|
||||
noop.LoggerProvider
|
||||
// ...
|
||||
}
|
||||
|
||||
It is strongly recommended that authors only embed
|
||||
go.opentelemetry.io/otel/log/noop if they choose this default behavior. That
|
||||
implementation is the only one OpenTelemetry authors can guarantee will fully
|
||||
implement all the API interfaces when a user updates their API.
|
||||
|
||||
[registry]: https://opentelemetry.io/ecosystem/registry/?language=go&component=log-bridge
|
||||
*/
|
||||
package log
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# Log Embedded
|
||||
|
||||
[](https://pkg.go.dev/go.opentelemetry.io/otel/log/embedded)
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package embedded provides interfaces embedded within the [OpenTelemetry Logs
|
||||
// Bridge API].
|
||||
//
|
||||
// Implementers of the [OpenTelemetry Logs API] can embed the relevant
|
||||
// type from this package into their implementation directly. Doing so will
|
||||
// result in a compilation error for users when the [OpenTelemetry Logs Bridge
|
||||
// API] is extended (which is something that can happen without a major version
|
||||
// bump of the API package).
|
||||
//
|
||||
// [OpenTelemetry Logs API]: https://pkg.go.dev/go.opentelemetry.io/otel/log
|
||||
package embedded
|
||||
|
||||
// LoggerProvider is embedded in the [Logs API LoggerProvider].
|
||||
//
|
||||
// Embed this interface in your implementation of the [Logs API
|
||||
// LoggerProvider] if you want users to experience a compilation error when the
|
||||
// [Logs Bridge API LoggerProvider] interface is extended (which is something
|
||||
// that can happen without a major version bump of the API package). This error
|
||||
// signals that they need to update to your latest implementation.
|
||||
//
|
||||
// [Logs API LoggerProvider]: https://pkg.go.dev/go.opentelemetry.io/otel/log#LoggerProvider
|
||||
type LoggerProvider interface{ loggerProvider() }
|
||||
|
||||
// Logger is embedded in the [Logs API Logger].
|
||||
//
|
||||
// Embed this interface in your implementation of the [Logs API Logger]
|
||||
// if you want users to experience a compilation error when the [Logs API
|
||||
// Logger] interface is extended (which is something that can happen without a
|
||||
// major version bump of the API package). This error signals that they need to
|
||||
// update to your latest implementation.
|
||||
//
|
||||
// [Logs API Logger]: https://pkg.go.dev/go.opentelemetry.io/otel/log#Logger
|
||||
type Logger interface{ logger() }
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/log/embedded"
|
||||
)
|
||||
|
||||
// Logger emits log records.
|
||||
//
|
||||
// Warning: Methods may be added to this interface in minor releases. See
|
||||
// package documentation on API implementation for information on how to set
|
||||
// the default behavior for unimplemented methods.
|
||||
type Logger interface {
|
||||
// Users of the interface can ignore this. This embedded type is only used
|
||||
// by implementations of this interface. See the "API Implementations"
|
||||
// section of the package documentation for more information.
|
||||
embedded.Logger
|
||||
|
||||
// Emit emits a log record.
|
||||
//
|
||||
// The record may be held by the implementation. Callers should not mutate
|
||||
// the record after it is passed.
|
||||
//
|
||||
// Implementations of this method need to be safe for a user to call
|
||||
// concurrently.
|
||||
Emit(ctx context.Context, record Record)
|
||||
|
||||
// Enabled reports whether the Logger emits for the given context and
|
||||
// param.
|
||||
//
|
||||
// Calling Enabled is optional. It is not required before calling
|
||||
// [Logger.Emit].
|
||||
//
|
||||
// Enabled is useful when constructing a [Record] is expensive. A caller can
|
||||
// call Enabled first and skip record construction when it returns false.
|
||||
// When constructing the Record is inexpensive, a caller can emit it
|
||||
// directly.
|
||||
//
|
||||
// The returned value is not static and may change over time. A cached value
|
||||
// can become stale.
|
||||
//
|
||||
// The passed param is likely to contain only partial information about a
|
||||
// record (e.g., a param with only the Severity set).
|
||||
// If a Logger needs more information than is provided, it
|
||||
// is said to be in an indeterminate state (see below).
|
||||
//
|
||||
// The returned value is true if the Logger emits for the provided context
|
||||
// and param, and false if the Logger does not emit. The returned value may be
|
||||
// true or false in an indeterminate state.
|
||||
// An implementation should default to returning true for an indeterminate
|
||||
// state, but may return false if there are valid reasons to do so in
|
||||
// particular circumstances (e.g., performance or correctness).
|
||||
//
|
||||
// The param should not be held by the implementation. A copy should be
|
||||
// made if the param needs to be held after the call returns.
|
||||
//
|
||||
// Implementations of this method need to be safe for a user to call
|
||||
// concurrently.
|
||||
Enabled(ctx context.Context, param EnabledParameters) bool
|
||||
}
|
||||
|
||||
// LoggerOption applies configuration options to a [Logger].
|
||||
type LoggerOption interface {
|
||||
// applyLogger applies a LoggerOption to a LoggerConfig.
|
||||
applyLogger(LoggerConfig) LoggerConfig
|
||||
}
|
||||
|
||||
// LoggerConfig contains options for a [Logger].
|
||||
type LoggerConfig struct {
|
||||
// Ensure forward compatibility by explicitly making this not comparable.
|
||||
noCmp [0]func() //nolint: unused // This is indeed used.
|
||||
|
||||
version string
|
||||
schemaURL string
|
||||
attrs attribute.Set
|
||||
}
|
||||
|
||||
type experimentalOption interface {
|
||||
Experimental()
|
||||
}
|
||||
|
||||
// NewLoggerConfig returns a new [LoggerConfig] with all the options applied.
|
||||
func NewLoggerConfig(options ...LoggerOption) LoggerConfig {
|
||||
var c LoggerConfig
|
||||
for _, opt := range options {
|
||||
if _, ok := opt.(experimentalOption); ok {
|
||||
continue
|
||||
}
|
||||
c = opt.applyLogger(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// InstrumentationVersion returns the version of the library providing
|
||||
// instrumentation.
|
||||
func (cfg LoggerConfig) InstrumentationVersion() string {
|
||||
return cfg.version
|
||||
}
|
||||
|
||||
// InstrumentationAttributes returns the attributes associated with the library
|
||||
// providing instrumentation.
|
||||
func (cfg LoggerConfig) InstrumentationAttributes() attribute.Set {
|
||||
return cfg.attrs
|
||||
}
|
||||
|
||||
// SchemaURL returns the schema URL of the library providing instrumentation.
|
||||
func (cfg LoggerConfig) SchemaURL() string {
|
||||
return cfg.schemaURL
|
||||
}
|
||||
|
||||
type loggerOptionFunc func(LoggerConfig) LoggerConfig
|
||||
|
||||
func (fn loggerOptionFunc) applyLogger(cfg LoggerConfig) LoggerConfig {
|
||||
return fn(cfg)
|
||||
}
|
||||
|
||||
// WithInstrumentationVersion returns a [LoggerOption] that sets the
|
||||
// instrumentation version of a [Logger].
|
||||
func WithInstrumentationVersion(version string) LoggerOption {
|
||||
return loggerOptionFunc(func(config LoggerConfig) LoggerConfig {
|
||||
config.version = version
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
// mergeSets returns the union of the keys in a and b. For duplicate keys, the
|
||||
// value associated with b is used.
|
||||
func mergeSets(a, b attribute.Set) attribute.Set {
|
||||
// NewMergeIterator uses the first value for any duplicates.
|
||||
iter := attribute.NewMergeIterator(&b, &a)
|
||||
merged := make([]attribute.KeyValue, 0, a.Len()+b.Len())
|
||||
for iter.Next() {
|
||||
merged = append(merged, iter.Attribute())
|
||||
}
|
||||
return attribute.NewSet(merged...)
|
||||
}
|
||||
|
||||
// WithInstrumentationAttributes returns a [LoggerOption] that sets the
|
||||
// instrumentation attributes of a [Logger].
|
||||
//
|
||||
// This is equivalent to calling WithInstrumentationAttributeSet with an
|
||||
// [attribute.Set] created from a clone of the passed attributes.
|
||||
// [WithInstrumentationAttributeSet] is recommended for more control.
|
||||
//
|
||||
// If multiple [WithInstrumentationAttributes] or [WithInstrumentationAttributeSet]
|
||||
// options are passed, the attributes will be merged together in the order
|
||||
// they are passed. For attributes with duplicate keys, the last value passed
|
||||
// will be used.
|
||||
func WithInstrumentationAttributes(attr ...attribute.KeyValue) LoggerOption {
|
||||
set := attribute.NewSet(slices.Clone(attr)...)
|
||||
return WithInstrumentationAttributeSet(set)
|
||||
}
|
||||
|
||||
// WithInstrumentationAttributeSet returns a [LoggerOption] that adds
|
||||
// instrumentation attributes to a [Logger].
|
||||
//
|
||||
// If multiple [WithInstrumentationAttributes] or [WithInstrumentationAttributeSet]
|
||||
// options are passed, the attributes will be merged together in the order
|
||||
// they are passed. For attributes with duplicate keys, the last value passed
|
||||
// will be used.
|
||||
func WithInstrumentationAttributeSet(set attribute.Set) LoggerOption {
|
||||
if set.Len() == 0 {
|
||||
return loggerOptionFunc(func(config LoggerConfig) LoggerConfig {
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
return loggerOptionFunc(func(config LoggerConfig) LoggerConfig {
|
||||
if config.attrs.Len() == 0 {
|
||||
config.attrs = set
|
||||
} else {
|
||||
config.attrs = mergeSets(config.attrs, set)
|
||||
}
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
// WithSchemaURL returns a [LoggerOption] that sets the schema URL for a
|
||||
// [Logger].
|
||||
func WithSchemaURL(schemaURL string) LoggerOption {
|
||||
return loggerOptionFunc(func(config LoggerConfig) LoggerConfig {
|
||||
config.schemaURL = schemaURL
|
||||
return config
|
||||
})
|
||||
}
|
||||
|
||||
// EnabledParameters represents the payload for [Logger.Enabled].
|
||||
type EnabledParameters struct {
|
||||
Severity Severity
|
||||
EventName string
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package log
|
||||
|
||||
import "go.opentelemetry.io/otel/log/embedded"
|
||||
|
||||
// LoggerProvider provides access to [Logger].
|
||||
//
|
||||
// Warning: Methods may be added to this interface in minor releases. See
|
||||
// package documentation on API implementation for information on how to set
|
||||
// the default behavior for unimplemented methods.
|
||||
type LoggerProvider interface {
|
||||
// Users of the interface can ignore this. This embedded type is only used
|
||||
// by implementations of this interface. See the "API Implementations"
|
||||
// section of the package documentation for more information.
|
||||
embedded.LoggerProvider
|
||||
|
||||
// Logger returns a new [Logger] with the provided name and configuration.
|
||||
//
|
||||
// The name needs to uniquely identify the source of logged code. It is
|
||||
// recommended that name be the Go package name of the library using a log
|
||||
// bridge (note: this is not the name of the bridge package). Most
|
||||
// commonly, this means a bridge will need to accept this value from its
|
||||
// users.
|
||||
//
|
||||
// An empty name is invalid. Implementations should retain the empty value as the
|
||||
// instrumentation scope name, return a working Logger, and report the invalid value.
|
||||
//
|
||||
// The version of the package using a bridge can be critical information
|
||||
// to include when logging. The bridge should accept this version
|
||||
// information and use the [WithInstrumentationVersion] option to configure
|
||||
// the Logger appropriately.
|
||||
//
|
||||
// Implementations of this method need to be safe for a user to call
|
||||
// concurrently.
|
||||
Logger(name string, options ...LoggerOption) Logger
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
// attributesInlineCount is the number of attributes that are efficiently
|
||||
// stored in an array within a Record. This value is borrowed from slog, which
|
||||
// performed a quantitative survey of log library use and found that this value
|
||||
// covers 95% of all use cases (https://go.dev/blog/slog#performance).
|
||||
const attributesInlineCount = 5
|
||||
|
||||
// Record represents a log record.
|
||||
// A log record with a non-empty event name is interpreted as an event record.
|
||||
type Record struct {
|
||||
// Ensure forward compatibility by explicitly making this not comparable.
|
||||
noCmp [0]func() //nolint: unused // This is indeed used.
|
||||
|
||||
eventName string
|
||||
timestamp time.Time
|
||||
observedTimestamp time.Time
|
||||
severity Severity
|
||||
severityText string
|
||||
body attribute.Value
|
||||
err error
|
||||
|
||||
// The fields below are for optimizing the implementation of Attributes and
|
||||
// AddAttributes. This design is borrowed from the slog Record type:
|
||||
// https://cs.opensource.google/go/go/+/refs/tags/go1.22.0:src/log/slog/record.go;l=20
|
||||
|
||||
// Allocation optimization: an inline array sized to hold
|
||||
// the majority of log calls (based on examination of open-source
|
||||
// code). It holds the start of the list of attributes.
|
||||
front [attributesInlineCount]attribute.KeyValue
|
||||
|
||||
// The number of attributes in front.
|
||||
nFront int
|
||||
|
||||
// The list of attributes except for those in front.
|
||||
// Invariants:
|
||||
// - len(back) > 0 if nFront == len(front)
|
||||
// - Unused array elements are zeroed to detect mistakes.
|
||||
back []attribute.KeyValue
|
||||
}
|
||||
|
||||
// EventName returns the event name.
|
||||
// A log record with a non-empty event name is interpreted as an event record.
|
||||
func (r *Record) EventName() string {
|
||||
return r.eventName
|
||||
}
|
||||
|
||||
// SetEventName sets the event name.
|
||||
// A log record with a non-empty event name is interpreted as an event record.
|
||||
// Event names should uniquely identify the structure of the event's attributes
|
||||
// and body.
|
||||
func (r *Record) SetEventName(s string) {
|
||||
r.eventName = s
|
||||
}
|
||||
|
||||
// Timestamp returns the time when the log record occurred.
|
||||
func (r *Record) Timestamp() time.Time {
|
||||
return r.timestamp
|
||||
}
|
||||
|
||||
// SetTimestamp sets the time when the log record occurred.
|
||||
func (r *Record) SetTimestamp(t time.Time) {
|
||||
r.timestamp = t
|
||||
}
|
||||
|
||||
// ObservedTimestamp returns the time when the log record was observed.
|
||||
func (r *Record) ObservedTimestamp() time.Time {
|
||||
return r.observedTimestamp
|
||||
}
|
||||
|
||||
// SetObservedTimestamp sets the time when the log record was observed.
|
||||
func (r *Record) SetObservedTimestamp(t time.Time) {
|
||||
r.observedTimestamp = t
|
||||
}
|
||||
|
||||
// Severity returns the [Severity] of the log record.
|
||||
func (r *Record) Severity() Severity {
|
||||
return r.severity
|
||||
}
|
||||
|
||||
// SetSeverity sets the [Severity] level of the log record.
|
||||
func (r *Record) SetSeverity(level Severity) {
|
||||
r.severity = level
|
||||
}
|
||||
|
||||
// SeverityText returns the text of the severity (also known as the log level)
|
||||
// as originally known at the source.
|
||||
func (r *Record) SeverityText() string {
|
||||
return r.severityText
|
||||
}
|
||||
|
||||
// SetSeverityText sets the text of the severity (also known as the log level)
|
||||
// as originally known at the source.
|
||||
func (r *Record) SetSeverityText(text string) {
|
||||
r.severityText = text
|
||||
}
|
||||
|
||||
// Body returns the body of the log record.
|
||||
func (r *Record) Body() attribute.Value {
|
||||
return r.body
|
||||
}
|
||||
|
||||
// SetBody sets the body of the log record.
|
||||
func (r *Record) SetBody(v attribute.Value) {
|
||||
r.body = v
|
||||
}
|
||||
|
||||
// Err returns the associated error if one has been set.
|
||||
func (r *Record) Err() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
// SetErr sets the associated error. Passing nil clears the error.
|
||||
func (r *Record) SetErr(err error) {
|
||||
r.err = err
|
||||
}
|
||||
|
||||
// WalkAttributes walks all attributes in the log record by calling f for each
|
||||
// [attribute.KeyValue] in the [Record]. Iteration stops if f returns false.
|
||||
func (r *Record) WalkAttributes(f func(attribute.KeyValue) bool) {
|
||||
for i := 0; i < r.nFront; i++ {
|
||||
if !f(r.front[i]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, a := range r.back {
|
||||
if !f(a) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddAttributes adds attributes to the log record.
|
||||
func (r *Record) AddAttributes(attrs ...attribute.KeyValue) {
|
||||
var i int
|
||||
for i = 0; i < len(attrs) && r.nFront < len(r.front); i++ {
|
||||
a := attrs[i]
|
||||
r.front[r.nFront] = a
|
||||
r.nFront++
|
||||
}
|
||||
|
||||
r.back = slices.Grow(r.back, len(attrs[i:]))
|
||||
r.back = append(r.back, attrs[i:]...)
|
||||
}
|
||||
|
||||
// AttributesLen returns the number of attributes in the log record.
|
||||
func (r *Record) AttributesLen() int {
|
||||
return r.nFront + len(r.back)
|
||||
}
|
||||
|
||||
// Clone returns a copy of the record with no shared state.
|
||||
// The original record and the clone can both be modified without interfering
|
||||
// with each other.
|
||||
func (r *Record) Clone() Record {
|
||||
res := *r
|
||||
res.back = slices.Clone(r.back)
|
||||
return res
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:generate stringer -type=Severity -linecomment
|
||||
|
||||
package log
|
||||
|
||||
// Severity represents a log record's severity (also known as its log level).
|
||||
// Smaller numerical values correspond to less severe log records (such as
|
||||
// debug events); larger numerical values correspond to more severe log records
|
||||
// (such as errors and critical events).
|
||||
type Severity int
|
||||
|
||||
// The following Severity values are defined by OpenTelemetry.
|
||||
const (
|
||||
// SeverityUndefined represents an unset Severity.
|
||||
SeverityUndefined Severity = 0 // UNDEFINED
|
||||
|
||||
// The following are severity values for fine-grained debugging log records.
|
||||
// They are typically disabled in default configurations.
|
||||
SeverityTrace1 Severity = 1 // TRACE
|
||||
SeverityTrace2 Severity = 2 // TRACE2
|
||||
SeverityTrace3 Severity = 3 // TRACE3
|
||||
SeverityTrace4 Severity = 4 // TRACE4
|
||||
|
||||
// The following are severity values for debugging log records.
|
||||
SeverityDebug1 Severity = 5 // DEBUG
|
||||
SeverityDebug2 Severity = 6 // DEBUG2
|
||||
SeverityDebug3 Severity = 7 // DEBUG3
|
||||
SeverityDebug4 Severity = 8 // DEBUG4
|
||||
|
||||
// The following are severity values for informational log records indicating
|
||||
// that an event happened.
|
||||
SeverityInfo1 Severity = 9 // INFO
|
||||
SeverityInfo2 Severity = 10 // INFO2
|
||||
SeverityInfo3 Severity = 11 // INFO3
|
||||
SeverityInfo4 Severity = 12 // INFO4
|
||||
|
||||
// The following are severity values for warning log records. These records
|
||||
// are not errors, but they are likely more important than informational
|
||||
// events.
|
||||
SeverityWarn1 Severity = 13 // WARN
|
||||
SeverityWarn2 Severity = 14 // WARN2
|
||||
SeverityWarn3 Severity = 15 // WARN3
|
||||
SeverityWarn4 Severity = 16 // WARN4
|
||||
|
||||
// The following are severity values for error log records indicating that
|
||||
// something went wrong.
|
||||
SeverityError1 Severity = 17 // ERROR
|
||||
SeverityError2 Severity = 18 // ERROR2
|
||||
SeverityError3 Severity = 19 // ERROR3
|
||||
SeverityError4 Severity = 20 // ERROR4
|
||||
|
||||
// The following are severity values for fatal log records, such as those
|
||||
// associated with an application or system crash.
|
||||
SeverityFatal1 Severity = 21 // FATAL
|
||||
SeverityFatal2 Severity = 22 // FATAL2
|
||||
SeverityFatal3 Severity = 23 // FATAL3
|
||||
SeverityFatal4 Severity = 24 // FATAL4
|
||||
|
||||
// The following are convenience definitions for the base severity of each
|
||||
// level.
|
||||
SeverityTrace = SeverityTrace1
|
||||
SeverityDebug = SeverityDebug1
|
||||
SeverityInfo = SeverityInfo1
|
||||
SeverityWarn = SeverityWarn1
|
||||
SeverityError = SeverityError1
|
||||
SeverityFatal = SeverityFatal1
|
||||
)
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
// Code generated by "stringer -type=Severity -linecomment"; DO NOT EDIT.
|
||||
|
||||
package log
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[SeverityUndefined-0]
|
||||
_ = x[SeverityTrace1-1]
|
||||
_ = x[SeverityTrace2-2]
|
||||
_ = x[SeverityTrace3-3]
|
||||
_ = x[SeverityTrace4-4]
|
||||
_ = x[SeverityDebug1-5]
|
||||
_ = x[SeverityDebug2-6]
|
||||
_ = x[SeverityDebug3-7]
|
||||
_ = x[SeverityDebug4-8]
|
||||
_ = x[SeverityInfo1-9]
|
||||
_ = x[SeverityInfo2-10]
|
||||
_ = x[SeverityInfo3-11]
|
||||
_ = x[SeverityInfo4-12]
|
||||
_ = x[SeverityWarn1-13]
|
||||
_ = x[SeverityWarn2-14]
|
||||
_ = x[SeverityWarn3-15]
|
||||
_ = x[SeverityWarn4-16]
|
||||
_ = x[SeverityError1-17]
|
||||
_ = x[SeverityError2-18]
|
||||
_ = x[SeverityError3-19]
|
||||
_ = x[SeverityError4-20]
|
||||
_ = x[SeverityFatal1-21]
|
||||
_ = x[SeverityFatal2-22]
|
||||
_ = x[SeverityFatal3-23]
|
||||
_ = x[SeverityFatal4-24]
|
||||
}
|
||||
|
||||
const _Severity_name = "UNDEFINEDTRACETRACE2TRACE3TRACE4DEBUGDEBUG2DEBUG3DEBUG4INFOINFO2INFO3INFO4WARNWARN2WARN3WARN4ERRORERROR2ERROR3ERROR4FATALFATAL2FATAL3FATAL4"
|
||||
|
||||
var _Severity_index = [...]uint8{0, 9, 14, 20, 26, 32, 37, 43, 49, 55, 59, 64, 69, 74, 78, 83, 88, 93, 98, 104, 110, 116, 121, 127, 133, 139}
|
||||
|
||||
func (i Severity) String() string {
|
||||
idx := int(i) - 0
|
||||
if i < 0 || idx >= len(_Severity_index)-1 {
|
||||
return "Severity(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _Severity_name[_Severity_index[idx]:_Severity_index[idx+1]]
|
||||
}
|
||||
+1
-1
@@ -320,7 +320,7 @@ type Observer interface {
|
||||
ObserveInt64(obsrv Int64Observable, value int64, opts ...ObserveOption)
|
||||
}
|
||||
|
||||
// Registration is an token representing the unique registration of a callback
|
||||
// Registration is a token representing the unique registration of a callback
|
||||
// for a set of instruments with a Meter.
|
||||
//
|
||||
// Warning: Methods may be added to this interface in minor releases. See
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package hextable provides a lookup table for decoding lowercase
|
||||
// hexadecimal.
|
||||
package hextable
|
||||
|
||||
// Rev is a reverse hex lookup table for lowercase hex digits. Valid
|
||||
// hexadecimal characters are indexed at their respective values. All other
|
||||
// characters, including the upper-case A-F disallowed by the W3C trace
|
||||
// context specification, are represented with '\xff'.
|
||||
//
|
||||
// The '\xff' character is used as invalid because no valid character has the
|
||||
// upper 4 bits set. Meaning, an efficient validation can be performed over
|
||||
// multiple character parsing by checking these bits remain zero.
|
||||
const Rev = "" +
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
|
||||
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\xff\xff\xff\xff\xff\xff" +
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
|
||||
"\xff\x0a\x0b\x0c\x0d\x0e\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
|
||||
+12
-15
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel/propagation/internal/hextable"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
@@ -126,27 +127,23 @@ func (TraceContext) extract(carrier TextMapCarrier) trace.SpanContext {
|
||||
return sc
|
||||
}
|
||||
|
||||
// upperHex detect hex is upper case Unicode characters.
|
||||
func upperHex(v string) bool {
|
||||
for _, c := range v {
|
||||
if c >= 'A' && c <= 'F' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func extractPart(dst []byte, h *string, n int) bool {
|
||||
part, left, _ := strings.Cut(*h, delimiter)
|
||||
*h = left
|
||||
// hex.Decode decodes unsupported upper-case characters, so exclude explicitly.
|
||||
if len(part) != n || upperHex(part) {
|
||||
if len(part) != n {
|
||||
return false
|
||||
}
|
||||
if p, err := hex.Decode(dst, []byte(part)); err != nil || p != n/2 {
|
||||
return false
|
||||
// hextable.Rev maps every invalid character to 0xff, including the
|
||||
// upper-case A-F the specification disallows. OR-ing every looked-up value
|
||||
// together lets a single check detect any invalid character, because no
|
||||
// valid value has the upper 4 bits set.
|
||||
invalidMark := byte(0)
|
||||
for i := 0; i < n; i += 2 {
|
||||
hi, lo := hextable.Rev[part[i]], hextable.Rev[part[i+1]]
|
||||
dst[i/2] = (hi << 4) | lo
|
||||
invalidMark |= hi | lo
|
||||
}
|
||||
return true
|
||||
return invalidMark&0xf0 == 0
|
||||
}
|
||||
|
||||
// Fields returns the keys who's values are set with Inject.
|
||||
|
||||
+524
-22
@@ -28,40 +28,142 @@ type rawValue struct {
|
||||
slice any
|
||||
}
|
||||
|
||||
// Value returns value with all map values deduplicated and whether it changed.
|
||||
type valueChanges uint8
|
||||
|
||||
const (
|
||||
depthLimitChanged valueChanges = 1 << iota
|
||||
deduplicationChanged
|
||||
)
|
||||
|
||||
// ValueDedup returns value with all map values deduplicated and whether
|
||||
// it changed.
|
||||
//
|
||||
// Duplicate map keys are resolved using last-value-wins semantics.
|
||||
func Value(value attribute.Value) (attribute.Value, bool) {
|
||||
func ValueDedup(value attribute.Value) (attribute.Value, bool) {
|
||||
switch value.Type() {
|
||||
case attribute.SLICE:
|
||||
return deduplicateSliceValue(value)
|
||||
return sliceValueDedup(value)
|
||||
case attribute.MAP:
|
||||
return deduplicateMapValue(value)
|
||||
return mapValueDedup(value)
|
||||
default:
|
||||
return value, false
|
||||
}
|
||||
}
|
||||
|
||||
// KeyValue returns kv with all map values deduplicated and whether it changed.
|
||||
func KeyValue(kv attribute.KeyValue) (attribute.KeyValue, bool) {
|
||||
value, changed := Value(kv.Value)
|
||||
// ValueDedupLimitDepth returns value with all map values
|
||||
// deduplicated and all array and map values limited to depthLimit. The boolean
|
||||
// results report depth limiting and deduplication changes, respectively.
|
||||
//
|
||||
// Duplicate map keys are resolved using last-value-wins semantics. Depth
|
||||
// starts at one for value and increments when descending into an array element
|
||||
// or map value. An array or map beyond a non-negative depthLimit is replaced by
|
||||
// an empty value. A negative depthLimit disables depth limiting.
|
||||
func ValueDedupLimitDepth(
|
||||
value attribute.Value,
|
||||
depthLimit int,
|
||||
) (attribute.Value, bool, bool) {
|
||||
if depthLimit < 0 {
|
||||
value, deduplicated := ValueDedup(value)
|
||||
return value, false, deduplicated
|
||||
}
|
||||
|
||||
// Keep the common scalar and primitive-array cases in this wrapper. Routing
|
||||
// them through the recursive, multi-result transform adds measurable
|
||||
// overhead to the dominant simple attribute path.
|
||||
switch value.Type() {
|
||||
case attribute.BOOLSLICE,
|
||||
attribute.INT64SLICE,
|
||||
attribute.FLOAT64SLICE,
|
||||
attribute.STRINGSLICE,
|
||||
attribute.BYTESLICE:
|
||||
if depthLimit == 0 {
|
||||
return attribute.Value{}, true, false
|
||||
}
|
||||
return value, false, false
|
||||
case attribute.SLICE, attribute.MAP:
|
||||
default:
|
||||
return value, false, false
|
||||
}
|
||||
|
||||
value, changes := valueDedupLimitDepth(value, depthLimit)
|
||||
return value, changes&depthLimitChanged != 0, changes&deduplicationChanged != 0
|
||||
}
|
||||
|
||||
// ValueLimitDepth returns value with all array and map values limited to
|
||||
// depthLimit. Map keys are not deduplicated.
|
||||
//
|
||||
// Depth starts at one for value and increments when descending into an array
|
||||
// element or map value. An array or map beyond a non-negative depthLimit is
|
||||
// replaced by an empty value. A negative depthLimit disables depth limiting.
|
||||
func ValueLimitDepth(value attribute.Value, depthLimit int) (attribute.Value, bool) {
|
||||
if depthLimit < 0 {
|
||||
return value, false
|
||||
}
|
||||
|
||||
switch value.Type() {
|
||||
case attribute.BOOLSLICE,
|
||||
attribute.INT64SLICE,
|
||||
attribute.FLOAT64SLICE,
|
||||
attribute.STRINGSLICE,
|
||||
attribute.BYTESLICE:
|
||||
if depthLimit == 0 {
|
||||
return attribute.Value{}, true
|
||||
}
|
||||
return value, false
|
||||
case attribute.SLICE, attribute.MAP:
|
||||
default:
|
||||
return value, false
|
||||
}
|
||||
|
||||
return valueLimitDepth(value, depthLimit)
|
||||
}
|
||||
|
||||
// KeyValueDedup returns kv with all map values deduplicated and whether
|
||||
// it changed.
|
||||
func KeyValueDedup(kv attribute.KeyValue) (attribute.KeyValue, bool) {
|
||||
value, changed := ValueDedup(kv.Value)
|
||||
if changed {
|
||||
kv.Value = value
|
||||
}
|
||||
return kv, changed
|
||||
}
|
||||
|
||||
// KeyValues returns kvs with all map values deduplicated and whether they changed.
|
||||
// KeyValueDedupLimitDepth returns kv with all map values
|
||||
// deduplicated and all array and map values limited to depthLimit. The boolean
|
||||
// results report depth limiting and deduplication changes, respectively.
|
||||
func KeyValueDedupLimitDepth(
|
||||
kv attribute.KeyValue,
|
||||
depthLimit int,
|
||||
) (attribute.KeyValue, bool, bool) {
|
||||
value, depthLimited, deduplicated := ValueDedupLimitDepth(kv.Value, depthLimit)
|
||||
if depthLimited || deduplicated {
|
||||
kv.Value = value
|
||||
}
|
||||
return kv, depthLimited, deduplicated
|
||||
}
|
||||
|
||||
// KeyValueLimitDepth returns kv with all array and map values limited to
|
||||
// depthLimit. Map keys are not deduplicated.
|
||||
func KeyValueLimitDepth(kv attribute.KeyValue, depthLimit int) (attribute.KeyValue, bool) {
|
||||
value, changed := ValueLimitDepth(kv.Value, depthLimit)
|
||||
if changed {
|
||||
kv.Value = value
|
||||
}
|
||||
return kv, changed
|
||||
}
|
||||
|
||||
// KeyValuesDedup returns kvs with all map values deduplicated and whether
|
||||
// they changed.
|
||||
//
|
||||
// The returned slice is the original kvs slice if no value needs
|
||||
// deduplication. Top-level keys in kvs are not deduplicated.
|
||||
func KeyValues(kvs []attribute.KeyValue) ([]attribute.KeyValue, bool) {
|
||||
func KeyValuesDedup(kvs []attribute.KeyValue) ([]attribute.KeyValue, bool) {
|
||||
// Preserve the caller's slice on the common no-op path. Once a changed
|
||||
// value is found, copy the prior values exactly once and fill the rest in
|
||||
// place as the scan continues.
|
||||
var normalized []attribute.KeyValue
|
||||
for i, kv := range kvs {
|
||||
kv, changed := KeyValue(kv)
|
||||
kv, changed := KeyValueDedup(kv)
|
||||
if normalized != nil {
|
||||
normalized[i] = kv
|
||||
continue
|
||||
@@ -80,22 +182,67 @@ func KeyValues(kvs []attribute.KeyValue) ([]attribute.KeyValue, bool) {
|
||||
return normalized, true
|
||||
}
|
||||
|
||||
// Set returns set with all map values deduplicated and whether it changed.
|
||||
// KeyValuesDedupLimitDepth returns kvs with all map values
|
||||
// deduplicated and all array and map values limited to depthLimit. The boolean
|
||||
// results report depth limiting and deduplication changes, respectively.
|
||||
//
|
||||
// The returned slice is the original kvs slice if no value needs
|
||||
// normalization. Top-level keys in kvs are not deduplicated.
|
||||
func KeyValuesDedupLimitDepth(
|
||||
kvs []attribute.KeyValue,
|
||||
depthLimit int,
|
||||
) ([]attribute.KeyValue, bool, bool) {
|
||||
if depthLimit < 0 {
|
||||
kvs, deduplicated := KeyValuesDedup(kvs)
|
||||
return kvs, false, deduplicated
|
||||
}
|
||||
|
||||
var normalized []attribute.KeyValue
|
||||
var allChanges valueChanges
|
||||
for i, kv := range kvs {
|
||||
value, changes := valueDedupLimitDepth(kv.Value, depthLimit)
|
||||
allChanges |= changes
|
||||
if changes != 0 {
|
||||
kv.Value = value
|
||||
}
|
||||
if normalized != nil {
|
||||
normalized[i] = kv
|
||||
continue
|
||||
}
|
||||
if changes == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.KeyValue, len(kvs))
|
||||
copy(normalized, kvs[:i])
|
||||
normalized[i] = kv
|
||||
}
|
||||
if normalized == nil {
|
||||
return kvs, false, false
|
||||
}
|
||||
return normalized,
|
||||
allChanges&depthLimitChanged != 0,
|
||||
allChanges&deduplicationChanged != 0
|
||||
}
|
||||
|
||||
// SetDedup returns set with all map values deduplicated and whether it
|
||||
// changed.
|
||||
//
|
||||
// The returned Set is the original set if no value needs deduplication.
|
||||
// Top-level key uniqueness remains attribute.Set's responsibility; this only
|
||||
// normalizes map attribute values.
|
||||
func Set(set attribute.Set) (attribute.Set, bool) {
|
||||
if set.Len() == 0 {
|
||||
func SetDedup(set attribute.Set) (attribute.Set, bool) {
|
||||
length := set.Len()
|
||||
if length == 0 {
|
||||
return set, false
|
||||
}
|
||||
|
||||
// Most attribute sets contain no duplicate map keys. Delay allocation until
|
||||
// the first changed value so the no-op path returns the original Set.
|
||||
var normalized []attribute.KeyValue
|
||||
for i := range set.Len() {
|
||||
for i := range length {
|
||||
kv, _ := set.Get(i)
|
||||
kv, changed := KeyValue(kv)
|
||||
kv, changed := KeyValueDedup(kv)
|
||||
if normalized != nil {
|
||||
normalized = append(normalized, kv)
|
||||
continue
|
||||
@@ -104,7 +251,7 @@ func Set(set attribute.Set) (attribute.Set, bool) {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.KeyValue, 0, set.Len())
|
||||
normalized = make([]attribute.KeyValue, 0, length)
|
||||
for j := range i {
|
||||
prior, _ := set.Get(j)
|
||||
normalized = append(normalized, prior)
|
||||
@@ -118,7 +265,359 @@ func Set(set attribute.Set) (attribute.Set, bool) {
|
||||
return attribute.NewSet(normalized...), true
|
||||
}
|
||||
|
||||
func deduplicateSliceValue(value attribute.Value) (attribute.Value, bool) {
|
||||
// SetDedupLimitDepth returns set with all map values deduplicated and
|
||||
// all array and map values limited to depthLimit. The boolean results report
|
||||
// depth limiting and deduplication changes, respectively.
|
||||
//
|
||||
// The returned Set is the original set if no value needs normalization.
|
||||
// Top-level key uniqueness remains attribute.Set's responsibility; this only
|
||||
// normalizes attribute values.
|
||||
func SetDedupLimitDepth(
|
||||
set attribute.Set,
|
||||
depthLimit int,
|
||||
) (attribute.Set, bool, bool) {
|
||||
if depthLimit < 0 {
|
||||
set, deduplicated := SetDedup(set)
|
||||
return set, false, deduplicated
|
||||
}
|
||||
length := set.Len()
|
||||
if length == 0 {
|
||||
return set, false, false
|
||||
}
|
||||
|
||||
var normalized []attribute.KeyValue
|
||||
var allChanges valueChanges
|
||||
for i := range length {
|
||||
kv, _ := set.Get(i)
|
||||
value, changes := valueDedupLimitDepth(kv.Value, depthLimit)
|
||||
allChanges |= changes
|
||||
if changes != 0 {
|
||||
kv.Value = value
|
||||
}
|
||||
if normalized != nil {
|
||||
normalized = append(normalized, kv)
|
||||
continue
|
||||
}
|
||||
if changes == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.KeyValue, 0, length)
|
||||
for j := range i {
|
||||
prior, _ := set.Get(j)
|
||||
normalized = append(normalized, prior)
|
||||
}
|
||||
normalized = append(normalized, kv)
|
||||
}
|
||||
if normalized == nil {
|
||||
return set, false, false
|
||||
}
|
||||
return attribute.NewSet(normalized...),
|
||||
allChanges&depthLimitChanged != 0,
|
||||
allChanges&deduplicationChanged != 0
|
||||
}
|
||||
|
||||
// SetLimitDepth returns set with all array and map values limited to
|
||||
// depthLimit. Map keys are not deduplicated.
|
||||
//
|
||||
// The returned Set is the original set if no value changes.
|
||||
func SetLimitDepth(set attribute.Set, depthLimit int) (attribute.Set, bool) {
|
||||
if depthLimit < 0 {
|
||||
return set, false
|
||||
}
|
||||
length := set.Len()
|
||||
if length == 0 {
|
||||
return set, false
|
||||
}
|
||||
|
||||
var normalized []attribute.KeyValue
|
||||
for i := range length {
|
||||
kv, _ := set.Get(i)
|
||||
value, changed := valueLimitDepth(kv.Value, depthLimit)
|
||||
if changed {
|
||||
kv.Value = value
|
||||
}
|
||||
if normalized != nil {
|
||||
normalized = append(normalized, kv)
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.KeyValue, 0, length)
|
||||
for j := range i {
|
||||
prior, _ := set.Get(j)
|
||||
normalized = append(normalized, prior)
|
||||
}
|
||||
normalized = append(normalized, kv)
|
||||
}
|
||||
if normalized == nil {
|
||||
return set, false
|
||||
}
|
||||
return attribute.NewSet(normalized...), true
|
||||
}
|
||||
|
||||
// valueDedupLimitDepth combines depth limiting with recursive map
|
||||
// deduplication. remainingDepth is non-negative. It checks the remaining
|
||||
// budget before reading collection storage and lazily rebuilds only changed
|
||||
// ancestry.
|
||||
func valueDedupLimitDepth(
|
||||
value attribute.Value,
|
||||
remainingDepth int,
|
||||
) (attribute.Value, valueChanges) {
|
||||
switch value.Type() {
|
||||
case attribute.BOOLSLICE,
|
||||
attribute.INT64SLICE,
|
||||
attribute.FLOAT64SLICE,
|
||||
attribute.STRINGSLICE,
|
||||
attribute.BYTESLICE:
|
||||
if remainingDepth == 0 {
|
||||
return attribute.Value{}, depthLimitChanged
|
||||
}
|
||||
case attribute.SLICE:
|
||||
if remainingDepth == 0 {
|
||||
return attribute.Value{}, depthLimitChanged
|
||||
}
|
||||
return sliceValueDedupLimitDepth(value, remainingDepth)
|
||||
case attribute.MAP:
|
||||
if remainingDepth == 0 {
|
||||
return attribute.Value{}, depthLimitChanged
|
||||
}
|
||||
return mapValueDedupLimitDepth(value, remainingDepth)
|
||||
}
|
||||
return value, 0
|
||||
}
|
||||
|
||||
// valueLimitDepth limits collection depth while preserving duplicate map keys.
|
||||
func valueLimitDepth(
|
||||
value attribute.Value,
|
||||
remainingDepth int,
|
||||
) (attribute.Value, bool) {
|
||||
switch value.Type() {
|
||||
case attribute.BOOLSLICE,
|
||||
attribute.INT64SLICE,
|
||||
attribute.FLOAT64SLICE,
|
||||
attribute.STRINGSLICE,
|
||||
attribute.BYTESLICE:
|
||||
if remainingDepth == 0 {
|
||||
return attribute.Value{}, true
|
||||
}
|
||||
case attribute.SLICE:
|
||||
if remainingDepth == 0 {
|
||||
return attribute.Value{}, true
|
||||
}
|
||||
return sliceValueLimitDepth(value, remainingDepth)
|
||||
case attribute.MAP:
|
||||
if remainingDepth == 0 {
|
||||
return attribute.Value{}, true
|
||||
}
|
||||
return mapValueLimitDepth(value, remainingDepth)
|
||||
}
|
||||
return value, false
|
||||
}
|
||||
|
||||
func sliceValueDedupLimitDepth(
|
||||
value attribute.Value,
|
||||
remainingDepth int,
|
||||
) (attribute.Value, valueChanges) {
|
||||
storage := valueStorage(value)
|
||||
length := valueLen(storage)
|
||||
if length <= 1 {
|
||||
if length == 1 {
|
||||
elem := valueAt(storage, 0)
|
||||
newElem, changes := valueDedupLimitDepth(
|
||||
elem,
|
||||
remainingDepth-1,
|
||||
)
|
||||
if changes != 0 {
|
||||
return attribute.SliceValue(newElem), changes
|
||||
}
|
||||
}
|
||||
return value, 0
|
||||
}
|
||||
|
||||
var normalized []attribute.Value
|
||||
var allChanges valueChanges
|
||||
for i := range length {
|
||||
elem := valueAt(storage, i)
|
||||
newElem, changes := valueDedupLimitDepth(
|
||||
elem,
|
||||
remainingDepth-1,
|
||||
)
|
||||
allChanges |= changes
|
||||
if normalized != nil {
|
||||
normalized[i] = newElem
|
||||
continue
|
||||
}
|
||||
if changes == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.Value, length)
|
||||
for j := range i {
|
||||
normalized[j] = valueAt(storage, j)
|
||||
}
|
||||
normalized[i] = newElem
|
||||
}
|
||||
if normalized == nil {
|
||||
return value, 0
|
||||
}
|
||||
return attribute.SliceValue(normalized...), allChanges
|
||||
}
|
||||
|
||||
func sliceValueLimitDepth(
|
||||
value attribute.Value,
|
||||
remainingDepth int,
|
||||
) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
length := valueLen(storage)
|
||||
if length <= 1 {
|
||||
if length == 1 {
|
||||
elem := valueAt(storage, 0)
|
||||
newElem, changed := valueLimitDepth(elem, remainingDepth-1)
|
||||
if changed {
|
||||
return attribute.SliceValue(newElem), true
|
||||
}
|
||||
}
|
||||
return value, false
|
||||
}
|
||||
|
||||
var normalized []attribute.Value
|
||||
for i := range length {
|
||||
elem := valueAt(storage, i)
|
||||
newElem, changed := valueLimitDepth(elem, remainingDepth-1)
|
||||
if normalized != nil {
|
||||
normalized[i] = newElem
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.Value, length)
|
||||
for j := range i {
|
||||
normalized[j] = valueAt(storage, j)
|
||||
}
|
||||
normalized[i] = newElem
|
||||
}
|
||||
if normalized == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.SliceValue(normalized...), true
|
||||
}
|
||||
|
||||
func mapValueDedupLimitDepth(
|
||||
value attribute.Value,
|
||||
remainingDepth int,
|
||||
) (attribute.Value, valueChanges) {
|
||||
storage := valueStorage(value)
|
||||
length := keyValueLen(storage)
|
||||
if length <= 1 {
|
||||
if length == 1 {
|
||||
kv := keyValueAt(storage, 0)
|
||||
newValue, changes := valueDedupLimitDepth(
|
||||
kv.Value,
|
||||
remainingDepth-1,
|
||||
)
|
||||
if changes != 0 {
|
||||
kv.Value = newValue
|
||||
return attribute.MapValue(kv), changes
|
||||
}
|
||||
}
|
||||
return value, 0
|
||||
}
|
||||
|
||||
var normalized []attribute.KeyValue
|
||||
var allChanges valueChanges
|
||||
for i := 0; i < length; {
|
||||
// attribute.MapValue stores key-values sorted by key using a stable
|
||||
// sort. Only the last entry in an equal-key run survives, so avoid
|
||||
// traversing values that will be discarded.
|
||||
first := keyValueAt(storage, i)
|
||||
j := i + 1
|
||||
for j < length && keyValueAt(storage, j).Key == first.Key {
|
||||
j++
|
||||
}
|
||||
|
||||
kv := keyValueAt(storage, j-1)
|
||||
newValue, changes := valueDedupLimitDepth(
|
||||
kv.Value,
|
||||
remainingDepth-1,
|
||||
)
|
||||
if changes != 0 {
|
||||
kv.Value = newValue
|
||||
}
|
||||
if j-i > 1 {
|
||||
changes |= deduplicationChanged
|
||||
}
|
||||
allChanges |= changes
|
||||
if normalized != nil {
|
||||
normalized = append(normalized, kv)
|
||||
} else if changes != 0 {
|
||||
if i == 0 && j == length {
|
||||
return attribute.MapValue(kv), allChanges
|
||||
}
|
||||
normalized = make([]attribute.KeyValue, 0, length-(j-i-1))
|
||||
for k := range i {
|
||||
normalized = append(normalized, keyValueAt(storage, k))
|
||||
}
|
||||
normalized = append(normalized, kv)
|
||||
}
|
||||
i = j
|
||||
}
|
||||
if normalized == nil {
|
||||
return value, 0
|
||||
}
|
||||
return attribute.MapValue(normalized...), allChanges
|
||||
}
|
||||
|
||||
func mapValueLimitDepth(
|
||||
value attribute.Value,
|
||||
remainingDepth int,
|
||||
) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
length := keyValueLen(storage)
|
||||
if length <= 1 {
|
||||
if length == 1 {
|
||||
kv := keyValueAt(storage, 0)
|
||||
newValue, changed := valueLimitDepth(kv.Value, remainingDepth-1)
|
||||
if changed {
|
||||
kv.Value = newValue
|
||||
return attribute.MapValue(kv), true
|
||||
}
|
||||
}
|
||||
return value, false
|
||||
}
|
||||
|
||||
var normalized []attribute.KeyValue
|
||||
for i := range length {
|
||||
kv := keyValueAt(storage, i)
|
||||
newValue, changed := valueLimitDepth(kv.Value, remainingDepth-1)
|
||||
if changed {
|
||||
kv.Value = newValue
|
||||
}
|
||||
if normalized != nil {
|
||||
normalized[i] = kv
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.KeyValue, length)
|
||||
for j := range i {
|
||||
normalized[j] = keyValueAt(storage, j)
|
||||
}
|
||||
normalized[i] = kv
|
||||
}
|
||||
if normalized == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.MapValue(normalized...), true
|
||||
}
|
||||
|
||||
func sliceValueDedup(value attribute.Value) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
length := valueLen(storage)
|
||||
|
||||
@@ -127,7 +626,7 @@ func deduplicateSliceValue(value attribute.Value) (attribute.Value, bool) {
|
||||
var normalized []attribute.Value
|
||||
for i := range length {
|
||||
elem := valueAt(storage, i)
|
||||
elem, changed := Value(elem)
|
||||
elem, changed := ValueDedup(elem)
|
||||
if normalized != nil {
|
||||
normalized[i] = elem
|
||||
continue
|
||||
@@ -148,14 +647,14 @@ func deduplicateSliceValue(value attribute.Value) (attribute.Value, bool) {
|
||||
return attribute.SliceValue(normalized...), true
|
||||
}
|
||||
|
||||
func deduplicateMapValue(value attribute.Value) (attribute.Value, bool) {
|
||||
func mapValueDedup(value attribute.Value) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
length := keyValueLen(storage)
|
||||
if length <= 1 {
|
||||
// A single map entry cannot duplicate its own key, but its value might
|
||||
// contain a map or slice that needs recursive normalization.
|
||||
if length == 1 {
|
||||
kv, changed := KeyValue(keyValueAt(storage, 0))
|
||||
kv, changed := KeyValueDedup(keyValueAt(storage, 0))
|
||||
if changed {
|
||||
return attribute.MapValue(kv), true
|
||||
}
|
||||
@@ -174,13 +673,16 @@ func deduplicateMapValue(value attribute.Value) (attribute.Value, bool) {
|
||||
j++
|
||||
}
|
||||
|
||||
kv, nestedChanged := KeyValue(keyValueAt(storage, j-1))
|
||||
kv, nestedChanged := KeyValueDedup(keyValueAt(storage, j-1))
|
||||
// j-i > 1 means the current key run contained duplicates.
|
||||
changed := nestedChanged || j-i > 1
|
||||
if normalized != nil {
|
||||
normalized = append(normalized, kv)
|
||||
} else if changed {
|
||||
normalized = make([]attribute.KeyValue, 0, length)
|
||||
if i == 0 && j == length {
|
||||
return attribute.MapValue(kv), true
|
||||
}
|
||||
normalized = make([]attribute.KeyValue, 0, length-(j-i-1))
|
||||
for k := range i {
|
||||
normalized = append(normalized, keyValueAt(storage, k))
|
||||
}
|
||||
|
||||
+277
-91
@@ -7,7 +7,7 @@
|
||||
package attrnorm
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -26,43 +26,24 @@ func Truncate(limit int, attr attribute.KeyValue) attribute.KeyValue {
|
||||
if limit < 0 {
|
||||
return attr
|
||||
}
|
||||
|
||||
// Keep scalar cases in this wrapper. Routing them through the recursive,
|
||||
// multi-result transform adds measurable overhead to the dominant simple
|
||||
// attribute path.
|
||||
switch attr.Value.Type() {
|
||||
case attribute.STRING:
|
||||
v := attr.Value.AsString()
|
||||
return attr.Key.String(truncate(limit, v))
|
||||
case attribute.STRINGSLICE:
|
||||
v := attr.Value.AsStringSlice()
|
||||
for i := range v {
|
||||
v[i] = truncate(limit, v[i])
|
||||
}
|
||||
return attr.Key.StringSlice(v)
|
||||
attr.Value = attribute.StringValue(truncate(limit, attr.Value.AsString()))
|
||||
case attribute.BYTESLICE:
|
||||
v := attr.Value.AsString()
|
||||
if len(v) > limit {
|
||||
return attr.Key.ByteSlice([]byte(v[:limit]))
|
||||
s := attr.Value.AsString()
|
||||
if len(s) > limit {
|
||||
attr.Value = attribute.ByteSliceValue([]byte(s[:limit]))
|
||||
}
|
||||
return attr
|
||||
case attribute.STRINGSLICE:
|
||||
attr.Value, _ = truncateStringSliceValue(limit, attr.Value)
|
||||
case attribute.SLICE:
|
||||
v := attr.Value.AsSlice()
|
||||
if !slices.ContainsFunc(v, func(e attribute.Value) bool { return needsTruncation(limit, e) }) {
|
||||
return attr
|
||||
}
|
||||
newV := make([]attribute.Value, len(v))
|
||||
for i, elem := range v {
|
||||
newV[i] = TruncateValue(limit, elem)
|
||||
}
|
||||
return attr.Key.Slice(newV...)
|
||||
attr.Value, _ = truncateSliceValue(limit, attr.Value)
|
||||
case attribute.MAP:
|
||||
v := attr.Value.AsMap()
|
||||
if !slices.ContainsFunc(v, func(kv attribute.KeyValue) bool { return needsTruncation(limit, kv.Value) }) {
|
||||
return attr
|
||||
}
|
||||
newV := make([]attribute.KeyValue, len(v))
|
||||
for i, elem := range v {
|
||||
elem.Value = TruncateValue(limit, elem.Value)
|
||||
newV[i] = elem
|
||||
}
|
||||
return attr.Key.Map(newV...)
|
||||
attr.Value, _ = truncateMapValue(limit, attr.Value)
|
||||
}
|
||||
return attr
|
||||
}
|
||||
@@ -76,85 +57,275 @@ func TruncateValue(limit int, v attribute.Value) attribute.Value {
|
||||
return v
|
||||
}
|
||||
|
||||
// Match Truncate's direct scalar path to avoid paying for the recursive,
|
||||
// multi-result transform when no composite traversal is needed.
|
||||
switch v.Type() {
|
||||
case attribute.STRING:
|
||||
return attribute.StringValue(truncate(limit, v.AsString()))
|
||||
case attribute.STRINGSLICE:
|
||||
ss := v.AsStringSlice()
|
||||
for i := range ss {
|
||||
ss[i] = truncate(limit, ss[i])
|
||||
}
|
||||
return attribute.StringSliceValue(ss)
|
||||
case attribute.BYTESLICE:
|
||||
// len(v.AsString()) is identical to len(v.AsByteSlice()) but
|
||||
// avoids allocating the full slice before truncation.
|
||||
s := v.AsString()
|
||||
if limit >= 0 && len(s) > limit {
|
||||
if len(s) > limit {
|
||||
return attribute.ByteSliceValue([]byte(s[:limit]))
|
||||
}
|
||||
case attribute.STRINGSLICE:
|
||||
v, _ = truncateStringSliceValue(limit, v)
|
||||
case attribute.SLICE:
|
||||
sl := v.AsSlice()
|
||||
if !slices.ContainsFunc(sl, func(e attribute.Value) bool { return needsTruncation(limit, e) }) {
|
||||
return v
|
||||
}
|
||||
newSl := make([]attribute.Value, len(sl))
|
||||
for i, elem := range sl {
|
||||
newSl[i] = TruncateValue(limit, elem)
|
||||
}
|
||||
return attribute.SliceValue(newSl...)
|
||||
v, _ = truncateSliceValue(limit, v)
|
||||
case attribute.MAP:
|
||||
m := v.AsMap()
|
||||
if !slices.ContainsFunc(m, func(kv attribute.KeyValue) bool { return needsTruncation(limit, kv.Value) }) {
|
||||
return v
|
||||
}
|
||||
newM := make([]attribute.KeyValue, len(m))
|
||||
for i, elem := range m {
|
||||
elem.Value = TruncateValue(limit, elem.Value)
|
||||
newM[i] = elem
|
||||
}
|
||||
return attribute.MapValue(newM...)
|
||||
v, _ = truncateMapValue(limit, v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// stringNeedsTruncation reports whether s would be modified by truncate for the
|
||||
// given limit.
|
||||
func stringNeedsTruncation(limit int, s string) bool {
|
||||
if limit < 0 || len(s) <= limit {
|
||||
return false
|
||||
}
|
||||
return utf8.RuneCountInString(s) > limit || !utf8.ValidString(s)
|
||||
}
|
||||
|
||||
// needsTruncation reports whether v would be modified by TruncateValue for the
|
||||
// given limit.
|
||||
func needsTruncation(limit int, v attribute.Value) bool {
|
||||
// truncateValue traverses composite values once and rebuilds them lazily on
|
||||
// the first changed child. Recursion provides O(depth) temporary stack space
|
||||
// without forcing a heap allocation on the common no-op path.
|
||||
func truncateValue(limit int, v attribute.Value) (attribute.Value, bool) {
|
||||
switch v.Type() {
|
||||
case attribute.STRING:
|
||||
return stringNeedsTruncation(limit, v.AsString())
|
||||
case attribute.BYTESLICE:
|
||||
// len(v.AsString()) is identical to len(v.AsByteSlice()) but
|
||||
// avoids memory allocation.
|
||||
if limit >= 0 && len(v.AsString()) > limit {
|
||||
return true
|
||||
s := v.AsString()
|
||||
truncated := truncate(limit, s)
|
||||
if truncated != s {
|
||||
return attribute.StringValue(truncated), true
|
||||
}
|
||||
case attribute.STRINGSLICE:
|
||||
for _, s := range v.AsStringSlice() {
|
||||
if stringNeedsTruncation(limit, s) {
|
||||
return true
|
||||
}
|
||||
return truncateStringSliceValue(limit, v)
|
||||
case attribute.BYTESLICE:
|
||||
// len(v.AsString()) is identical to len(v.AsByteSlice()) but
|
||||
// avoids allocating the full slice before truncation.
|
||||
s := v.AsString()
|
||||
if len(s) > limit {
|
||||
return attribute.ByteSliceValue([]byte(s[:limit])), true
|
||||
}
|
||||
case attribute.SLICE:
|
||||
return slices.ContainsFunc(v.AsSlice(), func(e attribute.Value) bool { return needsTruncation(limit, e) })
|
||||
return truncateSliceValue(limit, v)
|
||||
case attribute.MAP:
|
||||
return slices.ContainsFunc(
|
||||
v.AsMap(),
|
||||
func(kv attribute.KeyValue) bool { return needsTruncation(limit, kv.Value) },
|
||||
)
|
||||
return truncateMapValue(limit, v)
|
||||
}
|
||||
return false
|
||||
return v, false
|
||||
}
|
||||
|
||||
func truncateStringSliceValue(limit int, value attribute.Value) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
switch values := storage.(type) {
|
||||
case [0]string:
|
||||
return value, false
|
||||
case [1]string:
|
||||
return truncateStrings(limit, value, values[:])
|
||||
case [2]string:
|
||||
return truncateStrings(limit, value, values[:])
|
||||
case [3]string:
|
||||
return truncateStrings(limit, value, values[:])
|
||||
default:
|
||||
array := reflect.ValueOf(storage)
|
||||
if array.Kind() != reflect.Array || array.Type().Elem() != stringType {
|
||||
return value, false
|
||||
}
|
||||
return truncateStringArray(limit, value, array)
|
||||
}
|
||||
}
|
||||
|
||||
func truncateStrings(limit int, value attribute.Value, values []string) (attribute.Value, bool) {
|
||||
var truncated []string
|
||||
for i, s := range values {
|
||||
newS := truncate(limit, s)
|
||||
if truncated != nil {
|
||||
truncated[i] = newS
|
||||
continue
|
||||
}
|
||||
if newS == s {
|
||||
continue
|
||||
}
|
||||
|
||||
truncated = make([]string, len(values))
|
||||
copy(truncated, values[:i])
|
||||
truncated[i] = newS
|
||||
}
|
||||
if truncated == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.StringSliceValue(truncated), true
|
||||
}
|
||||
|
||||
func truncateStringArray(limit int, value attribute.Value, values reflect.Value) (attribute.Value, bool) {
|
||||
var truncated []string
|
||||
for i := range values.Len() {
|
||||
s := values.Index(i).String()
|
||||
newS := truncate(limit, s)
|
||||
if truncated != nil {
|
||||
truncated[i] = newS
|
||||
continue
|
||||
}
|
||||
if newS == s {
|
||||
continue
|
||||
}
|
||||
|
||||
truncated = make([]string, values.Len())
|
||||
for j := range i {
|
||||
truncated[j] = values.Index(j).String()
|
||||
}
|
||||
truncated[i] = newS
|
||||
}
|
||||
if truncated == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.StringSliceValue(truncated), true
|
||||
}
|
||||
|
||||
func truncateSliceValue(limit int, value attribute.Value) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
switch values := storage.(type) {
|
||||
case [0]attribute.Value:
|
||||
return value, false
|
||||
case [1]attribute.Value:
|
||||
return truncateValueSlice(limit, value, values[:])
|
||||
case [2]attribute.Value:
|
||||
return truncateValueSlice(limit, value, values[:])
|
||||
case [3]attribute.Value:
|
||||
return truncateValueSlice(limit, value, values[:])
|
||||
case [4]attribute.Value:
|
||||
return truncateValueSlice(limit, value, values[:])
|
||||
case [5]attribute.Value:
|
||||
return truncateValueSlice(limit, value, values[:])
|
||||
default:
|
||||
array := reflect.ValueOf(storage)
|
||||
if array.Kind() != reflect.Array || array.Type().Elem() != valueType {
|
||||
return value, false
|
||||
}
|
||||
return truncateValueArray(limit, value, array)
|
||||
}
|
||||
}
|
||||
|
||||
func truncateValueSlice(limit int, value attribute.Value, values []attribute.Value) (attribute.Value, bool) {
|
||||
var truncated []attribute.Value
|
||||
for i, elem := range values {
|
||||
newElem, changed := truncateValue(limit, elem)
|
||||
if truncated != nil {
|
||||
truncated[i] = newElem
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
truncated = make([]attribute.Value, len(values))
|
||||
copy(truncated, values[:i])
|
||||
truncated[i] = newElem
|
||||
}
|
||||
if truncated == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.SliceValue(truncated...), true
|
||||
}
|
||||
|
||||
func truncateValueArray(limit int, value attribute.Value, values reflect.Value) (attribute.Value, bool) {
|
||||
var truncated []attribute.Value
|
||||
for i := range values.Len() {
|
||||
elem := values.Index(i).Interface().(attribute.Value)
|
||||
newElem, changed := truncateValue(limit, elem)
|
||||
if truncated != nil {
|
||||
truncated[i] = newElem
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
truncated = make([]attribute.Value, values.Len())
|
||||
for j := range i {
|
||||
truncated[j] = values.Index(j).Interface().(attribute.Value)
|
||||
}
|
||||
truncated[i] = newElem
|
||||
}
|
||||
if truncated == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.SliceValue(truncated...), true
|
||||
}
|
||||
|
||||
func truncateMapValue(limit int, value attribute.Value) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
switch keyValues := storage.(type) {
|
||||
case [0]attribute.KeyValue:
|
||||
return value, false
|
||||
case [1]attribute.KeyValue:
|
||||
return truncateKeyValueSlice(limit, value, keyValues[:])
|
||||
case [2]attribute.KeyValue:
|
||||
return truncateKeyValueSlice(limit, value, keyValues[:])
|
||||
case [3]attribute.KeyValue:
|
||||
return truncateKeyValueSlice(limit, value, keyValues[:])
|
||||
case [4]attribute.KeyValue:
|
||||
return truncateKeyValueSlice(limit, value, keyValues[:])
|
||||
case [5]attribute.KeyValue:
|
||||
return truncateKeyValueSlice(limit, value, keyValues[:])
|
||||
default:
|
||||
array := reflect.ValueOf(storage)
|
||||
if array.Kind() != reflect.Array || array.Type().Elem() != keyValueType {
|
||||
return value, false
|
||||
}
|
||||
return truncateKeyValueArray(limit, value, array)
|
||||
}
|
||||
}
|
||||
|
||||
func truncateKeyValueSlice(
|
||||
limit int,
|
||||
value attribute.Value,
|
||||
keyValues []attribute.KeyValue,
|
||||
) (attribute.Value, bool) {
|
||||
var truncated []attribute.KeyValue
|
||||
for i, kv := range keyValues {
|
||||
newValue, changed := truncateValue(limit, kv.Value)
|
||||
if changed {
|
||||
kv.Value = newValue
|
||||
}
|
||||
if truncated != nil {
|
||||
truncated[i] = kv
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
truncated = make([]attribute.KeyValue, len(keyValues))
|
||||
copy(truncated, keyValues[:i])
|
||||
truncated[i] = kv
|
||||
}
|
||||
if truncated == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.MapValue(truncated...), true
|
||||
}
|
||||
|
||||
func truncateKeyValueArray(limit int, value attribute.Value, keyValues reflect.Value) (attribute.Value, bool) {
|
||||
var truncated []attribute.KeyValue
|
||||
for i := range keyValues.Len() {
|
||||
kv := keyValues.Index(i).Interface().(attribute.KeyValue)
|
||||
newValue, changed := truncateValue(limit, kv.Value)
|
||||
if changed {
|
||||
kv.Value = newValue
|
||||
}
|
||||
if truncated != nil {
|
||||
truncated[i] = kv
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
truncated = make([]attribute.KeyValue, keyValues.Len())
|
||||
for j := range i {
|
||||
truncated[j] = keyValues.Index(j).Interface().(attribute.KeyValue)
|
||||
}
|
||||
truncated[i] = kv
|
||||
}
|
||||
if truncated == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.MapValue(truncated...), true
|
||||
}
|
||||
|
||||
var stringType = reflect.TypeFor[string]()
|
||||
|
||||
// truncate returns a version of s truncated according to limit.
|
||||
//
|
||||
// If limit is negative, it returns the original string.
|
||||
@@ -168,7 +339,7 @@ func truncate(limit int, s string) string {
|
||||
// This prioritizes performance in the following order based on the most
|
||||
// common expected use cases.
|
||||
//
|
||||
// - Values shorter than the default limit (128).
|
||||
// - Values whose byte length does not exceed the limit.
|
||||
// - Strings with valid encodings that exceed the limit.
|
||||
// - No limit.
|
||||
// - Strings with invalid encodings that exceed the limit.
|
||||
@@ -188,14 +359,27 @@ func truncate(limit int, s string) string {
|
||||
continue
|
||||
}
|
||||
|
||||
// A zero limit also truncates a leading invalid byte or U+FFFD.
|
||||
if limit == 0 {
|
||||
return ""
|
||||
}
|
||||
// Range reports RuneError for both an invalid byte and a valid U+FFFD.
|
||||
// Only a size-1 decoding indicates invalid UTF-8.
|
||||
_, size := utf8.DecodeRuneInString(s[i:])
|
||||
if size == 1 {
|
||||
// Invalid encoding.
|
||||
// Removing this byte makes len(s)-1 an upper bound for a rebuilt
|
||||
// result. Keep the valid prefix before rescanning s[i:].
|
||||
b.Grow(len(s) - 1)
|
||||
_, _ = b.WriteString(s[:i])
|
||||
s = s[i:]
|
||||
break
|
||||
}
|
||||
|
||||
// A valid U+FFFD still consumes one character of the limit.
|
||||
count++
|
||||
if count > limit {
|
||||
return s[:i]
|
||||
}
|
||||
}
|
||||
|
||||
// Fast-path, no invalid input.
|
||||
@@ -203,7 +387,9 @@ func truncate(limit int, s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// Truncate while validating UTF-8.
|
||||
// The builder holds the valid prefix before the first invalid byte, and
|
||||
// count includes its runes. Skip malformed bytes without counting them
|
||||
// while copying the remaining valid runes up to the limit.
|
||||
for i := 0; i < len(s) && count < limit; {
|
||||
c := s[i]
|
||||
if c < utf8.RuneSelf {
|
||||
|
||||
+2
-2
@@ -79,7 +79,7 @@ func (l AggregationLastValue) copy() Aggregation { return l }
|
||||
func (AggregationLastValue) err() error { return nil }
|
||||
|
||||
// AggregationExplicitBucketHistogram is an Aggregation that summarizes a set of
|
||||
// measurements as an histogram with explicitly defined buckets.
|
||||
// measurements as a histogram with explicitly defined buckets.
|
||||
type AggregationExplicitBucketHistogram struct {
|
||||
// Boundaries are the increasing bucket boundary values. Boundary values
|
||||
// define bucket upper bounds. Buckets are exclusive of their lower
|
||||
@@ -138,7 +138,7 @@ func (h AggregationExplicitBucketHistogram) copy() Aggregation {
|
||||
}
|
||||
|
||||
// AggregationBase2ExponentialHistogram is an Aggregation that summarizes a set of
|
||||
// measurements as an histogram with bucket widths that grow exponentially.
|
||||
// measurements as a histogram with bucket widths that grow exponentially.
|
||||
type AggregationBase2ExponentialHistogram struct {
|
||||
// MaxSize is the maximum number of buckets to use for the histogram.
|
||||
MaxSize int32
|
||||
|
||||
+8
-1
@@ -4,6 +4,7 @@
|
||||
package metric
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -11,6 +12,8 @@ import (
|
||||
"go.opentelemetry.io/otel/internal/global"
|
||||
)
|
||||
|
||||
var errDurationOverflow = errors.New("duration overflows time.Duration")
|
||||
|
||||
// Environment variable names.
|
||||
const (
|
||||
// The time interval (in milliseconds) between the start of two export attempts.
|
||||
@@ -26,7 +29,7 @@ func envDuration(key string, defaultValue time.Duration) time.Duration {
|
||||
if v == "" {
|
||||
return defaultValue
|
||||
}
|
||||
d, err := strconv.Atoi(v)
|
||||
d, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
global.Error(err, "parse duration", "environment variable", key, "value", v)
|
||||
return defaultValue
|
||||
@@ -35,5 +38,9 @@ func envDuration(key string, defaultValue time.Duration) time.Duration {
|
||||
global.Error(errNonPositiveDuration, "non-positive duration", "environment variable", key, "value", v)
|
||||
return defaultValue
|
||||
}
|
||||
if d > int64(time.Duration(1<<63-1)/time.Millisecond) {
|
||||
global.Error(errDurationOverflow, "duration overflows time.Duration", "environment variable", key, "value", v)
|
||||
return defaultValue
|
||||
}
|
||||
return time.Duration(d) * time.Millisecond
|
||||
}
|
||||
|
||||
+6
@@ -24,6 +24,12 @@ 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 {
|
||||
// A negative size is invalid. The constructor cannot return an error, so
|
||||
// clamp it to zero and treat it as an empty reservoir (consistent with the
|
||||
// existing behavior for k == 0) rather than panicking in make below.
|
||||
if k < 0 {
|
||||
k = 0
|
||||
}
|
||||
r := &FixedSizeReservoir{
|
||||
storage: make([]measurement, k),
|
||||
}
|
||||
|
||||
+2
-2
@@ -49,8 +49,8 @@ func (v Value) Int64() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Float64 returns the value of v as an float64. If the ValueType of v is not
|
||||
// an Float64ValueType, 0 is returned.
|
||||
// Float64 returns the value of v as a float64. If the ValueType of v is not
|
||||
// a Float64ValueType, 0 is returned.
|
||||
func (v Value) Float64() float64 {
|
||||
if v.t == Float64ValueType {
|
||||
return math.Float64frombits(v.val)
|
||||
|
||||
+5
-2
@@ -142,6 +142,9 @@ type Stream struct {
|
||||
// the attribute will not be recorded, otherwise, if it returns true, it
|
||||
// will record the attribute.
|
||||
//
|
||||
// If nil, the instrument's advisory attribute keys are used. If the
|
||||
// instrument has no advisory attribute keys, all attributes are recorded.
|
||||
//
|
||||
// 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.
|
||||
@@ -207,11 +210,11 @@ func extractRawKVs[T any](opts []T) []attribute.KeyValue {
|
||||
}
|
||||
|
||||
func resolveAttributes(configAttrs attribute.Set, rawKVs []attribute.KeyValue) attribute.Set {
|
||||
configAttrs, _ = attrnorm.Set(configAttrs)
|
||||
configAttrs, _ = attrnorm.SetDedup(configAttrs)
|
||||
if len(rawKVs) == 0 {
|
||||
return configAttrs
|
||||
}
|
||||
rawKVs, _ = attrnorm.KeyValues(rawKVs)
|
||||
rawKVs, _ = attrnorm.KeyValuesDedup(rawKVs)
|
||||
merged := make([]attribute.KeyValue, 0, configAttrs.Len()+len(rawKVs))
|
||||
merged = append(merged, configAttrs.ToSlice()...)
|
||||
// rawKVs are appended after configAttrs, meaning they will override any duplicate keys in configAttrs.
|
||||
|
||||
+132
-14
@@ -27,10 +27,18 @@ type atomicCounter[N int64 | float64] struct {
|
||||
|
||||
// load returns the current value. The caller must ensure all calls to add have
|
||||
// returned prior to calling load.
|
||||
func (n *atomicCounter[N]) load() N {
|
||||
fval := math.Float64frombits(n.nFloatBits.Load())
|
||||
ival := n.nInt.Load()
|
||||
return N(fval + float64(ival))
|
||||
func (n *atomicCounter[N]) load() (value N) {
|
||||
switch any(value).(type) {
|
||||
case int64:
|
||||
value = N(n.nInt.Load())
|
||||
case float64:
|
||||
fval := math.Float64frombits(n.nFloatBits.Load())
|
||||
ival := n.nInt.Load()
|
||||
value = N(fval + float64(ival))
|
||||
default:
|
||||
panic("unsupported type")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (n *atomicCounter[N]) add(value N) {
|
||||
@@ -183,40 +191,53 @@ type hotColdWaitGroup struct {
|
||||
endedCounts [2]atomic.Uint64
|
||||
}
|
||||
|
||||
// hotIdx represents an index (0 or 1) to the currently active "hot" buffer
|
||||
// where concurrent writes are accepted.
|
||||
//
|
||||
// coldIdx represents an index (0 or 1) to the "cold" buffer where collection
|
||||
// reads and resets occur after a swap.
|
||||
//
|
||||
// These distinct types enforce at compile time that hot (write) and cold (read)
|
||||
// indices are not accidentally interchanged across call boundaries.
|
||||
type (
|
||||
hotIdx uint64
|
||||
coldIdx uint64
|
||||
)
|
||||
|
||||
// start returns the hot index that the writer should write to. The returned
|
||||
// hot index is 0 or 1. The caller must call done(hot index) after it finishes
|
||||
// its operation. start() is safe to call concurrently with other methods.
|
||||
func (l *hotColdWaitGroup) start() uint64 {
|
||||
func (l *hotColdWaitGroup) start() hotIdx {
|
||||
// We increment h.startedCountAndHotIdx so that the counter in the lower
|
||||
// 63 bits gets incremented. At the same time, we get the new value
|
||||
// back, which we can use to return the currently-hot index.
|
||||
return l.startedCountAndHotIdx.Add(1) >> 63
|
||||
return hotIdx(l.startedCountAndHotIdx.Add(1) >> 63)
|
||||
}
|
||||
|
||||
// done signals to the reader that an operation has fully completed.
|
||||
// done is safe to call concurrently.
|
||||
func (l *hotColdWaitGroup) done(hotIdx uint64) {
|
||||
func (l *hotColdWaitGroup) done(hotIdx hotIdx) {
|
||||
l.endedCounts[hotIdx].Add(1)
|
||||
}
|
||||
|
||||
// swapHotAndWait swaps the hot bit, waits for all start() calls to be done(),
|
||||
// and then returns the now-cold index for the reader to read from. The
|
||||
// returned index is 0 or 1. swapHotAndWait must not be called concurrently.
|
||||
func (l *hotColdWaitGroup) swapHotAndWait() uint64 {
|
||||
func (l *hotColdWaitGroup) swapHotAndWait() coldIdx {
|
||||
n := l.startedCountAndHotIdx.Load()
|
||||
coldIdx := (^n) >> 63
|
||||
coldIdxVal := (^n) >> 63
|
||||
// Swap the hot and cold index while resetting the started measurements
|
||||
// count to zero.
|
||||
n = l.startedCountAndHotIdx.Swap((coldIdx << 63))
|
||||
hotIdx := n >> 63
|
||||
n = l.startedCountAndHotIdx.Swap((coldIdxVal << 63))
|
||||
hotIdxVal := n >> 63
|
||||
startedCount := n & ((1 << 63) - 1)
|
||||
// Wait for all measurements to the previously-hot map to finish.
|
||||
for startedCount != l.endedCounts[hotIdx].Load() {
|
||||
for startedCount != l.endedCounts[hotIdxVal].Load() {
|
||||
runtime.Gosched() // Let measurements complete.
|
||||
}
|
||||
// reset the number of ended operations
|
||||
l.endedCounts[hotIdx].Store(0)
|
||||
return hotIdx
|
||||
l.endedCounts[hotIdxVal].Store(0)
|
||||
return coldIdx(hotIdxVal)
|
||||
}
|
||||
|
||||
// limitedSyncMap is a sync.Map which enforces the aggregation limit on
|
||||
@@ -270,6 +291,49 @@ func (m *limitedSyncMap[V]) LoadOrStoreAttr(lazy lazyFilteredAttributes, newValu
|
||||
return actual.(V)
|
||||
}
|
||||
|
||||
// LoadOrStoreAttrReclaiming is equivalent to LoadOrStoreAttr, except that a
|
||||
// slot released after overflow was created can be reused for a new attribute
|
||||
// set. The shared overflow entry remains available for measurements made while
|
||||
// all normal slots are occupied. It reports whether the value was loaded and
|
||||
// whether this lookup was routed to the shared overflow entry.
|
||||
func (m *limitedSyncMap[V]) LoadOrStoreAttrReclaiming(
|
||||
lazy lazyFilteredAttributes,
|
||||
newValue func(attribute.Set, bool) V,
|
||||
) (value V, loaded, overflowed bool) {
|
||||
distinct := lazy.Distinct()
|
||||
actual, loaded := m.Load(distinct)
|
||||
if loaded {
|
||||
return actual.(V), true, false
|
||||
}
|
||||
|
||||
m.lenMux.Lock()
|
||||
defer m.lenMux.Unlock()
|
||||
|
||||
actual, loaded = m.Load(distinct)
|
||||
if loaded {
|
||||
return actual.(V), true, false
|
||||
}
|
||||
|
||||
overflow, hasOverflow := m.Load(overflowSet.Equivalent())
|
||||
if m.aggLimit > 0 && hasOverflow && m.len >= m.aggLimit {
|
||||
return overflow.(V), true, true
|
||||
}
|
||||
|
||||
var attrs attribute.Set
|
||||
overflowed = m.aggLimit > 0 && !hasOverflow && m.len >= m.aggLimit-1
|
||||
if overflowed {
|
||||
attrs = overflowSet
|
||||
distinct = overflowSet.Equivalent()
|
||||
} else {
|
||||
attrs = lazy.Set()
|
||||
}
|
||||
actual, loaded = m.LoadOrStore(distinct, newValue(attrs, overflowed))
|
||||
if !loaded {
|
||||
m.len++
|
||||
}
|
||||
return actual.(V), loaded, overflowed
|
||||
}
|
||||
|
||||
func (m *limitedSyncMap[V]) Clear() {
|
||||
m.lenMux.Lock()
|
||||
defer m.lenMux.Unlock()
|
||||
@@ -277,8 +341,62 @@ func (m *limitedSyncMap[V]) Clear() {
|
||||
m.Map.Clear()
|
||||
}
|
||||
|
||||
// CompareAndDelete deletes the entry for key if its value is equal to old.
|
||||
// It reports whether the entry was deleted.
|
||||
func (m *limitedSyncMap[V]) CompareAndDelete(key attribute.Distinct, old V) bool {
|
||||
m.lenMux.Lock()
|
||||
defer m.lenMux.Unlock()
|
||||
if !m.Map.CompareAndDelete(key, old) {
|
||||
return false
|
||||
}
|
||||
m.len--
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *limitedSyncMap[V]) Len() int {
|
||||
m.lenMux.Lock()
|
||||
defer m.lenMux.Unlock()
|
||||
return m.len
|
||||
}
|
||||
|
||||
// hotColdMap manages two [limitedSyncMap] instances so that measurement and
|
||||
// collection operate on separate maps and do not lock each other out.
|
||||
//
|
||||
// Measurements write to the currently hot map returned by [hotColdMap.hot]
|
||||
// using the index from [hotColdWaitGroup.start]. Collection is performed by
|
||||
// calling [hotColdWaitGroup.swapHotAndWait] to atomically swap the hot and cold
|
||||
// maps and wait for in-flight writes to the cold map to complete. The caller
|
||||
// can then read the cold map via [hotColdMap.Range] and must call
|
||||
// [hotColdMap.Clear] on the cold index so that unused attribute sets do not
|
||||
// report in subsequent collection cycles and the cardinality limit budget is
|
||||
// reset.
|
||||
//
|
||||
// swapHotAndWait must not be called concurrently.
|
||||
type hotColdMap[V any] struct {
|
||||
hotColdWaitGroup
|
||||
hotColdValMap [2]limitedSyncMap[V]
|
||||
}
|
||||
|
||||
func (m *hotColdMap[V]) init(limit int) {
|
||||
m.hotColdValMap[0].aggLimit = limit
|
||||
m.hotColdValMap[1].aggLimit = limit
|
||||
}
|
||||
|
||||
func (m *hotColdMap[V]) hot(i hotIdx) *limitedSyncMap[V] {
|
||||
return &m.hotColdValMap[i]
|
||||
}
|
||||
|
||||
// Len returns the length of the specified map.
|
||||
func (m *hotColdMap[V]) Len(readIdx coldIdx) int {
|
||||
return m.hotColdValMap[readIdx].Len()
|
||||
}
|
||||
|
||||
// Range calls f sequentially for each key and value present in the map at readIdx.
|
||||
func (m *hotColdMap[V]) Range(readIdx coldIdx, f func(key, value any) bool) {
|
||||
m.hotColdValMap[readIdx].Range(f)
|
||||
}
|
||||
|
||||
// Clear clears the map at readIdx.
|
||||
func (m *hotColdMap[V]) Clear(readIdx coldIdx) {
|
||||
m.hotColdValMap[readIdx].Clear()
|
||||
}
|
||||
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package aggregate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/sdk/metric/internal/finish"
|
||||
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
||||
)
|
||||
|
||||
type finishSumValue[N int64 | float64] struct {
|
||||
lifecycle finish.Lifecycle
|
||||
value atomicCounter[N]
|
||||
attrs attribute.Set
|
||||
start time.Time
|
||||
dropExemplars bool
|
||||
reservoir FilteredExemplarReservoir[N]
|
||||
}
|
||||
|
||||
func newFinishSumValue[N int64 | float64](
|
||||
attrs attribute.Set,
|
||||
reservoir func(attribute.Set) FilteredExemplarReservoir[N],
|
||||
) *finishSumValue[N] {
|
||||
r := reservoir(attrs)
|
||||
_, drop := r.(*dropRes[N])
|
||||
point := &finishSumValue[N]{
|
||||
attrs: attrs,
|
||||
start: now(),
|
||||
dropExemplars: drop,
|
||||
reservoir: r,
|
||||
}
|
||||
return point
|
||||
}
|
||||
|
||||
func (v *finishSumValue[N]) measure(
|
||||
ctx context.Context,
|
||||
value N,
|
||||
lazy lazyFilteredAttributes,
|
||||
) bool {
|
||||
measurement, ok := v.lifecycle.AcquireMeasurement()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
v.measureAcquired(ctx, value, lazy, measurement)
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *finishSumValue[N]) measureOverflow(
|
||||
ctx context.Context,
|
||||
value N,
|
||||
lazy lazyFilteredAttributes,
|
||||
) bool {
|
||||
measurement, ok := v.lifecycle.AcquireSharedMeasurement()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
v.measureAcquired(ctx, value, lazy, measurement)
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *finishSumValue[N]) measureAcquired(
|
||||
ctx context.Context,
|
||||
value N,
|
||||
lazy lazyFilteredAttributes,
|
||||
measurement finish.Measurement,
|
||||
) {
|
||||
v.value.add(value)
|
||||
if v.dropExemplars {
|
||||
measurement.Release()
|
||||
return
|
||||
}
|
||||
defer measurement.Release()
|
||||
v.reservoir.Offer(ctx, value, lazy)
|
||||
}
|
||||
|
||||
func (v *finishSumValue[N]) finish(t time.Time) {
|
||||
v.lifecycle.Finish(t)
|
||||
}
|
||||
|
||||
func (v *finishSumValue[N]) collectCumulative(
|
||||
t time.Time,
|
||||
) (metricdata.DataPoint[N], bool, bool) {
|
||||
collection, ok := v.lifecycle.BeginCumulativeCollection(t)
|
||||
if !ok {
|
||||
return metricdata.DataPoint[N]{}, false, false
|
||||
}
|
||||
dp, retire := v.collect(v.start, collection)
|
||||
return dp, true, retire
|
||||
}
|
||||
|
||||
func (v *finishSumValue[N]) collectDelta(
|
||||
start time.Time,
|
||||
t time.Time,
|
||||
) (metricdata.DataPoint[N], bool, bool) {
|
||||
collection, ok := v.lifecycle.BeginDeltaCollection(t)
|
||||
if !ok {
|
||||
return metricdata.DataPoint[N]{}, false, false
|
||||
}
|
||||
dp, retire := v.collect(start, collection)
|
||||
return dp, true, retire
|
||||
}
|
||||
|
||||
func (v *finishSumValue[N]) collect(
|
||||
start time.Time,
|
||||
collection finish.Collection,
|
||||
) (metricdata.DataPoint[N], bool) {
|
||||
defer collection.Complete()
|
||||
|
||||
dp := metricdata.DataPoint[N]{
|
||||
Attributes: v.attrs,
|
||||
StartTime: start,
|
||||
Time: collection.Time(),
|
||||
Value: v.value.load(),
|
||||
}
|
||||
collectExemplars(&dp.Exemplars, v.reservoir.Collect)
|
||||
return dp, collection.ShouldRetire()
|
||||
}
|
||||
|
||||
func (v *finishSumValue[N]) shutdown() {
|
||||
v.lifecycle.Retire()
|
||||
}
|
||||
|
||||
// FinishSum contains the operations of a finish-aware Sum aggregation.
|
||||
type FinishSum[N int64 | float64] struct {
|
||||
Measure Measure[N]
|
||||
ComputeAggregation ComputeAggregation
|
||||
Finish func(attribute.Distinct, time.Time)
|
||||
Shutdown func()
|
||||
}
|
||||
|
||||
type finishSum[N int64 | float64] struct {
|
||||
collectMu sync.Mutex
|
||||
shutdownOnce sync.Once
|
||||
stopped atomic.Bool
|
||||
|
||||
values limitedSyncMap[*finishSumValue[N]]
|
||||
start time.Time
|
||||
temporality metricdata.Temporality
|
||||
monotonic bool
|
||||
reservoir func(attribute.Set) FilteredExemplarReservoir[N]
|
||||
}
|
||||
|
||||
func newFinishSum[N int64 | float64](
|
||||
monotonic bool,
|
||||
temporality metricdata.Temporality,
|
||||
limit int,
|
||||
reservoir func(attribute.Set) FilteredExemplarReservoir[N],
|
||||
) *finishSum[N] {
|
||||
var start time.Time
|
||||
if temporality != metricdata.DeltaTemporality {
|
||||
temporality = metricdata.CumulativeTemporality
|
||||
} else {
|
||||
start = now()
|
||||
}
|
||||
return &finishSum[N]{
|
||||
values: limitedSyncMap[*finishSumValue[N]]{
|
||||
aggLimit: limit,
|
||||
},
|
||||
start: start,
|
||||
temporality: temporality,
|
||||
monotonic: monotonic,
|
||||
reservoir: reservoir,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *finishSum[N]) measure(
|
||||
ctx context.Context,
|
||||
value N,
|
||||
lazy lazyFilteredAttributes,
|
||||
) {
|
||||
for {
|
||||
if s.stopped.Load() {
|
||||
return
|
||||
}
|
||||
var initial finish.Measurement
|
||||
point, loaded, overflowed := s.values.LoadOrStoreAttrReclaiming(lazy, func(
|
||||
attrs attribute.Set,
|
||||
overflow bool,
|
||||
) *finishSumValue[N] {
|
||||
point := newFinishSumValue(attrs, s.reservoir)
|
||||
// The point has not been published, so its new lifecycle cannot be
|
||||
// retired and this initial measurement admission cannot fail.
|
||||
if overflow {
|
||||
initial, _ = point.lifecycle.AcquireSharedMeasurement()
|
||||
} else {
|
||||
initial, _ = point.lifecycle.AcquireMeasurement()
|
||||
}
|
||||
return point
|
||||
})
|
||||
if !loaded {
|
||||
point.measureAcquired(ctx, value, lazy, initial)
|
||||
if s.stopped.Load() {
|
||||
s.retireAndDelete(point)
|
||||
}
|
||||
return
|
||||
}
|
||||
if overflowed {
|
||||
if point.measureOverflow(ctx, value, lazy) {
|
||||
if s.stopped.Load() {
|
||||
s.retireAndDelete(point)
|
||||
}
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if point.measure(ctx, value, lazy) {
|
||||
if s.stopped.Load() {
|
||||
s.retireAndDelete(point)
|
||||
}
|
||||
return
|
||||
}
|
||||
s.values.CompareAndDelete(lazy.Distinct(), point)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *finishSum[N]) retireAndDelete(point *finishSumValue[N]) {
|
||||
point.shutdown()
|
||||
s.values.CompareAndDelete(point.attrs.Equivalent(), point)
|
||||
}
|
||||
|
||||
func (s *finishSum[N]) finish(
|
||||
distinct attribute.Distinct,
|
||||
t time.Time,
|
||||
) {
|
||||
if s.stopped.Load() {
|
||||
return
|
||||
}
|
||||
value, ok := s.values.Load(distinct)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
point := value.(*finishSumValue[N])
|
||||
point.finish(t)
|
||||
}
|
||||
|
||||
func (s *finishSum[N]) collect(
|
||||
dest *metricdata.Aggregation, //nolint:gocritic // Required by ComputeAggregation.
|
||||
) int {
|
||||
s.collectMu.Lock()
|
||||
defer s.collectMu.Unlock()
|
||||
if s.stopped.Load() {
|
||||
return 0
|
||||
}
|
||||
|
||||
t := now()
|
||||
start := s.start
|
||||
if s.temporality == metricdata.DeltaTemporality {
|
||||
s.start = t
|
||||
}
|
||||
sData, _ := (*dest).(metricdata.Sum[N])
|
||||
sData.Temporality = s.temporality
|
||||
sData.IsMonotonic = s.monotonic
|
||||
points := reset(sData.DataPoints, 0, s.values.Len())
|
||||
|
||||
s.values.Range(func(key, raw any) bool {
|
||||
point := raw.(*finishSumValue[N])
|
||||
var dp metricdata.DataPoint[N]
|
||||
var emit, retire bool
|
||||
if s.temporality == metricdata.DeltaTemporality {
|
||||
dp, emit, retire = point.collectDelta(start, t)
|
||||
} else {
|
||||
dp, emit, retire = point.collectCumulative(t)
|
||||
}
|
||||
if !emit {
|
||||
return true
|
||||
}
|
||||
points = append(points, dp)
|
||||
|
||||
if retire {
|
||||
s.values.CompareAndDelete(key.(attribute.Distinct), point)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
sData.DataPoints = points
|
||||
*dest = sData
|
||||
return len(points)
|
||||
}
|
||||
|
||||
func (s *finishSum[N]) shutdown() {
|
||||
s.shutdownOnce.Do(func() {
|
||||
s.stopped.Store(true)
|
||||
s.collectMu.Lock()
|
||||
defer s.collectMu.Unlock()
|
||||
s.values.Range(func(_, raw any) bool {
|
||||
raw.(*finishSumValue[N]).shutdown()
|
||||
return true
|
||||
})
|
||||
s.values.Clear()
|
||||
})
|
||||
}
|
||||
|
||||
// FinishSum returns a Sum aggregation with exact-attribute lifecycle support.
|
||||
func (b Builder[N]) FinishSum(monotonic bool) FinishSum[N] {
|
||||
store := newFinishSum(
|
||||
monotonic,
|
||||
b.Temporality,
|
||||
b.AggregationLimit,
|
||||
b.resFunc(),
|
||||
)
|
||||
return FinishSum[N]{
|
||||
Measure: b.filter(store.measure),
|
||||
ComputeAggregation: store.collect,
|
||||
Finish: store.finish,
|
||||
Shutdown: store.shutdown,
|
||||
}
|
||||
}
|
||||
+15
-28
@@ -85,18 +85,8 @@ func (b *histogramPointCounters[N]) mergeIntoAndReset( // nolint:revive // Inten
|
||||
|
||||
// deltaHistogram is a histogram whose internal storage is reset when it is
|
||||
// collected.
|
||||
//
|
||||
// deltaHistogram's measure is implemented without locking, even when called
|
||||
// concurrently with collect. This is done by maintaining two separate maps:
|
||||
// one "hot" which is concurrently updated by measure(), and one "cold", which
|
||||
// is read and reset by collect(). The [hotcoldWaitGroup] allows collect() to
|
||||
// swap the hot and cold maps, and wait for updates to the cold map to complete
|
||||
// prior to reading. deltaHistogram swaps ald clears complete maps so that
|
||||
// unused attribute sets do not report in subsequent collect() calls.
|
||||
type deltaHistogram[N int64 | float64] struct {
|
||||
hcwg hotColdWaitGroup
|
||||
hotColdValMap [2]limitedSyncMap[*histogramPoint[N]]
|
||||
|
||||
vals hotColdMap[*histogramPoint[N]]
|
||||
start time.Time
|
||||
noMinMax bool
|
||||
noSum bool
|
||||
@@ -109,12 +99,13 @@ func (s *deltaHistogram[N]) measure(
|
||||
value N,
|
||||
lazy lazyFilteredAttributes,
|
||||
) {
|
||||
hotIdx := s.hcwg.start()
|
||||
defer s.hcwg.done(hotIdx)
|
||||
h := s.hotColdValMap[hotIdx].LoadOrStoreAttr(lazy, func(attr attribute.Set) *histogramPoint[N] {
|
||||
hotIdx := s.vals.start()
|
||||
defer s.vals.done(hotIdx)
|
||||
|
||||
h := s.vals.hot(hotIdx).LoadOrStoreAttr(lazy, func(attr attribute.Set) *histogramPoint[N] {
|
||||
r := s.newRes(attr)
|
||||
_, isDrop := r.(*dropRes[N])
|
||||
hPt := &histogramPoint[N]{
|
||||
return &histogramPoint[N]{
|
||||
res: r,
|
||||
attrs: attr,
|
||||
dropExemplars: isDrop,
|
||||
@@ -127,7 +118,6 @@ func (s *deltaHistogram[N]) measure(
|
||||
// counts = (-∞, 0], (0, 5.0], (5.0, 10.0], (10.0, +∞)
|
||||
histogramPointCounters: histogramPointCounters[N]{counts: make([]atomic.Uint64, len(s.bounds)+1)},
|
||||
}
|
||||
return hPt
|
||||
})
|
||||
|
||||
// This search will return an index in the range [0, len(s.bounds)], where
|
||||
@@ -162,17 +152,15 @@ func newDeltaHistogram[N int64 | float64](
|
||||
// complete control over the fix.
|
||||
b := slices.Clone(boundaries)
|
||||
slices.Sort(b)
|
||||
return &deltaHistogram[N]{
|
||||
h := &deltaHistogram[N]{
|
||||
start: now(),
|
||||
noMinMax: noMinMax,
|
||||
noSum: noSum,
|
||||
bounds: b,
|
||||
newRes: r,
|
||||
hotColdValMap: [2]limitedSyncMap[*histogramPoint[N]]{
|
||||
{aggLimit: limit},
|
||||
{aggLimit: limit},
|
||||
},
|
||||
}
|
||||
h.vals.init(limit)
|
||||
return h
|
||||
}
|
||||
|
||||
func (s *deltaHistogram[N]) collect(
|
||||
@@ -186,20 +174,19 @@ func (s *deltaHistogram[N]) collect(
|
||||
h.Temporality = metricdata.DeltaTemporality
|
||||
|
||||
// delta always clears values on collection
|
||||
readIdx := s.hcwg.swapHotAndWait()
|
||||
readIdx := s.vals.swapHotAndWait()
|
||||
|
||||
// Do not allow modification of our copy of bounds.
|
||||
bounds := slices.Clone(s.bounds)
|
||||
|
||||
// The len will not change while we iterate over values, since we waited
|
||||
// for all writes to finish to the cold values and len.
|
||||
n := s.hotColdValMap[readIdx].Len()
|
||||
n := s.vals.Len(readIdx)
|
||||
hDPts := reset(h.DataPoints, n, n)
|
||||
|
||||
var i int
|
||||
s.hotColdValMap[readIdx].Range(func(_, value any) bool {
|
||||
s.vals.Range(readIdx, func(_, value any) bool {
|
||||
val := value.(*histogramPoint[N])
|
||||
|
||||
count := val.loadCountsInto(&hDPts[i].BucketCounts)
|
||||
hDPts[i].Attributes = val.attrs
|
||||
hDPts[i].StartTime = s.start
|
||||
@@ -222,12 +209,12 @@ func (s *deltaHistogram[N]) collect(
|
||||
}
|
||||
|
||||
collectExemplars(&hDPts[i].Exemplars, val.res.Collect)
|
||||
|
||||
i++
|
||||
return true
|
||||
})
|
||||
// Unused attribute sets do not report.
|
||||
s.hotColdValMap[readIdx].Clear()
|
||||
s.vals.Clear(readIdx)
|
||||
|
||||
// The delta collection cycle resets.
|
||||
s.start = t
|
||||
|
||||
@@ -403,7 +390,7 @@ func (s *cumulativeHistogram[N]) collect(
|
||||
}
|
||||
// Once we've read the point, merge it back into the hot histogram
|
||||
// point since it is cumulative.
|
||||
hotIdx := (readIdx + 1) % 2
|
||||
hotIdx := hotIdx((readIdx + 1) % 2)
|
||||
val.hotColdPoint[readIdx].mergeIntoAndReset(&val.hotColdPoint[hotIdx], s.noMinMax, s.noSum)
|
||||
|
||||
collectExemplars(&dp.Exemplars, val.res.Collect)
|
||||
|
||||
+59
-57
@@ -21,18 +21,35 @@ type lastValuePoint[N int64 | float64] struct {
|
||||
dropExemplars bool
|
||||
}
|
||||
|
||||
// lastValueMap summarizes a set of measurements as the last one made.
|
||||
type lastValueMap[N int64 | float64] struct {
|
||||
newRes func(attribute.Set) FilteredExemplarReservoir[N]
|
||||
values limitedSyncMap[*lastValuePoint[N]]
|
||||
func newDeltaLastValue[N int64 | float64](
|
||||
limit int,
|
||||
r func(attribute.Set) FilteredExemplarReservoir[N],
|
||||
) *deltaLastValue[N] {
|
||||
s := &deltaLastValue[N]{
|
||||
newRes: r,
|
||||
start: now(),
|
||||
}
|
||||
s.vals.init(limit)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *lastValueMap[N]) measure(
|
||||
// deltaLastValue summarizes a set of measurements as the last one made.
|
||||
type deltaLastValue[N int64 | float64] struct {
|
||||
newRes func(attribute.Set) FilteredExemplarReservoir[N]
|
||||
start time.Time
|
||||
|
||||
vals hotColdMap[*lastValuePoint[N]]
|
||||
}
|
||||
|
||||
func (s *deltaLastValue[N]) measure(
|
||||
ctx context.Context,
|
||||
value N,
|
||||
lazy lazyFilteredAttributes,
|
||||
) {
|
||||
lv := s.values.LoadOrStoreAttr(lazy, func(attr attribute.Set) *lastValuePoint[N] {
|
||||
hotIdx := s.vals.start()
|
||||
defer s.vals.done(hotIdx)
|
||||
|
||||
lv := s.vals.hot(hotIdx).LoadOrStoreAttr(lazy, func(attr attribute.Set) *lastValuePoint[N] {
|
||||
r := s.newRes(attr)
|
||||
_, isDrop := r.(*dropRes[N])
|
||||
p := &lastValuePoint[N]{
|
||||
@@ -51,45 +68,6 @@ func (s *lastValueMap[N]) measure(
|
||||
}
|
||||
}
|
||||
|
||||
func newDeltaLastValue[N int64 | float64](
|
||||
limit int,
|
||||
r func(attribute.Set) FilteredExemplarReservoir[N],
|
||||
) *deltaLastValue[N] {
|
||||
return &deltaLastValue[N]{
|
||||
newRes: r,
|
||||
start: now(),
|
||||
hotColdValMap: [2]lastValueMap[N]{
|
||||
{
|
||||
newRes: r,
|
||||
values: limitedSyncMap[*lastValuePoint[N]]{aggLimit: limit},
|
||||
},
|
||||
{
|
||||
newRes: r,
|
||||
values: limitedSyncMap[*lastValuePoint[N]]{aggLimit: limit},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// deltaLastValue summarizes a set of measurements as the last one made.
|
||||
type deltaLastValue[N int64 | float64] struct {
|
||||
newRes func(attribute.Set) FilteredExemplarReservoir[N]
|
||||
start time.Time
|
||||
|
||||
hcwg hotColdWaitGroup
|
||||
hotColdValMap [2]lastValueMap[N]
|
||||
}
|
||||
|
||||
func (s *deltaLastValue[N]) measure(
|
||||
ctx context.Context,
|
||||
value N,
|
||||
lazy lazyFilteredAttributes,
|
||||
) {
|
||||
hotIdx := s.hcwg.start()
|
||||
defer s.hcwg.done(hotIdx)
|
||||
s.hotColdValMap[hotIdx].measure(ctx, value, lazy)
|
||||
}
|
||||
|
||||
func (s *deltaLastValue[N]) collect(
|
||||
dest *metricdata.Aggregation, //nolint:gocritic // The pointer is needed for the ComputeAggregation interface
|
||||
) int {
|
||||
@@ -110,14 +88,14 @@ func (s *deltaLastValue[N]) copyAndClearDpts(
|
||||
// the DataPoints is missed (better luck next time).
|
||||
gData, _ := (*dest).(metricdata.Gauge[N])
|
||||
// delta always clears values on collection
|
||||
readIdx := s.hcwg.swapHotAndWait()
|
||||
readIdx := s.vals.swapHotAndWait()
|
||||
// The len will not change while we iterate over values, since we waited
|
||||
// for all writes to finish to the cold values and len.
|
||||
n := s.hotColdValMap[readIdx].values.Len()
|
||||
n := s.vals.Len(readIdx)
|
||||
dPts := reset(gData.DataPoints, n, n)
|
||||
|
||||
var i int
|
||||
s.hotColdValMap[readIdx].values.Range(func(_, value any) bool {
|
||||
s.vals.Range(readIdx, func(_, value any) bool {
|
||||
v := value.(*lastValuePoint[N])
|
||||
dPts[i].Attributes = v.attrs
|
||||
dPts[i].StartTime = s.start
|
||||
@@ -127,17 +105,19 @@ func (s *deltaLastValue[N]) copyAndClearDpts(
|
||||
i++
|
||||
return true
|
||||
})
|
||||
gData.DataPoints = dPts
|
||||
// Do not report stale values.
|
||||
s.hotColdValMap[readIdx].values.Clear()
|
||||
s.vals.Clear(readIdx)
|
||||
|
||||
gData.DataPoints = dPts
|
||||
*dest = gData
|
||||
return i
|
||||
}
|
||||
|
||||
// cumulativeLastValue summarizes a set of measurements as the last one made.
|
||||
type cumulativeLastValue[N int64 | float64] struct {
|
||||
lastValueMap[N]
|
||||
start time.Time
|
||||
values limitedSyncMap[*lastValuePoint[N]]
|
||||
newRes func(attribute.Set) FilteredExemplarReservoir[N]
|
||||
start time.Time
|
||||
}
|
||||
|
||||
func newCumulativeLastValue[N int64 | float64](
|
||||
@@ -145,11 +125,33 @@ func newCumulativeLastValue[N int64 | float64](
|
||||
r func(attribute.Set) FilteredExemplarReservoir[N],
|
||||
) *cumulativeLastValue[N] {
|
||||
return &cumulativeLastValue[N]{
|
||||
lastValueMap: lastValueMap[N]{
|
||||
newRes: r,
|
||||
values: limitedSyncMap[*lastValuePoint[N]]{aggLimit: limit},
|
||||
},
|
||||
start: now(),
|
||||
values: limitedSyncMap[*lastValuePoint[N]]{aggLimit: limit},
|
||||
newRes: r,
|
||||
start: now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *cumulativeLastValue[N]) measure(
|
||||
ctx context.Context,
|
||||
value N,
|
||||
lazy lazyFilteredAttributes,
|
||||
) {
|
||||
lv := s.values.LoadOrStoreAttr(lazy, func(attr attribute.Set) *lastValuePoint[N] {
|
||||
r := s.newRes(attr)
|
||||
_, isDrop := r.(*dropRes[N])
|
||||
p := &lastValuePoint[N]{
|
||||
res: r,
|
||||
attrs: attr,
|
||||
startTime: now(),
|
||||
dropExemplars: isDrop,
|
||||
}
|
||||
p.value.Store(value)
|
||||
return p
|
||||
})
|
||||
|
||||
lv.value.Store(value)
|
||||
if !lv.dropExemplars {
|
||||
lv.res.Offer(ctx, value, lazy)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+71
-70
@@ -20,17 +20,37 @@ type sumValue[N int64 | float64] struct {
|
||||
dropExemplars bool
|
||||
}
|
||||
|
||||
type sumValueMap[N int64 | float64] struct {
|
||||
newRes func(attribute.Set) FilteredExemplarReservoir[N]
|
||||
values limitedSyncMap[*sumValue[N]]
|
||||
// newDeltaSum returns an aggregator that summarizes a set of measurements as
|
||||
// their arithmetic sum. Each sum is scoped by attributes and the aggregation
|
||||
// cycle the measurements were made in.
|
||||
func newDeltaSum[N int64 | float64](
|
||||
monotonic bool,
|
||||
limit int,
|
||||
r func(attribute.Set) FilteredExemplarReservoir[N],
|
||||
) *deltaSum[N] {
|
||||
s := &deltaSum[N]{
|
||||
monotonic: monotonic,
|
||||
start: now(),
|
||||
newRes: r,
|
||||
}
|
||||
s.vals.init(limit)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *sumValueMap[N]) measure(
|
||||
ctx context.Context,
|
||||
value N,
|
||||
lazy lazyFilteredAttributes,
|
||||
) {
|
||||
sv := s.values.LoadOrStoreAttr(lazy, func(attr attribute.Set) *sumValue[N] {
|
||||
// deltaSum is the storage for sums which resets every collection interval.
|
||||
type deltaSum[N int64 | float64] struct {
|
||||
monotonic bool
|
||||
start time.Time
|
||||
|
||||
vals hotColdMap[*sumValue[N]]
|
||||
newRes func(attribute.Set) FilteredExemplarReservoir[N]
|
||||
}
|
||||
|
||||
func (s *deltaSum[N]) measure(ctx context.Context, value N, lazy lazyFilteredAttributes) {
|
||||
hotIdx := s.vals.start()
|
||||
defer s.vals.done(hotIdx)
|
||||
|
||||
sv := s.vals.hot(hotIdx).LoadOrStoreAttr(lazy, func(attr attribute.Set) *sumValue[N] {
|
||||
r := s.newRes(attr)
|
||||
_, isDrop := r.(*dropRes[N])
|
||||
return &sumValue[N]{
|
||||
@@ -41,53 +61,11 @@ func (s *sumValueMap[N]) measure(
|
||||
}
|
||||
})
|
||||
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.
|
||||
if !sv.dropExemplars {
|
||||
sv.res.Offer(ctx, value, lazy)
|
||||
}
|
||||
}
|
||||
|
||||
// newDeltaSum returns an aggregator that summarizes a set of measurements as
|
||||
// their arithmetic sum. Each sum is scoped by attributes and the aggregation
|
||||
// cycle the measurements were made in.
|
||||
func newDeltaSum[N int64 | float64](
|
||||
monotonic bool,
|
||||
limit int,
|
||||
r func(attribute.Set) FilteredExemplarReservoir[N],
|
||||
) *deltaSum[N] {
|
||||
return &deltaSum[N]{
|
||||
monotonic: monotonic,
|
||||
start: now(),
|
||||
hotColdValMap: [2]sumValueMap[N]{
|
||||
{
|
||||
newRes: r,
|
||||
values: limitedSyncMap[*sumValue[N]]{aggLimit: limit},
|
||||
},
|
||||
{
|
||||
newRes: r,
|
||||
values: limitedSyncMap[*sumValue[N]]{aggLimit: limit},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// deltaSum is the storage for sums which resets every collection interval.
|
||||
type deltaSum[N int64 | float64] struct {
|
||||
monotonic bool
|
||||
start time.Time
|
||||
|
||||
hcwg hotColdWaitGroup
|
||||
hotColdValMap [2]sumValueMap[N]
|
||||
}
|
||||
|
||||
func (s *deltaSum[N]) measure(ctx context.Context, value N, lazy lazyFilteredAttributes) {
|
||||
hotIdx := s.hcwg.start()
|
||||
defer s.hcwg.done(hotIdx)
|
||||
s.hotColdValMap[hotIdx].measure(ctx, value, lazy)
|
||||
}
|
||||
|
||||
func (s *deltaSum[N]) collect(
|
||||
dest *metricdata.Aggregation, //nolint:gocritic // The pointer is needed for the ComputeAggregation interface
|
||||
) int {
|
||||
@@ -100,14 +78,14 @@ func (s *deltaSum[N]) collect(
|
||||
sData.IsMonotonic = s.monotonic
|
||||
|
||||
// delta always clears values on collection
|
||||
readIdx := s.hcwg.swapHotAndWait()
|
||||
readIdx := s.vals.swapHotAndWait()
|
||||
// The len will not change while we iterate over values, since we waited
|
||||
// for all writes to finish to the cold values and len.
|
||||
n := s.hotColdValMap[readIdx].values.Len()
|
||||
n := s.vals.Len(readIdx)
|
||||
dPts := reset(sData.DataPoints, n, n)
|
||||
|
||||
var i int
|
||||
s.hotColdValMap[readIdx].values.Range(func(_, value any) bool {
|
||||
s.vals.Range(readIdx, func(_, value any) bool {
|
||||
val := value.(*sumValue[N])
|
||||
collectExemplars(&dPts[i].Exemplars, val.res.Collect)
|
||||
dPts[i].Attributes = val.attrs
|
||||
@@ -117,7 +95,9 @@ func (s *deltaSum[N]) collect(
|
||||
i++
|
||||
return true
|
||||
})
|
||||
s.hotColdValMap[readIdx].values.Clear()
|
||||
// Unused attribute sets do not report.
|
||||
s.vals.Clear(readIdx)
|
||||
|
||||
// The delta collection cycle resets.
|
||||
s.start = t
|
||||
|
||||
@@ -138,19 +118,38 @@ func newCumulativeSum[N int64 | float64](
|
||||
return &cumulativeSum[N]{
|
||||
monotonic: monotonic,
|
||||
start: now(),
|
||||
sumValueMap: sumValueMap[N]{
|
||||
newRes: r,
|
||||
values: limitedSyncMap[*sumValue[N]]{aggLimit: limit},
|
||||
},
|
||||
values: limitedSyncMap[*sumValue[N]]{aggLimit: limit},
|
||||
newRes: r,
|
||||
}
|
||||
}
|
||||
|
||||
// deltaSum is the storage for sums which never reset.
|
||||
// cumulativeSum is the storage for sums which never reset.
|
||||
type cumulativeSum[N int64 | float64] struct {
|
||||
monotonic bool
|
||||
start time.Time
|
||||
|
||||
sumValueMap[N]
|
||||
values limitedSyncMap[*sumValue[N]]
|
||||
newRes func(attribute.Set) FilteredExemplarReservoir[N]
|
||||
}
|
||||
|
||||
func (s *cumulativeSum[N]) measure(ctx context.Context, value N, lazy lazyFilteredAttributes) {
|
||||
sv := s.values.LoadOrStoreAttr(lazy, func(attr attribute.Set) *sumValue[N] {
|
||||
r := s.newRes(attr)
|
||||
_, isDrop := r.(*dropRes[N])
|
||||
return &sumValue[N]{
|
||||
res: r,
|
||||
attrs: attr,
|
||||
startTime: now(),
|
||||
dropExemplars: isDrop,
|
||||
}
|
||||
})
|
||||
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.
|
||||
if !sv.dropExemplars {
|
||||
sv.res.Offer(ctx, value, lazy)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *cumulativeSum[N]) collect(
|
||||
@@ -209,7 +208,7 @@ func newPrecomputedSum[N int64 | float64](
|
||||
r func(attribute.Set) FilteredExemplarReservoir[N],
|
||||
) *precomputedSum[N] {
|
||||
return &precomputedSum[N]{
|
||||
deltaSum: newDeltaSum(monotonic, limit, r),
|
||||
deltaSum: newDeltaSum[N](monotonic, limit, r),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,14 +232,14 @@ func (s *precomputedSum[N]) delta(
|
||||
sData.IsMonotonic = s.monotonic
|
||||
|
||||
// delta always clears values on collection
|
||||
readIdx := s.hcwg.swapHotAndWait()
|
||||
readIdx := s.vals.swapHotAndWait()
|
||||
// The len will not change while we iterate over values, since we waited
|
||||
// for all writes to finish to the cold values and len.
|
||||
n := s.hotColdValMap[readIdx].values.Len()
|
||||
n := s.vals.Len(readIdx)
|
||||
dPts := reset(sData.DataPoints, n, n)
|
||||
|
||||
var i int
|
||||
s.hotColdValMap[readIdx].values.Range(func(key, value any) bool {
|
||||
s.vals.Range(readIdx, func(key, value any) bool {
|
||||
val := value.(*sumValue[N])
|
||||
n := val.n.load()
|
||||
|
||||
@@ -254,7 +253,8 @@ func (s *precomputedSum[N]) delta(
|
||||
i++
|
||||
return true
|
||||
})
|
||||
s.hotColdValMap[readIdx].values.Clear()
|
||||
// Unused attribute sets do not report.
|
||||
s.vals.Clear(readIdx)
|
||||
s.reported = newReported
|
||||
// The delta collection cycle resets.
|
||||
s.start = t
|
||||
@@ -277,14 +277,14 @@ func (s *precomputedSum[N]) cumulative(
|
||||
sData.IsMonotonic = s.monotonic
|
||||
|
||||
// cumulative precomputed always clears values on collection
|
||||
readIdx := s.hcwg.swapHotAndWait()
|
||||
readIdx := s.vals.swapHotAndWait()
|
||||
// The len will not change while we iterate over values, since we waited
|
||||
// for all writes to finish to the cold values and len.
|
||||
n := s.hotColdValMap[readIdx].values.Len()
|
||||
n := s.vals.Len(readIdx)
|
||||
dPts := reset(sData.DataPoints, n, n)
|
||||
|
||||
var i int
|
||||
s.hotColdValMap[readIdx].values.Range(func(_, value any) bool {
|
||||
s.vals.Range(readIdx, func(_, value any) bool {
|
||||
val := value.(*sumValue[N])
|
||||
collectExemplars(&dPts[i].Exemplars, val.res.Collect)
|
||||
dPts[i].Attributes = val.attrs
|
||||
@@ -294,7 +294,8 @@ func (s *precomputedSum[N]) cumulative(
|
||||
i++
|
||||
return true
|
||||
})
|
||||
s.hotColdValMap[readIdx].values.Clear()
|
||||
// Unused attribute sets do not report.
|
||||
s.vals.Clear(readIdx)
|
||||
|
||||
sData.DataPoints = dPts
|
||||
*dest = sData
|
||||
|
||||
+30
-22
@@ -28,40 +28,43 @@ type rawValue struct {
|
||||
slice any
|
||||
}
|
||||
|
||||
// Value returns value with all map values deduplicated and whether it changed.
|
||||
// ValueDedup returns value with all map values deduplicated and whether
|
||||
// it changed.
|
||||
//
|
||||
// Duplicate map keys are resolved using last-value-wins semantics.
|
||||
func Value(value attribute.Value) (attribute.Value, bool) {
|
||||
func ValueDedup(value attribute.Value) (attribute.Value, bool) {
|
||||
switch value.Type() {
|
||||
case attribute.SLICE:
|
||||
return deduplicateSliceValue(value)
|
||||
return sliceValueDedup(value)
|
||||
case attribute.MAP:
|
||||
return deduplicateMapValue(value)
|
||||
return mapValueDedup(value)
|
||||
default:
|
||||
return value, false
|
||||
}
|
||||
}
|
||||
|
||||
// KeyValue returns kv with all map values deduplicated and whether it changed.
|
||||
func KeyValue(kv attribute.KeyValue) (attribute.KeyValue, bool) {
|
||||
value, changed := Value(kv.Value)
|
||||
// KeyValueDedup returns kv with all map values deduplicated and whether
|
||||
// it changed.
|
||||
func KeyValueDedup(kv attribute.KeyValue) (attribute.KeyValue, bool) {
|
||||
value, changed := ValueDedup(kv.Value)
|
||||
if changed {
|
||||
kv.Value = value
|
||||
}
|
||||
return kv, changed
|
||||
}
|
||||
|
||||
// KeyValues returns kvs with all map values deduplicated and whether they changed.
|
||||
// KeyValuesDedup returns kvs with all map values deduplicated and whether
|
||||
// they changed.
|
||||
//
|
||||
// The returned slice is the original kvs slice if no value needs
|
||||
// deduplication. Top-level keys in kvs are not deduplicated.
|
||||
func KeyValues(kvs []attribute.KeyValue) ([]attribute.KeyValue, bool) {
|
||||
func KeyValuesDedup(kvs []attribute.KeyValue) ([]attribute.KeyValue, bool) {
|
||||
// Preserve the caller's slice on the common no-op path. Once a changed
|
||||
// value is found, copy the prior values exactly once and fill the rest in
|
||||
// place as the scan continues.
|
||||
var normalized []attribute.KeyValue
|
||||
for i, kv := range kvs {
|
||||
kv, changed := KeyValue(kv)
|
||||
kv, changed := KeyValueDedup(kv)
|
||||
if normalized != nil {
|
||||
normalized[i] = kv
|
||||
continue
|
||||
@@ -80,22 +83,24 @@ func KeyValues(kvs []attribute.KeyValue) ([]attribute.KeyValue, bool) {
|
||||
return normalized, true
|
||||
}
|
||||
|
||||
// Set returns set with all map values deduplicated and whether it changed.
|
||||
// SetDedup returns set with all map values deduplicated and whether it
|
||||
// changed.
|
||||
//
|
||||
// The returned Set is the original set if no value needs deduplication.
|
||||
// Top-level key uniqueness remains attribute.Set's responsibility; this only
|
||||
// normalizes map attribute values.
|
||||
func Set(set attribute.Set) (attribute.Set, bool) {
|
||||
if set.Len() == 0 {
|
||||
func SetDedup(set attribute.Set) (attribute.Set, bool) {
|
||||
length := set.Len()
|
||||
if length == 0 {
|
||||
return set, false
|
||||
}
|
||||
|
||||
// Most attribute sets contain no duplicate map keys. Delay allocation until
|
||||
// the first changed value so the no-op path returns the original Set.
|
||||
var normalized []attribute.KeyValue
|
||||
for i := range set.Len() {
|
||||
for i := range length {
|
||||
kv, _ := set.Get(i)
|
||||
kv, changed := KeyValue(kv)
|
||||
kv, changed := KeyValueDedup(kv)
|
||||
if normalized != nil {
|
||||
normalized = append(normalized, kv)
|
||||
continue
|
||||
@@ -104,7 +109,7 @@ func Set(set attribute.Set) (attribute.Set, bool) {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.KeyValue, 0, set.Len())
|
||||
normalized = make([]attribute.KeyValue, 0, length)
|
||||
for j := range i {
|
||||
prior, _ := set.Get(j)
|
||||
normalized = append(normalized, prior)
|
||||
@@ -118,7 +123,7 @@ func Set(set attribute.Set) (attribute.Set, bool) {
|
||||
return attribute.NewSet(normalized...), true
|
||||
}
|
||||
|
||||
func deduplicateSliceValue(value attribute.Value) (attribute.Value, bool) {
|
||||
func sliceValueDedup(value attribute.Value) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
length := valueLen(storage)
|
||||
|
||||
@@ -127,7 +132,7 @@ func deduplicateSliceValue(value attribute.Value) (attribute.Value, bool) {
|
||||
var normalized []attribute.Value
|
||||
for i := range length {
|
||||
elem := valueAt(storage, i)
|
||||
elem, changed := Value(elem)
|
||||
elem, changed := ValueDedup(elem)
|
||||
if normalized != nil {
|
||||
normalized[i] = elem
|
||||
continue
|
||||
@@ -148,14 +153,14 @@ func deduplicateSliceValue(value attribute.Value) (attribute.Value, bool) {
|
||||
return attribute.SliceValue(normalized...), true
|
||||
}
|
||||
|
||||
func deduplicateMapValue(value attribute.Value) (attribute.Value, bool) {
|
||||
func mapValueDedup(value attribute.Value) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
length := keyValueLen(storage)
|
||||
if length <= 1 {
|
||||
// A single map entry cannot duplicate its own key, but its value might
|
||||
// contain a map or slice that needs recursive normalization.
|
||||
if length == 1 {
|
||||
kv, changed := KeyValue(keyValueAt(storage, 0))
|
||||
kv, changed := KeyValueDedup(keyValueAt(storage, 0))
|
||||
if changed {
|
||||
return attribute.MapValue(kv), true
|
||||
}
|
||||
@@ -174,13 +179,16 @@ func deduplicateMapValue(value attribute.Value) (attribute.Value, bool) {
|
||||
j++
|
||||
}
|
||||
|
||||
kv, nestedChanged := KeyValue(keyValueAt(storage, j-1))
|
||||
kv, nestedChanged := KeyValueDedup(keyValueAt(storage, j-1))
|
||||
// j-i > 1 means the current key run contained duplicates.
|
||||
changed := nestedChanged || j-i > 1
|
||||
if normalized != nil {
|
||||
normalized = append(normalized, kv)
|
||||
} else if changed {
|
||||
normalized = make([]attribute.KeyValue, 0, length)
|
||||
if i == 0 && j == length {
|
||||
return attribute.MapValue(kv), true
|
||||
}
|
||||
normalized = make([]attribute.KeyValue, 0, length-(j-i-1))
|
||||
for k := range i {
|
||||
normalized = append(normalized, keyValueAt(storage, k))
|
||||
}
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package attrnorm
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"slices"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
// stackAttributes is the maximum number of unordered attributes copied to a
|
||||
// stack buffer for canonicalization. Larger inputs use an allocated copy to
|
||||
// keep sorting time bounded without modifying caller-owned attributes.
|
||||
const stackAttributes = 10
|
||||
|
||||
// NewDistinct returns the identity of kvs after normalization and filtering.
|
||||
// It does not retain or modify kvs. Duplicate keys use the last value supplied.
|
||||
// The filter is called once for each unique normalized attribute in ascending
|
||||
// key order.
|
||||
func NewDistinct(kvs []attribute.KeyValue, filter attribute.Filter) attribute.Distinct {
|
||||
if filter == nil {
|
||||
if distinct, ok := tryUnfilteredDistinct(kvs); ok {
|
||||
return distinct
|
||||
}
|
||||
} else if ordered(kvs) {
|
||||
return orderedDistinct(kvs, filter)
|
||||
}
|
||||
if len(kvs) <= stackAttributes {
|
||||
return sortedDistinct(kvs, filter)
|
||||
}
|
||||
return allocatedDistinct(kvs, filter)
|
||||
}
|
||||
|
||||
// tryUnfilteredDistinct hashes kvs while verifying their keys are ordered. The
|
||||
// partial hash is discarded if an out-of-order key is found.
|
||||
func tryUnfilteredDistinct(kvs []attribute.KeyValue) (attribute.Distinct, bool) {
|
||||
hasher := attribute.NewHasher()
|
||||
if len(kvs) == 0 {
|
||||
return hasher.Distinct(), true
|
||||
}
|
||||
|
||||
// pending identifies the last value seen for the current key.
|
||||
pending := 0
|
||||
for i := 1; i < len(kvs); i++ {
|
||||
if kvs[i].Key < kvs[pending].Key {
|
||||
return attribute.Distinct{}, false
|
||||
}
|
||||
if kvs[i].Key == kvs[pending].Key {
|
||||
pending = i
|
||||
continue
|
||||
}
|
||||
|
||||
normalized, _ := KeyValueDedup(kvs[pending])
|
||||
hasher.Write(normalized)
|
||||
pending = i
|
||||
}
|
||||
|
||||
normalized, _ := KeyValueDedup(kvs[pending])
|
||||
hasher.Write(normalized)
|
||||
return hasher.Distinct(), true
|
||||
}
|
||||
|
||||
// Keep this direct loop instead of slices.IsSortedFunc. It only needs a
|
||||
// greater-than comparison and benchmarks faster for typical attribute counts.
|
||||
func ordered(kvs []attribute.KeyValue) bool {
|
||||
for i := 1; i < len(kvs); i++ {
|
||||
if kvs[i-1].Key > kvs[i].Key {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// orderedDistinct hashes key-values whose keys are in ascending order. Equal
|
||||
// keys remain in input order so selecting the last retains last-value-wins.
|
||||
func orderedDistinct(kvs []attribute.KeyValue, filter attribute.Filter) attribute.Distinct {
|
||||
hasher := attribute.NewHasher()
|
||||
for i := 0; i < len(kvs); {
|
||||
j := i + 1
|
||||
for j < len(kvs) && kvs[j].Key == kvs[i].Key {
|
||||
j++
|
||||
}
|
||||
kv, _ := KeyValueDedup(kvs[j-1])
|
||||
if filter == nil || filter(kv) {
|
||||
hasher.Write(kv)
|
||||
}
|
||||
i = j
|
||||
}
|
||||
return hasher.Distinct()
|
||||
}
|
||||
|
||||
func sortedDistinct(kvs []attribute.KeyValue, filter attribute.Filter) attribute.Distinct {
|
||||
var scratch [stackAttributes]attribute.KeyValue
|
||||
cpy := scratch[:len(kvs)]
|
||||
copy(cpy, kvs)
|
||||
return sortDistinct(cpy, filter)
|
||||
}
|
||||
|
||||
func allocatedDistinct(kvs []attribute.KeyValue, filter attribute.Filter) attribute.Distinct {
|
||||
return sortDistinct(slices.Clone(kvs), filter)
|
||||
}
|
||||
|
||||
func sortDistinct(kvs []attribute.KeyValue, filter attribute.Filter) attribute.Distinct {
|
||||
slices.SortStableFunc(kvs, func(a, b attribute.KeyValue) int {
|
||||
return cmp.Compare(a.Key, b.Key)
|
||||
})
|
||||
return orderedDistinct(kvs, filter)
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package finish coordinates the lifecycle of finish-aware metric series.
|
||||
package finish
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package finish
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type lifecycleState uint32
|
||||
|
||||
const (
|
||||
lifecycleActive lifecycleState = iota
|
||||
lifecycleShared
|
||||
lifecycleFinishPending
|
||||
lifecycleRetired
|
||||
lifecycleCollecting
|
||||
)
|
||||
|
||||
// Lifecycle coordinates measurements, explicit finishes, collections, and
|
||||
// retirement for one metric series. Its zero value is ready for use.
|
||||
//
|
||||
// Lifecycle methods are safe to call concurrently. A successful measurement
|
||||
// acquisition and an emitted collection must each be completed exactly once.
|
||||
// A Lifecycle must not be copied after first use.
|
||||
//
|
||||
// The valid transitions are active to finish-pending on Finish, active or
|
||||
// finish-pending to shared on a shared measurement, finish-pending to active on
|
||||
// an ordinary measurement, any non-retired state to collecting on collection,
|
||||
// and collecting to its prior state or retired when collection completes.
|
||||
// Shared lifetimes ignore Finish. Retired is terminal.
|
||||
type Lifecycle struct {
|
||||
control sync.Mutex
|
||||
state atomic.Uint32
|
||||
writers atomic.Uint64
|
||||
// finished is meaningful only while a pending finish is being collected.
|
||||
// Reactivation leaves the old value in place so measurements remain lock-free.
|
||||
finished time.Time
|
||||
}
|
||||
|
||||
// Measurement represents admission to update one metric-series lifetime. A
|
||||
// successful call to Lifecycle.AcquireMeasurement returns a Measurement, which
|
||||
// must be released exactly once after the aggregate and exemplars are updated.
|
||||
type Measurement struct {
|
||||
lifecycle *Lifecycle
|
||||
}
|
||||
|
||||
// Release completes the admitted measurement.
|
||||
func (m Measurement) Release() {
|
||||
m.lifecycle.releaseMeasurement()
|
||||
}
|
||||
|
||||
// Collection describes an exclusive collection phase for a metric-series
|
||||
// lifetime. A Collection returned with true by a Lifecycle collection method
|
||||
// must be completed exactly once after the aggregate and exemplars are read.
|
||||
type Collection struct {
|
||||
lifecycle *Lifecycle
|
||||
time time.Time
|
||||
restore lifecycleState
|
||||
retire bool
|
||||
}
|
||||
|
||||
// Time returns the timestamp to use for the collected point.
|
||||
func (c Collection) Time() time.Time {
|
||||
return c.time
|
||||
}
|
||||
|
||||
// ShouldRetire reports whether Complete will retire the series lifetime.
|
||||
func (c Collection) ShouldRetire() bool {
|
||||
return c.retire
|
||||
}
|
||||
|
||||
// Complete ends the exclusive collection phase. It must be called exactly once.
|
||||
func (c Collection) Complete() {
|
||||
state := c.restore
|
||||
if c.retire {
|
||||
state = lifecycleRetired
|
||||
}
|
||||
c.lifecycle.finished = time.Time{}
|
||||
c.lifecycle.state.Store(uint32(state))
|
||||
c.lifecycle.control.Unlock()
|
||||
}
|
||||
|
||||
// AcquireMeasurement reserves this series lifetime for a measurement. It waits
|
||||
// for an in-progress collection and returns false only if the lifetime is
|
||||
// retired. A successful acquisition must be paired with exactly one call to
|
||||
// Measurement.Release after the aggregate and exemplars are updated.
|
||||
func (l *Lifecycle) AcquireMeasurement() (Measurement, bool) {
|
||||
for {
|
||||
// Increment before checking state so a collection either waits for this
|
||||
// attempt or closes admission before the attempt can update the aggregate.
|
||||
l.writers.Add(1)
|
||||
switch lifecycleState(l.state.Load()) {
|
||||
case lifecycleActive, lifecycleShared:
|
||||
return Measurement{lifecycle: l}, true
|
||||
case lifecycleFinishPending:
|
||||
if l.state.CompareAndSwap(
|
||||
uint32(lifecycleFinishPending),
|
||||
uint32(lifecycleActive),
|
||||
) {
|
||||
return Measurement{lifecycle: l}, true
|
||||
}
|
||||
l.releaseMeasurement()
|
||||
case lifecycleCollecting:
|
||||
l.releaseMeasurement()
|
||||
l.waitForCollection()
|
||||
case lifecycleRetired:
|
||||
l.releaseMeasurement()
|
||||
return Measurement{}, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AcquireSharedMeasurement reserves this series lifetime for a measurement and
|
||||
// atomically makes the lifetime shared. A shared lifetime cannot be finished.
|
||||
// It waits for an in-progress collection and returns false only if the lifetime
|
||||
// is retired. A successful acquisition must be paired with exactly one call to
|
||||
// Measurement.Release after the aggregate and exemplars are updated.
|
||||
func (l *Lifecycle) AcquireSharedMeasurement() (Measurement, bool) {
|
||||
for {
|
||||
// Increment before checking state so a collection either waits for this
|
||||
// attempt or closes admission before the attempt can update the aggregate.
|
||||
l.writers.Add(1)
|
||||
switch lifecycleState(l.state.Load()) {
|
||||
case lifecycleShared:
|
||||
return Measurement{lifecycle: l}, true
|
||||
case lifecycleActive:
|
||||
if l.state.CompareAndSwap(
|
||||
uint32(lifecycleActive),
|
||||
uint32(lifecycleShared),
|
||||
) {
|
||||
return Measurement{lifecycle: l}, true
|
||||
}
|
||||
l.releaseMeasurement()
|
||||
case lifecycleFinishPending:
|
||||
if l.state.CompareAndSwap(
|
||||
uint32(lifecycleFinishPending),
|
||||
uint32(lifecycleShared),
|
||||
) {
|
||||
return Measurement{lifecycle: l}, true
|
||||
}
|
||||
l.releaseMeasurement()
|
||||
case lifecycleCollecting:
|
||||
l.releaseMeasurement()
|
||||
l.waitForCollection()
|
||||
case lifecycleRetired:
|
||||
l.releaseMeasurement()
|
||||
return Measurement{}, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lifecycle) waitForCollection() {
|
||||
l.control.Lock()
|
||||
l.control.Unlock() //nolint:gocritic,staticcheck // Lock acquisition parks until collection completes.
|
||||
}
|
||||
|
||||
func (l *Lifecycle) releaseMeasurement() {
|
||||
// atomic.Uint64 has no subtraction operation. Adding MaxUint64 decrements
|
||||
// the writer count modulo 2^64. A matching acquisition guarantees the count
|
||||
// is nonzero.
|
||||
l.writers.Add(^uint64(0))
|
||||
}
|
||||
|
||||
// Finish marks an active series lifetime to be retired by its next collection
|
||||
// at the supplied time. It returns false if the lifetime is already pending,
|
||||
// shared, collecting, or retired. A measurement admitted after Finish
|
||||
// reactivates the lifetime.
|
||||
func (l *Lifecycle) Finish(at time.Time) bool {
|
||||
l.control.Lock()
|
||||
defer l.control.Unlock()
|
||||
if !l.state.CompareAndSwap(
|
||||
uint32(lifecycleActive),
|
||||
uint32(lifecycleFinishPending),
|
||||
) {
|
||||
return false
|
||||
}
|
||||
// Collection also holds control, so it cannot observe finish-pending before
|
||||
// its timestamp is available. A concurrent measurement may reactivate or
|
||||
// share the lifetime; in that case the timestamp is intentionally ignored.
|
||||
l.finished = at
|
||||
return true
|
||||
}
|
||||
|
||||
// BeginCumulativeCollection closes measurement admission and waits for admitted
|
||||
// measurements to complete. An active or shared lifetime returns to its prior
|
||||
// state when the returned Collection is completed; a finish-pending lifetime is
|
||||
// retired. It returns false without a Collection if the lifetime is retired. A
|
||||
// returned Collection must be completed exactly once.
|
||||
func (l *Lifecycle) BeginCumulativeCollection(at time.Time) (Collection, bool) {
|
||||
l.control.Lock()
|
||||
collection, ok := l.beginCollection(at)
|
||||
if !ok {
|
||||
l.control.Unlock()
|
||||
}
|
||||
return collection, ok
|
||||
}
|
||||
|
||||
// BeginDeltaCollection closes measurement admission and waits for admitted
|
||||
// measurements to complete. Every emitted lifetime is retired when the returned
|
||||
// Collection is completed. It returns false without a Collection if the
|
||||
// lifetime is retired. A returned Collection must be completed exactly once.
|
||||
func (l *Lifecycle) BeginDeltaCollection(at time.Time) (Collection, bool) {
|
||||
l.control.Lock()
|
||||
collection, ok := l.beginCollection(at)
|
||||
if !ok {
|
||||
l.control.Unlock()
|
||||
return Collection{}, false
|
||||
}
|
||||
collection.retire = true
|
||||
return collection, true
|
||||
}
|
||||
|
||||
func (l *Lifecycle) beginCollection(at time.Time) (Collection, bool) {
|
||||
state := lifecycleState(l.state.Load())
|
||||
if state == lifecycleRetired {
|
||||
return Collection{}, false
|
||||
}
|
||||
state = lifecycleState(l.state.Swap(uint32(lifecycleCollecting)))
|
||||
for l.writers.Load() != 0 {
|
||||
runtime.Gosched()
|
||||
}
|
||||
|
||||
if state == lifecycleFinishPending {
|
||||
return Collection{
|
||||
lifecycle: l,
|
||||
time: l.finished,
|
||||
restore: state,
|
||||
retire: true,
|
||||
}, true
|
||||
}
|
||||
return Collection{lifecycle: l, time: at, restore: state}, true
|
||||
}
|
||||
|
||||
// Retire permanently closes the series lifetime. It waits for admitted
|
||||
// measurements to complete and has no effect if the lifetime is already
|
||||
// retired.
|
||||
func (l *Lifecycle) Retire() {
|
||||
l.control.Lock()
|
||||
defer l.control.Unlock()
|
||||
if lifecycleState(l.state.Load()) == lifecycleRetired {
|
||||
return
|
||||
}
|
||||
l.state.Store(uint32(lifecycleCollecting))
|
||||
for l.writers.Load() != 0 {
|
||||
runtime.Gosched()
|
||||
}
|
||||
l.finished = time.Time{}
|
||||
l.state.Store(uint32(lifecycleRetired))
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
# 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.
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package x
|
||||
|
||||
import "strconv"
|
||||
|
||||
// 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(
|
||||
[]string{"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
|
||||
},
|
||||
)
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// DO NOT MODIFY. Generated by gotmpl.
|
||||
// source: internal/shared/x/x.go.tmpl
|
||||
|
||||
// Package x documents experimental features for [go.opentelemetry.io/otel/sdk/metric].
|
||||
package 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
|
||||
}
|
||||
+1
-1
@@ -190,7 +190,7 @@ type ExponentialHistogramDataPoint[N int64 | float64] struct {
|
||||
type ExponentialBucket struct {
|
||||
// Offset is the bucket index of the first entry in the Counts slice.
|
||||
Offset int32
|
||||
// Counts is an slice where Counts[i] carries the count of the bucket at
|
||||
// Counts is a slice where Counts[i] carries the count of the bucket at
|
||||
// index (Offset+i). Counts[i] is the count of values greater than
|
||||
// base^(Offset+i) and less than or equal to base^(Offset+i+1).
|
||||
Counts []uint64
|
||||
|
||||
+22
-9
@@ -14,7 +14,6 @@ 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.43.0"
|
||||
)
|
||||
@@ -29,6 +28,7 @@ const (
|
||||
type periodicReaderConfig struct {
|
||||
interval time.Duration
|
||||
timeout time.Duration
|
||||
maxExportBatchSize int
|
||||
producers []Producer
|
||||
cardinalityLimitSelector CardinalityLimitSelector
|
||||
}
|
||||
@@ -99,6 +99,21 @@ func WithInterval(d time.Duration) PeriodicReaderOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithMaxExportBatchSize configures the maximum number of metric data points in
|
||||
// a batch that are exported at once.
|
||||
//
|
||||
// If this option is not used or size is less than or equal to zero, no limit is
|
||||
// applied.
|
||||
func WithMaxExportBatchSize(size int) PeriodicReaderOption {
|
||||
return periodicReaderOptionFunc(func(conf periodicReaderConfig) periodicReaderConfig {
|
||||
if size <= 0 {
|
||||
return conf
|
||||
}
|
||||
conf.maxExportBatchSize = size
|
||||
return conf
|
||||
})
|
||||
}
|
||||
|
||||
// NewPeriodicReader returns a Reader that collects and exports metric data to
|
||||
// the exporter at a defined interval. By default, the returned Reader will
|
||||
// collect and export data every 60 seconds, and will cancel any attempts that
|
||||
@@ -116,6 +131,7 @@ func NewPeriodicReader(exporter Exporter, options ...PeriodicReaderOption) *Peri
|
||||
r := &PeriodicReader{
|
||||
interval: conf.interval,
|
||||
timeout: conf.timeout,
|
||||
batcher: batcher{size: conf.maxExportBatchSize},
|
||||
exporter: exporter,
|
||||
flushCh: make(chan chan error),
|
||||
cancel: cancel,
|
||||
@@ -127,16 +143,8 @@ 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() {
|
||||
defer func() { close(r.done) }()
|
||||
r.run(ctx, conf.interval)
|
||||
}()
|
||||
|
||||
var err error
|
||||
r.inst, err = observ.NewInstrumentation(
|
||||
semconv.OTelComponentTypePeriodicMetricReader.Value.AsString(),
|
||||
@@ -146,6 +154,11 @@ func NewPeriodicReader(exporter Exporter, options ...PeriodicReaderOption) *Peri
|
||||
otel.Handle(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() { close(r.done) }()
|
||||
r.run(ctx, conf.interval)
|
||||
}()
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
+11
-8
@@ -255,7 +255,7 @@ func (i *inserter[N]) Instrument(
|
||||
continue
|
||||
}
|
||||
matched = true
|
||||
in, id, e := i.cachedAggregator(inst.Scope, inst.Kind, stream, readerAggregation)
|
||||
in, id, e := i.cachedAggregator(inst.Scope, inst.Kind, stream, allowedKeys, readerAggregation)
|
||||
if e != nil {
|
||||
err = errors.Join(err, e)
|
||||
}
|
||||
@@ -284,13 +284,7 @@ func (i *inserter[N]) Instrument(
|
||||
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)
|
||||
in, _, e := i.cachedAggregator(inst.Scope, inst.Kind, stream, allowedKeys, readerAggregation)
|
||||
if e != nil {
|
||||
if err == nil {
|
||||
err = errCreatingAggregators
|
||||
@@ -364,6 +358,7 @@ func (i *inserter[N]) cachedAggregator(
|
||||
scope instrumentation.Scope,
|
||||
kind InstrumentKind,
|
||||
stream Stream,
|
||||
allowedKeys []attribute.Key,
|
||||
readerAggregation Aggregation,
|
||||
) (meas aggregate.Measure[N], aggID uint64, err error) {
|
||||
switch stream.Aggregation.(type) {
|
||||
@@ -378,6 +373,14 @@ func (i *inserter[N]) cachedAggregator(
|
||||
if stream.ExemplarReservoirProviderSelector == nil {
|
||||
stream.ExemplarReservoirProviderSelector = DefaultExemplarReservoirProviderSelector
|
||||
}
|
||||
// If attribute_keys is not provided on the view (AttributeFilter == nil),
|
||||
// use the advisory Attributes configured on the instrument (allowedKeys).
|
||||
// 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 stream.AttributeFilter == nil && allowedKeys != nil {
|
||||
stream.AttributeFilter = attribute.NewAllowKeysFilter(allowedKeys...)
|
||||
}
|
||||
|
||||
if err := isAggregatorCompatible(kind, stream.Aggregation); err != nil {
|
||||
return nil, 0, fmt.Errorf(
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ func (mp *MeterProvider) Meter(name string, options ...metric.MeterOption) metri
|
||||
}
|
||||
|
||||
c := metric.NewMeterConfig(options...)
|
||||
attrs, _ := attrnorm.Set(c.InstrumentationAttributes())
|
||||
attrs, _ := attrnorm.SetDedup(c.InstrumentationAttributes())
|
||||
s := instrumentation.Scope{
|
||||
Name: name,
|
||||
Version: c.InstrumentationVersion(),
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@ package metric
|
||||
|
||||
// version is the current release version of the metric SDK in use.
|
||||
func version() string {
|
||||
return "1.46.0"
|
||||
return "1.47.0"
|
||||
}
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ type View func(Instrument) (Stream, bool)
|
||||
// the Instrument the View matches against will be use for the Name,
|
||||
// Description, and Unit of the returned Stream and no Aggregation or
|
||||
// AttributeFilter are set. All non-zero-value fields of mask are used instead
|
||||
// of the default. If you need to zero out an Stream field returned from a
|
||||
// of the default. If you need to zero out a Stream field returned from a
|
||||
// View, create a View directly.
|
||||
func NewView(criteria Instrument, mask Stream) View {
|
||||
if criteria.IsEmpty() {
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ func NewSchemaless(attrs ...attribute.KeyValue) *Resource {
|
||||
return &Resource{}
|
||||
}
|
||||
|
||||
attrs, _ = attrnorm.KeyValues(attrs)
|
||||
attrs, _ = attrnorm.KeyValuesDedup(attrs)
|
||||
|
||||
// Ensure attributes comply with the specification:
|
||||
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/common/README.md#attribute
|
||||
|
||||
Generated
Vendored
+6
@@ -47,6 +47,8 @@ type BSP struct {
|
||||
processedQueueFullOpts []metric.AddOption
|
||||
}
|
||||
|
||||
// NewBSP creates new instrumentation for a BatchSpanProcessor.
|
||||
// It returns nil if observability is not enabled.
|
||||
func NewBSP(id int64, qLen func() int64, qMax int64) (*BSP, error) {
|
||||
if !x.Observability.Enabled() {
|
||||
return nil, nil
|
||||
@@ -108,12 +110,16 @@ func NewBSP(id int64, qLen func() int64, qMax int64) (*BSP, error) {
|
||||
}, err
|
||||
}
|
||||
|
||||
// Shutdown unregisters the callbacks registered by NewBSP.
|
||||
func (b *BSP) Shutdown() error { return b.reg.Unregister() }
|
||||
|
||||
// Processed records n spans as having finished processing successfully.
|
||||
func (b *BSP) Processed(ctx context.Context, n int64) {
|
||||
b.processed.Add(ctx, n, b.processedOpts...)
|
||||
}
|
||||
|
||||
// ProcessedQueueFull records n spans as having finished processing with a
|
||||
// queue-full error.
|
||||
func (b *BSP) ProcessedQueueFull(ctx context.Context, n int64) {
|
||||
b.processed.Add(ctx, n, b.processedQueueFullOpts...)
|
||||
}
|
||||
|
||||
+7
@@ -30,6 +30,8 @@ type Tracer struct {
|
||||
started metric.Int64Counter
|
||||
}
|
||||
|
||||
// NewTracer creates new instrumentation for a Tracer.
|
||||
// It returns an empty, disabled Tracer if observability is not enabled.
|
||||
func NewTracer() (Tracer, error) {
|
||||
if !x.Observability.Enabled() {
|
||||
return Tracer{}, nil
|
||||
@@ -52,8 +54,11 @@ func NewTracer() (Tracer, error) {
|
||||
return Tracer{enabled: true, live: l.Inst(), started: s.Inst()}, err
|
||||
}
|
||||
|
||||
// Enabled returns whether observability is enabled for t.
|
||||
func (t Tracer) Enabled() bool { return t.enabled }
|
||||
|
||||
// SpanStarted records the start of span, which was created with psc as its
|
||||
// parent span context.
|
||||
func (t Tracer) SpanStarted(ctx context.Context, psc trace.SpanContext, span trace.Span) {
|
||||
if !t.started.Enabled(ctx) {
|
||||
return
|
||||
@@ -84,10 +89,12 @@ func (t Tracer) SpanStarted(ctx context.Context, psc trace.SpanContext, span tra
|
||||
t.started.Add(ctx, 1, opts...)
|
||||
}
|
||||
|
||||
// SpanLive records span as a live span.
|
||||
func (t Tracer) SpanLive(ctx context.Context, span trace.Span) {
|
||||
t.spanLive(ctx, 1, span)
|
||||
}
|
||||
|
||||
// SpanEnded records span as no longer being live.
|
||||
func (t Tracer) SpanEnded(ctx context.Context, span trace.Span) {
|
||||
t.spanLive(ctx, -1, span)
|
||||
}
|
||||
|
||||
+48
-14
@@ -150,7 +150,10 @@ func (p *TracerProvider) Tracer(name string, opts ...trace.TracerOption) trace.T
|
||||
return noop.NewTracerProvider().Tracer(name, opts...)
|
||||
}
|
||||
c := trace.NewTracerConfig(opts...)
|
||||
attrs, _ := attrnorm.Set(c.InstrumentationAttributes())
|
||||
attrs, _, _ := attrnorm.SetDedupLimitDepth(
|
||||
c.InstrumentationAttributes(),
|
||||
p.spanLimits.AttributeValueDepthLimit,
|
||||
)
|
||||
if name == "" {
|
||||
name = defaultTracerName
|
||||
}
|
||||
@@ -255,13 +258,14 @@ func (p *TracerProvider) UnregisterSpanProcessor(sp SpanProcessor) {
|
||||
idx = i
|
||||
}
|
||||
}
|
||||
if stopOnce != nil {
|
||||
stopOnce.state.Do(func() {
|
||||
if err := sp.Shutdown(context.Background()); err != nil {
|
||||
otel.Handle(err)
|
||||
}
|
||||
})
|
||||
if stopOnce == nil {
|
||||
return
|
||||
}
|
||||
stopOnce.state.Do(func() {
|
||||
if err := sp.Shutdown(context.Background()); err != nil {
|
||||
otel.Handle(err)
|
||||
}
|
||||
})
|
||||
if len(spss) > 1 {
|
||||
copy(spss[idx:], spss[idx+1:])
|
||||
}
|
||||
@@ -429,6 +433,29 @@ func WithSampler(s Sampler) TracerProviderOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithAttributeValueDepthLimit sets the maximum allowed depth for attribute
|
||||
// values. Depth starts at one for the top-level value and increments when
|
||||
// descending into an array element or map value. An array or map beyond this
|
||||
// depth is replaced by an empty value.
|
||||
//
|
||||
// This limit applies to span, event, link, and instrumentation scope
|
||||
// attributes processed by this TracerProvider. It does not apply to Resource
|
||||
// attributes.
|
||||
//
|
||||
// Setting this to zero means the default limit of 64 is used. Setting this to
|
||||
// a negative value means no limit is applied.
|
||||
//
|
||||
// There is no environment variable for this limit.
|
||||
func WithAttributeValueDepthLimit(limit int) TracerProviderOption {
|
||||
if limit == 0 {
|
||||
limit = DefaultAttributeValueDepthLimit
|
||||
}
|
||||
return traceProviderOptionFunc(func(cfg tracerProviderConfig) tracerProviderConfig {
|
||||
cfg.spanLimits.AttributeValueDepthLimit = limit
|
||||
return cfg
|
||||
})
|
||||
}
|
||||
|
||||
// WithSpanLimits returns a TracerProviderOption that configures a
|
||||
// TracerProvider to use the SpanLimits sl. These SpanLimits bound any Span
|
||||
// created by a Tracer from the TracerProvider.
|
||||
@@ -448,6 +475,9 @@ func WithSpanLimits(sl SpanLimits) TracerProviderOption {
|
||||
if sl.AttributeValueLengthLimit <= 0 {
|
||||
sl.AttributeValueLengthLimit = DefaultAttributeValueLengthLimit
|
||||
}
|
||||
if sl.AttributeValueDepthLimit <= 0 {
|
||||
sl.AttributeValueDepthLimit = DefaultAttributeValueDepthLimit
|
||||
}
|
||||
if sl.AttributeCountLimit <= 0 {
|
||||
sl.AttributeCountLimit = DefaultAttributeCountLimit
|
||||
}
|
||||
@@ -473,13 +503,14 @@ func WithSpanLimits(sl SpanLimits) TracerProviderOption {
|
||||
// TracerProvider to use these limits. These limits bound any Span created by
|
||||
// a Tracer from the TracerProvider.
|
||||
//
|
||||
// The limits will be used as-is. Zero or negative values will not be changed
|
||||
// to the default value like WithSpanLimits does. Setting a limit to zero will
|
||||
// effectively disable the related resource it limits and setting to a
|
||||
// negative value will mean that resource is unlimited. Consequentially, this
|
||||
// means that the zero-value SpanLimits will disable all span resources.
|
||||
// Because of this, limits should be constructed using NewSpanLimits and
|
||||
// updated accordingly.
|
||||
// The limits will be used as-is, except that an AttributeValueDepthLimit of
|
||||
// zero means the default limit is used. Other zero or negative values will not
|
||||
// be changed to the default value like WithSpanLimits does. Setting a limit to
|
||||
// zero will effectively disable the related resource it limits and setting to
|
||||
// a negative value will mean that resource is unlimited. Consequentially, the
|
||||
// zero-value SpanLimits will disable all span resources except the attribute
|
||||
// value depth limit. Because of this, limits should be constructed using
|
||||
// NewSpanLimits and updated accordingly.
|
||||
//
|
||||
// If this or WithSpanLimits are not provided, the TracerProvider will use the
|
||||
// limits defined by environment variables, or the defaults if unset. Refer to
|
||||
@@ -525,5 +556,8 @@ func ensureValidTracerProviderConfig(cfg tracerProviderConfig) tracerProviderCon
|
||||
if cfg.resource == nil {
|
||||
cfg.resource = resource.Default()
|
||||
}
|
||||
if cfg.spanLimits.AttributeValueDepthLimit == 0 {
|
||||
cfg.spanLimits.AttributeValueDepthLimit = DefaultAttributeValueDepthLimit
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
+4
-5
@@ -46,14 +46,13 @@ func (e samplerArgParseError) Unwrap() error {
|
||||
}
|
||||
|
||||
func samplerFromEnv() (Sampler, error) {
|
||||
sampler, ok := os.LookupEnv(tracesSamplerKey)
|
||||
if !ok {
|
||||
sampler := strings.ToLower(strings.TrimSpace(os.Getenv(tracesSamplerKey)))
|
||||
if sampler == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sampler = strings.ToLower(strings.TrimSpace(sampler))
|
||||
samplerArg, hasSamplerArg := os.LookupEnv(tracesSamplerArgKey)
|
||||
samplerArg = strings.TrimSpace(samplerArg)
|
||||
samplerArg := strings.TrimSpace(os.Getenv(tracesSamplerArgKey))
|
||||
hasSamplerArg := samplerArg != ""
|
||||
|
||||
switch sampler {
|
||||
case samplerAlwaysOn:
|
||||
|
||||
+6
-1
@@ -104,10 +104,15 @@ func (ssp *simpleSpanProcessor) Shutdown(ctx context.Context) error {
|
||||
// span would need to be exported. Meaning, OnEnd would be called and
|
||||
// try acquiring the lock that is held here.
|
||||
ssp.exporterMu.Lock()
|
||||
done, shutdown := stopFunc(ssp.exporter)
|
||||
exp := ssp.exporter
|
||||
ssp.exporter = nil
|
||||
ssp.exporterMu.Unlock()
|
||||
|
||||
if exp == nil {
|
||||
return
|
||||
}
|
||||
|
||||
done, shutdown := stopFunc(exp)
|
||||
go shutdown()
|
||||
|
||||
// Wait for the exporter to shut down or the deadline to expire.
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import (
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// snapshot is an record of a spans state at a particular checkpointed time.
|
||||
// snapshot is a record of a span's state at a particular checkpointed time.
|
||||
// It is used as a read-only representation of that state.
|
||||
type snapshot struct {
|
||||
name string
|
||||
|
||||
+33
-18
@@ -278,8 +278,11 @@ func (s *recordingSpan) SetAttributes(attributes ...attribute.KeyValue) {
|
||||
s.addDroppedAttr(1)
|
||||
continue
|
||||
}
|
||||
a = dedupAttr(a)
|
||||
a = attrnorm.Truncate(s.tracer.provider.spanLimits.AttributeValueLengthLimit, a)
|
||||
a = normAttr(
|
||||
a,
|
||||
s.tracer.provider.spanLimits.AttributeValueDepthLimit,
|
||||
s.tracer.provider.spanLimits.AttributeValueLengthLimit,
|
||||
)
|
||||
s.attributes = append(s.attributes, a)
|
||||
s.attributesDirty = true
|
||||
}
|
||||
@@ -353,8 +356,11 @@ func (s *recordingSpan) addOverCapAttrs(limit int, attrs []attribute.KeyValue) {
|
||||
|
||||
if idx, ok := exists[a.Key]; ok {
|
||||
// Perform all updates before dropping, even when at capacity.
|
||||
a = dedupAttr(a)
|
||||
a = attrnorm.Truncate(s.tracer.provider.spanLimits.AttributeValueLengthLimit, a)
|
||||
a = normAttr(
|
||||
a,
|
||||
s.tracer.provider.spanLimits.AttributeValueDepthLimit,
|
||||
s.tracer.provider.spanLimits.AttributeValueLengthLimit,
|
||||
)
|
||||
s.attributes[idx] = a
|
||||
continue
|
||||
}
|
||||
@@ -364,22 +370,27 @@ func (s *recordingSpan) addOverCapAttrs(limit int, attrs []attribute.KeyValue) {
|
||||
// updates are checked and performed.
|
||||
s.addDroppedAttr(1)
|
||||
} else {
|
||||
a = dedupAttr(a)
|
||||
a = attrnorm.Truncate(s.tracer.provider.spanLimits.AttributeValueLengthLimit, a)
|
||||
a = normAttr(
|
||||
a,
|
||||
s.tracer.provider.spanLimits.AttributeValueDepthLimit,
|
||||
s.tracer.provider.spanLimits.AttributeValueLengthLimit,
|
||||
)
|
||||
s.attributes = append(s.attributes, a)
|
||||
exists[a.Key] = len(s.attributes) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dedupAttr(attr attribute.KeyValue) attribute.KeyValue {
|
||||
func normAttr(attr attribute.KeyValue, depthLimit, lengthLimit int) attribute.KeyValue {
|
||||
switch attr.Value.Type() {
|
||||
case attribute.SLICE, attribute.MAP:
|
||||
attr, _ = attrnorm.KeyValue(attr)
|
||||
return attr
|
||||
default:
|
||||
return attr
|
||||
if depthLimit < 0 {
|
||||
attr, _ = attrnorm.KeyValueDedup(attr)
|
||||
} else {
|
||||
attr, _, _ = attrnorm.KeyValueDedupLimitDepth(attr, depthLimit)
|
||||
}
|
||||
}
|
||||
return attrnorm.Truncate(lengthLimit, attr)
|
||||
}
|
||||
|
||||
// End ends the span. This method does nothing if the span is already ended or
|
||||
@@ -483,9 +494,7 @@ func (s *recordingSpan) RecordError(err error, opts ...trace.EventOption) {
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if !s.isRecording() {
|
||||
if !s.IsRecording() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -501,7 +510,7 @@ func (s *recordingSpan) RecordError(err error, opts ...trace.EventOption) {
|
||||
))
|
||||
}
|
||||
|
||||
s.addEvent(semconv.ExceptionEventName, opts...)
|
||||
s.AddEvent(semconv.ExceptionEventName, opts...)
|
||||
}
|
||||
|
||||
func typeStr(i any) string {
|
||||
@@ -540,7 +549,10 @@ func (s *recordingSpan) AddEvent(name string, o ...trace.EventOption) {
|
||||
// This method assumes s.mu.Lock is held by the caller.
|
||||
func (s *recordingSpan) addEvent(name string, o ...trace.EventOption) {
|
||||
c := trace.NewEventConfig(o...)
|
||||
attrs, _ := attrnorm.KeyValues(c.Attributes())
|
||||
attrs, _, _ := attrnorm.KeyValuesDedupLimitDepth(
|
||||
c.Attributes(),
|
||||
s.tracer.provider.spanLimits.AttributeValueDepthLimit,
|
||||
)
|
||||
e := Event{Name: name, Attributes: attrs, Time: c.Timestamp()}
|
||||
|
||||
// Discard attributes over limit.
|
||||
@@ -580,7 +592,7 @@ func (s *recordingSpan) Name() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
// Name returns the SpanContext of this span's parent span.
|
||||
// Parent returns the SpanContext of this span's parent span.
|
||||
func (s *recordingSpan) Parent() trace.SpanContext {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -720,7 +732,10 @@ func (s *recordingSpan) AddLink(link trace.Link) {
|
||||
return
|
||||
}
|
||||
|
||||
attrs, _ := attrnorm.KeyValues(link.Attributes)
|
||||
attrs, _, _ := attrnorm.KeyValuesDedupLimitDepth(
|
||||
link.Attributes,
|
||||
s.tracer.provider.spanLimits.AttributeValueDepthLimit,
|
||||
)
|
||||
l := Link{SpanContext: link.SpanContext, Attributes: attrs}
|
||||
|
||||
// Discard attributes over limit.
|
||||
|
||||
+24
-2
@@ -10,6 +10,10 @@ const (
|
||||
// attribute value length, unlimited.
|
||||
DefaultAttributeValueLengthLimit = -1
|
||||
|
||||
// DefaultAttributeValueDepthLimit is the default maximum allowed depth for
|
||||
// nested attribute values.
|
||||
DefaultAttributeValueDepthLimit = 64
|
||||
|
||||
// DefaultAttributeCountLimit is the default maximum number of attributes
|
||||
// a span can have.
|
||||
DefaultAttributeCountLimit = 128
|
||||
@@ -43,6 +47,20 @@ type SpanLimits struct {
|
||||
// Setting this to a negative value means no limit is applied.
|
||||
AttributeValueLengthLimit int
|
||||
|
||||
// AttributeValueDepthLimit is the maximum allowed depth for an attribute
|
||||
// value. Depth starts at one for the top-level value and increments when
|
||||
// descending into an array element or map value. An array or map beyond this
|
||||
// depth is replaced by an empty value.
|
||||
//
|
||||
// This limit applies to span, event, link, and instrumentation scope
|
||||
// attributes processed by a TracerProvider. It does not apply to Resource
|
||||
// attributes.
|
||||
//
|
||||
// Setting this to zero means the default limit is used.
|
||||
//
|
||||
// Setting this to a negative value means no limit is applied.
|
||||
AttributeValueDepthLimit int
|
||||
|
||||
// AttributeCountLimit is the maximum allowed span attribute count. Any
|
||||
// attribute added to a span once this limit is reached will be dropped.
|
||||
//
|
||||
@@ -88,12 +106,15 @@ type SpanLimits struct {
|
||||
AttributePerLinkCountLimit int
|
||||
}
|
||||
|
||||
// NewSpanLimits returns a SpanLimits with all limits set to the value their
|
||||
// corresponding environment variable holds, or the default if unset.
|
||||
// NewSpanLimits returns a SpanLimits with all limits set to their default.
|
||||
// Limits with a corresponding environment variable are set from the
|
||||
// environment when the variable is defined.
|
||||
//
|
||||
// • AttributeValueLengthLimit: OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT
|
||||
// (default: unlimited)
|
||||
//
|
||||
// • AttributeValueDepthLimit: default 64 (no environment variable)
|
||||
//
|
||||
// • AttributeCountLimit: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT (default: 128)
|
||||
//
|
||||
// • EventCountLimit: OTEL_SPAN_EVENT_COUNT_LIMIT (default: 128)
|
||||
@@ -107,6 +128,7 @@ type SpanLimits struct {
|
||||
func NewSpanLimits() SpanLimits {
|
||||
return SpanLimits{
|
||||
AttributeValueLengthLimit: env.SpanAttributeValueLength(DefaultAttributeValueLengthLimit),
|
||||
AttributeValueDepthLimit: DefaultAttributeValueDepthLimit,
|
||||
AttributeCountLimit: env.SpanAttributeCount(DefaultAttributeCountLimit),
|
||||
EventCountLimit: env.SpanEventCount(DefaultEventCountLimit),
|
||||
LinkCountLimit: env.SpanLinkCount(DefaultLinkCountLimit),
|
||||
|
||||
+1
-1
@@ -6,5 +6,5 @@ package sdk
|
||||
|
||||
// Version is the current release version of the OpenTelemetry SDK in use.
|
||||
func Version() string {
|
||||
return "1.46.0"
|
||||
return "1.47.0"
|
||||
}
|
||||
|
||||
+12
-2
@@ -372,7 +372,7 @@ func convAttrValue(value attribute.Value) telemetry.Value {
|
||||
return telemetry.Value{}
|
||||
}
|
||||
|
||||
// truncate returns a truncated version of s such that it contains less than
|
||||
// truncate returns a truncated version of s such that it contains at most
|
||||
// the limit number of characters. Truncation is applied by returning the limit
|
||||
// number of valid characters contained in s.
|
||||
//
|
||||
@@ -381,7 +381,7 @@ func convAttrValue(value attribute.Value) telemetry.Value {
|
||||
// UTF-8 is supported. When truncating, all invalid characters are dropped
|
||||
// before applying truncation.
|
||||
//
|
||||
// If s already contains less than the limit number of bytes, it is returned
|
||||
// If s already contains at most the limit number of bytes, it is returned
|
||||
// unchanged. No invalid characters are removed.
|
||||
func truncate(limit int, s string) string {
|
||||
// This prioritize performance in the following order based on the most
|
||||
@@ -407,6 +407,10 @@ func truncate(limit int, s string) string {
|
||||
continue
|
||||
}
|
||||
|
||||
// A zero limit also truncates a leading invalid byte or U+FFFD.
|
||||
if limit == 0 {
|
||||
return ""
|
||||
}
|
||||
_, size := utf8.DecodeRuneInString(s[i:])
|
||||
if size == 1 {
|
||||
// Invalid encoding.
|
||||
@@ -415,6 +419,12 @@ func truncate(limit int, s string) string {
|
||||
s = s[i:]
|
||||
break
|
||||
}
|
||||
|
||||
// A valid U+FFFD still consumes one character of the limit.
|
||||
count++
|
||||
if count > limit {
|
||||
return s[:i]
|
||||
}
|
||||
}
|
||||
|
||||
// Fast-path, no invalid input.
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ func (i *protoInt64) UnmarshalJSON(data []byte) error {
|
||||
// strings or integers.
|
||||
type protoUint64 uint64
|
||||
|
||||
// Int64 returns the protoUint64 as a uint64.
|
||||
// Uint64 returns the protoUint64 as a uint64.
|
||||
func (i *protoUint64) Uint64() uint64 { return uint64(*i) }
|
||||
|
||||
// UnmarshalJSON decodes both strings and integers.
|
||||
|
||||
+1
-1
@@ -327,7 +327,7 @@ func (v Value) String() string {
|
||||
// Try to handle this as gracefully as possible.
|
||||
//
|
||||
// Don't panic here. The goal here is to have developers find this
|
||||
// first if a slog.Kind is is not handled. It is
|
||||
// first if a slog.Kind is not handled. It is
|
||||
// preferable to have user's open issue asking why their attributes
|
||||
// have a "unhandled: " prefix than say that their code is panicking.
|
||||
return fmt.Sprintf("<unhandled telemetry.ValueKind: %s>", v.Kind())
|
||||
|
||||
+66
-12
@@ -134,13 +134,49 @@ func TraceIDFromHex(h string) (TraceID, error) {
|
||||
if len(h) != 32 {
|
||||
return [16]byte{}, errInvalidTraceIDLength
|
||||
}
|
||||
var b [16]byte
|
||||
invalidMark := byte(0)
|
||||
for i := 0; i < len(h); i += 4 {
|
||||
b[i/2] = (hexRev[h[i]] << 4) | hexRev[h[i+1]]
|
||||
b[i/2+1] = (hexRev[h[i+2]] << 4) | hexRev[h[i+3]]
|
||||
invalidMark |= hexRev[h[i]] | hexRev[h[i+1]] | hexRev[h[i+2]] | hexRev[h[i+3]]
|
||||
// Decoding every character at a constant index allows the compiler to
|
||||
// eliminate the bounds checks a loop over h would retain.
|
||||
n0, n1 := hexRev[h[0]], hexRev[h[1]]
|
||||
n2, n3 := hexRev[h[2]], hexRev[h[3]]
|
||||
n4, n5 := hexRev[h[4]], hexRev[h[5]]
|
||||
n6, n7 := hexRev[h[6]], hexRev[h[7]]
|
||||
n8, n9 := hexRev[h[8]], hexRev[h[9]]
|
||||
n10, n11 := hexRev[h[10]], hexRev[h[11]]
|
||||
n12, n13 := hexRev[h[12]], hexRev[h[13]]
|
||||
n14, n15 := hexRev[h[14]], hexRev[h[15]]
|
||||
n16, n17 := hexRev[h[16]], hexRev[h[17]]
|
||||
n18, n19 := hexRev[h[18]], hexRev[h[19]]
|
||||
n20, n21 := hexRev[h[20]], hexRev[h[21]]
|
||||
n22, n23 := hexRev[h[22]], hexRev[h[23]]
|
||||
n24, n25 := hexRev[h[24]], hexRev[h[25]]
|
||||
n26, n27 := hexRev[h[26]], hexRev[h[27]]
|
||||
n28, n29 := hexRev[h[28]], hexRev[h[29]]
|
||||
n30, n31 := hexRev[h[30]], hexRev[h[31]]
|
||||
|
||||
b := [16]byte{
|
||||
(n0 << 4) | n1,
|
||||
(n2 << 4) | n3,
|
||||
(n4 << 4) | n5,
|
||||
(n6 << 4) | n7,
|
||||
(n8 << 4) | n9,
|
||||
(n10 << 4) | n11,
|
||||
(n12 << 4) | n13,
|
||||
(n14 << 4) | n15,
|
||||
(n16 << 4) | n17,
|
||||
(n18 << 4) | n19,
|
||||
(n20 << 4) | n21,
|
||||
(n22 << 4) | n23,
|
||||
(n24 << 4) | n25,
|
||||
(n26 << 4) | n27,
|
||||
(n28 << 4) | n29,
|
||||
(n30 << 4) | n31,
|
||||
}
|
||||
|
||||
invalidMark := n0 | n1 | n2 | n3 | n4 | n5 | n6 | n7 |
|
||||
n8 | n9 | n10 | n11 | n12 | n13 | n14 | n15 |
|
||||
n16 | n17 | n18 | n19 | n20 | n21 | n22 | n23 |
|
||||
n24 | n25 | n26 | n27 | n28 | n29 | n30 | n31
|
||||
|
||||
// If the upper 4 bits of any byte are not zero, there was an invalid hex
|
||||
// character since invalid hex characters are 0xff in hexRev.
|
||||
if invalidMark&0xf0 != 0 {
|
||||
@@ -160,13 +196,31 @@ func SpanIDFromHex(h string) (SpanID, error) {
|
||||
if len(h) != 16 {
|
||||
return [8]byte{}, errInvalidSpanIDLength
|
||||
}
|
||||
var b [8]byte
|
||||
invalidMark := byte(0)
|
||||
for i := 0; i < len(h); i += 4 {
|
||||
b[i/2] = (hexRev[h[i]] << 4) | hexRev[h[i+1]]
|
||||
b[i/2+1] = (hexRev[h[i+2]] << 4) | hexRev[h[i+3]]
|
||||
invalidMark |= hexRev[h[i]] | hexRev[h[i+1]] | hexRev[h[i+2]] | hexRev[h[i+3]]
|
||||
// Decoding every character at a constant index allows the compiler to
|
||||
// eliminate the bounds checks a loop over h would retain.
|
||||
n0, n1 := hexRev[h[0]], hexRev[h[1]]
|
||||
n2, n3 := hexRev[h[2]], hexRev[h[3]]
|
||||
n4, n5 := hexRev[h[4]], hexRev[h[5]]
|
||||
n6, n7 := hexRev[h[6]], hexRev[h[7]]
|
||||
n8, n9 := hexRev[h[8]], hexRev[h[9]]
|
||||
n10, n11 := hexRev[h[10]], hexRev[h[11]]
|
||||
n12, n13 := hexRev[h[12]], hexRev[h[13]]
|
||||
n14, n15 := hexRev[h[14]], hexRev[h[15]]
|
||||
|
||||
b := [8]byte{
|
||||
(n0 << 4) | n1,
|
||||
(n2 << 4) | n3,
|
||||
(n4 << 4) | n5,
|
||||
(n6 << 4) | n7,
|
||||
(n8 << 4) | n9,
|
||||
(n10 << 4) | n11,
|
||||
(n12 << 4) | n13,
|
||||
(n14 << 4) | n15,
|
||||
}
|
||||
|
||||
invalidMark := n0 | n1 | n2 | n3 | n4 | n5 | n6 | n7 |
|
||||
n8 | n9 | n10 | n11 | n12 | n13 | n14 | n15
|
||||
|
||||
// If the upper 4 bits of any byte are not zero, there was an invalid hex
|
||||
// character since invalid hex characters are 0xff in hexRev.
|
||||
if invalidMark&0xf0 != 0 {
|
||||
|
||||
+1
-1
@@ -283,7 +283,7 @@ func (ts TraceState) Walk(f func(key, value string) bool) {
|
||||
// specification an error is returned with the original TraceState.
|
||||
//
|
||||
// If adding a new list-member means the TraceState would have more members
|
||||
// then is allowed, the new list-member will be inserted and the right-most
|
||||
// than is allowed, the new list-member will be inserted and the right-most
|
||||
// list-member will be dropped in the returned TraceState.
|
||||
func (ts TraceState) Insert(key, value string) (TraceState, error) {
|
||||
m, err := newMember(key, value)
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@ package otel
|
||||
|
||||
// Version is the current release version of OpenTelemetry in use.
|
||||
func Version() string {
|
||||
return "1.46.0"
|
||||
return "1.47.0"
|
||||
}
|
||||
|
||||
+9
-5
@@ -3,7 +3,7 @@
|
||||
|
||||
module-sets:
|
||||
stable-v1:
|
||||
version: v1.46.0
|
||||
version: v1.47.0
|
||||
modules:
|
||||
- go.opentelemetry.io/otel
|
||||
- go.opentelemetry.io/otel/bridge/opencensus
|
||||
@@ -17,25 +17,29 @@ module-sets:
|
||||
- go.opentelemetry.io/otel/exporters/stdout/stdoutmetric
|
||||
- go.opentelemetry.io/otel/exporters/stdout/stdouttrace
|
||||
- go.opentelemetry.io/otel/exporters/zipkin
|
||||
- go.opentelemetry.io/otel/log
|
||||
- go.opentelemetry.io/otel/metric
|
||||
- go.opentelemetry.io/otel/sdk
|
||||
- go.opentelemetry.io/otel/sdk/log
|
||||
- go.opentelemetry.io/otel/sdk/metric
|
||||
- go.opentelemetry.io/otel/trace
|
||||
experimental-metrics:
|
||||
version: v0.68.0
|
||||
version: v0.69.0
|
||||
modules:
|
||||
- go.opentelemetry.io/otel/exporters/prometheus
|
||||
- go.opentelemetry.io/otel/metric/x
|
||||
experimental-logs:
|
||||
version: v0.22.0
|
||||
version: v0.23.0
|
||||
modules:
|
||||
- go.opentelemetry.io/otel/log
|
||||
- go.opentelemetry.io/otel/log/logtest
|
||||
- go.opentelemetry.io/otel/sdk/log
|
||||
- go.opentelemetry.io/otel/sdk/log/logtest
|
||||
- go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc
|
||||
- go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp
|
||||
- go.opentelemetry.io/otel/exporters/stdout/stdoutlog
|
||||
experimental-traces:
|
||||
version: v0.1.0
|
||||
modules:
|
||||
- go.opentelemetry.io/otel/sdk/trace/x
|
||||
experimental-schema:
|
||||
version: v0.0.19
|
||||
modules:
|
||||
|
||||
Vendored
+28
-23
@@ -301,13 +301,13 @@ go.etcd.io/raft/v3/raftpb
|
||||
## explicit; go 1.24.0
|
||||
go.opentelemetry.io/auto/sdk
|
||||
go.opentelemetry.io/auto/sdk/internal/telemetry
|
||||
# go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0
|
||||
## explicit; go 1.25.0
|
||||
# go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0
|
||||
## explicit; go 1.26.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv
|
||||
# go.opentelemetry.io/otel v1.46.0
|
||||
## explicit; go 1.25.0
|
||||
# go.opentelemetry.io/otel v1.47.0
|
||||
## explicit; go 1.26.0
|
||||
go.opentelemetry.io/otel
|
||||
go.opentelemetry.io/otel/attribute
|
||||
go.opentelemetry.io/otel/attribute/internal
|
||||
@@ -318,13 +318,14 @@ go.opentelemetry.io/otel/internal/baggage
|
||||
go.opentelemetry.io/otel/internal/errorhandler
|
||||
go.opentelemetry.io/otel/internal/global
|
||||
go.opentelemetry.io/otel/propagation
|
||||
go.opentelemetry.io/otel/propagation/internal/hextable
|
||||
go.opentelemetry.io/otel/semconv/internal/metricpool
|
||||
go.opentelemetry.io/otel/semconv/v1.37.0
|
||||
go.opentelemetry.io/otel/semconv/v1.43.0
|
||||
go.opentelemetry.io/otel/semconv/v1.43.0/httpconv
|
||||
go.opentelemetry.io/otel/semconv/v1.43.0/otelconv
|
||||
# go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0
|
||||
## explicit; go 1.25.0
|
||||
# go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.47.0
|
||||
## explicit; go 1.26.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/counter
|
||||
@@ -334,12 +335,12 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/oconf
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/retry
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/transform
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc/internal/x
|
||||
# go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0
|
||||
## explicit; go 1.25.0
|
||||
# go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0
|
||||
## explicit; go 1.26.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal/tracetransform
|
||||
# go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0
|
||||
## explicit; go 1.25.0
|
||||
# go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0
|
||||
## explicit; go 1.26.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/counter
|
||||
@@ -348,13 +349,17 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/observ
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/retry
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/x
|
||||
# go.opentelemetry.io/otel/metric v1.46.0
|
||||
## explicit; go 1.25.0
|
||||
# go.opentelemetry.io/otel/log v1.47.0
|
||||
## explicit; go 1.26.0
|
||||
go.opentelemetry.io/otel/log
|
||||
go.opentelemetry.io/otel/log/embedded
|
||||
# go.opentelemetry.io/otel/metric v1.47.0
|
||||
## explicit; go 1.26.0
|
||||
go.opentelemetry.io/otel/metric
|
||||
go.opentelemetry.io/otel/metric/embedded
|
||||
go.opentelemetry.io/otel/metric/noop
|
||||
# go.opentelemetry.io/otel/sdk v1.46.0
|
||||
## explicit; go 1.25.0
|
||||
# go.opentelemetry.io/otel/sdk v1.47.0
|
||||
## explicit; go 1.26.0
|
||||
go.opentelemetry.io/otel/sdk
|
||||
go.opentelemetry.io/otel/sdk/instrumentation
|
||||
go.opentelemetry.io/otel/sdk/internal/attrnorm
|
||||
@@ -363,19 +368,19 @@ go.opentelemetry.io/otel/sdk/resource
|
||||
go.opentelemetry.io/otel/sdk/trace
|
||||
go.opentelemetry.io/otel/sdk/trace/internal/env
|
||||
go.opentelemetry.io/otel/sdk/trace/internal/observ
|
||||
# go.opentelemetry.io/otel/sdk/metric v1.46.0
|
||||
## explicit; go 1.25.0
|
||||
# go.opentelemetry.io/otel/sdk/metric v1.47.0
|
||||
## explicit; go 1.26.0
|
||||
go.opentelemetry.io/otel/sdk/metric
|
||||
go.opentelemetry.io/otel/sdk/metric/exemplar
|
||||
go.opentelemetry.io/otel/sdk/metric/internal
|
||||
go.opentelemetry.io/otel/sdk/metric/internal/aggregate
|
||||
go.opentelemetry.io/otel/sdk/metric/internal/attrnorm
|
||||
go.opentelemetry.io/otel/sdk/metric/internal/finish
|
||||
go.opentelemetry.io/otel/sdk/metric/internal/observ
|
||||
go.opentelemetry.io/otel/sdk/metric/internal/reservoir
|
||||
go.opentelemetry.io/otel/sdk/metric/internal/x
|
||||
go.opentelemetry.io/otel/sdk/metric/metricdata
|
||||
# go.opentelemetry.io/otel/trace v1.46.0
|
||||
## explicit; go 1.25.0
|
||||
# go.opentelemetry.io/otel/trace v1.47.0
|
||||
## explicit; go 1.26.0
|
||||
go.opentelemetry.io/otel/trace
|
||||
go.opentelemetry.io/otel/trace/embedded
|
||||
go.opentelemetry.io/otel/trace/internal/telemetry
|
||||
@@ -441,11 +446,11 @@ golang.org/x/text/width
|
||||
# golang.org/x/time v0.16.0
|
||||
## explicit; go 1.26.0
|
||||
golang.org/x/time/rate
|
||||
# google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5
|
||||
## explicit; go 1.25.0
|
||||
# google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc
|
||||
## explicit; go 1.26.0
|
||||
google.golang.org/genproto/googleapis/api/httpbody
|
||||
# google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5
|
||||
## explicit; go 1.25.0
|
||||
# google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc
|
||||
## explicit; go 1.26.0
|
||||
google.golang.org/genproto/googleapis/rpc/errdetails
|
||||
google.golang.org/genproto/googleapis/rpc/status
|
||||
# google.golang.org/grpc v1.83.2
|
||||
|
||||
Reference in New Issue
Block a user