11package metrics
22
33import (
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
2241type InstrumentedRoundTripper struct {
23- api string
24- base http. RoundTripper
42+ base http. RoundTripper
43+ componentName string
2544}
2645
2746func (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