blob: bbedfa35e4949c65ce94760d0228f13cd6bd5015 [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::Plugin;
use crate::{
component::COMPONENT_PHP_PDO_ID,
context::RequestContext,
execute::{get_this_mut, validate_num_args, AfterExecuteHook, BeforeExecuteHook, Noop},
tag::{TAG_DB_STATEMENT, TAG_DB_TYPE},
};
use anyhow::Context;
use dashmap::DashMap;
use once_cell::sync::Lazy;
use phper::{
arrays::ZArr,
classes::ClassEntry,
objects::ZObj,
sys,
values::{ExecuteData, ZVal},
};
use skywalking::{skywalking_proto::v3::SpanLayer, trace::span::Span};
use std::{any::Any, str::FromStr};
use tracing::{debug, warn};
static DSN_MAP: Lazy<DashMap<u32, Dsn>> = Lazy::new(Default::default);
static DTOR_MAP: Lazy<DashMap<u32, sys::zend_object_dtor_obj_t>> = Lazy::new(Default::default);
#[derive(Default, Clone)]
pub struct PdoPlugin;
impl Plugin for PdoPlugin {
fn class_names(&self) -> Option<&'static [&'static str]> {
Some(&["PDO", "PDOStatement"])
}
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("PDO"), "__construct") => Some(self.hook_pdo_construct()),
(Some("PDO"), f)
if [
"exec",
"query",
"prepare",
"commit",
"begintransaction",
"rollback",
]
.contains(&f) =>
{
Some(self.hook_pdo_methods(function_name))
}
(Some("PDOStatement"), f)
if ["execute", "fetch", "fetchAll", "fetchColumn", "fetchObject"].contains(&f) =>
{
Some(self.hook_pdo_statement_methods(function_name))
}
_ => None,
}
}
}
impl PdoPlugin {
fn hook_pdo_construct(&self) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
(
Box::new(|_, execute_data| {
validate_num_args(execute_data, 1)?;
let this = get_this_mut(execute_data)?;
let handle = this.handle();
hack_dtor(this, Some(pdo_dtor));
let dsn = execute_data.get_parameter(0);
let dsn = dsn.as_z_str().context("dsn isn't str")?.to_str()?;
debug!(dsn, handle, "construct PDO");
let dsn: Dsn = dsn.parse()?;
debug!(?dsn, "parse PDO dsn");
DSN_MAP.insert(handle, dsn);
Ok(Box::new(()))
}),
Noop::noop(),
)
}
fn hook_pdo_methods(
&self, function_name: &str,
) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
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 PDO method");
let mut span = with_dsn(handle, |dsn| {
create_exit_span_with_dsn(request_id, "PDO", &function_name, dsn)
})?;
if execute_data.num_args() >= 1 {
if let Some(statement) = execute_data.get_parameter(0).as_z_str() {
span.add_tag(TAG_DB_STATEMENT, statement.to_str()?);
}
}
Ok(Box::new(span) as _)
}),
Box::new(after_hook),
)
}
fn hook_pdo_statement_methods(
&self, function_name: &str,
) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
let function_name = function_name.to_owned();
(
Box::new(move |request_id, execute_data| {
let this = get_this_mut(execute_data)?;
let handle = this.handle();
debug!(handle, function_name, "call PDOStatement method");
let mut span = with_dsn(handle, |dsn| {
create_exit_span_with_dsn(request_id, "PDOStatement", &function_name, dsn)
})?;
if let Some(query) = this.get_property("queryString").as_z_str() {
span.add_tag(TAG_DB_STATEMENT, query.to_str()?);
} else {
warn!("PDOStatement queryString is empty");
}
Ok(Box::new(span) as _)
}),
Box::new(after_hook),
)
}
}
fn hack_dtor(this: &mut ZObj, new_dtor: sys::zend_object_dtor_obj_t) {
let handle = this.handle();
unsafe {
let ori_dtor = (*(*this.as_mut_ptr()).handlers).dtor_obj;
DTOR_MAP.insert(handle, ori_dtor);
(*((*this.as_mut_ptr()).handlers as *mut sys::zend_object_handlers)).dtor_obj = new_dtor;
}
}
unsafe extern "C" fn pdo_dtor(object: *mut sys::zend_object) {
debug!("call PDO dtor");
dtor(object);
}
unsafe extern "C" fn pdo_statement_dtor(object: *mut sys::zend_object) {
debug!("call PDOStatement dtor");
dtor(object);
}
unsafe extern "C" fn dtor(object: *mut sys::zend_object) {
let handle = ZObj::from_ptr(object).handle();
DSN_MAP.remove(&handle);
if let Some((_, Some(dtor))) = DTOR_MAP.remove(&handle) {
dtor(object);
}
}
fn after_hook(
_: Option<i64>, span: Box<dyn Any>, execute_data: &mut ExecuteData, return_value: &mut ZVal,
) -> crate::Result<()> {
if let Some(b) = return_value.as_bool() {
if !b {
return after_hook_when_false(
get_this_mut(execute_data)?,
&mut span.downcast::<Span>().unwrap(),
);
}
} else if let Some(obj) = return_value.as_mut_z_obj() {
let cls = obj.get_class();
let pdo_cls = ClassEntry::from_globals("PDOStatement").unwrap();
if cls.is_instance_of(pdo_cls) {
return after_hook_when_pdo_statement(get_this_mut(execute_data)?, obj);
} else {
let cls = cls.get_name().to_str()?;
debug!(cls, "not a subclass of PDOStatement");
}
}
Ok(())
}
fn after_hook_when_false(this: &mut ZObj, span: &mut Span) -> crate::Result<()> {
let info = this.call("errorInfo", [])?;
let info = info.as_z_arr().context("errorInfo isn't array")?;
let state = get_error_info_item(info, 0)?.expect_z_str()?.to_str()?;
let code = {
let code = get_error_info_item(info, 1)?;
// PDOStatement::fetch
// In all cases, false is returned on failure or if there are no more rows.
if code.get_type_info().is_null() {
return Ok(());
}
&code.expect_long()?.to_string()
};
let error = get_error_info_item(info, 2)?.expect_z_str()?.to_str()?;
let mut span_object = span.span_object_mut();
span_object.is_error = true;
span_object.add_log([("SQLSTATE", state), ("Error Code", code), ("Error", error)]);
Ok(())
}
fn after_hook_when_pdo_statement(pdo: &mut ZObj, pdo_statement: &mut ZObj) -> crate::Result<()> {
let dsn = DSN_MAP
.get(&pdo.handle())
.map(|r| r.value().clone())
.context("DSN not found")?;
let handle = pdo_statement.handle();
debug!(?dsn, handle, "Hook PDOStatement class");
DSN_MAP.insert(handle, dsn);
hack_dtor(pdo_statement, Some(pdo_statement_dtor));
Ok(())
}
fn get_error_info_item(info: &ZArr, i: u64) -> anyhow::Result<&ZVal> {
info.get(i)
.with_context(|| format!("errorInfo[{}] not exists", i))
}
fn create_exit_span_with_dsn(
request_id: Option<i64>, class_name: &str, function_name: &str, dsn: &Dsn,
) -> anyhow::Result<Span> {
RequestContext::try_with_global_ctx(request_id, |ctx| {
let mut span =
ctx.create_exit_span(&format!("{}->{}", class_name, function_name), &dsn.peer);
let mut span_object = span.span_object_mut();
span_object.set_span_layer(SpanLayer::Database);
span_object.component_id = COMPONENT_PHP_PDO_ID;
span_object.add_tag(TAG_DB_TYPE, &dsn.db_type);
span_object.add_tag("db.data_source", &dsn.data_source);
drop(span_object);
Ok(span)
})
}
fn with_dsn<T>(handle: u32, f: impl FnOnce(&Dsn) -> anyhow::Result<T>) -> anyhow::Result<T> {
DSN_MAP
.get(&handle)
.map(|r| f(r.value()))
.context("dsn not exists")?
}
#[derive(Debug, Clone)]
struct Dsn {
db_type: String,
data_source: String,
peer: String,
}
impl FromStr for Dsn {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut ss = s.splitn(2, ':');
let db_type = ss.next().context("unknown db type")?.to_owned();
let data_source = ss.next().context("unknown datasource")?.to_owned();
let mut host = "unknown";
let mut port = match &*db_type {
"mysql" => "3306",
"oci" => "1521", // Oracle
"sqlsrv" => "1433",
"pgsql" => "5432",
_ => "0",
};
let ss = data_source.split(';');
for s in ss {
if s.is_empty() {
continue;
}
let mut kv = s.splitn(2, '=');
let k = kv.next().context("unknown key")?;
let v = kv.next().context("unknown value")?;
// TODO compact the fields rather than mysql.
match k {
"host" => {
host = v;
}
"port" => {
port = v;
}
_ => {}
}
}
let peer = if host.contains(':') {
host.to_string()
} else {
host.to_string() + ":" + port
};
Ok(Dsn {
db_type,
data_source,
peer,
})
}
}