From e5b2b7e837e50882ee65426237b236956abd9093 Mon Sep 17 00:00:00 2001 From: Alexander Menshchikov Date: Fri, 6 Nov 2026 16:25:20 +0300 Subject: [PATCH] Add the prommetrics package The registration of the Go runtime and the process collectors, and the creation of the application metrics that follow the namespace and the registry of the context. The prommetrics package registers the collectors in the given registerer. The Without* options turn the individual groups of the runtime metrics off, and WithMemStats keeps the legacy go_memstats_* family that the runtime metrics otherwise replace. The ctxprom 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. The New*For functions create the metric and register it in the registry of the context. --- README.md | 1 + go.work | 1 + go.work.sum | 41 ++- prommetrics/.golangci.yml | 342 +++++++++++++++++++++ prommetrics/README.md | 147 +++++++++ prommetrics/ctxprom/ctxprom.go | 95 ++++++ prommetrics/ctxprom/ctxprom_test.go | 300 ++++++++++++++++++ prommetrics/go.mod | 20 ++ prommetrics/go.sum | 28 ++ prommetrics/go_collectors.go | 114 +++++++ prommetrics/go_collectors_internal_test.go | 106 +++++++ prommetrics/go_collectors_test.go | 48 +++ 12 files changed, 1240 insertions(+), 3 deletions(-) create mode 100644 prommetrics/.golangci.yml create mode 100644 prommetrics/README.md create mode 100644 prommetrics/ctxprom/ctxprom.go create mode 100644 prommetrics/ctxprom/ctxprom_test.go create mode 100644 prommetrics/go.mod create mode 100644 prommetrics/go.sum create mode 100644 prommetrics/go_collectors.go create mode 100644 prommetrics/go_collectors_internal_test.go create mode 100644 prommetrics/go_collectors_test.go 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) +}