diff --git a/frankenphp.c b/frankenphp.c index 5e070cd36..f7528816b 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -550,6 +550,27 @@ PHP_FUNCTION(mercure_publish) { RETURN_THROWS(); } +PHP_FUNCTION(frankenphp_log) { + zend_string *message = NULL; + zend_long level = 0; + zval *context = NULL; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_STR(message) + Z_PARAM_LONG(level) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY(context) + ZEND_PARSE_PARAMETERS_END(); + + char *ret = NULL; + ret = go_log_attrs(thread_index, message, level, context); + if (ret != NULL) { + zend_throw_exception(spl_ce_RuntimeException, ret, 0); + free(ret); + RETURN_THROWS(); + } +} + PHP_MINIT_FUNCTION(frankenphp) { zend_function *func; diff --git a/frankenphp.go b/frankenphp.go index 4b5c0972b..72105875d 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -682,6 +682,42 @@ func go_log(threadIndex C.uintptr_t, message *C.char, level C.int) { } } +//export go_log_attrs +func go_log_attrs(threadIndex C.uintptr_t, message *C.zend_string, level C.zend_long, cattrs *C.zval) *C.char { + var attrs map[string]any + + if cattrs == nil { + attrs = nil + } else { + var err error + if attrs, err = GoMap[any](unsafe.Pointer(cattrs)); err != nil { + // NOTE: return value is already formatted for a PHP exception message. + return C.CString("Failed to log message: converting attrs: " + err.Error()) + } + } + + ctx := phpThreads[threadIndex].context() + + if globalLogger.Enabled(ctx, slog.Level(level)) { + globalLogger.LogAttrs(ctx, + slog.Level(level), + GoString(unsafe.Pointer(message)), + mapToAttr(attrs)...) + } + + return nil +} + +func mapToAttr(input map[string]any) []slog.Attr { + out := make([]slog.Attr, 0, len(input)) + + for key, val := range input { + out = append(out, slog.Any(key, val)) + } + + return out +} + //export go_is_context_done func go_is_context_done(threadIndex C.uintptr_t) C.bool { return C.bool(phpThreads[threadIndex].frankenPHPContext().isDone) diff --git a/frankenphp.stub.php b/frankenphp.stub.php index 60ac5d588..9b19160f7 100644 --- a/frankenphp.stub.php +++ b/frankenphp.stub.php @@ -36,3 +36,9 @@ function apache_response_headers(): array|bool {} * @param string|string[] $topics */ function mercure_publish(string|array $topics, string $data = '', bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null): string {} + +/** + * @param int $level The importance or severity of a log event. The higher the level, the more important or severe the event. Common levels are -4 for debug, 0 for info, 4 for warn, and 8 for error. For more details, see: https://pkg.go.dev/log/slog#Level + * array $context Values of the array will be converted to the corresponding Go type (if supported by FrankenPHP) and added to the context of the structured logs using https://pkg.go.dev/log/slog#Attr + */ +function frankenphp_log(string $message, int $level = 0, array $context = []): void {} diff --git a/frankenphp_arginfo.h b/frankenphp_arginfo.h index 558c6e3cf..6417cfb70 100644 --- a/frankenphp_arginfo.h +++ b/frankenphp_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: cd534a8394f535a600bf45a333955d23b5154756 */ + * Stub hash: 28aa97e2c6102b3e51059dbd001ac65679f0bfda */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) @@ -35,6 +35,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mercure_publish, 0, 1, IS_STRING ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, retry, IS_LONG, 1, "null") ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_log, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, level, IS_LONG, 0, "0") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, context, IS_ARRAY, 0, "[]") +ZEND_END_ARG_INFO() ZEND_FUNCTION(frankenphp_handle_request); ZEND_FUNCTION(headers_send); @@ -42,7 +47,7 @@ ZEND_FUNCTION(frankenphp_finish_request); ZEND_FUNCTION(frankenphp_request_headers); ZEND_FUNCTION(frankenphp_response_headers); ZEND_FUNCTION(mercure_publish); - +ZEND_FUNCTION(frankenphp_log); static const zend_function_entry ext_functions[] = { ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request) @@ -55,5 +60,6 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(frankenphp_response_headers, arginfo_frankenphp_response_headers) ZEND_FALIAS(apache_response_headers, frankenphp_response_headers, arginfo_apache_response_headers) ZEND_FE(mercure_publish, arginfo_mercure_publish) + ZEND_FE(frankenphp_log, arginfo_frankenphp_log) ZEND_FE_END }; diff --git a/frankenphp_test.go b/frankenphp_test.go index 713797403..7e5204a2b 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -1013,7 +1013,34 @@ func FuzzRequest(f *testing.F) { // Headers should always be present even if empty assert.Contains(t, body, fmt.Sprintf("[CONTENT_TYPE] => %s", fuzzedString)) assert.Contains(t, body, fmt.Sprintf("[HTTP_FUZZED] => %s", fuzzedString)) - }, &testOptions{workerScript: "request-headers.php"}) }) } + +func TestFrankenPHPLog(t *testing.T) { + var buf bytes.Buffer + handler := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + logger := slog.New(handler) + + runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) { + body, _ := testGet("http://example.com/log_to_slog.php", handler, t) + assert.Empty(t, body) + }, &testOptions{ + logger: logger, + nbParallelRequests: 1, + nbWorkers: 1, + }) + + logOutput := buf.String() + + t.Logf("captured log output: %s", logOutput) + + for level, needle := range map[string]string{ + "debug attrs": `level=DEBUG msg="some debug message" "key int"=1`, + "info attrs": `level=INFO msg="some info message" "key string"=string`, + "warn attrs": `level=WARN msg="some warn message"`, + "error attrs": `level=ERROR msg="some error message" err="[a v]"`, + } { + assert.Containsf(t, logOutput, needle, "should contains %q log", level) + } +} diff --git a/testdata/log_to_slog.php b/testdata/log_to_slog.php new file mode 100644 index 000000000..b8e831229 --- /dev/null +++ b/testdata/log_to_slog.php @@ -0,0 +1,23 @@ + 1, +]); + +frankenphp_log("some info message", 0, [ + "key string" => "string", +]); + +frankenphp_log("some warn message", 4); + +frankenphp_log("some error message", 8, [ + "err" => ["a", "v"], +]);