diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 17c2dbc..36757dd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,8 @@ jobs: matrix: platform: [ubuntu-latest] go-version: - - 1.22.x + - 1.23.x + - 1.24.x runs-on: ${{ matrix.platform }} steps: - name: Install Go diff --git a/collections/slice.go b/collections/slice.go index 058ecd9..afc4352 100644 --- a/collections/slice.go +++ b/collections/slice.go @@ -105,6 +105,68 @@ func Append[T any](slices ...[]T) []T { return output } +func MatchAny(items []string, patterns ...string) (matches, negated bool) { + var matched = false + for _, item := range items { + matches, negated = MatchItem(item, patterns...) + matched = matched || matches + if negated { + return false, true + } + } + + return matched, false +} + +// matchItems returns true if any of the patterns in the list match the item. +// negative matches are supported by prefixing the item with a "!" and +// takes precendence over positive match. +// * matches everything +// to match prefix and suffix use "*" accordingly. +func MatchItem(item string, patterns ...string) (matches, negated bool) { + if len(patterns) == 0 { + return true, false + } + + slices.SortFunc(patterns, sortPatterns) + + //process negations first + for _, p := range patterns { + pattern, err := url.QueryUnescape(strings.TrimSpace(p)) + if err != nil { + continue + } + + if strings.HasPrefix(pattern, "!") { + if matchPattern(item, strings.TrimPrefix(pattern, "!")) { + return false, true + } + } + + } + + // then normal filters + for _, p := range patterns { + pattern, err := url.QueryUnescape(strings.TrimSpace(p)) + if err != nil { + continue + } + + if matchPattern(item, pattern) { + return true, false + } + } + + //nolint:gosimple + //lint:ignore S1008 ... + if IsExclusionOnlyPatterns(patterns) { + // If all the filters were exlusions, and none of the exclusions excluded the item, then it's a match + return true, false + } + + return false, false +} + // matchItems returns true if any of the patterns in the list match the item. // negative matches are supported by prefixing the item with a "!" and // takes precendence over positive match. diff --git a/deps/deps.go b/deps/deps.go index 7a02052..09f2942 100644 --- a/deps/deps.go +++ b/deps/deps.go @@ -216,6 +216,16 @@ var dependencies = map[string]Dependency{ MacosxARM: "https://github.com/PostgREST/postgrest/releases/download/{{.version}}/postgrest-{{.version}}-macos-aarch64.tar.xz", BinaryName: "postgrest", }, + + "postgrestV13": { + Version: "v13.0.5", + Linux: "https://github.com/PostgREST/postgrest/releases/download/{{.version}}/postgrest-{{.version}}-linux-static-x86-64.tar.xz", + LinuxARM: "https://github.com/PostgREST/postgrest/releases/download/{{.version}}/postgrest-{{.version}}-ubuntu-aarch64.tar.xz ", + Windows: "https://github.com/PostgREST/postgrest/releases/download/{{.version}}/postgrest-{{.version}}-windows-x86-64.zip", + Macosx: "https://github.com/PostgREST/postgrest/releases/download/{{.version}}/postgrest-{{.version}}-macos-x86-64.tar.xz", + MacosxARM: "https://github.com/PostgREST/postgrest/releases/download/{{.version}}/postgrest-{{.version}}-macos-aarch64.tar.xz", + BinaryName: "postgrest", + }, "yq": { Version: "v4.16.2", Template: "https://github.com/mikefarah/yq/releases/download/{{.version}}/yq_{{.os}}_{{.platform}}", diff --git a/http/request.go b/http/request.go index ca1ed2e..fa3ed42 100644 --- a/http/request.go +++ b/http/request.go @@ -10,6 +10,9 @@ import ( "net/url" "strings" "time" + + "github.com/flanksource/commons/console" + "github.com/flanksource/commons/logger" ) // Request represents an HTTP request that can be customized and executed. @@ -161,7 +164,7 @@ func (r *Request) Retry(maxRetries uint, baseDuration time.Duration, exponent fl // // JSON body // req.Body(map[string]string{"key": "value"}) // -// // String body +// // String body // req.Body("raw text data") // // // Reader body @@ -229,3 +232,22 @@ func (r *Request) do() (resp *Response, err error) { return response, nil } } + +func (r *Request) HeaderMap() map[string]string { + headers := make(map[string]string) + for k, v := range r.headers { + headers[k] = strings.Join(v, ", ") + } + return headers +} + +func (r *Request) Debug() string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("%s %s\n", r.method, logger.StripSecrets(r.url.String()))) + for k, v := range logger.StripSecretsFromMap(r.HeaderMap()) { + sb.WriteString(fmt.Sprintf(" %s: %s\n", console.Grayf(k), v)) + } + body, _ := io.ReadAll(r.body) + sb.WriteString(logger.StripSecrets(string(body))) + return sb.String() +} diff --git a/http/response.go b/http/response.go index eab72e9..1aa43ee 100644 --- a/http/response.go +++ b/http/response.go @@ -2,10 +2,14 @@ package http import ( "encoding/json" + "fmt" "io" "net/http" "strings" "time" + + "github.com/flanksource/commons/console" + "github.com/flanksource/commons/logger" ) // Response extends the stdlib http.Response type and extends its functionality @@ -88,3 +92,33 @@ func (h *Response) IsJSON() bool { return false } + +func (h *Response) HeaderMap() map[string]string { + headers := make(map[string]string) + for k, v := range h.Header { + headers[k] = strings.Join(v, ", ") + } + return headers +} + +func (h *Response) Debug() string { + // mimic the response, + add content-type and size + var sb strings.Builder + + if h.Request != nil { + sb.WriteString(h.Request.Debug()) + } + + sb.WriteString(fmt.Sprintf("\n====> Status: %d\n", h.StatusCode)) + for k, v := range logger.StripSecretsFromMap(h.HeaderMap()) { + sb.WriteString(fmt.Sprintf(" %s: %s\n", console.Grayf(k), v)) + } + if h.IsJSON() { + r, _ := h.AsJSON() + sb.WriteString(logger.Pretty(r)) + } else { + body, _ := io.ReadAll(h.Body) + sb.WriteString(string(body)) + } + return sb.String() +} diff --git a/logger/default.go b/logger/default.go index 13f4233..962a7a0 100644 --- a/logger/default.go +++ b/logger/default.go @@ -6,6 +6,7 @@ import ( "regexp" "strings" + "github.com/flanksource/commons/properties" "github.com/spf13/pflag" ) @@ -33,7 +34,6 @@ func (f *flagSet) Parse() error { logFlagset := pflag.NewFlagSet("logger", pflag.ContinueOnError) // standalone parsing of flags to ensure we always have the correct values f.bindFlags(logFlagset) - logFlagset.ParseErrorsWhitelist.UnknownFlags = true if err := logFlagset.Parse(os.Args[1:]); err != nil { return err } @@ -63,6 +63,40 @@ func IsJsonLogs() bool { return flags.jsonLogs } +type Flags struct { + Color, ReportCaller, JsonLogs, LogToStderr bool + Level string + LevelCount int +} + +func Configure(flags Flags) { + // Get the verbosity count from the parsed flags + if flags.LevelCount > 0 { + currentLogger.SetLogLevel(flags.LevelCount) + } else if level := flags.Level; level != "" && level != "info" { + currentLogger.SetLogLevel(level) + } + + // Apply other flags + if flags.JsonLogs { + properties.Set("log.json", "true") + } + + if flags.ReportCaller { + properties.Set("log.report.caller", "true") + } + + if flags.LogToStderr { + properties.Set("log.stderr", "true") + } + if !flags.Color { + properties.Set("log.color", "false") + } + + currentLogger = *New("") + +} + // BindFlags add flags to an existing flag set, // note that this is not an actual binding which occurs later during initialization func BindFlags(flags *pflag.FlagSet) { @@ -83,7 +117,7 @@ func UseCobraFlags(flags *pflag.FlagSet) { } else if level, err := flags.GetString("log-level"); err == nil && level != "" && level != "info" { currentLogger.SetLogLevel(level) } - + // Apply other flags if jsonLogs, err := flags.GetBool("json-logs"); err == nil && jsonLogs { currentLogger = New("") @@ -106,7 +140,8 @@ func Infof(format string, args ...interface{}) { // It automatically redacts common secret patterns like passwords, tokens, and API keys. // // Example: -// logger.Secretf("Connecting with password=%s", password) // password will be redacted +// +// logger.Secretf("Connecting with password=%s", password) // password will be redacted func Secretf(format string, args ...interface{}) { currentLogger.Tracef(StripSecrets(fmt.Sprintf(format, args...))) } @@ -115,7 +150,8 @@ func Secretf(format string, args ...interface{}) { // Useful for debugging complex data structures. // // Example: -// logger.Prettyf("User data:", userStruct) // Logs formatted struct +// +// logger.Prettyf("User data:", userStruct) // Logs formatted struct func Prettyf(msg string, obj interface{}) { currentLogger.Tracef(msg, Pretty(obj)) } @@ -143,11 +179,13 @@ func Tracef(format string, args ...interface{}) { func Fatalf(format string, args ...interface{}) { currentLogger.Fatalf(format, args...) } + // V returns a verbose logger for conditional logging at the specified level. // The level can be an integer or a LogLevel constant. // // Example: -// logger.V(2).Infof("Detailed info") // Only logs at verbosity 2+ +// +// logger.V(2).Infof("Detailed info") // Only logs at verbosity 2+ func V(level any) Verbose { return currentLogger.V(level) } @@ -160,9 +198,10 @@ func IsTraceEnabled() bool { // IsLevelEnabled returns true if the specified verbosity level is enabled. // // Example: -// if logger.IsLevelEnabled(3) { -// // Perform expensive operation only if logging at level 3 -// } +// +// if logger.IsLevelEnabled(3) { +// // Perform expensive operation only if logging at level 3 +// } func IsLevelEnabled(level int) bool { return currentLogger.V(level).Enabled() } @@ -176,8 +215,9 @@ func IsDebugEnabled() bool { // These values will be included in all log messages from the returned logger. // // Example: -// userLogger := logger.WithValues("user_id", 123, "session", "abc") -// userLogger.Infof("User action") // Logs with user_id=123 session=abc +// +// userLogger := logger.WithValues("user_id", 123, "session", "abc") +// userLogger.Infof("User action") // Logs with user_id=123 session=abc func WithValues(keysAndValues ...interface{}) Logger { return currentLogger.WithValues(keysAndValues...) } diff --git a/logger/slog.go b/logger/slog.go index 5f6ffce..6debe8b 100644 --- a/logger/slog.go +++ b/logger/slog.go @@ -48,7 +48,13 @@ var SlogTraceLevel slog.Level = slog.LevelDebug - 1 var SlogFatal = slog.LevelError + 1 func GetSlogLogger() SlogLogger { - return currentLogger.(SlogLogger) + switch currentLogger := currentLogger.(type) { + case SlogLogger: + return currentLogger + case *SlogLogger: + return *currentLogger + } + return SlogLogger{} } func onPropertyUpdate(props *properties.Properties) { @@ -167,8 +173,9 @@ func camelCaseWords(s string) []string { // Multiple names create a hierarchical logger (e.g., GetLogger("app", "db") creates "app.db"). // // Example: -// dbLogger := logger.GetLogger("database") -// apiLogger := logger.GetLogger("api", "v1") +// +// dbLogger := logger.GetLogger("database") +// apiLogger := logger.GetLogger("api", "v1") func GetLogger(names ...string) *SlogLogger { parent, _ := namedLoggers.Load(rootName) if len(names) == 0 {