Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(agent): use Observer API to hook into Zend Engine for PHPs 8.0+ #542

Merged
merged 68 commits into from
Mar 1, 2024

Conversation

zsistla
Copy link
Contributor

@zsistla zsistla commented Sep 29, 2022

Use Zend's Observer API to hook into Zend Engine and instrument user functions for PHPs 8.0+. This method of hooking into Zend Engine is compatible with Zend Engine's JIT, which will no longer be disabled when the agent is installed.

@zsistla
Copy link
Contributor Author

zsistla commented Sep 29, 2022

ok jenkins

…server API (#501)

* feat(agent): Registered function begin/end and error handlers with Observer API
1) Registered function begin/end handlers with Observer API
2) Created the function begin/end handler stubs.  Full functionality is schedule for another ticket.
3) Registered currently existing error handler with Observer API
4) created php_observer.c/h module to contain the observer api logic.

Testing:
1) Verified new function begin/end handlers were registered correctly and received zend_execute_data from PHP engine.
2) Current test cases verified that registering our current error handler directly caused no change in functionality.

Additional
* feat(agent): Don't call handlers for internal functions.
Currently internal functions are not handled by OAPI, but they will be in 8.2.  These functions are tailored to USER functions (similar to nr_php_execute) and we don't want internal functions filtered to these handlers.  This will default to INTERNAL functions being handled by the current implementation.  To change in the future, it's possible we'd need to implement handlers specific for internal functions (similar to nr_php_execute_internal).
…ta. (#505)

* feat(agent): Add/Update functions to utilize the OAPI zend_execute_data.

1) Added 4 new functions for use with OAPI instrumentation:
extern const char* nr_php_zend_execute_data_function_name(
    const zend_execute_data* execute_data);

extern const char* nr_php_zend_execute_data_filename(
    const zend_execute_data* execute_data);

extern const char* nr_php_zend_execute_data_scope_name(
    const zend_execute_data* execute_data);

extern uint32_t nr_php_zend_execute_data_lineno(
    const zend_execute_data* execute_data);
2) Added test cases to test new functionality.
3) Updated the following to reflect new OAPI paradigm and add additional checks for robustness:
* #define NR_PHP_USER_FN_THIS()
* nr_php_execute_scope
3) Added unit test cases to test new functions
4) Also verified functionality via integration tests.

* refactor(agent): If called correctly, new functions should work with PHP 7+.
…ctions. (#516)

* feat(agent): Propagate OAPI return values and update return value functions.
1) updated macros to pass the OAPI given return value throughout the userland system.
2) changed nr_php_get_return_value to return oapi given pointer
3) Added the OVERWRITE_ZEND_EXECUTE_DATA to allow us to toggle off during feature addition, but toggle off to maintain CI as long as possible.  It also gives the flexibility to revert to the instrumentation prior to OAPI.
4) macro to toggle between just using the OAPI return value when OAPI is enabled or calling nr_php_get_return_value_ptr when it is not OAPI.
…d zed. (#517)

* feat(agent): Propagate OAPI return values and update return value functions.
1) updated macros to pass the OAPI given return value throughout the userland system.
2) changed nr_php_get_return_value to return oapi given pointer
3) Added the OVERWRITE_ZEND_EXECUTE_DATA to allow us to toggle off during feature addition, but toggle off to maintain CI as long as possible.  It also gives the flexibility to revert to the instrumentation prior to OAPI.
4) macro to toggle between just using the OAPI return value when OAPI is enabled or calling nr_php_get_return_value_ptr when it is not OAPI.
* feat(agent): Add code level metrics functionality.
1) Add INI value to disable/enable code level metrics.
2) Create new function `nr_php_txn_add_code_level_metrics` that uses nr_php_zend_execute_data_* family of functions to extract CLM and add it as an `agent attribute`.
3) Added new NR_TXN_ATTRIBUTES for `code.namespace`, `code.lineno`, `code.filepath`, and `code.function`.
4) Added call to php_execute_enabled to call the new CLM function
4) Added new integration tests to exercise the new functionality.

Note:  CLM functionality is only compatible with PHP 7+.

* feat(agent): Code level metrics functionality.
1) updated `nr_php_execute_metadata_t` to hold additional information and moved the definition of the struct to php_execute.h.
2) `nr_php_execute_enabled` uses the metadata at the beginning so now we can simply release at the end.
3) In the case of a super short segment that would have been ignored, we do NOT add CLM (otherwise it wouldn't be ignored).  We wait until after we decide to ignore or not to add the CLM data.

* feat(agent): Updated test scripts.

* fix(agent): check clm string lengths will not be truncated

* fix(agent): check for empty/null CLM attributes

* style(agent): clang-format php_execute

* fix(agent): fix logic handling CLM string length

* refactor(agent): abstract out CLM while-loop code for readability
@lavarou lavarou force-pushed the oapi branch 2 times, most recently from ccf6bc3 to 7cad68d Compare November 1, 2022 20:56
zsistla and others added 6 commits November 1, 2022 17:26
Features:
* implement user functions instrumentation via PHP's Observer API
* remove function hooks that overwrite `zend_execute_ex` for user
functions
* add `special_instrumentation_before` callback to `nruserfn_t` for user
functions that need instrumentation to happen before the function
executes.
* add `nr_php_wrap_user_function_before_after` to install Observer API's
before and after function callbacks (to be used in lib_*.c and/or fw_*.c)
* add `nr_zend_call_oapi_special_before` to call Observer APIs
before function callback (to be used in `observer_fcall_begin` handler).
* store txn_start_time in a segments to so that it is available in
`observer_fcall_end` handler to verify and end the segment
* add reportedclass to nruserfn_t to account for functions existing in
one class table while reporting they belong to another class (details
commented in code).

Refactorings:
* add `nr_php_wraprec_matches` helper function
* add `wraprec` to segments to only make the call to get_wraprec_by_name
once (in the `observer_fcall_begin` handler and not again in the
`observer_fcall_end` handler)

Tests:
* update unit tests to test new features and changed functionality
* feat(agent): implement php_execute_show functionality for OAPI

* Update agent/php_execute.c

Co-authored-by: Michal Nowacki <[email protected]>

* style(agent): PR feedback, remove comment

* Update agent/php_execute.c

Co-authored-by: Michal Nowacki <[email protected]>
Framework detection (nr_execute_handle_framework) not only adds wraprecs
but also sets NRPRG(current_framework). It is important to set it before
function is called because its value affects other instrumentation, e.g.
when datastore segment for SQL operation is ended, a table name modify
function (nr_php_modify_table_name_fn) to shorten table name is selected
based on NRPRG(current_framework).

This fixes frameworks/magento/test_temp_tables.php and
frameworks/wordpress/test_site_specific_tables.php integration tests.
…563)

* fix(agent): fix check for max strlen when generating clm attributes

* style(tests): follow php function naming convention (camelCase)
Fix `drupal_http_request` instrumentation for PHP 8.0+ by using Observer
APIs `before` callback to add New Relic headers and `after` callback
to finalize external segment with metrics. Observer API instrumentation
requires the external request segment to be created in `before` callback
but available in `after` callback. Therefore it is made a NRPRG global.
1. Renamed nr_php_execute_metadata_add_code_level_metrics to
nr_php_observer_metadata_init as this will be the way we populate the
metadata. We don't need to do duplicate effort with
`nr_php_execute_metadata_init` anymore.
2. Since nr_php_execute_metadata_init is now for populating metadata,
moved all CLM checks out nr_php_observer_metadata_init and into
nr_php_txn_add_code_level_metrics where it is more appropriate.
3. Modified stacked segments to have a pointer to the metadata so if
they are closed off by an exception, they will properly propogate the
information. Added initializations/deninitializations.
4. For php_execute_enabled, only call nr_php_observer_metadata_init if
we need to.
5. Added nr_php_observer_exception_segment_end
 To handled closing off observer segments if an exception occurs.
6. Now use zend_throw_exception_hook (more info:
https://www.phpinternalsbook.com/php7/extensions_design/hooks.html
Note: This ONLY notifies when an exception is thrown. It gives no
indication if that exception was subsequently caught or not.
8. OAPI leaves dangling segments in the case of exceptions. These need
to be cleaned up for functions that rely on the current segment
(includes begin/end functions, stacked segment unwinding, and API calls)
9. New inline function re`nr_php_api_ensure_current_segment` to account
for dangling segments when calling our API.
10. TXN globals to keep track of the exception
11. Change in php_txn turn off recording after unwind the segments to
give timer to attach exception to dangling segment(s).
12. Modify stacked segments init/denit to handle additional segment
metadata variable.
13. Functions to clear/set TXN uncaught_exception variables.
14. Update metadata struct to retain more context of the segment.
15. Removed legacy exception code that wasn't getting called anymore.
16. Added tests.
17. Added a `clean`callback to the wraprec functionality and associated
unit tests.

With OAPI exceptions, the registered function handler (nr_php_observer_fcall_end) doesn't get called when an uncaught exception occurs, and therefore doesn't decrement the stack_depth counter.

All OAPI unhandled exception cleanup filters through: nr_php_observer_segment_end so we decrement php_cur_stack_depth there when we cleanup orphaned segments.
Additionally, since nr_php_observer_segment_end is an exit path, also call nr_php_show_oapi_metadata
New function nr_php_show_oapi_metadata called via the segment exception handling exit path (to correspond to nr_php_show_exec_return) to show the all the available function details when the special_flags.show_execute_* is toggled on. This will help when debugging.
Added additional test cases to ensure proper php_cur_stack_depth counting
@zsistla
Copy link
Contributor Author

zsistla commented Jan 21, 2023

ok jenkins

3 similar comments
@zsistla
Copy link
Contributor Author

zsistla commented Jan 21, 2023

ok jenkins

@zsistla
Copy link
Contributor Author

zsistla commented Jan 21, 2023

ok jenkins

@zsistla
Copy link
Contributor Author

zsistla commented Jan 21, 2023

ok jenkins

…tack when using OAPI (#582)

Symfony1 instrumentation utilizes the call stack, but should never be
run with PHP8+.
Occasionally, we want to instrument functions called with
call_user_func_array, when we otherwise wouldn't instrument that
function. To do this, we instrument the internal function
call_user_func_array and check for contexts in which we want to begin
instrumentation. However, call_user_func_array can be inlined by the
zend compiler. Previously, we were detecting inlined calls by
overwriting the DO_FCALL opcode. With OAPI, we no longer want to touch
the opcodes and are instead checking for the inlined calls during
observer_do_fcall.
@zsistla
Copy link
Contributor Author

zsistla commented Jan 26, 2023

ok jenkins

Consistent with other functions in php_execute, check if txn exists
before trying to use it.

The issue occurred during the first request, and because it is the
first, the appname is unknown and we don't create a txn. However, an
exception happened in that first request, our exception_hook handler got
triggered, and if that is triggered, we always try to close off dangling
segments. In this case, because we didn't check for txn==null, it
segfaulted as we tried to access txn elements.
Pr fixes that in two ways, 1) check for txn==null before closing off
segments. 2) don't record uncaught exception info generated from our
exception_hook handler if a txn is null.
```
2023-01-26 21:03:16.816 +0000 (29813 29813) verbosedebug: RINIT processing started
2023-01-26 21:03:16.816 +0000 (29813 29813) debug: added app='PHP Test Apps Laravel ads v2' license='07...4a'
2023-01-26 21:03:16.816 +0000 (29813 29813) verbosedebug: querying app='PHP Test Apps Laravel ads v2' from parent=4
2023-01-26 21:03:16.816 +0000 (29813 29813) verbosedebug: sending appinfo message, len=6492
2023-01-26 21:03:16.817 +0000 (29813 29813) debug: APPINFO reply unknown app='PHP Test Apps Laravel ads v2'
2023-01-26 21:03:16.817 +0000 (29813 29813) debug: unable to begin transaction: app 'PHP Test Apps Laravel ads v2' is unknown
```
note the last line: debug: 
unable to begin transaction: app 'PHP Test Apps Laravel ads v2' is
unknown
After #766 `nr_execute_handle_framework` needs filename length.
After #778 wordpress cleaned tags need not to be freed - they're memoized in
a hashmap that persists through the whole request and is destroyed in rshutdown.
@lavarou lavarou added the oapi label Dec 22, 2023
bduranleau-nr and others added 16 commits January 25, 2024 09:38
PHP embed SAPI, used by unit tests, demonstrates memory issues when PHP
code executed by way of `nr_php_call` or `tlib_php_request_eval` throws
a PHP Exception. This functionality is best suited to be exercised via
integration test.
After #798 there's no need to fix wrapping of transient user functions in
nr_php_add_custom_tracer_named because it is no longer used for those.
After #798 there's no need to pass any wraprec options when wraprecs are
created with nr_php_add_custom_tracer_named because it is no longer used
to create different types of wraprecs. It always creates non-transient
wraprecs and therefore always needs to generate instrumented function metrics.
After #768, filter hook's callback function instrumentation needs to create
wordpress metrics when hook callback function throws an exception. Therefore
its 'after' wrappers (after and clean) need to be set to do it on hook's
callback function either normal return or when it returns via an exception.
After #799, wordpress hooks stack handlers need to be able to generate hooks
metrics too when hooks are to be monitored but not hooks' callback functions,
i.e. when newrelic.framework.wordpress.hooks.options=threshold.
After #798, the tests expect Drupal\page_cache\StackMiddleware\PageCache::get to
be instrumented so that transient instrumentation will not get installed. But
empty files don't get executed by PHP 8.2+ when the agent uses Observer API to
hook into Zend engine, therefore mocks needs to do something in order for the
non-transient instrumentation to be installed (classes and class' methods need
to be available at the time non-transient instrumentation is installed). Enhance
Drupal\page_cache\StackMiddleware\PageCache with a 'noop' statement - echo "";`
zend_try/zend_catch is to handle Zend Exceptions not PHP exceptions -
see
[here](https://github.com/newrelic/newrelic-php-agent/blob/320ea571a11bc469d7d8179dfe51577b54df11df/agent/php_user_instrument.c#L17-L50)
for more details. They were added in this
[commit](66eccf7)
to handle misbehaving PHP embed SAPI that threw Zend Exception when PHP
code that was called via nr_php_call threw PHP Exception. PHP CLI or CGI
SAPIs don’t throw Zend Exception when PHP code that was called threw PHP
Exception and therefore zend_try/zend_catch is effectively a dead code.
Removing the need to manual handling of dangling segments, because Zend
calls all of the hooks we need.

- adds a boolean argument to nr_php_error_record_exception to control
whether we add the error to the current segment. This is needed because
the OAPI context of when the above is called is no longer during a
segment with an uncaught exception and was incorrectly adding the error
to the root segment.
- removes all language of "dangling segments". These no longer exist.
Zend calls all of the necessary `fcall_end`'s, even when an exception is
thrown. When this happens,` func_return_value` is a C `NULL` pointer
which is distinct from a `NULL` (but valid) zval when there is no return
value from a function. We use this `NULL` value to determine the
presence of an uncaught exception.
- no longer overwrites the exception hook; no longer stores a copy of
exceptions locally
- replace storing metadata in the segment, which was used to pair
segments from func_begin with func_end, with logic that always creates
segment in func_begin

Mostly undoes the work of
#580

---------

Co-authored-by: Michal Nowacki <[email protected]>
Co-authored-by: bduranleau-nr <[email protected]>
Co-authored-by: Amber Sistla <[email protected]>
Move exception handler instrumentation from fcall_begin to fcall_end.
This requires a change of how we are obtaining the fields of the
exception, because calling `nr_php_call` during the handling of an
exception does not play nicely with PHP.

---------

Co-authored-by: Michal Nowacki <[email protected]>
Because OAPI doesn't create a nice callstack that replicates the PHP
callstack, stacked segments make little/no sense in its context.
Instead, we want to utilize the segment pool available in txn's.

force_current_segment was how stacked segments were able to maintain
their context in a txn while not being on the segment stack. For OAPI,
there is no reason to use this field, as the current segment will just
be the top of the txn's segment stack.

---------

Co-authored-by: bduranleau-nr <[email protected]>
Co-authored-by: Michal Nowacki <[email protected]>
Segment order does not matter, as the collector reconstructs the tree
with timing and parenting info. As such, we can remove segments with
O(1) speed instead of O(N).

This is an alternative approach than
#780 and only 1 of
the 2 should be merged.

---------

Co-authored-by: Michal Nowacki <[email protected]>
)

* Enable the opcache.so extension by default in the integration_runner
* Ensure the opcache INI settings in the integration_runner environment
are enabled (these can be overwritten in the test case INI section if
needing to test without opcache)
* Added few tests to ensure compatibility with opcache disabled and to
demonstrate how to overwrite the value.

---------

Co-authored-by: ZNeumann <[email protected]>
Co-authored-by: Zach Neumann <[email protected]>
Co-authored-by: Michal Nowacki <[email protected]>
fix Drupal package detection

Use file that is loaded also on PHP 8.2 with include/require optimizations that
don't execute files without executable statements.
@lavarou lavarou changed the title DO NOT MERGE feat(agent): Update the agent with OAPI functionality. DO NOT MERGE feat(agent): use Observer API to hook into Zend Engine for PHPs 8.0+ Feb 23, 2024
Remove 'bootstrap/app.php' from the list of signature files used to detect 
Laravel - Laravel is detected with 'illuminate/foundation/application.php'
for all supported Laravel versions: 6, 7, 8, 9 and 10.
Using 'bootstrap/app.php' causes Laravel to be detected before Lumen
can be detected because 'bootstrap/app.php' is the second file loaded
when Lumen based app is handling the request and this results in Lumen
app being detected as Laravel.
@ZNeumann ZNeumann changed the title DO NOT MERGE feat(agent): use Observer API to hook into Zend Engine for PHPs 8.0+ feat(agent): use Observer API to hook into Zend Engine for PHPs 8.0+ Feb 28, 2024
@ZNeumann ZNeumann merged commit a57b812 into dev Mar 1, 2024
61 checks passed
@zsistla zsistla deleted the oapi branch September 13, 2024 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Development

Successfully merging this pull request may close these issues.

6 participants