Skip to content

Commit d0ca622

Browse files
raphaelcoefficburgesQ
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 56df266 commit d0ca622

File tree

6 files changed

+184
-7
lines changed

6 files changed

+184
-7
lines changed

frankenphp.c

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

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

frankenphp.go

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

685+
// go_log_attrs is a cgo-exported bridge between PHP and the Go slog logger.
686+
//
687+
// It is called from C/PHP and must not panic. All errors are reported by
688+
// returning a C-allocated error string; on success it returns NULL.
689+
//
690+
// Parameters:
691+
//
692+
// threadIndex:
693+
// - Index into the phpThreads table, used to retrieve the Go context for
694+
// the current PHP request/thread.
695+
//
696+
// message:
697+
// - Pointer to a C string containing the log message bytes. The memory
698+
// is owned by the caller and must NOT be freed by Go.
699+
//
700+
// len:
701+
// - Length of the message, in bytes, as seen from C (not including the
702+
// terminating NUL). This is passed to C.GoStringN to build the Go string.
703+
//
704+
// level:
705+
// - Numeric log level compatible with slog.Level values. It is cast to
706+
// slog.Level inside this function.
707+
//
708+
// cattrs:
709+
// - Pointer to a PHP zval representing an associative array of attributes,
710+
// or NULL. When non-NULL, it is converted to map[string]any via GoMap[any]
711+
// and then mapped to slog.Attr values (using slog.Any under the hood).
712+
//
713+
// Return value:
714+
//
715+
// On success:
716+
// - Returns NULL and the message is logged (if the logger is enabled at
717+
// the given level).
718+
//
719+
// On error:
720+
// - Returns a non-NULL *C.char pointing to a NUL-terminated error message
721+
// allocated with C.CString. The caller is responsible for releasing
722+
// this memory.
723+
//
724+
//export go_log_attrs
725+
func go_log_attrs(threadIndex C.uintptr_t, message *C.char, len C.int, level C.int, cattrs *C.zval) *C.char {
726+
var attrs map[string]any
727+
728+
if cattrs == nil {
729+
attrs = nil
730+
} else {
731+
var err error
732+
if attrs, err = GoMap[any](unsafe.Pointer(cattrs)); err != nil {
733+
// NOTE: return value is already formatted for a PHP exception message.
734+
return C.CString("Failed to log message: converting attrs: " + err.Error())
735+
}
736+
}
737+
738+
m := C.GoStringN(message, len)
739+
lvl := slog.Level(level)
740+
741+
ctx := phpThreads[threadIndex].context()
742+
743+
if globalLogger.Enabled(ctx, lvl) {
744+
globalLogger.LogAttrs(ctx, lvl, m, mapToAttr(attrs)...)
745+
}
746+
747+
return nil
748+
}
749+
750+
func mapToAttr(input map[string]any) []slog.Attr {
751+
out := make([]slog.Attr, 0, len(input))
752+
753+
for key, val := range input {
754+
out = append(out, slog.Any(key, val))
755+
}
756+
757+
return out
758+
}
759+
685760
//export go_is_context_done
686761
func go_is_context_done(threadIndex C.uintptr_t) C.bool {
687762
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: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1013,7 +1013,52 @@ func FuzzRequest(f *testing.F) {
10131013
// Headers should always be present even if empty
10141014
assert.Contains(t, body, fmt.Sprintf("[CONTENT_TYPE] => %s", fuzzedString))
10151015
assert.Contains(t, body, fmt.Sprintf("[HTTP_FUZZED] => %s", fuzzedString))
1016-
10171016
}, &testOptions{workerScript: "request-headers.php"})
10181017
})
10191018
}
1019+
1020+
func TestFrankenPHPLog(t *testing.T) {
1021+
type tcase struct {
1022+
name, file string
1023+
cases map[string]string
1024+
}
1025+
1026+
test := func(t *testing.T, tc tcase) {
1027+
t.Helper()
1028+
t.Run(tc.name, func(t *testing.T) {
1029+
t.Helper()
1030+
1031+
var buf bytes.Buffer
1032+
handler := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})
1033+
logger := slog.New(handler)
1034+
1035+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
1036+
body, _ := testGet("http://example.com/"+tc.file, handler, t)
1037+
assert.Empty(t, body)
1038+
}, &testOptions{
1039+
logger: logger,
1040+
nbParallelRequests: 1,
1041+
nbWorkers: 1,
1042+
})
1043+
1044+
logOutput := buf.String()
1045+
1046+
t.Logf("captured log output: %s", logOutput)
1047+
1048+
for level, needle := range tc.cases {
1049+
assert.Containsf(t, logOutput, needle, "should contains %q log", level)
1050+
}
1051+
})
1052+
}
1053+
1054+
test(t, tcase{
1055+
name: "log to slog attrs",
1056+
file: "log_to_slog.php",
1057+
cases: map[string]string{
1058+
"debug attrs": `level=DEBUG msg="some debug message" "key int"=1`,
1059+
"info attrs": `level=INFO msg="some info message" "key string"=string`,
1060+
"warn attrs": `level=WARN msg="some warn message"`,
1061+
"error attrs": `level=ERROR msg="some error message" err="[a v]"`,
1062+
},
1063+
})
1064+
}

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)