diff --git a/caddy/go.mod b/caddy/go.mod index 74ead77b02..d5d2ec594a 100644 --- a/caddy/go.mod +++ b/caddy/go.mod @@ -17,6 +17,7 @@ require ( github.com/prometheus/client_golang v1.25.0 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.12.1 + go.uber.org/zap/exp v0.3.0 ) require ( @@ -184,7 +185,6 @@ require ( go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.28.0 // indirect - go.uber.org/zap/exp v0.3.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/crypto v0.58.0 // indirect golang.org/x/crypto/x509roots/fallback v0.0.0-20261009200856-99e4382b128e // indirect diff --git a/caddy/php-cli.go b/caddy/php-cli.go index 53b754a59f..ff90cc9cff 100644 --- a/caddy/php-cli.go +++ b/caddy/php-cli.go @@ -1,12 +1,15 @@ package caddy import ( + "log/slog" "os" "path/filepath" "strings" + "github.com/caddyserver/caddy/v2" caddycmd "github.com/caddyserver/caddy/v2/cmd" "github.com/dunglas/frankenphp" + "go.uber.org/zap/exp/zapslog" "github.com/spf13/cobra" ) @@ -35,7 +38,8 @@ func cmdPHPCLI(fs caddycmd.Flags) (int, error) { } } - status := frankenphp.ExecuteScriptCLI(args[0], args) + logger := slog.New(zapslog.NewHandler(caddy.Log().Core(), zapslog.WithName("frankenphp"))) + status := frankenphp.ExecuteScriptCLI(args[0], args, frankenphp.WithLogger(logger)) os.Exit(status) diff --git a/cli.go b/cli.go index a96153a14a..23d66b5db3 100644 --- a/cli.go +++ b/cli.go @@ -2,11 +2,33 @@ package frankenphp // #include "frankenphp.h" import "C" -import "unsafe" +import ( + "fmt" + "os" + "unsafe" +) // ExecuteScriptCLI executes the PHP script passed as parameter. // It returns the exit status code of the script. -func ExecuteScriptCLI(script string, args []string) int { +// +// Only the WithLogger and WithContext options are supported, they configure frankenphp_log(). +func ExecuteScriptCLI(script string, args []string, opts ...Option) int { + o := &opt{} + for _, f := range opts { + if err := f(o); err != nil { + fmt.Fprintln(os.Stderr, err) + + return 1 + } + } + + if o.ctx != nil { + globalCtx = o.ctx + } + if o.logger != nil { + globalLogger = o.logger + } + // Ensure extensions are registered before CLI execution registerExtensions() diff --git a/cli_test.go b/cli_test.go index 75acc22efe..9f2376b8d8 100644 --- a/cli_test.go +++ b/cli_test.go @@ -241,7 +241,6 @@ func TestExecuteCLIHTTPFunctionsUnavailable(t *testing.T) { {"frankenphp_handle_request", "static function () {}"}, {"headers_send", "103"}, {"mercure_publish", "'https://example.com/topic', 'test'"}, - {"frankenphp_log", "'CLI feature detection'"}, } { t.Run(tt.name, func(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) @@ -266,6 +265,29 @@ var_export(function_exists($function)); } } +func TestExecuteCLILog(t *testing.T) { + if _, err := os.Stat("internal/testcli/testcli"); err != nil { + t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`") + } + + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "internal/testcli/testcli", "-r", ` +frankenphp_log('hello', FRANKENPHP_LOG_LEVEL_WARN, ['k' => 'v']); +try { + frankenphp_log('invalid context', FRANKENPHP_LOG_LEVEL_ERROR, ['k' => new stdClass()]); +} catch (RuntimeException $e) { + echo 'caught'; +} +`) + cmd.WaitDelay = time.Second + output, err := cmd.CombinedOutput() + require.NoError(t, ctx.Err(), "output: %s", output) + require.NoError(t, err, "output: %s", output) + assert.Contains(t, string(output), "WARN hello k=v") + assert.Contains(t, string(output), "caught") +} + func TestExecuteCLINativeHTTPFunctions(t *testing.T) { if _, err := os.Stat("internal/testcli/testcli"); err != nil { t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`") diff --git a/docs/logging.md b/docs/logging.md index e8df467aa6..76e4947ba7 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -59,6 +59,9 @@ When viewing the logs (e.g., via `docker compose logs`), the output will appear {"level":"warn","ts":1704067200,"logger":"frankenphp","msg":"Memory usage high","current_usage":10485760,"peak_usage":12582912} ``` +`frankenphp_log()` is also available in `frankenphp php-cli`, where it writes to the standard error output. +`php-cli` doesn't load the `Caddyfile`, so its `log` directives don't apply there. + ## `error_log()` FrankenPHP also allows logging using the standard `error_log()` function. If the `$message_type` parameter is `4` (SAPI), diff --git a/frankenphp.c b/frankenphp.c index ea72e5ecca..a5d2ae9c89 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -1222,16 +1222,25 @@ static zend_module_entry frankenphp_module = { TOSTRING(FRANKENPHP_VERSION), STANDARD_MODULE_PROPERTIES}; +static const zend_function_entry frankenphp_cli_functions[] = { + ZEND_FE(frankenphp_log, arginfo_frankenphp_log) ZEND_FE_END}; + +static PHP_MINIT_FUNCTION(frankenphp_cli) { + register_frankenphp_symbols(module_number); + + return SUCCESS; +} + /* same phpinfo section in CLI, without the server functions and hooks */ static zend_module_entry frankenphp_cli_module = { STANDARD_MODULE_HEADER, "frankenphp-cli", - NULL, /* function table */ - NULL, /* initialization */ - NULL, /* shutdown */ - NULL, /* request initialization */ - NULL, /* request shutdown */ - PHP_MINFO(frankenphp), /* information */ + frankenphp_cli_functions, /* function table */ + PHP_MINIT(frankenphp_cli), /* initialization */ + NULL, /* shutdown */ + NULL, /* request initialization */ + NULL, /* request shutdown */ + PHP_MINFO(frankenphp), /* information */ TOSTRING(FRANKENPHP_VERSION), STANDARD_MODULE_PROPERTIES};