Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHP 7.4でもOpenTelemetryゼロコード計装がしたい! / PHPerKaig...

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for Arthur Arthur
March 20, 2026

PHP 7.4でもOpenTelemetryゼロコード計装がしたい! / PHPerKaigi 2026

Avatar for Arthur

Arthur

March 20, 2026
Tweet

More Decks by Arthur

Other Decks in Programming

Transcript

  1. OpenTelemetryによる標準化 14 OpenTelemetryの エージェント OpenTelemetryの プロトコル name: system.cpu.time sum: dataPoints:

    - asInt: 12345678 OpenTelemetryの データ形式 収集先のバックエンドの違いによらず 同じものが使える オブザーバビリティ バックエンド
  2. 21

  3. 28 static void (*original_execute_ex)(zend_execute_data *execute_data) = NULL; static void my_excute_ex(zend_execute_data

    *execute_data) { // ここで前処理 original_execute_ex(execute_data); // ここで後処理 } PHP_MINIT_FUNCTION(my_ext) { original_execute_ex = zend_execute_ex; zend_execute_ex = my_execute_ex; return SUCCESS; }
  4. 29 static void (*original_execute_ex)(zend_execute_data *execute_data) = NULL; static void my_excute_ex(zend_execute_data

    *execute_data) { // ここで前処理 original_execute_ex(execute_data); // ここで後処理 } PHP_MINIT_FUNCTION(my_ext) { original_execute_ex = zend_execute_ex; zend_execute_ex = my_execute_ex; return SUCCESS; } 拡張モジュールロード時に PHP_MINIT_FUNCTION が呼ばれるイメージ(マクロ)
  5. 30 static void (*original_execute_ex)(zend_execute_data *execute_data) = NULL; static void my_excute_ex(zend_execute_data

    *execute_data) { // ここで前処理 original_execute_ex(execute_data); // ここで後処理 } PHP_MINIT_FUNCTION(my_ext) { original_execute_ex = zend_execute_ex; zend_execute_ex = my_execute_ex; return SUCCESS; } 元々のzend_execute_ex 関数ポインタを退避しておく
  6. 31 static void (*original_execute_ex)(zend_execute_data *execute_data) = NULL; static void my_excute_ex(zend_execute_data

    *execute_data) { // ここで前処理 original_execute_ex(execute_data); // ここで後処理 } PHP_MINIT_FUNCTION(my_ext) { original_execute_ex = zend_execute_ex; zend_execute_ex = my_execute_ex; return SUCCESS; } 元々のzend_execute_ex をwrapした関数を定義 上で定義した関数ポインタで zend_execute_exを上書き
  7. PHP 8: Observer API 先ほどの問題点を解決するため、PHP 8からZend EngineにObserver APIが導入された • スタック制限なし

    • PHP 8+のJIT下でもちゃんと動く • 特定の関数のみにフックを仕掛ける仕組みがある など、これまでの手法の多くの問題を解決している 33
  8. 34 static void my_observer_begin(zend_execute_data *execute_data) { // ここで前処理 } static

    void my_observer_end(zend_execute_data *execute_data, zval *return_value) { // ここで後処理 } static zend_observer_fcall_handlers my_observer_init(zend_execute_data *execute_data) { zend_observer_fcall_handlers handlers = {NULL, NULL}; if (execute_data->func->type == ZEND_USER_FUNCTION) { handlers.begin = my_observer_begin; handlers.end = my_observer_end; } return handlers; } PHP_MINIT_FUNCTION(my_ext) { zend_observer_fcall_register(my_observer_init); return SUCCESS; }
  9. 35 static void my_observer_begin(zend_execute_data *execute_data) { // ここで前処理 } static

    void my_observer_end(zend_execute_data *execute_data, zval *return_value) { // ここで後処理 } static zend_observer_fcall_handlers my_observer_init(zend_execute_data *execute_data) { zend_observer_fcall_handlers handlers = {NULL, NULL}; if (execute_data->func->type == ZEND_USER_FUNCTION) { handlers.begin = my_observer_begin; handlers.end = my_observer_end; } return handlers; } PHP_MINIT_FUNCTION(my_ext) { zend_observer_fcall_register(my_observer_init); return SUCCESS; }
  10. 39 static void my_observer_begin(zend_execute_data *execute_data) { // ここで前処理 } static

    void my_observer_end(zend_execute_data *execute_data, zval *return_value) { // ここで後処理 } static zend_observer_fcall_handlers my_observer_init(zend_execute_data *execute_data) { zend_observer_fcall_handlers handlers = {NULL, NULL}; if (execute_data->func->type == ZEND_USER_FUNCTION) { handlers.begin = my_observer_begin; handlers.end = my_observer_end; } return handlers; } PHP_MINIT_FUNCTION(my_ext) { zend_observer_fcall_register(my_observer_init); return SUCCESS; }
  11. 40 static zend_observer_fcall_handlers my_observer_init( zend_execute_data *execute_data ) { zend_observer_fcall_handlers handlers

    = {NULL, NULL}; if (execute_data->func->type == ZEND_USER_FUNCTION) { handlers.begin = my_observer_begin; handlers.end = my_observer_end; } return handlers; }
  12. 41 static zend_observer_fcall_handlers my_observer_init( zend_execute_data *execute_data ) { zend_observer_fcall_handlers handlers

    = {NULL, NULL}; if (execute_data->func->type == ZEND_USER_FUNCTION) { handlers.begin = my_observer_begin; handlers.end = my_observer_end; } return handlers; } 登録した関数オブザーバーの定義 (zend_observer_fcall_registerの引数)
  13. 42 static zend_observer_fcall_handlers my_observer_init( zend_execute_data *execute_data ) { zend_observer_fcall_handlers handlers

    = {NULL, NULL}; if (execute_data->func->type == ZEND_USER_FUNCTION) { handlers.begin = my_observer_begin; handlers.end = my_observer_end; } return handlers; } ユーザーランド関数ならフックハンドラを追加 (zend_execute_exに合わせた条件にした)
  14. 43 static zend_observer_fcall_handlers my_observer_init(zend_execute_data *execute_data) { zend_observer_fcall_handlers handlers = {NULL,

    NULL}; if (execute_data->func->type == ZEND_USER_FUNCTION) { handlers.begin = my_observer_begin; handlers.end = my_observer_end; } return handlers; } ユーザーランド関数でなければハンドラはなし
  15. 44 static void my_observer_begin(zend_execute_data *execute_data) { // ここで前処理 } static

    void my_observer_end(zend_execute_data *execute_data, zval *return_value) { // ここで後処理 } static zend_observer_fcall_handlers my_observer_init(zend_execute_data *execute_data) { zend_observer_fcall_handlers handlers = {NULL, NULL}; if (execute_data->func->type == ZEND_USER_FUNCTION) { handlers.begin = my_observer_begin; handlers.end = my_observer_end; } return handlers; } PHP_MINIT_FUNCTION(my_ext) { zend_observer_fcall_register(my_observer_init); return SUCCESS; }
  16. 45 static void my_observer_begin( zend_execute_data *execute_data ) { // ここで前処理

    } static void my_observer_end( zend_execute_data *execute_data, zval *return_value ) { // ここで後処理 }
  17. 46 static void my_observer_begin( zend_execute_data *execute_data ) { // ここで前処理

    } static void my_observer_end( zend_execute_data *execute_data, zval *return_value ) { // ここで後処理 } handlers.beginに設定した前処理のハンドラ zend_execute_exと同じシグネチャなので、 同等のことができる
  18. 47 static void my_observer_begin( zend_execute_data *execute_data ) { // ここで前処理

    } static void my_observer_end( zend_execute_data *execute_data, zval *return_value ) { // ここで後処理 } handlers.endに設定した後処理のハンドラ zend_execute_exと同じ引数 +関数の返却値が受け取れる
  19. 【補足】Observer APIの進化 PHP 8でObserver APIが導入された当初は、ユーザー ランドの関数だけにしかフックできなかった PHP 8.2からは内部関数にも対応 > Registered

    zend_observer_fcall_init handlers are now also called for internal functions. https://github.com/php/php-src/blob/php-8.2.0/UPGRADING.INTE RNALS#L54 48
  20. OpenTelemetry\Instrumentation\hook OpenTelemetry拡張は、任意の関数・メソッドにフックを仕掛 けられる関数 OpenTelemetry\Instrumentation\hook を提 供している。この関数により、Observer APIでフックさせたい 関数を拡張に登録する 50 OpenTelemetry\Instrumentation\hook(

    DemoClass::class, 'run', pre: static function (DemoClass $demo, array $params, string $class, string $function, ?string $filename, ?int $lineno) { … }, post: static function (DemoClass #demo, array $params, $returnValue, ?Throwable $exception) { … }, )
  21. 51 PHP_FUNCTION(OpenTelemetry_Instrumentation_hook) { zend_string *class_name; zend_string *function_name; zval *pre =

    NULL; zval *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(); RETURN_BOOL(add_observer(class_name, function_name, pre, post)); } https://github.com/open-telemetry/opentelemetry-php-instrumentation/blob/ad826b91 686dfd72fa01e2b3e360d563ab3b6343/ext/opentelemetry.c#L107-L122
  22. 52 PHP_FUNCTION(OpenTelemetry_Instrumentation_hook) { zend_string *class_name; zend_string *function_name; zval *pre =

    NULL; zval *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(); RETURN_BOOL(add_observer(class_name, function_name, pre, post)); } OpenTelemetry_Intrumentation_hook という名前の関数を定義 https://github.com/open-telemetry/opentelemetry-php-instrumentation/blob/ad826b91 686dfd72fa01e2b3e360d563ab3b6343/ext/opentelemetry.c#L107-L122
  23. 53 PHP_FUNCTION(OpenTelemetry_Instrumentation_hook) { zend_string *class_name; zend_string *function_name; zval *pre =

    NULL; zval *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(); RETURN_BOOL(add_observer(class_name, function_name, pre, post)); } hook()の引数定義とparse https://github.com/open-telemetry/opentelemetry-php-instrumentation/blob/ad826b91 686dfd72fa01e2b3e360d563ab3b6343/ext/opentelemetry.c#L107-L122
  24. 54 PHP_FUNCTION(OpenTelemetry_Instrumentation_hook) { zend_string *class_name; zend_string *function_name; zval *pre =

    NULL; zval *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(); RETURN_BOOL(add_observer(class_name, function_name, pre, post)); } add_observerが、ハッシュテーブルに フックの情報を保持しておく https://github.com/open-telemetry/opentelemetry-php-instrumentation/blob/ad826b91 686dfd72fa01e2b3e360d563ab3b6343/ext/opentelemetry.c#L107-L122
  25. 55 PHP_MINIT_FUNCTION(opentelemetry) { #if defined(ZTS) && defined(COMPILE_DL_OPENTELEMETRY) ZEND_TSRMLS_CACHE_UPDATE(); #endif REGISTER_INI_ENTRIES();

    check_conflicts(); if (!OTEL_G(disabled)) { opentelemetry_observer_init(INIT_FUNC_ARGS_PASSTHRU); } return SUCCESS; } https://github.com/open-telemetry/opentelemetry-php-instrumentation/blob/ad826b91 686dfd72fa01e2b3e360d563ab3b6343/ext/opentelemetry.c#L140-L154
  26. 56 PHP_MINIT_FUNCTION(opentelemetry) { #if defined(ZTS) && defined(COMPILE_DL_OPENTELEMETRY) ZEND_TSRMLS_CACHE_UPDATE(); #endif REGISTER_INI_ENTRIES();

    check_conflicts(); if (!OTEL_G(disabled)) { opentelemetry_observer_init(INIT_FUNC_ARGS_PASSTHRU); } return SUCCESS; } https://github.com/open-telemetry/opentelemetry-php-instrumentation/blob/ad826b91 686dfd72fa01e2b3e360d563ab3b6343/ext/opentelemetry.c#L140-L154 拡張モジュールロード時に PHP_MINIT_FUNCTION が呼ばれるイメージ(マクロ)
  27. 57 PHP_MINIT_FUNCTION(opentelemetry) { #if defined(ZTS) && defined(COMPILE_DL_OPENTELEMETRY) ZEND_TSRMLS_CACHE_UPDATE(); #endif REGISTER_INI_ENTRIES();

    check_conflicts(); if (!OTEL_G(disabled)) { opentelemetry_observer_init(INIT_FUNC_ARGS_PASSTHRU); } return SUCCESS; } https://github.com/open-telemetry/opentelemetry-php-instrumentation/blob/ad826b91 686dfd72fa01e2b3e360d563ab3b6343/ext/opentelemetry.c#L140-L154 オブザーバー初期化(フックの登録) をこの中でやっている
  28. 58 void opentelemetry_observer_init(INIT_FUNC_ARGS) { if (type != MODULE_TEMPORARY) { zend_observer_fcall_register(observer_fcall_init);

    op_array_extension = zend_get_op_array_extension_handle("opentelemetry"); #if PHP_VERSION_ID >= 80400 zend_get_internal_function_extension_handle("opentelemetry"); #endif } } https://github.com/open-telemetry/opentelemetry-php-instrumentation/blob/ad826b91 686dfd72fa01e2b3e360d563ab3b6343/ext/otel_observer.c#L1183-L1192
  29. 59 void opentelemetry_observer_init(INIT_FUNC_ARGS) { if (type != MODULE_TEMPORARY) { zend_observer_fcall_register(observer_fcall_init);

    op_array_extension = zend_get_op_array_extension_handle("opentelemetry"); #if PHP_VERSION_ID >= 80400 zend_get_internal_function_extension_handle("opentelemetry"); #endif } } https://github.com/open-telemetry/opentelemetry-php-instrumentation/blob/ad826b91 686dfd72fa01e2b3e360d563ab3b6343/ext/otel_observer.c#L1183-L1192 Observer APIの関数フックを登録する関数 observer_fcall_init内で、 OpenTelemetry\Intrumentation\hookにより 記録したフック情報をObserver APIの形式に変換
  30. 62 hook( \Illuminate\Database\Eloquent\Builder::class, 'find', pre: function ($builder, array $params, string

    $class, string $function, ?string $filename, ?int $lineno) { $model = $builder->getModel(); $builder = $this->instrumentation->tracer() ->spanBuilder($model::class . '::find') ->setSpanKind(SpanKind::KIND_INTERNAL) ->setAttribute(TraceAttributes::CODE_FUNCTION_NAME, sprintf('%s::%s', $class, $function)) …; $parent = Context::getCurrent(); $span = $builder->startSpan(); Context::storage()->attach($span->storeInContext($parent)); return $params; }, post: function ($builder, array $params, $result, ?Throwable $exception) { $this->endSpan($exception); } ) https://github.com/open-telemetry/opentelemetry-php-contrib/blob/2b6a02f67d85f7f9 4ebc285d2c08c3523d26e093/src/Instrumentation/Laravel/src/Hooks/Illuminate/Databas e/Eloquent/Model.php#L36-L60
  31. 63 hook( \Illuminate\Database\Eloquent\Builder::class, 'find', pre: function ($builder, array $params, string

    $class, string $function, ?string $filename, ?int $lineno) { // トレーススパンの開始 }, post: function ($builder, array $params, $result, ?Throwable $exception) { // トレーススパンの終了 } ) https://github.com/open-telemetry/opentelemetry-php-contrib/blob/2b6a02f67d85f7f9 4ebc285d2c08c3523d26e093/src/Instrumentation/Laravel/src/Hooks/Illuminate/Databas e/Eloquent/Model.php#L36-L60
  32. PHPゼロコード計装の仕組みまとめ • OpenTelemetry拡張モジュールは、任意の関数・メソッ ドに対してフックを仕掛けられる機能を提供する ◦ この仕組みの裏側には、PHP 8で導入されたZend Engineの Observer APIがあり、これまでのハックで起こる問題を解

    消している • ユーザーはフレームワーク・ライブラリごと用意された自 動計装ライブラリをインストールするだけで、テレメト リーを計装するフックをいい感じに登録してもらえる 64
  33. OBIは銀の弾丸ではない • PHPの通常のゼロコード計装と比較して、計装できる対象や情報 量がより限定的になる • Linux OSでない環境(BSD、Windowsなど)では使用できない • AWS FargateやGoogle

    Cloud Runなどのマネージドなコンテナ コンピューティング環境では使用できない • トレースが繋がらず断片的になることがある 69
  34. PHP 7.4でのゼロコード計装作戦 • ゼロコード計装ライブラリはrequire php ^8.0 ◦ よって、ゼロコード計装ライブラリは自分で作る必要が ある •

    フック登録にあたって、PHP 8+のObserver APIは使えな い ◦ よって、OpenTelemetry拡張は自分で作る必要がある ◦ zend_execute_exの仕組みで同じようなフック機構は実 現できるはず 72
  35. PHP 7.4でのゼロコード計装作戦 • OpenTelemetry SDKはrequire php ^8.1 ◦ ただし、過去のバージョン(v1.0.8)であればPHP 7.4

    にも対応している。今回はこれを使えば良さそう ◦ RectorというツールでPHPのダウングレードができるの で、最新のSDKのコードを変換するという手もある(そ れだけで済むならそうしたかった) 73
  36. 77

  37. 81 - use function OpenTelemetry\Instrumentation\hook; + use function OpenTelemetryPHP74\Instrumentation\hook; return

    hook( KernelContract::class, 'handle', - pre: function (...) { ⬛ }, + function (...) { ⬛ }, - post: function (...) { • } + function (...) { • } ) 書き換えは基本これだけ
  38. PHP 8+におけるゼロコード計装 • PHP 8ではZend EngineにObserver APIが追加され、関 数の呼び出しに対して、より堅牢な仕組みで割り込めるよ うになった •

    このおかげでPHP 8からは手軽に、そしてアプリケーショ ンの負荷やクラッシュリスクを抑えてOpenTelemetryの ゼロコード計装ができるようになった 86
  39. 参考文献 • Sammy Powers and Levi Morrison, “PHP 8: Observability

    baked right in,” Datadog, Feb 2, 2021. [Online]. Available: https://www.datadoghq.com/blog/engineering/php-8-observability-baked -right-in/ • Michihide Hotta, “PHP Extension 開発入門,” ネットワーク管理者(の卵)養成 講座, 2017. [Online]. Available: https://net-newbie.com/phpext/index.html • Benjamin Eberlei, “Hooks provided by PHP,” PHP Internal Books, Jul 30, 2019. [Online]. Available: https://www.phpinternalsbook.com/php7/extensions_design/hooks.html • OpenTelemetry Authors, “Documentation,” OpenTelemetry, 2019. [Online]. Avaibale: https://opentelemetry.io/docs/ 89