diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index a4c4d680..e00a318d 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -180,4 +180,4 @@ A more advanced example: https://github.com/open-telemetry/opentelemetry-php-con # Further reading -* https://www.phpinternalsbook.com/php7/build_system/building_extensions.html \ No newline at end of file +* https://www.phpinternalsbook.com/php7/build_system/building_extensions.html diff --git a/Makefile b/Makefile index 55d9f0f8..e6c74336 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PHP_VERSION ?= 8.3.0 +PHP_VERSION ?= 8.3.10 DISTRO ?= debian .DEFAULT_GOAL : help @@ -21,4 +21,6 @@ build: ## Build extension docker compose run $(DISTRO) ./build.sh test: ## Run tests docker compose run $(DISTRO) make test +remove-orphans: ## Remove orphaned containers + docker compose down --remove-orphans .PHONY: clean build test git-clean diff --git a/README.md b/README.md index 50070af5..efa3647a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,10 @@ Issues have been disabled for this repo in order to help maintain consistency be This is a PHP extension for OpenTelemetry, to enable auto-instrumentation. It is based on [zend_observer](https://www.datadoghq.com/blog/engineering/php-8-observability-baked-right-in/) and requires php8+ -The extension allows creating `pre` and `post` hook functions to arbitrary PHP functions and methods, which allows those methods to be wrapped with telemetry. +The extension allows: + +- creating `pre` and `post` hook functions to arbitrary PHP functions and methods, which allows those methods to be wrapped with telemetry +- adding attributes to functions and methods to enable observers at runtime In PHP 8.2+, internal/built-in PHP functions can also be observed. @@ -241,5 +244,90 @@ string(3) "new" string(8) "original" ``` +## Attribute-based hooking + +By applying attributes to source code, the OpenTelemetry extension can add hooks at runtime. + +Default pre and post hook methods are provided by the OpenTelemetry API: `OpenTelemetry\API\Instrumentation\Handler::pre` +and `::post`. + +This feature is disabled by default, but can be enabled by setting `opentelemetry.attr_hooks_enabled = On` in php.ini + +## Restrictions + +Attribute-based hooks can only be applied to a function/method that does not already have +hooks applied. +Only one hook can be applied to a function/method, including via interfaces. + +Since the attributes are evaluated at runtime, the extension checks whether a hook already +exists to decide whether it should apply a new runtime hook. + +## Configuration + +This feature can be configured via `.ini` by modifying the following entries: + +- `opentelemetry.attr_hooks_enabled` - boolean, default Off +- `opentelemetry.attr_pre_handler_function` - FQN of pre method/function +- `opentelemetry.attr_post_handler_function` - FQN of post method/function + +## `OpenTelemetry\API\Instrumentation\WithSpan` attribute + +This attribute is provided by the OpenTelemetry API can be applied to a function or class method. + +You can also provide optional parameters to the attribute, which control: +- span name +- span kind +- attributes + +```php +use OpenTelemetry\API\Instrumentation\WithSpan + +class MyClass +{ + #[WithSpan] + public function trace_me(): void + { + /* ... */ + } + + #[WithSpan('custom_span_name', SpanKind::KIND_INTERNAL, ['my-attr' => 'value'])] + public function trace_me_with_customization(): void + { + /* ... */ + } +} + +#[WithSpan] +function my_function(): void +{ + /* ... */ +} +``` + +## `OpenTelemetry\API\Instrumentation\SpanAttribute` attribute + +This attribute should be used in conjunction with `WithSpan`. It is applied to function/method +parameters, and causes those parameters and values to be passed through to the `pre` hook function +where they can be added as trace attributes. +There is one optional parameter, which controls the attribute key. If not set, the parameter name +is used. + +```php +use OpenTelemetry\API\Instrumentation\WithSpan +use OpenTelemetry\API\Instrumentation\SpanAttribute + +class MyClass +{ + #[WithSpan] + public function add_user( + #[SpanAttribute] string $username, + string $password, + #[SpanAttribute('a_better_attribute_name')] string $foo_bar_baz, + ): void + { + /* ... */ + } +``` + ## Contributing See [DEVELOPMENT.md](DEVELOPMENT.md) and https://github.com/open-telemetry/opentelemetry-php/blob/main/CONTRIBUTING.md diff --git a/docker-compose.yaml b/docker-compose.yaml index 617b0e2c..adce8b80 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,11 +1,10 @@ -version: '3.7' services: debian: build: context: docker dockerfile: Dockerfile.debian args: - PHP_VERSION: ${PHP_VERSION:-8.3.0} + PHP_VERSION: ${PHP_VERSION:-8.3.10} volumes: - ./ext:/usr/src/myapp environment: @@ -15,7 +14,7 @@ services: context: docker dockerfile: Dockerfile.alpine args: - PHP_VERSION: ${PHP_VERSION:-8.3.0} + PHP_VERSION: ${PHP_VERSION:-8.3.10} volumes: - ./ext:/usr/src/myapp environment: diff --git a/ext/.gitignore b/ext/.gitignore index 646379ad..86ef14d7 100644 --- a/ext/.gitignore +++ b/ext/.gitignore @@ -37,6 +37,7 @@ run-tests.php tests/**/*.diff tests/**/*.out tests/**/*.php +!tests/mocks/*.php tests/**/*.exp tests/**/*.log tests/**/*.sh diff --git a/ext/opentelemetry.c b/ext/opentelemetry.c index 0db31ae3..4ae6aee2 100644 --- a/ext/opentelemetry.c +++ b/ext/opentelemetry.c @@ -10,6 +10,7 @@ #include "otel_observer.h" #include "stdlib.h" #include "string.h" +#include "zend_attributes.h" #include "zend_closures.h" static int check_conflict(HashTable *registry, const char *extension_name) { @@ -86,9 +87,21 @@ STD_PHP_INI_ENTRY_EX("opentelemetry.allow_stack_extension", "Off", PHP_INI_ALL, OnUpdateBool, allow_stack_extension, zend_opentelemetry_globals, opentelemetry_globals, zend_ini_boolean_displayer_cb) +STD_PHP_INI_ENTRY_EX("opentelemetry.attr_hooks_enabled", "Off", PHP_INI_ALL, + OnUpdateBool, attr_hooks_enabled, + zend_opentelemetry_globals, opentelemetry_globals, + zend_ini_boolean_displayer_cb) +STD_PHP_INI_ENTRY("opentelemetry.attr_pre_handler_function", + "Opentelemetry\\API\\Instrumentation\\WithSpanHandler::pre", + PHP_INI_ALL, OnUpdateString, pre_handler_function_fqn, + zend_opentelemetry_globals, opentelemetry_globals) +STD_PHP_INI_ENTRY("opentelemetry.attr_post_handler_function", + "Opentelemetry\\API\\Instrumentation\\WithSpanHandler::post", + PHP_INI_ALL, OnUpdateString, post_handler_function_fqn, + zend_opentelemetry_globals, opentelemetry_globals) PHP_INI_END() -PHP_FUNCTION(hook) { +PHP_FUNCTION(OpenTelemetry_Instrumentation_hook) { zend_string *class_name; zend_string *function_name; zval *pre = NULL; diff --git a/ext/opentelemetry.stub.php b/ext/opentelemetry.stub.php index f84fd82b..688f364e 100644 --- a/ext/opentelemetry.stub.php +++ b/ext/opentelemetry.stub.php @@ -7,7 +7,7 @@ /** * @param string|null $class The (optional) hooked function's class. Null for a global/built-in function. * @param string $function The hooked function's name. - * @param \Closure|null $pre function($class, array $params, string $class, string $function, ?string $filename, ?int $lineno): $params + * @param \Closure|null $pre function($class, array $params, string $class, string $function, ?string $filename, ?int $lineno, ?array $span_args, ?array $span_attributes): $params * You may optionally return modified parameters. * @param \Closure|null $post function($class, array $params, $returnValue, ?Throwable $exception): $returnValue * You may optionally return modified return value. @@ -20,4 +20,4 @@ function hook( string $function, ?\Closure $pre = null, ?\Closure $post = null, -): bool {} +): bool {} \ No newline at end of file diff --git a/ext/opentelemetry_arginfo.h b/ext/opentelemetry_arginfo.h index 691c38ac..a130e22c 100644 --- a/ext/opentelemetry_arginfo.h +++ b/ext/opentelemetry_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 98cf39fd2bbea1a60b5978923e7d83e3954afb6e */ + * Stub hash: aa29142596154400c530f1194a7f29fbb9036929 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX( arginfo_OpenTelemetry_Instrumentation_hook, 0, 2, _IS_BOOL, 0) @@ -9,8 +9,8 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX( ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, post, Closure, 1, "null") ZEND_END_ARG_INFO() -ZEND_FUNCTION(hook); +ZEND_FUNCTION(OpenTelemetry_Instrumentation_hook); -static const zend_function_entry ext_functions[] = { - ZEND_NS_FE("OpenTelemetry\\Instrumentation", hook, - arginfo_OpenTelemetry_Instrumentation_hook) ZEND_FE_END}; +static const zend_function_entry ext_functions[] = {ZEND_NS_FALIAS( + "OpenTelemetry\\Instrumentation", hook, OpenTelemetry_Instrumentation_hook, + arginfo_OpenTelemetry_Instrumentation_hook) ZEND_FE_END}; diff --git a/ext/otel_observer.c b/ext/otel_observer.c index 16fd2012..2b808da2 100644 --- a/ext/otel_observer.c +++ b/ext/otel_observer.c @@ -5,10 +5,16 @@ #include "zend_execute.h" #include "zend_extensions.h" #include "zend_exceptions.h" +#include "zend_attributes.h" #include "php_opentelemetry.h" static int op_array_extension = -1; +const char *withspan_fqn_lc = "opentelemetry\\api\\instrumentation\\withspan"; +const char *spanattribute_fqn_lc = + "opentelemetry\\api\\instrumentation\\spanattribute"; +static char *with_span_attribute_args_keys[] = {"name", "span_kind"}; + typedef struct otel_observer { zend_llist pre_hooks; zend_llist post_hooks; @@ -60,7 +66,96 @@ static inline void func_get_function_name(zval *zv, zend_execute_data *ex) { ZVAL_STR_COPY(zv, ex->func->op_array.function_name); } -static void func_get_args(zval *zv, zend_execute_data *ex) { +static zend_function *find_function(zend_class_entry *ce, zend_string *name) { + zend_function *func; + ZEND_HASH_FOREACH_PTR(&ce->function_table, func) { + if (zend_string_equals(func->common.function_name, name)) { + return func; + } + } + ZEND_HASH_FOREACH_END(); + return NULL; +} + +// find SpanAttribute attribute on a parameter, or on a parameter of +// an interface +static zend_attribute *find_spanattribute_attribute(zend_function *func, + uint32_t i) { + zend_attribute *attr = zend_get_parameter_attribute_str( + func->common.attributes, spanattribute_fqn_lc, + strlen(spanattribute_fqn_lc), i); + + if (attr != NULL) { + return attr; + } + zend_class_entry *ce = func->common.scope; + if (ce && ce->num_interfaces > 0) { + zend_class_entry *interface_ce; + for (uint32_t i = 0; i < ce->num_interfaces; i++) { + interface_ce = ce->interfaces[i]; + if (interface_ce != NULL) { + // does the interface have the function we are looking for? + zend_function *iface_func = + find_function(interface_ce, func->common.function_name); + if (iface_func != NULL) { + // method found, check positional arg for attribute + attr = zend_get_parameter_attribute_str( + iface_func->common.attributes, spanattribute_fqn_lc, + strlen(spanattribute_fqn_lc), i); + if (attr != NULL) { + return attr; + } + } + } + } + } + + return NULL; +} + +// find WithSpan in attributes, or in interface method attributes +static zend_attribute *find_withspan_attribute(zend_function *func) { + zend_attribute *attr; + attr = zend_get_attribute_str(func->common.attributes, withspan_fqn_lc, + strlen(withspan_fqn_lc)); + if (attr != NULL) { + return attr; + } + zend_class_entry *ce = func->common.scope; + // if a method, check interfaces + if (ce && ce->num_interfaces > 0) { + zend_class_entry *interface_ce; + for (uint32_t i = 0; i < ce->num_interfaces; i++) { + interface_ce = ce->interfaces[i]; + if (interface_ce != NULL) { + // does the interface have the function we are looking for? + zend_function *iface_func = + find_function(interface_ce, func->common.function_name); + if (iface_func != NULL) { + // Method found in the interface, now check for attributes + attr = zend_get_attribute_str(iface_func->common.attributes, + withspan_fqn_lc, + strlen(withspan_fqn_lc)); + if (attr) { + return attr; + } + } + } + } + } + return NULL; +} + +static bool func_has_withspan_attribute(zend_execute_data *ex) { + zend_attribute *attr = find_withspan_attribute(ex->func); + + return attr != NULL; +} + +// get function args. any args with the +// SpanAttributes attribute are added to the attributes HashTable +static void func_get_args(zval *zv, HashTable *attributes, + zend_execute_data *ex, bool check_for_attributes) { zval *p, *q; uint32_t i, first_extra_arg; uint32_t arg_count = ZEND_CALL_NUM_ARGS(ex); @@ -98,6 +193,22 @@ static void func_get_args(zval *zv, zend_execute_data *ex) { ex->func->op_array.T); } while (i < arg_count) { + if (check_for_attributes && + ex->func->type != ZEND_INTERNAL_FUNCTION) { + zend_string *arg_name = ex->func->op_array.vars[i]; + zend_attribute *attribute = + find_spanattribute_attribute(ex->func, i); + if (attribute != NULL) { + if (attribute->argc) { + zend_string *key = Z_STR(attribute->args[0].value); + zend_hash_del(attributes, key); + zend_hash_add(attributes, key, p); + } else { + zend_hash_del(attributes, arg_name); + zend_hash_add(attributes, arg_name, p); + } + } + } q = p; if (EXPECTED(Z_TYPE_INFO_P(q) != IS_UNDEF)) { ZVAL_DEREF(q); @@ -192,6 +303,50 @@ static inline void func_get_lineno(zval *zv, zend_execute_data *ex) { } } +static inline void func_get_attribute_args(zval *zv, HashTable *attributes, + zend_execute_data *ex) { + if (!OTEL_G(attr_hooks_enabled)) { + ZVAL_EMPTY_ARRAY(zv); + return; + } + zend_attribute *attr = find_withspan_attribute(ex->func); + if (attr == NULL || attr->argc == 0) { + ZVAL_EMPTY_ARRAY(zv); + return; + } + + HashTable *ht; + ALLOC_HASHTABLE(ht); + zend_hash_init(ht, attr->argc, NULL, ZVAL_PTR_DTOR, 0); + zend_attribute_arg arg; + zend_string *key; + + for (uint32_t i = 0; i < attr->argc; i++) { + arg = attr->args[i]; + if (i == 2 || + (arg.name && zend_string_equals_literal(arg.name, "attributes"))) { + // attributes, append to a separate HashTable + if (Z_TYPE(arg.value) == IS_ARRAY) { + zend_hash_clean(attributes); // should already be empty + HashTable *array_ht = Z_ARRVAL_P(&arg.value); + zend_hash_copy(attributes, array_ht, zval_add_ref); + } + } else { + if (arg.name != NULL) { + zend_hash_add(ht, arg.name, &arg.value); + } else { + key = zend_string_init(with_span_attribute_args_keys[i], + strlen(with_span_attribute_args_keys[i]), + 0); + zend_hash_add(ht, key, &arg.value); + zend_string_release(key); + } + } + } + + ZVAL_ARR(zv, ht); +} + /** * Check if the object implements or extends the specified class */ @@ -432,16 +587,24 @@ static void observer_begin(zend_execute_data *execute_data, zend_llist *hooks) { return; } - zval params[6]; - uint32_t param_count = 6; + zval params[8]; + uint32_t param_count = 8; + HashTable *attributes; + ALLOC_HASHTABLE(attributes); + zend_hash_init(attributes, 0, NULL, ZVAL_PTR_DTOR, 0); + bool check_for_attributes = + OTEL_G(attr_hooks_enabled) && func_has_withspan_attribute(execute_data); func_get_this_or_called_scope(¶ms[0], execute_data); - func_get_args(¶ms[1], execute_data); + func_get_attribute_args(¶ms[6], attributes, execute_data); + func_get_args(¶ms[1], attributes, execute_data, check_for_attributes); func_get_declaring_scope(¶ms[2], execute_data); func_get_function_name(¶ms[3], execute_data); func_get_filename(¶ms[4], execute_data); func_get_lineno(¶ms[5], execute_data); + ZVAL_ARR(¶ms[7], attributes); + for (zend_llist_element *element = hooks->head; element; element = element->next) { zend_fcall_info fci = empty_fcall_info; @@ -608,7 +771,7 @@ static void observer_end(zend_execute_data *execute_data, zval *retval, uint32_t param_count = 8; func_get_this_or_called_scope(¶ms[0], execute_data); - func_get_args(¶ms[1], execute_data); + func_get_args(¶ms[1], NULL, execute_data, false); func_get_retval(¶ms[2], retval); func_get_exception(¶ms[3]); func_get_declaring_scope(¶ms[4], execute_data); @@ -759,15 +922,29 @@ static void find_class_observers(HashTable *ht, HashTable *type_visited_lookup, } } -static void find_method_observers(HashTable *ht, HashTable *type_visited_lookup, - zend_class_entry *ce, zend_string *fn, - zend_llist *pre_hooks, +static void find_method_observers(HashTable *ht, zend_class_entry *ce, + zend_string *fn, zend_llist *pre_hooks, zend_llist *post_hooks) { + // Below hashtable stores information + // whether type was already visited + // This information is used to prevent + // adding hooks more than once in the case + // of extensive class hierarchy + HashTable type_visited_lookup; + zend_hash_init(&type_visited_lookup, 8, NULL, NULL, 0); HashTable *lookup = zend_hash_find_ptr_lc(ht, fn); if (lookup) { - find_class_observers(lookup, type_visited_lookup, ce, pre_hooks, + find_class_observers(lookup, &type_visited_lookup, ce, pre_hooks, post_hooks); } + zend_hash_destroy(&type_visited_lookup); +} + +static zval create_attribute_observer_handler(char *fn) { + zval callable; + ZVAL_STRING(&callable, fn); + + return callable; } static otel_observer *resolve_observer(zend_execute_data *execute_data) { @@ -775,23 +952,22 @@ static otel_observer *resolve_observer(zend_execute_data *execute_data) { if (!fbc->common.function_name) { return NULL; } + bool has_withspan_attribute = func_has_withspan_attribute(execute_data); + + if (OTEL_G(attr_hooks_enabled) == false && has_withspan_attribute) { + php_error_docref(NULL, E_CORE_WARNING, + "OpenTelemetry: WithSpan attribute found but " + "attribute hooks disabled"); + } otel_observer observer_instance; init_observer(&observer_instance); if (fbc->op_array.scope) { - // Below hashtable stores information - // whether type was already visited - // This information is used to prevent - // adding hooks more than once in the case - // of extensive class hierarchy - HashTable type_visited_lookup; - zend_hash_init(&type_visited_lookup, 8, NULL, NULL, 0); - find_method_observers( - OTEL_G(observer_class_lookup), &type_visited_lookup, - fbc->op_array.scope, fbc->common.function_name, - &observer_instance.pre_hooks, &observer_instance.post_hooks); - zend_hash_destroy(&type_visited_lookup); + find_method_observers(OTEL_G(observer_class_lookup), + fbc->op_array.scope, fbc->common.function_name, + &observer_instance.pre_hooks, + &observer_instance.post_hooks); } else { find_observers(OTEL_G(observer_function_lookup), fbc->common.function_name, &observer_instance.pre_hooks, @@ -800,7 +976,39 @@ static otel_observer *resolve_observer(zend_execute_data *execute_data) { if (!zend_llist_count(&observer_instance.pre_hooks) && !zend_llist_count(&observer_instance.post_hooks)) { - return NULL; + if (OTEL_G(attr_hooks_enabled) && has_withspan_attribute) { + // there are no observers registered for this function/method, but + // it has a WithSpan attribute. Add configured attribute-based + // pre/post handlers as new observers. + zval pre = create_attribute_observer_handler( + OTEL_G(pre_handler_function_fqn)); + zval post = create_attribute_observer_handler( + OTEL_G(post_handler_function_fqn)); + add_observer(fbc->op_array.scope ? fbc->op_array.scope->name : NULL, + fbc->common.function_name, &pre, &post); + zval_ptr_dtor(&pre); + zval_ptr_dtor(&post); + // re-find to update pre/post hooks + if (fbc->op_array.scope) { + find_method_observers( + OTEL_G(observer_class_lookup), fbc->op_array.scope, + fbc->common.function_name, &observer_instance.pre_hooks, + &observer_instance.post_hooks); + } else { + find_observers(OTEL_G(observer_function_lookup), + fbc->common.function_name, + &observer_instance.pre_hooks, + &observer_instance.post_hooks); + } + + if (!zend_llist_count(&observer_instance.pre_hooks) && + !zend_llist_count(&observer_instance.post_hooks)) { + // failed to add hooks? + return NULL; + } + } else { + return NULL; + } } otel_observer *observer = create_observer(); copy_observer(&observer_instance, observer); diff --git a/ext/php_opentelemetry.h b/ext/php_opentelemetry.h index b55302c1..2288dd03 100644 --- a/ext/php_opentelemetry.h +++ b/ext/php_opentelemetry.h @@ -13,6 +13,9 @@ ZEND_BEGIN_MODULE_GLOBALS(opentelemetry) char *conflicts; int disabled; // module disabled? (eg due to conflicting extension loaded) int allow_stack_extension; + int attr_hooks_enabled; // attribute hooking enabled? + char *pre_handler_function_fqn; + char *post_handler_function_fqn; ZEND_END_MODULE_GLOBALS(opentelemetry) ZEND_EXTERN_MODULE_GLOBALS(opentelemetry) diff --git a/ext/tests/005.phpt b/ext/tests/005.phpt index b29febff..5e943188 100644 --- a/ext/tests/005.phpt +++ b/ext/tests/005.phpt @@ -13,7 +13,7 @@ function helloWorld() { helloWorld(); ?> --EXPECTF-- -array(6) { +array(8) { [0]=> NULL [1]=> @@ -27,6 +27,12 @@ array(6) { string(%d) "%s%etests%e005.php" [5]=> int(4) + [6]=> + array(0) { + } + [7]=> + array(0) { + } } string(4) "CALL" array(8) { diff --git a/ext/tests/006.phpt b/ext/tests/006.phpt index 03ac8773..09d86530 100644 --- a/ext/tests/006.phpt +++ b/ext/tests/006.phpt @@ -13,7 +13,7 @@ function helloWorld(string $a) { helloWorld('a'); ?> --EXPECTF-- -array(6) { +array(8) { [0]=> NULL [1]=> @@ -29,6 +29,12 @@ array(6) { string(%d) "%s%etests%e006.php" [5]=> int(4) + [6]=> + array(0) { + } + [7]=> + array(0) { + } } array(8) { [0]=> diff --git a/ext/tests/mocks/SpanAttribute.php b/ext/tests/mocks/SpanAttribute.php new file mode 100644 index 00000000..48bea6e5 --- /dev/null +++ b/ext/tests/mocks/SpanAttribute.php @@ -0,0 +1,14 @@ += 8.1'); ?> +--EXTENSIONS-- +opentelemetry +--INI-- +opentelemetry.attr_hooks_enabled = On +--FILE-- +foo('one', 'two'); +?> +--EXPECT-- +string(3) "pre" +array(2) { + ["renamed_one_from_interface"]=> + string(3) "one" + ["renamed_two_from_class"]=> + string(3) "two" +} +string(3) "foo" +string(4) "post" \ No newline at end of file diff --git a/ext/tests/span_attribute/function_params.phpt b/ext/tests/span_attribute/function_params.phpt new file mode 100644 index 00000000..5bf72a07 --- /dev/null +++ b/ext/tests/span_attribute/function_params.phpt @@ -0,0 +1,49 @@ +--TEST-- +Check if function params can be passed via SpanAttribute +--SKIPIF-- += 8.1'); ?> +--EXTENSIONS-- +opentelemetry +--INI-- +opentelemetry.attr_hooks_enabled = On +--FILE-- + +--EXPECT-- +string(3) "pre" +array(5) { + ["renamed_one"]=> + string(3) "one" + ["two"]=> + int(99) + ["renamed_three"]=> + float(3.14159) + ["four"]=> + bool(true) + ["six"]=> + string(3) "six" +} +string(3) "foo" +string(4) "post" \ No newline at end of file diff --git a/ext/tests/span_attribute/function_params_non_simple.phpt b/ext/tests/span_attribute/function_params_non_simple.phpt new file mode 100644 index 00000000..1c09ce08 --- /dev/null +++ b/ext/tests/span_attribute/function_params_non_simple.phpt @@ -0,0 +1,55 @@ +--TEST-- +Check if function non-simple types can be passed as function params +--SKIPIF-- += 8.1'); ?> +--EXTENSIONS-- +opentelemetry +--INI-- +opentelemetry.attr_hooks_enabled = On +--FILE-- + 'bar'], + new \stdClass(), + function(){return 'fn';}, + null, +); +?> +--EXPECT-- +string(3) "pre" +array(4) { + ["one"]=> + array(1) { + ["foo"]=> + string(3) "bar" + } + ["two"]=> + object(stdClass)#1 (0) { + } + ["three"]=> + object(Closure)#2 (0) { + } + ["four"]=> + NULL +} +string(3) "foo" +string(4) "post" \ No newline at end of file diff --git a/ext/tests/span_attribute/method_params.phpt b/ext/tests/span_attribute/method_params.phpt new file mode 100644 index 00000000..c68935d1 --- /dev/null +++ b/ext/tests/span_attribute/method_params.phpt @@ -0,0 +1,53 @@ +--TEST-- +Check if method params can be passed via SpanAttribute +--SKIPIF-- += 8.1'); ?> +--EXTENSIONS-- +opentelemetry +--INI-- +opentelemetry.attr_hooks_enabled = On +--FILE-- +foo('one', 99, 3.14159, true, 'five', 'six'); +?> +--EXPECT-- +string(3) "pre" +array(5) { + ["renamed_one"]=> + string(3) "one" + ["two"]=> + int(99) + ["renamed_three"]=> + float(3.14159) + ["four"]=> + bool(true) + ["six"]=> + string(3) "six" +} +string(3) "foo" +string(4) "post" \ No newline at end of file diff --git a/ext/tests/span_attribute/span_attribute_attribute_priority.phpt b/ext/tests/span_attribute/span_attribute_attribute_priority.phpt new file mode 100644 index 00000000..19c028a8 --- /dev/null +++ b/ext/tests/span_attribute/span_attribute_attribute_priority.phpt @@ -0,0 +1,42 @@ +--TEST-- +Check if attributes from SpanAttribute replace attributes with same name from WithSpan +--SKIPIF-- += 8.1'); ?> +--EXTENSIONS-- +opentelemetry +--INI-- +opentelemetry.attr_hooks_enabled = On +--FILE-- + 'one_from_withspan', 'two' => 'two_from_withspan'])] + function foo( + #[SpanAttribute] string $one, + ): void + { + var_dump('foo'); + } +} + +$c = new TestClass(); +$c->foo('one'); +?> +--EXPECT-- +string(3) "pre" +array(2) { + ["two"]=> + string(17) "two_from_withspan" + ["one"]=> + string(3) "one" +} +string(3) "foo" +string(4) "post" \ No newline at end of file diff --git a/ext/tests/with_span/attribute_named_params_passed_to_pre_hook.phpt b/ext/tests/with_span/attribute_named_params_passed_to_pre_hook.phpt new file mode 100644 index 00000000..0d66cf88 --- /dev/null +++ b/ext/tests/with_span/attribute_named_params_passed_to_pre_hook.phpt @@ -0,0 +1,46 @@ +--TEST-- +Check if named attribute parameters are passed to pre hook +--SKIPIF-- += 8.1'); ?> +--EXTENSIONS-- +opentelemetry +--INI-- +opentelemetry.attr_hooks_enabled = On +--FILE-- +foo(); +?> +--EXPECT-- +array(1) { + ["span_kind"]=> + int(3) +} +string(3) "foo" +string(4) "post" \ No newline at end of file diff --git a/ext/tests/with_span/attribute_on_function.phpt b/ext/tests/with_span/attribute_on_function.phpt new file mode 100644 index 00000000..41434501 --- /dev/null +++ b/ext/tests/with_span/attribute_on_function.phpt @@ -0,0 +1,32 @@ +--TEST-- +Check if custom attribute loaded +--SKIPIF-- += 8.1'); ?> +--EXTENSIONS-- +opentelemetry +--INI-- +opentelemetry.attr_hooks_enabled = On +--FILE-- +getAttributes()[0]->getName() == WithSpan::class); + +otel_attr_test(); +?> +--EXPECT-- +bool(true) +string(3) "pre" +string(4) "test" +string(4) "post" \ No newline at end of file diff --git a/ext/tests/with_span/attribute_on_interface.phpt b/ext/tests/with_span/attribute_on_interface.phpt new file mode 100644 index 00000000..9309ffd3 --- /dev/null +++ b/ext/tests/with_span/attribute_on_interface.phpt @@ -0,0 +1,51 @@ +--TEST-- +Check if custom attribute can be applied to an interface +--SKIPIF-- += 8.1'); ?> +--EXTENSIONS-- +opentelemetry +--INI-- +opentelemetry.attr_hooks_enabled = On +--FILE-- +sayFoo(); +$c->sayBar(); +?> +--EXPECT-- +string(3) "pre" +string(3) "foo" +string(4) "post" +string(3) "pre" +string(3) "bar" +string(4) "post" \ No newline at end of file diff --git a/ext/tests/with_span/attribute_on_interface_with_params.phpt b/ext/tests/with_span/attribute_on_interface_with_params.phpt new file mode 100644 index 00000000..b307c069 --- /dev/null +++ b/ext/tests/with_span/attribute_on_interface_with_params.phpt @@ -0,0 +1,59 @@ +--TEST-- +Check if WithSpan can be applied to an interface with attribute args +--SKIPIF-- += 8.1'); ?> +--EXTENSIONS-- +opentelemetry +--INI-- +opentelemetry.attr_hooks_enabled = On +--FILE-- + 'bar'])] + function sayFoo(): void; +} + +class TestClass implements TestInterface +{ + function sayFoo(): void + { + var_dump('foo'); + } +} + +(new TestClass())->sayFoo(); +?> +--EXPECT-- +string(3) "pre" +array(2) { + ["name"]=> + string(3) "one" + ["span_kind"]=> + int(99) +} +array(1) { + ["foo"]=> + string(3) "bar" +} +string(3) "foo" +string(4) "post" \ No newline at end of file diff --git a/ext/tests/with_span/attribute_on_method.phpt b/ext/tests/with_span/attribute_on_method.phpt new file mode 100644 index 00000000..024a08cd --- /dev/null +++ b/ext/tests/with_span/attribute_on_method.phpt @@ -0,0 +1,36 @@ +--TEST-- +Check if WithSpan can be applied to a method +--SKIPIF-- += 8.1'); ?> +--EXTENSIONS-- +opentelemetry +--INI-- +opentelemetry.attr_hooks_enabled = On +--FILE-- +getAttributes()[0]->getName() == WithSpan::class); + +$c = new TestClass(); +$c->sayFoo(); +?> +--EXPECT-- +bool(true) +string(3) "pre" +string(3) "foo" +string(4) "post" \ No newline at end of file diff --git a/ext/tests/with_span/attribute_params_passed_to_pre_hook.phpt b/ext/tests/with_span/attribute_params_passed_to_pre_hook.phpt new file mode 100644 index 00000000..6255a971 --- /dev/null +++ b/ext/tests/with_span/attribute_params_passed_to_pre_hook.phpt @@ -0,0 +1,55 @@ +--TEST-- +Check if WithSpan parameters are passed to pre hook +--SKIPIF-- += 8.1'); ?> +--EXTENSIONS-- +opentelemetry +--INI-- +opentelemetry.attr_hooks_enabled = On +--FILE-- + 'value1', 'attr2' => 3.14])] + function foo(): void + { + var_dump('foo'); + } +} + +(new Foo())->foo(); +?> +--EXPECT-- +array(2) { + ["name"]=> + string(6) "param1" + ["span_kind"]=> + int(99) +} +array(2) { + ["attr1"]=> + string(6) "value1" + ["attr2"]=> + float(3.14) +} +string(3) "foo" +string(4) "post" \ No newline at end of file diff --git a/ext/tests/with_span/attribute_params_skipped_if_hooked.phpt b/ext/tests/with_span/attribute_params_skipped_if_hooked.phpt new file mode 100644 index 00000000..d184e87e --- /dev/null +++ b/ext/tests/with_span/attribute_params_skipped_if_hooked.phpt @@ -0,0 +1,46 @@ +--TEST-- +Check if hooking a method takes priority over WithSpan +--SKIPIF-- += 8.1'); ?> +--DESCRIPTION-- +Attribute-based hooks are only applied if no other hooks are registered on a function or method. +--EXTENSIONS-- +opentelemetry +--INI-- +opentelemetry.attr_hooks_enabled = On +--FILE-- +foo(); +?> +--EXPECT-- +string(3) "pre" +string(3) "foo" +string(4) "post" \ No newline at end of file diff --git a/ext/tests/with_span/customize_handlers.phpt b/ext/tests/with_span/customize_handlers.phpt new file mode 100644 index 00000000..a27291eb --- /dev/null +++ b/ext/tests/with_span/customize_handlers.phpt @@ -0,0 +1,41 @@ +--TEST-- +Check if WithSpan handlers can be changed via config +--SKIPIF-- += 8.1'); ?> +--EXTENSIONS-- +opentelemetry +--INI-- +opentelemetry.attr_hooks_enabled = On +opentelemetry.attr_pre_handler_function = custom_pre +opentelemetry.attr_post_handler_function = custom_post +--FILE-- + +--EXPECT-- +string(10) "custom_pre" +string(11) "custom_post" +string(18) "custom_pre handler" +string(4) "test" +string(19) "custom_post handler" \ No newline at end of file diff --git a/ext/tests/with_span/disable.phpt b/ext/tests/with_span/disable.phpt new file mode 100644 index 00000000..6f56f4fd --- /dev/null +++ b/ext/tests/with_span/disable.phpt @@ -0,0 +1,39 @@ +--TEST-- +Check if attribute hooks can be disabled by config +--SKIPIF-- += 8.1'); ?> +--EXTENSIONS-- +opentelemetry +--INI-- +opentelemetry.attr_hooks_enabled = Off +--FILE-- + +--EXPECTF-- +Warning: %s: OpenTelemetry: WithSpan attribute found but attribute hooks disabled in Unknown on line %d +string(4) "test" diff --git a/ext/tests/with_span/invalid_callback.phpt b/ext/tests/with_span/invalid_callback.phpt new file mode 100644 index 00000000..ba9ff717 --- /dev/null +++ b/ext/tests/with_span/invalid_callback.phpt @@ -0,0 +1,26 @@ +--TEST-- +Invalid callback is ignored +--EXTENSIONS-- +opentelemetry +--INI-- +opentelemetry.attr_hooks_enabled = On +opentelemetry.attr_pre_handler_function = "Invalid::pre" +opentelemetry.attr_post_handler_function = "Also\Invalid::post" +--FILE-- + +--EXPECT-- +string(12) "Invalid::pre" +string(18) "Also\Invalid::post" +string(4) "test" \ No newline at end of file