From 0f17ed12c15c8c341bf9577fdb8c045fd2c2a1b4 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 13 Oct 2020 22:23:05 +0200 Subject: [PATCH] Add support for Healthchecks to monitor Diun watcher (#78) --- docs/assets/watch/healthchecks.png | Bin 0 -> 3152 bytes docs/config/watch.md | 46 ++++++++++++++- go.mod | 1 + go.sum | 2 + internal/app/diun.go | 30 +++++++++- internal/app/hc.go | 59 +++++++++++++++++++ internal/app/job.go | 29 +++++---- internal/config/config.go | 4 ++ internal/config/config_test.go | 4 ++ internal/config/fixtures/config.test.yml | 3 + internal/config/fixtures/config.validate.yml | 3 + internal/model/image.go | 1 + internal/model/notif.go | 29 +++++++++ internal/model/watch.go | 7 ++- internal/model/watch_healthchecks.go | 19 ++++++ 15 files changed, 217 insertions(+), 20 deletions(-) create mode 100644 docs/assets/watch/healthchecks.png create mode 100644 internal/app/hc.go create mode 100644 internal/model/watch_healthchecks.go diff --git a/docs/assets/watch/healthchecks.png b/docs/assets/watch/healthchecks.png new file mode 100644 index 0000000000000000000000000000000000000000..f1b29dc6201690c44f6f03d751ba141d5ee80cf3 GIT binary patch literal 3152 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 literal 0 HcmV?d00001 diff --git a/docs/config/watch.md b/docs/config/watch.md index b148596a..03b2aa39 100644 --- a/docs/config/watch.md +++ b/docs/config/watch.md @@ -1,6 +1,20 @@ # Watch configuration -## `workers` +## Overview + +```yaml +watch: + workers: 10 + schedule: "0 * * * *" + firstCheckNotif: false + healthchecks: + baseURL: https://hc-ping.com/ + uuid: 5bf66975-d4c7-4bf5-bcc8-b8d8a82ea278 +``` + +## Configuration + +### `workers` Maximum number of workers that will execute tasks concurrently. (default `10`) @@ -13,7 +27,7 @@ Maximum number of workers that will execute tasks concurrently. (default `10`) !!! abstract "Environment variables" * `DIUN_WATCH_WORKERS` -## `schedule` +### `schedule` [CRON expression](https://godoc.org/github.com/robfig/cron#hdr-CRON_Expression_Format) to schedule Diun watcher. (default `0 * * * *`) @@ -26,7 +40,7 @@ Maximum number of workers that will execute tasks concurrently. (default `10`) !!! abstract "Environment variables" * `DIUN_WATCH_SCHEDULE` -## `firstCheckNotif` +### `firstCheckNotif` Send notification at the very first analysis of an image. (default `false`) @@ -38,3 +52,29 @@ Send notification at the very first analysis of an image. (default `false`) !!! abstract "Environment variables" * `DIUN_WATCH_FIRSTCHECKNOTIF` + +### `healthchecks` + +Healthchecks allows to monitor Diun watcher by sending start and success notification +events to [healthchecks.io](https://healthchecks.io/). + +!!! tip + A [Docker image for Healthchecks](https://github.com/crazy-max/docker-healthchecks) is available if you want + to self-host your instance. + + + +!!! example "Config file" + ```yaml + watch: + healthchecks: + baseURL: https://hc-ping.com/ + uuid: 5bf66975-d4c7-4bf5-bcc8-b8d8a82ea278 + ``` + +!!! abstract "Environment variables" + * `DIUN_WATCH_HEALTHCHECKS_BASEURL` + * `DIUN_WATCH_HEALTHCHECKS_UUID` + +* `baseURL`: Base URL for the Healthchecks Ping API (default `https://hc-ping.com/`). +* `uuid`: UUID of an existing healthcheck (required). diff --git a/go.mod b/go.mod index 31ebed2f..28514cd5 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.13 require ( github.com/alecthomas/kong v0.2.11 github.com/containers/image/v5 v5.6.0 + github.com/crazy-max/gohealthchecks v0.2.0 github.com/crazy-max/gonfig v0.3.0 github.com/docker/docker v1.4.2-0.20200204220554-5f6d6f3f2203 github.com/docker/go-connections v0.4.0 diff --git a/go.sum b/go.sum index fa0b777c..b6bf50ff 100644 --- a/go.sum +++ b/go.sum @@ -86,6 +86,8 @@ github.com/containers/storage v1.23.5/go.mod h1:ha26Q6ngehFNhf3AWoXldvAvwI4jFe3E github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/crazy-max/gohealthchecks v0.2.0 h1:r+L9PuTwq+EpmzffDrLvxk7Ps+SVU+f2T5JHvfAHbv0= +github.com/crazy-max/gohealthchecks v0.2.0/go.mod h1:3DB7UfVoI5njYSAyqKkrBuBjf5OlmzjJZ1BlKC5+nWE= github.com/crazy-max/gonfig v0.3.0 h1:/HFdLQjXSNhImgeQgD2eXhc5svX4PhUkGSbl4fJRp4s= github.com/crazy-max/gonfig v0.3.0/go.mod h1:7vmzltkoa1RHpGB5fTom0ebnqelHdd7fzhtXTi8sVoQ= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= diff --git a/internal/app/diun.go b/internal/app/diun.go index 974eca10..c9aa33de 100644 --- a/internal/app/diun.go +++ b/internal/app/diun.go @@ -1,6 +1,7 @@ package app import ( + "net/url" "sync" "sync/atomic" "time" @@ -15,8 +16,10 @@ import ( kubernetesPrd "github.com/crazy-max/diun/v4/internal/provider/kubernetes" swarmPrd "github.com/crazy-max/diun/v4/internal/provider/swarm" "github.com/crazy-max/diun/v4/pkg/registry" + "github.com/crazy-max/gohealthchecks" "github.com/hako/durafmt" "github.com/panjf2000/ants/v2" + "github.com/pkg/errors" "github.com/robfig/cron/v3" "github.com/rs/zerolog/log" ) @@ -27,6 +30,7 @@ type Diun struct { cfg *config.Config cron *cron.Cron db *db.Client + hc *gohealthchecks.Client notif *notif.Client jobID cron.EntryID locker uint32 @@ -58,6 +62,19 @@ func New(meta model.Meta, cli model.Cli, cfg *config.Config, location *time.Loca } } + if cfg.Watch.Healthchecks != nil { + var hcBaseURL *url.URL + if len(cfg.Watch.Healthchecks.BaseURL) > 0 { + hcBaseURL, err = url.Parse(cfg.Watch.Healthchecks.BaseURL) + if err != nil { + return nil, errors.Wrap(err, "Cannot parse Healthchecks base URL") + } + } + diun.hc = gohealthchecks.NewClient(&gohealthchecks.ClientOptions{ + BaseURL: hcBaseURL, + }) + } + return diun, nil } @@ -104,10 +121,14 @@ func (di *Diun) Run() { } log.Info().Msg("Cron triggered") + entries := new(model.NotifEntries) + di.HealthchecksStart() + defer di.HealthchecksSuccess(entries) + di.wg = new(sync.WaitGroup) di.pool, _ = ants.NewPoolWithFunc(di.cfg.Watch.Workers, func(i interface{}) { job := i.(model.Job) - di.runJob(job) + entries.Add(di.runJob(job)) di.wg.Done() }, ants.WithLogger(new(logging.AntsLogger))) defer di.pool.Release() @@ -133,10 +154,17 @@ func (di *Diun) Run() { } di.wg.Wait() + log.Info(). + Int("added", entries.CountNew). + Int("updated", entries.CountUpdate). + Int("unchanged", entries.CountUnchange). + Int("failed", entries.CountError). + Msg("Jobs completed") } // Close closes diun func (di *Diun) Close() { + di.HealthchecksFail("Application closed") if di.cron != nil { di.cron.Stop() } diff --git a/internal/app/hc.go b/internal/app/hc.go new file mode 100644 index 00000000..aba13a92 --- /dev/null +++ b/internal/app/hc.go @@ -0,0 +1,59 @@ +package app + +import ( + "bytes" + "context" + "text/template" + + "github.com/crazy-max/diun/v4/internal/model" + "github.com/crazy-max/gohealthchecks" + "github.com/rs/zerolog/log" +) + +func (di *Diun) HealthchecksStart() { + if di.hc == nil { + return + } + + if err := di.hc.Start(context.Background(), gohealthchecks.PingingOptions{ + UUID: di.cfg.Watch.Healthchecks.UUID, + }); err != nil { + log.Error().Err(err).Msgf("Cannot send Healthchecks start event") + } +} + +func (di *Diun) HealthchecksSuccess(entries *model.NotifEntries) { + if di.hc == nil { + return + } + + var logsBuf bytes.Buffer + logsTpl := template.Must(template.New("").Parse(`{{ .CountTotal }} tag(s) have been scanned: +* {{ .CountNew }} new tag(s) found +* {{ .CountUpdate }} tag(s) updated +* {{ .CountUnchange }} tag(s) unchanged +* {{ .CountError }} tag(s) with error`)) + if err := logsTpl.Execute(&logsBuf, entries); err != nil { + log.Error().Err(err).Msgf("Cannot create logs for Healthchecks success event") + } + + if err := di.hc.Success(context.Background(), gohealthchecks.PingingOptions{ + UUID: di.cfg.Watch.Healthchecks.UUID, + Logs: logsBuf.String(), + }); err != nil { + log.Error().Err(err).Msgf("Cannot send Healthchecks success event") + } +} + +func (di *Diun) HealthchecksFail(logs string) { + if di.hc == nil { + return + } + + if err := di.hc.Fail(context.Background(), gohealthchecks.PingingOptions{ + UUID: di.cfg.Watch.Healthchecks.UUID, + Logs: logs, + }); err != nil { + log.Error().Err(err).Msgf("Cannot send Healthchecks fail event") + } +} diff --git a/internal/app/job.go b/internal/app/job.go index f8d6eca9..eb26d48d 100644 --- a/internal/app/job.go +++ b/internal/app/job.go @@ -141,7 +141,14 @@ func (di *Diun) createJob(job model.Job) { } } -func (di *Diun) runJob(job model.Job) { +func (di *Diun) runJob(job model.Job) (entry model.NotifEntry) { + var err error + entry = model.NotifEntry{ + Status: model.ImageStatusError, + Provider: job.Provider, + Image: job.RegImage, + } + sublog := log.With(). Str("provider", job.Provider). Str("image", job.RegImage.String()). @@ -155,7 +162,7 @@ func (di *Diun) runJob(job model.Job) { return } - liveManifest, err := job.Registry.Manifest(job.RegImage) + entry.Manifest, err = job.Registry.Manifest(job.RegImage) if err != nil { sublog.Warn().Err(err).Msg("Cannot get remote manifest") return @@ -167,19 +174,19 @@ func (di *Diun) runJob(job model.Job) { return } - status := model.ImageStatusUnchange if len(dbManifest.Name) == 0 { - status = model.ImageStatusNew + entry.Status = model.ImageStatusNew sublog.Info().Msg("New image found") - } else if !liveManifest.Created.Equal(*dbManifest.Created) { - status = model.ImageStatusUpdate + } else if !entry.Manifest.Created.Equal(*dbManifest.Created) { + entry.Status = model.ImageStatusUpdate sublog.Info().Msg("Image update found") } else { + entry.Status = model.ImageStatusUnchange sublog.Debug().Msg("No changes") return } - if err := di.db.PutManifest(job.RegImage, liveManifest); err != nil { + if err := di.db.PutManifest(job.RegImage, entry.Manifest); err != nil { sublog.Error().Err(err).Msg("Cannot write manifest to db") return } @@ -190,10 +197,6 @@ func (di *Diun) runJob(job model.Job) { return } - di.notif.Send(model.NotifEntry{ - Status: status, - Provider: job.Provider, - Image: job.RegImage, - Manifest: liveManifest, - }) + di.notif.Send(entry) + return } diff --git a/internal/config/config.go b/internal/config/config.go index 6bb08373..8a1e775e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -68,6 +68,10 @@ func (cfg *Config) validate(cli model.Cli) error { } } + if cfg.Watch.Healthchecks != nil && len(cfg.Watch.Healthchecks.UUID) == 0 { + return errors.New("Healthchecks UUID is required") + } + if cfg.Notif == nil && cli.TestNotif { return errors.New("At least one notifier is required") } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 85c56443..35feb470 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -51,6 +51,10 @@ func TestLoadFile(t *testing.T) { Workers: 100, Schedule: "*/30 * * * *", FirstCheckNotif: utl.NewTrue(), + Healthchecks: &model.Healthchecks{ + BaseURL: "https://hc-ping.com/", + UUID: "5bf66975-d4c7-4bf5-bcc8-b8d8a82ea278", + }, }, Notif: &model.Notif{ Amqp: &model.NotifAmqp{ diff --git a/internal/config/fixtures/config.test.yml b/internal/config/fixtures/config.test.yml index 84f55b03..c77c445a 100644 --- a/internal/config/fixtures/config.test.yml +++ b/internal/config/fixtures/config.test.yml @@ -5,6 +5,9 @@ watch: workers: 100 schedule: "*/30 * * * *" firstCheckNotif: true + healthchecks: + baseURL: https://hc-ping.com/ + uuid: 5bf66975-d4c7-4bf5-bcc8-b8d8a82ea278 notif: amqp: diff --git a/internal/config/fixtures/config.validate.yml b/internal/config/fixtures/config.validate.yml index 6ff79676..7eac3310 100644 --- a/internal/config/fixtures/config.validate.yml +++ b/internal/config/fixtures/config.validate.yml @@ -5,6 +5,9 @@ watch: workers: 100 schedule: "*/30 * * * *" firstCheckNotif: false + healthchecks: + baseURL: https://hc-ping.com/ + uuid: 5bf66975-d4c7-4bf5-bcc8-b8d8a82ea278 notif: amqp: diff --git a/internal/model/image.go b/internal/model/image.go index 062ede1d..15986d02 100644 --- a/internal/model/image.go +++ b/internal/model/image.go @@ -24,6 +24,7 @@ const ( ImageStatusNew = ImageStatus("new") ImageStatusUpdate = ImageStatus("update") ImageStatusUnchange = ImageStatus("unchange") + ImageStatusError = ImageStatus("error") ) // ImageStatus holds Docker image status analysis diff --git a/internal/model/notif.go b/internal/model/notif.go index 23a00a7c..add88dd0 100644 --- a/internal/model/notif.go +++ b/internal/model/notif.go @@ -4,6 +4,16 @@ import ( "github.com/crazy-max/diun/v4/pkg/registry" ) +// NotifEntries represents a list of notification entries +type NotifEntries struct { + Entries []NotifEntry + CountNew int + CountUpdate int + CountUnchange int + CountError int + CountTotal int +} + // NotifEntry represents a notification entry type NotifEntry struct { Status ImageStatus `json:"status,omitempty"` @@ -36,3 +46,22 @@ func (s *Notif) GetDefaults() *Notif { func (s *Notif) SetDefaults() { // noop } + +// Add adds a new notif entry +func (s *NotifEntries) Add(entry NotifEntry) { + s.Entries = append(s.Entries, entry) + switch entry.Status { + case ImageStatusNew: + s.CountNew++ + s.CountTotal++ + case ImageStatusUpdate: + s.CountUpdate++ + s.CountTotal++ + case ImageStatusUnchange: + s.CountUnchange++ + s.CountTotal++ + case ImageStatusError: + s.CountError++ + s.CountTotal++ + } +} diff --git a/internal/model/watch.go b/internal/model/watch.go index 9194e687..072606c9 100644 --- a/internal/model/watch.go +++ b/internal/model/watch.go @@ -6,9 +6,10 @@ import ( // Watch holds data necessary for watch configuration type Watch struct { - Workers int `yaml:"workers,omitempty" json:"workers,omitempty" validate:"required,min=1"` - Schedule string `yaml:"schedule,omitempty" json:"schedule,omitempty" validate:"required"` - FirstCheckNotif *bool `yaml:"firstCheckNotif,omitempty" json:"firstCheckNotif,omitempty" validate:"required"` + Workers int `yaml:"workers,omitempty" json:"workers,omitempty" validate:"required,min=1"` + Schedule string `yaml:"schedule,omitempty" json:"schedule,omitempty" validate:"required"` + FirstCheckNotif *bool `yaml:"firstCheckNotif,omitempty" json:"firstCheckNotif,omitempty" validate:"required"` + Healthchecks *Healthchecks `yaml:"healthchecks,omitempty" json:"healthchecks,omitempty"` } // GetDefaults gets the default values diff --git a/internal/model/watch_healthchecks.go b/internal/model/watch_healthchecks.go new file mode 100644 index 00000000..7d472e6e --- /dev/null +++ b/internal/model/watch_healthchecks.go @@ -0,0 +1,19 @@ +package model + +// Healthchecks holds data necessary for Healthchecks configuration +type Healthchecks struct { + BaseURL string `yaml:"baseURL,omitempty" json:"baseURL,omitempty"` + UUID string `yaml:"uuid,omitempty" json:"uuid,omitempty" validate:"required"` +} + +// GetDefaults gets the default values +func (s *Healthchecks) GetDefaults() *Healthchecks { + n := &Healthchecks{} + n.SetDefaults() + return n +} + +// SetDefaults sets the default values +func (s *Healthchecks) SetDefaults() { + // noop +}