mirror of
https://github.com/ivuorinen/f2b.git
synced 2026-03-07 13:58:15 +00:00
* fix: repair Renovate config and convert Makefile to go run (#117) - Remove non-existent `github>renovatebot/presets:golang` preset that broke Renovate PR creation - Replace deprecated `fileMatch` with `managerFilePatterns` in customManagers - Rewrite regex to match new Makefile pattern (renovate comment above version variable assignment) - Fix `matchFileNames` glob pattern (`*.mk` -> `**/*.mk`) - Convert all tool invocations from `go install` + global binary to `go run tool@version` for reproducible builds - Convert npm global tools to `npx --yes` invocations - Remove `dev-deps` and `check-deps` targets (tools auto-download) - Add mdformat pre-commit hook with GFM support and config - Add `fmt-md` Makefile target for manual markdown formatting - Update local golangci-lint pre-commit hook to use `go run` - Apply golangci-lint v2.10.1 auto-fixes (fmt.Fprintf optimization) - Add nolint:gosec annotations for legitimate exec.Command usage - Exclude .serena/ from mdformat and megalinter - Add markdown indent_size=unset in .editorconfig for CommonMark compat * chore(deps): update GitHub Actions to latest versions - anthropics/claude-code-action: v1.0.34 -> v1.0.64 - actions/setup-go: v6.2.0 -> v6.3.0 - actions/upload-artifact: v6.0.0 -> v7.0.0 - goreleaser/goreleaser-action: v6.4.0 -> v7.0.0 - docker/login-action: v3.6.0 -> v3.7.0 - ivuorinen/actions: v2026.01.21 -> v2026.02.24 * fix: address code review feedback - Fix issue template YAML frontmatter (replace underscore separators with proper --- delimiters); exclude templates from mdformat - Replace string(rune(n)) with strconv.Itoa(n) in test files to produce deterministic numeric directory names instead of Unicode characters - Remove stale `make dev-deps` reference in README, replace with `make dev-setup` - Extract ban/unban format strings into shared.MetricsFmtBanOperations and shared.MetricsFmtUnbanOperations constants - Replace hardcoded coverage percentages in README with evergreen phrasing * fix: address round 2 code review feedback for PR #120 - Fix corrupted path traversal example in docs/security.md - Fix Renovate .mk regex to match nested paths (.*\.mk$) - Update checkmake pre-commit hook to v0.3.2 to match Makefile - Add sync.WaitGroup to unsynchronized goroutines in security tests - Fix fmt-md target to use pre-commit run mdformat - Pin markdownlint-cli2 to v0.21.0 in lint-md target - Standardize //nolint:gosec to // #nosec annotations for gosec CLI * fix(ci): install PyYAML dependency for PR lint workflow The pr-lint workflow uses ivuorinen/actions/pr-lint which internally calls validate-inputs running a Python script that imports yaml. Python was set up but PyYAML was never installed, causing ModuleNotFoundError at runtime. * fix: address round 3 code review feedback for PR #120 - Wrap Windows-style path traversal example in backtick code span so backslashes render literally in docs/security.md - Add Renovate-managed MARKDOWNLINT_CLI2_VERSION variable in Makefile to match the pattern used by all other tool versions
136 lines
4.7 KiB
Go
136 lines
4.7 KiB
Go
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/ivuorinen/f2b/fail2ban"
|
|
"github.com/ivuorinen/f2b/shared"
|
|
)
|
|
|
|
// MetricsCmd returns the metrics command with injected client and config
|
|
func MetricsCmd(_ fail2ban.Client, config *Config) *cobra.Command {
|
|
return NewCommand(
|
|
"metrics",
|
|
"Show performance metrics",
|
|
[]string{"stats"},
|
|
func(cmd *cobra.Command, _ []string) error {
|
|
// Get the global metrics instance
|
|
metrics := GetGlobalMetrics()
|
|
snapshot := metrics.GetSnapshot()
|
|
|
|
// Output metrics based on format
|
|
if config != nil && config.Format == JSONFormat {
|
|
encoder := json.NewEncoder(GetCmdOutput(cmd))
|
|
encoder.SetIndent("", " ")
|
|
if err := encoder.Encode(snapshot); err != nil {
|
|
return fmt.Errorf("failed to encode metrics: %w", err)
|
|
}
|
|
} else {
|
|
// Plain text output - use a helper to simplify error handling
|
|
if err := printMetricsPlain(GetCmdOutput(cmd), snapshot); err != nil {
|
|
return fmt.Errorf("failed to print metrics: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// printMetricsPlain prints metrics in plain text format
|
|
func printMetricsPlain(output io.Writer, snapshot MetricsSnapshot) error {
|
|
// Use a string builder to build the output
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("F2B Performance Metrics\n")
|
|
sb.WriteString("======================\n\n")
|
|
|
|
// System metrics
|
|
sb.WriteString("System:\n")
|
|
fmt.Fprintf(&sb, " Uptime: %ds\n", snapshot.UptimeSeconds)
|
|
fmt.Fprintf(&sb, " Max Memory: %.2f MB\n", float64(snapshot.MaxMemoryUsage)/(1024*1024))
|
|
fmt.Fprintf(&sb, " Goroutines: %d\n\n", snapshot.GoroutineCount)
|
|
|
|
// Command metrics
|
|
sb.WriteString("Commands:\n")
|
|
fmt.Fprintf(&sb, shared.MetricsFmtTotalExecutions, snapshot.CommandExecutions)
|
|
fmt.Fprintf(&sb, shared.MetricsFmtTotalFailures, snapshot.CommandFailures)
|
|
if snapshot.CommandExecutions > 0 {
|
|
avgLatency := float64(snapshot.CommandTotalDuration) / float64(snapshot.CommandExecutions)
|
|
fmt.Fprintf(&sb, shared.MetricsFmtAverageLatencyTop, avgLatency)
|
|
}
|
|
sb.WriteString("\n")
|
|
|
|
// Ban/Unban metrics
|
|
sb.WriteString("Ban Operations:\n")
|
|
fmt.Fprintf(&sb, shared.MetricsFmtBanOperations, snapshot.BanOperations, snapshot.BanFailures)
|
|
fmt.Fprintf(&sb, shared.MetricsFmtUnbanOperations, snapshot.UnbanOperations, snapshot.UnbanFailures)
|
|
sb.WriteString("\n")
|
|
|
|
// Client metrics
|
|
sb.WriteString("Client Operations:\n")
|
|
fmt.Fprintf(&sb, shared.MetricsFmtTotalOperations, snapshot.ClientOperations)
|
|
fmt.Fprintf(&sb, shared.MetricsFmtTotalFailures, snapshot.ClientFailures)
|
|
if snapshot.ClientOperations > 0 {
|
|
avgLatency := float64(snapshot.ClientTotalDuration) / float64(snapshot.ClientOperations)
|
|
fmt.Fprintf(&sb, shared.MetricsFmtAverageLatencyTop, avgLatency)
|
|
}
|
|
sb.WriteString("\n")
|
|
|
|
// Validation metrics
|
|
sb.WriteString("Validation:\n")
|
|
fmt.Fprintf(&sb, " Cache Hits: %d\n", snapshot.ValidationCacheHits)
|
|
fmt.Fprintf(&sb, " Cache Misses: %d\n", snapshot.ValidationCacheMiss)
|
|
fmt.Fprintf(&sb, " Failures: %d\n", snapshot.ValidationFailures)
|
|
if total := snapshot.ValidationCacheHits + snapshot.ValidationCacheMiss; total > 0 {
|
|
hitRate := float64(snapshot.ValidationCacheHits) / float64(total) * 100
|
|
fmt.Fprintf(&sb, " Cache Hit Rate: %.2f%%\n", hitRate)
|
|
}
|
|
sb.WriteString("\n")
|
|
|
|
// Command latency distribution
|
|
if len(snapshot.CommandLatencyBuckets) > 0 {
|
|
sb.WriteString("Command Latency Distribution:\n")
|
|
formatLatencyBuckets(&sb, snapshot.CommandLatencyBuckets)
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
// Client latency distribution
|
|
if len(snapshot.ClientLatencyBuckets) > 0 {
|
|
sb.WriteString("Client Operation Latency Distribution:\n")
|
|
formatLatencyBuckets(&sb, snapshot.ClientLatencyBuckets)
|
|
}
|
|
|
|
// Write the entire string at once
|
|
_, err := output.Write([]byte(sb.String()))
|
|
return err
|
|
}
|
|
|
|
// formatLatencyBuckets writes latency bucket distribution to the builder.
|
|
// Keys are sorted for deterministic output.
|
|
func formatLatencyBuckets(sb *strings.Builder, buckets map[string]LatencyBucketSnapshot) {
|
|
// Sort keys for deterministic output
|
|
keys := make([]string, 0, len(buckets))
|
|
for name := range buckets {
|
|
keys = append(keys, name)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
for _, name := range keys {
|
|
bucket := buckets[name]
|
|
fmt.Fprintf(sb, shared.MetricsFmtOperationHeader, name)
|
|
fmt.Fprintf(sb, shared.MetricsFmtLatencyUnder1ms, bucket.Under1ms)
|
|
fmt.Fprintf(sb, shared.MetricsFmtLatencyUnder10ms, bucket.Under10ms)
|
|
fmt.Fprintf(sb, shared.MetricsFmtLatencyUnder100ms, bucket.Under100ms)
|
|
fmt.Fprintf(sb, shared.MetricsFmtLatencyUnder1s, bucket.Under1s)
|
|
fmt.Fprintf(sb, shared.MetricsFmtLatencyUnder10s, bucket.Under10s)
|
|
fmt.Fprintf(sb, shared.MetricsFmtLatencyOver10s, bucket.Over10s)
|
|
fmt.Fprintf(sb, shared.MetricsFmtAverageLatency, bucket.GetAverageLatency())
|
|
}
|
|
}
|