File Format

Zentinel uses KDL as its primary configuration format. KDL is a human-friendly document language that’s easy to read, write, and diff.

Why KDL?

FeatureBenefit
Human-readableClean syntax without excessive punctuation
Git-friendlyDiffs are clear and meaningful
Typed valuesNumbers, strings, booleans, #null
CommentsBoth line (//) and block (/* */)
HierarchicalNatural nesting for configuration blocks

Basic Syntax

Nodes

KDL documents are made of nodes. Each node has a name and optional values/properties:

// Simple node
my-node

// Node with a value
my-setting 4

// Node with a property
my-check type="http" interval-secs=10

// Node with children (block)
my-config {
    my-setting 4
    max-items 10000
}

Values and Properties

Values are positional arguments:

node-name "api"           // "api" is a value
multi-value "10.0.0.1" "10.0.0.2"  // Multiple values

Properties are named with =:

target address="10.0.0.1" weight=5
health-check type="http" interval-secs=10

Data Types

// Example showing KDL data types in Zentinel config

system {
    // Numbers (integer)
    worker-threads 4
    max-connections 1000
    graceful-shutdown-timeout-secs 30
}

listeners {
    listener "http" {
        // Strings (quoted)
        address "0.0.0.0:8080"
        protocol "http"
    }
}

routes {
    route "default" {
        // Booleans (#true, #false)
        enabled #true

        matches { path-prefix "/" }
        upstream "backend"
    }
}

upstreams {
    upstream "backend" {
        targets {
            target {
                address "127.0.0.1:3000"
                // Numbers (float)
                weight 1.5
            }
        }

        // Null values (#null)
        health-check #null
    }
}

Comments

// Line comment

/* Block comment
   spanning multiple
   lines */

system {
    worker-threads 4  // Inline comment
}

File Structure

A typical Zentinel configuration has these top-level blocks:

// System settings (use "system", not "server")
system {
    // ...
}

// Network listeners
listeners {
    listener "http" { address "0.0.0.0:8080" }
}

// Request routing
routes {
    // ...
}

// Backend servers
upstreams {
    // ...
}

// External agents (optional)
agents {
    // ...
}

// Request/response limits
limits {
    // ...
}

// Logging and metrics (optional)
observability {
    // ...
}

// Hierarchical organization (optional)
namespace "api" {
    limits { /* namespace-scoped limits */ }
    listeners { /* namespace-scoped listeners */ }
    upstreams { /* namespace-scoped upstreams */ }
    routes { /* namespace-scoped routes */ }
    agents { /* namespace-scoped agents */ }

    // Fine-grained isolation within namespace
    service "payments" {
        limits { /* service-scoped limits */ }
        listener "service-listener" { address "0.0.0.0:8081" }
        upstreams { /* service-scoped upstreams */ }
        routes { /* service-scoped routes */ }
    }
}

Note: The server block name is deprecated but still supported for backward compatibility. Use system for new configurations.

Schema Versioning

Zentinel configurations include a schema version for compatibility checking. This helps catch configuration issues when upgrading Zentinel.

// Declare schema version at the top of your config
schema-version "1.0"

system {
    // ...
}

Version Format

Schema versions use major.minor format:

VersionMeaning
1.0Initial stable schema
1.1Minor additions (backward compatible)
2.0Major changes (may require migration)

Compatibility Behavior

Config Version vs ZentinelResult
Exact match✓ Loads normally
Config older but supported✓ Loads normally
Config newer than Zentinel⚠ Loads with warning (some features may not work)
Config older than minimum✗ Rejected with error
Invalid format✗ Rejected with error

Omitting Version

If schema-version is not specified, Zentinel assumes the current version. For production deployments, explicitly specifying the version is recommended:

// Explicit version (recommended for production)
schema-version "1.0"

system { /* ... */ }

This ensures configuration files remain compatible when upgrading Zentinel, and provides clear error messages if migration is needed.

Complete Example

// Zentinel Configuration
// Production API Gateway

schema-version "1.0"

system {
    worker-threads 0          // 0 = auto-detect CPU cores
    max-connections 10000
    graceful-shutdown-timeout-secs 30
}

listeners {
    listener "https" {
        address "0.0.0.0:443"
        protocol "https"
        tls {
            cert-file "/etc/zentinel/certs/server.crt"
            key-file "/etc/zentinel/certs/server.key"
            min-version "1.2"
        }
    }

    listener "admin" {
        address "127.0.0.1:9090"
        protocol "http"
    }
}

routes {
    route "api" {
        priority 100
        matches {
            path-prefix "/api/"
            method "GET" "POST" "PUT" "DELETE"
        }
        upstream "backend"
        agents "auth" "ratelimit"
    }

    route "health" {
        priority 1000
        matches {
            path "/health"
        }
        service-type "builtin"
        builtin-handler "health"
    }
}

upstreams {
    upstream "backend" {
        targets {
            target { address "10.0.1.1:8080" weight=3 }
            target { address "10.0.1.2:8080" weight=2 }
            target { address "10.0.1.3:8080" weight=1 }
        }
        load-balancing "weighted_round_robin"
        health-check {
            type "http"
            path "/health"
            interval-secs 10
            timeout-secs 5
        }
    }
}

agents {
    agent "auth" {
        type "auth"
        unix-socket "/tmp/zentinel/auth.sock"
        timeout-ms 100
        failure-mode "closed"
    }

    agent "ratelimit" {
        type "rate_limit"
        unix-socket "/tmp/zentinel/ratelimit.sock"
        timeout-ms 50
        failure-mode "open"
    }
}

limits {
    max-header-size-bytes 8192
    max-header-count 100
    max-body-size-bytes 10485760  // 10MB
}

Alternative Formats

Zentinel also supports JSON and TOML for programmatic generation:

JSON

{
  "system": {
    "worker_threads": 4,
    "max_connections": 10000
  },
  "listeners": [
    {
      "id": "http",
      "address": "0.0.0.0:8080",
      "protocol": "http"
    }
  ]
}

TOML

[system]
worker_threads = 4
max_connections = 10000

[[listeners]]
id = "http"
address = "0.0.0.0:8080"
protocol = "http"

File format is auto-detected by extension:

  • .kdl → KDL
  • .json → JSON
  • .toml → TOML

Multi-File Configuration

For complex deployments, split configuration across multiple files:

/etc/zentinel/
├── zentinel.kdl        # Main config (includes others)
├── routes/
│   ├── api.kdl
│   ├── static.kdl
│   └── admin.kdl
├── upstreams/
│   ├── backend.kdl
│   └── cache.kdl
└── agents/
    └── security.kdl

Use directory loading:

zentinel --config-dir /etc/zentinel/

Or use include directives in your main config:

// zentinel.kdl
include "routes/*.kdl"
include "upstreams/*.kdl"
include "agents/*.kdl"

Include Behavior

The include directive is processed as a pre-processing step when loading configuration with Config::from_file() or zentinel --config. Include directives are resolved before the configuration is parsed, so included files can contain any top-level blocks (routes, upstreams, agents, etc.).

Glob patterns — Include paths support glob patterns (*, **, ?). Matched files are sorted alphabetically for deterministic ordering.

Relative paths — Patterns are resolved relative to the directory of the file containing the include directive. This means included files can themselves include other files using paths relative to their own location.

Recursive includes — Included files can contain their own include directives, which are expanded recursively.

Circular include detection — Zentinel tracks which files have already been included (using canonical paths) and returns an error if a circular include is detected.

No-match behavior — If a glob pattern matches no files, Zentinel logs a warning but continues loading. This allows optional includes like include "overrides/*.kdl" where the directory may be empty.

// routes/api.kdl — included file contains a top-level block
routes {
    route "api" {
        match {
            path-prefix "/api"
        }
        upstream "backend"
    }
}

Validation

Validate configuration before applying:

# Check syntax and semantics
zentinel --config zentinel.kdl --validate

# Dry-run mode
zentinel --config zentinel.kdl --dry-run

Common validation errors:

ErrorCause
Route references unknown upstreamTypo in upstream name
No listeners definedMissing listeners block
Invalid socket addressWrong format (need host:port)
Duplicate route IDTwo routes with same name

Hot Reload

Zentinel supports configuration reload without restart:

# Send SIGHUP to reload
kill -HUP $(cat /var/run/zentinel.pid)

# Or use the admin endpoint
curl -X POST http://localhost:9090/admin/reload

Reload behavior:

  1. Parse new configuration
  2. Validate syntax and semantics
  3. If valid, atomically swap configuration
  4. If invalid, keep old configuration and log error

Next Steps