2019-12-01 14:07:12 +01:00
|
|
|
package reqlog
|
|
|
|
|
|
|
|
import (
|
2020-09-19 01:27:55 +02:00
|
|
|
"bytes"
|
2020-09-26 23:36:48 +02:00
|
|
|
"context"
|
2020-09-19 01:27:55 +02:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2022-01-21 11:45:54 +01:00
|
|
|
"io"
|
2020-09-19 01:27:55 +02:00
|
|
|
"io/ioutil"
|
2019-12-01 14:07:12 +01:00
|
|
|
"net/http"
|
2022-01-21 11:45:54 +01:00
|
|
|
"net/url"
|
2020-09-19 01:27:55 +02:00
|
|
|
|
2022-01-21 11:45:54 +01:00
|
|
|
"github.com/oklog/ulid"
|
|
|
|
|
2022-03-31 15:12:54 +02:00
|
|
|
"github.com/dstotijn/hetty/pkg/filter"
|
2022-02-27 14:28:28 +01:00
|
|
|
"github.com/dstotijn/hetty/pkg/log"
|
2020-09-22 18:33:02 +02:00
|
|
|
"github.com/dstotijn/hetty/pkg/proxy"
|
2020-10-01 21:46:35 +02:00
|
|
|
"github.com/dstotijn/hetty/pkg/scope"
|
2019-12-01 14:07:12 +01:00
|
|
|
)
|
|
|
|
|
2020-10-01 21:46:35 +02:00
|
|
|
type contextKey int
|
|
|
|
|
2022-03-23 14:31:27 +01:00
|
|
|
const (
|
|
|
|
LogBypassedKey contextKey = iota
|
|
|
|
ReqLogIDKey
|
|
|
|
)
|
2020-10-01 21:46:35 +02:00
|
|
|
|
2022-01-21 11:45:54 +01:00
|
|
|
var (
|
|
|
|
ErrRequestNotFound = errors.New("reqlog: request not found")
|
|
|
|
ErrProjectIDMustBeSet = errors.New("reqlog: project ID must be set")
|
|
|
|
)
|
|
|
|
|
|
|
|
type RequestLog struct {
|
|
|
|
ID ulid.ULID
|
|
|
|
ProjectID ulid.ULID
|
2020-10-29 20:54:17 +01:00
|
|
|
|
2022-01-21 11:45:54 +01:00
|
|
|
URL *url.URL
|
|
|
|
Method string
|
|
|
|
Proto string
|
|
|
|
Header http.Header
|
|
|
|
Body []byte
|
2020-09-20 22:30:17 +02:00
|
|
|
|
2022-01-21 11:45:54 +01:00
|
|
|
Response *ResponseLog
|
2019-12-01 14:07:12 +01:00
|
|
|
}
|
|
|
|
|
2022-01-21 11:45:54 +01:00
|
|
|
type ResponseLog struct {
|
|
|
|
Proto string
|
|
|
|
StatusCode int
|
|
|
|
Status string
|
|
|
|
Header http.Header
|
|
|
|
Body []byte
|
2019-12-01 14:07:12 +01:00
|
|
|
}
|
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
type Service struct {
|
2022-02-22 14:10:39 +01:00
|
|
|
bypassOutOfScopeRequests bool
|
|
|
|
findReqsFilter FindRequestsFilter
|
|
|
|
activeProjectID ulid.ULID
|
|
|
|
scope *scope.Scope
|
|
|
|
repo Repository
|
2022-02-27 14:28:28 +01:00
|
|
|
logger log.Logger
|
2020-10-01 21:46:35 +02:00
|
|
|
}
|
|
|
|
|
2020-10-29 20:54:17 +01:00
|
|
|
type FindRequestsFilter struct {
|
2022-01-21 11:45:54 +01:00
|
|
|
ProjectID ulid.ULID
|
|
|
|
OnlyInScope bool
|
2022-03-31 15:12:54 +02:00
|
|
|
SearchExpr filter.Expression
|
2020-10-01 21:46:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type Config struct {
|
2025-01-13 23:15:18 +01:00
|
|
|
ActiveProjectID ulid.ULID
|
|
|
|
Scope *scope.Scope
|
|
|
|
Repository Repository
|
|
|
|
Logger log.Logger
|
2019-12-01 14:07:12 +01:00
|
|
|
}
|
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
func NewService(cfg Config) *Service {
|
|
|
|
s := &Service{
|
2025-01-13 23:15:18 +01:00
|
|
|
activeProjectID: cfg.ActiveProjectID,
|
|
|
|
repo: cfg.Repository,
|
|
|
|
scope: cfg.Scope,
|
|
|
|
logger: cfg.Logger,
|
2020-10-01 21:46:35 +02:00
|
|
|
}
|
2022-02-27 14:28:28 +01:00
|
|
|
|
|
|
|
if s.logger == nil {
|
|
|
|
s.logger = log.NewNopLogger()
|
|
|
|
}
|
|
|
|
|
|
|
|
return s
|
2020-10-29 20:54:17 +01:00
|
|
|
}
|
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
func (svc *Service) FindRequests(ctx context.Context) ([]RequestLog, error) {
|
2022-02-22 14:10:39 +01:00
|
|
|
return svc.repo.FindRequestLogs(ctx, svc.findReqsFilter, svc.scope)
|
2020-09-20 22:30:17 +02:00
|
|
|
}
|
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
func (svc *Service) FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error) {
|
2025-01-13 23:15:18 +01:00
|
|
|
return svc.repo.FindRequestLogByID(ctx, svc.activeProjectID, id)
|
2020-02-23 22:07:46 +01:00
|
|
|
}
|
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
func (svc *Service) ClearRequests(ctx context.Context, projectID ulid.ULID) error {
|
2022-01-21 11:45:54 +01:00
|
|
|
return svc.repo.ClearRequestLogs(ctx, projectID)
|
2020-11-28 15:48:19 +01:00
|
|
|
}
|
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
func (svc *Service) storeResponse(ctx context.Context, reqLogID ulid.ULID, res *http.Response) error {
|
2022-02-22 14:10:39 +01:00
|
|
|
resLog, err := ParseHTTPResponse(res)
|
2022-01-21 11:45:54 +01:00
|
|
|
if err != nil {
|
2022-02-22 14:10:39 +01:00
|
|
|
return err
|
2022-01-21 11:45:54 +01:00
|
|
|
}
|
|
|
|
|
2025-01-13 23:15:18 +01:00
|
|
|
return svc.repo.StoreResponseLog(ctx, svc.activeProjectID, reqLogID, resLog)
|
2020-09-19 01:27:55 +02:00
|
|
|
}
|
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
|
2020-09-19 01:27:55 +02:00
|
|
|
return func(req *http.Request) {
|
|
|
|
next(req)
|
|
|
|
|
|
|
|
clone := req.Clone(req.Context())
|
2021-04-25 16:23:53 +02:00
|
|
|
|
2020-09-19 01:27:55 +02:00
|
|
|
var body []byte
|
2021-04-25 16:23:53 +02:00
|
|
|
|
2020-09-19 01:27:55 +02:00
|
|
|
if req.Body != nil {
|
|
|
|
// TODO: Use io.LimitReader.
|
|
|
|
var err error
|
2021-04-25 16:23:53 +02:00
|
|
|
|
2020-09-19 01:27:55 +02:00
|
|
|
body, err = ioutil.ReadAll(req.Body)
|
|
|
|
if err != nil {
|
2022-02-27 14:28:28 +01:00
|
|
|
svc.logger.Errorw("Failed to read request body for logging.",
|
|
|
|
"error", err)
|
2020-09-19 01:27:55 +02:00
|
|
|
return
|
|
|
|
}
|
2021-04-25 16:23:53 +02:00
|
|
|
|
2020-09-19 01:27:55 +02:00
|
|
|
req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
2022-01-21 11:45:54 +01:00
|
|
|
clone.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
2020-09-19 01:27:55 +02:00
|
|
|
}
|
|
|
|
|
2022-01-21 11:45:54 +01:00
|
|
|
// Bypass logging if no project is active.
|
2022-02-22 14:10:39 +01:00
|
|
|
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
|
2020-10-01 21:46:35 +02:00
|
|
|
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
|
2020-10-04 11:50:03 +02:00
|
|
|
*req = *req.WithContext(ctx)
|
2021-04-25 16:23:53 +02:00
|
|
|
|
2022-02-27 14:28:28 +01:00
|
|
|
svc.logger.Debugw("Bypassed logging: no active project.",
|
|
|
|
"url", req.URL.String())
|
|
|
|
|
2020-10-01 21:46:35 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-21 11:45:54 +01:00
|
|
|
// Bypass logging if this setting is enabled and the incoming request
|
|
|
|
// doesn't match any scope rules.
|
2022-02-22 14:10:39 +01:00
|
|
|
if svc.bypassOutOfScopeRequests && !svc.scope.Match(clone, body) {
|
2020-10-11 17:09:39 +02:00
|
|
|
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
|
|
|
|
*req = *req.WithContext(ctx)
|
2021-04-25 16:23:53 +02:00
|
|
|
|
2022-02-27 14:28:28 +01:00
|
|
|
svc.logger.Debugw("Bypassed logging: request doesn't match any scope rules.",
|
|
|
|
"url", req.URL.String())
|
|
|
|
|
2020-10-11 17:09:39 +02:00
|
|
|
return
|
2022-01-21 11:45:54 +01:00
|
|
|
}
|
|
|
|
|
2022-03-23 14:31:27 +01:00
|
|
|
reqID, ok := proxy.RequestIDFromContext(req.Context())
|
|
|
|
if !ok {
|
|
|
|
svc.logger.Errorw("Bypassed logging: request doesn't have an ID.")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-01-21 11:45:54 +01:00
|
|
|
reqLog := RequestLog{
|
2022-03-23 14:31:27 +01:00
|
|
|
ID: reqID,
|
2022-02-22 14:10:39 +01:00
|
|
|
ProjectID: svc.activeProjectID,
|
2022-01-21 11:45:54 +01:00
|
|
|
Method: clone.Method,
|
|
|
|
URL: clone.URL,
|
|
|
|
Proto: clone.Proto,
|
|
|
|
Header: clone.Header,
|
|
|
|
Body: body,
|
|
|
|
}
|
|
|
|
|
|
|
|
err := svc.repo.StoreRequestLog(req.Context(), reqLog)
|
|
|
|
if err != nil {
|
2022-02-27 14:28:28 +01:00
|
|
|
svc.logger.Errorw("Failed to store request log.",
|
|
|
|
"error", err)
|
2020-09-19 01:27:55 +02:00
|
|
|
return
|
|
|
|
}
|
2021-04-25 16:23:53 +02:00
|
|
|
|
2022-02-27 14:28:28 +01:00
|
|
|
svc.logger.Debugw("Stored request log.",
|
|
|
|
"reqLogID", reqLog.ID.String(),
|
|
|
|
"url", reqLog.URL.String())
|
|
|
|
|
2022-03-23 14:31:27 +01:00
|
|
|
ctx := context.WithValue(req.Context(), ReqLogIDKey, reqLog.ID)
|
2020-10-04 11:50:03 +02:00
|
|
|
*req = *req.WithContext(ctx)
|
2020-09-19 01:27:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
|
2020-09-19 01:27:55 +02:00
|
|
|
return func(res *http.Response) error {
|
|
|
|
if err := next(res); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-10-01 21:46:35 +02:00
|
|
|
if bypassed, _ := res.Request.Context().Value(LogBypassedKey).(bool); bypassed {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-03-23 14:31:27 +01:00
|
|
|
reqLogID, ok := res.Request.Context().Value(ReqLogIDKey).(ulid.ULID)
|
2022-01-21 11:45:54 +01:00
|
|
|
if !ok {
|
2020-09-27 14:58:37 +02:00
|
|
|
return errors.New("reqlog: request is missing ID")
|
|
|
|
}
|
|
|
|
|
2020-09-19 01:27:55 +02:00
|
|
|
clone := *res
|
|
|
|
|
2022-03-23 14:31:27 +01:00
|
|
|
if res.Body != nil {
|
|
|
|
// TODO: Use io.LimitReader.
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("reqlog: could not read response body: %w", err)
|
|
|
|
}
|
2021-04-25 16:23:53 +02:00
|
|
|
|
2022-03-23 14:31:27 +01:00
|
|
|
res.Body = io.NopCloser(bytes.NewBuffer(body))
|
|
|
|
clone.Body = io.NopCloser(bytes.NewBuffer(body))
|
|
|
|
}
|
2020-09-19 01:27:55 +02:00
|
|
|
|
2020-09-27 14:58:37 +02:00
|
|
|
go func() {
|
2022-01-21 11:45:54 +01:00
|
|
|
if err := svc.storeResponse(context.Background(), reqLogID, &clone); err != nil {
|
2022-02-27 14:28:28 +01:00
|
|
|
svc.logger.Errorw("Failed to store response log.",
|
|
|
|
"error", err)
|
|
|
|
} else {
|
|
|
|
svc.logger.Debugw("Stored response log.",
|
|
|
|
"reqLogID", reqLogID.String())
|
2020-09-19 01:27:55 +02:00
|
|
|
}
|
2020-09-27 14:58:37 +02:00
|
|
|
}()
|
2020-09-19 01:27:55 +02:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2019-12-01 14:07:12 +01:00
|
|
|
}
|
2022-02-22 14:10:39 +01:00
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
func (svc *Service) SetActiveProjectID(id ulid.ULID) {
|
2022-02-22 14:10:39 +01:00
|
|
|
svc.activeProjectID = id
|
|
|
|
}
|
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
func (svc *Service) ActiveProjectID() ulid.ULID {
|
2022-02-22 14:10:39 +01:00
|
|
|
return svc.activeProjectID
|
|
|
|
}
|
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
func (svc *Service) SetFindReqsFilter(filter FindRequestsFilter) {
|
2022-02-22 14:10:39 +01:00
|
|
|
svc.findReqsFilter = filter
|
|
|
|
}
|
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
func (svc *Service) FindReqsFilter() FindRequestsFilter {
|
2022-02-22 14:10:39 +01:00
|
|
|
return svc.findReqsFilter
|
|
|
|
}
|
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
func (svc *Service) SetBypassOutOfScopeRequests(bypass bool) {
|
2022-02-22 14:10:39 +01:00
|
|
|
svc.bypassOutOfScopeRequests = bypass
|
|
|
|
}
|
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
func (svc *Service) BypassOutOfScopeRequests() bool {
|
2022-02-22 14:10:39 +01:00
|
|
|
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
|
|
|
|
}
|