You deployed. p99 latency spiked. Now what?
Open Grafana. Check Jaeger. Dig through logs. Read the commit diff. Connect the dots yourself β every single time.
I got tired of that loop, so I built lofi: a zero-config library that links deploy events to method-level latency and lets you diff them from the terminal.
$ lofi diff a3f9c1..d82e04
Deploy Diff a3f9c1 β d82e04
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Method Before After Delta
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
OrderService.createOrder() 14.23ms β 91.00ms +76.77ms β²
PaymentClient.validate() 22.10ms β 58.40ms +36.30ms β²
UserService.findById() 3.05ms β 3.12ms +0.07ms β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2 regression(s) detected
Regressed methods show in red. No dashboards required.
Two modes
deployment mode for teams not on Spring Boot.
| Actuator mode | Backend mode | |
|---|---|---|
| Works with | Spring Boot | Any language with an OTel SDK (Java, Python, Node.js, Go, Ruby...) |
| Instrumentation | Spring AOP β no code changes | OpenTelemetry SDK or Java Agent |
| Setup | One dependency |
lofi-otelcol + lofi-backend
|
| Storage |
~/.lofi/metrics.db (local SQLite) |
/data/metrics.db (local SQLite) |
The CLI auto-detects which mode the target is running β no extra flags needed.
How it works
(1) Actuator mode
lofi uses Spring AOP to automatically instrument every @Service, @Component, @Repository, @Controller, and @RestController bean in your application. No annotations on your business code. No agent. No code changes beyond adding the dependency.
When your app starts, it reads a GIT_COMMIT_HASH environment variable to know which deploy is running. Every method call gets timed (in nanoseconds) and flushed asynchronously to a local SQLite database at ~/.lofi/metrics.db. When you're ready to compare two deploys, you run lofi diff and it calls the actuator endpoint to compute the regression diff.
The library is careful about what it instruments:
- Spring internals (org.springframework.*) β skipped
- Jakarta Servlet filters and MVC interceptors β skipped (avoids Security filter chain conflicts)
- AspectJ @aspect classes β skipped (avoids proxy-on-proxy chaos)
- JDK dynamic proxies like Spring Data JPA repositories β skipped (their time is already captured through the enclosing service call)
(2) Backend mode
lofi-backend runs as a standalone server that receives span data from lofi-otelcol β a custom OpenTelemetry Collector binary. Your app sends traces via the standard OTLP protocol using any OTel SDK, and
lofi-otelcol extracts method duration and commit hash, then forwards them to lofi-backend.
Your App (any language)
β OTLP (HTTP :4318 / gRPC :4317)
βΌ
lofi-otelcol
β POST /lofi/ingest
βΌ
lofi-backend (:9292)
β
βΌ lofi-cli
# Start the full stack
docker compose up
# Java β via OTel Agent (no code changes)
OTEL_RESOURCE_ATTRIBUTES=deployment.commit.hash=$(git rev-parse --short HEAD) \
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
java -javaagent:opentelemetry-javaagent.jar -jar your-app.jar
# Python β via OTel SDK
GIT_COMMIT_HASH=py-v1 python main.py # spans sent via OTLP to lofi-otelcol
# Compare deploys
lofi diff py-v1..py-v2 --url http://localhost:9292
The only requirement: name your spans as ClassName.methodName and set deployment.commit.hash as a resource attribute. lofi-otelcol handles everything else.
Getting started in 5 steps (Actuator mode)
1. Add the dependency
Gradle:
implementation 'io.github.closeup1202:lofi-spring-boot-starter:0.2.3'
Maven:
<dependency>
<groupId>io.github.closeup1202</groupId>
<artifactId>lofi-spring-boot-starter</artifactId>
<version>0.2.3</version>
</dependency>
2. Expose actuator endpoints
management:
endpoints:
web:
exposure:
include: lofi
3. Set the commit hash and run
export GIT_COMMIT_HASH=$(git rev-parse --short HEAD)
./gradlew bootRun
# [LO-FI] Monitoring active β commit: a3f9c1 | store: sqlite | regression-threshold: 0.2
4. Verify metrics via actuator
Send some traffic to your app, then check the raw JSON directly:
curl http://localhost:8080/actuator/lofi/a3f9c1
{
"commitHash": "a3f9c1",
"deployedAt": "2024-11-01T09:00:00Z",
"metrics": [
{
"className": "com.example.OrderService",
"methodName": "createOrder",
"elapsedMs": 14.23,
"recordedAt": "2024-11-01T09:01:23Z"
}
]
}
Once you have two deploys' worth of data, you can also diff them directly:
curl "http://localhost:8080/actuator/lofi/diff?base=a3f9c1&head=d82e04"
5. Install the CLI for a better view
The CLI renders the same data as a formatted table with regression highlighting β easier to read at a glance than raw JSON.
Install the CLI
npm install -g @closeup1202/lofi-cli
Three commands:
- lofi diff .. β compare latency between two deploys
- lofi snapshot β inspect metrics for a single deploy
- lofi check .. β CI gate: fail if regression exceeds threshold
lofi diff a3f9c1..d82e04 --url http://localhost:9090
lofi snapshot a3f9c2 --url http://localhost:9090
lofi check a3f9c1..d82e04 --threshold-ms 50 --url http://localhost:9090
No commit hash? No problem
Not sure which hash to compare? Run lofi diff or lofi snapshot without arguments β
the CLI fetches your recorded deploys and lets you pick with arrow keys.
$ lofi diff --url http://localhost:9090
? Select base commit (before):
β― a3f9c1 (4/14/2026, 1:10:00 PM, 6 metrics)
d82e04 (4/13/2026, 9:22:00 AM, 12 metrics)
? Select head commit (after):
a3f9c1 (4/14/2026, 1:10:00 PM, 6 metrics)
β― d82e04 (4/13/2026, 9:22:00 AM, 12 metrics)
Using lofi check in CI
lofi check exits with code 1 if any method exceeds the threshold β designed to fail a CI step automatically.
# GitHub Actions
- name: Check latency regression
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: lofi check $BASE..$HEAD --threshold-ms 50 --url https://staging.myapp.com
lofi check is a staging β production gate, not a pre-deploy check. Both commits need to be deployed with metrics collected before the comparison is meaningful. If no metrics are found for a commit, it
prints a warning and exits cleanly β so adding it to an existing pipeline won't break anything.
You can also output results as JSON or markdown:
# Parse results in CI
lofi check $BASE..$HEAD --threshold-ms 50 --format json | jq '.exceeded[].signature'
# Post a report as a PR comment
lofi check $BASE..$HEAD --threshold-ms 50 --format markdown > report.md
gh pr comment $PR_NUMBER --body-file report.md
What it stores (and where)
All data stays local. lofi writes a SQLite file to ~/.lofi/metrics.db. No telemetry, no cloud, nothing leaves your machine unless you opt in to a dashboard (coming later).
If you're running in Docker or Kubernetes, mount a volume at /root/.lofi so the database survives container restarts:
docker run \
-e GIT_COMMIT_HASH=$(git rev-parse --short HEAD) \
-v $HOME/.lofi:/root/.lofi \
my-app
Spring Security note
If your app uses Spring Security, the actuator endpoints return 403 by default. The cleanest fix is management port isolation β run actuator on a separate internal port that's never exposed publicly:
management:
server:
port: 9090
lofi diff a3f9c1..d82e04 --url http://localhost:9090
No security config changes needed.
Configuration
lofi:
store-type: sqlite # or in-memory (for tests/dev)
regression-threshold: 0.2 # 20% increase = regression
buffer:
flush-threshold: 100
flush-delay-ms: 5000
queue-capacity: 1000
JSR-303 validation runs at startup β if you misconfigure a value, all violations are reported at once rather than stopping at the first one.
Current state and roadmap
lofi is in early development. It currently works best in single-pod environments (each pod has its own SQLite file). Multi-pod metric aggregation and a team dashboard are on the roadmap.
Requires Spring Boot 3.x and Java 17+.
- GitHub: https://github.com/closeup1202/lofi
- npm: https://www.npmjs.com/package/@closeup1202/lofi-cli
- Maven Central: https://central.sonatype.com/artifact/io.github.closeup1202/lofi-spring-boot-starter
Feedback welcome β open an issue or drop a comment below.













