Dravyn Tech · Monitoring & Threat Detection

Dravyn Sentinel, end to end

Application monitoring and security-event detection you self-host or point at Dravyn's own instance — request timing, error tracking, security-pattern flagging, and alerting, with SDKs for Node and Python.

FastAPI + ClickHouse Node.js & Python SDKs Real-time dashboard Core pipeline shipped MVP — see feature matrix

What Dravyn Sentinel is

A small SDK in your app, an ingestion API that stores everything it sends, and a dashboard that turns that stream into request-rate, error-rate, and latency charts — plus automatic flagging of SQL-injection, XSS, and brute-force patterns.

Instead of wiring up separate error tracking, APM, and log search tools, one SDK call captures all three: capture() for arbitrary log events, an auto-instrumented middleware for request timing, and uncaught-exception hooks for errors. Every event lands in the same store and is searchable from the same dashboard.

It's not a wrapper around a third-party SaaS. The ingestion API is a plain FastAPI service backed by your own ClickHouse instance (or SQLite for local dev), with Resend for alert email and an optional Slack webhook — both swappable via environment variables.

Core concepts

Event
The one row shape everything becomes: request, error, security, or log. Always carries a project_id, a type, and a timestamp; other fields depend on the type.
Project
A string ID you choose ("my-app") that every event is scoped to. There's no project-registration step — the first event ingested for a new project_id just starts showing up.
API key
A bearer token in SENTINEL_API_KEYS (comma-separated on the server). Any valid key can read or write any project — there's no per-key project scoping yet.
Security event
A request or log event that the rule engine re-tagged as type: "security" after matching a SQLi/XSS pattern or a brute-force login streak.
Stats window
A one-minute bucket of aggregated request count, error count, and p50/p95/p99 latency — what the dashboard's charts are built from.
Alert cooldown
Once an error-rate alert fires for a project, it won't fire again for that project for 10 minutes, even if the error rate stays high.

Architecture & repo layout

Three moving pieces, one event schema flowing through all of them: your app + SDK → ingestion API (FastAPI, :8000) → event store (ClickHouse, Postgres, or SQLite) → dashboard (Next.js, :3000).

ingestion/ # FastAPI log-ingestion API + storage layer app/main.py # App wiring, CORS, uptime-loop lifespan app/schemas.py # Unified Event model (request/error/security/log) app/store/ # EventStore: ClickHouseEventStore | PostgresEventStore | SqliteEventStore app/routers/ # /v1/events /v1/stats /v1/logs app/security_rules.py # SQLi/XSS pattern match + brute-force counter app/alerts.py # Error-rate threshold -> Resend email / Slack app/uptime.py # Background endpoint pinger -> logged as events app/ratelimit.py # In-memory sliding-window limiter (per API key) packages/ sentinel-js/ @dravyn/sentinel # Node.js/TS SDK sentinel-python/ dravyn-sentinel # Python SDK apps/ dashboard/ # Next.js 14 + Recharts real-time dashboard docker-compose.yml # ClickHouse + ingestion + dashboard, wired together

Stack per layer

LayerChoiceWhy
Ingestion APIFastAPIAsync, typed request/response models, auto-generated OpenAPI
Event store (scale)ClickHouseColumnar store built for high-volume append-only event data
Event store (free-tier prod)Postgres (Neon)Real persistence with no paid infra — events survive Render free-plan spin-downs/redeploys; swap in via SENTINEL_STORE=postgres
Event store (dev)SQLiteZero-infra local iteration — same EventStore interface, swap via SENTINEL_STORE
Alert emailResendSame provider Dravyn Auth and Dravyn Web use
DashboardNext.js 14 + RechartsServer routes keep the API key off the client; polling-based charts

Feature matrix

What's actually running today versus what still needs rolling-baseline anomaly detection or WebSocket push to feel complete.

Shipped

Log ingestion API + ClickHouse/Postgres/SQLite storageshipped
Node.js SDK — capture, captureError, Express middlewareshipped
Python SDK — capture, capture_exception, FastAPI middlewareshipped
Alerting — Resend email + Slack webhook, per-project cooldownshipped
Log search UI — filter by level/service/user/textshipped
Real-time dashboard (5s polling)shipped
Uptime monitoring — background pingershipped
Security-pattern detection — SQLi/XSS regex + brute-force countershipped, MVP rules
Self-serve API keys — Dravyn Auth login, per-key project scopingshipped

Partial / not built

Rolling-baseline anomaly detection (currently a fixed error-rate threshold)partial
WebSocket-pushed dashboard updates (currently polling)partial
IP-geolocation checks in security detectionnot built
Status page generator for uptime monitoringnot built
Redis-backed rate limiting / cooldown (currently single-process in-memory)not built
Read before relying on security detection for blocking

The SQLi/XSS regexes are intentionally broad and will produce false positives on legitimate text. They tag and alert — they never block a request. Tune _SQLI_PATTERN / _XSS_PATTERN in security_rules.py per project before treating this as a WAF.

Getting started

Full stack (Docker)

terminal
cd "Dravyn Sentinel"
docker compose up --build
# dashboard:  http://localhost:3000
# ingestion:  http://localhost:8000

Starts ClickHouse, the ingestion API, and the dashboard together — reads a .env file next to docker-compose.yml automatically.

Ingestion API only, no Docker

terminal
cd ingestion
python3.12 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000

Falls back to a local SQLite file (dev/test only) and loads ingestion/.env automatically via python-dotenv.

Python version

Use Python ≤ 3.13. Python 3.14 fails to build pydantic-core from source — PyO3 doesn't support 3.14 yet and no prebuilt wheel exists. Install 3.12 with brew install python@3.12 if needed.

Self-serve API keys

No admin required — generate your own scoped key from the dashboard.

The dashboard logs you in via Dravyn Auth on any page you visit (Sentinel is registered as its own Dravyn Auth project, separate from Dravyn Auth's own accounts). Head to /keys once logged in, create a project, and generate a key for it in a few clicks.

  1. Open /keys, create an account (first/last name, email + password with a strength meter, OTP-verified) or log in
  2. Name a project and pick a platform (Node.js, Python, or Other/REST) — the name is slugified into the project_id your SDK will use, e.g. "My App" → my-app; the platform just decides which quickstart snippet you're shown next, it doesn't restrict what actually calls the ingest API
  3. Click Generate key — the raw key is shown once, alongside a ready-to-paste quickstart snippet for the platform you picked

Signup can ask for more than name, email, and password — Dravyn Auth projects (including this one) can define extra registration fields from its portal. If Sentinel's project has any defined, they render automatically on this form via getCustomFields()/register(email, password, profile, customFields), and the values come back on the account like any other profile data.

Scoped, not admin

A self-serve key only works for the one project it was issued for — sending events tagged with a different project_id returns 403. This is different from the legacy SENTINEL_API_KEYS env-var allowlist (still used internally by Dravyn's own products), where any key can touch any project.

Everything here is persistent

Projects & keys are backed by Postgres (ACCOUNTS_DATABASE_URL), and raw event/log data is too (SENTINEL_STORE=postgres, SENTINEL_DATABASE_URL — defaults to the same database as ACCOUNTS_DATABASE_URL unless set separately). Nothing here still lives in a local file on the free plan, so none of it is wiped by Render's spin-downs or redeploys — see §15.

API reference

Every endpoint below requires Authorization: Bearer <api-key> except /healthz. Base URL defaults to http://localhost:8000 locally, https://dravyn-sentinel-ingestion.onrender.com in production (see §15 — no custom domain yet).

POST /v1/events

Ingest a batch of events. Runs security-rule classification and the alert-threshold check before returning.

request
{
  "events": [
    { "project_id": "demo", "type": "request", "method": "GET",
      "route": "/users", "status_code": 200, "duration_ms": 42 }
  ]
}
-> 200 { "data": { "accepted": 1, "flagged_security_events": 0 }, "meta": {} }

GET /v1/stats?project_id=&since_minutes=

Per-minute aggregates: request count, error count, p50/p95/p99 latency. since_minutes defaults to 60, capped at 1440.

GET /v1/logs?project_id=&level=&service=&user_id=&q=&since_minutes=&limit=

Search recent events, newest first. level/service/user_id are exact match, q is a substring match on message. limit defaults to 200, capped at 1000.

GET /healthz

No auth required. Returns {"status": "ok"} — use for container/load-balancer health checks.

Self-serve projects & keys — /v1/projects

A different auth model from everything else on this page: these take Authorization: Bearer <dravyn-auth-session-token>, not a Sentinel API key — this is the "logged into the dashboard" surface (see §06), not something your SDK calls.

RouteBody / paramsReturns
POST /v1/projectsname, platform — platform is "node" | "python" | "other"project, 404 if the slugified name already exists
GET /v1/projects—projects owned by the calling Dravyn Auth user
POST /v1/projects/:id/keys—{ api_key, ...record } — raw key shown once, never again
GET /v1/projects/:id/keys—keys (prefix only)
DELETE /v1/projects/:id/keys/:keyId—{ revoked: true }

Rate limits & errors

6,000 requests per rolling 60-second window, per API key. Exceeding it returns 429 with a Retry-After header. Every response follows { "data": ..., "meta": {} } on success, { "detail": ... } on error.

Node.js SDK — @dravyn/sentinel

server startup
import { init } from "@dravyn/sentinel";

init({
  apiKey: process.env.SENTINEL_API_KEY!,
  projectId: "my-app",
  ingestUrl: "https://dravyn-sentinel-ingestion.onrender.com", // or http://localhost:8000
  service: "api",
  captureUncaught: true, // hooks process uncaughtException / unhandledRejection
});
Express
import { sentinelMiddleware, sentinelErrorHandler } from "@dravyn/sentinel/express";

app.use(sentinelMiddleware());     // records one `request` event per response
// ...your routes...
app.use(sentinelErrorHandler());   // captures thrown errors before your handler
manual capture
import { capture, captureError } from "@dravyn/sentinel";

try {
  await chargeCard(order);
} catch (err) {
  captureError(err, { orderId: order.id });
  throw err;
}

Delivery is best-effort and non-blocking — if the ingestion API is unreachable, flush() swallows the error rather than crashing your app.

Python SDK — dravyn-sentinel

server startup
import dravyn_sentinel as sentinel

sentinel.init(
    api_key=os.environ["SENTINEL_API_KEY"],
    project_id="my-app",
    ingest_url="https://dravyn-sentinel-ingestion.onrender.com",
    service="api",
)
FastAPI
from dravyn_sentinel.fastapi import SentinelMiddleware

app.add_middleware(SentinelMiddleware)   # captures requests + unhandled exceptions
manual capture
from dravyn_sentinel import capture, capture_exception

try:
    process_payout(batch)
except Exception as exc:
    capture_exception(exc, {"batch_id": batch.id})
    raise

The client flushes on a background daemon thread and registers an atexit hook — a final flush runs on normal interpreter shutdown, but not on SIGKILL or a hard crash.

Security event detection

Every ingested request or log event is scanned before storage. A match re-tags the event type: "security" and bumps its level to at least warn.

CategoryTrigger
sql_injectionRegex match for UNION SELECT, OR 1=1, DROP TABLE, trailing SQL comments, tautology quotes
xssRegex match for <script>, onerror=, javascript:
brute_force≥5 401 responses for the same user_id within a 5-minute rolling window

Find flagged events with GET /v1/logs?project_id=…&level=warn, or filter the dashboard's Logs page by level and look at the Type column.

Alerting

After every ingest call, a background task checks the last 5 minutes of stats for that project. If the error rate crosses SENTINEL_ERROR_RATE_THRESHOLD (default 5%) with at least SENTINEL_ALERT_MIN_SAMPLES requests (default 20), it fires once via Resend email and/or a Slack webhook, then goes quiet for a 10-minute cooldown.

VariableMeaning
RESEND_API_KEYRequired for email delivery
SENTINEL_ALERT_EMAILRecipient address
SENTINEL_ALERT_EMAIL_FROMSender — must be a verified Resend domain
SENTINEL_ALERT_SLACK_WEBHOOKIncoming webhook URL; unset skips Slack
Fails silently

A misconfigured RESEND_API_KEY won't crash ingestion — it just won't deliver. Confirm SENTINEL_ALERT_EMAIL is set before assuming alerting is live.

Uptime monitoring

A background loop pings every URL in SENTINEL_UPTIME_TARGETS (comma-separated project_id=url pairs) on an interval and records the result as a log event with service: "uptime-monitor" — no separate status-page UI, it shows up in the same stats/search surface as everything else.

.env
SENTINEL_UPTIME_TARGETS=dravyn-web=https://dravyn.it.com,dravyn-auth=https://auth.dravyn.it.com
SENTINEL_UPTIME_INTERVAL_SECONDS=60

Dashboard

A Next.js app at apps/dashboard, entirely gated behind Dravyn Auth login — every page, not just §06's key management. There's no server-side admin key or hardcoded default project anymore; after logging in you see only your own projects, picked from a switcher in the nav, and every request goes straight from your browser to Dravyn Auth or the ingestion API using your own session.

  • Overview (/) — stat tiles, requests-vs-errors chart, latency-percentile chart for the selected project, polling every 5s.
  • Logs (/logs) — free-text search, level filter, table with time/level/type/service/message/route/status/duration, polling every 5s. Which project you're looking at is an explicit selector at the top of the page (not just the one in the nav bar) — the heading names it too, so it's never ambiguous which project's logs are on screen.
  • Projects & Keys (/keys) — create projects (name + platform), generate/revoke API keys, see a platform-specific quickstart snippet right after generating a key.

Viewing your own stats and logs needs no API key at all — /v1/stats and /v1/logs accept your Dravyn Auth session directly, checked against project ownership. A raw key is only for your own SDK integration code.

Fixed since the previous revision

This used to be a single unauthenticated project view (SENTINEL_DEFAULT_PROJECT_ID) with a bolted-on authenticated key-management page. The whole dashboard is gated now — see the "Recently shipped" callout in §15.

Deploying

PieceWhere
Ingestion APIDeployed via render.yaml, live at dravyn-sentinel-ingestion.onrender.com (free plan)
ClickHouseManaged instance or self-hosted — one node is enough at MVP scale
Dashboardapps/dashboard/Dockerfile, or Vercel like the other Dravyn Next.js apps

Lock allow_origins in main.py down to the dashboard's real origin before production — it's wide open by default for local dev. Move the rate limiter and alert cooldown to Redis before running more than one ingestion replica; both hold state in-process today.

What's left

Everything in the feature matrix marked partial or not built, plus:

  • A custom domain for the ingestion API — it's live on Render today at dravyn-sentinel-ingestion.onrender.com; sentinel.dravyn.it.com ended up pointed at the dashboard instead, so the API still needs its own hostname (a subdomain like ingest.sentinel.dravyn.it.com CNAME'd at it).
  • ClickHouse for real scale — the Postgres event store (SENTINEL_STORE=postgres) is real persistence, but it's a single table with bucketing/percentiles computed in Python, not a columnar store with query-time aggregation. Fine for self-serve/early-stage volume; switch to SENTINEL_STORE=clickhouse once that stops being true.
Recently shipped

Both SDKs are published (npm install @dravyn/sentinel, pip install dravyn-sentinel), self-serve API keys enforce per-project scoping, the entire dashboard requires Dravyn Auth login and shows only your own projects and data, and everything — self-serve projects/keys and raw event/log data alike — now persists on free-tier Postgres instead of a wipeable local file. Nothing on the free plan is lost to spin-down or redeploy anymore.