Skip to content

feat(debug): add more metrics in ThreadDebugState#2282

Open
alexandre-daubois wants to merge 3 commits intophp:mainfrom
alexandre-daubois:observability
Open

feat(debug): add more metrics in ThreadDebugState#2282
alexandre-daubois wants to merge 3 commits intophp:mainfrom
alexandre-daubois:observability

Conversation

@alexandre-daubois
Copy link
Member

I'm working on an observability tool and exposing those metrics would be useful. For each thread, it exposes the current URI being processed, the method, the request start date, the request count and the memory usage.

@dunglas
Copy link
Member

dunglas commented Mar 14, 2026

Could we measure the performance cost of this? Maybe should this feature be put behind a flag?

@henderkes
Copy link
Contributor

Did I just spend 10 hours attempting to figure out why my other branch segfaults on 8.2 debian images and it's not actually related to my branch in the end?

Certainly seems like it...

@alexandre-daubois
Copy link
Member Author

Could we measure the performance cost of this? Maybe should this feature be put behind a flag?

I'll do benchmarks soon and we'll be able to see from there!

@alexandre-daubois
Copy link
Member Author

This is the impact, on already existing benchmark:

  • HelloWorld:
    • main: 32.14us
    • branch: 33.17us
    • Delta: ~1us
  • Echo:
    • main: 29.07us
    • branch: 29.02us
    • Delta: None
  • ** ServerSuperGlobal**:
    • main: 62.15us
    • branch: 63.12us
    • Delta: ~1us
  • ** UncommonHeaders**:
    • main: 59.20
    • branch: 60.12
    • Delta: ~1us

Also, 0 additional allocation is recorded on the hot path. The impact is negligible and the metrics newly exposed would really help better monitor how things are going while processing requests.

@henderkes
Copy link
Contributor

Also, 0 additional allocation is recorded on the hot path. The impact is negligible and the metrics newly exposed would really help better monitor how things are going while processing requests.

Sounds reasonable to me.

Out of curiosity, does anybody know of a per-route monitoring solution that exposes metrics like number of requests, php request duration, etc.? Much like Sentry, Datadog or Tideways, but without all the low-level insights?

Or perhaps that's a good candidate for a go php extension to hook into the runtime, gather the data, hand it to Go and add it to the prometheus metrics?

@alexandre-daubois
Copy link
Member Author

Not that I'm aware of, but an extension for this purpose could indeed be a good idea

@henderkes
Copy link
Contributor

Not that I'm aware of, but an extension for this purpose could indeed be a good idea

Time to implement something. I'll create a proof of concept (I think this may even be implemented entirely in Go) and perhaps we can merge it upstream later.

@henderkes
Copy link
Contributor

henderkes commented Mar 17, 2026

image

That was actually much simpler than I thought, only around 400 lines of code.

sub.domain.com {
	# need to handle this in the module somehow, not sure how yet
	handle /metrics {
		@denied not remote_ip 127.0.0.1 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
		respond @denied 403
		metrics
	}
	route_metrics {
		path /metrics
		pattern "/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" "/:uuid"
		pattern "/(\d+)(/|$)" "/:id$2"
		pattern "^/.*" "$0"

		duration_buckets 1 5 10 25 50 100 250 1000
		status_buckets 204 403 418 # everything else falls in a 2XX or 4XX/5XX bucket
	}
}

@94noni
Copy link

94noni commented Mar 18, 2026

@alexandre-daubois @henderkes 👋🏻 this PR/discussion really got my attention
obviously the more data/hint we have on the low level part of this runtime, the better and thank you to go in that subject :)

I got some questions:

@alexandre-daubois iiuc, this PR may outputs more data behind the /frankenphp/threads endpoint

  • is that right ?
  • If so, I see no documentation of such endpoint, is it intended to be only "internally visible"?
  • may this endpoint return html and not json ? (thinking of the mercure gui for example for example)

@henderkes your last post 'pic looks very promising, it seems built on top of the metric of frankenphp/caddy and prometheus scrapping correct?
kind of same question as above, would it be possible to have such data visible as html somehow? I mean without all the infra layer of external monitoring

having, as a frankenphp/caddy module, such "low level visibility/monitoring tool" would be very valuable 👍🏻

I am writing/thinking out loud here, and not being really aware of go, frankenphp etc, I may miss some part ^^

thx !

@alexandre-daubois
Copy link
Member Author

is that right ?

Yes

If so, I see no documentation of such endpoint, is it intended to be only "internally visible"?

Nope, documentation should be done as well!

may this endpoint return html and not json ? (thinking of the mercure gui for example for example)

I don't think it should return HTML, I think it would be weird. FrankenPHP exports raw data, it's up to people to format them the way they want. Maybe with an external extension 🙂

@henderkes
Copy link
Contributor

  • If so, I see no documentation of such endpoint, is it intended to be only "internally visible"?

No, it needs to be documented.

Definitely json. Json is expected by prometheus.

@henderkes your last post 'pic looks very promising, it seems built on top of the metric of frankenphp/caddy and prometheus scrapping correct? kind of same question as above, would it be possible to have such data visible as html somehow? I mean without all the infra layer of external monitoring

No, that's app-specific prometheus metrics that can be visualised by Grafana or any other tool of your choice.

having, as a frankenphp/caddy module, such "low level visibility/monitoring tool" would be very valuable 👍🏻

I am writing/thinking out loud here, and not being really aware of go, frankenphp etc, I may miss some part ^^

I think you're missing Prometheus and Grafana :P

@94noni
Copy link

94noni commented Mar 18, 2026

thx I will try to document such endpoint 👍🏻

I definitly got the metric over prometheus (what I described above)
but rather wanted to ask if it is a mandatory stack and if this kind of "data/dashboard" can be output without such tooling (json or html)

anyhow, thanks both of you @alexandre-daubois @henderkes cheers !

@alexandre-daubois
Copy link
Member Author

alexandre-daubois commented Mar 19, 2026

I checked and actually, the whole thing isn't documented yet at all. I'd rather see this one merged and take care of the exhaustive documentation for the /threads endpoint in a follow-up PR 🙂

@dunglas
Copy link
Member

dunglas commented Mar 19, 2026

Maybe we could create a third-party Caddy module embedding https://github.com/prometheus/prometheus/blob/main/web/ui/README.md and proxying the Caddy Prometheus API?

@alexandre-daubois
Copy link
Member Author

Yes, that's a good idea!

@henderkes
Copy link
Contributor

henderkes commented Mar 20, 2026

https://github.com/henderkes/caddy-prometheus

I also tried embedding Grafana, but didn't have much (any) luck with it.

As for Prometheus, I tried putting it behind the admin api at 2019, which works for general scraping, but some Grafana dashboards aren't able to fetch data because of error invalid origin ''. I couldn't pinpoint how to fix those dashboards and think we should have a fully functional target anyway, so I opted for a listen directive instead.

There are still some edges to round out, will get it ready over the weekend I think. Won't implement all of https://prometheus.io/docs/prometheus/latest/feature_flags/ though.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends FrankenPHP’s thread-level debug/observability output by adding per-thread request and memory metrics (plus request metadata) to ThreadDebugState, and wires up metric collection across Go and the embedded PHP runtime.

Changes:

  • Add RequestCount, MemoryUsage, and current-request metadata (URI/method/start time) to ThreadDebugState.
  • Track per-thread request counts in Go and capture last per-thread PHP memory usage in C.
  • Add an admin API test validating the new metrics after serving requests.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
threadworker.go Increment per-thread request counter at end of each worker-mode request.
phpthread.go Add atomic request counter field and increment it after script execution.
phpmainthread.go Initialize/destroy the C-side thread metrics storage during thread lifecycle.
frankenphp.h Declare new C API for thread metrics (init/destroy/get memory usage).
frankenphp.c Implement thread metrics storage and update memory usage at request/script shutdown.
debugstate.go Expose new metrics and current-request metadata in ThreadDebugState.
caddy/admin_test.go Add test asserting request count and memory usage are populated after requests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@alexandre-daubois alexandre-daubois force-pushed the observability branch 3 times, most recently from 0a87699 to df39ae6 Compare March 24, 2026 11:09
@alexandre-daubois
Copy link
Member Author

Comments addressed and CI's green

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

threadworker.go:300

  • workerFrankenPHPContext / workerContext are cleared here without any synchronization, but DebugState() now reads the current request context from other goroutines. To avoid -race failures, these per-request handler fields need a consistent synchronization strategy (e.g., guard assignments/clears with thread.handlerMu or move the current-request pointer/metadata into phpThread using atomics).
	thread.requestCount.Add(1)

	fc.closeContext()
	thread.handler.(*workerThread).workerFrankenPHPContext = nil
	thread.handler.(*workerThread).workerContext = nil


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants