Make terminal log stream live-only by default
This commit is contained in:
@@ -15,13 +15,14 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultLogEventHistoryLimit = 100
|
||||
defaultLogEventHistoryLimit = 0
|
||||
maxLogEventHistoryLimit = 10000
|
||||
logEventHeartbeatInterval = 15 * time.Second
|
||||
managedLogSessionIDPrefix = "log-session:"
|
||||
)
|
||||
|
||||
// serverLogEvents streams platform-accepted server log history and live append events for the terminal drawer.
|
||||
// serverLogEvents streams platform-accepted live append events for the terminal drawer.
|
||||
// Callers can opt into a bounded current-session replay with historyLimit.
|
||||
func (h *coreHandlers) serverLogEvents(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeMethodNotAllowed(w, http.MethodGet)
|
||||
@@ -221,6 +222,9 @@ func (h *coreHandlers) writeCurrentLogSession(w http.ResponseWriter, serverInsta
|
||||
return nil, err
|
||||
}
|
||||
for _, stream := range active.streams {
|
||||
if historyLimit == 0 {
|
||||
emittedThrough[stream.ID] = stream.LatestSeq
|
||||
}
|
||||
if err := writeSSEJSON(w, "stream", "", dto.LogStreamFromDomain(stream)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -95,6 +95,63 @@ func TestLogEventsSSEReplaysHistory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogEventsSSEDefaultsToLiveOnly(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
hello := createLogIngestAPIFixtures(t, router)
|
||||
assertStatus(t, performJSON(t, router, http.MethodPost, "/api/v1/run/logs/batches", validLogBatchRequest(t, hello.SessionToken, 1, 2)), http.StatusOK)
|
||||
|
||||
recorder := performCancelledSSE(t, router, "/api/v1/server-instances/server-1/logs/events")
|
||||
assertStatus(t, recorder, http.StatusOK)
|
||||
body := recorder.Body.String()
|
||||
if !strings.Contains(body, "event: stream") || !strings.Contains(body, "event: ready") || strings.Contains(body, "event: log") {
|
||||
t.Fatalf("expected live-only SSE snapshot without history logs, body=%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogEventsSSELiveOnlyStartsAfterSnapshotTail(t *testing.T) {
|
||||
core := service.NewCoreService(repo.NewMemoryStore())
|
||||
if err := core.SeedLocalPlatformAdmin(); err != nil {
|
||||
t.Fatalf("seed platform admin: %v", err)
|
||||
}
|
||||
setupRouter := NewTestRouterWithCore(core)
|
||||
hello := createLogIngestAPIFixtures(t, setupRouter)
|
||||
initial := validLogBatchRequest(t, hello.SessionToken, 1, 1)
|
||||
hookResult := make(chan error, 1)
|
||||
hooked := &logStreamListHookCore{Core: core, hook: func() {
|
||||
_, err := core.IngestLogBatch(initial.ToDomain())
|
||||
hookResult <- err
|
||||
}}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/server-instances/server-1/logs/events?historyLimit=0", nil).WithContext(ctx)
|
||||
streamWriter, streamReader := newSSEPipeResponseWriter()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
NewTestRouterWithCore(hooked).ServeHTTP(streamWriter, request)
|
||||
_ = streamWriter.Close()
|
||||
close(done)
|
||||
}()
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
_ = streamReader.Close()
|
||||
<-done
|
||||
})
|
||||
if status := <-streamWriter.status; status != http.StatusOK {
|
||||
t.Fatalf("unexpected SSE status: %d", status)
|
||||
}
|
||||
reader := bufio.NewReader(streamReader)
|
||||
assertSSEEvent(t, reader, "session", `"logSessionId":"session-current"`)
|
||||
assertSSEEvent(t, reader, "stream", `"id":"log-1"`)
|
||||
assertSSEEvent(t, reader, "ready", `"streamCount":1`)
|
||||
if err := <-hookResult; err != nil {
|
||||
t.Fatalf("ingest during stream snapshot: %v", err)
|
||||
}
|
||||
next := validLogBatchRequest(t, hello.SessionToken, 2, 2)
|
||||
if _, err := core.IngestLogBatch(next.ToDomain()); err != nil {
|
||||
t.Fatalf("ingest next live batch: %v", err)
|
||||
}
|
||||
assertSSEEvent(t, reader, "log", `"seq":2`)
|
||||
}
|
||||
|
||||
func TestLogEventsSSEUsesServerWideNewestHistory(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
hello := createLogIngestAPIFixtures(t, router)
|
||||
|
||||
@@ -154,7 +154,7 @@ Lifecycle workflow responses include accepted status, action, bounded server ins
|
||||
- `GET /api/v1/server-instances/{id}/dependencies`: returns the target-matched plugin/profile dependency catalog, current safe probe status/evidence, typed plan summaries, and deterministic immutable plan digests.
|
||||
- `POST /api/v1/server-instances/{id}/dependencies/check`: accepts `DependencyJobRequest` and queues a `dependencies.check` run job for a declared logical probe key.
|
||||
- `POST /api/v1/server-instances/{id}/dependencies/install`: accepts `DependencyJobRequest` with an install plan key and the exact catalog `planDigest`; stale/missing digests are denied before job creation.
|
||||
Server-scoped terminal log streaming (`GET /api/v1/server-instances/{id}/logs/events`) is registered for the server detail terminal drawer and emits platform-accepted log SSE history/live events only. The raw log list/backfill routes (`logs/live` and `logs/backfill`) remain unavailable as product APIs; internal log ingest and cursor query remain available for run/platform maintenance flows.
|
||||
Server-scoped terminal log streaming (`GET /api/v1/server-instances/{id}/logs/events`) is registered for the server detail terminal drawer and emits platform-accepted live log SSE events by default. A caller can opt into bounded current-session replay with `historyLimit`; the raw log list/backfill routes (`logs/live` and `logs/backfill`) remain unavailable as product APIs, and internal log ingest and cursor query remain available for run/platform maintenance flows.
|
||||
|
||||
Runtime distribution and client-manager APIs require the current bearer session, server visibility, plugin-declared permissions, complete runtime bindings only for actions that truly depend on external logical bindings, and platform-builder readiness. Run-side lifecycle commands separately require run endpoint capability support and use plugin-declared lifecycle actions without making manual runtime-profile binding a user prerequisite. Responses and summaries expose artifact IDs, job IDs, checksums, key generations, fingerprints, status, and redacted `secret://runtime-keys/.../current` refs only. They do not expose raw run keys, client-manager keys, FTP passwords, database DSNs, RCON passwords, host paths, direct sockets, run endpoint private addresses, build workspace paths, or large inline logs.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user