---
id: w3/m5/t002
title: bex-api — BEX_LOKI_URL client; QueryLogs via LogQL from the existing LogQuery filters
worker: worker3
status: todo
estimate: 30m
depends_on: [w3/m5/t001]
---

## Objective

Teach the logs feature to read history from Loki: a `BEX_LOKI_URL` env var (like `BEX_PROM_URL`) selects a Loki-backed `QueryLogs` source that translates the existing `LogQuery` filter set (service, instance, level/type, text, limit, time range) into LogQL; when unset, the current pod-log path runs byte-identical. No REST/GraphQL/MCP shape changes — same envelope, same Render-matched limit semantics (default 20, cap 100).

## Context

- `lego/backend/internal/logs/service.go` — `PodLogSource` seam + `LogQuery.normalized()`; add a sibling Loki source behind the same domain interface rather than forking the service.
- Precedent for optional-backend wiring: `BEX_PROM_URL` (request metrics 503 when unset; here the fallback is pod logs, not 503).
- Time-range params exist in the query shape but are only best-effort over live logs today — Loki makes them real.

## Steps

1. Loki HTTP client (query_range) + `LogQuery` → LogQL mapping (label selectors from service/instance; line filters from text/level).
2. Source selection in the feature constructor from `BEX_LOKI_URL`; env-var row in CLAUDE.md table + `.env.example`/`.env.template` name mirrors if a var is added beyond the URL.
3. docs/ADR006-bex-api.md logs paragraph: note the backend swap, shapes unchanged.

## Files

- `lego/backend/internal/logs/` (loki source + wiring)
- `lego/operator/config/api/deployment.yaml` (env)
- `CLAUDE.md`, `docs/ADR006-bex-api.md`

## Acceptance criteria

- [ ] With `BEX_LOKI_URL` set: a time-range query returns historical lines matching the filters; limit semantics unchanged.
- [ ] Unset: pod-log behavior byte-identical (existing tests pass untouched).

## Out of scope

- The SSE tail (t003); retention (t004).
