Skip to content

Commit 1844ed9

Browse files
raphaelcoefficdunglas
authored andcommitted
feat: add frankenphp_log_attrs() as a PHP function
The CGO method allow to log a php message while binding an array of random type as slog.Attr.
1 parent 41da660 commit 1844ed9

6 files changed

Lines changed: 166 additions & 7 deletions

File tree

frankenphp.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,28 @@ PHP_FUNCTION(mercure_publish) {
549549
RETURN_THROWS();
550550
}
551551

552+
PHP_FUNCTION(frankenphp_log) {
553+
char *message = NULL;
554+
size_t message_len = 0;
555+
zend_long level = 0;
556+
zval *context = NULL;
557+
558+
ZEND_PARSE_PARAMETERS_START(2, 3)
559+
Z_PARAM_STRING(message, message_len)
560+
Z_PARAM_LONG(level)
561+
Z_PARAM_OPTIONAL
562+
Z_PARAM_ARRAY(context)
563+
ZEND_PARSE_PARAMETERS_END();
564+
565+
char * ret = NULL;
566+
ret = go_log_attrs(thread_index, message, message_len, (int)level, context);
567+
if (ret != NULL) {
568+
zend_throw_exception(spl_ce_RuntimeException, ret, 0);
569+
// free(ret); // NOTE: is the string copied by zend_throw ??
570+
RETURN_THROWS();
571+
}
572+
}
573+
552574
PHP_MINIT_FUNCTION(frankenphp) {
553575
zend_function *func;
554576

frankenphp.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,81 @@ func go_log(threadIndex C.uintptr_t, message *C.char, level C.int) {
693693
}
694694
}
695695

696+
// go_log_attrs is a cgo-exported bridge between PHP and the Go slog logger.
697+
//
698+
// It is called from C/PHP and must not panic. All errors are reported by
699+
// returning a C-allocated error string; on success it returns NULL.
700+
//
701+
// Parameters:
702+
//
703+
// threadIndex:
704+
// - Index into the phpThreads table, used to retrieve the Go context for
705+
// the current PHP request/thread.
706+
//
707+
// message:
708+
// - Pointer to a C string containing the log message bytes. The memory
709+
// is owned by the caller and must NOT be freed by Go.
710+
//
711+
// len:
712+
// - Length of the message, in bytes, as seen from C (not including the
713+
// terminating NUL). This is passed to C.GoStringN to build the Go string.
714+
//
715+
// level:
716+
// - Numeric log level compatible with slog.Level values. It is cast to
717+
// slog.Level inside this function.
718+
//
719+
// cattrs:
720+
// - Pointer to a PHP zval representing an associative array of attributes,
721+
// or NULL. When non-NULL, it is converted to map[string]any via GoMap[any]
722+
// and then mapped to slog.Attr values (using slog.Any under the hood).
723+
//
724+
// Return value:
725+
//
726+
// On success:
727+
// - Returns NULL and the message is logged (if the logger is enabled at
728+
// the given level).
729+
//
730+
// On error:
731+
// - Returns a non-NULL *C.char pointing to a NUL-terminated error message
732+
// allocated with C.CString. The caller is responsible for releasing
733+
// this memory.
734+
//
735+
//export go_log_attrs
736+
func go_log_attrs(threadIndex C.uintptr_t, message *C.char, len C.int, level C.int, cattrs *C.zval) *C.char {
737+
var attrs map[string]any
738+
739+
if cattrs == nil {
740+
attrs = nil
741+
} else {
742+
var err error
743+
if attrs, err = GoMap[any](unsafe.Pointer(cattrs)); err != nil {
744+
// NOTE: return value is already formatted for a PHP exception message.
745+
return C.CString("Failed to log message: converting attrs: " + err.Error())
746+
}
747+
}
748+
749+
m := C.GoStringN(message, len)
750+
lvl := slog.Level(level)
751+
752+
ctx := phpThreads[threadIndex].context()
753+
754+
if globalLogger.Enabled(ctx, lvl) {
755+
globalLogger.LogAttrs(ctx, lvl, m, mapToAttr(attrs)...)
756+
}
757+
758+
return nil
759+
}
760+
761+
func mapToAttr(input map[string]any) []slog.Attr {
762+
out := make([]slog.Attr, 0, len(input))
763+
764+
for key, val := range input {
765+
out = append(out, slog.Any(key, val))
766+
}
767+
768+
return out
769+
}
770+
696771
//export go_is_context_done
697772
func go_is_context_done(threadIndex C.uintptr_t) C.bool {
698773
return C.bool(phpThreads[threadIndex].frankenPHPContext().isDone)

frankenphp.stub.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,9 @@ function apache_response_headers(): array|bool {}
3636
* @param string|string[] $topics
3737
*/
3838
function mercure_publish(string|array $topics, string $data = '', bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null): string {}
39+
40+
/**
41+
* @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
42+
* array<string, any> $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
43+
*/
44+
function frankenphp_log(string $message, int $level = 0, array $context = []): void {}

frankenphp_arginfo.h

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: cd534a8394f535a600bf45a333955d23b5154756 */
2+
* Stub hash: 28aa97e2c6102b3e51059dbd001ac65679f0bfda */
33

44
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1, _IS_BOOL, 0)
55
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
@@ -35,25 +35,31 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mercure_publish, 0, 1, IS_STRING
3535
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, retry, IS_LONG, 1, "null")
3636
ZEND_END_ARG_INFO()
3737

38+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_log, 0, 1, IS_VOID, 0)
39+
ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0)
40+
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, level, IS_LONG, 0, "0")
41+
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, context, IS_ARRAY, 0, "[]")
42+
ZEND_END_ARG_INFO()
3843

3944
ZEND_FUNCTION(frankenphp_handle_request);
4045
ZEND_FUNCTION(headers_send);
4146
ZEND_FUNCTION(frankenphp_finish_request);
4247
ZEND_FUNCTION(frankenphp_request_headers);
4348
ZEND_FUNCTION(frankenphp_response_headers);
4449
ZEND_FUNCTION(mercure_publish);
45-
50+
ZEND_FUNCTION(frankenphp_log);
4651

4752
static const zend_function_entry ext_functions[] = {
4853
ZEND_FE(frankenphp_handle_request, arginfo_frankenphp_handle_request)
4954
ZEND_FE(headers_send, arginfo_headers_send)
5055
ZEND_FE(frankenphp_finish_request, arginfo_frankenphp_finish_request)
51-
ZEND_FALIAS(fastcgi_finish_request, frankenphp_finish_request, arginfo_fastcgi_finish_request)
56+
ZEND_RAW_FENTRY("fastcgi_finish_request", zif_frankenphp_finish_request, arginfo_fastcgi_finish_request, 0, NULL, NULL)
5257
ZEND_FE(frankenphp_request_headers, arginfo_frankenphp_request_headers)
53-
ZEND_FALIAS(apache_request_headers, frankenphp_request_headers, arginfo_apache_request_headers)
54-
ZEND_FALIAS(getallheaders, frankenphp_request_headers, arginfo_getallheaders)
58+
ZEND_RAW_FENTRY("apache_request_headers", zif_frankenphp_request_headers, arginfo_apache_request_headers, 0, NULL, NULL)
59+
ZEND_RAW_FENTRY("getallheaders", zif_frankenphp_request_headers, arginfo_getallheaders, 0, NULL, NULL)
5560
ZEND_FE(frankenphp_response_headers, arginfo_frankenphp_response_headers)
56-
ZEND_FALIAS(apache_response_headers, frankenphp_response_headers, arginfo_apache_response_headers)
61+
ZEND_RAW_FENTRY("apache_response_headers", zif_frankenphp_response_headers, arginfo_apache_response_headers, 0, NULL, NULL)
5762
ZEND_FE(mercure_publish, arginfo_mercure_publish)
63+
ZEND_FE(frankenphp_log, arginfo_frankenphp_log)
5864
ZEND_FE_END
5965
};

frankenphp_test.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1058,7 +1058,34 @@ func FuzzRequest(f *testing.F) {
10581058
// Headers should always be present even if empty
10591059
assert.Contains(t, body, fmt.Sprintf("[CONTENT_TYPE] => %s", fuzzedString))
10601060
assert.Contains(t, body, fmt.Sprintf("[HTTP_FUZZED] => %s", fuzzedString))
1061-
10621061
}, &testOptions{workerScript: "request-headers.php"})
10631062
})
10641063
}
1064+
1065+
func TestFrankenPHPLog(t *testing.T) {
1066+
var buf bytes.Buffer
1067+
handler := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})
1068+
logger := slog.New(handler)
1069+
1070+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
1071+
body, _ := testGet("http://example.com/log_to_slog.php", handler, t)
1072+
assert.Empty(t, body)
1073+
}, &testOptions{
1074+
logger: logger,
1075+
nbParallelRequests: 1,
1076+
nbWorkers: 1,
1077+
})
1078+
1079+
logOutput := buf.String()
1080+
1081+
t.Logf("captured log output: %s", logOutput)
1082+
1083+
for level, needle := range map[string]string{
1084+
"debug attrs": `level=DEBUG msg="some debug message" "key int"=1`,
1085+
"info attrs": `level=INFO msg="some info message" "key string"=string`,
1086+
"warn attrs": `level=WARN msg="some warn message"`,
1087+
"error attrs": `level=ERROR msg="some error message" err="[a v]"`,
1088+
} {
1089+
assert.Containsf(t, logOutput, needle, "should contains %q log", level)
1090+
}
1091+
}

testdata/log_to_slog.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
// NOTE: use CGO frankenphp_log method.
4+
// The message and it's optional arguments are expected to be logged by go' std slog system.
5+
// The log level should be respected out of the box by the std' slog.
6+
//
7+
// ac[0] expect the log message as string
8+
// ac[1] expect the slog.Level, from -8 to +8
9+
// ac[2] is an optional php map, which will be converted to a []slog.Attr
10+
11+
frankenphp_log("some debug message", -4, [
12+
"key int" => 1,
13+
]);
14+
15+
frankenphp_log("some info message", 0, [
16+
"key string" => "string",
17+
]);
18+
19+
frankenphp_log("some warn message", 4);
20+
21+
frankenphp_log("some error message", 8, [
22+
"err" => ["a", "v"],
23+
]);

0 commit comments

Comments
 (0)