Skip to content
quarry Artifact [cb7656cb10]
Login

Artifact [cb7656cb10]

Artifact cb7656cb1027322d17f0690a6f29e855ffff4ddf7bd5a7eee873b14cf1e401ef:


// exception.go persists the error journal. The in-memory ring
// (internal/errlog) is fast, bounded and deliberately lossy; it also
// dies with the process, which meant the Exceptions panel could only
// ever answer "what has broken since the last restart" — and a restart
// is frequently the thing that happened *because* something broke.
//
// This is the other half: one row per distinct signature, counted and
// dated, surviving restarts. Social state, not truth (RFC 0001 §11) —
// losing it loses a diagnosis, never a fact the DAG holds.
package oltp

import (
	"database/sql"
	"time"
)

const exceptionSchema = `
CREATE TABLE IF NOT EXISTS exception (
	sig     TEXT NOT NULL PRIMARY KEY, -- stable hash of the message
	message TEXT NOT NULL,
	surface TEXT NOT NULL, -- web | api | panic | browser | store | ingest | ci | mirror | chat | template
	path    TEXT NOT NULL, -- where it was FIRST seen
	repo    TEXT NOT NULL DEFAULT '',
	trace   TEXT NOT NULL DEFAULT '',
	count   INTEGER NOT NULL,
	first   TEXT NOT NULL,
	last    TEXT NOT NULL,
	-- Resolved marks a signature the maintainer has dealt with. It
	-- REOPENS on the next occurrence: an error that came back is not
	-- resolved, whatever anybody clicked.
	resolved_at TEXT NOT NULL DEFAULT ''
);
-- The panel orders by what is still happening, so that is the index.
CREATE INDEX IF NOT EXISTS exception_last ON exception(last DESC);`

// Exception is one persisted signature and its history.
type Exception struct {
	Sig      string
	Message  string
	Surface  string
	Path     string
	Repo     string
	Trace    string
	Count    int
	First    time.Time
	Last     time.Time
	Resolved bool
}

// RecordException upserts one occurrence. The first sighting keeps its
// path and trace; later ones only advance the count and the clock,
// because the first stack already describes the code path and a repeat
// adds nothing but bytes.
//
// A signature that recurs is un-resolved by the same statement that
// counts it: there is no window in which a returning error is both
// happening and marked handled.
func (d *DB) RecordException(e Exception) error {
	if e.Sig == "" {
		return nil
	}
	_, err := d.db.Exec(`
		INSERT INTO exception (sig, message, surface, path, repo, trace, count, first, last)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
		ON CONFLICT(sig) DO UPDATE SET
			count = exception.count + 1,
			last = excluded.last,
			resolved_at = ''`,
		e.Sig, e.Message, e.Surface, e.Path, e.Repo, e.Trace, e.Count,
		e.First.UTC().Format(time.RFC3339Nano), e.Last.UTC().Format(time.RFC3339Nano))
	return err
}

// Exceptions returns the most recently active signatures, newest first.
func (d *DB) Exceptions(limit int) ([]Exception, error) {
	if limit <= 0 {
		limit = 200
	}
	rows, err := d.db.Query(`
		SELECT sig, message, surface, path, repo, trace, count, first, last, resolved_at
		FROM exception ORDER BY last DESC LIMIT ?`, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []Exception
	for rows.Next() {
		var e Exception
		var first, last, resolved string
		if err := rows.Scan(&e.Sig, &e.Message, &e.Surface, &e.Path, &e.Repo,
			&e.Trace, &e.Count, &first, &last, &resolved); err != nil {
			return nil, err
		}
		e.First, _ = time.Parse(time.RFC3339Nano, first)
		e.Last, _ = time.Parse(time.RFC3339Nano, last)
		e.Resolved = resolved != ""
		out = append(out, e)
	}
	return out, rows.Err()
}

// ExceptionTotals is what the header counts: distinct signatures and
// total occurrences over the whole history, not this page's slice.
func (d *DB) ExceptionTotals() (signatures, occurrences int, err error) {
	var occ sql.NullInt64
	err = d.db.QueryRow(`SELECT count(*), sum(count) FROM exception`).Scan(&signatures, &occ)
	return signatures, int(occ.Int64), err
}

// ResolveException marks a signature handled. It is a note, not a
// promise: the next occurrence clears it (see RecordException).
func (d *DB) ResolveException(sig string) error {
	_, err := d.db.Exec(`UPDATE exception SET resolved_at = ? WHERE sig = ?`,
		time.Now().UTC().Format(time.RFC3339Nano), sig)
	return err
}