Repository: stephenhillier/starlette_exporter

Issue:
Feature request: skip_paths take patterns
The [flask-prometheus-exporter](https://github.com/rycus86/prometheus_flask_exporter) uses `excluded_paths` which can take regular expressions like "/management/.*" which then excludes all paths under that path. I would like to do the same (I am trying to replace flask-prometheus-exporter with starlette-exporter without affecting metrics), but "skip_paths" just takes a list of strings. To get the same functionality I would have to list every endpoint under "/management/" - and if ever another endpoint is added to management (in another module), I would have to remember to add that as well.

Therefore, I request that skip_paths accepts regular expressions, or some kind of pattern, or at least a leading base path. I believe this won't break any backwards compatibility since an exact string can be seen as a regular expression that just matches that string (especially given the limited charset of urls). If backwards compatibility is a concern, one could simply pass in re.Pattern objects or similar to skip_paths and treat instances of str as before.

Discussion:

Patch:
diff --git a/README.md b/README.md
index b4ec779..ecc4776 100644
--- a/README.md
+++ b/README.md
@@ -84,7 +84,7 @@ retrieves a value from the `Request` object. [See below](#labels) for examples.
 
 `buckets`: accepts an optional list of numbers to use as histogram buckets. The default value is `None`, which will cause the library to fall back on the Prometheus defaults (currently `[0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0]`).
 
-`skip_paths`: accepts an optional list of paths that will not collect metrics. The default value is `None`, which will cause the library to collect metrics on every requested path. This option is useful to avoid collecting metrics on health check, readiness or liveness probe endpoints.
+`skip_paths`: accepts an optional list of paths, or regular expressions for paths, that will not collect metrics. The default value is `None`, which will cause the library to collect metrics on every requested path. This option is useful to avoid collecting metrics on health check, readiness or liveness probe endpoints.
 
 `skip_methods`: accepts an optional list of methods that will not collect metrics. The default value is `None`, which will cause the library to collect request metrics with each method. This option is useful to avoid collecting metrics on requests related to the communication description for endpoints.
 
diff --git a/starlette_exporter/middleware.py b/starlette_exporter/middleware.py
index 8dfbab3..fa30197 100644
--- a/starlette_exporter/middleware.py
+++ b/starlette_exporter/middleware.py
@@ -1,5 +1,6 @@
 """ Middleware for exporting Prometheus metrics using Starlette """
 import logging
+import re
 import time
 from collections import OrderedDict
 from contextlib import suppress
@@ -101,9 +102,9 @@ class PrometheusMiddleware:
         self.kwargs = {}
         if buckets is not None:
             self.kwargs["buckets"] = buckets
-        self.skip_paths = []
+        self.skip_paths: List[re.Pattern] = []
         if skip_paths is not None:
-            self.skip_paths = skip_paths
+            self.skip_paths = [re.compile(path) for path in skip_paths]
         self.skip_methods = []
         if skip_methods is not None:
             self.skip_methods = skip_methods
@@ -264,7 +265,7 @@ class PrometheusMiddleware:
         if base_path and path.startswith(base_path):
             path = path[len(base_path) :]
 
-        if path in self.skip_paths or method in self.skip_methods:
+        if any(pattern.fullmatch(path) for pattern in self.skip_paths) or method in self.skip_methods:
             await self.app(scope, receive, send)
             return
 


Test patch:
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
index 86659b9..3afd779 100644
--- a/tests/test_middleware.py
+++ b/tests/test_middleware.py
@@ -391,6 +391,20 @@ class TestMiddleware:
         metrics = client.get("/metrics").content.decode()
         assert """path="/health""" not in metrics
 
+    def test_skip_paths__re(self, testapp):
+        """test skip_paths using regular expression"""
+        client = TestClient(testapp(skip_paths=[r"/h.*"]))
+        client.get("/health")
+        metrics = client.get("/metrics").content.decode()
+        assert """path="/health""" not in metrics
+
+    def test_skip_paths__re_partial(self, testapp):
+        """test skip_paths using regular expression"""
+        client = TestClient(testapp(skip_paths=[r"/h"]))
+        client.get("/health")
+        metrics = client.get("/metrics").content.decode()
+        assert """path="/health""" in metrics
+
     def test_skip_methods(self, testapp):
         """test that requests doesn't appear in the counter"""
         client = TestClient(testapp(skip_methods=["POST"]))