111 lines
3.0 KiB
Go
111 lines
3.0 KiB
Go
package outcome
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"regexp"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
maximumIdentifierBytes = 128
|
|
MaxBatchesPerStream = 16
|
|
)
|
|
|
|
var (
|
|
ErrInvalidBatch = errors.New("invalid proxy outcome batch")
|
|
identifier = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`)
|
|
)
|
|
|
|
// Stage identifies the furthest proxy-processing boundary reached by a
|
|
// request. It is deliberately independent of a particular transport error.
|
|
type Stage uint8
|
|
|
|
const (
|
|
StageUnspecified Stage = iota
|
|
StageDial
|
|
StageProxyHandshake
|
|
StageResponseHeaders
|
|
StageTunnel
|
|
)
|
|
|
|
// ErrorClass is a bounded category suitable for aggregation and metrics.
|
|
// It must never contain raw connection, target, or credential details.
|
|
type ErrorClass string
|
|
|
|
const (
|
|
ErrorClassDial ErrorClass = "dial"
|
|
ErrorClassTimeout ErrorClass = "timeout"
|
|
ErrorClassProxyResponse ErrorClass = "proxy_response"
|
|
ErrorClassHandshake ErrorClass = "handshake"
|
|
ErrorClassRelay ErrorClass = "relay"
|
|
ErrorClassCanceled ErrorClass = "canceled"
|
|
ErrorClassInternal ErrorClass = "internal"
|
|
)
|
|
|
|
type Event struct {
|
|
ProxyID string
|
|
RoutingName string
|
|
Stage Stage
|
|
Success bool
|
|
ErrorClass ErrorClass
|
|
Latency time.Duration
|
|
ObservedAt time.Time
|
|
}
|
|
|
|
type Batch struct {
|
|
WorkerID string
|
|
SessionID string
|
|
Sequence uint64
|
|
Events []Event
|
|
}
|
|
|
|
// NormalizeBatch validates one bounded report, deep-copies it, and returns a
|
|
// digest used by the Controller's durable idempotency fence.
|
|
func NormalizeBatch(batch Batch, maximumEvents int) (Batch, [sha256.Size]byte, error) {
|
|
if !validIdentifier(batch.WorkerID) || !validIdentifier(batch.SessionID) || batch.Sequence == 0 ||
|
|
maximumEvents <= 0 || len(batch.Events) == 0 || len(batch.Events) > maximumEvents {
|
|
return Batch{}, [sha256.Size]byte{}, ErrInvalidBatch
|
|
}
|
|
normalized := batch
|
|
normalized.Events = append([]Event(nil), batch.Events...)
|
|
for index := range normalized.Events {
|
|
event := &normalized.Events[index]
|
|
if !validIdentifier(event.ProxyID) || (event.RoutingName != "" && !validIdentifier(event.RoutingName)) ||
|
|
!validStage(event.Stage) || event.Latency < 0 || event.ObservedAt.IsZero() ||
|
|
(event.Success && event.ErrorClass != "") || (!event.Success && !validErrorClass(event.ErrorClass)) {
|
|
return Batch{}, [sha256.Size]byte{}, ErrInvalidBatch
|
|
}
|
|
event.ObservedAt = event.ObservedAt.UTC()
|
|
}
|
|
payload, err := json.Marshal(normalized)
|
|
if err != nil {
|
|
return Batch{}, [sha256.Size]byte{}, ErrInvalidBatch
|
|
}
|
|
return normalized, sha256.Sum256(payload), nil
|
|
}
|
|
|
|
func validIdentifier(value string) bool {
|
|
return len(value) <= maximumIdentifierBytes && identifier.MatchString(value)
|
|
}
|
|
|
|
func validStage(stage Stage) bool {
|
|
switch stage {
|
|
case StageDial, StageProxyHandshake, StageResponseHeaders, StageTunnel:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func validErrorClass(class ErrorClass) bool {
|
|
switch class {
|
|
case ErrorClassDial, ErrorClassTimeout, ErrorClassProxyResponse, ErrorClassHandshake,
|
|
ErrorClassRelay, ErrorClassCanceled, ErrorClassInternal:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|