Observability for developers who live in the terminal.
Project description
muga (Python SDK)
Muga observability SDK for Python. Ships logs to Muga via OpenTelemetry OTLP, captures uncaught exceptions, instruments Flask/FastAPI, and pings cron monitors.
Install
pip install muga
Framework extras:
pip install 'muga[flask]'
pip install 'muga[fastapi]'
Set your project token:
export MUGA_TOKEN=muga_xxxxxxxxxxxx
Quickstart
from muga import init, MugaLogger
init(service_name="api")
log = MugaLogger("api")
log.info("server started", {"port": "3000"})
init() reads MUGA_TOKEN and MUGA_ENDPOINT from env (or accepts token= / endpoint=). When the token is missing, init() does not raise — it prints a single warning to stderr and returns. MugaLogger calls and the framework middlewares stay importable; their emits become no-ops via OpenTelemetry's default NoOpLoggerProvider. This keeps dev/CI runs unblocked when no token is configured.
On successful init the resolved endpoint and service name are printed to stderr so misconfigurations (wrong MUGA_ENDPOINT, fallback to default) are visible without enabling debug=True.
Auto-exception capture
init() installs sys.excepthook, threading.excepthook, and an asyncio loop exception handler by default. They emit a FATAL/ERROR log with exception.type, exception.message, and exception.stacktrace attributes, force-flush the batch processor so the record drains before the interpreter exits, then delegate to the runtime's default behaviour.
Behaviour matrix
| Source | Severity | Triggered by |
|---|---|---|
sys.excepthook |
FATAL | Uncaught exception on the main thread. |
threading.excepthook |
ERROR | Uncaught exception in a worker thread. |
asyncio loop handler |
ERROR | Unhandled exception on a task in a loop attached at init() time. |
Caveats:
- The
asynciohandler is only attached to the loop that exists wheninit()runs. If your code creates a new loop later (asyncio.run(...), a freshasyncio.new_event_loop()), callinstall_exception_handlers(loop=...)from inside that loop, or re-callinit()once the loop is current. - All three hooks are idempotent — calling
init()twice does not double-install.
Opt out:
init(capture_exceptions=False)
Flask
from flask import Flask
from muga import init
from muga.flask import muga_flask
init()
app = Flask(__name__)
muga_flask(app)
One log per request: INFO for <500, ERROR for >=500. Attributes: http.method, http.path, http.status_code, http.duration_ms, http.user_agent.
FastAPI
from fastapi import FastAPI
from muga import init
from muga.fastapi import MugaFastAPIMiddleware
init()
app = FastAPI()
app.add_middleware(MugaFastAPIMiddleware)
Same attribute set as the Flask middleware.
Cron heartbeat
Stand-alone helper. Does not require init().
from muga import heartbeat
heartbeat("daily-cleanup")
POSTs to <endpoint>/v1/crons/daily-cleanup/ping with Authorization: Bearer $MUGA_TOKEN. Raises on missing token or non-2xx response. The cron name is URL-encoded.
For async code, wrap in a thread:
import asyncio
from muga import heartbeat
await asyncio.to_thread(heartbeat, "daily-cleanup")
Shutdown
Flush pending logs on graceful shutdown:
import signal
from muga import shutdown
def _handle(*_):
shutdown()
raise SystemExit(0)
signal.signal(signal.SIGTERM, _handle)
Configuration reference
| Argument | Env var | Default |
|---|---|---|
token |
MUGA_TOKEN |
(missing → SDK no-ops with a stderr warning) |
endpoint |
MUGA_ENDPOINT |
https://api.muga.sh |
service_name |
MUGA_SERVICE |
default |
capture_exceptions |
— | True |
debug |
— | False |
max_queue_size |
— | 10000 |
sampling_ratio |
— | 1.0 |
high_rate_warn_threshold |
— | 1000 |
high_rate_warn_window_s |
— | 60.0 |
max_retries |
— | 5 |
init() also registers atexit.register(shutdown) on first successful call so any process exit drains queued records. Repeat calls do not double-register.
Defenses against server pushback
A misbehaving paid client can flood the server with telemetry while OOM-ing itself. The SDK ships client-side defenses so a noisy host degrades gracefully instead of crashing or punching through quotas.
from muga import init
init(
max_queue_size=10_000, # bounded queue; drops oldest on overflow
sampling_ratio=0.25, # head-sample 25% of records before batching
high_rate_warn_threshold=1000, # warn once per window above this rate
high_rate_warn_window_s=60.0,
max_retries=5, # retry budget for 429 / 5xx / transport errors
)
What's enabled by default:
Retry-Afterhonoured on 429. When the server returns 429, the exporter parsesRetry-After(delta-seconds or HTTP-date) and waits that long before retrying.- Exponential backoff with jitter on 5xx and transport errors. Per-attempt sleep is
random() * min(cap, base * 2^attempt). Aftermax_retriesexhaustion the batch is dropped and a counter incremented. - Bounded queue with
drop_oldestpolicy. Each pipeline (logs, spans) gets its own bounded queue. On overflow the oldest record is dropped to free a slot for the newest, anddropped_countis incremented. - High-rate warning, deduplicated. A sliding window counts records over
high_rate_warn_window_s; once it crosseshigh_rate_warn_threshold, a singleWARNINGis logged via the standardloggingmodule. The SDK does not log the warning again until the next window. - Optional head sampling.
sampling_ratio < 1.0drops records before they enter the queue. For spans the decision is deterministic ontrace_idso a given trace either keeps all of its spans or none.
Motivating scenario
A user ships v1 of a worker that loops over a 10-million-row table and emits one INFO log per row. Without these defenses the SDK would buffer all ten million records in memory (OOM-ing the worker), then flood the ingest endpoint while the server applies its own rate limiter and starts returning 429. With the defaults the SDK now (a) caps in-memory buffering at 10k, (b) drops the oldest records to keep the most recent context, (c) honours the server's Retry-After, and (d) prints one WARNING to the worker's log so the operator sees something is wrong.
Migration note
max_queue_size=10000 is a new default in this version. Previous releases used the OTel SDK default (unbounded). If your application explicitly relied on unbounded buffering — for example, a short-lived script that emits a burst at the very end and counts on every record reaching the server — pass an explicit large cap:
init(max_queue_size=10_000_000)
There is no separate "unbounded" mode; pick a number high enough to absorb your peak.
Reporting bugs
File a SDK bug report — the form prompts for SDK version, runtime, minimal repro, and expected vs actual. For anything potentially security-sensitive (token leaks, signature bypass, etc.), email security@muga.sh instead of opening a public issue.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file muga-0.1.4b0.tar.gz.
File metadata
- Download URL: muga-0.1.4b0.tar.gz
- Upload date:
- Size: 32.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e4569ef833cce339a51881cfb99acf23cf5a83847524e8c81d3ecd64dd006433
|
|
| MD5 |
fc39cd92f4efd5a2f85611d12504d559
|
|
| BLAKE2b-256 |
0704745af177a2e142c12a9b1e1c815156037cee0dcd15d4aeb99c5980abaabb
|
Provenance
The following attestation bundles were made for muga-0.1.4b0.tar.gz:
Publisher:
publish-python.yml on mugahq/muga-server
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
muga-0.1.4b0.tar.gz -
Subject digest:
e4569ef833cce339a51881cfb99acf23cf5a83847524e8c81d3ecd64dd006433 - Sigstore transparency entry: 1498026898
- Sigstore integration time:
-
Permalink:
mugahq/muga-server@b96019e6d4626a59840d19fb3363ac846e92a830 -
Branch / Tag:
refs/tags/python-v0.1.4-beta.0 - Owner: https://github.com/mugahq
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python.yml@b96019e6d4626a59840d19fb3363ac846e92a830 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file muga-0.1.4b0-py3-none-any.whl.
File metadata
- Download URL: muga-0.1.4b0-py3-none-any.whl
- Upload date:
- Size: 21.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
15098d6490c8cfdfde3a4be938a0e8bebdc5aa536aff634855e37bd051678d5f
|
|
| MD5 |
37418d8162841283f42aae500905ccd4
|
|
| BLAKE2b-256 |
b7a1cd4cd358ebcbd9b69555e4cd699c62ce11b492aae3234c4dd772fc7b9854
|
Provenance
The following attestation bundles were made for muga-0.1.4b0-py3-none-any.whl:
Publisher:
publish-python.yml on mugahq/muga-server
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
muga-0.1.4b0-py3-none-any.whl -
Subject digest:
15098d6490c8cfdfde3a4be938a0e8bebdc5aa536aff634855e37bd051678d5f - Sigstore transparency entry: 1498027037
- Sigstore integration time:
-
Permalink:
mugahq/muga-server@b96019e6d4626a59840d19fb3363ac846e92a830 -
Branch / Tag:
refs/tags/python-v0.1.4-beta.0 - Owner: https://github.com/mugahq
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python.yml@b96019e6d4626a59840d19fb3363ac846e92a830 -
Trigger Event:
workflow_dispatch
-
Statement type: