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): Add RabbitMQ instrumentation using the php-amqplib library #1009

Draft
wants to merge 16 commits into
base: dev
Choose a base branch
from

Conversation

zsistla
Copy link
Contributor

@zsistla zsistla commented Jan 21, 2025

Initial commit does the following:

  • Detect library via magic file
  • Detect package and version information.
  • Basic unit tests

Subsequent commits:

  • Add attributes needed for rabbitMQ to message segment
  • Instrument basic_publish and basic_get
  • add unit tests, multiverse tests
  • added support for PHP 7.x

Remaining:

  • More testing
  • Add DT headers

Initial commit does the following:
* Detect library via magic file
* Detect package and version information.
* Basic unit tests
@newrelic-php-agent-bot
Copy link

newrelic-php-agent-bot commented Jan 21, 2025

Test Suite Status Result
Multiverse 4/8 passing
SOAK 72/72 passing

Initial commit does the following:
* Detect library via magic file
* Detect package and version information.
* Basic unit tests
@zsistla zsistla force-pushed the feat/rabbitmq_instrumentation branch from 2df782f to 0b184ca Compare January 22, 2025 23:01
…ewrelic-php-agent into feat/rabbitmq_instrumentation
agent/lib_php_amqplib.c Outdated Show resolved Hide resolved
Test version detection when class exists but version const doesn't.
Fixed typos.
@codecov-commenter
Copy link

codecov-commenter commented Jan 23, 2025

Codecov Report

Attention: Patch coverage is 37.05882% with 107 lines in your changes missing coverage. Please review.

Project coverage is 77.76%. Comparing base (94a41f5) to head (2335634).

Files with missing lines Patch % Lines
agent/lib_php_amqplib.c 10.52% 85 Missing ⚠️
axiom/nr_span_event.c 56.25% 21 Missing ⚠️
axiom/nr_segment_traces.c 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #1009      +/-   ##
==========================================
- Coverage   78.02%   77.76%   -0.26%     
==========================================
  Files         197      198       +1     
  Lines       27418    27586     +168     
==========================================
+ Hits        21392    21453      +61     
- Misses       6026     6133     +107     
Flag Coverage Δ
agent-for-php-7.2 77.89% <37.50%> (-0.26%) ⬇️
agent-for-php-7.3 77.91% <37.50%> (-0.26%) ⬇️
agent-for-php-7.4 77.62% <37.50%> (-0.26%) ⬇️
agent-for-php-8.0 77.01% <38.65%> (-0.24%) ⬇️
agent-for-php-8.1 77.52% <38.65%> (-0.24%) ⬇️
agent-for-php-8.2 77.13% <38.65%> (-0.24%) ⬇️
agent-for-php-8.3 77.13% <38.65%> (-0.24%) ⬇️
agent-for-php-8.4 77.14% <38.65%> (-0.24%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@zsistla zsistla added this to the next-release milestone Jan 27, 2025
* Creates message segment on basic_publish call.
 * While the RabbitMQ tutorial for using with the dockerized RabbitMQ setup
 * correctly and loads the PhpAmqpLib\\Channel\\AMQPChannel class in time for
 * the agent to wrap the instrumented functions, there are AWS MQ_BROKER
 * specific but valid scenarios where the PhpAmqpLib\\Channel\\AMQPChannel class
 * file does not explicitly load or does not load in time, and the instrumented
 * functions are NEVER wrapped regardless of how many times they are called in
 * one txn.  Specifically, this centered around the very slight but impactful
 * differences when using the PhpAmqpLib\Connection\AMQPStreamConnection which
 * causes an explicit load of the AMQPChannel class/file and
 * PhpAmqpLib\Connection\AMQPSSLConnection which does NOT cause an explicit load
 * of the AMQPChannelclass/file. The following method is thus the only way to
 * ensure the class is loaded in time for the functions to be wrapped.
result = zend_eval_string(
"(function() {"
" $nr_php_amqplib_version = '';"
" try {"
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe PHP provides tools to make sure this constant exists so it would avoid triggering an exception in the first place.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would be doing an PHP extra call when this call already verifies and retrieves the value and is future proofed against any future exceptions. Additionally, it will automatically load the class if it's not already loaded.

amqp_port
= nr_php_zend_hash_index_find(Z_ARRVAL_P(connect_constructor_params),
AMQP_CONSTRUCT_PARAMS_PORT_INDEX);
if (nr_php_is_zval_valid_scalar(amqp_port)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This checks if it is a bool, a valid string, long or double. Are these all valid values for the port?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, only allow long:
095f82d

= nr_php_zend_hash_index_find(Z_ARRVAL_P(connect_constructor_params),
AMQP_CONSTRUCT_PARAMS_PORT_INDEX);
if (nr_php_is_zval_valid_scalar(amqp_port)) {
message_params->server_port = Z_LVAL_P(amqp_port);
Copy link
Contributor

Choose a reason for hiding this comment

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

The check above does not guarantee the value is a long.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

verify long: 095f82d

*/
message_params.destination_name
= ENSURE_PERSISTENCE(Z_STRVAL_P(amqp_exchange));
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

What if ampq_exchange is not a string at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The parameter list for the function call dictates that exchange is a string.

Additionally, the check here guarantees it is a valid non-empty string before we set it (and strdup for php 7.x) here.

* strdup server_address, destination_name, and
* messaging_destination_routing_key.
*/
nr_free(message_params.server_address);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it could be cleaner to have a matching macro to ENSURE_PERSISTENCE that could be used to un-persist these values in the appropriate way for the PHP is use.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great idea!
71e5e02

* Default in case of empty string. */
message_params.messaging_destination_publish_name
= Z_STRVAL_P(amqp_exchange);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

What if ampq_exchange is not a string? (This applies to this pattern in any wrappers in this PR).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The function param list and/or class definition dictates strings or not.
nr_php_is_zval_non_empty_string verifies that the value is not null, is not empty, and is a zval string. Only after that do we do anything with it.


/* Extract the version for aws-sdk 3+ */
nr_php_amqplib_handle_version();
nr_php_amqplib_ensure_class();
Copy link
Contributor

Choose a reason for hiding this comment

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

I am concerned on how this all works - if the file used to trigger this enable function is, for example, in a sequence of PHP calls that was in response to autoloading the class we need, and inside this sequence we start another sequence to load this class - is composer setup to handle this?

Is there not a file we can use in the situations this class is not triggered (the AWS context I guess?).

Copy link
Contributor Author

@zsistla zsistla Jan 31, 2025

Choose a reason for hiding this comment

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

No. None of the files (autoload files or files in general that get loaded through the course of the test case) can cause this class to be loaded in order to wrap the function properly during AWS test cases. This is different behavior from the rabbitmq server used with multiverse so the difference is either due to the slight variance in making an SSL connection or due to the older rabbitmq engine that is running in AWS. None of the files that loaded when running AWS tests worked to allow our instrumentation to be wrapped (different than the non-ssl, non-aws rabbitmq server which actually loaded the file as well as the class).

All this is doing is calling class_exists so pretty innocuous. This is a php function which other functions can call in the middle of their logic and have easily handled as well. It's analogous to function calls that make calls to other classes. If you turn the show_* on, classes get loaded inside classes all the time because unless the file is loaded previously, the class doesn't actually get loaded until it's used.

Yes composer can handle it. That change was enough to finally get the functions wrapped for AWS setups, and the segments were created and made it to the backend with appropriate attributes.

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