package api import ( "encoding/json" "io" "net/http" "strconv" "time" "github.com/seaweedfs/seaweedfs/telemetry/proto" "github.com/seaweedfs/seaweedfs/telemetry/server/storage" protobuf "google.golang.org/protobuf/proto" ) type Handler struct { storage *storage.PrometheusStorage } func NewHandler(storage *storage.PrometheusStorage) *Handler { return &Handler{storage: storage} } func (h *Handler) CollectTelemetry(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } contentType := r.Header.Get("Content-Type") // Only accept protobuf content type if contentType != "application/x-protobuf" && contentType != "application/protobuf" { http.Error(w, "Content-Type must be application/x-protobuf", http.StatusUnsupportedMediaType) return } // Read protobuf request; real reports are well under 1 KB body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxRequestBytes)) if err != nil { http.Error(w, "Failed to read request body", http.StatusBadRequest) return } req := &proto.TelemetryRequest{} if err := protobuf.Unmarshal(body, req); err != nil { http.Error(w, "Invalid protobuf data", http.StatusBadRequest) return } data := req.Data if data == nil { http.Error(w, "Missing telemetry data", http.StatusBadRequest) return } if err := validateTelemetryData(data); err != nil { http.Error(w, "Invalid telemetry data: "+err.Error(), http.StatusBadRequest) return } // Set timestamp if not provided if data.Timestamp == 0 { data.Timestamp = time.Now().Unix() } // Store the telemetry data if err := h.storage.StoreTelemetry(data); err != nil { http.Error(w, "Failed to store data", http.StatusInternalServerError) return } // Return protobuf response resp := &proto.TelemetryResponse{ Success: true, Message: "Telemetry data received", } respData, err := protobuf.Marshal(resp) if err != nil { http.Error(w, "Failed to marshal response", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/x-protobuf") w.WriteHeader(http.StatusOK) w.Write(respData) } func (h *Handler) GetStats(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } stats, err := h.storage.GetStats() if err != nil { http.Error(w, "Failed to get stats", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(stats) } func (h *Handler) GetInstances(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } limitStr := r.URL.Query().Get("limit") limit := 100 // default if limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 1000 { limit = l } } instances, err := h.storage.GetInstances(limit) if err != nil { http.Error(w, "Failed to get instances", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(instances) } func (h *Handler) GetMetrics(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } daysStr := r.URL.Query().Get("days") days := 30 // default if daysStr != "" { if d, err := strconv.Atoi(daysStr); err == nil && d > 0 && d <= 365 { days = d } } metrics, err := h.storage.GetMetrics(days) if err != nil { http.Error(w, "Failed to get metrics", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(metrics) } func (h *Handler) GetClusterSizes(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } days := 30 // default if daysStr := r.URL.Query().Get("days"); daysStr != "" { if d, err := strconv.Atoi(daysStr); err == nil && d > 0 && d <= 365 { days = d } } limit := 20 // default if limitStr := r.URL.Query().Get("limit"); limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 1000 { limit = l } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(h.storage.GetClusterSizeSeries(days, limit)) } func (h *Handler) GetVersions(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } days := 30 // default if daysStr := r.URL.Query().Get("days"); daysStr != "" { if d, err := strconv.Atoi(daysStr); err == nil && d > 0 && d <= 365 { days = d } } limit := 8 // default, the number of colours the version stack has if limitStr := r.URL.Query().Get("limit"); limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 1000 { limit = l } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(h.storage.GetVersionSeries(days, limit)) } func (h *Handler) GetHistory(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } clusterId := r.URL.Query().Get("cluster_id") if clusterId == "" { http.Error(w, "cluster_id is required", http.StatusBadRequest) return } days := 90 // default if daysStr := r.URL.Query().Get("days"); daysStr != "" { if d, err := strconv.Atoi(daysStr); err == nil && d > 0 && d <= 365 { days = d } } samples, ok := h.storage.GetHistory(clusterId, days) if !ok { http.Error(w, "Unknown cluster_id", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "cluster_id": clusterId, "samples": samples, }) }