[chore] update otel libraries (#3740)

* chore: update otel dependencies

* refactor: combine tracing & metrics in observability package

* chore: update example tracing compose file
This commit is contained in:
Dominik Süß 2025-02-06 12:14:37 +01:00 committed by GitHub
commit dd094e4012
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
217 changed files with 6873 additions and 2734 deletions

View file

@ -13,7 +13,8 @@ func InstrumentationScope(il instrumentation.Scope) *commonpb.InstrumentationSco
return nil
}
return &commonpb.InstrumentationScope{
Name: il.Name,
Version: il.Version,
Name: il.Name,
Version: il.Version,
Attributes: Iterator(il.Attributes.Iter()),
}
}

View file

@ -97,8 +97,8 @@ func span(sd tracesdk.ReadOnlySpan) *tracepb.Span {
SpanId: sid[:],
TraceState: sd.SpanContext().TraceState().String(),
Status: status(sd.Status().Code, sd.Status().Description),
StartTimeUnixNano: uint64(sd.StartTime().UnixNano()),
EndTimeUnixNano: uint64(sd.EndTime().UnixNano()),
StartTimeUnixNano: uint64(max(0, sd.StartTime().UnixNano())), // nolint:gosec // Overflow checked.
EndTimeUnixNano: uint64(max(0, sd.EndTime().UnixNano())), // nolint:gosec // Overflow checked.
Links: links(sd.Links()),
Kind: spanKind(sd.SpanKind()),
Name: sd.Name(),
@ -178,7 +178,7 @@ func buildSpanFlags(sc trace.SpanContext) uint32 {
flags |= tracepb.SpanFlags_SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK
}
return uint32(flags)
return uint32(flags) // nolint:gosec // Flags is a bitmask and can't be negative
}
// spanEvents transforms span Events to an OTLP span events.
@ -192,7 +192,7 @@ func spanEvents(es []tracesdk.Event) []*tracepb.Span_Event {
for i := 0; i < len(es); i++ {
events[i] = &tracepb.Span_Event{
Name: es[i].Name,
TimeUnixNano: uint64(es[i].Time.UnixNano()),
TimeUnixNano: uint64(max(0, es[i].Time.UnixNano())), // nolint:gosec // Overflow checked.
Attributes: KeyValues(es[i].Attributes),
DroppedAttributesCount: clampUint32(es[i].DroppedAttributeCount),
}

View file

@ -229,7 +229,12 @@ func (c *client) exportContext(parent context.Context) (context.Context, context
}
if c.metadata.Len() > 0 {
ctx = metadata.NewOutgoingContext(ctx, c.metadata)
md := c.metadata
if outMD, ok := metadata.FromOutgoingContext(ctx); ok {
md = metadata.Join(md, outMD)
}
ctx = metadata.NewOutgoingContext(ctx, md)
}
// Unify the client stopCtx with the parent.
@ -289,7 +294,7 @@ func (c *client) MarshalLog() interface{} {
Type string
Endpoint string
}{
Type: "otlphttpgrpc",
Type: "otlptracegrpc",
Endpoint: c.endpoint,
}
}

View file

@ -98,7 +98,7 @@ func cleanPath(urlPath string, defaultPath string) string {
return defaultPath
}
if !path.IsAbs(tmp) {
tmp = fmt.Sprintf("/%s", tmp)
tmp = "/" + tmp
}
return tmp
}
@ -125,7 +125,7 @@ func NewGRPCConfig(opts ...GRPCOption) Config {
if cfg.ServiceConfig != "" {
cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultServiceConfig(cfg.ServiceConfig))
}
// Priroritize GRPCCredentials over Insecure (passing both is an error).
// Prioritize GRPCCredentials over Insecure (passing both is an error).
if cfg.Traces.GRPCCredentials != nil {
cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(cfg.Traces.GRPCCredentials))
} else if cfg.Traces.Insecure {
@ -278,9 +278,7 @@ func WithEndpointURL(v string) GenericOption {
cfg.Traces.Endpoint = u.Host
cfg.Traces.URLPath = u.Path
if u.Scheme != "https" {
cfg.Traces.Insecure = true
}
cfg.Traces.Insecure = u.Scheme != "https"
return cfg
})

View file

@ -166,8 +166,7 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
}()
}
switch sc := resp.StatusCode; {
case sc >= 200 && sc <= 299:
if sc := resp.StatusCode; sc >= 200 && sc <= 299 {
// Success, do not retry.
// Read the partial success message, if any.
var respData bytes.Buffer
@ -194,34 +193,33 @@ func (d *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
}
}
return nil
}
// Error cases.
case sc == http.StatusTooManyRequests,
sc == http.StatusBadGateway,
sc == http.StatusServiceUnavailable,
sc == http.StatusGatewayTimeout:
// Retry-able failures.
rErr := newResponseError(resp.Header, nil)
// server may return a message with the response
// body, so we read it to include in the error
// message to be returned. It will help in
// debugging the actual issue.
var respData bytes.Buffer
if _, err := io.Copy(&respData, resp.Body); err != nil {
return err
}
respStr := strings.TrimSpace(respData.String())
if len(respStr) == 0 {
respStr = "(empty)"
}
bodyErr := fmt.Errorf("body: %s", respStr)
// server may return a message with the response
// body, so we read it to include in the error
// message to be returned. It will help in
// debugging the actual issue.
var respData bytes.Buffer
if _, err := io.Copy(&respData, resp.Body); err != nil {
_ = resp.Body.Close()
return err
}
// overwrite the error message with the response body
// if it is not empty
if respStr := strings.TrimSpace(respData.String()); respStr != "" {
// Include response for context.
e := errors.New(respStr)
rErr = newResponseError(resp.Header, e)
}
return rErr
switch resp.StatusCode {
case http.StatusTooManyRequests,
http.StatusBadGateway,
http.StatusServiceUnavailable,
http.StatusGatewayTimeout:
// Retryable failure.
return newResponseError(resp.Header, bodyErr)
default:
return fmt.Errorf("failed to send to %s: %s", request.URL, resp.Status)
// Non-retryable failure.
return fmt.Errorf("failed to send to %s: %s (%w)", request.URL, resp.Status, bodyErr)
}
})
}
@ -278,7 +276,7 @@ func (d *client) MarshalLog() interface{} {
Endpoint string
Insecure bool
}{
Type: "otlphttphttp",
Type: "otlptracehttp",
Endpoint: d.cfg.Endpoint,
Insecure: d.cfg.Insecure,
}
@ -328,7 +326,7 @@ func newResponseError(header http.Header, wrapped error) error {
func (e retryableError) Error() string {
if e.err != nil {
return fmt.Sprintf("retry-able request failure: %s", e.err.Error())
return "retry-able request failure: " + e.err.Error()
}
return "retry-able request failure"

View file

@ -22,7 +22,7 @@ target URL to which the exporter sends telemetry.
The value must contain a scheme ("http" or "https") and host.
The value may additionally contain a port and a path.
The value should not contain a query string or fragment.
The configuration can be overridden by [WithEndpoint], [WithEndpointURL], [WitnInsecure], and [WithURLPath] options.
The configuration can be overridden by [WithEndpoint], [WithEndpointURL], [WithInsecure], and [WithURLPath] options.
OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TRACES_HEADERS (default: none) -
key-value pairs used as headers associated with HTTP requests.

View file

@ -98,7 +98,7 @@ func cleanPath(urlPath string, defaultPath string) string {
return defaultPath
}
if !path.IsAbs(tmp) {
tmp = fmt.Sprintf("/%s", tmp)
tmp = "/" + tmp
}
return tmp
}
@ -125,7 +125,7 @@ func NewGRPCConfig(opts ...GRPCOption) Config {
if cfg.ServiceConfig != "" {
cfg.DialOptions = append(cfg.DialOptions, grpc.WithDefaultServiceConfig(cfg.ServiceConfig))
}
// Priroritize GRPCCredentials over Insecure (passing both is an error).
// Prioritize GRPCCredentials over Insecure (passing both is an error).
if cfg.Traces.GRPCCredentials != nil {
cfg.DialOptions = append(cfg.DialOptions, grpc.WithTransportCredentials(cfg.Traces.GRPCCredentials))
} else if cfg.Traces.Insecure {
@ -278,9 +278,7 @@ func WithEndpointURL(v string) GenericOption {
cfg.Traces.Endpoint = u.Host
cfg.Traces.URLPath = u.Path
if u.Scheme != "https" {
cfg.Traces.Insecure = true
}
cfg.Traces.Insecure = u.Scheme != "https"
return cfg
})

View file

@ -5,5 +5,5 @@ package otlptrace // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace"
// Version is the current release version of the OpenTelemetry OTLP trace exporter in use.
func Version() string {
return "1.29.0"
return "1.34.0"
}

View file

@ -7,6 +7,7 @@ import (
"strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/sdk/metric"
@ -131,7 +132,10 @@ func WithoutScopeInfo() Option {
// have special behavior based on their name.
func WithNamespace(ns string) Option {
return optionFunc(func(cfg config) config {
ns = sanitizeName(ns)
if model.NameValidationScheme != model.UTF8Validation {
// Only sanitize if prometheus does not support UTF-8.
ns = model.EscapeName(ns, model.NameEscapingScheme)
}
if !strings.HasSuffix(ns, "_") {
// namespace and metric names should be separated with an underscore,
// adds a trailing underscore if there is not one already.

View file

@ -11,11 +11,10 @@ import (
"slices"
"strings"
"sync"
"unicode"
"unicode/utf8"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/model"
"google.golang.org/protobuf/proto"
"go.opentelemetry.io/otel"
@ -34,15 +33,14 @@ const (
scopeInfoMetricName = "otel_scope_info"
scopeInfoDescription = "Instrumentation Scope metadata"
scopeNameLabel = "otel_scope_name"
scopeVersionLabel = "otel_scope_version"
traceIDExemplarKey = "trace_id"
spanIDExemplarKey = "span_id"
)
var (
scopeInfoKeys = [2]string{"otel_scope_name", "otel_scope_version"}
errScopeInvalid = errors.New("invalid scope")
)
var errScopeInvalid = errors.New("invalid scope")
// Exporter is a Prometheus Exporter that embeds the OTel metric.Reader
// interface for easy instantiation with a MeterProvider.
@ -188,7 +186,11 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
}
for _, scopeMetrics := range metrics.ScopeMetrics {
var keys, values [2]string
n := len(c.resourceKeyVals.keys) + 2 // resource attrs + scope name + scope version
kv := keyVals{
keys: make([]string, 0, n),
vals: make([]string, 0, n),
}
if !c.disableScopeInfo {
scopeInfo, err := c.scopeInfo(scopeMetrics.Scope)
@ -203,10 +205,13 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
ch <- scopeInfo
keys = scopeInfoKeys
values = [2]string{scopeMetrics.Scope.Name, scopeMetrics.Scope.Version}
kv.keys = append(kv.keys, scopeNameLabel, scopeVersionLabel)
kv.vals = append(kv.vals, scopeMetrics.Scope.Name, scopeMetrics.Scope.Version)
}
kv.keys = append(kv.keys, c.resourceKeyVals.keys...)
kv.vals = append(kv.vals, c.resourceKeyVals.vals...)
for _, m := range scopeMetrics.Metrics {
typ := c.metricType(m)
if typ == nil {
@ -225,25 +230,27 @@ func (c *collector) Collect(ch chan<- prometheus.Metric) {
switch v := m.Data.(type) {
case metricdata.Histogram[int64]:
addHistogramMetric(ch, v, m, keys, values, name, c.resourceKeyVals)
addHistogramMetric(ch, v, m, name, kv)
case metricdata.Histogram[float64]:
addHistogramMetric(ch, v, m, keys, values, name, c.resourceKeyVals)
addHistogramMetric(ch, v, m, name, kv)
case metricdata.Sum[int64]:
addSumMetric(ch, v, m, keys, values, name, c.resourceKeyVals)
addSumMetric(ch, v, m, name, kv)
case metricdata.Sum[float64]:
addSumMetric(ch, v, m, keys, values, name, c.resourceKeyVals)
addSumMetric(ch, v, m, name, kv)
case metricdata.Gauge[int64]:
addGaugeMetric(ch, v, m, keys, values, name, c.resourceKeyVals)
addGaugeMetric(ch, v, m, name, kv)
case metricdata.Gauge[float64]:
addGaugeMetric(ch, v, m, keys, values, name, c.resourceKeyVals)
addGaugeMetric(ch, v, m, name, kv)
}
}
}
}
func addHistogramMetric[N int64 | float64](ch chan<- prometheus.Metric, histogram metricdata.Histogram[N], m metricdata.Metrics, ks, vs [2]string, name string, resourceKV keyVals) {
func addHistogramMetric[N int64 | float64](ch chan<- prometheus.Metric, histogram metricdata.Histogram[N], m metricdata.Metrics, name string, kv keyVals) {
for _, dp := range histogram.DataPoints {
keys, values := getAttrs(dp.Attributes, ks, vs, resourceKV)
keys, values := getAttrs(dp.Attributes)
keys = append(keys, kv.keys...)
values = append(values, kv.vals...)
desc := prometheus.NewDesc(name, m.Description, keys, nil)
buckets := make(map[float64]uint64, len(dp.Bounds))
@ -263,14 +270,16 @@ func addHistogramMetric[N int64 | float64](ch chan<- prometheus.Metric, histogra
}
}
func addSumMetric[N int64 | float64](ch chan<- prometheus.Metric, sum metricdata.Sum[N], m metricdata.Metrics, ks, vs [2]string, name string, resourceKV keyVals) {
func addSumMetric[N int64 | float64](ch chan<- prometheus.Metric, sum metricdata.Sum[N], m metricdata.Metrics, name string, kv keyVals) {
valueType := prometheus.CounterValue
if !sum.IsMonotonic {
valueType = prometheus.GaugeValue
}
for _, dp := range sum.DataPoints {
keys, values := getAttrs(dp.Attributes, ks, vs, resourceKV)
keys, values := getAttrs(dp.Attributes)
keys = append(keys, kv.keys...)
values = append(values, kv.vals...)
desc := prometheus.NewDesc(name, m.Description, keys, nil)
m, err := prometheus.NewConstMetric(desc, valueType, float64(dp.Value), values...)
@ -278,14 +287,20 @@ func addSumMetric[N int64 | float64](ch chan<- prometheus.Metric, sum metricdata
otel.Handle(err)
continue
}
m = addExemplars(m, dp.Exemplars)
// GaugeValues don't support Exemplars at this time
// https://github.com/prometheus/client_golang/blob/aef8aedb4b6e1fb8ac1c90790645169125594096/prometheus/metric.go#L199
if valueType != prometheus.GaugeValue {
m = addExemplars(m, dp.Exemplars)
}
ch <- m
}
}
func addGaugeMetric[N int64 | float64](ch chan<- prometheus.Metric, gauge metricdata.Gauge[N], m metricdata.Metrics, ks, vs [2]string, name string, resourceKV keyVals) {
func addGaugeMetric[N int64 | float64](ch chan<- prometheus.Metric, gauge metricdata.Gauge[N], m metricdata.Metrics, name string, kv keyVals) {
for _, dp := range gauge.DataPoints {
keys, values := getAttrs(dp.Attributes, ks, vs, resourceKV)
keys, values := getAttrs(dp.Attributes)
keys = append(keys, kv.keys...)
values = append(values, kv.vals...)
desc := prometheus.NewDesc(name, m.Description, keys, nil)
m, err := prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(dp.Value), values...)
@ -297,61 +312,58 @@ func addGaugeMetric[N int64 | float64](ch chan<- prometheus.Metric, gauge metric
}
}
// getAttrs parses the attribute.Set to two lists of matching Prometheus-style
// keys and values. It sanitizes invalid characters and handles duplicate keys
// (due to sanitization) by sorting and concatenating the values following the spec.
func getAttrs(attrs attribute.Set, ks, vs [2]string, resourceKV keyVals) ([]string, []string) {
keysMap := make(map[string][]string)
itr := attrs.Iter()
for itr.Next() {
kv := itr.Attribute()
key := strings.Map(sanitizeRune, string(kv.Key))
if _, ok := keysMap[key]; !ok {
keysMap[key] = []string{kv.Value.Emit()}
} else {
// if the sanitized key is a duplicate, append to the list of keys
keysMap[key] = append(keysMap[key], kv.Value.Emit())
}
}
// getAttrs converts the attribute.Set to two lists of matching Prometheus-style
// keys and values.
func getAttrs(attrs attribute.Set) ([]string, []string) {
keys := make([]string, 0, attrs.Len())
values := make([]string, 0, attrs.Len())
for key, vals := range keysMap {
keys = append(keys, key)
slices.Sort(vals)
values = append(values, strings.Join(vals, ";"))
}
itr := attrs.Iter()
if ks[0] != "" {
keys = append(keys, ks[:]...)
values = append(values, vs[:]...)
if model.NameValidationScheme == model.UTF8Validation {
// Do not perform sanitization if prometheus supports UTF-8.
for itr.Next() {
kv := itr.Attribute()
keys = append(keys, string(kv.Key))
values = append(values, kv.Value.Emit())
}
} else {
// It sanitizes invalid characters and handles duplicate keys
// (due to sanitization) by sorting and concatenating the values following the spec.
keysMap := make(map[string][]string)
for itr.Next() {
kv := itr.Attribute()
key := model.EscapeName(string(kv.Key), model.NameEscapingScheme)
if _, ok := keysMap[key]; !ok {
keysMap[key] = []string{kv.Value.Emit()}
} else {
// if the sanitized key is a duplicate, append to the list of keys
keysMap[key] = append(keysMap[key], kv.Value.Emit())
}
}
for key, vals := range keysMap {
keys = append(keys, key)
slices.Sort(vals)
values = append(values, strings.Join(vals, ";"))
}
}
for idx := range resourceKV.keys {
keys = append(keys, resourceKV.keys[idx])
values = append(values, resourceKV.vals[idx])
}
return keys, values
}
func createInfoMetric(name, description string, res *resource.Resource) (prometheus.Metric, error) {
keys, values := getAttrs(*res.Set(), [2]string{}, [2]string{}, keyVals{})
keys, values := getAttrs(*res.Set())
desc := prometheus.NewDesc(name, description, keys, nil)
return prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(1), values...)
}
func createScopeInfoMetric(scope instrumentation.Scope) (prometheus.Metric, error) {
keys := scopeInfoKeys[:]
desc := prometheus.NewDesc(scopeInfoMetricName, scopeInfoDescription, keys, nil)
return prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(1), scope.Name, scope.Version)
}
attrs := make([]attribute.KeyValue, 0, scope.Attributes.Len()+2) // resource attrs + scope name + scope version
attrs = append(attrs, scope.Attributes.ToSlice()...)
attrs = append(attrs, attribute.String(scopeNameLabel, scope.Name))
attrs = append(attrs, attribute.String(scopeVersionLabel, scope.Version))
func sanitizeRune(r rune) rune {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == ':' || r == '_' {
return r
}
return '_'
keys, values := getAttrs(attribute.NewSet(attrs...))
desc := prometheus.NewDesc(scopeInfoMetricName, scopeInfoDescription, keys, nil)
return prometheus.NewConstMetric(desc, prometheus.GaugeValue, float64(1), values...)
}
var unitSuffixes = map[string]string{
@ -392,7 +404,11 @@ var unitSuffixes = map[string]string{
// getName returns the sanitized name, prefixed with the namespace and suffixed with unit.
func (c *collector) getName(m metricdata.Metrics, typ *dto.MetricType) string {
name := sanitizeName(m.Name)
name := m.Name
if model.NameValidationScheme != model.UTF8Validation {
// Only sanitize if prometheus does not support UTF-8.
name = model.EscapeName(name, model.NameEscapingScheme)
}
addCounterSuffix := !c.withoutCounterSuffixes && *typ == dto.MetricType_COUNTER
if addCounterSuffix {
// Remove the _total suffix here, as we will re-add the total suffix
@ -411,59 +427,6 @@ func (c *collector) getName(m metricdata.Metrics, typ *dto.MetricType) string {
return name
}
func sanitizeName(n string) string {
// This algorithm is based on strings.Map from Go 1.19.
const replacement = '_'
valid := func(i int, r rune) bool {
// Taken from
// https://github.com/prometheus/common/blob/dfbc25bd00225c70aca0d94c3c4bb7744f28ace0/model/metric.go#L92-L102
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_' || r == ':' || (r >= '0' && r <= '9' && i > 0) {
return true
}
return false
}
// This output buffer b is initialized on demand, the first time a
// character needs to be replaced.
var b strings.Builder
for i, c := range n {
if valid(i, c) {
continue
}
if i == 0 && c >= '0' && c <= '9' {
// Prefix leading number with replacement character.
b.Grow(len(n) + 1)
_ = b.WriteByte(byte(replacement))
break
}
b.Grow(len(n))
_, _ = b.WriteString(n[:i])
_ = b.WriteByte(byte(replacement))
width := utf8.RuneLen(c)
n = n[i+width:]
break
}
// Fast path for unchanged input.
if b.Cap() == 0 { // b.Grow was not called above.
return n
}
for _, c := range n {
// Due to inlining, it is more performant to invoke WriteByte rather then
// WriteRune.
if valid(1, c) { // We are guaranteed to not be at the start.
_ = b.WriteByte(byte(c))
} else {
_ = b.WriteByte(byte(replacement))
}
}
return b.String()
}
func (c *collector) metricType(m metricdata.Metrics) *dto.MetricType {
switch v := m.Data.(type) {
case metricdata.Histogram[int64], metricdata.Histogram[float64]:
@ -489,7 +452,7 @@ func (c *collector) createResourceAttributes(res *resource.Resource) {
defer c.mu.Unlock()
resourceAttrs, _ := res.Set().Filter(c.resourceAttributesFilter)
resourceKeys, resourceValues := getAttrs(resourceAttrs, [2]string{}, [2]string{}, keyVals{})
resourceKeys, resourceValues := getAttrs(resourceAttrs)
c.resourceKeyVals = keyVals{keys: resourceKeys, vals: resourceValues}
}
@ -584,7 +547,8 @@ func addExemplars[N int64 | float64](m prometheus.Metric, exemplars []metricdata
func attributesToLabels(attrs []attribute.KeyValue) prometheus.Labels {
labels := make(map[string]string)
for _, attr := range attrs {
labels[string(attr.Key)] = attr.Value.Emit()
key := model.EscapeName(string(attr.Key), model.NameEscapingScheme)
labels[key] = attr.Value.Emit()
}
return labels
}