205 lines
5.7 KiB
Markdown
205 lines
5.7 KiB
Markdown
# 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` (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)
|
||
|
||
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.
|