Files
browser/platform/validator/log_ingest.go
T

129 lines
4.6 KiB
Go

package validator
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"browser.local/platform/domain"
)
const (
MaxLogBatchEntries = 512
MaxLogQueryLimit = 10000
)
func ValidateLogBatchIngest(batch domain.LogBatchIngest) error {
var violations []string
violations = appendRequired(violations, "runEndpointId", batch.RunEndpointID)
violations = appendRequired(violations, "sessionToken", batch.SessionToken)
violations = appendRequired(violations, "logStreamId", batch.LogStreamID)
violations = appendRequired(violations, "serverInstanceId", batch.ServerInstanceID)
violations = appendRequired(violations, "streamKey", batch.StreamKey)
violations = appendRequired(violations, "checksum", batch.Checksum)
if !validLogStreamSource(batch.Source) {
violations = append(violations, "source is invalid")
}
hasSessionID := strings.TrimSpace(batch.LogSessionID) != ""
hasSessionStart := !batch.SessionStartedAt.IsZero()
// Older process streams persisted their start time before Run assigned a
// generation id. Keep those batches ingestible so a durable Run spool can
// drain without blocking newer generation-scoped console output.
if hasSessionID && !hasSessionStart {
violations = append(violations, "logSessionId and sessionStartedAt must be provided together")
}
if (hasSessionID || hasSessionStart) && batch.Source != domain.LogStreamSourceProcess {
violations = append(violations, "log session metadata is only valid for process streams")
}
if batch.FirstSeq == 0 || batch.LastSeq == 0 {
violations = append(violations, "sequence range must be positive")
}
if batch.FirstSeq > batch.LastSeq {
violations = append(violations, "firstSeq must not exceed lastSeq")
}
if batch.Compression != "" && batch.Compression != "none" {
violations = append(violations, "compression is invalid")
}
if len(batch.Entries) == 0 {
violations = append(violations, "entries must not be empty")
}
if len(batch.Entries) > MaxLogBatchEntries {
violations = append(violations, fmt.Sprintf("entries must not exceed %d", MaxLogBatchEntries))
}
if len(batch.Entries) > 0 {
expectedCount := int(batch.LastSeq - batch.FirstSeq + 1)
if expectedCount != len(batch.Entries) {
violations = append(violations, "sequence range must match entry count")
}
}
for i, entry := range batch.Entries {
if entry.Seq != batch.FirstSeq+uint64(i) {
violations = append(violations, fmt.Sprintf("entries[%d].seq must be contiguous", i))
}
for key := range entry.Fields {
if strings.TrimSpace(key) == "" {
violations = append(violations, fmt.Sprintf("entries[%d].fields key is required", i))
}
}
}
if batch.Checksum != "" {
computed, err := LogEntriesChecksum(batch.Entries)
if err != nil {
violations = append(violations, "checksum cannot be computed")
} else if batch.Checksum != computed {
violations = append(violations, "checksum does not match entries")
}
}
return finish(violations)
}
func ValidateLogStreamCursorQuery(query domain.LogStreamCursorQuery) error {
var violations []string
violations = appendRequired(violations, "logStreamId", query.LogStreamID)
if query.Limit < 0 {
violations = append(violations, "limit must not be negative")
}
if query.Limit > MaxLogQueryLimit {
violations = append(violations, fmt.Sprintf("limit must not exceed %d", MaxLogQueryLimit))
}
return finish(violations)
}
func ValidateRunLogStreamProgress(request domain.RunLogStreamProgress) error {
var violations []string
violations = appendRequired(violations, "runEndpointId", request.RunEndpointID)
violations = appendRequired(violations, "sessionToken", request.SessionToken)
violations = appendRequired(violations, "serverInstanceId", request.ServerInstanceID)
violations = appendRequired(violations, "logStreamId", request.LogStreamID)
return finish(violations)
}
func LogEntriesChecksum(entries []domain.LogEntry) (string, error) {
stable := make([]logEntryChecksumBody, len(entries))
for i, entry := range entries {
stable[i] = logEntryChecksumBody{
Seq: entry.Seq,
Timestamp: entry.Timestamp.UTC().Format("2006-01-02T15:04:05.000000000Z07:00"),
Level: entry.Level,
Line: entry.Line,
Fields: entry.Fields,
}
}
encoded, err := json.Marshal(stable)
if err != nil {
return "", err
}
sum := sha256.Sum256(encoded)
return "sha256:" + hex.EncodeToString(sum[:]), nil
}
type logEntryChecksumBody struct {
Seq uint64 `json:"seq"`
Timestamp string `json:"timestamp"`
Level string `json:"level,omitempty"`
Line string `json:"line"`
Fields map[string]string `json:"fields,omitempty"`
}