# Monocle HTTP/SSE Server
Monocle provides an HTTP API server with SSE (Server-Sent Events) streaming for
BGP search and REST endpoints for all other Monocle capabilities.
## Starting the Server
```bash
monocle server
# Starting HTTP server on 127.0.0.1:8080 (auth: false)
```
With custom address/port and auth:
```bash
monocle server --address 0.0.0.0 --port 3000 --auth-enabled true --auth-token my-secret
```
## Endpoints
### `GET /health`
Returns `OK` (200). Intended for container health checks. Always open, even
when auth is enabled.
### `GET /api/v1/system/info`
Returns server metadata as JSON:
```json
{
"server_version": "1.4.0",
"api_version": "v1",
"endpoints": ["/health", "/api/v1/system/info", "/api/v1/search/stream", ...]
}
```
### `POST /api/v1/search/stream`
Streams BGP search results as Server-Sent Events (`text/event-stream`).
**Request body:**
```json
{
"filters": {
"prefix": ["1.1.1.0/24"],
"include_super": true,
"include_sub": false,
"start_ts": "2024-01-01T00:00:00Z",
"end_ts": "2024-01-01T00:10:00Z",
"collector": "rrc00",
"project": "riperis",
"dump_type": "updates"
},
"batch_size": 100,
"max_results": 10000
}
```
**Filter fields:**
| `prefix` | `Vec<String>` | Prefixes to match (CIDR) |
| `include_super` | `bool` | Include super-prefixes |
| `include_sub` | `bool` | Include sub-prefixes |
| `origin_asn` | `Vec<String>` | Filter by origin ASN |
| `peer_asn` | `Vec<String>` | Filter by peer ASN |
| `peer_ip` | `Vec<String>` | Filter by peer IP |
| `communities` | `Vec<String>` | Filter by BGP communities |
| `as_path` | `Option<String>` | AS path regex |
| `start_ts` | `String` | Start timestamp (required) |
| `end_ts` | `String` | End timestamp (required) |
| `collector` | `Option<String>` | Collector ID (e.g., `rrc00`) |
| `project` | `Option<String>` | Project (`riperis` or `routeviews`) |
| `dump_type` | `Option<String>` | `updates`, `rib`, or `rib_updates` |
**SSE events:**
| `started` | `{batch_size, max_results?, timeout_secs?}` | Stream started |
| `progress` | `SearchProgress` (varies) | Broker query, file start/complete, progress update |
| `elements` | `{total_so_far, collector, elements[]}` | Batch of BGP elements |
| `completed` | `SearchStreamResult` | Final: search completed or reached `max_results` |
| `cancelled` | `SearchStreamResult` | Final: client disconnected, with partial stats |
| `error` | `SearchStreamResult` | Final: search failed or timed out, with partial stats |
**Final event invariant:** A stream emits at most one final event
(`completed`, `cancelled`, or `error`). No events follow a final event.
`SearchStreamResult.stats.matched_elements` counts filtered BGP elements emitted
by the stream, not raw BGP messages. `source_bytes_compressed` is the
broker-advertised compressed size of selected files; `source_bytes_exact` is
false when any file required its rough-size fallback. `matching_collectors` and
`matching_files` list only sources that emitted at least one matched element.
**Cancellation:** Close the HTTP connection to cancel. The server detects the
drop and stops the search worker via an `Arc<AtomicBool>` flag.
**Backpressure:** The SSE channel is bounded (capacity 32). Element batches
are never dropped. Progress events may be coalesced under backpressure.
**Example:**
```bash
curl -N \
-H 'Accept: text/event-stream' \
-H 'Content-Type: application/json' \
-d '{"filters":{"start_ts":"2024-01-01T00:00:00Z","end_ts":"2024-01-01T00:01:00Z","collector":"rrc00","project":"riperis","dump_type":"updates"},"batch_size":100}' \
http://127.0.0.1:8080/api/v1/search/stream
```
**Browser (fetch streaming):**
```js
const res = await fetch('/api/v1/search/stream', {
method: 'POST',
headers: { 'content-type': 'application/json', 'accept': 'text/event-stream' },
body: JSON.stringify(request),
});
const reader = res.body.getReader();
// parse SSE frames from chunks
```
### Stateless REST endpoints
#### `POST /api/v1/time/parse`
```bash
curl -s -X POST http://localhost:8080/api/v1/time/parse \
-H 'Content-Type: application/json' \
-d '{"times":["2024-01-01T00:00:00Z","1704067200"]}'
```
#### `POST /api/v1/country/lookup`
```bash
curl -s -X POST http://localhost:8080/api/v1/country/lookup \
-H 'Content-Type: application/json' \
-d '{"query":"United"}'
```
#### `POST /api/v1/ip/lookup`
```bash
curl -s -X POST http://localhost:8080/api/v1/ip/lookup \
-H 'Content-Type: application/json' \
-d '{"ip":"1.1.1.1","simple":true}'
```
#### `GET /api/v1/ip/public`
```bash
curl -s http://localhost:8080/api/v1/ip/public
```
### Database-backed REST endpoints
These require local data to be loaded. If data is missing, they return
`503 NOT_INITIALIZED`. Refresh via the `/refresh` endpoints first.
#### `GET /api/v1/database/status`
```bash
curl -s http://localhost:8080/api/v1/database/status
```
#### `POST /api/v1/database/refresh`
```bash
# Refresh a single source
curl -s -X POST http://localhost:8080/api/v1/database/refresh \
-H 'Content-Type: application/json' \
-d '{"source":"rpki"}'
# Refresh all sources
curl -s -X POST http://localhost:8080/api/v1/database/refresh \
-H 'Content-Type: application/json' \
-d '{"source":"all"}'
```
Sources: `rpki`, `as2rel`, `asinfo`, `all` (`pfx2as` not yet supported via API).
#### `GET /api/v1/rpki/roa/lookup`
```bash
# All ROAs
curl -s http://localhost:8080/api/v1/rpki/roa/lookup
# By ASN
curl -s "http://localhost:8080/api/v1/rpki/roa/lookup?asn=13335"
```
#### `GET /api/v1/rpki/aspa/lookup`
```bash
# By customer ASN
curl -s "http://localhost:8080/api/v1/rpki/aspa/lookup?customer_asn=13335"
```
#### `POST /api/v1/rpki/roa/validate`
```bash
curl -s -X POST http://localhost:8080/api/v1/rpki/roa/validate \
-H 'Content-Type: application/json' \
-d '{"prefix":"1.1.1.0/24","asn":13335}'
```
#### `GET /api/v1/pfx2as/lookup`
```bash
curl -s "http://localhost:8080/api/v1/pfx2as/lookup?prefix=1.1.1.0/24&mode=longest"
```
#### `GET /api/v1/as2rel/relationship`
```bash
curl -s "http://localhost:8080/api/v1/as2rel/relationship?asn1=13335&asn2=1299"
```
#### `POST /api/v1/as2rel/search`
```bash
curl -s -X POST http://localhost:8080/api/v1/as2rel/search \
-H 'Content-Type: application/json' \
-d '{"asns":[13335]}'
```
#### `POST /api/v1/as2rel/refresh`
```bash
curl -s -X POST http://localhost:8080/api/v1/as2rel/refresh
```
#### `POST /api/v1/inspect/query`
```bash
curl -s -X POST http://localhost:8080/api/v1/inspect/query \
-H 'Content-Type: application/json' \
-d '{"queries":["13335","1.1.1.0/24"]}'
```
#### `POST /api/v1/inspect/refresh`
```bash
curl -s -X POST http://localhost:8080/api/v1/inspect/refresh
```
## Error Handling
**Pre-stream errors** (invalid params, validation failures): HTTP 400 or 503
with `ApiErrorResponse` JSON body. No SSE headers are sent.
**In-stream errors** (broker failure, parse error, timeout): SSE `error` event
with `ApiErrorResponse` as data. HTTP status is already 200.
```json
{
"code": "INVALID_PARAMS",
"message": "start-ts is not a valid time string: "
}
```
Error codes: `INVALID_REQUEST`, `INVALID_PARAMS`, `CANCELLED`, `SEARCH_FAILED`,
`NOT_INITIALIZED`, `INTERNAL_ERROR`.
## Auth
When `server_auth_enabled` is true, `/api/v1/*` requires:
```
Authorization: Bearer <token>
```
`/health` stays open for container health checks. The server refuses to start
if auth is enabled but the token is empty.
```bash
# Config
server_auth_enabled = true
server_auth_token = "my-secret"
# Env
MONOCLE_SERVER_AUTH_ENABLED=true
MONOCLE_SERVER_AUTH_TOKEN=my-secret
# CLI
monocle server --auth-enabled true --auth-token my-secret
```
## Configuration
Server settings are part of `MonocleConfig` and configurable via `monocle.toml`
or `MONOCLE_*` environment variables. See `monocle.toml.example` for all options.
```toml
search_concurrency = 0 # 0 = auto/rayon default
server_address = "127.0.0.1"
server_port = 8080
server_max_search_batch_size = 100
server_max_search_results = 0 # 0 = unlimited
server_search_timeout_secs = 0 # 0 = no timeout
server_max_concurrent_searches = 3 # 0 = unlimited; excess requests get 429
server_auth_enabled = false
server_auth_token = ""
```
CLI flags override config values:
```bash
monocle server --address 0.0.0.0 --port 9000 --max-search-batch-size 50 --concurrency 4
```
## Docker Deployment
```bash
# Build and run with docker compose
docker compose up -d
# Health check
curl http://localhost:8080/health
# Search
curl -N -H 'Accept: text/event-stream' \
-H 'Content-Type: application/json' \
-d '{"filters":{"start_ts":"2024-01-01T00:00:00Z","end_ts":"2024-01-01T00:01:00Z","collector":"rrc00","dump_type":"updates"}}' \
http://localhost:8080/api/v1/search/stream
```
Volumes:
- `/data/monocle` — SQLite database (RPKI, AS2Rel, Pfx2as, ASInfo)
- `/cache/monocle` — MRT file cache
See `docker-compose.yml` for full configuration including auth, cache TTLs,
and health checks.
## CLI Remote Search
The CLI can search against a remote Monocle service instead of locally:
```bash
monocle search -t 2024-01-01T00:00:00Z -T 2024-01-01T00:01:00Z \
-c rrc00 \
--remote-url http://monocle.example.net:8080/api/v1/search/stream \
--remote-token my-secret
```
Output is formatted using the same formatters as local search (`--format`,
`--json`, etc.).
## Architecture
```
src/server/
├── mod.rs — Server state, startup, /health, Axum router, auth wiring
├── auth.rs — Token-based auth middleware
├── http.rs — REST router, API error types, /api/v1/system/info
├── search.rs — SSE search handler, wire DTOs, sequential worker loop
└── rest/
├── mod.rs — Module declarations
├── time.rs — Time parsing
├── country.rs — Country lookup
├── ip.rs — IP information lookup
├── rpki.rs — RPKI ROA/ASPA lookup and ROA validation
├── pfx2as.rs — Prefix-to-ASN lookup
├── as2rel.rs — AS relationship search/lookup/refresh
├── inspect.rs — Unified AS/prefix inspection
└── database.rs — Database status and refresh
```
The search worker runs in `spawn_blocking` and calls `SearchFilters` utility
methods (`to_broker_items`, `to_parser`) directly. No new lens method is
needed — the lens layer is unchanged. Cancellation uses `Arc<AtomicBool>` set
when the SSE response is dropped.
All DB-backed REST handlers open `MonocleDatabase` per-request in
`spawn_blocking` (the database connection is `!Send`). All DB-backed endpoints
return `503 NOT_INITIALIZED` if required data is missing — there is no
auto-refresh (users refresh via explicit `/refresh` endpoints).