Skip to content

Commit b380ebf

Browse files
add examples for axum and actix-web (#59)
* add examples for `axum` and `actix-web` * Specify write token in example usage --------- Co-authored-by: Matthew Kim <[email protected]>
1 parent 4ba6907 commit b380ebf

File tree

3 files changed

+493
-2
lines changed

3 files changed

+493
-2
lines changed

Cargo.toml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,22 @@ chrono = "0.4.39"
3636
regex = "1.11.1"
3737

3838
[dev-dependencies]
39-
async-trait = "0.1.88"
4039
insta = "1.42.1"
4140
opentelemetry_sdk = { version = "0.30", default-features = false, features = ["testing"] }
4241
regex = "1.11.1"
43-
tokio = {version = "1.44.1", features = ["test-util"] }
42+
tokio = {version = "1.44.1", features = ["test-util", "macros", "rt-multi-thread"] }
4443
ulid = "1.2.0"
4544

45+
# Dependencies for examples
46+
axum = { version = "0.8", features = ["macros"] }
47+
axum-tracing-opentelemetry = { version = "0.29", features = ["tracing_level_info"] }
48+
axum-otel-metrics = "0.11.0"
49+
actix-web = "4.0"
50+
opentelemetry-instrumentation-actix-web = { version = "0.22", features = ["metrics"] }
51+
serde = { version = "1.0", features = ["derive"] }
52+
serde_json = "1.0"
53+
futures-util = "0.3"
54+
4655
[features]
4756
default = ["export-http-protobuf"]
4857
serde = ["dep:serde"]

examples/actix-web.rs

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
//! Example demonstrating Logfire integration with Actix Web framework.
2+
//!
3+
//! This example shows how to:
4+
//! - Set up Logfire with Actix Web
5+
//! - Instrument HTTP requests with tracing and metrics middleware (OpenTelemetry)
6+
//! - Log custom events within route handlers
7+
//! - Create spans for business logic
8+
//! - Track metrics for request counts
9+
//!
10+
//! Run with: `cargo run --example actix-web`
11+
//! Make sure to set a write token as an environment variable (`LOGFIRE_TOKEN`)
12+
//! <https://logfire.pydantic.dev/docs/how-to-guides/create-write-tokens>/
13+
14+
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
15+
use actix_web::{
16+
App, HttpRequest, HttpResponse, HttpServer, Result as ActixResult,
17+
middleware::{DefaultHeaders, Logger},
18+
web,
19+
};
20+
use futures_util::future::{LocalBoxFuture, Ready, ready};
21+
use opentelemetry::KeyValue;
22+
use opentelemetry::metrics::Counter;
23+
use opentelemetry_instrumentation_actix_web::{RequestMetrics, RequestTracing};
24+
use serde::{Deserialize, Serialize};
25+
use std::sync::LazyLock;
26+
use std::task::{Context, Poll};
27+
use tracing::Instrument;
28+
29+
static REQUEST_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
30+
logfire::u64_counter("http_requests_total")
31+
.with_description("Total number of HTTP requests")
32+
.with_unit("{request}")
33+
.build()
34+
});
35+
36+
#[derive(Serialize, Deserialize)]
37+
struct User {
38+
id: u32,
39+
name: String,
40+
email: String,
41+
}
42+
43+
#[derive(Deserialize)]
44+
struct CreateUserRequest {
45+
name: String,
46+
email: String,
47+
}
48+
49+
#[actix_web::main]
50+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
51+
// Initialize Logfire
52+
let shutdown_handler = logfire::configure()
53+
.install_panic_handler()
54+
.finish()
55+
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
56+
57+
logfire::info!("Starting Actix Web server with Logfire integration");
58+
59+
HttpServer::new(|| {
60+
App::new()
61+
.wrap(RequestTracing::new())
62+
.wrap(RequestMetrics::default())
63+
.wrap(CustomRequestCount)
64+
.wrap(Logger::default())
65+
.wrap(DefaultHeaders::new().add(("X-Version", env!("CARGO_PKG_VERSION"))))
66+
.route("/", web::get().to(root))
67+
.route("/users/{id}", web::get().to(get_user))
68+
.route("/users", web::post().to(create_user))
69+
.route("/health", web::get().to(health_check))
70+
})
71+
.bind("127.0.0.1:3000")?
72+
.run()
73+
.await?;
74+
75+
shutdown_handler.shutdown()?;
76+
77+
Ok(())
78+
}
79+
80+
async fn root() -> HttpResponse {
81+
async {
82+
logfire::info!("Root endpoint accessed");
83+
HttpResponse::Ok().body("Hello, Actix Web with Logfire!")
84+
}
85+
.instrument(logfire::span!("Handling root request"))
86+
.await
87+
}
88+
89+
async fn get_user(path: web::Path<u32>) -> ActixResult<HttpResponse> {
90+
let user_id = path.into_inner();
91+
async {
92+
logfire::info!("Fetching user with ID: {user_id}", user_id = user_id as i64);
93+
94+
// Simulate database lookup
95+
tokio::time::sleep(std::time::Duration::from_millis(10))
96+
.instrument(logfire::span!("Database query for user"))
97+
.await;
98+
99+
logfire::debug!(
100+
"Database query completed for user {user_id}",
101+
user_id = user_id as i64
102+
);
103+
104+
if user_id == 0 {
105+
logfire::warn!(
106+
"Invalid user ID requested: {user_id}",
107+
user_id = user_id as i64
108+
);
109+
return Ok(HttpResponse::BadRequest().finish());
110+
}
111+
112+
if user_id > 1000 {
113+
logfire::error!("User {user_id} not found", user_id = user_id as i64);
114+
return Ok(HttpResponse::NotFound().finish());
115+
}
116+
117+
let user = User {
118+
id: user_id,
119+
name: format!("User {user_id}"),
120+
email: format!("user{user_id}@example.com"),
121+
};
122+
123+
logfire::info!(
124+
"Successfully retrieved user {user_id}",
125+
user_id = user_id as i64
126+
);
127+
128+
Ok(HttpResponse::Ok().json(user))
129+
}
130+
.instrument(logfire::span!("Fetching user {user_id}", user_id = user_id))
131+
.await
132+
}
133+
134+
async fn create_user(payload: web::Json<CreateUserRequest>) -> ActixResult<HttpResponse> {
135+
async {
136+
logfire::info!(
137+
"Creating new user: {name} <{email}>",
138+
name = &payload.name,
139+
email = &payload.email
140+
);
141+
142+
// Validate input
143+
if payload.name.is_empty() || payload.email.is_empty() {
144+
logfire::warn!("Invalid user data provided");
145+
return Ok(HttpResponse::BadRequest().finish());
146+
}
147+
148+
// Simulate user creation
149+
tokio::time::sleep(std::time::Duration::from_millis(20))
150+
.instrument(logfire::span!("Database user creation"))
151+
.await;
152+
153+
let user = User {
154+
id: 42, // In a real app, this would be generated
155+
name: payload.name.clone(),
156+
email: payload.email.clone(),
157+
};
158+
159+
logfire::info!(
160+
"Successfully created user {id} with name {name}",
161+
id = user.id as i64,
162+
name = &user.name
163+
);
164+
165+
Ok(HttpResponse::Created().json(user))
166+
}
167+
.instrument(logfire::span!(
168+
"Creating user {name}",
169+
name = &payload.name,
170+
email = &payload.email
171+
))
172+
.await
173+
}
174+
175+
async fn health_check(req: HttpRequest) -> HttpResponse {
176+
async {
177+
let user_agent = req
178+
.headers()
179+
.get("user-agent")
180+
.and_then(|v| v.to_str().ok())
181+
.unwrap_or("unknown")
182+
.to_string();
183+
logfire::debug!(
184+
"Health check from user-agent: {user_agent}",
185+
user_agent = user_agent
186+
);
187+
188+
HttpResponse::Ok().json(serde_json::json!({
189+
"status": "healthy",
190+
"timestamp": chrono::Utc::now().to_rfc3339(),
191+
"version": env!("CARGO_PKG_VERSION")
192+
}))
193+
}
194+
.instrument(logfire::span!("Health check"))
195+
.await
196+
}
197+
198+
struct CustomRequestCount;
199+
200+
impl<S, B> Transform<S, ServiceRequest> for CustomRequestCount
201+
where
202+
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error>,
203+
S::Future: 'static,
204+
B: 'static,
205+
{
206+
type Response = ServiceResponse<B>;
207+
type Error = actix_web::Error;
208+
type InitError = ();
209+
type Transform = CustomRequestCountMiddleware<S>;
210+
type Future = Ready<Result<Self::Transform, Self::InitError>>;
211+
212+
fn new_transform(&self, service: S) -> Self::Future {
213+
ready(Ok(CustomRequestCountMiddleware { service }))
214+
}
215+
}
216+
217+
struct CustomRequestCountMiddleware<S> {
218+
service: S,
219+
}
220+
221+
impl<S, B> Service<ServiceRequest> for CustomRequestCountMiddleware<S>
222+
where
223+
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error>,
224+
S::Future: 'static,
225+
B: 'static,
226+
{
227+
type Response = ServiceResponse<B>;
228+
type Error = actix_web::Error;
229+
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
230+
231+
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
232+
self.service.poll_ready(cx)
233+
}
234+
235+
fn call(&self, req: ServiceRequest) -> Self::Future {
236+
let method = req.method().clone();
237+
let path = req.path().to_string();
238+
let fut = self.service.call(req);
239+
Box::pin(async move {
240+
let res = fut.await?;
241+
REQUEST_COUNTER.add(
242+
1,
243+
&[
244+
KeyValue::new("method", method.to_string()),
245+
KeyValue::new("route", path),
246+
KeyValue::new("status_code", res.status().as_u16() as i64),
247+
],
248+
);
249+
Ok(res)
250+
})
251+
}
252+
}

0 commit comments

Comments
 (0)