Skip to content

frankenphp_total_threads is registered as a Counter but is semantically a Gauge #2481

Description

@cattz

Summary

The frankenphp_total_threads metric is registered as a Prometheus Counter, but its value is semantically a Gauge — it reflects the current configured PHP thread-pool size, which only changes on (re)configuration, not a monotonically increasing total.

In metrics.go (current main):

totalThreads       prometheus.Counter
// ...
totalThreads: prometheus.NewCounter(prometheus.CounterOpts{
    Name: "frankenphp_total_threads",
    ...
}),
// ...
func (m *PrometheusMetrics) TotalThreads(num int) {
    ...
    m.totalThreads.Add(float64(num))
}

By contrast, the sibling thread metrics frankenphp_busy_threads and frankenphp_queue_depth are correctly registered with prometheus.NewGauge.

Two problems follow from the Counter registration:

  1. The exposition emits # TYPE frankenphp_total_threads counter. Prometheus-conformant consumers (Datadog's OpenMetrics V2 check, Grafana Agent, etc.) honor the declared type and treat it as a rate / apply .as_count(), so a steady non-incrementing value reads as 0. Today consumers must add an explicit per-metric type: gauge override to recover the real value.
  2. The update path uses Counter.Add(num), so any call to TotalThreads(num) after the first accumulates rather than reporting the current pool size — incorrect once the thread count changes (e.g. thread autoscaling / reconfiguration).

Reproduction

Enable metrics (e.g. servers { metrics }) and scrape the metrics endpoint:

$ curl -s <metrics-endpoint> | grep -A1 frankenphp_total_threads
# TYPE frankenphp_total_threads counter
frankenphp_total_threads <N>

Suggested fix

Register total_threads as a Gauge and report it with .Set():

totalThreads       prometheus.Gauge
// ...
totalThreads: prometheus.NewGauge(prometheus.GaugeOpts{
    Name: "frankenphp_total_threads",
    ...
}),
// ...
func (m *PrometheusMetrics) TotalThreads(num int) {
    ...
    m.totalThreads.Set(float64(num))
}

This matches the existing treatment of busy_threads / queue_depth and lets standard Prometheus consumers read it correctly without per-metric type overrides.

Happy to send a PR if this looks right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions