refactor(web): split gateway logs out of the status endpoint (#1504)
- add a dedicated /api/gateway/logs endpoint for incremental log polling - keep /api/gateway/status focused on runtime and health data only - update frontend log fetching to use the new API and add backend tests covering the status/logs separation and cleared-log behavior
This commit is contained in:
parent
6b72326be1
commit
c69c48ad46
4 changed files with 126 additions and 31 deletions
|
|
@ -52,6 +52,7 @@ var gatewayHealthGet = func(url string, timeout time.Duration) (*http.Response,
|
||||||
func (h *Handler) registerGatewayRoutes(mux *http.ServeMux) {
|
func (h *Handler) registerGatewayRoutes(mux *http.ServeMux) {
|
||||||
mux.HandleFunc("GET /api/gateway/status", h.handleGatewayStatus)
|
mux.HandleFunc("GET /api/gateway/status", h.handleGatewayStatus)
|
||||||
mux.HandleFunc("GET /api/gateway/events", h.handleGatewayEvents)
|
mux.HandleFunc("GET /api/gateway/events", h.handleGatewayEvents)
|
||||||
|
mux.HandleFunc("GET /api/gateway/logs", h.handleGatewayLogs)
|
||||||
mux.HandleFunc("POST /api/gateway/logs/clear", h.handleGatewayClearLogs)
|
mux.HandleFunc("POST /api/gateway/logs/clear", h.handleGatewayClearLogs)
|
||||||
mux.HandleFunc("POST /api/gateway/start", h.handleGatewayStart)
|
mux.HandleFunc("POST /api/gateway/start", h.handleGatewayStart)
|
||||||
mux.HandleFunc("POST /api/gateway/stop", h.handleGatewayStop)
|
mux.HandleFunc("POST /api/gateway/stop", h.handleGatewayStop)
|
||||||
|
|
@ -560,16 +561,16 @@ func (h *Handler) handleGatewayClearLogs(w http.ResponseWriter, r *http.Request)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGatewayStatus returns the gateway run status, health info, and logs.
|
// handleGatewayStatus returns the gateway run status and health info.
|
||||||
//
|
//
|
||||||
// GET /api/gateway/status
|
// GET /api/gateway/status
|
||||||
func (h *Handler) handleGatewayStatus(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleGatewayStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
data := h.gatewayStatusData(r, true)
|
data := h.gatewayStatusData()
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(data)
|
json.NewEncoder(w).Encode(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) gatewayStatusData(r *http.Request, includeLogs bool) map[string]any {
|
func (h *Handler) gatewayStatusData() map[string]any {
|
||||||
data := map[string]any{}
|
data := map[string]any{}
|
||||||
cfg, cfgErr := config.LoadConfig(h.configPath)
|
cfg, cfgErr := config.LoadConfig(h.configPath)
|
||||||
configDefaultModel := ""
|
configDefaultModel := ""
|
||||||
|
|
@ -661,16 +662,22 @@ func (h *Handler) gatewayStatusData(r *http.Request, includeLogs bool) map[strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if includeLogs {
|
|
||||||
appendGatewayLogs(r, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendGatewayLogs reads log_offset and log_run_id query params from the request
|
// handleGatewayLogs returns buffered gateway logs, optionally incrementally.
|
||||||
// and populates the response data map with incremental log lines.
|
//
|
||||||
func appendGatewayLogs(r *http.Request, data map[string]any) {
|
// GET /api/gateway/logs
|
||||||
|
func (h *Handler) handleGatewayLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := gatewayLogsData(r)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// gatewayLogsData reads log_offset and log_run_id query params from the request
|
||||||
|
// and returns incremental log lines.
|
||||||
|
func gatewayLogsData(r *http.Request) map[string]any {
|
||||||
|
data := map[string]any{}
|
||||||
clientOffset := 0
|
clientOffset := 0
|
||||||
clientRunID := -1
|
clientRunID := -1
|
||||||
|
|
||||||
|
|
@ -692,7 +699,7 @@ func appendGatewayLogs(r *http.Request, data map[string]any) {
|
||||||
data["logs"] = []string{}
|
data["logs"] = []string{}
|
||||||
data["log_total"] = 0
|
data["log_total"] = 0
|
||||||
data["log_run_id"] = 0
|
data["log_run_id"] = 0
|
||||||
return
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
// If runID changed, reset offset to get all logs from new run
|
// If runID changed, reset offset to get all logs from new run
|
||||||
|
|
@ -709,6 +716,7 @@ func appendGatewayLogs(r *http.Request, data map[string]any) {
|
||||||
data["logs"] = lines
|
data["logs"] = lines
|
||||||
data["log_total"] = total
|
data["log_total"] = total
|
||||||
data["log_run_id"] = runID
|
data["log_run_id"] = runID
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGatewayEvents serves an SSE stream of gateway state change events.
|
// handleGatewayEvents serves an SSE stream of gateway state change events.
|
||||||
|
|
@ -751,7 +759,7 @@ func (h *Handler) handleGatewayEvents(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// currentGatewayStatus returns the current gateway status as a JSON string.
|
// currentGatewayStatus returns the current gateway status as a JSON string.
|
||||||
func (h *Handler) currentGatewayStatus() string {
|
func (h *Handler) currentGatewayStatus() string {
|
||||||
data := h.gatewayStatusData(nil, false)
|
data := h.gatewayStatusData()
|
||||||
encoded, _ := json.Marshal(data)
|
encoded, _ := json.Marshal(data)
|
||||||
return string(encoded)
|
return string(encoded)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -707,6 +707,79 @@ func TestGatewayRestartReturnsErrorStatusWhenReplacementFailsToStart(t *testing.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGatewayStatusExcludesLogsFields(t *testing.T) {
|
||||||
|
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||||
|
h := NewHandler(configPath)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
h.RegisterRoutes(mux)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/gateway/status", nil)
|
||||||
|
mux.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body map[string]any
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||||
|
t.Fatalf("unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := body["logs"]; ok {
|
||||||
|
t.Fatalf("logs unexpectedly present in status response: %#v", body["logs"])
|
||||||
|
}
|
||||||
|
if _, ok := body["log_total"]; ok {
|
||||||
|
t.Fatalf("log_total unexpectedly present in status response: %#v", body["log_total"])
|
||||||
|
}
|
||||||
|
if _, ok := body["log_run_id"]; ok {
|
||||||
|
t.Fatalf("log_run_id unexpectedly present in status response: %#v", body["log_run_id"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGatewayLogsReturnsIncrementalHistory(t *testing.T) {
|
||||||
|
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||||
|
h := NewHandler(configPath)
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
h.RegisterRoutes(mux)
|
||||||
|
|
||||||
|
gateway.logs.Clear()
|
||||||
|
gateway.logs.Append("first line")
|
||||||
|
gateway.logs.Append("second line")
|
||||||
|
runID := gateway.logs.RunID()
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(
|
||||||
|
http.MethodGet,
|
||||||
|
"/api/gateway/logs?log_offset=1&log_run_id="+strconv.Itoa(runID),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
mux.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("logs status = %d, want %d", rec.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body map[string]any
|
||||||
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||||
|
t.Fatalf("unmarshal logs response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, ok := body["logs"].([]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("logs missing or not array: %#v", body["logs"])
|
||||||
|
}
|
||||||
|
if len(logs) != 1 || logs[0] != "second line" {
|
||||||
|
t.Fatalf("logs = %#v, want [\"second line\"]", logs)
|
||||||
|
}
|
||||||
|
if got := body["log_total"]; got != float64(2) {
|
||||||
|
t.Fatalf("log_total = %#v, want 2", got)
|
||||||
|
}
|
||||||
|
if got := body["log_run_id"]; got != float64(runID) {
|
||||||
|
t.Fatalf("log_run_id = %#v, want %d", got, runID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGatewayClearLogsResetsBufferedHistory(t *testing.T) {
|
func TestGatewayClearLogsResetsBufferedHistory(t *testing.T) {
|
||||||
configPath := filepath.Join(t.TempDir(), "config.json")
|
configPath := filepath.Join(t.TempDir(), "config.json")
|
||||||
h := NewHandler(configPath)
|
h := NewHandler(configPath)
|
||||||
|
|
@ -743,33 +816,36 @@ func TestGatewayClearLogsResetsBufferedHistory(t *testing.T) {
|
||||||
t.Fatalf("log_run_id = %d, want > %d", int(clearRunID), previousRunID)
|
t.Fatalf("log_run_id = %d, want > %d", int(clearRunID), previousRunID)
|
||||||
}
|
}
|
||||||
|
|
||||||
statusRec := httptest.NewRecorder()
|
logsRec := httptest.NewRecorder()
|
||||||
statusReq := httptest.NewRequest(
|
logsReq := httptest.NewRequest(
|
||||||
http.MethodGet,
|
http.MethodGet,
|
||||||
"/api/gateway/status?log_offset=0&log_run_id="+strconv.Itoa(previousRunID),
|
"/api/gateway/logs?log_offset=0&log_run_id="+strconv.Itoa(previousRunID),
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
mux.ServeHTTP(statusRec, statusReq)
|
mux.ServeHTTP(logsRec, logsReq)
|
||||||
|
|
||||||
if statusRec.Code != http.StatusOK {
|
if logsRec.Code != http.StatusOK {
|
||||||
t.Fatalf("status code = %d, want %d", statusRec.Code, http.StatusOK)
|
t.Fatalf("logs code = %d, want %d", logsRec.Code, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
var statusBody map[string]any
|
var logsBody map[string]any
|
||||||
if err := json.Unmarshal(statusRec.Body.Bytes(), &statusBody); err != nil {
|
if err := json.Unmarshal(logsRec.Body.Bytes(), &logsBody); err != nil {
|
||||||
t.Fatalf("unmarshal status response: %v", err)
|
t.Fatalf("unmarshal logs response: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logs, ok := statusBody["logs"].([]any)
|
logs, ok := logsBody["logs"].([]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("logs missing or not array: %#v", statusBody["logs"])
|
t.Fatalf("logs missing or not array: %#v", logsBody["logs"])
|
||||||
}
|
}
|
||||||
if len(logs) != 0 {
|
if len(logs) != 0 {
|
||||||
t.Fatalf("logs len = %d, want 0", len(logs))
|
t.Fatalf("logs len = %d, want 0", len(logs))
|
||||||
}
|
}
|
||||||
if got := statusBody["log_total"]; got != float64(0) {
|
if got := logsBody["log_total"]; got != float64(0) {
|
||||||
t.Fatalf("log_total = %#v, want 0", got)
|
t.Fatalf("log_total = %#v, want 0", got)
|
||||||
}
|
}
|
||||||
|
if got := logsBody["log_run_id"]; got != clearBody["log_run_id"] {
|
||||||
|
t.Fatalf("log_run_id = %#v, want %#v", got, clearBody["log_run_id"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindPicoclawBinary_EnvOverride(t *testing.T) {
|
func TestFindPicoclawBinary_EnvOverride(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,13 @@ interface GatewayStatusResponse {
|
||||||
pid?: number
|
pid?: number
|
||||||
boot_default_model?: string
|
boot_default_model?: string
|
||||||
config_default_model?: string
|
config_default_model?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GatewayLogsResponse {
|
||||||
logs?: string[]
|
logs?: string[]
|
||||||
log_total?: number
|
log_total?: number
|
||||||
log_run_id?: number
|
log_run_id?: number
|
||||||
[key: string]: unknown
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GatewayActionResponse {
|
interface GatewayActionResponse {
|
||||||
|
|
@ -31,10 +34,14 @@ async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||||
return res.json() as Promise<T>
|
return res.json() as Promise<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGatewayStatus(options?: {
|
export async function getGatewayStatus(): Promise<GatewayStatusResponse> {
|
||||||
|
return request<GatewayStatusResponse>("/api/gateway/status")
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGatewayLogs(options?: {
|
||||||
log_offset?: number
|
log_offset?: number
|
||||||
log_run_id?: number
|
log_run_id?: number
|
||||||
}): Promise<GatewayStatusResponse> {
|
}): Promise<GatewayLogsResponse> {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (options?.log_offset !== undefined) {
|
if (options?.log_offset !== undefined) {
|
||||||
params.set("log_offset", options.log_offset.toString())
|
params.set("log_offset", options.log_offset.toString())
|
||||||
|
|
@ -43,7 +50,7 @@ export async function getGatewayStatus(options?: {
|
||||||
params.set("log_run_id", options.log_run_id.toString())
|
params.set("log_run_id", options.log_run_id.toString())
|
||||||
}
|
}
|
||||||
const queryString = params.toString() ? `?${params.toString()}` : ""
|
const queryString = params.toString() ? `?${params.toString()}` : ""
|
||||||
return request<GatewayStatusResponse>(`/api/gateway/status${queryString}`)
|
return request<GatewayLogsResponse>(`/api/gateway/logs${queryString}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startGateway(): Promise<GatewayActionResponse> {
|
export async function startGateway(): Promise<GatewayActionResponse> {
|
||||||
|
|
@ -70,4 +77,8 @@ export async function clearGatewayLogs(): Promise<GatewayActionResponse> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { GatewayStatusResponse, GatewayActionResponse }
|
export type {
|
||||||
|
GatewayStatusResponse,
|
||||||
|
GatewayLogsResponse,
|
||||||
|
GatewayActionResponse,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useAtomValue } from "jotai"
|
import { useAtomValue } from "jotai"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
import { clearGatewayLogs, getGatewayStatus } from "@/api/gateway"
|
import { clearGatewayLogs, getGatewayLogs } from "@/api/gateway"
|
||||||
import { gatewayAtom } from "@/store/gateway"
|
import { gatewayAtom } from "@/store/gateway"
|
||||||
|
|
||||||
export function useGatewayLogs() {
|
export function useGatewayLogs() {
|
||||||
|
|
@ -49,7 +49,7 @@ export function useGatewayLogs() {
|
||||||
const requestToken = syncTokenRef.current
|
const requestToken = syncTokenRef.current
|
||||||
const requestOffset = logOffsetRef.current
|
const requestOffset = logOffsetRef.current
|
||||||
const requestRunId = logRunIdRef.current
|
const requestRunId = logRunIdRef.current
|
||||||
const data = await getGatewayStatus({
|
const data = await getGatewayLogs({
|
||||||
log_offset: requestOffset,
|
log_offset: requestOffset,
|
||||||
log_run_id: requestRunId,
|
log_run_id: requestRunId,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue