use std::time::Duration;
use crate::plugin::PluginId;
use crate::qname::QName;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum InvocationKind {
Scalar,
Aggregate,
Window,
Procedure,
LocyAggregate,
LocyPredicate,
Operator,
Index,
Storage,
Algorithm,
Crdt,
Hook,
Trigger,
BackgroundJob,
Type,
Auth,
Authz,
Connector,
}
impl InvocationKind {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Scalar => "scalar",
Self::Aggregate => "aggregate",
Self::Window => "window",
Self::Procedure => "procedure",
Self::LocyAggregate => "locy_aggregate",
Self::LocyPredicate => "locy_predicate",
Self::Operator => "operator",
Self::Index => "index",
Self::Storage => "storage",
Self::Algorithm => "algorithm",
Self::Crdt => "crdt",
Self::Hook => "hook",
Self::Trigger => "trigger",
Self::BackgroundJob => "background_job",
Self::Type => "type",
Self::Auth => "auth",
Self::Authz => "authz",
Self::Connector => "connector",
}
}
}
pub fn record_invocation(
plugin: &PluginId,
qname: &QName,
kind: InvocationKind,
rows: u64,
elapsed: Duration,
ok: bool,
) {
tracing::debug!(
plugin.id = plugin.as_str(),
plugin.qname = %qname,
plugin.kind = kind.as_str(),
batch.rows = rows,
duration_ms = elapsed.as_millis() as u64,
result.ok = ok,
"plugin.invoke"
);
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct TraceContext {
pub trace_id: Vec<u8>,
pub span_id: Vec<u8>,
pub trace_flags: u8,
}
impl TraceContext {
#[must_use]
pub fn to_traceparent(&self) -> Option<String> {
use std::fmt::Write as _;
if self.trace_id.len() != 16 || self.span_id.len() != 8 {
return None;
}
let mut s = String::with_capacity(55);
s.push_str("00-");
for b in &self.trace_id {
let _ = write!(s, "{b:02x}");
}
s.push('-');
for b in &self.span_id {
let _ = write!(s, "{b:02x}");
}
let _ = write!(s, "-{:02x}", self.trace_flags);
Some(s)
}
}
#[must_use]
pub fn current_trace_context() -> TraceContext {
#[cfg(feature = "otel")]
{
use opentelemetry::trace::TraceContextExt as _;
use tracing_opentelemetry::OpenTelemetrySpanExt as _;
let span = tracing::Span::current();
let ctx = span.context();
let span_ref = ctx.span();
let sc = span_ref.span_context();
if sc.is_valid() {
return TraceContext {
trace_id: sc.trace_id().to_bytes().to_vec(),
span_id: sc.span_id().to_bytes().to_vec(),
trace_flags: sc.trace_flags().to_u8(),
};
}
}
TraceContext::default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invocation_kind_strings_are_stable() {
assert_eq!(InvocationKind::Scalar.as_str(), "scalar");
assert_eq!(InvocationKind::Procedure.as_str(), "procedure");
assert_eq!(InvocationKind::LocyAggregate.as_str(), "locy_aggregate");
assert_eq!(InvocationKind::BackgroundJob.as_str(), "background_job");
}
#[test]
fn record_invocation_does_not_panic_without_subscriber() {
record_invocation(
&PluginId::new("test"),
&QName::builtin("identity"),
InvocationKind::Scalar,
128,
Duration::from_micros(50),
true,
);
}
#[test]
fn trace_context_empty_without_otel_layer() {
let c = current_trace_context();
assert!(c.trace_id.is_empty());
assert!(c.span_id.is_empty());
assert!(c.to_traceparent().is_none());
}
#[test]
fn empty_context_has_no_traceparent() {
assert!(TraceContext::default().to_traceparent().is_none());
}
#[cfg(feature = "otel")]
#[test]
fn current_trace_context_extracts_valid_context_under_otel_layer() {
use opentelemetry::trace::TracerProvider as _;
use tracing_subscriber::prelude::*;
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder().build();
let tracer = provider.tracer("uni-plugin-test");
let subscriber =
tracing_subscriber::registry().with(tracing_opentelemetry::layer().with_tracer(tracer));
tracing::subscriber::with_default(subscriber, || {
let span = tracing::info_span!("otel-test-span");
let _enter = span.enter();
let c = current_trace_context();
assert_eq!(c.trace_id.len(), 16, "trace id should be 16 bytes");
assert_eq!(c.span_id.len(), 8, "span id should be 8 bytes");
let tp = c
.to_traceparent()
.expect("a valid context renders a traceparent");
assert!(tp.starts_with("00-"), "traceparent: {tp}");
assert_eq!(tp.len(), 55, "traceparent: {tp}");
});
}
}