Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions internal/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/doganarif/govisual/internal/model"
"github.com/doganarif/govisual/internal/options"
"github.com/doganarif/govisual/internal/store"
)

Expand Down Expand Up @@ -39,7 +40,7 @@ func (w *responseWriter) Write(b []byte) (int, error) {
}

// Wrap wraps an http.Handler with the request visualization middleware
func Wrap(handler http.Handler, store store.Store, logRequestBody, logResponseBody bool, pathMatcher PathMatcher) http.Handler {
func Wrap(handler http.Handler, store store.Store, config *options.Config, pathMatcher PathMatcher) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if the path should be ignored
if pathMatcher != nil && pathMatcher.ShouldIgnorePath(r.URL.Path) {
Expand All @@ -51,7 +52,7 @@ func Wrap(handler http.Handler, store store.Store, logRequestBody, logResponseBo
reqLog := model.NewRequestLog(r)

// Capture request body if enabled
if logRequestBody && r.Body != nil {
if config.LogRequestBody && r.Body != nil {
// Read the body
bodyBytes, _ := io.ReadAll(r.Body)
r.Body.Close()
Expand All @@ -65,7 +66,7 @@ func Wrap(handler http.Handler, store store.Store, logRequestBody, logResponseBo

// Create response writer wrapper
var resWriter *responseWriter
if logResponseBody {
if config.LogResponseBody {
resWriter = &responseWriter{
ResponseWriter: w,
statusCode: 200, // Default status code
Expand Down Expand Up @@ -111,7 +112,7 @@ func Wrap(handler http.Handler, store store.Store, logRequestBody, logResponseBo
}

// Capture response body if enabled
if logResponseBody && resWriter.buffer != nil {
if config.LogResponseBody && resWriter.buffer != nil {
reqLog.ResponseBody = resWriter.buffer.String()
}

Expand Down
8 changes: 7 additions & 1 deletion internal/middleware/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/doganarif/govisual/internal/model"
"github.com/doganarif/govisual/internal/options"
)

// mockStore implements store.Store for testing
Expand Down Expand Up @@ -61,7 +62,12 @@ func TestWrapMiddleware(t *testing.T) {
w.Write([]byte("hello world"))
})

wrapped := Wrap(handler, store, true, true, &mockPathMatcher{})
config := &options.Config{
LogRequestBody: true,
LogResponseBody: true,
}

wrapped := Wrap(handler, store, config, &mockPathMatcher{})

req := httptest.NewRequest("POST", "/test?x=1", strings.NewReader("sample-body"))
req.Header.Set("X-Test", "test")
Expand Down
9 changes: 5 additions & 4 deletions internal/middleware/profiling.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/doganarif/govisual/internal/model"
"github.com/doganarif/govisual/internal/options"
"github.com/doganarif/govisual/internal/profiling"
"github.com/doganarif/govisual/internal/store"
)
Expand All @@ -21,7 +22,7 @@ type ProfilingConfig struct {
}

// WrapWithProfiling wraps an http.Handler with request visualization and performance profiling
func WrapWithProfiling(handler http.Handler, store store.Store, logRequestBody, logResponseBody bool, pathMatcher PathMatcher, profiler *profiling.Profiler) http.Handler {
func WrapWithProfiling(handler http.Handler, store store.Store, config *options.Config, pathMatcher PathMatcher, profiler *profiling.Profiler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if the path should be ignored
if pathMatcher != nil && pathMatcher.ShouldIgnorePath(r.URL.Path) {
Expand Down Expand Up @@ -53,7 +54,7 @@ func WrapWithProfiling(handler http.Handler, store store.Store, logRequestBody,
r = r.WithContext(ctx)

// Capture request body if enabled
if logRequestBody && r.Body != nil {
if config.LogRequestBody && r.Body != nil {
bodyBytes, _ := io.ReadAll(r.Body)
r.Body.Close()
reqLog.RequestBody = string(bodyBytes)
Expand All @@ -71,7 +72,7 @@ func WrapWithProfiling(handler http.Handler, store store.Store, logRequestBody,
ctx: ctx,
}

if logResponseBody {
if config.LogResponseBody {
resWriter.responseWriter.buffer = &bytes.Buffer{}
}

Expand Down Expand Up @@ -131,7 +132,7 @@ func WrapWithProfiling(handler http.Handler, store store.Store, logRequestBody,
}

// Capture response body if enabled
if logResponseBody && resWriter.responseWriter.buffer != nil {
if config.LogResponseBody && resWriter.responseWriter.buffer != nil {
reqLog.ResponseBody = resWriter.responseWriter.buffer.String()
}

Expand Down
82 changes: 82 additions & 0 deletions internal/options/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package options

import (
"database/sql"
"path/filepath"
"strings"
"time"

"github.com/doganarif/govisual/internal/profiling"
"github.com/doganarif/govisual/internal/store"
)

type Config struct {
MaxRequests int

DashboardPath string

LogRequestBody bool

LogResponseBody bool

IgnorePaths []string

// OpenTelemetry configuration
EnableOpenTelemetry bool

ServiceName string

ServiceVersion string

OTelEndpoint string

// Storage configuration
StorageType store.StorageType

// Connection string for database stores
ConnectionString string

// TableName for SQL database stores
TableName string

// TTL for Redis store in seconds
RedisTTL int

// Existing database connection for SQLite
ExistingDB *sql.DB

// Performance Profiling configuration
EnableProfiling bool

ProfileType profiling.ProfileType

ProfileThreshold time.Duration

MaxProfileMetrics int
}

// ShouldIgnorePath checks if a path should be ignored based on the configured patterns
func (c *Config) ShouldIgnorePath(path string) bool {
// First check if it's the dashboard path which should always be ignored to prevent recursive logging
if path == c.DashboardPath || strings.HasPrefix(path, c.DashboardPath+"/") {
return true
}

// Then check against provided ignore patterns
for _, pattern := range c.IgnorePaths {
matched, err := filepath.Match(pattern, path)
if err == nil && matched {
return true
}

// Special handling for path groups with trailing slash
if len(pattern) > 0 && pattern[len(pattern)-1] == '/' {
// If pattern ends with /, check if path starts with pattern
if len(path) >= len(pattern) && path[:len(pattern)] == pattern {
return true
}
}
}

return false
}
Loading