Skip to content

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

@cattz

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions