Level 2: Concurrent Log Analyzer (Concurrency & Context)¶
Welcome to the second level! Here, you'll tackle one of Go's greatest strengths: concurrency.
In Python, achieving true multi-core concurrency is difficult due to the Global Interpreter Lock (GIL), requiring complex multiprocessing setups. In Go, we use lightweight Goroutines (green threads managed by the Go runtime) and Channels (typed pipes to communicate between goroutines safely).
Feature Requirements¶
Your goal is to build a log analyzer that:
1. Receives a slice of file paths to log files.
2. Reads and parses all files concurrently.
3. Counts the occurrences of log levels: INFO, WARNING, and ERROR.
4. Accepts a context.Context to handle timeouts. If processing takes too long, it must exit gracefully and return a timeout error.
5. Aggregates and returns a single unified LogResult count.
Concurrency Concept Cheat Sheet¶
- Goroutines: Prefix a function call with the
gokeyword. It runs asynchronously in the background. - Channels: Channels are used to pass data between goroutines safely.
- WaitGroup: Used to wait for a collection of goroutines to finish.
- Context: Used for deadlines, cancellations, and metadata propagation.
Getting Started¶
- Look at analyzer.go to see the stubs and comments.
- Look at analyzer_test.go for the test cases.
- Run the tests (they will fail initially):
- Implement the concurrent parser in analyzer.go.
Complete Implementation (analyzer.go)¶
package loganalyzer
import (
"bufio"
"context"
"os"
"strings"
"sync"
)
// LogResult holds the count of log messages grouped by level.
type LogResult struct {
InfoCount int
WarningCount int
ErrorCount int
}
func parseFile(file string )( LogResult, error) {
// Simulate parsing
fileOpen , err := os.Open(file)
if err!=nil{
return LogResult{}, err
}
defer fileOpen.Close()
result := LogResult{}
scanner := bufio.NewScanner(fileOpen)
for scanner.Scan(){
line:= strings.ToLower(scanner.Text())
if strings.Contains(line,"info"){
result.InfoCount++
} else if strings.Contains(line,"warning"){
result.WarningCount++
} else if strings.Contains(line,"error"){
result.ErrorCount++
}
}
if err :=scanner.Err(); err != nil{
return LogResult{} , err
}
return result, nil
}
// AnalyzeLogs reads and parses the provided files concurrently.
// It counts lines containing "INFO", "WARNING", and "ERROR".
// If the ctx is cancelled or times out, the function should return immediately with ctx.Err().
func AnalyzeLogs(ctx context.Context, filePaths []string) (LogResult, error) {
// TODO: Implement concurrent log analysis.
//
files := filePaths
resultsChan := make(chan LogResult , len(filePaths))
var wg sync.WaitGroup
for _,file :=range files{
wg.Add(1)
go func(file string){
defer wg.Done()
res ,err := parseFile(file)
if err != nil{
resultsChan <- LogResult{}
}else{
resultsChan <- res
}
}(file)
}
go func(){
wg.Wait()
close(resultsChan)
}()
total := LogResult{}
// 3. Loop for each file and use select to handle context timeouts
for i := 0; i < len(filePaths); i++ {
select {
case res, ok := <-resultsChan:
if !ok {
break
}
// 4. Aggregate the counts
total.InfoCount += res.InfoCount
total.WarningCount += res.WarningCount
total.ErrorCount += res.ErrorCount
case <-ctx.Done():
// Exit immediately if the context is cancelled or timed out
return LogResult{}, ctx.Err()
}
}
return total, nil
// Recommended Plan:
// 1. Create a channel to receive results (or errors) from each file parsing goroutine.
// 2. Spawn a goroutine for each file in filePaths.
// 3. Inside each goroutine:
// - Open the file.
// - Read line-by-line (hint: use bufio.NewScanner).
// - For each line, check if it contains "INFO", "WARNING", or "ERROR" (hint: strings.Contains).
// - Crucially, check if the context is cancelled periodically or check ctx.Done() during work.
// - Send the file's LogResult back on the channel.
// 4. In the main routine, collect results from the channel.
// - Use a select statement to either read a result from the channel or handle context cancellation:
// select {
// case res := <-resultsChan:
// // Add to running totals
// case <-ctx.Done():
// return LogResult{}, ctx.Err()
// }
// 5. Aggregate and return the total LogResult.
}