src/sydra/http.zig
Purpose
Implements SydraDB’s HTTP server:
- Accepts TCP connections and parses HTTP requests
- Routes requests to API handlers (
/api/v1/*,/metrics,/debug/*) - Bridges HTTP requests into the engine (ingest/query) and sydraQL execution
For the user-facing contract, see Reference: HTTP API.
See also
- Engine (ingest queue, WAL, memtable, retention)
- Types (
SeriesId,Point, hashing helpers) - Query execution entrypoint (
POST /api/v1/sydraql) - Compatibility layer overview (debug endpoints under
/debug/compat/*)
Public API
pub fn runHttp(handle: *alloc_mod.AllocatorHandle, eng: *Engine, port: u16) !void
- Binds
0.0.0.0:<port>withreuse_address = true. - Accepts connections in a loop.
- Spawns a detached thread per connection (
connectionWorker).
runHttp accept loop (excerpt)
pub fn runHttp(handle: *alloc_mod.AllocatorHandle, eng: *Engine, port: u16) !void {
var address = try std.net.Address.parseIp4("0.0.0.0", port);
var server = try address.listen(.{ .reuse_address = true });
defer server.deinit();
while (true) {
const connection = server.accept() catch |err| switch (err) {
error.ConnectionResetByPeer, error.ConnectionAborted => continue,
else => return err,
};
const worker = std.Thread.spawn(.{}, connectionWorker, .{ handle, eng, connection }) catch |spawn_err| {
std.log.err("http spawn failed: {s}", .{@errorName(spawn_err)});
connection.stream.close();
continue;
};
worker.detach();
}
}
Connection lifecycle
fn connectionWorker(...) void
- Uses
std.heap.c_allocatorfor request handling and JSON construction. - Calls
handleConnection(...).
fn handleConnection(...) !void
- Creates per-connection read/write buffers (
[4096]u8each). - Initializes
std.http.Server. - Loops
receiveHead()and callshandleRequest(...).
If Expect: 100-continue fails, the code replies 417 Expectation Failed and closes the connection.
Request routing and auth
fn handleRequest(...) !void
Routing:
- Splits
req.head.targetintopathandqueryat the first?. - Enforces auth for
/api/*only wheneng.config.auth_tokenis non-empty:- Requires
Authorization: Bearer <token> - Responds
401 unauthorizedwithkeep_alive = falseon failure
- Requires
API auth guard (excerpt)
if (std.mem.startsWith(u8, path, "/api/") and eng.config.auth_token.len != 0) {
const maybe_auth = findHeader(req, "authorization");
if (maybe_auth) |auth| {
if (!(std.mem.startsWith(u8, auth, "Bearer ") and std.mem.eql(u8, auth[7..], eng.config.auth_token))) {
try req.respond("unauthorized", .{ .status = .unauthorized, .keep_alive = false });
return;
}
} else {
try req.respond("unauthorized", .{ .status = .unauthorized, .keep_alive = false });
return;
}
}
Route table (path + method → handler):
GET /metrics→handleMetricsGET /debug/compat/stats→handleCompatStatsGET /debug/compat/catalog→handleCompatCatalogGET /debug/alloc/stats→handleAllocStatsGET /status→handleStatus(see “Known issues” below)POST /api/v1/ingest→handleIngestPOST /api/v1/query/range→handleQueryGET /api/v1/query/range→handleQueryGetPOST /api/v1/query/find→handleFindPOST /api/v1/sydraql→handleSydraql
Routing (excerpt)
if (std.mem.eql(u8, path, "/metrics") and method == .GET) {
return try handleMetrics(alloc, eng, req);
}
if (std.mem.eql(u8, path, "/api/v1/ingest") and method == .POST) {
return try handleIngest(alloc, eng, req);
}
if (std.mem.eql(u8, path, "/api/v1/sydraql") and method == .POST) {
return try handleSydraql(alloc, eng, req);
}
Handlers (high level)
fn handleMetrics(...) !void
Emits Prometheus text exposition from engine counters, including:
sydradb_ingest_totalsydradb_flush_totalsydradb_flush_seconds_totalsydradb_flush_points_totalsydradb_wal_bytes_totalsydradb_queue_depthsydradb_memtable_bytes
fn handleIngest(...) !void
Consumes NDJSON and ingests each line:
- Computes
series_idusingtypes.seriesIdFrom(series, tags_json)(see Types). - If
tagsis present, it is stringified to JSON (extractTagsJson) and also recorded viaeng.noteTags(...)(see Engine). - If
valueis missing, it will searchfieldsfor the first numeric value (iteration order dependent).
Returns {"ingested":<count>}.
fn handleQuery(...) !void (POST JSON)
- Requires
Content-Length. - Expects JSON
{start,end,series_id|series[,tags]}. - Calls
queryAndRespondand returns an array of points.
fn handleQueryGet(...) !void (GET query string)
Supports query parameters:
series_id=<u64>orseries=<string>tags=<string>(defaults to{})start=<i64>andend=<i64>
fn handleFind(...) !void
Accepts JSON:
tags(object): exact tag matchesop(string):"and"(default) or"or"
Returns an array of series_id values.
fn handleSydraql(...) !void
Executes sydraQL (POST body is plain text) and responds with:
columns: column metadatarows: row arraysstats: timings + operator stats (viawriteStatsObject)
handleSydraql request validation + exec (excerpt)
const content_len = req.head.content_length orelse {
return respondJsonError(alloc, req, .length_required, "length required");
};
if (content_len > 256 * 1024) {
return respondJsonError(alloc, req, .payload_too_large, "payload too large");
}
const len: usize = @intCast(content_len);
const body_slice = try body_reader.*.take(len);
const body = try alloc.dupe(u8, body_slice);
defer alloc.free(body);
const sydraql = std.mem.trim(u8, body, " \t\r\n");
if (sydraql.len == 0) {
return respondJsonError(alloc, req, .bad_request, "query required");
}
var cursor = query_exec.execute(alloc, eng, sydraql) catch |err| {
return respondExecutionError(alloc, req, err);
};
defer cursor.deinit();
Utilities and local types
const default_tags_json = "{}"const TagsJson = struct { value: []const u8, owned: ?[]u8 }fn extractTagsJson(...) !TagsJson– converts a JSON object into a JSON stringfn respondJsonError(...) !void–{"error":"..."}error payloadsfn writeStatsObject(...) !void– emits thestatsobject for sydraQL responsesfn findHeader(...) ?[]const u8– case-insensitive header lookup
Known issues (as observed in source)
- The
/statusroute check usestd.mem.eql(typo) instead ofstd.mem.eql, which will prevent building until corrected in the source.