2023-02-03 20:05:04 +01:00
|
|
|
// Copyright (C) 2023 Opsmate, Inc.
|
|
|
|
//
|
|
|
|
// This Source Code Form is subject to the terms of the Mozilla
|
|
|
|
// Public License, v. 2.0. If a copy of the MPL was not distributed
|
|
|
|
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
//
|
|
|
|
// This software is distributed WITHOUT A WARRANTY OF ANY KIND.
|
|
|
|
// See the Mozilla Public License for details.
|
|
|
|
|
|
|
|
package monitor
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
"log"
|
|
|
|
insecurerand "math/rand"
|
2023-02-19 14:45:01 +01:00
|
|
|
"path/filepath"
|
2023-02-03 20:05:04 +01:00
|
|
|
"software.sslmate.com/src/certspotter/loglist"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2023-02-03 23:12:48 +01:00
|
|
|
reloadLogListIntervalMin = 30 * time.Minute
|
|
|
|
reloadLogListIntervalMax = 90 * time.Minute
|
2023-02-03 20:05:04 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
func randomDuration(min, max time.Duration) time.Duration {
|
|
|
|
return min + time.Duration(insecurerand.Int63n(int64(max-min+1)))
|
|
|
|
}
|
|
|
|
|
|
|
|
func reloadLogListInterval() time.Duration {
|
|
|
|
return randomDuration(reloadLogListIntervalMin, reloadLogListIntervalMax)
|
|
|
|
}
|
|
|
|
|
|
|
|
type task struct {
|
2023-02-06 03:08:01 +01:00
|
|
|
log *loglist.Log
|
2023-02-03 20:05:04 +01:00
|
|
|
stop context.CancelFunc
|
|
|
|
}
|
|
|
|
|
|
|
|
type daemon struct {
|
2023-02-06 03:08:13 +01:00
|
|
|
config *Config
|
|
|
|
taskgroup *errgroup.Group
|
|
|
|
tasks map[LogID]task
|
|
|
|
logsLoadedAt time.Time
|
|
|
|
logListToken *loglist.ModificationToken
|
|
|
|
logListError string
|
2023-02-06 03:08:01 +01:00
|
|
|
logListErrorAt time.Time
|
2023-02-03 20:05:04 +01:00
|
|
|
}
|
|
|
|
|
2023-02-06 03:08:01 +01:00
|
|
|
func (daemon *daemon) healthCheck(ctx context.Context) error {
|
2023-02-06 15:18:37 +01:00
|
|
|
if time.Since(daemon.logsLoadedAt) >= daemon.config.HealthCheckInterval {
|
2023-02-19 14:45:01 +01:00
|
|
|
textPath := filepath.Join(daemon.config.StateDir, "healthchecks", healthCheckFilename())
|
|
|
|
event := &staleLogListEvent{
|
2023-02-06 03:08:01 +01:00
|
|
|
Source: daemon.config.LogListSource,
|
|
|
|
LastSuccess: daemon.logsLoadedAt,
|
|
|
|
LastError: daemon.logListError,
|
|
|
|
LastErrorTime: daemon.logListErrorAt,
|
2023-02-19 14:45:01 +01:00
|
|
|
TextPath: textPath,
|
|
|
|
}
|
|
|
|
if err := event.save(); err != nil {
|
|
|
|
return fmt.Errorf("error saving stale log list event: %w", err)
|
|
|
|
}
|
|
|
|
if err := notify(ctx, daemon.config, event); err != nil {
|
2023-02-06 03:08:01 +01:00
|
|
|
return fmt.Errorf("error notifying about stale log list: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, task := range daemon.tasks {
|
|
|
|
if err := healthCheckLog(ctx, daemon.config, task.log); err != nil {
|
|
|
|
return fmt.Errorf("error checking health of log %q: %w", task.log.URL, err)
|
|
|
|
}
|
|
|
|
}
|
2023-02-03 20:05:04 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (daemon *daemon) startTask(ctx context.Context, ctlog *loglist.Log) task {
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
daemon.taskgroup.Go(func() error {
|
|
|
|
defer cancel()
|
|
|
|
err := monitorLogContinously(ctx, daemon.config, ctlog)
|
|
|
|
if daemon.config.Verbose {
|
|
|
|
log.Printf("task for log %s stopped with error %s", ctlog.URL, err)
|
|
|
|
}
|
|
|
|
if ctx.Err() == context.Canceled && errors.Is(err, context.Canceled) {
|
|
|
|
return nil
|
|
|
|
} else {
|
|
|
|
return fmt.Errorf("error while monitoring %s: %w", ctlog.URL, err)
|
|
|
|
}
|
|
|
|
})
|
2023-02-06 03:08:01 +01:00
|
|
|
return task{log: ctlog, stop: cancel}
|
2023-02-03 20:05:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (daemon *daemon) loadLogList(ctx context.Context) error {
|
2023-02-03 21:21:24 +01:00
|
|
|
newLogList, newToken, err := getLogList(ctx, daemon.config.LogListSource, daemon.logListToken)
|
|
|
|
if errors.Is(err, loglist.ErrNotModified) {
|
|
|
|
return nil
|
|
|
|
} else if err != nil {
|
2023-02-03 20:05:04 +01:00
|
|
|
return err
|
|
|
|
}
|
2023-02-03 21:21:24 +01:00
|
|
|
|
2023-02-03 20:05:04 +01:00
|
|
|
if daemon.config.Verbose {
|
2023-02-03 21:21:24 +01:00
|
|
|
log.Printf("fetched %d logs from %q", len(newLogList), daemon.config.LogListSource)
|
2023-02-03 20:05:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
for logID, task := range daemon.tasks {
|
2023-02-03 21:21:24 +01:00
|
|
|
if _, exists := newLogList[logID]; exists {
|
2023-02-03 20:05:04 +01:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
if daemon.config.Verbose {
|
|
|
|
log.Printf("stopping task for log %s", logID.Base64String())
|
|
|
|
}
|
|
|
|
task.stop()
|
|
|
|
delete(daemon.tasks, logID)
|
|
|
|
}
|
2023-02-03 21:21:24 +01:00
|
|
|
for logID, ctlog := range newLogList {
|
2023-02-03 20:05:04 +01:00
|
|
|
if _, isRunning := daemon.tasks[logID]; isRunning {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if daemon.config.Verbose {
|
|
|
|
log.Printf("starting task for log %s (%s)", logID.Base64String(), ctlog.URL)
|
|
|
|
}
|
|
|
|
daemon.tasks[logID] = daemon.startTask(ctx, ctlog)
|
|
|
|
}
|
|
|
|
daemon.logsLoadedAt = time.Now()
|
2023-02-03 21:21:24 +01:00
|
|
|
daemon.logListToken = newToken
|
2023-02-03 20:05:04 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (daemon *daemon) run(ctx context.Context) error {
|
|
|
|
if err := prepareStateDir(daemon.config.StateDir); err != nil {
|
|
|
|
return fmt.Errorf("error preparing state directory: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := daemon.loadLogList(ctx); err != nil {
|
|
|
|
return fmt.Errorf("error loading log list: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
reloadLogListTicker := time.NewTicker(reloadLogListInterval())
|
|
|
|
defer reloadLogListTicker.Stop()
|
|
|
|
|
2023-02-06 15:18:37 +01:00
|
|
|
healthCheckTicker := time.NewTicker(daemon.config.HealthCheckInterval)
|
2023-02-03 20:05:04 +01:00
|
|
|
defer healthCheckTicker.Stop()
|
|
|
|
|
|
|
|
for ctx.Err() == nil {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
case <-reloadLogListTicker.C:
|
|
|
|
if err := daemon.loadLogList(ctx); err != nil {
|
2023-02-06 03:08:01 +01:00
|
|
|
daemon.logListError = err.Error()
|
|
|
|
daemon.logListErrorAt = time.Now()
|
2023-02-03 20:05:04 +01:00
|
|
|
recordError(fmt.Errorf("error reloading log list (will try again later): %w", err))
|
|
|
|
}
|
|
|
|
reloadLogListTicker.Reset(reloadLogListInterval())
|
|
|
|
case <-healthCheckTicker.C:
|
|
|
|
if err := daemon.healthCheck(ctx); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ctx.Err()
|
|
|
|
}
|
|
|
|
|
|
|
|
func Run(ctx context.Context, config *Config) error {
|
|
|
|
group, ctx := errgroup.WithContext(ctx)
|
|
|
|
daemon := &daemon{
|
|
|
|
config: config,
|
|
|
|
taskgroup: group,
|
|
|
|
tasks: make(map[LogID]task),
|
|
|
|
}
|
|
|
|
group.Go(func() error { return daemon.run(ctx) })
|
|
|
|
return group.Wait()
|
|
|
|
}
|