Skip to content

fix(phpfpm): emit request duration as a number, not a pointer address - #23

Merged
dimoschi merged 1 commit into
masterfrom
fix/request-duration-json
Sep 13, 2026
Merged

dimoschi merged 1 commit into
masterfrom
fix/request-duration-json

Conversation

@dimoschi

Copy link
Copy Markdown
Collaborator

Problem

php-fpm_exporter get --out json produced output that does not parse. Every process entry carried a pointer address where a number belonged:

"request duration":0xc000129a48

requestDuration.MarshalJSON called fmt.Sprint on its pointer receiver:

func (rd *requestDuration) MarshalJSON() ([]byte, error) {
	stamp := fmt.Sprint(rd)   // rd is *requestDuration
	return []byte(stamp), nil
}

fmt formats a pointer to a non-struct type as its address. The sibling timestamp.MarshalJSON dereferences first (time.Time(*t)), which is why only this one was affected.

It is reachable: cmd/get.go calls json.Marshal(pm), and although pm itself is not addressable, pm.Pools is a slice — slice elements are addressable through the backing array, so encoding/json does reach the pointer-receiver method on the nested process fields.

Fix

Both methods now use strconv.FormatInt on the value. Keeping them symmetric matters here: the bug was one method drifting from its neighbour, and identical shape makes the wrong form harder to reintroduce. fmt is no longer used in the file.

Tests

Written failing first. Before the fix:

--- FAIL: TestRequestDurationMarshalJSONEmitsTheNumber/typical
    expected: "295"
    actual  : "0x48f74664160"
--- FAIL: TestPoolManagerMarshalsToValidJSON
    Error: Received unexpected error (invalid character 'x')
  • TestRequestDurationMarshalJSONEmitsTheNumber — typical, zero and large values
  • TestPoolManagerMarshalsToValidJSON — marshals a populated PoolManager the way get --out json does, then unmarshals it back, asserting both request duration and start time survive. This covers the addressability path described above, so a regression in either method is caught.

Verification

End-to-end from a separate consuming module using only the public API, unmarshalling a realistic PHP-FPM status payload into a Pool and re-marshalling it:

{"pools":[{"pool":"academy",...,"processes":[{"pid":23,"state":"Idle",...,"request duration":295,...}]}],"podPhases":null}
--> valid JSON, request duration survived

go test ./... -race passes. golangci-lint (v2.12.2, as CI pins) reports 5 issues, unchanged from master, none new.

Scope

This is upstream code and the bug predates the fork. Only the two MarshalJSON methods and the now-unused fmt import changed; no metric, label or scraping behaviour is touched.

requestDuration.MarshalJSON called fmt.Sprint on the *pointer* receiver rather
than the value. fmt formats a pointer to a non-struct as its address, so every
process entry carried something like

  "request duration":0xc000129a48

and the whole document produced by `get --out json` failed to parse. The sibling
timestamp.MarshalJSON dereferences before formatting, which is why only this one
was wrong.

Both now use strconv.FormatInt on the value, so the two are symmetric and the
wrong form cannot be copied back from its neighbour. That also leaves fmt unused
in the file.

Verified through the public API from a consuming module: a status payload
unmarshalled into a Pool and re-marshalled now yields "request duration":295 and
round-trips.
@dimoschi
dimoschi merged commit 7e6385d into master Sep 13, 2026
7 checks passed
@dimoschi
dimoschi deleted the fix/request-duration-json branch September 13, 2026 19:38
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.

1 participant