Add mongodb pluhgin. (#83)
* Add mongodb pluhgin.
* Update documents.
* Add integration tests.
* Fix CI.
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 3136a71..5fd122f 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -106,7 +106,7 @@
bcmath, calendar, ctype, dom, exif, gettext, iconv, intl, json, mbstring,
mysqli, mysqlnd, opcache, pdo, pdo_mysql, phar, posix, readline, redis,
memcached, swoole-${{ matrix.version.swoole }}, xml, xmlreader, xmlwriter,
- yaml, zip
+ yaml, zip, mongodb
- name: Setup php-fpm for Linux
if: matrix.os == 'ubuntu-20.04'
diff --git a/README.md b/README.md
index a3b339a..651bb8d 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@
* [ ] [php-rdkafka](https://github.com/arnaud-lb/php-rdkafka)
* [x] [predis](https://github.com/predis/predis)
* [x] [php-amqplib](https://github.com/php-amqplib/php-amqplib) for Message Queuing Producer
+ * [x] [MongoDB](https://www.php.net/manual/en/set.mongodb.php)
* Swoole Ecosystem
diff --git a/Vagrantfile b/Vagrantfile
index e826de0..b58e0aa 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -24,6 +24,7 @@
config.vm.network "forwarded_port", guest: 6379, host: 6379
config.vm.network "forwarded_port", guest: 11211, host: 11211
config.vm.network "forwarded_port", guest: 5672, host: 5672
+ config.vm.network "forwarded_port", guest: 27017, host: 27017
config.vm.synced_folder ".", "/vagrant"
diff --git a/docker-compose.yml b/docker-compose.yml
index 825d64a..3cc802f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -51,3 +51,11 @@
environment:
- RABBITMQ_DEFAULT_USER=guest
- RABBITMQ_DEFAULT_PASS=guest
+
+ mongo:
+ image: mongo:4.4.10
+ ports:
+ - "27017:27017"
+ environment:
+ MONGO_INITDB_ROOT_USERNAME: root
+ MONGO_INITDB_ROOT_PASSWORD: example
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 1f99cce..9ad8476 100644
--- a/docs/en/setup/service-agent/php-agent/Supported-list.md
+++ b/docs/en/setup/service-agent/php-agent/Supported-list.md
@@ -14,8 +14,9 @@
* [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-amqplib](https://github.com/php-amqplib/php-amqplib) for Message Queuing Producer
+* [MongoDB](https://www.php.net/manual/en/set.mongodb.php)
## Supported PHP library
* [predis](https://github.com/predis/predis)
+* [php-amqplib](https://github.com/php-amqplib/php-amqplib) for Message Queuing Producer
diff --git a/src/component.rs b/src/component.rs
index 6e42e71..59adfff 100644
--- a/src/component.rs
+++ b/src/component.rs
@@ -25,3 +25,4 @@
pub const COMPONENT_PHP_MEMCACHED_ID: i32 = 20;
pub const COMPONENT_PHP_REDIS_ID: i32 = 7;
pub const COMPONENT_AMQP_PRODUCER_ID: i32 = 144;
+pub const COMPONENT_MONGODB_ID: i32 = 9;
diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs
index 71acd06..8ee7c16 100644
--- a/src/plugin/mod.rs
+++ b/src/plugin/mod.rs
@@ -16,6 +16,7 @@
mod plugin_amqplib;
mod plugin_curl;
mod plugin_memcached;
+mod plugin_mongodb;
mod plugin_mysqli;
mod plugin_pdo;
mod plugin_predis;
@@ -41,6 +42,7 @@
Box::<plugin_memcached::MemcachedPlugin>::default(),
Box::<plugin_redis::RedisPlugin>::default(),
Box::<plugin_amqplib::AmqplibPlugin>::default(),
+ Box::<plugin_mongodb::MongodbPlugin>::default(),
]
});
diff --git a/src/plugin/plugin_mongodb.rs b/src/plugin/plugin_mongodb.rs
new file mode 100644
index 0000000..53b58ab
--- /dev/null
+++ b/src/plugin/plugin_mongodb.rs
@@ -0,0 +1,191 @@
+// 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 super::{log_exception, Plugin};
+use crate::{
+ component::COMPONENT_MONGODB_ID,
+ context::RequestContext,
+ execute::{get_this_mut, AfterExecuteHook, BeforeExecuteHook},
+ tag::TAG_DB_TYPE,
+};
+use phper::{
+ objects::ZObj,
+ values::{ExecuteData, ZVal},
+};
+use skywalking::{
+ proto::v3::SpanLayer,
+ trace::span::{AbstractSpan, Span},
+};
+use std::any::Any;
+use tracing::{debug, error};
+
+const MANAGER_CLASS_NAME: &str = r"MongoDB\Driver\Manager";
+
+#[derive(Default, Clone)]
+pub struct MongodbPlugin;
+
+impl Plugin for MongodbPlugin {
+ #[inline]
+ fn class_names(&self) -> Option<&'static [&'static str]> {
+ Some(&[MANAGER_CLASS_NAME])
+ }
+
+ #[inline]
+ 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(MANAGER_CLASS_NAME), f)
+ if ["executebulkwrite", "executequery"].contains(&&*f.to_ascii_lowercase()) =>
+ {
+ Some(self.hook_manager_execute_namespace_method(function_name))
+ }
+ (Some(MANAGER_CLASS_NAME), f)
+ if [
+ "executecommand",
+ "executereadcommand",
+ "executereadwritecommand",
+ "executewritecommand",
+ ]
+ .contains(&&*f.to_ascii_lowercase()) =>
+ {
+ Some(self.hook_manager_execute_db_method(function_name))
+ }
+ _ => None,
+ }
+ }
+}
+
+impl MongodbPlugin {
+ fn hook_manager_execute_namespace_method(
+ &self, function_name: &str,
+ ) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
+ let function_name = function_name.to_owned();
+ (
+ Box::new(move |request_id, execute_data| {
+ before_manager_crud_hook(
+ request_id,
+ execute_data,
+ &function_name,
+ CrudScope::Namespace,
+ )
+ }),
+ Box::new(after_manager_crud_hook),
+ )
+ }
+
+ fn hook_manager_execute_db_method(
+ &self, function_name: &str,
+ ) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
+ let function_name = function_name.to_owned();
+ (
+ Box::new(move |request_id, execute_data| {
+ before_manager_crud_hook(request_id, execute_data, &function_name, CrudScope::Db)
+ }),
+ Box::new(after_manager_crud_hook),
+ )
+ }
+}
+
+enum CrudScope {
+ Namespace,
+ Db,
+}
+
+fn before_manager_crud_hook(
+ request_id: Option<i64>, execute_data: &mut ExecuteData, function_name: &str, scope: CrudScope,
+) -> crate::Result<Box<dyn Any>> {
+ let this = get_this_mut(execute_data)?;
+ let handle = this.handle();
+ debug!(handle, function_name, "call MongoDB Manager CRUD method");
+
+ let mut span = RequestContext::try_with_global_ctx(request_id, |ctx| {
+ // Since the driver connects to the database lazily, peer here is empty and
+ // reset it in after hook.
+ Ok(ctx.create_exit_span(&format!("{}->{}", MANAGER_CLASS_NAME, function_name), ""))
+ })?;
+
+ let mut span_object = span.span_object_mut();
+ span_object.set_span_layer(SpanLayer::Database);
+ span_object.component_id = COMPONENT_MONGODB_ID;
+ span_object.add_tag(TAG_DB_TYPE, "MongoDB");
+
+ if let Some(id) = execute_data
+ .get_parameter(0)
+ .as_z_str()
+ .and_then(|s| s.to_str().ok())
+ {
+ match scope {
+ CrudScope::Namespace => {
+ let mut segments = id.split('.');
+ if let Some(db) = segments.next() {
+ span_object.add_tag("mongo.db", db);
+ }
+ if let Some(collection) = segments.next() {
+ span_object.add_tag("mongo.collection", collection);
+ }
+ }
+ CrudScope::Db => {
+ span_object.add_tag("mongo.db", id);
+ }
+ }
+ }
+
+ Ok(Box::new(span))
+}
+
+fn after_manager_crud_hook(
+ _: Option<i64>, span: Box<dyn Any>, execute_data: &mut ExecuteData, _return_value: &mut ZVal,
+) -> crate::Result<()> {
+ let mut span = span.downcast::<Span>().unwrap();
+
+ let this = get_this_mut(execute_data)?;
+ let peer = match get_peer(this) {
+ Ok(peer) => peer,
+ Err(err) => {
+ error!(?err, "get peer failed");
+ "".to_string()
+ }
+ };
+ span.span_object_mut().peer = peer;
+
+ log_exception(&mut *span);
+
+ Ok(())
+}
+
+fn get_peer(this: &mut ZObj) -> phper::Result<String> {
+ let mut addr = Vec::new();
+
+ let mut servers = this.call("getServers", [])?;
+ let servers = servers.expect_mut_z_arr()?;
+
+ for (_, server) in servers.iter_mut() {
+ let server = server.expect_mut_z_obj()?;
+
+ let host = server.call("getHost", [])?;
+ let host = host.expect_z_str()?.to_str()?;
+
+ let port = server.call("getPort", [])?.expect_long()?;
+
+ addr.push(format!("{}:{}", host, port));
+ }
+
+ Ok(addr.join(";"))
+}
diff --git a/tests/data/expected_context.yaml b/tests/data/expected_context.yaml
index 7ff9a3d..3b45f41 100644
--- a/tests/data/expected_context.yaml
+++ b/tests/data/expected_context.yaml
@@ -15,7 +15,7 @@
segmentItems:
- serviceName: skywalking-agent-test-1
- segmentSize: 17
+ segmentSize: 18
segments:
- segmentId: "not null"
spans:
@@ -574,13 +574,13 @@
tags:
- { key: db.type, value: mysql }
- {
- key: db.data_source,
- value: "dbname=skywalking;host=127.0.0.1:3306;",
- }
+ key: db.data_source,
+ value: "dbname=skywalking;host=127.0.0.1:3306;",
+ }
- {
- key: db.statement,
- value: "SELECT * FROM `mysql`.`user` WHERE `User` = :user",
- }
+ key: db.statement,
+ value: "SELECT * FROM `mysql`.`user` WHERE `User` = :user",
+ }
- operationName: PDOStatement->execute
parentSpanId: 0
spanId: 9
@@ -595,13 +595,13 @@
tags:
- { key: db.type, value: mysql }
- {
- key: db.data_source,
- value: "dbname=skywalking;host=127.0.0.1:3306;",
- }
+ key: db.data_source,
+ value: "dbname=skywalking;host=127.0.0.1:3306;",
+ }
- {
- key: db.statement,
- value: "SELECT * FROM `mysql`.`user` WHERE `User` = :user",
- }
+ key: db.statement,
+ value: "SELECT * FROM `mysql`.`user` WHERE `User` = :user",
+ }
- operationName: PDOStatement->fetchAll
parentSpanId: 0
spanId: 10
@@ -616,13 +616,13 @@
tags:
- { key: db.type, value: mysql }
- {
- key: db.data_source,
- value: "dbname=skywalking;host=127.0.0.1:3306;",
- }
+ key: db.data_source,
+ value: "dbname=skywalking;host=127.0.0.1:3306;",
+ }
- {
- key: db.statement,
- value: "SELECT * FROM `mysql`.`user` WHERE `User` = :user",
- }
+ key: db.statement,
+ value: "SELECT * FROM `mysql`.`user` WHERE `User` = :user",
+ }
- operationName: PDO->__construct
parentSpanId: 0
spanId: 11
@@ -654,13 +654,10 @@
tags:
- { key: db.type, value: mysql }
- {
- key: db.data_source,
- value: "dbname=skywalking;host=127.0.0.1;port=3306",
- }
- - {
- key: db.statement,
- value: "SELECT * FROM not_exist",
- }
+ key: db.data_source,
+ value: "dbname=skywalking;host=127.0.0.1;port=3306",
+ }
+ - { key: db.statement, value: "SELECT * FROM not_exist" }
- operationName: PDOStatement->execute
parentSpanId: 0
spanId: 13
@@ -675,13 +672,10 @@
tags:
- { key: db.type, value: mysql }
- {
- key: db.data_source,
- value: "dbname=skywalking;host=127.0.0.1;port=3306",
- }
- - {
- key: db.statement,
- value: "SELECT * FROM not_exist",
- }
+ key: db.data_source,
+ value: "dbname=skywalking;host=127.0.0.1;port=3306",
+ }
+ - { key: db.statement, value: "SELECT * FROM not_exist" }
logs:
- logEvent:
- { key: error.kind, value: PDOException }
@@ -781,7 +775,7 @@
- { key: url, value: "http://127.0.0.1:9011/predis.php" }
- { key: http.method, value: GET }
- { key: http.status_code, value: "200" }
- - segmentId: 'not null'
+ - segmentId: "not null"
spans:
- operationName: mysqli->__construct
parentSpanId: 0
@@ -808,11 +802,8 @@
peer: 127.0.0.1:3306
skipAnalysis: false
tags:
- - {key: db.type, value: mysql}
- - {
- key: db.statement,
- value: "SELECT 1",
- }
+ - { key: db.type, value: mysql }
+ - { key: db.statement, value: "SELECT 1" }
- operationName: mysqli->__construct
parentSpanId: 0
spanId: 3
@@ -838,7 +829,7 @@
peer: 127.0.0.1:3306
skipAnalysis: false
tags:
- - {key: db.type, value: mysql}
+ - { key: db.type, value: mysql }
- {
key: db.statement,
value: "SELECT * FROM `mysql`.`user` WHERE `User` = 'root'",
@@ -858,7 +849,7 @@
- { key: url, value: "http://127.0.0.1:9011/mysqli.php" }
- { key: http.method, value: GET }
- { key: http.status_code, value: "200" }
- - segmentId: 'not null'
+ - segmentId: "not null"
spans:
- operationName: Memcached->set
parentSpanId: 0
@@ -1179,6 +1170,89 @@
- { key: url, value: "http://127.0.0.1:9011/rabbitmq.php" }
- { key: http.method, value: GET }
- { key: http.status_code, value: "200" }
+ - segmentId: "not null"
+ spans:
+ - operationName: "MongoDB\\Driver\\Manager->executeCommand"
+ parentSpanId: 0
+ spanId: 1
+ spanLayer: Database
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 9
+ isError: false
+ spanType: Exit
+ peer: 127.0.0.1:27017
+ skipAnalysis: false
+ tags:
+ - { key: db.type, value: MongoDB }
+ - { key: mongo.db, value: admin }
+ - operationName: "MongoDB\\Driver\\Manager->executeBulkWrite"
+ parentSpanId: 0
+ spanId: 2
+ spanLayer: Database
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 9
+ isError: false
+ spanType: Exit
+ peer: 127.0.0.1:27017
+ skipAnalysis: false
+ tags:
+ - { key: db.type, value: MongoDB }
+ - { key: mongo.db, value: my_db }
+ - { key: mongo.collection, value: my_collection }
+ - operationName: "MongoDB\\Driver\\Manager->executeQuery"
+ parentSpanId: 0
+ spanId: 3
+ spanLayer: Database
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 9
+ isError: false
+ spanType: Exit
+ peer: 127.0.0.1:27017
+ skipAnalysis: false
+ tags:
+ - { key: db.type, value: MongoDB }
+ - { key: mongo.db, value: my_db }
+ - { key: mongo.collection, value: my_collection }
+ - operationName: "MongoDB\\Driver\\Manager->executeCommand"
+ parentSpanId: 0
+ spanId: 4
+ spanLayer: Database
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 9
+ isError: true
+ spanType: Exit
+ peer: ""
+ skipAnalysis: false
+ tags:
+ - { key: db.type, value: MongoDB }
+ - { key: mongo.db, value: admin }
+ logs:
+ - logEvent:
+ - {
+ key: error.kind,
+ value: "MongoDB\\Driver\\Exception\\ConnectionTimeoutException",
+ }
+ - { key: message, value: "not null" }
+ - { key: stack, value: "not null" }
+ - operationName: GET:/mongodb.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: "http://127.0.0.1:9011/mongodb.php" }
+ - { key: http.method, value: GET }
+ - { key: http.status_code, value: "200" }
- serviceName: skywalking-agent-test-2
segmentSize: 1
segments:
@@ -1301,7 +1375,7 @@
- { key: http.method, value: GET }
- { key: http.status_code, value: "200" }
- serviceName: skywalking-agent-test-2-swoole
- segmentSize: 8
+ segmentSize: 9
segments:
- segmentId: "not null"
spans:
@@ -1674,3 +1748,34 @@
- { key: url, value: "http://127.0.0.1:9502/predis" }
- { key: http.method, value: GET }
- { key: http.status_code, value: "200" }
+ - segmentId: "not null"
+ spans:
+ - operationName: "MongoDB\\Driver\\Manager->executeCommand"
+ parentSpanId: 0
+ spanId: 1
+ spanLayer: Database
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 9
+ isError: false
+ spanType: Exit
+ peer: 127.0.0.1:27017
+ skipAnalysis: false
+ tags:
+ - { key: db.type, value: MongoDB }
+ - { key: mongo.db, value: admin }
+ - operationName: GET:/mongodb
+ 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: "http://127.0.0.1:9502/mongodb" }
+ - { key: http.method, value: GET }
+ - { key: http.status_code, value: "200" }
diff --git a/tests/e2e.rs b/tests/e2e.rs
index 00b1f72..90ad6e3 100644
--- a/tests/e2e.rs
+++ b/tests/e2e.rs
@@ -56,6 +56,7 @@
request_fpm_memcached().await;
request_fpm_redis().await;
request_fpm_rabbitmq().await;
+ request_fpm_mongodb().await;
request_swoole_curl().await;
request_swoole_2_curl().await;
request_swoole_2_pdo().await;
@@ -63,6 +64,7 @@
request_swoole_2_memcached().await;
request_swoole_2_redis().await;
request_swoole_2_predis().await;
+ request_swoole_2_mongodb().await;
sleep(Duration::from_secs(3)).await;
request_collector_validate().await;
}
@@ -140,6 +142,14 @@
.await;
}
+async fn request_fpm_mongodb() {
+ request_common(
+ HTTP_CLIENT.get(format!("http://{}/mongodb.php", PROXY_SERVER_1_ADDRESS)),
+ "ok",
+ )
+ .await;
+}
+
async fn request_swoole_curl() {
request_common(
HTTP_CLIENT.get(format!("http://{}/curl", SWOOLE_SERVER_1_ADDRESS)),
@@ -196,6 +206,14 @@
.await;
}
+async fn request_swoole_2_mongodb() {
+ request_common(
+ HTTP_CLIENT.get(format!("http://{}/mongodb", SWOOLE_SERVER_2_ADDRESS)),
+ "ok",
+ )
+ .await;
+}
+
async fn request_collector_validate() {
request_common(
HTTP_CLIENT
diff --git a/tests/php/fpm/mongodb.php b/tests/php/fpm/mongodb.php
new file mode 100644
index 0000000..c4d20e5
--- /dev/null
+++ b/tests/php/fpm/mongodb.php
@@ -0,0 +1,45 @@
+<?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.
+
+$manager = new MongoDB\Driver\Manager("mongodb://root:example@127.0.0.1:27017");
+
+{
+ $command = new MongoDB\Driver\Command(['ping' => 1]);
+ $manager->executeCommand('admin', $command);
+}
+
+{
+ $bulk = new MongoDB\Driver\BulkWrite;
+ $bulk->insert(['x' => 1, 'y' => 'foo']);
+ $bulk->insert(['x' => 2, 'y' => 'bar']);
+ $bulk->insert(['x' => 3, 'y' => 'baz']);
+ $manager->executeBulkWrite('my_db.my_collection', $bulk);
+}
+
+{
+ $query = new MongoDB\Driver\Query(['x' => 1], []);
+ $manager->executeQuery('my_db.my_collection', $query);
+}
+
+try {
+ $manager2 = new MongoDB\Driver\Manager("mongodb://root:example@127.0.0.1:27018,127.0.0.1:27019");
+ $command = new MongoDB\Driver\Command(['ping' => 1]);
+ $manager2->executeCommand('admin', $command);
+} catch(MongoDB\Driver\Exception\ConnectionTimeoutException $e) {
+}
+
+echo "ok";
diff --git a/tests/php/swoole/main.2.php b/tests/php/swoole/main.2.php
index 47cfcc0..9d8f0d8 100644
--- a/tests/php/swoole/main.2.php
+++ b/tests/php/swoole/main.2.php
@@ -97,6 +97,14 @@
Assert::same($client->get('foo002'), 'bar002');
}
break;
+
+ case '/mongodb':
+ {
+ $manager = new MongoDB\Driver\Manager("mongodb://root:example@127.0.0.1:27017");
+ $command = new MongoDB\Driver\Command(['ping' => 1]);
+ $manager->executeCommand('admin', $command);
+ }
+ break;
default:
throw new DomainException("Unknown operation");