Skip to content

Commit 3dd3172

Browse files
committed
feat: real-time log monitoring
1 parent 792ba5f commit 3dd3172

8 files changed

Lines changed: 1478 additions & 14 deletions

File tree

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,26 @@ A powerful multi-dialect SQL query analysis tool written in Go that provides com
1717

1818
## 📦 Installation
1919

20-
### Prerequisites
20+
### Option 1: Using Nix (Recommended) 🎯
2121

22-
- Go 1.21 or higher
22+
With [Nix flakes](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html), you get a reproducible development environment:
2323

24-
### Build from Source
24+
```bash
25+
# Try it without installing
26+
nix run github:Chahine-tech/sql-parser-go -- --help
27+
28+
# Or enter development environment
29+
nix develop
30+
31+
# Or install globally
32+
nix profile install github:Chahine-tech/sql-parser-go
33+
```
34+
35+
See [NIX.md](NIX.md) for detailed Nix setup and usage.
36+
37+
### Option 2: Build from Source
38+
39+
**Prerequisites:** Go 1.21 or higher
2540

2641
```bash
2742
# Clone the repository

cmd/sqlparser/main.go

Lines changed: 157 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/Chahine-tech/sql-parser-go/pkg/analyzer"
1515
"github.com/Chahine-tech/sql-parser-go/pkg/dialect"
1616
"github.com/Chahine-tech/sql-parser-go/pkg/logger"
17+
"github.com/Chahine-tech/sql-parser-go/pkg/monitor"
1718
"github.com/Chahine-tech/sql-parser-go/pkg/parser"
1819
)
1920

@@ -31,14 +32,17 @@ const banner = `
3132

3233
func main() {
3334
var (
34-
queryFile = flag.String("query", "", "File containing the SQL query")
35-
queryText = flag.String("sql", "", "SQL query string")
36-
logFile = flag.String("log", "", "SQL Server log file")
37-
outputFormat = flag.String("output", "json", "Output format (json, table)")
38-
verbose = flag.Bool("verbose", false, "Verbose mode")
39-
configFile = flag.String("config", "", "Configuration file path")
40-
dialectFlag = flag.String("dialect", "", "SQL dialect (mysql, postgresql, sqlserver, sqlite, oracle)")
41-
showHelp = flag.Bool("help", false, "Show help")
35+
queryFile = flag.String("query", "", "File containing the SQL query")
36+
queryText = flag.String("sql", "", "SQL query string")
37+
logFile = flag.String("log", "", "SQL Server log file")
38+
outputFormat = flag.String("output", "json", "Output format (json, table)")
39+
verbose = flag.Bool("verbose", false, "Verbose mode")
40+
configFile = flag.String("config", "", "Configuration file path")
41+
dialectFlag = flag.String("dialect", "", "SQL dialect (mysql, postgresql, sqlserver, sqlite, oracle)")
42+
showHelp = flag.Bool("help", false, "Show help")
43+
watchMode = flag.Bool("watch", false, "Watch log file for real-time monitoring")
44+
tailLines = flag.Int("tail", 10, "Number of lines to tail when starting watch mode")
45+
slowThreshold = flag.Float64("slow", 1.0, "Slow query threshold in seconds")
4246
)
4347
flag.Parse()
4448

@@ -74,9 +78,16 @@ func main() {
7478
os.Exit(1)
7579
}
7680
} else if *logFile != "" {
77-
if err := parseLogFile(*logFile, cfg, *verbose); err != nil {
78-
fmt.Printf("Error parsing log file: %v\n", err)
79-
os.Exit(1)
81+
if *watchMode {
82+
if err := watchLogFile(*logFile, cfg, *verbose, *tailLines, *slowThreshold); err != nil {
83+
fmt.Printf("Error watching log file: %v\n", err)
84+
os.Exit(1)
85+
}
86+
} else {
87+
if err := parseLogFile(*logFile, cfg, *verbose); err != nil {
88+
fmt.Printf("Error parsing log file: %v\n", err)
89+
os.Exit(1)
90+
}
8091
}
8192
} else {
8293
showUsage()
@@ -91,18 +102,23 @@ func showUsage() {
91102
fmt.Println(" sqlparser -query file.sql Analyze SQL query from file")
92103
fmt.Println(" sqlparser -sql \"SELECT * FROM...\" Analyze SQL query from string")
93104
fmt.Println(" sqlparser -log logfile.log Parse SQL Server log file")
105+
fmt.Println(" sqlparser -log logfile.log -watch Watch log file in real-time")
94106
fmt.Println()
95107
fmt.Println("Options:")
96108
fmt.Println(" -output FORMAT Output format: json, table (default: json)")
97109
fmt.Println(" -dialect DIALECT SQL dialect: mysql, postgresql, sqlserver, sqlite, oracle (default: sqlserver)")
98110
fmt.Println(" -verbose Enable verbose output")
99111
fmt.Println(" -config FILE Configuration file path")
112+
fmt.Println(" -watch Enable real-time log monitoring (use with -log)")
113+
fmt.Println(" -tail N Number of lines to tail when starting watch (default: 10)")
114+
fmt.Println(" -slow SECONDS Slow query threshold in seconds (default: 1.0)")
100115
fmt.Println(" -help Show this help")
101116
fmt.Println()
102117
fmt.Println("Examples:")
103118
fmt.Println(" sqlparser -query complex_query.sql -output json -dialect mysql")
104119
fmt.Println(" sqlparser -sql \"SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id\" -dialect postgresql")
105120
fmt.Println(" sqlparser -log sqlserver.log -output table -verbose")
121+
fmt.Println(" sqlparser -log sqlserver.log -watch -tail 20 -slow 2.0 -dialect mysql")
106122
}
107123

108124
func analyzeQueryFile(filename string, cfg *config.Config, verbose bool) error {
@@ -182,6 +198,136 @@ func analyzeQueryString(sql string, cfg *config.Config, verbose bool) error {
182198
return outputAnalysis(analysis, suggestions, cfg)
183199
}
184200

201+
func watchLogFile(filename string, cfg *config.Config, verbose bool, tailLines int, slowThreshold float64) error {
202+
if verbose {
203+
fmt.Printf("🔍 Starting real-time log monitoring: %s\n", filename)
204+
fmt.Printf("Dialect: %s\n", cfg.Parser.Dialect)
205+
fmt.Printf("Slow query threshold: %.2fs\n", slowThreshold)
206+
fmt.Printf("Tailing last %d lines...\n\n", tailLines)
207+
}
208+
209+
// Create context for graceful shutdown
210+
ctx, cancel := context.WithCancel(context.Background())
211+
defer cancel()
212+
213+
// Create watcher
214+
watcher := monitor.NewLogWatcher(filename)
215+
lines := make(chan string, 100) // Buffered channel for log lines
216+
217+
// Start watching (with tail)
218+
var err error
219+
if tailLines > 0 {
220+
err = watcher.StartWithTail(ctx, lines, tailLines)
221+
} else {
222+
err = watcher.Start(ctx, lines)
223+
}
224+
if err != nil {
225+
return fmt.Errorf("failed to start watcher: %w", err)
226+
}
227+
228+
// Create alert manager
229+
alertMgr := monitor.NewAlertManager()
230+
231+
// Add alert rules
232+
alertMgr.AddRule(&monitor.SlowQueryRule{Threshold: slowThreshold})
233+
alertMgr.AddRule(&monitor.ParseErrorRule{})
234+
alertMgr.AddRule(&monitor.OptimizationRule{MinSeverity: "medium"})
235+
alertMgr.AddRule(&monitor.FullTableScanRule{})
236+
237+
// Add console alert handler
238+
alertMgr.AddHandler(monitor.ConsoleAlertHandler)
239+
240+
// Create processor
241+
processor := monitor.NewLogProcessor(cfg.Parser.Dialect)
242+
processor.SetQueryHandler(func(pq *monitor.ProcessedQuery) {
243+
// Check alerts first
244+
alertMgr.Check(pq)
245+
246+
// Print query information
247+
fmt.Printf("[%s] Duration: %.3fs | Database: %s | User: %s\n",
248+
pq.Timestamp.Format("15:04:05"),
249+
pq.Duration,
250+
pq.Database,
251+
pq.User)
252+
253+
if len(pq.Query) > 100 {
254+
fmt.Printf(" Query: %s...\n", pq.Query[:97])
255+
} else {
256+
fmt.Printf(" Query: %s\n", pq.Query)
257+
}
258+
259+
// Show analysis if available
260+
if pq.Analysis != nil {
261+
if len(pq.Analysis.Tables) > 0 {
262+
tables := make([]string, len(pq.Analysis.Tables))
263+
for i, t := range pq.Analysis.Tables {
264+
tables[i] = t.Name
265+
}
266+
fmt.Printf(" Tables: %s\n", strings.Join(tables, ", "))
267+
}
268+
269+
// Show optimizations if any
270+
if len(pq.Analysis.EnhancedSuggestions) > 0 {
271+
fmt.Printf(" ⚠️ %d optimization suggestions\n", len(pq.Analysis.EnhancedSuggestions))
272+
for _, opt := range pq.Analysis.EnhancedSuggestions {
273+
fmt.Printf(" - [%s] %s\n", opt.Severity, opt.Description)
274+
}
275+
}
276+
}
277+
278+
fmt.Println()
279+
})
280+
281+
// Start processor
282+
go processor.Start(ctx, lines)
283+
284+
// Print statistics periodically
285+
ticker := time.NewTicker(30 * time.Second)
286+
defer ticker.Stop()
287+
288+
// Handle Ctrl+C
289+
sigChan := make(chan os.Signal, 1)
290+
291+
fmt.Println("📊 Real-time monitoring started. Press Ctrl+C to stop.")
292+
fmt.Println(strings.Repeat("=", 80))
293+
fmt.Println()
294+
295+
// Main loop
296+
for {
297+
select {
298+
case <-ticker.C:
299+
// Print statistics
300+
stats := processor.GetStatistics().GetSnapshot()
301+
fmt.Println()
302+
fmt.Println(strings.Repeat("=", 80))
303+
fmt.Println(stats.String())
304+
305+
// Print alert counts
306+
alertCounts := alertMgr.GetAlertCounts()
307+
if len(alertCounts) > 0 {
308+
fmt.Println("\nAlerts:")
309+
for level, count := range alertCounts {
310+
fmt.Printf(" %s: %d\n", level.String(), count)
311+
}
312+
}
313+
fmt.Println(strings.Repeat("=", 80))
314+
fmt.Println()
315+
316+
case <-sigChan:
317+
fmt.Println("\n\nStopping monitoring...")
318+
cancel()
319+
watcher.Stop()
320+
321+
// Print final statistics
322+
stats := processor.GetStatistics().GetSnapshot()
323+
fmt.Println()
324+
fmt.Println("Final Statistics:")
325+
fmt.Println(stats.String())
326+
return nil
327+
}
328+
}
329+
}
330+
185331
func parseLogFile(filename string, cfg *config.Config, verbose bool) error {
186332
if verbose {
187333
fmt.Printf("Parsing log file: %s\n", filename)

examples/logs/sample_queries.log

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2024-01-15T10:30:45.123Z Query SELECT * FROM users WHERE id = 1
2+
2024-01-15T10:30:46.234Z Query SELECT u.name, u.email, o.total FROM users u JOIN orders o ON u.id = o.user_id WHERE o.status = 'completed'
3+
2024-01-15T10:30:47.456Z Query INSERT INTO audit_log (user_id, action, timestamp) VALUES (123, 'login', NOW())
4+
2024-01-15T10:30:48.789Z Query UPDATE users SET last_login = NOW() WHERE id = 123
5+
2024-01-15T10:30:50.123Z Query SELECT COUNT(*) FROM orders WHERE created_at > '2024-01-01'
6+
2024-01-15T10:30:51.456Z Query DELETE FROM sessions WHERE expires_at < NOW()
7+
2024-01-15T10:30:52.789Z Query SELECT * FROM products
8+
2024-01-15T10:30:54.123Z Query UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 456
9+
2024-01-15T10:30:55.456Z Query SELECT p.name, p.price, c.name as category FROM products p LEFT JOIN categories c ON p.category_id = c.id ORDER BY p.created_at DESC LIMIT 10
10+
2024-01-15T10:30:57.789Z Query CREATE INDEX idx_users_email ON users(email)
11+
2024-01-15T10:30:59.123Z Query WITH recent_orders AS (SELECT * FROM orders WHERE created_at > CURRENT_DATE - INTERVAL '30 days') SELECT COUNT(*) FROM recent_orders
12+
2024-01-15T10:31:01.456Z Query MERGE INTO inventory t USING order_items s ON t.product_id = s.product_id WHEN MATCHED THEN UPDATE SET quantity = t.quantity - s.quantity
13+
2024-01-15T10:31:03.789Z Query SELECT * FROM logs WHERE level = 'ERROR' AND timestamp > NOW() - INTERVAL '1 hour'

0 commit comments

Comments
 (0)