This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package metricquery
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/itworx/pulse/internal/auth"
|
||||
"github.com/itworx/pulse/internal/problem"
|
||||
"github.com/itworx/pulse/internal/promqlbinding"
|
||||
"github.com/itworx/pulse/internal/queryplan"
|
||||
)
|
||||
|
||||
const maxRequestBody = 64 << 10
|
||||
|
||||
type Handler struct{ Service *Service }
|
||||
|
||||
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if _, ok := auth.PrincipalFromContext(r.Context()); !ok {
|
||||
problem.Write(w, r, http.StatusUnauthorized, "UNAUTHORIZED", "Authentication required", "Authentication is required to query metrics.", nil)
|
||||
return
|
||||
}
|
||||
if h.Service == nil {
|
||||
problem.Write(w, r, http.StatusServiceUnavailable, "METRIC_SOURCE_UNAVAILABLE", "Metric source unavailable", "The configured metric source is not available.", nil)
|
||||
return
|
||||
}
|
||||
var response Response
|
||||
var err error
|
||||
switch strings.TrimPrefix(r.URL.Path, "/api/v1/metrics/") {
|
||||
case "inspect":
|
||||
var request queryplan.Request
|
||||
if !decodeJSON(w, r, &request) {
|
||||
return
|
||||
}
|
||||
inspector, inspectErr := h.Service.Inspect(r.Context(), request)
|
||||
if inspectErr != nil {
|
||||
writeQueryError(w, r, inspectErr)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"inspector": inspector})
|
||||
return
|
||||
case "query-range":
|
||||
var request queryplan.Request
|
||||
if decodeJSON(w, r, &request) {
|
||||
response, err = h.Service.ExecuteRange(r.Context(), request)
|
||||
}
|
||||
case "query":
|
||||
var request InstantRequest
|
||||
if decodeJSON(w, r, &request) {
|
||||
response, err = h.Service.ExecuteInstant(r.Context(), request)
|
||||
}
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeQueryError(w, r, err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func decodeJSON(w http.ResponseWriter, r *http.Request, destination any) bool {
|
||||
body, readErr := io.ReadAll(io.LimitReader(r.Body, maxRequestBody+1))
|
||||
if readErr != nil || len(body) > maxRequestBody {
|
||||
problem.Write(w, r, http.StatusRequestEntityTooLarge, "QUERY_BODY_LIMIT", "Query body too large", "The metric query body exceeds the allowed size.", nil)
|
||||
return false
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(body))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(destination); err != nil {
|
||||
problem.Write(w, r, http.StatusBadRequest, "QUERY_BODY_INVALID", "Invalid query body", "The metric query body is invalid.", nil)
|
||||
return false
|
||||
}
|
||||
var extra any
|
||||
if err := decoder.Decode(&extra); err != io.EOF {
|
||||
problem.Write(w, r, http.StatusBadRequest, "QUERY_BODY_INVALID", "Invalid query body", "The metric query body contains trailing data.", nil)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func writeQueryError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
if errors.Is(err, ErrInspectorUnauthorized) {
|
||||
problem.Write(w, r, http.StatusForbidden, "QUERY_INSPECTOR_FORBIDDEN", "Query inspector forbidden", "The query inspector requires operate permission.", nil)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
var sourceErr sourceError
|
||||
if errors.Is(err, ErrSourceUnavailable) || errors.As(err, &sourceErr) {
|
||||
problem.Write(w, r, http.StatusServiceUnavailable, "QUERY_SOURCE_UNAVAILABLE", "Metric source unavailable", "The metric source could not be queried.", nil)
|
||||
return
|
||||
}
|
||||
var planError queryplan.Error
|
||||
if errors.As(err, &planError) {
|
||||
status := http.StatusUnprocessableEntity
|
||||
if planError.Code == queryplan.ErrUnauthorized.Code {
|
||||
status = http.StatusUnauthorized
|
||||
}
|
||||
problem.Write(w, r, status, planError.Code, "Metric query rejected", planError.Detail, map[string]string{"field": planError.Field})
|
||||
return
|
||||
}
|
||||
var bindingError promqlbinding.Error
|
||||
if errors.As(err, &bindingError) {
|
||||
status := http.StatusUnprocessableEntity
|
||||
if bindingError.Code == "PROMQL_RAW_UNAUTHORIZED" {
|
||||
status = http.StatusForbidden
|
||||
}
|
||||
problem.Write(w, r, status, bindingError.Code, "Metric query rejected", bindingError.Detail, map[string]string{"field": bindingError.Field})
|
||||
return
|
||||
}
|
||||
problem.Write(w, r, http.StatusServiceUnavailable, "QUERY_UNAVAILABLE", "Metric query unavailable", "The metric query could not be completed.", nil)
|
||||
}
|
||||
Reference in New Issue
Block a user