sentinel

module
v0.0.0-...-36bd69e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: GPL-3.0

README

Sentinel

Minimalistic system to track errors for your backend service.

sdk → ingestor-go (HTTP :8080) → NATS JetStream → processor-go → PostgreSQL

dashboard-web (SvelteKit + Auth.js + Drizzle) reads the same PostgreSQL database to render issues, occurrences, organizations, and alerting configuration.

[!IMPORTANT] Several features in specs/ are marked "Completed", which records a merge event, not verified behavior. Before trusting any feature-complete claim, read docs/memory/VERIFIED_STATE.md — it records what has actually been proven to run, and with which command.

As of 2026-08-01 (b895df1 on main), all 32 rows of the E2E recovery plan's use-case matrix were green, covering key management, rate limiting, alerting, observability (structured logs, /metrics, a distributed trace joining ingestor → NATS → processor), and idempotent event writes, and CI was green on main — all 9 check runs — for the first time in this repository's history; the previous baseline b9e2018 was red. The suite has grown since (agent API e2e coverage, see below): as of 2026-08-17 (SENTINEL_E2E=1 go test -tags=e2e ./tests/e2e/81 passed, 0 skipped, per CLAUDE.md).

The dashboard features shipped before the 2026-08-01 milestone (org-wide alerts, invitation acceptance, issue relations, API-key management, member management) each merged with their own tests passing, and three of the five did not execute at runtime until the UI parity remediation (47 findings, all closed). "Merged" and "reviewed" are not evidence here; a named command with an observed result is.

Remaining known gaps, none of them a regression: the DLQ drainer ships gated off by default (DLQ_DRAINER_ENABLED/DLQ_DRAINER_EXECUTE), rate limiting is still non-atomic and fails open (VERIFIED_STATE.md S10), and invitations now depend entirely on working email delivery — a deliberate consequence of no longer returning the raw token to any client. The degradation buffer was deliberately deleted rather than fixed (S9).

Repository layout

  • apps/ingestor-go — auth, rate limiting, validation, publish to NATS. The only externally exposed service.
  • apps/processor-go — consumes NATS, normalizes/masks/fingerprints events, writes to PostgreSQL.
  • apps/dashboard-web — SvelteKit UI and JSON API, reads the same database. Also exposes an /api/agent/* surface for registered agents to triage issues (claim/comment/status/relations) — see docs/agents/SENTINEL_AGENT_GUIDE.md and docs/agents/openapi.agent.yaml.
  • packages/shared-go — shared Postgres pool, NATS pub/sub, and Redis client code.
  • packages/proto + gen/ — the ErrorEvent wire contract (buf + protovalidate).
  • packages/db-migrations — goose-based SQL migrations, one flat directory for every target (see docs/memory/ARCHITECTURE.md).
  • packages/sdk-go — the public Go client SDK.
  • tests/{unit,integration,contract,e2e,load} — root-module tests; integration tests use testcontainers.

The root module, packages/sdk-go, and packages/db-migrations are three separate Go modules joined by a committed go.work for local development (GOWORK=off is still used in CI-equivalent checks to exercise the real, published-pseudo-version dependency path — see docs/memory/ARCHITECTURE.md).

Prerequisites

Tool Version Check
Go 1.25+ (see go.mod) go version
Node.js 20+ node --version
pnpm 9+ pnpm --version
Docker or Podman, with Compose v2 any recent docker compose version
go-task 3.x task --version
jq any recent jq --version
NATS CLI any recent nats --version

go-task is not installed by default on a fresh machine — every task ... command below will fail with command not found until you install it. See the next section.

jq is a hard dependency of scripts/wait-healthy.sh (used by task test-e2e and task infra-up readiness checks); without it the script exits immediately with an error. The nats CLI is a hard dependency of scripts/nats-init.sh:14 (until nats server check connection ...) if you ever run that script directly on the host — without the binary the until loop never succeeds and the script hangs forever instead of failing. In the normal task infra-up / task test-e2e flow this isn't an issue: NATS stream setup runs inside the nats-init compose container, which bundles its own nats CLI (see scripts/Dockerfile.nats-init) — the host-installed nats CLI is only needed if you run ./scripts/nats-init.sh or task nats-init by hand against a NATS instance you started yourself.

Installing go-task

The commands below (verified against v3.52.0 on Linux) install a single static binary with no root required:

# Official installer — installs into ./bin by default; -b sets the target directory.
# Put a directory that's already on your $PATH, e.g. ~/.local/bin (create it first if needed).
mkdir -p ~/.local/bin
sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin

# Make sure that directory is on PATH (add to your shell rc if not already):
export PATH="$HOME/.local/bin:$PATH"

task --version

Alternatives if you prefer a package manager:

# Arch / CachyOS
sudo pacman -S go-task

# Homebrew (macOS/Linux)
brew install go-task

# via `go install` (slow: pulls a large dependency tree; only if you already have Go and no
# package manager available)
go install github.com/go-task/task/v3/cmd/task@latest

If you genuinely cannot install task, every task <name> command in this repo has a raw equivalent — see Raw command equivalents below.

Quickstart

git clone <this-repo>
cd sentinel

# 1. Environment
cp .env.example .env
# Edit .env only if you need to change ports/credentials; defaults work for local dev.

# 2. Start infrastructure + build and boot the app containers
#    (postgres, redis, nats, minio (attachment storage), a one-shot `migrate` container that applies
#    schema, then ingestor/processor/dashboard, which wait for `migrate` to succeed before starting.
#    jaeger (tracing) and a retention `cron` container also come up but are inert unless enabled.)
task infra-up
# or: docker compose up -d

# 3. Verify the ingestor is up
curl -s localhost:8080/health

# 4. Verify migrations landed
task db-migrate CMD=status

task infra-up brings up the whole stack including a migrate service that applies every pending migration before ingestor/processor/dashboard are allowed to start (see the depends_on: service_completed_successfully wiring in docker-compose.yml). task db-migrate (below) is for running migrations outside that flow — e.g. against a postgres container you started by hand, or to check status / roll back / re-baseline without restarting the whole stack.

Common tasks
task --list                 # see every available task with its description
task build                  # build ingestor, processor, and dashboard
task test-unit               # go test ./tests/unit/...
task test-integration        # go test ./tests/integration/... (testcontainers ONLY if nothing is on :8080 — otherwise it hits your shared dev DB; use FORCE_TESTCONTAINERS=1 to isolate)
task test-e2e                # infra-up, wait healthy, test-integration, infra-down
task dev-ingestor             # go run apps/ingestor-go against your local .env
task dev-processor            # go run apps/processor-go
task dev-dashboard            # pnpm dev in apps/dashboard-web
task infra-down               # stop the compose stack
task clean                   # stop stack (removing volumes+images) and remove build artifacts
Database migrations (task db-migrate)

Migrations live as flat, timestamp-versioned .sql files in packages/db-migrations/migrations/ and are applied by the goose-based CLI at packages/db-migrations/cmd/migrate (see decision "Adopt Goose for All Database Migrations" in docs/memory/DECISIONS.md). The db-migrate task is a single, parameterized wrapper around it — not five separate tasks, because go-task does not template task names (only values inside a task body), so a Taskfile that tries db:{{.TARGET}}-up: as a map key registers a task literally named db:{{.TARGET}}-up, which nobody can call. TARGET and CMD are ordinary task variables instead:

task db-migrate                                        # TARGET=processor CMD=up (defaults)
task db-migrate TARGET=ingestor CMD=status
task db-migrate TARGET=dashboard CMD=down
task db-migrate TARGET=processor CMD=baseline VERSION=1716508800
task db-migrate TARGET=processor CMD=reset CONFIRM=yes  # DANGEROUS: drops and recreates the schema

[!WARNING] packages/db-migrations/cmd/migrate requires migrate <command> [flags] — the command (up/down/status/baseline) must come before -target (and any other flag), not after. This isn't a standard Go flag package (which accepts flags before or after positional args); it's a hand-rolled loop in main.go that reads os.Args[1] as the command unconditionally, so -target=processor up fails with Error: -target is required while up -target=processor (or up -target processor) works. task db-migrate and the raw commands in this README already use the correct order — but if you invoke cmd/migrate directly (including from docker-compose.yml's migrate service), get the order wrong and it will fail fast with that misleading "-target is required" error rather than a parse error.

TARGET selects which DB_URL_<TARGET> environment variable to connect with (see .env.example) — processor, ingestor, and dashboard all point at the same single Postgres instance in local/compose dev (one flat migration directory applies identically regardless of which DSN was used, per docs/memory/ARCHITECTURE.md); this only matters if you ever point a target at a different database. If the relevant DB_URL_<TARGET> isn't set in your environment or .env, db-migrate falls back to postgres://sentinel:changeme@localhost:5432/sentinel?sslmode=disable, matching the default docker-compose.yml credentials.

Raw command equivalents (no go-task required)

Every task command above has an equivalent plain shell command. Taskfile.yml is the source of truth if these drift; the ones below were verified working against a live docker compose up -d postgres.

# infra-up / infra-down
docker compose up -d
docker compose down

# db-migrate (defaults: TARGET=processor CMD=up)
cd packages/db-migrations
DB_URL_PROCESSOR="postgres://sentinel:changeme@localhost:5432/sentinel?sslmode=disable" \
  go run ./cmd/migrate up -target processor
# status / down / baseline work the same way:
DB_URL_PROCESSOR="postgres://sentinel:changeme@localhost:5432/sentinel?sslmode=disable" \
  go run ./cmd/migrate status -target processor
DB_URL_PROCESSOR="postgres://sentinel:changeme@localhost:5432/sentinel?sslmode=disable" \
  go run ./cmd/migrate baseline -target processor -version 1716508800

# build-ingestor / build-processor
(cd apps/ingestor-go && go build -o bin/ingestor .)
(cd apps/processor-go && go build -o bin/processor .)

# build-dashboard
(cd apps/dashboard-web && pnpm install && pnpm build)

# dev-ingestor / dev-processor
(cd apps/ingestor-go && go run .)
(cd apps/processor-go && go run .)

# dev-dashboard
(cd apps/dashboard-web && pnpm dev)

# test-unit / test-integration / test-load
go test ./tests/unit/... -v
go test ./tests/integration/... -v -count=1
go test ./tests/load/... -v -count=1

# nats-init
./scripts/nats-init.sh

# db-shell
docker compose exec postgres psql -U sentinel -d sentinel

Using the Go SDK: APIKey vs ProjectKey (breaking change in v0.2.0)

packages/sdk-go's Config has two separate fields that are easy to conflate — getting this wrong is a silent 100% event-rejection rate (this was S16 in docs/memory/VERIFIED_STATE.md; it happened for real):

Field What it is Where it travels
APIKey The secret credential (sent_live_... for a project-scoped key, sent_org_... for an org-wide key) X-API-Key header only — never the body
ProjectKey The target project's unique name (projects.name) — an identifier, not a secret Event body, project_key field
sentinel.Init(sentinel.Config{
    APIKey:     "sent_live_...",        // the secret — required
    ProjectKey: "my-project",           // required for an org-wide key; optional for a project-scoped
                                         // key (whose project is already fixed by the credential — a
                                         // mismatch here is rejected with 403, never silently trusted)
    Endpoint:   "http://localhost:8080/ingest",
})

Before v0.2.0, a single ProjectKey field held the secret and was sent as project_key in the body; the server resolved that value against projects.name, never found it, and every event was accepted (202) then silently dead-lettered. Config.Validate() now detects the swap (an APIKey-shaped value in ProjectKey, or vice versa) and returns an error instead of failing silently — if you are upgrading from a pre-v0.2.0 client, move your secret into APIKey.

More documentation

Directories

Path Synopsis
apps
ingestor-go command
processor-go command
processor-go/degradation
Package degradation gates event processing on database health.
Package degradation gates event processing on database health.
processor-go/dlqmonitor
Package dlqmonitor turns packages/shared-go/nats.DLQStats into an actionable severity signal and, on a state transition, an operational alert dispatched through apps/processor-go/alerts.
Package dlqmonitor turns packages/shared-go/nats.DLQStats into an actionable severity signal and, on a state transition, an operational alert dispatched through apps/processor-go/alerts.
processor-go/procmetrics
Package procmetrics holds the processor's OTel metric instruments and the handful of narrow recording helpers that use them.
Package procmetrics holds the processor's OTel metric instruments and the handful of narrow recording helpers that use them.
processor-go/webhooks
Package webhooks implements N3b: the outbound webhook delivery dispatcher.
Package webhooks implements N3b: the outbound webhook delivery dispatcher.
gen
packages
shared-go/obs
Package obs is the shared observability foundation for the Go services (ingestor, processor): structured logging via log/slog, an OpenTelemetry provider bootstrap for traces and metrics, and the NATS header carrier that lets a W3C trace context cross the publish/consume hop.
Package obs is the shared observability foundation for the Go services (ingestor, processor): structured logging via log/slog, an OpenTelemetry provider bootstrap for traces and metrics, and the NATS header carrier that lets a W3C trace context cross the publish/consume hop.
db-migrations module
sdk-go module
tests
tools
dlq command
Command dlq is the replay tool D10 calls out as missing: "Nothing consumes the DLQ.
Command dlq is the replay tool D10 calls out as missing: "Nothing consumes the DLQ.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL