mirror of
				https://github.com/SSLMate/certspotter.git
				synced 2025-07-03 10:47:17 +02:00 
			
		
		
		
	Store log errors in state directory
Instead of writing log errors to stderr, write them to a file in the state directory. When reporting a health check failure, include the path to the file and the last several lines. Log files are named by date, and the last 7 days are kept. Closes #106
This commit is contained in:
		
							parent
							
								
									5a8dd2ca82
								
							
						
					
					
						commit
						4fbbc5818e
					
				@ -192,7 +192,6 @@ func main() {
 | 
				
			|||||||
		ScriptDir: defaultScriptDir(),
 | 
							ScriptDir: defaultScriptDir(),
 | 
				
			||||||
		Email:     flags.email,
 | 
							Email:     flags.email,
 | 
				
			||||||
		Stdout:    flags.stdout,
 | 
							Stdout:    flags.stdout,
 | 
				
			||||||
		Quiet:     !flags.verbose,
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	config := &monitor.Config{
 | 
						config := &monitor.Config{
 | 
				
			||||||
		LogListSource:       flags.logs,
 | 
							LogListSource:       flags.logs,
 | 
				
			||||||
@ -241,6 +240,19 @@ func main() {
 | 
				
			|||||||
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
 | 
						ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
 | 
				
			||||||
	defer stop()
 | 
						defer stop()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go func() {
 | 
				
			||||||
 | 
							ticker := time.NewTicker(24*time.Hour)
 | 
				
			||||||
 | 
							defer ticker.Stop()
 | 
				
			||||||
 | 
							for {
 | 
				
			||||||
 | 
								fsstate.PruneOldErrors()
 | 
				
			||||||
 | 
								select {
 | 
				
			||||||
 | 
								case <-ctx.Done():
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								case <-ticker.C:
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := monitor.Run(ctx, config); ctx.Err() == context.Canceled && errors.Is(err, context.Canceled) {
 | 
						if err := monitor.Run(ctx, config); ctx.Err() == context.Canceled && errors.Is(err, context.Canceled) {
 | 
				
			||||||
		if flags.verbose {
 | 
							if flags.verbose {
 | 
				
			||||||
			fmt.Fprintf(os.Stderr, "%s: exiting due to SIGINT or SIGTERM\n", programName)
 | 
								fmt.Fprintf(os.Stderr, "%s: exiting due to SIGINT or SIGTERM\n", programName)
 | 
				
			||||||
 | 
				
			|||||||
@ -50,11 +50,21 @@ type daemon struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (daemon *daemon) healthCheck(ctx context.Context) error {
 | 
					func (daemon *daemon) healthCheck(ctx context.Context) error {
 | 
				
			||||||
	if time.Since(daemon.logsLoadedAt) >= daemon.config.HealthCheckInterval {
 | 
						if time.Since(daemon.logsLoadedAt) >= daemon.config.HealthCheckInterval {
 | 
				
			||||||
 | 
							errors, err := daemon.config.State.GetErrors(ctx, nil, recentErrorCount)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("error getting recent errors: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							var errorsDir string
 | 
				
			||||||
 | 
							if fsstate, ok := daemon.config.State.(*FilesystemState); ok {
 | 
				
			||||||
 | 
								errorsDir = fsstate.errorDir(nil)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		info := &StaleLogListInfo{
 | 
							info := &StaleLogListInfo{
 | 
				
			||||||
			Source:        daemon.config.LogListSource,
 | 
								Source:        daemon.config.LogListSource,
 | 
				
			||||||
			LastSuccess:   daemon.logsLoadedAt,
 | 
								LastSuccess:   daemon.logsLoadedAt,
 | 
				
			||||||
			LastError:     daemon.logListError,
 | 
								LastError:     daemon.logListError,
 | 
				
			||||||
			LastErrorTime: daemon.logListErrorAt,
 | 
								LastErrorTime: daemon.logListErrorAt,
 | 
				
			||||||
 | 
								RecentErrors:  errors,
 | 
				
			||||||
 | 
								ErrorsDir:     errorsDir,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if err := daemon.config.State.NotifyHealthCheckFailure(ctx, nil, info); err != nil {
 | 
							if err := daemon.config.State.NotifyHealthCheckFailure(ctx, nil, info); err != nil {
 | 
				
			||||||
			return fmt.Errorf("error notifying about stale log list: %w", err)
 | 
								return fmt.Errorf("error notifying about stale log list: %w", err)
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,9 @@ import (
 | 
				
			|||||||
	"encoding/hex"
 | 
						"encoding/hex"
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func randomFileSuffix() string {
 | 
					func randomFileSuffix() string {
 | 
				
			||||||
@ -69,3 +71,47 @@ func fileExists(filename string) bool {
 | 
				
			|||||||
	_, err := os.Lstat(filename)
 | 
						_, err := os.Lstat(filename)
 | 
				
			||||||
	return err == nil
 | 
						return err == nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func tailFile(filename string, linesWanted int) ([]byte, int, error) {
 | 
				
			||||||
 | 
						file, err := os.Open(filename)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer file.Close()
 | 
				
			||||||
 | 
						return tail(file, linesWanted, 4096)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func tail(r io.ReadSeeker, linesWanted int, chunkSize int) ([]byte, int, error) {
 | 
				
			||||||
 | 
						var buf []byte
 | 
				
			||||||
 | 
						linesGot := 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						offset, err := r.Seek(0, io.SeekEnd)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for offset > 0 {
 | 
				
			||||||
 | 
							readSize := chunkSize
 | 
				
			||||||
 | 
							if offset < int64(readSize) {
 | 
				
			||||||
 | 
								readSize = int(offset)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							offset -= int64(readSize)
 | 
				
			||||||
 | 
							if _, err := r.Seek(offset, io.SeekStart); err != nil {
 | 
				
			||||||
 | 
								return nil, 0, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							buf = slices.Grow(buf, readSize)
 | 
				
			||||||
 | 
							copy(buf[readSize:len(buf)+readSize], buf)
 | 
				
			||||||
 | 
							buf = buf[:len(buf)+readSize]
 | 
				
			||||||
 | 
							if _, err := io.ReadFull(r, buf[:readSize]); err != nil {
 | 
				
			||||||
 | 
								return nil, 0, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for i := readSize; i > 0; i-- {
 | 
				
			||||||
 | 
								if buf[i-1] == '\n' {
 | 
				
			||||||
 | 
									if linesGot == linesWanted {
 | 
				
			||||||
 | 
										return buf[i:], linesGot, nil
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									linesGot++
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return buf, linesGot, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -20,12 +20,17 @@ import (
 | 
				
			|||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path/filepath"
 | 
						"path/filepath"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"software.sslmate.com/src/certspotter/cttypes"
 | 
						"software.sslmate.com/src/certspotter/cttypes"
 | 
				
			||||||
	"software.sslmate.com/src/certspotter/loglist"
 | 
						"software.sslmate.com/src/certspotter/loglist"
 | 
				
			||||||
	"software.sslmate.com/src/certspotter/merkletree"
 | 
						"software.sslmate.com/src/certspotter/merkletree"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const keepErrorDays = 7
 | 
				
			||||||
 | 
					const errorDateFormat = "2006-01-02"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type FilesystemState struct {
 | 
					type FilesystemState struct {
 | 
				
			||||||
	StateDir  string
 | 
						StateDir  string
 | 
				
			||||||
	CacheDir  string
 | 
						CacheDir  string
 | 
				
			||||||
@ -34,7 +39,7 @@ type FilesystemState struct {
 | 
				
			|||||||
	ScriptDir string
 | 
						ScriptDir string
 | 
				
			||||||
	Email     []string
 | 
						Email     []string
 | 
				
			||||||
	Stdout    bool
 | 
						Stdout    bool
 | 
				
			||||||
	Quiet     bool
 | 
						errorMu   sync.Mutex
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *FilesystemState) logStateDir(logID LogID) string {
 | 
					func (s *FilesystemState) logStateDir(logID LogID) string {
 | 
				
			||||||
@ -57,8 +62,9 @@ func (s *FilesystemState) PrepareLog(ctx context.Context, logID LogID) error {
 | 
				
			|||||||
		sthsDirPath         = filepath.Join(stateDirPath, "unverified_sths")
 | 
							sthsDirPath         = filepath.Join(stateDirPath, "unverified_sths")
 | 
				
			||||||
		malformedDirPath    = filepath.Join(stateDirPath, "malformed_entries")
 | 
							malformedDirPath    = filepath.Join(stateDirPath, "malformed_entries")
 | 
				
			||||||
		healthchecksDirPath = filepath.Join(stateDirPath, "healthchecks")
 | 
							healthchecksDirPath = filepath.Join(stateDirPath, "healthchecks")
 | 
				
			||||||
 | 
							errorsDirPath       = filepath.Join(stateDirPath, "errors")
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	for _, dirPath := range []string{stateDirPath, sthsDirPath, malformedDirPath, healthchecksDirPath} {
 | 
						for _, dirPath := range []string{stateDirPath, sthsDirPath, malformedDirPath, healthchecksDirPath, errorsDirPath} {
 | 
				
			||||||
		if err := os.Mkdir(dirPath, 0777); err != nil && !errors.Is(err, fs.ErrExist) {
 | 
							if err := os.Mkdir(dirPath, 0777); err != nil && !errors.Is(err, fs.ErrExist) {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -227,6 +233,13 @@ func (s *FilesystemState) healthCheckDir(ctlog *loglist.Log) string {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *FilesystemState) errorDir(ctlog *loglist.Log) string {
 | 
				
			||||||
 | 
						if ctlog == nil {
 | 
				
			||||||
 | 
							return filepath.Join(s.StateDir, "errors")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return filepath.Join(s.logStateDir(ctlog.LogID), "errors")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *FilesystemState) NotifyHealthCheckFailure(ctx context.Context, ctlog *loglist.Log, info HealthCheckFailure) error {
 | 
					func (s *FilesystemState) NotifyHealthCheckFailure(ctx context.Context, ctlog *loglist.Log, info HealthCheckFailure) error {
 | 
				
			||||||
	textPath := filepath.Join(s.healthCheckDir(ctlog), healthCheckFilename())
 | 
						textPath := filepath.Join(s.healthCheckDir(ctlog), healthCheckFilename())
 | 
				
			||||||
	environ := []string{
 | 
						environ := []string{
 | 
				
			||||||
@ -248,13 +261,80 @@ func (s *FilesystemState) NotifyHealthCheckFailure(ctx context.Context, ctlog *l
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *FilesystemState) NotifyError(ctx context.Context, ctlog *loglist.Log, err error) error {
 | 
					func (s *FilesystemState) NotifyError(ctx context.Context, ctlog *loglist.Log, notifyErr error) error {
 | 
				
			||||||
	if !s.Quiet {
 | 
						var (
 | 
				
			||||||
		if ctlog == nil {
 | 
							now      = time.Now()
 | 
				
			||||||
			log.Print(err)
 | 
							filePath = filepath.Join(s.errorDir(ctlog), now.Format(errorDateFormat))
 | 
				
			||||||
		} else {
 | 
							line     = now.Format(time.RFC3339) + " " + notifyErr.Error() + "\n"
 | 
				
			||||||
			log.Print(ctlog.GetMonitoringURL(), ": ", err)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						s.errorMu.Lock()
 | 
				
			||||||
 | 
						defer s.errorMu.Unlock()
 | 
				
			||||||
 | 
						file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer file.Close()
 | 
				
			||||||
 | 
						if _, err := file.WriteString(line); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return file.Close()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *FilesystemState) GetErrors(ctx context.Context, ctlog *loglist.Log, count int) (string, error) {
 | 
				
			||||||
 | 
						dir := s.errorDir(ctlog)
 | 
				
			||||||
 | 
						now := time.Now()
 | 
				
			||||||
 | 
						var buf []byte
 | 
				
			||||||
 | 
						for daysBack := 0; count > 0 && daysBack < keepErrorDays; daysBack++ {
 | 
				
			||||||
 | 
							datePath := filepath.Join(dir, now.AddDate(0, 0, -daysBack).Format(errorDateFormat))
 | 
				
			||||||
 | 
							dateBuf, dateLines, err := tailFile(datePath, count)
 | 
				
			||||||
 | 
							if errors.Is(err, fs.ErrNotExist) {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							} else if err != nil {
 | 
				
			||||||
 | 
								return "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							buf = append(dateBuf, buf...)
 | 
				
			||||||
 | 
							count -= dateLines
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return string(buf), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *FilesystemState) PruneOldErrors() {
 | 
				
			||||||
 | 
						cutoff := time.Now().AddDate(0, 0, -keepErrorDays)
 | 
				
			||||||
 | 
						pruneDir := func(dir string) {
 | 
				
			||||||
 | 
							entries, err := os.ReadDir(dir)
 | 
				
			||||||
 | 
							if errors.Is(err, fs.ErrNotExist) {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							} else if err != nil {
 | 
				
			||||||
 | 
								log.Printf("unable to read error directory: %s", err)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for _, entry := range entries {
 | 
				
			||||||
 | 
								if entry.IsDir() {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								date, err := time.Parse(errorDateFormat, entry.Name())
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									continue
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if date.Before(cutoff) {
 | 
				
			||||||
 | 
									if err := os.Remove(filepath.Join(dir, entry.Name())); err != nil && !errors.Is(err, fs.ErrNotExist) {
 | 
				
			||||||
 | 
										log.Printf("unable to remove old error file: %s", err)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
	return nil
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						pruneDir(filepath.Join(s.StateDir, "errors"))
 | 
				
			||||||
 | 
						logsDir := filepath.Join(s.StateDir, "logs")
 | 
				
			||||||
 | 
						logDirs, err := os.ReadDir(logsDir)
 | 
				
			||||||
 | 
						if err != nil && !errors.Is(err, fs.ErrNotExist) {
 | 
				
			||||||
 | 
							log.Printf("unable to read logs directory: %s", err)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, d := range logDirs {
 | 
				
			||||||
 | 
							if !d.IsDir() {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							pruneDir(filepath.Join(logsDir, d.Name(), "errors"))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -19,6 +19,8 @@ import (
 | 
				
			|||||||
	"software.sslmate.com/src/certspotter/loglist"
 | 
						"software.sslmate.com/src/certspotter/loglist"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const recentErrorCount = 10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func healthCheckFilename() string {
 | 
					func healthCheckFilename() string {
 | 
				
			||||||
	return time.Now().UTC().Format(time.RFC3339) + ".txt"
 | 
						return time.Now().UTC().Format(time.RFC3339) + ".txt"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -48,20 +50,37 @@ func healthCheckLog(ctx context.Context, config *Config, ctlog *loglist.Log) err
 | 
				
			|||||||
		return fmt.Errorf("error loading STHs: %w", err)
 | 
							return fmt.Errorf("error loading STHs: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var errorsDir string
 | 
				
			||||||
 | 
						if fsstate, ok := config.State.(*FilesystemState); ok {
 | 
				
			||||||
 | 
							errorsDir = fsstate.errorDir(ctlog)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if len(sths) == 0 {
 | 
						if len(sths) == 0 {
 | 
				
			||||||
 | 
							errors, err := config.State.GetErrors(ctx, ctlog, recentErrorCount)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("error getting recent errors: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		info := &StaleSTHInfo{
 | 
							info := &StaleSTHInfo{
 | 
				
			||||||
			Log:          ctlog,
 | 
								Log:          ctlog,
 | 
				
			||||||
			LastSuccess:  lastSuccess,
 | 
								LastSuccess:  lastSuccess,
 | 
				
			||||||
			LatestSTH:    verifiedSTH,
 | 
								LatestSTH:    verifiedSTH,
 | 
				
			||||||
 | 
								RecentErrors: errors,
 | 
				
			||||||
 | 
								ErrorsDir:    errorsDir,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if err := config.State.NotifyHealthCheckFailure(ctx, ctlog, info); err != nil {
 | 
							if err := config.State.NotifyHealthCheckFailure(ctx, ctlog, info); err != nil {
 | 
				
			||||||
			return fmt.Errorf("error notifying about stale STH: %w", err)
 | 
								return fmt.Errorf("error notifying about stale STH: %w", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
 | 
							errors, err := config.State.GetErrors(ctx, ctlog, recentErrorCount)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("error getting recent errors: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		info := &BacklogInfo{
 | 
							info := &BacklogInfo{
 | 
				
			||||||
			Log:          ctlog,
 | 
								Log:          ctlog,
 | 
				
			||||||
			LatestSTH:    sths[len(sths)-1],
 | 
								LatestSTH:    sths[len(sths)-1],
 | 
				
			||||||
			Position:     position,
 | 
								Position:     position,
 | 
				
			||||||
 | 
								RecentErrors: errors,
 | 
				
			||||||
 | 
								ErrorsDir:    errorsDir,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if err := config.State.NotifyHealthCheckFailure(ctx, ctlog, info); err != nil {
 | 
							if err := config.State.NotifyHealthCheckFailure(ctx, ctlog, info); err != nil {
 | 
				
			||||||
			return fmt.Errorf("error notifying about backlog: %w", err)
 | 
								return fmt.Errorf("error notifying about backlog: %w", err)
 | 
				
			||||||
@ -80,12 +99,16 @@ type StaleSTHInfo struct {
 | 
				
			|||||||
	Log          *loglist.Log
 | 
						Log          *loglist.Log
 | 
				
			||||||
	LastSuccess  time.Time               // may be zero
 | 
						LastSuccess  time.Time               // may be zero
 | 
				
			||||||
	LatestSTH    *cttypes.SignedTreeHead // may be nil
 | 
						LatestSTH    *cttypes.SignedTreeHead // may be nil
 | 
				
			||||||
 | 
						RecentErrors string
 | 
				
			||||||
 | 
						ErrorsDir    string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type BacklogInfo struct {
 | 
					type BacklogInfo struct {
 | 
				
			||||||
	Log          *loglist.Log
 | 
						Log          *loglist.Log
 | 
				
			||||||
	LatestSTH    *StoredSTH
 | 
						LatestSTH    *StoredSTH
 | 
				
			||||||
	Position     uint64
 | 
						Position     uint64
 | 
				
			||||||
 | 
						RecentErrors string
 | 
				
			||||||
 | 
						ErrorsDir    string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type StaleLogListInfo struct {
 | 
					type StaleLogListInfo struct {
 | 
				
			||||||
@ -93,6 +116,8 @@ type StaleLogListInfo struct {
 | 
				
			|||||||
	LastSuccess   time.Time
 | 
						LastSuccess   time.Time
 | 
				
			||||||
	LastError     string
 | 
						LastError     string
 | 
				
			||||||
	LastErrorTime time.Time
 | 
						LastErrorTime time.Time
 | 
				
			||||||
 | 
						RecentErrors  string
 | 
				
			||||||
 | 
						ErrorsDir     string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (e *StaleSTHInfo) LastSuccessString() string {
 | 
					func (e *StaleSTHInfo) LastSuccessString() string {
 | 
				
			||||||
@ -120,33 +145,45 @@ func (e *StaleSTHInfo) Text() string {
 | 
				
			|||||||
	text := new(strings.Builder)
 | 
						text := new(strings.Builder)
 | 
				
			||||||
	fmt.Fprintf(text, "certspotter has been unable to contact %s since %s. Consequentially, certspotter may fail to notify you about certificates in this log.\n", e.Log.GetMonitoringURL(), e.LastSuccessString())
 | 
						fmt.Fprintf(text, "certspotter has been unable to contact %s since %s. Consequentially, certspotter may fail to notify you about certificates in this log.\n", e.Log.GetMonitoringURL(), e.LastSuccessString())
 | 
				
			||||||
	fmt.Fprintf(text, "\n")
 | 
						fmt.Fprintf(text, "\n")
 | 
				
			||||||
	fmt.Fprintf(text, "For details, enable -verbose and see certspotter's stderr output.\n")
 | 
					 | 
				
			||||||
	fmt.Fprintf(text, "\n")
 | 
					 | 
				
			||||||
	if e.LatestSTH != nil {
 | 
						if e.LatestSTH != nil {
 | 
				
			||||||
		fmt.Fprintf(text, "Latest known log size = %d\n", e.LatestSTH.TreeSize)
 | 
							fmt.Fprintf(text, "Latest known log size = %d\n", e.LatestSTH.TreeSize)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		fmt.Fprintf(text, "Latest known log size = none\n")
 | 
							fmt.Fprintf(text, "Latest known log size = none\n")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						if e.RecentErrors != "" {
 | 
				
			||||||
 | 
							fmt.Fprintf(text, "\n")
 | 
				
			||||||
 | 
							fmt.Fprintf(text, "Recent errors (see %s for complete records):\n", e.ErrorsDir)
 | 
				
			||||||
 | 
							fmt.Fprintf(text, "\n")
 | 
				
			||||||
 | 
							fmt.Fprint(text, e.RecentErrors)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return text.String()
 | 
						return text.String()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
func (e *BacklogInfo) Text() string {
 | 
					func (e *BacklogInfo) Text() string {
 | 
				
			||||||
	text := new(strings.Builder)
 | 
						text := new(strings.Builder)
 | 
				
			||||||
	fmt.Fprintf(text, "certspotter has been unable to download entries from %s in a timely manner. Consequentially, certspotter may be slow to notify you about certificates in this log.\n", e.Log.GetMonitoringURL())
 | 
						fmt.Fprintf(text, "certspotter has been unable to download entries from %s in a timely manner. Consequentially, certspotter may be slow to notify you about certificates in this log.\n", e.Log.GetMonitoringURL())
 | 
				
			||||||
	fmt.Fprintf(text, "\n")
 | 
						fmt.Fprintf(text, "\n")
 | 
				
			||||||
	fmt.Fprintf(text, "For details, enable -verbose and see certspotter's stderr output.\n")
 | 
					 | 
				
			||||||
	fmt.Fprintf(text, "\n")
 | 
					 | 
				
			||||||
	fmt.Fprintf(text, "Current log size = %d (as of %s)\n", e.LatestSTH.TreeSize, e.LatestSTH.StoredAt)
 | 
						fmt.Fprintf(text, "Current log size = %d (as of %s)\n", e.LatestSTH.TreeSize, e.LatestSTH.StoredAt)
 | 
				
			||||||
	fmt.Fprintf(text, "Current position = %d\n", e.Position)
 | 
						fmt.Fprintf(text, "Current position = %d\n", e.Position)
 | 
				
			||||||
	fmt.Fprintf(text, "         Backlog = %d\n", e.Backlog())
 | 
						fmt.Fprintf(text, "         Backlog = %d\n", e.Backlog())
 | 
				
			||||||
 | 
						if e.RecentErrors != "" {
 | 
				
			||||||
 | 
							fmt.Fprintf(text, "\n")
 | 
				
			||||||
 | 
							fmt.Fprintf(text, "Recent errors (see %s for complete records):\n", e.ErrorsDir)
 | 
				
			||||||
 | 
							fmt.Fprintf(text, "\n")
 | 
				
			||||||
 | 
							fmt.Fprint(text, e.RecentErrors)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return text.String()
 | 
						return text.String()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
func (e *StaleLogListInfo) Text() string {
 | 
					func (e *StaleLogListInfo) Text() string {
 | 
				
			||||||
	text := new(strings.Builder)
 | 
						text := new(strings.Builder)
 | 
				
			||||||
	fmt.Fprintf(text, "certspotter has been unable to retrieve the log list from %s since %s.\n", e.Source, e.LastSuccess)
 | 
						fmt.Fprintf(text, "certspotter has been unable to retrieve the log list from %s since %s.\n", e.Source, e.LastSuccess)
 | 
				
			||||||
	fmt.Fprintf(text, "\n")
 | 
						fmt.Fprintf(text, "\n")
 | 
				
			||||||
	fmt.Fprintf(text, "Last error (at %s): %s\n", e.LastErrorTime, e.LastError)
 | 
					 | 
				
			||||||
	fmt.Fprintf(text, "\n")
 | 
					 | 
				
			||||||
	fmt.Fprintf(text, "Consequentially, certspotter may not be monitoring all logs, and might fail to detect certificates.\n")
 | 
						fmt.Fprintf(text, "Consequentially, certspotter may not be monitoring all logs, and might fail to detect certificates.\n")
 | 
				
			||||||
 | 
						if e.RecentErrors != "" {
 | 
				
			||||||
 | 
							fmt.Fprintf(text, "\n")
 | 
				
			||||||
 | 
							fmt.Fprintf(text, "Recent errors (see %s for complete records):\n", e.ErrorsDir)
 | 
				
			||||||
 | 
							fmt.Fprintf(text, "\n")
 | 
				
			||||||
 | 
							fmt.Fprint(text, e.RecentErrors)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return text.String()
 | 
						return text.String()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -85,4 +85,7 @@ type StateProvider interface {
 | 
				
			|||||||
	// not associated with a log.  Note that most errors are transient, and
 | 
						// not associated with a log.  Note that most errors are transient, and
 | 
				
			||||||
	// certspotter will retry the failed operation later.
 | 
						// certspotter will retry the failed operation later.
 | 
				
			||||||
	NotifyError(context.Context, *loglist.Log, error) error
 | 
						NotifyError(context.Context, *loglist.Log, error) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Retrieve the specified number of most recent errors.
 | 
				
			||||||
 | 
						GetErrors(context.Context, *loglist.Log, int) (string, error)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -145,7 +145,7 @@ func prepareStateDir(stateDir string) error {
 | 
				
			|||||||
		return fmt.Errorf("%s was created by a newer version of certspotter; upgrade to the latest version of certspotter or remove this directory to start from scratch", stateDir)
 | 
							return fmt.Errorf("%s was created by a newer version of certspotter; upgrade to the latest version of certspotter or remove this directory to start from scratch", stateDir)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, subdir := range []string{"certs", "logs", "healthchecks"} {
 | 
						for _, subdir := range []string{"certs", "logs", "healthchecks", "errors"} {
 | 
				
			||||||
		if err := os.Mkdir(filepath.Join(stateDir, subdir), 0777); err != nil && !errors.Is(err, fs.ErrExist) {
 | 
							if err := os.Mkdir(filepath.Join(stateDir, subdir), 0777); err != nil && !errors.Is(err, fs.ErrExist) {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user