From def8ffd12d32b2e8656152b1eea46017dc8f8eaa Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 17 Aug 2023 15:50:52 -0400 Subject: [PATCH] logging: switch to using log/slog (#138) --- .github/actions/terraform-linter/action.yml | 5 +- .github/workflows/ci.yml | 4 +- .github/workflows/go-lint.yml | 4 +- README.md | 4 +- bqutil/query.go | 6 +- bqutil/query_test.go | 5 +- cli/flags.go | 40 +++ cli/flags_test.go | 64 +++++ containertest/README.md | 4 +- gcputil/gcputil.go | 2 +- go.mod | 55 ++-- go.sum | 127 ++++----- logging/formats.go | 60 +++++ logging/handler.go | 90 +++++++ logging/interceptor.go | 14 +- logging/interceptor_test.go | 51 ++-- logging/levels.go | 131 +++++++++ logging/logger.go | 277 ++++++++++---------- logging/logger_test.go | 174 +++++++++++- logging/logging_doc_test.go | 78 ++++++ logging/testing.go | 55 ++++ logging/testing_test.go | 42 +++ mysqltest/README.md | 4 +- serving/serving.go | 16 +- 24 files changed, 1020 insertions(+), 292 deletions(-) create mode 100644 logging/formats.go create mode 100644 logging/handler.go create mode 100644 logging/levels.go create mode 100644 logging/logging_doc_test.go create mode 100644 logging/testing.go create mode 100644 logging/testing_test.go diff --git a/.github/actions/terraform-linter/action.yml b/.github/actions/terraform-linter/action.yml index 851551a2..cf0bfc7f 100644 --- a/.github/actions/terraform-linter/action.yml +++ b/.github/actions/terraform-linter/action.yml @@ -31,9 +31,10 @@ runs: - id: 'setup-go' uses: 'actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753' # ratchet:actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.21' - id: 'run-linter' shell: 'bash' working-directory: 'abcxyz-pkg' - run: 'go run ./cmd/terraform-linter ${GITHUB_WORKSPACE}/${{inputs.directory}}' + run: |- + go run ./cmd/terraform-linter ${GITHUB_WORKSPACE}/${{inputs.directory}} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85f5d19f..7e8cc6f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,12 +30,12 @@ jobs: go_lint: uses: './.github/workflows/go-lint.yml' with: - go_version: '1.20' + go_version: '1.21' go_test: uses: './.github/workflows/go-test.yml' with: - go_version: '1.20' + go_version: '1.21' terraform_lint: uses: './.github/workflows/terraform-lint.yml' diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml index f8192e2a..83c3befc 100644 --- a/.github/workflows/go-lint.yml +++ b/.github/workflows/go-lint.yml @@ -32,7 +32,7 @@ on: golangci_lint_version: description: 'Version of golangci linter to use' type: 'string' - default: 'v1.53' + default: 'v1.54' jobs: # modules checks if the go modules are all up-to-date. While rare with modern @@ -51,7 +51,6 @@ jobs: uses: 'actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753' # ratchet:actions/setup-go@v4 with: go-version: '${{ inputs.go_version }}' - cache: false - name: 'Check modules' shell: 'bash' @@ -82,6 +81,7 @@ jobs: uses: 'actions/setup-go@fac708d6674e30b6ba41289acaab6d4b75aa0753' # ratchet:actions/setup-go@v4 with: go-version: '${{ inputs.go_version }}' + cache: false - name: 'Lint (download default configuration)' id: 'load-default-config' diff --git a/README.md b/README.md index 7422bfb9..4066e47d 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ jobs: lint: uses: 'abcxyz/pkg/.github/workflows/go-lint.yml@main' with: - go_version: '1.20' + go_version: '1.21' ``` Linting is done via [golangci-lint](https://golangci-lint.run/). If a @@ -68,7 +68,7 @@ jobs: lint: uses: 'abcxyz/pkg/.github/workflows/go-test.yml@main' with: - go_version: '1.20' + go_version: '1.21' ``` Testing is done via the `go test` command with: diff --git a/bqutil/query.go b/bqutil/query.go index b6ed68b4..07fe9bdd 100644 --- a/bqutil/query.go +++ b/bqutil/query.go @@ -110,13 +110,13 @@ func (q *bqQuery[T]) Execute(ctx context.Context) ([]T, error) { // result, err := RetryQueryEntries(ctx, q, 1, backoff) func RetryQueryEntries[T any](ctx context.Context, q Query[T], wantCount int, backoff retry.Backoff) ([]T, error) { logger := logging.FromContext(ctx) - logger.Debugw("Start retrying query", "query", q.String()) + logger.DebugContext(ctx, "start retrying query", "query", q.String()) var result []T if err := retry.Do(ctx, backoff, func(ctx context.Context) error { entries, err := q.Execute(ctx) if err != nil { - logger.Debugw("Query failed; will retry", "error", err) + logger.DebugContext(ctx, "query failed; will retry", "error", err) return retry.RetryableError(err) } @@ -126,7 +126,7 @@ func RetryQueryEntries[T any](ctx context.Context, q Query[T], wantCount int, ba return nil } - logger.Debugw("Not enough entries; will retry", "gotCount", gotCount, "wantCount", wantCount) + logger.DebugContext(ctx, "not enough entries; will retry", "got_count", gotCount, "want_count", wantCount) return retry.RetryableError(fmt.Errorf("not enough entries")) }); err != nil { return nil, fmt.Errorf("retry backoff exhausted: %w", err) diff --git a/bqutil/query_test.go b/bqutil/query_test.go index f0a4d979..5cfdac9d 100644 --- a/bqutil/query_test.go +++ b/bqutil/query_test.go @@ -24,8 +24,6 @@ import ( "github.com/abcxyz/pkg/testutil" "github.com/google/go-cmp/cmp" "github.com/sethvargo/go-retry" - "go.uber.org/zap/zapcore" - "go.uber.org/zap/zaptest" ) type fakeQuery[T any] struct { @@ -94,8 +92,7 @@ func TestRetryQueryEntries(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - ctx := logging.WithLogger(context.Background(), - logging.TestLogger(t, zaptest.Level(zapcore.DebugLevel))) + ctx := logging.WithLogger(context.Background(), logging.TestLogger(t)) wantCount := len(tc.wantEntries) diff --git a/cli/flags.go b/cli/flags.go index 1a893b60..957e3ac7 100644 --- a/cli/flags.go +++ b/cli/flags.go @@ -20,13 +20,16 @@ import ( "flag" "fmt" "io" + "log/slog" "os" "sort" "strconv" "strings" "time" + "github.com/abcxyz/pkg/logging" "github.com/abcxyz/pkg/timeutil" + "github.com/kr/text" "github.com/posener/complete/v2" "github.com/posener/complete/v2/predict" @@ -868,6 +871,43 @@ func (f *FlagSection) Uint64Var(i *Uint64Var) { }) } +type LogLevelVar struct { + Logger *slog.Logger +} + +func (f *FlagSection) LogLevelVar(i *LogLevelVar) { + parser := func(s string) (slog.Level, error) { + v, err := logging.LookupLevel(s) + if err != nil { + return 0, err + } + return v, nil + } + + printer := func(v slog.Level) string { return logging.LevelString(v) } + + setter := func(_ *slog.Level, val slog.Level) { logging.SetLevel(i.Logger, val) } + + // trick the CLI into thinking we need a value to set. + var fake slog.Level + + levelNames := logging.LevelNames() + + Flag(f, &Var[slog.Level]{ + Name: "log-level", + Aliases: []string{"l"}, + Usage: `Sets the logging verbosity. Valid values include: ` + + strings.Join(levelNames, ",") + `.`, + Example: "warn", + Default: slog.LevelInfo, + Predict: predict.Set(levelNames), + Target: &fake, + Parser: parser, + Printer: printer, + Setter: setter, + }) +} + // wrapAtLengthWithPadding wraps the given text at the maxLineLength, taking // into account any provided left padding. func wrapAtLengthWithPadding(s string, pad int) string { diff --git a/cli/flags_test.go b/cli/flags_test.go index 15bb0cd0..277e458a 100644 --- a/cli/flags_test.go +++ b/cli/flags_test.go @@ -15,13 +15,16 @@ package cli import ( + "context" "flag" "fmt" "io" + "log/slog" "reflect" "strings" "testing" + "github.com/abcxyz/pkg/logging" "github.com/abcxyz/pkg/testutil" "github.com/google/go-cmp/cmp" ) @@ -314,3 +317,64 @@ func ExampleFlagSet_AfterParse_checkIfError() { func ptrTo[T any](v T) *T { return &v } + +func TestLogLevelVar(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + cases := []struct { + name string + args []string + + wantLevel slog.Level + wantError string + }{ + { + name: "empty", + args: nil, + wantLevel: slog.LevelInfo, + }, + { + name: "long", + args: []string{"-log-level", "debug"}, + wantLevel: slog.LevelDebug, + }, + { + name: "short", + args: []string{"-l", "debug"}, + wantLevel: slog.LevelDebug, + }, + { + name: "invalid", + args: []string{"-log-level", "pants"}, + wantError: `invalid value "pants" for flag -log-level`, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + logger := logging.DefaultLogger() + + set := NewFlagSet() + f := set.NewSection("FLAGS") + + f.LogLevelVar(&LogLevelVar{ + Logger: logger, + }) + + err := set.Parse(tc.args) + if diff := testutil.DiffErrString(err, tc.wantError); diff != "" { + t.Error(diff) + } + + if !logger.Handler().Enabled(ctx, tc.wantLevel) { + t.Errorf("expected handler to be at least %s", tc.wantLevel) + } + }) + } +} diff --git a/containertest/README.md b/containertest/README.md index cbef2d46..ec283eeb 100644 --- a/containertest/README.md +++ b/containertest/README.md @@ -101,7 +101,7 @@ jobs: steps: - uses: actions/setup-go@v4 with: - go-version: '1.20' # Optional + go-version: '1.21' # Optional ``` ... and *don't* do this: @@ -111,5 +111,5 @@ jobs: test: name: Go Test runs-on: ubuntu-latest - container: golang:1.20 # DON'T DO THIS + container: golang:1.21 # DON'T DO THIS ``` diff --git a/gcputil/gcputil.go b/gcputil/gcputil.go index cb10051f..40265381 100644 --- a/gcputil/gcputil.go +++ b/gcputil/gcputil.go @@ -43,7 +43,7 @@ func ProjectID(ctx context.Context) string { v, err := metadata.ProjectID() if err != nil { - logging.FromContext(ctx).Errorw("failed to get project id", "error", err) + logging.FromContext(ctx).ErrorContext(ctx, "failed to get project id", "error", err) return "" } return v diff --git a/go.mod b/go.mod index e36d638c..7b535bdc 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/abcxyz/pkg -go 1.20 +go 1.21 require ( cloud.google.com/go/bigquery v1.53.0 @@ -8,7 +8,7 @@ require ( github.com/go-sql-driver/mysql v1.7.1 github.com/google/go-cmp v0.5.9 github.com/hashicorp/hcl/v2 v2.17.0 - github.com/jackc/pgx/v5 v5.4.2 + github.com/jackc/pgx/v5 v5.4.3 github.com/kr/text v0.2.0 github.com/lestrrat-go/jwx/v2 v2.0.11 github.com/mattn/go-isatty v0.0.19 @@ -16,29 +16,27 @@ require ( github.com/posener/complete/v2 v2.1.0 github.com/sethvargo/go-envconfig v0.9.0 github.com/sethvargo/go-retry v0.2.4 - go.uber.org/zap v1.24.0 - golang.org/x/oauth2 v0.10.0 + golang.org/x/oauth2 v0.11.0 golang.org/x/sync v0.3.0 - golang.org/x/text v0.11.0 - google.golang.org/api v0.126.0 - google.golang.org/grpc v1.56.2 + golang.org/x/text v0.12.0 + google.golang.org/api v0.135.0 + google.golang.org/grpc v1.57.0 google.golang.org/protobuf v1.31.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - cloud.google.com/go v0.110.4 // indirect - cloud.google.com/go/compute v1.22.0 // indirect - cloud.google.com/go/iam v1.1.0 // indirect + cloud.google.com/go v0.110.7 // indirect + cloud.google.com/go/compute v1.23.0 // indirect + cloud.google.com/go/iam v1.1.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/agext/levenshtein v1.2.3 // indirect - github.com/andybalholm/brotli v1.0.4 // indirect - github.com/apache/arrow/go/v12 v12.0.0 // indirect - github.com/apache/thrift v0.16.0 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/apache/arrow/go/v12 v12.0.1 // indirect + github.com/apache/thrift v0.18.1 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect - github.com/benbjohnson/clock v1.3.5 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/containerd/continuity v0.4.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect @@ -51,18 +49,18 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/flatbuffers v2.0.8+incompatible // indirect + github.com/google/flatbuffers v23.5.26+incompatible // indirect github.com/google/s2a-go v0.1.4 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect - github.com/googleapis/gax-go/v2 v2.11.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect + github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/klauspost/asmfmt v1.3.2 // indirect - github.com/klauspost/compress v1.15.9 // indirect - github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect @@ -76,10 +74,9 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/runc v1.1.8 // indirect - github.com/pierrec/lz4/v4 v4.1.15 // indirect + github.com/pierrec/lz4/v4 v4.1.18 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/posener/script v1.2.0 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect @@ -88,17 +85,15 @@ require ( github.com/zclconf/go-cty v1.13.2 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opencensus.io v0.24.0 // indirect - go.uber.org/atomic v1.11.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.11.0 // indirect + golang.org/x/crypto v0.12.0 // indirect golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.12.0 // indirect - golang.org/x/sys v0.10.0 // indirect - golang.org/x/tools v0.11.0 // indirect + golang.org/x/net v0.14.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/tools v0.12.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230726155614-23370e0ffb3e // indirect + google.golang.org/genproto v0.0.0-20230807174057-1744710a1577 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230807174057-1744710a1577 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index f6f5df17..dbf3abe9 100644 --- a/go.sum +++ b/go.sum @@ -1,39 +1,41 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.110.4 h1:1JYyxKMN9hd5dR2MYTPWkGUgcoxVVhg0LKNKEo0qvmk= -cloud.google.com/go v0.110.4/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= +cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= +cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= cloud.google.com/go/bigquery v1.53.0 h1:K3wLbjbnSlxhuG5q4pntHv5AEbQM1QqHKGYgwFIqOTg= cloud.google.com/go/bigquery v1.53.0/go.mod h1:3b/iXjRQGU4nKa87cXeg6/gogLjO8C6PmuM8i5Bi/u4= -cloud.google.com/go/compute v1.22.0 h1:cB8R6FtUtT1TYGl5R3xuxnW6OUIc/DrT2aiR16TTG7Y= -cloud.google.com/go/compute v1.22.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= +cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/datacatalog v1.14.1 h1:cFPBt8V5V2T3mu/96tc4nhcMB+5cYcpwjBfn79bZDI8= -cloud.google.com/go/iam v1.1.0 h1:67gSqaPukx7O8WLLHMa0PNs3EBGd2eE4d+psbO/CO94= -cloud.google.com/go/iam v1.1.0/go.mod h1:nxdHjaKfCr7fNYx/HJMM8LgiMugmveWlkatear5gVyk= +cloud.google.com/go/datacatalog v1.16.0 h1:qVeQcw1Cz93/cGu2E7TYUPh8Lz5dn5Ws2siIuQ17Vng= +cloud.google.com/go/datacatalog v1.16.0/go.mod h1:d2CevwTG4yedZilwe+v3E3ZBDRMobQfSG/a6cCCN5R4= +cloud.google.com/go/iam v1.1.2 h1:gacbrBdWcoVmGLozRuStX45YKvJtzIjJdAolzUs1sm4= +cloud.google.com/go/iam v1.1.2/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI= +cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc= cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apache/arrow/go/v12 v12.0.0 h1:xtZE63VWl7qLdB0JObIXvvhGjoVNrQ9ciIHG2OK5cmc= -github.com/apache/arrow/go/v12 v12.0.0/go.mod h1:d+tV/eHZZ7Dz7RPrFKtPK02tpr+c9/PEd/zm8mDS9Vg= -github.com/apache/thrift v0.16.0 h1:qEy6UW60iVOlUy+b9ZR0d5WzUWYGOo4HfopoyBaNmoY= -github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= +github.com/apache/arrow/go/v12 v12.0.1 h1:JsR2+hzYYjgSUkBSaahpqCetqZMr76djX80fF/DiJbg= +github.com/apache/arrow/go/v12 v12.0.1/go.mod h1:weuTY7JvTG/HDPtMQxEUp7pU73vkLWMLpY67QwZ/WWw= +github.com/apache/thrift v0.18.1 h1:lNhK/1nqjbwbiOPDBPFJVKxgDEGSepKuTh6OLiXW8kg= +github.com/apache/thrift v0.18.1/go.mod h1:rdQn/dCcDKEWjjylUeueum4vQEjG2v8v2PqriUnbr+I= github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= -github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= -github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -49,6 +51,7 @@ github.com/containerd/continuity v0.4.1 h1:wQnVrjIyQ8vhU2sgOiL5T07jo+ouqc2bnKsv5 github.com/containerd/continuity v0.4.1/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -73,6 +76,7 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -82,7 +86,6 @@ github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -101,8 +104,8 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM= -github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= +github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -113,6 +116,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= @@ -120,10 +124,10 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= -github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= +github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM= +github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= @@ -133,20 +137,22 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.4.2 h1:u1gmGDwbdRUZiwisBm/Ky2M14uQyUP65bG8+20nnyrg= -github.com/jackc/pgx/v5 v5.4.2/go.mod h1:q6iHT8uDNXWiFNOlRqJzBTaSH3+2xCXkokxHZC5qWFY= +github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= -github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= @@ -161,6 +167,7 @@ github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmt github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2 h1:hRGSmZu7j271trc9sneMrpOW7GN5ngLm8YUZIPzf394= +github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= @@ -181,8 +188,8 @@ github.com/opencontainers/runc v1.1.8 h1:zICRlc+C1XzivLc3nzE+cbJV4LIi8tib6YG0MqC github.com/opencontainers/runc v1.1.8/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50= github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= -github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= -github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= +github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -193,8 +200,8 @@ github.com/posener/script v1.2.0 h1:DrZz0qFT8lCLkYNi1PleLDANFnKxJ2VmlNPJbAkVLsE= github.com/posener/script v1.2.0/go.mod h1:s4sVvRXtdc/1aK6otTSeW2BVXndO8MsoOVUwK74zcg4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sethvargo/go-envconfig v0.9.0 h1:Q6FQ6hVEeTECULvkJZakq3dZMeBQ3JUpcKMfPQbKMDE= @@ -228,28 +235,23 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw= +golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 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= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -276,12 +278,12 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= -golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -308,8 +310,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -322,8 +324,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -334,8 +336,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= -golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -343,8 +345,9 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E= -google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o= -google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= +google.golang.org/api v0.135.0 h1:6Vgfj6uPMXcyy66waYWBwmkeNB+9GmUlJDOzkukPQYQ= +google.golang.org/api v0.135.0/go.mod h1:Bp77uRFgwsSKI0BWH573F5Q6wSlznwI2NFayLOp/7mQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= @@ -353,12 +356,12 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 h1:Au6te5hbKUV8pIYWHqOUZ1pva5qK/rwbIhoXEUB9Lu8= -google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:O9kGHb51iE/nOGvQaDUuadVYqovW56s5emA88lQnj6Y= -google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529 h1:s5YSX+ZH5b5vS9rnpGymvIyMpLRJizowqDlOuyjXnTk= -google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230726155614-23370e0ffb3e h1:S83+ibolgyZ0bqz7KEsUOPErxcv4VzlszxY+31OfB/E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/genproto v0.0.0-20230807174057-1744710a1577 h1:Tyk/35yqszRCvaragTn5NnkY6IiKk/XvHzEWepo71N0= +google.golang.org/genproto v0.0.0-20230807174057-1744710a1577/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= +google.golang.org/genproto/googleapis/api v0.0.0-20230807174057-1744710a1577 h1:xv8KoglAClYGkprUSmDTKaILtzfD8XzG9NYVXMprjKo= +google.golang.org/genproto/googleapis/api v0.0.0-20230807174057-1744710a1577/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577 h1:wukfNtZmZUurLN/atp2hiIeTKn7QJWIQdHzqmsOnAOk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= 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= @@ -367,8 +370,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI= -google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= 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= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -384,6 +387,7 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -392,5 +396,6 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= +gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/logging/formats.go b/logging/formats.go new file mode 100644 index 00000000..e9e60585 --- /dev/null +++ b/logging/formats.go @@ -0,0 +1,60 @@ +// Copyright 2023 The Authors (see AUTHORS file) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logging + +import ( + "fmt" + "slices" + "strings" +) + +// Format represents the logging format. +type Format string + +const ( + FormatJSON = Format("JSON") + FormatText = Format("TEXT") +) + +const ( + formatJSONName = string(FormatJSON) + formatTextName = string(FormatText) +) + +var formatNames = []string{ + formatJSONName, + formatTextName, +} + +// FormatNames returns the list of all log format names. +func FormatNames() []string { + return slices.Clone(formatNames) +} + +// LookupFormat attempts to get the formatter that corresponds to the given +// name. If no such formatter exists, it returns an error. If the empty string +// is given, it returns the JSON formatter. +func LookupFormat(name string) (Format, error) { + switch v := strings.ToUpper(strings.TrimSpace(name)); v { + case "": + return FormatJSON, nil + case formatJSONName: + return FormatJSON, nil + case formatTextName: + return FormatText, nil + default: + return "", fmt.Errorf("no such format %q, valid formats are %q", name, formatNames) + } +} diff --git a/logging/handler.go b/logging/handler.go new file mode 100644 index 00000000..9d115bd6 --- /dev/null +++ b/logging/handler.go @@ -0,0 +1,90 @@ +// Copyright 2023 The Authors (see AUTHORS file) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logging + +import ( + "context" + "log/slog" +) + +// LevelableHandler is an interface which defines a handler that is able to +// dynamically set its level. +type LevelableHandler interface { + // SetLevel dynamically sets the level on the handler. + SetLevel(level slog.Level) +} + +// Ensure we are a slog handler. +var _ slog.Handler = (*LevelHandler)(nil) + +// Ensure we are a levelable handler. +var _ LevelableHandler = (*LevelHandler)(nil) + +// LevelHandler is a wrapper around a LevelHandler that gives us the ability to configure +// level at runtime without users needing to manage a separate LevelVar. +type LevelHandler struct { + handler slog.Handler + levelVar *slog.LevelVar +} + +// NewLevelHandler creates a new handler that is capable of dynamically setting +// a level in a concurrency-safe way. +func NewLevelHandler(leveler slog.Leveler, h slog.Handler) *LevelHandler { + if lh, ok := h.(*LevelHandler); ok { + h = lh.Handler() + } + + // Ensure what we got is a LeverVar. We also don't want someone giving us a + // LevelVar and managing it outside of our lifecycle. + levelVar := new(slog.LevelVar) + levelVar.Set(leveler.Level()) + + return &LevelHandler{ + handler: h, + levelVar: levelVar, + } +} + +// SetLevel implements the levelable interface. It adjusts the level of the +// logger. It is safe for concurrent use. +func (h *LevelHandler) SetLevel(level slog.Level) { + h.levelVar.Set(level) +} + +// Enabled implements Handler.Enabled by reporting whether level is at least as +// large as h's level. +func (h *LevelHandler) Enabled(_ context.Context, level slog.Level) bool { + return level >= h.levelVar.Level() +} + +// Handle implements Handler.Handle. +func (h *LevelHandler) Handle(ctx context.Context, r slog.Record) error { + return h.handler.Handle(ctx, r) //nolint:wrapcheck // Want passthrough +} + +// WithAttrs implements Handler.WithAttrs. +func (h *LevelHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return NewLevelHandler(h.levelVar, h.handler.WithAttrs(attrs)) +} + +// WithGroup implements Handler.WithGroup. +func (h *LevelHandler) WithGroup(name string) slog.Handler { + return NewLevelHandler(h.levelVar, h.handler.WithGroup(name)) +} + +// Handler returns the Handler wrapped by h. +func (h *LevelHandler) Handler() slog.Handler { + return h.handler +} diff --git a/logging/interceptor.go b/logging/interceptor.go index 063a5417..3e46bacd 100644 --- a/logging/interceptor.go +++ b/logging/interceptor.go @@ -17,10 +17,10 @@ package logging import ( "context" "fmt" + "log/slog" "net/http" "strings" - "go.uber.org/zap" "google.golang.org/grpc" grpcmetadata "google.golang.org/grpc/metadata" ) @@ -36,13 +36,13 @@ var ( // GRPCStreamingInterceptor returns client-side a gRPC streaming interceptor // that populates a logger with trace data in the context. -func GRPCStreamingInterceptor(inLogger *zap.SugaredLogger, projectID string) grpc.StreamClientInterceptor { +func GRPCStreamingInterceptor(inLogger *slog.Logger, projectID string) grpc.StreamClientInterceptor { return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { // Only override the logger if it's the default logger. This is only used // for testing and is intentionally a strict object equality check because // the default logger is a global default in the logger package. logger := inLogger - if existing := FromContext(ctx); existing != Default() { + if existing := FromContext(ctx); existing != DefaultLogger() { logger = existing } ctx = WithLogger(ctx, logger) @@ -65,13 +65,13 @@ func GRPCStreamingInterceptor(inLogger *zap.SugaredLogger, projectID string) grp // GRPCUnaryInterceptor returns a client-side gRPC unary interceptor that // populates a logger with trace data in the context. -func GRPCUnaryInterceptor(inLogger *zap.SugaredLogger, projectID string) grpc.UnaryServerInterceptor { +func GRPCUnaryInterceptor(inLogger *slog.Logger, projectID string) grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { // Only override the logger if it's the default logger. This is only used // for testing and is intentionally a strict object equality check because // the default logger is a global default in the logger package. logger := inLogger - if existing := FromContext(ctx); existing != Default() { + if existing := FromContext(ctx); existing != DefaultLogger() { logger = existing } ctx = WithLogger(ctx, logger) @@ -90,7 +90,7 @@ func GRPCUnaryInterceptor(inLogger *zap.SugaredLogger, projectID string) grpc.Un // HTTPInterceptor returns an HTTP middleware that populates a logger with trace // data onto the incoming and outgoing [http.Request] context. -func HTTPInterceptor(inLogger *zap.SugaredLogger, projectID string) func(http.Handler) http.Handler { +func HTTPInterceptor(inLogger *slog.Logger, projectID string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -99,7 +99,7 @@ func HTTPInterceptor(inLogger *zap.SugaredLogger, projectID string) func(http.Ha // for testing and is intentionally a strict object equality check because // the default logger is a global default in the logger package. logger := inLogger - if existing := FromContext(ctx); existing != Default() { + if existing := FromContext(ctx); existing != DefaultLogger() { logger = existing } ctx = WithLogger(ctx, logger) diff --git a/logging/interceptor_test.go b/logging/interceptor_test.go index bd791f45..b4b49779 100644 --- a/logging/interceptor_test.go +++ b/logging/interceptor_test.go @@ -17,13 +17,12 @@ package logging import ( "bytes" "context" + "log/slog" "net/http" "net/http/httptest" "strings" "testing" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) @@ -39,28 +38,28 @@ func TestGRPCStreamingInterceptor(t *testing.T) { { name: "no_headers", headers: nil, - exp: "INFO\ttest", + exp: "level=INFO msg=test", }, { name: "headers_with_no_trace", headers: map[string]string{ "X-Foo": "bar", }, - exp: "INFO\ttest", + exp: "level=INFO msg=test", }, { name: "headers_with_invalid_trace", headers: map[string]string{ "X-Cloud-Trace-Context": "", }, - exp: "INFO\ttest", + exp: "level=INFO msg=test", }, { name: "with_trace_header", headers: map[string]string{ "X-Cloud-Trace-Context": "105445aa7843bc8bf206b12000100000/1;o=1", }, - exp: "INFO\ttest\t{\"logging.googleapis.com/trace\": \"projects/my-project/traces/105445aa7843bc8bf206b12000100000\"}", + exp: "level=INFO msg=test logging.googleapis.com/trace=projects/my-project/traces/105445aa7843bc8bf206b12000100000", }, } @@ -84,7 +83,7 @@ func TestGRPCStreamingInterceptor(t *testing.T) { streamer := func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) { logger := FromContext(ctx) - logger.Info("test") + logger.InfoContext(ctx, "test") return nil, nil } @@ -120,28 +119,28 @@ func TestGRPCUnaryInterceptor(t *testing.T) { { name: "no_headers", headers: nil, - exp: "INFO\ttest", + exp: "level=INFO msg=test", }, { name: "headers_with_no_trace", headers: map[string]string{ "X-Foo": "bar", }, - exp: "INFO\ttest", + exp: "level=INFO msg=test", }, { name: "headers_with_invalid_trace", headers: map[string]string{ "X-Cloud-Trace-Context": "", }, - exp: "INFO\ttest", + exp: "level=INFO msg=test", }, { name: "with_trace_header", headers: map[string]string{ "X-Cloud-Trace-Context": "105445aa7843bc8bf206b12000100000/1;o=1", }, - exp: "INFO\ttest\t{\"logging.googleapis.com/trace\": \"projects/my-project/traces/105445aa7843bc8bf206b12000100000\"}", + exp: "level=INFO msg=test logging.googleapis.com/trace=projects/my-project/traces/105445aa7843bc8bf206b12000100000", }, } @@ -159,7 +158,7 @@ func TestGRPCUnaryInterceptor(t *testing.T) { } unaryHandler := func(ctx context.Context, req any) (any, error) { logger := FromContext(ctx) - logger.Info("test") + logger.InfoContext(ctx, "test") return nil, nil } @@ -194,28 +193,28 @@ func TestHTTPInterceptor(t *testing.T) { { name: "no_headers", headers: nil, - exp: "INFO\ttest", + exp: "level=INFO msg=test", }, { name: "headers_with_no_trace", headers: map[string]string{ "X-Foo": "bar", }, - exp: "INFO\ttest", + exp: "level=INFO msg=test", }, { name: "headers_with_invalid_trace", headers: map[string]string{ "X-Cloud-Trace-Context": "", }, - exp: "INFO\ttest", + exp: "level=INFO msg=test", }, { name: "with_trace_header", headers: map[string]string{ "X-Cloud-Trace-Context": "105445aa7843bc8bf206b12000100000/1;o=1", }, - exp: "INFO\ttest\t{\"logging.googleapis.com/trace\": \"projects/my-project/traces/105445aa7843bc8bf206b12000100000\"}", + exp: "level=INFO msg=test logging.googleapis.com/trace=projects/my-project/traces/105445aa7843bc8bf206b12000100000", }, } @@ -239,7 +238,7 @@ func TestHTTPInterceptor(t *testing.T) { interceptor := HTTPInterceptor(originalLogger, "my-project") interceptor(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logger := FromContext(r.Context()) - logger.Info("test") + logger.InfoContext(ctx, "test") })).ServeHTTP(w, r) if got, want := strings.TrimSpace(buf.String()), tc.exp; got != want { @@ -255,7 +254,7 @@ func TestHTTPInterceptor(t *testing.T) { // testLogger creates a logger suitable for testing that writes log messages to // a buffer. It returns the logger and a pointer to the buffer. -func testLogger(tb testing.TB) (*zap.SugaredLogger, *bytes.Buffer) { +func testLogger(tb testing.TB) (*slog.Logger, *bytes.Buffer) { tb.Helper() var buf bytes.Buffer @@ -263,13 +262,15 @@ func testLogger(tb testing.TB) (*zap.SugaredLogger, *bytes.Buffer) { buf.Reset() }) - cfg := zap.NewDevelopmentEncoderConfig() - cfg.TimeKey = "" - - logger := zap.New(zapcore.NewCore( - zapcore.NewConsoleEncoder(cfg), - zap.CombineWriteSyncers(zapcore.AddSync(&buf)), - zapcore.InfoLevel)).Sugar() + logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{ + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + // Drop time key for deterministic tests + if a.Key == slog.TimeKey { + return slog.Attr{} + } + return a + }, + })) return logger, &buf } diff --git a/logging/levels.go b/logging/levels.go new file mode 100644 index 00000000..70c9c0da --- /dev/null +++ b/logging/levels.go @@ -0,0 +1,131 @@ +// Copyright 2023 The Authors (see AUTHORS file) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logging + +import ( + "fmt" + "log/slog" + "slices" + "strings" +) + +const ( + LevelDebug = slog.Level(-4) + LevelInfo = slog.Level(0) + LevelNotice = slog.Level(2) + LevelWarning = slog.Level(4) + LevelError = slog.Level(8) + LevelEmergency = slog.Level(12) +) + +const ( + levelUnknownName = "UNKNOWN" + levelDebugName = "DEBUG" + levelInfoName = "INFO" + levelNoticeName = "NOTICE" + levelWarningName = "WARNING" + levelErrorName = "ERROR" + levelEmergencyName = "EMERGENCY" +) + +var ( + levelUnknownSlogValue = slog.StringValue(levelUnknownName) + levelDebugSlogValue = slog.StringValue(levelDebugName) + levelInfoSlogValue = slog.StringValue(levelInfoName) + levelNoticeSlogValue = slog.StringValue(levelNoticeName) + levelWarningSlogValue = slog.StringValue(levelWarningName) + levelErrorSlogValue = slog.StringValue(levelErrorName) + levelEmergencySlogValue = slog.StringValue(levelEmergencyName) +) + +var levelNames = []string{ + levelDebugName, + levelInfoName, + levelNoticeName, + levelWarningName, + levelErrorName, + levelEmergencyName, +} + +// LevelNames returns the list of all log level names. +func LevelNames() []string { + return slices.Clone(levelNames) +} + +// LookupLevel attempts to get the level that corresponds to the given name. If +// no such level exists, it returns an error. If the empty string is given, it +// returns Info level. +func LookupLevel(name string) (slog.Level, error) { + switch v := strings.ToUpper(strings.TrimSpace(name)); v { + case "": + return LevelInfo, nil + case levelDebugName: + return LevelDebug, nil + case levelInfoName: + return LevelInfo, nil + case levelNoticeName: + return LevelNotice, nil + case levelWarningName: + return LevelWarning, nil + case levelErrorName: + return LevelError, nil + case levelEmergencyName: + return LevelEmergency, nil + default: + return 0, fmt.Errorf("no such level %q, valid levels are %q", name, levelNames) + } +} + +// LevelSlogValue returns the [slog.Value] representation of the level. +func LevelSlogValue(l slog.Level) slog.Value { + switch l { + case LevelDebug: + return levelDebugSlogValue + case LevelInfo: + return levelInfoSlogValue + case LevelNotice: + return levelNoticeSlogValue + case LevelWarning: + return levelWarningSlogValue + case LevelError: + return levelErrorSlogValue + case LevelEmergency: + return levelEmergencySlogValue + default: + return levelUnknownSlogValue + } +} + +// LevelString returns the string representation of the given level. Note this +// is different from calling String() on the Level, which uses the slog +// implementation. +func LevelString(l slog.Level) string { + switch l { + case LevelDebug: + return levelDebugName + case LevelInfo: + return levelInfoName + case LevelNotice: + return levelNoticeName + case LevelWarning: + return levelWarningName + case LevelError: + return levelErrorName + case LevelEmergency: + return levelEmergencyName + default: + return levelUnknownName + } +} diff --git a/logging/logger.go b/logging/logger.go index 60a4e2d7..7c17af8b 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Authors (see AUTHORS file) +// Copyright 2023 The Authors (see AUTHORS file) // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,179 +12,188 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package logging sets up and configures standard logging. +// Package logging is an opinionated structured logging library based on +// [log/slog]. +// +// This package also aliases most top-level functions in [log/slog] to reduce +// the need to manage the additional import. package logging import ( "context" + "fmt" + "io" + "log/slog" + "math" "os" + "strconv" "strings" "sync" - "time" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "go.uber.org/zap/zaptest" + "github.com/abcxyz/pkg/timeutil" ) // contextKey is a private string type to prevent collisions in the context map. type contextKey string -const ( - // loggerKey points to the value in the context where the logger is stored. - loggerKey = contextKey("logger") -) +// loggerKey points to the value in the context where the logger is stored. +const loggerKey = contextKey("logger") -var ( - // defaultLogger is the default logger. It is initialized once per package - // include upon calling DefaultLogger. - defaultLogger *zap.SugaredLogger - defaultLoggerOnce sync.Once -) +// defaultLogger returns a function that returns the default logger. which +// writes JSON output to stdout at the "Info" level. +// +// It is initialized once when called the first time. +var defaultLoggerOnce = sync.OnceValue[*slog.Logger](func() *slog.Logger { + return New(os.Stdout, LevelInfo, FormatJSON, false) +}) + +// New creates a new logger in the specified format and writes to the provided +// writer at the provided level. Use the returned leveler to dynamically change +// the level to a different value after creation. +// +// If debug is true, the logging level is set to the lowest possible value +// (meaning all messages will be printed), and the output will include source +// information. This is very expensive, and you should not enable it unless +// actively debugging. +// +// It returns the configured logger and a leveler which can be used to change +// the logger's level dynamically. The leveler does not require locking to +// change the level. +func New(w io.Writer, level slog.Level, format Format, debug bool) *slog.Logger { + opts := &slog.HandlerOptions{ + ReplaceAttr: cloudLoggingAttrsEncoder(), + } + + // Enable the most detailed log level and add source information in debug + // mode. + if debug { + opts.AddSource = true + level = math.MinInt + } -// NewFromEnv creates a new logger from env vars. -// Set envPrefix+"LOG_LEVEL" to overwrite log level. Default log level is warning. -// Set envPrefix+"LOG_MODE" to overwrite log mode. Default log mode is production. -func NewFromEnv(envPrefix string) *zap.SugaredLogger { - level := os.Getenv(envPrefix + "LOG_LEVEL") - logMode := strings.ToLower(strings.TrimSpace(os.Getenv(envPrefix + "LOG_MODE"))) - devMode := strings.HasPrefix(logMode, "dev") - var cfg zap.Config - if devMode { - cfg = zap.NewDevelopmentConfig() - cfg.EncoderConfig = developmentEncoderConfig - } else { - cfg = zap.NewProductionConfig() - cfg.EncoderConfig = productionEncoderConfig + switch format { + case FormatJSON: + return slog.New(NewLevelHandler(level, slog.NewJSONHandler(w, opts))) + case FormatText: + return slog.New(NewLevelHandler(level, slog.NewTextHandler(w, opts))) + default: + panic(fmt.Sprintf("unknown log format %q", format)) } +} - var l zapcore.Level - if err := l.Set(level); err != nil { - // Invalid level? Default to warn. - l = zapcore.WarnLevel +// NewFromEnv is a convenience function for creating a logger that is configured +// from the environment. It sources the following environment variables, +// optionally prefixed with the given prefix: +// +// - LOG_LEVEL: string representation of the log level. It panics if no such log level exists. +// - LOG_FORMAT: format in which to output logs (e.g. json, text). It panics if no such format exists. +// - LOG_DEBUG: enable the most detailed debug logging. It panics iff the given value is not a valid boolean. +func NewFromEnv(envPrefix string) *slog.Logger { + return newFromEnv(envPrefix, os.Getenv) +} + +// newFromEnv is a helper that makes it easier to test [NewFromEnv]. +func newFromEnv(envPrefix string, getenv func(string) string) *slog.Logger { + levelEnvVarKey := envPrefix + "LOG_LEVEL" + levelEnvVarValue := strings.TrimSpace(getenv(levelEnvVarKey)) + level, err := LookupLevel(levelEnvVarValue) + if err != nil { + panic(fmt.Sprintf("invalid value for %s: %s", levelEnvVarKey, err)) } - cfg.Level = zap.NewAtomicLevelAt(l) - logger, err := cfg.Build() + formatEnvVarKey := envPrefix + "LOG_FORMAT" + formatEnvVarValue := strings.TrimSpace(getenv(formatEnvVarKey)) + format, err := LookupFormat(formatEnvVarValue) if err != nil { - logger = zap.NewNop() + panic(fmt.Sprintf("invalid value for %s: %s", formatEnvVarKey, err)) + } + + debugEnvVarKey := envPrefix + "LOG_DEBUG" + debugEnvVarValue := strings.TrimSpace(getenv(debugEnvVarKey)) + debug, err := strconv.ParseBool(debugEnvVarValue) + if err != nil { + if debugEnvVarValue != "" { + panic(fmt.Sprintf("invalid value for %s: %s", debugEnvVarKey, err)) + } } - return logger.Sugar() + return New(os.Stdout, level, format, debug) } -// Default creates a default logger. To overwrite log level and mode, set -// LOG_LEVEL and LOG_MODE. -func Default() *zap.SugaredLogger { - defaultLoggerOnce.Do(func() { - defaultLogger = NewFromEnv("") - }) - return defaultLogger +// SetLevel adjusts the level on the provided logger. The handler on the given +// logger must be a [LevelableHandler] or else this function panics. If you +// created a logger through this package, it will automatically satisfy that +// interface. +// +// This function is safe for concurrent use. +// +// It returns the provided logger for convenience and easier chaining. +func SetLevel(logger *slog.Logger, level slog.Level) *slog.Logger { + if typ, ok := logger.Handler().(LevelableHandler); ok { + typ.SetLevel(level) + return logger + } + + panic("handler is not capable of setting levels") +} + +// DefaultLogger creates a default logger. +func DefaultLogger() *slog.Logger { + return defaultLoggerOnce() } // WithLogger creates a new context with the provided logger attached. -func WithLogger(ctx context.Context, logger *zap.SugaredLogger) context.Context { +func WithLogger(ctx context.Context, logger *slog.Logger) context.Context { return context.WithValue(ctx, loggerKey, logger) } // FromContext returns the logger stored in the context. If no such logger // exists, a default logger is returned. -func FromContext(ctx context.Context) *zap.SugaredLogger { - if logger, ok := ctx.Value(loggerKey).(*zap.SugaredLogger); ok { +func FromContext(ctx context.Context) *slog.Logger { + if logger, ok := ctx.Value(loggerKey).(*slog.Logger); ok { return logger } - return Default() + return DefaultLogger() } -// TestLogger returns a logger configured for tests. It will only output log -// information if specific test fails or is run in verbose mode. See [zaptest] -// for more information. +// cloudLoggingAttrsEncoder updates the [slog.Record] attributes to match the +// key names and [format for Google Cloud Logging]. // -// func TestMyThing(t *testing.T) { -// logger := logging.TestLogger(t) -// thing := &MyThing{logger: logger} -// } -// -// [zaptest]: https://pkg.go.dev/go.uber.org/zap/zaptest -func TestLogger(tb zaptest.TestingT, opts ...zaptest.LoggerOption) *zap.SugaredLogger { - warnLevelOpt := zaptest.Level(zap.WarnLevel) - opts = append([]zaptest.LoggerOption{warnLevelOpt}, opts...) - return zaptest.NewLogger(tb, opts...).Sugar() -} - -const ( - timestamp = "timestamp" - severity = "severity" - logger = "logger" - caller = "caller" - message = "message" - stacktrace = "stacktrace" - - levelDebug = "DEBUG" - levelInfo = "INFO" - levelWarning = "WARNING" - levelError = "ERROR" - levelCritical = "CRITICAL" - levelAlert = "ALERT" - levelEmergency = "EMERGENCY" -) - -var productionEncoderConfig = zapcore.EncoderConfig{ - TimeKey: timestamp, - LevelKey: severity, - NameKey: logger, - CallerKey: caller, - MessageKey: message, - StacktraceKey: stacktrace, - LineEnding: zapcore.DefaultLineEnding, - EncodeLevel: levelEncoder(), - EncodeTime: timeEncoder(), - EncodeDuration: zapcore.SecondsDurationEncoder, - EncodeCaller: zapcore.ShortCallerEncoder, -} +// [format for Google Cloud Logging]: https://cloud.google.com/logging/docs/structured-logging#special-payload-fields +func cloudLoggingAttrsEncoder() func([]string, slog.Attr) slog.Attr { + const ( + keySeverity = "severity" + keyError = "error" + keyMessage = "message" + ) + + return func(groups []string, a slog.Attr) slog.Attr { + // Google Cloud Logging uses "severity" instead of "level": + // https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity + if a.Key == slog.LevelKey { + a.Key = keySeverity + + // Use the custom level names to match Google Cloud logging. + val := a.Value.Any() + typ, ok := val.(slog.Level) + if !ok { + panic(fmt.Sprintf("level is not slog.Level (got %T)", val)) + } + a.Value = LevelSlogValue(typ) + } -var developmentEncoderConfig = zapcore.EncoderConfig{ - TimeKey: "", - LevelKey: "L", - NameKey: "N", - CallerKey: "C", - FunctionKey: zapcore.OmitKey, - MessageKey: "M", - StacktraceKey: "S", - LineEnding: zapcore.DefaultLineEnding, - EncodeLevel: zapcore.CapitalLevelEncoder, - EncodeTime: zapcore.ISO8601TimeEncoder, - EncodeDuration: zapcore.StringDurationEncoder, - EncodeCaller: zapcore.ShortCallerEncoder, -} + // Google Cloud Logging uses "message" instead of "msg": + // https://cloud.google.com/logging/docs/structured-logging#special-payload-fields + if a.Key == slog.MessageKey { + a.Key = keyMessage + } -// levelEncoder transforms a zap level to the associated stackdriver level. -func levelEncoder() zapcore.LevelEncoder { - return func(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { - switch l { - case zapcore.InvalidLevel: - enc.AppendString(levelAlert) - case zapcore.DebugLevel: - enc.AppendString(levelDebug) - case zapcore.InfoLevel: - enc.AppendString(levelInfo) - case zapcore.WarnLevel: - enc.AppendString(levelWarning) - case zapcore.ErrorLevel: - enc.AppendString(levelError) - case zapcore.DPanicLevel: - enc.AppendString(levelCritical) - case zapcore.PanicLevel: - enc.AppendString(levelAlert) - case zapcore.FatalLevel: - enc.AppendString(levelEmergency) + // Re-format durations to be their string format. + if a.Value.Kind() == slog.KindDuration { + val := a.Value.Duration() + a.Value = slog.StringValue(timeutil.HumanDuration(val)) } - } -} -// timeEncoder encodes the time as RFC3339 nano. -func timeEncoder() zapcore.TimeEncoder { - return func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { - enc.AppendString(t.Format(time.RFC3339Nano)) + return a } } diff --git a/logging/logger_test.go b/logging/logger_test.go index 2d5573ca..c13f1009 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Authors (see AUTHORS file) +// Copyright 2023 The Authors (see AUTHORS file) // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,19 +15,179 @@ package logging import ( + "bytes" "context" + "fmt" + "io" + "log/slog" + "strings" "testing" - - "go.uber.org/zap" ) +func TestNew(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + t.Run("default", func(t *testing.T) { + t.Parallel() + + var b bytes.Buffer + logger := New(&b, LevelInfo, FormatText, false) + + logger.Log(ctx, -19022, "very low level") + logger.Log(ctx, LevelWarning, "engine failure") + + if got, want := b.String(), "very low level"; strings.Contains(got, want) { + t.Errorf("expected %q to not contain %q", got, want) + } + if got, want := b.String(), "source="; strings.Contains(got, want) { + t.Errorf("expected %q to not contain %q", got, want) + } + if got, want := b.String(), "engine failure"; !strings.Contains(got, want) { + t.Errorf("expected %q to contain %q", got, want) + } + }) + + t.Run("json", func(t *testing.T) { + t.Parallel() + + var b bytes.Buffer + logger := New(&b, LevelInfo, FormatJSON, false) + logger.Log(ctx, LevelWarning, "engine failure") + + if got, want := b.String(), `"message":"engine failure"`; !strings.Contains(got, want) { + t.Errorf("expected %q to contain %q", got, want) + } + }) + + t.Run("debug", func(t *testing.T) { + t.Parallel() + + var b bytes.Buffer + logger := New(&b, 0, FormatText, true) + + logger.Log(ctx, -19022, "very low level") + + if got, want := b.String(), "very low level"; !strings.Contains(got, want) { + t.Errorf("expected %q to contain %q", got, want) + } + if got, want := b.String(), "source="; !strings.Contains(got, want) { + t.Errorf("expected %q to contain %q", got, want) + } + }) +} + +func TestNewFromEnv(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + cases := []struct { + name string + envPrefix string + env map[string]string + + wantLevel slog.Level + wantPanic string + }{ + { + name: "empty", + wantLevel: LevelInfo, + }, + + // levels + { + name: "custom_level", + env: map[string]string{ + "LOG_LEVEL": "debug", + }, + wantLevel: LevelDebug, + }, + { + name: "invalid_level", + env: map[string]string{ + "LOG_LEVEL": "pants", + }, + wantPanic: "invalid value for LOG_LEVEL: no such level", + }, + + // formats + { + name: "custom_format", + env: map[string]string{ + "LOG_FORMAT": "json", + }, + }, + { + name: "invalid_format", + env: map[string]string{ + "LOG_FORMAT": "pants", + }, + wantPanic: "invalid value for LOG_FORMAT: no such format", + }, + + // debug + { + name: "custom_debug", + env: map[string]string{ + "LOG_DEBUG": "1", + }, + }, + { + name: "invalid_format", + env: map[string]string{ + "LOG_DEBUG": "pants", + }, + wantPanic: "invalid value for LOG_DEBUG: strconv.ParseBool", + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + defer func() { + if tc.wantPanic != "" { + if r := recover(); r != nil { + if got, want := fmt.Sprintf("%v", r), tc.wantPanic; !strings.Contains(got, want) { + t.Errorf("expected %q to contain %q", got, want) + } + } + } + }() + + logger := newFromEnv(tc.envPrefix, func(k string) string { + return tc.env[k] + }) + + if !logger.Handler().Enabled(ctx, tc.wantLevel) { + t.Errorf("expected handler to be at least %s", tc.wantLevel) + } + }) + } +} + +func TestDefaultLogger(t *testing.T) { + t.Parallel() + + logger1 := DefaultLogger() + logger2 := DefaultLogger() + + if logger1 != logger2 { + t.Errorf("expected default logger to be a singleton (got %v and %v)", logger1, logger2) + } +} + func TestContext(t *testing.T) { t.Parallel() - logger1 := zap.NewNop().Sugar() - logger2 := zap.NewExample().Sugar() + logger1 := slog.New(slog.NewTextHandler(io.Discard, nil)) + logger2 := slog.New(slog.NewTextHandler(io.Discard, nil)) - checkFromContext(context.Background(), t, Default()) + checkFromContext(context.Background(), t, DefaultLogger()) ctx := WithLogger(context.Background(), logger1) checkFromContext(ctx, t, logger1) @@ -36,7 +196,7 @@ func TestContext(t *testing.T) { checkFromContext(ctx, t, logger2) } -func checkFromContext(ctx context.Context, tb testing.TB, want *zap.SugaredLogger) { +func checkFromContext(ctx context.Context, tb testing.TB, want *slog.Logger) { tb.Helper() if got := FromContext(ctx); want != got { diff --git a/logging/logging_doc_test.go b/logging/logging_doc_test.go new file mode 100644 index 00000000..470038cc --- /dev/null +++ b/logging/logging_doc_test.go @@ -0,0 +1,78 @@ +// Copyright 2023 The Authors (see AUTHORS file) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logging_test + +import ( + "bytes" + "context" + "log/slog" + "os" + + "github.com/abcxyz/pkg/logging" +) + +var logger *slog.Logger + +var myLogger = logging.DefaultLogger() + +func ExampleNewFromEnv() { + logger = logging.NewFromEnv("MY_APP_") +} + +func ExampleNewFromEnv_setLevel() { + logger = logging.SetLevel(logging.NewFromEnv("MY_APP_"), slog.LevelWarn) +} + +func ExampleNew() { + // Write to a buffer instead of stdout + var b bytes.Buffer + logger = logging.New(&b, slog.LevelInfo, logging.FormatJSON, false) +} + +func ExampleSetLevel() { + logging.SetLevel(myLogger, slog.LevelDebug) // level is now debug +} + +func ExampleSetLevel_safe() { + // This example demonstrates the totally safe way to set a level, assuming you + // don't know if the logger is capable of changing levels dynamically. + typ, ok := myLogger.Handler().(logging.LevelableHandler) + if !ok { + // not capable of setting levels + } + typ.SetLevel(slog.LevelDebug) // level is now debug +} + +func ExampleDefaultLogger() { + logger = logging.DefaultLogger() +} + +func ExampleWithLogger() { + ctx := context.Background() + + logger = logging.New(os.Stdout, slog.LevelDebug, logging.FormatText, true) + ctx = logging.WithLogger(ctx, logger) + + logger = logging.FromContext(ctx) // same logger +} + +func ExampleFromContext() { + ctx := context.Background() + + logger = logging.New(os.Stdout, slog.LevelDebug, logging.FormatText, true) + ctx = logging.WithLogger(ctx, logger) + + logger = logging.FromContext(ctx) // same logger +} diff --git a/logging/testing.go b/logging/testing.go new file mode 100644 index 00000000..b9a000ea --- /dev/null +++ b/logging/testing.go @@ -0,0 +1,55 @@ +// Copyright 2023 The Authors (see AUTHORS file) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logging + +import ( + "io" + "log/slog" + "testing" +) + +// TestLogger creates a new logger for use in tests. It will only log messages +// when tests fail and the tests were run with verbose (-v). +func TestLogger(tb testing.TB) *slog.Logger { + tb.Helper() + + w := &testingWriter{tb} + return slog.New(slog.NewTextHandler(w, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelDebug, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + // Drop time key since the test failures will include timestamps + if a.Key == slog.TimeKey { + return slog.Attr{} + } + return a + }, + })) +} + +var _ io.Writer = (*testingWriter)(nil) + +type testingWriter struct { + tb testing.TB +} + +func (t *testingWriter) Write(b []byte) (int, error) { + if !testing.Verbose() { + return 0, nil + } + + t.tb.Log(string(b)) + return len(b), nil +} diff --git a/logging/testing_test.go b/logging/testing_test.go new file mode 100644 index 00000000..01e31cfd --- /dev/null +++ b/logging/testing_test.go @@ -0,0 +1,42 @@ +// Copyright 2023 The Authors (see AUTHORS file) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logging_test + +import ( + "context" + "testing" + + "github.com/abcxyz/pkg/logging" +) + +//nolint:thelper // These are examples +func ExampleTestLogger() { + _ = func(t *testing.T) { // func TestMyThing(t *testing.T) + logger = logging.TestLogger(t) + } +} + +//nolint:thelper // These are examples +func ExampleTestLogger_context() { + _ = func(t *testing.T) { // func TestMyThing(t *testing.T) + // Most tests rely on the logger in the context, so here's a fast way to + // inject a test logger into the context. + ctx := logging.WithLogger(context.Background(), logging.TestLogger(t)) + + // Use ctx in tests. Anything that extracts a logger from the context will + // get the test logger now. + _ = ctx + } +} diff --git a/mysqltest/README.md b/mysqltest/README.md index ec5ec033..984953b0 100644 --- a/mysqltest/README.md +++ b/mysqltest/README.md @@ -88,7 +88,7 @@ jobs: steps: - uses: actions/setup-go@v4 with: - go-version: '1.20' # Optional + go-version: '1.21' # Optional ``` ... and *don't* do this: @@ -98,5 +98,5 @@ jobs: test: name: Go Test runs-on: ubuntu-latest - container: golang:1.20 # DON'T DO THIS + container: golang:1.21 # DON'T DO THIS ``` diff --git a/serving/serving.go b/serving/serving.go index 6d749004..f344847f 100644 --- a/serving/serving.go +++ b/serving/serving.go @@ -108,8 +108,8 @@ func (s *Server) StartHTTP(ctx context.Context, srv *http.Server) error { go func() { defer close(doneCh) - logger.Infow("server is starting", "ip", s.ip, "port", s.port) - defer logger.Infow("server is stopped") + logger.InfoContext(ctx, "server is starting", "ip", s.ip, "port", s.port) + defer logger.InfoContext(ctx, "server is stopped") if err := srv.Serve(s.listener); err != nil && !errors.Is(err, http.ErrServerClosed) { select { @@ -124,14 +124,14 @@ func (s *Server) StartHTTP(ctx context.Context, srv *http.Server) error { case err := <-errCh: return fmt.Errorf("failed to serve: %w", err) case <-ctx.Done(): - logger.Debugw("provided context is done") + logger.DebugContext(ctx, "provided context is done") } // Shutdown the server. shutdownCtx, done := context.WithTimeout(context.Background(), 10*time.Second) defer done() - logger.Debugw("server is shutting down") + logger.DebugContext(ctx, "server is shutting down") if err := srv.Shutdown(shutdownCtx); err != nil { return fmt.Errorf("failed to shutdown server: %w", err) } @@ -179,8 +179,8 @@ func (s *Server) StartGRPC(ctx context.Context, srv *grpc.Server) error { go func() { defer close(doneCh) - logger.Infow("server is starting", "ip", s.ip, "port", s.port) - defer logger.Infow("server is stopped") + logger.InfoContext(ctx, "server is starting", "ip", s.ip, "port", s.port) + defer logger.InfoContext(ctx, "server is stopped") if err := srv.Serve(s.listener); err != nil && !errors.Is(err, grpc.ErrServerStopped) { select { @@ -195,10 +195,10 @@ func (s *Server) StartGRPC(ctx context.Context, srv *grpc.Server) error { case err := <-errCh: return fmt.Errorf("failed to serve: %w", err) case <-ctx.Done(): - logger.Debugw("provided context is done") + logger.DebugContext(ctx, "provided context is done") } - logger.Debugw("server is shutting down") + logger.DebugContext(ctx, "server is shutting down") srv.GracefulStop() close(errCh)