You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Expose a Prometheus-scrapable metrics endpoint for multiprocess workers #3191
Raised in discussion #2539, where the ask was per-worker instrumentation scrapable by a Prometheus-compatible system.
The instrumentation half has since landed. PR #2201 added pr_agent/telemetry/, opening a pr_agent.command span per request (pr_agent/agent/pr_agent.py:188) and incrementing a pr_agent.commands counter labelled by command and provider (pr_agent/telemetry/meter.py:80-82). Because every webhook adapter calls PRAgent().handle_request inside the forked worker, this already runs per worker rather than in the master. It is configured under [otel] in configuration.toml and is off by default.
What is still missing is a Prometheus scrape target. pr_agent/telemetry/types.py defines exporters as console, otlp and none only, and meter.py builds a PeriodicExportingMetricReader around one of those, so the only route to Prometheus today is through an OTel Collector with a Prometheus exporter. That is a reasonable deployment for some, but it is an extra hop for anyone who already runs Prometheus and nothing else.
A native endpoint has to handle gunicorn's multiprocess model, which is why pr_agent/servers/gunicorn_config.py:4 still carries a commented-out from prometheus_client import multiprocess from an earlier attempt, and why post_fork currently only re-runs setup_logger.
Scope: decide whether to add a prometheus exporter option backed by prometheus_client multiprocess mode with a PROMETHEUS_MULTIPROC_DIR, or to document the OTel Collector route as the supported answer and remove the dead import.
I dug through the current telemetry wiring and the native Prometheus route is feasible, including the gunicorn multiprocess part. What is already in place: the pr_agent.commands counter and the pr_agent.command span, with the exporter selected in _create_metric_exporter (pr_agent/telemetry/meter.py:45) off the ExporterType values (console, otlp, none) configured under [otel].
The plan for a first class scrape target:
Dependency. Add prometheus_client and opentelemetry-exporter-prometheus pinned to the same 1.43.0 as the otel SDK already in pyproject.toml. PrometheusMetricExporter plugs straight into the existing PeriodicExportingMetricReader, so the meter does not need restructuring.
Exporter type. Add prometheus to ExporterType and to the [otel] config, with an optional port. _create_metric_exporter returns a PrometheusMetricExporter bound to a CollectorRegistry.
Scrape endpoint. The webhook apps are FastAPI (github_app, gitlab_webhook, azuredevops_server_webhook), so a small shared helper can attach a GET /metrics route serving generate_latest(REGISTRY) with the CONTENT_TYPE_LATEST header. No new server or process is needed.
Multiprocess mode. gunicorn already runs preload_app = True with UvicornWorkers, which is the fork model prometheus_client multiprocess mode is built for. That explains the commented out from prometheus_client import multiprocess still sitting at pr_agent/servers/gunicorn_config.py:4. Wiring: set PROMETHEUS_MULTIPROC_DIR and prepare the directory in when_ready, add child_exit calling multiprocess.mark_process_dead(worker.pid), and back the /metrics handler with a MultiProcessCollector.
One latent gap to note. The PeriodicExportingMetricReader background thread does not survive fork, so under multiprocess workers the otlp and console exporters already have a flush problem today. The prometheus route avoids that by pushing into the shared multiprocess registry from the worker side, but the reader lifecycle deserves an explicit comment either way.
The native endpoint fits the codebase cleanly. I am happy to open the PR for it, or to take the other scope (document the OTel Collector route and remove the dead import) if the maintainers prefer the lighter answer. Can I take this one? Assigning me works.
Thanks, this is a thorough read and the native route is the one I would take. Assigned.
The commented-out multiprocess import at gunicorn_config.py:4 being the tell is a good catch, and the child_exit plus PROMETHEUS_MULTIPROC_DIR wiring is exactly the missing half.
Two things for the PR. Adding prometheus_client and opentelemetry-exporter-prometheus needs to be flagged in the description as a dependency change, and please keep the endpoint opt-in via ExporterType so nothing new is exposed by default. Your point 5 on the PeriodicExportingMetricReader not surviving fork is a separate bug worth its own issue rather than folding in here.
Implemented exporter_type = "prometheus" in PR #3238 (branch feature/prometheus-metrics-3191). Following the maintainer's guidance I flagged the dependency change in the PR description.
Summary of what landed:
A new prometheusOTEL.EXPORTER_TYPE renders the pr_agent.commands counter as a native GET /metrics endpoint on the gunicorn-served apps (github_app, gitlab_webhook, azuredevops_server_webhook, gitea_app). The endpoint is mounted only when this exporter is selected, so a default deployment exposes nothing new.
Multiprocess correctness under preload_app = True: gunicorn provisions a shared PROMETHEUS_MULTIPROC_DIR in when_ready before the first fork and deregisters workers in child_exit; each worker writes its own per-pid state file and /metrics merges them with a MultiProcessCollector.
Dependency decision: I added only prometheus-client==0.21.1. I initially assumed opentelemetry-exporter-prometheus would plug into PeriodicExportingMetricReader as an exporter, but after checking the package that assumption was wrong: it ships a PrometheusMetricReader and its docs state no multiprocessing support, which is exactly the requirement here, and it only exists as a 0.x pre-release. So I implemented a small in-house bridge on prometheus_client directly and called out the deviation in the PR description for maintainer review.
Scope: the exporter is command-metrics only (a startup warning makes the no-spans behavior explicit), and point 5 from my earlier comment — the PeriodicExportingMetricReader background thread not surviving fork for otlp/console — is left out of this PR as a separate issue, as suggested.
Verification: unit tests cover the export translation, a multiprocess end-to-end test across two interpreter processes, config/validation, the endpoint mounting guard, and the import-order safety that keeps prometheus_client out of the gunicorn master. Full unit suite passes (4459), ruff and pre-commit clean.
Activity
utsab345 commentedon Sep 9, 2026
I dug through the current telemetry wiring and the native Prometheus route is feasible, including the gunicorn multiprocess part. What is already in place: the
pr_agent.commandscounter and thepr_agent.commandspan, with the exporter selected in_create_metric_exporter(pr_agent/telemetry/meter.py:45) off theExporterTypevalues (console, otlp, none) configured under[otel].The plan for a first class scrape target:
Dependency. Add
prometheus_clientandopentelemetry-exporter-prometheuspinned to the same 1.43.0 as the otel SDK already inpyproject.toml.PrometheusMetricExporterplugs straight into the existingPeriodicExportingMetricReader, so the meter does not need restructuring.Exporter type. Add
prometheustoExporterTypeand to the[otel]config, with an optional port._create_metric_exporterreturns aPrometheusMetricExporterbound to aCollectorRegistry.Scrape endpoint. The webhook apps are FastAPI (github_app, gitlab_webhook, azuredevops_server_webhook), so a small shared helper can attach a
GET /metricsroute servinggenerate_latest(REGISTRY)with theCONTENT_TYPE_LATESTheader. No new server or process is needed.Multiprocess mode. gunicorn already runs
preload_app = Truewith UvicornWorkers, which is the fork modelprometheus_clientmultiprocess mode is built for. That explains the commented outfrom prometheus_client import multiprocessstill sitting at pr_agent/servers/gunicorn_config.py:4. Wiring: setPROMETHEUS_MULTIPROC_DIRand prepare the directory inwhen_ready, addchild_exitcallingmultiprocess.mark_process_dead(worker.pid), and back the/metricshandler with aMultiProcessCollector.One latent gap to note. The
PeriodicExportingMetricReaderbackground thread does not survive fork, so under multiprocess workers the otlp and console exporters already have a flush problem today. The prometheus route avoids that by pushing into the shared multiprocess registry from the worker side, but the reader lifecycle deserves an explicit comment either way.The native endpoint fits the codebase cleanly. I am happy to open the PR for it, or to take the other scope (document the OTel Collector route and remove the dead import) if the maintainers prefer the lighter answer. Can I take this one? Assigning me works.
IsmaelMartinez commentedon Sep 9, 2026
Thanks, this is a thorough read and the native route is the one I would take. Assigned.
The commented-out
multiprocessimport atgunicorn_config.py:4being the tell is a good catch, and thechild_exitplusPROMETHEUS_MULTIPROC_DIRwiring is exactly the missing half.Two things for the PR. Adding
prometheus_clientandopentelemetry-exporter-prometheusneeds to be flagged in the description as a dependency change, and please keep the endpoint opt-in viaExporterTypeso nothing new is exposed by default. Your point 5 on thePeriodicExportingMetricReadernot surviving fork is a separate bug worth its own issue rather than folding in here.utsab345 commentedon Sep 9, 2026
Implemented
exporter_type = "prometheus"in PR #3238 (branchfeature/prometheus-metrics-3191). Following the maintainer's guidance I flagged the dependency change in the PR description.Summary of what landed:
prometheusOTEL.EXPORTER_TYPErenders thepr_agent.commandscounter as a nativeGET /metricsendpoint on the gunicorn-served apps (github_app,gitlab_webhook,azuredevops_server_webhook,gitea_app). The endpoint is mounted only when this exporter is selected, so a default deployment exposes nothing new.preload_app = True: gunicorn provisions a sharedPROMETHEUS_MULTIPROC_DIRinwhen_readybefore the first fork and deregisters workers inchild_exit; each worker writes its own per-pid state file and/metricsmerges them with aMultiProcessCollector.prometheus-client==0.21.1. I initially assumedopentelemetry-exporter-prometheuswould plug intoPeriodicExportingMetricReaderas an exporter, but after checking the package that assumption was wrong: it ships aPrometheusMetricReaderand its docs state no multiprocessing support, which is exactly the requirement here, and it only exists as a 0.x pre-release. So I implemented a small in-house bridge onprometheus_clientdirectly and called out the deviation in the PR description for maintainer review.PeriodicExportingMetricReaderbackground thread not surviving fork for otlp/console — is left out of this PR as a separate issue, as suggested.prometheus_clientout of the gunicorn master. Full unit suite passes (4459), ruff and pre-commit clean.feat(telemetry): expose a native prometheus scrape endpoint