Skip to content

Conversation

@raphaelcoeffic
Copy link

As discussed in #1961, there is no real way to pass a severity/level to any log handler offered by PHP that would make it to the FrankenPHP layer. This new function allows applications embedding FrankenPHP to integrate PHP logging into the application itself, thus offering a more streamlined experience.

@raphaelcoeffic raphaelcoeffic force-pushed the log-message branch 2 times, most recently from ff20363 to 8e172c5 Compare November 16, 2025 18:53
@AlliBalliBaba
Copy link
Contributor

AlliBalliBaba commented Nov 16, 2025

You should probably also add an integration test to frankenphp_test.go or so and and create a log.php in testdata that does the actual logging. I think this should be possible with a custom logger, that just writes to a buffer, something like this (haven't tried):

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, i int) {
		body, _ := testGet(fmt.Sprintf("http://example.com/log.php", i), handler, t)
		assert.Equal(t, body, fmt.Sprintf("whatever the script output looks like", i))
	}, &testOptions{logger: logger})

	// time.Sleep(time.Millisecond * 10) // not sure if a sleep is needed to avoid race conditions

	logOutput := buf.String()
	assert.Contains(t, logOutput, "whatever was logged")
}

@dunglas
Copy link
Member

dunglas commented Nov 16, 2025

I'm working on this too. I wonder if we shouldn't expose slog more directly: give the ability to add structured logs, custom OpenTelemetry-compatible levels etc?

What I have in mind was to expose a class implementing both PSR-3 and a special Log($level, $message, ...$context) method giving direct access to slog capabilities.

WDYT?

@raphaelcoeffic
Copy link
Author

What I have in mind was to expose a class implementing both PSR-3 and a special Log($level, $message, ...$context) method giving direct access to slog capabilities.

That sounds great, indeed! That would maybe help us filter the logging for special categories.

In the application we're developing, the PHP code currently produces an "audit log" (ex: "user A added a new user") by writing directly to a file. This file currently needs to be watched to retrieve the audit events and forward them to the monitoring server. It would be great if we could just pipe these events through the same logging pipeline inside the Go code.

@dunglas let me know if you need any help with this more capable implementation.

@burgesQ burgesQ force-pushed the log-message branch 3 times, most recently from c0ddb8c to 9cc7b0d Compare November 17, 2025 13:06
@dunglas
Copy link
Member

dunglas commented Nov 17, 2025

Actually, I wonder if we couldn't just update your PR to provide a single function having this API:

/**
 * @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<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
 */
frankenphp_log(string $message, int $level = 0, array $context = []);

This will provide the low-level brick to have PSR-3 and similar implementations user-land.
We could even patch Monolog to add support for this.

This will also keep full compatibility with Go and Caddy logging systems, as well as with OpenTelemetry, because of the more open log level.

WDYT?

@raphaelcoeffic
Copy link
Author

raphaelcoeffic commented Nov 17, 2025

So that means keeping the old SAPI function frankenphp_log_message() unchanged (especially, with the same log level mapping, that is PHP's) while adding that new function, which would use a different level mapping (Go's log levels, right?), is that correct?

@AlliBalliBaba
Copy link
Contributor

You can probably just use this function to convert an arbitrary array to a map.

Currently it will take a zval, after #1894 you'll be able to pass the array directly.

@burgesQ burgesQ force-pushed the log-message branch 2 times, most recently from 24339e5 to 7f3e43e Compare November 18, 2025 10:40
*/
function mercure_publish(string|array $topics, string $data = '', bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null): string {}

function frankenphp_log_message(string $message, int $level): void {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the benefit of keeping this function?

Copy link

@burgesQ burgesQ Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

none, let me remove it

frankenphp.go Outdated
Comment on lines 645 to 650
var lvl slog.Level
if level < -8 || level > 8 {
lvl = slog.LevelInfo
} else {
lvl = slog.Level(level)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var lvl slog.Level
if level < -8 || level > 8 {
lvl = slog.LevelInfo
} else {
lvl = slog.Level(level)
}
lvl = slog.Level(level)

According to the docs, any int can be used as level: https://pkg.go.dev/log/slog#hdr-Levels

frankenphp.go Outdated

ctx := phpThreads[threadIndex].context()

// NOTE: no need to check for logger enabled levels.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should do it anyway as early as possible to prevent useless work and allocations.

@burgesQ burgesQ force-pushed the log-message branch 9 times, most recently from d0ca622 to 5083c4f Compare November 19, 2025 19:05
The CGO method allow to log a php message while binding an array of random
type as slog.Attr.
@AlliBalliBaba
Copy link
Contributor

Otherwise starting to look good👍, CI failures are unrelated to this PR I think (apart from the clang-format one)

frankenphp.c Outdated
Comment on lines 553 to 567
PHP_FUNCTION(frankenphp_log) {
char *message = NULL;
size_t message_len = 0;
zend_long level = 0;
zval *context = NULL;

ZEND_PARSE_PARAMETERS_START(2, 3)
Z_PARAM_STRING(message, message_len)
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, message_len, (int)level, context);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
PHP_FUNCTION(frankenphp_log) {
char *message = NULL;
size_t message_len = 0;
zend_long level = 0;
zval *context = NULL;
ZEND_PARSE_PARAMETERS_START(2, 3)
Z_PARAM_STRING(message, message_len)
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, message_len, (int)level, context);
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);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

frankenphp.go Outdated
}

//export go_log_attrs
func go_log_attrs(threadIndex C.uintptr_t, message *C.char, len C.int, level C.int, cattrs *C.zval) *C.char {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func go_log_attrs(threadIndex C.uintptr_t, message *C.char, len C.int, level C.int, cattrs *C.zval) *C.char {
func go_log_attrs(threadIndex C.uintptr_t, message *C.zend_string, level C.zend_long, cattrs *C.zval) *C.char {

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

frankenphp.go Outdated
Comment on lines 699 to 709
m := C.GoStringN(message, len)
lvl := slog.Level(level)

ctx := phpThreads[threadIndex].context()

if globalLogger.Enabled(ctx, lvl) {
globalLogger.LogAttrs(ctx, lvl, m, mapToAttr(attrs)...)
}

return nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
m := C.GoStringN(message, len)
lvl := slog.Level(level)
ctx := phpThreads[threadIndex].context()
if globalLogger.Enabled(ctx, lvl) {
globalLogger.LogAttrs(ctx, lvl, m, mapToAttr(attrs)...)
}
return nil
}
ctx := phpThreads[threadIndex].context()
if globalLogger.Enabled(ctx, lvl) {
globalLogger.LogAttrs(ctx, slog.Level(level), GoString(unsafe.Pointer(m)), mapToAttr(attrs)...)
}
return nil
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, franken.gp:705 won't compile
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants