mirror of
https://github.com/hanzoai/traces.git
synced 2026-10-30 18:20:41 +00:00
* frontend, distributor: Add tenant ID validation
Use the same validation than in Mimir for consistency.
In addition, reject empty IDs ("").
* Validate tenantID everywhere except in frontend handlers
We can't do it in frontend handlers because we need to wait for the multitenant middleware,
but we rely on the validation middleware.
Validating everywhere follows the same pattern as Mimir and ensures we don't miss any edge case.
* Check status codes
* Add test for user overrides API
* add comment
223 lines
6.5 KiB
Go
223 lines
6.5 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/go-kit/log/level"
|
|
jsoniter "github.com/json-iterator/go"
|
|
"go.opentelemetry.io/otel/codes"
|
|
|
|
"github.com/grafana/tempo/modules/overrides/userconfigurable/client"
|
|
"github.com/grafana/tempo/pkg/api"
|
|
"github.com/grafana/tempo/pkg/util/tracing"
|
|
"github.com/grafana/tempo/pkg/validation"
|
|
"github.com/grafana/tempo/tempodb/backend"
|
|
)
|
|
|
|
const (
|
|
headerEtag = "ETag"
|
|
headerIfMatch = "If-Match"
|
|
|
|
errNoIfMatchHeader = "must specify If-Match header"
|
|
errCouldNotParseSkipConflictingOverridesCheckParameter = "could not parse skip-conflicting-overrides-check, must be a boolean value"
|
|
|
|
queryParamScope = "scope"
|
|
queryParamScopeAPI = "api"
|
|
queryParamScopeMerged = "merged"
|
|
|
|
queryParamSkipConflictingOverridesCheck = "skip-conflicting-overrides-check"
|
|
)
|
|
|
|
// GetHandler retrieves the user-configured overrides from the backend.
|
|
func (a *UserConfigOverridesAPI) GetHandler(w http.ResponseWriter, r *http.Request) {
|
|
var err error
|
|
ctx, f := a.logRequest(r.Context(), "UserConfigOverridesAPI.GetHandler", r)
|
|
defer f(&err)
|
|
|
|
userID, err := validation.ExtractValidTenantID(ctx)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
scope := queryParamScopeAPI
|
|
if value, ok := r.URL.Query()[queryParamScope]; ok {
|
|
scope = value[0]
|
|
}
|
|
if scope != queryParamScopeAPI && scope != queryParamScopeMerged {
|
|
http.Error(w, fmt.Sprintf("unknown scope \"%s\", valid options are api and merged", scope), http.StatusBadRequest)
|
|
}
|
|
|
|
limits, version, err := a.get(ctx, userID)
|
|
if err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
|
|
if scope == queryParamScopeMerged {
|
|
limits = limitsFromOverrides(a.overrides, userID)
|
|
}
|
|
|
|
err = writeLimits(w, limits, version)
|
|
}
|
|
|
|
// PostHandler accepts post requests with json payload and writes it to config backend.
|
|
func (a *UserConfigOverridesAPI) PostHandler(w http.ResponseWriter, r *http.Request) {
|
|
var err error
|
|
ctx, f := a.logRequest(r.Context(), "UserConfigOverridesAPI.PostHandler", r)
|
|
defer f(&err)
|
|
|
|
userID, err := validation.ExtractValidTenantID(ctx)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
ifMatchVersion := r.Header.Get(headerIfMatch)
|
|
if ifMatchVersion == "" {
|
|
http.Error(w, errNoIfMatchHeader, http.StatusPreconditionRequired)
|
|
return
|
|
}
|
|
|
|
skipConflictingOverridesCheck := false
|
|
if value, ok := r.URL.Query()[queryParamSkipConflictingOverridesCheck]; ok && len(value) > 0 {
|
|
skipConflictingOverridesCheck, err = strconv.ParseBool(value[0])
|
|
if err != nil {
|
|
http.Error(w, errCouldNotParseSkipConflictingOverridesCheckParameter, http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
limits, err := a.parseLimits(r.Body)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
version, err := a.set(ctx, userID, limits, backend.Version(ifMatchVersion), skipConflictingOverridesCheck)
|
|
if err != nil {
|
|
writeError(w, err)
|
|
}
|
|
|
|
w.Header().Set(headerEtag, string(version))
|
|
}
|
|
|
|
func (a *UserConfigOverridesAPI) PatchHandler(w http.ResponseWriter, r *http.Request) {
|
|
var err error
|
|
ctx, f := a.logRequest(r.Context(), "UserConfigOverridesAPI.PatchHandler", r)
|
|
defer f(&err)
|
|
|
|
userID, err := validation.ExtractValidTenantID(ctx)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
patch, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
skipConflictingOverridesCheck := false
|
|
if value, ok := r.URL.Query()[queryParamSkipConflictingOverridesCheck]; ok && len(value) > 0 {
|
|
skipConflictingOverridesCheck, err = strconv.ParseBool(value[0])
|
|
if err != nil {
|
|
http.Error(w, errCouldNotParseSkipConflictingOverridesCheckParameter, http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
// update method does the version check internally so we don't need `If-Match` header for the PATCH calls.
|
|
// not requiring `If-Match` is by design to make sure PATCH is easy to use and users can use it to
|
|
// Patch the parts of the overrides without calling GET first to obtain ETag for `If-Match`
|
|
patchedLimits, version, err := a.update(ctx, userID, patch, skipConflictingOverridesCheck)
|
|
if err != nil {
|
|
writeError(w, err)
|
|
return
|
|
}
|
|
|
|
err = writeLimits(w, patchedLimits, version)
|
|
}
|
|
|
|
func (a *UserConfigOverridesAPI) DeleteHandler(w http.ResponseWriter, r *http.Request) {
|
|
var err error
|
|
ctx, f := a.logRequest(r.Context(), "UserConfigOverridesAPI.DeleteHandler", r)
|
|
defer f(&err)
|
|
|
|
userID, err := validation.ExtractValidTenantID(ctx)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
ifMatchVersion := r.Header.Get(headerIfMatch)
|
|
if ifMatchVersion == "" {
|
|
http.Error(w, errNoIfMatchHeader, http.StatusPreconditionRequired)
|
|
return
|
|
}
|
|
|
|
err = a.delete(ctx, userID, backend.Version(ifMatchVersion))
|
|
if err != nil {
|
|
writeError(w, err)
|
|
}
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, err error) {
|
|
if errors.Is(err, backend.ErrDoesNotExist) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
if errors.Is(err, backend.ErrVersionDoesNotMatch) || errors.Is(err, backend.ErrVersionInvalid) {
|
|
http.Error(w, err.Error(), http.StatusPreconditionFailed)
|
|
return
|
|
}
|
|
var valErr *validationError
|
|
if ok := errors.As(err, &valErr); ok {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if errors.Is(err, errConflictingRuntimeOverrides) {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
|
|
func writeLimits(w http.ResponseWriter, limits *client.Limits, version backend.Version) error {
|
|
data, err := jsoniter.Marshal(limits)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return err
|
|
}
|
|
|
|
w.Header().Set(headerEtag, string(version))
|
|
w.Header().Set(api.HeaderContentType, api.HeaderAcceptJSON)
|
|
_, _ = w.Write(data)
|
|
return nil
|
|
}
|
|
|
|
func (a *UserConfigOverridesAPI) logRequest(ctx context.Context, handler string, r *http.Request) (context.Context, func(*error)) {
|
|
ctx, span := tracer.Start(ctx, handler)
|
|
traceID, _ := tracing.ExtractTraceID(ctx)
|
|
|
|
level.Info(a.logger).Log("traceID", traceID, "method", r.Method, "url", r.URL.RequestURI(), "user-agent", r.UserAgent())
|
|
|
|
return ctx, func(errPtr *error) {
|
|
err := *errPtr
|
|
|
|
if err != nil && !errors.Is(err, backend.ErrDoesNotExist) {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, "")
|
|
level.Error(a.logger).Log("traceID", traceID, "method", r.Method, "url", r.URL.RequestURI(), "user-agent", r.UserAgent(), "err", err)
|
|
}
|
|
|
|
span.End()
|
|
}
|
|
}
|