google / google/slo-generator

PrometheusSelfExporter broken with Flask >= 2.2: add_url_rule after first request

Open
#612 0 comments 1 reaction 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.