Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
# AGENTS.md

## Communication style

When presenting changes, summaries, or any explanation to the user, follow `writing-style-guide.md`.
- Simplify the language, not the technical idea.
- Use short, active, spoken-style sentences.
- Show the concrete case before the general rule.
- No marketing, hype, filler, or unnecessary summaries.

## Toolchain

- **Zig 0.15.2** minimum, pinned in `build.zig.zon`
- **Zig 0.16.0** minimum, pinned in `build.zig.zon`
- Requires `librdkafka-dev` (`apt install librdkafka-dev` / `brew install librdkafka`)
- On macOS, `build.zig` hardcodes `/usr/local/Cellar/librdkafka/2.13.0` include/lib paths
- **Always `rm -rf .zig-cache zig-out zig-pkg/` before switching Zig versions** — stale cache causes build failures and runtime corruption
Expand Down Expand Up @@ -139,6 +147,6 @@ These are used consistently across the codebase and must be referenced as-is:
| 0.15.2 | Yes | 52/52 (7 leaks) | **Broken** | No log output, no HTTP server — `std.fs.File.stdout()` I/O change in logger.zig breaks httpz |
| 0.16.0 | Yes | 52+21+3 | Yes | Works in this env with vendored deps; benchmark HTTP server binds and serves |

- See `recommendation.md` for full analysis and 0.16.0 migration plan
- See `ZIG_LEARNINGS.md` for the 0.16.0 migration plan and upgrade notes
- **0.15.2 runtime issue**: `src/logger.zig` uses `std.fs.File.stdout().writer(&stdout_buffer)` pattern which silently fails under 0.15.2 — stdout fd becomes a socket, HTTP server never binds
- **0.16.0 now works here**: the 6 dependency `build.zig` files were updated for the `Module`-based link API and the deps are vendored in `zig-pkg/`, so `zig build {test,test-integration,test-validation,bench}` all pass under 0.16.0.
19 changes: 14 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,27 @@ clean:
rm -rf examples/zero-s3/.zig-cache examples/zero-s3/zig-out examples/zero-s3/zig-pkg
rm -rf examples/zero-autocrud/.zig-cache examples/zero-autocrud/zig-out examples/zero-autocrud/zig-pkg
rm -rf examples/zero-cli/.zig-cache examples/zero-cli/zig-out examples/zero-cli/zig-pkg
rm -rf examples/zero-duckdb/.zig-cache examples/zero-duckdb/zig-out examples/zero-duckdb/zig-pkg
rm -rf examples/zero-otel/.zig-cache examples/zero-otel/zig-out examples/zero-otel/zig-pkg
rm -rf examples/zero-search/.zig-cache examples/zero-search/zig-out examples/zero-search/zig-pkg
rm -rf examples/zero-nosql/.zig-cache examples/zero-nosql/zig-out examples/zero-nosql/zig-pkg
rm -rf examples/zero-timeseries/.zig-cache examples/zero-timeseries/zig-out examples/zero-timeseries/zig-pkg

release:
zig build --release=fast
fast:
zig build --release=fast --summary all

release-prod:
small:
zig build --release=small --summary all
zig build bench --release=small --summary all

release-base:
base:
zig build -Dcpu=baseline --release=safe --summary all

ut:
coverage:
zig build test -Dcoverage --summary all

log:
git log --pretty=format:"%h%x09%an%x09%ad%x09%s"

size:
ls -alth ./zig-out/bin
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@

**One binary. No GC. Build config-driven microservices in Zig.**

**Zero** is a batteries-included web framework for [Zig](https://ziglang.org) that wires REST, SQL, NoSQL, cache, pub/sub, auth, GraphQL, Protobuf, search, metrics and tracing into a single static binary and configured almost entirely through `.env`.
**Zero** is a batteries-included web framework for [Zig](https://ziglang.org). It wires REST, SQL, NoSQL, cache, pub/sub, auth, GraphQL, Protobuf, search, metrics, and tracing into one static binary, and you configure almost everything through `.env`.

- **Zero boilerplate** — databases, queues, auth and observability plug in with no glue code.
- **One static binary** — ~16–65 MiB RSS, no runtime, ships anywhere (including Kubernetes).
- **One static binary** — ~16–65 MiB RSS, no managed runtime, ships anywhere (including Kubernetes).
- **Observable by default** — structured JSON logs, Prometheus metrics, distributed tracing and health endpoints from the first request.
- **Fast and small** — tens of thousands of requests/sec at ~50 MiB RSS, no GC pauses, no JIT warm-up.

Expand Down Expand Up @@ -67,13 +67,13 @@ zig build run
curl localhost:8080/json # => {"msg":"hello zero!"}
```

That's the whole app. Everything else - Postgres, Redis, Kafka, auth, metrics, is opt-in through configuration.
That's the whole app. Everything else — Postgres, Redis, Kafka, auth, and metrics — is opt-in through configuration.

Full walkthrough in [Hello Zero](https://zerofmk.in/hello-zero) and [Getting Started](https://zerofmk.in/started).

## Why Zero?

If you want Go's ergonomics without its runtime, or Node's speed without its footprint, Zero gives you a strongly-opinionated Zig framework: explicit memory, a single binary, and the microservice building blocks you'd otherwise wire together by hand.
If you want Go's ergonomics without its runtime, or Node's speed without its footprint, Zero is a strongly-opinionated Zig framework. You get explicit memory control, a single binary, and the microservice building blocks you'd otherwise wire together by hand.

Start with [Getting Started](https://zerofmk.in/started) or jump straight to the [Examples](https://zerofmk.in/examples).

Expand Down Expand Up @@ -105,7 +105,7 @@ See [feature parity](https://zerofmk.in/parity) for the full roadmap.

Recent additions (full detail on [zerofmk.in](https://zerofmk.in)):

- **Zig 0.16 + `std.Io` injection** — `App.new(allocator, io, em)` threads the process I/O reactor through `container`/`Context`; tests are consolidated at each file's end. See [Migrating to 0.16](https://zerofmk.in/migrating-0.16).
- **Zig 0.16 + `std.Io` injection** — `App.new(allocator, io, em)` routes the process I/O reactor through `container`/`Context`; tests now live at the end of each file. See [Migrating to 0.16](https://zerofmk.in/migrating-0.16).

- **DuckDB in-process OLAP** — register an embedded SQL engine with `app.addDuckDB(":memory:")`, no external service. See [DuckDB](https://zerofmk.in/duckdb).

Expand All @@ -125,9 +125,9 @@ Recent additions (full detail on [zerofmk.in](https://zerofmk.in)):
- **Bootstrap arena** — Pre-allocated memory for framework bootstrap (bounded RSS). See [Architecture](https://zerofmk.in/architecture).

- **Outbound rate limiting & REST handlers** — per-service rate limits and struct-model REST handlers for external services. See [Rate Limiter](https://zerofmk.in/rate-limiter) and [REST Handler](https://zerofmk.in/rest-handler).
- **Resilience** — circuit breakers, request timeouts/bulkheads, and pub/sub reconnect + dead-letter. See [Resilience](https://zerofmk.in/resilience).
- **Resilience** — circuit breakers, request timeouts and bulkheads, and pub/sub reconnect with dead-letter. See [Resilience](https://zerofmk.in/resilience).

- **Observability** — distributed tracing and structured metrics/tracing wired in from the first request. See [Observability](https://zerofmk.in/observability).
- **Observability** — distributed tracing and structured metrics wired in from the first request. See [Observability](https://zerofmk.in/observability).

- **Benchmarks in CI** — reproducible throughput/latency/RSS runs. See [Benchmark](https://zerofmk.in/benchmark).

Expand Down Expand Up @@ -208,7 +208,7 @@ The complete list of keys (Redis, DuckDB, InfluxDB, Solr, Cassandra, Kafka, MQTT

## Resilience

Resilience is configured, not coded. Request timeouts/bulkheads, datasource circuit breakers, pub/sub auto-reconnect with dead-letter, and structured logging are all on by default or via env. For outbound services you can also set limits explicitly:
Resilience is configured, not coded. Request timeouts and bulkheads, datasource circuit breakers, pub/sub auto-reconnect with dead-letter, and structured logging are all on by default or set through env. For outbound services, you can also set limits explicitly:

```zig
var svc_opts: zero.client.ServiceOptions = .{};
Expand All @@ -231,7 +231,7 @@ See [Observability](https://zerofmk.in/observability)

## Data & Stores

Attach a datastore with one call; `ctx.SQL`, `ctx.KV`, `ctx.FileStore` light up automatically.
Attach a datastore with one call; `ctx.SQL`, `ctx.KV`, `ctx.FileStore` become available automatically.

```zig
// In-process OLAP SQL — no external service required.
Expand Down
2 changes: 1 addition & 1 deletion bench/baseline.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"scenarios":[{"name":"health","peak_rss_mib":102.140625,"drss_kib":5228,"leak":false},{"name":"health-json","peak_rss_mib":102.19921875,"drss_kib":100,"leak":false},{"name":"health-html","peak_rss_mib":102.2265625,"drss_kib":72,"leak":false},{"name":"index","peak_rss_mib":102.24609375,"drss_kib":64,"leak":false},{"name":"text","peak_rss_mib":102.28125,"drss_kib":80,"leak":false},{"name":"json","peak_rss_mib":102.3046875,"drss_kib":68,"leak":false},{"name":"keys","peak_rss_mib":102.30078125,"drss_kib":40,"leak":false},{"name":"db","peak_rss_mib":102.2890625,"drss_kib":68,"leak":false},{"name":"proto-get","peak_rss_mib":102.33203125,"drss_kib":88,"leak":false},{"name":"proto","peak_rss_mib":102.30859375,"drss_kib":20,"leak":false},{"name":"graphql-get","peak_rss_mib":103.01953125,"drss_kib":820,"leak":false},{"name":"graphql","peak_rss_mib":103.0859375,"drss_kib":112,"leak":false},{"name":"filestore-get","peak_rss_mib":101.59765625,"drss_kib":0,"leak":false},{"name":"filestore","peak_rss_mib":100.90625,"drss_kib":84,"leak":false},{"name":"duckdb-query","peak_rss_mib":106.88671875,"drss_kib":6168,"leak":false}]}
{"scenarios":[{"name":"health","peak_rss_mib":244.87109375,"drss_kib":6008,"leak":false},{"name":"health-json","peak_rss_mib":244.9453125,"drss_kib":112,"leak":false},{"name":"health-html","peak_rss_mib":244.96875,"drss_kib":64,"leak":false},{"name":"startup","peak_rss_mib":244.99609375,"drss_kib":68,"leak":false},{"name":"index","peak_rss_mib":245.01171875,"drss_kib":56,"leak":false},{"name":"text","peak_rss_mib":245.0390625,"drss_kib":68,"leak":false},{"name":"json","peak_rss_mib":245.06640625,"drss_kib":68,"leak":false},{"name":"keys","peak_rss_mib":245.09375,"drss_kib":68,"leak":false},{"name":"db","peak_rss_mib":245.125,"drss_kib":72,"leak":false},{"name":"duckdb-query","peak_rss_mib":254.8046875,"drss_kib":9952,"leak":false},{"name":"proto-get","peak_rss_mib":254.8359375,"drss_kib":72,"leak":false},{"name":"proto","peak_rss_mib":254.86328125,"drss_kib":68,"leak":false},{"name":"graphql-get","peak_rss_mib":255.57421875,"drss_kib":768,"leak":false},{"name":"graphql","peak_rss_mib":255.6015625,"drss_kib":68,"leak":false},{"name":"filestore-get","peak_rss_mib":255.6328125,"drss_kib":72,"leak":false},{"name":"filestore","peak_rss_mib":255.66015625,"drss_kib":68,"leak":false}]}
27 changes: 19 additions & 8 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/zero.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});

// OpenTelemetry SDK (alpha). The `sdk` module links libc itself; we also set
// link_libc on the zero module so every consumer artifact links libc too.
const opentelemetry = b.dependency("opentelemetry", .{});
module.addImport("opentelemetry-sdk", opentelemetry.module("sdk"));

// // `protobuf` is re-exported by `zero` (the generated `*.pb.zig` structs do
// // `@import("zero").protobuf`). It must be wired into the module so the
// // `zero-proto` (and any protobuf) example compiles.
Expand Down Expand Up @@ -79,6 +85,7 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/tests.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
test_module.addImport("pg", pgz.module("pg"));
test_module.addImport("httpz", httpz.module("httpz"));
Expand All @@ -93,6 +100,7 @@ pub fn build(b: *std.Build) void {
test_module.addImport("nats", nats.module("nats"));
test_module.addImport("protobuf", protobuf.module("protobuf"));
test_module.addImport("graphql", graphql.module("graphql"));
test_module.addImport("opentelemetry-sdk", opentelemetry.module("sdk"));
test_module.addImport("zero", module);

if (builtin.os.tag == .macos) {
Expand All @@ -116,6 +124,7 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/tests_integration.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
integration_module.addImport("pg", pgz.module("pg"));
integration_module.addImport("httpz", httpz.module("httpz"));
Expand All @@ -130,6 +139,7 @@ pub fn build(b: *std.Build) void {
integration_module.addImport("nats", nats.module("nats"));
integration_module.addImport("protobuf", protobuf.module("protobuf"));
integration_module.addImport("graphql", graphql.module("graphql"));
integration_module.addImport("opentelemetry-sdk", opentelemetry.module("sdk"));
integration_module.addImport("zero", module);

if (builtin.os.tag == .macos) {
Expand Down Expand Up @@ -157,6 +167,7 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/tests_validation.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
validation_module.addImport("pg", pgz.module("pg"));
validation_module.addImport("httpz", httpz.module("httpz"));
Expand All @@ -171,6 +182,7 @@ pub fn build(b: *std.Build) void {
validation_module.addImport("nats", nats.module("nats"));
validation_module.addImport("protobuf", protobuf.module("protobuf"));
validation_module.addImport("graphql", graphql.module("graphql"));
validation_module.addImport("opentelemetry-sdk", opentelemetry.module("sdk"));
validation_module.addImport("zero", module);

if (builtin.os.tag == .macos) {
Expand Down Expand Up @@ -201,6 +213,7 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/bench/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
bench_module.addImport("pg", pgz.module("pg"));
bench_module.addImport("httpz", httpz.module("httpz"));
Expand All @@ -214,6 +227,7 @@ pub fn build(b: *std.Build) void {
bench_module.addImport("sqlite", sqlite.module("sqlite"));
bench_module.addImport("nats", nats.module("nats"));
bench_module.addImport("graphql", graphql.module("graphql"));
bench_module.addImport("opentelemetry-sdk", opentelemetry.module("sdk"));
bench_module.addImport("zero", module);

if (builtin.os.tag == .macos) {
Expand Down Expand Up @@ -261,6 +275,11 @@ pub fn build(b: *std.Build) void {
.name = "zero",
.root_module = module,
});
const install_zero = b.addInstallArtifact(binary, .{});
const zero_step = b.step("zero", "Build the zero CLI (./zig-out/bin/zero)");
zero_step.dependOn(&install_zero.step);
// `zig build` (the default step) also produces the zero CLI.
b.getInstallStep().dependOn(&install_zero.step);

// Protobuf code generation. `zig build gen-proto` compiles .proto files in
// `proto/` into Zig structs under `src/proto/`. The first run downloads
Expand All @@ -278,12 +297,4 @@ pub fn build(b: *std.Build) void {
},
});
gen_proto.dependOn(&protoc_step.step);

if (b.option(
bool,
"install-zero",
"install zero cli",
) orelse false) {
b.installArtifact(binary);
}
}
11 changes: 10 additions & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.{
.name = .zero,
.version = "0.0.2",
.version = "0.5.1",
.fingerprint = 0xabdef192c03b44cb,
.minimum_zig_version = "0.16.0",
.dependencies = .{
Expand Down Expand Up @@ -66,6 +66,15 @@
.url = "git+https://github.com/im-ng/graphql-zig#92ea2176b6f1945bde6acf35af0f9ca3ff770af3",
.hash = "graphql-0.2.0-13OEDvCaAgCb8FLxnOIW_63Zn8Yu-UVyLbZYP7R8g6MD",
},
// OpenTelemetry SDK (alpha, tracks main). Gated behind OTEL_EXPERIMENTAL
// in config; see src/otel.zig. Tracks a pinned commit for reproducibility.
// .opentelemetry = .{
// .path = "../opentelemetry-zig",
// },
.opentelemetry = .{
.url = "git+https://github.com/im-ng/opentelemetry-zig.git#928f309694b0dc8ee02c45b4067b96f3d9cc01fd",
.hash = "opentelemetry-0.0.1-U_uKJ8NrIwD5nbZmmLowE41NMPGhiwKQ1sCb3K3L-0lV",
},
},
.paths = .{
"build.zig",
Expand Down
Loading
Loading