Skip to content

Commit 8811191

Browse files
authored
Feat: Add web server (#2)
* Empty commit * First iteration of web server * Add version API and frontend disclaimers * Bump version to 0.0.2 and use intl date format * Markdown-html output * Fix display style * Fix style
1 parent ba0efe0 commit 8811191

File tree

18 files changed

+1651
-177
lines changed

18 files changed

+1651
-177
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ bin/
55
*.dll
66
*.so
77
*.dylib
8+
metrics-analyzer
89

910
# Test artifacts
1011
coverage.out
@@ -27,6 +28,9 @@ go.work.sum
2728
*.tmp
2829
*.log
2930

31+
# Frontend build output
32+
dist/
33+
3034
# Demo GIFs (hosted on vhs.charm.sh)
3135
demo/*.gif
3236

Makefile

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
1-
.PHONY: build test lint clean validate-rules
1+
.PHONY: build test lint clean validate-rules build-web release-build
2+
3+
VERSION ?= $(shell cat VERSION 2>/dev/null || echo dev)
4+
BUILD_TIME ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
5+
WEB_LDFLAGS := -X 'main.buildVersion=$(VERSION)' -X 'main.buildTime=$(BUILD_TIME)'
6+
RELEASE_OS ?= linux
7+
RELEASE_ARCH ?= amd64
28

39
build:
410
go build -o bin/metrics-analyzer ./cmd/metrics-analyzer
511

12+
build-web:
13+
go build -ldflags "$(WEB_LDFLAGS)" -o bin/web-server ./web/server
14+
15+
release-build:
16+
mkdir -p dist
17+
GOOS=$(RELEASE_OS) GOARCH=$(RELEASE_ARCH) go build -o dist/metrics-analyzer-$(VERSION)-$(RELEASE_OS)-$(RELEASE_ARCH) ./cmd/metrics-analyzer
18+
GOOS=$(RELEASE_OS) GOARCH=$(RELEASE_ARCH) go build -ldflags "$(WEB_LDFLAGS)" -o dist/web-server-$(VERSION)-$(RELEASE_OS)-$(RELEASE_ARCH) ./web/server
19+
620
test:
721
go test -v ./...
822

VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.0.2

cmd/metrics-analyzer/main.go

Lines changed: 16 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@ import (
44
"flag"
55
"fmt"
66
"os"
7-
"path/filepath"
87
"strings"
98

10-
"github.com/stackrox/sensor-metrics-analyzer/internal/evaluator"
11-
"github.com/stackrox/sensor-metrics-analyzer/internal/loadlevel"
12-
"github.com/stackrox/sensor-metrics-analyzer/internal/parser"
9+
"github.com/stackrox/sensor-metrics-analyzer/internal/analyzer"
1310
"github.com/stackrox/sensor-metrics-analyzer/internal/reporter"
1411
"github.com/stackrox/sensor-metrics-analyzer/internal/rules"
1512
"github.com/stackrox/sensor-metrics-analyzer/internal/tui"
@@ -87,61 +84,18 @@ func analyzeCommand() {
8784
}
8885
}
8986

90-
// Extract cluster name from filename if not provided
91-
if *clusterName == "" {
92-
*clusterName = extractClusterName(metricsFile)
93-
}
94-
95-
// Load load detection rules
96-
fmt.Fprintf(os.Stderr, "Loading load detection rules from %s...\n", *loadLevelDir)
97-
loadRules, err := rules.LoadLoadDetectionRules(*loadLevelDir)
98-
if err != nil {
99-
fmt.Fprintf(os.Stderr, "Warning: Failed to load load detection rules: %v\n", err)
100-
loadRules = []rules.LoadDetectionRule{}
101-
}
102-
103-
// Load evaluation rules
104-
fmt.Fprintf(os.Stderr, "Loading rules from %s...\n", *rulesDir)
105-
rulesList, err := rules.LoadRules(*rulesDir)
106-
if err != nil {
107-
fmt.Fprintf(os.Stderr, "Failed to load rules: %v\n", err)
108-
os.Exit(1)
109-
}
110-
fmt.Fprintf(os.Stderr, "Loaded %d rules\n", len(rulesList))
111-
112-
// Parse metrics
113-
fmt.Fprintf(os.Stderr, "Parsing metrics from %s...\n", metricsFile)
114-
metrics, err := parser.ParseFile(metricsFile)
87+
report, err := analyzer.AnalyzeFile(metricsFile, analyzer.Options{
88+
RulesDir: *rulesDir,
89+
LoadLevelDir: *loadLevelDir,
90+
ClusterName: *clusterName,
91+
LoadLevelOverride: *loadLevelOverride,
92+
ACSVersionOverride: *acsVersionOverride,
93+
Logger: os.Stderr,
94+
})
11595
if err != nil {
116-
fmt.Fprintf(os.Stderr, "Failed to parse metrics: %v\n", err)
96+
fmt.Fprintf(os.Stderr, "Failed to analyze metrics: %v\n", err)
11797
os.Exit(1)
11898
}
119-
fmt.Fprintf(os.Stderr, "Parsed %d metrics\n", len(metrics))
120-
121-
// Detect ACS version
122-
acsVersion := *acsVersionOverride
123-
if acsVersion == "" {
124-
if detected, ok := metrics.DetectACSVersion(); ok {
125-
acsVersion = detected
126-
fmt.Fprintf(os.Stderr, "Detected ACS version: %s\n", acsVersion)
127-
} else {
128-
fmt.Fprintf(os.Stderr, "Warning: Could not detect ACS version\n")
129-
}
130-
}
131-
132-
// Detect load level
133-
loadDetector := loadlevel.NewDetector(loadRules)
134-
detectedLoadLevel, err := loadlevel.DetectWithOverride(metrics, loadDetector, rules.LoadLevel(*loadLevelOverride))
135-
if err != nil {
136-
fmt.Fprintf(os.Stderr, "Warning: Load level detection failed: %v\n", err)
137-
detectedLoadLevel = rules.LoadLevelMedium
138-
}
139-
fmt.Fprintf(os.Stderr, "Detected load level: %s\n", detectedLoadLevel)
140-
141-
// Evaluate all rules
142-
fmt.Fprintf(os.Stderr, "Evaluating rules...\n")
143-
report := evaluator.EvaluateAllRules(rulesList, metrics, detectedLoadLevel, acsVersion)
144-
report.ClusterName = *clusterName
14599

146100
// Generate report
147101
var outputContent string
@@ -165,10 +119,12 @@ func analyzeCommand() {
165119
return
166120
}
167121
case "markdown":
168-
outputContent = reporter.GenerateMarkdown(report, *templatePath)
169-
if outputContent == "" {
170-
fmt.Fprintf(os.Stderr, "Warning: Markdown generation returned empty content\n")
122+
markdown, mdErr := reporter.GenerateMarkdown(report, *templatePath)
123+
if mdErr != nil {
124+
fmt.Fprintf(os.Stderr, "Markdown generation failed: %v\n", mdErr)
125+
os.Exit(1)
171126
}
127+
outputContent = markdown
172128
default:
173129
fmt.Fprintf(os.Stderr, "Unknown format: %s\n", *format)
174130
os.Exit(1)
@@ -255,13 +211,7 @@ func listRulesCommand() {
255211
}
256212

257213
func extractClusterName(filename string) string {
258-
base := filepath.Base(filename)
259-
// Remove extension
260-
name := strings.TrimSuffix(base, filepath.Ext(base))
261-
// Remove common prefixes/suffixes
262-
name = strings.TrimSuffix(name, "-sensor-metrics")
263-
name = strings.TrimSuffix(name, "-metrics")
264-
return name
214+
return analyzer.ExtractClusterName(filename)
265215
}
266216

267217
func printUsage() {

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/charmbracelet/lipgloss v1.1.0
1010
github.com/fatih/color v1.18.0
1111
github.com/jedib0t/go-pretty/v6 v6.7.1
12+
github.com/stretchr/testify v1.11.1
1213
golang.org/x/term v0.29.0
1314
)
1415

@@ -19,6 +20,7 @@ require (
1920
github.com/charmbracelet/x/ansi v0.10.1 // indirect
2021
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
2122
github.com/charmbracelet/x/term v0.2.1 // indirect
23+
github.com/davecgh/go-spew v1.1.1 // indirect
2224
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
2325
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
2426
github.com/mattn/go-colorable v0.1.13 // indirect
@@ -28,8 +30,10 @@ require (
2830
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
2931
github.com/muesli/cancelreader v0.2.2 // indirect
3032
github.com/muesli/termenv v0.16.0 // indirect
33+
github.com/pmezard/go-difflib v1.0.0 // indirect
3134
github.com/rivo/uniseg v0.4.7 // indirect
3235
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
3336
golang.org/x/sys v0.36.0 // indirect
3437
golang.org/x/text v0.22.0 // indirect
38+
gopkg.in/yaml.v3 v3.0.1 // indirect
3539
)

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
4848
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
4949
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
5050
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
51-
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
52-
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
51+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
52+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
5353
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
5454
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
5555
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
@@ -63,5 +63,7 @@ golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
6363
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
6464
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
6565
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
66+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
67+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
6668
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
6769
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/analyzer/analyzer.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package analyzer
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"path/filepath"
7+
"strings"
8+
9+
"github.com/stackrox/sensor-metrics-analyzer/internal/evaluator"
10+
"github.com/stackrox/sensor-metrics-analyzer/internal/loadlevel"
11+
"github.com/stackrox/sensor-metrics-analyzer/internal/parser"
12+
"github.com/stackrox/sensor-metrics-analyzer/internal/rules"
13+
)
14+
15+
// Options controls analysis behavior and logging.
16+
type Options struct {
17+
RulesDir string
18+
LoadLevelDir string
19+
ClusterName string
20+
LoadLevelOverride string
21+
ACSVersionOverride string
22+
Logger io.Writer
23+
}
24+
25+
// AnalyzeFile parses metrics and evaluates rules, returning the analysis report.
26+
func AnalyzeFile(metricsFile string, opts Options) (rules.AnalysisReport, error) {
27+
logOut := opts.Logger
28+
if logOut == nil {
29+
logOut = io.Discard
30+
}
31+
32+
rulesDir := opts.RulesDir
33+
if rulesDir == "" {
34+
return rules.AnalysisReport{}, fmt.Errorf("rules directory is required")
35+
}
36+
37+
loadLevelDir := opts.LoadLevelDir
38+
if loadLevelDir == "" {
39+
loadLevelDir = filepath.Join(rulesDir, "load-level")
40+
}
41+
42+
clusterName := opts.ClusterName
43+
if clusterName == "" {
44+
clusterName = ExtractClusterName(metricsFile)
45+
}
46+
47+
fmt.Fprintf(logOut, "Loading load detection rules from %s...\n", loadLevelDir)
48+
loadRules, err := rules.LoadLoadDetectionRules(loadLevelDir)
49+
if err != nil {
50+
fmt.Fprintf(logOut, "Warning: Failed to load load detection rules: %v\n", err)
51+
loadRules = []rules.LoadDetectionRule{}
52+
}
53+
54+
fmt.Fprintf(logOut, "Loading rules from %s...\n", rulesDir)
55+
rulesList, err := rules.LoadRules(rulesDir)
56+
if err != nil {
57+
return rules.AnalysisReport{}, fmt.Errorf("failed to load rules: %w", err)
58+
}
59+
fmt.Fprintf(logOut, "Loaded %d rules\n", len(rulesList))
60+
61+
fmt.Fprintf(logOut, "Parsing metrics from %s...\n", metricsFile)
62+
metrics, err := parser.ParseFile(metricsFile)
63+
if err != nil {
64+
return rules.AnalysisReport{}, fmt.Errorf("failed to parse metrics: %w", err)
65+
}
66+
fmt.Fprintf(logOut, "Parsed %d metrics\n", len(metrics))
67+
68+
acsVersion := opts.ACSVersionOverride
69+
if acsVersion == "" {
70+
if detected, ok := metrics.DetectACSVersion(); ok {
71+
acsVersion = detected
72+
fmt.Fprintf(logOut, "Detected ACS version: %s\n", acsVersion)
73+
} else {
74+
fmt.Fprintf(logOut, "Warning: Could not detect ACS version\n")
75+
}
76+
}
77+
78+
loadDetector := loadlevel.NewDetector(loadRules)
79+
detectedLoadLevel, err := loadlevel.DetectWithOverride(metrics, loadDetector, rules.LoadLevel(opts.LoadLevelOverride))
80+
if err != nil {
81+
fmt.Fprintf(logOut, "Warning: Load level detection failed: %v\n", err)
82+
detectedLoadLevel = rules.LoadLevelMedium
83+
}
84+
fmt.Fprintf(logOut, "Detected load level: %s\n", detectedLoadLevel)
85+
86+
fmt.Fprintf(logOut, "Evaluating rules...\n")
87+
report := evaluator.EvaluateAllRules(rulesList, metrics, detectedLoadLevel, acsVersion)
88+
report.ClusterName = clusterName
89+
90+
return report, nil
91+
}
92+
93+
// ExtractClusterName derives a cluster name from a file name.
94+
func ExtractClusterName(filename string) string {
95+
base := filepath.Base(filename)
96+
name := strings.TrimSuffix(base, filepath.Ext(base))
97+
name = strings.TrimSuffix(name, "-sensor-metrics")
98+
name = strings.TrimSuffix(name, "-metrics")
99+
return name
100+
}

internal/analyzer/analyzer_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package analyzer
2+
3+
import (
4+
"bytes"
5+
"path/filepath"
6+
"runtime"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestAnalyzeFile(t *testing.T) {
13+
t.Parallel()
14+
15+
_, thisFile, _, ok := runtime.Caller(0)
16+
if !ok {
17+
t.Fatal("AnalyzeFile() failed to resolve test file path")
18+
}
19+
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(thisFile)))
20+
metricsFile := filepath.Join(repoRoot, "testdata", "fixtures", "sample_metrics.txt")
21+
rulesDir := filepath.Join(repoRoot, "automated-rules")
22+
23+
var logs bytes.Buffer
24+
report, err := AnalyzeFile(metricsFile, Options{
25+
RulesDir: rulesDir,
26+
Logger: &logs,
27+
})
28+
assert.NoError(t, err)
29+
30+
assert.NotEmpty(t, report.ClusterName, "AnalyzeFile() cluster name is empty")
31+
assert.False(t, report.Timestamp.IsZero(), "AnalyzeFile() timestamp is zero")
32+
assert.NotEmpty(t, report.LoadLevel, "AnalyzeFile() load level is empty")
33+
assert.NotEmpty(t, report.Results, "AnalyzeFile() returned no results")
34+
assert.Equal(t, report.Summary.TotalAnalyzed, len(report.Results), "AnalyzeFile() summary mismatch")
35+
statusTotal := report.Summary.RedCount + report.Summary.YellowCount + report.Summary.GreenCount
36+
assert.LessOrEqual(t, statusTotal, report.Summary.TotalAnalyzed, "AnalyzeFile() summary counts exceed total")
37+
}

0 commit comments

Comments
 (0)