Add log viewer for relay owners (v0.37.3)
Some checks failed
Go / build-and-release (push) Has been cancelled
Some checks failed
Go / build-and-release (push) Has been cancelled
- Add in-memory ring buffer for log storage (configurable via ORLY_LOG_BUFFER_SIZE) - Add owner-only log viewer in web UI with infinite scroll - Add log level selector with runtime level changes - Add clear logs functionality - Update Blossom refresh button to use 🔄 emoji style Files modified: - pkg/logbuffer/buffer.go: Ring buffer implementation - pkg/logbuffer/writer.go: Buffered writer hook for log capture - app/config/config.go: Add ORLY_LOG_BUFFER_SIZE env var - app/handle-logs.go: Log API handlers - app/server.go: Register log routes - app/web/src/LogView.svelte: Log viewer component - app/web/src/App.svelte: Add logs tab (owner-only) - app/web/src/BlossomView.svelte: Update refresh button style 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
185
app/handle-logs.go
Normal file
185
app/handle-logs.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
lol "lol.mleku.dev"
|
||||
"lol.mleku.dev/chk"
|
||||
|
||||
"git.mleku.dev/mleku/nostr/httpauth"
|
||||
"next.orly.dev/pkg/acl"
|
||||
"next.orly.dev/pkg/logbuffer"
|
||||
)
|
||||
|
||||
// LogsResponse is the response structure for GET /api/logs
|
||||
type LogsResponse struct {
|
||||
Logs []logbuffer.LogEntry `json:"logs"`
|
||||
Total int `json:"total"`
|
||||
HasMore bool `json:"has_more"`
|
||||
}
|
||||
|
||||
// LogLevelResponse is the response structure for GET /api/logs/level
|
||||
type LogLevelResponse struct {
|
||||
Level string `json:"level"`
|
||||
}
|
||||
|
||||
// LogLevelRequest is the request structure for POST /api/logs/level
|
||||
type LogLevelRequest struct {
|
||||
Level string `json:"level"`
|
||||
}
|
||||
|
||||
// handleGetLogs handles GET /api/logs
|
||||
func (s *Server) handleGetLogs(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate NIP-98 authentication
|
||||
valid, pubkey, err := httpauth.CheckAuth(r)
|
||||
if chk.E(err) || !valid {
|
||||
errorMsg := "NIP-98 authentication validation failed"
|
||||
if err != nil {
|
||||
errorMsg = err.Error()
|
||||
}
|
||||
http.Error(w, errorMsg, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Check permissions - require owner level only
|
||||
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
|
||||
if accessLevel != "owner" {
|
||||
http.Error(w, "Owner permission required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if log buffer is available
|
||||
if logbuffer.GlobalBuffer == nil {
|
||||
http.Error(w, "Log buffer not enabled", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse query parameters
|
||||
offset := 0
|
||||
limit := 100
|
||||
if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
|
||||
if v, err := strconv.Atoi(offsetStr); err == nil && v >= 0 {
|
||||
offset = v
|
||||
}
|
||||
}
|
||||
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
|
||||
if v, err := strconv.Atoi(limitStr); err == nil && v > 0 && v <= 500 {
|
||||
limit = v
|
||||
}
|
||||
}
|
||||
|
||||
// Get logs from buffer
|
||||
logs := logbuffer.GlobalBuffer.Get(offset, limit)
|
||||
total := logbuffer.GlobalBuffer.Count()
|
||||
hasMore := offset+len(logs) < total
|
||||
|
||||
response := LogsResponse{
|
||||
Logs: logs,
|
||||
Total: total,
|
||||
HasMore: hasMore,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// handleClearLogs handles POST /api/logs/clear
|
||||
func (s *Server) handleClearLogs(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate NIP-98 authentication
|
||||
valid, pubkey, err := httpauth.CheckAuth(r)
|
||||
if chk.E(err) || !valid {
|
||||
errorMsg := "NIP-98 authentication validation failed"
|
||||
if err != nil {
|
||||
errorMsg = err.Error()
|
||||
}
|
||||
http.Error(w, errorMsg, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Check permissions - require owner level only
|
||||
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
|
||||
if accessLevel != "owner" {
|
||||
http.Error(w, "Owner permission required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if log buffer is available
|
||||
if logbuffer.GlobalBuffer == nil {
|
||||
http.Error(w, "Log buffer not enabled", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
// Clear the buffer
|
||||
logbuffer.GlobalBuffer.Clear()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
// handleLogLevel handles GET and POST /api/logs/level
|
||||
func (s *Server) handleLogLevel(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
s.handleGetLogLevel(w, r)
|
||||
case http.MethodPost:
|
||||
s.handleSetLogLevel(w, r)
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetLogLevel handles GET /api/logs/level
|
||||
func (s *Server) handleGetLogLevel(w http.ResponseWriter, r *http.Request) {
|
||||
// No auth required for reading log level
|
||||
level := logbuffer.GetCurrentLevel()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(LogLevelResponse{Level: level})
|
||||
}
|
||||
|
||||
// handleSetLogLevel handles POST /api/logs/level
|
||||
func (s *Server) handleSetLogLevel(w http.ResponseWriter, r *http.Request) {
|
||||
// Validate NIP-98 authentication
|
||||
valid, pubkey, err := httpauth.CheckAuth(r)
|
||||
if chk.E(err) || !valid {
|
||||
errorMsg := "NIP-98 authentication validation failed"
|
||||
if err != nil {
|
||||
errorMsg = err.Error()
|
||||
}
|
||||
http.Error(w, errorMsg, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Check permissions - require owner level only
|
||||
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
|
||||
if accessLevel != "owner" {
|
||||
http.Error(w, "Owner permission required", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var req LogLevelRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate and set log level
|
||||
level := logbuffer.SetCurrentLevel(req.Level)
|
||||
lol.SetLogLevel(level)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(LogLevelResponse{Level: level})
|
||||
}
|
||||
Reference in New Issue
Block a user