log.go raw

   1  // Copyright 2024 Google LLC
   2  //
   3  // Licensed under the Apache License, Version 2.0 (the "License");
   4  // you may not use this file except in compliance with the License.
   5  // You may obtain a copy of the License at
   6  //
   7  //      http://www.apache.org/licenses/LICENSE-2.0
   8  //
   9  // Unless required by applicable law or agreed to in writing, software
  10  // distributed under the License is distributed on an "AS IS" BASIS,
  11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12  // See the License for the specific language governing permissions and
  13  // limitations under the License.
  14  
  15  package metadata
  16  
  17  import (
  18  	"bytes"
  19  	"context"
  20  	"encoding/json"
  21  	"fmt"
  22  	"log/slog"
  23  	"net/http"
  24  	"strings"
  25  )
  26  
  27  // Code below this point is copied from github.com/googleapis/gax-go/v2/internallog
  28  // to avoid the dependency. The compute/metadata module is used by too many
  29  // non-client library modules that can't justify the dependency.
  30  
  31  // The handler returned if logging is not enabled.
  32  type noOpHandler struct{}
  33  
  34  func (h noOpHandler) Enabled(_ context.Context, _ slog.Level) bool {
  35  	return false
  36  }
  37  
  38  func (h noOpHandler) Handle(_ context.Context, _ slog.Record) error {
  39  	return nil
  40  }
  41  
  42  func (h noOpHandler) WithAttrs(_ []slog.Attr) slog.Handler {
  43  	return h
  44  }
  45  
  46  func (h noOpHandler) WithGroup(_ string) slog.Handler {
  47  	return h
  48  }
  49  
  50  // httpRequest returns a lazily evaluated [slog.LogValuer] for a
  51  // [http.Request] and the associated body.
  52  func httpRequest(req *http.Request, body []byte) slog.LogValuer {
  53  	return &request{
  54  		req:     req,
  55  		payload: body,
  56  	}
  57  }
  58  
  59  type request struct {
  60  	req     *http.Request
  61  	payload []byte
  62  }
  63  
  64  func (r *request) LogValue() slog.Value {
  65  	if r == nil || r.req == nil {
  66  		return slog.Value{}
  67  	}
  68  	var groupValueAttrs []slog.Attr
  69  	groupValueAttrs = append(groupValueAttrs, slog.String("method", r.req.Method))
  70  	groupValueAttrs = append(groupValueAttrs, slog.String("url", r.req.URL.String()))
  71  
  72  	var headerAttr []slog.Attr
  73  	for k, val := range r.req.Header {
  74  		headerAttr = append(headerAttr, slog.String(k, strings.Join(val, ",")))
  75  	}
  76  	if len(headerAttr) > 0 {
  77  		groupValueAttrs = append(groupValueAttrs, slog.Any("headers", headerAttr))
  78  	}
  79  
  80  	if len(r.payload) > 0 {
  81  		if attr, ok := processPayload(r.payload); ok {
  82  			groupValueAttrs = append(groupValueAttrs, attr)
  83  		}
  84  	}
  85  	return slog.GroupValue(groupValueAttrs...)
  86  }
  87  
  88  // httpResponse returns a lazily evaluated [slog.LogValuer] for a
  89  // [http.Response] and the associated body.
  90  func httpResponse(resp *http.Response, body []byte) slog.LogValuer {
  91  	return &response{
  92  		resp:    resp,
  93  		payload: body,
  94  	}
  95  }
  96  
  97  type response struct {
  98  	resp    *http.Response
  99  	payload []byte
 100  }
 101  
 102  func (r *response) LogValue() slog.Value {
 103  	if r == nil {
 104  		return slog.Value{}
 105  	}
 106  	var groupValueAttrs []slog.Attr
 107  	groupValueAttrs = append(groupValueAttrs, slog.String("status", fmt.Sprint(r.resp.StatusCode)))
 108  
 109  	var headerAttr []slog.Attr
 110  	for k, val := range r.resp.Header {
 111  		headerAttr = append(headerAttr, slog.String(k, strings.Join(val, ",")))
 112  	}
 113  	if len(headerAttr) > 0 {
 114  		groupValueAttrs = append(groupValueAttrs, slog.Any("headers", headerAttr))
 115  	}
 116  
 117  	if len(r.payload) > 0 {
 118  		if attr, ok := processPayload(r.payload); ok {
 119  			groupValueAttrs = append(groupValueAttrs, attr)
 120  		}
 121  	}
 122  	return slog.GroupValue(groupValueAttrs...)
 123  }
 124  
 125  func processPayload(payload []byte) (slog.Attr, bool) {
 126  	peekChar := payload[0]
 127  	if peekChar == '{' {
 128  		// JSON object
 129  		var m map[string]any
 130  		if err := json.Unmarshal(payload, &m); err == nil {
 131  			return slog.Any("payload", m), true
 132  		}
 133  	} else if peekChar == '[' {
 134  		// JSON array
 135  		var m []any
 136  		if err := json.Unmarshal(payload, &m); err == nil {
 137  			return slog.Any("payload", m), true
 138  		}
 139  	} else {
 140  		// Everything else
 141  		buf := &bytes.Buffer{}
 142  		if err := json.Compact(buf, payload); err != nil {
 143  			// Write raw payload incase of error
 144  			buf.Write(payload)
 145  		}
 146  		return slog.String("payload", buf.String()), true
 147  	}
 148  	return slog.Attr{}, false
 149  }
 150