Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
563 changes: 563 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

23 changes: 3 additions & 20 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,20 +1,3 @@
[package]
name = "otelx"
version = "0.0.1"
edition = "2024"
authors = ["Lucas Jahier <lucas.jahier@stratorys.com>"]
description = "A minimal and unified interface for using OpenTelemetry and OpenTelemetry SDK in Rust."
license-file = "LICENSE"
repository = "https://github.com/caffeidine/otelx"
homepage = "https://github.com/caffeidine/otelx"
categories = [
"development-tools::debugging",
"development-tools::profiling",
"development-tools",
]
keywords = ["opentelemetry", "tracing", "telemetry", "observability"]


[dependencies]
opentelemetry = "0.28"
opentelemetry_sdk = "0.28"
[workspace]
resolver = "2"
members = ["otelx", "otelx-axum", "otelx-attributes", "otelx-core"]
30 changes: 30 additions & 0 deletions otelx-attributes/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "otelx-attributes"
version = "0.0.1"
edition = "2024"
authors = ["Lucas Jahier <lucas.jahier@stratorys.com>"]
description = "A minimal and unified interface for using OpenTelemetry and OpenTelemetry SDK in Rust."
license-file = "LICENSE"
repository = "https://github.com/caffeidine/otelx"
homepage = "https://github.com/caffeidine/otelx"
categories = [
"development-tools::debugging",
"development-tools::profiling",
"development-tools",
]
keywords = [
"opentelemetry",
"tracing",
"telemetry",
"observability",
"macro",
"instrument",
]

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0.94"
quote = "1.0.40"
syn = { version = "2.0.100", features = ["full"] }
52 changes: 52 additions & 0 deletions otelx-attributes/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::punctuated::Punctuated;
use syn::{
Meta, Token,
parse::{Parse, ParseStream},
};

struct Args(Punctuated<Meta, Token![,]>);

impl Parse for Args {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Args(Punctuated::parse_terminated(input)?))
}
}

#[proc_macro_attribute]
pub fn tracing(args: TokenStream, input: TokenStream) -> TokenStream {
let Args(args) = syn::parse_macro_input!(args as Args);
let mut semantic = None;
let mut span_name = None;

for meta in args.iter() {
if let Meta::NameValue(nv) = meta {
if nv.path.is_ident("semantic") {
semantic = Some(nv.value.clone());
} else if nv.path.is_ident("span_name") {
span_name = Some(nv.value.clone());
}
}
}
let semantic = semantic.expect("semantic parameter is required");
let span_name = span_name.expect("span_name parameter is required");

let input_fn = syn::parse_macro_input!(input as syn::ItemFn);
let attrs = &input_fn.attrs;
let vis = &input_fn.vis;
let sig = &input_fn.sig;
let block = &input_fn.block;

let expanded = quote! {
#(#attrs)*
#vis #sig {
otelx::trace_with_adapter(#semantic, #span_name, async move {
let __res = async move #block .await;
otelx_axum::AxumResponse(__res)
}).await.into()
}
};

TokenStream::from(expanded)
}
12 changes: 12 additions & 0 deletions otelx-axum/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "otelx-axum"
version = "0.1.0"
edition = "2024"

[dependencies]
http = { git = "https://github.com/ljahier/http.git", branch = "feature/impl-display-for-version" }
axum = "0.8.1"
opentelemetry = "0.28.0"
opentelemetry_sdk = "0.28.0"

otelx-core = { path = "../otelx-core" }
72 changes: 72 additions & 0 deletions otelx-axum/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use axum::response::Response;
use opentelemetry::KeyValue;
use opentelemetry::trace::{SpanRef, Status};
use otelx_core::{TraceAdapter, Traceable};
use std::future::Future;

pub trait AxumTraceable {
fn record_axum_span(&self, span: SpanRef<'_>);
}

impl<T> AxumTraceable for Response<T> {
fn record_axum_span(&self, span: SpanRef<'_>) {
let status = self.status().as_u16();
span.set_attribute(KeyValue::new("http.status_code", status as i64));
if self.status().is_server_error() {
span.set_status(Status::error("internal server error"));
} else {
span.set_status(Status::Ok);
}
span.end();
}
}

pub struct AxumResponse<T>(pub Response<T>);

impl<T> Traceable for AxumResponse<T> {
fn record_span(&self, span: SpanRef<'_>) {
self.0.record_axum_span(span);
}
}

impl<T> From<AxumResponse<T>> for Response<T> {
fn from(wrapper: AxumResponse<T>) -> Self {
wrapper.0
}
}

pub struct AxumTraceAdapter;

pub struct AdaptedAxumFuture<F, T> {
inner: F,
_phantom: std::marker::PhantomData<T>,
}

impl<F, T> Future for AdaptedAxumFuture<F, T>
where
F: Future<Output = AxumResponse<T>>,
{
type Output = AxumResponse<T>;

fn poll(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
let fut = unsafe { self.as_mut().map_unchecked_mut(|s| &mut s.inner) };
fut.poll(cx)
}
}

impl<F, T> TraceAdapter<F> for AxumTraceAdapter
where
F: Future<Output = AxumResponse<T>>,
{
type Output = AdaptedAxumFuture<F, T>;

fn adapt(fut: F) -> Self::Output {
AdaptedAxumFuture {
inner: fut,
_phantom: std::marker::PhantomData,
}
}
}
7 changes: 7 additions & 0 deletions otelx-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "otelx-core"
version = "0.1.0"
edition = "2024"

[dependencies]
opentelemetry = "0.28.0"
14 changes: 14 additions & 0 deletions otelx-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use std::future::Future;

use opentelemetry::trace::SpanRef;

pub trait Traceable {
fn record_span(&self, span: SpanRef<'_>);
}

/// Trait that adapts a Future before tracing.
pub trait TraceAdapter<Fut> {
type Output: Future;

fn adapt(fut: Fut) -> Self::Output;
}
32 changes: 32 additions & 0 deletions otelx/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[package]
name = "otelx"
version = "0.0.1"
edition = "2024"
authors = ["Lucas Jahier <lucas.jahier@stratorys.com>"]
description = "A minimal and unified interface for using OpenTelemetry and OpenTelemetry SDK in Rust."
license-file = "LICENSE"
repository = "https://github.com/caffeidine/otelx"
homepage = "https://github.com/caffeidine/otelx"
categories = [
"development-tools::debugging",
"development-tools::profiling",
"development-tools",
]
keywords = ["opentelemetry", "tracing", "telemetry", "observability"]

[dependencies]
async-trait = "0.1.88"
log = "0.4.26"
opentelemetry = { version = "0.28", features = ["trace"] }
opentelemetry-stdout = "0.28.0"
opentelemetry_sdk = { version = "0.28", features = ["trace"] }
tokio = { version = "1.44.1", features = ["full"] }

otelx-core = { path = "../otelx-core" }
otelx-attributes = { path = "../otelx-attributes" }
otelx-axum = { path = "../otelx-axum", optional = true }


[features]
default = []
axum = ["otelx-axum"]
13 changes: 13 additions & 0 deletions otelx/src/finalizer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use crate::Span;

pub trait SpanFinalizer {
fn finalize_span(span: &Span, result: &Self);
}

impl<T> SpanFinalizer for T {
default fn finalize_span(_span: &Span, _result: &Self) {}
}

pub fn finalize_span<T: SpanFinalizer>(span: &Span, result: &T) {
T::finalize_span(span, result);
}
11 changes: 11 additions & 0 deletions otelx/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
pub mod tracing;

pub use opentelemetry::global::BoxedSpan;
pub use opentelemetry::trace::Span;
pub use opentelemetry::{Context, KeyValue};

pub use tracing::{trace_block, trace_with_adapter};

pub use otelx_core::{TraceAdapter, Traceable};

pub use otelx_attributes::tracing;
17 changes: 17 additions & 0 deletions otelx/src/trace_block.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use crate::{SpanFinalizer, create_span};
use std::future::Future;

pub async fn trace_block<T, F>(semantic: &str, span_name: &str, fut: F) -> T
where
F: Future<Output = T>,
T: SpanFinalizer,
{
let (cx, span) = create_span(semantic, span_name, None, None);

let result = fut.await;

T::finalize_span(&span, &result);
span.end();

result
}
102 changes: 102 additions & 0 deletions otelx/src/tracing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#[cfg(feature = "axum")]
use otelx_axum::AxumTraceAdapter as ActiveTraceAdapter;

#[cfg(not(feature = "axum"))]
use crate::tracing::IdentityAdapter as ActiveTraceAdapter;

use std::future::Future;

use opentelemetry::{
Context, KeyValue,
global::{self, BoxedSpan},
trace::{Span, TraceContextExt, Tracer},
};

use otelx_core::{TraceAdapter, Traceable};

/// Create a new span and return a new Context containing it.
/// This is the simple and default way to start tracing.
pub fn create_span(
semantic: &str,
span_name: &str,
extra_attrs: Option<Vec<KeyValue>>,
parent_cx: Option<&Context>,
) -> Context {
let (span, _) = start_span(semantic, span_name, extra_attrs, parent_cx);
Context::current_with_span(span)
}

/// Advanced usage: returns the raw span and its parent context.
/// Useful when you want to pass the parent context to another async block before propagating the new span.
pub fn start_span(
semantic: &str,
span_name: &str,
extra_attrs: Option<Vec<KeyValue>>,
parent: Option<&Context>,
) -> (BoxedSpan, Context) {
let tracer = global::tracer("otelx.tracing.start_span");

let parent_cx = parent.cloned().unwrap_or_else(Context::current);

let mut span = tracer.start_with_context(span_name.to_string(), &parent_cx);

let mut attributes = vec![
KeyValue::new("otelx.semantic", semantic.to_string()),
KeyValue::new("otelx.span_name", span_name.to_string()),
];

if let Some(mut extra) = extra_attrs {
attributes.append(&mut extra);
}

span.set_attributes(attributes);

(span, parent_cx)
}

/// Core tracing block for types implementing `Traceable`.
/// Automatically finalizes the span and records it from the returned result.
pub async fn trace_block<T, F>(semantic: &str, span_name: &str, fut: F) -> T
where
F: Future<Output = T>,
T: Traceable,
{
let cx = create_span(semantic, span_name, None, None);
let span = cx.span();

let result = fut.await;
result.record_span(span);

result
}

/// Adapter-aware tracing block.
/// Uses the active trace adapter (Axum, SQLx, etc.) selected via feature flags.
pub fn trace_with_adapter<Fut, T>(
semantic: &str,
span_name: &str,
fut: Fut,
) -> impl Future<Output = T>
where
Fut: Future<Output = T>,
T: Traceable,
ActiveTraceAdapter: TraceAdapter<Fut>,
<ActiveTraceAdapter as TraceAdapter<Fut>>::Output: Future<Output = T>,
{
let adapted = ActiveTraceAdapter::adapt(fut);
trace_block(semantic, span_name, adapted)
}

// Default adapter for when no specific feature (like axum or sqlx) is enabled.
pub struct IdentityAdapter;

impl<Fut> TraceAdapter<Fut> for IdentityAdapter
where
Fut: Future,
{
type Output = Fut;

fn adapt(fut: Fut) -> Self::Output {
fut
}
}
Loading