From 8e1d202a80cbd44bc32913b9c8de245a75f8f617 Mon Sep 17 00:00:00 2001 From: Cihan Eran Date: Thu, 9 Oct 2025 02:12:10 +0300 Subject: [PATCH] feat: extract internal config struct - moved Config struct to internal/options/config.go - updated wrap middleware signature to pass config pointer --- internal/middleware/middleware.go | 9 +- internal/middleware/middleware_test.go | 8 +- internal/middleware/profiling.go | 9 +- internal/options/config.go | 82 +++++++++++++++++ options.go | 119 +++++-------------------- wrap.go | 4 +- 6 files changed, 124 insertions(+), 107 deletions(-) create mode 100644 internal/options/config.go diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index 0760f27..610e13c 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -8,6 +8,7 @@ import ( "time" "github.com/doganarif/govisual/internal/model" + "github.com/doganarif/govisual/internal/options" "github.com/doganarif/govisual/internal/store" ) @@ -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) { @@ -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() @@ -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 @@ -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() } diff --git a/internal/middleware/middleware_test.go b/internal/middleware/middleware_test.go index 5d4379b..44995b3 100644 --- a/internal/middleware/middleware_test.go +++ b/internal/middleware/middleware_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/doganarif/govisual/internal/model" + "github.com/doganarif/govisual/internal/options" ) // mockStore implements store.Store for testing @@ -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") diff --git a/internal/middleware/profiling.go b/internal/middleware/profiling.go index dec87fb..00dde9f 100644 --- a/internal/middleware/profiling.go +++ b/internal/middleware/profiling.go @@ -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" ) @@ -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) { @@ -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) @@ -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{} } @@ -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() } diff --git a/internal/options/config.go b/internal/options/config.go new file mode 100644 index 0000000..9252074 --- /dev/null +++ b/internal/options/config.go @@ -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 +} diff --git a/options.go b/options.go index cfbfc20..a1b179a 100644 --- a/options.go +++ b/options.go @@ -3,135 +3,89 @@ package govisual import ( "database/sql" "fmt" - "path/filepath" - "strings" "time" + "github.com/doganarif/govisual/internal/options" "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 -} - // Option is a function that modifies the configuration -type Option func(*Config) +type Option func(*options.Config) // WithMaxRequests sets the maximum number of requests to store func WithMaxRequests(max int) Option { - return func(c *Config) { + return func(c *options.Config) { c.MaxRequests = max } } // WithDashboardPath sets the path to access the dashboard func WithDashboardPath(path string) Option { - return func(c *Config) { + return func(c *options.Config) { c.DashboardPath = path } } // WithRequestBodyLogging enables or disables request body logging func WithRequestBodyLogging(enabled bool) Option { - return func(c *Config) { + return func(c *options.Config) { c.LogRequestBody = enabled } } // WithResponseBodyLogging enables or disables response body logging func WithResponseBodyLogging(enabled bool) Option { - return func(c *Config) { + return func(c *options.Config) { c.LogResponseBody = enabled } } // WithIgnorePaths sets the path patterns to ignore func WithIgnorePaths(patterns ...string) Option { - return func(c *Config) { + return func(c *options.Config) { c.IgnorePaths = append(c.IgnorePaths, patterns...) } } // WithOpenTelemetry enables or disables OpenTelemetry instrumentation func WithOpenTelemetry(enabled bool) Option { - return func(c *Config) { + return func(c *options.Config) { c.EnableOpenTelemetry = enabled } } // WithServiceName sets the service name for OpenTelemetry func WithServiceName(name string) Option { - return func(c *Config) { + return func(c *options.Config) { c.ServiceName = name } } // WithServiceVersion sets the service version for OpenTelemetry func WithServiceVersion(version string) Option { - return func(c *Config) { + return func(c *options.Config) { c.ServiceVersion = version } } // WithOTelEndpoint sets the OTLP endpoint for exporting telemetry data func WithOTelEndpoint(endpoint string) Option { - return func(c *Config) { + return func(c *options.Config) { c.OTelEndpoint = endpoint } } // WithMemoryStorage configures the application to use in-memory storage func WithMemoryStorage() Option { - return func(c *Config) { + return func(c *options.Config) { c.StorageType = store.StorageTypeMemory } } // WithPostgresStorage configures the application to use PostgreSQL storage func WithPostgresStorage(connStr string, tableName string) Option { - return func(c *Config) { + return func(c *options.Config) { c.StorageType = store.StorageTypePostgres c.ConnectionString = connStr c.TableName = tableName @@ -140,7 +94,7 @@ func WithPostgresStorage(connStr string, tableName string) Option { // WithSQLiteStorage configures the application to use SQLite storage func WithSQLiteStorage(dbPath string, tableName string) Option { - return func(c *Config) { + return func(c *options.Config) { c.StorageType = store.StorageTypeSQLite c.ConnectionString = dbPath c.TableName = tableName @@ -149,7 +103,7 @@ func WithSQLiteStorage(dbPath string, tableName string) Option { // WithSQLiteStorageDB configures the application to use SQLite storage with an existing database connection func WithSQLiteStorageDB(db *sql.DB, tableName string) Option { - return func(c *Config) { + return func(c *options.Config) { c.StorageType = store.StorageTypeSQLiteWithDB c.ExistingDB = db c.TableName = tableName @@ -158,7 +112,7 @@ func WithSQLiteStorageDB(db *sql.DB, tableName string) Option { // WithRedisStorage configures the application to use Redis storage func WithRedisStorage(connStr string, ttlSeconds int) Option { - return func(c *Config) { + return func(c *options.Config) { c.StorageType = store.StorageTypeRedis c.ConnectionString = connStr c.RedisTTL = ttlSeconds @@ -167,71 +121,44 @@ func WithRedisStorage(connStr string, ttlSeconds int) Option { // WithMongoDBStorage configures the application to use MongoDB storage func WithMongoDBStorage(uri, databaseName, collectionName string) Option { - return func(c *Config) { + return func(c *options.Config) { c.StorageType = store.StorageTypeMongoDB c.ConnectionString = uri c.TableName = fmt.Sprintf("%s.%s", databaseName, collectionName) } } -// ShouldIgnorePath checks if a path should be ignored based on the configured patterns -// 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 -} - // WithProfiling enables or disables performance profiling func WithProfiling(enabled bool) Option { - return func(c *Config) { + return func(c *options.Config) { c.EnableProfiling = enabled } } // WithProfileType sets the types of profiling to perform func WithProfileType(profileType profiling.ProfileType) Option { - return func(c *Config) { + return func(c *options.Config) { c.ProfileType = profileType } } // WithProfileThreshold sets the minimum duration to trigger profiling func WithProfileThreshold(threshold time.Duration) Option { - return func(c *Config) { + return func(c *options.Config) { c.ProfileThreshold = threshold } } // WithMaxProfileMetrics sets the maximum number of profile metrics to store func WithMaxProfileMetrics(max int) Option { - return func(c *Config) { + return func(c *options.Config) { c.MaxProfileMetrics = max } } // defaultConfig returns the default configuration -func defaultConfig() *Config { - return &Config{ +func defaultConfig() *options.Config { + return &options.Config{ MaxRequests: 100, DashboardPath: "/__viz", LogRequestBody: false, diff --git a/wrap.go b/wrap.go index 2c5a666..e024da1 100644 --- a/wrap.go +++ b/wrap.go @@ -116,9 +116,9 @@ func Wrap(handler http.Handler, opts ...Option) http.Handler { // Create middleware wrapper with profiling support var wrapped http.Handler if profiler != nil { - wrapped = middleware.WrapWithProfiling(handler, requestStore, config.LogRequestBody, config.LogResponseBody, config, profiler) + wrapped = middleware.WrapWithProfiling(handler, requestStore, config, config, profiler) } else { - wrapped = middleware.Wrap(handler, requestStore, config.LogRequestBody, config.LogResponseBody, config) + wrapped = middleware.Wrap(handler, requestStore, config, config) } // Initialize OpenTelemetry if enabled