Haunted Stack Traces:使用本機運行的 Gemma 4 解釋 Java 錯誤
Haunted Stack Traces: Explaining Java Errors with Gemma 4, Running Locally
Haunted Stack Trace 是一個 Spring Boot 錯誤處理器,透過 Ollama 在本機運行 Gemma 4,解釋 Java 堆疊追蹤中的錯誤原因及修正方法。它使用 Gemma 4 (12B),不需要 API key,堆疊追蹤不會離開用戶的電腦。
This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine
What I Built
Every Java developer knows the moment: the app crashes, the console fills with red, and somewhere in sixty lines of stack trace is the one line that matters.
The Haunted Stack Trace is a Spring Boot error handler possessed by the ghosts of deprecated Java APIs. When the app throws an exception, it does not return a plain 500 page. The stack trace goes to Gemma 4, running locally through Ollama, and a ghost answers: java.util.Date, Vector, Hashtable, Applet or StringTokenizer, whichever fits the crime.
The ghost tells you:
- the exception and the culprit: the exact line in your code, copied from the trace, not invented
- the cause, in plain English
- the fix, practical and specific
- and then a whisper, because it is Halloween
{
"ghost": "java.util.Date",
"exception": "java.time.format.DateTimeParseException",
"culprit": "...CursedController.halloweenDate(CursedController.java:63)",
"cause": "LocalDate.parse expects ISO-8601 (yyyy-MM-dd), but '31/10/2026' is in a different format.",
"fix": "Use LocalDate.parse(dateString, DateTimeFormatter.ofPattern(\"dd/MM/yyyy\")).",
"whisper": "You reach for the current moment, but you are trapped in a format of the past."
}
I took the theme literally: the ghosts in the machine are the dead APIs we all still trip over. java.util.Date was replaced by java.time, Vector by ArrayList, and they are still haunting codebases. Here, at least, they are being useful.
There are two ways to wake them:
-
Curse buttons that call endpoints which crash on purpose (
NullPointerException,ConcurrentModificationException,DateTimeParseExceptionand others) - The séance: paste any Java stack trace from any project, and the ghost explains it
Demo
It runs locally by design, so there is no deployed link. You can run it yourself in about five minutes (instructions in the repo), with your Wi-Fi off if you want to check my claim.
Code
Maurodc90
/
haunted-stack-trace
Entry for the DEV Hacktoberfest 2026 Open-Source AI Challenge (Ghost in the Machine). A Spring Boot error handler possessed by ghosts of deprecated Java APIs: explains stack traces with Gemma 4 running locally via Ollama. No API key, nothing leaves your machine.
The Haunted Stack Trace
A Spring Boot error handler possessed by the ghosts of deprecated Java APIs.
When the application throws an exception, it does not return a plain 500 page. The stack trace is sent to Gemma 4, an open-weight model running locally through Ollama, and a ghost (java.util.Date, Vector, Hashtable, Applet or StringTokenizer) answers with what broke, the exact line, why it happened and how to fix it. Then it whispers something unsettling.
Everything runs on your machine. No API key, no internet, and no stack trace ever leaves your laptop.
Built as an entry for the DEV Hacktoberfest 2026 Open-Source AI Challenge (Week 4: Ghost in the Machine).
Example
GET /cursed/date calls LocalDate.parse("31/10/2026"):
{
"ghost": "java.util.Date"
"exception": "java.time.format.DateTimeParseException",
"culprit": "com.maurodc90.hauntedstacktrace.controller.CursedController.halloweenDate(CursedController.java:63)",
"cause": "The LocalDate.parse method expects…
How I Built It
Stack: Java 21, Spring Boot 4.1, Spring AI 2.0, Ollama, Gemma 4 (12B), plain HTML/CSS/JS, GitHub Actions.
The flow is short:
- A controller throws a
RuntimeException. - A
@RestControllerAdvicecatches it and turns the stack trace into text. -
GhostServicesends the top 25 lines of the trace to Gemma through Spring AI'sChatClient. -
BeanOutputConvertermaps the reply onto a Java record,Haunting.
return chatClient.prompt()
.system(SYSTEM_PROMPT)
.user(trimmedTrace)
.options(OllamaChatOptions.builder()
.model(model)
.temperature(0.4)
.disableThinking()
.numPredict(400))
.call()
.entity(converter);
That looks simple. Getting there was not, and the failures taught me more than the success did.
Ghost #1: "structured output is unavailable"
My first version asked Ollama to enforce a JSON Schema for the answer. Ollama replied with HTTP 501 {"error":"structured output is unavailable"} for this Gemma 4 build. Spring AI treated that as a transient error and retried with growing waits, 10 seconds, then 50, so my first request simply hung. I dropped the enforced schema and let BeanOutputConverter put the format instructions into the prompt and parse the reply. The structure still held.
Ghost #2: the request that never ended
Then I typed something that was not a stack trace into the séance box: "give me a new error". Six minutes later it was still summoning. ollama ps showed the model stuck on Stopping.... Nothing limited how long an answer could be.
I added a cap of 400 tokens. Now real stack traces came back empty: No content to map due to end-of-input.
The cause: Gemma 4 thinks silently before it answers, and that reasoning counts towards the token budget. On nonsense input it reasoned forever. With a cap, it spent the whole budget thinking and never wrote the JSON. One line, disableThinking(), fixed both problems and made answers faster.
Ghost #3: garbage in
The real fix for nonsense input was to never send it to the model at all. The séance now checks for at least one stack frame (at com.example.Foo.bar(Foo.java:42)) before calling Gemma. Anything else gets an instant refusal from the ghost of Applet: "You knock, but bring no bones. The dead answer only to stack traces."
Smaller decisions that mattered
- Facts and role-play live in separate fields. The joke never leaks into the fix.
-
RuntimeException, notException. A browser asking forfavicon.icoraisesNoResourceFoundException. If I caught everyException, every 404 would wake the model. - A fallback answer. If the model fails, the handler still responds instead of crashing inside the crash handler.
-
textContent, neverinnerHTML. Model output is never treated as HTML on the page. -
Tests without a model. The controller tests mock
GhostService, so GitHub Actions runs them on every push without Ollama.
Where the ghost stops
I pasted a trace from a bug in my own Expense Tracker: two Lombok @Data entities, AppUser and Expense, whose generated hashCode() methods call each other until the stack overflows. The ghost of Vector found the loop between the two classes immediately. But its fix was generic, because the trace never mentions Lombok. The real fix, @EqualsAndHashCode.Exclude on the relationship, was mine. The ghost is a good colleague, not an oracle, and I think that is the honest way to use these tools.
Why Does Open Innovation Matter?
Because stack traces are not harmless. They contain package names, class names, file paths, sometimes values. Pasting your company's stack traces into a hosted AI service means your company's code leaving the building. With an open-weight model on my own machine, nothing leaves. That is not a feature I bolted on. It is the reason the project exists.
What open made possible here:
- It works offline and costs nothing to run. No API key, no rate limit, no bill per crash.
-
I could see inside it when it broke. When a request hung,
ollama psshowed me the model stuck onStopping..., and I restarted it myself. When answers came back empty, I could turn thinking off with one flag. With a closed API, I would have been looking at a timeout and guessing. -
Swapping is one line. The model name lives in
application.yml. If 12B is too weak for someone's traces, they can trygemma4:26b. Spring AI'sChatClienthides the provider, so the same code could point at a hosted model if a team chose to.
To be fair to closed models: a large hosted model would probably write better fixes for unusual traces. But for this job, explaining everyday exceptions on a developer's own laptop, a 12B open model was good enough, and its privacy was worth more than the extra quality.
My Agent Session
I built this pair-programming with Claude. Claude wrote most of the code. I decided what to build, ran and tested every step, hit every bug above on my own machine, chose the fixes, and reviewed the code until I could explain each part. I'm a junior Java developer, and working this way, fast with AI but understanding every line, is how I want to work.
The Spirit of It
The moment it landed was not a test passing. It was explaining the project to my partner, who does not write code.
I said: imagine an app that tells you every day which chair I sit in at class. One day I don't come. The app looks for me, finds nothing, and crashes. That is a NullPointerException. Java's message says what happened, in a language only programmers read. This app explains why, in plain words, through a ghost.
She understood it in one go. That's the point of the project: a crash is just a ghost of something you expected to be there.
Prize Categories
- Best Use of Gemma: Gemma 4 (12B) runs locally through Ollama and is the core of the app.
- Best Use of GitHub Copilot: the project is automated with GitHub Actions; every push runs the test suite on GitHub's runners.
來源:DEV Community · dev.to