mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Replace GraphQL server with Connect RPC
This commit is contained in:
@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user