From 36751b00e41b6c52e93b872887f7b8a9ba87f1ea Mon Sep 17 00:00:00 2001 From: Javier Marcos <1271349+javuto@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:20:19 +0200 Subject: [PATCH] Updated pre-commit dependencies and fixed linter issues --- .pre-commit-config.yaml | 7 +- cmd/api/handlers/auth_oidc_test.go | 8 +-- cmd/api/handlers/auth_resolve.go | 6 +- cmd/api/handlers/events.go | 2 +- cmd/api/handlers/handlers.go | 2 +- cmd/api/handlers/nodes_inactivity_test.go | 7 +- cmd/api/handlers/query_dispatch_test.go | 2 +- cmd/api/main.go | 12 ++-- cmd/cli/alert.go | 2 +- cmd/cli/main.go | 2 +- cmd/cli/shell_module_extras.go | 2 +- cmd/cli/shell_store.go | 2 +- cmd/cli/shell_store_extras.go | 2 +- cmd/tls/main.go | 12 ++-- pkg/auth/oidc/provider.go | 6 +- pkg/auth/saml/config_test.go | 4 +- pkg/auth/saml/provider.go | 7 +- pkg/auth/saml/provider_test.go | 7 +- pkg/cache/cache_test.go | 2 +- pkg/carves/utils.go | 13 ++-- pkg/nodes/checkins.go | 2 +- pkg/nodes/checkins_test.go | 2 +- tools/fake_news_go/fake_news.go | 81 ----------------------- 23 files changed, 60 insertions(+), 132 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 92b0ec9dc..35498b459 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-ast - id: check-json @@ -14,6 +14,7 @@ repos: # locally; the hook still scans staged files for leaked AWS keys. args: [--allow-missing-credentials] - id: detect-private-key + exclude: "pkg/auth/saml/provider.go" - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace @@ -23,14 +24,14 @@ repos: - id: requirements-txt-fixer - repo: https://github.com/Bahjat/pre-commit-golang - rev: v1.0.5 + rev: v1.0.6 hooks: - id: go-fmt-import - id: go-static-check # install https://staticcheck.io/docs/ - id: go-unit-tests - repo: https://github.com/golangci/golangci-lint - rev: v2.11.4 + rev: v2.13.2 hooks: - id: golangci-lint args: [--config=.golangci.yml] diff --git a/cmd/api/handlers/auth_oidc_test.go b/cmd/api/handlers/auth_oidc_test.go index d73436420..6ae4e3aa7 100644 --- a/cmd/api/handlers/auth_oidc_test.go +++ b/cmd/api/handlers/auth_oidc_test.go @@ -29,10 +29,10 @@ import ( // happy-path verification is already covered by the pkg-level tests, // so we don't need a working /token endpoint here. type fakeIdP struct { - srv *httptest.Server - key *rsa.PrivateKey - keyID string - issuer string // overridden after srv.URL is known + srv *httptest.Server + key *rsa.PrivateKey + keyID string + issuer string // overridden after srv.URL is known } func newFakeIdP(t *testing.T) *fakeIdP { diff --git a/cmd/api/handlers/auth_resolve.go b/cmd/api/handlers/auth_resolve.go index 9d3fead4e..dadec2954 100644 --- a/cmd/api/handlers/auth_resolve.go +++ b/cmd/api/handlers/auth_resolve.go @@ -88,7 +88,7 @@ func (h *HandlersApi) resolveFederatedUser(identity auth.ResolvedIdentity, polic if existing.AuthSource != policy.authSource { linkedLocal := existing.AuthSource == "" if err := h.Users.ChangeAuthSource(existing.Username, policy.authSource); err != nil { - return users.AdminUser{}, fmt.Errorf("%w: updating auth source: %v", ErrAuthUserRejected, err) + return users.AdminUser{}, fmt.Errorf("%w: updating auth source: %w", ErrAuthUserRejected, err) } existing.AuthSource = policy.authSource if linkedLocal { @@ -135,14 +135,14 @@ func (h *HandlersApi) resolveFederatedUser(identity auth.ResolvedIdentity, polic false, // service = false ) if err != nil { - return users.AdminUser{}, fmt.Errorf("%w: new user: %v", ErrAuthUserRejected, err) + return users.AdminUser{}, fmt.Errorf("%w: new user: %w", ErrAuthUserRejected, err) } // Tag the row with the provider type (oidc / saml) so the Users // page can display the right badge. Purely informational; the auth // flow itself doesn't gate on this field. u.AuthSource = policy.authSource if err := h.Users.Create(u); err != nil { - return users.AdminUser{}, fmt.Errorf("%w: create user: %v", ErrAuthUserRejected, err) + return users.AdminUser{}, fmt.Errorf("%w: create user: %w", ErrAuthUserRejected, err) } return u, nil } diff --git a/cmd/api/handlers/events.go b/cmd/api/handlers/events.go index f073a7975..42c29b89b 100644 --- a/cmd/api/handlers/events.go +++ b/cmd/api/handlers/events.go @@ -99,7 +99,7 @@ func (h *HandlersApi) EventsHandler(w http.ResponseWriter, r *http.Request) { } environmentID := uint(0) environmentUUID := "all" - if !(globalOnly && envSelector == "all") { + if !globalOnly || envSelector != "all" { env, err := h.Envs.Get(envSelector) if err != nil { apiErrorResponse(w, "environment not found", http.StatusNotFound, nil) diff --git a/cmd/api/handlers/handlers.go b/cmd/api/handlers/handlers.go index 9471aa27e..96881f88b 100644 --- a/cmd/api/handlers/handlers.go +++ b/cmd/api/handlers/handlers.go @@ -1,7 +1,6 @@ package handlers import ( - "github.com/jmpsec/osctrl/pkg/events" "net/http" "time" @@ -14,6 +13,7 @@ import ( "github.com/jmpsec/osctrl/pkg/config" "github.com/jmpsec/osctrl/pkg/console" "github.com/jmpsec/osctrl/pkg/environments" + "github.com/jmpsec/osctrl/pkg/events" "github.com/jmpsec/osctrl/pkg/fileexplorer" "github.com/jmpsec/osctrl/pkg/geoip" "github.com/jmpsec/osctrl/pkg/health" diff --git a/cmd/api/handlers/nodes_inactivity_test.go b/cmd/api/handlers/nodes_inactivity_test.go index 4eb90234c..2d68f7eca 100644 --- a/cmd/api/handlers/nodes_inactivity_test.go +++ b/cmd/api/handlers/nodes_inactivity_test.go @@ -80,14 +80,15 @@ func TestNodeConsumersEnvironmentThresholds(t *testing.T) { if tc.active == (i == 1) { want = 1 } - if tc.paged { + switch { + case tc.paged: require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) var page types.NodesPagedResponse require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &page)) require.Equal(t, want, page.TotalItems) - } else if want == 0 { + case want == 0: require.Equal(t, http.StatusNotFound, rr.Code, rr.Body.String()) - } else { + default: require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) var got []nodes.OsqueryNode require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &got)) diff --git a/cmd/api/handlers/query_dispatch_test.go b/cmd/api/handlers/query_dispatch_test.go index 924543933..679114e03 100644 --- a/cmd/api/handlers/query_dispatch_test.go +++ b/cmd/api/handlers/query_dispatch_test.go @@ -58,7 +58,7 @@ func TestCreateQueryInvalidatesDispatchAfterCommit(t *testing.T) { require.NoError(t, err) require.True(t, cached, "do not invalidate while query creation is uncommitted") if rollback { - tx.AddError(errors.New("target write failed")) + _ = tx.AddError(errors.New("target write failed")) } if name == "canceled-request" { cancel() diff --git a/cmd/api/main.go b/cmd/api/main.go index dc2c38a70..0ab265994 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -611,7 +611,6 @@ func osctrlAPIService() { if err != nil { log.Fatal().Err(err).Msg("invalid events configuration") } - defer eventBus.Close() queriesmgr.Events = eventBus filecarves.Events = eventBus if alertsMgr != nil { @@ -1352,9 +1351,8 @@ func osctrlAPIService() { } if tlsTermination { srv.TLSConfig = &tls.Config{ - MinVersion: tls.VersionTLS12, - CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, - PreferServerCipherSuites: true, + MinVersion: tls.VersionTLS12, + CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, CipherSuites: []uint16{ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, @@ -1378,11 +1376,13 @@ func osctrlAPIService() { // Stop live streams before draining on process shutdown or restart. shutdownSignals := make(chan os.Signal, 1) signal.Notify(shutdownSignals, syscall.SIGINT, syscall.SIGTERM) - defer signal.Stop(shutdownSignals) // Wait for either a server error or a restart signal. select { case err := <-serverErr: if err != nil && !errors.Is(err, http.ErrServerClosed) { + if eventBus != nil { + eventBus.Close() + } log.Fatal().Msgf("ListenAndServe: %v", err) } case <-shutdownSignals: @@ -1390,10 +1390,10 @@ func osctrlAPIService() { eventBus.Close() } drainCtx, cancelDrain := context.WithTimeout(context.Background(), restartDrainTimeout) - defer cancelDrain() if err := srv.Shutdown(drainCtx); err != nil { log.Err(err).Msg("error draining HTTP server") } + cancelDrain() case <-restartCh: if eventBus != nil { eventBus.Close() diff --git a/cmd/cli/alert.go b/cmd/cli/alert.go index 026d2dab5..b68bc02be 100644 --- a/cmd/cli/alert.go +++ b/cmd/cli/alert.go @@ -5,12 +5,12 @@ import ( "encoding/csv" "encoding/json" "fmt" - "github.com/jmpsec/osctrl/pkg/apiclient" "os" "strconv" "strings" "github.com/jmpsec/osctrl/pkg/alerts" + "github.com/jmpsec/osctrl/pkg/apiclient" "github.com/olekukonko/tablewriter" "github.com/urfave/cli/v3" ) diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 47eeffaaf..703d531c6 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -3,11 +3,11 @@ package main import ( "context" "fmt" - "github.com/jmpsec/osctrl/pkg/apiclient" "os" "path/filepath" "strconv" + "github.com/jmpsec/osctrl/pkg/apiclient" "github.com/jmpsec/osctrl/pkg/auditlog" "github.com/jmpsec/osctrl/pkg/backend" "github.com/jmpsec/osctrl/pkg/carves" diff --git a/cmd/cli/shell_module_extras.go b/cmd/cli/shell_module_extras.go index f74ab03e5..11f916c41 100644 --- a/cmd/cli/shell_module_extras.go +++ b/cmd/cli/shell_module_extras.go @@ -3,12 +3,12 @@ package main import ( "encoding/json" "fmt" - "github.com/jmpsec/osctrl/pkg/apiclient" "sort" "strconv" "strings" "time" + "github.com/jmpsec/osctrl/pkg/apiclient" "github.com/jmpsec/osctrl/pkg/console" "github.com/jmpsec/osctrl/pkg/fileexplorer" "github.com/jmpsec/osctrl/pkg/posture" diff --git a/cmd/cli/shell_store.go b/cmd/cli/shell_store.go index a7b753108..1a78804cb 100644 --- a/cmd/cli/shell_store.go +++ b/cmd/cli/shell_store.go @@ -4,12 +4,12 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/jmpsec/osctrl/pkg/apiclient" "path" "strconv" "strings" "time" + "github.com/jmpsec/osctrl/pkg/apiclient" "github.com/jmpsec/osctrl/pkg/auditlog" "github.com/jmpsec/osctrl/pkg/carves" "github.com/jmpsec/osctrl/pkg/config" diff --git a/cmd/cli/shell_store_extras.go b/cmd/cli/shell_store_extras.go index a26201fe0..c78ab9a96 100644 --- a/cmd/cli/shell_store_extras.go +++ b/cmd/cli/shell_store_extras.go @@ -2,8 +2,8 @@ package main import ( "fmt" - "github.com/jmpsec/osctrl/pkg/apiclient" + "github.com/jmpsec/osctrl/pkg/apiclient" "github.com/jmpsec/osctrl/pkg/console" "github.com/jmpsec/osctrl/pkg/fileexplorer" "github.com/jmpsec/osctrl/pkg/posture" diff --git a/cmd/tls/main.go b/cmd/tls/main.go index 93ab1d559..d151af2c6 100644 --- a/cmd/tls/main.go +++ b/cmd/tls/main.go @@ -469,7 +469,6 @@ func osctrlService() { if err != nil { log.Fatal().Err(err).Msg("invalid events configuration") } - defer eventBus.Close() queriesmgr.Events = eventBus filecarves.Events = eventBus if alertsMgr != nil { @@ -586,9 +585,8 @@ func osctrlService() { if flagParams.TLS.Termination { log.Info().Msg("TLS Termination is enabled") srv.TLSConfig = &tls.Config{ - MinVersion: tls.VersionTLS12, - CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, - PreferServerCipherSuites: true, + MinVersion: tls.VersionTLS12, + CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, CipherSuites: []uint16{ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, @@ -616,12 +614,18 @@ func osctrlService() { sinkStatsWriter.Stop() stopAlerts(alertsRefreshStop, alertsInactiveStop, alertsRetentionStop, alertsWorker) if err != nil && !errors.Is(err, http.ErrServerClosed) { + if eventBus != nil { + eventBus.Close() + } log.Fatal().Msgf("ListenAndServe: %v", err) } case <-restartCh: stopCommandWatcher() sinkStatsWriter.Stop() stopAlerts(alertsRefreshStop, alertsInactiveStop, alertsRetentionStop, alertsWorker) + if eventBus != nil { + eventBus.Close() + } log.Info().Msg("TLS service command consumed — exiting for restart") os.Exit(1) } diff --git a/pkg/auth/oidc/provider.go b/pkg/auth/oidc/provider.go index 817591db5..d0751c4b7 100644 --- a/pkg/auth/oidc/provider.go +++ b/pkg/auth/oidc/provider.go @@ -253,7 +253,7 @@ func (p *Provider) HandleCallback(parentCtx context.Context, r *http.Request, st if err != nil { // Wrap, don't merge — callers may want to log err // server-side without exposing it to clients. - return auth.ResolvedIdentity{}, fmt.Errorf("%w: %v", ErrTokenExchange, err) + return auth.ResolvedIdentity{}, fmt.Errorf("%w: %w", ErrTokenExchange, err) } // (6) id_token present. @@ -265,7 +265,7 @@ func (p *Provider) HandleCallback(parentCtx context.Context, r *http.Request, st // (7) Verify signature + iss + aud + exp + nbf. idToken, err := p.verifier.Verify(ctx, rawIDToken) if err != nil { - return auth.ResolvedIdentity{}, fmt.Errorf("%w: %v", ErrIDTokenVerify, err) + return auth.ResolvedIdentity{}, fmt.Errorf("%w: %w", ErrIDTokenVerify, err) } // (8) Nonce match. @@ -276,7 +276,7 @@ func (p *Provider) HandleCallback(parentCtx context.Context, r *http.Request, st // Decode the claims we care about. var claims idTokenClaims if err := idToken.Claims(&claims); err != nil { - return auth.ResolvedIdentity{}, fmt.Errorf("%w: claims decode: %v", ErrIDTokenVerify, err) + return auth.ResolvedIdentity{}, fmt.Errorf("%w: claims decode: %w", ErrIDTokenVerify, err) } // Also decode into a generic map so pickUsername can look up // custom claim names like "nickname" (Auth0) or "upn" (Entra) diff --git a/pkg/auth/saml/config_test.go b/pkg/auth/saml/config_test.go index eae6ee855..f562b3316 100644 --- a/pkg/auth/saml/config_test.go +++ b/pkg/auth/saml/config_test.go @@ -58,12 +58,12 @@ func TestConfigValidate_Failures(t *testing.T) { }, { name: "no entity id", - mut: func(c *Config) { c.EntityID = "" }, + mut: func(c *Config) { c.EntityID = "" }, want: "EntityID is required", }, { name: "no acs url", - mut: func(c *Config) { c.ACSURL = "" }, + mut: func(c *Config) { c.ACSURL = "" }, want: "ACSURL is required", }, { diff --git a/pkg/auth/saml/provider.go b/pkg/auth/saml/provider.go index 9af1ad744..096a70900 100644 --- a/pkg/auth/saml/provider.go +++ b/pkg/auth/saml/provider.go @@ -288,7 +288,7 @@ func (p *Provider) loginURLAndRequestID(state auth.State) (string, string, error // above. This is the security perimeter. func (p *Provider) HandleCallback(_ context.Context, r *http.Request, state auth.State) (auth.ResolvedIdentity, error) { if err := r.ParseForm(); err != nil { - return auth.ResolvedIdentity{}, fmt.Errorf("%w: parse form: %v", ErrParseResponse, err) + return auth.ResolvedIdentity{}, fmt.Errorf("%w: parse form: %w", ErrParseResponse, err) } // (1) SAMLResponse field must be present. @@ -326,11 +326,12 @@ func (p *Provider) HandleCallback(_ context.Context, r *http.Request, state auth // the PrivateErr at WARN so operators can diagnose IdP / cert / // audience mismatches; the client still sees only the sentinel. logEvent := log.Warn().Err(err) - if ire, ok := err.(*crewjam.InvalidResponseError); ok && ire != nil && ire.PrivateErr != nil { + var ire *crewjam.InvalidResponseError + if errors.As(err, &ire) && ire != nil && ire.PrivateErr != nil { logEvent = logEvent.AnErr("private", ire.PrivateErr) } logEvent.Msg("saml: ParseResponse failed") - return auth.ResolvedIdentity{}, fmt.Errorf("%w: %v", ErrParseResponse, err) + return auth.ResolvedIdentity{}, fmt.Errorf("%w: %w", ErrParseResponse, err) } // (4) Replay defense — the assertion just passed signature + diff --git a/pkg/auth/saml/provider_test.go b/pkg/auth/saml/provider_test.go index 383781c53..89840547c 100644 --- a/pkg/auth/saml/provider_test.go +++ b/pkg/auth/saml/provider_test.go @@ -2,6 +2,7 @@ package saml import ( "context" + "errors" "net/http" "net/http/httptest" "net/url" @@ -128,7 +129,7 @@ func TestHandleCallback_StateMismatch(t *testing.T) { _, err := p.HandleCallback(context.Background(), r, auth.State{EnvUUID: "global", Nonce: "n-the-real-nonce", OAuthState: "the-real-nonce"}) - if err != ErrStateMismatch { + if !errors.Is(err, ErrStateMismatch) { t.Errorf("expected ErrStateMismatch, got %v", err) } } @@ -147,7 +148,7 @@ func TestHandleCallback_MissingSAMLResponse(t *testing.T) { _, err := p.HandleCallback(context.Background(), r, auth.State{EnvUUID: "global", Nonce: "n-the-real-nonce", OAuthState: "the-real-nonce"}) - if err != ErrMissingSAMLResponse { + if !errors.Is(err, ErrMissingSAMLResponse) { t.Errorf("expected ErrMissingSAMLResponse, got %v", err) } } @@ -239,7 +240,7 @@ func errIs(got, target error) bool { if got == nil { return false } - if got == target { + if errors.Is(got, target) { return true } // fmt.Errorf("%w: ...", target) wraps the sentinel. diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index c4d096c19..b57fdf2f5 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -36,7 +36,7 @@ func TestRedisConnectionErrorLeavesOtherErrorsAlone(t *testing.T) { baseErr := errors.New("dial tcp 127.0.0.1:6379: connect: connection refused") err := redisConnectionError(config.YAMLConfigurationRedis{}, baseErr) - if err != baseErr { + if !errors.Is(err, baseErr) { t.Fatalf("error = %v, want original error", err) } } diff --git a/pkg/carves/utils.go b/pkg/carves/utils.go index cbe80d5a2..51ecb0986 100644 --- a/pkg/carves/utils.go +++ b/pkg/carves/utils.go @@ -89,12 +89,13 @@ func GenCarveName() string { // escapeSQLString returns s with every single quote doubled, so the // result is safe to interpolate inside a SQL string literal — osquery -// (SQLite) follows the standard rule that '' inside a literal is one -// literal quote, and there is no backslash escape to consider. This -// is the SQL-injection defense for GenCarveQuery: a path containing -// `'; DROP TABLE x; --` becomes `''; DROP TABLE x; --`, which the -// parser sees as the contents of the string literal — there is no -// way to escape the surrounding quotes. +// (SQLite) follows the standard rule that two consecutive single +// quotes inside a literal represent one literal quote, and there is +// no backslash escape to consider. This is the SQL-injection defense +// for GenCarveQuery: a path containing a quote followed by a +// semicolon and DROP TABLE becomes a doubled quote, which the parser +// sees as the contents of the string literal — there is no way to +// escape the surrounding quotes. func escapeSQLString(s string) string { return strings.ReplaceAll(s, "'", "''") } diff --git a/pkg/nodes/checkins.go b/pkg/nodes/checkins.go index 82f8d573e..22071699e 100644 --- a/pkg/nodes/checkins.go +++ b/pkg/nodes/checkins.go @@ -34,7 +34,7 @@ func (n *NodeManager) UpdateCheckins(updates map[uint]Checkin) error { } sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) precision := time.Nanosecond - switch n.DB.Dialector.Name() { + switch n.DB.Name() { case "mysql": precision = time.Millisecond // GORM's default DATETIME(3). case "postgres": diff --git a/pkg/nodes/checkins_test.go b/pkg/nodes/checkins_test.go index fea6de00d..c9f27eed0 100644 --- a/pkg/nodes/checkins_test.go +++ b/pkg/nodes/checkins_test.go @@ -28,7 +28,7 @@ func TestUpdateCheckinsExternalBackend(t *testing.T) { if dsn == "" { t.Skip("set OSCTRL_TEST_" + backend + "_DSN to test this engine") } - var dialect gorm.Dialector = postgres.Open(dsn) + var dialect = postgres.Open(dsn) if backend == "MYSQL" { dialect = mysql.Open(dsn) } diff --git a/tools/fake_news_go/fake_news.go b/tools/fake_news_go/fake_news.go index bc070eaa1..656b2572c 100644 --- a/tools/fake_news_go/fake_news.go +++ b/tools/fake_news_go/fake_news.go @@ -1323,72 +1323,6 @@ func printSummary() { fmt.Printf("\n%s\n\n", strings.Repeat("=", 90)) } -// printDashboard prints a real-time dashboard -func printDashboard() { - // Clear screen and move cursor to top - fmt.Print("\033[2J\033[H") - - uptime := globalStats.GetUptime() - - fmt.Printf("FAKE NEWS GENERATOR — REAL-TIME DASHBOARD\n") - fmt.Printf("Uptime: %s | Last Update: %s\n", uptime.Round(time.Second), time.Now().Format("15:04:05")) - fmt.Printf("%s\n", strings.Repeat("-", 100)) - - operations := []struct { - name string - op OperationType - }{ - {"Enroll", EnrollOp}, - {"Status", StatusOp}, - {"Result", ResultOp}, - {"Config", ConfigOp}, - {"Query Read", QueryReadOp}, - {"Query Write", QueryWriteOp}, - } - - fmt.Printf(" %-14s %10s %10s %10s %10s %10s %10s %10s\n", - "Operation", "Count", "Success%", "Min(ms)", "Avg(ms)", "Max(ms)", "P95(ms)", "P99(ms)") - fmt.Printf(" %s\n", strings.Repeat("-", 96)) - - for _, op := range operations { - stats := globalStats.GetOperationStats(op.op) - min, max, avg, p95, p99, count, success, _ := stats.GetStats() - - if count > 0 { - successRate := float64(success) / float64(count) * 100 - fmt.Printf(" %-14s %10d %9.1f%% %10d %10d %10d %10d %10d\n", - op.name, count, successRate, min.Milliseconds(), avg.Milliseconds(), max.Milliseconds(), p95.Milliseconds(), p99.Milliseconds()) - } else { - fmt.Printf(" %-14s %10d %10s %10s %10s %10s %10s %10s\n", - op.name, 0, "-", "-", "-", "-", "-", "-") - } - } - fmt.Printf(" %s\n\n", strings.Repeat("-", 96)) - - // URL statistics - fmt.Printf("URL STATISTICS\n") - fmt.Printf(" %s\n", strings.Repeat("-", 96)) - fmt.Printf(" %-55s %8s %8s %8s %9s\n", "URL", "Total", "Success", "Failed", "Success%") - fmt.Printf(" %s\n", strings.Repeat("-", 96)) - - urlStats := globalStats.GetURLStats() - if len(urlStats) > 0 { - for url, stats := range urlStats { - _, _, _, _, _, count, success, fail := stats.GetStats() - if count > 0 { - successRate := float64(success) / float64(count) * 100 - if len(url) > 55 { - url = "..." + url[len(url)-52:] - } - fmt.Printf(" %-55s %8d %8d %8d %8.1f%%\n", url, count, success, fail, successRate) - } - } - } else { - fmt.Printf(" %-55s %8s %8s %8s %9s\n", "No data yet", "-", "-", "-", "-") - } - fmt.Printf(" %s\n", strings.Repeat("-", 96)) -} - // printJSONStats prints statistics in JSON format func printJSONStats() { stats := make(map[string]interface{}) @@ -1481,21 +1415,6 @@ func logOperationWithURL(opType OperationType, nodeName string, url string, late } } -// summaryReporter runs periodic summary reports -func summaryReporter(ctx context.Context, interval time.Duration) { - ticker := time.NewTicker(interval) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - printSummary() - } - } -} - func newGlobalStats() *GlobalStats { return &GlobalStats{ startTime: time.Now(),