You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by he...@apache.org on 2022/10/27 08:09:30 UTC

[skywalking-php] branch master updated: Add plugin for memcached. (#27)

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

heyanlong 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 e144dea  Add plugin for memcached. (#27)
e144dea is described below

commit e144deae28b07ba0d6625780ddfc749fc289e8e4
Author: jmjoy <jm...@apache.org>
AuthorDate: Thu Oct 27 16:09:24 2022 +0800

    Add plugin for memcached. (#27)
    
    * Add plugin for memcached.
---
 .github/workflows/rust.yml                         |   2 +-
 README.md                                          |   2 +-
 Vagrantfile                                        |   1 +
 docker-compose.yml                                 |   5 +
 .../service-agent/php-agent/Supported-list.md      |   1 +
 src/component.rs                                   |   1 +
 src/plugin/mod.rs                                  |   2 +
 src/plugin/plugin_memcached.rs                     | 248 +++++++++++++++++++++
 tests/data/expected_context.yaml                   | 107 ++++++++-
 tests/e2e.rs                                       |  11 +-
 src/component.rs => tests/php/fpm/memcached.php    |  35 ++-
 11 files changed, 402 insertions(+), 13 deletions(-)

diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index aa732ec..0430487 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -85,7 +85,7 @@ jobs:
           tools: php-config, composer:v2
           extensions: >
             bcmath, calendar, ctype, dom, exif, gettext, iconv, intl, json, mbstring,
-            mysqli, mysqlnd, opcache, pdo, pdo_mysql, phar, posix, readline,
+            mysqli, mysqlnd, opcache, pdo, pdo_mysql, phar, posix, readline, memcached,
             swoole-${{ matrix.version.swoole }}, xml, xmlreader, xmlwriter, yaml, zip
 
       - name: Setup php-fpm for Linux
diff --git a/README.md b/README.md
index 66bbf84..cc757df 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ SkyWalking PHP Agent requires SkyWalking 8.4+ and PHP 7.2+
   * [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)
+  * [x] [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)
   * [ ] [php-rdkafka](https://github.com/arnaud-lb/php-rdkafka)
diff --git a/Vagrantfile b/Vagrantfile
index c24e060..af9fb2c 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -22,6 +22,7 @@ Vagrant.configure("2") do |config|
     config.vm.network "forwarded_port", guest: 12800, host: 12800
     config.vm.network "forwarded_port", guest: 3306, host: 3306
     config.vm.network "forwarded_port", guest: 6379, host: 6379
+    config.vm.network "forwarded_port", guest: 11211, host: 11211
 
     config.vm.synced_folder ".", "/vagrant"
 
diff --git a/docker-compose.yml b/docker-compose.yml
index 69a3a24..99fd301 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -38,3 +38,8 @@ services:
       - "6379:6379"
     environment:
       - REDIS_PASSWORD=password
+
+  memcached:
+    image: memcached:1.6.17
+    ports:
+      - "11211:11211"
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 56f9ccf..d4227e7 100644
--- a/docs/en/setup/service-agent/php-agent/Supported-list.md
+++ b/docs/en/setup/service-agent/php-agent/Supported-list.md
@@ -12,6 +12,7 @@ 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)
+* [Memcached](https://www.php.net/manual/en/book.memcached.php)
 
 ## Support PHP library
 
diff --git a/src/component.rs b/src/component.rs
index 3e5c93f..cb6ab19 100644
--- a/src/component.rs
+++ b/src/component.rs
@@ -22,3 +22,4 @@ pub const COMPONENT_PHP_CURL_ID: i32 = 8002;
 pub const COMPONENT_PHP_PDO_ID: i32 = 8003;
 pub const COMPONENT_PHP_MYSQLI_ID: i32 = 8004;
 pub const COMPONENT_PHP_PREDIS_ID: i32 = 8006;
+pub const COMPONENT_PHP_MEMCACHED_ID: i32 = 20;
diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs
index 411fcec..5b16556 100644
--- a/src/plugin/mod.rs
+++ b/src/plugin/mod.rs
@@ -14,6 +14,7 @@
 // limitations under the License.
 
 mod plugin_curl;
+mod plugin_memcached;
 mod plugin_mysqli;
 mod plugin_pdo;
 mod plugin_predis;
@@ -31,6 +32,7 @@ static PLUGINS: Lazy<Vec<Box<DynPlugin>>> = Lazy::new(|| {
         Box::new(plugin_swoole::SwooleServerPlugin::default()),
         Box::new(plugin_swoole::SwooleHttpResponsePlugin::default()),
         Box::new(plugin_predis::PredisPlugin::default()),
+        Box::new(plugin_memcached::MemcachedPlugin::default()),
     ]
 });
 
diff --git a/src/plugin/plugin_memcached.rs b/src/plugin/plugin_memcached.rs
new file mode 100644
index 0000000..969a756
--- /dev/null
+++ b/src/plugin/plugin_memcached.rs
@@ -0,0 +1,248 @@
+// 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::Plugin;
+use crate::{
+    component::COMPONENT_PHP_MEMCACHED_ID,
+    context::RequestContext,
+    execute::{get_this_mut, AfterExecuteHook, BeforeExecuteHook},
+};
+use anyhow::{bail, Context};
+use once_cell::sync::Lazy;
+use phper::{functions::call, values::ExecuteData};
+use skywalking::{skywalking_proto::v3::SpanLayer, trace::span::Span};
+use tracing::{debug, warn};
+
+static MEC_KEYS_COMMANDS: Lazy<Vec<String>> = Lazy::new(|| {
+    [
+        "set",
+        "setByKey",
+        "setMulti",
+        "setMultiByKey",
+        "add",
+        "addByKey",
+        "replace",
+        "replaceByKey",
+        "append",
+        "appendByKey",
+        "prepend",
+        "prependByKey",
+        "get",
+        "getByKey",
+        "getMulti",
+        "getMultiByKey",
+        "getAllKeys",
+        "delete",
+        "deleteByKey",
+        "deleteMulti",
+        "deleteMultiByKey",
+        "increment",
+        "incrementByKey",
+        "decrement",
+        "decrementByKey",
+        "getStats",
+        "isPersistent",
+        "isPristine",
+        "flush",
+        "flushBuffers",
+        "getDelayed",
+        "getDelayedByKey",
+        "fetch",
+        "fetchAll",
+        "addServer",
+        "addServers",
+        "getOption",
+        "setOption",
+        "setOptions",
+        "getResultCode",
+        "getServerList",
+        "resetServerList",
+        "getVersion",
+        "quit",
+        "setSaslAuthData",
+        "touch",
+        "touchByKey",
+    ]
+    .into_iter()
+    .map(str::to_ascii_lowercase)
+    .collect()
+});
+
+static MEC_STR_KEYS_COMMANDS: Lazy<Vec<String>> = Lazy::new(|| {
+    [
+        "set",
+        "setByKey",
+        "setMulti",
+        "setMultiByKey",
+        "add",
+        "addByKey",
+        "replace",
+        "replaceByKey",
+        "append",
+        "appendByKey",
+        "prepend",
+        "prependByKey",
+        "get",
+        "getByKey",
+        "getMulti",
+        "getMultiByKey",
+        "getAllKeys",
+        "delete",
+        "deleteByKey",
+        "deleteMulti",
+        "deleteMultiByKey",
+        "increment",
+        "incrementByKey",
+        "decrement",
+        "decrementByKey",
+    ]
+    .into_iter()
+    .map(str::to_ascii_lowercase)
+    .collect()
+});
+
+#[derive(Default, Clone)]
+pub struct MemcachedPlugin;
+
+impl Plugin for MemcachedPlugin {
+    fn class_names(&self) -> Option<&'static [&'static str]> {
+        Some(&["Memcached"])
+    }
+
+    fn function_name_prefix(&self) -> Option<&'static str> {
+        None
+    }
+
+    fn hook(
+        &self, class_name: Option<&str>, function_name: &str,
+    ) -> Option<(
+        Box<crate::execute::BeforeExecuteHook>,
+        Box<crate::execute::AfterExecuteHook>,
+    )> {
+        match (class_name, function_name) {
+            (Some(class_name @ "Memcached"), f)
+                if MEC_KEYS_COMMANDS.contains(&f.to_ascii_lowercase()) =>
+            {
+                Some(self.hook_memcached_methods(class_name, function_name))
+            }
+            _ => None,
+        }
+    }
+}
+
+impl MemcachedPlugin {
+    fn hook_memcached_methods(
+        &self, class_name: &str, function_name: &str,
+    ) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
+        let class_name = class_name.to_owned();
+        let function_name = function_name.to_owned();
+        (
+            Box::new(move |request_id, execute_data| {
+                let peer = if MEC_STR_KEYS_COMMANDS.contains(&function_name.to_ascii_lowercase()) {
+                    let mut f = || {
+                        let key = {
+                            let key = execute_data.get_parameter(0);
+                            if !key.get_type_info().is_string() {
+                                // The `*Multi` methods will failed here.
+                                bail!("The argument key of {} isn't string", &function_name);
+                            }
+                            key.clone()
+                        };
+                        let this = get_this_mut(execute_data)?;
+                        let info = this.call(&"getServerByKey".to_ascii_lowercase(), [key])?;
+                        let info = info.as_z_arr().context("Server isn't array")?;
+                        let host = info
+                            .get("host")
+                            .context("Server host not exists")?
+                            .as_z_str()
+                            .context("Server host isn't string")?
+                            .to_str()?;
+                        let port = info
+                            .get("port")
+                            .context("Server port not exists")?
+                            .as_long()
+                            .context("Server port isn't long")?;
+                        Ok::<_, anyhow::Error>(format!("{}:{}", host, port))
+                    };
+                    match f() {
+                        Ok(peer) => peer,
+                        Err(err) => {
+                            warn!(?err, "Get peer failed");
+                            "".to_owned()
+                        }
+                    }
+                } else {
+                    "".to_owned()
+                };
+
+                debug!(peer, "Get memcached peer");
+
+                let span = RequestContext::try_with_global_ctx(request_id, |ctx| {
+                    let mut span =
+                        ctx.create_exit_span(&format!("{}->{}", class_name, function_name), &peer);
+                    span.with_span_object_mut(|obj| {
+                        obj.set_span_layer(SpanLayer::Cache);
+                        obj.component_id = COMPONENT_PHP_MEMCACHED_ID;
+                        obj.add_tag("db.type", "memcached");
+
+                        match get_command(execute_data, &function_name) {
+                            Ok(cmd) => {
+                                obj.add_tag("memcached.command", cmd);
+                            }
+                            Err(err) => {
+                                warn!(?err, "get command failed");
+                            }
+                        }
+                    });
+                    Ok(span)
+                })?;
+
+                Ok(Box::new(span) as _)
+            }),
+            Box::new(|_, span, _, return_value| {
+                let mut span = span.downcast::<Span>().unwrap();
+                if let Some(b) = return_value.as_bool() {
+                    if !b {
+                        span.with_span_object_mut(|span| {
+                            span.is_error = true;
+                        });
+                    }
+                }
+                Ok(())
+            }),
+        )
+    }
+}
+
+fn get_command(execute_data: &mut ExecuteData, function_name: &str) -> anyhow::Result<String> {
+    let num_args = execute_data.num_args();
+    let mut items = Vec::with_capacity(num_args + 1);
+    items.push(function_name.to_owned());
+
+    for i in 0..num_args {
+        let parameter = execute_data.get_parameter(i);
+        let s = if parameter.get_type_info().is_array() {
+            let result = call("json_encode", [parameter.clone()])?;
+            result.expect_z_str()?.to_str()?.to_string()
+        } else {
+            let mut parameter = parameter.clone();
+            parameter.convert_to_string();
+            parameter.expect_z_str()?.to_str()?.to_string()
+        };
+        items.push(s)
+    }
+
+    Ok(items.join(" "))
+}
diff --git a/tests/data/expected_context.yaml b/tests/data/expected_context.yaml
index a05679e..f5ca94a 100644
--- a/tests/data/expected_context.yaml
+++ b/tests/data/expected_context.yaml
@@ -15,7 +15,7 @@
 
 segmentItems:
   - serviceName: skywalking-agent-test-1
-    segmentSize: 9
+    segmentSize: 10
     segments:
       - segmentId: "not null"
         spans:
@@ -576,6 +576,111 @@ segmentItems:
               - {key: url, value: /mysqli.php}
               - {key: http.method, value: GET}
               - {key: http.status_code, value: '200'}
+      - segmentId: 'not null'
+        spans:
+          - operationName: Memcached->addServer
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: false
+            spanType: Exit
+            peer: ""
+            skipAnalysis: false
+            tags:
+              - { key: db.type, value: memcached }
+              - { key: memcached.command, value: addServer 127.0.0.1 11211 }
+          - operationName: Memcached->set
+            parentSpanId: 0
+            spanId: 2
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:11211
+            skipAnalysis: false
+            tags:
+              - { key: db.type, value: memcached }
+              - { key: memcached.command, value: set foo Hello! }
+          - operationName: Memcached->set
+            parentSpanId: 0
+            spanId: 3
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:11211
+            skipAnalysis: false
+            tags:
+              - { key: db.type, value: memcached }
+              - { key: memcached.command, value: set bar Memcached... }
+          - operationName: Memcached->get
+            parentSpanId: 0
+            spanId: 4
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:11211
+            skipAnalysis: false
+            tags:
+              - { key: db.type, value: memcached }
+              - { key: memcached.command, value: get foo }
+          - operationName: Memcached->get
+            parentSpanId: 0
+            spanId: 5
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:11211
+            skipAnalysis: false
+            tags:
+              - { key: db.type, value: memcached }
+              - { key: memcached.command, value: get bar }
+          - operationName: Memcached->setMulti
+            parentSpanId: 0
+            spanId: 6
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 20
+            isError: false
+            spanType: Exit
+            peer: ""
+            skipAnalysis: false
+            tags:
+              - { key: db.type, value: memcached }
+              - {
+                  key: memcached.command,
+                  value:
+                    'setMulti {"key1":"value1","key2":"value2","key3":"value3"}',
+                }
+          - operationName: GET:/memcached.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: /memcached.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 cbdaf9c..a15671b 100644
--- a/tests/e2e.rs
+++ b/tests/e2e.rs
@@ -49,9 +49,10 @@ async fn e2e() {
 async fn run_e2e() {
     request_fpm_curl().await;
     request_fpm_pdo().await;
+    request_fpm_predis().await;
     request_fpm_mysqli().await;
+    request_fpm_memcached().await;
     request_swoole_curl().await;
-    request_fpm_predis().await;
     sleep(Duration::from_secs(3)).await;
     request_collector_validate().await;
 }
@@ -88,6 +89,14 @@ async fn request_fpm_predis() {
     .await;
 }
 
+async fn request_fpm_memcached() {
+    request_common(
+        HTTP_CLIENT.get(format!("http://{}/memcached.php", PROXY_SERVER_1_ADDRESS)),
+        "ok",
+    )
+    .await;
+}
+
 async fn request_swoole_curl() {
     request_common(
         HTTP_CLIENT.get(format!("http://{}/curl", SWOOLE_SERVER_1_ADDRESS)),
diff --git a/src/component.rs b/tests/php/fpm/memcached.php
similarity index 59%
copy from src/component.rs
copy to tests/php/fpm/memcached.php
index 3e5c93f..dd59904 100644
--- a/src/component.rs
+++ b/tests/php/fpm/memcached.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,14 +13,29 @@
 // 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";
+
+{
+    $mc = new Memcached();
+    $mc->addServer("127.0.0.1", 11211);
+
+    $mc->set("foo", "Hello!");
+    $mc->set("bar", "Memcached...");
+
+    Assert::same($mc->get("foo"), 'Hello!');
+    Assert::same($mc->get("bar"), "Memcached...");
 
-//! Component ID
-//!
-//! <https://github.com/apache/skywalking/blob/014861535015745ae3f7b99acd7d14500b3b3927/oap-server/server-starter/src/main/resources/component-libraries.yml>
+    $items = array(
+        'key1' => 'value1',
+        'key2' => 'value2',
+        'key3' => 'value3'
+    );
+    $mc->setMulti($items);
+}
 
-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;
-pub const COMPONENT_PHP_PREDIS_ID: i32 = 8006;
+echo "ok";