MALS — Mosaic Agent Log System
Phase 1 — Standalone Service
MALS is a lightweight, centralized structured logging service for AI agents. It provides a FastAPI REST API backed by PostgreSQL, allowing any agent or bot to ship structured log entries (with levels, categories, metadata, and trace IDs) and query them later for debugging, alerting, and health summaries. It is designed to be embedded into Mosaic Stack as a future integration, but is fully functional as a standalone service.
Quick Start
1. Clone and configure
git clone https://git.mosaicstack.dev/jason.woltje/mals.git
cd mals
cp .env.example .env
Edit .env and fill in at minimum:
POSTGRES_PASSWORD=your-strong-password-here
MALS_API_KEY=your-secret-api-key-here
2. Start services
docker compose up -d
3. Run migrations
docker compose exec api alembic upgrade head
4. Verify health
curl http://localhost:8421/health
# → {"status":"ok","db":"connected","version":"0.1.0"}
Using the Python Client
Install the package (once published, or from source):
pip install mals
# or from source:
pip install -e ./path/to/mals
Async usage
import asyncio
import os
from mals.client import MALSClient
client = MALSClient(
base_url=os.environ["MALS_URL"], # e.g. http://10.1.1.45:8421
api_key=os.environ["MALS_API_KEY"],
agent_id="crypto", # identifies your service
session_key="agent:crypto:discord:...", # optional — for session correlation
source="openclaw", # optional — default "api"
)
async def main():
# Info log
await client.log("Bot started", level="info", category="lifecycle")
# Error with traceback capture
try:
raise RuntimeError("Flash loan failed")
except Exception as exc:
await client.error("Unexpected error in main loop", exc=exc, category="trading")
# Batch log
await client.batch([
{"message": "Order placed", "level": "info", "metadata": {"pair": "ETH/USDC"}},
{"message": "Order filled", "level": "info", "metadata": {"pair": "ETH/USDC"}},
])
# Summary for this agent
summary = await client.summary(since_hours=24)
print(summary["by_agent"])
asyncio.run(main())
Synchronous usage (from non-async code)
client.sync_log("Bot restarted", level="warn", category="lifecycle")
client.sync_error("DB connection lost", exc=exc)
API Reference
All routes require Authorization: Bearer <MALS_API_KEY> except GET /health.
GET /health
No auth. Returns service status.
POST /logs
Ingest a single log entry. Returns {"id": "uuid", "created_at": "..."}.
Body fields:
| Field | Type | Required | Description |
|---|---|---|---|
agent_id |
string | ✅ | Agent or service name (max 64 chars) |
message |
string | ✅ | Log message |
level |
string | — | debug|info|warn|error|critical (default: info) |
category |
string | — | Freeform tag (e.g. deploy, trading) |
session_key |
string | — | Agent session identifier |
source |
string | — | Source system (default: api) |
metadata |
object | — | Arbitrary JSON metadata |
trace_id |
UUID | — | Distributed tracing ID |
parent_id |
UUID | — | Parent log entry ID for hierarchical traces |
POST /logs/batch
Array of log entries. Returns {"inserted": N}.
GET /logs
List log entries. Query params: agent_id, level, category, source, resolved (bool), since (ISO datetime), until, search (text in message), limit (1–1000, default 100), offset.
Returns {"total": N, "items": [...]}.
GET /logs/summary
Params: since (ISO datetime, default 24h ago), agent_id (optional filter).
Returns error counts by agent, unresolved errors, new errors (not seen in previous period), and recurring errors (same message 3+ times).
GET /logs/agents
List all distinct agents with last activity timestamp and 24h error count.
PATCH /logs/{id}/resolve
Body: {"resolved_by": "jarvis"}. Marks entry resolved.
POST /logs/resolve-batch
Body: {"ids": ["uuid1", ...], "resolved_by": "jarvis"}. Bulk resolve.
Portainer Deployment (w-docker0)
-
Build and push the image to your registry (or build on the host):
docker build -t mals:latest . -
In Portainer → Stacks → Add Stack, paste
docker-compose.portainer.yml. -
Set environment variables in the Portainer UI (or via a
.envsecrets file):POSTGRES_PASSWORDMALS_API_KEYMALS_DOMAIN(if using Traefik)
-
Deploy. After the first start, run migrations:
docker exec <api-container-id> alembic upgrade head
Development
# Install with dev dependencies
uv sync --dev
# Run tests
uv run pytest tests/ -v
# Lint
uv run ruff check src/
# Type check
uv run mypy src/
# Run locally (needs a running Postgres)
DATABASE_URL=postgresql+asyncpg://mals:mals@localhost:5434/mals \
MALS_API_KEY=dev-key \
uv run uvicorn mals.main:app --reload
Future: Mosaic Stack Integration
MALS is designed for eventual integration into the Mosaic Stack platform:
- The Python client will become an internal SDK used by all Mosaic agents
- The FastAPI service will be mounted as a sub-application in the Mosaic NestJS gateway (via a sidecar or internal network call)
- The
/logs/summaryendpoint will feed the Mosaic Mission Control dashboard - Log entries will be linked to Mosaic sessions via
session_key
No changes to the schema or API contract are expected for this integration — the standalone Phase 1 design is integration-ready.