diff --git a/Cargo.toml b/Cargo.toml index 0e9c09c6..2613db69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "datadog-opentelemetry", "datadog-opentelemetry/examples/propagator", "datadog-opentelemetry/examples/simple_tracing", + "dd-trace-examples", ] # https://doc.rust-lang.org/cargo/reference/resolver.html#feature-resolver-version-2 @@ -33,7 +34,7 @@ opentelemetry_sdk = { version = "0.31.0", features = [ opentelemetry = { version = "0.31.0", features = [ "trace", ], default-features = false } -opentelemetry-semantic-conventions = { version = "0.31.0", features = [ +opentelemetry-semantic-conventions = { version = "0.30.0", features = [ "semconv_experimental", ] } tokio = { version = "1.44.1" } diff --git a/dd-trace-examples/Cargo.toml b/dd-trace-examples/Cargo.toml new file mode 100644 index 00000000..ba0d3d54 --- /dev/null +++ b/dd-trace-examples/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "dd-trace-examples" +rust-version.workspace = true +edition.workspace = true +version.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +description.workspace = true + +[dependencies] +# Datadog OpenTelemetry +datadog-opentelemetry = { path = "../datadog-opentelemetry" } + +# Tracing ecosystem +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-opentelemetry = "0.32" + +# HTTP server +axum = "0.7" +tokio = { version = "1.44.1", features = ["full"] } + +# OpenTelemetry +opentelemetry = { version = "0.31.0", features = ["trace"] } +opentelemetry_sdk = { version = "0.31.0", features = ["trace"] } +opentelemetry-http = { version = "0.31.0" } + +# Utilities +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tower = "0.4" +tower-http = { version = "0.5", features = ["cors", "trace"] } +rand = "0.8" diff --git a/dd-trace-examples/README.md b/dd-trace-examples/README.md new file mode 100644 index 00000000..d7c74907 --- /dev/null +++ b/dd-trace-examples/README.md @@ -0,0 +1,145 @@ +# Datadog OpenTelemetry Example + +This example demonstrates how to use `datadog-opentelemetry` with the `tracing` crate and `tracing-opentelemetry` bridge in an Axum HTTP server. + +## Features + +- **Datadog OpenTelemetry Integration**: Uses the `datadog-opentelemetry` crate for sending traces to Datadog +- **Tracing Bridge**: Demonstrates the `tracing-opentelemetry` bridge for seamless integration +- **HTTP Server**: Built with Axum framework +- **Structured Logging**: Uses the `tracing` crate for structured logging +- **Custom Spans**: Shows how to create custom spans with attributes +- **REST API**: Includes endpoints for user management and health checks + +## Prerequisites + +1. **Datadog Agent**: Make sure you have a Datadog agent running locally or accessible +2. **Rust**: Ensure you have Rust 1.84.1+ installed + +## Running the Example + +### 1. Start the Datadog Agent (if running locally) + +```bash +# Using Docker +docker run -d --name datadog-agent \ + -e DD_API_KEY=your_api_key \ + -e DD_APM_ENABLED=true \ + -e DD_APM_NON_LOCAL_TRAFFIC=true \ + -p 8126:8126 \ + datadog/agent:latest + +# Or using the official install script +DD_API_KEY=your_api_key DD_APM_ENABLED=true bash -c "$(curl -L https://s1.datadoghq.com/install_script_agent7.sh)" +``` + +### 2. Run the Example + +```bash +# From the examples directory +cargo run + +# Or from the workspace root +cargo run -p dd-trace-examples +``` + +The server will start on `http://localhost:3000` + +### 3. Test the API + +```bash +# Health check +curl http://localhost:3000/health + +# Get user by ID +curl http://localhost:3000/users/123 + +# Create a new user +curl -X POST http://localhost:3000/users \ + -H "Content-Type: application/json" \ + -d '{"name": "John Doe", "email": "john@example.com"}' +``` + +## Configuration + +The example is configured to send traces to `http://localhost:8126/v0.5/traces` (default Datadog agent endpoint). You can modify the configuration in the `main()` function: + +```rust +let pipeline = DatadogPipeline::new() + .with_service_name("dd-trace-example") + .with_service_version("1.0.0") + .with_env("development") + .with_trace_endpoint("http://localhost:8126/v0.5/traces") + .build()?; +``` + +## Key Components + +### 1. Datadog Pipeline Setup + +```rust +use datadog_opentelemetry::DatadogPipeline; + +let pipeline = DatadogPipeline::new() + .with_service_name("dd-trace-example") + .with_service_version("1.0.0") + .with_env("development") + .build()?; +``` + +### 2. Tracing Integration + +```rust +use tracing::{info, instrument}; +use tracing_opentelemetry::OpenTelemetrySpanExt; + +#[instrument(skip(tracer))] +async fn get_user(Path(id): Path, tracer: Extension) -> Json> { + let span = tracer.start("get_user"); + span.set_attribute(opentelemetry::KeyValue::new("user.id", id.to_string())); + // ... function logic + span.end(); +} +``` + +### 3. Global Tracer Setup + +```rust +use opentelemetry::global; + +let tracer = pipeline.tracer(); +global::set_tracer_provider(pipeline.trace_provider()); +``` + +## What You'll See + +1. **Console Logs**: Structured logging output showing the application flow +2. **Datadog Traces**: Spans and traces sent to your Datadog agent +3. **Custom Attributes**: User ID, name, and email attributes on spans +4. **Performance Metrics**: Timing information for each operation + +## Troubleshooting + +### Common Issues + +1. **Connection Refused**: Make sure the Datadog agent is running and accessible +2. **Permission Denied**: Check that the agent endpoint is correct and accessible +3. **No Traces in Datadog**: Verify your API key and agent configuration + +### Debug Mode + +The example includes debug logging for the datadog-opentelemetry crate: + +```rust +tracing_subscriber::fmt() + .with_env_filter("info,datadog_opentelemetry=debug") + .init(); +``` + +## Next Steps + +- Modify the service name, version, and environment +- Add more custom attributes to spans +- Implement error handling and error spans +- Add metrics collection +- Configure sampling and filtering rules diff --git a/dd-trace-examples/src/main.rs b/dd-trace-examples/src/main.rs new file mode 100644 index 00000000..5a8325df --- /dev/null +++ b/dd-trace-examples/src/main.rs @@ -0,0 +1,139 @@ +use axum::{extract::Request, http::Method, response::Json, routing::get, Router}; +use datadog_opentelemetry::{self, configuration::Config, log::LevelFilter as DdLevelFilter}; +use opentelemetry::trace::TracerProvider; +use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; +use tower_http::{ + cors::{Any, CorsLayer}, + trace::{DefaultOnFailure, DefaultOnRequest, DefaultOnResponse, TraceLayer}, +}; +use tracing::{field::Empty, info, instrument, level_filters::LevelFilter, Level}; +use tracing_opentelemetry::OpenTelemetrySpanExt; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer}; + +#[derive(Debug, Serialize, Deserialize)] +struct User { + id: u32, + name: String, + email: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct CreateUserRequest { + name: String, + email: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct ApiResponse { + success: bool, + data: Option, + message: String, +} + +#[instrument] +async fn health_check() -> Json> { + // Simulate health check + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + + info!("Health check performed"); + + Json(ApiResponse { + success: true, + data: None, + message: "Service is healthy".to_string(), + }) +} + +#[instrument] +async fn root() -> &'static str { + info!("Root endpoint accessed"); + "Datadog OpenTelemetry Example API\n\nAvailable endpoints:\n- GET /health - Health check\n- GET /users/{id} - Get user by ID\n- POST /users - Create new user" +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + info!("Starting Datadog OpenTelemetry example application..."); + + // Initialize Datadog OpenTelemetry pipeline + let tracer_provider = datadog_opentelemetry::tracing() + .with_config( + Config::builder() + .set_trace_agent_url("http://0.0.0.0:8126".into()) + .set_service("dd-trace-example".to_string()) + .set_version("1.0.0".to_string()) + .set_env("development".to_string()) + .set_log_level_filter(DdLevelFilter::Info) + .build(), + ) + .init(); + + info!("Datadog pipeline initialized successfully"); + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer().with_filter(LevelFilter::DEBUG)) + .with( + tracing_opentelemetry::layer().with_tracer(tracer_provider.tracer("dd-trace-example")), + ) + .try_init()?; + + // Create CORS layer + let cors = CorsLayer::new() + .allow_methods([Method::GET, Method::POST]) + .allow_origin(Any); + + // Build our application with a route + let app = Router::new() + .route("/", get(root)) + .route("/health", get(health_check)) + .layer(cors) + .layer( + TraceLayer::new_for_http() + .make_span_with(make_span) + .on_request(DefaultOnRequest::new().level(Level::DEBUG)) + .on_response(DefaultOnResponse::new().level(Level::DEBUG)) + .on_failure(DefaultOnFailure::new().level(Level::ERROR)), + ); + + // Run it + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + info!("Starting server on {}", addr); + + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await?; + + // Shutdown the tracer provider + tracer_provider.shutdown()?; + + Ok(()) +} + +fn make_span(request: &Request) -> tracing::Span { + let route = request.uri().path(); + let span = tracing::info_span!( + target: "otel::tracing", + "incoming request", + // Tracing span name must be a static string, but we can use this field to use a + // dynamic string for the opentelemetry span name. + // See https://github.com/tokio-rs/tracing/pull/732. + otel.name = %route, + http.grpc_status = Empty, + http.grpc_status_str = Empty, + error.message = Empty, + rpc.system = "grpc", + uri = %request.uri(), + route = route, + org_id = Empty, + upstream_req_id = Empty, + query_source = Empty, + ); + + // Extract tracing information from incoming request and propagate it + // to this span + let remote_parent_ctx = opentelemetry::global::get_text_map_propagator(|propagator| { + let extractor = opentelemetry_http::HeaderExtractor(request.headers()); + propagator.extract(&extractor) + }); + + let _ = span.set_parent(remote_parent_ctx.clone()); + span +} diff --git a/examples/Cargo.toml b/examples/Cargo.toml new file mode 100644 index 00000000..c5242467 --- /dev/null +++ b/examples/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "dd-trace-examples" +rust-version.workspace = true +edition.workspace = true +version.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +description.workspace = true + +[dependencies] +# Datadog OpenTelemetry +datadog-opentelemetry = { path = "../datadog-opentelemetry" } + +# Tracing ecosystem +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-opentelemetry = "0.24" + +# HTTP server +axum = "0.7" +tokio = { version = "1.44.1", features = ["full"] } + +# OpenTelemetry +opentelemetry = { version = "0.30.0", features = ["trace"] } +opentelemetry_sdk = { version = "0.30.0", features = ["trace"] } + +# Utilities +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tower = "0.4" +tower-http = { version = "0.5", features = ["cors", "trace"] } +rand = "0.8" diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..d7c74907 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,145 @@ +# Datadog OpenTelemetry Example + +This example demonstrates how to use `datadog-opentelemetry` with the `tracing` crate and `tracing-opentelemetry` bridge in an Axum HTTP server. + +## Features + +- **Datadog OpenTelemetry Integration**: Uses the `datadog-opentelemetry` crate for sending traces to Datadog +- **Tracing Bridge**: Demonstrates the `tracing-opentelemetry` bridge for seamless integration +- **HTTP Server**: Built with Axum framework +- **Structured Logging**: Uses the `tracing` crate for structured logging +- **Custom Spans**: Shows how to create custom spans with attributes +- **REST API**: Includes endpoints for user management and health checks + +## Prerequisites + +1. **Datadog Agent**: Make sure you have a Datadog agent running locally or accessible +2. **Rust**: Ensure you have Rust 1.84.1+ installed + +## Running the Example + +### 1. Start the Datadog Agent (if running locally) + +```bash +# Using Docker +docker run -d --name datadog-agent \ + -e DD_API_KEY=your_api_key \ + -e DD_APM_ENABLED=true \ + -e DD_APM_NON_LOCAL_TRAFFIC=true \ + -p 8126:8126 \ + datadog/agent:latest + +# Or using the official install script +DD_API_KEY=your_api_key DD_APM_ENABLED=true bash -c "$(curl -L https://s1.datadoghq.com/install_script_agent7.sh)" +``` + +### 2. Run the Example + +```bash +# From the examples directory +cargo run + +# Or from the workspace root +cargo run -p dd-trace-examples +``` + +The server will start on `http://localhost:3000` + +### 3. Test the API + +```bash +# Health check +curl http://localhost:3000/health + +# Get user by ID +curl http://localhost:3000/users/123 + +# Create a new user +curl -X POST http://localhost:3000/users \ + -H "Content-Type: application/json" \ + -d '{"name": "John Doe", "email": "john@example.com"}' +``` + +## Configuration + +The example is configured to send traces to `http://localhost:8126/v0.5/traces` (default Datadog agent endpoint). You can modify the configuration in the `main()` function: + +```rust +let pipeline = DatadogPipeline::new() + .with_service_name("dd-trace-example") + .with_service_version("1.0.0") + .with_env("development") + .with_trace_endpoint("http://localhost:8126/v0.5/traces") + .build()?; +``` + +## Key Components + +### 1. Datadog Pipeline Setup + +```rust +use datadog_opentelemetry::DatadogPipeline; + +let pipeline = DatadogPipeline::new() + .with_service_name("dd-trace-example") + .with_service_version("1.0.0") + .with_env("development") + .build()?; +``` + +### 2. Tracing Integration + +```rust +use tracing::{info, instrument}; +use tracing_opentelemetry::OpenTelemetrySpanExt; + +#[instrument(skip(tracer))] +async fn get_user(Path(id): Path, tracer: Extension) -> Json> { + let span = tracer.start("get_user"); + span.set_attribute(opentelemetry::KeyValue::new("user.id", id.to_string())); + // ... function logic + span.end(); +} +``` + +### 3. Global Tracer Setup + +```rust +use opentelemetry::global; + +let tracer = pipeline.tracer(); +global::set_tracer_provider(pipeline.trace_provider()); +``` + +## What You'll See + +1. **Console Logs**: Structured logging output showing the application flow +2. **Datadog Traces**: Spans and traces sent to your Datadog agent +3. **Custom Attributes**: User ID, name, and email attributes on spans +4. **Performance Metrics**: Timing information for each operation + +## Troubleshooting + +### Common Issues + +1. **Connection Refused**: Make sure the Datadog agent is running and accessible +2. **Permission Denied**: Check that the agent endpoint is correct and accessible +3. **No Traces in Datadog**: Verify your API key and agent configuration + +### Debug Mode + +The example includes debug logging for the datadog-opentelemetry crate: + +```rust +tracing_subscriber::fmt() + .with_env_filter("info,datadog_opentelemetry=debug") + .init(); +``` + +## Next Steps + +- Modify the service name, version, and environment +- Add more custom attributes to spans +- Implement error handling and error spans +- Add metrics collection +- Configure sampling and filtering rules diff --git a/examples/src/main.rs b/examples/src/main.rs new file mode 100644 index 00000000..afbae61c --- /dev/null +++ b/examples/src/main.rs @@ -0,0 +1,162 @@ +use axum::{ + extract::Path, + http::{Method, StatusCode}, + response::Json, + routing::{get, post}, + Router, +}; +use datadog_opentelemetry::DatadogPipeline; +use opentelemetry::{ + global, + trace::{Span, Tracer}, +}; +use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; +use tower_http::cors::{Any, CorsLayer}; +use tracing::{info, instrument, warn}; +use tracing_opentelemetry::OpenTelemetrySpanExt; + +#[derive(Debug, Serialize, Deserialize)] +struct User { + id: u32, + name: String, + email: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct CreateUserRequest { + name: String, + email: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct ApiResponse { + success: bool, + data: Option, + message: String, +} + +#[instrument(skip(tracer))] +async fn get_user(Path(id): Path, tracer: axum::extract::Extension) -> Json> { + let span = tracer.start("get_user"); + span.set_attribute(opentelemetry::KeyValue::new("user.id", id.to_string())); + + // Simulate some work + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + + let user = User { + id, + name: format!("User {}", id), + email: format!("user{}@example.com", id), + }; + + info!("Retrieved user: {:?}", user); + span.end(); + + Json(ApiResponse { + success: true, + data: Some(user), + message: "User retrieved successfully".to_string(), + }) +} + +#[instrument(skip(tracer))] +async fn create_user( + Json(payload): Json, + tracer: axum::extract::Extension, +) -> Json> { + let span = tracer.start("create_user"); + span.set_attribute(opentelemetry::KeyValue::new("user.name", payload.name.clone())); + span.set_attribute(opentelemetry::KeyValue::new("user.email", payload.email.clone())); + + // Simulate some work + tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; + + let user = User { + id: rand::random::() % 10000, + name: payload.name, + email: payload.email, + }; + + info!("Created user: {:?}", user); + span.end(); + + Json(ApiResponse { + success: true, + data: Some(user), + message: "User created successfully".to_string(), + }) +} + +#[instrument(skip(tracer))] +async fn health_check(tracer: axum::extract::Extension) -> Json> { + let span = tracer.start("health_check"); + + // Simulate health check + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + + info!("Health check performed"); + span.end(); + + Json(ApiResponse { + success: true, + data: None, + message: "Service is healthy".to_string(), + }) +} + +#[instrument] +async fn root() -> &'static str { + info!("Root endpoint accessed"); + "Datadog OpenTelemetry Example API\n\nAvailable endpoints:\n- GET /health - Health check\n- GET /users/{id} - Get user by ID\n- POST /users - Create new user" +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing subscriber + tracing_subscriber::fmt() + .with_env_filter("info,datadog_opentelemetry=debug") + .init(); + + info!("Starting Datadog OpenTelemetry example application..."); + + // Initialize Datadog OpenTelemetry pipeline + let pipeline = DatadogPipeline::new() + .with_service_name("dd-trace-example") + .with_service_version("1.0.0") + .with_env("development") + .with_trace_endpoint("http://localhost:8126/v0.5/traces") // Default Datadog agent endpoint + .build()?; + + info!("Datadog pipeline initialized successfully"); + + // Get the tracer from the pipeline + let tracer = pipeline.tracer(); + + // Set the global tracer + global::set_tracer_provider(pipeline.trace_provider()); + + // Create CORS layer + let cors = CorsLayer::new() + .allow_methods([Method::GET, Method::POST]) + .allow_origin(Any); + + // Build our application with a route + let app = Router::new() + .route("/", get(root)) + .route("/health", get(health_check)) + .route("/users/:id", get(get_user)) + .route("/users", post(create_user)) + .layer(cors) + .layer(axum::extract::Extension(tracer)); + + // Run it + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + info!("Starting server on {}", addr); + + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await?; + + Ok(()) +}