Persistence: - one delivery state per sink, with claim, crash recovery, and index replacement - retention for delivered and dead records - LMDB writes moved off the event loop Pipeline: - one attempt per dispatch, with the next try scheduled by next_retry_at - backpressure through max_queue_size - reload re-reads YAML; SIGTERM drains and SIGHUP reloads Plugins: - journald source, enricher and filter processors, Loki sink - built-ins register when entry points are missing API and CLI: - health, metrics, per-sink queue, and dead-letter replay - startup prints the pipeline on one line Tests: - dispatch, plugins, metrics, CLI, and persistence coverage
8.2 KiB
daena
Collect, buffer, and forward logs and events from Linux to central observability backends.
Overview
Daena is a modular, extensible Linux service that:
- Collects logs, events, and telemetry from pluggable sources
- Normalizes and processes records through a configurable pipeline
- Persists data locally in LMDB for durability, replay, and buffering
- Forwards records reliably to one or more remote sinks
- Exposes a gRPC API for management and inspection
- Provides a CLI for operational tasks
- Degrades gracefully under failures with retry, backoff, and dead-letter queues
Architecture
Sources → [Processors] → LMDB (one delivery per sink) → dispatchers
↻ per-sink retry, no head-of-line sleep
Key design decisions:
- Plugin architecture: Sources, processors, and sinks are loaded via
Python entry points. Add new ones by installing a package or registering
in
pyproject.toml. - Pluggable persistence: The
Backendprotocol abstracts storage. MVP uses LMDB. Future: NATS JetStream. - Reliability-first: Records survive restarts, sink failures trigger exponential backoff, and permanently failed records land in a dead-letter queue.
Quick start
# Prerequisites: pyenv with Python 3.13, uv installed
# Clone and set up
git clone <repo> && cd daena
pyenv local 3.13.5
uv sync --extra dev
# Run with dummy source/sink (no external dependencies)
uv run python -c "
import asyncio
from daena.config import DaenaConfig, SourceConfig, SinkConfig
from daena.core.runtime import DaenaRuntime
async def main():
config = DaenaConfig()
config.sources = [SourceConfig(name='test', type='dummy',
options={'interval': 1.0, 'message': 'hello'})]
config.sinks = [SinkConfig(name='log', type='dummy')]
config.persistence.lmdb.path = '/tmp/daena_data'
runtime = DaenaRuntime(config)
await runtime.start()
await asyncio.sleep(5)
print(f'Records: {runtime.backend.count()}')
await runtime.stop()
asyncio.run(main())
"
# Run tests
uv run pytest tests/ -v
# Start gRPC server. Startup prints the pipeline. --verbose logs each record.
uv run daena-serve
uv run daena-serve --verbose
# Use the CLI (point at a running daena gRPC API)
uv run daena status
uv run daena health
uv run daena queue
Project structure
daena/
├── pyproject.toml # Project metadata, dependencies, entry points
├── Dockerfile # Container image
├── proto/daena/v1/daena.proto # gRPC service definition
├── config/daena.yaml # Example configuration
├── deploy/daena.service # systemd unit file
├── .github/workflows/ci.yml # CI pipeline
├── src/daena/
│ ├── app.py # `daena-serve` entry point (async gRPC)
│ ├── config.py # YAML → pydantic config
│ ├── logutil.py # structlog setup
│ ├── exceptions.py # Domain exceptions
│ ├── api/
│ │ ├── grpc_server.py # DaenaServicer + DaenaGRPCServer
│ │ └── pb2/ # Generated gRPC stubs
│ ├── cli/ # Typer CLI (gRPC client)
│ ├── core/
│ │ ├── runtime.py # Service lifecycle
│ │ └── pipeline.py # Collection → persist → dispatch
│ ├── persistence/
│ │ ├── backend.py # Abstract Backend protocol
│ │ ├── models.py # RecordState, StoredRecord
│ │ └── lmdb.py # LMDB implementation
│ ├── pipeline/
│ │ ├── base.py # Source/Processor/Sink ABCs
│ │ ├── record.py # In-memory Record dataclass
│ │ ├── sources/ # Built-in sources (dummy)
│ │ ├── sinks/ # Built-in sinks (dummy, http)
│ │ └── processors/ # Built-in processors (passthrough)
│ └── plugins/
│ ├── loader.py # Entry-point discovery
│ └── registry.py # Plugin registry
└── tests/
Configuration
Daena loads configuration from /etc/daena/daena.yaml, ./daena.yaml, or
./config/daena.yaml. See config/daena.yaml for a full example.
logging:
level: info
format: json
api:
grpc:
host: 127.0.0.1
port: 8642
max_workers: 10
persistence:
backend: lmdb
lmdb:
path: /var/lib/daena/data
map_size: 104857600
sources:
- name: system_logs
type: journald # future
options: {}
processors:
- name: add_hostname
type: enricher # future
options: {}
sinks:
- name: central
type: http
options:
url: https://logs.example.com/ingest
gRPC API
The gRPC API runs on 127.0.0.1:8642 with the following RPCs:
| RPC | Description |
|---|---|
GetStatus |
Service status and version |
GetHealth |
Health, including oldest inflight age (ok, degraded, down) |
GetConfig |
Loaded configuration as JSON |
ListPlugins |
Registered sources, processors, and sinks |
GetQueueState |
Delivery counts, including a per-sink breakdown |
ListPendingRecords |
Pending records with pagination |
ListDeadRecords |
Dead-letter records with pagination |
ListSinks |
Configured sinks |
ReloadConfig |
Re-read the config file and restart the pipeline |
ReplayDead |
Requeue dead-letter deliveries |
Reflect the gRPC service
# Install grpcurl to inspect the service
grpcurl -plaintext 127.0.0.1:8642 list
grpcurl -plaintext 127.0.0.1:8642 describe daena.v1.Daena
CLI
Daena provides a CLI that connects to the gRPC server:
daena --target 127.0.0.1:8642 status
daena health # ok / degraded / down
daena queue # Inspect queue state, per sink
daena records pending
daena records dead
daena replay # Requeue dead-letter deliveries
daena sinks # List configured sinks
daena plugins # List registered plugins
daena config # Show loaded configuration
daena version # Show version
Deployment
systemd
# Create daena user
sudo useradd -r -s /sbin/nologin -d /var/lib/daena daena
# Install package
uv pip install .
# Set up directories
sudo mkdir -p /etc/daena /var/lib/daena /var/log/daena /run/daena
sudo cp config/daena.yaml /etc/daena/
sudo cp deploy/daena.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now daena
Docker
# Build image
docker build -t daena:latest .
# Run with config
docker run -d \
--name daena \
-v /path/to/daena.yaml:/etc/daena/daena.yaml:ro \
-v daena-data:/var/lib/daena \
-p 8642:8642 \
daena:latest
Extending
Add a new source by creating a class that inherits daena.pipeline.base.Source
and registering it as an entry point:
# my_sources.py
from daena.pipeline.base import Source
from daena.pipeline.record import Record
class MySource(Source):
async def start(self):
pass
async def stop(self):
pass
# pyproject.toml
[project.entry-points."daena.sources"]
my_source = "my_sources:MySource"
Same pattern applies for Processor and Sink base classes.
Built-in plugins:
| Kind | Name | Role |
|---|---|---|
| source | dummy |
Interval test events (count stops after N) |
| source | journald |
journalctl --follow with an optional cursor file |
| processor | passthrough |
No-op |
| processor | enricher |
Hostname and static fields |
| processor | filter |
Drop or keep by field equality |
| sink | dummy |
Accept locally |
| sink | http |
JSON POST |
| sink | loki |
Loki push API |
SIGHUP re-reads the config file (systemd ExecReload). SIGTERM drains and closes LMDB. Prometheus text is served at http://127.0.0.1:8643/metrics when metrics are enabled. Each sink has its own delivery state, and a failed attempt is scheduled with next_retry_at instead of blocking the dispatcher.
Branch workflow
main— stable integration branchdev— active development branch
Feature work happens on dev or topic branches. Merges to main represent
stable release checkpoints.