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
 15package metadata
 16
 17import (
 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.
 32type noOpHandler struct{}
 33
 34func (h noOpHandler) Enabled(_ context.Context, _ slog.Level) bool {
 35	return false
 36}
 37
 38func (h noOpHandler) Handle(_ context.Context, _ slog.Record) error {
 39	return nil
 40}
 41
 42func (h noOpHandler) WithAttrs(_ []slog.Attr) slog.Handler {
 43	return h
 44}
 45
 46func (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.
 52func httpRequest(req *http.Request, body []byte) slog.LogValuer {
 53	return &request{
 54		req:     req,
 55		payload: body,
 56	}
 57}
 58
 59type request struct {
 60	req     *http.Request
 61	payload []byte
 62}
 63
 64func (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.
 90func httpResponse(resp *http.Response, body []byte) slog.LogValuer {
 91	return &response{
 92		resp:    resp,
 93		payload: body,
 94	}
 95}
 96
 97type response struct {
 98	resp    *http.Response
 99	payload []byte
100}
101
102func (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
125func 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}