diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index b6834a5b..6a52c1b5 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -32,6 +32,7 @@ jobs: - "fxconfig" - "fxgenerate" - "fxlog" + - "fxmetrics" - "fxtrace" steps: - name: Checkout diff --git a/.github/workflows/fxmetrics-ci.yml b/.github/workflows/fxmetrics-ci.yml new file mode 100644 index 00000000..422cbf82 --- /dev/null +++ b/.github/workflows/fxmetrics-ci.yml @@ -0,0 +1,31 @@ +name: "fxmetrics-ci" + +on: + push: + branches: + - "feat**" + - "fix**" + - "hotfix**" + - "chore**" + paths: + - "fxmetrics/**.go" + - "fxmetrics/go.mod" + - "fxmetrics/go.sum" + pull_request: + types: + - opened + - synchronize + - reopened + branches: + - main + paths: + - "fxmetrics/**.go" + - "fxmetrics/go.mod" + - "fxmetrics/go.sum" + +jobs: + ci: + uses: ./.github/workflows/common-ci.yml + secrets: inherit + with: + module: "fxmetrics" diff --git a/README.md b/README.md index ebff39f8..7cf78c50 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,13 @@ Yokai is using [Fx](https://github.com/uber-go/fx) for its plugin system. Yokai's `Fx modules` are the plugins for your Yokai application. -| Fx Module | Description | -|--------------------------|------------------------------------| -| [fxconfig](fxconfig) | Fx module for [config](config) | -| [fxgenerate](fxgenerate) | Fx module for [generate](generate) | -| [fxlog](fxlog) | Fx module for [log](log) | -| [fxtrace](fxtrace) | Fx module for [trace](trace) | +| Fx Module | Description | +|--------------------------|-------------------------------------------------------------------------| +| [fxconfig](fxconfig) | Fx module for [config](config) | +| [fxgenerate](fxgenerate) | Fx module for [generate](generate) | +| [fxlog](fxlog) | Fx module for [log](log) | +| [fxmetrics](fxmetrics) | Fx module for [prometheus](https://github.com/prometheus/client_golang) | +| [fxtrace](fxtrace) | Fx module for [trace](trace) | They can also be used in any [Fx](https://github.com/uber-go/fx) based Go application. diff --git a/fxmetrics/.golangci.yml b/fxmetrics/.golangci.yml new file mode 100644 index 00000000..3fd6eb40 --- /dev/null +++ b/fxmetrics/.golangci.yml @@ -0,0 +1,64 @@ +run: + timeout: 5m + concurrency: 8 + +linters: + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + - decorder + - dogsled + - dupl + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - forbidigo + - forcetypeassert + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - godox + - gofmt + - goheader + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - gosimple + - govet + - grouper + - importas + - ineffassign + - interfacebloat + - logrlint + - maintidx + - makezero + - misspell + - nestif + - nilerr + - nilnil + - nlreturn + - nolintlint + - nosprintfhostport + - prealloc + - predeclared + - promlinter + - reassign + - staticcheck + - tenv + - thelper + - tparallel + - typecheck + - unconvert + - unparam + - unused + - usestdlibvars + - whitespace diff --git a/fxmetrics/README.md b/fxmetrics/README.md new file mode 100644 index 00000000..9df45db8 --- /dev/null +++ b/fxmetrics/README.md @@ -0,0 +1,206 @@ +# Fx Metrics Module + +[![ci](https://github.com/ankorstore/yokai/actions/workflows/fxmetrics-ci.yml/badge.svg)](https://github.com/ankorstore/yokai/actions/workflows/fxmetrics-ci.yml) +[![go report](https://goreportcard.com/badge/github.com/ankorstore/yokai/fxmetrics)](https://goreportcard.com/report/github.com/ankorstore/yokai/fxmetrics) +[![codecov](https://codecov.io/gh/ankorstore/yokai/graph/badge.svg?token=ghUBlFsjhR&flag=fxmetrics)](https://app.codecov.io/gh/ankorstore/yokai/tree/main/fxmetrics) +[![Deps](https://img.shields.io/badge/osi-deps-blue)](https://deps.dev/go/github.com%2Fankorstore%2Fyokai%2Ffxmetrics) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/ankorstore/yokai/fxmetrics)](https://pkg.go.dev/github.com/ankorstore/yokai/fxmetrics) + +> [Fx](https://uber-go.github.io/fx/) module for [prometheus](https://github.com/prometheus/client_golang). + + +* [Installation](#installation) +* [Documentation](#documentation) + * [Dependencies](#dependencies) + * [Loading](#loading) + * [Registration](#registration) + * [Override](#override) + * [Testing](#testing) + + +## Installation + +```shell +go get github.com/ankorstore/yokai/fxmetrics +``` + +## Documentation + +### Dependencies + +This module is intended to be used alongside: + +- the [fxconfig](https://github.com/ankorstore/yokai/tree/main/fxconfig) module +- the [fxlog](https://github.com/ankorstore/yokai/tree/main/fxlog) module + +### Loading + +To load the module in your Fx application: + +```go +package main + +import ( + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/prometheus/client_golang/prometheus" + "go.uber.org/fx" +) + +func main() { + fx.New( + fxconfig.FxConfigModule, // load the module dependencies + fxlog.FxLogModule, + fxmetrics.FxMetricsModule, // load the module + fx.Invoke(func(registry *prometheus.Registry) { // invoke the metrics registry + // ... + }), + ).Run() +} +``` + +### Registration + +This module provides the possibility to register your metrics [collectors](https://github.com/prometheus/client_golang/blob/main/prometheus/collector.go) in a common `*prometheus.Registry` via `AsMetricsCollector()`: + +```go +package main + +import ( + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/prometheus/client_golang/prometheus" + "go.uber.org/fx" +) + +var SomeCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "some_total", + Help: "some help", +}) + +func main() { + fx.New( + fxconfig.FxConfigModule, // load the module dependencies + fxlog.FxLogModule, + fxmetrics.FxMetricsModule, // load the module + fx.Options( + fxmetrics.AsMetricsCollector(SomeCounter), // register the counter + ), + fx.Invoke(func() { + SomeCounter.Inc() // manipulate the counter + }), + ).Run() +} +``` + +**Important**: even if convenient, it's recommended to **NOT** use the [promauto](https://github.com/prometheus/client_golang/tree/main/prometheus/promauto) way of registering metrics, +but to use instead `fxmetrics.AsMetricsCollector()`, as `promauto` uses a global registry that leads to data race +conditions in testing. + +Also, if you want to register several collectors at once, you can use `fxmetrics.AsMetricsCollectors()` + +### Override + +By default, the `*prometheus.Registry` is created by the [DefaultMetricsRegistryFactory](factory.go). + +If needed, you can provide your own factory and override the module: + +```go +package main + +import ( + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/prometheus/client_golang/prometheus" + "go.uber.org/fx" +) + +type CustomMetricsRegistryFactory struct{} + +func NewCustomMetricsRegistryFactory() fxmetrics.MetricsRegistryFactory { + return &CustomMetricsRegistryFactory{} +} + +func (f *CustomMetricsRegistryFactory) Create() (*prometheus.Registry, error) { + return prometheus.NewPedanticRegistry(), nil +} + +func main() { + fx.New( + fxconfig.FxConfigModule, // load the module dependencies + fxlog.FxLogModule, + fxmetrics.FxMetricsModule, // load the module + fx.Decorate(NewCustomMetricsRegistryFactory), // override the module with a custom factory + fx.Invoke(func(registry *prometheus.Registry) { // invoke the custom registry + // ... + }), + ).Run() +} +``` + +### Testing + +This module provides the possibility to easily test your metrics with the prometheus package [testutil](https://github.com/prometheus/client_golang/tree/main/prometheus/testutil) helpers. + +```go +package main_test + +import ( + "strings" + "testing" + + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" + "go.uber.org/fx" + "go.uber.org/fx/fxtest" +) + +var SomeCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "some_total", + Help: "some help", +}) + +func TestSomeCounter(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + var registry *prometheus.Registry + + fxtest.New( + t, + fx.NopLogger, + fxconfig.FxConfigModule, + fxlog.FxLogModule, + fxmetrics.FxMetricsModule, + fx.Options( + fxmetrics.AsMetricsCollector(SomeCounter), + ), + fx.Invoke(func() { + SomeCounter.Add(9) + }), + fx.Populate(®istry), + ).RequireStart().RequireStop() + + // metric assertions + expectedHelp := ` + # HELP some_total some help + # TYPE some_total counter + ` + expectedMetric := ` + some_total 9 + ` + + err := testutil.GatherAndCompare( + registry, + strings.NewReader(expectedHelp+expectedMetric), + "some_total", + ) + assert.NoError(t, err) +} +``` diff --git a/fxmetrics/factory.go b/fxmetrics/factory.go new file mode 100644 index 00000000..80953519 --- /dev/null +++ b/fxmetrics/factory.go @@ -0,0 +1,23 @@ +package fxmetrics + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +// MetricsRegistryFactory is the interface for [prometheus.Registry] factories. +type MetricsRegistryFactory interface { + Create() (*prometheus.Registry, error) +} + +// DefaultMetricsRegistryFactory is the default [MetricsRegistryFactory] implementation. +type DefaultMetricsRegistryFactory struct{} + +// NewDefaultMetricsRegistryFactory returns a [DefaultMetricsRegistryFactory], implementing [MetricsRegistryFactory]. +func NewDefaultMetricsRegistryFactory() MetricsRegistryFactory { + return &DefaultMetricsRegistryFactory{} +} + +// Create returns a new [prometheus.Registry]. +func (f *DefaultMetricsRegistryFactory) Create() (*prometheus.Registry, error) { + return prometheus.NewRegistry(), nil +} diff --git a/fxmetrics/factory_test.go b/fxmetrics/factory_test.go new file mode 100644 index 00000000..f3a84178 --- /dev/null +++ b/fxmetrics/factory_test.go @@ -0,0 +1,29 @@ +package fxmetrics_test + +import ( + "testing" + + "github.com/ankorstore/yokai/fxmetrics" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" +) + +func TestDefaultMetricsRegistryFactory(t *testing.T) { + t.Parallel() + + factory := fxmetrics.NewDefaultMetricsRegistryFactory() + + assert.IsType(t, &fxmetrics.DefaultMetricsRegistryFactory{}, factory) + assert.Implements(t, (*fxmetrics.MetricsRegistryFactory)(nil), factory) +} + +func TestCreate(t *testing.T) { + t.Parallel() + + factory := fxmetrics.NewDefaultMetricsRegistryFactory() + + registry, err := factory.Create() + assert.NoError(t, err) + + assert.IsType(t, &prometheus.Registry{}, registry) +} diff --git a/fxmetrics/go.mod b/fxmetrics/go.mod new file mode 100644 index 00000000..bc944118 --- /dev/null +++ b/fxmetrics/go.mod @@ -0,0 +1,51 @@ +module github.com/ankorstore/yokai/fxmetrics + +go 1.20 + +require ( + github.com/ankorstore/yokai/fxconfig v1.0.0 + github.com/ankorstore/yokai/fxlog v1.0.0 + github.com/ankorstore/yokai/log v1.0.0 + github.com/prometheus/client_golang v1.18.0 + github.com/stretchr/testify v1.8.4 + go.uber.org/fx v1.20.1 +) + +require ( + github.com/ankorstore/yokai/config v1.1.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/rs/zerolog v1.31.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.18.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.opentelemetry.io/otel v1.16.0 // indirect + go.opentelemetry.io/otel/trace v1.16.0 // indirect + go.uber.org/dig v1.17.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/fxmetrics/go.sum b/fxmetrics/go.sum new file mode 100644 index 00000000..820a7b22 --- /dev/null +++ b/fxmetrics/go.sum @@ -0,0 +1,116 @@ +github.com/ankorstore/yokai/config v1.1.0 h1:z6xnsVXAbWhhjcb5kqVaw0VlaGZziGc7Di1bJlt5rf0= +github.com/ankorstore/yokai/config v1.1.0/go.mod h1:yDANaMWIOfAUkAMClG22Q4bzQk91NLwWK3WbL5IFnbg= +github.com/ankorstore/yokai/fxconfig v1.0.0 h1:zaYOLfpurqFJuS/IHeAXPOzrCNuttsFjJWNOK39opR4= +github.com/ankorstore/yokai/fxconfig v1.0.0/go.mod h1:p+x6Jp8aLv1+uE1qO42KF+yahBK+VJdPP1/YReBjJ7M= +github.com/ankorstore/yokai/fxlog v1.0.0 h1:ujq/XxgCK0uwKCNSt86XEYR2vqYbXZX2/lA/pQHZX4A= +github.com/ankorstore/yokai/fxlog v1.0.0/go.mod h1:juQnBYNddDVOa7Ukhw8axLYWyibDDMJAwG7MDpluKnk= +github.com/ankorstore/yokai/log v1.0.0 h1:9NsM0J+1O028WuNDW7vr0yeUdWDX1JKYTkuz7hiYCSs= +github.com/ankorstore/yokai/log v1.0.0/go.mod h1:lyBRVA8VkrmlNjaR2jVTH9XjV06ioolWTuDVN6wF0vk= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +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.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= +go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= +go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= +go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= +go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk= +go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE= +golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/fxmetrics/module.go b/fxmetrics/module.go new file mode 100644 index 00000000..97330845 --- /dev/null +++ b/fxmetrics/module.go @@ -0,0 +1,52 @@ +package fxmetrics + +import ( + "github.com/ankorstore/yokai/log" + "github.com/prometheus/client_golang/prometheus" + "go.uber.org/fx" +) + +// ModuleName is the module name. +const ModuleName = "metrics" + +// FxMetricsModule is the [Fx] metrics module. +// +// [Fx]: https://github.com/uber-go/fx +var FxMetricsModule = fx.Module( + ModuleName, + fx.Provide( + NewDefaultMetricsRegistryFactory, + NewFxMetricsRegistry, + ), +) + +// FxMetricsRegistryParam allows injection of the required dependencies in [NewFxMetricsRegistry]. +type FxMetricsRegistryParam struct { + fx.In + Factory MetricsRegistryFactory + Logger *log.Logger + Collectors []prometheus.Collector `group:"metrics-collectors"` +} + +// NewFxMetricsRegistry returns a [prometheus.Registry]. +func NewFxMetricsRegistry(p FxMetricsRegistryParam) (*prometheus.Registry, error) { + registry, err := p.Factory.Create() + if err != nil { + p.Logger.Error().Err(err).Msg("failed to create metrics registry") + + return nil, err + } + + for _, collector := range p.Collectors { + err = registry.Register(collector) + if err != nil { + p.Logger.Error().Err(err).Msgf("failed to register metrics collector %+T", collector) + + return nil, err + } else { + p.Logger.Debug().Msgf("registered metrics collector %+T", collector) + } + } + + return registry, err +} diff --git a/fxmetrics/module_test.go b/fxmetrics/module_test.go new file mode 100644 index 00000000..09a9aac8 --- /dev/null +++ b/fxmetrics/module_test.go @@ -0,0 +1,110 @@ +package fxmetrics_test + +import ( + "strings" + "testing" + + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/ankorstore/yokai/fxmetrics/testdata/factory" + "github.com/ankorstore/yokai/fxmetrics/testdata/metrics" + "github.com/ankorstore/yokai/fxmetrics/testdata/spy" + "github.com/ankorstore/yokai/log/logtest" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" + "go.uber.org/fx" + "go.uber.org/fx/fxtest" +) + +func TestModule(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + var logBuffer logtest.TestLogBuffer + var registry *prometheus.Registry + + fxtest.New( + t, + fx.NopLogger, + fxconfig.FxConfigModule, + fxlog.FxLogModule, + fxmetrics.FxMetricsModule, + fx.Options( + fxmetrics.AsMetricsCollector(metrics.FxMetricsTestCounter), + ), + fx.Invoke(func() { + metrics.FxMetricsTestCounter.Add(9) + }), + fx.Populate(&logBuffer, ®istry), + ).RequireStart().RequireStop() + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "debug", + "message": "registered metrics collector *prometheus.counter", + }) + + expectedHelp := ` + # HELP test_total test help + # TYPE test_total counter + ` + expectedMetric := ` + test_total 9 + ` + + err := testutil.GatherAndCompare( + registry, + strings.NewReader(expectedHelp+expectedMetric), + "test_total", + ) + assert.NoError(t, err) +} + +func TestModuleErrorWithDuplicatedCollector(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + var logBuffer logtest.TestLogBuffer + var registry *prometheus.Registry + + spyTB := spy.NewSpyTB() + + fxtest.New( + spyTB, + fx.NopLogger, + fxconfig.FxConfigModule, + fxlog.FxLogModule, + fxmetrics.FxMetricsModule, + fx.Options( + fxmetrics.AsMetricsCollectors(metrics.FxMetricsTestCounter, metrics.FxMetricsDuplicatedTestCounter), + ), + fx.Populate(&logBuffer, ®istry), + ).RequireStart().RequireStop() + + assert.Empty(t, spyTB.Logs()) + + assert.NotZero(t, spyTB.Failures()) + assert.Contains(t, spyTB.Errors().String(), "duplicate metrics collector registration attempted") +} + +func TestModuleDecoration(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + var logBuffer logtest.TestLogBuffer + var registry *prometheus.Registry + + spyTB := spy.NewSpyTB() + + fxtest.New( + spyTB, + fxconfig.FxConfigModule, + fxlog.FxLogModule, + fxmetrics.FxMetricsModule, + fx.Decorate(factory.NewTestMetricsRegistryFactory), + fx.Populate(&logBuffer, ®istry), + ).RequireStart().RequireStop() + + assert.Contains(t, spyTB.Logs().String(), "NewTestMetricsRegistryFactory") + + assert.NotZero(t, spyTB.Failures()) + assert.Contains(t, spyTB.Errors().String(), "custom error") +} diff --git a/fxmetrics/register.go b/fxmetrics/register.go new file mode 100644 index 00000000..9dab822d --- /dev/null +++ b/fxmetrics/register.go @@ -0,0 +1,37 @@ +package fxmetrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "go.uber.org/fx" +) + +// AsMetricsCollector registers a [prometheus.Collector] into Fx. +func AsMetricsCollector(collector prometheus.Collector) fx.Option { + return fx.Supply( + fx.Annotate( + collector, + fx.As(new(prometheus.Collector)), + fx.ResultTags(`group:"metrics-collectors"`), + ), + ) +} + +// AsMetricsCollectors registers a list of [prometheus.Collector] into Fx. +func AsMetricsCollectors(collectors ...prometheus.Collector) fx.Option { + registrations := []fx.Option{} + + for _, collector := range collectors { + registrations = append( + registrations, + fx.Supply( + fx.Annotate( + collector, + fx.As(new(prometheus.Collector)), + fx.ResultTags(`group:"metrics-collectors"`), + ), + ), + ) + } + + return fx.Options(registrations...) +} diff --git a/fxmetrics/register_test.go b/fxmetrics/register_test.go new file mode 100644 index 00000000..8b988e46 --- /dev/null +++ b/fxmetrics/register_test.go @@ -0,0 +1,26 @@ +package fxmetrics_test + +import ( + "fmt" + "testing" + + "github.com/ankorstore/yokai/fxmetrics" + "github.com/ankorstore/yokai/fxmetrics/testdata/metrics" + "github.com/stretchr/testify/assert" +) + +func TestAsMetricsCollector(t *testing.T) { + t.Parallel() + + result := fxmetrics.AsMetricsCollector(metrics.FxMetricsTestCounter) + + assert.Equal(t, "fx.supplyOption", fmt.Sprintf("%T", result)) +} + +func TestAsMetricsCollectors(t *testing.T) { + t.Parallel() + + result := fxmetrics.AsMetricsCollectors(metrics.FxMetricsTestCounter, metrics.FxMetricsDuplicatedTestCounter) + + assert.Equal(t, "fx.optionGroup", fmt.Sprintf("%T", result)) +} diff --git a/fxmetrics/testdata/config/config.yaml b/fxmetrics/testdata/config/config.yaml new file mode 100644 index 00000000..0ca0a251 --- /dev/null +++ b/fxmetrics/testdata/config/config.yaml @@ -0,0 +1,6 @@ +app: + name: dev +modules: + log: + level: debug + output: test diff --git a/fxmetrics/testdata/factory/factory.go b/fxmetrics/testdata/factory/factory.go new file mode 100644 index 00000000..ac439795 --- /dev/null +++ b/fxmetrics/testdata/factory/factory.go @@ -0,0 +1,18 @@ +package factory + +import ( + "fmt" + + "github.com/ankorstore/yokai/fxmetrics" + "github.com/prometheus/client_golang/prometheus" +) + +type TestMetricsRegistryFactory struct{} + +func NewTestMetricsRegistryFactory() fxmetrics.MetricsRegistryFactory { + return &TestMetricsRegistryFactory{} +} + +func (f *TestMetricsRegistryFactory) Create() (*prometheus.Registry, error) { + return nil, fmt.Errorf("custom error") +} diff --git a/fxmetrics/testdata/metrics/metrics.go b/fxmetrics/testdata/metrics/metrics.go new file mode 100644 index 00000000..71a3e41f --- /dev/null +++ b/fxmetrics/testdata/metrics/metrics.go @@ -0,0 +1,13 @@ +package metrics + +import "github.com/prometheus/client_golang/prometheus" + +var FxMetricsTestCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "test_total", + Help: "test help", +}) + +var FxMetricsDuplicatedTestCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "test_total", + Help: "test help", +}) diff --git a/fxmetrics/testdata/spy/spy.go b/fxmetrics/testdata/spy/spy.go new file mode 100644 index 00000000..c08248cf --- /dev/null +++ b/fxmetrics/testdata/spy/spy.go @@ -0,0 +1,42 @@ +package spy + +import ( + "bytes" + "fmt" +) + +type SpyTB struct { + failures int + errors *bytes.Buffer + logs *bytes.Buffer +} + +func NewSpyTB() *SpyTB { + return &SpyTB{0, &bytes.Buffer{}, &bytes.Buffer{}} +} + +func (t *SpyTB) Failures() int { + return t.failures +} + +func (t *SpyTB) Errors() *bytes.Buffer { + return t.errors +} + +func (t *SpyTB) Logs() *bytes.Buffer { + return t.logs +} + +func (t *SpyTB) FailNow() { + t.failures++ +} + +func (t *SpyTB) Errorf(format string, args ...interface{}) { + fmt.Fprintf(t.errors, format, args...) + t.errors.WriteRune('\n') +} + +func (t *SpyTB) Logf(format string, args ...interface{}) { + fmt.Fprintf(t.logs, format, args...) + t.logs.WriteRune('\n') +} diff --git a/release-please-config.json b/release-please-config.json index 8c0e63df..a3761f75 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -46,14 +46,19 @@ "component": "fxconfig", "tag-separator": "/" }, + "fxgenerate": { + "release-type": "go", + "component": "fxgenerate", + "tag-separator": "/" + }, "fxlog": { "release-type": "go", "component": "fxlog", "tag-separator": "/" }, - "fxgenerate": { + "fxmetrics": { "release-type": "go", - "component": "fxgenerate", + "component": "fxmetrics", "tag-separator": "/" }, "fxtrace": {