Skip to main content
Glama
Suteerth03
by Suteerth03

LogLens

An MCP (Model Context Protocol) server for log analysis. Instead of pasting a log file into a chat and asking an LLM to debug it, LogLens exposes log search, context retrieval, and incident summarization as tools that any MCP-compatible client (Claude Desktop, Claude Code, or your own agent) can call directly — with targeted retrieval instead of context-stuffing, and a self-verification step to catch unsupported root-cause claims before they're returned.

Why this exists

Built as a public, portfolio version of an AI-powered log analyzer that won 1st place at an internal hackathon. Short version of why this beats paste-into-chat: production logs don't fit in a context window, a chat-paste isn't callable by other systems, and raw prompting has no mechanism to check whether the model's answer is actually grounded in the log data.

Related MCP server: Log Analyzer MCP

Status

  • Core server, 3 tools — working end-to-end against a sample log. ✅

  • LLM-based root-cause generation with a hallucination-verification loop that checks the hypothesis on two independent axes and retries once if it fails either. ✅

  • Mixed-provider architecture (Groq + Gemini) — the verifier runs on a different model family than the generator, so the check doesn't share the generator's blind spots. ✅

  • 8-case eval suite, deterministic scoring, 8/8 passing. ✅

  • Dockerized, runs as a network-reachable HTTP server, verified end-to-end against a live container. ✅

  • Deployed on Azure Container Apps. ✅ Live: https://loglens.livelymushroom-3f0e315d.centralindia.azurecontainerapps.io

  • Next: a demo GIF.

Tools

Tool

What it does

search_logs

Keyword search over the log file; returns matches with surrounding context and a line id.

get_error_context

Given a line id, returns a wider window of lines — full stack traces, sequence of events.

summarize_incident

Extracts search terms from a natural-language question, retrieves evidence (lexical + time-window + global-anomaly expansion), generates a root-cause hypothesis, then independently verifies it on two axes — retrying once if the verifier rejects it.

Architecture

Question ──▶ extract search terms (Groq, gpt-oss-20b)
                 │
                 ▼
         search_logs (lexical match)
                 │
                 ▼
    + time-window expansion (asymmetric: 900s before / 180s after —
      causes precede symptoms)
                 │
                 ▼
    + global anomaly scan (all WARN/ERROR lines, not just in-window —
      the explaining line is often itself a warning)
                 │
                 ▼
       generate hypothesis (Groq, gpt-oss-120b)
                 │
                 ▼
    verify: soundness + completeness (Gemini — DIFFERENT provider
    from the generator, on purpose; falls back to same-provider
    Groq if Gemini is unavailable, and reports which happened)
                 │
          unsound/incomplete? ──▶ regenerate once, feeding back
                 │                the lines the first pass overlooked
                 ▼
              answer

Two providers, deliberately — not just a cost workaround

Groq carries extraction and hypothesis generation; Gemini carries verification. This started as a quota workaround (Gemini's free tier caps at 20 requests/day; Groq's is far more generous) but turned into a real architectural improvement: a verifier running on the same model that produced a claim shares that model's blind spots. Checking the claim with a different model family makes the hallucination check genuinely independent, not just a second opinion from the same source. Verification.independent reports whether a given answer actually got the cross-provider check or fell back to same-provider (Gemini down/unconfigured) — surfaced, not hidden.

The cross-service retrieval gap — found via testing, fixed at the right layer

Early testing surfaced a real limitation: summarize_incident found the immediate cause of a checkout failure (DB pool exhaustion) but missed the upstream cause the sample log also encodes — a long-running query on a different service holding the connection. Two structural problems:

  • Retrieval was purely lexical. Extracted terms were checkout-scoped, so inventory-service lines could never enter the evidence set no matter how good the reasoning was. Fixed with time-window expansion, asymmetric on purpose (900s before / 180s after) — causes precede symptoms, often by more than a short symmetric window would catch — plus a global scan for anomalous (WARN/ERROR) lines regardless of window, since the explaining line is often itself a warning ("NTP sync failed", "rotation skipped").

  • The verifier could only rubber-stamp. It originally saw only the lines a claim cited, which made it structurally incapable of noticing an incomplete answer — a claim describing a symptom will always look supported by the lines it chose to cite. It now sees the full evidence set and scores soundness and completeness independently; a sound-but- incomplete verdict feeds the overlooked lines back into regeneration.

Eval suite

npx tsx evals/run-evals.ts          # all 8 cases
npx tsx evals/run-evals.ts 03 08    # a subset, by id substring

8 cases spanning distinct failure archetypes: cross-service resource contention, unbounded-cache OOM, retry-storm amplification, a bad deploy, two compounding causes, a single-bad-node clock skew, a healthy log (correct answer is "nothing failed"), and a loud-symptom-masking-subtle-cause case built specifically to exercise the completeness axis. Scoring is deterministic — concept groups with synonyms, plus required evidence citations, no LLM judge — so runs are reproducible. The report separates retrieval misses (evidence never reached the model) from reasoning misses (evidence was there, answer still wrong), since those need different fixes.

Current result: 8/8 passing, 0 retrieval misses, 0 reasoning misses.

Debugging this suite is itself a decent engineering story: an asymmetric time window and a global anomaly scan fixed real retrieval brittleness; on Groq's free tier, max_tokens is a reservation against a per-minute token budget, not a pay-for-what-you-use ceiling — an oversized value gets a 413 regardless of actual prompt size; reasoning_effort: "low" was needed on the gpt-oss models, which otherwise spend the budget on reasoning tokens and truncate before emitting valid JSON; and the harness itself had two scoring bugs (Unicode punctuation variants, then a plain-space variant of the same compound identifier) that reported correct answers as failures — worth knowing when you write your own eval harness: it needs debugging too.

Docker

docker build -t loglens:local .
docker run -d -p 3000:3000 \
  -e GROQ_API_KEY=your-key \
  -e GEMINI_API_KEY=your-key \
  loglens:local
curl http://localhost:3000/health

Multi-stage build (compile with devDependencies, run with production-only deps + a non-root user + a container healthcheck on /health). The container runs the HTTP transport (MCP_TRANSPORT=http, set by default in the image) rather than stdio, since a deployed container has no parent process to spawn it locally the way Claude Desktop/Code do.

Real bug found and fixed while wiring this up, worth knowing if you build your own stateless streamable-HTTP MCP server: the SDK's stateless mode requires a fresh transport per request — reusing one transport across requests silently 500s every request after the first, with no thrown exception to catch. Separately, a single McpServer can only be connected to one transport at a time ("Already connected to a transport"). The fix (see createServer() and the HTTP handler in src/index.ts) creates both a fresh McpServer and a fresh StreamableHTTPServerTransport per request — cheap, since the server only holds tool definitions, no per-connection state (none of these tools carry state between calls anyway). Verified against a real running container: search_logs and a full summarize_incident pass both completed correctly end-to-end after the fix.

Deployment

Live: https://loglens.livelymushroom-3f0e315d.centralindia.azurecontainerapps.io

Deployed on Azure Container Apps (Consumption plan). The image is pushed to Docker Hub (public — nothing sensitive is baked into it; secrets are injected at runtime, not build time) rather than Azure Container Registry, which avoids ACR's ~$5/month Basic-tier cost entirely:

docker tag loglens:local <dockerhub-user>/loglens:latest
docker push <dockerhub-user>/loglens:latest

az group create --name loglens-rg --location centralindia
az containerapp env create --name loglens-env --resource-group loglens-rg --location centralindia

az containerapp create \
  --name loglens \
  --resource-group loglens-rg \
  --environment loglens-env \
  --image docker.io/<dockerhub-user>/loglens:latest \
  --target-port 3000 \
  --ingress external \
  --min-replicas 0 --max-replicas 1 \
  --cpu 0.25 --memory 0.5Gi \
  --secrets groq-api-key=<key> gemini-api-key=<key> \
  --env-vars GROQ_API_KEY=secretref:groq-api-key GEMINI_API_KEY=secretref:gemini-api-key

--min-replicas 0 is deliberate, not a default left alone. Azure's "Always Free" grant (180K vCPU-seconds + 2M requests/month) is usage-metered, not time-based — an always-on replica at even the smallest size (0.25 vCPU) burns through that grant in about 8 days of continuous uptime, then starts drawing on the temporary $200 trial credit instead of staying free indefinitely. Scale-to-zero means billing only accrues on actual requests, which is what keeps a low-traffic demo project genuinely free long-term rather than free for 30 days. Tradeoff: a request after idle time takes a few seconds to cold-start a replica.

Verified end-to-end against the live deployment (not just a health check): tool listing and a real search_logs call both returned correct results through the public URL.

Access control — public ingress needs it

--ingress external means the URL is reachable by anyone on the internet. summarize_incident spends the deployer's own Groq/Gemini quota on every call regardless of who's asking, not the caller's — so an unprotected public endpoint means anyone who finds the URL can drain that quota. The data itself isn't sensitive (a synthetic sample log), so this is a cost/ availability risk, not a privacy one — but real enough that it needed a real fix rather than being left open by accident.

Two options were available: a hard auth gate (shared-secret header, implemented in src/index.ts and available via LOGLENS_ACCESS_TOKEN for a private deployment), or open access protected by rate limiting so anyone can try the live demo without a handoff step. The public instance runs the second — appropriate for a demo link shared with recruiters/ interviewers where a token exchange would be friction, not security.

src/rateLimit.ts enforces two independent limits, checked before a request reaches the LLM pipeline at all, and scoped to summarize_incident specifically — search_logs/get_error_context cost nothing and stay unlimited:

  • Per-IP: 3 calls/hour, stops one source (or a runaway script) from hogging it.

  • Global daily cap: 15 calls/day across all visitors combined. This matters more than it might look — Gemini's free tier caps at 20 requests/day total, shared regardless of how many distinct people show up, so a per-IP limit alone wouldn't protect it; ten different visitors making two calls each would still exhaust it.

In-memory, deliberately — this runs at --max-replicas 1, and scale-to-zero already resets in-memory state on a cold start, so a persistent store would be false precision for a single-instance demo.

Verified against the live deployment: search_logs succeeds with no header at all; 4 concurrent summarize_incident calls from the same source returned exactly 3× 200 and 1× 429, matching the per-IP limit exactly.

A real Azure CLI gotcha hit while wiring this up, worth knowing: az containerapp update --set-env-vars does not fully replace the env var list the way "set" implies — omitting a previously-set key does not remove it, and using the mutable :latest image tag meant Azure didn't detect a change worth deploying at all (no new revision was created) until --revision-suffix forced one explicitly. Removing an env var requires setting it to an explicit empty value (KEY=""), not omitting it.

Environment variables

Variable

Required for

Notes

GROQ_API_KEY

summarize_incident (extraction + hypothesis)

Get one free at console.groq.com/keys. search_logs and get_error_context work without it.

GEMINI_API_KEY

independent verification

Get one free at aistudio.google.com/apikey. Without it, verification falls back to same-provider (Groq) and is no longer independent — reported via Verification.independent, not silently downgraded.

LOGLENS_LOG_FILE

optional

Point at a real log file instead of the bundled sample.

LOGLENS_ACCESS_TOKEN

optional, alternative to rate limiting

Requires every /mcp request to carry a matching X-LogLens-Token header — a hard gate for a private deployment. Unset (the public instance's setting) relies on the built-in rate limiting instead, so anyone can try the demo without a handoff step.

MCP_TRANSPORT

optional

http runs the network-reachable server (used by Docker); unset/anything else runs stdio (used by Claude Desktop/Code).

PORT

optional

HTTP port, default 3000.

Setup

npm install
npm run build

By default the server reads fixtures/sample.log, a synthetic incident (a long-running unindexed query on inventory-service exhausts a shared DB connection pool, cascading into checkout-service failures). Point it at a real log file instead with:

LOGLENS_LOG_FILE=/path/to/real.log node dist/index.js

Smoke tests (no MCP client needed)

npx tsx scripts/smoke-test.ts                              # stdio transport
npx tsx scripts/smoke-test-http.ts http://localhost:3000/mcp  # HTTP transport

Spawns (or connects to) the server and calls all three tools — useful for verifying it works before wiring up a real client. A full summarize_incident pass is 3-4 sequential LLM calls and can take 30-90s; pass a generous timeout if calling it programmatically (both scripts do).

Connect to Claude Desktop

Edit %APPDATA%\Claude\claude_desktop_config.json (Windows) and add:

{
  "mcpServers": {
    "loglens": {
      "command": "node",
      "args": ["C:\\Users\\sarve\\OneDrive\\Desktop\\LogLens\\dist\\index.js"],
      "env": {
        "GROQ_API_KEY": "your-groq-key",
        "GEMINI_API_KEY": "your-gemini-key"
      }
    }
  }
}

The env block is required, not optional — MCP clients spawn the server with a sanitized environment by default, not your shell's full environment, so the keys won't be visible to summarize_incident without it even if set globally on your machine.

Restart Claude Desktop, then ask it something like "search the logs for 'pool exhausted'" — it should call search_logs automatically.

Connect to Claude Code

claude mcp add loglens --scope user --env GROQ_API_KEY=your-groq-key --env GEMINI_API_KEY=your-gemini-key -- node C:\Users\sarve\OneDrive\Desktop\LogLens\dist\index.js

(Same reason as above — --env passes the keys explicitly since the spawned process doesn't inherit your shell environment by default.)

Project structure

src/
  index.ts       MCP server (dual transport: stdio + HTTP) + tool registration
  logParser.ts   log loading, search, time-window expansion, anomaly scan
  summarize.ts   the summarize_incident pipeline: extract -> retrieve -> hypothesize -> verify -> retry
  providers.ts   Groq + Gemini clients, model config, schema-constrained JSON generation
fixtures/
  sample.log     synthetic incident for local testing
evals/
  cases.ts       8 eval case definitions
  run-evals.ts   deterministic scoring harness
  logs/          synthetic logs for eval cases 02-08
scripts/
  smoke-test.ts       stdio transport smoke test
  smoke-test-http.ts  HTTP transport smoke test
Dockerfile       multi-stage build, non-root user, container healthcheck

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • -
    license
    Not graded
    quality
    Not graded
    maintenance
    Provides comprehensive logging and monitoring capabilities for MCP services with real-time log tailing, advanced search, error analysis, and anomaly detection. Enables centralized log aggregation, correlation tracking, and health monitoring across all MCP ecosystem services.
    -
  • F
    license
    B
    quality
    C
    maintenance
    Enables AI-assisted analysis of log files through advanced searching, filtering, and test execution capabilities. Supports time-based queries, pattern matching, test summarization, and code coverage reporting directly within compatible MCP clients.
    12
    -