use opentelemetry::trace::{SpanKind as OtelSpanKind, Status};
use std::collections::HashMap;
use std::time::SystemTime;
use tracing::Level;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpanKind {
Server,
Client,
Producer,
Consumer,
Internal,
}
impl From<SpanKind> for OtelSpanKind {
fn from(kind: SpanKind) -> Self {
match kind {
SpanKind::Server => OtelSpanKind::Server,
SpanKind::Client => OtelSpanKind::Client,
SpanKind::Producer => OtelSpanKind::Producer,
SpanKind::Consumer => OtelSpanKind::Consumer,
SpanKind::Internal => OtelSpanKind::Internal,
}
}
}
#[derive(Debug, Clone)]
pub struct SpanStatus {
code: SpanStatusCode,
message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpanStatusCode {
Ok,
Error,
Unset,
}
impl SpanStatus {
pub fn ok() -> Self {
Self {
code: SpanStatusCode::Ok,
message: String::new(),
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
code: SpanStatusCode::Error,
message: message.into(),
}
}
pub fn unset() -> Self {
Self {
code: SpanStatusCode::Unset,
message: String::new(),
}
}
}
impl From<SpanStatus> for Status {
fn from(status: SpanStatus) -> Self {
match status.code {
SpanStatusCode::Ok => Status::Ok,
SpanStatusCode::Error => Status::error(status.message),
SpanStatusCode::Unset => Status::Unset,
}
}
}
pub struct SpanBuilder {
name: String,
kind: SpanKind,
attributes: HashMap<String, String>,
events: Vec<SpanEvent>,
links: Vec<SpanLink>,
start_time: Option<SystemTime>,
level: Level,
}
impl SpanBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
kind: SpanKind::Internal,
attributes: HashMap::new(),
events: Vec::new(),
links: Vec::new(),
start_time: None,
level: Level::INFO,
}
}
pub fn with_kind(mut self, kind: SpanKind) -> Self {
self.kind = kind;
self
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.insert(key.into(), value.into());
self
}
pub fn with_attributes<I, K, V>(mut self, attributes: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
for (key, value) in attributes {
self.attributes.insert(key.into(), value.into());
}
self
}
pub fn with_event(mut self, event: SpanEvent) -> Self {
self.events.push(event);
self
}
pub fn with_link(mut self, link: SpanLink) -> Self {
self.links.push(link);
self
}
pub fn with_start_time(mut self, time: SystemTime) -> Self {
self.start_time = Some(time);
self
}
pub fn with_level(mut self, level: Level) -> Self {
self.level = level;
self
}
pub fn start(self) -> tracing::Span {
match self.level {
Level::TRACE => {
tracing::trace_span!(
target: "revoke_trace",
"{}",
self.name,
otel.kind = ?self.kind
)
}
Level::DEBUG => {
tracing::debug_span!(
target: "revoke_trace",
"{}",
self.name,
otel.kind = ?self.kind
)
}
Level::INFO => {
tracing::info_span!(
target: "revoke_trace",
"{}",
self.name,
otel.kind = ?self.kind
)
}
Level::WARN => {
tracing::warn_span!(
target: "revoke_trace",
"{}",
self.name,
otel.kind = ?self.kind
)
}
Level::ERROR => {
tracing::error_span!(
target: "revoke_trace",
"{}",
self.name,
otel.kind = ?self.kind
)
}
}
}
}
#[derive(Debug, Clone)]
pub struct SpanEvent {
name: String,
timestamp: SystemTime,
attributes: HashMap<String, String>,
}
impl SpanEvent {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
timestamp: SystemTime::now(),
attributes: HashMap::new(),
}
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.insert(key.into(), value.into());
self
}
pub fn with_timestamp(mut self, timestamp: SystemTime) -> Self {
self.timestamp = timestamp;
self
}
}
#[derive(Debug, Clone)]
pub struct SpanLink {
#[allow(dead_code)]
context: crate::context::TraceContext,
attributes: HashMap<String, String>,
}
impl SpanLink {
pub fn new(context: crate::context::TraceContext) -> Self {
Self {
context,
attributes: HashMap::new(),
}
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.insert(key.into(), value.into());
self
}
}
pub trait SpanExt {
fn record_exception(&self, error: &dyn std::error::Error);
fn set_status(&self, status: SpanStatus);
fn add_event(&self, event: SpanEvent);
}
impl SpanExt for tracing::Span {
fn record_exception(&self, error: &dyn std::error::Error) {
self.record("exception.type", &error.to_string());
self.record("exception.message", &error.to_string());
if let Some(source) = error.source() {
self.record("exception.stacktrace", &source.to_string());
}
}
fn set_status(&self, status: SpanStatus) {
self.record("otel.status_code", format!("{:?}", status.code).as_str());
if !status.message.is_empty() {
self.record("otel.status_message", &status.message);
}
}
fn add_event(&self, event: SpanEvent) {
tracing::event!(
target: "revoke_trace",
parent: self,
Level::INFO,
name = %event.name,
timestamp = ?event.timestamp,
?event.attributes,
"span event"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_span_builder() {
let _span = SpanBuilder::new("test_operation")
.with_kind(SpanKind::Server)
.with_attribute("http.method", "GET")
.with_attribute("http.url", "/api/users")
.with_level(Level::DEBUG)
.start();
}
#[test]
fn test_span_status() {
let ok_status = SpanStatus::ok();
let error_status = SpanStatus::error("Something went wrong");
let _otel_status: Status = ok_status.into();
let _otel_error: Status = error_status.into();
}
}