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/11/10 07:08:22 UTC

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

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 c550167  Add plugin for phpredis. (#29)
c550167 is described below

commit c550167d9ca9b5654c1b78e9635f7a4f7311cf92
Author: jmjoy <jm...@apache.org>
AuthorDate: Thu Nov 10 15:08:17 2022 +0800

    Add plugin for phpredis. (#29)
---
 .github/workflows/rust.yml                         |   5 +-
 Cargo.lock                                         |   6 +-
 Cargo.toml                                         |   1 +
 README.md                                          |   2 +-
 .../service-agent/php-agent/Supported-list.md      |   1 +
 src/component.rs                                   |   1 +
 src/lib.rs                                         |   1 +
 src/plugin/mod.rs                                  |   2 +
 src/plugin/plugin_redis.rs                         | 372 +++++++++++++++++++++
 src/{component.rs => tag.rs}                       |  16 +-
 src/util.rs                                        |  72 +++-
 tests/data/expected_context.yaml                   | 149 ++++++++-
 tests/e2e.rs                                       |  15 +
 src/component.rs => tests/php/fpm/redis.fail.php   |  25 +-
 src/component.rs => tests/php/fpm/redis.succ.php   |  27 +-
 15 files changed, 660 insertions(+), 35 deletions(-)

diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 0430487..b407ec1 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -85,8 +85,9 @@ 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, memcached,
-            swoole-${{ matrix.version.swoole }}, xml, xmlreader, xmlwriter, yaml, zip
+            mysqli, mysqlnd, opcache, pdo, pdo_mysql, phar, posix, readline, redis,
+            memcached, swoole-${{ matrix.version.swoole }}, xml, xmlreader, xmlwriter,
+            yaml, zip
 
       - name: Setup php-fpm for Linux
         if: matrix.os == 'ubuntu-20.04'
diff --git a/Cargo.lock b/Cargo.lock
index 1b08c5b..3000865 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1782,10 +1782,11 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.85"
+version = "1.0.87"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
+checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
 dependencies = [
+ "indexmap",
  "itoa",
  "ryu",
  "serde",
@@ -1885,6 +1886,7 @@ dependencies = [
  "phper",
  "prost",
  "reqwest",
+ "serde_json",
  "skywalking",
  "systemstat",
  "tempfile",
diff --git a/Cargo.toml b/Cargo.toml
index 79875cb..788c3e3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -46,6 +46,7 @@ libc = "0.2.132"
 once_cell = "1.14.0"
 phper = "0.5.1"
 prost = "0.11.0"
+serde_json = { version = "1.0.87", features = ["preserve_order"] }
 skywalking = "0.4.0"
 systemstat = "0.2.0"
 tempfile = "3.3.0"
diff --git a/README.md b/README.md
index cc757df..0d39aae 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ SkyWalking PHP Agent requires SkyWalking 8.4+ and PHP 7.2+
   * [x] [PDO](https://www.php.net/manual/en/book.pdo.php)
   * [x] [MySQL Improved](https://www.php.net/manual/en/book.mysqli.php)
   * [x] [Memcached](https://www.php.net/manual/en/book.memcached.php)
-  * [ ] [phpredis](https://github.com/phpredis/phpredis)
+  * [x] [phpredis](https://github.com/phpredis/phpredis)
   * [ ] [php-amqp](https://github.com/php-amqp/php-amqp)
   * [ ] [php-rdkafka](https://github.com/arnaud-lb/php-rdkafka)
   * [x] [predis](https://github.com/predis/predis)
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 d4227e7..4e11b15 100644
--- a/docs/en/setup/service-agent/php-agent/Supported-list.md
+++ b/docs/en/setup/service-agent/php-agent/Supported-list.md
@@ -13,6 +13,7 @@ The following plugins provide the distributed tracing capability.
 * [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)
+* [phpredis](https://github.com/phpredis/phpredis)
 
 ## Support PHP library
 
diff --git a/src/component.rs b/src/component.rs
index cb6ab19..b6aaf08 100644
--- a/src/component.rs
+++ b/src/component.rs
@@ -23,3 +23,4 @@ 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;
+pub const COMPONENT_PHP_REDIS_ID: i32 = 7;
diff --git a/src/lib.rs b/src/lib.rs
index dd97960..6569149 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -25,6 +25,7 @@ mod execute;
 mod module;
 mod plugin;
 mod request;
+mod tag;
 mod util;
 mod worker;
 
diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs
index 5b16556..070185c 100644
--- a/src/plugin/mod.rs
+++ b/src/plugin/mod.rs
@@ -18,6 +18,7 @@ mod plugin_memcached;
 mod plugin_mysqli;
 mod plugin_pdo;
 mod plugin_predis;
+mod plugin_redis;
 mod plugin_swoole;
 
 use crate::execute::{AfterExecuteHook, BeforeExecuteHook};
@@ -33,6 +34,7 @@ static PLUGINS: Lazy<Vec<Box<DynPlugin>>> = Lazy::new(|| {
         Box::new(plugin_swoole::SwooleHttpResponsePlugin::default()),
         Box::new(plugin_predis::PredisPlugin::default()),
         Box::new(plugin_memcached::MemcachedPlugin::default()),
+        Box::new(plugin_redis::RedisPlugin::default()),
     ]
 });
 
diff --git a/src/plugin/plugin_redis.rs b/src/plugin/plugin_redis.rs
new file mode 100644
index 0000000..e376d07
--- /dev/null
+++ b/src/plugin/plugin_redis.rs
@@ -0,0 +1,372 @@
+// 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 std::{any::Any, collections::HashSet};
+
+use super::Plugin;
+use crate::{
+    component::COMPONENT_PHP_REDIS_ID,
+    context::RequestContext,
+    execute::{get_this_mut, AfterExecuteHook, BeforeExecuteHook, Noop},
+    tag::{TAG_CACHE_CMD, TAG_CACHE_KEY, TAG_CACHE_OP, TAG_CACHE_TYPE},
+};
+use anyhow::Context;
+use dashmap::DashMap;
+use once_cell::sync::Lazy;
+use phper::{
+    eg,
+    objects::ZObj,
+    sys,
+    values::{ExecuteData, ZVal},
+};
+use skywalking::{skywalking_proto::v3::SpanLayer, trace::span::Span};
+use tracing::{debug, warn};
+
+static PEER_MAP: Lazy<DashMap<u32, Peer>> = Lazy::new(Default::default);
+
+static FREE_MAP: Lazy<DashMap<u32, sys::zend_object_free_obj_t>> = Lazy::new(Default::default);
+
+static REDIS_READ_COMMANDS: Lazy<HashSet<String>> = Lazy::new(|| {
+    [
+        "blPop",
+        "brPop",
+        "get",
+        "getBit",
+        "getKeys",
+        "getMultiple",
+        "getRange",
+        "hExists",
+        "hGet",
+        "hGetAll",
+        "hKeys",
+        "hLen",
+        "hMGet",
+        "hScan",
+        "hStrLen",
+        "hVals",
+        "keys",
+        "lGet",
+        "lGetRange",
+        "lLen",
+        "lRange",
+        "lSize",
+        "mGet",
+        "sContains",
+        "sGetMembers",
+        "sIsMember",
+        "sMembers",
+        "sScan",
+        "sSize",
+        "strLen",
+        "zCount",
+        "zRange",
+        "zRangeByLex",
+        "zRangeByScore",
+        "zScan",
+        "zSize",
+    ]
+    .into_iter()
+    .map(str::to_ascii_lowercase)
+    .collect()
+});
+
+static REDIS_WRITE_COMMANDS: Lazy<HashSet<String>> = Lazy::new(|| {
+    [
+        "append",
+        "bRPopLPush",
+        "decr",
+        "decrBy",
+        "del",
+        "delete",
+        "hDel",
+        "hIncrBy",
+        "hIncrByFloat",
+        "hMSet",
+        "hSet",
+        "hSetNx",
+        "incr",
+        "incrBy",
+        "incrByFloat",
+        "lInsert",
+        "lPush",
+        "lPushx",
+        "lRem",
+        "lRemove",
+        "lSet",
+        "lTrim",
+        "listTrim",
+        "mSet",
+        "mSetNX",
+        "pSetEx",
+        "rPopLPush",
+        "rPush",
+        "rPushX",
+        "randomKey",
+        "sAdd",
+        "sInter",
+        "sInterStore",
+        "sMove",
+        "sRandMember",
+        "sRem",
+        "sRemove",
+        "set",
+        "setBit",
+        "setEx",
+        "setNx",
+        "setRange",
+        "setTimeout",
+        "sort",
+        "unlink",
+        "zAdd",
+        "zDelete",
+        "zDeleteRangeByRank",
+        "zDeleteRangeByScore",
+        "zIncrBy",
+        "zRem",
+        "zRemRangeByRank",
+        "zRemRangeByScore",
+        "zRemove",
+        "zRemoveRangeByScore",
+    ]
+    .into_iter()
+    .map(str::to_ascii_lowercase)
+    .collect()
+});
+
+static REDIS_ALL_COMMANDS: Lazy<HashSet<String>> = Lazy::new(|| {
+    let mut commands = HashSet::new();
+    commands.extend(REDIS_READ_COMMANDS.iter().map(Clone::clone));
+    commands.extend(REDIS_WRITE_COMMANDS.iter().map(Clone::clone));
+    commands
+});
+
+#[derive(Default, Clone)]
+pub struct RedisPlugin;
+
+impl Plugin for RedisPlugin {
+    #[inline]
+    fn class_names(&self) -> Option<&'static [&'static str]> {
+        Some(&["Redis"])
+    }
+
+    #[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("Redis"), "__construct") => Some(self.hook_redis_construct()),
+            (Some(class_name @ "Redis"), f)
+                if ["connect", "open", "pconnect", "popen"].contains(&f) =>
+            {
+                Some(self.hook_redis_connect(class_name, function_name))
+            }
+            (Some(class_name @ "Redis"), f)
+                if REDIS_ALL_COMMANDS.contains(&f.to_ascii_lowercase()) =>
+            {
+                Some(self.hook_redis_methods(class_name, function_name))
+            }
+            _ => None,
+        }
+    }
+}
+
+impl RedisPlugin {
+    /// TODO Support first optional argument as config for phpredis 6.0+.
+    /// <https://github.com/phpredis/phpredis/blob/cc2383f07666e6afefd7b58995fb607d9967d650/README.markdown#example-1>
+    fn hook_redis_construct(&self) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
+        (
+            Box::new(|_, execute_data| {
+                let this = get_this_mut(execute_data)?;
+                hack_free(this, Some(redis_dtor));
+
+                Ok(Box::new(()))
+            }),
+            Noop::noop(),
+        )
+    }
+
+    fn hook_redis_connect(
+        &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| {
+                if execute_data.num_args() < 2 {
+                    debug!("argument count less than 2, skipped.");
+                    return Ok(Box::new(()));
+                }
+
+                let host = {
+                    let mut f = || {
+                        Ok::<_, anyhow::Error>(
+                            execute_data
+                                .get_parameter(0)
+                                .as_z_str()
+                                .context("isn't string")?
+                                .to_str()?
+                                .to_owned(),
+                        )
+                    };
+                    match f() {
+                        Ok(host) => host,
+                        Err(err) => {
+                            warn!(?err, "parse first argument to host failed, skipped.");
+                            return Ok(Box::new(()));
+                        }
+                    }
+                };
+                let port = {
+                    let mut f = || {
+                        execute_data
+                            .get_parameter(1)
+                            .as_long()
+                            .context("isn't long")
+                    };
+                    match f() {
+                        Ok(port) => port,
+                        Err(err) => {
+                            warn!(?err, "parse second argument to port failed, skipped.");
+                            return Ok(Box::new(()));
+                        }
+                    }
+                };
+
+                let this = get_this_mut(execute_data)?;
+                let addr = format!("{}:{}", host, port);
+                debug!(addr, "Get redis peer");
+                PEER_MAP.insert(this.handle(), Peer { addr: addr.clone() });
+
+                let mut span = RequestContext::try_with_global_ctx(request_id, |ctx| {
+                    Ok(ctx.create_exit_span(&format!("{}->{}", class_name, function_name), &addr))
+                })?;
+
+                span.with_span_object_mut(|span| {
+                    span.set_span_layer(SpanLayer::Cache);
+                    span.component_id = COMPONENT_PHP_REDIS_ID;
+                    span.add_tag(TAG_CACHE_TYPE, "redis");
+                });
+
+                Ok(Box::new(span))
+            }),
+            Box::new(after_hook),
+        )
+    }
+
+    fn hook_redis_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 handle = get_this_mut(execute_data)?.handle();
+                debug!(handle, function_name, "call redis method");
+                let peer = PEER_MAP
+                    .get(&handle)
+                    .map(|r| r.value().addr.clone())
+                    .unwrap_or_default();
+
+                let key = execute_data
+                    .get_parameter(0)
+                    .as_z_str()
+                    .and_then(|s| s.to_str().ok());
+                let op = if REDIS_READ_COMMANDS.contains(&function_name.to_ascii_lowercase()) {
+                    "read"
+                } else {
+                    "write"
+                };
+
+                debug!(handle, function_name, key, op, "call redis command");
+
+                let mut span = RequestContext::try_with_global_ctx(request_id, |ctx| {
+                    Ok(ctx.create_exit_span(&format!("{}->{}", class_name, function_name), &peer))
+                })?;
+
+                span.with_span_object_mut(|span| {
+                    span.set_span_layer(SpanLayer::Cache);
+                    span.component_id = COMPONENT_PHP_REDIS_ID;
+                    span.add_tag(TAG_CACHE_TYPE, "redis");
+                    span.add_tag(TAG_CACHE_CMD, function_name);
+                    span.add_tag(TAG_CACHE_OP, op);
+                    if let Some(key) = key {
+                        span.add_tag(TAG_CACHE_KEY, key)
+                    }
+                });
+
+                Ok(Box::new(span))
+            }),
+            Box::new(after_hook),
+        )
+    }
+}
+
+struct Peer {
+    addr: String,
+}
+
+fn hack_free(this: &mut ZObj, new_free: sys::zend_object_free_obj_t) {
+    let handle = this.handle();
+
+    unsafe {
+        let ori_free = (*(*this.as_mut_ptr()).handlers).free_obj;
+        FREE_MAP.insert(handle, ori_free);
+        (*((*this.as_mut_ptr()).handlers as *mut sys::zend_object_handlers)).free_obj = new_free;
+    }
+}
+
+unsafe extern "C" fn redis_dtor(object: *mut sys::zend_object) {
+    debug!("call Redis free");
+
+    let handle = ZObj::from_ptr(object).handle();
+
+    PEER_MAP.remove(&handle);
+    if let Some((_, Some(free))) = FREE_MAP.remove(&handle) {
+        free(object);
+    }
+}
+
+fn after_hook(
+    _request_id: Option<i64>, span: Box<dyn Any>, _execute_data: &mut ExecuteData,
+    _return_value: &mut ZVal,
+) -> anyhow::Result<()> {
+    let mut span = span.downcast::<Span>().unwrap();
+
+    let ex = unsafe { ZObj::try_from_mut_ptr(eg!(exception)) };
+    if let Some(ex) = ex {
+        span.with_span_object_mut(|span| {
+            span.is_error = true;
+
+            let mut logs = Vec::new();
+            if let Ok(class_name) = ex.get_class().get_name().to_str() {
+                logs.push(("Exception Class", class_name.to_owned()));
+            }
+            if let Some(message) = ex.get_property("message").as_z_str() {
+                if let Ok(message) = message.to_str() {
+                    logs.push(("Exception Message", message.to_owned()));
+                }
+            }
+            if !logs.is_empty() {
+                span.add_log(logs);
+            }
+        });
+    }
+
+    Ok(())
+}
diff --git a/src/component.rs b/src/tag.rs
similarity index 64%
copy from src/component.rs
copy to src/tag.rs
index cb6ab19..69ca800 100644
--- a/src/component.rs
+++ b/src/tag.rs
@@ -13,13 +13,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License..
 
-//! Component ID
+//! Tags
 //!
-//! <https://github.com/apache/skywalking/blob/014861535015745ae3f7b99acd7d14500b3b3927/oap-server/server-starter/src/main/resources/component-libraries.yml>
+//! Virtual Cache
+//!
+//! https://skywalking.apache.org/docs/main/next/en/setup/service-agent/virtual-cache/
 
-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;
-pub const COMPONENT_PHP_MEMCACHED_ID: i32 = 20;
+pub const TAG_CACHE_TYPE: &str = "cache.type";
+pub const TAG_CACHE_OP: &str = "cache.op";
+pub const TAG_CACHE_CMD: &str = "cache.cmd";
+pub const TAG_CACHE_KEY: &str = "cache.key";
diff --git a/src/util.rs b/src/util.rs
index 460c39f..1643780 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -16,7 +16,8 @@
 use anyhow::bail;
 use chrono::Local;
 use once_cell::sync::Lazy;
-use phper::{sys, values::ZVal};
+use phper::{arrays::IterKey, sys, values::ZVal};
+use serde_json::{json, Number, Value};
 use std::{
     ffi::CStr,
     panic::{catch_unwind, UnwindSafe},
@@ -112,3 +113,72 @@ pub fn catch_unwind_anyhow<F: FnOnce() -> anyhow::Result<R> + UnwindSafe, R>(
 pub fn get_sapi_module_name() -> &'static CStr {
     unsafe { CStr::from_ptr(sys::sapi_module.name) }
 }
+
+/// Use for later scene.
+#[allow(dead_code)]
+pub fn json_encode_values(values: &[ZVal]) -> serde_json::Result<String> {
+    fn add(json_value: &mut Value, key: Option<String>, item: Value) {
+        match key {
+            Some(key) => {
+                json_value.as_object_mut().unwrap().insert(key, item);
+            }
+            None => {
+                json_value.as_array_mut().unwrap().push(item);
+            }
+        }
+    }
+
+    fn handle(json_value: &mut Value, key: Option<String>, val: &ZVal) {
+        let type_info = val.get_type_info();
+
+        if type_info.is_null() {
+            add(json_value, key, Value::Null);
+        } else if type_info.is_true() {
+            add(json_value, key, Value::Bool(true));
+        } else if type_info.is_false() {
+            add(json_value, key, Value::Bool(false));
+        } else if type_info.is_long() {
+            let i = val.as_long().unwrap();
+            add(json_value, key, Value::Number(i.into()));
+        } else if type_info.is_double() {
+            let d = val.as_double().unwrap();
+            let n = match Number::from_f64(d) {
+                Some(n) => Value::Number(n),
+                None => Value::String("<NaN>".to_owned()),
+            };
+            add(json_value, key, n);
+        } else if type_info.is_string() {
+            let s = val
+                .as_z_str()
+                .unwrap()
+                .to_str()
+                .map(ToOwned::to_owned)
+                .unwrap_or_default();
+            add(json_value, key, Value::String(s));
+        } else if type_info.is_array() {
+            let arr = val.as_z_arr().unwrap();
+            let is_arr = arr.iter().all(|(key, _)| matches!(key, IterKey::Index(_)));
+            let mut new_json_value = if is_arr { json!([]) } else { json!({}) };
+            for (key, new_val) in arr.iter() {
+                if is_arr {
+                    handle(&mut new_json_value, None, new_val);
+                } else {
+                    let key = match key {
+                        IterKey::Index(i) => i.to_string(),
+                        IterKey::ZStr(s) => s.to_str().map(ToOwned::to_owned).unwrap_or_default(),
+                    };
+                    handle(&mut new_json_value, Some(key), new_val);
+                }
+            }
+            add(json_value, key, new_json_value);
+        } else if type_info.is_object() {
+            add(json_value, key, Value::String("<Object>".to_owned()));
+        }
+    }
+
+    let mut json_value = json!([]);
+    for val in values {
+        handle(&mut json_value, None, val);
+    }
+    serde_json::to_string(&json_value)
+}
diff --git a/tests/data/expected_context.yaml b/tests/data/expected_context.yaml
index f5ca94a..f366211 100644
--- a/tests/data/expected_context.yaml
+++ b/tests/data/expected_context.yaml
@@ -15,7 +15,7 @@
 
 segmentItems:
   - serviceName: skywalking-agent-test-1
-    segmentSize: 10
+    segmentSize: 12
     segments:
       - segmentId: "not null"
         spans:
@@ -681,6 +681,153 @@ segmentItems:
               - { key: url, value: /memcached.php }
               - { key: http.method, value: GET }
               - { key: http.status_code, value: "200" }
+      - segmentId: "not null"
+        spans:
+          - operationName: Redis->connect
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7
+            isError: false
+            spanType: Exit
+            peer: "127.0.0.1:6379"
+            skipAnalysis: false
+            tags:
+              - key: cache.type
+                value: redis
+          - operationName: Redis->mset
+            parentSpanId: 0
+            spanId: 2
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:6379
+            skipAnalysis: false
+            tags:
+              - key: cache.type
+                value: redis
+              - key: cache.cmd
+                value: mset
+              - key: cache.op
+                value: write
+          - operationName: Redis->get
+            parentSpanId: 0
+            spanId: 3
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:6379
+            skipAnalysis: false
+            tags:
+              - key: cache.type
+                value: redis
+              - key: cache.cmd
+                value: get
+              - key: cache.op
+                value: read
+              - key: cache.key
+                value: key0
+          - operationName: Redis->get
+            parentSpanId: 0
+            spanId: 4
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7
+            isError: false
+            spanType: Exit
+            peer: 127.0.0.1:6379
+            skipAnalysis: false
+            tags:
+              - key: cache.type
+                value: redis
+              - key: cache.cmd
+                value: get
+              - key: cache.op
+                value: read
+              - key: cache.key
+                value: key1
+          - operationName: GET:/redis.succ.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: /redis.succ.php }
+              - { key: http.method, value: GET }
+              - { key: http.status_code, value: "200" }
+      - segmentId: "not null"
+        spans:
+          - operationName: Redis->connect
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7
+            isError: false
+            spanType: Exit
+            peer: "127.0.0.1:6379"
+            skipAnalysis: false
+            tags:
+              - key: cache.type
+                value: redis
+          - operationName: Redis->set
+            parentSpanId: 0
+            spanId: 2
+            spanLayer: Cache
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7
+            isError: true
+            spanType: Exit
+            peer: 127.0.0.1:6379
+            skipAnalysis: false
+            tags:
+              - key: cache.type
+                value: redis
+              - key: cache.cmd
+                value: set
+              - key: cache.op
+                value: write
+              - key: cache.key
+                value: foo
+            logs:
+              - logEvent:
+                  - { key: Exception Class, value: RedisException }
+                  - {
+                      key: Exception Message,
+                      value: NOAUTH Authentication required.,
+                    }
+          - operationName: GET:/redis.fail.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: /redis.fail.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 a15671b..bc3f876 100644
--- a/tests/e2e.rs
+++ b/tests/e2e.rs
@@ -52,6 +52,7 @@ async fn run_e2e() {
     request_fpm_predis().await;
     request_fpm_mysqli().await;
     request_fpm_memcached().await;
+    request_fpm_redis().await;
     request_swoole_curl().await;
     sleep(Duration::from_secs(3)).await;
     request_collector_validate().await;
@@ -97,6 +98,20 @@ async fn request_fpm_memcached() {
     .await;
 }
 
+async fn request_fpm_redis() {
+    request_common(
+        HTTP_CLIENT.get(format!("http://{}/redis.succ.php", PROXY_SERVER_1_ADDRESS)),
+        "ok",
+    )
+    .await;
+
+    request_common(
+        HTTP_CLIENT.get(format!("http://{}/redis.fail.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/redis.fail.php
similarity index 61%
copy from src/component.rs
copy to tests/php/fpm/redis.fail.php
index cb6ab19..be6610f 100644
--- a/src/component.rs
+++ b/tests/php/fpm/redis.fail.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,15 +13,18 @@
 // 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";
 
-//! Component ID
-//!
-//! <https://github.com/apache/skywalking/blob/014861535015745ae3f7b99acd7d14500b3b3927/oap-server/server-starter/src/main/resources/component-libraries.yml>
+try {
+    $client = new Redis();
+    $client->connect("127.0.0.1", 6379);
+    $client->set('foo', 'bar');
+} catch (RedisException $e) {
+}
 
-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;
-pub const COMPONENT_PHP_MEMCACHED_ID: i32 = 20;
+echo "ok";
diff --git a/src/component.rs b/tests/php/fpm/redis.succ.php
similarity index 61%
copy from src/component.rs
copy to tests/php/fpm/redis.succ.php
index cb6ab19..1abc743 100644
--- a/src/component.rs
+++ b/tests/php/fpm/redis.succ.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,15 +13,20 @@
 // 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";
 
-//! Component ID
-//!
-//! <https://github.com/apache/skywalking/blob/014861535015745ae3f7b99acd7d14500b3b3927/oap-server/server-starter/src/main/resources/component-libraries.yml>
+{
+    $client = new Redis();
+    $client->connect("127.0.0.1", 6379);
+    $client->auth('password');
+    $client->mSet(['key0' => 'value0', 'key1' => 'value1']);
+    Assert::same($client->get('key0'), 'value0');
+    Assert::same($client->get('key1'), 'value1');
+}
 
-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;
-pub const COMPONENT_PHP_MEMCACHED_ID: i32 = 20;
+echo "ok";