blob: 84ab071c80a7eb8fb1dcf088fc073c31e74cccb2 [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 super::{log_exception, style::ApiStyle, Plugin};
use crate::{
component::COMPONENT_PHP_MEMCACHED_ID,
context::RequestContext,
execute::{AfterExecuteHook, BeforeExecuteHook, Noop},
tag::{CacheOp, TAG_CACHE_CMD, TAG_CACHE_KEY, TAG_CACHE_OP, TAG_CACHE_TYPE},
};
use dashmap::DashMap;
use once_cell::sync::Lazy;
use phper::{
arrays::IterKey,
objects::ZObj,
values::{ExecuteData, ZVal},
};
use skywalking::{
proto::v3::SpanLayer,
trace::span::{HandleSpanObject, Span},
};
use std::{any::Any, collections::HashMap};
use tracing::{debug, error, instrument, warn};
static PEER_MAP: Lazy<DashMap<u32, String>> = Lazy::new(Default::default);
/// The method parameters is empty.
static MEMCACHE_EMPTY_METHOD_MAPPING: Lazy<HashMap<&str, TagInfo<'static>>> =
Lazy::new(|| [("flush", TagInfo::new(None, None))].into_iter().collect());
/// The method first parameter is key.
static MEMCACHE_KEY_METHOD_MAPPING: Lazy<HashMap<&str, TagInfo<'static>>> = Lazy::new(|| {
[
("set", TagInfo::new(Some("set"), Some(CacheOp::Write))),
("add", TagInfo::new(Some("add"), Some(CacheOp::Write))),
(
"replace",
TagInfo::new(Some("replace"), Some(CacheOp::Write)),
),
("get", TagInfo::new(Some("get"), Some(CacheOp::Read))),
("delete", TagInfo::new(Some("delete"), Some(CacheOp::Write))),
(
"increment",
TagInfo::new(Some("increment"), Some(CacheOp::Write)),
),
(
"decrement",
TagInfo::new(Some("decrement"), Some(CacheOp::Write)),
),
]
.into_iter()
.collect()
});
struct TagInfo<'a> {
cmd: Option<&'a str>,
op: Option<CacheOp>,
}
impl<'a> TagInfo<'a> {
#[inline]
fn new(cmd: Option<&'a str>, op: Option<CacheOp>) -> Self {
Self { cmd, op }
}
}
#[derive(Default, Clone)]
pub struct MemcachePlugin;
impl Plugin for MemcachePlugin {
fn class_names(&self) -> Option<&'static [&'static str]> {
Some(&["Memcache", "MemcachePool"])
}
fn function_name_prefix(&self) -> Option<&'static str> {
Some("memcache_")
}
fn hook(
&self, class_name: Option<&str>, function_name: &str,
) -> Option<(
Box<crate::execute::BeforeExecuteHook>,
Box<crate::execute::AfterExecuteHook>,
)> {
let lowercase_function_name = function_name.to_ascii_lowercase();
let function_name = function_name.to_owned();
match (class_name, &*lowercase_function_name) {
(Some("Memcache" | "MemcachePool"), "connect" | "addserver" | "close") => {
Some(self.hook_memcache_server(
class_name.map(ToOwned::to_owned),
function_name,
ApiStyle::OO,
))
}
(None, "memcache_add_server" | "memcache_close") => {
Some(self.hook_memcache_server(None, function_name, ApiStyle::Procedural))
}
(Some("Memcache" | "MemcachePool"), f)
if MEMCACHE_EMPTY_METHOD_MAPPING.contains_key(f) =>
{
Some(self.hook_memcache_empty_methods(
class_name.map(ToOwned::to_owned),
function_name,
ApiStyle::OO,
))
}
(None, f) if MEMCACHE_EMPTY_METHOD_MAPPING.contains_key(&f["memcache_".len()..]) => {
Some(self.hook_memcache_empty_methods(None, function_name, ApiStyle::Procedural))
}
(Some("Memcache" | "MemcachePool"), f)
if MEMCACHE_KEY_METHOD_MAPPING.contains_key(f) =>
{
Some(self.hook_memcache_key_methods(
class_name.map(ToOwned::to_owned),
function_name,
ApiStyle::OO,
))
}
(None, f) if MEMCACHE_KEY_METHOD_MAPPING.contains_key(&f["memcache_".len()..]) => {
Some(self.hook_memcache_key_methods(None, function_name, ApiStyle::Procedural))
}
_ => None,
}
}
}
impl MemcachePlugin {
fn hook_memcache_server(
&self, class_name: Option<String>, function_name: String, style: ApiStyle,
) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
(
Box::new(move |_, execute_data| {
let this = style.get_this_mut(execute_data)?;
let handle = this.handle();
PEER_MAP.remove(&handle);
debug!(
handle,
?class_name,
function_name,
"remove peers cache when server added"
);
Ok(Box::new(()))
}),
Noop::noop(),
)
}
#[instrument(skip_all)]
fn hook_memcache_empty_methods(
&self, class_name: Option<String>, function_name: String, style: ApiStyle,
) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
(
Box::new(move |request_id, execute_data| {
let tag_info = MEMCACHE_EMPTY_METHOD_MAPPING
.get(&*get_tag_key(class_name.as_deref(), &function_name))
.unwrap();
let this = style.get_this_mut(execute_data)?;
let peer = get_peer(this);
let span = create_exit_span(
style,
request_id,
class_name.as_deref(),
&function_name,
&peer,
tag_info,
None,
)?;
Ok(Box::new(span))
}),
Box::new(after_hook),
)
}
#[instrument(skip_all)]
fn hook_memcache_key_methods(
&self, class_name: Option<String>, function_name: String, style: ApiStyle,
) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
(
Box::new(move |request_id, execute_data| {
let tag_info = MEMCACHE_KEY_METHOD_MAPPING
.get(&*get_tag_key(class_name.as_deref(), &function_name))
.unwrap();
let key = style
.get_mut_parameter(execute_data, 0)
.as_z_str()
.and_then(|s| s.to_str().ok())
.map(ToOwned::to_owned)
.unwrap_or_default();
let this = style.get_this_mut(execute_data)?;
let peer = get_peer(this);
let span = create_exit_span(
style,
request_id,
class_name.as_deref(),
&function_name,
&peer,
tag_info,
Some(&key),
)?;
Ok(Box::new(span))
}),
Box::new(after_hook),
)
}
}
#[instrument(skip_all)]
fn after_hook(
_: Option<i64>, span: Box<dyn Any>, _: &mut ExecuteData, return_value: &mut ZVal,
) -> crate::Result<()> {
let mut span = span.downcast::<Span>().expect("Downcast to Span failed");
if let Some(b) = return_value.as_bool() {
if !b {
span.span_object_mut().is_error = true;
}
}
log_exception(&mut *span);
Ok(())
}
fn create_exit_span(
style: ApiStyle, request_id: Option<i64>, class_name: Option<&str>, function_name: &str,
remote_peer: &str, tag_info: &TagInfo<'_>, key: Option<&str>,
) -> anyhow::Result<Span> {
RequestContext::try_with_global_ctx(request_id, |ctx| {
let mut span = ctx.create_exit_span(
&style.generate_peer_name(class_name, function_name),
remote_peer,
);
let span_object = span.span_object_mut();
span_object.set_span_layer(SpanLayer::Cache);
span_object.component_id = COMPONENT_PHP_MEMCACHED_ID;
span_object.add_tag(TAG_CACHE_TYPE, "memcache");
if let Some(cmd) = tag_info.cmd {
span_object.add_tag(TAG_CACHE_CMD, cmd);
}
if let Some(op) = &tag_info.op {
span_object.add_tag(TAG_CACHE_OP, op.to_string());
};
if let Some(key) = key {
span_object.add_tag(TAG_CACHE_KEY, key)
}
Ok(span)
})
}
fn get_peer(this: &mut ZObj) -> String {
let handle = this.handle();
PEER_MAP
.entry(handle)
.or_insert_with(|| {
debug!(
handle,
"start to call {:?}::getExtendedStats method",
this.get_class().get_name()
);
let stats = match this.call("getExtendedStats", []) {
Ok(stats) => stats,
Err(err) => {
error!(
?err,
"call {:?}::getExtendedStats method failed",
this.get_class().get_name()
);
return "".to_owned();
}
};
stats
.as_z_arr()
.map(|arr| {
arr.iter()
.map(|(key, _)| match key {
IterKey::Index(i) => i.to_string(),
IterKey::ZStr(s) => s.to_str().unwrap_or_default().to_string(),
})
.collect::<Vec<_>>()
.join(",")
})
.unwrap_or_default()
})
.value()
.clone()
}
fn get_tag_key(class_name: Option<&str>, function_name: &str) -> String {
match class_name {
Some(_) => function_name.to_ascii_lowercase(),
None => function_name["memcache_".len()..].to_string(),
}
}