Skip to content

Commit 3fb8d73

Browse files
authored
Refactor: Extract shared log aggregation logic into generic helper (#3465)
1 parent 612c806 commit 3fb8d73

4 files changed

Lines changed: 574 additions & 135 deletions

File tree

pkg/cli/access_log.go

Lines changed: 40 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,35 @@ type DomainAnalysis struct {
3535
DeniedCount int
3636
}
3737

38+
// GetAllowedDomains returns the list of allowed domains
39+
func (d *DomainAnalysis) GetAllowedDomains() []string {
40+
return d.AllowedDomains
41+
}
42+
43+
// GetDeniedDomains returns the list of denied domains
44+
func (d *DomainAnalysis) GetDeniedDomains() []string {
45+
return d.DeniedDomains
46+
}
47+
48+
// SetAllowedDomains sets the list of allowed domains
49+
func (d *DomainAnalysis) SetAllowedDomains(domains []string) {
50+
d.AllowedDomains = domains
51+
}
52+
53+
// SetDeniedDomains sets the list of denied domains
54+
func (d *DomainAnalysis) SetDeniedDomains(domains []string) {
55+
d.DeniedDomains = domains
56+
}
57+
58+
// AddMetrics adds metrics from another analysis
59+
func (d *DomainAnalysis) AddMetrics(other LogAnalysis) {
60+
if otherDomain, ok := other.(*DomainAnalysis); ok {
61+
d.TotalRequests += otherDomain.TotalRequests
62+
d.AllowedCount += otherDomain.AllowedCount
63+
d.DeniedCount += otherDomain.DeniedCount
64+
}
65+
}
66+
3867
// parseSquidAccessLog parses a squid access log file and extracts domain information
3968
func parseSquidAccessLog(logPath string, verbose bool) (*DomainAnalysis, error) {
4069
file, err := os.Open(logPath)
@@ -175,70 +204,18 @@ func analyzeAccessLogs(runDir string, verbose bool) (*DomainAnalysis, error) {
175204

176205
// analyzeMultipleAccessLogs analyzes multiple separate access log files
177206
func analyzeMultipleAccessLogs(accessLogsDir string, verbose bool) (*DomainAnalysis, error) {
178-
files, err := filepath.Glob(filepath.Join(accessLogsDir, "access-*.log"))
179-
if err != nil {
180-
return nil, fmt.Errorf("failed to find access log files: %w", err)
181-
}
182-
183-
if len(files) == 0 {
184-
if verbose {
185-
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("No access log files found in %s", accessLogsDir)))
186-
}
187-
return nil, nil
188-
}
189-
190-
if verbose {
191-
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Analyzing %d access log files from %s", len(files), accessLogsDir)))
192-
}
193-
194-
// Aggregate analysis from all files
195-
aggregatedAnalysis := &DomainAnalysis{
196-
AllowedDomains: []string{},
197-
DeniedDomains: []string{},
198-
}
199-
200-
allAllowedDomains := make(map[string]bool)
201-
allDeniedDomains := make(map[string]bool)
202-
203-
for _, file := range files {
204-
if verbose {
205-
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Parsing %s", filepath.Base(file))))
206-
}
207-
208-
analysis, err := parseSquidAccessLog(file, verbose)
209-
if err != nil {
210-
if verbose {
211-
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to parse %s: %v", filepath.Base(file), err)))
207+
return aggregateLogFiles(
208+
accessLogsDir,
209+
"access-*.log",
210+
verbose,
211+
parseSquidAccessLog,
212+
func() *DomainAnalysis {
213+
return &DomainAnalysis{
214+
AllowedDomains: []string{},
215+
DeniedDomains: []string{},
212216
}
213-
continue
214-
}
215-
216-
// Aggregate the metrics
217-
aggregatedAnalysis.TotalRequests += analysis.TotalRequests
218-
aggregatedAnalysis.AllowedCount += analysis.AllowedCount
219-
aggregatedAnalysis.DeniedCount += analysis.DeniedCount
220-
221-
// Collect unique domains
222-
for _, domain := range analysis.AllowedDomains {
223-
allAllowedDomains[domain] = true
224-
}
225-
for _, domain := range analysis.DeniedDomains {
226-
allDeniedDomains[domain] = true
227-
}
228-
}
229-
230-
// Convert maps to sorted slices
231-
for domain := range allAllowedDomains {
232-
aggregatedAnalysis.AllowedDomains = append(aggregatedAnalysis.AllowedDomains, domain)
233-
}
234-
for domain := range allDeniedDomains {
235-
aggregatedAnalysis.DeniedDomains = append(aggregatedAnalysis.DeniedDomains, domain)
236-
}
237-
238-
sort.Strings(aggregatedAnalysis.AllowedDomains)
239-
sort.Strings(aggregatedAnalysis.DeniedDomains)
240-
241-
return aggregatedAnalysis, nil
217+
},
218+
)
242219
}
243220

244221
// formatDomainWithEcosystem formats a domain with its ecosystem identifier if found

pkg/cli/firewall_log.go

Lines changed: 49 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,43 @@ type FirewallAnalysis struct {
122122
RequestsByDomain map[string]DomainRequestStats
123123
}
124124

125+
// GetAllowedDomains returns the list of allowed domains
126+
func (f *FirewallAnalysis) GetAllowedDomains() []string {
127+
return f.AllowedDomains
128+
}
129+
130+
// GetDeniedDomains returns the list of denied domains
131+
func (f *FirewallAnalysis) GetDeniedDomains() []string {
132+
return f.DeniedDomains
133+
}
134+
135+
// SetAllowedDomains sets the list of allowed domains
136+
func (f *FirewallAnalysis) SetAllowedDomains(domains []string) {
137+
f.AllowedDomains = domains
138+
}
139+
140+
// SetDeniedDomains sets the list of denied domains
141+
func (f *FirewallAnalysis) SetDeniedDomains(domains []string) {
142+
f.DeniedDomains = domains
143+
}
144+
145+
// AddMetrics adds metrics from another analysis
146+
func (f *FirewallAnalysis) AddMetrics(other LogAnalysis) {
147+
if otherFirewall, ok := other.(*FirewallAnalysis); ok {
148+
f.TotalRequests += otherFirewall.TotalRequests
149+
f.AllowedRequests += otherFirewall.AllowedRequests
150+
f.DeniedRequests += otherFirewall.DeniedRequests
151+
152+
// Merge request stats by domain
153+
for domain, stats := range otherFirewall.RequestsByDomain {
154+
existing := f.RequestsByDomain[domain]
155+
existing.Allowed += stats.Allowed
156+
existing.Denied += stats.Denied
157+
f.RequestsByDomain[domain] = existing
158+
}
159+
}
160+
}
161+
125162
// DomainRequestStats tracks request statistics per domain
126163
type DomainRequestStats struct {
127164
Allowed int
@@ -374,77 +411,17 @@ func analyzeFirewallLogs(runDir string, verbose bool) (*FirewallAnalysis, error)
374411

375412
// analyzeMultipleFirewallLogs analyzes multiple firewall log files in a directory
376413
func analyzeMultipleFirewallLogs(logsDir string, verbose bool) (*FirewallAnalysis, error) {
377-
files, err := filepath.Glob(filepath.Join(logsDir, "*.log"))
378-
if err != nil {
379-
return nil, fmt.Errorf("failed to find firewall log files: %w", err)
380-
}
381-
382-
if len(files) == 0 {
383-
if verbose {
384-
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("No firewall log files found in %s", logsDir)))
385-
}
386-
return nil, nil
387-
}
388-
389-
if verbose {
390-
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Analyzing %d firewall log files from %s", len(files), logsDir)))
391-
}
392-
393-
// Aggregate analysis from all files
394-
aggregated := &FirewallAnalysis{
395-
AllowedDomains: []string{},
396-
DeniedDomains: []string{},
397-
RequestsByDomain: make(map[string]DomainRequestStats),
398-
}
399-
400-
allAllowedDomains := make(map[string]bool)
401-
allDeniedDomains := make(map[string]bool)
402-
403-
for _, file := range files {
404-
if verbose {
405-
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Parsing %s", filepath.Base(file))))
406-
}
407-
408-
analysis, err := parseFirewallLog(file, verbose)
409-
if err != nil {
410-
if verbose {
411-
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to parse %s: %v", filepath.Base(file), err)))
414+
return aggregateLogFiles(
415+
logsDir,
416+
"*.log",
417+
verbose,
418+
parseFirewallLog,
419+
func() *FirewallAnalysis {
420+
return &FirewallAnalysis{
421+
AllowedDomains: []string{},
422+
DeniedDomains: []string{},
423+
RequestsByDomain: make(map[string]DomainRequestStats),
412424
}
413-
continue
414-
}
415-
416-
// Aggregate metrics
417-
aggregated.TotalRequests += analysis.TotalRequests
418-
aggregated.AllowedRequests += analysis.AllowedRequests
419-
aggregated.DeniedRequests += analysis.DeniedRequests
420-
421-
// Collect unique domains
422-
for _, domain := range analysis.AllowedDomains {
423-
allAllowedDomains[domain] = true
424-
}
425-
for _, domain := range analysis.DeniedDomains {
426-
allDeniedDomains[domain] = true
427-
}
428-
429-
// Merge request stats by domain
430-
for domain, stats := range analysis.RequestsByDomain {
431-
existing := aggregated.RequestsByDomain[domain]
432-
existing.Allowed += stats.Allowed
433-
existing.Denied += stats.Denied
434-
aggregated.RequestsByDomain[domain] = existing
435-
}
436-
}
437-
438-
// Convert sets to sorted slices
439-
for domain := range allAllowedDomains {
440-
aggregated.AllowedDomains = append(aggregated.AllowedDomains, domain)
441-
}
442-
for domain := range allDeniedDomains {
443-
aggregated.DeniedDomains = append(aggregated.DeniedDomains, domain)
444-
}
445-
446-
sort.Strings(aggregated.AllowedDomains)
447-
sort.Strings(aggregated.DeniedDomains)
448-
449-
return aggregated, nil
425+
},
426+
)
450427
}

pkg/cli/log_aggregation.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"sort"
8+
9+
"github.com/githubnext/gh-aw/pkg/console"
10+
)
11+
12+
// LogAnalysis is an interface that both DomainAnalysis and FirewallAnalysis implement
13+
type LogAnalysis interface {
14+
// GetAllowedDomains returns the list of allowed domains
15+
GetAllowedDomains() []string
16+
// GetDeniedDomains returns the list of denied domains
17+
GetDeniedDomains() []string
18+
// SetAllowedDomains sets the list of allowed domains
19+
SetAllowedDomains(domains []string)
20+
// SetDeniedDomains sets the list of denied domains
21+
SetDeniedDomains(domains []string)
22+
// AddMetrics adds metrics from another analysis
23+
AddMetrics(other LogAnalysis)
24+
}
25+
26+
// LogParser is a function type that parses a single log file
27+
type LogParser[T LogAnalysis] func(logPath string, verbose bool) (T, error)
28+
29+
// aggregateLogFiles is a generic helper that aggregates multiple log files
30+
// It handles file discovery, parsing, domain deduplication, and sorting
31+
func aggregateLogFiles[T LogAnalysis](
32+
logsDir string,
33+
globPattern string,
34+
verbose bool,
35+
parser LogParser[T],
36+
newAnalysis func() T,
37+
) (T, error) {
38+
var zero T
39+
40+
// Find log files matching the pattern
41+
files, err := filepath.Glob(filepath.Join(logsDir, globPattern))
42+
if err != nil {
43+
return zero, fmt.Errorf("failed to find log files: %w", err)
44+
}
45+
46+
if len(files) == 0 {
47+
if verbose {
48+
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("No log files found in %s", logsDir)))
49+
}
50+
return zero, nil
51+
}
52+
53+
if verbose {
54+
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Analyzing %d log files from %s", len(files), logsDir)))
55+
}
56+
57+
// Initialize aggregated analysis
58+
aggregated := newAnalysis()
59+
60+
// Track unique domains across all files
61+
allAllowedDomains := make(map[string]bool)
62+
allDeniedDomains := make(map[string]bool)
63+
64+
// Parse each file and aggregate results
65+
for _, file := range files {
66+
if verbose {
67+
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Parsing %s", filepath.Base(file))))
68+
}
69+
70+
analysis, err := parser(file, verbose)
71+
if err != nil {
72+
if verbose {
73+
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to parse %s: %v", filepath.Base(file), err)))
74+
}
75+
continue
76+
}
77+
78+
// Aggregate metrics
79+
aggregated.AddMetrics(analysis)
80+
81+
// Collect unique domains
82+
for _, domain := range analysis.GetAllowedDomains() {
83+
allAllowedDomains[domain] = true
84+
}
85+
for _, domain := range analysis.GetDeniedDomains() {
86+
allDeniedDomains[domain] = true
87+
}
88+
}
89+
90+
// Convert maps to sorted slices
91+
allowedDomains := make([]string, 0, len(allAllowedDomains))
92+
for domain := range allAllowedDomains {
93+
allowedDomains = append(allowedDomains, domain)
94+
}
95+
sort.Strings(allowedDomains)
96+
97+
deniedDomains := make([]string, 0, len(allDeniedDomains))
98+
for domain := range allDeniedDomains {
99+
deniedDomains = append(deniedDomains, domain)
100+
}
101+
sort.Strings(deniedDomains)
102+
103+
// Set the sorted domain lists
104+
aggregated.SetAllowedDomains(allowedDomains)
105+
aggregated.SetDeniedDomains(deniedDomains)
106+
107+
return aggregated, nil
108+
}

0 commit comments

Comments
 (0)