Skip to content

Commit a480cb0

Browse files
[release-v1.34] Refactor SDK metrics: rename metrics from cloud_provider_stackit_http* to stackit_api_http.* (#1531)
* refactor metrics * Only use IaaS in tests, as this is used in most repos * Add PR Review suggestions --------- Co-authored-by: Maximilian Geberl <maximilian.geberl@digits.schwarz>
1 parent 165ec46 commit a480cb0

5 files changed

Lines changed: 230 additions & 163 deletions

File tree

‎cmd/stackit-csi-plugin/main.go‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,8 @@ func handle(ctx context.Context) {
126126
klog.Fatal(err)
127127
}
128128

129-
iaasHTTPClient := metrics.NewInstrumentedHTTPClient(metrics.APINameIaaS)
130129
iaasOpts := []sdkconfig.ConfigurationOption{
131-
sdkconfig.WithHTTPClient(iaasHTTPClient),
130+
sdkconfig.WithHTTPClient(metrics.NewHTTPClient("stackit-csi-plugin")),
132131
}
133132

134133
if cfg.Global.APIEndpoints.IaasAPI != "" {

‎pkg/ccm/stackit.go‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,8 @@ func BuildObservability() (*MetricsRemoteWrite, error) {
117117

118118
// NewCloudControllerManager creates a new instance of the stackit struct from a stackitconfig struct
119119
func NewCloudControllerManager(cfg *stackitconfig.CCMConfig, obs *MetricsRemoteWrite) (*CloudControllerManager, error) {
120-
lbHTTPClient := metrics.NewInstrumentedHTTPClient(metrics.APINameLoadBalancer)
121120
lbOpts := []sdkconfig.ConfigurationOption{
122-
sdkconfig.WithHTTPClient(lbHTTPClient),
121+
sdkconfig.WithHTTPClient(metrics.NewHTTPClient("cloud-controller-manager")),
123122
}
124123

125124
if cfg.Global.APIEndpoints.LoadBalancerAPI != "" {
@@ -139,9 +138,8 @@ func NewCloudControllerManager(cfg *stackitconfig.CCMConfig, obs *MetricsRemoteW
139138
return nil, fmt.Errorf("failed to create lb client: %v", err)
140139
}
141140

142-
iaasHTTPClient := metrics.NewInstrumentedHTTPClient(metrics.APINameIaaS)
143141
iaasOpts := []sdkconfig.ConfigurationOption{
144-
sdkconfig.WithHTTPClient(iaasHTTPClient),
142+
sdkconfig.WithHTTPClient(metrics.NewHTTPClient("cloud-controller-manager")),
145143
}
146144

147145
if cfg.Global.APIEndpoints.IaasAPI != "" {

‎pkg/metrics/http.go‎

Lines changed: 78 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,49 @@
11
package metrics
22

33
import (
4-
"fmt"
54
"net/http"
5+
"runtime"
66
"strconv"
77
"strings"
88
"time"
9+
"unicode"
10+
"unicode/utf8"
911

1012
"github.com/prometheus/client_golang/prometheus"
1113
)
1214

13-
func NewInstrumentedHTTPClient(api string) *http.Client {
14-
return &http.Client{
15-
Transport: &InstrumentedRoundTripper{
16-
api: api,
17-
base: http.DefaultTransport,
18-
},
15+
const UnknownOperation = "UnknownOperation"
16+
17+
func NewHTTPClient(componentName string) *http.Client {
18+
return WrapHTTPClient(http.DefaultClient, componentName)
19+
}
20+
21+
func WrapHTTPClient(client *http.Client, componentName string) *http.Client {
22+
if client == nil {
23+
return nil
24+
}
25+
wrappedClient := *client
26+
27+
baseTransport := client.Transport
28+
if baseTransport == nil {
29+
baseTransport = http.DefaultTransport
30+
}
31+
32+
// Chain your instrumented round tripper
33+
wrappedClient.Transport = &InstrumentedRoundTripper{
34+
base: baseTransport,
35+
componentName: componentName,
1936
}
37+
38+
return &wrappedClient
2039
}
2140

2241
type InstrumentedRoundTripper struct {
23-
api string
24-
base http.RoundTripper
42+
base http.RoundTripper
43+
componentName string
2544
}
2645

2746
func (rt *InstrumentedRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
28-
operation := operationFromRequest(request)
29-
3047
startTime := time.Now()
3148
response, err := rt.base.RoundTrip(request)
3249
duration := time.Since(startTime)
@@ -36,10 +53,17 @@ func (rt *InstrumentedRoundTripper) RoundTrip(request *http.Request) (*http.Resp
3653
statusCode = strconv.Itoa(response.StatusCode)
3754
}
3855

56+
// request.Host is optional so we can fallback to request.URL.Host (if available)
57+
host := request.Host
58+
if host == "" && request.URL != nil {
59+
host = request.URL.Host
60+
}
61+
3962
labels := prometheus.Labels{
40-
apiLabel: rt.api,
63+
componentLabel: rt.componentName,
64+
hostLabel: host,
4165
methodLabel: request.Method,
42-
operationLabel: operation,
66+
operationLabel: getSDKOperationName(),
4367
codeLabel: statusCode,
4468
}
4569

@@ -56,30 +80,49 @@ func (rt *InstrumentedRoundTripper) RoundTrip(request *http.Request) (*http.Resp
5680
return response, err
5781
}
5882

59-
func operationFromRequest(request *http.Request) string {
60-
verb := strings.ToLower(request.Method)
83+
// getSDKOperationName returns the name of the STACKIT SDK function. To do this the function gets the last 10 callers and checks
84+
// for functions from the stackitcloud/stackit-sdk-go. It fall back to UnknownOperation if no function was found.
85+
func getSDKOperationName() string {
86+
pc := make([]uintptr, 10)
6187

62-
pathElements := strings.Split(request.URL.Path, "/")
63-
if len(pathElements) <= 1 {
64-
return fmt.Sprintf("%s_%s", verb, request.URL.Path)
88+
// Skip 3 because the first 3 are always Callers, getSDKOperationName, RoundTrip.
89+
n := runtime.Callers(3, pc)
90+
if n == 0 {
91+
return UnknownOperation
6592
}
66-
// since the path starts with a '/', the first path element is the empty string
67-
pathElements = pathElements[1:]
68-
69-
// the subject is always the last or the second to last element:
70-
// .../subject -> even number of path elements
71-
// .../subject/<some-id> -> odd number of path elements
72-
var subject string
73-
if len(pathElements) == 1 {
74-
// edge case
75-
subject = pathElements[0]
76-
} else if len(pathElements)%2 == 0 {
77-
// even
78-
subject = pathElements[len(pathElements)-1]
79-
} else {
80-
// odd
81-
subject = pathElements[len(pathElements)-2] + "_instance"
93+
94+
frames := runtime.CallersFrames(pc[:n])
95+
moreFrames := true
96+
for moreFrames {
97+
var frame runtime.Frame
98+
frame, moreFrames = frames.Next()
99+
100+
if !strings.Contains(frame.Function, "stackitcloud/stackit-sdk-go") {
101+
continue
102+
}
103+
104+
parts := strings.Split(frame.Function, ".")
105+
if len(parts) > 0 {
106+
funcName := parts[len(parts)-1]
107+
108+
// Skip function names with 0 len
109+
// Skip Execute, because there is a function with more detailed name
110+
// Skip RoundTrip, because this only the RoundTrip for the AuthFlow
111+
if funcName == "" ||
112+
funcName == "Execute" ||
113+
funcName == "RoundTrip" {
114+
continue
115+
}
116+
117+
// Skip Private functions
118+
r, _ := utf8.DecodeRuneInString(funcName)
119+
if !unicode.IsUpper(r) {
120+
continue
121+
}
122+
123+
return strings.TrimSuffix(funcName, "Execute")
124+
}
82125
}
83126

84-
return fmt.Sprintf("%s_%s", verb, subject)
127+
return UnknownOperation
85128
}

0 commit comments

Comments
 (0)