Documentation
¶
Overview ¶
Package phpfpm is a Go library for talking to PHP-FPM.
It covers the four things a caller needs in order to reason about a running PHP-FPM installation: parsing the effective configuration, discovering running masters and their pools, scraping live status over FastCGI, and controlling the master (validate, reload).
It exists as its own module because two consumers need it and neither should depend on the other: fpm-exporter reads, and fpm-tune both reads and writes. The code began life inside fpm-exporter's internal/phpfpm, where it could not be imported.
Two conventions run through the package:
Nothing here owns a logger. Entry points that need one take a *slog.Logger. A library with a package-level logger cannot be embedded twice with different destinations, and it makes tests order-dependent.
Reload, never restart. Reload sends SIGUSR2, which has the master re-read its configuration and cycle workers gracefully. Callers changing pm.* settings should pair it with Validate first — an invalid drop-in that reaches a reload takes the pool down.
Index ¶
- Constants
- Variables
- func InvalidateConfigCache(binary, configPath string)
- func MasterPID(pidFile string) (int, error)
- func ParseAddress(addr string, path string) (scheme, address, scriptPath string, err error)
- func Reload(pid int) error
- func ReloadAndWait(ctx context.Context, pid int, settle time.Duration, log *slog.Logger) error
- func RemoveOpcacheScript()
- func Validate(ctx context.Context, binary, configPath string) error
- func VerifyMaster(pid int) error
- type Discovered
- type EffectiveConfig
- type Info
- type Memory
- type OpcacheStatus
- type Pool
- type PoolOutcome
- type PoolProcess
- type Result
- type Stats
- type Target
Constants ¶
const DefaultTimeout = 3 * time.Second
DefaultTimeout bounds a FastCGI dial when a Target does not set one.
Variables ¶
var ErrNotAMaster = errors.New("not a php-fpm master process")
ErrNotAMaster reports that a pid is not a php-fpm master process.
Functions ¶
func InvalidateConfigCache ¶ added in v0.3.0
func InvalidateConfigCache(binary, configPath string)
InvalidateConfigCache forgets the parsed configuration.
Call it after reloading php-fpm with a changed configuration. Without it, a long-running process keeps reporting the settings it saw at startup: a tool that writes pool settings would never observe its own changes, and would show an operator a "currently configured" value that has not been true for hours.
Passing a binary and config path forgets just that pair; passing empty strings forgets everything.
func MasterPID ¶
MasterPID reads a php-fpm master's pid from its pid file.
Discover finds masters by scanning the process table, which is the right tool when you do not know what is running. When you already know which pool you are reloading, the pid file is the authoritative answer and does not require permission to read another user's /proc entry.
func ParseAddress ¶
func Reload ¶
Reload asks a running master to re-read its configuration.
SIGUSR2 is a graceful reload: the master re-reads the config and cycles its workers, letting each finish the request it is serving. It is deliberately not a restart — a restart drops in-flight requests, and on a host serving many sites that is a visible outage for a change that was meant to be routine.
Callers changing pool settings should Validate first. Reload does not check the configuration it is asking the master to adopt.
func ReloadAndWait ¶
ReloadAndWait reloads a master and waits for it to come back.
A reload that kills the master is the failure this exists to detect: Reload itself returns nil as soon as the signal is delivered, which is well before the master has re-read anything. Callers that are about to rely on the new configuration — or that need to know whether to roll back — need the confirmation rather than the delivery.
Liveness is checked with signal 0, which tests for the process without delivering anything.
func RemoveOpcacheScript ¶
func RemoveOpcacheScript()
RemoveOpcacheScript deletes the generated status script. Safe to call when no script was ever written, and safe to call while scrapes are in flight — the next probe recreates it.
func Validate ¶
Validate checks a configuration with `php-fpm -t` without applying it.
This is the guard that makes writing pool configuration survivable. PHP-FPM re-reads its configuration on reload, and a syntax error or an impossible value does not fail gracefully — the master refuses to come back and every pool it served goes down. Validating first turns that outage into a returned error.
The configPath is the master config, not the drop-in: `-t` walks the include tree, so a broken fragment is caught through its parent.
func VerifyMaster ¶ added in v0.5.0
VerifyMaster confirms that a pid really is a php-fpm master, immediately before it is signalled.
This replaced a `pid <= 1` refusal that was wrong in both directions.
It refused the most common deployment there is. In the official php:8.3-fpm image the master IS pid 1, so `fpm-tune apply` wrote the configuration, declined to reload, reported the master as dead and rolled the whole change back — verified against that image, where every apply failed this way.
And it did not guard against the thing that actually matters. A pid is only a promise about the instant it was read: between discovery and the reload the master can exit and the kernel can hand the number to something else. SIGUSR2 to a process that did not ask for it terminates it by default, so the old check would happily kill an unrelated program while carefully declining to signal init. Container pid namespaces start at 1 and stay small, which is exactly where recycling is likely.
Checking what the process IS covers both, and costs one /proc read.
Types ¶
type Discovered ¶
type Discovered struct {
Name string
ConfigPath string
StatusPath string
Binary string
Socket string
StatusSocket string
// PID is the master process serving this pool.
//
// Carried from the process scan because the pid file is not a reliable
// alternative: the official php:8.3-fpm image ships `pid` commented out, so
// there is no file to read — and a caller that could not identify the master
// would write pool configuration and never reload it.
PID int
}
Discovered is one pool found by scanning the process table.
func Discover ¶
func Discover(log *slog.Logger) ([]Discovered, error)
Discover scans the process table for PHP-FPM masters and returns the pools they serve.
It parses each master's effective configuration, which means executing the discovered binary — see trustedPath for why that is gated on ownership.
log may be nil.
type EffectiveConfig ¶
func ParseConfig ¶
func ParseConfig(FPMBinaryPath string, FPMConfigPath string) (*EffectiveConfig, error)
ParseConfig runs `php-fpm -tt` and parses its report of the effective configuration. Results are cached per binary+config pair.
The cache never expires on its own, which is right for the common case — the parse forks php-fpm and a scrape loop would otherwise do it every few seconds — but it means a caller that CHANGES the configuration has to say so. See InvalidateConfigCache.
type OpcacheStatus ¶
type OpcacheStatus struct {
Enabled bool `json:"opcache_enabled"`
MemoryUsage Memory `json:"memory_usage"`
Statistics Stats `json:"opcache_statistics"`
}
func GetOpcacheStatus ¶
func GetOpcacheStatus(ctx context.Context, target Target) (*OpcacheStatus, error)
type Pool ¶
type Pool struct {
Address string `json:"address"`
Path string `json:"path"`
Name string `json:"pool"`
ProcessManager string `json:"process manager"`
StartTime int64 `json:"start time"`
StartSince int64 `json:"start since"`
AcceptedConnections int64 `json:"accepted conn"`
ListenQueue int64 `json:"listen queue"`
MaxListenQueue int64 `json:"max listen queue"`
ListenQueueLength int64 `json:"listen queue len"`
IdleProcesses int64 `json:"idle processes"`
ActiveProcesses int64 `json:"active processes"`
TotalProcesses int64 `json:"total processes"`
MaxActiveProcesses int64 `json:"max active processes"`
MaxChildrenReached int64 `json:"max children reached"`
SlowRequests int64 `json:"slow requests"`
MemoryPeak int64 `json:"memory peak"`
Processes []PoolProcess `json:"processes"`
ProcessesCpu *float64 `json:"processes_cpu"`
ProcessesMemory *float64 `json:"processes_memory"`
Config map[string]string `json:"config,omitempty"`
OpcacheStatus OpcacheStatus `json:"opcache_status,omitempty"`
PhpInfo Info `json:"php_info,omitempty"`
}
type PoolOutcome ¶
type PoolOutcome struct {
// Name is the pool's configured or discovered name, used to label a
// failure. A successful scrape prefers the name PHP-FPM itself reports.
Name string
Socket string
Result *Result
Err error
}
PoolOutcome is what happened to one configured pool during a scrape: either a Result, or the error that prevented one. Failures are values rather than log lines so the collector can emit up=0 for a pool that did not answer -- a pool that silently vanishes from the output is indistinguishable from a pool that was removed from the configuration.
func Scrape ¶
Scrape reads one pool's status page. Its client and response body close at the end of this call rather than at the end of a whole batch.
log may be nil.
func ScrapeAll ¶
ScrapeAll scrapes every target. It returns one outcome per target, in the order given, and an error only when nothing at all could be collected.
It took fpm-exporter's whole *config.Config before this package was extracted. A library takes the work it is asked to do; deciding which pools exist belongs to the caller.
log may be nil.
type PoolProcess ¶
type PoolProcess struct {
PID int `json:"pid"`
State string `json:"state"`
StartTime int64 `json:"start time"`
StartSince int64 `json:"start since"`
Requests int64 `json:"requests"`
RequestDuration int64 `json:"request duration"`
RequestMethod string `json:"request method"`
RequestURI string `json:"request uri"`
ContentLength int64 `json:"content length"`
User string `json:"user"`
Script string `json:"script"`
LastRequestCPU float64 `json:"last request cpu"`
LastRequestMemory float64 `json:"last request memory"`
// CurrentRSS is the worker's resident memory, in bytes.
//
// Not part of PHP-FPM's status output — it is filled in from the operating
// system using PID, because the size of a worker is the number any capacity
// decision turns on and the status page does not carry it. Zero means the
// worker could not be read, usually because it exited between the status
// response and the lookup.
CurrentRSS int64 `json:"current_rss"`
}
type Stats ¶
type Stats struct {
NumCachedScripts uint64 `json:"num_cached_scripts"`
Hits uint64 `json:"hits"`
Misses uint64 `json:"misses"`
BlacklistMisses uint64 `json:"blacklist_misses"`
OomRestarts uint64 `json:"oom_restarts"`
HashRestarts uint64 `json:"hash_restarts"`
ManualRestarts uint64 `json:"manual_restarts"`
HitRate float64 `json:"opcache_hit_rate"`
}
type Target ¶
type Target struct {
// Name identifies the pool in results when the pool itself could not be
// reached. A successful scrape uses the name PHP-FPM reports; this is the
// fallback so a failing pool is still labelled with something meaningful.
Name string `mapstructure:"name"`
// Socket is the pool's FastCGI address ("unix:///run/php-fpm.sock",
// "tcp://127.0.0.1:9000", or a bare path).
Socket string `mapstructure:"socket"`
// StatusSocket is where the status page is served, when it differs from
// Socket. Empty means Socket.
StatusSocket string `mapstructure:"status_socket"`
StatusPath string `mapstructure:"status_path"`
// ConfigPath and Binary let ParseConfig recover the effective configuration
// for this pool. Both come from the master's command line during Discover.
ConfigPath string `mapstructure:"config_path"`
Binary string `mapstructure:"binary"`
// PID is the master serving this pool, when it is known. Zero means unknown,
// not "no master".
PID int `mapstructure:"-"`
// Timeout bounds the FastCGI dial for this pool. Zero means DefaultTimeout.
Timeout time.Duration `mapstructure:"timeout"`
}
Target describes how to reach one PHP-FPM pool.
It replaces the FPMPoolConfig this package used to take from fpm-exporter's internal config. A library cannot depend on one application's configuration type — the second consumer has a different one, and neither should have to adopt the other's.
The mapstructure tags are kept so a viper-based caller can decode straight into this type rather than maintaining a parallel struct and a copy function.
func TargetFromDiscovered ¶
func TargetFromDiscovered(d Discovered) Target
TargetFromDiscovered builds a scrape target from a discovered pool.