---
id: w1/m2.5/t004
title: Extract internal/logs (logs feature)
worker: worker1
status: done
estimate: 30m
depends_on: [w1/m2.5/t001]
---

## Objective

Move the logs feature into `internal/logs`: the `Logs`/`QueryLogs`/`FollowLogs` verbs, the `LogEntry` model + `parseLogLine`, the pod-log collection, the `PodLogSource`/`PodLogStream` injected dependencies, and the REST/GraphQL/MCP fragments (including the SSE live-tail) — plus tests.

## Context

- Source spans `internal/api/logs.go`, `podlogs.go`, `rest_logs.go`, the log parts of `core.go` (`Logs`, `collectPodLogs`, `readPodLogs`, `parseLogLine`, `LogEntry`, `PodLogSource`), and `logs_test.go`.
- Core stays clientset-free: logs reach pods only through the injected `PodLogSource`/`PodLogStream` (kept out of the domain so it fakes cleanly) — preserve that seam.
- MCP `list_logs` (resource array) and the REST query/live-tail must keep Render-consistent shapes.

## Steps

1. Create `internal/logs` with `service.go`, `models.go` (`LogEntry`, `parseLogLine`), the injected-source types, and `rest.go`/`graphql.go`/`mcp.go` fragments.
2. Depend on `core` for base/gate/errors (`ErrLogsUnavailable` stays with the feature or `core` as fits).
3. Move `logs_test.go`; keep the timestamp-sort / tail-cap / unavailable-source assertions.

## Files

- `lego/backend/internal/logs/{service.go,models.go,rest.go,graphql.go,mcp.go,*_test.go}`
- `lego/backend/internal/api/{logs.go,podlogs.go,rest_logs.go,core.go}` (remove moved code)

## Acceptance criteria

- [ ] `go build ./... && go test ./...` green.
- [ ] Query + live-tail reachable over REST/GraphQL/MCP; `list_logs` shape unchanged.
- [ ] No pod-log source wired ⇒ `ErrLogsUnavailable` surfaces as before.

## Out of scope

- Metrics (t005); apikeys/postgres (t006).
