š Project: Log Analyzer
š Lesson 12: Logging & Debugging
ā± 25-30 min
š Intermediate
šÆ Objective
Build a tool that reads, parses, filters, and analyzes log files. Generate statistics, filter by severity or date range, and export filtered results.
Requirements
- Parse log entries with timestamp, level, logger name, and message
- Filter logs by severity level (INFO, WARNING, SEVERE, etc.)
- Filter by date range (start and end dates)
- Search logs by keyword
- Generate statistics (counts by level, most active loggers)
- Export filtered results to a new file
Expected Output
Terminal
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā LOG ANALYZER ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š Reading log file: app.log
ā
Found 150 log entries
š Log Level Distribution:
āāāāāāāāāāāāāāāāāāāāāāāāāā
INFO : āāāāāāāāāāāāāāāā 85 (56.7%)
WARNING : āāāāāāāā 42 (28.0%)
SEVERE : āāā 15 (10.0%)
CONFIG : ā 5 (3.3%)
FINE : ā 3 (2.0%)
š Most Active Loggers:
1. http_client : 45 entries
2. auth_service : 32 entries
3. data_parser : 28 entries
ā ļø Recent Warnings (last 5):
[10:30] auth_service: Failed login attempt
[10:28] http_client: Slow response detected
...
š Exporting filtered results to: warnings_only.log
ā
Exported 42 entries
import 'dart:io';
import 'dart:math';
// Step 1: Log entry model
enum LogLevel {
finest(0), finer(1), fine(2), config(3), info(4), warning(5), severe(6), shout(7);
final int severity;
const LogLevel(this.severity);
static LogLevel? parse(String name) {
try {
return LogLevel.values.firstWhere((l) => l.name == name.toLowerCase());
} catch (e) {
return null;
}
}
String get displayName => name.toUpperCase();
}
class LogEntry {
final DateTime timestamp;
final LogLevel level;
final String loggerName;
final String message;
LogEntry({
required this.timestamp,
required this.level,
required this.loggerName,
required this.message,
});
factory LogEntry.parse(String line) {
// Format: [2026-05-25 10:30:45.123] INFO: logger_name: Message here
final regex = RegExp(r'\[(.*?)\]\s+(\w+):\s+(\w+):\s+(.*)');
final match = regex.firstMatch(line);
if (match == null) {
throw FormatException('Invalid log format: $line');
}
return LogEntry(
timestamp: DateTime.parse(match.group(1)!),
level: LogLevel.parse(match.group(2)!) ?? LogLevel.info,
loggerName: match.group(3)!,
message: match.group(4)!,
);
}
@override
String toString() => '[${timestamp.toIso8601String()}] ${level.displayName}: $loggerName: $message';
}
// Step 2: Filter criteria
class LogFilter {
LogLevel? minLevel;
DateTime? startDate;
DateTime? endDate;
String? keyword;
String? loggerName;
bool matches(LogEntry entry) {
if (minLevel != null && entry.level.severity < minLevel!.severity) return false;
if (startDate != null && entry.timestamp.isBefore(startDate!)) return false;
if (endDate != null && entry.timestamp.isAfter(endDate!)) return false;
if (keyword != null && !entry.message.toLowerCase().contains(keyword!.toLowerCase())) return false;
if (loggerName != null && entry.loggerName != loggerName) return false;
return true;
}
}
// Step 3: Statistics
class LogStatistics {
final Map levelCounts = {};
final Map loggerCounts = {};
int totalEntries = 0;
DateTime? firstEntry;
DateTime? lastEntry;
void addEntry(LogEntry entry) {
totalEntries++;
levelCounts[entry.level] = (levelCounts[entry.level] ?? 0) + 1;
loggerCounts[entry.loggerName] = (loggerCounts[entry.loggerName] ?? 0) + 1;
if (firstEntry == null || entry.timestamp.isBefore(firstEntry!)) {
firstEntry = entry.timestamp;
}
if (lastEntry == null || entry.timestamp.isAfter(lastEntry!)) {
lastEntry = entry.timestamp;
}
}
void display() {
print('\nš Log Level Distribution:');
print('ā' * 30);
final maxCount = levelCounts.values.fold(0, (a, b) => a > b ? a : b);
final sortedLevels = levelCounts.entries.toList()
..sort((a, b) => b.key.severity.compareTo(a.key.severity));
for (var entry in sortedLevels) {
final barLength = maxCount > 0 ? (entry.value / maxCount * 20).round() : 0;
final bar = 'ā' * barLength;
final percentage = (entry.value / totalEntries * 100).toStringAsFixed(1);
print('${entry.key.displayName.padRight(8)} : $bar ${entry.value} ($percentage%)');
}
print('\nš Most Active Loggers:');
final sortedLoggers = loggerCounts.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
for (int i = 0; i < (sortedLoggers.length < 5 ? sortedLoggers.length : 5); i++) {
print(' ${i + 1}. ${sortedLoggers[i].key.padRight(15)} : ${sortedLoggers[i].value} entries');
}
if (firstEntry != null && lastEntry != null) {
final duration = lastEntry!.difference(firstEntry!);
print('\nš
Time Range: ${firstEntry!.toIso8601String()} to ${lastEntry!.toIso8601String()}');
print('ā±ļø Duration: ${duration.inMinutes} minutes');
}
}
}
// Step 4: Log Analyzer
class LogAnalyzer {
final List _entries = [];
void loadFromString(String content) {
_entries.clear();
final lines = content.split('\n');
for (var line in lines) {
if (line.trim().isEmpty) continue;
try {
_entries.add(LogEntry.parse(line));
} catch (e) {
// Skip malformed lines
}
}
}
void loadFromFile(String filePath) {
final file = File(filePath);
if (!file.existsSync()) {
throw FileSystemException('File not found', filePath);
}
loadFromString(file.readAsStringSync());
}
List filter(LogFilter filter) {
return _entries.where((entry) => filter.matches(entry)).toList();
}
LogStatistics getStatistics([List? entries]) {
final data = entries ?? _entries;
final stats = LogStatistics();
for (var entry in data) {
stats.addEntry(entry);
}
return stats;
}
void exportToFile(List entries, String filePath) {
final file = File(filePath);
final buffer = StringBuffer();
for (var entry in entries) {
buffer.writeln(entry.toString());
}
file.writeAsStringSync(buffer.toString());
print('š Exported ${entries.length} entries to: $filePath');
}
void displayRecent(List entries, int count, LogLevel? minLevel) {
var filtered = entries;
if (minLevel != null) {
filtered = entries.where((e) => e.level.severity >= minLevel.severity).toList();
}
filtered.sort((a, b) => b.timestamp.compareTo(a.timestamp));
final display = filtered.take(count).toList();
for (var entry in display) {
final time = '${entry.timestamp.hour.toString().padLeft(2, '0')}:${entry.timestamp.minute.toString().padLeft(2, '0')}';
print(' [$time] ${entry.loggerName}: ${entry.message}');
}
}
}
// Step 5: Generate sample logs
String generateSampleLogs() {
final random = Random();
final buffer = StringBuffer();
final loggers = ['http_client', 'auth_service', 'data_parser', 'api_handler', 'db_connector'];
final levels = ['INFO', 'INFO', 'INFO', 'WARNING', 'WARNING', 'SEVERE', 'CONFIG', 'FINE'];
final messages = {
'http_client': ['Request completed', 'Slow response detected', 'Connection timeout', 'Retry attempt'],
'auth_service': ['User logged in', 'Failed login attempt', 'Token refreshed', 'Session expired'],
'data_parser': ['JSON parsed successfully', 'Invalid format detected', 'Large dataset processed'],
'api_handler': ['API call started', 'Rate limit approaching', 'Response cached'],
'db_connector': ['Query executed', 'Connection pool exhausted', 'Slow query detected'],
};
DateTime time = DateTime(2026, 5, 25, 8, 0, 0);
for (int i = 0; i < 150; i++) {
time = time.add(Duration(seconds: 30 + random.nextInt(120)));
final logger = loggers[random.nextInt(loggers.length)];
final level = levels[random.nextInt(levels.length)];
final loggerMessages = messages[logger]!;
final message = loggerMessages[random.nextInt(loggerMessages.length)];
buffer.writeln('[${time.toIso8601String().replaceAll('T', ' ').substring(0, 23)}] $level: $logger: $message');
}
return buffer.toString();
}
// Step 6: Main program
void main() {
print('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
print('ā LOG ANALYZER ā');
print('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
// Generate sample logs
final sampleLogs = generateSampleLogs();
File('sample_app.log').writeAsStringSync(sampleLogs);
final analyzer = LogAnalyzer();
// Load logs
print('š Reading log file: sample_app.log');
analyzer.loadFromFile('sample_app.log');
print('ā
Found ${analyzer._entries.length} log entries');
// Full statistics
final stats = analyzer.getStatistics();
stats.display();
// Filter warnings and above
print('\nā ļø Filtering WARNING and above...');
final warningFilter = LogFilter()..minLevel = LogLevel.warning;
final warnings = analyzer.filter(warningFilter);
print('Found ${warnings.length} entries at WARNING or above');
print('\nā ļø Recent Warnings (last 5):');
analyzer.displayRecent(warnings, 5, LogLevel.warning);
// Filter by keyword
print('\nš Searching for "error"...');
final errorFilter = LogFilter()..keyword = 'error';
final errors = analyzer.filter(errorFilter);
print('Found ${errors.length} entries containing "error"');
// Filter by date range
print('\nš
Filtering last hour...');
final dateFilter = LogFilter()
..startDate = DateTime(2026, 5, 25, 10, 0, 0)
..endDate = DateTime(2026, 5, 25, 11, 0, 0);
final recentEntries = analyzer.filter(dateFilter);
print('Found ${recentEntries.length} entries in the last hour');
// Export warnings
print('\nš Exporting warnings to file...');
analyzer.exportToFile(warnings, 'warnings_only.log');
// Cleanup
File('sample_app.log').deleteSync();
print('\nā
Analysis complete!');
}