knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/observability/metrics/k8s/tools.go (about)

     1  /*
     2  Copyright 2025 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package k8s
    18  
    19  import (
    20  	"context"
    21  	"net/url"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	"knative.dev/pkg/observability/semconv"
    27  	"knative.dev/pkg/observability/semconv/httpconv"
    28  
    29  	"go.opentelemetry.io/otel"
    30  	"go.opentelemetry.io/otel/attribute"
    31  	"go.opentelemetry.io/otel/metric"
    32  	"k8s.io/client-go/tools/metrics"
    33  )
    34  
    35  const (
    36  	scopeName = "knative.dev/pkg/observability/metrics/k8s"
    37  
    38  	resultMetricName        = "kn.k8s.client.http.response.status_code"
    39  	resultMetricDescription = "Count of response codes partitioned by method and host"
    40  )
    41  
    42  var latencyBounds = []float64{0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10}
    43  
    44  type ClientProvider struct {
    45  	latencyMetric httpconv.ClientRequestDuration
    46  	resultMetric  metric.Int64Counter
    47  }
    48  
    49  func NewClientMetricProvider(opts ...Option) (*ClientProvider, error) {
    50  	options := options{
    51  		meterProvider: otel.GetMeterProvider(),
    52  	}
    53  
    54  	cp := &ClientProvider{}
    55  
    56  	for _, opt := range opts {
    57  		opt(&options)
    58  	}
    59  
    60  	meter := options.meterProvider.Meter(scopeName)
    61  
    62  	var err error
    63  	cp.latencyMetric, err = httpconv.NewClientRequestDuration(
    64  		meter,
    65  		metric.WithExplicitBucketBoundaries(latencyBounds...),
    66  	)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  
    71  	cp.resultMetric, err = meter.Int64Counter(
    72  		resultMetricName,
    73  		metric.WithDescription(resultMetricDescription),
    74  		metric.WithUnit("{item}"),
    75  	)
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	return cp, nil
    81  }
    82  
    83  func (cp *ClientProvider) RequestLatencyMetric() metrics.LatencyMetric {
    84  	return &latency{cp}
    85  }
    86  
    87  func (cp *ClientProvider) RequestResultMetric() metrics.ResultMetric {
    88  	return &result{cp}
    89  }
    90  
    91  type latency struct {
    92  	cp *ClientProvider
    93  }
    94  
    95  func (l *latency) Observe(ctx context.Context, verb string, u url.URL, latency time.Duration) {
    96  	serverAddress := u.Hostname()
    97  	serverPort := 80
    98  
    99  	if u.Scheme == "https" {
   100  		serverPort = 443
   101  	}
   102  
   103  	if portStr := u.Port(); portStr != "" {
   104  		if port, err := strconv.Atoi(portStr); err != nil {
   105  			serverPort = port
   106  		}
   107  	}
   108  
   109  	elapsedTime := float64(latency) / float64(time.Second)
   110  
   111  	l.cp.latencyMetric.Record(ctx, elapsedTime,
   112  		httpconv.RequestMethodAttr(strings.ToUpper(verb)),
   113  		serverAddress,
   114  		serverPort,
   115  		l.cp.latencyMetric.AttrURLTemplate(u.Path),
   116  		l.cp.latencyMetric.AttrURLScheme(u.Scheme),
   117  	)
   118  }
   119  
   120  type result struct {
   121  	cp *ClientProvider
   122  }
   123  
   124  func (r *result) Increment(ctx context.Context, code string, method string, host string) {
   125  	var attrs attribute.Set
   126  
   127  	// assume we hit the happy path most frequently then in
   128  	// that case we want to avoid the strconv.Atoi parsing
   129  	if code == "200" {
   130  		attrs = attribute.NewSet(
   131  			semconv.ServerAddressKey.String(host),
   132  			semconv.HTTPRequestMethodKey.String(strings.ToUpper(method)),
   133  			semconv.HTTPResponseStatusCodeKey.Int(200),
   134  		)
   135  	} else if c, err := strconv.Atoi(code); err != nil {
   136  		attrs = attribute.NewSet(
   137  			semconv.ServerAddressKey.String(host),
   138  			semconv.HTTPRequestMethodKey.String(strings.ToUpper(method)),
   139  			semconv.HTTPResponseStatusCodeKey.Int(c),
   140  		)
   141  	} else {
   142  		attrs = attribute.NewSet(
   143  			semconv.ServerAddressKey.String(host),
   144  			semconv.HTTPRequestMethodKey.String(strings.ToUpper(method)),
   145  		)
   146  	}
   147  
   148  	r.cp.resultMetric.Add(ctx, 1, metric.WithAttributeSet(attrs))
   149  }
   150  
   151  var (
   152  	_ metrics.LatencyMetric = (*latency)(nil)
   153  	_ metrics.ResultMetric  = (*result)(nil)
   154  )