#[cfg(any(feature = "daemon", feature = "tempo", feature = "jaeger-query"))]
pub mod auth_header;
pub mod jaeger;
#[cfg(feature = "jaeger-query")]
pub mod jaeger_query;
pub mod json;
#[cfg(any(feature = "tempo", feature = "jaeger-query"))]
pub mod lookback;
pub mod mysql_stat;
pub mod otlp;
pub mod pg_stat;
#[cfg(feature = "tempo")]
pub mod tempo;
#[cfg(any(feature = "tempo", feature = "jaeger-query"))]
pub(crate) mod url_enc;
pub mod zipkin;
use crate::event::SpanEvent;
const NON_SQL_DB_SYSTEMS: &[&str] = &[
"redis",
"memcached",
"mongodb",
"cassandra",
"dynamodb",
"couchbase",
"couchdb",
"elasticsearch",
"opensearch",
"neo4j",
"hbase",
"geode",
"influxdb",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TagIoKind {
Sql,
HttpOut,
}
impl TagIoKind {
pub(crate) const fn event_type(self) -> crate::event::EventType {
match self {
Self::Sql => crate::event::EventType::Sql,
Self::HttpOut => crate::event::EventType::HttpOut,
}
}
}
#[must_use]
pub(crate) fn is_non_sql_db_system(system: &str) -> bool {
NON_SQL_DB_SYSTEMS
.iter()
.any(|s| system.eq_ignore_ascii_case(s))
}
const SQL_DB_SYSTEMS: &[&str] = &[
"postgresql",
"mysql",
"mariadb",
"mssql",
"oracle",
"db2",
"sqlite",
"h2",
"hsqldb",
"derby",
"cockroachdb",
"clickhouse",
"spanner",
"redshift",
"snowflake",
"bigquery",
"trino",
"presto",
"vertica",
"teradata",
"hive",
"sql",
];
#[must_use]
pub(crate) fn is_sql_db_system(system: &str) -> bool {
SQL_DB_SYSTEMS
.iter()
.any(|s| system.eq_ignore_ascii_case(s))
}
#[must_use]
pub(crate) fn canonical_db_system(system: &str) -> &str {
const ALIASES: &[(&str, &str)] = &[
("postgres", "postgresql"),
("sqlserver", "mssql"),
("sql server", "mssql"),
("microsoft.sql_server", "mssql"),
("oracle.db", "oracle"),
("ibm.db2", "db2"),
("gcp.spanner", "spanner"),
("aws.redshift", "redshift"),
("aws.dynamodb", "dynamodb"),
];
for &(alias, canonical) in ALIASES {
if system.eq_ignore_ascii_case(alias) {
return canonical;
}
}
system
}
pub(crate) const ANCESTOR_WALK_MAX_DEPTH: usize = 8;
const CODE_FRAME_SEPARATORS: [char; 4] = ['.', '\\', ':', '#'];
#[must_use]
pub(crate) fn namespace_from_qualified_name(fq: &str) -> Option<&str> {
fq.rsplit_once('.')
.or_else(|| fq.rsplit_once('\\'))
.map(|(ns, _)| ns)
}
fn usable_code_frame_part(s: &str) -> Option<&str> {
let s = s.trim();
(!s.is_empty() && !s.contains(char::is_control)).then_some(s)
}
const FRAMEWORK_FRAME_PREFIXES: &[&str] = &[
"Symfony\\",
"Illuminate\\",
"Laravel\\",
"Doctrine\\",
"Slim\\",
"DI\\",
"PDO::",
"PDOStatement::",
"org.springframework.",
"org.apache.",
"org.hibernate.",
"org.eclipse.",
"jakarta.",
"javax.",
"java.",
"io.quarkus.",
"io.vertx.",
"io.helidon.",
"io.netty.",
"com.zaxxer.",
"ActiveRecord::",
"ActionController::",
"ActiveSupport::",
"Rack::",
];
#[must_use]
pub(crate) fn code_frame_endpoint(
namespace: Option<&str>,
function: Option<&str>,
) -> Option<String> {
let frame = match (
namespace.and_then(usable_code_frame_part),
function.and_then(usable_code_frame_part),
) {
(Some(ns), Some(f))
if f.len() > ns.len()
&& f.starts_with(ns)
&& f[ns.len()..].starts_with(CODE_FRAME_SEPARATORS) =>
{
f.to_string()
}
(Some(ns), Some(f)) => format!("{ns}{}{f}", frame_separator(ns)),
(Some(ns), None) => ns.to_string(),
(None, Some(f)) if f.contains(CODE_FRAME_SEPARATORS) => f.to_string(),
_ => return None,
};
if FRAMEWORK_FRAME_PREFIXES
.iter()
.any(|p| frame.starts_with(p))
{
return None;
}
let frame = if frame.contains('#') {
frame.replace('#', ".")
} else {
frame
};
(!frame.contains(['?', '@'])).then_some(frame)
}
fn frame_separator(namespace: &str) -> &'static str {
if namespace.contains('\\') || namespace.contains("::") {
"::"
} else {
"."
}
}
pub trait IngestSource {
type Error: std::error::Error;
fn ingest(&self, raw: &[u8]) -> Result<Vec<SpanEvent>, Self::Error>;
}
#[cfg(test)]
mod tests {
use super::{
NON_SQL_DB_SYSTEMS, SQL_DB_SYSTEMS, canonical_db_system, code_frame_endpoint,
is_non_sql_db_system, is_sql_db_system,
};
#[test]
fn namespace_derivation_matches_each_language() {
use super::namespace_from_qualified_name as ns;
assert_eq!(ns("com.foo.PurgeJob.execute"), Some("com.foo.PurgeJob"));
assert_eq!(ns("Slim\\App::handle"), Some("Slim"));
assert_eq!(
code_frame_endpoint(ns("Slim\\App::handle"), Some("Slim\\App::handle")),
None,
"framework frame, denied whichever way the namespace derives"
);
assert_eq!(ns("myapp::worker::run"), None);
assert_eq!(ns("execute"), None);
}
#[test]
fn code_frame_endpoint_joins_legacy_pair() {
assert_eq!(
code_frame_endpoint(Some("com.foo.PurgeJob"), Some("execute")).as_deref(),
Some("com.foo.PurgeJob.execute")
);
}
#[test]
fn code_frame_endpoint_does_not_repeat_a_qualified_name() {
for (ns, f) in [
("com.foo.OrderService", "com.foo.OrderService.findItems"),
("App\\Jobs\\PurgeJob", "App\\Jobs\\PurgeJob::handle"),
("myapp::worker", "myapp::worker::run"),
] {
assert_eq!(code_frame_endpoint(Some(ns), Some(f)).as_deref(), Some(f));
}
}
#[test]
fn code_frame_endpoint_joins_with_the_namespace_separator() {
for (ns, f, expected) in [
(
"App\\Jobs\\PurgeJob",
"handle",
"App\\Jobs\\PurgeJob::handle",
),
("myapp::worker", "run", "myapp::worker::run"),
("com.foo.PurgeJob", "execute", "com.foo.PurgeJob.execute"),
] {
assert_eq!(
code_frame_endpoint(Some(ns), Some(f)).as_deref(),
Some(expected)
);
}
}
#[test]
fn code_frame_endpoint_rejects_framework_kernel_frames() {
let cases = [
(
None,
Some("Symfony\\Component\\HttpKernel\\HttpKernel::handle"),
),
(None, Some("Illuminate\\Foundation\\Http\\Kernel::handle")),
(None, Some("PDOStatement::execute")),
(Some("Slim\\App"), Some("handle")),
(
Some("org.apache.catalina.core.StandardWrapper"),
Some("invoke"),
),
];
for (ns, f) in cases {
assert_eq!(code_frame_endpoint(ns, f), None, "{ns:?} {f:?}");
}
assert_eq!(
code_frame_endpoint(Some("App\\Jobs\\PurgeJob"), Some("handle")).as_deref(),
Some("App\\Jobs\\PurgeJob::handle")
);
}
#[test]
fn code_frame_endpoint_rejects_what_the_sanitizer_would_truncate() {
for (ns, f) in [
(Some("Order"), Some("valid?")),
(Some("App\\Jobs"), Some("class@anonymous")),
] {
assert_eq!(code_frame_endpoint(ns, f), None, "{ns:?} {f:?}");
}
}
#[test]
fn code_frame_endpoint_rewrites_hash_to_dot() {
assert_eq!(
code_frame_endpoint(None, Some("MyClass#method")).as_deref(),
Some("MyClass.method")
);
}
#[test]
fn code_frame_endpoint_rejects_unusable_input() {
let cases = [
(None, None, "nothing at all"),
(Some(""), None, "blank namespace"),
(Some(""), Some("execute"), "blank namespace, leading dot"),
(Some(" "), Some(""), "whitespace only"),
(None, Some("execute"), "bare function name, too generic"),
(None, Some("run\u{1b}[2J"), "control characters"),
(
Some("com.foo\u{7}"),
Some("run"),
"control char in namespace",
),
];
for (ns, f, desc) in cases {
assert_eq!(code_frame_endpoint(ns, f), None, "{desc}");
}
}
#[test]
fn sql_and_non_sql_lists_are_disjoint() {
for s in SQL_DB_SYSTEMS {
assert!(!is_non_sql_db_system(s), "{s} is in both lists");
}
for s in NON_SQL_DB_SYSTEMS {
assert!(!is_sql_db_system(s), "{s} is in both lists");
}
}
#[test]
fn canonical_aliases_resolve_to_a_classified_system() {
let alias_inputs = [
"postgres",
"sqlserver",
"sql server",
"microsoft.sql_server",
"oracle.db",
"ibm.db2",
"gcp.spanner",
"aws.redshift",
"aws.dynamodb",
];
for input in alias_inputs {
let canonical = canonical_db_system(input);
let sql = is_sql_db_system(canonical);
let non_sql = is_non_sql_db_system(canonical);
assert!(
sql ^ non_sql,
"{input} -> {canonical} classified as neither or both (sql={sql}, non_sql={non_sql})"
);
}
}
}