4da9accabfda4976aadc281cd382204c6b75ffd5
Complete migration from FastAPI/HTTP to gRPC: Proto definition (proto/daena/v1/daena.proto): - Daena service with 9 RPCs: GetStatus, GetHealth, GetConfig, ListPlugins, GetQueueState, ListPendingRecords, ListDeadRecords, ListSinks, ReloadConfig - Messages for all request/response types with proper field types gRPC server (src/daena/api/grpc_server.py): - Async gRPC aio server with DaenaServicer implementation - DaenaGRPCServer with start/stop/serve_forever lifecycle - All endpoints delegate to DaenaRuntime gRPC client (src/daena/cli/main.py): - CLI now uses grpc.insecure_channel + DaenaStub - Same commands: status, health, queue, sinks, plugins, config, version Config changes: - Remove FastAPI/uvicorn/http config, replace with grpc config - GRPCConfig with host/port/max_workers Dependency changes: - Remove fastapi, uvicorn, starlette, anyio - Add grpcio, grpcio-tools, protobuf - Keep httpx for HTTP sink forwarding (separate from management API) Tests: - 8 new gRPC integration tests (test_grpc.py) - All 31 tests passing, ruff clean
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 REST 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] → Pipeline → Persistence (LMDB) → Sink Dispatcher → Remote
↻ retry queue
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
# Use the CLI (point at a running daena API)
uv run daena status
uv run daena health
uv run daena queue
Project structure
daena/
├── pyproject.toml # Project metadata, dependencies, entry points
├── config/daena.yaml # Example configuration
├── deploy/daena.service # systemd unit file
├── src/daena/
│ ├── app.py # `daena-serve` entry point (uvicorn)
│ ├── config.py # YAML → pydantic config
│ ├── logutil.py # structlog setup
│ ├── exceptions.py # Domain exceptions
│ ├── api/ # FastAPI REST API (v1 versioned)
│ ├── cli/ # Typer CLI
│ ├── 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:
http:
host: 127.0.0.1
port: 8642
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
REST API
The API runs on http://127.0.0.1:8642/api/v1:
| Endpoint | Description |
|---|---|
GET /system/status |
Service status |
GET /system/health |
Health check |
GET /system/config |
Loaded configuration |
GET /plugins |
Registered plugins |
GET /queue |
Queue state counts |
GET /queue/pending |
Pending records |
GET /queue/dead |
Dead-letter records |
GET /sinks |
Configured sinks |
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
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.
Branch workflow
main— stable integration branchdev— active development branch
Feature work happens on dev or topic branches. Merges to main represent
stable release checkpoints.
Languages
Python
98.5%
Shell
1.2%
Dockerfile
0.3%