Files
mals/README.md

205 lines
5.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](https://git.mosaicstack.dev/jason.woltje/mosaic-mono-v1) as a future integration, but is fully functional as a standalone service.
---
## Quick Start
### 1. Clone and configure
```bash
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
```bash
docker compose up -d
```
### 3. Run migrations
```bash
docker compose exec api alembic upgrade head
```
### 4. Verify health
```bash
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):
```bash
pip install mals
# or from source:
pip install -e ./path/to/mals
```
### Async usage
```python
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)
```python
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` (11000, 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)
1. Build and push the image to your registry (or build on the host):
```bash
docker build -t mals:latest .
```
2. In Portainer → Stacks → Add Stack, paste `docker-compose.portainer.yml`.
3. Set environment variables in the Portainer UI (or via a `.env` secrets file):
- `POSTGRES_PASSWORD`
- `MALS_API_KEY`
- `MALS_DOMAIN` (if using Traefik)
4. Deploy. After the first start, run migrations:
```bash
docker exec <api-container-id> alembic upgrade head
```
---
## Development
```bash
# 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/summary` endpoint 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.