Files
observatory/internal/storage/log_rollup.go
T
gamertan 92a66db3df docs: publish Preview 19 dogfood evidence
Export the reviewed allowlisted snapshot from private source commit 05928cebd01b586cf9e9d4b8c8537a7605a6068c. This records the exact candidate, bounded capacity result, stateful migration scratch requirement, authenticated batch identity proof, and immediate live acceptance evidence.

AI-Assisted: OpenAI Codex
Signed-off-by: Cole Speelman <crspeelman@gmail.com>
2026-08-18 21:47:08 -04:00

163 lines
8.1 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only
package storage
import (
"context"
"database/sql"
"errors"
"fmt"
"math"
"strconv"
"time"
"gamertan.com/observatory/internal/model"
)
const (
logRollupVersion = 1
logRollupWindow = 5 * time.Minute
maxLogRollupGroupsBatch = model.MaxRecords
)
const logObservationBytesSQL = `64+LENGTH(project_id)+LENGTH(environment_id)+LENGTH(service_id)+LENGTH(source_id)+LENGTH(stream_id)+LENGTH(signal)+LENGTH(timestamp)+LENGTH(name)+COALESCE(LENGTH(severity),0)+COALESCE(LENGTH(body),0)+COALESCE(LENGTH(trace_id),0)+COALESCE(LENGTH(span_id),0)+COALESCE(LENGTH(correlation_id),0)+LENGTH(attributes_json)`
type logRollup struct {
projectID, environmentID, serviceID, route string
bucket int64
status, routePresent int
count, scannedBytes int64
}
func ensureLogRollups(ctx context.Context, db *sql.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin log rollup migration: %w", err)
}
defer tx.Rollback()
for _, statement := range []string{
`CREATE TABLE IF NOT EXISTS log_rollup_state (id INTEGER PRIMARY KEY CHECK(id=1),version INTEGER NOT NULL CHECK(version BETWEEN 0 AND 1))`,
`INSERT OR IGNORE INTO log_rollup_state(id,version) VALUES(1,0)`,
`CREATE TABLE IF NOT EXISTS log_status_route_rollups_5m (
organization_id TEXT NOT NULL,
project_id TEXT NOT NULL,
environment_id TEXT NOT NULL,
service_id TEXT NOT NULL,
bucket_start INTEGER NOT NULL,
status INTEGER NOT NULL CHECK(status BETWEEN 100 AND 999),
route TEXT NOT NULL,
route_present INTEGER NOT NULL CHECK(route_present IN (0,1)),
observation_count INTEGER NOT NULL CHECK(typeof(observation_count)='integer' AND observation_count > 0),
scanned_bytes INTEGER NOT NULL CHECK(typeof(scanned_bytes)='integer' AND scanned_bytes >= 0),
PRIMARY KEY(organization_id,project_id,environment_id,service_id,bucket_start,status,route,route_present)
)`,
`CREATE TABLE IF NOT EXISTS log_rollup_segments (segment_digest TEXT PRIMARY KEY)`,
`CREATE INDEX IF NOT EXISTS log_rollups_status_time ON log_status_route_rollups_5m(organization_id,status,bucket_start)`,
`CREATE INDEX IF NOT EXISTS log_rollups_scope_time ON log_status_route_rollups_5m(organization_id,project_id,environment_id,service_id,bucket_start,status)`,
} {
if _, err = tx.ExecContext(ctx, statement); err != nil {
return fmt.Errorf("migrate log rollups: %w", err)
}
}
var version int
if err = tx.QueryRowContext(ctx, `SELECT version FROM log_rollup_state WHERE id=1`).Scan(&version); err != nil {
return errors.New("read log rollup migration state")
}
if version == 0 {
if _, err = tx.ExecContext(ctx, `DELETE FROM log_status_route_rollups_5m`); err != nil {
return errors.New("clear incomplete log rollup migration")
}
if _, err = tx.ExecContext(ctx, `DELETE FROM log_rollup_segments`); err != nil {
return errors.New("clear incomplete log rollup ledger")
}
status := `json_extract(attributes_json,'$."http.status_code"')`
route := `json_extract(attributes_json,'$."http.route"')`
statement := `INSERT INTO log_status_route_rollups_5m(organization_id,project_id,environment_id,service_id,bucket_start,status,route,route_present,observation_count,scanned_bytes)
SELECT organization_id,project_id,environment_id,service_id,CAST(unixepoch(timestamp)/300 AS INTEGER)*300,CAST(` + status + ` AS INTEGER),COALESCE(CAST(` + route + ` AS TEXT),''),CASE WHEN json_type(attributes_json,'$."http.route"') IS NULL THEN 0 ELSE 1 END,COUNT(*),SUM(` + logObservationBytesSQL + `)
FROM observations WHERE signal=? AND printf('%d',CAST(` + status + ` AS INTEGER))=` + status + ` AND CAST(` + status + ` AS INTEGER) BETWEEN 100 AND 999
GROUP BY organization_id,project_id,environment_id,service_id,CAST(unixepoch(timestamp)/300 AS INTEGER)*300,CAST(` + status + ` AS INTEGER),COALESCE(CAST(` + route + ` AS TEXT),''),CASE WHEN json_type(attributes_json,'$."http.route"') IS NULL THEN 0 ELSE 1 END`
if _, err = tx.ExecContext(ctx, statement, model.SignalLogs); err != nil {
return errors.New("backfill log rollups")
}
if _, err = tx.ExecContext(ctx, `INSERT INTO log_rollup_segments(segment_digest) SELECT DISTINCT segment_digest FROM observations WHERE signal=?`, model.SignalLogs); err != nil {
return errors.New("record backfilled log rollup segments")
}
if _, err = tx.ExecContext(ctx, `UPDATE log_rollup_state SET version=? WHERE id=1 AND version=0`, logRollupVersion); err != nil {
return errors.New("activate log rollup migration")
}
}
if version != 0 && version != logRollupVersion {
return errors.New("unsupported log rollup version")
}
if err = tx.Commit(); err != nil {
return errors.New("commit log rollup migration")
}
return nil
}
func projectLogRollups(ctx context.Context, tx *sql.Tx, scope model.Scope, batch model.Batch, segmentDigest string, observationBytes []int64) error {
if batch.Signal != model.SignalLogs {
return nil
}
if len(observationBytes) != len(batch.Records) {
return errors.New("log projection byte evidence is incomplete")
}
ledger, err := tx.ExecContext(ctx, `INSERT OR IGNORE INTO log_rollup_segments(segment_digest) VALUES(?)`, segmentDigest)
if err != nil {
return errors.New("record log rollup segment")
}
inserted, err := ledger.RowsAffected()
if err != nil {
return errors.New("inspect log rollup segment")
}
if inserted == 0 {
return nil
}
groups := map[string]*logRollup{}
for index, observation := range batch.Records {
statusText, ok := observation.Attributes["http.status_code"]
if !ok {
continue
}
status, statusErr := strconv.Atoi(statusText)
if statusErr != nil || strconv.Itoa(status) != statusText || status < 100 || status > 999 {
continue
}
route, routeOK := observation.Attributes["http.route"]
routePresent := 0
if routeOK {
routePresent = 1
}
bucket := observation.Timestamp.UTC().Truncate(logRollupWindow).Unix()
key := scope.ProjectID + "\x00" + scope.EnvironmentID + "\x00" + scope.ServiceID + "\x00" + strconv.FormatInt(bucket, 10) + "\x00" + statusText + "\x00" + strconv.Itoa(routePresent) + "\x00" + route
group := groups[key]
if group == nil {
group = &logRollup{projectID: scope.ProjectID, environmentID: scope.EnvironmentID, serviceID: scope.ServiceID, bucket: bucket, status: status, route: route, routePresent: routePresent}
groups[key] = group
if len(groups) > maxLogRollupGroupsBatch {
return fmt.Errorf("log batch exceeds %d rollup groups", maxLogRollupGroupsBatch)
}
}
bytes := observationBytes[index]
if bytes < 0 {
return errors.New("log projection byte evidence is invalid")
}
if group.count == math.MaxInt64 || bytes > math.MaxInt64-group.scannedBytes {
return errors.New("log rollup count exceeds integer range")
}
group.count++
group.scannedBytes += bytes
}
for _, group := range groups {
_, err = tx.ExecContext(ctx, `INSERT INTO log_status_route_rollups_5m(organization_id,project_id,environment_id,service_id,bucket_start,status,route,route_present,observation_count,scanned_bytes) VALUES(?,?,?,?,?,?,?,?,?,?) ON CONFLICT(organization_id,project_id,environment_id,service_id,bucket_start,status,route,route_present) DO UPDATE SET observation_count=log_status_route_rollups_5m.observation_count+excluded.observation_count,scanned_bytes=log_status_route_rollups_5m.scanned_bytes+excluded.scanned_bytes`, scope.OrganizationID, group.projectID, group.environmentID, group.serviceID, group.bucket, group.status, group.route, group.routePresent, group.count, group.scannedBytes)
if err != nil {
return errors.New("merge log rollup")
}
}
return nil
}
func projectedObservationBytes(scope model.Scope, batch model.Batch, observation model.Observation, attributesBytes int) int64 {
return int64(64 + len(scope.ProjectID) + len(scope.EnvironmentID) + len(scope.ServiceID) + len(batch.SourceID) + len(batch.StreamID) + len(batch.Signal) + len(observation.Timestamp.UTC().Format(time.RFC3339Nano)) + len(observation.Name) + len(observation.Severity) + len(observation.Body) + len(observation.TraceID) + len(observation.SpanID) + len(observation.CorrelationID) + attributesBytes)
}