145 lines
4.4 KiB
Go
145 lines
4.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// maxRenderedBodyBytes caps how many bytes of a stored event
|
|
// body reach the event log page. Bodies come from the
|
|
// unauthenticated receiver under the 1 MB ingest cap and
|
|
// renderTemplate buffers a whole page before writing it, so
|
|
// an uncapped page of paginationPerPage events is tens of
|
|
// megabytes of resident memory per concurrent viewer.
|
|
const maxRenderedBodyBytes = 8192
|
|
|
|
// eventLogColumns is the event log's projection. The casts to
|
|
// blob are load-bearing: they make substr and length count
|
|
// bytes rather than characters, so the cap bounds the page in
|
|
// bytes whatever the payload's encoding. Cutting in SQLite
|
|
// rather than in Go is the point of the projection — an
|
|
// oversized body never becomes a Go string at all.
|
|
const eventLogColumns = "id, created_at, method, content_type, " +
|
|
"resubmitted_from_id, " +
|
|
"substr(cast(body as blob), 1, ?) AS body, " +
|
|
"length(cast(body as blob)) AS body_bytes"
|
|
|
|
// EventLogView is the display-safe projection of an event for
|
|
// the event log page, alongside DeliveryView and TargetView.
|
|
// It carries a capped body plus the true stored size, so the
|
|
// page can mark a body as truncated without ever holding the
|
|
// whole thing.
|
|
type EventLogView struct {
|
|
ID string
|
|
CreatedAt time.Time
|
|
Method string
|
|
ContentType string
|
|
|
|
// Body holds at most maxRenderedBodyBytes bytes of the
|
|
// stored body.
|
|
Body string
|
|
|
|
// BodyBytes is the true size of the stored body.
|
|
BodyBytes int64
|
|
|
|
// BodyTruncated reports that the stored body was larger
|
|
// than the cap, so the page owes the reader a marker.
|
|
BodyTruncated bool
|
|
|
|
// ResubmittedFromID names the event this one was copied
|
|
// from, empty for an event that arrived on the receiver.
|
|
ResubmittedFromID string
|
|
|
|
// ResubmitCount is how many events have been resubmitted
|
|
// from this one. Both directions are shown, because after
|
|
// a few resubmits of one captured event the log is
|
|
// otherwise a row of identical bodies with nothing saying
|
|
// which came from which.
|
|
ResubmitCount int
|
|
|
|
Deliveries []DeliveryView
|
|
}
|
|
|
|
// ResubmittedFrom reports that this event is a copy of another.
|
|
func (v EventLogView) ResubmittedFrom() bool {
|
|
return v.ResubmittedFromID != ""
|
|
}
|
|
|
|
// BodyShownBytes is how many body bytes the page is actually
|
|
// rendering, which the truncation marker reports beside the
|
|
// true size.
|
|
func (v EventLogView) BodyShownBytes() int {
|
|
return len(v.Body)
|
|
}
|
|
|
|
// eventLogRow is one row of the event log projection. Its
|
|
// body column arrives already cut to the cap by SQLite, with
|
|
// the true size beside it.
|
|
type eventLogRow struct {
|
|
ID string
|
|
CreatedAt time.Time
|
|
Method string
|
|
ContentType string
|
|
ResubmittedFromID *string
|
|
Body []byte
|
|
BodyBytes int64
|
|
}
|
|
|
|
// view projects a loaded row for rendering.
|
|
func (r *eventLogRow) view() EventLogView {
|
|
body := r.Body
|
|
truncated := r.BodyBytes > int64(len(body))
|
|
|
|
// Only a cut body can have been left mid-sequence by
|
|
// this query. A whole body is passed through exactly as
|
|
// stored, however malformed.
|
|
if truncated {
|
|
body = trimPartialRune(body)
|
|
}
|
|
|
|
var from string
|
|
if r.ResubmittedFromID != nil {
|
|
from = *r.ResubmittedFromID
|
|
}
|
|
|
|
return EventLogView{
|
|
ID: r.ID,
|
|
CreatedAt: r.CreatedAt,
|
|
Method: r.Method,
|
|
ContentType: r.ContentType,
|
|
Body: string(body),
|
|
BodyBytes: r.BodyBytes,
|
|
BodyTruncated: truncated,
|
|
ResubmittedFromID: from,
|
|
}
|
|
}
|
|
|
|
// trimPartialRune drops a trailing UTF-8 sequence that the
|
|
// byte-wise cut left incomplete, so a multi-byte rune severed
|
|
// at the cap does not surface as a mojibake tail.
|
|
//
|
|
// Bytes that are merely invalid UTF-8 are left exactly as
|
|
// stored: this service receives binary payloads, and rewriting
|
|
// them would misreport what was delivered. The distinction is
|
|
// utf8.FullRune's — it reports a complete sequence for an
|
|
// invalid encoding too, since that decodes to a width-1 error
|
|
// rune, so only a valid prefix still waiting for its
|
|
// continuation bytes is removed. A tail with no rune start in
|
|
// its last utf8.UTFMax bytes cannot be an incomplete sequence
|
|
// either, and is likewise left alone.
|
|
func trimPartialRune(b []byte) []byte {
|
|
for i := len(b) - 1; i >= 0 && len(b)-i <= utf8.UTFMax; i-- {
|
|
if !utf8.RuneStart(b[i]) {
|
|
continue
|
|
}
|
|
|
|
if utf8.FullRune(b[i:]) {
|
|
return b
|
|
}
|
|
|
|
return b[:i]
|
|
}
|
|
|
|
return b
|
|
}
|