A Java command-line application that parses heterogeneous log files containing APM metrics, application events, and HTTP request logs, classifies each line, and writes per-type aggregations as JSON files.
The design uses Chain of Responsibility for log classification and Strategy for type-specific aggregation, so adding a new log type is a matter of dropping in three small classes — no modifications to existing code.
- Features
- Requirements
- Build
- Usage
- Log Types
- Output Specification
- Project Structure
- Design
- Extending with a New Log Type
- Testing
- Troubleshooting
- Classifies and parses three log types from a single mixed file
- Computes statistical aggregations:
- APM: min, median, average, max per metric
- Request: min, max, p50/p90/p95/p99 response times + 2XX/4XX/5XX counts per endpoint
- Application: counts by severity level
- Silently skips corrupted or unrecognized lines
- Always writes all three output files, even when a type has zero entries (empty
{}) - Configurable output directory via
--output - 42 unit tests covering parsing, aggregation, JSON writing, and end-to-end runs
- JDK 17+
- Maven 3.6+
java -version
mvn -version# Compile + run tests + produce executable JAR
mvn clean packageThis produces target/log-analyzer.jar (a shaded JAR with all dependencies bundled).
To skip tests:
mvn package -DskipTestsjava -jar target/log-analyzer.jar --file <input.txt> [--output <dir>]| Flag | Required | Default | Description |
|---|---|---|---|
--file |
yes | — | Path to the input log file (.txt) |
--output |
no | output |
Directory where the three JSON files are written (created if missing) |
# Default output directory: ./output/
java -jar target/log-analyzer.jar --file sample.txt
# Custom output directory
java -jar target/log-analyzer.jar --file sample.txt --output resultsAPM logs: 7 -> output/apm.json
Request logs: 7 -> output/request.json
Application logs: 7 -> output/application.json
The directory will contain:
apm.jsonrequest.jsonapplication.json
All log lines follow a key=value format separated by spaces. Quoted values support spaces:
key1=value1 key2="value with spaces" key3=value3
Detection: line contains both metric= AND value=
timestamp=2024-02-24T16:22:15Z metric=cpu_usage_percent host=webserver1 value=72
Required keys: metric, value (numeric)
Detection: line contains both request_method= AND response_status=
timestamp=2024-02-24T16:22:25Z request_method=POST request_url="/api/update" response_status=202 response_time_ms=200 host=webserver1
Required keys: request_method, request_url, response_status (int), response_time_ms (numeric)
Detection: line contains level= and is NOT an APM or Request line
timestamp=2024-02-24T16:22:20Z level=INFO message="Scheduled maintenance starting" host=webserver1
Required keys: level (normalized to uppercase)
Lines that don't match any handler, or that match a handler but fail field validation/parsing, are silently ignored.
{
"cpu_usage_percent": {
"minimum": 65.0,
"median": 68.5,
"average": 68.5,
"max": 72.0
}
}{
"/api/status": {
"response_times": {
"min": 100.0,
"max": 300.0,
"50_percentile": 165.0,
"90_percentile": 264.0,
"95_percentile": 282.0,
"99_percentile": 296.4
},
"status_codes": {
"2XX": 3,
"4XX": 0,
"5XX": 1
}
}
}Percentiles use linear interpolation: index = (p / 100) * (n - 1).
{
"DEBUG": 1,
"ERROR": 2,
"INFO": 3,
"WARNING": 1
}If no entries of a given type are found, the file is still written as { }.
cmd-log-parser/
├── pom.xml
├── README.md
├── .gitignore
├── sample.txt # 21-line sample mixed log
│
├── src/main/java/com/loganalyzer/
│ ├── LogAnalyzer.java # main entry point, CLI parsing
│ ├── model/
│ │ ├── LogEntry.java # abstract base
│ │ ├── APMLogEntry.java
│ │ ├── RequestLogEntry.java
│ │ └── ApplicationLogEntry.java
│ ├── handler/ # Chain of Responsibility
│ │ ├── LogHandler.java # abstract handler + key/value parser
│ │ ├── APMLogHandler.java
│ │ ├── RequestLogHandler.java
│ │ └── ApplicationLogHandler.java
│ ├── strategy/ # Strategy pattern
│ │ ├── AggregationStrategy.java # interface
│ │ ├── APMAggregationStrategy.java
│ │ ├── RequestAggregationStrategy.java
│ │ └── ApplicationAggregationStrategy.java
│ └── util/
│ └── JSONWriter.java # Jackson pretty-print writer
│
└── src/test/java/com/loganalyzer/
├── HandlerTest.java # 12 tests
├── AggregationTest.java # 4 tests
├── LogAnalyzerTest.java # 10 tests
├── ModelTest.java # 10 tests
└── JSONWriterTest.java # 6 tests
line → APMLogHandler → RequestLogHandler → ApplicationLogHandler → (drop)
Each handler decides at runtime whether it can handle a given line based on which keys are present. Lines that no handler accepts simply fall off the end of the chain.
Why:
- Log type isn't tagged on the line — it must be inferred from content
- Each handler has a single responsibility
- New types are inserted as new links — existing handlers untouched
- Invalid lines are handled by the same fall-through mechanism as unsupported types
interface AggregationStrategy<T extends LogEntry> {
Map<String, Object> aggregate(List<T> entries);
}Each log type has a fundamentally different aggregation algorithm (statistics vs. percentiles vs. counts). The strategy interface keeps that logic isolated and independently testable.
| Pros | Cons |
|---|---|
| Open/Closed: add types without touching existing code | More classes than a monolithic implementation |
| Each component testable in isolation | Slight overhead for chain traversal |
| Type-safe aggregation via generics | Requires familiarity with the patterns |
Suppose you want to add Security logs (threat_level=...):
- Model —
model/SecurityLogEntry.javaextendingLogEntry - Handler —
handler/SecurityLogHandler.javaextendingLogHandler, withcanHandle()returningline.contains("threat_level=")and aparse()method - Strategy —
strategy/SecurityAggregationStrategy.javaimplementingAggregationStrategy<SecurityLogEntry> - Wire it up in
LogAnalyzer.run()— append the handler to the chain and write its output:
appHandler.setNext(securityHandler);
// ...
writer.writeToFile(
new SecurityAggregationStrategy().aggregate(toTyped(securityHandler.getEntries())),
outputDir.resolve("security.json").toString());Total: 3 new files + ~3 lines in LogAnalyzer.
# Run all tests
mvn test
# Run a single class
mvn test -Dtest=HandlerTest
# Run a single method
mvn test -Dtest=HandlerTest#chainRoutesEachLineToCorrectHandler| Test class | Tests | Focus |
|---|---|---|
HandlerTest |
12 | Parsing, chain routing, edge cases, malformed input |
AggregationTest |
4 | Each strategy's calculations, empty inputs |
LogAnalyzerTest |
10 | CLI arg parsing, end-to-end run, output dir handling |
ModelTest |
10 | Constructors, getters, getType(), toString() |
JSONWriterTest |
6 | File writing, null/empty maps, nested data, overwrites |
| Total | 42 |
| Symptom | Cause / Fix |
|---|---|
Usage: java -jar log-analyzer.jar --file ... |
--file flag missing |
Error: file not found |
Check the path passed to --file |
All output JSON files are { } |
Input file has no lines matching any handler — check formatting |
mvn not found |
Install Maven 3.6+ |
Tests fail with ClassNotFoundException |
Run mvn clean test to rebuild |