[Feature] Add Mysql Improved Extension (#18)
Co-authored-by: jmjoy <jmjoy@apache.org>
diff --git a/README.md b/README.md
index e0c59ae..0144013 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,7 @@
* 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 @@
* [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 @@
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 @@
- { 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 @@
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 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 @@
.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_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/tests/php/fpm/mysqli.php b/tests/php/fpm/mysqli.php
new file mode 100644
index 0000000..de90a7f
--- /dev/null
+++ b/tests/php/fpm/mysqli.php
@@ -0,0 +1,35 @@
+<?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.
+// 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 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);
+}
+
+{
+ $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);
+}
+
+echo "ok";