Replace GraphQL server with Connect RPC

This commit is contained in:
David Stotijn
2025-02-05 21:54:59 +01:00
parent 52c83a1989
commit 6889c9c183
53 changed files with 5875 additions and 11685 deletions

View File

@ -6,13 +6,13 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"github.com/oklog/ulid"
"connectrpc.com/connect"
"github.com/oklog/ulid/v2"
"github.com/dstotijn/hetty/pkg/filter"
httppb "github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/log"
"github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/scope"
@ -26,48 +26,21 @@ const (
)
var (
ErrRequestNotFound = errors.New("reqlog: request not found")
ErrRequestLogNotFound = errors.New("reqlog: request not found")
ErrProjectIDMustBeSet = errors.New("reqlog: project ID must be set")
)
type RequestLog struct {
ID ulid.ULID
ProjectID ulid.ULID
URL *url.URL
Method string
Proto string
Header http.Header
Body []byte
Response *ResponseLog
}
type ResponseLog struct {
Proto string
StatusCode int
Status string
Header http.Header
Body []byte
}
type Service struct {
bypassOutOfScopeRequests bool
findReqsFilter FindRequestsFilter
activeProjectID ulid.ULID
reqsFilter *RequestLogsFilter
activeProjectID string
scope *scope.Scope
repo Repository
logger log.Logger
}
type FindRequestsFilter struct {
ProjectID ulid.ULID
OnlyInScope bool
SearchExpr filter.Expression
}
type Config struct {
ActiveProjectID ulid.ULID
ActiveProjectID string
Scope *scope.Scope
Repository Repository
Logger log.Logger
@ -88,25 +61,92 @@ func NewService(cfg Config) *Service {
return s
}
func (svc *Service) FindRequests(ctx context.Context) ([]RequestLog, error) {
return svc.repo.FindRequestLogs(ctx, svc.findReqsFilter, svc.scope)
func (svc *Service) ListHttpRequestLogs(ctx context.Context, req *connect.Request[ListHttpRequestLogsRequest]) (*connect.Response[ListHttpRequestLogsResponse], error) {
projectID := svc.activeProjectID
if projectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
}
reqLogs, err := svc.repo.FindRequestLogs(ctx, projectID, svc.filterRequestLog)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("reqlog: failed to find request logs: %w", err))
}
return connect.NewResponse(&ListHttpRequestLogsResponse{
HttpRequestLogs: reqLogs,
}), nil
}
func (svc *Service) FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error) {
func (svc *Service) filterRequestLog(reqLog *HttpRequestLog) (bool, error) {
if svc.reqsFilter.GetOnlyInScope() && svc.scope != nil && !reqLog.MatchScope(svc.scope) {
return false, nil
}
var f filter.Expression
var err error
if expr := svc.reqsFilter.GetSearchExpr(); expr != "" {
f, err = filter.ParseQuery(expr)
if err != nil {
return false, fmt.Errorf("failed to parse search expression: %w", err)
}
}
if f == nil {
return true, nil
}
match, err := reqLog.Matches(f)
if err != nil {
return false, fmt.Errorf("failed to match search expression for request log (id: %v): %w", reqLog.Id, err)
}
return match, nil
}
func (svc *Service) FindRequestLogByID(ctx context.Context, id string) (*HttpRequestLog, error) {
if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
}
return svc.repo.FindRequestLogByID(ctx, svc.activeProjectID, id)
}
func (svc *Service) ClearRequests(ctx context.Context, projectID ulid.ULID) error {
return svc.repo.ClearRequestLogs(ctx, projectID)
// GetHttpRequestLog implements HttpRequestLogServiceHandler.
func (svc *Service) GetHttpRequestLog(ctx context.Context, req *connect.Request[GetHttpRequestLogRequest]) (*connect.Response[GetHttpRequestLogResponse], error) {
id, err := ulid.Parse(req.Msg.Id)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}
reqLog, err := svc.repo.FindRequestLogByID(ctx, svc.activeProjectID, id.String())
if errors.Is(err, ErrRequestLogNotFound) {
return nil, connect.NewError(connect.CodeNotFound, err)
}
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
return connect.NewResponse(&GetHttpRequestLogResponse{
HttpRequestLog: reqLog,
}), nil
}
func (svc *Service) storeResponse(ctx context.Context, reqLogID ulid.ULID, res *http.Response) error {
resLog, err := ParseHTTPResponse(res)
func (svc *Service) ClearHttpRequestLogs(ctx context.Context, req *connect.Request[ClearHttpRequestLogsRequest]) (*connect.Response[ClearHttpRequestLogsResponse], error) {
err := svc.repo.ClearRequestLogs(ctx, svc.activeProjectID)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("reqlog: failed to clear request logs: %w", err))
}
return connect.NewResponse(&ClearHttpRequestLogsResponse{}), nil
}
func (svc *Service) storeResponse(ctx context.Context, reqLogID string, res *http.Response) error {
respb, err := httppb.ParseHTTPResponse(res)
if err != nil {
return err
}
return svc.repo.StoreResponseLog(ctx, svc.activeProjectID, reqLogID, resLog)
return svc.repo.StoreResponseLog(ctx, svc.activeProjectID, reqLogID, respb)
}
func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
@ -121,19 +161,19 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
// TODO: Use io.LimitReader.
var err error
body, err = ioutil.ReadAll(req.Body)
body, err = io.ReadAll(req.Body)
if err != nil {
svc.logger.Errorw("Failed to read request body for logging.",
"error", err)
return
}
req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
clone.Body = ioutil.NopCloser(bytes.NewBuffer(body))
req.Body = io.NopCloser(bytes.NewBuffer(body))
clone.Body = io.NopCloser(bytes.NewBuffer(body))
}
// Bypass logging if no project is active.
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
if svc.activeProjectID == "" {
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
*req = *req.WithContext(ctx)
@ -161,14 +201,37 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
return
}
reqLog := RequestLog{
ID: reqID,
ProjectID: svc.activeProjectID,
Method: clone.Method,
URL: clone.URL,
Proto: clone.Proto,
Header: clone.Header,
Body: body,
proto, ok := httppb.ProtoMap[clone.Proto]
if !ok {
svc.logger.Errorw("Bypassed logging: request has an invalid protocol.",
"proto", clone.Proto)
return
}
method, ok := httppb.MethodMap[clone.Method]
if !ok {
svc.logger.Errorw("Bypassed logging: request has an invalid method.",
"method", clone.Method)
return
}
headers := []*httppb.Header{}
for k, v := range clone.Header {
for _, vv := range v {
headers = append(headers, &httppb.Header{Key: k, Value: vv})
}
}
reqLog := &HttpRequestLog{
Id: reqID.String(),
ProjectId: svc.activeProjectID,
Request: &httppb.Request{
Url: clone.URL.String(),
Method: method,
Protocol: proto,
Headers: headers,
Body: body,
},
}
err := svc.repo.StoreRequestLog(req.Context(), reqLog)
@ -179,10 +242,10 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
}
svc.logger.Debugw("Stored request log.",
"reqLogID", reqLog.ID.String(),
"url", reqLog.URL.String())
ctx := context.WithValue(req.Context(), ReqLogIDKey, reqLog.ID)
"reqLogID", reqLog.Id,
"url", reqLog.Request.Url,
)
ctx := context.WithValue(req.Context(), ReqLogIDKey, reqID)
*req = *req.WithContext(ctx)
}
}
@ -216,7 +279,7 @@ func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
}
go func() {
if err := svc.storeResponse(context.Background(), reqLogID, &clone); err != nil {
if err := svc.storeResponse(context.Background(), reqLogID.String(), &clone); err != nil {
svc.logger.Errorw("Failed to store response log.",
"error", err)
} else {
@ -229,20 +292,16 @@ func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
}
}
func (svc *Service) SetActiveProjectID(id ulid.ULID) {
func (svc *Service) SetActiveProjectID(id string) {
svc.activeProjectID = id
}
func (svc *Service) ActiveProjectID() ulid.ULID {
func (svc *Service) ActiveProjectID() string {
return svc.activeProjectID
}
func (svc *Service) SetFindReqsFilter(filter FindRequestsFilter) {
svc.findReqsFilter = filter
}
func (svc *Service) FindReqsFilter() FindRequestsFilter {
return svc.findReqsFilter
func (svc *Service) SetRequestLogsFilter(filter *RequestLogsFilter) {
svc.reqsFilter = filter
}
func (svc *Service) SetBypassOutOfScopeRequests(bypass bool) {
@ -252,18 +311,3 @@ func (svc *Service) SetBypassOutOfScopeRequests(bypass bool) {
func (svc *Service) BypassOutOfScopeRequests() bool {
return svc.bypassOutOfScopeRequests
}
func ParseHTTPResponse(res *http.Response) (ResponseLog, error) {
body, err := io.ReadAll(res.Body)
if err != nil {
return ResponseLog{}, fmt.Errorf("reqlog: could not read body: %w", err)
}
return ResponseLog{
Proto: res.Proto,
StatusCode: res.StatusCode,
Status: res.Status,
Header: res.Header,
Body: body,
}, nil
}