Skip to content

Commit deb1679

Browse files
committed
feat: implement grpc and http servers
1 parent a8b40ab commit deb1679

File tree

11 files changed

+5016
-0
lines changed

11 files changed

+5016
-0
lines changed

internal/server/api/handlers.go

Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"net/http"
7+
"strconv"
8+
"time"
9+
10+
"github.com/go-chi/chi/v5"
11+
"github.com/sirrobot01/mcpulse/internal/database"
12+
"github.com/sirrobot01/mcpulse/internal/ingestion"
13+
"github.com/sirrobot01/mcpulse/internal/models"
14+
"go.uber.org/zap"
15+
)
16+
17+
// Handlers holds all API handlers
18+
type Handlers struct {
19+
repo *database.Repository
20+
ingest *ingestion.Service
21+
logger *zap.Logger
22+
}
23+
24+
// NewHandlers creates new API handlers
25+
func NewHandlers(repo *database.Repository, ingest *ingestion.Service, logger *zap.Logger) *Handlers {
26+
return &Handlers{
27+
repo: repo,
28+
ingest: ingest,
29+
logger: logger,
30+
}
31+
}
32+
33+
// Health handles GET /health
34+
func (h *Handlers) Health(w http.ResponseWriter, r *http.Request) {
35+
h.sendJSON(w, http.StatusOK, map[string]interface{}{
36+
"status": "ok",
37+
"time": time.Now(),
38+
})
39+
}
40+
41+
// Version handles GET /version
42+
func (h *Handlers) Version(w http.ResponseWriter, r *http.Request) {
43+
h.sendJSON(w, http.StatusOK, map[string]interface{}{
44+
"version": "1.0.0",
45+
"build": "dev",
46+
})
47+
}
48+
49+
// ListServers handles GET /api/v1/servers
50+
func (h *Handlers) ListServers(w http.ResponseWriter, r *http.Request) {
51+
servers, err := h.repo.ListServers(r.Context())
52+
if err != nil {
53+
h.logger.Error("failed to list servers", zap.Error(err))
54+
h.sendError(w, http.StatusInternalServerError, "failed to list servers")
55+
return
56+
}
57+
58+
h.sendJSON(w, http.StatusOK, map[string]interface{}{
59+
"servers": servers,
60+
})
61+
}
62+
63+
// GetServer handles GET /api/v1/servers/:id
64+
func (h *Handlers) GetServer(w http.ResponseWriter, r *http.Request) {
65+
serverID := chi.URLParam(r, "id")
66+
if serverID == "" {
67+
h.sendError(w, http.StatusBadRequest, "server ID required")
68+
return
69+
}
70+
71+
server, err := h.repo.GetServer(r.Context(), serverID)
72+
if err != nil {
73+
h.logger.Error("failed to get server", zap.Error(err))
74+
h.sendError(w, http.StatusInternalServerError, "failed to get server")
75+
return
76+
}
77+
78+
if server == nil {
79+
h.sendError(w, http.StatusNotFound, "server not found")
80+
return
81+
}
82+
83+
h.sendJSON(w, http.StatusOK, server)
84+
}
85+
86+
// Ingest handles POST /api/v1/ingest
87+
func (h *Handlers) Ingest(w http.ResponseWriter, r *http.Request) {
88+
var req models.IngestRequest
89+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
90+
h.logger.Warn("invalid ingest payload", zap.Error(err))
91+
h.sendError(w, http.StatusBadRequest, "invalid JSON payload")
92+
return
93+
}
94+
95+
resp, err := h.ingest.Ingest(r.Context(), req.Metrics)
96+
if errors.Is(err, ingestion.ErrRateLimited) {
97+
w.Header().Set("Retry-After", "1")
98+
h.sendJSON(w, http.StatusTooManyRequests, map[string]interface{}{
99+
"error": "rate limit exceeded",
100+
"retry_after": 1,
101+
})
102+
return
103+
}
104+
if errors.Is(err, ingestion.ErrBufferFull) {
105+
h.sendError(w, http.StatusServiceUnavailable, "ingestion pipeline saturated")
106+
return
107+
}
108+
if err != nil {
109+
h.logger.Error("failed to ingest metrics", zap.Error(err))
110+
h.sendError(w, http.StatusInternalServerError, "failed to ingest metrics")
111+
return
112+
}
113+
114+
h.sendJSON(w, http.StatusOK, resp)
115+
}
116+
117+
// GetServerMetrics handles GET /api/v1/servers/:id/metrics
118+
func (h *Handlers) GetServerMetrics(w http.ResponseWriter, r *http.Request) {
119+
serverID := chi.URLParam(r, "id")
120+
if serverID == "" {
121+
h.sendError(w, http.StatusBadRequest, "server ID required")
122+
return
123+
}
124+
125+
// Parse time range
126+
from, to, interval := h.parseTimeRange(r)
127+
128+
metrics, err := h.repo.GetServerMetrics(r.Context(), serverID, from, to, interval)
129+
if err != nil {
130+
h.logger.Error("failed to get server metrics", zap.Error(err))
131+
h.sendError(w, http.StatusInternalServerError, "failed to get server metrics")
132+
return
133+
}
134+
135+
h.sendJSON(w, http.StatusOK, metrics)
136+
}
137+
138+
// GetTools handles GET /api/v1/servers/:id/tools
139+
func (h *Handlers) GetTools(w http.ResponseWriter, r *http.Request) {
140+
serverID := chi.URLParam(r, "id")
141+
if serverID == "" {
142+
h.sendError(w, http.StatusBadRequest, "server ID required")
143+
return
144+
}
145+
146+
// Parse time range
147+
from, to, _ := h.parseTimeRange(r)
148+
149+
// Parse pagination
150+
limit, offset := h.parsePagination(r)
151+
152+
tools, total, err := h.repo.GetToolMetrics(r.Context(), serverID, from, to, limit, offset)
153+
if err != nil {
154+
h.logger.Error("failed to get tools", zap.Error(err))
155+
h.sendError(w, http.StatusInternalServerError, "failed to get tools")
156+
return
157+
}
158+
159+
h.sendJSON(w, http.StatusOK, map[string]interface{}{
160+
"server_id": serverID,
161+
"tools": tools,
162+
"pagination": models.Pagination{
163+
Total: total,
164+
Limit: limit,
165+
Offset: offset,
166+
},
167+
})
168+
}
169+
170+
// GetToolTimeline handles GET /api/v1/servers/:id/tools/:name/timeline
171+
func (h *Handlers) GetToolTimeline(w http.ResponseWriter, r *http.Request) {
172+
serverID := chi.URLParam(r, "id")
173+
toolName := chi.URLParam(r, "name")
174+
175+
if serverID == "" || toolName == "" {
176+
h.sendError(w, http.StatusBadRequest, "server ID and tool name required")
177+
return
178+
}
179+
180+
// Parse time range
181+
from, to, interval := h.parseTimeRange(r)
182+
183+
timeline, err := h.repo.GetToolTimeline(r.Context(), serverID, toolName, from, to, interval)
184+
if err != nil {
185+
h.logger.Error("failed to get tool timeline", zap.Error(err))
186+
h.sendError(w, http.StatusInternalServerError, "failed to get tool timeline")
187+
return
188+
}
189+
190+
h.sendJSON(w, http.StatusOK, map[string]interface{}{
191+
"server_id": serverID,
192+
"tool_name": toolName,
193+
"timeline": timeline,
194+
})
195+
}
196+
197+
// GetErrors handles GET /api/v1/servers/:id/errors
198+
func (h *Handlers) GetErrors(w http.ResponseWriter, r *http.Request) {
199+
serverID := chi.URLParam(r, "id")
200+
if serverID == "" {
201+
h.sendError(w, http.StatusBadRequest, "server ID required")
202+
return
203+
}
204+
205+
// Parse time range
206+
from, to, _ := h.parseTimeRange(r)
207+
208+
// Parse pagination
209+
limit, offset := h.parsePagination(r)
210+
211+
// Parse optional tool filter
212+
var toolName *string
213+
if tool := r.URL.Query().Get("tool"); tool != "" {
214+
toolName = &tool
215+
}
216+
217+
errs, total, err := h.repo.GetErrors(r.Context(), serverID, from, to, toolName, limit, offset)
218+
if err != nil {
219+
h.logger.Error("failed to get errs", zap.Error(err))
220+
h.sendError(w, http.StatusInternalServerError, "failed to get errs")
221+
return
222+
}
223+
224+
h.sendJSON(w, http.StatusOK, map[string]interface{}{
225+
"server_id": serverID,
226+
"errs": errs,
227+
"pagination": models.Pagination{
228+
Total: total,
229+
Limit: limit,
230+
Offset: offset,
231+
},
232+
})
233+
}
234+
235+
// GetSessions handles GET /api/v1/servers/:id/sessions
236+
func (h *Handlers) GetSessions(w http.ResponseWriter, r *http.Request) {
237+
serverID := chi.URLParam(r, "id")
238+
if serverID == "" {
239+
h.sendError(w, http.StatusBadRequest, "server ID required")
240+
return
241+
}
242+
243+
// Parse time range
244+
from, _, _ := h.parseTimeRange(r)
245+
246+
// Parse pagination
247+
limit, offset := h.parsePagination(r)
248+
249+
// Parse status filter
250+
activeOnly := r.URL.Query().Get("status") == "active"
251+
252+
sessions, total, err := h.repo.GetSessions(r.Context(), serverID, from, activeOnly, limit, offset)
253+
if err != nil {
254+
h.logger.Error("failed to get sessions", zap.Error(err))
255+
h.sendError(w, http.StatusInternalServerError, "failed to get sessions")
256+
return
257+
}
258+
259+
h.sendJSON(w, http.StatusOK, map[string]interface{}{
260+
"server_id": serverID,
261+
"sessions": sessions,
262+
"pagination": models.Pagination{
263+
Total: total,
264+
Limit: limit,
265+
Offset: offset,
266+
},
267+
})
268+
}
269+
270+
// parseTimeRange parses time range from query parameters
271+
func (h *Handlers) parseTimeRange(r *http.Request) (from, to time.Time, interval string) {
272+
// Default to last 24 hours
273+
to = time.Now()
274+
from = to.Add(-24 * time.Hour)
275+
276+
// Parse 'from' parameter
277+
if fromStr := r.URL.Query().Get("from"); fromStr != "" {
278+
if d, err := parseDuration(fromStr); err == nil {
279+
from = time.Now().Add(-d)
280+
} else if t, err := time.Parse(time.RFC3339, fromStr); err == nil {
281+
from = t
282+
}
283+
}
284+
285+
// Parse 'to' parameter
286+
if toStr := r.URL.Query().Get("to"); toStr != "" {
287+
if toStr == "now" {
288+
to = time.Now()
289+
} else if t, err := time.Parse(time.RFC3339, toStr); err == nil {
290+
to = t
291+
}
292+
}
293+
294+
// Parse 'interval' parameter
295+
interval = r.URL.Query().Get("interval")
296+
if interval == "" {
297+
// Auto-select interval based on time range
298+
duration := to.Sub(from)
299+
if duration <= 6*time.Hour {
300+
interval = "5m"
301+
} else if duration <= 2*24*time.Hour {
302+
interval = "1h"
303+
} else if duration <= 30*24*time.Hour {
304+
interval = "1d"
305+
} else {
306+
interval = "1w"
307+
}
308+
}
309+
310+
return from, to, interval
311+
}
312+
313+
// parsePagination parses pagination parameters
314+
func (h *Handlers) parsePagination(r *http.Request) (limit, offset int) {
315+
limit = 20 // default
316+
317+
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
318+
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
319+
limit = l
320+
}
321+
}
322+
323+
if limit > 100 {
324+
limit = 100 // max
325+
}
326+
327+
if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
328+
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
329+
offset = o
330+
}
331+
}
332+
333+
return limit, offset
334+
}
335+
336+
// parseDuration parses duration strings like "24h", "7d", "30d"
337+
func parseDuration(s string) (time.Duration, error) {
338+
if len(s) < 2 {
339+
return 0, nil
340+
}
341+
342+
unit := s[len(s)-1]
343+
valueStr := s[:len(s)-1]
344+
value, err := strconv.Atoi(valueStr)
345+
if err != nil {
346+
return 0, err
347+
}
348+
349+
switch unit {
350+
case 'm':
351+
return time.Duration(value) * time.Minute, nil
352+
case 'h':
353+
return time.Duration(value) * time.Hour, nil
354+
case 'd':
355+
return time.Duration(value) * 24 * time.Hour, nil
356+
case 'w':
357+
return time.Duration(value) * 7 * 24 * time.Hour, nil
358+
default:
359+
return 0, nil
360+
}
361+
}
362+
363+
// sendJSON sends a JSON response
364+
func (h *Handlers) sendJSON(w http.ResponseWriter, status int, data interface{}) {
365+
w.Header().Set("Content-Type", "application/json")
366+
w.WriteHeader(status)
367+
_ = json.NewEncoder(w).Encode(data)
368+
}
369+
370+
// sendError sends an error response
371+
func (h *Handlers) sendError(w http.ResponseWriter, status int, message string) {
372+
h.sendJSON(w, status, map[string]interface{}{
373+
"error": message,
374+
})
375+
}

0 commit comments

Comments
 (0)