diff --git a/Makefile b/Makefile index 23b0eae36f..a617bb2e0d 100644 --- a/Makefile +++ b/Makefile @@ -1244,7 +1244,7 @@ test_opentelemetry_beta: _test_opentelemetry_beta_setup tests/Frameworks/Custom/ $(call run_opentelemetry_tests) _test_opentelemetry_1_setup: global_test_run_dependencies - $(call setup_opentelemetry,tests/OpenTelemetry/composer-1.json) + $(call setup_opentelemetry,tests/OpenTelemetry/composer-1$(shell [ $(PHP_MAJOR_MINOR) -le 81 ] && echo "-pre-8.1" || echo '').json) test_opentelemetry_1: _test_opentelemetry_1_setup tests/Frameworks/Custom/OpenTelemetry/composer.lock-php$(PHP_MAJOR_MINOR) tests/OpenTelemetry/composer.lock-php$(PHP_MAJOR_MINOR) $(call run_opentelemetry_tests) diff --git a/config.m4 b/config.m4 index 241f81657b..c4b7a1f39c 100644 --- a/config.m4 +++ b/config.m4 @@ -137,6 +137,7 @@ if test "$PHP_DDTRACE" != "no"; then EXTRA_PHP_SOURCES="\ ext/handlers_curl.c \ ext/hook/uhook_attributes.c \ + ext/hook/uhook_otel.c \ " ZAI_RESOLVER_SUFFIX="" diff --git a/config.w32 b/config.w32 index dc151bd508..c4fcb9e606 100644 --- a/config.w32 +++ b/config.w32 @@ -67,7 +67,7 @@ if (PHP_DDTRACE != 'no') { var DDTRACE_HOOK_SOURCES = "uhook.c uhook_legacy.c"; if (version >= 800) { - DDTRACE_HOOK_SOURCES += " uhook_attributes.c"; + DDTRACE_HOOK_SOURCES += " uhook_attributes.c uhook_otel.c"; } var zai_dirname = configure_module_dirname + "/zend_abstract_interface"; diff --git a/ext/ddtrace.c b/ext/ddtrace.c index bf583eff36..389bd62a7a 100644 --- a/ext/ddtrace.c +++ b/ext/ddtrace.c @@ -2510,6 +2510,12 @@ PHP_FUNCTION(dd_trace_internal_fn) { ddog_CharSlice path = dd_zend_string_to_CharSlice(Z_STR_P(ZVAL_VARARG_PARAM(params, 0))); ddtrace_detect_composer_installed_json(&ddtrace_sidecar, ddtrace_sidecar_instance_id, &DDTRACE_G(sidecar_queue_id), path); RETVAL_TRUE; + } else if (params_count == 2 && FUNCTION_NAME_MATCHES("mark_integration_loaded")) { + zval *name = ZVAL_VARARG_PARAM(params, 0); + zval *version = ZVAL_VARARG_PARAM(params, 1); + if (Z_TYPE_P(name) == IS_STRING && Z_TYPE_P(version) == IS_STRING) { + ddtrace_telemetry_notify_integration_version(Z_STRVAL_P(name), Z_STRLEN_P(name), Z_STRVAL_P(version), Z_STRLEN_P(version)); + } } else if (FUNCTION_NAME_MATCHES("dump_sidecar")) { if (!ddtrace_sidecar) { RETURN_FALSE; @@ -3313,8 +3319,11 @@ PHP_FUNCTION(DDTrace_curl_multi_exec_get_request_spans) { RETURN_NULL(); } -static const zend_module_dep ddtrace_module_deps[] = {ZEND_MOD_REQUIRED("json") ZEND_MOD_REQUIRED("standard") - ZEND_MOD_END}; +static const zend_module_dep ddtrace_module_deps[] = { + ZEND_MOD_REQUIRED("json") + ZEND_MOD_REQUIRED("standard") + ZEND_MOD_OPTIONAL("openetelemetry") // make sure we load after otel to insert the hook function if it doesn't exist yet + ZEND_MOD_END}; zend_module_entry ddtrace_module_entry = {STANDARD_MODULE_HEADER_EX, NULL, ddtrace_module_deps, PHP_DDTRACE_EXTNAME, diff --git a/ext/hook/uhook.c b/ext/hook/uhook.c index e186583ecc..2fafdc5f11 100644 --- a/ext/hook/uhook.c +++ b/ext/hook/uhook.c @@ -1103,6 +1103,7 @@ static void dd_uhook_closure_free_wrapper(zend_object *object) { #if PHP_VERSION_ID >= 80000 void zai_uhook_attributes_minit(void); +void dd_register_opentelemetry_wrapper(void); #endif void zai_uhook_minit(int module_number) { ddtrace_hook_data_ce = register_class_DDTrace_HookData(); @@ -1116,6 +1117,7 @@ void zai_uhook_minit(int module_number) { #if PHP_VERSION_ID >= 80000 zai_uhook_attributes_minit(); + dd_register_opentelemetry_wrapper(); #endif // get hold of a Closure object to access handlers diff --git a/ext/hook/uhook_otel.c b/ext/hook/uhook_otel.c new file mode 100644 index 0000000000..57ce16104e --- /dev/null +++ b/ext/hook/uhook_otel.c @@ -0,0 +1,282 @@ +#include +#include +#include +#include "uhook.h" +#include "../configuration.h" +#include "../span.h" +#include + +#include + +typedef struct { + zend_object *begin; + zend_object *end; +} dd_uhook_def; + +void dd_otel_call(zend_execute_data *execute_data, zval *retval, zend_object *closure, zval *rv) { + zval args[8]; + + int offset = retval ? 2 : 0; + if (EX(func)->op_array.scope) { + if (EX(func)->op_array.fn_flags & ZEND_ACC_STATIC) { + ZVAL_STR(&args[0], zend_get_called_scope(execute_data)->name); + } else { + ZVAL_COPY_VALUE(&args[0], &EX(This)); + } + ZVAL_STR(&args[2 + offset], EX(func)->op_array.scope->name); + } else { + ZVAL_NULL(&args[0]); + ZVAL_NULL(&args[2 + offset]); + } + ZVAL_ARR(&args[1], dd_uhook_collect_args(execute_data)); + ZVAL_STR(&args[3 + offset], EX(func)->op_array.function_name); + if (ZEND_USER_CODE(EX(func)->type)) { + ZVAL_STR(&args[4 + offset], EX(func)->op_array.filename); + ZVAL_LONG(&args[5 + offset], EX(func)->op_array.line_start); + } else { + ZVAL_NULL(&args[4 + offset]); + ZVAL_NULL(&args[5 + offset]); + } + if (!retval) { + ZVAL_EMPTY_ARRAY(&args[6]); + ZVAL_EMPTY_ARRAY(&args[7]); + } else { + ZVAL_COPY_VALUE(&args[2], retval); + if (EG(exception)) { + ZVAL_OBJ(&args[3], EG(exception)); + } else { + ZVAL_NULL(&args[3]); + } + } + + zend_fcall_info fci = empty_fcall_info; + zend_fcall_info_cache fcc = empty_fcall_info_cache; + + zval zv; + ZVAL_OBJ(&zv, closure); + ZVAL_UNDEF(rv); + + zend_fcall_info_init(&zv, 0, &fci, &fcc, NULL, NULL); + fci.retval = rv; + fci.params = args; + fci.param_count = 8; + + zai_sandbox sandbox; + zai_sandbox_open(&sandbox); + if (zend_call_function(&fci, &fcc) == SUCCESS || PG(last_error_message)) { + dd_uhook_report_sandbox_error(execute_data, closure); + } + zai_sandbox_close(&sandbox); + + zval_ptr_dtor(&args[1]); +} + +static bool dd_uhook_begin(zend_ulong invocation, zend_execute_data *execute_data, void *auxiliary, void *dynamic) { + UNUSED(invocation, dynamic); + dd_uhook_def *def = auxiliary; + + LOGEV(HOOK_TRACE, dd_uhook_log_invocation(log, execute_data, "begin", def->begin);); + + zval rv; + dd_otel_call(execute_data, NULL, def->begin, &rv); + + if (Z_TYPE(rv) == IS_ARRAY) { + zend_ulong arg_offset; + zend_string *strkey; + zval *val; + zend_function *fbc = EX(func); + + ZEND_HASH_FOREACH_KEY_VAL(Z_ARR(rv), arg_offset, strkey, val) { + bool found = false; + if (strkey) { + uint32_t num_args = fbc->common.num_args; + // As per zend_handle_named_arg() + if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) + || EXPECTED(fbc->common.fn_flags & ZEND_ACC_USER_ARG_INFO)) { + for (uint32_t i = 0; i < num_args; i++) { + zend_arg_info *arg_info = &fbc->op_array.arg_info[i]; + if (zend_string_equals(strkey, arg_info->name)) { + arg_offset = i; + found = true; + } + } + } else { + for (uint32_t i = 0; i < num_args; i++) { + zend_internal_arg_info *arg_info = &fbc->internal_function.arg_info[i]; + size_t len = strlen(arg_info->name); + if (zend_string_equals_cstr(strkey, arg_info->name, len)) { + arg_offset = i; + found = true; + } + } + } + } else { + found = arg_offset < fbc->common.num_args; + } + + uint32_t current_num_args = EX_NUM_ARGS(); + zval *arg = EX_VAR_NUM(arg_offset); + if (!found) { + if (!(fbc->common.fn_flags & ZEND_ACC_VARIADIC)) { + continue; // Unknown parameter + } + + if (strkey) { + /* Unknown named parameter that will be collected into a variadic. */ + if (!(EX_CALL_INFO() & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS)) { + ZEND_ADD_CALL_FLAG(execute_data, ZEND_CALL_HAS_EXTRA_NAMED_PARAMS); + EX(extra_named_params) = zend_new_array(0); + } + + Z_TRY_ADDREF_P(val); + zend_hash_update(EX(extra_named_params), strkey, val); + continue; + } else { + + if (arg_offset > current_num_args) { + if (!ZEND_USER_CODE(fbc->type)) { + if ((uint32_t)arg_offset - current_num_args > EG(vm_stack_end) - EX_VAR_NUM(0)) { + continue; // we won't mess with adding new frames + } +#if PHP_VERSION_ID >= 80200 + // temporaries like the last observed frame are already initialized. Move them. + memmove(EX_VAR_NUM(arg_offset), EX_VAR_NUM(EX_NUM_ARGS()), fbc->common.T * sizeof(zval *)); +#endif + for (uint32_t i = fbc->common.num_args; i < (uint32_t)arg_offset; ++i) { + ZVAL_UNDEF(EX_VAR_NUM(i)); + } + } else { + arg = EX_VAR_NUM(fbc->op_array.last_var + fbc->op_array.T - fbc->op_array.num_args + arg_offset); + for (zval *extra_arg = EX_VAR_NUM(fbc->op_array.last_var + fbc->op_array.T); extra_arg < arg; ++extra_arg) { + ZVAL_UNDEF(extra_arg); + } + } + } + } + } + if (arg_offset >= EX_NUM_ARGS()) { + EX_NUM_ARGS() = arg_offset + 1; + uint32_t num_extra_args = EX_NUM_ARGS() - current_num_args; + + if (num_extra_args > 1) { + for (uint32_t i = current_num_args; i < MIN(fbc->common.num_args, arg_offset); ++i) { + ZVAL_UNDEF(EX_VAR_NUM(i)); + } + ZEND_ADD_CALL_FLAG(execute_data, ZEND_CALL_MAY_HAVE_UNDEF); + } + } + zval_ptr_dtor(arg); + ZVAL_COPY(arg, val); + } ZEND_HASH_FOREACH_END(); + } + zval_ptr_dtor(&rv); + + return true; +} + +static void dd_uhook_end(zend_ulong invocation, zend_execute_data *execute_data, zval *retval, void *auxiliary, void *dynamic) { + UNUSED(invocation, dynamic); + dd_uhook_def *def = auxiliary; + + LOGEV(HOOK_TRACE, dd_uhook_log_invocation(log, execute_data, "end", def->end);); + + zval rv; + dd_otel_call(execute_data, retval, def->end, &rv); + + if (!Z_ISUNDEF(rv)) { + const zend_function *func = zend_get_closure_method_def(def->end); + if (func->common.fn_flags & ZEND_ACC_HAS_RETURN_TYPE && (ZEND_TYPE_PURE_MASK(func->common.arg_info[-1].type) & IS_VOID) == 0) { + zval_ptr_dtor(retval); + ZVAL_COPY_VALUE(retval, &rv); + } else { + zval_ptr_dtor(&rv); + } + } +} + +static void dd_uhook_dtor(void *data) { + dd_uhook_def *def = data; + if (def->begin) { + OBJ_RELEASE(def->begin); + } + if (def->end) { + OBJ_RELEASE(def->end); + } + efree(def); +} + +PHP_FUNCTION(DDTrace_OpenTelemetry_Instrumentation_hook) { + zend_string *class_name, *function_name; + zval *pre = NULL, *post = NULL; + + ZEND_PARSE_PARAMETERS_START(2, 4) + Z_PARAM_STR_OR_NULL(class_name) + Z_PARAM_STR(function_name) + Z_PARAM_OPTIONAL + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(pre, zend_ce_closure) + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(post, zend_ce_closure) + ZEND_PARSE_PARAMETERS_END(); + + dd_uhook_def *def = emalloc(sizeof(*def)); + def->begin = pre ? Z_OBJ_P(pre) : NULL; + if (def->begin) { + GC_ADDREF(def->begin); + } + def->end = post ? Z_OBJ_P(post) : NULL; + if (def->end) { + GC_ADDREF(def->end); + } + + zai_str class_str = ZAI_STR_EMPTY; + if (class_name) { + class_str = (zai_str)ZAI_STR_FROM_ZSTR(class_name); + } + zai_str func_str = ZAI_STR_FROM_ZSTR(function_name); + + + bool success = zai_hook_install_generator(class_str, func_str, + def->begin ? dd_uhook_begin : NULL, NULL, NULL, def->end ? dd_uhook_end : NULL, + ZAI_HOOK_AUX(def, dd_uhook_dtor), 0) != -1; + + if (!success) { + dd_uhook_dtor(def); + } else { + LOG(HOOK_TRACE, "Installing an otel hook function at %s:%d on %s %s%s%s", + zend_get_executed_filename(), zend_get_executed_lineno(), + class_name ? "method" : "function", + class_name ? ZSTR_VAL(class_name) : "", + class_name ? "::" : "", + ZSTR_VAL(function_name)); + } + RETURN_BOOL(success); +} + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_OpenTelemetry_Instrumentation_hook, 0, 2, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, class, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, function, IS_STRING, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, pre, Closure, 1, "null") + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, post, Closure, 1, "null") +ZEND_END_ARG_INFO() + +static zend_function_entry dd_otel_functions[] = { + ZEND_NS_FALIAS("OpenTelemetry\\Instrumentation", hook, DDTrace_OpenTelemetry_Instrumentation_hook, arginfo_OpenTelemetry_Instrumentation_hook) + ZEND_FE_END +}; + +zif_handler dd_extension_loaded; +ZEND_FUNCTION(DDTrace_extension_loaded) { + dd_extension_loaded(INTERNAL_FUNCTION_PARAM_PASSTHRU); + if (Z_TYPE_P(return_value) == IS_FALSE && EX_NUM_ARGS() > 0 && Z_TYPE_P(EX_VAR_NUM(0)) == IS_STRING && zend_string_equals_cstr(Z_STR_P(EX_VAR_NUM(0)), ZEND_STRL("opentelemetry"))) { + RETURN_TRUE; + } +} + +void dd_register_opentelemetry_wrapper(void) { + if (!zend_hash_str_find_ptr_lc(CG(function_table), dd_otel_functions->fname, strlen(dd_otel_functions->fname))) { + zend_register_functions(NULL, dd_otel_functions, NULL, MODULE_PERSISTENT); + + zend_internal_function *extension_loaded = zend_hash_str_find_ptr(CG(function_table), ZEND_STRL("extension_loaded")); + dd_extension_loaded = extension_loaded->handler; + extension_loaded->handler = ZEND_FN(DDTrace_extension_loaded); + } +} diff --git a/ext/telemetry.c b/ext/telemetry.c index 75f5be5b09..5e0fec18a0 100644 --- a/ext/telemetry.c +++ b/ext/telemetry.c @@ -185,10 +185,14 @@ void ddtrace_telemetry_finalize(void) { } void ddtrace_telemetry_notify_integration(const char *name, size_t name_len) { + ddtrace_telemetry_notify_integration_version(name, name_len, "", 0); +} + +void ddtrace_telemetry_notify_integration_version(const char *name, size_t name_len, const char *version, size_t version_len) { if (ddtrace_sidecar && get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED()) { ddog_CharSlice integration = (ddog_CharSlice) {.len = name_len, .ptr = name}; - ddog_sidecar_telemetry_addIntegration(&ddtrace_sidecar, ddtrace_sidecar_instance_id, &DDTRACE_G(sidecar_queue_id), integration, - DDOG_CHARSLICE_C(""), true); + ddog_CharSlice ver = (ddog_CharSlice) {.len = version_len, .ptr = version}; + ddog_sidecar_telemetry_addIntegration(&ddtrace_sidecar, ddtrace_sidecar_instance_id, &DDTRACE_G(sidecar_queue_id), integration, ver, true); } } diff --git a/ext/telemetry.h b/ext/telemetry.h index 78c97d12dd..6f4111f70b 100644 --- a/ext/telemetry.h +++ b/ext/telemetry.h @@ -26,6 +26,7 @@ void ddtrace_telemetry_rinit(void); void ddtrace_telemetry_rshutdown(void); ddog_TelemetryWorkerHandle *ddtrace_build_telemetry_handle(void); void ddtrace_telemetry_notify_integration(const char *name, size_t name_len); +void ddtrace_telemetry_notify_integration_version(const char *name, size_t name_len, const char *version, size_t version_len); void ddtrace_telemetry_finalize(void); void ddtrace_telemetry_register_services(ddog_SidecarTransport *sidecar); void ddtrace_telemetry_inc_spans_created(ddtrace_span_data *span); diff --git a/libdatadog b/libdatadog index aa33143a3b..d491cea9e9 160000 --- a/libdatadog +++ b/libdatadog @@ -1 +1 @@ -Subproject commit aa33143a3b8318a75252c8a0a0a9cd38e3ed6c81 +Subproject commit d491cea9e94e67fd57c6bdb3e1263e355b7b4dc8 diff --git a/src/DDTrace/OpenTelemetry/CachedInstrumentation.php b/src/DDTrace/OpenTelemetry/CachedInstrumentation.php new file mode 100644 index 0000000000..3f767aa4ee --- /dev/null +++ b/src/DDTrace/OpenTelemetry/CachedInstrumentation.php @@ -0,0 +1,58 @@ +name, $this->version); +}); + +\DDTrace\install_hook('OpenTelemetry\API\Instrumentation\CachedInstrumentation::tracer', null, function (\DDTrace\HookData $hook) { + $tracer = $hook->returned; + + $name = $this->name; + if (strpos($name, "io.opentelemetry.contrib.php.") === 0) { + $name = substr($name, strlen("io.opentelemetry.contrib.php.")); + } + $name = "otel.$name"; + + $hook->overrideReturnValue(new class($tracer, $name) implements \OpenTelemetry\API\Trace\TracerInterface { + public $tracer; + public $name; + + public function __construct($tracer, $name) + { + $this->tracer = $tracer; + $this->name = $name; + } + + public function spanBuilder(string $spanName): \OpenTelemetry\API\Trace\SpanBuilderInterface + { + $spanBuilder = $this->tracer->spanBuilder($spanName); + //print new \Exception; + $spanBuilder->setAttribute("component", $this->name); + return $spanBuilder; + } + + public function isEnabled(): bool + { + return $this->tracer->isEnabled(); + } + + public function getInstrumentationScope(): InstrumentationScopeInterface + { + if (!method_exists($this->tracer, 'getInstrumentationScope')) { + throw new \Error("There is no getInstrumentationScope method available for " . get_class($this->tracer)); + } + return $this->tracer->getInstrumentationScope(); + } + + public function updateConfig(Configurator $configurator): void + { + $this->tracer->updateConfig($configurator); + } + }); +}); diff --git a/src/bridge/_files_opentelemetry.php b/src/bridge/_files_opentelemetry.php index 58e0945a18..e5e2a91d70 100644 --- a/src/bridge/_files_opentelemetry.php +++ b/src/bridge/_files_opentelemetry.php @@ -6,4 +6,5 @@ __DIR__ . '/../DDTrace/OpenTelemetry/SpanContext.php', __DIR__ . '/../DDTrace/OpenTelemetry/Span.php', __DIR__ . '/../DDTrace/OpenTelemetry/SpanBuilder.php', + __DIR__ . '/../DDTrace/OpenTelemetry/CachedInstrumentation.php', ]; diff --git a/tests/Frameworks/Custom/OpenTelemetry/composer.json b/tests/Frameworks/Custom/OpenTelemetry/composer.json index c5811adc60..adcb6bd49f 100644 --- a/tests/Frameworks/Custom/OpenTelemetry/composer.json +++ b/tests/Frameworks/Custom/OpenTelemetry/composer.json @@ -1,6 +1,8 @@ { "require": { "php": ">=7.4", - "open-telemetry/sdk": "^1.0" + "open-telemetry/sdk": "^1.0", + "symfony/http-client": "*", + "nyholm/psr7": "*" } } diff --git a/tests/OpenTelemetry/Integration/SpanProvenanceTest.php b/tests/OpenTelemetry/Integration/SpanProvenanceTest.php new file mode 100644 index 0000000000..85c14fa032 --- /dev/null +++ b/tests/OpenTelemetry/Integration/SpanProvenanceTest.php @@ -0,0 +1,31 @@ +isolateTracer(function () { + $context = Configurator::create() + ->withTracerProvider(new TracerProvider) + ->storeInContext(); + Context::storage()->attach($context); + + file_get_contents("/etc/passwd"); + }); + $this->assertEquals($traces[0][0]["resource"], "file_get_contents"); + $this->assertEquals($traces[0][0]["name"], "internal"); + $this->assertEquals($traces[0][0]["meta"]["component"], "otel.io"); + } +} diff --git a/tests/OpenTelemetry/composer-1-pre-8.1.json b/tests/OpenTelemetry/composer-1-pre-8.1.json new file mode 100644 index 0000000000..f968bafcf6 --- /dev/null +++ b/tests/OpenTelemetry/composer-1-pre-8.1.json @@ -0,0 +1,9 @@ +{ + "name": "datadog/dd-trace-tests", + "require": { + "open-telemetry/sdk": "1.0.*||1.1.*", + "open-telemetry/extension-propagator-b3": "1.0.*||1.1.*", + "open-telemetry/opentelemetry-logger-monolog": "1.0.*||1.1.*" + }, + "minimum-stability": "stable" +} diff --git a/tests/OpenTelemetry/composer-1.json b/tests/OpenTelemetry/composer-1.json index f968bafcf6..02c4a86374 100644 --- a/tests/OpenTelemetry/composer-1.json +++ b/tests/OpenTelemetry/composer-1.json @@ -3,7 +3,13 @@ "require": { "open-telemetry/sdk": "1.0.*||1.1.*", "open-telemetry/extension-propagator-b3": "1.0.*||1.1.*", - "open-telemetry/opentelemetry-logger-monolog": "1.0.*||1.1.*" + "open-telemetry/opentelemetry-logger-monolog": "1.0.*||1.1.*", + "open-telemetry/opentelemetry-auto-io": "^0.0.12" }, - "minimum-stability": "stable" + "minimum-stability": "stable", + "config": { + "platform": { + "ext-opentelemetry": "1" + } + } } diff --git a/tests/phpunit.xml b/tests/phpunit.xml index c44f1f70f5..a8d6bc60c4 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -80,6 +80,7 @@ ./OpenTelemetry/Unit/Propagation ./OpenTelemetry/Integration/API ./OpenTelemetry/Integration/Context + ./OpenTelemetry/Integration/SpanProvenanceTest.php ./OpenTelemetry/Integration/Logs ./OpenTelemetry/Integration/SDK ./OpenTelemetry/Integration/InteroperabilityTest.php