Move module path from github.com/smart-mcp-proxy/synapbus to github.com/synapbus/synapbus across all Go imports (47 files). Add constellation logo options generated via FLUX 1.1 Pro. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
121 lines
2.8 KiB
Go
121 lines
2.8 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/synapbus/synapbus/internal/trace"
|
|
)
|
|
|
|
// ExportTraces handles GET /api/traces/export.
|
|
func (h *TracesHandler) ExportTraces(w http.ResponseWriter, r *http.Request) {
|
|
ownerID, ok := OwnerIDFromContext(r.Context())
|
|
if !ok {
|
|
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
filter := trace.TraceFilter{
|
|
OwnerID: fmt.Sprintf("%d", ownerID),
|
|
AgentName: r.URL.Query().Get("agent_name"),
|
|
Action: r.URL.Query().Get("action"),
|
|
}
|
|
|
|
if since := r.URL.Query().Get("since"); since != "" {
|
|
t, err := time.Parse(time.RFC3339, since)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"invalid 'since' parameter"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
filter.Since = &t
|
|
}
|
|
|
|
if until := r.URL.Query().Get("until"); until != "" {
|
|
t, err := time.Parse(time.RFC3339, until)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"invalid 'until' parameter"}`, http.StatusBadRequest)
|
|
return
|
|
}
|
|
filter.Until = &t
|
|
}
|
|
|
|
// Determine format
|
|
format := r.URL.Query().Get("format")
|
|
if format == "" {
|
|
accept := r.Header.Get("Accept")
|
|
switch accept {
|
|
case "text/csv":
|
|
format = "csv"
|
|
default:
|
|
format = "json"
|
|
}
|
|
}
|
|
|
|
dateStr := time.Now().Format("2006-01-02")
|
|
|
|
switch format {
|
|
case "csv":
|
|
h.exportCSV(w, r, filter, dateStr)
|
|
default:
|
|
h.exportJSON(w, r, filter, dateStr)
|
|
}
|
|
}
|
|
|
|
func (h *TracesHandler) exportJSON(w http.ResponseWriter, r *http.Request, filter trace.TraceFilter, dateStr string) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="traces-%s.json"`, dateStr))
|
|
w.Header().Set("Transfer-Encoding", "chunked")
|
|
|
|
// Write opening bracket
|
|
w.Write([]byte("["))
|
|
|
|
first := true
|
|
err := h.store.QueryStream(r.Context(), filter, func(t trace.Trace) error {
|
|
if !first {
|
|
w.Write([]byte(","))
|
|
}
|
|
first = false
|
|
|
|
b, err := json.Marshal(t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = w.Write(b)
|
|
return err
|
|
})
|
|
|
|
if err != nil {
|
|
// Best effort — headers already sent
|
|
w.Write([]byte(fmt.Sprintf(`]`)))
|
|
return
|
|
}
|
|
|
|
w.Write([]byte("]"))
|
|
}
|
|
|
|
func (h *TracesHandler) exportCSV(w http.ResponseWriter, r *http.Request, filter trace.TraceFilter, dateStr string) {
|
|
w.Header().Set("Content-Type", "text/csv")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="traces-%s.csv"`, dateStr))
|
|
w.Header().Set("Transfer-Encoding", "chunked")
|
|
|
|
cw := csv.NewWriter(w)
|
|
defer cw.Flush()
|
|
|
|
// Write header
|
|
cw.Write([]string{"id", "agent_name", "action", "details", "timestamp"})
|
|
|
|
h.store.QueryStream(r.Context(), filter, func(t trace.Trace) error {
|
|
return cw.Write([]string{
|
|
strconv.FormatInt(t.ID, 10),
|
|
t.AgentName,
|
|
t.Action,
|
|
string(t.Details),
|
|
t.Timestamp.Format(time.RFC3339),
|
|
})
|
|
})
|
|
}
|