diff --git a/README.md b/README.md index 1af1a5b..7184288 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ one of them does not pull anything else in. | [ctxkey](./ctxkey) | Unique, pointer-backed context keys. | | [deferred](./deferred) | Helpers for the `defer` calls. | | [dispatcher](./dispatcher) | Routing of keyed events to handlers, with priorities, propagation control and wildcards. | +| [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. | | [strcase](./strcase) | Detection of the case of a string and conversion between the case styles. | diff --git a/go.work b/go.work index 6d8ef79..40f58de 100644 --- a/go.work +++ b/go.work @@ -5,6 +5,7 @@ use ( ./ctxkey ./deferred ./dispatcher + ./prommetrics ./protomap ./slogger ./strcase diff --git a/go.work.sum b/go.work.sum index 70a84d4..c989296 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,14 +1,49 @@ +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/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/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/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +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= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +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/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.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= -github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +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= +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/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/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= 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= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/prommetrics/.golangci.yml b/prommetrics/.golangci.yml new file mode 100644 index 0000000..7f28cfa --- /dev/null +++ b/prommetrics/.golangci.yml @@ -0,0 +1,342 @@ +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. + - 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 + + 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. + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/prommetrics/README.md b/prommetrics/README.md new file mode 100644 index 0000000..6feca54 --- /dev/null +++ b/prommetrics/README.md @@ -0,0 +1,147 @@ +# prommetrics + +Registration of the Prometheus collectors that describe the Go runtime and the +process, and the creation of the application metrics that follow the namespace +and the registry of the [context](https://pkg.go.dev/context). + +The module is split into two packages: + +- [`github.com/nbgrp/pkg/prommetrics`](./go_collectors.go) — the registration of + the Go runtime and the process collectors. +- [`github.com/nbgrp/pkg/prommetrics/ctxprom`](./ctxprom/ctxprom.go) — the + creation of the metrics that take the registry and the namespace from the + context. + +## Public API + +### The `prommetrics` package + +```go +type GoCollectorsOption func(*flags) + +func WithoutProcess() GoCollectorsOption +func WithoutCPU() GoCollectorsOption +func WithoutGC() GoCollectorsOption +func WithoutMemory() GoCollectorsOption +func WithoutScheduler() GoCollectorsOption +func WithoutSync() GoCollectorsOption + +func WithMemStats() GoCollectorsOption + +func MustRegisterGoCollectors(r prometheus.Registerer, opts ...GoCollectorsOption) +``` + +`MustRegisterGoCollectors` registers the Go collector and the process collector +in the given registry. The options turn the individual groups of the metrics +off: + +| Option | Metrics that are removed | +|------------------|--------------------------------------------------------------| +| `WithoutProcess` | the `process_*` metrics | +| `WithoutCPU` | the `go_cpu_classes_*` metrics | +| `WithoutGC` | the `go_gc_*` metrics | +| `WithoutMemory` | the `go_memory_classes_*` metrics | +| `WithoutScheduler` | the `go_sched_*` metrics | +| `WithoutSync` | the `go_sync_*` metrics | + +The runtime metrics come from the +[`runtime/metrics`](https://pkg.go.dev/runtime/metrics) package, so their exact +set depends on the Go version. + +`WithMemStats` is the only option that turns the metrics **on**: it keeps the +legacy `go_memstats_*` family, which is based on `runtime.MemStats`. Without +it the family is disabled, since the runtime metrics cover the same ground. + +```go +prommetrics.MustRegisterGoCollectors(registry) +// 79 metric families: the runtime and the process metrics. + +prommetrics.MustRegisterGoCollectors(registry, prommetrics.WithMemStats()) +// 100 metric families: the same plus the legacy go_memstats_* ones. +``` + +The options are independent of each other, so they can be combined freely. + +### The `ctxprom` package + +```go +func ToContext(ctx context.Context, r prometheus.Registerer) context.Context +func FromContext(ctx context.Context) prometheus.Registerer +func WithNamespace(ctx context.Context, namespace string) context.Context + +func NewCounterFor(ctx context.Context, opts prometheus.CounterOpts) prometheus.Counter +func NewCounterVecFor(ctx context.Context, opts prometheus.CounterOpts, labelValues []string) *prometheus.CounterVec +func NewGaugeFor(ctx context.Context, opts prometheus.GaugeOpts) prometheus.Gauge +func NewGaugeVecFor(ctx context.Context, opts prometheus.GaugeOpts, labelValues []string) *prometheus.GaugeVec +func NewSummaryFor(ctx context.Context, opts prometheus.SummaryOpts) prometheus.Summary +func NewSummaryVecFor(ctx context.Context, opts prometheus.SummaryOpts, labelValues []string) *prometheus.SummaryVec +func NewHistogramFor(ctx context.Context, opts prometheus.HistogramOpts) prometheus.Histogram +func NewHistogramVecFor(ctx context.Context, opts prometheus.HistogramOpts, labelValues []string) *prometheus.HistogramVec + +func MustRegisterGoCollectors(ctx context.Context, opts ...prommetrics.GoCollectorsOption) +``` + +The package keeps the registry and the namespace of the application in the +context, so the code that creates the metrics does not have to pass them +around: + +- `ToContext` puts the registry into the context, `FromContext` takes it from + there. If the context has no registry, `prometheus.DefaultRegisterer` is + used. +- `WithNamespace` puts the namespace into the context. The namespace of the + options takes priority over it: it is applied only when the options leave it + empty. +- The `New*For` functions create the metric and register it in the registry of + the context. They are the [`promauto`](https://pkg.go.dev/github.com/prometheus/client_golang/prometheus/promauto) + constructors, so they panic if the registration fails. +- `MustRegisterGoCollectors` registers the collectors of the + [`prommetrics`](#the-prommetrics-package) package in the registry of the + context. + +## Usage + +```go +ctx := ctxprom.ToContext(ctx, registry) +ctx = ctxprom.WithNamespace(ctx, "myapp") + +ctxprom.MustRegisterGoCollectors(ctx) + +requests := ctxprom.NewCounterVecFor(ctx, prometheus.CounterOpts{ + Name: "requests_total", + Help: "Total number of requests.", +}, []string{"code"}) + +requests.WithLabelValues("200").Inc() +``` + +The counter is registered as `myapp_requests_total`, in the registry of the +context. + +## Errors + +`MustRegisterGoCollectors` panics if the collectors cannot be registered. The +default registerer already contains them, since the `prometheus` package +registers them on its own, so the registry must be put into the context before +the call: + +```go +ctx := ctxprom.ToContext(ctx, prometheus.NewRegistry()) + +ctxprom.MustRegisterGoCollectors(ctx) // ok +``` + +The same applies to the `prommetrics` package: the default registerer must not +be passed to `MustRegisterGoCollectors` directly. + +The metric constructors panic if the metric cannot be registered, e.g. when a +metric with the same name is already registered. + +## Concurrency + +The registry and the namespace of the context are read-only: the context is +never modified after it is created, and the metrics are safe for concurrent +use, as any Prometheus metric is. + +`MustRegisterGoCollectors` is not safe for concurrent use with the same +registry: the collectors are registered with `MustRegister`, so a second call +with the same registry panics. diff --git a/prommetrics/ctxprom/ctxprom.go b/prommetrics/ctxprom/ctxprom.go new file mode 100644 index 0000000..ff8c304 --- /dev/null +++ b/prommetrics/ctxprom/ctxprom.go @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package ctxprom + +import ( + "context" + + "github.com/nbgrp/pkg/prommetrics" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +type registererKey struct{} +type namespaceKey struct{} + +func ToContext(ctx context.Context, r prometheus.Registerer) context.Context { + return context.WithValue(ctx, registererKey{}, r) +} + +func FromContext(ctx context.Context) prometheus.Registerer { + if r, ok := ctx.Value(registererKey{}).(prometheus.Registerer); ok { + return r + } + return prometheus.DefaultRegisterer +} + +func WithNamespace(ctx context.Context, namespace string) context.Context { + return context.WithValue(ctx, namespaceKey{}, namespace) +} + +func NewCounterFor(ctx context.Context, opts prometheus.CounterOpts) prometheus.Counter { + if opts.Namespace == "" { + opts.Namespace, _ = ctx.Value(namespaceKey{}).(string) + } + return promauto.With(FromContext(ctx)).NewCounter(opts) +} + +func NewCounterVecFor(ctx context.Context, opts prometheus.CounterOpts, labelValues []string) *prometheus.CounterVec { + if opts.Namespace == "" { + opts.Namespace, _ = ctx.Value(namespaceKey{}).(string) + } + return promauto.With(FromContext(ctx)).NewCounterVec(opts, labelValues) +} + +func NewGaugeFor(ctx context.Context, opts prometheus.GaugeOpts) prometheus.Gauge { + if opts.Namespace == "" { + opts.Namespace, _ = ctx.Value(namespaceKey{}).(string) + } + return promauto.With(FromContext(ctx)).NewGauge(opts) +} + +func NewGaugeVecFor(ctx context.Context, opts prometheus.GaugeOpts, labelValues []string) *prometheus.GaugeVec { + if opts.Namespace == "" { + opts.Namespace, _ = ctx.Value(namespaceKey{}).(string) + } + return promauto.With(FromContext(ctx)).NewGaugeVec(opts, labelValues) +} + +func NewSummaryFor(ctx context.Context, opts prometheus.SummaryOpts) prometheus.Summary { + if opts.Namespace == "" { + opts.Namespace, _ = ctx.Value(namespaceKey{}).(string) + } + return promauto.With(FromContext(ctx)).NewSummary(opts) +} + +func NewSummaryVecFor(ctx context.Context, opts prometheus.SummaryOpts, labelValues []string) *prometheus.SummaryVec { + if opts.Namespace == "" { + opts.Namespace, _ = ctx.Value(namespaceKey{}).(string) + } + return promauto.With(FromContext(ctx)).NewSummaryVec(opts, labelValues) +} + +func NewHistogramFor(ctx context.Context, opts prometheus.HistogramOpts) prometheus.Histogram { + if opts.Namespace == "" { + opts.Namespace, _ = ctx.Value(namespaceKey{}).(string) + } + return promauto.With(FromContext(ctx)).NewHistogram(opts) +} + +func NewHistogramVecFor(ctx context.Context, opts prometheus.HistogramOpts, labelValues []string) *prometheus.HistogramVec { + if opts.Namespace == "" { + opts.Namespace, _ = ctx.Value(namespaceKey{}).(string) + } + return promauto.With(FromContext(ctx)).NewHistogramVec(opts, labelValues) +} + +// MustRegisterGoCollectors registers the Go runtime and the process collectors +// in the registerer of the context. +// +// It panics if the collectors cannot be registered. The default registerer +// already contains them, since the prometheus package registers them on its +// own, so the registerer must be put into the context before the call. +func MustRegisterGoCollectors(ctx context.Context, opts ...prommetrics.GoCollectorsOption) { + prommetrics.MustRegisterGoCollectors(FromContext(ctx), opts...) +} diff --git a/prommetrics/ctxprom/ctxprom_test.go b/prommetrics/ctxprom/ctxprom_test.go new file mode 100644 index 0000000..66d012e --- /dev/null +++ b/prommetrics/ctxprom/ctxprom_test.go @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package ctxprom_test + +import ( + "context" + "testing" + + "github.com/nbgrp/pkg/prommetrics" + . "github.com/nbgrp/pkg/prommetrics/ctxprom" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// gatherFamilies collects the metric families of the given gatherer and returns +// them as a map of a family name to the "name=value" pairs of its labels. +func gatherFamilies(t *testing.T, g prometheus.Gatherer) map[string][]string { + t.Helper() + + mfs, err := g.Gather() + require.NoError(t, err) + + families := make(map[string][]string, len(mfs)) + for _, mf := range mfs { + var labels []string + for _, m := range mf.GetMetric() { + for _, l := range m.GetLabel() { + labels = append(labels, l.GetName()+"="+l.GetValue()) + } + } + families[mf.GetName()] = labels + } + + return families +} + +func TestToContext(t *testing.T) { + t.Parallel() + + reg := prometheus.NewRegistry() + ctx := ToContext(t.Context(), reg) + + assert.Same(t, reg, FromContext(ctx)) +} + +func TestFromContext_Default(t *testing.T) { + t.Parallel() + + assert.Same(t, prometheus.DefaultRegisterer, FromContext(t.Context())) +} + +func TestFromContext_NilRegisterer(t *testing.T) { + t.Parallel() + + ctx := ToContext(t.Context(), nil) + + assert.Same(t, prometheus.DefaultRegisterer, FromContext(ctx)) +} + +func TestToContext_KeepsParentContext(t *testing.T) { + t.Parallel() + + parent := t.Context() + ctx := ToContext(parent, prometheus.NewRegistry()) + + assert.Same(t, prometheus.DefaultRegisterer, FromContext(parent)) + assert.NotSame(t, prometheus.DefaultRegisterer, FromContext(ctx)) +} + +func TestWithNamespace(t *testing.T) { + t.Parallel() + + ctx := WithNamespace(t.Context(), "app") + + // The namespace is only visible through the metrics created with the context. + reg := prometheus.NewRegistry() + NewCounterFor(ToContext(ctx, reg), prometheus.CounterOpts{Name: "requests_total", Help: "Total requests."}) + + assert.Contains(t, gatherFamilies(t, reg), "app_requests_total") +} + +func TestWithNamespace_Overridden(t *testing.T) { + t.Parallel() + + ctx := WithNamespace(WithNamespace(t.Context(), "app"), "worker") + + reg := prometheus.NewRegistry() + NewCounterFor(ToContext(ctx, reg), prometheus.CounterOpts{Name: "requests_total", Help: "Total requests."}) + + assert.Contains(t, gatherFamilies(t, reg), "worker_requests_total") +} + +// TestNewMetrics checks that every metric constructor takes the namespace from +// the context, gives the priority to the namespace of the options and registers +// the metric in the registerer of the context. +func TestNewMetrics(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + // family is the name of the metric family without a namespace. + family string + // create creates the metric with the given namespace in its options. + create func(ctx context.Context, namespace string) prometheus.Collector + // labels are the expected labels of the metric. + labels []string + }{ + { + name: "counter", + family: "requests_total", + create: func(ctx context.Context, namespace string) prometheus.Collector { + return NewCounterFor(ctx, prometheus.CounterOpts{ + Namespace: namespace, + Name: "requests_total", + Help: "Total requests.", + }) + }, + }, + { + name: "counter_vec", + family: "requests_by_code_total", + labels: []string{"code=200"}, + create: func(ctx context.Context, namespace string) prometheus.Collector { + vec := NewCounterVecFor(ctx, prometheus.CounterOpts{ + Namespace: namespace, + Name: "requests_by_code_total", + Help: "Total requests by code.", + }, []string{"code"}) + vec.WithLabelValues("200") + + return vec + }, + }, + { + name: "gauge", + family: "workers", + create: func(ctx context.Context, namespace string) prometheus.Collector { + return NewGaugeFor(ctx, prometheus.GaugeOpts{ + Namespace: namespace, + Name: "workers", + Help: "Number of workers.", + }) + }, + }, + { + name: "gauge_vec", + family: "workers_by_queue", + labels: []string{"queue=main"}, + create: func(ctx context.Context, namespace string) prometheus.Collector { + vec := NewGaugeVecFor(ctx, prometheus.GaugeOpts{ + Namespace: namespace, + Name: "workers_by_queue", + Help: "Number of workers by queue.", + }, []string{"queue"}) + vec.WithLabelValues("main") + + return vec + }, + }, + { + name: "summary", + family: "request_duration_seconds", + create: func(ctx context.Context, namespace string) prometheus.Collector { + return NewSummaryFor(ctx, prometheus.SummaryOpts{ + Namespace: namespace, + Name: "request_duration_seconds", + Help: "Request duration.", + }) + }, + }, + { + name: "summary_vec", + family: "request_duration_by_code_seconds", + labels: []string{"code=200"}, + create: func(ctx context.Context, namespace string) prometheus.Collector { + vec := NewSummaryVecFor(ctx, prometheus.SummaryOpts{ + Namespace: namespace, + Name: "request_duration_by_code_seconds", + Help: "Request duration by code.", + }, []string{"code"}) + vec.WithLabelValues("200") + + return vec + }, + }, + { + name: "histogram", + family: "request_size_bytes", + create: func(ctx context.Context, namespace string) prometheus.Collector { + return NewHistogramFor(ctx, prometheus.HistogramOpts{ + Namespace: namespace, + Name: "request_size_bytes", + Help: "Request size.", + }) + }, + }, + { + name: "histogram_vec", + family: "request_size_by_code_bytes", + labels: []string{"code=200"}, + create: func(ctx context.Context, namespace string) prometheus.Collector { + vec := NewHistogramVecFor(ctx, prometheus.HistogramOpts{ + Namespace: namespace, + Name: "request_size_by_code_bytes", + Help: "Request size by code.", + }, []string{"code"}) + vec.WithLabelValues("200") + + return vec + }, + }, + } + + scenarios := []struct { + name string + // ctxNamespace is the namespace stored in the context. + ctxNamespace string + // optsNamespace is the namespace set in the options of the metric. + optsNamespace string + // wantPrefix is the expected prefix of the name of the metric family. + wantPrefix string + }{ + { + name: "namespace from context", + ctxNamespace: "app", + wantPrefix: "app_", + }, + { + name: "namespace from options", + ctxNamespace: "app", + optsNamespace: "worker", + wantPrefix: "worker_", + }, + { + name: "without namespace", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + for _, sc := range scenarios { + t.Run(sc.name, func(t *testing.T) { + t.Parallel() + + reg := prometheus.NewRegistry() + ctx := ToContext(t.Context(), reg) + if sc.ctxNamespace != "" { + ctx = WithNamespace(ctx, sc.ctxNamespace) + } + + tt.create(ctx, sc.optsNamespace) + + families := gatherFamilies(t, reg) + want := sc.wantPrefix + tt.family + require.Contains(t, families, want) + assert.ElementsMatch(t, tt.labels, families[want]) + + // The metric is registered in the registerer of the context only. + assert.NotContains(t, gatherFamilies(t, prometheus.DefaultGatherer), want) + }) + } + }) + } +} + +func TestMustRegisterGoCollectors(t *testing.T) { + t.Parallel() + + reg := prometheus.NewRegistry() + MustRegisterGoCollectors(ToContext(t.Context(), reg)) + + families := gatherFamilies(t, reg) + assert.Contains(t, families, "go_goroutines") + assert.Contains(t, families, "process_cpu_seconds_total") +} + +func TestMustRegisterGoCollectors_Options(t *testing.T) { + t.Parallel() + + reg := prometheus.NewRegistry() + MustRegisterGoCollectors(ToContext(t.Context(), reg), prommetrics.WithoutProcess()) + + families := gatherFamilies(t, reg) + assert.Contains(t, families, "go_goroutines") + assert.NotContains(t, families, "process_cpu_seconds_total") +} + +// TestMustRegisterGoCollectors_DefaultRegisterer checks that the collectors are +// registered in the default registerer, which already contains them since the +// prometheus package registers them on its own. +func TestMustRegisterGoCollectors_DefaultRegisterer(t *testing.T) { + t.Parallel() + + assert.Panics(t, func() { + MustRegisterGoCollectors(t.Context()) + }) +} diff --git a/prommetrics/go.mod b/prommetrics/go.mod new file mode 100644 index 0000000..914ee90 --- /dev/null +++ b/prommetrics/go.mod @@ -0,0 +1,20 @@ +module github.com/nbgrp/pkg/prommetrics + +go 1.26.0 + +require ( + github.com/prometheus/client_golang v1.25.0 + github.com/stretchr/testify v1.12.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.3 // indirect + github.com/prometheus/common v0.72.0 // indirect + github.com/prometheus/procfs v0.22.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/sys v0.48.0 // indirect + google.golang.org/protobuf v1.36.12 // indirect +) diff --git a/prommetrics/go.sum b/prommetrics/go.sum new file mode 100644 index 0000000..878246a --- /dev/null +++ b/prommetrics/go.sum @@ -0,0 +1,28 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKqbYtNOXuiK0= +github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk= +github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= +github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= +github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= +github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= +github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics= +github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +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= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/prommetrics/go_collectors.go b/prommetrics/go_collectors.go new file mode 100644 index 0000000..7d73a3e --- /dev/null +++ b/prommetrics/go_collectors.go @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package prommetrics + +import ( + "regexp" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" +) + +const ( + withoutProcess uint = 1 << iota + withoutCPU + withoutGC + withoutMemory + withoutScheduler + withoutSync + withMemStats +) + +type flags uint + +func (ff flags) noFlag(f uint) bool { + return uint(ff)&f == 0 +} + +type GoCollectorsOption func(*flags) + +func WithoutProcess() GoCollectorsOption { + return func(o *flags) { + *o = flags(uint(*o) | withoutProcess) + } +} +func WithoutCPU() GoCollectorsOption { + return func(o *flags) { + *o = flags(uint(*o) | withoutCPU) + } +} +func WithoutGC() GoCollectorsOption { + return func(o *flags) { + *o = flags(uint(*o) | withoutGC) + } +} +func WithoutMemory() GoCollectorsOption { + return func(o *flags) { + *o = flags(uint(*o) | withoutMemory) + } +} +func WithoutScheduler() GoCollectorsOption { + return func(o *flags) { + *o = flags(uint(*o) | withoutScheduler) + } +} +func WithoutSync() GoCollectorsOption { + return func(o *flags) { + *o = flags(uint(*o) | withoutSync) + } +} +func WithMemStats() GoCollectorsOption { + return func(o *flags) { + *o = flags(uint(*o) | withMemStats) + } +} + +// MustRegisterGoCollectors registers the Go runtime and the process collectors +// in the given registerer. +// +// It panics if the collectors cannot be registered. The default registerer +// already contains them, since the prometheus package registers them on its +// own, so it must not be used here. +func MustRegisterGoCollectors(r prometheus.Registerer, opts ...GoCollectorsOption) { + var ff flags + for _, opt := range opts { + opt(&ff) + } + + if ff.noFlag(withoutProcess) { + r.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) + } + + var rules []collectors.GoRuntimeMetricsRule + if ff.noFlag(withoutCPU) { + rules = append(rules, collectors.GoRuntimeMetricsRule{Matcher: regexp.MustCompile(`^/cpu/classes/.*`)}) + } + if ff.noFlag(withoutGC) { + rules = append(rules, collectors.MetricsGC) + } + if ff.noFlag(withoutMemory) { + rules = append(rules, collectors.MetricsMemory) + } + if ff.noFlag(withoutScheduler) { + rules = append(rules, collectors.MetricsScheduler) + } + if ff.noFlag(withoutSync) { + rules = append(rules, collectors.GoRuntimeMetricsRule{Matcher: regexp.MustCompile(`^/sync/.*`)}) + } + + switch { + case ff.noFlag(withMemStats) && len(rules) == 0: + r.MustRegister(collectors.NewGoCollector( + collectors.WithGoCollectorMemStatsMetricsDisabled(), + )) + case ff.noFlag(withMemStats): + r.MustRegister(collectors.NewGoCollector( + collectors.WithGoCollectorMemStatsMetricsDisabled(), + collectors.WithGoCollectorRuntimeMetrics(rules...), + )) + default: + r.MustRegister(collectors.NewGoCollector( + collectors.WithGoCollectorRuntimeMetrics(rules...), + )) + } +} diff --git a/prommetrics/go_collectors_internal_test.go b/prommetrics/go_collectors_internal_test.go new file mode 100644 index 0000000..d464cd9 --- /dev/null +++ b/prommetrics/go_collectors_internal_test.go @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package prommetrics + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestInitOptions(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + opts []GoCollectorsOption + expected uint + }{ + { + name: "default options", + opts: nil, + expected: 0, + }, + { + name: "without process", + opts: []GoCollectorsOption{WithoutProcess()}, + expected: withoutProcess, + }, + { + name: "without cpu", + opts: []GoCollectorsOption{WithoutCPU()}, + expected: withoutCPU, + }, + { + name: "without gc", + opts: []GoCollectorsOption{WithoutGC()}, + expected: withoutGC, + }, + { + name: "without memory", + opts: []GoCollectorsOption{WithoutMemory()}, + expected: withoutMemory, + }, + { + name: "without scheduler", + opts: []GoCollectorsOption{WithoutScheduler()}, + expected: withoutScheduler, + }, + { + name: "without sync", + opts: []GoCollectorsOption{WithoutSync()}, + expected: withoutSync, + }, + { + name: "multiple options", + opts: []GoCollectorsOption{WithoutProcess(), WithoutCPU(), WithoutGC()}, + expected: withoutProcess | withoutCPU | withoutGC, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var wo flags + for _, opt := range tt.opts { + opt(&wo) + } + + assert.Equal(t, tt.expected, uint(wo)) + }) + } +} + +func TestWithoutNoFlag(t *testing.T) { + t.Parallel() + + t.Run("without single flag", func(t *testing.T) { + t.Parallel() + + var wo flags + WithoutGC()(&wo) + + assert.True(t, wo.noFlag(withoutProcess)) + assert.True(t, wo.noFlag(withoutCPU)) + assert.False(t, wo.noFlag(withoutGC)) + assert.True(t, wo.noFlag(withoutMemory)) + assert.True(t, wo.noFlag(withoutScheduler)) + assert.True(t, wo.noFlag(withoutSync)) + }) + + t.Run("without multiple flag", func(t *testing.T) { + t.Parallel() + + var wo flags + WithoutGC()(&wo) + WithoutSync()(&wo) + + assert.True(t, wo.noFlag(withoutProcess)) + assert.True(t, wo.noFlag(withoutCPU)) + assert.False(t, wo.noFlag(withoutGC)) + assert.True(t, wo.noFlag(withoutMemory)) + assert.True(t, wo.noFlag(withoutScheduler)) + assert.False(t, wo.noFlag(withoutSync)) + }) +} diff --git a/prommetrics/go_collectors_test.go b/prommetrics/go_collectors_test.go new file mode 100644 index 0000000..9249a5a --- /dev/null +++ b/prommetrics/go_collectors_test.go @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: BSD-3-Clause + +package prommetrics_test + +import ( + "testing" + + . "github.com/nbgrp/pkg/prommetrics" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// gatherFamilies returns the names of the metric families registered in the +// given registry. +func gatherFamilies(t *testing.T, reg *prometheus.Registry) map[string]bool { + t.Helper() + + mfs, err := reg.Gather() + require.NoError(t, err) + + families := make(map[string]bool, len(mfs)) + for _, mf := range mfs { + families[mf.GetName()] = true + } + + return families +} + +// TestMustRegisterGoCollectors_WithMemStats checks that the legacy MemStats +// metrics are registered only when the WithMemStats option is set: without it +// they are disabled in favor of the runtime metrics. +func TestMustRegisterGoCollectors_WithMemStats(t *testing.T) { + t.Parallel() + + plainReg := prometheus.NewRegistry() + MustRegisterGoCollectors(plainReg) + + memStatsReg := prometheus.NewRegistry() + MustRegisterGoCollectors(memStatsReg, WithMemStats()) + + plain := gatherFamilies(t, plainReg) + memStats := gatherFamilies(t, memStatsReg) + + assert.False(t, plain["go_memstats_alloc_bytes"]) + assert.True(t, memStats["go_memstats_alloc_bytes"]) + assert.NotEqual(t, plain, memStats) +}