You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by jm...@apache.org on 2022/10/14 06:15:11 UTC

[skywalking-php] branch master updated: [Feature] Add Mysql Improved Extension (#18)

This is an automated email from the ASF dual-hosted git repository.

jmjoy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking-php.git


The following commit(s) were added to refs/heads/master by this push:
     new 3c5df9e  [Feature] Add Mysql Improved Extension (#18)
3c5df9e is described below

commit 3c5df9ea3f7800fdaa3cebb20ee9f00597e2de33
Author: 何延龙 <he...@gmail.com>
AuthorDate: Fri Oct 14 14:15:06 2022 +0800

    [Feature] Add Mysql Improved Extension (#18)
    
    Co-authored-by: jmjoy <jm...@apache.org>
---
 README.md                                          |   1 +
 .../service-agent/php-agent/Supported-list.md      |   1 +
 src/component.rs                                   |   1 +
 src/plugin/mod.rs                                  |   2 +
 src/plugin/plugin_mysqli.rs                        | 149 +++++++++++++++++++++
 tests/data/expected_context.yaml                   |  53 +++++++-
 tests/e2e.rs                                       |  18 ++-
 src/component.rs => tests/php/fpm/mysqli.php       |  27 +++-
 8 files changed, 240 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index e0c59ae..0144013 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,7 @@ SkyWalking PHP Agent requires SkyWalking 8.4+ and PHP 7.0+
 * PHP-FPM Ecosystem
   * [x] [cURL](https://www.php.net/manual/en/book.curl.php#book.curl)
   * [x] [PDO](https://www.php.net/manual/en/book.pdo.php)
+  * [x] [MySQL Improved](https://www.php.net/manual/en/book.mysqli.php)
   * [ ] [Memcached](https://www.php.net/manual/en/book.memcached.php)
   * [ ] [phpredis](https://github.com/phpredis/phpredis)
   * [ ] [php-amqp](https://github.com/php-amqp/php-amqp)
diff --git a/docs/en/setup/service-agent/php-agent/Supported-list.md b/docs/en/setup/service-agent/php-agent/Supported-list.md
index 0804886..d9d52ee 100644
--- a/docs/en/setup/service-agent/php-agent/Supported-list.md
+++ b/docs/en/setup/service-agent/php-agent/Supported-list.md
@@ -11,5 +11,6 @@ The following plugins provide the distributed tracing capability.
 
 * [cURL](https://www.php.net/manual/en/book.curl.php#book.curl)
 * [PDO](https://www.php.net/manual/en/book.pdo.php)
+* [MySQL Improved](https://www.php.net/manual/en/book.mysqli.php)
 
 ## Support PHP library
diff --git a/src/component.rs b/src/component.rs
index f6b0f8b..f51b122 100644
--- a/src/component.rs
+++ b/src/component.rs
@@ -20,3 +20,4 @@
 pub const COMPONENT_PHP_ID: i32 = 8001;
 pub const COMPONENT_PHP_CURL_ID: i32 = 8002;
 pub const COMPONENT_PHP_PDO_ID: i32 = 8003;
+pub const COMPONENT_PHP_MYSQLI_ID: i32 = 8004;
diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs
index cffafc9..badaf97 100644
--- a/src/plugin/mod.rs
+++ b/src/plugin/mod.rs
@@ -14,6 +14,7 @@
 // limitations under the License.
 
 mod plugin_curl;
+mod plugin_mysqli;
 mod plugin_pdo;
 mod plugin_swoole;
 
@@ -25,6 +26,7 @@ static PLUGINS: Lazy<Vec<Box<DynPlugin>>> = Lazy::new(|| {
     vec![
         Box::new(plugin_curl::CurlPlugin::default()),
         Box::new(plugin_pdo::PdoPlugin::default()),
+        Box::new(plugin_mysqli::MySQLImprovedPlugin::default()),
         Box::new(plugin_swoole::SwooleServerPlugin::default()),
         Box::new(plugin_swoole::SwooleHttpResponsePlugin::default()),
     ]
diff --git a/src/plugin/plugin_mysqli.rs b/src/plugin/plugin_mysqli.rs
new file mode 100644
index 0000000..b04883d
--- /dev/null
+++ b/src/plugin/plugin_mysqli.rs
@@ -0,0 +1,149 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use anyhow::Context;
+use dashmap::DashMap;
+use once_cell::sync::Lazy;
+use skywalking::{skywalking_proto::v3::SpanLayer, trace::span::Span};
+use tracing::debug;
+
+use crate::{
+    component::COMPONENT_PHP_MYSQLI_ID,
+    context::RequestContext,
+    execute::{get_this_mut, AfterExecuteHook, BeforeExecuteHook, Noop},
+};
+
+use super::Plugin;
+
+static MYSQL_MAP: Lazy<DashMap<u32, MySQLInfo>> = Lazy::new(Default::default);
+
+#[derive(Default, Clone)]
+pub struct MySQLImprovedPlugin;
+
+impl Plugin for MySQLImprovedPlugin {
+    fn class_names(&self) -> Option<&'static [&'static str]> {
+        Some(&["mysqli"])
+    }
+
+    fn function_name_prefix(&self) -> Option<&'static str> {
+        None
+    }
+
+    fn hook(
+        &self, class_name: Option<&str>, function_name: &str,
+    ) -> Option<(Box<BeforeExecuteHook>, Box<AfterExecuteHook>)> {
+        match (class_name, function_name) {
+            (Some("mysqli"), "__construct") => Some(self.hook_mysqli_construct()),
+            (Some("mysqli"), f) if ["query"].contains(&f) => {
+                Some(self.hook_mysqli_methods(function_name))
+            }
+            _ => None,
+        }
+    }
+}
+
+impl MySQLImprovedPlugin {
+    fn hook_mysqli_construct(&self) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
+        (
+            Box::new(|_, execute_data| {
+                let this = get_this_mut(execute_data)?;
+                let handle = this.handle();
+                let mut info: MySQLInfo = MySQLInfo {
+                    hostname: "127.0.0.1".to_string(),
+                    port: 3306,
+                };
+
+                let num_args = execute_data.num_args();
+                if num_args >= 1 {
+                    // host only
+                    let hostname = execute_data.get_parameter(0);
+                    let hostname = hostname
+                        .as_z_str()
+                        .context("hostname isn't str")?
+                        .to_str()?;
+                    debug!(hostname, "mysqli hostname");
+
+                    info.hostname = hostname.to_owned();
+                }
+                if num_args >= 5 {
+                    let port = execute_data.get_parameter(4);
+                    let port = port.as_long().context("port isn't str")?;
+                    debug!(port, "mysqli port");
+                    info.port = port
+                }
+
+                MYSQL_MAP.insert(handle, info);
+                Ok(Box::new(()))
+            }),
+            Noop::noop(),
+        )
+    }
+
+    fn hook_mysqli_methods(
+        &self, function_name: &str,
+    ) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
+        let function_name = function_name.to_owned();
+        (
+            Box::new(move |_, execute_data| {
+                let this = get_this_mut(execute_data)?;
+                let handle = this.handle();
+
+                debug!(handle, function_name, "call mysql method");
+
+                let mut span = with_info(handle, |info| {
+                    create_mysqli_exit_span("mysqli", &function_name, info)
+                })?;
+
+                if execute_data.num_args() >= 1 {
+                    if let Some(statement) = execute_data.get_parameter(0).as_z_str() {
+                        span.add_tag("db.statement", statement.to_str()?);
+                    }
+                }
+
+                Ok(Box::new(span) as _)
+            }),
+            Noop::noop(),
+        )
+    }
+}
+
+fn create_mysqli_exit_span(
+    class_name: &str, function_name: &str, info: &MySQLInfo,
+) -> anyhow::Result<Span> {
+    RequestContext::try_with_global_ctx(None, |ctx| {
+        let mut span = ctx.create_exit_span(
+            &format!("{}->{}", class_name, function_name),
+            &format!("{}:{}", info.hostname, info.port),
+        );
+        span.with_span_object_mut(|obj| {
+            obj.set_span_layer(SpanLayer::Database);
+            obj.component_id = COMPONENT_PHP_MYSQLI_ID;
+            obj.add_tag("db.type", "mysql");
+        });
+        Ok(span)
+    })
+}
+
+fn with_info<T>(handle: u32, f: impl FnOnce(&MySQLInfo) -> anyhow::Result<T>) -> anyhow::Result<T> {
+    MYSQL_MAP
+        .get(&handle)
+        .map(|r| f(r.value()))
+        .context("info not exists")?
+}
+
+struct MySQLInfo {
+    hostname: String,
+    port: i64,
+}
diff --git a/tests/data/expected_context.yaml b/tests/data/expected_context.yaml
index 2ca62ea..f012b67 100644
--- a/tests/data/expected_context.yaml
+++ b/tests/data/expected_context.yaml
@@ -15,7 +15,7 @@
 
 segmentItems:
   - serviceName: skywalking-agent-test-1
-    segmentSize: 7
+    segmentSize: 8
     segments:
       - segmentId: "not null"
         spans:
@@ -342,6 +342,57 @@ segmentItems:
               - { key: url, value: /pdo.php }
               - { key: http.method, value: GET }
               - { key: http.status_code, value: "200" }
+      - segmentId: 'not null'
+        spans:
+          - operationName: mysqli->query
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Database
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 8004
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:3306
+            skipAnalysis: false
+            tags:
+              - {key: db.type, value: mysql}
+              - {
+                  key: db.statement,
+                  value: "SELECT 1",
+                }
+          - operationName: mysqli->query
+            parentSpanId: 0
+            spanId: 2
+            spanLayer: Database
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 8004
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:3306
+            skipAnalysis: false
+            tags:
+              - {key: db.type, value: mysql}
+              - {
+                  key: db.statement,
+                  value: "SELECT * FROM `mysql`.`user` WHERE `User` = 'root'",
+                }
+          - operationName: GET:/mysqli.php
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 8001
+            isError: false
+            spanType: Entry
+            peer: ""
+            skipAnalysis: false
+            tags:
+              - {key: url, value: /mysqli.php}
+              - {key: http.method, value: GET}
+              - {key: http.status_code, value: '200'}
   - serviceName: skywalking-agent-test-2
     segmentSize: 1
     segments:
diff --git a/tests/e2e.rs b/tests/e2e.rs
index 47a6093..22a2c17 100644
--- a/tests/e2e.rs
+++ b/tests/e2e.rs
@@ -24,6 +24,7 @@ use std::{
     time::Duration,
 };
 use tokio::{fs::File, runtime::Handle, task, time::sleep};
+use tracing::info;
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
 async fn e2e() {
@@ -48,6 +49,7 @@ async fn e2e() {
 async fn run_e2e() {
     request_fpm_curl().await;
     request_fpm_pdo().await;
+    request_fpm_mysqli().await;
     request_swoole_curl().await;
     sleep(Duration::from_secs(3)).await;
     request_collector_validate().await;
@@ -69,6 +71,14 @@ async fn request_fpm_pdo() {
     .await;
 }
 
+async fn request_fpm_mysqli() {
+    request_common(
+        HTTP_CLIENT.get(format!("http://{}/mysqli.php", PROXY_SERVER_1_ADDRESS)),
+        "ok",
+    )
+    .await;
+}
+
 async fn request_swoole_curl() {
     request_common(
         HTTP_CLIENT.get(format!("http://{}/curl", SWOOLE_SERVER_1_ADDRESS)),
@@ -94,8 +104,8 @@ async fn request_collector_validate() {
 
 async fn request_common(request_builder: RequestBuilder, actual_content: impl Into<String>) {
     let response = request_builder.send().await.unwrap();
-    assert_eq!(
-        (response.status(), response.text().await.unwrap()),
-        (StatusCode::OK, actual_content.into())
-    );
+    let status = response.status();
+    let content = response.text().await.unwrap();
+    info!(content, "response content");
+    assert_eq!((status, content), (StatusCode::OK, actual_content.into()));
 }
diff --git a/src/component.rs b/tests/php/fpm/mysqli.php
similarity index 59%
copy from src/component.rs
copy to tests/php/fpm/mysqli.php
index f6b0f8b..de90a7f 100644
--- a/src/component.rs
+++ b/tests/php/fpm/mysqli.php
@@ -1,3 +1,5 @@
+<?php
+
 // Licensed to the Apache Software Foundation (ASF) under one or more
 // contributor license agreements.  See the NOTICE file distributed with
 // this work for additional information regarding copyright ownership.
@@ -11,12 +13,23 @@
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
-// limitations under the License..
+// limitations under the License.
+
+use Webmozart\Assert\Assert;
+
+require_once dirname(__DIR__) . "/vendor/autoload.php";
+
+{
+    $mysqli = new mysqli("127.0.0.1", "root", "password", "skywalking", 3306);
+    $result = $mysqli->query("SELECT 1");
+    Assert::notFalse($result);
+}
 
-//! Component ID
-//!
-//! <https://github.com/apache/skywalking/blob/014861535015745ae3f7b99acd7d14500b3b3927/oap-server/server-starter/src/main/resources/component-libraries.yml>
+{
+    $mysqli = new mysqli("127.0.0.1", "root", "password", "skywalking", 3306);
+    $result = $mysqli->query("SELECT * FROM `mysql`.`user` WHERE `User` = 'root'");
+    $rs = $result->fetch_all();
+    Assert::same(count($rs), 2);
+}
 
-pub const COMPONENT_PHP_ID: i32 = 8001;
-pub const COMPONENT_PHP_CURL_ID: i32 = 8002;
-pub const COMPONENT_PHP_PDO_ID: i32 = 8003;
+echo "ok";