143 lines
5.0 KiB
Go
143 lines
5.0 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 && !logLineChecksumMatches(batch) {
|
|
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,
|
|
Redacted: entry.Redacted,
|
|
}
|
|
}
|
|
encoded, err := json.Marshal(stable)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sum := sha256.Sum256(encoded)
|
|
return "sha256:" + hex.EncodeToString(sum[:]), nil
|
|
}
|
|
|
|
func logLineChecksumMatches(batch domain.LogBatchIngest) bool {
|
|
if len(batch.Entries) != 1 {
|
|
return false
|
|
}
|
|
return batch.Checksum == LogLineChecksum(batch.Entries[0].Line)
|
|
}
|
|
|
|
func LogLineChecksum(value string) string {
|
|
sum := sha256.Sum256([]byte(value))
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
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"`
|
|
Redacted bool `json:"redacted"`
|
|
}
|