Files
zeekayandHanzo Dev 20ec5d62ea routing: query service and gateway read a path segment, not gorilla's map
19 reads across three files stop naming gorilla to address a resource by id and
go through coretypes.Param: 13 in the query-service handler (rule id, integration
id, pipeline version, six funnel ids), 1 in the parser (trace id), 5 in the
gateway (key id, limit id). Registration is untouched — this is the read side.

Where a site had bound `vars := mux.Vars(r)` only to index it once, the map is
gone with it; a handler wants the VALUE, and keeping a map alive to hold one
string was the router leaking into the body.

The query half stays exactly where it was. ParseSearchTracesParams now reads the
trace id from the PATH and the span id from the QUERY through two different
calls, which is what they always were.

http_handler.go keeps `mux.NewRouter().UseEncodedPath()` and the websocket
upgrader: those are the internal tree and its transport, converted as one piece
with the other 70 registration files, not here.

parser.go drops the gorilla import entirely; gateway/handler.go likewise.

Tests: neither package had one that exercised a path segment, so both go green
whether the read works or not — this repo has already shipped that defect. Each
now drives a REAL router registered at the REAL template. The gateway rows go
router -> handler -> Gateway and assert the id the gateway was HANDED, so an
empty segment fails on the recorded value rather than on a status code. Under a
Param that returns "", all four tests go red; before them, both packages stayed
green.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 18:53:20 -07:00

288 lines
7.0 KiB
Go

package gateway
import (
"encoding/json"
"net/http"
"strconv"
"github.com/hanzoai/o11y/pkg/errors"
"github.com/hanzoai/o11y/pkg/http/render"
"github.com/hanzoai/o11y/pkg/types/authtypes"
"github.com/hanzoai/o11y/pkg/types/coretypes"
"github.com/hanzoai/o11y/pkg/types/gatewaytypes"
"github.com/hanzoai/o11y/pkg/valuer"
)
const (
DefaultPage = 1
DefaultPageSize = 10
)
type handler struct {
gateway Gateway
}
func NewHandler(gateway Gateway) Handler {
return &handler{
gateway: gateway,
}
}
func (handler *handler) GetIngestionKeys(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
pageString := r.URL.Query().Get("page")
perPageString := r.URL.Query().Get("per_page")
page, err := parseIntWithDefaultValue(pageString, DefaultPage)
if err != nil {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "page must be a valid integer"))
return
}
perPage, err := parseIntWithDefaultValue(perPageString, DefaultPageSize)
if err != nil {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "per_page must be a valid integer"))
return
}
response, err := handler.gateway.GetIngestionKeys(ctx, orgID, page, perPage)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, response)
}
func (handler *handler) SearchIngestionKeys(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
pageString := r.URL.Query().Get("page")
perPageString := r.URL.Query().Get("per_page")
name := r.URL.Query().Get("name")
page, err := parseIntWithDefaultValue(pageString, DefaultPage)
if err != nil {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "page must be a valid integer"))
return
}
perPage, err := parseIntWithDefaultValue(perPageString, DefaultPageSize)
if err != nil {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "per_page must be a valid integer"))
return
}
response, err := handler.gateway.SearchIngestionKeysByName(ctx, orgID, name, page, perPage)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, response)
}
func (handler *handler) CreateIngestionKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
var req gatewaytypes.PostableIngestionKey
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid request body"))
return
}
response, err := handler.gateway.CreateIngestionKey(ctx, orgID, req.Name, req.Tags, req.ExpiresAt)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, response)
}
func (handler *handler) UpdateIngestionKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
keyID := coretypes.Param(r, "keyId")
if keyID == "" {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "keyId is required"))
return
}
var req gatewaytypes.PostableIngestionKey
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid request body"))
return
}
err = handler.gateway.UpdateIngestionKey(ctx, orgID, keyID, req.Name, req.Tags, req.ExpiresAt)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) DeleteIngestionKey(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
keyID := coretypes.Param(r, "keyId")
if keyID == "" {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "keyId is required"))
return
}
err = handler.gateway.DeleteIngestionKey(ctx, orgID, keyID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) CreateIngestionKeyLimit(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
keyID := coretypes.Param(r, "keyId")
if keyID == "" {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "keyId is required"))
return
}
var req gatewaytypes.PostableIngestionKeyLimit
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid request body"))
return
}
response, err := handler.gateway.CreateIngestionKeyLimit(ctx, orgID, keyID, req.Signal, req.Config, req.Tags)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, response)
}
func (handler *handler) UpdateIngestionKeyLimit(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
limitID := coretypes.Param(r, "limitId")
if limitID == "" {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "limitId is required"))
return
}
var req gatewaytypes.UpdatableIngestionKeyLimit
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid request body"))
return
}
err = handler.gateway.UpdateIngestionKeyLimit(ctx, orgID, limitID, req.Config, req.Tags)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) DeleteIngestionKeyLimit(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
limitID := coretypes.Param(r, "limitId")
if limitID == "" {
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "limitId is required"))
return
}
err = handler.gateway.DeleteIngestionKeyLimit(ctx, orgID, limitID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func parseIntWithDefaultValue(value string, defaultValue int) (int, error) {
if value == "" {
return defaultValue, nil
}
result, err := strconv.Atoi(value)
if err != nil {
return 0, err
}
return result, nil
}