phpfpm

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 28, 2026 License: MIT Imports: 21 Imported by: 0

README

phpfpm

Go library for talking to PHP-FPM: pool discovery, effective-configuration parsing, status scraping over FastCGI, and control operations.

It is the shared domain layer beneath fpm-exporter (which reads) and fpm-tune (which reads and writes). Neither depends on the other; both depend on this.

What it does

Area Entry point
Parse the effective configuration ParseConfig — runs php-fpm -tt and returns global settings plus one map per pool
Find running masters Discover — locates php-fpm processes, their pools, and the master PID
Scrape live status Scrape (one pool) and ScrapeAll (many, concurrently) — per-pool counters and per-worker RSS
Read opcache state GetOpcacheStatus
Validate and reload Validate (php-fpm -t), Reload (SIGUSR2), ReloadAndWait, MasterPID

Design notes

No package-level logger. Every entry point that can log takes a *slog.Logger. A library that owns a global logger cannot be embedded twice with different destinations, and it makes tests order-dependent.

Reload, never restart. Reload sends SIGUSR2, which makes the master re-read its configuration and cycle workers gracefully. Callers that change pm.* must pair it with Validate first: an invalid drop-in that reaches a reload does not degrade — the master refuses to come back, and every pool it served goes with it. Verified against a real master: a drop-in naming a pool that no longer exists makes php-fpm -t exit 78, and reloading with that file present kills the master permanently.

The configuration cache never expires on its own. ParseConfig forks php-fpm, so its result is cached per binary+config pair for the life of the process. That is right for a scrape loop and wrong for anything that CHANGES the configuration: such a caller must call InvalidateConfigCache after reloading, or it will keep reading the settings it saw at startup. A consumer that missed this reported a pool as configured for 4 workers hours after setting it to 12.

Status

Extracted from fpm-exporter's internal/phpfpm, where it could not be imported. The behaviour and its test fixtures moved unchanged.

License

MIT

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

View Source
const DefaultTimeout = 3 * time.Second

DefaultTimeout bounds a FastCGI dial when a Target does not set one.

Variables

View Source
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

func MasterPID(pidFile string) (int, error)

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 ParseAddress(addr string, path string) (scheme, address, scriptPath string, err error)

func Reload

func Reload(pid int) error

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

func ReloadAndWait(ctx context.Context, pid int, settle time.Duration, log *slog.Logger) error

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

func Validate(ctx context.Context, binary, configPath string) error

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

func VerifyMaster(pid int) error

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

type EffectiveConfig struct {
	Global map[string]string
	Pools  map[string]map[string]string
}

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 Info

type Info struct {
	Version    string
	Extensions []string
}

func GetPHPStats

func GetPHPStats(ctx context.Context, target Target) (*Info, error)

type Memory

type Memory struct {
	UsedMemory       uint64  `json:"used_memory"`
	FreeMemory       uint64  `json:"free_memory"`
	WastedMemory     uint64  `json:"wasted_memory"`
	CurrentWastedPct float64 `json:"current_wasted_percentage"`
}

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

func Scrape(ctx context.Context, target Target, log *slog.Logger) PoolOutcome

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

func ScrapeAll(ctx context.Context, targets []Target, log *slog.Logger) ([]PoolOutcome, error)

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 Result

type Result struct {
	Timestamp time.Time
	Pools     map[string]Pool
	Global    map[string]string `json:"global_config,omitempty"`
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL