Mitsuki's built-in instrumentation records what your application is doing (requests per route, latency, calls and failures per service method, scheduled task runs, process CPU and memory) and exposes it on two endpoints: a JSON summary for people and a Prometheus text endpoint for monitoring systems.
It plays the role Spring Boot Actuator and Micrometer play in a Spring application, or prometheus-fastapi-instrumentator in a FastAPI one.
The difference is that it also measures your own components, each invoked method's failure rate, call time, etc., not only the HTTP layer, and you enable it with a single line.
Everything below comes from the instrumentation_demo example in the Mitsuki repository, a small users-and-orders API.
Mitsuki
Mitsuki, is a web development framework, focused on bringing enterprise strength without the enterprise pain to Python.
It's opinionated, lightweight and performant.
Installing
Instrumentation samples process CPU and memory through psutil, which ships as an optional extra:
pip install "mitsuki[metrics]"
Enabling It
Put @Instrumented() on the application class:
# app.py
from mitsuki import Application, Instrumented, Value
@Instrumented()
@Application
class App:
"""
Application entry point.
@Instrumented() on the application instruments every
@RestController, @Service, @Repository and @CrudRepository. It takes effect when instrumentation.enabled and metrics.enabled are set in application.yml.
"""
port: int = Value("${server.port:8000}")
if __name__ == "__main__":
App.run(host="0.0.0.0")
Then switch it on in configuration:
# application.yml
instrumentation:
enabled: true
track_memory: false
metrics:
enabled: true
path: /metrics
Both keys are needed. metrics.enabled turns on the metrics registry and its endpoints; instrumentation.enabled turns on the recording of requests and component calls.
track_memory adds Python's traced memory (via tracemalloc) on top of the process CPU and memory that are always sampled. Note that tracemalloc slows down every allocation. It's meant to be used for debugging rather than monitoring.
The decorator on the application instruments every @Controller, @Service and @Repository:
But also, the methods within them are tracked as well, so each method gets a failure rate, response time, etc:
To scope it to a smaller subset of components instead, put @Instrumented() on individual components and leave it off the application.
To exclude one component from application-wide instrumentation, use @Instrumented(enabled=False) on it. The Instrumentation & Metrics documentation covers both in greater detail.
What Gets Recorded
| Metric | Type | Labels | Records |
|---|---|---|---|
http_requests_total |
counter |
method, path, status
|
Every HTTP request |
http_request_duration_seconds |
histogram |
method, path
|
Request latency |
component_calls_total |
counter |
component, method, status
|
Every call to a public method of an instrumented component, with status of success or failure
|
component_duration_seconds |
histogram |
component, method
|
Method duration |
scheduler_task_executions_total |
counter |
task, status
|
Every @Scheduled run |
scheduler_task_duration_seconds |
histogram | task |
Task duration |
scheduler_tasks_running |
gauge | task |
Runs in progress |
system_memory_bytes |
gauge |
type (rss, vms) |
Process memory, sampled every 5 seconds |
system_cpu_percent |
gauge | Process CPU, sampled every 5 seconds | |
system_traced_memory_bytes |
gauge |
type (current, peak) |
Python traced memory, sampled every 5 seconds; only with track_memory: true
|
A few notes:
-
Methods are wrapped once, at startup. Every public method of an instrumented component is wrapped when the application starts. Methods starting with
_are not instrumented, and so are static methods, class methods and properties. -
Repositories are included. For a
@CrudRepository, the generatedsaveandfind_all, methods implemented from their names likefind_by_user_id,@Querymethods and methods you write yourself are all recorded under the repository's name.
This is the demo's component table on the Grafana dashboard set up in the next post, with both repositories listed next to the services and controllers:
Human Readable and Prometheus Format
/metrics returns a JSON summary with averages already computed. The demo README's example requests, one more order, and two user lookups, one of them for a user that doesn't exist:
curl http://localhost:8000/metrics
{
"enabled": true,
"timestamp": "2026-09-29T07:32:26.689899+00:00",
"scheduler": {
"tasks": [
{
"name": "OrderReconciliationService.reconcile_orders",
"executions": 1,
"failures": 0,
"average_duration_ms": 9.51,
"status": "idle"
}
],
"total_tasks": 1,
"running_tasks": 0
},
"instrumentation": {
"system": {
"memory": {
"rss_bytes": 55386112,
"rss_mb": 52.82,
"vms_bytes": 181608448,
"vms_mb": 173.2
},
"cpu": {
"percent": 0.0
}
},
"http": {
"total_requests": 11,
"requests_by_method": {
"POST": 5,
"GET": 6
},
"responses_by_status": {
"201": 5,
"200": 5,
"404": 1
},
"latency": {
"avg_ms": 3.42,
"total_seconds": 0.04
}
},
"components": {
"OrderRepository": {
"calls": 7,
"avg_duration_ms": 2.92,
"methods": {
"find_all": {
"calls": 3,
"avg_duration_ms": 3.92
},
"find_by_user_id": {
"calls": 1,
"avg_duration_ms": 4.03
},
"save": {
"calls": 3,
"avg_duration_ms": 1.56
}
}
},
...
}
}
}
/metrics/prometheus returns every metric as labelled series in the Prometheus text format: the raw counters, gauges and histogram buckets behind that summary, plus any custom metrics your code records, which the JSON summary leaves out:
curl http://localhost:8000/metrics/prometheus
# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="POST",path="/api/users",status="201"} 2.0
http_requests_total{method="GET",path="/api/users",status="200"} 1.0
http_requests_total{method="POST",path="/api/orders",status="201"} 3.0
...
/metrics |
/metrics/prometheus |
|
|---|---|---|
| Format | Nested JSON | Prometheus text format |
| Contents | Totals and averages since startup | Raw counters, gauges and histogram buckets |
| Per-route breakdown | No | Yes, via the path label |
| Percentiles | No | Yes, computed by Prometheus from the histograms |
| Custom metrics | No | Yes |
| Use it for | A quick look, health checks, scripts | Dashboards, alerting, history |
Securing the Endpoints
Both endpoints expose your route table, component names and traffic volume, so in production they should obviously not be public.
metrics.allowed_ips restricts them to listed addresses and CIDR ranges. The demo allows your machine and Docker networks:
metrics:
enabled: true
path: /metrics
# Addresses allowed to read the metrics endpoints. An empty list allows all.
# - 127.0.0.1: requests from your machine when running app.py directly
# - 172.16.0.0/12: Docker networks, which is where Prometheus scrapes from
# - 192.168.0.0/16: requests from your machine to the container on Docker
# Desktop, which forwards them from 192.168.65.1
allowed_ips: ["127.0.0.1", "172.16.0.0/12", "192.168.0.0/16"]
An empty list allows everyone. A refused request gets a 404 with the body Not Found for every HTTP method, so a client outside the list can't easily probe whether the endpoints are there. The refusal is logged as a warning on the server side. With allowed_ips: ["10.8.0.0/16"], a request from the same machine logs:
2026-09-29 16:31:09,251 - mitsuki - WARNING - Metrics access denied for IP: 127.0.0.1
Note: The check uses the client address the server reports. Behind a reverse proxy or load balancer, Granian, the default server, reports the proxy as the client, so external requests look internal: allowlisting the proxy's address (for example 127.0.0.1, which the demo allows) opens the endpoints to anyone the proxy serves. In other words, if you use a load balancer or nginx or similar proxies, the external requests are translated into internal requests, and in 0.2.0 - the Granian engine won't be able to tell them apart. This is planned to be patched in future versions alongside broader IP allowlisting support.
Limitations
-
Metrics are per process. Each worker process keeps its own metrics. With
server.workersgreater than 1, each scrape reaches one worker, and Prometheus sees counters jump between workers' values. - Metrics are in memory. They reset when the application restarts. Prometheus handles counter resets, but the JSON endpoint only covers the time since startup.
Next Steps
- Set up Prometheus and Grafana on top of these endpoints with the demo's Docker Compose file: Mitsuki API Observability with Grafana and Prometheus - demo on GitHub.
- Record your own business metrics next to the automatic ones: Creating Custom Business Metrics in Mitsuki - demo on GitHub.
- For every configuration option, see the Instrumentation & Metrics documentation.




Top comments (0)