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 (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):

    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:

    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/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.

Description
Mosaic Agent Log System — standalone Phase 1
Readme 110 KiB
Languages
Python 98.5%
Dockerfile 1.5%