use opentelemetry::KeyValue;
use opentelemetry_semantic_conventions::attribute;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum QueryTextMode {
#[default]
Full,
Obfuscated,
Off,
}
#[derive(Debug, Clone)]
pub(crate) struct ConnectionAttributes {
pub system: &'static str,
pub host: Option<String>,
pub port: Option<u16>,
pub namespace: Option<String>,
pub network_peer_address: Option<String>,
pub network_peer_port: Option<u16>,
pub network_protocol_name: Option<String>,
pub network_transport: Option<String>,
pub pool_name: Option<String>,
pub query_text_mode: QueryTextMode,
}
impl ConnectionAttributes {
pub fn base_key_values(&self) -> Vec<KeyValue> {
let mut attrs = Vec::with_capacity(9);
attrs.push(KeyValue::new(attribute::DB_SYSTEM_NAME, self.system));
if let Some(ref host) = self.host {
attrs.push(KeyValue::new(attribute::SERVER_ADDRESS, host.clone()));
}
if let Some(port) = self.port {
attrs.push(KeyValue::new(attribute::SERVER_PORT, i64::from(port)));
}
if let Some(ref ns) = self.namespace {
attrs.push(KeyValue::new(attribute::DB_NAMESPACE, ns.clone()));
}
if let Some(ref addr) = self.network_peer_address {
attrs.push(KeyValue::new(attribute::NETWORK_PEER_ADDRESS, addr.clone()));
}
if let Some(port) = self.network_peer_port {
attrs.push(KeyValue::new(attribute::NETWORK_PEER_PORT, i64::from(port)));
}
if let Some(ref proto) = self.network_protocol_name {
attrs.push(KeyValue::new(
attribute::NETWORK_PROTOCOL_NAME,
proto.clone(),
));
}
if let Some(ref transport) = self.network_transport {
attrs.push(KeyValue::new(
attribute::NETWORK_TRANSPORT,
transport.clone(),
));
}
if let Some(ref name) = self.pool_name {
attrs.push(KeyValue::new(
attribute::DB_CLIENT_CONNECTION_POOL_NAME,
name.clone(),
));
}
attrs
}
}
pub(crate) fn span_name(
system: &str,
operation: Option<&str>,
collection: Option<&str>,
summary: Option<&str>,
) -> String {
fn nonempty(o: Option<&str>) -> Option<&str> {
o.filter(|s| !s.is_empty())
}
if let Some(s) = nonempty(summary) {
return s.to_owned();
}
match (nonempty(operation), nonempty(collection)) {
(Some(op), Some(coll)) => format!("{op} {coll}"),
(Some(op), None) => op.to_owned(),
_ => system.to_owned(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn span_name_with_operation_and_collection() {
assert_eq!(
span_name("postgresql", Some("SELECT"), Some("users"), None),
"SELECT users"
);
}
#[test]
fn span_name_with_operation_only() {
assert_eq!(
span_name("postgresql", Some("SELECT"), None, None),
"SELECT"
);
}
#[test]
fn span_name_fallback_to_system() {
assert_eq!(span_name("sqlite", None, None, None), "sqlite");
}
#[test]
fn span_name_collection_without_operation_falls_back() {
assert_eq!(span_name("mysql", None, Some("orders"), None), "mysql");
}
#[test]
fn span_name_summary_wins_over_operation_and_collection() {
assert_eq!(
span_name(
"postgresql",
Some("SELECT"),
Some("users"),
Some("daily report")
),
"daily report"
);
}
#[test]
fn span_name_summary_alone() {
assert_eq!(
span_name("sqlite", None, None, Some("custom name")),
"custom name"
);
}
#[test]
fn span_name_empty_operation_falls_through_to_system() {
assert_eq!(span_name("sqlite", Some(""), None, None), "sqlite");
}
#[test]
fn span_name_empty_summary_falls_through() {
assert_eq!(
span_name("sqlite", Some("SELECT"), Some("users"), Some("")),
"SELECT users"
);
}
#[test]
fn span_name_empty_op_and_coll_falls_through_to_system() {
assert_eq!(span_name("sqlite", Some(""), Some(""), None), "sqlite");
}
#[test]
fn span_name_empty_op_with_coll_falls_through_to_system() {
assert_eq!(span_name("sqlite", Some(""), Some("users"), None), "sqlite");
}
#[test]
fn base_key_values_all_fields() {
let attrs = ConnectionAttributes {
system: "postgresql",
host: Some("localhost".into()),
port: Some(5432),
namespace: Some("mydb".into()),
network_peer_address: Some("127.0.0.1".into()),
network_peer_port: Some(5432),
network_protocol_name: Some("postgresql".into()),
network_transport: Some("tcp".into()),
pool_name: Some("primary".into()),
query_text_mode: QueryTextMode::Full,
};
let kvs = attrs.base_key_values();
assert_eq!(kvs.len(), 9);
assert_eq!(kvs[0].key.as_str(), "db.system.name");
assert_eq!(kvs[1].key.as_str(), "server.address");
assert_eq!(kvs[2].key.as_str(), "server.port");
assert_eq!(kvs[3].key.as_str(), "db.namespace");
assert_eq!(kvs[4].key.as_str(), "network.peer.address");
assert_eq!(kvs[5].key.as_str(), "network.peer.port");
assert_eq!(kvs[6].key.as_str(), "network.protocol.name");
assert_eq!(kvs[7].key.as_str(), "network.transport");
assert_eq!(kvs[8].key.as_str(), "db.client.connection.pool.name");
}
#[test]
fn base_key_values_minimal() {
let attrs = ConnectionAttributes {
system: "sqlite",
host: None,
port: None,
namespace: None,
network_peer_address: None,
network_peer_port: None,
network_protocol_name: None,
network_transport: None,
pool_name: None,
query_text_mode: QueryTextMode::Off,
};
let kvs = attrs.base_key_values();
assert_eq!(kvs.len(), 1);
assert_eq!(kvs[0].key.as_str(), "db.system.name");
}
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(128))]
#[test]
fn span_name_is_non_empty(
system in "[a-z]{1,16}",
op in proptest::option::of(".{0,64}"),
coll in proptest::option::of(".{0,64}"),
summary in proptest::option::of(".{0,64}"),
) {
let name = span_name(&system, op.as_deref(), coll.as_deref(), summary.as_deref());
prop_assert!(!name.is_empty());
}
#[test]
fn span_name_summary_wins(
system in ".{0,16}",
op in proptest::option::of(".{0,64}"),
coll in proptest::option::of(".{0,64}"),
summary in ".{1,64}",
) {
let name = span_name(&system, op.as_deref(), coll.as_deref(), Some(summary.as_str()));
prop_assert_eq!(name, summary);
}
#[test]
fn span_name_op_coll_synthesis(
system in ".{0,16}",
op in ".{1,64}",
coll in ".{1,64}",
) {
let name = span_name(&system, Some(&op), Some(&coll), None);
prop_assert_eq!(name, format!("{op} {coll}"));
}
#[test]
fn span_name_bare_system_fallback(system in ".{0,16}") {
let name = span_name(&system, None, None, None);
prop_assert_eq!(name, system);
}
#[test]
fn span_name_collection_alone_is_ignored(
system in ".{0,16}",
coll in ".{0,64}",
) {
let name = span_name(&system, None, Some(&coll), None);
prop_assert_eq!(name, system);
}
#[test]
fn span_name_no_panic(
system in any::<String>(),
op in proptest::option::of(any::<String>()),
coll in proptest::option::of(any::<String>()),
summary in proptest::option::of(any::<String>()),
) {
let _ = span_name(&system, op.as_deref(), coll.as_deref(), summary.as_deref());
}
#[test]
fn base_key_values_length_matches_populated_fields(
host in proptest::option::of("[a-z]{1,16}"),
port in proptest::option::of(any::<u16>()),
namespace in proptest::option::of("[a-z]{1,16}"),
network_peer_address in proptest::option::of("[0-9.:]{1,32}"),
network_peer_port in proptest::option::of(any::<u16>()),
network_protocol_name in proptest::option::of("[a-z]{1,16}"),
network_transport in proptest::option::of("[a-z]{1,8}"),
pool_name in proptest::option::of("[a-z0-9-]{1,32}"),
) {
let attrs = ConnectionAttributes {
system: "sqlite",
host: host.clone(),
port,
namespace: namespace.clone(),
network_peer_address: network_peer_address.clone(),
network_peer_port,
network_protocol_name: network_protocol_name.clone(),
network_transport: network_transport.clone(),
pool_name: pool_name.clone(),
query_text_mode: QueryTextMode::Off,
};
let kvs = attrs.base_key_values();
let expected = 1
+ usize::from(host.is_some())
+ usize::from(port.is_some())
+ usize::from(namespace.is_some())
+ usize::from(network_peer_address.is_some())
+ usize::from(network_peer_port.is_some())
+ usize::from(network_protocol_name.is_some())
+ usize::from(network_transport.is_some())
+ usize::from(pool_name.is_some());
prop_assert_eq!(kvs.len(), expected);
prop_assert_eq!(kvs[0].key.as_str(), "db.system.name");
let keys: Vec<&str> = kvs.iter().map(|k| k.key.as_str()).collect();
prop_assert_eq!(
keys.contains(&"network.protocol.name"),
network_protocol_name.is_some(),
);
prop_assert_eq!(
keys.contains(&"network.transport"),
network_transport.is_some(),
);
prop_assert_eq!(
keys.contains(&"db.client.connection.pool.name"),
pool_name.is_some(),
);
}
}
}