From 7e91beef2796ff6fd0f18af9b082f4e6ce75139c Mon Sep 17 00:00:00 2001 From: Alexander Menshchikov Date: Fri, 6 Nov 2026 21:06:02 +0300 Subject: [PATCH 1/3] Exclude the fieldalignment check from the test files The check is not useful for the tests: the structures of the test helpers are not optimized for the size, and the reordering of their fields only makes them harder to read. The check is still enabled for the production code. --- closer/.golangci.yml | 3 + ctxkey/.golangci.yml | 3 + deferred/.golangci.yml | 3 + dispatcher/.golangci.yml | 3 + prommetrics/.golangci.yml | 3 + protomap/.golangci.yml | 3 + slogger/.golangci.yml | 3 + slogger/grpcslog/.golangci.yml | 350 +++++++++++++++++ slogger/grpcslog/README.md | 79 ++++ slogger/grpcslog/go.mod | 5 + slogger/grpcslog/go.sum | 2 + slogger/grpcslog/grpcslog.go | 3 + slogger/grpcslog/grpcslog_internal_test.go | 237 ++++++++++++ slogger/grpcslog/grpcslog_test.go | 419 +++++++++++++++++++++ slogger/grpcslog/options.go | 3 + strcase/.golangci.yml | 5 +- sync/.golangci.yml | 3 + time/.golangci.yml | 5 +- 18 files changed, 1128 insertions(+), 4 deletions(-) create mode 100644 slogger/grpcslog/.golangci.yml create mode 100644 slogger/grpcslog/README.md create mode 100644 slogger/grpcslog/go.mod create mode 100644 slogger/grpcslog/go.sum create mode 100644 slogger/grpcslog/grpcslog.go create mode 100644 slogger/grpcslog/grpcslog_internal_test.go create mode 100644 slogger/grpcslog/grpcslog_test.go create mode 100644 slogger/grpcslog/options.go diff --git a/closer/.golangci.yml b/closer/.golangci.yml index 7f28cfa..62d996f 100644 --- a/closer/.golangci.yml +++ b/closer/.golangci.yml @@ -79,6 +79,9 @@ linters: linters: [ errcheck ] - path: (.+)_test\.go linters: [ gocognit, goconst, mnd ] + - path: (.+)_test\.go + linters: [ govet ] + text: 'fieldalignment:' - text: 'assigned to ctx' linters: [ wastedassign ] - text: 'ctx is never used' diff --git a/ctxkey/.golangci.yml b/ctxkey/.golangci.yml index ac96fb5..164c260 100644 --- a/ctxkey/.golangci.yml +++ b/ctxkey/.golangci.yml @@ -79,6 +79,9 @@ linters: linters: [ errcheck ] - path: (.+)_test\.go linters: [ gocognit, goconst, mnd ] + - path: (.+)_test\.go + linters: [ govet ] + text: 'fieldalignment:' - text: 'assigned to ctx' linters: [ wastedassign ] - text: 'ctx is never used' diff --git a/deferred/.golangci.yml b/deferred/.golangci.yml index d6bf931..10dedd8 100644 --- a/deferred/.golangci.yml +++ b/deferred/.golangci.yml @@ -79,6 +79,9 @@ linters: linters: [ errcheck ] - path: (.+)_test\.go linters: [ gocognit, goconst, mnd ] + - path: (.+)_test\.go + linters: [ govet ] + text: 'fieldalignment:' - text: 'assigned to ctx' linters: [ wastedassign ] - text: 'ctx is never used' diff --git a/dispatcher/.golangci.yml b/dispatcher/.golangci.yml index 7f28cfa..62d996f 100644 --- a/dispatcher/.golangci.yml +++ b/dispatcher/.golangci.yml @@ -79,6 +79,9 @@ linters: linters: [ errcheck ] - path: (.+)_test\.go linters: [ gocognit, goconst, mnd ] + - path: (.+)_test\.go + linters: [ govet ] + text: 'fieldalignment:' - text: 'assigned to ctx' linters: [ wastedassign ] - text: 'ctx is never used' diff --git a/prommetrics/.golangci.yml b/prommetrics/.golangci.yml index 7f28cfa..62d996f 100644 --- a/prommetrics/.golangci.yml +++ b/prommetrics/.golangci.yml @@ -79,6 +79,9 @@ linters: linters: [ errcheck ] - path: (.+)_test\.go linters: [ gocognit, goconst, mnd ] + - path: (.+)_test\.go + linters: [ govet ] + text: 'fieldalignment:' - text: 'assigned to ctx' linters: [ wastedassign ] - text: 'ctx is never used' diff --git a/protomap/.golangci.yml b/protomap/.golangci.yml index 61e915f..dd08290 100644 --- a/protomap/.golangci.yml +++ b/protomap/.golangci.yml @@ -80,6 +80,9 @@ linters: linters: [ errcheck ] - path: (.+)_test\.go linters: [ gocognit, goconst, mnd ] + - path: (.+)_test\.go + linters: [ govet ] + text: 'fieldalignment:' - text: 'assigned to ctx' linters: [ wastedassign ] - text: 'ctx is never used' diff --git a/slogger/.golangci.yml b/slogger/.golangci.yml index e9e3af8..01e620c 100644 --- a/slogger/.golangci.yml +++ b/slogger/.golangci.yml @@ -80,6 +80,9 @@ linters: linters: [ errcheck ] - path: (.+)_test\.go linters: [ gocognit, goconst, mnd ] + - path: (.+)_test\.go + linters: [ govet ] + text: 'fieldalignment:' - text: 'assigned to ctx' linters: [ wastedassign ] - text: 'ctx is never used' diff --git a/slogger/grpcslog/.golangci.yml b/slogger/grpcslog/.golangci.yml new file mode 100644 index 0000000..d2ecab6 --- /dev/null +++ b/slogger/grpcslog/.golangci.yml @@ -0,0 +1,350 @@ +version: "2" + +run: + timeout: 10m + +severity: + default: "critical" + +formatters: + enable: + - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification. + - goimports # Check import statements are formatted according to the 'goimport' command. + + settings: + gofmt: + rewrite-rules: + - pattern: 'a[b:len(a)]' + replacement: 'a[b:]' + +linters: + default: none + enable: + - asasalint # Check for pass []any as any in variadic func(…any). + - asciicheck # Checks that all code identifiers does not have non-ASCII symbols in the name. + - bidichk # Checks for dangerous unicode character sequences. + - containedctx # Containedctx is a linter that detects struct contained context.Context field. + - contextcheck # Check whether the function uses a non-inherited context. + - copyloopvar # Copyloopvar is a linter detects places where loop variables are copied. + - depguard # Go linter that checks if package imports are in a list of acceptable packages. + - dupword # Checks for duplicate words in the source code. + - durationcheck # Check for two durations multiplied together. + - embeddedstructfieldcheck # Embedded types should be at the top of the field list of a struct, and there must be an empty line separating embedded fields from regular fields. + - errcheck # Errcheck is a program for checking for unchecked errors in Go code. These unchecked errors can be critical bugs in some cases. + - errname # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error. + - errorlint # Errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. + - exptostd # Detects functions from golang.org/x/exp/ that can be replaced by std functions. + - fatcontext # Detects nested contexts in loops and function literals. + - forcetypeassert # Find forced type assertions. + - funcorder # Checks the order of functions, methods, and constructors. + - gocheckcompilerdirectives # Checks that go compiler directive comments (//go:) are valid. + - gocognit # Computes and checks the cognitive complexity of functions. + - goconst # Finds repeated strings that could be replaced by a constant. + - gocritic # Provides diagnostics that check for bugs, performance and style issues. + - goheader # Checks if file header matches to pattern. + - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. + - gosec # Inspects source code for security problems. + - govet # Vet examines Go source code and reports suspicious constructs. + - iface # Detect the incorrect use of interfaces, helping developers avoid interface pollution. + - inamedparam # Reports interfaces with unnamed method parameters. + - intrange # Intrange is a linter to find places where for loops could make use of an integer range. + - iotamixing # Checks if iotas are being used in const blocks with other non-iota declarations. + - makezero # Finds slice declarations with non-zero initial length. + - mirror # Reports wrong mirror patterns of bytes/strings usage. + - mnd # An analyzer to detect magic numbers. + - modernize # A suite of analyzers that suggest simplifications to Go code, using modern language and library features. + - nakedret # Checks that functions with naked returns are not longer than a maximum size (can be zero). + - nilnil # Checks that there is no simultaneous return of nil error and an invalid value. + - nolintlint # Reports ill-formed or insufficient nolint directives. + - nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL. + - perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative. + - prealloc # Finds slice declarations that could potentially be pre-allocated. + - reassign # Checks that package variables are not reassigned. + - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. + - sloglint # Ensure consistent code style when using log/slog. + - staticcheck # It's a set of rules from staticcheck. + - testifylint # Checks usage of github.com/stretchr/testify. + - testpackage # Linter that makes you use a separate _test package. + - thelper # Thelper detects tests helpers which do not start with the t.Helper() method. + - tparallel # Tparallel detects inappropriate usage of t.Parallel() method in your Go test codes. + - unconvert # Remove unnecessary type conversions. + - unused # Checks Go code for unused constants, variables, functions and types. + - usestdlibvars # A linter that detect the possibility to use variables/constants from the Go standard library. + - usetesting # Reports uses of functions with replacement inside the testing package. + - wastedassign # Finds wasted assignment statements. + - whitespace # Checks for unnecessary newlines at the start and end of functions, if, for, etc. + + exclusions: + rules: + - text: 'Error return value of .((os\.)?std(out|err)\..*|.*print(f|ln)?). is not checked' + linters: [ errcheck ] + - path: (.+)_test\.go + linters: [ gocognit, goconst, mnd ] + - text: 'assigned to ctx' + linters: [ wastedassign ] + - text: 'ctx is never used' + linters: [ staticcheck ] + + paths: + - \\.git$ + - assets$ + - bench(mark)?s?$ + - bin$ + - build$ + - configs?$ + - deploy(ment)?s?$ + - docs$ + - githooks$ + - scripts$ + + generated: strict + + settings: + depguard: + rules: + main: + list-mode: lax + deny: + - pkg: 'math/rand$' + desc: 'use math/rand/v2' + - pkg: 'github.com/pkg/errors' + desc: 'Should be replaced by standard lib errors package' + + dupword: + comments-only: true + + embeddedstructfieldcheck: + forbid-mutex: true + + errcheck: + exclude-functions: + - encoding/json.Marshal + - encoding/json.MarshalIndent + + gocognit: + min-complexity: 10 + + goconst: + min-len: 2 + min-occurrences: 2 + + gocritic: + disable-all: true + enabled-checks: + ## DIAGNOSTIC + - appendAssign # Detects suspicious append result assignments. + - argOrder # Detects suspicious arguments order. + - badCall # Detects suspicious function calls. + - badCond # Detects suspicious condition expressions. + - badLock # Detects suspicious mutex lock/unlock operations. + - badRegexp # Detects suspicious regexp patterns. + - badSorting # Detects bad usage of sort package. + - badSyncOnceFunc # Detects bad usage of sync.OnceFunc. + - builtinShadowDecl # Detects top-level declarations that shadow the predeclared identifiers. + - caseOrder # Detects erroneous case order inside switch statements. + - codegenComment # Detects malformed ‘code generated’ file comments. + - commentedOutCode # Detects commented-out code inside function bodies. + - deferInLoop # Detects loops inside functions that use defer. + - deprecatedComment # Detects malformed ‘deprecated’ doc-comments. + - dupArg # Detects suspicious duplicated arguments. + - dupBranchBody # Detects duplicated branch bodies inside conditional statements. + - dupCase # Detects duplicated case clauses inside switch or select statements. + - dupOption # Detects duplicated option function arguments in variadic function calls. + - dupSubExpr # Detects suspicious duplicated sub-expressions. + - dynamicFmtString # Detects suspicious formatting strings usage. + - emptyDecl # Detects suspicious empty declarations blocks. + - evalOrder # Detects unwanted dependencies on the evaluation order. + - exitAfterDefer # Detects calls to exit/fatal inside functions that use defer. + - externalErrorReassign # Detects suspicious reassignment of error from another package. + - filepathJoin # Detects problems in filepath.Join() function calls. + - flagDeref # Detects immediate dereferencing of flag package pointers. + - flagName # Detects suspicious flag names. + - mapKey # Detects suspicious map literal keys. + - nilValReturn # Detects return statements those results evaluate to nil. + - offBy1 # Detects various off-by-one kind of errors. + - rangeAppendAll # Detects append all its data while range it. + - regexpPattern # Detects suspicious regexp patterns. + - returnAfterHttpError # Detects suspicious http.Error call without following return. + - sloppyLen # Detects usage of len when result is obvious or doesn’t make sense. + - sloppyReassign # Detects suspicious/confusing re-assignments. + - sloppyTypeAssert # Detects redundant type assertions. + - sortSlice # Detects suspicious sort.Slice calls. + - sprintfQuotedString # Detects “%s” formatting directives that can be replaced with %q. + - sqlQuery # Detects issue in Query() and Exec() calls. + - syncMapLoadAndDelete # Detects sync.Map load+delete operations that can be replaced with LoadAndDelete. + - truncateCmp # Detects potential truncation issues when comparing ints of different sizes. + - uncheckedInlineErr # Detects unchecked errors in if statements. + - unnecessaryDefer # Detects redundantly deferred calls. + - weakCond # Detects conditions that are unsafe due to not being exhaustive. + + ## STYLE + - assignOp # Detects assignments that can be simplified by using assignment operators. + - boolExprSimplify # Detects bool expressions that can be simplified. + - builtinShadow # Detects when predeclared identifiers are shadowed in assignments. + - captLocal # Detects capitalized names for local variables. + - commentFormatting # Detects comments with non-idiomatic formatting. + - commentedOutImport # Detects commented-out imports. + - defaultCaseOrder # Detects when default case in switch isn’t on 1st or last position. + - deferUnlambda # Detects deferred function literals that can be simplified. + - docStub # Detects comments that silence go lint complaints about doc-comment. + - dupImport # Detects multiple imports of the same package under different aliases. + - elseif # Detects else with nested if statement that can be replaced with else-if. + - emptyFallthrough # Detects fallthrough that can be avoided by using multi case values. + - emptyStringTest # Detects empty string checks that can be written more idiomatically. + - exposedSyncMutex # Detects exposed methods from sync.Mutex and sync.RWMutex. + - hexLiteral # Detects hex literals that have mixed case letter digits. + - httpNoBody # Detects nil usages in http.NewRequest calls, suggesting http.NoBody as an alternative. + - ifElseChain # Detects repeated if-else statements and suggests to replace them with switch statement. + - importShadow # Detects when imported package names shadowed in the assignments. + - initClause # Detects non-assignment statements inside if/switch init clause. + - methodExprCall # Detects method expression call that can be replaced with a method call. + - nestingReduce # Finds where nesting level could be reduced. + - newDeref # Detects immediate dereferencing of new expressions. + - octalLiteral # Detects old-style octal literals. + - paramTypeCombine # Detects if function parameters could be combined by type and suggest the way to do it. + - preferFilepathJoin # Detects concatenation with os.PathSeparator which can be replaced with filepath.Join. + - ptrToRefParam # Detects input and output parameters that have a type of pointer to referential type. + - redundantSprint # Detects redundant fmt.Sprint calls. + - regexpMust # Detects regexp.Compile* that can be replaced with regexp.MustCompile*. + - regexpSimplify # Detects regexp patterns that can be simplified. + - singleCaseSwitch # Detects switch statements that could be better written as if statement. + - stringConcatSimplify # Detects string concat operations that can be simplified. + - stringsCompare # Detects strings.Compare usage. + - switchTrue # Detects switch-over-bool statements that use explicit true tag value. + - timeExprSimplify # Detects manual conversion to milli- or microseconds. + - todoCommentWithoutDetail # Detects TODO comments without detail/assignee. + # tooManyResultsChecker # Detects function with too many results. + - typeAssertChain # Detects repeated type assertions and suggests to replace them with type switch statement. + - typeDefFirst # Detects method declarations preceding the type definition itself. + - typeSwitchVar # Detects type switches that can benefit from type guard clause with variable. + - typeUnparen # Detects unneeded parenthesis inside type expressions and suggests to remove them. + - underef # Detects dereference expressions that can be omitted. + - unlabelStmt # Detects redundant statement labels. + - unlambda # Detects function literals that can be simplified. + # unnamedResult # Detects unnamed results that may benefit from names. + # unnecessaryBlock # Detects unnecessary braced statement blocks. + - unslice # Detects slice expressions that can be simplified to sliced expression itself. + - valSwap # Detects value swapping code that are not using parallel assignment. + # whyNoLint # Ensures that //nolint comments include an explanation. + - wrapperFunc # Detects function calls that can be replaced with convenience wrappers. + - yodaStyleExpr # Detects Yoda style expressions and suggests to replace them. + + ## PERFORMANCE + - appendCombine # Detects append chains to the same slice that can be done in a single append call. + - equalFold # Detects unoptimal strings/bytes case-insensitive comparison. + - hugeParam # Detects params that incur excessive amount of copying. + - indexAlloc # Detects strings.Index calls that may cause unwanted allocs. + - preferDecodeRune # Detects expressions like []rune(s)[0] that may cause unwanted rune slice allocation. + - preferFprint # Detects fmt.Sprint(f/ln) calls which can be replaced with fmt.Fprint(f/ln). + - preferStringWriter # Detects w.Write or io.WriteString calls which can be replaced with w.WriteString. + - preferWriteByte # Detects WriteRune calls with rune literal argument that is single byte and reports to use WriteByte instead. + - rangeExprCopy # Detects expensive copies of for loop range expressions. + - rangeValCopy # Detects loops that copy big objects during each iteration. + - sliceClear # Detects slice clear loops, suggests an idiom that is recognized by the Go compiler. + - stringXbytes # Detects redundant conversions between string and []byte. + - zeroByteRepeat # Detects bytes.Repeat with 0 value. + + goheader: + template-path: ../../.github/goheader.tmpl + + gomoddirectives: + replace-local: true + + gosec: + excludes: + # _ instead of err checks + - G104 + + govet: + enable-all: true + + iface: + enable: + - identical + - opaque + - unused + + inamedparam: + skip-single-param: true + + iotamixing: + report-individual: true + + nakedret: + max-func-lines: 10 + + nilnil: + detect-opposite: true + + nolintlint: + require-specific: true + + prealloc: + range-loops: true + + reassign: + patterns: + - '.*' + + revive: + severity: error + rules: + - name: atomic # Check for commonly mistaken usages of the sync/atomic package. + - name: confusing-naming # Methods or fields of struct that have names different only by capitalization could be confusing. + - name: confusing-results # Function or methods that return multiple, no named, values of the same type could induce error. + - name: constant-logical-expr # The rule spots logical expressions that evaluate always to the same value. + - name: context-as-argument # By convention, context.Context should be the first parameter of a function. This rule spots function declarations that do not follow the convention. + - name: context-keys-type # Basic types should not be used as a key in context.WithValue. + - name: deep-exit # Packages exposing functions that can stop program execution by exiting are hard to reuse. This rule looks for program exits in functions other than main() or init(). + - name: defer # This rule warns on some common mistakes when using defer statement. + arguments: + - - 'call-chain' + - 'loop' + - 'method-call' + - 'recover' + - 'immediate-recover' + - 'return' + - name: duplicated-imports # It is possible to unintentionally import the same package twice. This rule looks for packages that are imported two or more times. + - name: early-return # In GO it is idiomatic to minimize nesting statements, a typical example is to avoid if-then-else constructions. + - name: empty-block # Empty blocks make code less readable and could be a symptom of a bug or unfinished refactoring. + - name: empty-lines # Sometimes gofmt is not enough to enforce a common formatting of a code-base; this rule warns when there are heading or trailing newlines in code blocks. + - name: error-return # By convention, for the sake of readability, the errors should be last in the list of returned values by a function. + - name: error-strings # By convention, for better readability, error messages should not be capitalized or end with punctuation or a newline. + - name: errorf # It is possible to get a simpler program by replacing errors.New(fmt.Sprintf()) with fmt.Errorf(). This rule spots that kind of simplification opportunities. + - name: forbidden-call-in-wg-go # Warns on forbidden calls inside calls to wg.Go. + - name: identical-branches # An if-then-else conditional with identical implementations in both branches is an error. + - name: identical-ifelseif-branches # An if ... else if chain with identical branches makes maintenance harder and might be a source of bugs. Duplicated branches should be consolidated in one. + - name: identical-ifelseif-conditions # An if ... else if chain with identical conditions can lead to unreachable code and is a potential source of bugs while making the code harder to read and maintain. + - name: identical-switch-branches # A switch with identical branches makes maintenance harder and might be a source of bugs. Duplicated branches should be consolidated in one case clause. + - name: identical-switch-conditions # A switch statement with cases with the same condition can lead to unreachable code and is a potential source of bugs while making the code harder to read and maintain. + - name: if-return # Checking if an error is nil to just after return the error or nil is redundant. + - name: increment-decrement # By convention, for better readability, incrementing an integer variable by 1 is recommended to be done using the ++ operator. This rule spots expressions like i += 1 and i -= 1 and proposes to change them into i++ and i--. + - name: inefficient-map-lookup # This rule identifies code that iteratively searches for a key in a map. + - name: optimize-operands-order # Conditional expressions can be written to take advantage of short circuit evaluation and speed up its average evaluation time by forcing the evaluation of less time-consuming terms before more costly ones. This rule spots logical expressions where the order of evaluation of terms seems non optimal. + - name: range # This rule suggests a shorter way of writing ranges that do not use the second value. + - name: redefines-builtin-id # Constant names like false, true, nil, function names like append, make, and basic type names like bool, and byte are not reserved words of the language; therefore the can be redefined. Even if possible, redefining these built in names can lead to bugs very difficult to detect. + - name: redundant-build-tag # This rule warns about redundant build tag comments // +build when //go:build is present. gofmt in Go 1.17+ automatically adds the //go:build constraint, making the // +build comment unnecessary. + - name: string-of-int # Explicit type conversion string(i) where i has an integer type other than rune might behave not as expected by the developer (e.g. string(42) is not "42"). This rule spot that kind of suspicious conversions. + - name: unchecked-type-assertion # This rule checks whether a type assertion result is checked (the ok value), preventing unexpected panics. + arguments: + - accept-ignored-assertion-result: true + - name: use-any # Since Go 1.18, interface{} has an alias: any. This rule proposes to replace instances of interface{} with any. + - name: use-errors-new # This rules identifies calls to fmt.Errorf that can be safely replaced by, the more efficient, errors.New. + - name: use-fmt-print # This rule proposes to replace calls to built-in print and println with their equivalents from fmt standard package. + - name: use-waitgroup-go # Since Go 1.25 the sync package proposes the WaitGroup.Go method. This method is a shorter and safer replacement for the idiom wg.Add ... go { ... wg.Done ... }. + - name: useless-break # This rule warns on useless break statements in case clauses of switch and select statements. + - name: useless-fallthrough # This rule warns on useless fallthrough statements in case clauses of switch statements. A fallthrough is considered useless if it's the single statement of a case clause block. + - name: unnecessary-if # Identifies `if-else` statements that can be replaced by simpler statements. + - name: unnecessary-stmt # This rule suggests to remove redundant statements like a break at the end of a case block, for improving the code's readability. + - name: unreachable-code # This rule spots and proposes to remove unreachable code. + - name: waitgroup-by-value # Function parameters that are passed by value, are in fact a copy of the original argument. Passing a copy of a sync.WaitGroup is usually not what the developer wants to do. This rule warns when a sync.WaitGroup expected as a by-value parameter in a function or method. + + sloglint: + attr-only: true + context: "scope" + msg-style: "capitalized" + key-naming-case: "snake" + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/slogger/grpcslog/README.md b/slogger/grpcslog/README.md new file mode 100644 index 0000000..e9bf8ac --- /dev/null +++ b/slogger/grpcslog/README.md @@ -0,0 +1,79 @@ +# grpcslog + +A [`grpclog.LoggerV2`](https://pkg.go.dev/google.golang.org/grpc/grpclog) implementation +that writes the gRPC messages to a [`log/slog`](https://pkg.go.dev/log/slog) logger. + +The logger is installed as the global gRPC logger, so all the messages of gRPC +go to the same place as the rest of the application logs: + +```go +logger, err := grpcslog.NewGrpcLogger(ctx) +if err != nil { + return err +} + +grpclog.SetLoggerV2(logger) +``` + +## Public API + +```go +func NewGrpcLogger(ctx context.Context) (*GrpcLogger, error) +func NewGrpcLoggerWithVerbosity(ctx context.Context, v int) (*GrpcLogger, error) +``` + +Both constructors return an error if the environment variables contain invalid +values. + +## Configuration + +The logger is configured with the same environment variables as the default +gRPC logger: + +| Variable | Values | Default | +|-----------------------------|----------------------------|---------| +| `GRPC_GO_LOG_SEVERITY_LEVEL` | `INFO`, `WARNING`, `ERROR` | `ERROR` | +| `GRPC_GO_LOG_VERBOSITY_LEVEL` | a non-negative integer | `0` | +| `GRPC_GO_LOG_FORMATTER` | `json`, `text` | `text` | + +- `GRPC_GO_LOG_SEVERITY_LEVEL` — the severity level of the logged messages. The + messages below the level are dropped. +- `GRPC_GO_LOG_VERBOSITY_LEVEL` — the verbosity level of the logger. It sets + the threshold of the `V` method, so the verbose messages of gRPC are logged + only if their level is not greater than the verbosity level of the logger. + The variable takes precedence over the level passed to + `NewGrpcLoggerWithVerbosity`. +- `GRPC_GO_LOG_FORMATTER` — the format of the logged messages: `json` or + `text`. The value is case-insensitive. + +The messages are written to `os.Stdout`, like the messages of the +[`slogger`](../) package. + +## Verbosity + +The verbosity level of gRPC is a threshold: the higher it is, the more verbose +messages are logged. The `V` method reports whether a message of the given +verbose level should be logged: + +```go +logger, err := grpcslog.NewGrpcLoggerWithVerbosity(ctx, 2) +if err != nil { + return err +} + +logger.V(2) // true +logger.V(3) // false +``` + +The verbose messages of gRPC are written with the info severity, so they are +only logged if the severity level allows the info records. + +## Errors + +The constructors return an error if the severity level is unknown or if the +verbosity level is not a non-negative integer: + +``` +unknown gRPC log severity level "bogus" +invalid gRPC log verbosity level "bogus": strconv.Atoi: parsing "bogus": invalid syntax +``` diff --git a/slogger/grpcslog/go.mod b/slogger/grpcslog/go.mod new file mode 100644 index 0000000..b4e19f8 --- /dev/null +++ b/slogger/grpcslog/go.mod @@ -0,0 +1,5 @@ +module github.com/nbgrp/pkg/slogger/grpcslog + +go 1.26.0 + +require google.golang.org/grpc v1.85.0 diff --git a/slogger/grpcslog/go.sum b/slogger/grpcslog/go.sum new file mode 100644 index 0000000..c8ed087 --- /dev/null +++ b/slogger/grpcslog/go.sum @@ -0,0 +1,2 @@ +google.golang.org/grpc v1.85.0 h1:BPBCfuVRQ1qvBhKDOq9uvQllMvO59rKsYhXpp7Dd8vw= +google.golang.org/grpc v1.85.0/go.mod h1:0rQrOD58yjal/0EaVbGBANindixsCZNQPDV3VKtaKuQ= diff --git a/slogger/grpcslog/grpcslog.go b/slogger/grpcslog/grpcslog.go new file mode 100644 index 0000000..b661980 --- /dev/null +++ b/slogger/grpcslog/grpcslog.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package grpcslog diff --git a/slogger/grpcslog/grpcslog_internal_test.go b/slogger/grpcslog/grpcslog_internal_test.go new file mode 100644 index 0000000..1b84083 --- /dev/null +++ b/slogger/grpcslog/grpcslog_internal_test.go @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package grpcslog + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// record is a logged message. +type record struct { + Level string `json:"level"` + Msg string `json:"msg"` +} + +// TestLogMethods checks that the messages are logged with the expected +// severity levels and contents. +func TestLogMethods(t *testing.T) { + tests := []struct { + name string + log func(l *GrpcLogger) + level string + msg string + }{ + { + name: "Info", + log: func(l *GrpcLogger) { l.Info("message") }, + level: "INFO", + msg: "message", + }, + { + name: "Infoln", + log: func(l *GrpcLogger) { l.Infoln("message") }, + level: "INFO", + msg: "message\n", + }, + { + name: "Infof", + log: func(l *GrpcLogger) { l.Infof("mes%s", "sage") }, + level: "INFO", + msg: "message", + }, + { + name: "Warning", + log: func(l *GrpcLogger) { l.Warning("message") }, + level: "WARN", + msg: "message", + }, + { + name: "Warningln", + log: func(l *GrpcLogger) { l.Warningln("message") }, + level: "WARN", + msg: "message\n", + }, + { + name: "Warningf", + log: func(l *GrpcLogger) { l.Warningf("mes%s", "sage") }, + level: "WARN", + msg: "message", + }, + { + name: "Error", + log: func(l *GrpcLogger) { l.Error("message") }, + level: "ERROR", + msg: "message", + }, + { + name: "Errorln", + log: func(l *GrpcLogger) { l.Errorln("message") }, + level: "ERROR", + msg: "message\n", + }, + { + name: "Errorf", + log: func(l *GrpcLogger) { l.Errorf("mes%s", "sage") }, + level: "ERROR", + msg: "message", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", "INFO") + t.Setenv("GRPC_GO_LOG_FORMATTER", "json") + + buf := &bytes.Buffer{} + l, err := newGrpcLoggerWithWriter(t.Context(), 0, buf) + require.NoError(t, err) + + tt.log(l) + + records := parseRecords(t, buf.String()) + require.Len(t, records, 1) + assert.Equal(t, tt.level, records[0].Level) + assert.Equal(t, tt.msg, records[0].Msg) + }) + } +} + +// TestFatalMethods checks that the fatal methods log the message and exit the +// process with the code 1. +func TestFatalMethods(t *testing.T) { + tests := []struct { + name string + log func(l *GrpcLogger) + }{ + {name: "Fatal", log: func(l *GrpcLogger) { l.Fatal("fatal message") }}, + {name: "Fatalln", log: func(l *GrpcLogger) { l.Fatalln("fatal message") }}, + {name: "Fatalf", log: func(l *GrpcLogger) { l.Fatalf("fatal %s", "message") }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", "ERROR") + t.Setenv("GRPC_GO_LOG_FORMATTER", "json") + + orig := exit + t.Cleanup(func() { exit = orig }) + + var code int + exit = func(c int) { code = c } + + buf := &bytes.Buffer{} + l, err := newGrpcLoggerWithWriter(t.Context(), 0, buf) + require.NoError(t, err) + + tt.log(l) + + assert.Equal(t, 1, code) + + records := parseRecords(t, buf.String()) + require.Len(t, records, 1) + assert.Equal(t, "ERROR", records[0].Level) + assert.Contains(t, records[0].Msg, "fatal message") + }) + } +} + +// TestSeverityLevel checks that the severity level is taken from the +// environment variable. +func TestSeverityLevel(t *testing.T) { + tests := []struct { + name string + value string + want []string + }{ + {name: "unset", value: "", want: []string{"ERROR"}}, + {name: "error", value: "ERROR", want: []string{"ERROR"}}, + {name: "warning", value: "WARNING", want: []string{"WARN", "ERROR"}}, + {name: "info", value: "INFO", want: []string{"INFO", "WARN", "ERROR"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", tt.value) + t.Setenv("GRPC_GO_LOG_FORMATTER", "json") + + buf := &bytes.Buffer{} + l, err := newGrpcLoggerWithWriter(t.Context(), 0, buf) + require.NoError(t, err) + + l.Info("info message") + l.Warning("warning message") + l.Error("error message") + + got := make([]string, 0, len(tt.want)) + for _, record := range parseRecords(t, buf.String()) { + got = append(got, record.Level) + } + + assert.Equal(t, tt.want, got) + }) + } +} + +// TestLogFormat checks that the format of the messages is taken from the +// environment variable. +func TestLogFormat(t *testing.T) { + tests := []struct { + name string + value string + want string + }{ + {name: "unset", value: "", want: "text"}, + {name: "text", value: "text", want: "text"}, + {name: "json", value: "json", want: "json"}, + {name: "json in upper case", value: "JSON", want: "json"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", "INFO") + t.Setenv("GRPC_GO_LOG_FORMATTER", tt.value) + + buf := &bytes.Buffer{} + l, err := newGrpcLoggerWithWriter(t.Context(), 0, buf) + require.NoError(t, err) + + l.Info("info message") + + switch tt.want { + case "json": + records := parseRecords(t, buf.String()) + require.Len(t, records, 1) + assert.Equal(t, "info message", records[0].Msg) + case "text": + assert.Contains(t, buf.String(), "level=INFO") + assert.Contains(t, buf.String(), "msg=\"info message\"") + } + }) + } +} + +// parseRecords parses the JSON messages written to the buffer. +func parseRecords(t *testing.T, out string) []record { + t.Helper() + + var records []record + + for line := range strings.SplitSeq(strings.TrimSpace(out), "\n") { + if line == "" { + continue + } + + var r record + require.NoError(t, json.Unmarshal([]byte(line), &r)) + + records = append(records, r) + } + + return records +} diff --git a/slogger/grpcslog/grpcslog_test.go b/slogger/grpcslog/grpcslog_test.go new file mode 100644 index 0000000..9f514ff --- /dev/null +++ b/slogger/grpcslog/grpcslog_test.go @@ -0,0 +1,419 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package grpcslog_test + +import ( + "bytes" + "encoding/json" + "io" + "os" + "os/exec" + "strings" + "testing" + + "github.com/nbgrp/pkg/slogger/grpcslog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// fatalMethodEnvVar tells the helper test which fatal method to call. +const fatalMethodEnvVar = "GRPCSLOG_TEST_FATAL_METHOD" + +// TestV checks the verbosity semantics of the logger. +// +// The verbosity level of gRPC is a threshold: the higher it is, the more +// verbose messages are logged. A message of the verbose level is logged if +// its level is not greater than the verbosity level of the logger. +func TestV(t *testing.T) { + tests := []struct { + name string + verbosity int + level int + want bool + }{ + {name: "level 0 with zero verbosity", verbosity: 0, level: 0, want: true}, + {name: "level 1 with zero verbosity", verbosity: 0, level: 1, want: false}, + {name: "level 2 with zero verbosity", verbosity: 0, level: 2, want: false}, + {name: "level 0 with verbosity 2", verbosity: 2, level: 0, want: true}, + {name: "level 1 with verbosity 2", verbosity: 2, level: 1, want: true}, + {name: "level 2 with verbosity 2", verbosity: 2, level: 2, want: true}, + {name: "level 3 with verbosity 2", verbosity: 2, level: 3, want: false}, + {name: "level 2 with verbosity 3", verbosity: 3, level: 2, want: true}, + {name: "level 4 with verbosity 3", verbosity: 3, level: 4, want: false}, + {name: "negative level with zero verbosity", verbosity: 0, level: -1, want: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("GRPC_GO_LOG_VERBOSITY_LEVEL", "") + + l, err := grpcslog.NewGrpcLoggerWithVerbosity(t.Context(), tt.verbosity) + require.NoError(t, err) + + assert.Equal(t, tt.want, l.V(tt.level)) + }) + } +} + +// TestV_EnabledLevels checks that the enabled verbose levels form a prefix: +// if a level is enabled, all the lower levels are enabled too. +func TestV_EnabledLevels(t *testing.T) { + for verbosity := range 5 { + t.Setenv("GRPC_GO_LOG_VERBOSITY_LEVEL", "") + + l, err := grpcslog.NewGrpcLoggerWithVerbosity(t.Context(), verbosity) + require.NoError(t, err) + + for level := range 5 { + want := level <= verbosity + + assert.Equalf(t, want, l.V(level), "verbosity %d, level %d", verbosity, level) + } + } +} + +// TestNewGrpcLogger_Verbosity checks that the verbosity level is taken from +// the environment variable. +func TestNewGrpcLogger_Verbosity(t *testing.T) { + t.Setenv("GRPC_GO_LOG_VERBOSITY_LEVEL", "2") + + l, err := grpcslog.NewGrpcLogger(t.Context()) + require.NoError(t, err) + + assert.True(t, l.V(0)) + assert.True(t, l.V(1)) + assert.True(t, l.V(2)) + assert.False(t, l.V(3)) +} + +// TestNewGrpcLoggerWithVerbosity_EnvPrecedence checks that the environment +// variable takes precedence over the verbosity level passed to the +// constructor. +func TestNewGrpcLoggerWithVerbosity_EnvPrecedence(t *testing.T) { + t.Setenv("GRPC_GO_LOG_VERBOSITY_LEVEL", "3") + + l, err := grpcslog.NewGrpcLoggerWithVerbosity(t.Context(), 1) + require.NoError(t, err) + + assert.True(t, l.V(3)) + assert.False(t, l.V(4)) +} + +// TestNewGrpcLoggerWithVerbosity_EnvUnset checks that the verbosity level +// passed to the constructor is used if the environment variable is not set. +func TestNewGrpcLoggerWithVerbosity_EnvUnset(t *testing.T) { + t.Setenv("GRPC_GO_LOG_VERBOSITY_LEVEL", "") + + l, err := grpcslog.NewGrpcLoggerWithVerbosity(t.Context(), 2) + require.NoError(t, err) + + assert.True(t, l.V(2)) + assert.False(t, l.V(3)) +} + +// TestNewGrpcLogger_SeverityLevel checks that the severity level is taken +// from the environment variable. +func TestNewGrpcLogger_SeverityLevel(t *testing.T) { + tests := []struct { + name string + value string + want []string + }{ + {name: "unset", value: "", want: []string{"ERROR"}}, + {name: "error", value: "ERROR", want: []string{"ERROR"}}, + {name: "warning", value: "WARNING", want: []string{"WARN", "ERROR"}}, + {name: "info", value: "INFO", want: []string{"INFO", "WARN", "ERROR"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", tt.value) + t.Setenv("GRPC_GO_LOG_FORMATTER", "json") + + out := captureStderr(t, func() { + l, err := grpcslog.NewGrpcLogger(t.Context()) + require.NoError(t, err) + + l.Info("info message") + l.Warning("warning message") + l.Error("error message") + }) + + var got []string + for _, record := range parseRecords(t, out) { + got = append(got, record.Level) + } + + assert.Equal(t, tt.want, got) + }) + } +} + +// TestNewGrpcLogger_Format checks that the format of the messages is taken +// from the environment variable. +func TestNewGrpcLogger_Format(t *testing.T) { + tests := []struct { + name string + value string + want string + }{ + {name: "unset", value: "", want: "text"}, + {name: "text", value: "text", want: "text"}, + {name: "json", value: "json", want: "json"}, + {name: "json in upper case", value: "JSON", want: "json"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", "INFO") + t.Setenv("GRPC_GO_LOG_FORMATTER", tt.value) + + out := captureStderr(t, func() { + l, err := grpcslog.NewGrpcLogger(t.Context()) + require.NoError(t, err) + + l.Info("info message") + }) + + switch tt.want { + case "json": + records := parseRecords(t, out) + require.Len(t, records, 1) + assert.Equal(t, "info message", records[0].Msg) + case "text": + assert.Contains(t, out, "level=INFO") + assert.Contains(t, out, "msg=\"info message\"") + } + }) + } +} + +// TestNewGrpcLogger_InvalidEnv checks that the invalid values of the +// environment variables are reported. +func TestNewGrpcLogger_InvalidEnv(t *testing.T) { + tests := []struct { + name string + key string + value string + want string + }{ + { + name: "unknown severity level", + key: "GRPC_GO_LOG_SEVERITY_LEVEL", + value: "bogus", + want: `unknown gRPC log severity level "bogus"`, + }, + { + name: "invalid verbosity level", + key: "GRPC_GO_LOG_VERBOSITY_LEVEL", + value: "bogus", + want: `invalid gRPC log verbosity level "bogus"`, + }, + { + name: "negative verbosity level", + key: "GRPC_GO_LOG_VERBOSITY_LEVEL", + value: "-1", + want: `invalid gRPC log verbosity level "-1"`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", "") + t.Setenv("GRPC_GO_LOG_VERBOSITY_LEVEL", "") + t.Setenv(tt.key, tt.value) + + _, err := grpcslog.NewGrpcLogger(t.Context()) + require.ErrorContains(t, err, tt.want) + }) + } +} + +// TestLogMethods checks that the messages are logged with the expected +// severity levels and contents. +func TestLogMethods(t *testing.T) { + tests := []struct { + name string + log func(l *grpcslog.GrpcLogger) + level string + msg string + }{ + { + name: "Info", + log: func(l *grpcslog.GrpcLogger) { l.Info("message") }, + level: "INFO", + msg: "message", + }, + { + name: "Infoln", + log: func(l *grpcslog.GrpcLogger) { l.Infoln("message") }, + level: "INFO", + msg: "message\n", + }, + { + name: "Infof", + log: func(l *grpcslog.GrpcLogger) { l.Infof("mes%s", "sage") }, + level: "INFO", + msg: "message", + }, + { + name: "Warning", + log: func(l *grpcslog.GrpcLogger) { l.Warning("message") }, + level: "WARN", + msg: "message", + }, + { + name: "Warningln", + log: func(l *grpcslog.GrpcLogger) { l.Warningln("message") }, + level: "WARN", + msg: "message\n", + }, + { + name: "Warningf", + log: func(l *grpcslog.GrpcLogger) { l.Warningf("mes%s", "sage") }, + level: "WARN", + msg: "message", + }, + { + name: "Error", + log: func(l *grpcslog.GrpcLogger) { l.Error("message") }, + level: "ERROR", + msg: "message", + }, + { + name: "Errorln", + log: func(l *grpcslog.GrpcLogger) { l.Errorln("message") }, + level: "ERROR", + msg: "message\n", + }, + { + name: "Errorf", + log: func(l *grpcslog.GrpcLogger) { l.Errorf("mes%s", "sage") }, + level: "ERROR", + msg: "message", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", "INFO") + t.Setenv("GRPC_GO_LOG_FORMATTER", "json") + + out := captureStderr(t, func() { + l, err := grpcslog.NewGrpcLogger(t.Context()) + require.NoError(t, err) + + tt.log(l) + }) + + records := parseRecords(t, out) + require.Len(t, records, 1) + assert.Equal(t, tt.level, records[0].Level) + assert.Equal(t, tt.msg, records[0].Msg) + }) + } +} + +// TestFatalMethods checks that the fatal methods log the message and exit the +// process with the code 1. +// +// The methods are called in a subprocess, because they exit the process. +func TestFatalMethods(t *testing.T) { + tests := []struct { + name string + method string + }{ + {name: "Fatal", method: "Fatal"}, + {name: "Fatalln", method: "Fatalln"}, + {name: "Fatalf", method: "Fatalf"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var stderr bytes.Buffer + + cmd := exec.Command(os.Args[0], "-test.run=^TestFatalHelper$") + cmd.Env = append(os.Environ(), fatalMethodEnvVar+"="+tt.method) + cmd.Stderr = &stderr + + err := cmd.Run() + + var exitErr *exec.ExitError + require.ErrorAs(t, err, &exitErr) + assert.Equal(t, 1, exitErr.ExitCode()) + assert.Contains(t, stderr.String(), "fatal message") + }) + } +} + +// TestFatalHelper calls the fatal method selected by the environment +// variable. It is run in a subprocess by TestFatalMethods. +func TestFatalHelper(t *testing.T) { + method := os.Getenv(fatalMethodEnvVar) + if method == "" { + t.Skip("not a helper process") + } + + l, err := grpcslog.NewGrpcLogger(t.Context()) + require.NoError(t, err) + + switch method { + case "Fatal": + l.Fatal("fatal message") + case "Fatalln": + l.Fatalln("fatal message") + case "Fatalf": + l.Fatalf("fatal %s", "message") + default: + t.Fatalf("unknown method %q", method) + } +} + +// record is a logged message. +type record struct { + Level string `json:"level"` + Msg string `json:"msg"` +} + +// parseRecords parses the JSON messages logged to the standard error output. +func parseRecords(t *testing.T, out string) []record { + t.Helper() + + var records []record + + for line := range strings.SplitSeq(strings.TrimSpace(out), "\n") { + if line == "" { + continue + } + + var r record + require.NoError(t, json.Unmarshal([]byte(line), &r)) + + records = append(records, r) + } + + return records +} + +// captureStderr redirects the standard error output to a pipe for the +// duration of the function and returns the captured output. +func captureStderr(t *testing.T, f func()) string { + t.Helper() + + r, w, err := os.Pipe() + require.NoError(t, err) + + orig := os.Stderr + os.Stderr = w + defer func() { os.Stderr = orig }() + + f() + + require.NoError(t, w.Close()) + + out, err := io.ReadAll(r) + require.NoError(t, err) + require.NoError(t, r.Close()) + + return string(out) +} diff --git a/slogger/grpcslog/options.go b/slogger/grpcslog/options.go new file mode 100644 index 0000000..b661980 --- /dev/null +++ b/slogger/grpcslog/options.go @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package grpcslog diff --git a/strcase/.golangci.yml b/strcase/.golangci.yml index 3e6f20b..3feaf3c 100644 --- a/strcase/.golangci.yml +++ b/strcase/.golangci.yml @@ -79,6 +79,9 @@ linters: linters: [ errcheck ] - path: (.+)_test\.go linters: [ gocognit, goconst, mnd ] + - path: (.+)_test\.go + linters: [ govet ] + text: 'fieldalignment:' - text: 'assigned to ctx' linters: [ wastedassign ] - text: 'ctx is never used' @@ -256,8 +259,6 @@ linters: govet: enable-all: true - disable: - - fieldalignment iface: enable: diff --git a/sync/.golangci.yml b/sync/.golangci.yml index d6bf931..10dedd8 100644 --- a/sync/.golangci.yml +++ b/sync/.golangci.yml @@ -79,6 +79,9 @@ linters: linters: [ errcheck ] - path: (.+)_test\.go linters: [ gocognit, goconst, mnd ] + - path: (.+)_test\.go + linters: [ govet ] + text: 'fieldalignment:' - text: 'assigned to ctx' linters: [ wastedassign ] - text: 'ctx is never used' diff --git a/time/.golangci.yml b/time/.golangci.yml index ea57071..199bcf6 100644 --- a/time/.golangci.yml +++ b/time/.golangci.yml @@ -81,6 +81,9 @@ linters: linters: [ errcheck ] - path: (.+)_test\.go linters: [ gocognit, goconst, mnd ] + - path: (.+)_test\.go + linters: [ govet ] + text: 'fieldalignment:' - text: 'assigned to ctx' linters: [ wastedassign ] - text: 'ctx is never used' @@ -262,8 +265,6 @@ linters: govet: enable-all: true - disable: - - fieldalignment iface: enable: From 90a0c6066b25d41de2f0a47c38cf241aa87ef8be Mon Sep 17 00:00:00 2001 From: Alexander Menshchikov Date: Fri, 6 Nov 2026 21:06:05 +0300 Subject: [PATCH 2/3] Update testify to v1.12.1 The version is aligned with the one used by the other modules of the repository. --- closer/go.mod | 9 ++------- closer/go.sum | 16 ++-------------- ctxkey/go.mod | 2 +- ctxkey/go.sum | 21 ++------------------- 4 files changed, 7 insertions(+), 41 deletions(-) diff --git a/closer/go.mod b/closer/go.mod index 626d228..c39962f 100644 --- a/closer/go.mod +++ b/closer/go.mod @@ -3,13 +3,8 @@ module github.com/nbgrp/pkg/closer go 1.20 require ( - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.12.1 go.uber.org/goleak v1.3.0 ) -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) +require go.yaml.in/yaml/v3 v3.0.5 // indirect diff --git a/closer/go.sum b/closer/go.sum index 139b8e2..3569113 100644 --- a/closer/go.sum +++ b/closer/go.sum @@ -1,16 +1,4 @@ -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -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= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= diff --git a/ctxkey/go.mod b/ctxkey/go.mod index a276442..37f7d70 100644 --- a/ctxkey/go.mod +++ b/ctxkey/go.mod @@ -2,4 +2,4 @@ module github.com/nbgrp/pkg/ctxkey go 1.14 -require github.com/stretchr/testify v1.9.0 +require github.com/stretchr/testify v1.12.1 diff --git a/ctxkey/go.sum b/ctxkey/go.sum index 8f0c6a1..617baf9 100644 --- a/ctxkey/go.sum +++ b/ctxkey/go.sum @@ -1,19 +1,2 @@ -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= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= From 0ca44865263bc133d2c6f673ec23d55bace04d20 Mon Sep 17 00:00:00 2001 From: Alexander Menshchikov Date: Fri, 6 Nov 2026 21:06:10 +0300 Subject: [PATCH 3/3] Add the grpcslog package The grpclog.LoggerV2 implementation that writes the gRPC messages to the slog logger. The logger is created by the caller and passed to the New constructor, so the level and the format of the records are set in it. The package manages only the verbosity level of the V method: it is taken from the GRPC_GO_LOG_VERBOSITY_LEVEL environment variable or from the WithVerbosity option, and the environment variable takes precedence. --- README.md | 1 + go.work | 1 + go.work.sum | 54 ++++ slogger/README.md | 6 + slogger/grpcslog/.golangci.yml | 3 + slogger/grpcslog/README.md | 57 ++-- slogger/grpcslog/go.mod | 7 +- slogger/grpcslog/go.sum | 4 + slogger/grpcslog/grpcslog.go | 127 +++++++++ slogger/grpcslog/grpcslog_internal_test.go | 177 +----------- slogger/grpcslog/grpcslog_test.go | 305 +++++---------------- slogger/grpcslog/options.go | 13 + 12 files changed, 315 insertions(+), 440 deletions(-) diff --git a/README.md b/README.md index 7184288..e561523 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ one of them does not pull anything else in. | [prommetrics](./prommetrics) | Registration of the Prometheus collectors and the context-aware creation of the metrics. | | [protomap](./protomap) | Deserialization of protobuf messages by their descriptor name. | | [slogger](./slogger) | A `log/slog` logger with a configurable format, a hot-reloadable level and context helpers. | +| [slogger/grpcslog](./slogger/grpcslog) | A `grpclog.LoggerV2` implementation that writes the gRPC messages to the logger. | | [strcase](./strcase) | Detection of the case of a string and conversion between the case styles. | | [sync](./sync) | A `sync.Once` that keeps retrying until the guarded function succeeds. | | [time](./time) | A duration type that is serialized as a human-readable string. | diff --git a/go.work b/go.work index 40f58de..dc76a88 100644 --- a/go.work +++ b/go.work @@ -8,6 +8,7 @@ use ( ./prommetrics ./protomap ./slogger + ./slogger/grpcslog ./strcase ./sync ./time diff --git a/go.work.sum b/go.work.sum index c989296..40dbcf3 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,14 +1,40 @@ +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.35.0 h1:bN1gA3of5bXtbnLsRPrwfmbbe7A5UWFlcTHseujLnpc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.35.0/go.mod h1:Yj5vHEz/aAepZGliRJsA6uvHAVAQyEwajq9ORCHPxzM= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= +github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= 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= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.39.1-0.20260819172001-e6e3fd93e4be h1:SWe0x6yfglnxuvOiYgTTnNq7QD/yvqthOh1RBI8Bj8w= +github.com/envoyproxy/go-control-plane/envoy v1.39.1-0.20260819172001-e6e3fd93e4be/go.mod h1:PYEOlng9XcrulfyWpm49jECTPV0LT4q8cO7fLW/xwgk= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -29,21 +55,49 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spiffe/go-spiffe/v2 v2.8.1 h1:eXZMLsu+3MLEPJyGJkolqtVrteZfQdUpOWj6LTiDl/E= +github.com/spiffe/go-spiffe/v2 v2.8.1/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/gcp v1.45.0 h1:9jR0ZPRok9ryaOQ2Wx8rg5F7Aon59mxrqbVI60/vlBk= +go.opentelemetry.io/contrib/detectors/gcp v1.45.0/go.mod h1:VSme3o2fvSg5bVg0dRzyHaj4Z5EVhG+g2Fde6LKzmQA= +go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= +go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= +go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= +go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= +go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= +go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= +go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= +go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= +go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= +go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98= golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260817212433-ac3dfec99bb1 h1:lrupDmKL3p5kEX1M92oan027eCKcouzjuPbH6YBK+Rs= +google.golang.org/genproto/googleapis/api v0.0.0-20260817212433-ac3dfec99bb1/go.mod h1:q/3oV3jAi5vwelxsVAprMBC8BcM2zmNe+IjRGd+9/ks= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1 h1:utmqiUzZAEINbLmRyAt1QjVkMyLQ+lvIwCpLaB1rZfw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= diff --git a/slogger/README.md b/slogger/README.md index 197b4fa..097dd13 100644 --- a/slogger/README.md +++ b/slogger/README.md @@ -10,6 +10,12 @@ The module is split into two packages: pass the logger and its attributes through the [context](https://pkg.go.dev/context). +There is also a separate module next to it: + +- [`github.com/nbgrp/pkg/slogger/grpcslog`](./grpcslog/) — the + [`grpclog.LoggerV2`](https://pkg.go.dev/google.golang.org/grpc/grpclog) + implementation that writes the gRPC messages to the logger. + ## Public API The `slogger` package provides: diff --git a/slogger/grpcslog/.golangci.yml b/slogger/grpcslog/.golangci.yml index d2ecab6..4955f26 100644 --- a/slogger/grpcslog/.golangci.yml +++ b/slogger/grpcslog/.golangci.yml @@ -80,6 +80,9 @@ linters: linters: [ errcheck ] - path: (.+)_test\.go linters: [ gocognit, goconst, mnd ] + - path: (.+)_test\.go + linters: [ govet ] + text: 'fieldalignment:' - text: 'assigned to ctx' linters: [ wastedassign ] - text: 'ctx is never used' diff --git a/slogger/grpcslog/README.md b/slogger/grpcslog/README.md index e9bf8ac..e7d41bd 100644 --- a/slogger/grpcslog/README.md +++ b/slogger/grpcslog/README.md @@ -7,47 +7,49 @@ The logger is installed as the global gRPC logger, so all the messages of gRPC go to the same place as the rest of the application logs: ```go -logger, err := grpcslog.NewGrpcLogger(ctx) +logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + +grpcLogger, err := grpcslog.New(logger) if err != nil { return err } -grpclog.SetLoggerV2(logger) +grpclog.SetLoggerV2(grpcLogger) ``` ## Public API ```go -func NewGrpcLogger(ctx context.Context) (*GrpcLogger, error) -func NewGrpcLoggerWithVerbosity(ctx context.Context, v int) (*GrpcLogger, error) +func New(logger *slog.Logger, opts ...Option) (*GrpcLogger, error) + +func WithVerbosity(v int) Option ``` -Both constructors return an error if the environment variables contain invalid -values. +`New` returns an error if the `GRPC_GO_LOG_VERBOSITY_LEVEL` environment variable +contains an invalid value. + +### WithVerbosity + +Sets the verbosity level of the logger: the threshold of the `V` method. ## Configuration -The logger is configured with the same environment variables as the default -gRPC logger: +The level and the format of the messages are set in the logger passed to `New`, +so only the verbosity level is configured by the package itself: -| Variable | Values | Default | -|-----------------------------|----------------------------|---------| -| `GRPC_GO_LOG_SEVERITY_LEVEL` | `INFO`, `WARNING`, `ERROR` | `ERROR` | -| `GRPC_GO_LOG_VERBOSITY_LEVEL` | a non-negative integer | `0` | -| `GRPC_GO_LOG_FORMATTER` | `json`, `text` | `text` | +| Variable | Values | Default | +|-----------------------------|-------------------------|---------| +| `GRPC_GO_LOG_VERBOSITY_LEVEL` | a non-negative integer | `0` | -- `GRPC_GO_LOG_SEVERITY_LEVEL` — the severity level of the logged messages. The - messages below the level are dropped. - `GRPC_GO_LOG_VERBOSITY_LEVEL` — the verbosity level of the logger. It sets the threshold of the `V` method, so the verbose messages of gRPC are logged only if their level is not greater than the verbosity level of the logger. - The variable takes precedence over the level passed to - `NewGrpcLoggerWithVerbosity`. -- `GRPC_GO_LOG_FORMATTER` — the format of the logged messages: `json` or - `text`. The value is case-insensitive. + The variable takes precedence over the level passed to `WithVerbosity`. -The messages are written to `os.Stdout`, like the messages of the -[`slogger`](../) package. +**`GRPC_GO_LOG_SEVERITY_LEVEL` and `GRPC_GO_LOG_FORMATTER` are not applied.** +They are read by the default gRPC logger only: the level and the format of the +records are defined by the logger, so the environment variables have no effect +on them. ## Verbosity @@ -56,24 +58,23 @@ messages are logged. The `V` method reports whether a message of the given verbose level should be logged: ```go -logger, err := grpcslog.NewGrpcLoggerWithVerbosity(ctx, 2) +grpcLogger, err := grpcslog.New(logger, grpcslog.WithVerbosity(2)) if err != nil { return err } -logger.V(2) // true -logger.V(3) // false +grpcLogger.V(2) // true +grpcLogger.V(3) // false ``` The verbose messages of gRPC are written with the info severity, so they are -only logged if the severity level allows the info records. +only logged if the level of the logger allows the info records. ## Errors -The constructors return an error if the severity level is unknown or if the -verbosity level is not a non-negative integer: +The constructor returns an error if the verbosity level is not a non-negative +integer: ``` -unknown gRPC log severity level "bogus" invalid gRPC log verbosity level "bogus": strconv.Atoi: parsing "bogus": invalid syntax ``` diff --git a/slogger/grpcslog/go.mod b/slogger/grpcslog/go.mod index b4e19f8..aeb6f51 100644 --- a/slogger/grpcslog/go.mod +++ b/slogger/grpcslog/go.mod @@ -2,4 +2,9 @@ module github.com/nbgrp/pkg/slogger/grpcslog go 1.26.0 -require google.golang.org/grpc v1.85.0 +require ( + github.com/stretchr/testify v1.12.1 + google.golang.org/grpc v1.85.0 +) + +require go.yaml.in/yaml/v3 v3.0.5 // indirect diff --git a/slogger/grpcslog/go.sum b/slogger/grpcslog/go.sum index c8ed087..4dc1ec4 100644 --- a/slogger/grpcslog/go.sum +++ b/slogger/grpcslog/go.sum @@ -1,2 +1,6 @@ +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= google.golang.org/grpc v1.85.0 h1:BPBCfuVRQ1qvBhKDOq9uvQllMvO59rKsYhXpp7Dd8vw= google.golang.org/grpc v1.85.0/go.mod h1:0rQrOD58yjal/0EaVbGBANindixsCZNQPDV3VKtaKuQ= diff --git a/slogger/grpcslog/grpcslog.go b/slogger/grpcslog/grpcslog.go index b661980..14c4b42 100644 --- a/slogger/grpcslog/grpcslog.go +++ b/slogger/grpcslog/grpcslog.go @@ -1,3 +1,130 @@ // SPDX-License-Identifier: BSD-3-Clause +// Package grpcslog provides a [grpclog.LoggerV2] implementation that writes +// the gRPC messages to a [log/slog.Logger]. package grpcslog + +import ( + "fmt" + "log/slog" + "os" + "strconv" + + "google.golang.org/grpc/grpclog" +) + +var _ grpclog.LoggerV2 = (*GrpcLogger)(nil) + +// exit terminates the process after a fatal message. It is a variable to make +// it possible to test the fatal methods. +var exit = os.Exit + +// GrpcLogger writes the gRPC messages to a [slog.Logger]. +type GrpcLogger struct { + l *slog.Logger + v int +} + +// New initializes and returns a [GrpcLogger] that writes the gRPC messages to +// the given logger. +// +// The GRPC_GO_LOG_VERBOSITY_LEVEL environment variable takes precedence over +// the verbosity level set with [WithVerbosity]. +// +// It returns an error if the environment variable contains an invalid value. +func New(logger *slog.Logger, opts ...Option) (*GrpcLogger, error) { + l := &GrpcLogger{l: logger} + + for _, opt := range opts { + opt(l) + } + + verbosity, err := verbosityLevel(l.v) + if err != nil { + return nil, err + } + l.v = verbosity + + return l, nil +} + +func (l *GrpcLogger) Info(args ...any) { + l.l.Info(fmt.Sprint(args...)) +} + +func (l *GrpcLogger) Infoln(args ...any) { + l.l.Info(fmt.Sprintln(args...)) +} + +func (l *GrpcLogger) Infof(format string, args ...any) { + l.l.Info(fmt.Sprintf(format, args...)) +} + +func (l *GrpcLogger) Warning(args ...any) { + l.l.Warn(fmt.Sprint(args...)) +} + +func (l *GrpcLogger) Warningln(args ...any) { + l.l.Warn(fmt.Sprintln(args...)) +} + +func (l *GrpcLogger) Warningf(format string, args ...any) { + l.l.Warn(fmt.Sprintf(format, args...)) +} + +func (l *GrpcLogger) Error(args ...any) { + l.l.Error(fmt.Sprint(args...)) +} + +func (l *GrpcLogger) Errorln(args ...any) { + l.l.Error(fmt.Sprintln(args...)) +} + +func (l *GrpcLogger) Errorf(format string, args ...any) { + l.l.Error(fmt.Sprintf(format, args...)) +} + +func (l *GrpcLogger) Fatal(args ...any) { + l.l.Error(fmt.Sprint(args...)) + exit(1) +} + +func (l *GrpcLogger) Fatalln(args ...any) { + l.l.Error(fmt.Sprintln(args...)) + exit(1) +} + +func (l *GrpcLogger) Fatalf(format string, args ...any) { + l.l.Error(fmt.Sprintf(format, args...)) + exit(1) +} + +// V reports whether the verbosity level is at least the requested verbose +// level, like the default gRPC logger does. +// +// The verbose messages of gRPC are written with the info severity, so they are +// only logged if the level of the logger allows the info records. +func (l *GrpcLogger) V(level int) bool { + return level <= l.v +} + +// verbosityLevel returns the verbosity level of the logger based on the +// GRPC_GO_LOG_VERBOSITY_LEVEL environment variable. The environment variable +// takes precedence over the given level. +func verbosityLevel(v int) (int, error) { + value := os.Getenv("GRPC_GO_LOG_VERBOSITY_LEVEL") + if value == "" { + return v, nil + } + + parsed, err := strconv.Atoi(value) + if err != nil { + return 0, fmt.Errorf("invalid gRPC log verbosity level %q: %w", value, err) + } + + if parsed < 0 { + return 0, fmt.Errorf("invalid gRPC log verbosity level %q: the level must not be negative", value) + } + + return parsed, nil +} diff --git a/slogger/grpcslog/grpcslog_internal_test.go b/slogger/grpcslog/grpcslog_internal_test.go index 1b84083..f473183 100644 --- a/slogger/grpcslog/grpcslog_internal_test.go +++ b/slogger/grpcslog/grpcslog_internal_test.go @@ -5,6 +5,7 @@ package grpcslog import ( "bytes" "encoding/json" + "log/slog" "strings" "testing" @@ -12,96 +13,6 @@ import ( "github.com/stretchr/testify/require" ) -// record is a logged message. -type record struct { - Level string `json:"level"` - Msg string `json:"msg"` -} - -// TestLogMethods checks that the messages are logged with the expected -// severity levels and contents. -func TestLogMethods(t *testing.T) { - tests := []struct { - name string - log func(l *GrpcLogger) - level string - msg string - }{ - { - name: "Info", - log: func(l *GrpcLogger) { l.Info("message") }, - level: "INFO", - msg: "message", - }, - { - name: "Infoln", - log: func(l *GrpcLogger) { l.Infoln("message") }, - level: "INFO", - msg: "message\n", - }, - { - name: "Infof", - log: func(l *GrpcLogger) { l.Infof("mes%s", "sage") }, - level: "INFO", - msg: "message", - }, - { - name: "Warning", - log: func(l *GrpcLogger) { l.Warning("message") }, - level: "WARN", - msg: "message", - }, - { - name: "Warningln", - log: func(l *GrpcLogger) { l.Warningln("message") }, - level: "WARN", - msg: "message\n", - }, - { - name: "Warningf", - log: func(l *GrpcLogger) { l.Warningf("mes%s", "sage") }, - level: "WARN", - msg: "message", - }, - { - name: "Error", - log: func(l *GrpcLogger) { l.Error("message") }, - level: "ERROR", - msg: "message", - }, - { - name: "Errorln", - log: func(l *GrpcLogger) { l.Errorln("message") }, - level: "ERROR", - msg: "message\n", - }, - { - name: "Errorf", - log: func(l *GrpcLogger) { l.Errorf("mes%s", "sage") }, - level: "ERROR", - msg: "message", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", "INFO") - t.Setenv("GRPC_GO_LOG_FORMATTER", "json") - - buf := &bytes.Buffer{} - l, err := newGrpcLoggerWithWriter(t.Context(), 0, buf) - require.NoError(t, err) - - tt.log(l) - - records := parseRecords(t, buf.String()) - require.Len(t, records, 1) - assert.Equal(t, tt.level, records[0].Level) - assert.Equal(t, tt.msg, records[0].Msg) - }) - } -} - // TestFatalMethods checks that the fatal methods log the message and exit the // process with the code 1. func TestFatalMethods(t *testing.T) { @@ -116,17 +27,14 @@ func TestFatalMethods(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", "ERROR") - t.Setenv("GRPC_GO_LOG_FORMATTER", "json") - - orig := exit - t.Cleanup(func() { exit = orig }) + original := exit + t.Cleanup(func() { exit = original }) var code int exit = func(c int) { code = c } buf := &bytes.Buffer{} - l, err := newGrpcLoggerWithWriter(t.Context(), 0, buf) + l, err := New(slog.New(slog.NewJSONHandler(buf, nil))) require.NoError(t, err) tt.log(l) @@ -141,79 +49,10 @@ func TestFatalMethods(t *testing.T) { } } -// TestSeverityLevel checks that the severity level is taken from the -// environment variable. -func TestSeverityLevel(t *testing.T) { - tests := []struct { - name string - value string - want []string - }{ - {name: "unset", value: "", want: []string{"ERROR"}}, - {name: "error", value: "ERROR", want: []string{"ERROR"}}, - {name: "warning", value: "WARNING", want: []string{"WARN", "ERROR"}}, - {name: "info", value: "INFO", want: []string{"INFO", "WARN", "ERROR"}}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", tt.value) - t.Setenv("GRPC_GO_LOG_FORMATTER", "json") - - buf := &bytes.Buffer{} - l, err := newGrpcLoggerWithWriter(t.Context(), 0, buf) - require.NoError(t, err) - - l.Info("info message") - l.Warning("warning message") - l.Error("error message") - - got := make([]string, 0, len(tt.want)) - for _, record := range parseRecords(t, buf.String()) { - got = append(got, record.Level) - } - - assert.Equal(t, tt.want, got) - }) - } -} - -// TestLogFormat checks that the format of the messages is taken from the -// environment variable. -func TestLogFormat(t *testing.T) { - tests := []struct { - name string - value string - want string - }{ - {name: "unset", value: "", want: "text"}, - {name: "text", value: "text", want: "text"}, - {name: "json", value: "json", want: "json"}, - {name: "json in upper case", value: "JSON", want: "json"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", "INFO") - t.Setenv("GRPC_GO_LOG_FORMATTER", tt.value) - - buf := &bytes.Buffer{} - l, err := newGrpcLoggerWithWriter(t.Context(), 0, buf) - require.NoError(t, err) - - l.Info("info message") - - switch tt.want { - case "json": - records := parseRecords(t, buf.String()) - require.Len(t, records, 1) - assert.Equal(t, "info message", records[0].Msg) - case "text": - assert.Contains(t, buf.String(), "level=INFO") - assert.Contains(t, buf.String(), "msg=\"info message\"") - } - }) - } +// record is a logged message. +type record struct { + Level string `json:"level"` + Msg string `json:"msg"` } // parseRecords parses the JSON messages written to the buffer. diff --git a/slogger/grpcslog/grpcslog_test.go b/slogger/grpcslog/grpcslog_test.go index 9f514ff..3f86095 100644 --- a/slogger/grpcslog/grpcslog_test.go +++ b/slogger/grpcslog/grpcslog_test.go @@ -5,19 +5,46 @@ package grpcslog_test import ( "bytes" "encoding/json" - "io" - "os" - "os/exec" + "log/slog" "strings" + "sync" "testing" - "github.com/nbgrp/pkg/slogger/grpcslog" + . "github.com/nbgrp/pkg/slogger/grpcslog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// fatalMethodEnvVar tells the helper test which fatal method to call. -const fatalMethodEnvVar = "GRPCSLOG_TEST_FATAL_METHOD" +// record is a logged message. +type record struct { + Level string `json:"level"` + Msg string `json:"msg"` +} + +// safeBuffer is a buffer that is safe for concurrent use. +type safeBuffer struct { + buf bytes.Buffer + mu sync.Mutex +} + +func (b *safeBuffer) Write(p []byte) (int, error) { + b.mu.Lock() + defer b.mu.Unlock() + + return b.buf.Write(p) +} + +func (b *safeBuffer) String() string { + b.mu.Lock() + defer b.mu.Unlock() + + return b.buf.String() +} + +// newLogger returns a logger that writes the JSON records to the given buffer. +func newLogger(buf *safeBuffer) *slog.Logger { + return slog.New(slog.NewJSONHandler(buf, nil)) +} // TestV checks the verbosity semantics of the logger. // @@ -47,7 +74,7 @@ func TestV(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Setenv("GRPC_GO_LOG_VERBOSITY_LEVEL", "") - l, err := grpcslog.NewGrpcLoggerWithVerbosity(t.Context(), tt.verbosity) + l, err := New(newLogger(&safeBuffer{}), WithVerbosity(tt.verbosity)) require.NoError(t, err) assert.Equal(t, tt.want, l.V(tt.level)) @@ -55,29 +82,12 @@ func TestV(t *testing.T) { } } -// TestV_EnabledLevels checks that the enabled verbose levels form a prefix: -// if a level is enabled, all the lower levels are enabled too. -func TestV_EnabledLevels(t *testing.T) { - for verbosity := range 5 { - t.Setenv("GRPC_GO_LOG_VERBOSITY_LEVEL", "") - - l, err := grpcslog.NewGrpcLoggerWithVerbosity(t.Context(), verbosity) - require.NoError(t, err) - - for level := range 5 { - want := level <= verbosity - - assert.Equalf(t, want, l.V(level), "verbosity %d, level %d", verbosity, level) - } - } -} - -// TestNewGrpcLogger_Verbosity checks that the verbosity level is taken from -// the environment variable. -func TestNewGrpcLogger_Verbosity(t *testing.T) { +// TestNew_Verbosity checks that the verbosity level is taken from the +// environment variable. +func TestNew_Verbosity(t *testing.T) { t.Setenv("GRPC_GO_LOG_VERBOSITY_LEVEL", "2") - l, err := grpcslog.NewGrpcLogger(t.Context()) + l, err := New(newLogger(&safeBuffer{})) require.NoError(t, err) assert.True(t, l.V(0)) @@ -86,132 +96,33 @@ func TestNewGrpcLogger_Verbosity(t *testing.T) { assert.False(t, l.V(3)) } -// TestNewGrpcLoggerWithVerbosity_EnvPrecedence checks that the environment -// variable takes precedence over the verbosity level passed to the -// constructor. -func TestNewGrpcLoggerWithVerbosity_EnvPrecedence(t *testing.T) { +// TestNew_VerbosityEnvPrecedence checks that the environment variable takes +// precedence over the verbosity level passed to the constructor. +func TestNew_VerbosityEnvPrecedence(t *testing.T) { t.Setenv("GRPC_GO_LOG_VERBOSITY_LEVEL", "3") - l, err := grpcslog.NewGrpcLoggerWithVerbosity(t.Context(), 1) + l, err := New(newLogger(&safeBuffer{}), WithVerbosity(1)) require.NoError(t, err) assert.True(t, l.V(3)) assert.False(t, l.V(4)) } -// TestNewGrpcLoggerWithVerbosity_EnvUnset checks that the verbosity level -// passed to the constructor is used if the environment variable is not set. -func TestNewGrpcLoggerWithVerbosity_EnvUnset(t *testing.T) { - t.Setenv("GRPC_GO_LOG_VERBOSITY_LEVEL", "") - - l, err := grpcslog.NewGrpcLoggerWithVerbosity(t.Context(), 2) - require.NoError(t, err) - - assert.True(t, l.V(2)) - assert.False(t, l.V(3)) -} - -// TestNewGrpcLogger_SeverityLevel checks that the severity level is taken -// from the environment variable. -func TestNewGrpcLogger_SeverityLevel(t *testing.T) { - tests := []struct { - name string - value string - want []string - }{ - {name: "unset", value: "", want: []string{"ERROR"}}, - {name: "error", value: "ERROR", want: []string{"ERROR"}}, - {name: "warning", value: "WARNING", want: []string{"WARN", "ERROR"}}, - {name: "info", value: "INFO", want: []string{"INFO", "WARN", "ERROR"}}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", tt.value) - t.Setenv("GRPC_GO_LOG_FORMATTER", "json") - - out := captureStderr(t, func() { - l, err := grpcslog.NewGrpcLogger(t.Context()) - require.NoError(t, err) - - l.Info("info message") - l.Warning("warning message") - l.Error("error message") - }) - - var got []string - for _, record := range parseRecords(t, out) { - got = append(got, record.Level) - } - - assert.Equal(t, tt.want, got) - }) - } -} - -// TestNewGrpcLogger_Format checks that the format of the messages is taken -// from the environment variable. -func TestNewGrpcLogger_Format(t *testing.T) { - tests := []struct { - name string - value string - want string - }{ - {name: "unset", value: "", want: "text"}, - {name: "text", value: "text", want: "text"}, - {name: "json", value: "json", want: "json"}, - {name: "json in upper case", value: "JSON", want: "json"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", "INFO") - t.Setenv("GRPC_GO_LOG_FORMATTER", tt.value) - - out := captureStderr(t, func() { - l, err := grpcslog.NewGrpcLogger(t.Context()) - require.NoError(t, err) - - l.Info("info message") - }) - - switch tt.want { - case "json": - records := parseRecords(t, out) - require.Len(t, records, 1) - assert.Equal(t, "info message", records[0].Msg) - case "text": - assert.Contains(t, out, "level=INFO") - assert.Contains(t, out, "msg=\"info message\"") - } - }) - } -} - -// TestNewGrpcLogger_InvalidEnv checks that the invalid values of the -// environment variables are reported. -func TestNewGrpcLogger_InvalidEnv(t *testing.T) { +// TestNew_InvalidEnv checks that the invalid values of the environment +// variables are reported. +func TestNew_InvalidEnv(t *testing.T) { tests := []struct { name string - key string value string want string }{ - { - name: "unknown severity level", - key: "GRPC_GO_LOG_SEVERITY_LEVEL", - value: "bogus", - want: `unknown gRPC log severity level "bogus"`, - }, { name: "invalid verbosity level", - key: "GRPC_GO_LOG_VERBOSITY_LEVEL", value: "bogus", want: `invalid gRPC log verbosity level "bogus"`, }, { name: "negative verbosity level", - key: "GRPC_GO_LOG_VERBOSITY_LEVEL", value: "-1", want: `invalid gRPC log verbosity level "-1"`, }, @@ -219,11 +130,9 @@ func TestNewGrpcLogger_InvalidEnv(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", "") - t.Setenv("GRPC_GO_LOG_VERBOSITY_LEVEL", "") - t.Setenv(tt.key, tt.value) + t.Setenv("GRPC_GO_LOG_VERBOSITY_LEVEL", tt.value) - _, err := grpcslog.NewGrpcLogger(t.Context()) + _, err := New(newLogger(&safeBuffer{})) require.ErrorContains(t, err, tt.want) }) } @@ -234,61 +143,61 @@ func TestNewGrpcLogger_InvalidEnv(t *testing.T) { func TestLogMethods(t *testing.T) { tests := []struct { name string - log func(l *grpcslog.GrpcLogger) + log func(l *GrpcLogger) level string msg string }{ { name: "Info", - log: func(l *grpcslog.GrpcLogger) { l.Info("message") }, + log: func(l *GrpcLogger) { l.Info("message") }, level: "INFO", msg: "message", }, { name: "Infoln", - log: func(l *grpcslog.GrpcLogger) { l.Infoln("message") }, + log: func(l *GrpcLogger) { l.Infoln("message") }, level: "INFO", msg: "message\n", }, { name: "Infof", - log: func(l *grpcslog.GrpcLogger) { l.Infof("mes%s", "sage") }, + log: func(l *GrpcLogger) { l.Infof("mes%s", "sage") }, level: "INFO", msg: "message", }, { name: "Warning", - log: func(l *grpcslog.GrpcLogger) { l.Warning("message") }, + log: func(l *GrpcLogger) { l.Warning("message") }, level: "WARN", msg: "message", }, { name: "Warningln", - log: func(l *grpcslog.GrpcLogger) { l.Warningln("message") }, + log: func(l *GrpcLogger) { l.Warningln("message") }, level: "WARN", msg: "message\n", }, { name: "Warningf", - log: func(l *grpcslog.GrpcLogger) { l.Warningf("mes%s", "sage") }, + log: func(l *GrpcLogger) { l.Warningf("mes%s", "sage") }, level: "WARN", msg: "message", }, { name: "Error", - log: func(l *grpcslog.GrpcLogger) { l.Error("message") }, + log: func(l *GrpcLogger) { l.Error("message") }, level: "ERROR", msg: "message", }, { name: "Errorln", - log: func(l *grpcslog.GrpcLogger) { l.Errorln("message") }, + log: func(l *GrpcLogger) { l.Errorln("message") }, level: "ERROR", msg: "message\n", }, { name: "Errorf", - log: func(l *grpcslog.GrpcLogger) { l.Errorf("mes%s", "sage") }, + log: func(l *GrpcLogger) { l.Errorf("mes%s", "sage") }, level: "ERROR", msg: "message", }, @@ -296,17 +205,13 @@ func TestLogMethods(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Setenv("GRPC_GO_LOG_SEVERITY_LEVEL", "INFO") - t.Setenv("GRPC_GO_LOG_FORMATTER", "json") - - out := captureStderr(t, func() { - l, err := grpcslog.NewGrpcLogger(t.Context()) - require.NoError(t, err) + buf := &safeBuffer{} + l, err := New(newLogger(buf)) + require.NoError(t, err) - tt.log(l) - }) + tt.log(l) - records := parseRecords(t, out) + records := parseRecords(t, buf.String()) require.Len(t, records, 1) assert.Equal(t, tt.level, records[0].Level) assert.Equal(t, tt.msg, records[0].Msg) @@ -314,68 +219,7 @@ func TestLogMethods(t *testing.T) { } } -// TestFatalMethods checks that the fatal methods log the message and exit the -// process with the code 1. -// -// The methods are called in a subprocess, because they exit the process. -func TestFatalMethods(t *testing.T) { - tests := []struct { - name string - method string - }{ - {name: "Fatal", method: "Fatal"}, - {name: "Fatalln", method: "Fatalln"}, - {name: "Fatalf", method: "Fatalf"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var stderr bytes.Buffer - - cmd := exec.Command(os.Args[0], "-test.run=^TestFatalHelper$") - cmd.Env = append(os.Environ(), fatalMethodEnvVar+"="+tt.method) - cmd.Stderr = &stderr - - err := cmd.Run() - - var exitErr *exec.ExitError - require.ErrorAs(t, err, &exitErr) - assert.Equal(t, 1, exitErr.ExitCode()) - assert.Contains(t, stderr.String(), "fatal message") - }) - } -} - -// TestFatalHelper calls the fatal method selected by the environment -// variable. It is run in a subprocess by TestFatalMethods. -func TestFatalHelper(t *testing.T) { - method := os.Getenv(fatalMethodEnvVar) - if method == "" { - t.Skip("not a helper process") - } - - l, err := grpcslog.NewGrpcLogger(t.Context()) - require.NoError(t, err) - - switch method { - case "Fatal": - l.Fatal("fatal message") - case "Fatalln": - l.Fatalln("fatal message") - case "Fatalf": - l.Fatalf("fatal %s", "message") - default: - t.Fatalf("unknown method %q", method) - } -} - -// record is a logged message. -type record struct { - Level string `json:"level"` - Msg string `json:"msg"` -} - -// parseRecords parses the JSON messages logged to the standard error output. +// parseRecords parses the JSON messages written to the buffer. func parseRecords(t *testing.T, out string) []record { t.Helper() @@ -394,26 +238,3 @@ func parseRecords(t *testing.T, out string) []record { return records } - -// captureStderr redirects the standard error output to a pipe for the -// duration of the function and returns the captured output. -func captureStderr(t *testing.T, f func()) string { - t.Helper() - - r, w, err := os.Pipe() - require.NoError(t, err) - - orig := os.Stderr - os.Stderr = w - defer func() { os.Stderr = orig }() - - f() - - require.NoError(t, w.Close()) - - out, err := io.ReadAll(r) - require.NoError(t, err) - require.NoError(t, r.Close()) - - return string(out) -} diff --git a/slogger/grpcslog/options.go b/slogger/grpcslog/options.go index b661980..553c895 100644 --- a/slogger/grpcslog/options.go +++ b/slogger/grpcslog/options.go @@ -1,3 +1,16 @@ // SPDX-License-Identifier: BSD-3-Clause package grpcslog + +type Option func(*GrpcLogger) + +// WithVerbosity sets the verbosity level of the logger: the threshold of the +// [GrpcLogger.V] method. +// +// The GRPC_GO_LOG_VERBOSITY_LEVEL environment variable takes precedence over +// the given level. +func WithVerbosity(v int) Option { + return func(l *GrpcLogger) { + l.v = v + } +}