use api_bones::{OrganizationContext, PrincipalKind};
use opentelemetry::{Array, KeyValue, StringValue, Value};
use tracing_opentelemetry::OpenTelemetrySpanExt;
pub const ENDUSER_ID: &str = "enduser.id";
pub const ENDUSER_ORG_ID: &str = "enduser.org_id";
pub const ENDUSER_ORG_PATH: &str = "enduser.org_path";
pub const ENDUSER_PRINCIPAL_KIND: &str = "enduser.principal_kind";
pub fn emit_enduser_fields(ctx: &OrganizationContext) {
emit_enduser_fields_on(&tracing::Span::current(), ctx);
}
pub fn emit_enduser_fields_on(span: &tracing::Span, ctx: &OrganizationContext) {
span.set_attribute(ENDUSER_ID, ctx.principal.id.as_str().to_owned());
span.set_attribute(ENDUSER_ORG_ID, ctx.org_id.inner().to_string());
span.set_attribute(
ENDUSER_ORG_PATH,
Value::Array(Array::String(
ctx.org_path
.iter()
.map(|id| StringValue::from(id.inner().to_string()))
.collect(),
)),
);
span.set_attribute(
ENDUSER_PRINCIPAL_KIND,
principal_kind_str(ctx.principal.kind),
);
}
pub(crate) fn principal_kind_str(kind: PrincipalKind) -> &'static str {
match kind {
PrincipalKind::User => "user",
PrincipalKind::Service => "service",
PrincipalKind::System => "system",
_ => "unknown",
}
}
#[must_use]
pub fn enduser_key_values(ctx: &OrganizationContext) -> [KeyValue; 4] {
[
KeyValue::new(ENDUSER_ID, ctx.principal.id.as_str().to_owned()),
KeyValue::new(ENDUSER_ORG_ID, ctx.org_id.inner().to_string()),
KeyValue::new(
ENDUSER_ORG_PATH,
Value::Array(Array::String(
ctx.org_path
.iter()
.map(|id| StringValue::from(id.inner().to_string()))
.collect(),
)),
),
KeyValue::new(
ENDUSER_PRINCIPAL_KIND,
principal_kind_str(ctx.principal.kind),
),
]
}
#[cfg(test)]
mod tests {
use super::*;
use api_bones::{OrgId, Principal, RequestId};
use opentelemetry::Key;
use uuid::Uuid;
fn ctx_with_path(org_id: OrgId, parents: &[OrgId]) -> OrganizationContext {
let mut path = parents.to_vec();
path.push(org_id);
OrganizationContext::new(org_id, Principal::human(Uuid::new_v4()), RequestId::new())
.with_org_path(path)
}
#[test]
fn principal_kind_str_maps_each_variant() {
assert_eq!(principal_kind_str(PrincipalKind::User), "user");
assert_eq!(principal_kind_str(PrincipalKind::Service), "service");
assert_eq!(principal_kind_str(PrincipalKind::System), "system");
}
#[test]
fn enduser_key_values_has_all_four_attributes() {
let org_id = OrgId::generate();
let ctx = ctx_with_path(org_id, &[]);
let kvs = enduser_key_values(&ctx);
let keys: Vec<&str> = kvs.iter().map(|kv| kv.key.as_str()).collect();
assert!(keys.contains(&ENDUSER_ID));
assert!(keys.contains(&ENDUSER_ORG_ID));
assert!(keys.contains(&ENDUSER_ORG_PATH));
assert!(keys.contains(&ENDUSER_PRINCIPAL_KIND));
}
#[test]
fn enduser_org_path_is_a_string_array_not_a_joined_string() {
let root = OrgId::generate();
let leaf = OrgId::generate();
let ctx = ctx_with_path(leaf, &[root]);
let kvs = enduser_key_values(&ctx);
let org_path = kvs
.iter()
.find(|kv| kv.key == Key::new(ENDUSER_ORG_PATH))
.expect("org_path KeyValue present");
match &org_path.value {
Value::Array(Array::String(segments)) => {
assert_eq!(segments.len(), 2, "root + leaf = 2 segments");
assert_eq!(segments[0].as_str(), root.inner().to_string());
assert_eq!(segments[1].as_str(), leaf.inner().to_string());
}
other => panic!("expected Value::Array(Array::String), got {other:?}"),
}
}
#[test]
fn enduser_org_path_empty_when_path_is_empty() {
let org_id = OrgId::generate();
let ctx =
OrganizationContext::new(org_id, Principal::human(Uuid::new_v4()), RequestId::new());
let kvs = enduser_key_values(&ctx);
let org_path = kvs
.iter()
.find(|kv| kv.key == Key::new(ENDUSER_ORG_PATH))
.expect("org_path KeyValue present");
match &org_path.value {
Value::Array(Array::String(segments)) => assert!(segments.is_empty()),
other => panic!("expected empty Value::Array(Array::String), got {other:?}"),
}
}
#[test]
fn enduser_principal_kind_for_human_is_user() {
let org_id = OrgId::generate();
let ctx = ctx_with_path(org_id, &[]);
let kvs = enduser_key_values(&ctx);
let kind = kvs
.iter()
.find(|kv| kv.key == Key::new(ENDUSER_PRINCIPAL_KIND))
.expect("principal_kind KeyValue present");
assert_eq!(kind.value.as_str().as_ref(), "user");
}
#[test]
fn enduser_principal_kind_for_system_is_system() {
let org_id = OrgId::generate();
let ctx = OrganizationContext::new(
org_id,
Principal::system("otel-bootstrap.test"),
RequestId::new(),
);
let kvs = enduser_key_values(&ctx);
let kind = kvs
.iter()
.find(|kv| kv.key == Key::new(ENDUSER_PRINCIPAL_KIND))
.expect("principal_kind KeyValue present");
assert_eq!(kind.value.as_str().as_ref(), "system");
}
#[test]
fn enduser_org_id_is_the_uuid_string_of_ctx_org_id() {
let org_id = OrgId::generate();
let ctx = ctx_with_path(org_id, &[]);
let kvs = enduser_key_values(&ctx);
let kv_org_id = kvs
.iter()
.find(|kv| kv.key == Key::new(ENDUSER_ORG_ID))
.expect("org_id KeyValue present");
assert_eq!(
kv_org_id.value.as_str().as_ref(),
org_id.inner().to_string()
);
}
#[test]
fn enduser_id_is_the_principal_id_string() {
let org_id = OrgId::generate();
let id = Uuid::new_v4();
let ctx = OrganizationContext::new(org_id, Principal::human(id), RequestId::new());
let kvs = enduser_key_values(&ctx);
let kv_id = kvs
.iter()
.find(|kv| kv.key == Key::new(ENDUSER_ID))
.expect("id KeyValue present");
assert_eq!(kv_id.value.as_str().as_ref(), id.to_string());
}
#[test]
fn emit_enduser_fields_is_noop_without_active_span() {
let org_id = OrgId::generate();
let ctx = ctx_with_path(org_id, &[]);
emit_enduser_fields(&ctx);
}
#[test]
fn emit_enduser_fields_on_disabled_span_is_infallible() {
let org_id = OrgId::generate();
let ctx = ctx_with_path(org_id, &[]);
emit_enduser_fields_on(&tracing::Span::none(), &ctx);
}
}