internallog.go

  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.
 31package internallog
 32
 33import (
 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.
 48func 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.
 57func HTTPRequest(req *http.Request, body []byte) slog.LogValuer {
 58	return &request{
 59		req:     req,
 60		payload: body,
 61	}
 62}
 63
 64type request struct {
 65	req     *http.Request
 66	payload []byte
 67}
 68
 69func (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.
 95func HTTPResponse(resp *http.Response, body []byte) slog.LogValuer {
 96	return &response{
 97		resp:    resp,
 98		payload: body,
 99	}
100}
101
102type response struct {
103	resp    *http.Response
104	payload []byte
105}
106
107func (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
130func 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}