This project demonstrates how to integrate OpenTelemetry with Jaeger for distributed tracing in a .NET 9 Web API application, including Entity Framework Core database operations. Now with Azure Application Insights support!
- Understand OpenTelemetry fundamentals
- Set up Jaeger for distributed tracing
- Implement custom spans and attributes
- Monitor API performance and behavior
- Track database operations with EF Core instrumentation
- Handle exceptions with automatic tracing
- Deploy Azure Application Insights with Infrastructure as Code π
- .NET 9 SDK
- Docker Desktop
- SQL Server LocalDB (included with Visual Studio) or SQL Server Express
- Visual Studio 2022 or Visual Studio Code (optional)
- Azure Developer CLI (azd) - Install Guide
- Azure subscription with active credits
- Azure CLI (optional but recommended)
First, start the Jaeger container using Docker Compose:
docker-compose up -dThis will start Jaeger with the following ports:
- 16686: Jaeger UI (http://localhost:16686)
- 14268: Jaeger agent endpoint
- 4317: OTLP gRPC receiver
- 4318: OTLP HTTP receiver
Ensure SQL Server LocalDB is running:
sqllocaldb start MSSQLLocalDBBuild and run the application:
dotnet build JaegerDemo.Api.csproj
dotnet run --project JaegerDemo.Api.csprojThe database will be automatically created on first run using EF Core migrations.
The API will be available at:
- HTTPS: https://localhost:7064
- HTTP: http://localhost:5182
Use the provided HTTP file (JaegerDemo.Api.http) or make requests manually:
# Get weather forecast
curl https://localhost:7064/weatherforecast -k
# Get weather for a specific city
curl https://localhost:7064/weather/London -k
# Save weather data to database
curl -X POST https://localhost:7064/weather/record -k \
-H "Content-Type: application/json" \
-d '{"city": "London", "temperature": 18, "summary": "Mild"}'
# Get weather records for a city
curl https://localhost:7064/weather/records/London -k
# Get all cities with weather data
curl https://localhost:7064/weather/cities -k- Open your browser and navigate to http://localhost:16686
- Select "JaegerDemo.Api" from the service dropdown
- Click "Find Traces" to see your application traces
- Notice the database query spans within your traces!
This project includes infrastructure-as-code for deploying Azure Application Insights using Azure Developer CLI (azd).
- Azure Developer CLI (azd) - Install Guide
- Azure subscription with active credits
- Azure CLI (optional but recommended)
# Navigate to project root
cd D:\Source\Repos\JaegerGettingStarted
# Deploy with automatic configuration update
.\infra\AzureAppInsight\deploy.ps1 -UpdateAppSettings# Navigate to project root
cd ~/JaegerGettingStarted
# Make script executable (first time only)
chmod +x infra/AzureAppInsight/deploy.sh
# Deploy with automatic configuration update
./infra/AzureAppInsight/deploy.sh -uazd auth login
azd env new jaeger-demo
azd provision- β Azure Resource Group
- β Log Analytics Workspace
- β Application Insights resource
- β Connection string automatically retrieved
- β
Optional:
appsettings.jsonautomatically updated
After deployment, your application sends telemetry to BOTH:
- Jaeger (localhost) - For local development and learning
- Azure Application Insights - For production-grade monitoring
View traces in:
- Jaeger UI: http://localhost:16686 (instant feedback)
- Azure Portal: Application Insights β Transaction search (2-3 min delay)
- Free tier: First 5 GB/month is free
- Most development apps: Stay within free tier
- Beyond free tier: ~$2.30/GB
- Set daily caps to control costs (see infrastructure docs)
Detailed guides available in infra/AzureAppInsight/:
- README.md - Complete deployment guide
- QUICK_REFERENCE.md - Command reference and troubleshooting
- deploy.ps1 / deploy.sh - Automated deployment scripts
azd down| Method | Endpoint | Description |
|---|---|---|
| GET | /weatherforecast |
Get a 5-day weather forecast |
| GET | /weather/{city} |
Get current weather for a specific city |
| Method | Endpoint | Description |
|---|---|---|
| POST | /weather/record |
Save a weather record to SQL Server database |
| GET | /weather/records/{city} |
Get last 10 weather records for a specific city |
| GET | /weather/latest/{city} |
Get the most recent weather record for a city |
| GET | /weather/cities |
Get all cities with weather data |
POST /weather/record
{
"city": "London",
"temperature": 18,
"summary": "Mild"
}GET /weather/records/LondonResponse:
[
{
"id": 1,
"city": "London",
"temperature": 18,
"summary": "Mild",
"recordedAt": "2025-01-24T16:30:45.123Z"
}
]The application automatically traces:
- HTTP requests (ASP.NET Core instrumentation with automatic exception recording)
- HTTP client calls (HttpClient instrumentation)
- Database queries (Entity Framework Core instrumentation) - NEW!
- SQL queries are visible in Jaeger spans
- Query execution times are tracked
- Database connection information is captured
This demo demonstrates two different approaches to working with OpenTelemetry activities:
app.MapGet("/weatherforecast", async (ILogger<Program> logger) =>
{
// ASP.NET Core middleware already created an activity for this HTTP request
// We're just adding custom tags to that existing activity
Activity.Current?.SetTag("forecast.count", 5);
await Task.Delay(Random.Shared.Next(50, 200));
var forecast = Enumerable.Range(1, 5).Select(index => /* ... */).ToArray();
Activity.Current?.SetSuccess();
return forecast;
});What happens in Jaeger:
- ONE span:
GET /weatherforecast(created by ASP.NET Core middleware) - The span includes our custom tags:
forecast.count=5 - Clean and simple for straightforward endpoints
app.MapGet("/weather/{city}", async (string city, ILogger<Program> logger) =>
{
// We're creating NEW child activities under the ASP.NET Core activity
using var activity = activitySource.StartActivity("get-weather-for-city");
activity?.SetTag("city", city);
using var apiActivity = activitySource.StartActivity("external-weather-api-call");
apiActivity?.SetTag("api.endpoint", "external-weather-service");
await Task.Delay(Random.Shared.Next(100, 500));
// ... rest of code
});What happens in Jaeger:
- Three spans in a parent-child hierarchy:
- Parent:
GET /weather/{city}(ASP.NET Core middleware) - Child 1:
get-weather-for-city(our custom span) - Child 2:
external-weather-api-call(nested under child 1)
- Parent:
- Better visibility into internal operations
- Useful for breaking down complex operations
| Approach | Use When | Benefits |
|---|---|---|
| Activity.Current | Simple endpoints with no sub-operations | Cleaner traces, less overhead |
| Child Spans | Complex operations that need breakdown | Better observability, timing of sub-operations |
Key Insight: ASP.NET Core middleware always creates a root activity for every HTTP request. You're choosing whether to:
- Enhance it with tags (
Activity.Current) - Create child spans to break down the work (
activitySource.StartActivity())
The application creates custom spans for:
generate-weather-forecast: Weather forecast generationget-weather-for-city: Single city weather processingexternal-weather-api-call: Simulated external API callssave-weather-record: Saving weather data to database - NEW!get-weather-records: Fetching weather records from database - NEW!get-latest-weather: Getting latest weather record - NEW!get-all-cities: Fetching all cities with data - NEW!
Each span includes relevant attributes:
forecast.count: Number of forecast dayscity: City name being processedapi.endpoint: External API endpoint namerecord.id: Database record ID - NEW!records.count: Number of records fetched - NEW!record.found: Whether a record was found - NEW!temperature: Temperature value - NEW!
EF Core instrumentation automatically creates spans for:
INSERToperations with SQL statementsSELECToperations with SQL statements- Database connection operations
- Tags include:
db.system:mssqldb.name:WeatherDbdb.statement: Actual SQL query executed
All exceptions are automatically recorded in traces with:
- Exception type (e.g.,
SqlException,InvalidOperationException) - Full error message
- Complete stack trace
- No manual try-catch blocks required!
- SQL Server LocalDB for development
- Entity Framework Core 9.0 for data access
- Automatic migrations on application startup
Located in appsettings.json:
{
"ConnectionStrings": {
"WeatherDb": "Server=(localdb)\\mssqllocaldb;Database=WeatherDb;Trusted_Connection=True;TrustServerCertificate=True"
}
}WeatherRecords table:
Id(int, primary key)City(nvarchar)Temperature(int)Summary(nvarchar)RecordedAt(datetime2)
Connect to (localdb)\mssqllocaldb using:
- SQL Server Object Explorer in Visual Studio
- Azure Data Studio
- SQL Server Management Studio (SSMS)
The OpenTelemetry configuration is in Program.cs:
builder.Services.AddOpenTelemetry()
.WithTracing(tracerProviderBuilder =>
{
tracerProviderBuilder
.SetResourceBuilder(ResourceBuilder.CreateDefault()
.AddService("JaegerDemo.Api", "1.0.0"))
.SetSampler(new AlwaysOnSampler())
.AddAspNetCoreInstrumentation(options =>
{
options.RecordException = true; // Automatic exception recording
})
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation() // Database tracing
.AddSource("JaegerDemo.Api")
.AddOtlpExporter(options =>
{
options.Protocol = OtlpExportProtocol.Grpc;
options.Endpoint = new Uri("http://localhost:4317");
});
});- AlwaysOnSampler: All requests are sampled (good for development/learning)
- Automatic exception recording: No try-catch boilerplate needed
- EF Core instrumentation: Database queries automatically traced
- Exception tracing middleware: Captures all unhandled exceptions
The Jaeger container is configured in docker-compose.yml with OTLP support enabled.
- AZURE_APPLICATION_INSIGHTS_INTEGRATION.md: Complete Azure integration guide π
- EF_CORE_OPENTELEMETRY.md: Entity Framework Core integration guide
- SQL_SERVER_MIGRATION.md: Complete SQL Server setup guide
- QUICK_START_SQL_SERVER.md: Quick verification steps
- ERROR_HANDLING_GUIDE.md: Exception tracing guide with examples
- REDUCING_BOILERPLATE.md: Best practices for clean code
- CODE_SIMPLIFICATION_APPLIED.md: Before/after code comparisons
- infra/AzureAppInsight/README.md: Infrastructure deployment guide π
- OpenTelemetry .NET Documentation
- Jaeger Documentation
- ASP.NET Core OpenTelemetry
- EF Core OpenTelemetry
By exploring this demo, you'll understand:
- β How to set up OpenTelemetry in .NET 9
- β How to integrate Jaeger for distributed tracing
- β How to create custom spans and add attributes
- β How to automatically trace database operations
- β How to handle exceptions with automatic recording
- β How to reduce observability boilerplate code
- β How to debug performance issues using traces
- β How to track SQL queries and their execution time
- β Automatic HTTP tracing (ASP.NET Core)
- β Custom activity spans with tags and events
- β Database query tracing (Entity Framework Core)
- β Automatic exception recording (no try-catch needed!)
- β SQL Server integration with LocalDB
- β OTLP over gRPC export to Jaeger
- β Azure Application Insights integration π
- β Infrastructure as Code (Bicep) π
- β Azure Developer CLI (azd) deployment π
- β Clean code patterns with extension methods
- β Error visualization in Jaeger UI
- β Performance monitoring of database queries
- β Production-ready cloud monitoring π
Happy Tracing! π If you have questions, check the documentation in the Help/ and infra/ folders or create an issue on GitHub.