Skip to content
Merged
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
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
workflow_dispatch:
push:
branches: [ "main" ]
tags: [ "*" ]
paths-ignore:
- "**/*.md"
- "CODEOWNERS"
Expand Down Expand Up @@ -46,3 +47,30 @@ jobs:
- name: Test with pytest
run: |
pytest
- name: Export OpenAPI
run: |
PYTHONPATH=. python scripts/export_openapi.py
- name: Upload OpenAPI artifact
uses: actions/upload-artifact@v4
with:
name: openapi.json
path: clients/openapi.json

publish-openapi-release:
if: startsWith(github.ref, 'refs/tags/')
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download OpenAPI artifact
uses: actions/download-artifact@v4
with:
name: openapi.json
path: artifacts
- name: Attach openapi.json to GitHub Release
uses: softprops/action-gh-release@v2
with:
files: artifacts/openapi.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@ logs/

# Local data
data/

# Generated API clients (keep README + thin Python wrapper)
clients/go/*.go
clients/python/generated/
38 changes: 37 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
.PHONY: help install install-dev \
db-up db-down docker-up docker-down docker-demo docker-logs \
init migrate demo ingest explain clusters ask \
api web web-serve worker test test-unit test-int test-cov lint format clean
api web web-serve worker test test-unit test-int test-cov lint format \
openapi client-go client-python clean

PYTHON := python
PIP := pip
SAMPLE := sample_data/sample_incident
OPENAPI := clients/openapi.json

help:
@echo "raglogs — incident explanation tool"
Expand Down Expand Up @@ -38,6 +40,9 @@ help:
@echo " make test-cov Unit tests with coverage"
@echo " make lint Ruff lint"
@echo " make format Ruff format"
@echo " make openapi Export OpenAPI schema to clients/openapi.json"
@echo " make client-go Generate Go client (oapi-codegen; no-op if missing)"
@echo " make client-python Optional OpenAPI Python generator, or use src/clients/v1.py"
@echo " make clean Remove build artifacts"

# ── Setup ─────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -146,6 +151,37 @@ lint:
format:
ruff format src/ tests/

# ── OpenAPI / clients ─────────────────────────────────────────────────────────

openapi:
PYTHONPATH=. $(PYTHON) scripts/export_openapi.py

# Generates clients/go/client.go when oapi-codegen is installed.
# Missing binary: print install hint and exit 0 so CI without Go still passes.
client-go: openapi
@if command -v oapi-codegen >/dev/null 2>&1; then \
mkdir -p clients/go; \
oapi-codegen -generate client,types -package raglogs -o clients/go/client.go $(OPENAPI); \
echo "Wrote clients/go/client.go"; \
else \
echo "oapi-codegen not found. Install with:"; \
echo " go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest"; \
echo "Then re-run: make client-go"; \
fi

# Committed client is src/clients/v1.py. Optional generator dump is gitignored.
client-python: openapi
@echo "Committed typed client: src/clients/v1.py (targets /v1)."
@if command -v openapi-python-client >/dev/null 2>&1; then \
mkdir -p clients/python/generated; \
openapi-python-client generate --path $(OPENAPI) --output-path clients/python/generated --overwrite; \
echo "Wrote clients/python/generated/"; \
else \
echo "Optional generator openapi-python-client not installed."; \
echo " pip install openapi-python-client"; \
echo "Using the thin committed client instead (src/clients/v1.py)."; \
fi

# ── Cleanup ───────────────────────────────────────────────────────────────────

clean:
Expand Down
65 changes: 40 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ raglogs ingest --adapter loki --param query='{namespace="prod"}' \
```

```bash
curl -X POST http://localhost:8000/ingestions \
curl -X POST http://localhost:8000/v1/ingestions \
-H "Content-Type: application/json" \
-d '{"adapter":"loki","params":{"query":"{app=\"api\"}"},"since":"1h"}'
```
Expand Down Expand Up @@ -833,7 +833,7 @@ Fingerprinting can still split one incident across multiple clusters when wordin
- **Disabled (default).** `EMBEDDINGS_PROVIDER=disabled` skips the merge pass entirely. Clustering is fingerprint-only and deterministic — the same logs always produce the same clusters.
- **Enabled.** With `openai` or `local`, representatives are embedded at analysis time (in memory; not written to pgvector). Pairs with cosine similarity ≥ `CLUSTER_MERGE_SIMILARITY_THRESHOLD` (default **0.92**) are merged via connected components. Merged `count` is the sum of member counts; services and levels are summed; `first_seen` is the earliest timestamp and `last_seen` the latest; importance is recomputed. The canonical fingerprint is the member with the highest importance score. `ClusterRun.algorithm` is `fingerprint+semantic` when embeddings were used, even if no pair crossed the threshold.
- **Fail open.** If the embeddings backend is missing, raises, or returns unusable vectors, clustering continues with the fingerprint-only set.
- **Ask vs merge.** Semantic `ask` uses the *stored* `log_embeddings` table (populated by `raglogs ingest --with-embeddings`) and `ASK_SEMANTIC_MIN_SIMILARITY` (default **0.75**). Cluster merge still uses its own in-memory pass and threshold. Similar-incident search (`POST /query/similar` / historical cluster ANN) is not in this release. Compare still applies its own heuristic collapse for webhook retries / queue growth after clustering.
- **Ask vs merge.** Semantic `ask` uses the *stored* `log_embeddings` table (populated by `raglogs ingest --with-embeddings`) and `ASK_SEMANTIC_MIN_SIMILARITY` (default **0.75**). Cluster merge still uses its own in-memory pass and threshold. Similar-incident search (`POST /v1/query/similar` / historical cluster ANN) is not in this release. Compare still applies its own heuristic collapse for webhook retries / queue growth after clustering.

Local embeddings require the optional extra: `pip install 'raglogs[local-embeddings]'` (`sentence-transformers`). If that import fails, merge is skipped.

Expand Down Expand Up @@ -915,17 +915,17 @@ export AUTH_ENABLED=true
raglogs keys create --role admin --name "local"
# copy the rlk_… secret from the panel — it is shown only once

curl -X POST http://localhost:8000/query/explain \
curl -X POST http://localhost:8000/v1/query/explain \
-H "Authorization: Bearer rlk_…" \
-H "Content-Type: application/json" \
-d '{"since": "30m", "no_llm": true}'
```

| Role | Allowed |
|---|---|
| `ingest` | `POST /ingestions` |
| `query` | `GET /ingestions*`, `POST /query/*`, web UI (`GET /`, `/static`), OpenAPI (`/docs`) |
| `admin` | everything, including `GET /config` |
| `ingest` | `POST /v1/ingestions` (and the deprecated `/ingestions` alias) |
| `query` | `GET /v1/ingestions*`, `POST /v1/query/*`, web UI (`GET /`, `/static`), OpenAPI (`/docs`) |
| `admin` | everything, including `GET /v1/config` |

`GET /health` and `GET /metrics` (path reserved; no Prometheus body yet) are always unauthenticated. `/docs` is **not** exempt.

Expand All @@ -949,22 +949,28 @@ If auth is disabled and the process binds a non-loopback address (`0.0.0.0`, `::

| Method | Endpoint | Description |
|---|---|---|
| `GET` | `/health` | Service and DB health check |
| `POST` | `/ingestions` | Enqueue an ingest job (`adapter`: `file`, `cloudwatch`, or `datadog`) |
| `GET` | `/ingestions` | List recent completed ingestion jobs, newest first |
| `GET` | `/ingestions/{job_id}` | Poll ingestion job status |
| `GET` | `/ingestions/latest` | ID of the most recently completed ingestion job, if any |
| `POST` | `/query/explain` | Explain a time window |
| `POST` | `/query/ask` | Answer a natural language question |
| `POST` | `/query/clusters` | List top clusters |
| `POST` | `/query/timeline` | Reconstruct incident timeline for a window |
| `POST` | `/query/compare` | Diff two time windows (same semantics as `raglogs compare`) |
| `GET` | `/config` | Read effective configuration |
| `GET` | `/health` | Service and DB health check (unversioned) |
| `POST` | `/v1/ingestions` | Enqueue an ingest job (`adapter`: `file`, `cloudwatch`, `datadog`, `loki`, or `k8s`) |
| `GET` | `/v1/ingestions` | List recent completed ingestion jobs, newest first |
| `GET` | `/v1/ingestions/{job_id}` | Poll ingestion job status |
| `GET` | `/v1/ingestions/latest` | ID of the most recently completed ingestion job, if any |
| `POST` | `/v1/query/explain` | Explain a time window |
| `POST` | `/v1/query/ask` | Answer a natural language question |
| `POST` | `/v1/query/clusters` | List top clusters |
| `POST` | `/v1/query/timeline` | Reconstruct incident timeline for a window |
| `POST` | `/v1/query/compare` | Diff two time windows (same semantics as `raglogs compare`) |
| `GET` | `/v1/config` | Read effective configuration |

Unversioned `/ingestions`, `/query/*`, and `/config` remain as **deprecated aliases** for one release. They behave the same as the `/v1` paths and send `Deprecation: true` plus a `Link: </v1/...>; rel="successor-version"` header. `/health`, the web UI (`/`), and `/static` stay unversioned.

**Compatibility policy.** Additive changes stay in `v1`. Breaking path or method removals require `v2`. JSON response bodies are unchanged in this release (a stable evidence schema is tracked separately as G7).

**OpenAPI and clients.** Export the spec with `make openapi` (`clients/openapi.json`). CI uploads that file as a workflow artifact and attaches it to GitHub Releases on tags. A thin typed Python client ships as `src.clients.v1.RaglogsClient` (targets `/v1`). `make client-go` runs [oapi-codegen](https://github.com/oapi-codegen/oapi-codegen) into `clients/go/` when the binary is installed; otherwise it prints the install command and exits 0. See `clients/README.md`.

**Example**

```bash
curl -X POST http://localhost:8000/query/explain \
curl -X POST http://localhost:8000/v1/query/explain \
-H "Content-Type: application/json" \
-d '{"since": "30m", "no_llm": true}'
```
Expand All @@ -987,20 +993,20 @@ curl -X POST http://localhost:8000/query/explain \
}
```

**Explain** — `POST /query/explain` accepts the same window filters as the CLI. Optional `"format": "markdown"` adds a paste-ready `markdown` incident report field alongside the JSON payload (same shape as `raglogs explain --format markdown`).
**Explain** — `POST /v1/query/explain` accepts the same window filters as the CLI. Optional `"format": "markdown"` adds a paste-ready `markdown` incident report field alongside the JSON payload (same shape as `raglogs explain --format markdown`).

**Timeline** — `POST /query/timeline` accepts the same window filters as the CLI (`since` or `from_time`/`to_time`, optional `service`, `env`, `all_ingestions`, `ingestion_job_id`). Set `"format": "text"` to include a plain-text `text` field alongside `events`.
**Timeline** — `POST /v1/query/timeline` accepts the same window filters as the CLI (`since` or `from_time`/`to_time`, optional `service`, `env`, `all_ingestions`, `ingestion_job_id`). Set `"format": "text"` to include a plain-text `text` field alongside `events`.

```bash
curl -X POST http://localhost:8000/query/timeline \
curl -X POST http://localhost:8000/v1/query/timeline \
-H "Content-Type: application/json" \
-d '{"since": "2h", "format": "json"}'
```

**Compare** — `POST /query/compare` matches `raglogs compare`: either `"since"` + `"baseline"` (durations, window A ends at request time) or explicit `window_a_from` / `window_a_to` / `window_b_from` / `window_b_to`. Optional `"format": "text"` adds a rendered `text` field.
**Compare** — `POST /v1/query/compare` matches `raglogs compare`: either `"since"` + `"baseline"` (durations, window A ends at request time) or explicit `window_a_from` / `window_a_to` / `window_b_from` / `window_b_to`. Optional `"format": "text"` adds a rendered `text` field.

```bash
curl -X POST http://localhost:8000/query/compare \
curl -X POST http://localhost:8000/v1/query/compare \
-H "Content-Type: application/json" \
-d '{"since": "30m", "baseline": "24h"}'
```
Expand Down Expand Up @@ -1028,11 +1034,11 @@ just the server.
Pick a time window (presets or a duration like `2h`), then switch between the
**Explain**, **Timeline**, **Compare**, and **Ask** tabs. The **ingestion**
dropdown in the top bar lists your 25 most recent completed ingestions (via
`GET /ingestions`) and defaults to the latest one, matching the CLI; pick a
`GET /v1/ingestions`) and defaults to the latest one, matching the CLI; pick a
different ingestion or "All ingestions" to change what a query is scoped to.

The UI is server-rendered (Jinja2 + vanilla JS/CSS, no CORS, no node/npm) and
calls the same `/query/*` JSON endpoints listed above.
calls the same `/v1/query/*` JSON endpoints listed above.

With default `AUTH_ENABLED=false` the UI is open — fine for local dev. When
auth is on, load the UI with a `query` or `admin` bearer token (the browser
Expand Down Expand Up @@ -1060,6 +1066,13 @@ make api
make lint
make format

# Export OpenAPI spec (clients/openapi.json)
make openapi

# Optional generated clients (see clients/README.md)
make client-go
make client-python

# Full clean
make clean
```
Expand All @@ -1073,6 +1086,7 @@ raglogs/
│ ├── api/routes/ FastAPI route handlers
│ ├── api/auth/ API keys, roles, OIDC, bind-host guard
│ ├── cli/commands/ Typer CLI commands
│ ├── clients/ Thin typed HTTP client targeting /v1
│ ├── config/ Pydantic settings
│ ├── core/
│ │ ├── clustering/ Fingerprint grouping, semantic merge, importance scoring
Expand All @@ -1088,6 +1102,7 @@ raglogs/
│ ├── db/ SQLAlchemy models, session management
│ └── utils/ Time window parsing, hashing helpers
├── migrations/ Alembic migration scripts
├── clients/ OpenAPI spec + client codegen docs
├── sample_data/ Demo incident logs (deploy, billing, api)
└── tests/
├── unit/ Tests — parsers, normalization, clustering, time
Expand Down
42 changes: 42 additions & 0 deletions clients/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# OpenAPI and generated clients

Canonical HTTP routes live under `/v1/`. Export the spec with:

```bash
make openapi
# writes clients/openapi.json
```

`clients/openapi.json` is the published contract. CI uploads it as a workflow
artifact on every push/PR and attaches it to GitHub Releases on tags.

## Python

A thin typed httpx client ships in-tree:

```python
from src.clients.v1 import RaglogsClient

with RaglogsClient("http://localhost:8000", token="rlk_…") as client:
explanation = client.explain(since="30m", no_llm=True)
```

`make client-python` optionally runs `openapi-python-client` into
`clients/python/generated/` when that tool is installed. Generated trees are
gitignored; prefer the committed client for day-to-day use.

```bash
pip install openapi-python-client # optional
make client-python
```

## Go

```bash
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
make client-go
```

`make client-go` writes `clients/go/client.go` when `oapi-codegen` is on
`PATH`. If the binary is missing it prints the install command above and
exits 0 so CI is not required to have Go.
Loading
Loading