diff --git a/README.md b/README.md index cad7643..c61ddf8 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ Command line arguments: - `-tls-enabled`: Whether to enable TLS termination (default: `false`) - `-tls-cert-file`: Path to TLS certificate file - `-tls-key-file`: Path to TLS certificate private key file +- `-prometheus-listen-addr`: Listen address where /metrics endpoint is exposed (default: `:9090`) +- `-prometheus-metrics-enabled`: Enable the Prometheus metrics endpoint (default: `false`) - `-log-level`: Log level selection: `debug`, `info`, `warn`, `error` (default: `info`) - `-log-format`: Log output format selection: `json`, `console` (default: `console`) - `-version`: Print version info and exit diff --git a/cmd/terraform-registry/main.go b/cmd/terraform-registry/main.go index efd6734..21db74e 100644 --- a/cmd/terraform-registry/main.go +++ b/cmd/terraform-registry/main.go @@ -25,23 +25,27 @@ import ( "github.com/nrkno/terraform-registry/pkg/registry" "github.com/nrkno/terraform-registry/pkg/store/github" "github.com/nrkno/terraform-registry/pkg/store/s3" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/zap" ) var ( - listenAddr string - accessLogDisabled bool - accessLogIgnoredPaths string - authDisabled bool - authTokensFile string - envJSONFiles string - tlsEnabled bool - tlsCertFile string - tlsKeyFile string - storeType string - logLevelStr string - logFormatStr string - printVersionInfo bool + listenAddr string + accessLogDisabled bool + accessLogIgnoredPaths string + authDisabled bool + authTokensFile string + envJSONFiles string + tlsEnabled bool + tlsCertFile string + tlsKeyFile string + storeType string + prometheusListenAddr string + prometheusMetricsEnabled bool + logLevelStr string + logFormatStr string + printVersionInfo bool S3Region string S3Bucket string @@ -84,6 +88,8 @@ func init() { flag.StringVar(&logLevelStr, "log-level", "info", "Levels: debug, info, warn, error") flag.StringVar(&logFormatStr, "log-format", "console", "Formats: json, console") flag.BoolVar(&printVersionInfo, "version", false, "Print version info and exit") + flag.StringVar(&prometheusListenAddr, "prometheus-listen-addr", ":9090", "Listen address where /metrics endpoint is exposed.") + flag.BoolVar(&prometheusMetricsEnabled, "prometheus-metrics-enabled", false, "Enable the Prometheus metrics endpoint.") flag.StringVar(&gitHubOwnerFilter, "github-owner-filter", "", "GitHub org/user repository filter") flag.StringVar(&gitHubTopicFilter, "github-topic-filter", "", "GitHub topic repository filter") @@ -187,6 +193,16 @@ func main() { logger.Fatal("invalid store type", zap.String("selected", storeType)) } + if prometheusMetricsEnabled { + prometheus. + WrapRegistererWithPrefix("terraform_registry_", prometheus.DefaultRegisterer). + MustRegister(reg.Metrics()) + + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.Handler()) + go prometheusListenAndServe(mux) + } + listenAndServe(reg) } @@ -217,6 +233,33 @@ func listenAndServe(handler http.Handler) { } } +func prometheusListenAndServe(handler http.Handler) { + srv := http.Server{ + Addr: prometheusListenAddr, + Handler: handler, + ReadTimeout: 3 * time.Second, + ReadHeaderTimeout: 3 * time.Second, + WriteTimeout: 3 * time.Second, + IdleTimeout: 60 * time.Second, // keep-alive timeout + } + + logger.Info("starting Prometheus metrics HTTP server", + zap.Bool("tls", tlsEnabled), + zap.String("listenAddr", prometheusListenAddr), + ) + if tlsEnabled { + err := srv.ListenAndServeTLS(tlsCertFile, tlsKeyFile) + logger.Panic("prometheusListenAndServe", + zap.Errors("err", []error{err}), + ) + } else { + err := srv.ListenAndServe() + logger.Panic("prometheusListenAndServe", + zap.Errors("err", []error{err}), + ) + } +} + // watchFile reads the contents of the file at `filename`, first immediately, then at at every `interval`. // If and only if the file contents have changed since the last invocation of `callback` it is called again. // Note that the callback will always be called initially when this function is called. diff --git a/go.mod b/go.mod index 0cf068b..ec40ac1 100644 --- a/go.mod +++ b/go.mod @@ -16,23 +16,31 @@ require ( github.com/hashicorp/go-version v1.6.0 github.com/matryer/is v1.4.1 github.com/migueleliasweb/go-github-mock v0.0.23 + github.com/prometheus/client_golang v1.18.0 github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.27.0 golang.org/x/oauth2 v0.17.0 ) require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-github/v59 v59.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // 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/stretchr/objx v0.5.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/crypto v0.19.0 // indirect golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect diff --git a/go.sum b/go.sum index fc7c0bb..a8ea260 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ github.com/aws/aws-sdk-go v1.50.26 h1:tuv8+dje59DBK1Pj65tSCdD36oamBxKYJgbng4bFylc= github.com/aws/aws-sdk-go v1.50.26/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -27,12 +31,28 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +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/migueleliasweb/go-github-mock v0.0.23 h1:GOi9oX/+Seu9JQ19V8bPDLqDI7M9iEOjo3g8v1k6L2c= github.com/migueleliasweb/go-github-mock v0.0.23/go.mod h1:NsT8FGbkvIZQtDu38+295sZEX8snaUiiQgsGxi6GUxk= 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/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/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 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= @@ -56,6 +76,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= @@ -70,10 +92,12 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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/pkg/core/core.go b/pkg/core/core.go index 385bf32..db95c77 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -5,7 +5,11 @@ package core -import "context" +import ( + "context" + + "github.com/prometheus/client_golang/prometheus" +) type ModuleVersion struct { // Version is a SemVer version string that specifies the version for a module. @@ -19,4 +23,5 @@ type ModuleVersion struct { type ModuleStore interface { ListModuleVersions(ctx context.Context, namespace, name, provider string) ([]*ModuleVersion, error) GetModuleVersion(ctx context.Context, namespace, name, provider, version string) (*ModuleVersion, error) + Metrics() prometheus.Collector } diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 3047d13..cc43918 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -16,6 +16,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/nrkno/terraform-registry/pkg/core" + "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" ) @@ -152,6 +153,17 @@ func (reg *Registry) RequestLogger() func(next http.Handler) http.Handler { // SPDX-SnippetEnd +// Metrics returns a Prometheus registry containing metrics for the module store. +func (reg *Registry) Metrics() prometheus.Collector { + root := prometheus.NewRegistry() + + if reg.moduleStore != nil { + prometheus.WrapRegistererWithPrefix("store_module_", root).MustRegister(reg.moduleStore.Metrics()) + } + + return root +} + func (reg *Registry) ServeHTTP(w http.ResponseWriter, r *http.Request) { reg.router.ServeHTTP(w, r) } diff --git a/pkg/store/github/github.go b/pkg/store/github/github.go index 5202773..5f10b54 100644 --- a/pkg/store/github/github.go +++ b/pkg/store/github/github.go @@ -14,6 +14,7 @@ import ( "github.com/google/go-github/v43/github" goversion "github.com/hashicorp/go-version" "github.com/nrkno/terraform-registry/pkg/core" + "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" "golang.org/x/oauth2" ) @@ -200,3 +201,8 @@ func (s *GitHubStore) searchRepositories(ctx context.Context) ([]*github.Reposit return allRepos, nil } + +// Metrics returns a registry with metrics for this store. +func (s *GitHubStore) Metrics() prometheus.Collector { + return nil +} diff --git a/pkg/store/memory/memory.go b/pkg/store/memory/memory.go index acebe22..2fb2b01 100644 --- a/pkg/store/memory/memory.go +++ b/pkg/store/memory/memory.go @@ -11,6 +11,7 @@ import ( "sync" "github.com/nrkno/terraform-registry/pkg/core" + "github.com/prometheus/client_golang/prometheus" ) // MemoryStore is an in-memory store implementation without a backend. @@ -72,3 +73,7 @@ func (s *MemoryStore) GetModuleVersion(ctx context.Context, namespace, name, pro return nil, fmt.Errorf("version '%s' not found for module '%s'", version, key) } + +func (s *MemoryStore) Metrics() prometheus.Collector { + return nil +} diff --git a/pkg/store/s3/s3.go b/pkg/store/s3/s3.go index 688b16d..258600c 100644 --- a/pkg/store/s3/s3.go +++ b/pkg/store/s3/s3.go @@ -13,6 +13,7 @@ import ( "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3/s3iface" "github.com/nrkno/terraform-registry/pkg/core" + "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" ) @@ -144,3 +145,7 @@ func isValidModuleSourcePath(path string) bool { r := regexp.MustCompile("^" + addrRegExp + "/" + verRegExp) return r.MatchString(path) } + +func (s *S3Store) Metrics() prometheus.Collector { + return nil +}