Skip to content

Commit c24f63e

Browse files
committed
Instrument requests in FrankenPHP worker mode
Wrap callable passed to `frankenphp_handle_request` to emulate PHP request's lifecycle events in begin and end handlers of the callable. This way each handled request becomes a distinct transaction. This method does not rely on internal implementation of FrankenPHP worker mode but rather relies on its public interface that must be implemented by any application using FrankenPHP worker mode. Also reset the state (end the transaction) after initial RINIT executed when worker script is started.
1 parent 0ad0d80 commit c24f63e

6 files changed

Lines changed: 155 additions & 13 deletions

File tree

agent/config.m4

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ if test "$PHP_NEWRELIC" = "yes"; then
221221
php_call.c php_curl.c php_curl_md.c php_datastore.c php_environment.c \
222222
php_error.c php_execute.c php_explain.c php_explain_mysqli.c \
223223
php_explain_pdo_mysql.c php_extension.c php_file_get_contents.c \
224-
php_globals.c php_hash.c php_header.c php_httprequest_send.c \
224+
php_frankenphp.c php_globals.c php_hash.c php_header.c php_httprequest_send.c \
225225
php_internal_instrument.c php_memcached.c php_minit.c php_mshutdown.c php_mysql.c \
226226
php_mysqli.c php_newrelic.c php_nrini.c php_observer.c php_output.c php_pdo.c \
227227
php_pdo_mysql.c php_pdo_pgsql.c php_pgsql.c php_psr7.c php_redis.c \

agent/php_frankenphp.c

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#include "php_frankenphp.h"
7+
#include "php_agent.h"
8+
#include "php_user_instrument.h"
9+
#include "php_wrapper.h"
10+
11+
#ifdef ZTS
12+
13+
static void nr_php_frankenphp_request_handler_fcall_begin(
14+
zend_execute_data* execute_data NRUNUSED) {
15+
nrl_verbosedebug(NRL_INSTRUMENT,
16+
"frankenphp_request_handler_fcall_begin started");
17+
// emulate rinit
18+
nr_php_txn_begin(0, 0);
19+
nrl_verbosedebug(NRL_INSTRUMENT,
20+
"frankenphp_request_handler_fcall_begin done");
21+
}
22+
23+
static void nr_php_frankenphp_request_handler_fcall_end(
24+
zend_execute_data* execute_data NRUNUSED,
25+
zval* return_value NRUNUSED) {
26+
nrl_verbosedebug(NRL_INSTRUMENT,
27+
"frankenphp_request_handler_fcall_end started");
28+
// emulate rshutdown
29+
nr_php_txn_end(0, 0);
30+
nrl_verbosedebug(NRL_INSTRUMENT, "frankenphp_request_handler_fcall_end done");
31+
}
32+
33+
// Wrap the request handler with before and after hooks to emulate rinit and
34+
// rshutdown
35+
void nr_php_frankenphp_handle_request(INTERNAL_FUNCTION_PARAMETERS) {
36+
zval* function = NULL;
37+
zend_function* zf;
38+
nruserfn_t* wr = NULL;
39+
40+
// Always end current transaction started when worker was started
41+
// Maybe store package data?
42+
nr_php_txn_end(1, 0);
43+
44+
zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "z",
45+
&function);
46+
zf = nr_php_zval_to_function(function);
47+
48+
wr = nr_php_wrap_callable(zf, NULL);
49+
if (NULL == wr) {
50+
nrl_verbosedebug(NRL_INSTRUMENT,
51+
"Failed to create wraprec for frankenphp request handler");
52+
return;
53+
}
54+
wr->fcall_handlers.begin = nr_php_frankenphp_request_handler_fcall_begin;
55+
wr->fcall_handlers.end = nr_php_frankenphp_request_handler_fcall_end;
56+
}
57+
#endif /* ZTS */

agent/php_frankenphp.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#ifndef PHP_FRANKENPHP_H
7+
#define PHP_FRANKENPHP_H
8+
9+
#include "php_includes.h"
10+
11+
#ifdef ZTS
12+
13+
extern void nr_php_frankenphp_handle_request(INTERNAL_FUNCTION_PARAMETERS);
14+
15+
#endif
16+
17+
#endif

agent/php_internal_instrument.c

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "php_explain.h"
1313
#include "php_explain_mysqli.h"
1414
#include "php_file_get_contents.h"
15+
#include "php_frankenphp.h"
1516
#include "php_globals.h"
1617
#include "php_hash.h"
1718
#include "php_httprequest_send.h"
@@ -113,6 +114,28 @@ static void nr_php_instrument_delegate(nrinternalfn_t* wraprec,
113114
(wraprec->inner_wrapper)(INTERNAL_FUNCTION_PARAM_PASSTHRU, wraprec);
114115
}
115116

117+
#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO
118+
/*
119+
* This delegate is used for the wrappers, which need to execute its inner
120+
* wrapper on every call, even when not recording. This can be used for starting
121+
* the transaction and thus needs to execute its inner wrapper to start
122+
* recording.
123+
*/
124+
static void nr_php_instrument_delegate_always(nrinternalfn_t* wraprec,
125+
INTERNAL_FUNCTION_PARAMETERS) {
126+
if ((NULL == wraprec) || (NULL == wraprec->oldhandler)
127+
|| (NULL == wraprec->inner_wrapper)) {
128+
/*
129+
* This conditional should never happen: The wraprec and its fields
130+
* should be properly populated during construction. Perhaps this should
131+
* be changed into some sort of assertion.
132+
*/
133+
return;
134+
}
135+
(wraprec->inner_wrapper)(INTERNAL_FUNCTION_PARAM_PASSTHRU, wraprec);
136+
}
137+
#endif
138+
116139
#define NR_OUTER_WRAPPER_NAME(RAW) _nr_outer_wrapper_function_##RAW
117140
#define NR_OUTER_GLOBAL_NAME(RAW) _nr_outer_wrapper_global_##RAW
118141
#define NR_INNER_WRAPPER_NAME(RAW) _nr_inner_wrapper_function_##RAW
@@ -142,6 +165,13 @@ static void nr_php_instrument_delegate(nrinternalfn_t* wraprec,
142165
nr_php_instrument_delegate(NR_OUTER_GLOBAL_NAME(RAW), \
143166
INTERNAL_FUNCTION_PARAM_PASSTHRU); \
144167
}
168+
#define NR_OUTER_WRAPPER_DELEGATE_ALWAYS(RAW) \
169+
static nrinternalfn_t* NR_OUTER_GLOBAL_NAME(RAW) = NULL; \
170+
static void ZEND_FASTCALL NR_OUTER_WRAPPER_NAME(RAW)( \
171+
INTERNAL_FUNCTION_PARAMETERS) { \
172+
nr_php_instrument_delegate_always(NR_OUTER_GLOBAL_NAME(RAW), \
173+
INTERNAL_FUNCTION_PARAM_PASSTHRU); \
174+
}
145175
#endif /* PHP < 7.3 */
146176

147177
/*
@@ -3069,6 +3099,18 @@ NR_INNER_WRAPPER(exception_common) {
30693099
}
30703100
}
30713101

3102+
#ifdef ZTS
3103+
NR_INNER_WRAPPER(frankenphp_handle_request) {
3104+
nrl_verbosedebug(NRL_INIT, "frankenphp_handle_request started");
3105+
3106+
nr_php_frankenphp_handle_request(INTERNAL_FUNCTION_PARAM_PASSTHRU);
3107+
3108+
// Let frankenphp take over
3109+
nr_wrapper->oldhandler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
3110+
nrl_verbosedebug(NRL_INIT, "frankenphp_handle_request done");
3111+
}
3112+
#endif
3113+
30723114
NR_OUTER_WRAPPER(mysql_select_db)
30733115
NR_OUTER_WRAPPER(mysql_selectdb)
30743116
NR_OUTER_WRAPPER(mysql_close)
@@ -3379,6 +3421,10 @@ NR_OUTER_WRAPPER(dl)
33793421
NR_OUTER_WRAPPER(set_exception_handler)
33803422
NR_OUTER_WRAPPER(restore_exception_handler)
33813423

3424+
#ifdef ZTS
3425+
NR_OUTER_WRAPPER_DELEGATE_ALWAYS(frankenphp_handle_request)
3426+
#endif /* ZTS */
3427+
33823428
nrinternalfn_t* nr_wrapped_internal_functions = NULL;
33833429

33843430
static nrinternalfn_t* nr_get_wraprec(void) {
@@ -3948,6 +3994,10 @@ void nr_php_generate_internal_wrap_records(void) {
39483994
exception_common, 0, 0)
39493995
NR_INTERNAL_WRAPREC("restore_exception_handler", restore_exception_handler,
39503996
exception_common, 0, 0)
3997+
#ifdef ZTS
3998+
NR_INTERNAL_WRAPREC("frankenphp_handle_request", frankenphp_handle_request,
3999+
frankenphp_handle_request, 0, 0)
4000+
#endif
39514001
}
39524002

39534003
/*

agent/php_observer.c

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
static zend_observer_fcall_handlers nr_php_fcall_register_handlers(
7676
zend_execute_data* execute_data) {
7777
zend_observer_fcall_handlers handlers = {NULL, NULL};
78+
nruserfn_t* wr = NULL;
7879
if (NULL == execute_data) {
7980
return handlers;
8081
}
@@ -83,7 +84,11 @@ static zend_observer_fcall_handlers nr_php_fcall_register_handlers(
8384
return handlers;
8485
}
8586

86-
if (0 == nr_php_recording()) {
87+
if (0 == nr_php_recording()
88+
#ifdef ZTS
89+
&& !nr_streq(sapi_module.name, "frankenphp")
90+
#endif
91+
) {
8792
return handlers;
8893
}
8994

@@ -110,23 +115,33 @@ static zend_observer_fcall_handlers nr_php_fcall_register_handlers(
110115
return handlers;
111116
}
112117

113-
if (!ZEND_OP_ARRAY_EXTENSION(
114-
NR_OP_ARRAY, NR_PHP_PROCESS_GLOBALS(op_array_extension_handle))) {
118+
// clang-format off
119+
wr = ZEND_OP_ARRAY_EXTENSION(NR_OP_ARRAY, NR_PHP_PROCESS_GLOBALS(op_array_extension_handle));
120+
if (NULL == wr) {
115121
zend_string* func_name = NR_OP_ARRAY->function_name;
116-
zend_string* scope_name
117-
= OP_ARRAY_IS_A_METHOD(NR_OP_ARRAY) ? NR_OP_ARRAY->scope->name : NULL;
118-
nruserfn_t* wr
119-
= nr_php_user_instrument_wraprec_hashmap_get(func_name, scope_name);
120-
// store the wraprec in the op_array extension for the duration of the
121-
// request for later lookup
122-
ZEND_OP_ARRAY_EXTENSION(NR_OP_ARRAY,
123-
NR_PHP_PROCESS_GLOBALS(op_array_extension_handle))
124-
= wr;
122+
zend_string* scope_name = OP_ARRAY_IS_A_METHOD(NR_OP_ARRAY) ? NR_OP_ARRAY->scope->name : NULL;
123+
wr = nr_php_user_instrument_wraprec_hashmap_get(func_name, scope_name);
124+
// store the wraprec in the op_array extension for the duration of the request for later lookup
125+
ZEND_OP_ARRAY_EXTENSION(NR_OP_ARRAY, NR_PHP_PROCESS_GLOBALS(op_array_extension_handle)) = wr;
126+
} else {
127+
if (nrl_should_print(NRL_VERBOSEDEBUG, NRL_INSTRUMENT)) {
128+
nrl_verbosedebug(NRL_INSTRUMENT, "%s - wraprec already installed", __func__);
129+
}
125130
}
126131

132+
// use default handlers for all user functions
127133
handlers.begin = nr_php_observer_fcall_begin;
128134
handlers.end = nr_php_observer_fcall_end;
135+
136+
// override default handlers with wraprec's custom handlers
137+
if (NULL != wr) {
138+
if (NULL != wr->fcall_handlers.begin)
139+
handlers.begin = wr->fcall_handlers.begin;
140+
if (NULL != wr->fcall_handlers.end)
141+
handlers.end = wr->fcall_handlers.end;
142+
}
129143
return handlers;
144+
// clang-format on
130145
}
131146

132147
#if ZEND_MODULE_API_NO > ZEND_8_0_X_API_NO /* PHP8.1+ */

agent/php_user_instrument.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ typedef struct _nruserfn_t {
110110
#if ZEND_MODULE_API_NO >= ZEND_7_4_X_API_NO
111111
char* wordpress_plugin_theme;
112112
#endif
113+
#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO
114+
zend_observer_fcall_handlers fcall_handlers;
115+
#endif
113116
} nruserfn_t;
114117

115118
#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO

0 commit comments

Comments
 (0)