Replace SQLite with BadgerDB

This commit is contained in:
David Stotijn
2022-01-21 11:45:54 +01:00
parent 8a3b3cbf02
commit d84d2d0905
49 changed files with 2496 additions and 2677 deletions

View File

@ -1,318 +0,0 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package reqlog_test
import (
"context"
"github.com/dstotijn/hetty/pkg/proj"
"sync"
)
// Ensure, that ProjServiceMock does implement proj.Service.
// If this is not the case, regenerate this file with moq.
var _ proj.Service = &ProjServiceMock{}
// ProjServiceMock is a mock implementation of proj.Service.
//
// func TestSomethingThatUsesService(t *testing.T) {
//
// // make and configure a mocked proj.Service
// mockedService := &ProjServiceMock{
// ActiveProjectFunc: func() (proj.Project, error) {
// panic("mock out the ActiveProject method")
// },
// CloseFunc: func() error {
// panic("mock out the Close method")
// },
// DeleteFunc: func(name string) error {
// panic("mock out the Delete method")
// },
// OnProjectCloseFunc: func(fn proj.OnProjectCloseFn) {
// panic("mock out the OnProjectClose method")
// },
// OnProjectOpenFunc: func(fn proj.OnProjectOpenFn) {
// panic("mock out the OnProjectOpen method")
// },
// OpenFunc: func(ctx context.Context, name string) (proj.Project, error) {
// panic("mock out the Open method")
// },
// ProjectsFunc: func() ([]proj.Project, error) {
// panic("mock out the Projects method")
// },
// }
//
// // use mockedService in code that requires proj.Service
// // and then make assertions.
//
// }
type ProjServiceMock struct {
// ActiveProjectFunc mocks the ActiveProject method.
ActiveProjectFunc func() (proj.Project, error)
// CloseFunc mocks the Close method.
CloseFunc func() error
// DeleteFunc mocks the Delete method.
DeleteFunc func(name string) error
// OnProjectCloseFunc mocks the OnProjectClose method.
OnProjectCloseFunc func(fn proj.OnProjectCloseFn)
// OnProjectOpenFunc mocks the OnProjectOpen method.
OnProjectOpenFunc func(fn proj.OnProjectOpenFn)
// OpenFunc mocks the Open method.
OpenFunc func(ctx context.Context, name string) (proj.Project, error)
// ProjectsFunc mocks the Projects method.
ProjectsFunc func() ([]proj.Project, error)
// calls tracks calls to the methods.
calls struct {
// ActiveProject holds details about calls to the ActiveProject method.
ActiveProject []struct {
}
// Close holds details about calls to the Close method.
Close []struct {
}
// Delete holds details about calls to the Delete method.
Delete []struct {
// Name is the name argument value.
Name string
}
// OnProjectClose holds details about calls to the OnProjectClose method.
OnProjectClose []struct {
// Fn is the fn argument value.
Fn proj.OnProjectCloseFn
}
// OnProjectOpen holds details about calls to the OnProjectOpen method.
OnProjectOpen []struct {
// Fn is the fn argument value.
Fn proj.OnProjectOpenFn
}
// Open holds details about calls to the Open method.
Open []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Name is the name argument value.
Name string
}
// Projects holds details about calls to the Projects method.
Projects []struct {
}
}
lockActiveProject sync.RWMutex
lockClose sync.RWMutex
lockDelete sync.RWMutex
lockOnProjectClose sync.RWMutex
lockOnProjectOpen sync.RWMutex
lockOpen sync.RWMutex
lockProjects sync.RWMutex
}
// ActiveProject calls ActiveProjectFunc.
func (mock *ProjServiceMock) ActiveProject() (proj.Project, error) {
if mock.ActiveProjectFunc == nil {
panic("ProjServiceMock.ActiveProjectFunc: method is nil but Service.ActiveProject was just called")
}
callInfo := struct {
}{}
mock.lockActiveProject.Lock()
mock.calls.ActiveProject = append(mock.calls.ActiveProject, callInfo)
mock.lockActiveProject.Unlock()
return mock.ActiveProjectFunc()
}
// ActiveProjectCalls gets all the calls that were made to ActiveProject.
// Check the length with:
// len(mockedService.ActiveProjectCalls())
func (mock *ProjServiceMock) ActiveProjectCalls() []struct {
} {
var calls []struct {
}
mock.lockActiveProject.RLock()
calls = mock.calls.ActiveProject
mock.lockActiveProject.RUnlock()
return calls
}
// Close calls CloseFunc.
func (mock *ProjServiceMock) Close() error {
if mock.CloseFunc == nil {
panic("ProjServiceMock.CloseFunc: method is nil but Service.Close was just called")
}
callInfo := struct {
}{}
mock.lockClose.Lock()
mock.calls.Close = append(mock.calls.Close, callInfo)
mock.lockClose.Unlock()
return mock.CloseFunc()
}
// CloseCalls gets all the calls that were made to Close.
// Check the length with:
// len(mockedService.CloseCalls())
func (mock *ProjServiceMock) CloseCalls() []struct {
} {
var calls []struct {
}
mock.lockClose.RLock()
calls = mock.calls.Close
mock.lockClose.RUnlock()
return calls
}
// Delete calls DeleteFunc.
func (mock *ProjServiceMock) Delete(name string) error {
if mock.DeleteFunc == nil {
panic("ProjServiceMock.DeleteFunc: method is nil but Service.Delete was just called")
}
callInfo := struct {
Name string
}{
Name: name,
}
mock.lockDelete.Lock()
mock.calls.Delete = append(mock.calls.Delete, callInfo)
mock.lockDelete.Unlock()
return mock.DeleteFunc(name)
}
// DeleteCalls gets all the calls that were made to Delete.
// Check the length with:
// len(mockedService.DeleteCalls())
func (mock *ProjServiceMock) DeleteCalls() []struct {
Name string
} {
var calls []struct {
Name string
}
mock.lockDelete.RLock()
calls = mock.calls.Delete
mock.lockDelete.RUnlock()
return calls
}
// OnProjectClose calls OnProjectCloseFunc.
func (mock *ProjServiceMock) OnProjectClose(fn proj.OnProjectCloseFn) {
if mock.OnProjectCloseFunc == nil {
panic("ProjServiceMock.OnProjectCloseFunc: method is nil but Service.OnProjectClose was just called")
}
callInfo := struct {
Fn proj.OnProjectCloseFn
}{
Fn: fn,
}
mock.lockOnProjectClose.Lock()
mock.calls.OnProjectClose = append(mock.calls.OnProjectClose, callInfo)
mock.lockOnProjectClose.Unlock()
mock.OnProjectCloseFunc(fn)
}
// OnProjectCloseCalls gets all the calls that were made to OnProjectClose.
// Check the length with:
// len(mockedService.OnProjectCloseCalls())
func (mock *ProjServiceMock) OnProjectCloseCalls() []struct {
Fn proj.OnProjectCloseFn
} {
var calls []struct {
Fn proj.OnProjectCloseFn
}
mock.lockOnProjectClose.RLock()
calls = mock.calls.OnProjectClose
mock.lockOnProjectClose.RUnlock()
return calls
}
// OnProjectOpen calls OnProjectOpenFunc.
func (mock *ProjServiceMock) OnProjectOpen(fn proj.OnProjectOpenFn) {
if mock.OnProjectOpenFunc == nil {
panic("ProjServiceMock.OnProjectOpenFunc: method is nil but Service.OnProjectOpen was just called")
}
callInfo := struct {
Fn proj.OnProjectOpenFn
}{
Fn: fn,
}
mock.lockOnProjectOpen.Lock()
mock.calls.OnProjectOpen = append(mock.calls.OnProjectOpen, callInfo)
mock.lockOnProjectOpen.Unlock()
mock.OnProjectOpenFunc(fn)
}
// OnProjectOpenCalls gets all the calls that were made to OnProjectOpen.
// Check the length with:
// len(mockedService.OnProjectOpenCalls())
func (mock *ProjServiceMock) OnProjectOpenCalls() []struct {
Fn proj.OnProjectOpenFn
} {
var calls []struct {
Fn proj.OnProjectOpenFn
}
mock.lockOnProjectOpen.RLock()
calls = mock.calls.OnProjectOpen
mock.lockOnProjectOpen.RUnlock()
return calls
}
// Open calls OpenFunc.
func (mock *ProjServiceMock) Open(ctx context.Context, name string) (proj.Project, error) {
if mock.OpenFunc == nil {
panic("ProjServiceMock.OpenFunc: method is nil but Service.Open was just called")
}
callInfo := struct {
Ctx context.Context
Name string
}{
Ctx: ctx,
Name: name,
}
mock.lockOpen.Lock()
mock.calls.Open = append(mock.calls.Open, callInfo)
mock.lockOpen.Unlock()
return mock.OpenFunc(ctx, name)
}
// OpenCalls gets all the calls that were made to Open.
// Check the length with:
// len(mockedService.OpenCalls())
func (mock *ProjServiceMock) OpenCalls() []struct {
Ctx context.Context
Name string
} {
var calls []struct {
Ctx context.Context
Name string
}
mock.lockOpen.RLock()
calls = mock.calls.Open
mock.lockOpen.RUnlock()
return calls
}
// Projects calls ProjectsFunc.
func (mock *ProjServiceMock) Projects() ([]proj.Project, error) {
if mock.ProjectsFunc == nil {
panic("ProjServiceMock.ProjectsFunc: method is nil but Service.Projects was just called")
}
callInfo := struct {
}{}
mock.lockProjects.Lock()
mock.calls.Projects = append(mock.calls.Projects, callInfo)
mock.lockProjects.Unlock()
return mock.ProjectsFunc()
}
// ProjectsCalls gets all the calls that were made to Projects.
// Check the length with:
// len(mockedService.ProjectsCalls())
func (mock *ProjServiceMock) ProjectsCalls() []struct {
} {
var calls []struct {
}
mock.lockProjects.RLock()
calls = mock.calls.Projects
mock.lockProjects.RUnlock()
return calls
}

View File

@ -2,18 +2,16 @@ package reqlog
import (
"context"
"net/http"
"time"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/scope"
)
type Repository interface {
FindRequestLogs(ctx context.Context, filter FindRequestsFilter, scope *scope.Scope) ([]Request, error)
FindRequestLogByID(ctx context.Context, id int64) (Request, error)
AddRequestLog(ctx context.Context, req http.Request, body []byte, timestamp time.Time) (*Request, error)
AddResponseLog(ctx context.Context, reqID int64, res http.Response, body []byte, timestamp time.Time) (*Response, error) // nolint:lll
ClearRequestLogs(ctx context.Context) error
UpsertSettings(ctx context.Context, module string, settings interface{}) error
FindSettingsByModule(ctx context.Context, module string, settings interface{}) error
FindRequestLogs(ctx context.Context, filter FindRequestsFilter, scope *scope.Scope) ([]RequestLog, error)
FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error)
StoreRequestLog(ctx context.Context, reqLog RequestLog) error
StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog ResponseLog) error
ClearRequestLogs(ctx context.Context, projectID ulid.ULID) error
}

View File

@ -7,9 +7,8 @@ import (
"context"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
"net/http"
"github.com/oklog/ulid"
"sync"
"time"
)
// Ensure, that RepoMock does implement reqlog.Repository.
@ -22,26 +21,20 @@ var _ reqlog.Repository = &RepoMock{}
//
// // make and configure a mocked reqlog.Repository
// mockedRepository := &RepoMock{
// AddRequestLogFunc: func(ctx context.Context, req http.Request, body []byte, timestamp time.Time) (*reqlog.Request, error) {
// panic("mock out the AddRequestLog method")
// },
// AddResponseLogFunc: func(ctx context.Context, reqID int64, res http.Response, body []byte, timestamp time.Time) (*reqlog.Response, error) {
// panic("mock out the AddResponseLog method")
// },
// ClearRequestLogsFunc: func(ctx context.Context) error {
// ClearRequestLogsFunc: func(ctx context.Context, projectID ulid.ULID) error {
// panic("mock out the ClearRequestLogs method")
// },
// FindRequestLogByIDFunc: func(ctx context.Context, id int64) (reqlog.Request, error) {
// FindRequestLogByIDFunc: func(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error) {
// panic("mock out the FindRequestLogByID method")
// },
// FindRequestLogsFunc: func(ctx context.Context, filter reqlog.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]reqlog.Request, error) {
// FindRequestLogsFunc: func(ctx context.Context, filter reqlog.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]reqlog.RequestLog, error) {
// panic("mock out the FindRequestLogs method")
// },
// FindSettingsByModuleFunc: func(ctx context.Context, module string, settings interface{}) error {
// panic("mock out the FindSettingsByModule method")
// StoreRequestLogFunc: func(ctx context.Context, reqLog reqlog.RequestLog) error {
// panic("mock out the StoreRequestLog method")
// },
// UpsertSettingsFunc: func(ctx context.Context, module string, settings interface{}) error {
// panic("mock out the UpsertSettings method")
// StoreResponseLogFunc: func(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
// panic("mock out the StoreResponseLog method")
// },
// }
//
@ -50,64 +43,36 @@ var _ reqlog.Repository = &RepoMock{}
//
// }
type RepoMock struct {
// AddRequestLogFunc mocks the AddRequestLog method.
AddRequestLogFunc func(ctx context.Context, req http.Request, body []byte, timestamp time.Time) (*reqlog.Request, error)
// AddResponseLogFunc mocks the AddResponseLog method.
AddResponseLogFunc func(ctx context.Context, reqID int64, res http.Response, body []byte, timestamp time.Time) (*reqlog.Response, error)
// ClearRequestLogsFunc mocks the ClearRequestLogs method.
ClearRequestLogsFunc func(ctx context.Context) error
ClearRequestLogsFunc func(ctx context.Context, projectID ulid.ULID) error
// FindRequestLogByIDFunc mocks the FindRequestLogByID method.
FindRequestLogByIDFunc func(ctx context.Context, id int64) (reqlog.Request, error)
FindRequestLogByIDFunc func(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error)
// FindRequestLogsFunc mocks the FindRequestLogs method.
FindRequestLogsFunc func(ctx context.Context, filter reqlog.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]reqlog.Request, error)
FindRequestLogsFunc func(ctx context.Context, filter reqlog.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]reqlog.RequestLog, error)
// FindSettingsByModuleFunc mocks the FindSettingsByModule method.
FindSettingsByModuleFunc func(ctx context.Context, module string, settings interface{}) error
// StoreRequestLogFunc mocks the StoreRequestLog method.
StoreRequestLogFunc func(ctx context.Context, reqLog reqlog.RequestLog) error
// UpsertSettingsFunc mocks the UpsertSettings method.
UpsertSettingsFunc func(ctx context.Context, module string, settings interface{}) error
// StoreResponseLogFunc mocks the StoreResponseLog method.
StoreResponseLogFunc func(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error
// calls tracks calls to the methods.
calls struct {
// AddRequestLog holds details about calls to the AddRequestLog method.
AddRequestLog []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Req is the req argument value.
Req http.Request
// Body is the body argument value.
Body []byte
// Timestamp is the timestamp argument value.
Timestamp time.Time
}
// AddResponseLog holds details about calls to the AddResponseLog method.
AddResponseLog []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ReqID is the reqID argument value.
ReqID int64
// Res is the res argument value.
Res http.Response
// Body is the body argument value.
Body []byte
// Timestamp is the timestamp argument value.
Timestamp time.Time
}
// ClearRequestLogs holds details about calls to the ClearRequestLogs method.
ClearRequestLogs []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ProjectID is the projectID argument value.
ProjectID ulid.ULID
}
// FindRequestLogByID holds details about calls to the FindRequestLogByID method.
FindRequestLogByID []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ID is the id argument value.
ID int64
ID ulid.ULID
}
// FindRequestLogs holds details about calls to the FindRequestLogs method.
FindRequestLogs []struct {
@ -118,148 +83,58 @@ type RepoMock struct {
// ScopeMoqParam is the scopeMoqParam argument value.
ScopeMoqParam *scope.Scope
}
// FindSettingsByModule holds details about calls to the FindSettingsByModule method.
FindSettingsByModule []struct {
// StoreRequestLog holds details about calls to the StoreRequestLog method.
StoreRequestLog []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Module is the module argument value.
Module string
// Settings is the settings argument value.
Settings interface{}
// ReqLog is the reqLog argument value.
ReqLog reqlog.RequestLog
}
// UpsertSettings holds details about calls to the UpsertSettings method.
UpsertSettings []struct {
// StoreResponseLog holds details about calls to the StoreResponseLog method.
StoreResponseLog []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Module is the module argument value.
Module string
// Settings is the settings argument value.
Settings interface{}
// ReqLogID is the reqLogID argument value.
ReqLogID ulid.ULID
// ResLog is the resLog argument value.
ResLog reqlog.ResponseLog
}
}
lockAddRequestLog sync.RWMutex
lockAddResponseLog sync.RWMutex
lockClearRequestLogs sync.RWMutex
lockFindRequestLogByID sync.RWMutex
lockFindRequestLogs sync.RWMutex
lockFindSettingsByModule sync.RWMutex
lockUpsertSettings sync.RWMutex
}
// AddRequestLog calls AddRequestLogFunc.
func (mock *RepoMock) AddRequestLog(ctx context.Context, req http.Request, body []byte, timestamp time.Time) (*reqlog.Request, error) {
if mock.AddRequestLogFunc == nil {
panic("RepoMock.AddRequestLogFunc: method is nil but Repository.AddRequestLog was just called")
}
callInfo := struct {
Ctx context.Context
Req http.Request
Body []byte
Timestamp time.Time
}{
Ctx: ctx,
Req: req,
Body: body,
Timestamp: timestamp,
}
mock.lockAddRequestLog.Lock()
mock.calls.AddRequestLog = append(mock.calls.AddRequestLog, callInfo)
mock.lockAddRequestLog.Unlock()
return mock.AddRequestLogFunc(ctx, req, body, timestamp)
}
// AddRequestLogCalls gets all the calls that were made to AddRequestLog.
// Check the length with:
// len(mockedRepository.AddRequestLogCalls())
func (mock *RepoMock) AddRequestLogCalls() []struct {
Ctx context.Context
Req http.Request
Body []byte
Timestamp time.Time
} {
var calls []struct {
Ctx context.Context
Req http.Request
Body []byte
Timestamp time.Time
}
mock.lockAddRequestLog.RLock()
calls = mock.calls.AddRequestLog
mock.lockAddRequestLog.RUnlock()
return calls
}
// AddResponseLog calls AddResponseLogFunc.
func (mock *RepoMock) AddResponseLog(ctx context.Context, reqID int64, res http.Response, body []byte, timestamp time.Time) (*reqlog.Response, error) {
if mock.AddResponseLogFunc == nil {
panic("RepoMock.AddResponseLogFunc: method is nil but Repository.AddResponseLog was just called")
}
callInfo := struct {
Ctx context.Context
ReqID int64
Res http.Response
Body []byte
Timestamp time.Time
}{
Ctx: ctx,
ReqID: reqID,
Res: res,
Body: body,
Timestamp: timestamp,
}
mock.lockAddResponseLog.Lock()
mock.calls.AddResponseLog = append(mock.calls.AddResponseLog, callInfo)
mock.lockAddResponseLog.Unlock()
return mock.AddResponseLogFunc(ctx, reqID, res, body, timestamp)
}
// AddResponseLogCalls gets all the calls that were made to AddResponseLog.
// Check the length with:
// len(mockedRepository.AddResponseLogCalls())
func (mock *RepoMock) AddResponseLogCalls() []struct {
Ctx context.Context
ReqID int64
Res http.Response
Body []byte
Timestamp time.Time
} {
var calls []struct {
Ctx context.Context
ReqID int64
Res http.Response
Body []byte
Timestamp time.Time
}
mock.lockAddResponseLog.RLock()
calls = mock.calls.AddResponseLog
mock.lockAddResponseLog.RUnlock()
return calls
lockClearRequestLogs sync.RWMutex
lockFindRequestLogByID sync.RWMutex
lockFindRequestLogs sync.RWMutex
lockStoreRequestLog sync.RWMutex
lockStoreResponseLog sync.RWMutex
}
// ClearRequestLogs calls ClearRequestLogsFunc.
func (mock *RepoMock) ClearRequestLogs(ctx context.Context) error {
func (mock *RepoMock) ClearRequestLogs(ctx context.Context, projectID ulid.ULID) error {
if mock.ClearRequestLogsFunc == nil {
panic("RepoMock.ClearRequestLogsFunc: method is nil but Repository.ClearRequestLogs was just called")
}
callInfo := struct {
Ctx context.Context
Ctx context.Context
ProjectID ulid.ULID
}{
Ctx: ctx,
Ctx: ctx,
ProjectID: projectID,
}
mock.lockClearRequestLogs.Lock()
mock.calls.ClearRequestLogs = append(mock.calls.ClearRequestLogs, callInfo)
mock.lockClearRequestLogs.Unlock()
return mock.ClearRequestLogsFunc(ctx)
return mock.ClearRequestLogsFunc(ctx, projectID)
}
// ClearRequestLogsCalls gets all the calls that were made to ClearRequestLogs.
// Check the length with:
// len(mockedRepository.ClearRequestLogsCalls())
func (mock *RepoMock) ClearRequestLogsCalls() []struct {
Ctx context.Context
Ctx context.Context
ProjectID ulid.ULID
} {
var calls []struct {
Ctx context.Context
Ctx context.Context
ProjectID ulid.ULID
}
mock.lockClearRequestLogs.RLock()
calls = mock.calls.ClearRequestLogs
@ -268,13 +143,13 @@ func (mock *RepoMock) ClearRequestLogsCalls() []struct {
}
// FindRequestLogByID calls FindRequestLogByIDFunc.
func (mock *RepoMock) FindRequestLogByID(ctx context.Context, id int64) (reqlog.Request, error) {
func (mock *RepoMock) FindRequestLogByID(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error) {
if mock.FindRequestLogByIDFunc == nil {
panic("RepoMock.FindRequestLogByIDFunc: method is nil but Repository.FindRequestLogByID was just called")
}
callInfo := struct {
Ctx context.Context
ID int64
ID ulid.ULID
}{
Ctx: ctx,
ID: id,
@ -290,11 +165,11 @@ func (mock *RepoMock) FindRequestLogByID(ctx context.Context, id int64) (reqlog.
// len(mockedRepository.FindRequestLogByIDCalls())
func (mock *RepoMock) FindRequestLogByIDCalls() []struct {
Ctx context.Context
ID int64
ID ulid.ULID
} {
var calls []struct {
Ctx context.Context
ID int64
ID ulid.ULID
}
mock.lockFindRequestLogByID.RLock()
calls = mock.calls.FindRequestLogByID
@ -303,7 +178,7 @@ func (mock *RepoMock) FindRequestLogByIDCalls() []struct {
}
// FindRequestLogs calls FindRequestLogsFunc.
func (mock *RepoMock) FindRequestLogs(ctx context.Context, filter reqlog.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]reqlog.Request, error) {
func (mock *RepoMock) FindRequestLogs(ctx context.Context, filter reqlog.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]reqlog.RequestLog, error) {
if mock.FindRequestLogsFunc == nil {
panic("RepoMock.FindRequestLogsFunc: method is nil but Repository.FindRequestLogs was just called")
}
@ -341,80 +216,76 @@ func (mock *RepoMock) FindRequestLogsCalls() []struct {
return calls
}
// FindSettingsByModule calls FindSettingsByModuleFunc.
func (mock *RepoMock) FindSettingsByModule(ctx context.Context, module string, settings interface{}) error {
if mock.FindSettingsByModuleFunc == nil {
panic("RepoMock.FindSettingsByModuleFunc: method is nil but Repository.FindSettingsByModule was just called")
// StoreRequestLog calls StoreRequestLogFunc.
func (mock *RepoMock) StoreRequestLog(ctx context.Context, reqLog reqlog.RequestLog) error {
if mock.StoreRequestLogFunc == nil {
panic("RepoMock.StoreRequestLogFunc: method is nil but Repository.StoreRequestLog was just called")
}
callInfo := struct {
Ctx context.Context
Module string
Settings interface{}
Ctx context.Context
ReqLog reqlog.RequestLog
}{
Ctx: ctx,
Module: module,
Settings: settings,
Ctx: ctx,
ReqLog: reqLog,
}
mock.lockFindSettingsByModule.Lock()
mock.calls.FindSettingsByModule = append(mock.calls.FindSettingsByModule, callInfo)
mock.lockFindSettingsByModule.Unlock()
return mock.FindSettingsByModuleFunc(ctx, module, settings)
mock.lockStoreRequestLog.Lock()
mock.calls.StoreRequestLog = append(mock.calls.StoreRequestLog, callInfo)
mock.lockStoreRequestLog.Unlock()
return mock.StoreRequestLogFunc(ctx, reqLog)
}
// FindSettingsByModuleCalls gets all the calls that were made to FindSettingsByModule.
// StoreRequestLogCalls gets all the calls that were made to StoreRequestLog.
// Check the length with:
// len(mockedRepository.FindSettingsByModuleCalls())
func (mock *RepoMock) FindSettingsByModuleCalls() []struct {
Ctx context.Context
Module string
Settings interface{}
// len(mockedRepository.StoreRequestLogCalls())
func (mock *RepoMock) StoreRequestLogCalls() []struct {
Ctx context.Context
ReqLog reqlog.RequestLog
} {
var calls []struct {
Ctx context.Context
Module string
Settings interface{}
Ctx context.Context
ReqLog reqlog.RequestLog
}
mock.lockFindSettingsByModule.RLock()
calls = mock.calls.FindSettingsByModule
mock.lockFindSettingsByModule.RUnlock()
mock.lockStoreRequestLog.RLock()
calls = mock.calls.StoreRequestLog
mock.lockStoreRequestLog.RUnlock()
return calls
}
// UpsertSettings calls UpsertSettingsFunc.
func (mock *RepoMock) UpsertSettings(ctx context.Context, module string, settings interface{}) error {
if mock.UpsertSettingsFunc == nil {
panic("RepoMock.UpsertSettingsFunc: method is nil but Repository.UpsertSettings was just called")
// StoreResponseLog calls StoreResponseLogFunc.
func (mock *RepoMock) StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
if mock.StoreResponseLogFunc == nil {
panic("RepoMock.StoreResponseLogFunc: method is nil but Repository.StoreResponseLog was just called")
}
callInfo := struct {
Ctx context.Context
Module string
Settings interface{}
ReqLogID ulid.ULID
ResLog reqlog.ResponseLog
}{
Ctx: ctx,
Module: module,
Settings: settings,
ReqLogID: reqLogID,
ResLog: resLog,
}
mock.lockUpsertSettings.Lock()
mock.calls.UpsertSettings = append(mock.calls.UpsertSettings, callInfo)
mock.lockUpsertSettings.Unlock()
return mock.UpsertSettingsFunc(ctx, module, settings)
mock.lockStoreResponseLog.Lock()
mock.calls.StoreResponseLog = append(mock.calls.StoreResponseLog, callInfo)
mock.lockStoreResponseLog.Unlock()
return mock.StoreResponseLogFunc(ctx, reqLogID, resLog)
}
// UpsertSettingsCalls gets all the calls that were made to UpsertSettings.
// StoreResponseLogCalls gets all the calls that were made to StoreResponseLog.
// Check the length with:
// len(mockedRepository.UpsertSettingsCalls())
func (mock *RepoMock) UpsertSettingsCalls() []struct {
// len(mockedRepository.StoreResponseLogCalls())
func (mock *RepoMock) StoreResponseLogCalls() []struct {
Ctx context.Context
Module string
Settings interface{}
ReqLogID ulid.ULID
ResLog reqlog.ResponseLog
} {
var calls []struct {
Ctx context.Context
Module string
Settings interface{}
ReqLogID ulid.ULID
ResLog reqlog.ResponseLog
}
mock.lockUpsertSettings.RLock()
calls = mock.calls.UpsertSettings
mock.lockUpsertSettings.RUnlock()
mock.lockStoreResponseLog.RLock()
calls = mock.calls.StoreResponseLog
mock.lockStoreResponseLog.RUnlock()
return calls
}

View File

@ -4,15 +4,18 @@ import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net/http"
"net/url"
"time"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
@ -22,127 +25,109 @@ type contextKey int
const LogBypassedKey contextKey = 0
const moduleName = "reqlog"
var (
ErrRequestNotFound = errors.New("reqlog: request not found")
ErrProjectIDMustBeSet = errors.New("reqlog: project ID must be set")
)
var ErrRequestNotFound = errors.New("reqlog: request not found")
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
type Request struct {
ID int64
Request http.Request
Body []byte
Timestamp time.Time
Response *Response
type RequestLog struct {
ID ulid.ULID
ProjectID ulid.ULID
URL *url.URL
Method string
Proto string
Header http.Header
Body []byte
Response *ResponseLog
}
type Response struct {
ID int64
RequestID int64
Response http.Response
Body []byte
Timestamp time.Time
type ResponseLog struct {
Proto string
StatusCode int
Status string
Header http.Header
Body []byte
}
type Service struct {
BypassOutOfScopeRequests bool
FindReqsFilter FindRequestsFilter
ActiveProjectID ulid.ULID
scope *scope.Scope
repo Repository
}
type FindRequestsFilter struct {
OnlyInScope bool
SearchExpr search.Expression `json:"-"`
RawSearchExpr string
ProjectID ulid.ULID
OnlyInScope bool
SearchExpr search.Expression
}
type Config struct {
Scope *scope.Scope
Repository Repository
ProjectService proj.Service
BypassOutOfScopeRequests bool
Scope *scope.Scope
Repository Repository
}
func NewService(cfg Config) *Service {
svc := &Service{
scope: cfg.Scope,
repo: cfg.Repository,
BypassOutOfScopeRequests: cfg.BypassOutOfScopeRequests,
return &Service{
repo: cfg.Repository,
scope: cfg.Scope,
}
cfg.ProjectService.OnProjectOpen(func(_ string) error {
err := svc.repo.FindSettingsByModule(context.Background(), moduleName, svc)
if errors.Is(err, proj.ErrNoSettings) {
return nil
}
if err != nil {
return fmt.Errorf("reqlog: could not load settings: %w", err)
}
return nil
})
cfg.ProjectService.OnProjectClose(func(_ string) error {
svc.BypassOutOfScopeRequests = false
svc.FindReqsFilter = FindRequestsFilter{}
return nil
})
return svc
}
func (svc *Service) FindRequests(ctx context.Context) ([]Request, error) {
func (svc *Service) FindRequests(ctx context.Context) ([]RequestLog, error) {
return svc.repo.FindRequestLogs(ctx, svc.FindReqsFilter, svc.scope)
}
func (svc *Service) FindRequestLogByID(ctx context.Context, id int64) (Request, error) {
func (svc *Service) FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error) {
return svc.repo.FindRequestLogByID(ctx, id)
}
func (svc *Service) SetRequestLogFilter(ctx context.Context, filter FindRequestsFilter) error {
svc.FindReqsFilter = filter
return svc.repo.UpsertSettings(ctx, "reqlog", svc)
func (svc *Service) ClearRequests(ctx context.Context, projectID ulid.ULID) error {
return svc.repo.ClearRequestLogs(ctx, projectID)
}
func (svc *Service) ClearRequests(ctx context.Context) error {
return svc.repo.ClearRequestLogs(ctx)
}
func (svc *Service) addRequest(
ctx context.Context,
req http.Request,
body []byte,
timestamp time.Time,
) (*Request, error) {
return svc.repo.AddRequestLog(ctx, req, body, timestamp)
}
func (svc *Service) addResponse(
ctx context.Context,
reqID int64,
res http.Response,
body []byte,
timestamp time.Time,
) (*Response, error) {
func (svc *Service) storeResponse(ctx context.Context, reqLogID ulid.ULID, res *http.Response) error {
if res.Header.Get("Content-Encoding") == "gzip" {
gzipReader, err := gzip.NewReader(bytes.NewBuffer(body))
gzipReader, err := gzip.NewReader(res.Body)
if err != nil {
return nil, fmt.Errorf("reqlog: could not create gzip reader: %w", err)
return fmt.Errorf("could not create gzip reader: %w", err)
}
defer gzipReader.Close()
body, err = ioutil.ReadAll(gzipReader)
if err != nil {
return nil, fmt.Errorf("reqlog: could not read gzipped response body: %w", err)
buf := &bytes.Buffer{}
if _, err := io.Copy(buf, gzipReader); err != nil {
return fmt.Errorf("could not read gzipped response body: %w", err)
}
res.Body = io.NopCloser(buf)
}
return svc.repo.AddResponseLog(ctx, reqID, res, body, timestamp)
body, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("could not read body: %w", err)
}
resLog := ResponseLog{
Proto: res.Proto,
StatusCode: res.StatusCode,
Status: res.Status,
Header: res.Header,
Body: body,
}
return svc.repo.StoreResponseLog(ctx, reqLogID, resLog)
}
func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
return func(req *http.Request) {
now := time.Now()
next(req)
clone := req.Clone(req.Context())
@ -160,10 +145,19 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
}
req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
clone.Body = ioutil.NopCloser(bytes.NewBuffer(body))
}
// Bypass logging if no project is active.
if svc.ActiveProjectID.Compare(ulid.ULID{}) == 0 {
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
*req = *req.WithContext(ctx)
return
}
// Bypass logging if this setting is enabled and the incoming request
// doens't match any rules of the scope.
// doesn't match any scope rules.
if svc.BypassOutOfScopeRequests && !svc.scope.Match(clone, body) {
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
*req = *req.WithContext(ctx)
@ -171,26 +165,29 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
return
}
reqLog, err := svc.addRequest(req.Context(), *clone, body, now)
if errors.Is(err, proj.ErrNoProject) {
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
*req = *req.WithContext(ctx)
reqLog := RequestLog{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
ProjectID: svc.ActiveProjectID,
Method: clone.Method,
URL: clone.URL,
Proto: clone.Proto,
Header: clone.Header,
Body: body,
}
return
} else if err != nil {
err := svc.repo.StoreRequestLog(req.Context(), reqLog)
if err != nil {
log.Printf("[ERROR] Could not store request log: %v", err)
return
}
ctx := context.WithValue(req.Context(), proxy.ReqIDKey, reqLog.ID)
ctx := context.WithValue(req.Context(), proxy.ReqLogIDKey, reqLog.ID)
*req = *req.WithContext(ctx)
}
}
func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
return func(res *http.Response) error {
now := time.Now()
if err := next(res); err != nil {
return err
}
@ -199,8 +196,8 @@ func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
return nil
}
reqID, _ := res.Request.Context().Value(proxy.ReqIDKey).(int64)
if reqID == 0 {
reqLogID, ok := res.Request.Context().Value(proxy.ReqLogIDKey).(ulid.ULID)
if !ok {
return errors.New("reqlog: request is missing ID")
}
@ -213,9 +210,10 @@ func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
}
res.Body = ioutil.NopCloser(bytes.NewBuffer(body))
clone.Body = ioutil.NopCloser(bytes.NewBuffer(body))
go func() {
if _, err := svc.addResponse(context.Background(), reqID, clone, body, now); err != nil {
if err := svc.storeResponse(context.Background(), reqLogID, &clone); err != nil {
log.Printf("[ERROR] Could not store response log: %v", err)
}
}()
@ -223,33 +221,3 @@ func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
return nil
}
}
// UnmarshalJSON implements json.Unmarshaler.
func (f *FindRequestsFilter) UnmarshalJSON(b []byte) error {
var dto struct {
OnlyInScope bool
RawSearchExpr string
}
if err := json.Unmarshal(b, &dto); err != nil {
return err
}
filter := FindRequestsFilter{
OnlyInScope: dto.OnlyInScope,
RawSearchExpr: dto.RawSearchExpr,
}
if dto.RawSearchExpr != "" {
expr, err := search.ParseQuery(dto.RawSearchExpr)
if err != nil {
return err
}
filter.SearchExpr = expr
}
*f = filter
return nil
}

View File

@ -1,124 +1,43 @@
package reqlog_test
//go:generate moq -out proj_mock_test.go -pkg reqlog_test ../proj Service:ProjServiceMock
//go:generate moq -out repo_mock_test.go -pkg reqlog_test . Repository:RepoMock
//go:generate go run github.com/matryer/moq -out repo_mock_test.go -pkg reqlog_test . Repository:RepoMock
import (
"context"
"io/ioutil"
"io"
"math/rand"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/google/go-cmp/cmp"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
)
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
//nolint:paralleltest
func TestNewService(t *testing.T) {
projSvcMock := &ProjServiceMock{
OnProjectOpenFunc: func(fn proj.OnProjectOpenFn) {},
OnProjectCloseFunc: func(fn proj.OnProjectCloseFn) {},
}
func TestRequestModifier(t *testing.T) {
repoMock := &RepoMock{
FindSettingsByModuleFunc: func(_ context.Context, _ string, _ interface{}) error {
StoreRequestLogFunc: func(_ context.Context, _ reqlog.RequestLog) error {
return nil
},
}
svc := reqlog.NewService(reqlog.Config{
ProjectService: projSvcMock,
Repository: repoMock,
})
t.Run("registered handlers for project open and close", func(t *testing.T) {
got := len(projSvcMock.OnProjectOpenCalls())
if exp := 1; exp != got {
t.Fatalf("incorrect `proj.Service.OnProjectOpen` calls (expected: %v, got: %v)", exp, got)
}
got = len(projSvcMock.OnProjectCloseCalls())
if exp := 1; exp != got {
t.Fatalf("incorrect `proj.Service.OnProjectClose` calls (expected: %v, got: %v)", exp, got)
}
})
t.Run("calls handler when project is opened", func(t *testing.T) {
// Mock opening a project.
err := projSvcMock.OnProjectOpenCalls()[0].Fn("foobar")
if err != nil {
t.Errorf("unexpected error (expected: nil, got: %v)", err)
}
// Assert that settings were fetched from repository, with `svc` as the
// destination.
got := len(repoMock.FindSettingsByModuleCalls())
if exp := 1; exp != got {
t.Fatalf("incorrect `proj.Service.OnProjectOpen` calls (expected: %v, got: %v)", exp, got)
}
findSettingsByModuleCall := repoMock.FindSettingsByModuleCalls()[0]
expModule := "reqlog"
expSettings := svc
if expModule != findSettingsByModuleCall.Module {
t.Fatalf("incorrect `module` argument for `proj.Service.OnProjectOpen` (expected: %v, got: %v)",
expModule, findSettingsByModuleCall.Module)
}
if expSettings != findSettingsByModuleCall.Settings {
t.Fatalf("incorrect `settings` argument for `proj.Service.OnProjectOpen` (expected: %v, got: %v)",
expModule, findSettingsByModuleCall.Settings)
}
})
t.Run("calls handler when project is closed", func(t *testing.T) {
// Mock updating service settings.
svc.BypassOutOfScopeRequests = true
svc.FindReqsFilter = reqlog.FindRequestsFilter{OnlyInScope: true}
// Mock closing a project.
err := projSvcMock.OnProjectCloseCalls()[0].Fn("foobar")
if err != nil {
t.Errorf("unexpected error (expected: nil, got: %v)", err)
}
// Assert that settings were set to defaults on project close.
expBypassOutOfScopeReqs := false
expFindReqsFilter := reqlog.FindRequestsFilter{}
if expBypassOutOfScopeReqs != svc.BypassOutOfScopeRequests {
t.Fatalf("incorrect `Service.BypassOutOfScopeRequests` value (expected: %v, got: %v)",
expBypassOutOfScopeReqs, svc.BypassOutOfScopeRequests)
}
if expFindReqsFilter != svc.FindReqsFilter {
t.Fatalf("incorrect `Service.FindReqsFilter` value (expected: %v, got: %v)",
expFindReqsFilter, svc.FindReqsFilter)
}
})
}
//nolint:paralleltest
func TestRequestModifier(t *testing.T) {
projSvcMock := &ProjServiceMock{
OnProjectOpenFunc: func(fn proj.OnProjectOpenFn) {},
OnProjectCloseFunc: func(fn proj.OnProjectCloseFn) {},
}
repoMock := &RepoMock{
AddRequestLogFunc: func(_ context.Context, _ http.Request, _ []byte, _ time.Time) (*reqlog.Request, error) {
return &reqlog.Request{}, nil
},
}
svc := reqlog.NewService(reqlog.Config{
ProjectService: projSvcMock,
Repository: repoMock,
Repository: repoMock,
Scope: &scope.Scope{},
})
svc.ActiveProjectID = ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
next := func(req *http.Request) {
req.Body = ioutil.NopCloser(strings.NewReader("modified body"))
req.Body = io.NopCloser(strings.NewReader("modified body"))
}
reqModFn := svc.RequestModifier(next)
req := httptest.NewRequest("GET", "https://example.com/", strings.NewReader("bar"))
@ -126,49 +45,54 @@ func TestRequestModifier(t *testing.T) {
reqModFn(req)
t.Run("request log was stored in repository", func(t *testing.T) {
got := len(repoMock.AddRequestLogCalls())
if exp := 1; exp != got {
t.Fatalf("incorrect `proj.Service.AddRequestLog` calls (expected: %v, got: %v)", exp, got)
gotCount := len(repoMock.StoreRequestLogCalls())
if expCount := 1; expCount != gotCount {
t.Fatalf("incorrect `proj.Service.AddRequestLog` calls (expected: %v, got: %v)", expCount, gotCount)
}
})
t.Run("ran next modifier first, before calling repository", func(t *testing.T) {
got := repoMock.AddRequestLogCalls()[0].Body
if exp := "modified body"; exp != string(got) {
t.Fatalf("incorrect `body` argument for `Repository.AddRequestLogCalls` (expected: %v, got: %v)", exp, string(got))
exp := reqlog.RequestLog{
ID: ulid.ULID{}, // Empty value
ProjectID: svc.ActiveProjectID,
Method: req.Method,
URL: req.URL,
Proto: req.Proto,
Header: req.Header,
Body: []byte("modified body"),
}
got := repoMock.StoreRequestLogCalls()[0].ReqLog
got.ID = ulid.ULID{} // Override to empty value so we can compare against expected value.
if diff := cmp.Diff(exp, got); diff != "" {
t.Fatalf("request log not equal (-exp, +got):\n%v", diff)
}
})
}
//nolint:paralleltest
func TestResponseModifier(t *testing.T) {
projSvcMock := &ProjServiceMock{
OnProjectOpenFunc: func(fn proj.OnProjectOpenFn) {},
OnProjectCloseFunc: func(fn proj.OnProjectCloseFn) {},
}
repoMock := &RepoMock{
AddResponseLogFunc: func(_ context.Context, _ int64, _ http.Response,
_ []byte, _ time.Time) (*reqlog.Response, error) {
return &reqlog.Response{}, nil
StoreResponseLogFunc: func(_ context.Context, _ ulid.ULID, _ reqlog.ResponseLog) error {
return nil
},
}
svc := reqlog.NewService(reqlog.Config{
ProjectService: projSvcMock,
Repository: repoMock,
Repository: repoMock,
})
svc.ActiveProjectID = ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
next := func(res *http.Response) error {
res.Body = ioutil.NopCloser(strings.NewReader("modified body"))
res.Body = io.NopCloser(strings.NewReader("modified body"))
return nil
}
resModFn := svc.ResponseModifier(next)
req := httptest.NewRequest("GET", "https://example.com/", strings.NewReader("bar"))
req = req.WithContext(context.WithValue(req.Context(), proxy.ReqIDKey, int64(42)))
reqLogID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
req = req.WithContext(context.WithValue(req.Context(), proxy.ReqLogIDKey, reqLogID))
res := &http.Response{
Request: req,
Body: ioutil.NopCloser(strings.NewReader("bar")),
Body: io.NopCloser(strings.NewReader("bar")),
}
if err := resModFn(res); err != nil {
@ -178,16 +102,23 @@ func TestResponseModifier(t *testing.T) {
t.Run("request log was stored in repository", func(t *testing.T) {
// Dirty (but simple) wait for other goroutine to finish calling repository.
time.Sleep(10 * time.Millisecond)
got := len(repoMock.AddResponseLogCalls())
got := len(repoMock.StoreResponseLogCalls())
if exp := 1; exp != got {
t.Fatalf("incorrect `proj.Service.AddResponseLog` calls (expected: %v, got: %v)", exp, got)
}
})
t.Run("ran next modifier first, before calling repository", func(t *testing.T) {
got := repoMock.AddResponseLogCalls()[0].Body
if exp := "modified body"; exp != string(got) {
t.Fatalf("incorrect `body` argument for `Repository.AddResponseLogCalls` (expected: %v, got: %v)", exp, string(got))
}
t.Run("ran next modifier first, before calling repository", func(t *testing.T) {
got := repoMock.StoreResponseLogCalls()[0].ResLog.Body
if exp := "modified body"; exp != string(got) {
t.Fatalf("incorrect `ResponseLog.Body` value (expected: %v, got: %v)", exp, string(got))
}
})
t.Run("called repository with request log id", func(t *testing.T) {
got := repoMock.StoreResponseLogCalls()[0].ReqLogID
if exp := reqLogID; exp.Compare(got) != 0 {
t.Fatalf("incorrect `reqLogID` argument for `Repository.AddResponseLogCalls` (expected: %v, got: %v)", exp.String(), got.String())
}
})
})
}

235
pkg/reqlog/search.go Normal file
View File

@ -0,0 +1,235 @@
package reqlog
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
)
var reqLogSearchKeyFns = map[string]func(rl RequestLog) string{
"req.id": func(rl RequestLog) string { return rl.ID.String() },
"req.proto": func(rl RequestLog) string { return rl.Proto },
"req.url": func(rl RequestLog) string {
if rl.URL == nil {
return ""
}
return rl.URL.String()
},
"req.method": func(rl RequestLog) string { return rl.Method },
"req.body": func(rl RequestLog) string { return string(rl.Body) },
"req.timestamp": func(rl RequestLog) string { return ulid.Time(rl.ID.Time()).String() },
}
var resLogSearchKeyFns = map[string]func(rl ResponseLog) string{
"res.proto": func(rl ResponseLog) string { return rl.Proto },
"res.statusCode": func(rl ResponseLog) string { return strconv.Itoa(rl.StatusCode) },
"res.statusReason": func(rl ResponseLog) string { return rl.Status },
"res.body": func(rl ResponseLog) string { return string(rl.Body) },
}
// TODO: Request and response headers search key functions.
// Matches returns true if the supplied search expression evaluates to true.
func (reqLog RequestLog) Matches(expr search.Expression) (bool, error) {
switch e := expr.(type) {
case search.PrefixExpression:
return reqLog.matchPrefixExpr(e)
case search.InfixExpression:
return reqLog.matchInfixExpr(e)
case search.StringLiteral:
return reqLog.matchStringLiteral(e)
default:
return false, fmt.Errorf("expression type (%T) not supported", expr)
}
}
func (reqLog RequestLog) matchPrefixExpr(expr search.PrefixExpression) (bool, error) {
switch expr.Operator {
case search.TokOpNot:
match, err := reqLog.Matches(expr.Right)
if err != nil {
return false, err
}
return !match, nil
default:
return false, errors.New("operator is not supported")
}
}
func (reqLog RequestLog) matchInfixExpr(expr search.InfixExpression) (bool, error) {
switch expr.Operator {
case search.TokOpAnd:
left, err := reqLog.Matches(expr.Left)
if err != nil {
return false, err
}
right, err := reqLog.Matches(expr.Right)
if err != nil {
return false, err
}
return left && right, nil
case search.TokOpOr:
left, err := reqLog.Matches(expr.Left)
if err != nil {
return false, err
}
right, err := reqLog.Matches(expr.Right)
if err != nil {
return false, err
}
return left || right, nil
}
left, ok := expr.Left.(search.StringLiteral)
if !ok {
return false, errors.New("left operand must be a string literal")
}
leftVal := reqLog.getMappedStringLiteral(left.Value)
if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe {
right, ok := expr.Right.(*regexp.Regexp)
if !ok {
return false, errors.New("right operand must be a regular expression")
}
switch expr.Operator {
case search.TokOpRe:
return right.MatchString(leftVal), nil
case search.TokOpNotRe:
return !right.MatchString(leftVal), nil
}
}
right, ok := expr.Right.(search.StringLiteral)
if !ok {
return false, errors.New("right operand must be a string literal")
}
rightVal := reqLog.getMappedStringLiteral(right.Value)
switch expr.Operator {
case search.TokOpEq:
return leftVal == rightVal, nil
case search.TokOpNotEq:
return leftVal != rightVal, nil
case search.TokOpGt:
// TODO(?) attempt to parse as int.
return leftVal > rightVal, nil
case search.TokOpLt:
// TODO(?) attempt to parse as int.
return leftVal < rightVal, nil
case search.TokOpGtEq:
// TODO(?) attempt to parse as int.
return leftVal >= rightVal, nil
case search.TokOpLtEq:
// TODO(?) attempt to parse as int.
return leftVal <= rightVal, nil
default:
return false, errors.New("unsupported operator")
}
}
func (reqLog RequestLog) getMappedStringLiteral(s string) string {
switch {
case strings.HasPrefix(s, "req."):
fn, ok := reqLogSearchKeyFns[s]
if ok {
return fn(reqLog)
}
case strings.HasPrefix(s, "res."):
if reqLog.Response == nil {
return ""
}
fn, ok := resLogSearchKeyFns[s]
if ok {
return fn(*reqLog.Response)
}
}
return s
}
func (reqLog RequestLog) matchStringLiteral(strLiteral search.StringLiteral) (bool, error) {
for _, fn := range reqLogSearchKeyFns {
if strings.Contains(
strings.ToLower(fn(reqLog)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
}
}
if reqLog.Response != nil {
for _, fn := range resLogSearchKeyFns {
if strings.Contains(
strings.ToLower(fn(*reqLog.Response)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
}
}
}
return false, nil
}
func (reqLog RequestLog) MatchScope(s *scope.Scope) bool {
for _, rule := range s.Rules() {
if rule.URL != nil && reqLog.URL != nil {
if matches := rule.URL.MatchString(reqLog.URL.String()); matches {
return true
}
}
for key, values := range reqLog.Header {
var keyMatches, valueMatches bool
if rule.Header.Key != nil {
if matches := rule.Header.Key.MatchString(key); matches {
keyMatches = true
}
}
if rule.Header.Value != nil {
for _, value := range values {
if matches := rule.Header.Value.MatchString(value); matches {
valueMatches = true
break
}
}
}
// When only key or value is set, match on whatever is set.
// When both are set, both must match.
switch {
case rule.Header.Key != nil && rule.Header.Value == nil && keyMatches:
return true
case rule.Header.Key == nil && rule.Header.Value != nil && valueMatches:
return true
case rule.Header.Key != nil && rule.Header.Value != nil && keyMatches && valueMatches:
return true
}
}
if rule.Body != nil {
if matches := rule.Body.Match(reqLog.Body); matches {
return true
}
}
}
return false
}

203
pkg/reqlog/search_test.go Normal file
View File

@ -0,0 +1,203 @@
package reqlog_test
import (
"testing"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/search"
)
func TestRequestLogMatch(t *testing.T) {
t.Parallel()
tests := []struct {
name string
query string
requestLog reqlog.RequestLog
expectedMatch bool
expectedError error
}{
{
name: "infix expression, equal operator, match",
query: "req.body = foo",
requestLog: reqlog.RequestLog{
Body: []byte("foo"),
},
expectedMatch: true,
expectedError: nil,
},
{
name: "infix expression, not equal operator, match",
query: "req.body != bar",
requestLog: reqlog.RequestLog{
Body: []byte("foo"),
},
expectedMatch: true,
expectedError: nil,
},
{
name: "infix expression, greater than operator, match",
query: "req.body > a",
requestLog: reqlog.RequestLog{
Body: []byte("b"),
},
expectedMatch: true,
expectedError: nil,
},
{
name: "infix expression, less than operator, match",
query: "req.body < b",
requestLog: reqlog.RequestLog{
Body: []byte("a"),
},
expectedMatch: true,
expectedError: nil,
},
{
name: "infix expression, greater than or equal operator, match greater than",
query: "req.body >= a",
requestLog: reqlog.RequestLog{
Body: []byte("b"),
},
expectedMatch: true,
expectedError: nil,
},
{
name: "infix expression, greater than or equal operator, match equal",
query: "req.body >= a",
requestLog: reqlog.RequestLog{
Body: []byte("a"),
},
expectedMatch: true,
expectedError: nil,
},
{
name: "infix expression, less than or equal operator, match less than",
query: "req.body <= b",
requestLog: reqlog.RequestLog{
Body: []byte("a"),
},
expectedMatch: true,
expectedError: nil,
},
{
name: "infix expression, less than or equal operator, match equal",
query: "req.body <= b",
requestLog: reqlog.RequestLog{
Body: []byte("b"),
},
expectedMatch: true,
expectedError: nil,
},
{
name: "infix expression, regular expression operator, match",
query: `req.body =~ "^foo(.*)$"`,
requestLog: reqlog.RequestLog{
Body: []byte("foobar"),
},
expectedMatch: true,
expectedError: nil,
},
{
name: "infix expression, negate regular expression operator, match",
query: `req.body !~ "^foo(.*)$"`,
requestLog: reqlog.RequestLog{
Body: []byte("xoobar"),
},
expectedMatch: true,
expectedError: nil,
},
{
name: "infix expression, and operator, match",
query: "req.body = bar AND res.body = yolo",
requestLog: reqlog.RequestLog{
Body: []byte("bar"),
Response: &reqlog.ResponseLog{
Body: []byte("yolo"),
},
},
expectedMatch: true,
expectedError: nil,
},
{
name: "infix expression, or operator, match",
query: "req.body = bar OR res.body = yolo",
requestLog: reqlog.RequestLog{
Body: []byte("foo"),
Response: &reqlog.ResponseLog{
Body: []byte("yolo"),
},
},
expectedMatch: true,
expectedError: nil,
},
{
name: "prefix expression, not operator, match",
query: "NOT (req.body = bar)",
requestLog: reqlog.RequestLog{
Body: []byte("foo"),
},
expectedMatch: true,
expectedError: nil,
},
{
name: "string literal expression, match in request log",
query: "foo",
requestLog: reqlog.RequestLog{
Body: []byte("foo"),
},
expectedMatch: true,
expectedError: nil,
},
{
name: "string literal expression, no match",
query: "foo",
requestLog: reqlog.RequestLog{
Body: []byte("bar"),
},
expectedMatch: false,
expectedError: nil,
},
{
name: "string literal expression, match in response log",
query: "foo",
requestLog: reqlog.RequestLog{
Response: &reqlog.ResponseLog{
Body: []byte("foo"),
},
},
expectedMatch: true,
expectedError: nil,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
searchExpr, err := search.ParseQuery(tt.query)
assertError(t, nil, err)
got, err := tt.requestLog.Matches(searchExpr)
assertError(t, tt.expectedError, err)
if tt.expectedMatch != got {
t.Errorf("expected match result: %v, got: %v", tt.expectedMatch, got)
}
})
}
}
func assertError(t *testing.T, exp, got error) {
t.Helper()
switch {
case exp == nil && got != nil:
t.Fatalf("expected: nil, got: %v", got)
case exp != nil && got == nil:
t.Fatalf("expected: %v, got: nil", exp.Error())
case exp != nil && got != nil && exp.Error() != got.Error():
t.Fatalf("expected: %v, got: %v", exp.Error(), got.Error())
}
}