Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/main/java/com/stacklens/JSONlogs/JsonLogLine.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.stacklens.JSONlogs;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonIgnoreProperties(ignoreUnknown = true)
public class JsonLogLine {
private String timestamp;
private String level;
private String message;

@JsonProperty("stack_trace")
private String stackTrace;

// Getters and Setters
public String getTimestamp() { return timestamp; }
public void setTimestamp(String timestamp) { this.timestamp = timestamp; }

public String getLevel() { return level; }
public void setLevel(String level) { this.level = level; }

public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }

public String getStackTrace() { return stackTrace; }
public void setStackTrace(String stackTrace) { this.stackTrace = stackTrace; }
}
39 changes: 39 additions & 0 deletions src/main/java/com/stacklens/JSONlogs/LogProcessor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.stacklens.JSONlogs;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;

public class LogProcessor {

private final ObjectMapper objectMapper = new ObjectMapper();

public boolean isJsonLog(String logLine) {
if (logLine == null) return false;
String trimmed = logLine.trim();
return trimmed.startsWith("{") && trimmed.endsWith("}");
}

public String parseToPlainText(String logLine) {
try {
JsonLogStructure jsonLog = objectMapper.readValue(logLine, JsonLogStructure.class);
StringBuilder plainText = new StringBuilder();

if (jsonLog.message() != null) {
plainText.append(jsonLog.message());
}
if (jsonLog.stackTrace() != null && !jsonLog.stackTrace().isEmpty()) {
plainText.append("\n").append(jsonLog.stackTrace());
}
return plainText.toString();
} catch (Exception e) {
return logLine;
}
}

@JsonIgnoreProperties(ignoreUnknown = true)
record JsonLogStructure(
@JsonProperty("message") String message,
@JsonProperty("stack_trace") String stackTrace
) {}
Comment on lines +19 to +38

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve structured fields during normalization to avoid lossy classifier input.

parseToPlainText currently drops timestamp and level, and may return an empty string for valid JSON logs that lack message. Since preprocessing is now always applied before classification, this silently removes useful signal from the pipeline.

Suggested fix
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@
     public String parseToPlainText(String logLine) {
         try {
-            JsonLogStructure jsonLog = objectMapper.readValue(logLine, JsonLogStructure.class);
+            JsonLogLine jsonLog = objectMapper.readValue(logLine, JsonLogLine.class);
             StringBuilder plainText = new StringBuilder();
-            
-            if (jsonLog.message() != null) {
-                plainText.append(jsonLog.message());
+
+            if (jsonLog.getTimestamp() != null && !jsonLog.getTimestamp().isBlank()) {
+                plainText.append(jsonLog.getTimestamp());
             }
-            if (jsonLog.stackTrace() != null && !jsonLog.stackTrace().isEmpty()) {
-                plainText.append("\n").append(jsonLog.stackTrace());
+            if (jsonLog.getLevel() != null && !jsonLog.getLevel().isBlank()) {
+                if (plainText.length() > 0) plainText.append(" ");
+                plainText.append(jsonLog.getLevel());
             }
-            return plainText.toString();
+            if (jsonLog.getMessage() != null && !jsonLog.getMessage().isBlank()) {
+                if (plainText.length() > 0) plainText.append(" ");
+                plainText.append(jsonLog.getMessage());
+            }
+            if (jsonLog.getStackTrace() != null && !jsonLog.getStackTrace().isBlank()) {
+                if (plainText.length() > 0) plainText.append("\n");
+                plainText.append(jsonLog.getStackTrace());
+            }
+            return plainText.length() > 0 ? plainText.toString() : logLine;
         } catch (Exception e) {
             return logLine;
         }
     }
-
-    `@JsonIgnoreProperties`(ignoreUnknown = true)
-    record JsonLogStructure(
-        `@JsonProperty`("message") String message,
-        `@JsonProperty`("stack_trace") String stackTrace
-    ) {}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/stacklens/JSONlogs/LogProcessor.java` around lines 19 - 38,
parseToPlainText drops structured fields and can produce an empty string for
JSON logs without a "message"; update the JsonLogStructure record to include
`@JsonProperty`("timestamp") String timestamp and `@JsonProperty`("level") String
level, then change parseToPlainText to incorporate timestamp and level into the
output (e.g. prefix or include them when building plainText alongside message
and stackTrace) so meaningful fields are preserved for the classifier; ensure
the method returns logLine only as a last-resort fallback (i.e., if all
extracted fields are null/empty) and not when timestamp/level exist.

}
35 changes: 23 additions & 12 deletions src/main/java/com/stacklens/analyzer/LogAnalyzer.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.stacklens.analyzer;

import com.stacklens.classifier.IssueClassifier;
import com.stacklens.JSONlogs.LogProcessor;
import com.stacklens.model.AnalysisResult;
import com.stacklens.model.Issue;
import com.stacklens.classifier.IssueClassifier;

import java.io.BufferedReader;
import java.io.IOException;
Expand All @@ -15,38 +15,49 @@
import java.util.List;
import java.util.stream.Collectors;

/**
* Reads log content (from file, stdin, or inline text) and coordinates analysis.
*/
public class LogAnalyzer {

private final IssueClassifier classifier;
private final LogProcessor logProcessor;

public LogAnalyzer() {
this.classifier = new IssueClassifier();
this.logProcessor = new LogProcessor();
}

LogAnalyzer(IssueClassifier classifier) {
this.classifier = classifier;
this.logProcessor = new LogProcessor();
}

private List<String> preprocessLines(List<String> lines) {
return lines.stream()
.map(line -> {
if (logProcessor.isJsonLog(line)) {
return logProcessor.parseToPlainText(line);
}
return line;
}).flatMap(line -> Arrays.stream(line.split("\\r?\\n")))
.collect(Collectors.toList());
}

/** Reads a log file from disk and analyzes its contents. */
public AnalysisResult analyzeFile(Path filePath) throws IOException {
List<String> lines = Files.readAllLines(filePath);
return new AnalysisResult(filePath.toString(), classifier.classify(lines));
List<String> processedLines = preprocessLines(lines);
return new AnalysisResult(filePath.toString(), classifier.classify(processedLines));
}

/** Reads from an InputStream (e.g. System.in when using stdin mode). */
public AnalysisResult analyzeStream(InputStream stream, String sourceLabel) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
List<String> lines = reader.lines().collect(Collectors.toList());
return new AnalysisResult(sourceLabel, classifier.classify(lines));
List<String> processedLines = preprocessLines(lines);
return new AnalysisResult(sourceLabel, classifier.classify(processedLines));
}
}

/** Analyzes a stack trace or log text pasted directly as a string. */
public AnalysisResult analyzeText(String text) {
List<String> lines = Arrays.asList(text.split("\\r?\\n"));
return new AnalysisResult("inline text", classifier.classify(lines));
List<String> processedLines = preprocessLines(lines);
return new AnalysisResult("inline text", classifier.classify(processedLines));
}
}
}