11package print
22
33import (
4+ "bytes"
45 "encoding/json"
56 "fmt"
7+ "io"
8+ "net/http"
9+ "slices"
610 "sort"
711 "strings"
12+
13+ "github.com/stackitcloud/stackit-sdk-go/core/config"
814)
915
16+ var defaultHTTPHeaders = []string {"Accept" , "Content-Type" , "Content-Length" , "User-Agent" , "Date" , "Referrer-Policy" }
17+
1018// BuildDebugStrFromInputModel converts an input model to a user-friendly string representation.
1119// This function converts the input model to a map, removes empty values, and generates a string representation of the map.
1220// The purpose of this function is to provide a more readable output than the default JSON representation.
@@ -31,6 +39,9 @@ func BuildDebugStrFromInputModel(model any) (string, error) {
3139// The string representation is in the format: [key1: value1, key2: value2, ...]
3240// The keys are ordered alphabetically to make the output deterministic.
3341func BuildDebugStrFromMap (inputMap map [string ]any ) string {
42+ if inputMap == nil {
43+ return "[]"
44+ }
3445 // Sort the keys to make the output deterministic
3546 keys := make ([]string , 0 , len (inputMap ))
3647 for key := range inputMap {
@@ -44,7 +55,25 @@ func BuildDebugStrFromMap(inputMap map[string]any) string {
4455 if isEmpty (value ) {
4556 continue
4657 }
47- keyValues = append (keyValues , fmt .Sprintf ("%s: %v" , key , value ))
58+
59+ valueStr := fmt .Sprintf ("%v" , value )
60+
61+ switch value := value .(type ) {
62+ case map [string ]any :
63+ valueStr = BuildDebugStrFromMap (value )
64+ case []any :
65+ sliceStr := make ([]string , len (value ))
66+ for i , item := range value {
67+ if itemMap , ok := item .(map [string ]any ); ok {
68+ sliceStr [i ] = BuildDebugStrFromMap (itemMap )
69+ } else {
70+ sliceStr [i ] = fmt .Sprintf ("%v" , item )
71+ }
72+ }
73+ valueStr = BuildDebugStrFromSlice (sliceStr )
74+ }
75+
76+ keyValues = append (keyValues , fmt .Sprintf ("%s: %v" , key , valueStr ))
4877 }
4978
5079 result := strings .Join (keyValues , ", " )
@@ -57,6 +86,174 @@ func BuildDebugStrFromSlice(inputSlice []string) string {
5786 return fmt .Sprintf ("[%s]" , sliceStr )
5887}
5988
89+ // buildHeaderMap converts a map to a user-friendly string representation.
90+ // This function also filters the headers based on the includeHeaders parameter.
91+ // If includeHeaders is empty, the default header filters are used.
92+ func buildHeaderMap (headers http.Header , includeHeaders []string ) map [string ]any {
93+ headersMap := make (map [string ]any )
94+ for key , values := range headers {
95+ headersMap [key ] = strings .Join (values , ", " )
96+ }
97+
98+ headersToInclude := defaultHTTPHeaders
99+ if len (includeHeaders ) != 0 {
100+ headersToInclude = includeHeaders
101+ }
102+ for key := range headersMap {
103+ if ! slices .Contains (headersToInclude , key ) {
104+ delete (headersMap , key )
105+ }
106+ }
107+
108+ return headersMap
109+ }
110+
111+ // drainBody reads all of b to memory and then returns two equivalent
112+ // ReadClosers yielding the same bytes.
113+ //
114+ // It returns an error if the initial slurp of all bytes fails. It does not attempt
115+ // to make the returned ReadClosers have identical error-matching behavior.
116+ // Taken directly from the httputil package
117+ // https://cs.opensource.google/go/go/+/refs/tags/go1.22.2:src/net/http/httputil/dump.go;drc=1d45a7ef560a76318ed59dfdb178cecd58caf948;l=25
118+ func drainBody (b io.ReadCloser ) (r1 , r2 io.ReadCloser , err error ) {
119+ if b == nil || b == http .NoBody {
120+ // No copying needed. Preserve the magic sentinel meaning of NoBody.
121+ return http .NoBody , http .NoBody , nil
122+ }
123+ var buf bytes.Buffer
124+ if _ , err = buf .ReadFrom (b ); err != nil {
125+ return nil , b , err
126+ }
127+ if err := b .Close (); err != nil {
128+ return nil , b , err
129+ }
130+ return io .NopCloser (& buf ), io .NopCloser (bytes .NewReader (buf .Bytes ())), nil
131+ }
132+
133+ // BuildDebugStrFromHTTPRequest converts an HTTP request to a user-friendly string representation.
134+ // This function also receives a list of headers to include in the output, if empty, the default headers are used.
135+ // The return value is a list of strings that should be printed separately.
136+ func BuildDebugStrFromHTTPRequest (req * http.Request , includeHeaders []string ) ([]string , error ) {
137+ if req == nil {
138+ return nil , fmt .Errorf ("request is nil" )
139+ }
140+ if req .URL == nil || req .Proto == "" || req .Method == "" {
141+ return nil , fmt .Errorf ("request is invalid" )
142+ }
143+
144+ status := fmt .Sprintf ("request to %s: %s %s" , req .URL , req .Method , req .Proto )
145+
146+ headersMap := buildHeaderMap (req .Header , includeHeaders )
147+ headers := fmt .Sprintf ("request headers: %v" , BuildDebugStrFromMap (headersMap ))
148+
149+ var save io.ReadCloser
150+ var err error
151+
152+ save , req .Body , err = drainBody (req .Body )
153+ if err != nil {
154+ return []string {status , headers }, fmt .Errorf ("drain response body: %w" , err )
155+ }
156+ bodyBytes , err := io .ReadAll (req .Body )
157+ if err != nil {
158+ return []string {status , headers }, fmt .Errorf ("read response body: %w" , err )
159+ }
160+ req .Body = save
161+ var bodyMap map [string ]any
162+ if len (bodyBytes ) != 0 {
163+ if err := json .Unmarshal (bodyBytes , & bodyMap ); err != nil {
164+ return nil , fmt .Errorf ("unmarshal response body: %w" , err )
165+ }
166+ }
167+ if len (bodyMap ) == 0 {
168+ return []string {status , headers }, nil
169+ }
170+ body := fmt .Sprintf ("request body: %s" , BuildDebugStrFromMap (bodyMap ))
171+
172+ return []string {status , headers , body }, nil
173+ }
174+
175+ // BuildDebugStrFromHTTPResponse converts an HTTP response to a user-friendly string representation.
176+ // This function also receives a list of headers to include in the output, if empty, the default headers are used.
177+ // The return value is a list of strings that should be printed separately.
178+ func BuildDebugStrFromHTTPResponse (resp * http.Response , includeHeaders []string ) ([]string , error ) {
179+ if resp == nil {
180+ return nil , fmt .Errorf ("response is nil" )
181+ }
182+
183+ if resp .Request == nil || resp .Proto == "" || resp .Status == "" {
184+ return nil , fmt .Errorf ("response is invalid" )
185+ }
186+
187+ status := fmt .Sprintf ("response from %s: %s %s" , resp .Request .URL , resp .Proto , resp .Status )
188+
189+ headersMap := buildHeaderMap (resp .Header , includeHeaders )
190+ headers := fmt .Sprintf ("response headers: %v" , BuildDebugStrFromMap (headersMap ))
191+
192+ var save io.ReadCloser
193+ var err error
194+
195+ save , resp .Body , err = drainBody (resp .Body )
196+ if err != nil {
197+ return []string {status , headers }, fmt .Errorf ("drain response body: %w" , err )
198+ }
199+ bodyBytes , err := io .ReadAll (resp .Body )
200+ if err != nil {
201+ return []string {status , headers }, fmt .Errorf ("read response body: %w" , err )
202+ }
203+ resp .Body = save
204+ var bodyMap map [string ]any
205+ if len (bodyBytes ) != 0 {
206+ if err := json .Unmarshal (bodyBytes , & bodyMap ); err != nil {
207+ return nil , fmt .Errorf ("unmarshal response body: %w" , err )
208+ }
209+ }
210+ if len (bodyMap ) == 0 {
211+ return []string {status , headers }, nil
212+ }
213+ body := fmt .Sprintf ("response body: %s" , BuildDebugStrFromMap (bodyMap ))
214+
215+ return []string {status , headers , body }, nil
216+ }
217+
218+ // RequestResponseCapturer is a middleware that captures the request and response of an HTTP request.
219+ // Receives a printer and a list of headers to include in the output
220+ // If the list of headers is empty, the default headers are used.
221+ // The printer is used to print the captured data.
222+ func RequestResponseCapturer (p * Printer , includeHeaders []string ) config.Middleware {
223+ return func (rt http.RoundTripper ) http.RoundTripper {
224+ return & roundTripperWithCapture {rt , p , includeHeaders }
225+ }
226+ }
227+
228+ type roundTripperWithCapture struct {
229+ transport http.RoundTripper
230+ p * Printer
231+ debugHttpHeaders []string
232+ }
233+
234+ func (rt roundTripperWithCapture ) RoundTrip (req * http.Request ) (* http.Response , error ) {
235+ reqStr , err := BuildDebugStrFromHTTPRequest (req , rt .debugHttpHeaders )
236+ if err != nil {
237+ rt .p .Debug (ErrorLevel , "printing request to debug logs: %v" , err )
238+ }
239+ for _ , line := range reqStr {
240+ rt .p .Debug (DebugLevel , line )
241+ }
242+ resp , err := rt .transport .RoundTrip (req )
243+ defer func () {
244+ if err == nil {
245+ respStrSlice , tempErr := BuildDebugStrFromHTTPResponse (resp , rt .debugHttpHeaders )
246+ if tempErr != nil {
247+ rt .p .Debug (ErrorLevel , "printing HTTP response to debug logs: %v" , tempErr )
248+ }
249+ for _ , line := range respStrSlice {
250+ rt .p .Debug (DebugLevel , line )
251+ }
252+ }
253+ }()
254+ return resp , err
255+ }
256+
60257// isEmpty checks if a value is empty (nil, empty string, zero value for other types)
61258func isEmpty (value interface {}) bool {
62259 if value == nil {
0 commit comments