diff --git a/cmd/run.go b/cmd/run.go index 4c91b69..bce9889 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -7,6 +7,7 @@ import ( "github.com/nlewo/comin/internal/http" "github.com/nlewo/comin/internal/manager" "github.com/nlewo/comin/internal/poller" + "github.com/nlewo/comin/internal/prometheus" "github.com/nlewo/comin/internal/repository" "github.com/nlewo/comin/internal/utils" "github.com/sirupsen/logrus" @@ -39,9 +40,13 @@ var runCmd = &cobra.Command{ os.Exit(1) } - manager := manager.New(repository, gitConfig.Path, cfg.Hostname, machineId) + metrics := prometheus.New() + manager := manager.New(repository, metrics, gitConfig.Path, cfg.Hostname, machineId) go poller.Poller(manager, cfg.Remotes) - go http.Serve(manager, cfg.HttpServer.Address, cfg.HttpServer.Port) + http.Serve(manager, + metrics, + cfg.ApiServer.ListenAddress, cfg.ApiServer.Port, + cfg.Exporter.ListenAddress, cfg.Exporter.Port) manager.Run() }, } diff --git a/flake.nix b/flake.nix index bf313dd..06e1434 100644 --- a/flake.nix +++ b/flake.nix @@ -26,7 +26,7 @@ p == "README.md" ); }; - vendorHash = "sha256-7rh1t3DkKfJvUOkPjdi2vqS8JTZpWtI61mTBKDHcPVk="; + vendorHash = "sha256-9qObgfXvMkwE+1BVZNQXVhKhL6LqMqyIUhGnXf8q9SI="; buildInputs = [ final.makeWrapper ]; postInstall = '' # This is because Nix needs Git at runtime by the go-git library @@ -45,6 +45,10 @@ hostname = cfg.services.comin.hostname; state_dir = "/var/lib/comin"; remotes = cfg.services.comin.remotes; + exporter = { + listen_address = cfg.services.comin.exporter.listen_address; + port = cfg.services.comin.exporter.port; + }; }; cominConfigYaml = yaml.generate "comin.yaml" cominConfig; in { @@ -67,6 +71,35 @@ nixosConfigurations."".config.system.build.toplevel ''; }; + exporter = mkOption { + description = "Options for the Prometheus exporter."; + default = {}; + type = submodule { + options = { + listen_address = mkOption { + type = str; + description = '' + Address to listen on for the Prometheus exporter. Empty string will listen on all interfaces. + ''; + default = ""; + }; + port = mkOption { + type = int; + description = '' + Port to listen on for the Prometheus exporter. + ''; + default = 4243; + }; + openFirewall = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Open port in firewall for incoming connections to the Prometheus exporter. + ''; + }; + }; + }; + }; remotes = mkOption { description = "Ordered list of repositories to pull"; type = listOf (submodule { @@ -181,6 +214,7 @@ config = lib.mkIf cfg.services.comin.enable { nixpkgs.overlays = [ self.overlay ]; environment.systemPackages = [ pkgs.comin ]; + networking.firewall.allowedTCPPorts = lib.optional cfg.services.comin.exporter.openFirewall cfg.services.comin.exporter.port; systemd.services.comin = { wantedBy = [ "multi-user.target" ]; path = [ config.nix.package ]; diff --git a/go.mod b/go.mod index d842f7f..096f06c 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,8 @@ require ( dario.cat/mergo v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cloudflare/circl v1.3.6 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -30,15 +32,20 @@ require ( github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.19.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/skeema/knownhosts v1.2.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.17.0 // indirect + golang.org/x/crypto v0.18.0 // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect golang.org/x/tools v0.16.1 // indirect + google.golang.org/protobuf v1.32.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6064291..5f95a38 100644 --- a/go.sum +++ b/go.sum @@ -12,7 +12,11 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= +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/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -137,6 +141,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +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.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +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-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= @@ -187,6 +199,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -223,6 +237,8 @@ golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -270,6 +286,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -325,6 +343,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/config/config.go b/internal/config/config.go index 1b735da..d858c1f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -32,11 +32,17 @@ func Read(path string) (config types.Configuration, err error) { } } - if config.HttpServer.Address == "" { - config.HttpServer.Address = "127.0.0.1" + if config.ApiServer.ListenAddress == "" { + config.ApiServer.ListenAddress = "127.0.0.1" } - if config.HttpServer.Port == 0 { - config.HttpServer.Port = 4242 + if config.ApiServer.Port == 0 { + config.ApiServer.Port = 4242 + } + if config.Exporter.ListenAddress == "" { + config.Exporter.ListenAddress = "0.0.0.0" + } + if config.Exporter.Port == 0 { + config.Exporter.Port = 4243 } if config.StateFilepath == "" { config.StateFilepath = filepath.Join(config.StateDir, "state.json") diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 6bd3079..0ffa661 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -32,9 +32,13 @@ func TestConfig(t *testing.T) { Timeout: 300, }, }, - HttpServer: types.HttpServer{ - Address: "127.0.0.1", - Port: 4242, + ApiServer: types.HttpServer{ + ListenAddress: "127.0.0.1", + Port: 4242, + }, + Exporter: types.HttpServer{ + ListenAddress: "0.0.0.0", + Port: 4243, }, } config, err := Read(configPath) diff --git a/internal/http/http.go b/internal/http/http.go index e248568..7c7cfba 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -8,6 +8,7 @@ import ( "os" "github.com/nlewo/comin/internal/manager" + "github.com/nlewo/comin/internal/prometheus" "github.com/sirupsen/logrus" ) @@ -24,16 +25,34 @@ func handlerStatus(m manager.Manager, w http.ResponseWriter, r *http.Request) { return } -func Serve(m manager.Manager, address string, port int) { +// Serve starts http servers. We create two HTTP servers to easily be +// able to expose metrics publicly while keeping on localhost only the +// API. +func Serve(m manager.Manager, p prometheus.Prometheus, apiAddress string, apiPort int, metricsAddress string, metricsPort int) { handlerStatusFn := func(w http.ResponseWriter, r *http.Request) { handlerStatus(m, w, r) return } - http.HandleFunc("/status", handlerStatusFn) - url := fmt.Sprintf("%s:%d", address, port) - logrus.Infof("Starting the webhook server on %s", url) - if err := http.ListenAndServe(url, nil); err != nil { - logrus.Errorf("Error while running the webhook server: %s", err) - os.Exit(1) - } + + muxStatus := http.NewServeMux() + muxStatus.HandleFunc("/status", handlerStatusFn) + muxMetrics := http.NewServeMux() + muxMetrics.Handle("/metrics", p.Handler()) + + go func() { + url := fmt.Sprintf("%s:%d", apiAddress, apiPort) + logrus.Infof("Starting the API server on %s", url) + if err := http.ListenAndServe(url, muxStatus); err != nil { + logrus.Errorf("Error while running the API server: %s", err) + os.Exit(1) + } + }() + go func() { + url := fmt.Sprintf("%s:%d", metricsAddress, metricsPort) + logrus.Infof("Starting the metrics server on %s", url) + if err := http.ListenAndServe(url, muxMetrics); err != nil { + logrus.Errorf("Error while running the metrics server: %s", err) + os.Exit(1) + } + }() } diff --git a/internal/manager/manager.go b/internal/manager/manager.go index a7d5e4f..c119cf3 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -7,6 +7,7 @@ import ( "github.com/nlewo/comin/internal/deployment" "github.com/nlewo/comin/internal/generation" "github.com/nlewo/comin/internal/nix" + "github.com/nlewo/comin/internal/prometheus" "github.com/nlewo/comin/internal/repository" "github.com/nlewo/comin/internal/utils" "github.com/sirupsen/logrus" @@ -52,9 +53,11 @@ type Manager struct { repositoryStatusCh chan repository.RepositoryStatus triggerDeploymentCh chan generation.Generation + + prometheus prometheus.Prometheus } -func New(r repository.Repository, path, hostname, machineId string) Manager { +func New(r repository.Repository, p prometheus.Prometheus, path, hostname, machineId string) Manager { return Manager{ repository: r, repositoryPath: path, @@ -70,6 +73,7 @@ func New(r repository.Repository, path, hostname, machineId string) Manager { deploymentResultCh: make(chan deployment.DeploymentResult), repositoryStatusCh: make(chan repository.RepositoryStatus), triggerDeploymentCh: make(chan generation.Generation, 1), + prometheus: p, } } @@ -131,6 +135,7 @@ func (m Manager) onDeployment(ctx context.Context, deploymentResult deployment.D m.needToBeRestarted = true } m.isRunning = false + m.prometheus.SetDeploymentInfo(m.deployment.Generation.SelectedCommitId, deployment.StatusToString(m.deployment.Status)) return m } @@ -138,6 +143,17 @@ func (m Manager) onRepositoryStatus(ctx context.Context, rs repository.Repositor logrus.Debugf("Fetch done with %#v", rs) m.isFetching = false m.repositoryStatus = rs + + for _, r := range rs.Remotes { + if r.LastFetched { + status := "failed" + if r.FetchErrorMsg == "" { + status = "succeeded" + } + m.prometheus.IncFetchCounter(r.Name, status) + } + } + if rs.SelectedCommitId == m.generation.SelectedCommitId && rs.SelectedBranchIsTesting == m.generation.SelectedBranchIsTesting { logrus.Debugf("The repository status is the same than the previous one") m.isRunning = false diff --git a/internal/manager/manager_test.go b/internal/manager/manager_test.go index 5639433..7e53dfe 100644 --- a/internal/manager/manager_test.go +++ b/internal/manager/manager_test.go @@ -6,11 +6,16 @@ import ( "time" "github.com/nlewo/comin/internal/deployment" + "github.com/nlewo/comin/internal/prometheus" "github.com/nlewo/comin/internal/repository" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) +type metricsMock struct{} + +func (m metricsMock) SetDeploymentInfo(commitId, status string) {} + type repositoryMock struct { rsCh chan repository.RepositoryStatus } @@ -28,7 +33,7 @@ func (r *repositoryMock) FetchAndUpdate(ctx context.Context, remoteName string) func TestRun(t *testing.T) { logrus.SetLevel(logrus.DebugLevel) r := newRepositoryMock() - m := New(r, "", "", "") + m := New(r, prometheus.New(), "", "", "") evalDone := make(chan struct{}) buildDone := make(chan struct{}) @@ -89,7 +94,7 @@ func TestRun(t *testing.T) { func TestFetchBusy(t *testing.T) { logrus.SetLevel(logrus.DebugLevel) r := newRepositoryMock() - m := New(r, "", "", "machine-id") + m := New(r, prometheus.New(), "", "", "machine-id") go m.Run() assert.Equal(t, State{}, m.GetState()) @@ -104,7 +109,7 @@ func TestFetchBusy(t *testing.T) { func TestRestartComin(t *testing.T) { logrus.SetLevel(logrus.DebugLevel) r := newRepositoryMock() - m := New(r, "", "", "machine-id") + m := New(r, prometheus.New(), "", "", "machine-id") dCh := make(chan deployment.DeploymentResult) m.deploymentResultCh = dCh isCominRestarted := false @@ -126,7 +131,7 @@ func TestRestartComin(t *testing.T) { func TestOptionnalMachineId(t *testing.T) { logrus.SetLevel(logrus.DebugLevel) r := newRepositoryMock() - m := New(r, "", "", "the-test-machine-id") + m := New(r, prometheus.New(), "", "", "the-test-machine-id") evalDone := make(chan struct{}) buildDone := make(chan struct{}) @@ -158,7 +163,7 @@ func TestOptionnalMachineId(t *testing.T) { func TestIncorrectMachineId(t *testing.T) { logrus.SetLevel(logrus.DebugLevel) r := newRepositoryMock() - m := New(r, "", "", "the-test-machine-id") + m := New(r, prometheus.New(), "", "", "the-test-machine-id") evalDone := make(chan struct{}) buildDone := make(chan struct{}) diff --git a/internal/prometheus/prometheus.go b/internal/prometheus/prometheus.go new file mode 100644 index 0000000..45659fd --- /dev/null +++ b/internal/prometheus/prometheus.go @@ -0,0 +1,50 @@ +package prometheus + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +type Prometheus struct { + promRegistry *prometheus.Registry + deploymentInfo *prometheus.GaugeVec + fetchCounter *prometheus.CounterVec +} + +func New() Prometheus { + promReg := prometheus.NewRegistry() + deploymentInfo := prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "comin_deployment_info", + Help: "Info of the last deployment.", + }, []string{"commit_id", "status"}) + fetchCounter := prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "comin_fetch_count", + Help: "Number of fetches per status", + }, []string{"remote_name", "status"}) + promReg.MustRegister(deploymentInfo) + promReg.MustRegister(fetchCounter) + return Prometheus{ + promRegistry: promReg, + deploymentInfo: deploymentInfo, + fetchCounter: fetchCounter, + } +} + +func (m Prometheus) Handler() http.Handler { + return promhttp.HandlerFor( + m.promRegistry, + promhttp.HandlerOpts{ + EnableOpenMetrics: false, + }) +} + +func (m Prometheus) IncFetchCounter(remoteName, status string) { + m.fetchCounter.With(prometheus.Labels{"remote_name": remoteName, "status": status}).Inc() +} + +func (m Prometheus) SetDeploymentInfo(commitId, status string) { + m.deploymentInfo.Reset() + m.deploymentInfo.With(prometheus.Labels{"commit_id": commitId, "status": status}).Set(1) +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 17c510c..73a5889 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -51,14 +51,12 @@ func (r *repository) FetchAndUpdate(ctx context.Context, remoteName string) (rsC } func (r *repository) Fetch(remoteName string) (err error) { - var remotes []types.Remote var found bool r.RepositoryStatus.Error = nil r.RepositoryStatus.ErrorMsg = "" if remoteName != "" { for _, remote := range r.GitConfig.Remotes { if remote.Name == remoteName { - remotes = append(remotes, remote) found = true } } @@ -67,12 +65,15 @@ func (r *repository) Fetch(remoteName string) (err error) { r.RepositoryStatus.ErrorMsg = err.Error() return fmt.Errorf("The remote '%s' doesn't exist", remoteName) } - } else { - remotes = r.GitConfig.Remotes } - for _, remote := range remotes { + for _, remote := range r.GitConfig.Remotes { repositoryStatusRemote := r.RepositoryStatus.GetRemote(remote.Name) + repositoryStatusRemote.LastFetched = false + if remoteName != "" && remote.Name != remoteName { + continue + } + repositoryStatusRemote.LastFetched = true if err = fetch(*r, remote); err != nil { repositoryStatusRemote.FetchErrorMsg = err.Error() } else { diff --git a/internal/repository/repository_status.go b/internal/repository/repository_status.go index 9371e7a..8ec768b 100644 --- a/internal/repository/repository_status.go +++ b/internal/repository/repository_status.go @@ -32,6 +32,9 @@ type Remote struct { Testing *TestingBranch `json:"testing,omitempty"` FetchedAt time.Time `json:"fetched_at,omitempty"` Fetched bool `json:"fetched,omitempty"` + // Is this remote the last festched one? This is mainly useful + // to increase Prometheus counters.b + LastFetched bool `json:"last_fetched,omitempty"` } type RepositoryStatus struct { diff --git a/internal/repository/repository_test.go b/internal/repository/repository_test.go index 059e20e..88027c3 100644 --- a/internal/repository/repository_test.go +++ b/internal/repository/repository_test.go @@ -330,6 +330,11 @@ func TestMultipleRemote(t *testing.T) { assert.Equal(t, "main", r.RepositoryStatus.SelectedBranchName) assert.Equal(t, "r1", r.RepositoryStatus.SelectedRemoteName) + assert.Equal(t, "r1", r.RepositoryStatus.Remotes[0].Name) + assert.False(t, r.RepositoryStatus.Remotes[0].LastFetched) + assert.Equal(t, "r2", r.RepositoryStatus.Remotes[1].Name) + assert.True(t, r.RepositoryStatus.Remotes[1].LastFetched) + // Fetch the r1 remote // r1/main: c1 - c2 - c3 - c4 - c5 - c6 - c8 - *c9 // r2/main: c1 - c2 - c3 - c4 - c5 - c6 diff --git a/internal/types/types.go b/internal/types/types.go index ebc131c..91b6a42 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -38,8 +38,8 @@ type Branches struct { } type HttpServer struct { - Address string `yaml:"address"` - Port int `yaml:"port"` + ListenAddress string `yaml:"listen_address"` + Port int `yaml:"port"` } type Configuration struct { @@ -47,5 +47,6 @@ type Configuration struct { StateDir string `yaml:"state_dir"` StateFilepath string `yaml:"state_filepath"` Remotes []Remote `yaml:"remotes"` - HttpServer HttpServer `yaml:"http_server"` + ApiServer HttpServer `yaml:"api_server"` + Exporter HttpServer `yaml:"exporter"` } diff --git a/readme.md b/readme.md index f58fb92..531aa37 100644 --- a/readme.md +++ b/readme.md @@ -11,6 +11,7 @@ configuration associated to the machine. - Poll multiple Git remotes to avoid SPOF - Support machines migrations - Fast iterations with local remotes +- Observable thanks to exposed Prometheus metrics ## Quick start