Files
ITWorx-Pulse-Public/internal/metricquery/handler.go
T
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

126 lines
4.2 KiB
Go

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)
}