Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions cmd/stackit-csi-plugin/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,8 @@ func handle(ctx context.Context) {
klog.Fatal(err)
}

iaasHTTPClient := metrics.NewInstrumentedHTTPClient(metrics.APINameIaaS)
iaasOpts := []sdkconfig.ConfigurationOption{
sdkconfig.WithHTTPClient(iaasHTTPClient),
sdkconfig.WithHTTPClient(metrics.NewHTTPClient("stackit-csi-plugin")),
}

if cfg.Global.APIEndpoints.IaasAPI != "" {
Expand Down
6 changes: 2 additions & 4 deletions pkg/ccm/stackit.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,8 @@ func BuildObservability() (*MetricsRemoteWrite, error) {

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

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

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

if cfg.Global.APIEndpoints.IaasAPI != "" {
Expand Down
113 changes: 78 additions & 35 deletions pkg/metrics/http.go
Original file line number Diff line number Diff line change
@@ -1,32 +1,49 @@
package metrics

import (
"fmt"
"net/http"
"runtime"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"

"github.com/prometheus/client_golang/prometheus"
)

func NewInstrumentedHTTPClient(api string) *http.Client {
return &http.Client{
Transport: &InstrumentedRoundTripper{
api: api,
base: http.DefaultTransport,
},
const UnknownOperation = "UnknownOperation"

func NewHTTPClient(componentName string) *http.Client {
return WrapHTTPClient(http.DefaultClient, componentName)
}

func WrapHTTPClient(client *http.Client, componentName string) *http.Client {
if client == nil {
return nil
}
wrappedClient := *client
Comment thread
dergeberl marked this conversation as resolved.

baseTransport := client.Transport
if baseTransport == nil {
baseTransport = http.DefaultTransport
}

// Chain your instrumented round tripper
wrappedClient.Transport = &InstrumentedRoundTripper{
base: baseTransport,
componentName: componentName,
}

return &wrappedClient
}

type InstrumentedRoundTripper struct {
api string
base http.RoundTripper
base http.RoundTripper
componentName string
}

func (rt *InstrumentedRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
operation := operationFromRequest(request)

startTime := time.Now()
response, err := rt.base.RoundTrip(request)
duration := time.Since(startTime)
Expand All @@ -36,10 +53,17 @@ func (rt *InstrumentedRoundTripper) RoundTrip(request *http.Request) (*http.Resp
statusCode = strconv.Itoa(response.StatusCode)
}

// request.Host is optional so we can fallback to request.URL.Host (if available)
host := request.Host
if host == "" && request.URL != nil {
host = request.URL.Host
}

labels := prometheus.Labels{
apiLabel: rt.api,
componentLabel: rt.componentName,
hostLabel: host,
methodLabel: request.Method,
operationLabel: operation,
operationLabel: getSDKOperationName(),
codeLabel: statusCode,
}

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

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

pathElements := strings.Split(request.URL.Path, "/")
if len(pathElements) <= 1 {
return fmt.Sprintf("%s_%s", verb, request.URL.Path)
// Skip 3 because the first 3 are always Callers, getSDKOperationName, RoundTrip.
n := runtime.Callers(3, pc)
if n == 0 {
return UnknownOperation
}
// since the path starts with a '/', the first path element is the empty string
pathElements = pathElements[1:]

// the subject is always the last or the second to last element:
// .../subject -> even number of path elements
// .../subject/<some-id> -> odd number of path elements
var subject string
if len(pathElements) == 1 {
// edge case
subject = pathElements[0]
} else if len(pathElements)%2 == 0 {
// even
subject = pathElements[len(pathElements)-1]
} else {
// odd
subject = pathElements[len(pathElements)-2] + "_instance"

frames := runtime.CallersFrames(pc[:n])
moreFrames := true
for moreFrames {
var frame runtime.Frame
frame, moreFrames = frames.Next()

if !strings.Contains(frame.Function, "stackitcloud/stackit-sdk-go") {
continue
}

parts := strings.Split(frame.Function, ".")
if len(parts) > 0 {
funcName := parts[len(parts)-1]

// Skip function names with 0 len
// Skip Execute, because there is a function with more detailed name
// Skip RoundTrip, because this only the RoundTrip for the AuthFlow
if funcName == "" ||
funcName == "Execute" ||
funcName == "RoundTrip" {
continue
}

// Skip Private functions
r, _ := utf8.DecodeRuneInString(funcName)
if !unicode.IsUpper(r) {
continue
}

return strings.TrimSuffix(funcName, "Execute")
}
}

return fmt.Sprintf("%s_%s", verb, subject)
return UnknownOperation
}
Loading