DEV Community

Cover image for Your App Got Slower After Deploy — Here’s How to Find the Exact Method
closeup1202
closeup1202

Posted on • Edited on

Your App Got Slower After Deploy — Here’s How to Find the Exact Method

You deploy. Everything seems fine.

Then suddenly — something is slower.
But you have no idea which method caused it.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# Start the full stack
docker compose up
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode
# Python — via OTel SDK
GIT_COMMIT_HASH=py-v1 python main.py  # spans sent via OTLP to lofi-otelcol
Enter fullscreen mode Exit fullscreen mode
# Compare deploys
lofi diff py-v1..py-v2 --url http://localhost:9292
Enter fullscreen mode Exit fullscreen mode

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.4.1'
Enter fullscreen mode Exit fullscreen mode
Maven:

<dependency>
  <groupId>io.github.closeup1202</groupId>
  <artifactId>lofi-spring-boot-starter</artifactId>
  <version>0.4.1</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

2. Expose actuator endpoints

  management:
    endpoints:
      web:
        exposure:
          include: lofi
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
      }
    ]
  }
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
lofi diff a3f9c1..d82e04 --url http://localhost:9090
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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+.

Feedback welcome — open an issue or drop a comment below.

Top comments (0)