Repository: Agent-Hellboy/gunicorn-prometheus-exporter
Pull request #11: Change worker naming approch from os.pid to custom worker_<age>_<time…
Author: login: Agent-Hellboy
Created: 2025-05-18T12:58:22Z | Merged: 2025-05-18T19:45:49Z

…stamp>

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Documentation**
  - Enhanced documentation with detailed instructions for Prometheus integration, metric labels, worker ID format, and example PromQL queries.
  - Updated example documentation to reflect metrics server port change and added Prometheus UI setup guidance.
- **Bug Fixes**
  - Updated worker ID format for improved lifecycle tracking and metric consistency.
  - Implemented automatic cleanup of old metrics associated with outdated worker IDs.
- **Tests**
  - Improved and expanded test coverage for new worker ID format and metric cleanup functionality.
- **Chores**
  - Updated ignored files in version control configuration.
  - Removed support for Python 3.8 and 3.9 in testing and project metadata.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/.gitignore b/.gitignore
index 0e5acba6..d199085f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,9 @@ __pycache__/
 *.py[cod]
 *$py.class
 
+explore
+trace.json
+
 # C extensions
 *.so
 example*
diff --git a/README.md b/README.md
index b15ac23f..fccd440f 100644
--- a/README.md
+++ b/README.md
@@ -45,18 +45,65 @@ gunicorn --config gunicorn.conf.py your_app:app
 
 ### Worker Metrics
 
+Each worker is identified by a unique worker ID in the format `worker_<age>_<timestamp>`. This ensures:
+- Unique identification across worker restarts
+- Easy tracking of worker lifecycle
+- Consistent metrics across worker replacements
+
+Available worker metrics:
+
 - `gunicorn_worker_requests`: Total number of requests handled by each worker
+  - Labels: `worker_id` (format: `worker_<age>_<timestamp>`)
 - `gunicorn_worker_request_duration_seconds`: Request duration in seconds
+  - Labels: `worker_id`
+  - Buckets: 0.1, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0, +Inf
 - `gunicorn_worker_memory_bytes`: Memory usage of each worker process
+  - Labels: `worker_id`
 - `gunicorn_worker_cpu_percent`: CPU usage of each worker process
+  - Labels: `worker_id`
 - `gunicorn_worker_uptime_seconds`: Uptime of each worker process
-- `gunicorn_worker_failed_requests`: Total number of failed requests with method, endpoint, and error type
-- `gunicorn_worker_error_handling`: Total number of errors handled with method, endpoint, and error type
-- `gunicorn_worker_state`: Current state of the worker (running, quit, abort)
+  - Labels: `worker_id`
+- `gunicorn_worker_failed_requests`: Total number of failed requests
+  - Labels: `worker_id`, `method`, `endpoint`, `error_type`
+- `gunicorn_worker_error_handling`: Total number of errors handled
+  - Labels: `worker_id`, `method`, `endpoint`, `error_type`
+- `gunicorn_worker_state`: Current state of the worker
+  - Labels: `worker_id`, `state`, `timestamp`
+  - Values: 1 (running), 0 (stopped)
+
+### Example PromQL Queries
+
+Here are some useful PromQL queries for monitoring your Gunicorn workers:
+
+```promql
+# Current active workers and their uptime
+gunicorn_worker_uptime_seconds{worker_id=~"worker_.*"}
+
+# Worker memory usage (in MB)
+gunicorn_worker_memory_bytes{worker_id=~"worker_.*"} / 1024 / 1024
+
+# Request rate per worker (requests per second)
+rate(gunicorn_worker_requests{worker_id=~"worker_.*"}[5m])
+
+# Average request duration per worker
+rate(gunicorn_worker_request_duration_seconds_sum{worker_id=~"worker_.*"}[5m]) 
+/ 
+rate(gunicorn_worker_request_duration_seconds_count{worker_id=~"worker_.*"}[5m])
+
+# Error rate per worker
+rate(gunicorn_worker_failed_requests{worker_id=~"worker_.*"}[5m])
+
+# CPU usage per worker
+gunicorn_worker_cpu_percent{worker_id=~"worker_.*"}
+
+# Worker state changes
+changes(gunicorn_worker_state{worker_id=~"worker_.*"}[5m])
+```
 
 ### Master Process Metrics
 
-- `gunicorn_master_worker_restart_total`: Total number of worker restarts with reason
+- `gunicorn_master_worker_restart_total`: Total number of worker restarts
+  - Labels: `reason` (e.g., "restart", "timeout", "error")
 
 To enable master process metrics, use the PrometheusMaster class:
 
@@ -72,18 +119,39 @@ from gunicorn_prometheus_exporter.plugin import PrometheusMaster
 gunicorn.arbiter.Arbiter = PrometheusMaster
 ```
 
+## Configuration
 
-## Development
+### Prometheus UI Setup
 
-```bash
-# Install dependencies
-pip install -e ".[dev]"
+To visualize the metrics using Prometheus UI:
 
-# Run tests
-pytest
+1. Create a `prometheus.yml` configuration file:
+```yaml
+global:
+  scrape_interval: 15s
+
+# Scrape our example exporter
+scrape_configs:
+  - job_name: 'gunicorn-prometheus-exporter'         
+    metrics_path: '/metrics'       
+    static_configs:
+      - targets: ['127.0.0.1:9090'] # our example exporter
 ```
 
-## Configuration
+2. Run Prometheus using Docker:
+```bash
+docker run -d \
+  --name prometheus \
+  --network host \
+  -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml:ro \
+  prom/prometheus
+```
+
+3. Access Prometheus UI at `http://localhost:9090` to:
+   - View metrics in the Graph interface
+   - Use PromQL queries to analyze worker performance
+   - Create graphs and dashboards
+   - Set up alerts based on metric thresholds
 
 ### Gunicorn Configuration
 
@@ -127,8 +195,6 @@ The exporter supports the following configuration options:
 - `PROMETHEUS_MULTIPROC_DIR`: Directory for multiprocess metrics (default: `/tmp/prometheus`)
 - `PROMETHEUS_METRICS_PORT`: Port for metrics endpoint (default: 8000)
 
-
-
 ## License
 
 MIT License - Prince Roshan
diff --git a/example/README.md b/example/README.md
index 270db871..d8a62358 100644
--- a/example/README.md
+++ b/example/README.md
@@ -22,7 +22,7 @@ chmod 777 /tmp/metrics_test
 
 # 2) Set env vars
 export PROMETHEUS_MULTIPROC_DIR=/tmp/metrics_test
-export PROMETHEUS_METRICS_PORT=9090
+export PROMETHEUS_METRICS_PORT=9091
 
 # 3) Start Gunicorn (uses your gunicorn.conf.py)
 gunicorn --config gunicorn.conf.py app:app 
@@ -31,56 +31,78 @@ gunicorn --config gunicorn.conf.py app:app
 ## Testing
 
 1. The application will be available at `http://localhost:8080`
-2. The metrics will be available at `http://localhost:9090/metrics`
+2. The metrics will be available at `http://localhost:9091/metrics`
 
 Try these endpoints:
 - `http://localhost:8080/` - Returns "Hello, World!"
 - `http://localhost:8080/slow` - Simulates a slow request
-- `http://localhost:9090/metrics` - View Prometheus metrics
+- `http://localhost:9091/metrics` - View Prometheus metrics
 
 ```bash
-curl -s http://localhost:9090/metrics | grep gunicorn_worker_
-# HELP gunicorn_worker_memory_bytes Memory usage of the worker process
-# TYPE gunicorn_worker_memory_bytes gauge
-gunicorn_worker_memory_bytes{pid="30134",worker_id="30128"} 2.54976e+07
-gunicorn_worker_memory_bytes{pid="30133",worker_id="30128"} 2.54976e+07
-# HELP gunicorn_worker_cpu_percent CPU usage of the worker process
-# TYPE gunicorn_worker_cpu_percent gauge
-gunicorn_worker_cpu_percent{pid="30134",worker_id="30128"} 0.0
-gunicorn_worker_cpu_percent{pid="30133",worker_id="30128"} 0.0
-# HELP gunicorn_worker_uptime_seconds Uptime of the worker process
-# TYPE gunicorn_worker_uptime_seconds gauge
-gunicorn_worker_uptime_seconds{pid="30134",worker_id="30128"} 26.000351667404175
-gunicorn_worker_uptime_seconds{pid="30133",worker_id="30128"} 28.92296528816223
-# HELP gunicorn_worker_request_duration_seconds Request duration in seconds
-# TYPE gunicorn_worker_request_duration_seconds histogram
-gunicorn_worker_request_duration_seconds_sum{worker_id="30128"} 0.6077625751495361
-gunicorn_worker_request_duration_seconds_bucket{le="0.1",worker_id="30128"} 1.0
-gunicorn_worker_request_duration_seconds_bucket{le="0.5",worker_id="30128"} 1.0
-gunicorn_worker_request_duration_seconds_bucket{le="1.0",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="2.5",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="5.0",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="10.0",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="30.0",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="60.0",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="+Inf",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_count{worker_id="30128"} 2.0
-# HELP gunicorn_worker_requests_total Total number of requests handled by this worker
-# TYPE gunicorn_worker_requests_total counter
-gunicorn_worker_requests_total{worker_id="30128"} 2.0
+curl -s http://localhost:9091/metrics | grep gunicorn_worker_
+# Example metrics output...
+```
+
+## Setting up Prometheus UI
+
+1. Create a `prometheus.yml` configuration file:
+```yaml
+global:
+  scrape_interval: 15s
+
+# Scrape our example exporter
+scrape_configs:
+  - job_name: 'gunicorn-prometheus-exporter'         
+    metrics_path: '/metrics'       
+    static_configs:
+      - targets: ['127.0.0.1:9091'] # our example exporter
+```
+
+2. Run Prometheus using Docker:
+```bash
+docker run -d \
+  --name prometheus \
+  --network host \
+  -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml:ro \
+  prom/prometheus
+```
+
+3. Access Prometheus UI at `http://localhost:9090` to:
+   - View metrics in the Graph interface
+   - Use PromQL queries to analyze worker performance
+   - Create graphs and dashboards
+   - Set up alerts based on metric thresholds
+
+Example PromQL queries:
+```promql
+# Current active workers and their uptime
+gunicorn_worker_uptime_seconds{worker_id=~"worker_.*"}
+
+# Worker memory usage (in MB)
+gunicorn_worker_memory_bytes{worker_id=~"worker_.*"} / 1024 / 1024
+
+# Request rate per worker (requests per second)
+rate(gunicorn_worker_requests_total{worker_id=~"worker_.*"}[5m])
+
+# Average request duration per worker
+rate(gunicorn_worker_request_duration_seconds_sum{worker_id=~"worker_.*"}[5m]) 
+/ 
+rate(gunicorn_worker_request_duration_seconds_count{worker_id=~"worker_.*"}[5m])
 ```
 
 ## Available Metrics
 
 The example will generate the following metrics:
-- Request counts and latencies
-- Worker memory and CPU usage
-- Worker uptime
-- Failed requests (if any)
+- `gunicorn_worker_requests_total`: Total number of requests handled by each worker
+- `gunicorn_worker_request_duration_seconds`: Request duration histogram
+- `gunicorn_worker_memory_bytes`: Worker memory usage
+- `gunicorn_worker_cpu_percent`: Worker CPU usage
+- `gunicorn_worker_uptime_seconds`: Worker uptime
+- `gunicorn_worker_failed_requests`: Failed request counts (if any)
 
 ## Notes
 
-- The metrics server runs on port 9090 by default
+- The metrics server runs on port 9091 by default
 - The application runs on port 8080
 - The multiprocess directory is required for proper metric collection
-- Make sure the multiprocess directory is writable by the Gunicorn process 
+- Make sure the multiprocess directory is writable by the Gunicorn process
diff --git a/example/gunicorn.conf.py b/example/gunicorn.conf.py
index 285a4251..a55be7be 100644
--- a/example/gunicorn.conf.py
+++ b/example/gunicorn.conf.py
@@ -8,11 +8,12 @@
 - Exports metrics on port 9090 at /metrics endpoint, aggregating across all workers
 """
 
-import os
 import logging
+import os
 
 import gunicorn.arbiter
-from prometheus_client import start_http_server, CollectorRegistry, multiprocess
+from prometheus_client import CollectorRegistry, multiprocess, start_http_server
+
 
 # —————————————————————————————————————————————————————————————————————————————
 # Hook to start a multiprocess‐aware Prometheus metrics server when Gunicorn is ready
@@ -23,7 +24,7 @@ def when_ready(server):
         logging.warning("PROMETHEUS_MULTIPROC_DIR not set; skipping metrics server")
         return
 
-    port = int(os.environ.get("PROMETHEUS_METRICS_PORT", 9090))
+    port = int(os.environ.get("PROMETHEUS_METRICS_PORT", 9091))
     logging.basicConfig(level=logging.INFO)
     logger = logging.getLogger(__name__)
     logger.info(f"Starting Prometheus multiprocess metrics server on :{port}")
diff --git a/pyproject.toml b/pyproject.toml
index d5d0614c..335c4f5b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -13,8 +13,6 @@ classifiers = [
     "Intended Audience :: Developers",
     # License classifier removed per Flit requirements
     "Programming Language :: Python :: 3",
-    "Programming Language :: Python :: 3.8",
-    "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
diff --git a/src/gunicorn_prometheus_exporter/metrics.py b/src/gunicorn_prometheus_exporter/metrics.py
index 6320a02b..2544eedf 100644
--- a/src/gunicorn_prometheus_exporter/metrics.py
+++ b/src/gunicorn_prometheus_exporter/metrics.py
@@ -83,17 +83,17 @@ def _samples(cls):
         """Get metric samples."""
         return cls._metric._samples()
 
-    @property
+    @classmethod
     def _name(cls):
         """Get metric name."""
         return cls._metric._name
 
-    @property
+    @classmethod
     def _documentation(cls):
         """Get metric documentation."""
         return cls._metric._documentation
 
-    @property
+    @classmethod
     def _labelnames(cls):
         """Get metric label names."""
         return cls._metric._labelnames
diff --git a/src/gunicorn_prometheus_exporter/plugin.py b/src/gunicorn_prometheus_exporter/plugin.py
index 4e963b3e..d0ee114f 100644
--- a/src/gunicorn_prometheus_exporter/plugin.py
+++ b/src/gunicorn_prometheus_exporter/plugin.py
@@ -15,7 +15,6 @@
 """
 
 import logging
-import os
 import time
 
 import psutil
@@ -42,15 +41,41 @@ class PrometheusWorker(SyncWorker):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.start_time = time.time()
-        self.worker_id = os.getpid()
+        # Create a unique worker ID using worker age and timestamp
+        # Format: worker_<age>_<timestamp>
+        self.worker_id = f"worker_{self.age}_{int(self.start_time)}"
         self.process = psutil.Process()
-        logger.info("PrometheusWorker initialized")
-
-    def init_process(self):
-        """Initialize the worker process."""
-        logger.info("Initializing worker process")
-        super().init_process()
-        logger.info("Worker process initialized")
+        logger.info(f"PrometheusWorker initialized with ID: {self.worker_id}")
+
+    def _clear_old_metrics(self):
+        """Clear only the old PID‐based worker samples."""
+        for MetricClass in [
+            WORKER_REQUESTS,
+            WORKER_REQUEST_DURATION,
+            WORKER_MEMORY,
+            WORKER_CPU,
+            WORKER_UPTIME,
+            WORKER_FAILED_REQUESTS,
+            WORKER_ERROR_HANDLING,
+            WORKER_STATE,
+        ]:
+            metric = MetricClass._metric
+            labelnames = list(metric._labelnames)
+
+            # 1) Collect the old label‐tuples to delete
+            to_delete = []
+            for label_values in list(metric._metrics.keys()):
+                try:
+                    wid = label_values[labelnames.index("worker_id")]
+                except ValueError:
+                    continue
+
+                if not isinstance(wid, str) or not wid.startswith("worker_"):
+                    to_delete.append(label_values)
+
+            # 2) Remove them from the internal store
+            for key in to_delete:
+                metric._metrics.pop(key, None)
 
     def update_worker_metrics(self):
         """Update worker metrics."""
diff --git a/tests/test_worker.py b/tests/test_worker.py
index 7a170382..245c1d7b 100644
--- a/tests/test_worker.py
+++ b/tests/test_worker.py
@@ -44,7 +44,10 @@ def worker():
 
 def test_worker_initialization(worker):
     """Test that worker initializes correctly."""
-    assert worker.worker_id == os.getpid()
+    assert worker.worker_id.startswith("worker_")
+    assert worker.worker_id.count("_") == 2  # Format: worker_<age>_<timestamp>
+    assert worker.worker_id.split("_")[1].isdigit()  # age is a number
+    assert worker.worker_id.split("_")[2].isdigit()  # timestamp is a number
     assert worker.process is not None
     assert worker.start_time > 0
 
@@ -219,3 +222,27 @@ def test_worker_state(worker):
         assert float(abort_sample.labels["timestamp"]) == pytest.approx(
             time.time(), rel=1e-3
         )
+
+
+def test_clear_old_metrics(worker):
+    """Test that old metrics are cleared properly."""
+    # Add some old format metrics
+    old_worker_id = "12345"
+    WORKER_REQUESTS.inc(worker_id=old_worker_id)
+    WORKER_MEMORY.set(1000, worker_id=old_worker_id)
+
+    # Add some new format metrics
+    WORKER_REQUESTS.inc(worker_id=worker.worker_id)
+    WORKER_MEMORY.set(2000, worker_id=worker.worker_id)
+
+    # Clear old metrics
+    worker._clear_old_metrics()
+
+    # Check that old metrics are gone but new ones remain
+    samples = list(WORKER_REQUESTS.collect())
+    assert len(samples) == 1
+    assert samples[0].samples[0].labels["worker_id"] == str(worker.worker_id)
+
+    samples = list(WORKER_MEMORY.collect())
+    assert len(samples) == 1
+    assert samples[0].samples[0].labels["worker_id"] == str(worker.worker_id)
diff --git a/tox.ini b/tox.ini
index 7124fa80..a9e4a45c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py38, py39, py310, py311, py312, py313
+envlist = py310, py311, py312, py313
 isolated_build = true
 
 [testenv]


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>tests/test_worker.py (1)</summary><blockquote>

`227-250`: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Fix test_clear_old_metrics to address pipeline failure.**

The test is failing in CI with a worker ID mismatch and error about missing 'remove' attribute.




Based on the pipeline errors, there are two issues:
1. The worker ID format in the test doesn't match what's expected in the implementation
2. The 'WorkerRequests' class doesn't have a 'remove' method

Consider updating your test to match the format used in the implementation (worker_v2_) and ensure the metrics classes have the appropriate remove method:

```diff
-    # Add some new format metrics
-    new_worker_id = f"worker_{worker.age}_{int(worker.start_time)}"
+    # Use same format as in the implementation
+    new_worker_id = f"worker_v2_{worker.age}_{int(worker.start_time)}"
```

---

🏁 Script executed:

```shell
#!/bin/bash
# Check the implementation of the _clear_old_metrics method
rg -A 20 "_clear_old_metrics" src/
```

Length of output: 2054

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate the definition of WORKER_REQUESTS and related metric classes
rg -n "WORKER_REQUESTS" -C3 src/
rg -n "class WorkerRequests" -C3 src/
rg -n "def remove" -n src/gunicorn_prometheus_exporter
```

Length of output: 2924

---



</details>

**Fix worker ID prefix in test and implement proper metric removal**

The CI failures reveal two issues in clearing old metrics:

1. Test uses the wrong worker ID format  
   `_clear_old_metrics()` only removes IDs starting with `"worker_v2_"`, but the test instantiates `"worker_{...}"`.  
2. Metric classes lack a `remove()` method  
   The plugin calls `metric.remove(sample.labels)`, but `BaseMetric` (and derived `Worker*` classes) don’t implement `remove()`, so an `AttributeError` is raised.

To resolve:

- In tests/test_worker.py:
  ```diff
  def test_clear_old_metrics(worker):
      """Test that old metrics are cleared properly."""
      # Add some new format metrics
  -   new_worker_id = f"worker_{worker.age}_{int(worker.start_time)}"
  +   new_worker_id = f"worker_v2_{worker.age}_{int(worker.start_time)}"
  ```

- In src/gunicorn_prometheus_exporter/plugin.py (inside `_clear_old_metrics()`):
  ```diff
  -                    metric.remove(sample.labels)
  +                    metric.remove_sample(**sample.labels)
  ```
  or alternatively add a `remove()` alias in `BaseMetric` (src/gunicorn_prometheus_exporter/metrics.py) that forwards to `remove_sample()`.

With these changes, old metrics will be cleared without errors and your test will pass.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: CI</summary>

[error] 245-245: Test failure in test_clear_old_metrics: AssertionError due to mismatch in worker_id labels ('worker_0_1747573127' != 'worker_0_1747573128'). Also logged error: 'Error clearing old metrics: type object 'WorkerRequests' has no attribute 'remove''.

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>example/gunicorn.conf.py (1)</summary><blockquote>

`14-14`: **Remove unused import.**

The `gunicorn.arbiter` module is imported but not used anywhere in this file.

```diff
-import gunicorn.arbiter
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

14-14: `gunicorn.arbiter` imported but unused

Remove unused import: `gunicorn.arbiter`

(F401)

</details>

</details>

</blockquote></details>
<details>
<summary>example/README.md (1)</summary><blockquote>

`70-74`: **Minor grammar fix needed.**

Line 70 is missing "the" article before "Prometheus UI".

```diff
-3. Access Prometheus UI at `http://localhost:9090` to:
+3. Access the Prometheus UI at `http://localhost:9090` to:
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~70-~70: You might be missing the article “the” here.
Context: ...l:ro \   prom/prometheus ```  3. Access Prometheus UI at `http://localhost:9090` to:    - ...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

</blockquote></details>
<details>
<summary>README.md (1)</summary><blockquote>

`134-139`: **Port number inconsistency in Prometheus example**  


Earlier documentation (and `example/README.md`) reference port **9091**, but the `prometheus.yml` sample still targets **9090**:

```yaml
static_configs:
  - targets: ['127.0.0.1:9090']
```

Aligning these values prevents copy-paste configuration errors:

```diff
-      - targets: ['127.0.0.1:9090'] # our example exporter
+      - targets: ['127.0.0.1:9091'] # our example exporter
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8d18b25f455bbf585cd7c62f437eb8752809606c and 9403a367e90b24f748ec0a50c6991282bf85a360.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `.gitignore` (1 hunks)
* `README.md` (2 hunks)
* `example/README.md` (1 hunks)
* `example/gunicorn.conf.py` (1 hunks)
* `src/gunicorn_prometheus_exporter/plugin.py` (1 hunks)
* `tests/test_worker.py` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>tests/test_worker.py (3)</summary><blockquote>

<details>
<summary>tests/conftest.py (1)</summary>

* `worker_id` (26-28)

</details>
<details>
<summary>src/gunicorn_prometheus_exporter/metrics.py (3)</summary>

* `inc` (102-103)
* `collect` (77-79)
* `labels` (72-74)

</details>
<details>
<summary>src/gunicorn_prometheus_exporter/plugin.py (1)</summary>

* `_clear_old_metrics` (58-78)

</details>

</blockquote></details>
<details>
<summary>src/gunicorn_prometheus_exporter/plugin.py (1)</summary><blockquote>

<details>
<summary>src/gunicorn_prometheus_exporter/metrics.py (2)</summary>

* `_samples` (82-84)
* `labels` (72-74)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.11.9)</summary>

<details>
<summary>example/gunicorn.conf.py</summary>

14-14: `gunicorn.arbiter` imported but unused

Remove unused import: `gunicorn.arbiter`

(F401)

</details>

</details>
<details>
<summary>🪛 GitHub Actions: CI</summary>

<details>
<summary>tests/test_worker.py</summary>

[error] 245-245: Test failure in test_clear_old_metrics: AssertionError due to mismatch in worker_id labels ('worker_0_1747573127' != 'worker_0_1747573128'). Also logged error: 'Error clearing old metrics: type object 'WorkerRequests' has no attribute 'remove''.

</details>

</details>
<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>example/README.md</summary>

[uncategorized] ~70-~70: You might be missing the article “the” here.
Context: ...l:ro \   prom/prometheus ```  3. Access Prometheus UI at `http://localhost:9090` to:    - ...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>
<details>
<summary>README.md</summary>

[uncategorized] ~55-~55: Loose punctuation mark.
Context: ...r metrics:  - `gunicorn_worker_requests`: Total number of requests handled by eac...

(UNLIKELY_OPENING_PUNCTUATION)

---

[uncategorized] ~105-~105: Loose punctuation mark.
Context: ...- `gunicorn_master_worker_restart_total`: Total number of worker restarts   - Lab...

(UNLIKELY_OPENING_PUNCTUATION)

---

[uncategorized] ~150-~150: You might be missing the article “the” here.
Context: ...l:ro \   prom/prometheus ```  3. Access Prometheus UI at `http://localhost:9090` to:    - ...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>.gitignore (1)</summary>

`6-7`: **LGTM! Added appropriate entries to gitignore.**

The additions of 'explore' and 'trace.json' will help keep development and debugging files out of version control.

</details>
<details>
<summary>example/gunicorn.conf.py (1)</summary>

`11-15`: **Reordering of imports is fine.**

The imports have been reordered, which doesn't affect functionality.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

14-14: `gunicorn.arbiter` imported but unused

Remove unused import: `gunicorn.arbiter`

(F401)

</details>

</details>

</details>
<details>
<summary>tests/test_worker.py (1)</summary>

`45-50`: **LGTM! Updated tests reflect new worker ID format.**

The test has been correctly updated to verify the new `worker_<age>_<timestamp>` format.

</details>
<details>
<summary>example/README.md (3)</summary>

`46-91`: **Great addition of Prometheus UI setup and example queries!**

This section provides valuable guidance for users on setting up and using Prometheus with the exporter.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~70-~70: You might be missing the article “the” here.
Context: ...l:ro \   prom/prometheus ```  3. Access Prometheus UI at `http://localhost:9090` to:    - ...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

---

`96-105`: **LGTM! Improved metrics documentation.**

The expanded list of available metrics provides clearer documentation for users.

---

`77-90`: <details>
<summary>❌ Incorrect review comment</summary>

**Ensure PromQL queries match the new worker ID format.**

The worker ID format in queries should match what's implemented in the code.

Based on the code snippets, it appears the new format is "worker_v2_" but the queries use "worker_".



Update the queries to use the correct format if needed:

```diff
-# Current active workers and their uptime
-gunicorn_worker_uptime_seconds{worker_id=~"worker_.*"}
+# Current active workers and their uptime
+gunicorn_worker_uptime_seconds{worker_id=~"worker_v2_.*"}
```

(Apply similar changes to other queries if needed)

---

🏁 Script executed:

```shell
#!/bin/bash
# Check the worker ID format in the implementation
rg "worker_id" src/ -A 2 -B 2
```

Length of output: 7085

---



</details>

**No updates needed to PromQL queries**

The plugin still generates IDs as `worker_<age>_<timestamp>`, and the existing regex `worker_.*` in README.md will match these labels. There is no `worker_v2_` prefix in the code, so you can ignore the suggestion to switch to `worker_v2_.*`. 

> Likely an incorrect or invalid review comment.

</details>
<details>
<summary>src/gunicorn_prometheus_exporter/plugin.py (1)</summary>

`44-47`: **Consider clarifying the new worker-ID versioning scheme.**  

The new identifier drops the “v2” infix that `_clear_old_metrics()` relies on (see below).  If the intent is to make this the canonical v2 scheme, either:

1. Rename the prefix to `worker_v2_…`, or  
2. Change the clean-up logic to detect *numeric* (PID-only) IDs rather than relying on a string prefix.

Failing to keep both sites in sync will silently delete freshly-emitted metrics.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-18T13:01:42Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>example/README.md (1)</summary><blockquote>

`25-25`: **Port references are now consistent.**

All references to the metrics port have been updated to use 9091, which is consistent with the configuration in `gunicorn.conf.py`. This resolves the previous inconsistency issues.




Also applies to: 34-34, 39-39, 42-42

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>example/gunicorn.conf.py (1)</summary><blockquote>

`14-14`: **Remove unused import.**

The `gunicorn.arbiter` module is imported but not used in this file.

```diff
-import gunicorn.arbiter
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.11.9)</summary>

14-14: `gunicorn.arbiter` imported but unused

Remove unused import: `gunicorn.arbiter`

(F401)

</details>

</details>

</blockquote></details>
<details>
<summary>example/README.md (1)</summary><blockquote>

`70-70`: **Minor grammatical suggestion.**

Consider adding the article "the" before "Prometheus UI" for better grammar.

```diff
-3. Access Prometheus UI at `http://localhost:9090` to:
+3. Access the Prometheus UI at `http://localhost:9090` to:
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~70-~70: You might be missing the article “the” here.
Context: ...l:ro \   prom/prometheus ```  3. Access Prometheus UI at `http://localhost:9090` to:    - ...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9403a367e90b24f748ec0a50c6991282bf85a360 and 0e8fa7832825a49c1e5c43f970e5a26d14eb7c56.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `example/README.md` (2 hunks)
* `example/gunicorn.conf.py` (2 hunks)
* `src/gunicorn_prometheus_exporter/metrics.py` (1 hunks)
* `src/gunicorn_prometheus_exporter/plugin.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>src/gunicorn_prometheus_exporter/plugin.py (2)</summary><blockquote>

<details>
<summary>tests/conftest.py (1)</summary>

* `worker_id` (26-28)

</details>
<details>
<summary>src/gunicorn_prometheus_exporter/metrics.py (1)</summary>

* `_labelnames` (97-99)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>example/README.md</summary>

[uncategorized] ~70-~70: You might be missing the article “the” here.
Context: ...l:ro \   prom/prometheus ```  3. Access Prometheus UI at `http://localhost:9090` to:    - ...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>
<details>
<summary>🪛 Ruff (0.11.9)</summary>

<details>
<summary>example/gunicorn.conf.py</summary>

14-14: `gunicorn.arbiter` imported but unused

Remove unused import: `gunicorn.arbiter`

(F401)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>src/gunicorn_prometheus_exporter/plugin.py (2)</summary>

`44-48`: **Worker ID format change improves clarity and tracking.**

The new worker ID format (`worker_<age>_<timestamp>`) is more informative than the previous PID-based approach. This change makes worker identification more readable and provides additional context about when the worker was created, which is valuable for troubleshooting and monitoring.

---

`50-79`: **The metric cleanup implementation correctly handles legacy worker IDs.**

The new `_clear_old_metrics` method properly targets only the old PID-based worker samples by checking if the worker ID doesn't start with "worker_". The method correctly avoids the API misuse from the previous implementation by directly manipulating the internal metrics dictionary.

This is a robust approach that:
1. Preserves metrics from workers using the new ID format
2. Only removes metrics from legacy workers 
3. Efficiently performs the cleanup operation

</details>
<details>
<summary>src/gunicorn_prometheus_exporter/metrics.py (1)</summary>

`86-99`: **Conversion from properties to class methods is appropriate.**

Converting `_name`, `_documentation`, and `_labelnames` from properties to class methods aligns with how these are being used in the codebase. Since these methods access class-level attributes rather than instance-level data, making them class methods improves code clarity and maintainability.

</details>
<details>
<summary>example/gunicorn.conf.py (2)</summary>

`15-15`: **Import order reorganization looks good.**

The reordering of imports from the prometheus_client module provides better organization.

---

`27-27`: **Port change from 9090 to 9091 avoids conflict with default Prometheus port.**

Changing the default metrics port to 9091 is a good choice since 9090 is the default Prometheus server port. This change helps avoid port conflicts when running both services on the same host.

</details>
<details>
<summary>example/README.md (2)</summary>

`46-91`: **Comprehensive Prometheus UI setup documentation is valuable.**

The new section on setting up the Prometheus UI provides detailed, step-by-step instructions that will be very helpful for users. The inclusion of example PromQL queries tailored to the exporter's metrics is especially useful for users trying to build monitoring dashboards.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~70-~70: You might be missing the article “the” here.
Context: ...l:ro \   prom/prometheus ```  3. Access Prometheus UI at `http://localhost:9090` to:    - ...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

---

`96-101`: **Improved metrics documentation.**

The expanded metrics documentation provides clearer descriptions of each metric and better reflects the actual implementation.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-18T19:17:39Z

Review comments:
diff_hunk: @@ -31,56 +31,78 @@ gunicorn --config gunicorn.conf.py app:app
 ## Testing
 
 1. The application will be available at `http://localhost:8080`
-2. The metrics will be available at `http://localhost:9090/metrics`
+2. The metrics will be available at `http://localhost:9091/metrics`
 
 Try these endpoints:
 - `http://localhost:8080/` - Returns "Hello, World!"
 - `http://localhost:8080/slow` - Simulates a slow request
-- `http://localhost:9090/metrics` - View Prometheus metrics
+- `http://localhost:9091/metrics` - View Prometheus metrics
 
 ```bash
-curl -s http://localhost:9090/metrics | grep gunicorn_worker_
-# HELP gunicorn_worker_memory_bytes Memory usage of the worker process
-# TYPE gunicorn_worker_memory_bytes gauge
-gunicorn_worker_memory_bytes{pid="30134",worker_id="30128"} 2.54976e+07
-gunicorn_worker_memory_bytes{pid="30133",worker_id="30128"} 2.54976e+07
-# HELP gunicorn_worker_cpu_percent CPU usage of the worker process
-# TYPE gunicorn_worker_cpu_percent gauge
-gunicorn_worker_cpu_percent{pid="30134",worker_id="30128"} 0.0
-gunicorn_worker_cpu_percent{pid="30133",worker_id="30128"} 0.0
-# HELP gunicorn_worker_uptime_seconds Uptime of the worker process
-# TYPE gunicorn_worker_uptime_seconds gauge
-gunicorn_worker_uptime_seconds{pid="30134",worker_id="30128"} 26.000351667404175
-gunicorn_worker_uptime_seconds{pid="30133",worker_id="30128"} 28.92296528816223
-# HELP gunicorn_worker_request_duration_seconds Request duration in seconds
-# TYPE gunicorn_worker_request_duration_seconds histogram
-gunicorn_worker_request_duration_seconds_sum{worker_id="30128"} 0.6077625751495361
-gunicorn_worker_request_duration_seconds_bucket{le="0.1",worker_id="30128"} 1.0
-gunicorn_worker_request_duration_seconds_bucket{le="0.5",worker_id="30128"} 1.0
-gunicorn_worker_request_duration_seconds_bucket{le="1.0",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="2.5",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="5.0",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="10.0",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="30.0",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="60.0",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="+Inf",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_count{worker_id="30128"} 2.0
-# HELP gunicorn_worker_requests_total Total number of requests handled by this worker
-# TYPE gunicorn_worker_requests_total counter
-gunicorn_worker_requests_total{worker_id="30128"} 2.0
+curl -s http://localhost:9091/metrics | grep gunicorn_worker_
+# Example metrics output...
+```
+
+## Setting up Prometheus UI
+
+1. Create a `prometheus.yml` configuration file:
+```yaml
+global:
+  scrape_interval: 15s
+
+# Scrape our example exporter
+scrape_configs:
+  - job_name: 'gunicorn-prometheus-exporter'         
+    metrics_path: '/metrics'       
+    static_configs:
+      - targets: ['127.0.0.1:9090'] # our example exporter
+```
+
+2. Run Prometheus using Docker:
+```bash
+docker run -d \
+  --name prometheus \
+  --network host \
+  -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml:ro \
+  prom/prometheus
+```
+
+3. Access Prometheus UI at `http://localhost:9090` to:
+   - View metrics in the Graph interface
+   - Use PromQL queries to analyze worker performance
+   - Create graphs and dashboards
+   - Set up alerts based on metric thresholds
+
+Example PromQL queries:
+```promql
+# Current active workers and their uptime
+gunicorn_worker_uptime_seconds{worker_id=~"worker_.*"}
+
+# Worker memory usage (in MB)
+gunicorn_worker_memory_bytes{worker_id=~"worker_.*"} / 1024 / 1024
+
+# Request rate per worker (requests per second)
+rate(gunicorn_worker_requests_total{worker_id=~"worker_.*"}[5m])
+
+# Average request duration per worker
+rate(gunicorn_worker_request_duration_seconds_sum{worker_id=~"worker_.*"}[5m]) 
+/ 
+rate(gunicorn_worker_request_duration_seconds_count{worker_id=~"worker_.*"}[5m])
 ```
 
 ## Available Metrics
 
 The example will generate the following metrics:
-- Request counts and latencies
-- Worker memory and CPU usage
-- Worker uptime
-- Failed requests (if any)
+- `gunicorn_worker_requests_total`: Total number of requests handled by each worker
+- `gunicorn_worker_request_duration_seconds`: Request duration histogram
+- `gunicorn_worker_memory_bytes`: Worker memory usage
+- `gunicorn_worker_cpu_percent`: Worker CPU usage
+- `gunicorn_worker_uptime_seconds`: Worker uptime
+- `gunicorn_worker_failed_requests`: Failed request counts (if any)
 
 ## Notes
 
-- The metrics server runs on port 9090 by default
+- The metrics server runs on port 9091 by default
path: example/README.md
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix port inconsistency.**

This line mentions port 9091, but earlier setup instructions use 9090.

```diff
-The metrics server runs on port 9091 by default
+The metrics server runs on port 9090 by default
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
- The metrics server runs on port 9090 by default
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In example/README.md at line 105, the documentation incorrectly states that the
metrics server runs on port 9091, while earlier instructions specify port 9090.
Update this line to reflect port 9090 to maintain consistency throughout the
README.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-18T13:01:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,56 +31,78 @@
 ## Testing
 
 1. The application will be available at `http://localhost:8080`
-2. The metrics will be available at `http://localhost:9090/metrics`
+2. The metrics will be available at `http://localhost:9091/metrics`
 
 Try these endpoints:
 - `http://localhost:8080/` - Returns "Hello, World!"
 - `http://localhost:8080/slow` - Simulates a slow request
-- `http://localhost:9090/metrics` - View Prometheus metrics
+- `http://localhost:9091/metrics` - View Prometheus metrics
 
 ```bash
-curl -s http://localhost:9090/metrics | grep gunicorn_worker_
-# HELP gunicorn_worker_memory_bytes Memory usage of the worker process
-# TYPE gunicorn_worker_memory_bytes gauge
-gunicorn_worker_memory_bytes{pid="30134",worker_id="30128"} 2.54976e+07
-gunicorn_worker_memory_bytes{pid="30133",worker_id="30128"} 2.54976e+07
-# HELP gunicorn_worker_cpu_percent CPU usage of the worker process
-# TYPE gunicorn_worker_cpu_percent gauge
-gunicorn_worker_cpu_percent{pid="30134",worker_id="30128"} 0.0
-gunicorn_worker_cpu_percent{pid="30133",worker_id="30128"} 0.0
-# HELP gunicorn_worker_uptime_seconds Uptime of the worker process
-# TYPE gunicorn_worker_uptime_seconds gauge
-gunicorn_worker_uptime_seconds{pid="30134",worker_id="30128"} 26.000351667404175
-gunicorn_worker_uptime_seconds{pid="30133",worker_id="30128"} 28.92296528816223
-# HELP gunicorn_worker_request_duration_seconds Request duration in seconds
-# TYPE gunicorn_worker_request_duration_seconds histogram
-gunicorn_worker_request_duration_seconds_sum{worker_id="30128"} 0.6077625751495361
-gunicorn_worker_request_duration_seconds_bucket{le="0.1",worker_id="30128"} 1.0
-gunicorn_worker_request_duration_seconds_bucket{le="0.5",worker_id="30128"} 1.0
-gunicorn_worker_request_duration_seconds_bucket{le="1.0",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="2.5",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="5.0",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="10.0",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="30.0",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="60.0",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_bucket{le="+Inf",worker_id="30128"} 2.0
-gunicorn_worker_request_duration_seconds_count{worker_id="30128"} 2.0
-# HELP gunicorn_worker_requests_total Total number of requests handled by this worker
-# TYPE gunicorn_worker_requests_total counter
-gunicorn_worker_requests_total{worker_id="30128"} 2.0
+curl -s http://localhost:9091/metrics | grep gunicorn_worker_
+# Example metrics output...
+```
path: example/README.md
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Update port references for consistency.**

The README now refers to port 9091 for metrics, but the setup instructions still use 9090.

Either update the setup instructions or change these references back to 9090 for consistency:

```diff
-2. The metrics will be available at `http://localhost:9091/metrics`
+2. The metrics will be available at `http://localhost:9090/metrics`

...

-curl -s http://localhost:9091/metrics | grep gunicorn_worker_
+curl -s http://localhost:9090/metrics | grep gunicorn_worker_
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
2. The metrics will be available at `http://localhost:9090/metrics`

Try these endpoints:
- `http://localhost:8080/` - Returns "Hello, World!"
- `http://localhost:8080/slow` - Simulates a slow request
- `http://localhost:9091/metrics` - View Prometheus metrics

```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In example/README.md between lines 34 and 44, the port number for metrics is
inconsistently referenced as 9091 in some places and 9090 in the setup
instructions. To fix this, update all port references to use the same port
number consistently, either change all to 9091 or revert all to 9090, ensuring
the README matches the actual setup configuration.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-18T13:01:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,16 +41,42 @@ class PrometheusWorker(SyncWorker):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.start_time = time.time()
-        self.worker_id = os.getpid()
+        # Create a unique worker ID using worker age and timestamp
+        # Format: worker_<age>_<timestamp>
+        self.worker_id = f"worker_{self.age}_{int(self.start_time)}"
         self.process = psutil.Process()
-        logger.info("PrometheusWorker initialized")
+        logger.info(f"PrometheusWorker initialized with ID: {self.worker_id}")
 
     def init_process(self):
         """Initialize the worker process."""
         logger.info("Initializing worker process")
+        # Clear any old metrics with PID-based worker IDs
+        self._clear_old_metrics()
         super().init_process()
         logger.info("Worker process initialized")
 
+    def _clear_old_metrics(self):
+        """Clear metrics with old PID-based worker IDs."""
+        try:
+            # Clear metrics for all workers that don't use the new format
+            for metric in [
+                WORKER_REQUESTS,
+                WORKER_REQUEST_DURATION,
+                WORKER_MEMORY,
+                WORKER_CPU,
+                WORKER_UPTIME,
+                WORKER_FAILED_REQUESTS,
+                WORKER_ERROR_HANDLING,
+                WORKER_STATE,
+            ]:
+                for sample in metric._samples():
+                    if not str(sample.labels.get("worker_id", "")).startswith(
+                        "worker_v2_"
+                    ):
+                        metric.remove(sample.labels)
+        except Exception as e:
+            logger.error(f"Error clearing old metrics: {e}")
+
path: src/gunicorn_prometheus_exporter/plugin.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Cleanup logic wipes out *all* new-format samples & mis-uses the Prometheus client API**  


Two problems combine here:

1. `if not …startswith("worker_v2_"):`  
   Any sample whose `worker_id` **doesn’t** start with `worker_v2_` is removed.  
   Because the new IDs are `worker_<age>_<ts>`, every sample the current worker produces will also be removed.

2. `metric.remove(sample.labels)`  
   `remove()` expects *label values* (`*labelvalues`) **or** keyword args (`**labels`), not a dict positional argument.  
   Passing a dict raises a `TypeError` on most metric classes.

Proposed minimal fix:

```diff
@@
-                for sample in metric._samples():
-                    if not str(sample.labels.get("worker_id", "")).startswith(
-                        "worker_v2_"
-                    ):
-                        metric.remove(sample.labels)
+                for sample in metric._samples():
+                    worker_lbl = str(sample.labels.get("worker_id", ""))
+                    # Old IDs were pure PIDs – all digits.
+                    if worker_lbl.isdigit():
+                        # `remove` accepts keyword label mapping
+                        metric.remove(**sample.labels)
```

This:

• Preserves the newly-generated `worker_<age>_<ts>` samples.  
• Targets only legacy numeric IDs.  
• Uses the supported `remove(**labels)` call signature.

Please adjust unit tests accordingly to ensure the worker’s own metrics remain visible after startup.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In src/gunicorn_prometheus_exporter/plugin.py lines 58 to 79, the cleanup logic
incorrectly removes all new-format samples because it checks for worker IDs not
starting with "worker_v2_" instead of targeting legacy numeric IDs, and it
misuses the Prometheus client API by passing a dict to metric.remove() instead
of keyword arguments. Fix this by changing the condition to identify legacy
numeric worker IDs correctly and call metric.remove() with keyword arguments
unpacked from the labels dict (remove(**sample.labels)). Also, update unit tests
to verify that new-format worker metrics remain after cleanup.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 0e8fa78
created_at: 2025-05-18T13:01:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-18T12:58:28Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The updates introduce a new worker ID format for Prometheus metrics, update documentation to reflect this and provide detailed Prometheus integration instructions, and add logic to clear old metrics. Tests are adjusted to validate the new worker ID format and metric cleanup, while minor import order and ignore file adjustments are made.

## Changes

| File(s)                                               | Change Summary |
|-------------------------------------------------------|---------------|
| .gitignore                                            | Added `explore` and `trace.json` to ignored files. |
| README.md                                             | Expanded documentation: detailed worker metrics, new worker ID format, Prometheus integration, example PromQL queries, and UI setup instructions. Improved formatting and structure. |
| example/README.md                                     | Updated metrics port to 9091, revised example output, added Prometheus UI setup, and expanded metric/query documentation. |
| example/gunicorn.conf.py                              | Reordered and reformatted import statements; changed default Prometheus metrics port from 9090 to 9091. |
| src/gunicorn_prometheus_exporter/plugin.py            | Changed worker ID generation to `worker_<age>_<timestamp>`, added `_clear_old_metrics` to remove old PID-based metrics, updated logging, and adjusted initialization logic. |
| src/gunicorn_prometheus_exporter/metrics.py           | Converted `BaseMetric` class properties `_name`, `_documentation`, and `_labelnames` to class methods. |
| tests/test_worker.py                                  | Updated tests for new worker ID format, added test for clearing old metrics, and verified correct metric retention/removal. |
| pyproject.toml                                        | Removed Python 3.8 and 3.9 from Python version classifiers. |
| tox.ini                                              | Removed Python 3.8 and 3.9 from tox testing environments. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Master as Gunicorn Master
    participant Worker as PrometheusWorker
    participant Prometheus as Prometheus Server

    Master->>Worker: Fork new worker process (age, timestamp)
    Worker->>Worker: Generate worker_id = "worker_<age>_<timestamp>"
    Worker->>Worker: _clear_old_metrics()
    Worker->>Worker: Remove metrics with old worker_id format
    Worker->>Prometheus: Expose metrics with new worker_id
    Prometheus->>Worker: Scrape metrics endpoint
    Worker-->>Prometheus: Return metrics (with new worker_id)
```

## Poem

> In the warren of code, a new ID appears,  
> "worker_age_timestamp"—it hops and it cheers!  
> Old metrics are swept with a twitch of the nose,  
> Prometheus watches as the new garden grows.  
> With docs and with tests, our patch is complete—  
> A rabbit’s delight: metrics tidy and neat! 🐇✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!NOTE]
> <details>
> <summary>⚡️ AI Code Reviews for VS Code, Cursor, Windsurf</summary>
> 
> CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
> Learn more [here](http://coderabbit.ai/ide).
> 
> </details>

---

> [!NOTE]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e8e0fcaa070140ad73fea66c6910cd4378b1da98 and ee0c509024ce19309b7f3dfe7916c8657aeb09e1.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `src/gunicorn_prometheus_exporter/plugin.py` (1 hunks)
* `tests/test_worker.py` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* tests/test_worker.py
* src/gunicorn_prometheus_exporter/plugin.py

</details>

</details>
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Agent-Hellboy/gunicorn-prometheus-exporter&utm_content=11):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov-commenter
created_at: 2025-05-18T12:58:55Z
body: ## [Codecov](https://app.codecov.io/gh/Agent-Hellboy/gunicorn-prometheus-exporter/pull/11?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Prince+Roshan) Report
:x: Patch coverage is `89.47368%` with `2 lines` in your changes missing coverage. Please review.
:white_check_mark: Project coverage is 93.22%. Comparing base ([`8d18b25`](https://app.codecov.io/gh/Agent-Hellboy/gunicorn-prometheus-exporter/commit/8d18b25f455bbf585cd7c62f437eb8752809606c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Prince+Roshan)) to head ([`ee0c509`](https://app.codecov.io/gh/Agent-Hellboy/gunicorn-prometheus-exporter/commit/ee0c509024ce19309b7f3dfe7916c8657aeb09e1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Prince+Roshan)).
:warning: Report is 440 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/Agent-Hellboy/gunicorn-prometheus-exporter/pull/11?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Prince+Roshan) | Patch % | Lines |
|---|---|---|
| [src/gunicorn\_prometheus\_exporter/plugin.py](https://app.codecov.io/gh/Agent-Hellboy/gunicorn-prometheus-exporter/pull/11?src=pr&el=tree&filepath=src%2Fgunicorn_prometheus_exporter%2Fplugin.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Prince+Roshan#diff-c3JjL2d1bmljb3JuX3Byb21ldGhldXNfZXhwb3J0ZXIvcGx1Z2luLnB5) | 87.50% | [2 Missing :warning: ](https://app.codecov.io/gh/Agent-Hellboy/gunicorn-prometheus-exporter/pull/11?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Prince+Roshan) |

<details><summary>Additional details and impacted files</summary>



```diff
@@            Coverage Diff             @@
##             main      #11      +/-   ##
==========================================
+ Coverage   92.34%   93.22%   +0.87%     
==========================================
  Files           4        4              
  Lines         183      192       +9     
==========================================
+ Hits          169      179      +10     
+ Misses         14       13       -1     
```

| [Flag](https://app.codecov.io/gh/Agent-Hellboy/gunicorn-prometheus-exporter/pull/11/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Prince+Roshan) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/Agent-Hellboy/gunicorn-prometheus-exporter/pull/11/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Prince+Roshan) | `93.22% <89.47%> (+0.87%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Prince+Roshan#carryforward-flags-in-the-pull-request-comment) to find out more.
</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Agent-Hellboy/gunicorn-prometheus-exporter/pull/11?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Prince+Roshan).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Prince+Roshan).
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE