mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-16 03:20:50 +02:00
* telemetry: validate reports on the collect endpoint /api/collect is anonymous, so reports can't be authenticated, but a real master can't produce a non-UUID topology_id, a version outside N.NN(-enterprise), an unknown GOOS/GOARCH, or absurd counts — reject those to keep casual junk out of the collected data, and cap the request body at 4 KB. * telemetry: integration test fixtures pass collect validation The test's topology id and version were exactly the junk shapes the new validation rejects; use a UUID and a plain version number.
184 lines
4.5 KiB
Go
184 lines
4.5 KiB
Go
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) 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,
|
|
})
|
|
}
|