blob: e376d070cf545ca283fcbf13f2acdab78b3606a1 [file] [log] [blame]
// 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(())
}