Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ jobs:

- name: Install dependencies for Linux
if: matrix.os == 'ubuntu-24.04'
run: sudo apt-get update && sudo apt-get install -y llvm-18-dev libclang-18-dev protobuf-compiler libsasl2-dev
run: sudo apt-get update && sudo apt-get install -y llvm-18-dev libclang-18-dev protobuf-compiler libsasl2-dev librabbitmq-dev

Comment thread
jmjoy marked this conversation as resolved.
- name: Install protobuf for Macos
if: matrix.os == 'macos-14'
Expand All @@ -110,7 +110,7 @@ jobs:
bcmath, calendar, ctype, dom, exif, gettext, iconv, intl, json, mbstring,
mysqli, mysqlnd, opcache, pdo, pdo_mysql, phar, posix, readline, redis,
memcached, swoole-${{ matrix.flag.swoole_version }}, xml, xmlreader, xmlwriter,
yaml, zip, mongodb, memcache
yaml, zip, mongodb, memcache, amqp

- name: Setup php-fpm for Linux
if: matrix.os == 'ubuntu-24.04'
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ SkyWalking PHP Agent requires SkyWalking 8.4+ and PHP 7.2+
* [x] [MySQL Improved](https://www.php.net/manual/en/book.mysqli.php)
* [x] [Memcached](https://www.php.net/manual/en/book.memcached.php)
* [x] [phpredis](https://github.com/phpredis/phpredis)
* [ ] [php-amqp](https://github.com/php-amqp/php-amqp)
* [x] [php-amqp](https://github.com/php-amqp/php-amqp) for Message Queuing Producer
* [ ] [php-rdkafka](https://github.com/arnaud-lb/php-rdkafka)
* [x] [predis](https://github.com/predis/predis)
* [x] [php-amqplib](https://github.com/php-amqplib/php-amqplib) for Message Queuing Producer
Expand Down
2 changes: 2 additions & 0 deletions src/plugin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

mod plugin_amqp;
mod plugin_amqplib;
mod plugin_curl;
mod plugin_memcache;
Expand Down Expand Up @@ -48,6 +49,7 @@ static PLUGINS: Lazy<Vec<Box<DynPlugin>>> = Lazy::new(|| {
Box::<plugin_predis::PredisPlugin>::default(),
Box::<plugin_memcached::MemcachedPlugin>::default(),
Box::<plugin_redis::RedisPlugin>::default(),
Box::<plugin_amqp::AmqpPlugin>::default(),
Box::<plugin_amqplib::AmqplibPlugin>::default(),
Box::<plugin_mongodb::MongodbPlugin>::default(),
Box::<plugin_memcache::MemcachePlugin>::default(),
Expand Down
160 changes: 160 additions & 0 deletions src/plugin/plugin_amqp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// 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, log_exception};
use crate::{
component::COMPONENT_AMQP_PRODUCER_ID,
context::{RequestContext, SW_HEADER},
execute::{AfterExecuteHook, BeforeExecuteHook, get_this_mut, validate_num_args},
tag::{TAG_MQ_BROKER, TAG_MQ_QUEUE, TAG_MQ_TOPIC},
};
use phper::{objects::ZObj, values::ExecuteData};
use skywalking::{
proto::v3::SpanLayer,
trace::span::{HandleSpanObject, Span},
};

#[derive(Default, Clone)]
pub struct AmqpPlugin;

impl Plugin for AmqpPlugin {
fn class_names(&self) -> Option<&'static [&'static str]> {
Some(&["AMQPExchange"])
}

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 @ "AMQPExchange"), function_name @ "publish") => {
Some(self.hook_exchange_publish(class_name, function_name))
}
_ => None,
}
}
}

impl AmqpPlugin {
fn hook_exchange_publish(
&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| {
validate_num_args(execute_data, 2)?;

let this = get_this_mut(execute_data)?;
Comment thread
jmjoy marked this conversation as resolved.

let peer = Self::get_peer(this)?;

let exchange = this
.call("getName", [])
.ok()
.and_then(|v| {
v.as_z_str()
.and_then(|s| s.to_str().ok())
.map(ToOwned::to_owned)
})
.unwrap_or_default();

let routing_key = execute_data
.get_parameter(1)
.as_z_str()
.and_then(|s| s.to_str().ok())
.map(ToOwned::to_owned)
.unwrap_or_else(|| "unknown".to_owned());

let span = Self::create_exit_span(
request_id,
&class_name,
&function_name,
&peer,
&exchange,
&routing_key,
)?;

Self::inject_sw_header(request_id, execute_data, &peer)?;

Ok(Box::new(span))
}),
Box::new(move |_, span, _, _| {
let mut span = span.downcast::<Span>().unwrap();
log_exception(&mut *span);
Ok(())
}),
)
}

fn get_peer(this: &mut ZObj) -> crate::Result<String> {
let mut channel = this.call("getChannel", [])?;
let channel = channel
.expect_mut_z_obj()
.map_err(|e| anyhow::anyhow!("channel isn't object: {}", e))?;
let mut connection = channel.call("getConnection", [])?;
let connection = connection
.expect_mut_z_obj()
.map_err(|e| anyhow::anyhow!("connection isn't object: {}", e))?;
let host = connection.call("getHost", [])?;
let host = host
.expect_z_str()
.map_err(|e| anyhow::anyhow!("host isn't string: {}", e))?
.to_str()?;
let port = connection.call("getPort", [])?;
let port = port.as_long().unwrap_or_default();
Ok(format!("{}:{}", host, port))
}

fn create_exit_span(
request_id: Option<i64>, class_name: &str, function_name: &str, peer: &str, exchange: &str,
routing_key: &str,
) -> crate::Result<Span> {
let mut span = RequestContext::try_with_global_ctx(request_id, |ctx| {
Ok(ctx.create_exit_span(&format!("{}->{}", class_name, function_name), peer))
})?;

let span_object = span.span_object_mut();
span_object.set_span_layer(SpanLayer::Mq);
span_object.component_id = COMPONENT_AMQP_PRODUCER_ID;
span_object.add_tag(TAG_MQ_BROKER, peer);
span_object.add_tag(TAG_MQ_TOPIC, exchange);
span_object.add_tag(TAG_MQ_QUEUE, routing_key);

Ok(span)
}

fn inject_sw_header(
request_id: Option<i64>, execute_data: &mut ExecuteData, peer: &str,
) -> crate::Result<()> {
let sw_header = RequestContext::try_get_sw_header(request_id, peer)?;

let num_args = execute_data.num_args();
if num_args > 3 {
let headers = execute_data.get_mut_parameter(3);
if let Some(headers) = headers.as_mut_z_arr() {
headers.insert(SW_HEADER, sw_header);
}
}

Ok(())
}
}
64 changes: 63 additions & 1 deletion tests/data/expected_context.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

segmentItems:
- serviceName: skywalking-agent-test-1
segmentSize: 20
segmentSize: 21
segments:
- segmentId: "not null"
spans:
Expand Down Expand Up @@ -1209,6 +1209,68 @@ segmentItems:
- { key: url, value: "http://127.0.0.1:9011/rabbitmq.php" }
- { key: http.method, value: GET }
- { key: http.status_code, value: "200" }
- segmentId: "not null"
spans:
- operationName: AMQPExchange->publish
parentSpanId: 0
spanId: 1
spanLayer: MQ
startTime: gt 0
endTime: gt 0
componentId: 144
isError: false
spanType: Exit
peer: 127.0.0.1:5672
skipAnalysis: false
tags:
- { key: mq.broker, value: "127.0.0.1:5672" }
- { key: mq.topic, value: "" }
- { key: mq.queue, value: queue_test }
- operationName: AMQPExchange->publish
parentSpanId: 0
spanId: 2
spanLayer: MQ
startTime: gt 0
endTime: gt 0
componentId: 144
isError: false
spanType: Exit
peer: 127.0.0.1:5672
skipAnalysis: false
tags:
- { key: mq.broker, value: "127.0.0.1:5672" }
- { key: mq.topic, value: exchange_test }
- { key: mq.queue, value: routing_test }
- operationName: AMQPExchange->publish
parentSpanId: 0
spanId: 3
spanLayer: MQ
startTime: gt 0
endTime: gt 0
componentId: 144
isError: false
spanType: Exit
peer: 127.0.0.1:5672
skipAnalysis: false
tags:
- { key: mq.broker, value: "127.0.0.1:5672" }
- { key: mq.topic, value: "" }
- { key: mq.queue, value: not_exists }
- operationName: GET:/amqp.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: "http://127.0.0.1:9011/amqp.php" }
- { key: http.method, value: GET }
- { key: http.status_code, value: "200" }
- segmentId: "not null"
spans:
- operationName: "MongoDB\\Driver\\Manager->executeCommand"
Expand Down
9 changes: 9 additions & 0 deletions tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ async fn run_e2e() {
request_fpm_memcached().await;
request_fpm_redis().await;
request_fpm_rabbitmq().await;
request_fpm_amqp().await;
request_fpm_mongodb().await;
request_fpm_memcache().await;
request_fpm_monolog().await;
Expand Down Expand Up @@ -152,6 +153,14 @@ async fn request_fpm_rabbitmq() {
.await;
}

async fn request_fpm_amqp() {
request_common(
HTTP_CLIENT.get(format!("http://{}/amqp.php", PROXY_SERVER_1_ADDRESS)),
"ok",
)
.await;
}

async fn request_fpm_mongodb() {
request_common(
HTTP_CLIENT.get(format!("http://{}/mongodb.php", PROXY_SERVER_1_ADDRESS)),
Expand Down
51 changes: 51 additions & 0 deletions tests/php/fpm/amqp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?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.
// 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.


$connection = new AMQPConnection(['host' => '127.0.0.1', 'port' => 5672, 'login' => 'guest', 'password' => 'guest']);
$connection->connect();
$channel = new AMQPChannel($connection);

$queue = new AMQPQueue($channel);
$queue->setName('queue_test');
$queue->setFlags(AMQP_NOPARAM);
$queue->declareQueue();

$exchange = new AMQPExchange($channel);
$exchange->setName('exchange_test');
$exchange->setType(AMQP_EX_TYPE_DIRECT);
$exchange->declareExchange();

$queue->bind('exchange_test', 'routing_test');

{
$exchange = new AMQPExchange($channel);
$exchange->publish('Hello World!', 'queue_test', AMQP_NOPARAM, []);
}

{
$exchange = new AMQPExchange($channel);
$exchange->setName('exchange_test');
$exchange->publish('Hello World!', 'routing_test', AMQP_NOPARAM, []);
}

{
$exchange = new AMQPExchange($channel);
$exchange->publish('Hello World!', 'not_exists', AMQP_NOPARAM, ['foo' => 'bar']);
}
Comment on lines +108 to +126

echo "ok";
Loading