Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -1,26 +1,40 @@
name: Release

on:
release:
types: [created]

permissions:
contents: write
packages: write
contents: read

jobs:
test:
name: Test
uses: ./.github/workflows/test.yaml

releases-matrix:
name: Release Go Binary
needs: test
runs-on: ubuntu-latest
permissions:
contents: write
strategy:
matrix:
goos: [linux, darwin]
goarch: [amd64, arm64]
env:
# A static binary runs on both glibc (Debian) and musl (Alpine) images.
CGO_ENABLED: "0"
steps:
- uses: actions/checkout@v3
- uses: wangyoucao577/go-release-action@v1
- uses: actions/checkout@v7
- uses: wangyoucao577/go-release-action@v1.55
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: go.mod
project_path: "./cmd/docker-fpm-wrapper"
binary_name: "docker-fpm-wrapper"
extra_files: LICENSE README.md
ldflags: "-s -w"
sha256sum: true
extra_files: LICENSE README.md CHANGELOG.md
58 changes: 58 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Test

on:
push:
branches: [master]
pull_request:
workflow_call:

permissions:
contents: read

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- uses: golangci/golangci-lint-action@v9
with:
version: v2.15.0
args: --build-tags e2e

unit:
name: Unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- run: go test -race ./...

e2e:
name: E2E tests (${{ matrix.image }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
image:
# PHP 5.6 and 7.4 are EOL, but the wrapper must support them.
- php:5.6-fpm-alpine
- php:7.4-fpm-alpine
- php:8.2-fpm-alpine
- php:8.3-fpm-alpine
- php:8.4-fpm-alpine
- php:8.5-fpm-alpine
- php:8.5-fpm
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- run: go test -race -count=1 -tags e2e ./e2e/...
env:
E2E_PHP_IMAGE: ${{ matrix.image }}
30 changes: 0 additions & 30 deletions .travis.yml

This file was deleted.

34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](http://semver.org).

---

## [1.1.0] - 2026-10-21

### Added

- `--session-cleanup-interval` removes the expired PHP session files, as the `sessionclean` cron job of Debian does. The cleanup is off by default.

### Changed

- The minimum Go version is 1.27. Update all dependencies.
- The wrapper reads the php-fpm config with `php-fpm -tt` and runs php-fpm with an overlay config in `--run-dir`. The overlay moves `error_log` and the slowlogs to FIFOs in `--run-dir`. Before, the wrapper parsed the config itself and replaced the files at `error_log` and `slowlog` with FIFOs. These problems are gone:
- The wrapper did not see `error_log` in an included file, so the error log of the official Docker images was not proxied.
- The wrapper did not find a relative `include`, for example `include=etc/php-fpm.d/*.conf` of the PHP 7.4 images.
- Without root, the wrapper could not create a FIFO for an `error_log` in `/var/log`.
- `error_log = syslog` also goes through the proxy. Use `--fpm-no-errlog` to keep it.
- The slowlog parser uses the `request_slowlog_trace_depth` of php-fpm (default 20). Before, the default was 64.
- When php-fpm rejects the config at start, the wrapper exits with the exit code of php-fpm, for example 78.
- Parse the flags and the environment variables with `go-flags` instead of `pflag` and `viper-lite`. The names of the flags and the environment variables do not change. These behaviors change:
- An empty environment variable sets an empty value. For example, `WRAPPER_PIPE=` disables the pipe. Before, the wrapper ignored empty environment variables.
- An argument before `--` that is not a flag of the wrapper stops the wrapper at start. Before, the wrapper ignored it. The wrapper passes the arguments after `--` to php-fpm, as before.
- `--log-level` accepts a name or a number from `-1` to `5`. Other values stop the wrapper at start.
- `--help` shows the environment variables and the defaults, and exits with code 0.
- The release binaries are static (`CGO_ENABLED=0`). The `linux/amd64` binary of 1.0.2 needed glibc and did not start on Alpine images.
- Replace the manual e2e test with automated e2e tests on testcontainers-go.
- Replace Travis CI with GitHub Actions: lint, unit tests, and e2e tests on PHP 5.6, 7.4, and 8.2–8.5.

### Fixed

- `--shutdown-delay` also applies to SIGQUIT. The php-fpm images have `STOPSIGNAL SIGQUIT`, so Docker and Kubernetes stopped php-fpm without the delay.
- The error log proxy writes a line in an unknown format to stderr without a change and continues. Before, it stopped at the first such line, and all next entries of the error log were lost. Such lines come, for example, from a pool with `decorate_workers_output = no`.
- The wrapper pipe (`FPM_WRAPPER_PIPE`) has mode `0666` for all umask values. Before, the umask gave `0644`, and workers that run as a different user than the master could not write to the pipe.
- The wrapper takes each section of the php-fpm config except `[global]` as a pool. Before, it ignored the pools without `pm.status_path`: their slowlog was not proxied. When such a pool was the last section, the wrapper did not start.
- The wrapper replaces each `$pool` in each directive of a pool, as php-fpm does. Before, it replaced only the first `$pool` in `slowlog`, so a `listen = /run/php/$pool.sock` pool had no metrics.
- The wrapper exits with an error when `--log-encoder` has an unknown value. Before, it panicked.

## [1.0.1] - 2025-01-10

### Fixed
Expand Down
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.PHONY: lint test e2e

lint:
golangci-lint run --build-tags e2e ./...

test:
go test -race ./...

# The e2e tests need Docker. Set E2E_PHP_IMAGE to use a different php-fpm image.
# The tests build the wrapper at run time, so the test cache cannot see a change of the code.
e2e:
go test -race -count=1 -tags e2e ./e2e/...
129 changes: 127 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,128 @@
# Docker PHP-Fpm Wrapper
# Docker PHP-FPM Wrapper

Solution for php app logging running under php-fpm in container + prometheus monitoring + gracefull shutdown in k8s
A solution to improve PHP application logging when running under php-fpm in a containerized environment. It also provides Prometheus monitoring and a graceful shutdown mechanism tailored for Kubernetes deployments.

## Overview

This project wraps around PHP-FPM, intercepting its logs and exposing Prometheus metrics. It is especially useful for containerized PHP applications running in Kubernetes, providing:

- **Enhanced Logging:** Routes php-fpm logs through a customizable logging proxy.
- **Slowlog Proxy:** Parse and proxy PHP-FPM slowlog output.
- **Logging Proxy:** Configure Unix pipe or socket for app log forwarding.
- **Prometheus Monitoring:** Exposes application metrics to help monitor application performance.
- **Graceful Shutdown:** Implements a delay mechanism to ensure smooth shutdowns in Kubernetes environments.
- **Session Cleanup:** Removes expired PHP session files, as the `sessionclean` cron job of Debian does.

## Available Configuration Options

The following options can be configured via command-line flags or environment variables.
The name of the environment variable is the name of the flag in upper case, with `_` instead of `-`:
for example, `FPM_CONFIG` for `--fpm-config`. A flag takes precedence over its environment variable.
Run `docker-fpm-wrapper --help` to see all options.

The wrapper passes the arguments after `--` to php-fpm:

```sh
docker-fpm-wrapper -f /usr/local/sbin/php-fpm -- -d memory_limit=256M
```

- **--log-level**
- *Description:* Sets the internal log level: a name (`debug`, `info`, `warn`, `error`) or a number from `-1` (debug) to `5` (fatal).
- *Default:* `-1`

- **--log-encoder**
- *Description:* Chooses the internal logging encoder: `auto`, `console`, or `json`. `auto` is `console` on a terminal and `json` otherwise.
- *Default:* `auto`

- **--fpm, -f**
- *Description:* Path to the PHP-FPM executable.
- *Default:* _(Required)_

- **--fpm-config, -c**
- *Description:* Path to the PHP-FPM configuration file.
- *Default:* `/etc/php/php-fpm.conf`

- **--run-dir**
- *Description:* Directory for the generated php-fpm config and the log FIFOs. The user of the wrapper must have write access to it.
- *Default:* `/tmp/docker-fpm-wrapper`

- **--fpm-no-errlog**
- *Description:* Disable php-fpm error log parsing and proxy. php-fpm then uses the `error_log` of its config.
- *Default:* `false`

- **--fpm-no-slowlog**
- *Description:* Disable php-fpm slow log parsing and proxy. php-fpm then uses the `slowlog` of each pool.
- *Default:* `false`

- **--wrapper-pipe, -p**
- *Description:* Path to the logging pipe. Set to an empty string (`""`) to disable.
- *Default:* `/tmp/fpm-wrapper-pipe`

- **--wrapper-socket, -s**
- *Description:* Path to the logging socket. Set to `null` to disable.
- *Default:* `/tmp/fpm-wrapper.sock`

- **--line-buffer-size**
- *Description:* Specifies the maximum log line size in bytes.
- *Default:* `16384` (16*1024 bytes)

- **--listen**
- *Description:* The address for Prometheus to scrape metrics.
- *Default:* `:8080`

- **--metrics-path**
- *Description:* The path to expose Prometheus metrics.
- *Default:* `/metrics`

- **--shutdown-delay**
- *Description:* Delay before the process shutdown to allow graceful termination. The wrapper waits on SIGTERM and SIGQUIT, then sends SIGQUIT to php-fpm.
- *Default:* `500ms`

- **--session-cleanup-interval**
- *Description:* Interval of the removal of expired session files. Set to `0` to disable.
- *Default:* `0`

## How the Wrapper Runs php-fpm

The wrapper does not parse the php-fpm config itself. At start, it does these steps:

1. It runs `php-fpm -tt` and reads the config that php-fpm shows: the pools after all includes, `$pool` variables, and defaults.
2. It writes an overlay config to `--run-dir`. The overlay includes the config from `--fpm-config` and moves `error_log` and the `slowlog` of each pool to FIFOs in `--run-dir`.
3. It starts php-fpm with the overlay config and parses the FIFOs.

The wrapper does not change the files of the config, and php-fpm does not need write access to the directories of `error_log` and `slowlog`.
`error_log = /proc/self/fd/2` of the official Docker images and `error_log = syslog` also go through the proxy.
Use `--fpm-no-errlog` or `--fpm-no-slowlog` to keep the paths of the config.

`php-fpm -tt` opens the `slowlog` of each pool with `request_slowlog_timeout`. The user of the wrapper must have write access to these paths.

The wrapper works with php-fpm of PHP 5.6 and later. It does not need root.

## Session Cleanup

Debian packages of PHP set `session.gc_probability = 0` and remove the expired session files with a cron job.
A container usually has no cron. Set `--session-cleanup-interval` (for example, `30m`) to let the wrapper do this work:

1. The wrapper reads the session directives from `php-fpm -i` and from the `php_value` and `php_admin_value` directives of each pool.
2. It cleans up the `session.save_path` directories of the pools with `session.save_handler = files`, also the subdirectories of `N;/path`.
3. It removes the `sess_*` files that are older than `session.gc_maxlifetime`. When two or more pools use one directory, the longest lifetime applies.
4. It does not remove the session files that php-fpm has open. It updates their modification time.

Do not enable the cleanup when the application sets `session.gc_maxlifetime` with `ini_set()` at run time.
The wrapper cannot see this value and can remove live sessions.

## Development

Run the checks before you push:

- `make lint` runs `golangci-lint`.
- `make test` runs the unit tests.
- `make e2e` runs the end-to-end tests. These tests need Docker.

The end-to-end tests start the wrapper with php-fpm in a container (testcontainers-go).
The default image is `php:8.5-fpm-alpine`. CI also runs the tests on PHP 5.6, 7.4, and 8.2–8.5.
The PHP scripts of the tests must work on PHP 5.6. To use a different image, set `E2E_PHP_IMAGE`:

```sh
E2E_PHP_IMAGE=php:8.2-fpm make e2e
```
Loading
Loading