From 36f7640c0e73cbe65e980a036891a7b32a95c4e5 Mon Sep 17 00:00:00 2001 From: Cody Oss Date: Thu, 31 Oct 2024 13:47:13 -0500 Subject: [PATCH 1/3] feat(internallog): add a logging support package These added packages will be used by our libraries to create loggers and help create nicely formated log lines in a Cloud Logging format. --- go.work.sum | 10 +- v2/internallog/grpclog/grpclog.go | 88 +++++++++ v2/internallog/grpclog/grpclog_test.go | 90 +++++++++ v2/internallog/grpclog/testdata/request.log | 1 + v2/internallog/grpclog/testdata/response.log | 1 + v2/internallog/internal/bookpb/book.pb.go | 186 +++++++++++++++++ v2/internallog/internal/bookpb/book.proto | 43 ++++ v2/internallog/internal/internal.go | 132 +++++++++++++ v2/internallog/internal/logtest/logtest.go | 104 ++++++++++ v2/internallog/internallog.go | 154 +++++++++++++++ v2/internallog/internallog_test.go | 187 ++++++++++++++++++ v2/internallog/testdata/envar-debug.log | 4 + v2/internallog/testdata/envar-error.log | 1 + v2/internallog/testdata/envar-info.log | 3 + v2/internallog/testdata/envar-warn.log | 2 + v2/internallog/testdata/httpRequest-array.log | 1 + v2/internallog/testdata/httpRequest-form.log | 1 + v2/internallog/testdata/httpRequest.log | 1 + v2/internallog/testdata/httpResponse.log | 1 + v2/internallog/testdata/off.log | 0 20 files changed, 1008 insertions(+), 2 deletions(-) create mode 100644 v2/internallog/grpclog/grpclog.go create mode 100644 v2/internallog/grpclog/grpclog_test.go create mode 100755 v2/internallog/grpclog/testdata/request.log create mode 100755 v2/internallog/grpclog/testdata/response.log create mode 100644 v2/internallog/internal/bookpb/book.pb.go create mode 100644 v2/internallog/internal/bookpb/book.proto create mode 100644 v2/internallog/internal/internal.go create mode 100644 v2/internallog/internal/logtest/logtest.go create mode 100644 v2/internallog/internallog.go create mode 100644 v2/internallog/internallog_test.go create mode 100755 v2/internallog/testdata/envar-debug.log create mode 100755 v2/internallog/testdata/envar-error.log create mode 100755 v2/internallog/testdata/envar-info.log create mode 100755 v2/internallog/testdata/envar-warn.log create mode 100755 v2/internallog/testdata/httpRequest-array.log create mode 100755 v2/internallog/testdata/httpRequest-form.log create mode 100755 v2/internallog/testdata/httpRequest.log create mode 100755 v2/internallog/testdata/httpResponse.log create mode 100755 v2/internallog/testdata/off.log diff --git a/go.work.sum b/go.work.sum index f0f1cc64..8905f9fc 100644 --- a/go.work.sum +++ b/go.work.sum @@ -34,6 +34,7 @@ cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9 cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro= cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s= cloud.google.com/go/auth v0.6.1/go.mod h1:eFHG7zDzbXHKmjJddFG/rBlcGp6t25SwRUiEQSlO4x4= +cloud.google.com/go/auth v0.7.0/go.mod h1:D+WqdrpcjmiCgWrXmLLxOVq1GACoE36chW6KXoEvuIw= cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= cloud.google.com/go/automl v1.13.1/go.mod h1:1aowgAHWYZU27MybSCFiukPO7xnyawv7pt3zK4bheQE= cloud.google.com/go/automl v1.13.7/go.mod h1:E+s0VOsYXUdXpq0y4gNZpi0A/s6y9+lAarmV5Eqlg40= @@ -71,6 +72,7 @@ cloud.google.com/go/compute v1.27.0/go.mod h1:LG5HwRmWFKM2C5XxHRiNzkLLXW48WwvyVC cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.4.0/go.mod h1:SIQh1Kkb4ZJ8zJ874fqVkslA29PRXuleyj6vOzlbK7M= cloud.google.com/go/contactcenterinsights v1.9.1/go.mod h1:bsg/R7zGLYMVxFFzfh9ooLTruLRCG9fnzhH9KznHhbM= cloud.google.com/go/contactcenterinsights v1.13.2/go.mod h1:AfkSB8t7mt2sIY6WpfO61nD9J9fcidIchtxm9FqJVXk= cloud.google.com/go/container v1.22.1/go.mod h1:lTNExE2R7f+DLbAN+rJiKTisauFCaoDq6NURZ83eVH4= @@ -375,6 +377,7 @@ golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIi golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -445,6 +448,7 @@ golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -493,6 +497,8 @@ google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnz google.golang.org/genproto v0.0.0-20240604185151-ef581f913117/go.mod h1:lesfX/+9iA+3OdqeCpoDddJaNxVB1AB6tD7EfqMmprc= google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d h1:PksQg4dV6Sem3/HkBX+Ltq8T0ke0PKIRBNBatoDTVls= google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:s7iA721uChleev562UJO2OYB0PPT9CMFjV+Ce7VJH5M= +google.golang.org/genproto v0.0.0-20240708141625-4ad9e859172b h1:dSTjko30weBaMj3eERKc0ZVXW4GudCswM3m+P++ukU0= +google.golang.org/genproto v0.0.0-20240708141625-4ad9e859172b/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= @@ -509,6 +515,7 @@ google.golang.org/genproto/googleapis/bytestream v0.0.0-20240325203815-454cdb8f5 google.golang.org/genproto/googleapis/bytestream v0.0.0-20240513163218-0867130af1f8/go.mod h1:RCpt0+3mpEDPldc32vXBM8ADXlFL95T8Chxx0nv0/zE= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240604185151-ef581f913117/go.mod h1:0J6mmn3XAEjfNbPvpH63c0RXCjGNFcCzlEfWSN4In+k= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:/oe3+SiHAwz6s+M25PyTygWm3lnrhmGqIuIfkoUocqk= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20240708141625-4ad9e859172b/go.mod h1:5/MT647Cn/GGhwTpXC7QqcaR5Cnee4v4MKCU1/nwnIQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o= @@ -522,6 +529,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240624140628-dc46fd24d27d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -536,8 +544,6 @@ google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpX google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= -google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= -google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/v2/internallog/grpclog/grpclog.go b/v2/internallog/grpclog/grpclog.go new file mode 100644 index 00000000..bf1d864b --- /dev/null +++ b/v2/internallog/grpclog/grpclog.go @@ -0,0 +1,88 @@ +// Copyright 2024, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Package grpclog in intended for internal use by generated clients only. +package grpclog + +import ( + "context" + "encoding/json" + "log/slog" + "strings" + + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +// ProtoMessageRequest returns a lazily evaluated [slog.LogValuer] for +// the provided message. The context is used to extract outgoing headers. +func ProtoMessageRequest(ctx context.Context, msg proto.Message) slog.LogValuer { + return &protoMessage{ctx: ctx, msg: msg} +} + +// ProtoMessageResponse returns a lazily evaluated [slog.LogValuer] for +// the provided message. +func ProtoMessageResponse(msg proto.Message) slog.LogValuer { + return &protoMessage{msg: msg} +} + +type protoMessage struct { + ctx context.Context + msg proto.Message +} + +func (m *protoMessage) LogValue() slog.Value { + if m == nil || m.msg == nil { + return slog.Value{} + } + + var groupValueAttrs []slog.Attr + + if m.ctx != nil { + var headerAttr []slog.Attr + if m, ok := metadata.FromOutgoingContext(m.ctx); ok { + for k, v := range m { + headerAttr = append(headerAttr, slog.String(k, strings.Join(v, ","))) + } + } + if len(headerAttr) > 0 { + groupValueAttrs = append(groupValueAttrs, slog.Any("headers", headerAttr)) + } + } + mo := protojson.MarshalOptions{AllowPartial: true, UseEnumNumbers: true} + if b, err := mo.Marshal(m.msg); err == nil { + var m map[string]any + if err := json.Unmarshal(b, &m); err == nil { + groupValueAttrs = append(groupValueAttrs, slog.Any("payload", m)) + } + } + + return slog.GroupValue(groupValueAttrs...) +} diff --git a/v2/internallog/grpclog/grpclog_test.go b/v2/internallog/grpclog/grpclog_test.go new file mode 100644 index 00000000..212a882c --- /dev/null +++ b/v2/internallog/grpclog/grpclog_test.go @@ -0,0 +1,90 @@ +// Copyright 2024, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// - Redistributions of source code must retain the above copyright +// +// notice, this list of conditions and the following disclaimer. +// - Redistributions in binary form must reproduce the above +// +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// - Neither the name of Google Inc. nor the names of its +// +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package grpclog + +import ( + "context" + "flag" + "log/slog" + "os" + "testing" + + "github.com/googleapis/gax-go/v2/internallog/internal" + "github.com/googleapis/gax-go/v2/internallog/internal/bookpb" + "github.com/googleapis/gax-go/v2/internallog/internal/logtest" + "google.golang.org/grpc/metadata" +) + +// To update conformance tests in this package run `go test -update_golden` +func TestMain(m *testing.M) { + flag.Parse() + os.Exit(m.Run()) +} + +func TestLog_protoMessageRequest(t *testing.T) { + golden := "request.log" + t.Setenv(internal.LoggingLevelEnvVar, "debug") + logger, f := setupLogger(t, golden) + ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("foo", "bar")) + book := &bookpb.Book{ + Title: "The book", + Author: "The author", + } + logger.DebugContext(ctx, "msg", "request", ProtoMessageRequest(ctx, book)) + f.Close() + logtest.DiffTest(t, f.Name(), golden) +} + +func TestLog_protoMessageResponse(t *testing.T) { + golden := "response.log" + t.Setenv(internal.LoggingLevelEnvVar, "debug") + logger, f := setupLogger(t, golden) + ctx := context.Background() + book := &bookpb.Book{ + Title: "The book", + Author: "The author", + } + logger.DebugContext(ctx, "msg", "response", ProtoMessageResponse(book)) + f.Close() + logtest.DiffTest(t, f.Name(), golden) +} + +func setupLogger(t *testing.T, golden string) (*slog.Logger, *os.File) { + t.Helper() + f, err := os.CreateTemp(t.TempDir(), golden) + if err != nil { + t.Fatal(err) + } + logger := internal.NewLoggerWithWriter(f) + return logger, f +} diff --git a/v2/internallog/grpclog/testdata/request.log b/v2/internallog/grpclog/testdata/request.log new file mode 100755 index 00000000..b28921e2 --- /dev/null +++ b/v2/internallog/grpclog/testdata/request.log @@ -0,0 +1 @@ +{"message":"msg","request":{"headers":{"foo":"bar"},"payload":{"author":"The author","title":"The book"}},"severity":"DEBUG"} diff --git a/v2/internallog/grpclog/testdata/response.log b/v2/internallog/grpclog/testdata/response.log new file mode 100755 index 00000000..1067f751 --- /dev/null +++ b/v2/internallog/grpclog/testdata/response.log @@ -0,0 +1 @@ +{"message":"msg","response":{"payload":{"author":"The author","title":"The book"}},"severity":"DEBUG"} diff --git a/v2/internallog/internal/bookpb/book.pb.go b/v2/internallog/internal/bookpb/book.pb.go new file mode 100644 index 00000000..098205c4 --- /dev/null +++ b/v2/internallog/internal/bookpb/book.pb.go @@ -0,0 +1,186 @@ +// Copyright 2024, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.33.0 +// protoc v5.28.2 +// source: book.proto + +package bookpb + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// A single book in the library. +type Book struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The title of the book. + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` + // The name of the book author. + Author string `protobuf:"bytes,2,opt,name=author,proto3" json:"author,omitempty"` +} + +func (x *Book) Reset() { + *x = Book{} + if protoimpl.UnsafeEnabled { + mi := &file_book_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Book) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Book) ProtoMessage() {} + +func (x *Book) ProtoReflect() protoreflect.Message { + mi := &file_book_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Book.ProtoReflect.Descriptor instead. +func (*Book) Descriptor() ([]byte, []int) { + return file_book_proto_rawDescGZIP(), []int{0} +} + +func (x *Book) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *Book) GetAuthor() string { + if x != nil { + return x.Author + } + return "" +} + +var File_book_proto protoreflect.FileDescriptor + +var file_book_proto_rawDesc = []byte{ + 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x62, 0x6f, + 0x6f, 0x6b, 0x22, 0x34, 0x0a, 0x04, 0x42, 0x6f, 0x6f, 0x6b, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, + 0x74, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x61, 0x70, 0x69, + 0x73, 0x2f, 0x67, 0x61, 0x78, 0x2d, 0x67, 0x6f, 0x2f, 0x76, 0x32, 0x2f, 0x63, 0x6c, 0x6f, 0x67, + 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x62, 0x6f, 0x6f, 0x6b, 0x70, 0x62, + 0x3b, 0x62, 0x6f, 0x6f, 0x6b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_book_proto_rawDescOnce sync.Once + file_book_proto_rawDescData = file_book_proto_rawDesc +) + +func file_book_proto_rawDescGZIP() []byte { + file_book_proto_rawDescOnce.Do(func() { + file_book_proto_rawDescData = protoimpl.X.CompressGZIP(file_book_proto_rawDescData) + }) + return file_book_proto_rawDescData +} + +var file_book_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_book_proto_goTypes = []interface{}{ + (*Book)(nil), // 0: book.Book +} +var file_book_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_book_proto_init() } +func file_book_proto_init() { + if File_book_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_book_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Book); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_book_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_book_proto_goTypes, + DependencyIndexes: file_book_proto_depIdxs, + MessageInfos: file_book_proto_msgTypes, + }.Build() + File_book_proto = out.File + file_book_proto_rawDesc = nil + file_book_proto_goTypes = nil + file_book_proto_depIdxs = nil +} diff --git a/v2/internallog/internal/bookpb/book.proto b/v2/internallog/internal/bookpb/book.proto new file mode 100644 index 00000000..672e4edb --- /dev/null +++ b/v2/internallog/internal/bookpb/book.proto @@ -0,0 +1,43 @@ +// Copyright 2024, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package book; + +option go_package = "github.com/googleapis/gax-go/v2/internallog/internal/bookpb;bookpb"; + +// A single book in the library. +message Book { + // The title of the book. + string title = 1; + + // The name of the book author. + string author = 2; +} diff --git a/v2/internallog/internal/internal.go b/v2/internallog/internal/internal.go new file mode 100644 index 00000000..23652a2f --- /dev/null +++ b/v2/internallog/internal/internal.go @@ -0,0 +1,132 @@ +// Copyright 2024, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package internal + +import ( + "context" + "io" + "log/slog" + "os" + "strings" + "time" +) + +const ( + // LoggingLevelEnvVar is the environment variable used to enable logging + // at a particular level. + LoggingLevelEnvVar = "GOOGLE_SDK_GO_LOGGING_LEVEL" + + googLvlKey = "severity" + googMsgKey = "message" + googSourceKey = "sourceLocation" + googTimeKey = "timestamp" +) + +// NewLoggerWithWriter is exposed for testing. +func NewLoggerWithWriter(w io.Writer) *slog.Logger { + lvl, loggingEnabled := checkLoggingLevel() + if !loggingEnabled { + return slog.New(noOpHandler{}) + } + return slog.New(newGCPSlogHandler(lvl, w)) +} + +// checkLoggingLevel returned the configured logging level and whether or not +// logging is enabled. +func checkLoggingLevel() (slog.Leveler, bool) { + sLevel := strings.ToLower(os.Getenv(LoggingLevelEnvVar)) + var level slog.Level + switch sLevel { + case "debug": + level = slog.LevelDebug + case "info": + level = slog.LevelInfo + case "warn": + level = slog.LevelWarn + case "error": + level = slog.LevelError + default: + return nil, false + } + return level, true +} + +// newGCPSlogHandler returns a Handler that is configured to output in a JSON +// format with well-known keys. For more information on this format see +// https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields. +func newGCPSlogHandler(lvl slog.Leveler, w io.Writer) slog.Handler { + return slog.NewJSONHandler(w, &slog.HandlerOptions{ + Level: lvl, + ReplaceAttr: replaceAttr, + }) +} + +// replaceAttr remaps default Go logging keys to match what is expected in +// cloud logging. +func replaceAttr(groups []string, a slog.Attr) slog.Attr { + if groups == nil { + if a.Key == slog.LevelKey { + a.Key = googLvlKey + return a + } else if a.Key == slog.MessageKey { + a.Key = googMsgKey + return a + } else if a.Key == slog.SourceKey { + a.Key = googSourceKey + return a + } else if a.Key == slog.TimeKey { + a.Key = googTimeKey + if a.Value.Kind() == slog.KindTime { + a.Value = slog.StringValue(a.Value.Time().Format(time.RFC3339)) + } + return a + } + } + return a +} + +// The handler returned if logging is not enabled. +type noOpHandler struct{} + +func (h noOpHandler) Enabled(_ context.Context, _ slog.Level) bool { + return false +} + +func (h noOpHandler) Handle(_ context.Context, _ slog.Record) error { + return nil +} + +func (h noOpHandler) WithAttrs(_ []slog.Attr) slog.Handler { + return h +} + +func (h noOpHandler) WithGroup(_ string) slog.Handler { + return h +} diff --git a/v2/internallog/internal/logtest/logtest.go b/v2/internallog/internal/logtest/logtest.go new file mode 100644 index 00000000..3bfbafb0 --- /dev/null +++ b/v2/internallog/internal/logtest/logtest.go @@ -0,0 +1,104 @@ +// Copyright 2024, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Pacakage logtest is a helper for validating logging tests. +// +// To update conformance tests in this package run `go test -update_golden` +package logtest + +import ( + "bytes" + "encoding/json" + "flag" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" +) + +var updateGolden = flag.Bool("update-golden", false, "update golden files") + +// DiffTest is a test helper, testing got against contents of a goldenFile. +func DiffTest(t *testing.T, tempFile, goldenFile string) { + rawGot, err := os.ReadFile(tempFile) + if err != nil { + t.Fatal(err) + } + t.Helper() + if *updateGolden { + got := removeLogVariance(t, rawGot) + if err := os.WriteFile(filepath.Join("testdata", goldenFile), got, os.ModePerm); err != nil { + t.Fatal(err) + } + return + } + + want, err := os.ReadFile(filepath.Join("testdata", goldenFile)) + if err != nil { + t.Fatal(err) + } + got := removeLogVariance(t, rawGot) + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("mismatch(-want, +got): %s", diff) + } +} + +// removeLogVariance removes parts of log lines that may differ between runs +// and/or machines. +func removeLogVariance(t *testing.T, in []byte) []byte { + if len(in) == 0 { + return in + } + bs := bytes.Split(in, []byte("\n")) + for i, b := range bs { + if len(b) == 0 { + continue + } + m := map[string]any{} + if err := json.Unmarshal(b, &m); err != nil { + t.Fatal(err) + } + delete(m, "timestamp") + if sl, ok := m["sourceLocation"].(map[string]any); ok { + delete(sl, "file") + // So that if test cases move around in this file they don't cause + // failures + delete(sl, "line") + } + b2, err := json.Marshal(m) + if err != nil { + t.Fatal(err) + } + t.Logf("%s", b2) + bs[i] = b2 + } + return bytes.Join(bs, []byte("\n")) +} diff --git a/v2/internallog/internallog.go b/v2/internallog/internallog.go new file mode 100644 index 00000000..b9b0a8bd --- /dev/null +++ b/v2/internallog/internallog.go @@ -0,0 +1,154 @@ +// Copyright 2024, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Package internallog in intended for internal use by generated clients only. +package internallog + +import ( + "bytes" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "os" + "strings" + + "github.com/googleapis/gax-go/v2/internallog/internal" +) + +// New returns a new [slog.Logger] default logger, or the provided logger if +// non-nil. The returned logger will be a no-op logger unless the environment +// variable GOOGLE_SDK_DEBUG_LOGGING is set. +func New(l *slog.Logger) *slog.Logger { + if l != nil { + return l + } + return internal.NewLoggerWithWriter(os.Stderr) +} + +// HTTPRequest returns a lazily evaluated [slog.LogValuer] for a +// [http.Response] and the associated body. +func HTTPRequest(req *http.Request, body []byte) slog.LogValuer { + return &request{ + req: req, + payload: body, + } +} + +type request struct { + req *http.Request + payload []byte +} + +func (r *request) LogValue() slog.Value { + if r == nil || r.req == nil { + return slog.Value{} + } + var groupValueAttrs []slog.Attr + groupValueAttrs = append(groupValueAttrs, slog.String("method", r.req.Method)) + groupValueAttrs = append(groupValueAttrs, slog.String("url", r.req.URL.String())) + + var headerAttr []slog.Attr + for k, val := range r.req.Header { + headerAttr = append(headerAttr, slog.String(k, strings.Join(val, ","))) + } + if len(headerAttr) > 0 { + groupValueAttrs = append(groupValueAttrs, slog.Any("headers", headerAttr)) + } + + if len(r.payload) > 0 { + if attr, ok := processPayload(r.payload); ok { + groupValueAttrs = append(groupValueAttrs, attr) + } + } + return slog.GroupValue(groupValueAttrs...) +} + +// HTTPResponse returns a lazily evaluated [slog.LogValuer] for a +// [http.Response] and the associated body. +func HTTPResponse(resp *http.Response, body []byte) slog.LogValuer { + return &response{ + resp: resp, + payload: body, + } +} + +type response struct { + resp *http.Response + payload []byte +} + +func (r *response) LogValue() slog.Value { + if r == nil { + return slog.Value{} + } + var groupValueAttrs []slog.Attr + groupValueAttrs = append(groupValueAttrs, slog.String("status", fmt.Sprint(r.resp.StatusCode))) + + var headerAttr []slog.Attr + for k, val := range r.resp.Header { + headerAttr = append(headerAttr, slog.String(k, strings.Join(val, ","))) + } + if len(headerAttr) > 0 { + groupValueAttrs = append(groupValueAttrs, slog.Any("headers", headerAttr)) + } + + if len(r.payload) > 0 { + if attr, ok := processPayload(r.payload); ok { + groupValueAttrs = append(groupValueAttrs, attr) + } + } + return slog.GroupValue(groupValueAttrs...) +} + +func processPayload(payload []byte) (slog.Attr, bool) { + peekChar := payload[0] + if peekChar == '{' { + // JSON object + var m map[string]any + if err := json.Unmarshal(payload, &m); err == nil { + return slog.Any("payload", m), true + } + } else if peekChar == '[' { + // JSON array + var m []any + if err := json.Unmarshal(payload, &m); err == nil { + return slog.Any("payload", m), true + } + } else { + // Everything else + buf := &bytes.Buffer{} + if err := json.Compact(buf, payload); err != nil { + // Write raw payload incase of error + buf.Write(payload) + } + return slog.String("payload", buf.String()), true + } + return slog.Attr{}, false +} diff --git a/v2/internallog/internallog_test.go b/v2/internallog/internallog_test.go new file mode 100644 index 00000000..03cc78c1 --- /dev/null +++ b/v2/internallog/internallog_test.go @@ -0,0 +1,187 @@ +// Copyright 2024, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package internallog + +import ( + "bytes" + "context" + "flag" + "io" + "log/slog" + "net/http" + "net/url" + "os" + "strings" + "testing" + + "github.com/googleapis/gax-go/v2/internallog/internal" + "github.com/googleapis/gax-go/v2/internallog/internal/logtest" +) + +// To update conformance tests in this package run `go test -update_golden` +func TestMain(m *testing.M) { + flag.Parse() + os.Exit(m.Run()) +} + +func TestLog_off(t *testing.T) { + golden := "off.log" + logger, f := setupLogger(t, golden) + logger.Error("one") + logger.Info("two") + logger.Warn("three") + logger.Debug("four") + f.Close() + logtest.DiffTest(t, f.Name(), golden) +} + +func TestLog_envarError(t *testing.T) { + golden := "envar-error.log" + t.Setenv(internal.LoggingLevelEnvVar, "eRrOr") + logger, f := setupLogger(t, golden) + logger.Error("one") + logger.Info("two") + logger.Warn("three") + logger.Debug("four") + f.Close() + logtest.DiffTest(t, f.Name(), golden) +} + +func TestLog_envarInfo(t *testing.T) { + golden := "envar-info.log" + t.Setenv(internal.LoggingLevelEnvVar, "info") + logger, f := setupLogger(t, golden) + logger.Error("one") + logger.Info("two") + logger.Warn("three") + logger.Debug("four") + f.Close() + logtest.DiffTest(t, f.Name(), golden) +} + +func TestLog_envarWarn(t *testing.T) { + golden := "envar-warn.log" + t.Setenv(internal.LoggingLevelEnvVar, "warn") + logger, f := setupLogger(t, golden) + logger.Error("one") + logger.Info("two") + logger.Warn("three") + logger.Debug("four") + f.Close() + logtest.DiffTest(t, f.Name(), golden) +} + +func TestLog_envarDebug(t *testing.T) { + golden := "envar-debug.log" + t.Setenv(internal.LoggingLevelEnvVar, "debug") + logger, f := setupLogger(t, golden) + logger.Error("one") + logger.Info("two") + logger.Warn("three") + logger.Debug("four") + f.Close() + logtest.DiffTest(t, f.Name(), golden) +} + +func TestLog_HTTPRequest(t *testing.T) { + golden := "httpRequest.log" + t.Setenv(internal.LoggingLevelEnvVar, "debug") + logger, f := setupLogger(t, golden) + ctx := context.Background() + body := []byte(`{"secret":"shh, it's a secret"}`) + request, err := http.NewRequest(http.MethodPost, "https://example.com", bytes.NewReader(body)) + if err != nil { + t.Fatal(err) + } + request.Header.Add("foo", "bar") + logger.DebugContext(ctx, "msg", "request", HTTPRequest(request, body)) + f.Close() + logtest.DiffTest(t, f.Name(), golden) +} + +func TestLog_HTTPResponse(t *testing.T) { + golden := "httpResponse.log" + t.Setenv(internal.LoggingLevelEnvVar, "debug") + logger, f := setupLogger(t, golden) + ctx := context.Background() + body := []byte(`{"secret":"shh, it's a secret"}`) + response := &http.Response{ + StatusCode: 200, + Header: http.Header{"Foo": []string{"bar"}}, + Body: io.NopCloser(bytes.NewReader(body)), + } + logger.DebugContext(ctx, "msg", "response", HTTPResponse(response, body)) + f.Close() + logtest.DiffTest(t, f.Name(), golden) +} + +func TestLog_HTTPRequest_formData(t *testing.T) { + golden := "httpRequest-form.log" + t.Setenv(internal.LoggingLevelEnvVar, "debug") + logger, f := setupLogger(t, golden) + ctx := context.Background() + form := url.Values{} + form.Add("foo", "bar") + form.Add("baz", "qux") + request, err := http.NewRequest(http.MethodPost, "https://example.com", strings.NewReader(form.Encode())) + if err != nil { + t.Fatal(err) + } + request.Header.Add("foo", "bar") + logger.DebugContext(ctx, "msg", "request", HTTPRequest(request, []byte(form.Encode()))) + f.Close() + logtest.DiffTest(t, f.Name(), golden) +} + +func TestLog_HTTPRequest_jsonArray(t *testing.T) { + golden := "httpRequest-array.log" + t.Setenv(internal.LoggingLevelEnvVar, "debug") + logger, f := setupLogger(t, golden) + ctx := context.Background() + body := []byte(`[{"secret":"shh, it's a secret"},{"secret":"and, another"}]`) + request, err := http.NewRequest(http.MethodPost, "https://example.com", bytes.NewReader(body)) + if err != nil { + t.Fatal(err) + } + request.Header.Add("foo", "bar") + logger.DebugContext(ctx, "msg", "request", HTTPRequest(request, body)) + f.Close() + logtest.DiffTest(t, f.Name(), golden) +} + +func setupLogger(t *testing.T, golden string) (*slog.Logger, *os.File) { + t.Helper() + f, err := os.CreateTemp(t.TempDir(), golden) + if err != nil { + t.Fatal(err) + } + logger := internal.NewLoggerWithWriter(f) + return logger, f +} diff --git a/v2/internallog/testdata/envar-debug.log b/v2/internallog/testdata/envar-debug.log new file mode 100755 index 00000000..ed69d01d --- /dev/null +++ b/v2/internallog/testdata/envar-debug.log @@ -0,0 +1,4 @@ +{"message":"one","severity":"ERROR"} +{"message":"two","severity":"INFO"} +{"message":"three","severity":"WARN"} +{"message":"four","severity":"DEBUG"} diff --git a/v2/internallog/testdata/envar-error.log b/v2/internallog/testdata/envar-error.log new file mode 100755 index 00000000..af29f227 --- /dev/null +++ b/v2/internallog/testdata/envar-error.log @@ -0,0 +1 @@ +{"message":"one","severity":"ERROR"} diff --git a/v2/internallog/testdata/envar-info.log b/v2/internallog/testdata/envar-info.log new file mode 100755 index 00000000..24cb09a2 --- /dev/null +++ b/v2/internallog/testdata/envar-info.log @@ -0,0 +1,3 @@ +{"message":"one","severity":"ERROR"} +{"message":"two","severity":"INFO"} +{"message":"three","severity":"WARN"} diff --git a/v2/internallog/testdata/envar-warn.log b/v2/internallog/testdata/envar-warn.log new file mode 100755 index 00000000..d658b0ef --- /dev/null +++ b/v2/internallog/testdata/envar-warn.log @@ -0,0 +1,2 @@ +{"message":"one","severity":"ERROR"} +{"message":"three","severity":"WARN"} diff --git a/v2/internallog/testdata/httpRequest-array.log b/v2/internallog/testdata/httpRequest-array.log new file mode 100755 index 00000000..4814c04c --- /dev/null +++ b/v2/internallog/testdata/httpRequest-array.log @@ -0,0 +1 @@ +{"message":"msg","request":{"headers":{"Foo":"bar"},"method":"POST","payload":[{"secret":"shh, it's a secret"},{"secret":"and, another"}],"url":"https://example.com"},"severity":"DEBUG"} diff --git a/v2/internallog/testdata/httpRequest-form.log b/v2/internallog/testdata/httpRequest-form.log new file mode 100755 index 00000000..46d6ad14 --- /dev/null +++ b/v2/internallog/testdata/httpRequest-form.log @@ -0,0 +1 @@ +{"message":"msg","request":{"headers":{"Foo":"bar"},"method":"POST","payload":"baz=qux\u0026foo=bar","url":"https://example.com"},"severity":"DEBUG"} diff --git a/v2/internallog/testdata/httpRequest.log b/v2/internallog/testdata/httpRequest.log new file mode 100755 index 00000000..54739c0a --- /dev/null +++ b/v2/internallog/testdata/httpRequest.log @@ -0,0 +1 @@ +{"message":"msg","request":{"headers":{"Foo":"bar"},"method":"POST","payload":{"secret":"shh, it's a secret"},"url":"https://example.com"},"severity":"DEBUG"} diff --git a/v2/internallog/testdata/httpResponse.log b/v2/internallog/testdata/httpResponse.log new file mode 100755 index 00000000..fe39de77 --- /dev/null +++ b/v2/internallog/testdata/httpResponse.log @@ -0,0 +1 @@ +{"message":"msg","response":{"headers":{"Foo":"bar"},"payload":{"secret":"shh, it's a secret"},"status":"200"},"severity":"DEBUG"} diff --git a/v2/internallog/testdata/off.log b/v2/internallog/testdata/off.log new file mode 100755 index 00000000..e69de29b From a316fec18753298be6f93f0e3d9ed94ab40f5288 Mon Sep 17 00:00:00 2001 From: Cody Oss Date: Thu, 7 Nov 2024 10:39:49 -0600 Subject: [PATCH 2/3] fixup lints --- v2/internallog/internal/internal.go | 2 ++ v2/internallog/internal/logtest/logtest.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/v2/internallog/internal/internal.go b/v2/internallog/internal/internal.go index 23652a2f..19f4be35 100644 --- a/v2/internallog/internal/internal.go +++ b/v2/internallog/internal/internal.go @@ -27,6 +27,8 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// Package internal provides some common logic and types to other logging +// sub-packages. package internal import ( diff --git a/v2/internallog/internal/logtest/logtest.go b/v2/internallog/internal/logtest/logtest.go index 3bfbafb0..976534fa 100644 --- a/v2/internallog/internal/logtest/logtest.go +++ b/v2/internallog/internal/logtest/logtest.go @@ -27,7 +27,7 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// Pacakage logtest is a helper for validating logging tests. +// Package logtest is a helper for validating logging tests. // // To update conformance tests in this package run `go test -update_golden` package logtest From fd39d2a1a7f3dadf08b692852896e9fefe9bad63 Mon Sep 17 00:00:00 2001 From: Cody Oss Date: Tue, 12 Nov 2024 16:54:38 -0600 Subject: [PATCH 3/3] fix typo --- v2/internallog/internallog.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/internallog/internallog.go b/v2/internallog/internallog.go index b9b0a8bd..91b648a6 100644 --- a/v2/internallog/internallog.go +++ b/v2/internallog/internallog.go @@ -53,7 +53,7 @@ func New(l *slog.Logger) *slog.Logger { } // HTTPRequest returns a lazily evaluated [slog.LogValuer] for a -// [http.Response] and the associated body. +// [http.Request] and the associated body. func HTTPRequest(req *http.Request, body []byte) slog.LogValuer { return &request{ req: req,