#![cfg(feature = "profiling")]
use std::error::Error;
use std::sync::OnceLock;
#[cfg(feature = "profiling-bridge-pyroscope-rs")]
use opentelemetry::trace::TraceContextExt;
fn validate_pyroscope_endpoint(endpoint: &str) -> Result<(), Box<dyn Error>> {
use url::Url;
if endpoint.starts_with("unix://") {
return Ok(());
}
if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
let url = Url::parse(endpoint)?;
if !url.username().is_empty() || url.password().is_some() {
return Err(format!(
"pyroscope endpoint must not contain userinfo; got: {endpoint} (ADR platform/0203 AC1)"
).into());
}
let host = url.host_str().unwrap_or("");
match host {
"127.0.0.1" | "::1" | "[::1]" | "localhost" => Ok(()),
_ => Err(format!(
"pyroscope endpoint must target loopback (127.0.0.1, ::1, localhost, or unix socket); \
got: {endpoint} (ADR platform/0203 AC1)"
).into()),
}
} else {
Err(
format!("pyroscope endpoint must be http://, https://, or unix://; got: {endpoint}")
.into(),
)
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ProfilingIdentity {
pub host_name: Option<String>,
pub deployment_environment: Option<String>,
pub service_version: Option<String>,
}
#[cfg(feature = "profiling-bridge-pyroscope-rs")]
impl ProfilingIdentity {
fn tag_pairs(&self) -> Vec<(&'static str, &str)> {
let mut pairs = Vec::new();
if let Some(host) = self.host_name.as_deref().filter(|s| !s.is_empty()) {
pairs.push(("host_name", host));
}
if let Some(env) = self
.deployment_environment
.as_deref()
.filter(|s| !s.is_empty())
{
pairs.push(("deployment_environment", env));
}
if let Some(version) = self.service_version.as_deref().filter(|s| !s.is_empty()) {
pairs.push(("service_version", version));
}
pairs
}
}
pub struct ProfilingHandle {
#[cfg(feature = "profiling-bridge-pyroscope-rs")]
agent: Option<pyroscope::PyroscopeAgent<pyroscope::pyroscope::PyroscopeAgentRunning>>,
#[cfg(feature = "profiling-memory-jemalloc")]
memory_agent: Option<pyroscope::PyroscopeAgent<pyroscope::pyroscope::PyroscopeAgentRunning>>,
}
#[cfg(feature = "profiling-bridge-pyroscope-rs")]
impl Drop for ProfilingHandle {
fn drop(&mut self) {
if let Some(agent) = self.agent.take() {
let _ = agent.stop();
}
#[cfg(feature = "profiling-memory-jemalloc")]
if let Some(agent) = self.memory_agent.take() {
let _ = agent.stop();
}
}
}
#[cfg(feature = "profiling-bridge-pyroscope-rs")]
type BoxedTagFn = Box<dyn Fn(String, String) -> pyroscope::Result<()> + Send + Sync>;
#[cfg(feature = "profiling-bridge-pyroscope-rs")]
static PROFILING_TAG_FNS: OnceLock<(BoxedTagFn, BoxedTagFn)> = OnceLock::new();
#[cfg(feature = "profiling-bridge-pyroscope-rs")]
static PROFILING_STARTED: OnceLock<()> = OnceLock::new();
#[cfg(feature = "profiling-bridge-pyroscope-rs")]
pub(crate) fn start_pyroscope_bridge(
service_name: &str,
pyroscope_endpoint: &str,
identity: &ProfilingIdentity,
) -> Result<Option<ProfilingHandle>, Box<dyn Error>> {
use pyroscope::backend::{BackendConfig, PprofConfig, pprof_backend};
validate_pyroscope_endpoint(pyroscope_endpoint)?;
if PROFILING_STARTED.set(()).is_err() {
return Ok(None);
}
let tags = identity.tag_pairs();
let agent = pyroscope::pyroscope::PyroscopeAgentBuilder::new(
pyroscope_endpoint,
service_name,
100,
"pyroscope-rs",
env!("CARGO_PKG_VERSION"),
pprof_backend(PprofConfig { sample_rate: 100 }, BackendConfig::default()),
)
.tags(tags.clone())
.build()?
.start()?;
let (add_tag, remove_tag) = agent.tag_wrapper();
PROFILING_TAG_FNS
.set((Box::new(add_tag), Box::new(remove_tag)))
.ok();
Ok(Some(ProfilingHandle {
agent: Some(agent),
#[cfg(feature = "profiling-memory-jemalloc")]
memory_agent: start_memory_agent(service_name, pyroscope_endpoint, &tags)?,
}))
}
#[cfg(feature = "profiling-memory-jemalloc")]
fn start_memory_agent(
service_name: &str,
pyroscope_endpoint: &str,
tags: &[(&'static str, &str)],
) -> Result<
Option<pyroscope::PyroscopeAgent<pyroscope::pyroscope::PyroscopeAgentRunning>>,
Box<dyn Error>,
> {
use pyroscope::backend::jemalloc::jemalloc_backend;
let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
pyroscope::pyroscope::PyroscopeAgentBuilder::new(
pyroscope_endpoint,
service_name,
100,
"pyroscope-rs",
env!("CARGO_PKG_VERSION"),
jemalloc_backend(),
)
.tags(tags.to_vec())
.build()
}));
let agent = match built {
Ok(Ok(agent)) => agent,
Ok(Err(e)) => {
tracing::warn!(
error = %e,
"jemalloc heap profiling unavailable โ continuing without it; \
check the global allocator is jemalloc and prof:true,prof_active:true is set"
);
return Ok(None);
}
Err(_) => {
tracing::warn!(
"jemalloc heap profiling unavailable โ this process is not using \
jemalloc as its global allocator; continuing without it"
);
return Ok(None);
}
};
match agent.start() {
Ok(running) => {
tracing::info!("jemalloc heap profiling started");
Ok(Some(running))
}
Err(e) => {
tracing::warn!(error = %e, "jemalloc heap profiling failed to start โ continuing without it");
Ok(None)
}
}
}
#[cfg(all(feature = "profiling", not(feature = "profiling-bridge-pyroscope-rs")))]
pub(crate) fn start_pyroscope_bridge(
_service_name: &str,
_pyroscope_endpoint: &str,
_identity: &ProfilingIdentity,
) -> Result<Option<ProfilingHandle>, Box<dyn Error>> {
Ok(None)
}
#[cfg(feature = "profiling-bridge-pyroscope-rs")]
pub struct ProfilingTagLayer;
#[cfg(feature = "profiling-bridge-pyroscope-rs")]
impl<S> tracing_subscriber::Layer<S> for ProfilingTagLayer
where
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
fn on_enter(&self, _id: &tracing::span::Id, _ctx: tracing_subscriber::layer::Context<'_, S>) {
if let Some((add_tag, _)) = PROFILING_TAG_FNS.get() {
let cx = opentelemetry::Context::current();
let span_ref = cx.span();
let span_context = span_ref.span_context();
if span_context.is_valid() {
let trace_id = span_context.trace_id();
let span_id = span_context.span_id();
let _ = add_tag("trace_id".to_string(), format!("{trace_id:x}"));
let _ = add_tag("span_id".to_string(), format!("{span_id:x}"));
}
}
}
fn on_exit(&self, _id: &tracing::span::Id, _ctx: tracing_subscriber::layer::Context<'_, S>) {
if let Some((_, remove_tag)) = PROFILING_TAG_FNS.get() {
let cx = opentelemetry::Context::current();
let span_ref = cx.span();
let span_context = span_ref.span_context();
if span_context.is_valid() {
let trace_id = span_context.trace_id();
let span_id = span_context.span_id();
let _ = remove_tag("trace_id".to_string(), format!("{trace_id:x}"));
let _ = remove_tag("span_id".to_string(), format!("{span_id:x}"));
}
}
}
}
#[cfg(all(test, feature = "profiling-bridge-pyroscope-rs"))]
mod tests {
use super::*;
#[test]
fn start_bridge_with_nonexistent_server() {
let result = start_pyroscope_bridge(
"test-svc",
"http://localhost:4040",
&ProfilingIdentity::default(),
);
assert!(
result.is_ok(),
"pyroscope agent start() is lazy and does not eagerly connect"
);
if let Ok(Some(_handle)) = result {
}
}
#[test]
fn start_bridge_multiple_times_ignores_second() {
let result1 = start_pyroscope_bridge(
"test-svc-1",
"http://localhost:4040",
&ProfilingIdentity::default(),
);
assert!(result1.is_ok());
let result2 = start_pyroscope_bridge(
"test-svc-2",
"http://localhost:4041",
&ProfilingIdentity::default(),
);
assert!(result2.is_ok());
assert!(result2.unwrap().is_none());
}
#[test]
fn validate_endpoint_accepts_loopback_ipv4() {
assert!(validate_pyroscope_endpoint("http://127.0.0.1:4040").is_ok());
}
#[test]
fn validate_endpoint_accepts_loopback_ipv6() {
assert!(validate_pyroscope_endpoint("http://[::1]:4040").is_ok());
}
#[test]
fn validate_endpoint_accepts_localhost() {
assert!(validate_pyroscope_endpoint("http://localhost:4040").is_ok());
}
#[test]
fn validate_endpoint_accepts_https_loopback() {
assert!(validate_pyroscope_endpoint("https://127.0.0.1:4040").is_ok());
}
#[test]
fn validate_endpoint_rejects_routable_ipv4() {
assert!(validate_pyroscope_endpoint("http://10.0.0.1:4040").is_err());
}
#[test]
fn validate_endpoint_rejects_userinfo_bypass() {
assert!(validate_pyroscope_endpoint("http://127.0.0.1:4040@evil.com/").is_err());
}
#[test]
fn validate_endpoint_rejects_userinfo_with_password() {
assert!(validate_pyroscope_endpoint("http://user:pass@localhost:4040").is_err());
}
#[test]
fn validate_endpoint_rejects_unix_socket_check() {
assert!(validate_pyroscope_endpoint("unix:///var/run/profiling.sock").is_ok());
}
}
#[cfg(all(
test,
feature = "profiling",
not(feature = "profiling-bridge-pyroscope-rs")
))]
mod tests_no_bridge {
use super::*;
#[test]
fn start_bridge_returns_none() {
let result = start_pyroscope_bridge(
"test-svc",
"http://localhost:4040",
&ProfilingIdentity::default(),
);
assert!(result.is_ok());
if let Ok(handle) = result {
assert!(handle.is_none());
}
}
}