PrometheusSelfExporter broken with Flask >= 2.2: add_url_rule after first request
- Dominant language
- Python
- Stars
- 564
- Forks
- 88
- PR merge metrics
- No merged PRs in 30d
Description
## Description
The `PrometheusSelfExporter` fails with `AssertionError` when used with Flask >= 2.2. The exporter calls `current_app.add_url_rule("/metrics", ...)` in its `__init__()` method, which runs the first time an SLO is exported — not during app startup.
Starting with Flask 2.2, calling `add_url_rule()` after the application has handled its first request raises:
```
AssertionError: The setup method 'add_url_rule' can no longer be called on the application.
It has already handled its first request, any changes will not be applied consistently.
```
## Root Cause
The issue is in `slo_generator/exporters/prometheus_self.py:37`:
```python
def __init__(self, **kwargs):
if not PrometheusSelfExporter.REGISTERED_URL:
current_app.add_url_rule("/metrics", view_func=self.serve_metrics)
PrometheusSelfExporter.REGISTERED_URL = True
```
The `/metrics` route registration happens lazily (on first export), not at app startup. By the time the first SLO is computed and exported, the Flask app has already processed requests (Gunicorn worker warm-up, health check probes, etc.), so `add_url_rule()` is blocked.
## How to Reproduce
1. Run slo-generator in API mode with Flask >= 2.2 (the current Docker image ships Flask 3.0.2)
2. Configure `prometheus_self` exporter in `shared_config.yaml`
3. Send a POST request with an SLO config
4. The export step fails with the AssertionError above
## Current Workaround
Pin Flask < 2.2 and Werkzeug < 2.4 in the Docker image:
```dockerfile
FROM google/slo-generator:2.6.0
RUN pip install --no-cache-dir 'Flask>=2.0,<2.2' 'Werkzeug>=2.0,<2.4'
```
This installs Flask 2.1.3 + Werkzeug 2.3.8 which don't have the `_check_setup_finished` restriction.
## Suggested Fix
Register the `/metrics` route during app initialization instead of lazily in the exporter constructor. For example, using the Functions Framework entry point or a Flask `before_first_request` hook (deprecated in 2.3, but alternatives exist):
```python
# Option A: Register via Werkzeug URL map directly (works with any Flask version)
@classmethod
def _register_url(cls):
if cls.REGISTERED_URL:
return
try:
current_app.add_url_rule("/metrics", view_func=cls.serve_metrics)
except AssertionError:
from werkzeug.routing import Rule
current_app.url_map.add(Rule("/metrics", endpoint="serve_metrics"))
current_app.view_functions["serve_metrics"] = cls.serve_metrics
cls.REGISTERED_URL = True
```
```python
# Option B: Register at module import time via the app factory
# (requires changes to slo_generator/api/main.py)
```
## Environment
- slo-generator: 2.6.0
- Flask: 3.0.2 (as shipped in the Docker image)
- Python: 3.9
- Running in Kubernetes with Gunicorn via Functions Framework
Contributor guide
Assessment
This issue has not been assessed yet.