Observo Documentation
Observo is a self-hosted centralized log management platform. It ingests logs in real time via a REST API or Python handler, surfaces them in a searchable dashboard, and alerts you when something goes wrong.
This documentation covers everything from initial setup to advanced configuration. Use the sidebar to jump to any section.
Getting Started
Get from zero to your first log entry in under 5 minutes.
Create an account
Log in to your Observo instance and create your first user via the admin panel or the login page.
Create a project
From the dashboard, click New Project. Give it a name and select an environment (development, staging, or production).
Copy your credentials
From the project detail page, copy your Project ID and API Key. You'll need these to authenticate log submissions.
Send your first log
Install the Python handler or make a curl request — and watch your logs appear in real time.
curl -X POST https://observo-log.vendlyghana.space/api/v1/ingest/ \
-H "X-Project-ID: YOUR_PROJECT_ID" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"level": "INFO", "message": "Hello, Observo!"}'
# 201 Created → {"id": "log_01j...", "created": "2026-03-16T..."}
Core Concepts
Understanding the key building blocks of Observo.
Projects
A Project is the top-level container for logs. Each project has a unique
project_id and one or more api_keys. Logs are always scoped to a project.
Use separate projects for different applications or environments.
Log Entries
A Log Entry contains a level, message, logger name,
timestamp, and optional extra metadata. Observo stores all fields and makes them
fully searchable.
Alert Rules
Alert Rules define conditions on your logs (e.g., "ERROR count > 5 in 1 minute") and fire notifications when triggered. Notifications can go to email, Slack, or a custom webhook.
Python Handler
The observo-handler package provides a drop-in Python logging handler.
It ships logs in the background via a thread-safe queue with batching and fail-safe delivery.
Installation
pip install observo-handler
Basic Usage
import logging from observo_handler import ObservoHandler # Configure handler handler = ObservoHandler( project_id="your-project-id", api_key="obs_live_xxxxxxxxxxxx", observo_url="https://observo-log.vendlyghana.space/api/v1/ingest/", batch_size=10, # flush after 10 logs flush_interval=5, # flush every 5 seconds ) # Attach to any logger logger = logging.getLogger("myapp") logger.setLevel(logging.DEBUG) logger.addHandler(handler) # Use standard logging — no API calls needed in your code logger.info("Application started") logger.warning("Disk usage above 80%%", extra={"disk_pct": 82}) logger.error("Unhandled exception", exc_info=True)
Handler Configuration
| Parameter | Type | Default | Description |
|---|---|---|---|
| project_id | str | required | Your project's unique ID |
| api_key | str | required | Your project's API key |
| observo_url | str | required | Full URL to the ingest endpoint |
| batch_size | int | 10 | Number of logs before forcing a flush |
| flush_interval | int | 5 | Seconds between automatic flushes |
| timeout | int | 5 | HTTP request timeout in seconds |
| max_retries | int | 3 | Retry attempts on network failure |
Django Integration
Integrate Observo into a Django project using the standard LOGGING configuration
and optional request ID middleware.
LOGGING Settings
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
"observo": {
"class": "observo_handler.ObservoHandler",
"project_id": env("OBSERVO_PROJECT_ID"),
"api_key": env("OBSERVO_API_KEY"),
"observo_url": env("OBSERVO_URL"),
"batch_size": 20,
"flush_interval": 10,
},
},
"root": {
"handlers": ["console", "observo"],
"level": "INFO",
},
}
Request ID Middleware
Add RequestIDMiddleware to inject a unique request_id into every log entry,
making it easy to trace all logs for a given HTTP request.
MIDDLEWARE = [
"observo_handler.middleware.RequestIDMiddleware", # First!
"django.middleware.security.SecurityMiddleware",
# ... rest of your middleware
]
Flask Integration
import logging from flask import Flask from observo_handler import ObservoHandler app = Flask(__name__) # Attach to Flask's app logger handler = ObservoHandler( project_id="my-flask-app", api_key="obs_live_xxxxxxxxxxxx", observo_url="https://observo-log.vendlyghana.space/api/v1/ingest/", ) app.logger.addHandler(handler) app.logger.setLevel(logging.INFO) @app.route("/") def index(): app.logger.info("Home page visited") return "Hello!"
API Reference
Complete reference for the Observo REST API. Base URL: https://observo-log.vendlyghana.space/api/v1/
Endpoints
/api/v1/ingest/
/api/v1/logs/
/api/v1/logs/{id}/
/api/v1/projects/
/health/
Ingest a Log Entry
POST /api/v1/ingest/
X-Project-ID: your-project-id
Authorization: Bearer your-api-key
Content-Type: application/json
{
"level": "ERROR",
"message": "Payment failed for order #1234",
"logger": "payments.stripe",
"timestamp": "2026-03-16T12:41:09.000Z",
"extra": {
"user_id": 42,
"order_id": 1234,
"amount": 4999
}
}
HTTP/1.1 201 Created
{
"id": "log_01j9xk...",
"created": "2026-03-16T12:41:09.123Z"
}
Log Entry Fields
| Field | Type | Required | Description |
|---|---|---|---|
| level | string | Yes | DEBUG, INFO, WARNING, ERROR, or CRITICAL |
| message | string | Yes | The log message text |
| logger | string | No | Logger name (e.g. "myapp.views") |
| timestamp | ISO 8601 | No | Defaults to server receive time |
| extra | object | No | Arbitrary JSON metadata |
| request_id | string | No | For tracing related log entries |
| user_id | string/int | No | Associated user identifier |
Authentication
Every API request must include two headers: your Project ID and API Key. Both are available from your project's settings page.
X-Project-ID: your-project-id Authorization: Bearer your-api-key
Log Levels
| Level | Numeric | When to use |
|---|---|---|
| DEBUG | 10 | Detailed diagnostic information for development |
| INFO | 20 | Normal application events (requests, transactions) |
| WARNING | 30 | Something unexpected, but the app is still working |
| ERROR | 40 | A failure that prevented a specific operation |
| CRITICAL | 50 | A serious failure — the app may be unable to continue |
Logging Best Practices
The difference between a useful log and a useless one is context. A log that says
api_request tells you something happened. A log that says
api_request POST /api/v1/checkout 422 89ms user=u_8821 tells you what to fix.
payment_failed
with no user ID, amount, or error code is nearly useless.
Log Context, Not Just Events
# These tell you something happened — but not what, who, or why
logger.info('api_request')
logger.error('payment_failed')
logger.info('user_login')
# Rich context you can actually debug from
logger.info('api_request POST /api/v1/checkout 200 143ms user=u_8821')
logger.error(
'payment_failed: Stripe declined card',
extra={'extra_data': {
'user_id': 'u_8821',
'amount': 4999,
'currency': 'USD',
'stripe_error': 'insufficient_funds',
'card_last4': '4242',
}})
logger.info(
'user_login success',
extra={'extra_data': {
'user_id': 'u_8821',
'ip': '41.66.12.5',
'method': 'email_password',
}}
)
Use Log Levels Correctly
| Level | When to use | Example |
|---|---|---|
| DEBUG | Internal state during development | cache_miss key=user:8821:profile |
| INFO | Normal operations you want a record of | order_created order_id=ORD-991 total=$49.99 |
| WARNING | Unexpected but the app recovered | rate_limit_approaching user=u_8821 75/100 |
| ERROR | Something failed and needs attention | db_query_failed table=orders timeout=30s |
| CRITICAL | Service broken or data at risk | database_unreachable all_connections_exhausted |
Always Use exc_info=True on Errors
# Bad — no stack trace, impossible to debug except Exception as e: logger.error(f'Payment failed: {e}') # Good — full stack trace sent to Observo automatically except Exception as e: logger.error('payment_processing_failed', exc_info=True, extra={'extra_data': { 'order_id': order.id, 'user_id': order.user_id, 'amount': float(order.total), }})
Never Log Sensitive Data
| Never log | Instead log |
|---|---|
| Passwords | login_attempt user=u_8821 |
| Full credit card numbers | card_last4=4242 |
| API secrets or tokens | api_key_used=obs_...k9x2 (masked) |
| Social security numbers | ssn_verified=true |
| Private health info | health_check_completed user=u_8821 |
Alerting
Alert rules fire notifications when logs match conditions you define. Configure alerts from Project → Alert Rules → New Rule.
Rule Conditions
| Condition | Example |
|---|---|
| level | level = ERROR |
| message contains | message contains "OutOfMemory" |
| count threshold | ERROR count > 5 in 5 minutes |
| logger | logger starts with "payments." |
Notification Channels
Observo supports the following notification channels:
Sends an HTML email with log context. Requires SMTP configuration in settings.
Slack
Posts a formatted message to a Slack channel via Incoming Webhook URL.
Webhook
POSTs a JSON payload to any URL. Works with PagerDuty, Discord, Teams, and more.
AI Analysis
Observo's AI features require an OPENAI_API_KEY in your environment.
Once configured, you can ask questions about your logs in plain English.
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4o # optional, defaults to gpt-4
With AI enabled, the Chat section of your dashboard lets you query logs conversationally — e.g. "Show me all errors in the last hour related to payments" or "What's causing the spike in warnings at 2am?"
Self-Hosting
Deploy Observo on your own server using Docker Compose.
Prerequisites
Docker, Docker Compose, and a server with at least 1 GB RAM.
Docker Compose
# Clone the repo git clone https://github.com/kennedy-ak/observo.git cd observo # Copy and edit environment file cp .env.example .env nano .env # Start all services docker-compose up -d # Run migrations docker-compose exec web python manage.py migrate # Create your admin user docker-compose exec web python manage.py createsuperuser
Configuration
Configure Observo via environment variables in your .env file.
| Variable | Required | Description |
|---|---|---|
| SECRET_KEY | Yes | Django secret key — use a long random string |
| DATABASE_URL | Yes | postgres://user:pass@host:5432/db |
| REDIS_URL | Yes | redis://localhost:6379/0 |
| DEBUG | No | Set to False in production |
| ALLOWED_HOSTS | No | Comma-separated list of allowed hostnames |
| OPENAI_API_KEY | No | Required for AI features |
| OPENAI_MODEL | No | OpenAI model name (default: gpt-4) |
| EMAIL_HOST | No | SMTP host for email alerts |
| EMAIL_PORT | No | SMTP port (default: 587) |
| EMAIL_HOST_USER | No | SMTP username |
| EMAIL_HOST_PASSWORD | No | SMTP password |