internallog.go raw

   1  // Copyright 2024, Google Inc.
   2  // All rights reserved.
   3  //
   4  // Redistribution and use in source and binary forms, with or without
   5  // modification, are permitted provided that the following conditions are
   6  // met:
   7  //
   8  //     * Redistributions of source code must retain the above copyright
   9  // notice, this list of conditions and the following disclaimer.
  10  //     * Redistributions in binary form must reproduce the above
  11  // copyright notice, this list of conditions and the following disclaimer
  12  // in the documentation and/or other materials provided with the
  13  // distribution.
  14  //     * Neither the name of Google Inc. nor the names of its
  15  // contributors may be used to endorse or promote products derived from
  16  // this software without specific prior written permission.
  17  //
  18  // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  19  // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  20  // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  21  // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  22  // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  23  // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  24  // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  25  // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  26  // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  27  // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  28  // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  29  
  30  // Package internallog in intended for internal use by generated clients only.
  31  package internallog
  32  
  33  import (
  34  	"bytes"
  35  	"encoding/json"
  36  	"fmt"
  37  	"log/slog"
  38  	"net/http"
  39  	"os"
  40  	"strings"
  41  
  42  	"github.com/googleapis/gax-go/v2/internallog/internal"
  43  )
  44  
  45  // New returns a new [slog.Logger] default logger, or the provided logger if
  46  // non-nil. The returned logger will be a no-op logger unless the environment
  47  // variable GOOGLE_SDK_GO_LOGGING_LEVEL is set.
  48  func New(l *slog.Logger) *slog.Logger {
  49  	if l != nil {
  50  		return l
  51  	}
  52  	return internal.NewLoggerWithWriter(os.Stderr)
  53  }
  54  
  55  // HTTPRequest returns a lazily evaluated [slog.LogValuer] for a
  56  // [http.Request] and the associated body.
  57  func HTTPRequest(req *http.Request, body []byte) slog.LogValuer {
  58  	return &request{
  59  		req:     req,
  60  		payload: body,
  61  	}
  62  }
  63  
  64  type request struct {
  65  	req     *http.Request
  66  	payload []byte
  67  }
  68  
  69  func (r *request) LogValue() slog.Value {
  70  	if r == nil || r.req == nil {
  71  		return slog.Value{}
  72  	}
  73  	var groupValueAttrs []slog.Attr
  74  	groupValueAttrs = append(groupValueAttrs, slog.String("method", r.req.Method))
  75  	groupValueAttrs = append(groupValueAttrs, slog.String("url", r.req.URL.String()))
  76  
  77  	var headerAttr []slog.Attr
  78  	for k, val := range r.req.Header {
  79  		headerAttr = append(headerAttr, slog.String(k, strings.Join(val, ",")))
  80  	}
  81  	if len(headerAttr) > 0 {
  82  		groupValueAttrs = append(groupValueAttrs, slog.Any("headers", headerAttr))
  83  	}
  84  
  85  	if len(r.payload) > 0 {
  86  		if attr, ok := processPayload(r.payload); ok {
  87  			groupValueAttrs = append(groupValueAttrs, attr)
  88  		}
  89  	}
  90  	return slog.GroupValue(groupValueAttrs...)
  91  }
  92  
  93  // HTTPResponse returns a lazily evaluated [slog.LogValuer] for a
  94  // [http.Response] and the associated body.
  95  func HTTPResponse(resp *http.Response, body []byte) slog.LogValuer {
  96  	return &response{
  97  		resp:    resp,
  98  		payload: body,
  99  	}
 100  }
 101  
 102  type response struct {
 103  	resp    *http.Response
 104  	payload []byte
 105  }
 106  
 107  func (r *response) LogValue() slog.Value {
 108  	if r == nil {
 109  		return slog.Value{}
 110  	}
 111  	var groupValueAttrs []slog.Attr
 112  	groupValueAttrs = append(groupValueAttrs, slog.String("status", fmt.Sprint(r.resp.StatusCode)))
 113  
 114  	var headerAttr []slog.Attr
 115  	for k, val := range r.resp.Header {
 116  		headerAttr = append(headerAttr, slog.String(k, strings.Join(val, ",")))
 117  	}
 118  	if len(headerAttr) > 0 {
 119  		groupValueAttrs = append(groupValueAttrs, slog.Any("headers", headerAttr))
 120  	}
 121  
 122  	if len(r.payload) > 0 {
 123  		if attr, ok := processPayload(r.payload); ok {
 124  			groupValueAttrs = append(groupValueAttrs, attr)
 125  		}
 126  	}
 127  	return slog.GroupValue(groupValueAttrs...)
 128  }
 129  
 130  func processPayload(payload []byte) (slog.Attr, bool) {
 131  	peekChar := payload[0]
 132  	if peekChar == '{' {
 133  		// JSON object
 134  		var m map[string]any
 135  		if err := json.Unmarshal(payload, &m); err == nil {
 136  			return slog.Any("payload", m), true
 137  		}
 138  	} else if peekChar == '[' {
 139  		// JSON array
 140  		var m []any
 141  		if err := json.Unmarshal(payload, &m); err == nil {
 142  			return slog.Any("payload", m), true
 143  		}
 144  	} else {
 145  		// Everything else
 146  		buf := &bytes.Buffer{}
 147  		if err := json.Compact(buf, payload); err != nil {
 148  			// Write raw payload incase of error
 149  			buf.Write(payload)
 150  		}
 151  		return slog.String("payload", buf.String()), true
 152  	}
 153  	return slog.Attr{}, false
 154  }
 155