Building “Production-Grade” APIs in .NET - Part 4: You Can’t Fix What You Can’t See: Observability in .NET APIs"

Author
By Giannis Georgopoulos ·


Series: Building "Production-Grade" APIs in .NET

  1. Intro - Building “Production-Grade” APIs in .NETBuilding “Production-Grade” APIs in .NET – IntroSeries: Building "Production-Grade" APIs in .NET 1. Intro - Building “Production-Grade” APIs in .NET 1. Part 1 - Designing Clean, Intuitive APIs That Don’t Confuse Consumers 1. Part 2 - A Professional Looking API 1. Part 3 - Logging Like a Pro in .NET 1. Part 4 - Observability .NET Many engineers build and deploy APIs into production. So we have an API running in production — does that mean it’s truly production-grade? More often than not, the answer is no. We write the code, test it lo
  2. Part 1 - Designing Clean, Intuitive APIs That Don’t Confuse ConsumersBuilding “Production-Grade” APIs in .NET - Part 1: Designing Clean and Intuitive APIsSeries: Building "Production-Grade" APIs in .NET 1. Intro - Building “Production-Grade” APIs in .NET 1. Part 1 - Designing Clean, Intuitive APIs That Don’t Confuse Consumers 1. Part 2 - A Professional Looking API 1. Part 3 - Logging Like a Pro in .NET 1. Part 4 - Observability .NET Let’s say you join a new team and find this in one of the core API controllers: [HttpPost("updateOrder")] public async Task UpdateOrder([FromBody] OrderDto order) { var updatedOrder = await _orderService.Upd
  3. Part 2 - A Professional Looking APIBuilding “Production-Grade” APIs in .NET – Part 2: A Professional Looking APISeries: Building "Production-Grade" APIs in .NET 1. Intro - Building “Production-Grade” APIs in .NET 1. Part 1 - Designing Clean, Intuitive APIs That Don’t Confuse Consumers 1. Part 2 - A Professional Looking API 1. Part 3 - Logging Like a Pro in .NET 1. Part 4 - Observability .NET Your OpenAPI spec is the contract that defines how consumers interact with your API. It powers visual tools that help developers understand, test, and integrate with your endpoints. This spec powers visual tools
  4. Part 3 - Logging Like a Pro in .NETBuilding “Production-Grade” APIs in .NET - Part 3: Logging Like a Pro in .NETSeries: Building "Production-Grade" APIs in .NET 1. Intro - Building “Production-Grade” APIs in .NET 1. Part 1 - Designing Clean, Intuitive APIs That Don’t Confuse Consumers 1. Part 2 - A Professional Looking API 1. Part 3 - Logging Like a Pro in .NET 1. Part 4 - Observability .NET Logs are your primary tool for understanding what your API is doing in production. Whether it's debugging a bug report, identifying performance issues, or reacting to an incident logs are your first and best sign
  5. Part 4 - Observability .NETBuilding “Production-Grade” APIs in .NET - Part 4: You Can’t Fix What You Can’t See: Observability in .NET APIs"Series: Building "Production-Grade" APIs in .NET 1. Intro - Building “Production-Grade” APIs in .NET 1. Part 1 - Designing Clean, Intuitive APIs That Don’t Confuse Consumers 1. Part 2 - A Professional Looking API 1. Part 3 - Logging Like a Pro in .NET 1. Part 4 - Observability .NET Logs are a great starting point — but they’re not enough. In small systems, logs might be all you need. But once you’re dealing with a distributed system, logging alone quickly falls short. Imagine a typical r

Logs are a great starting point — but they’re not enough.

In small systems, logs might be all you need. But once you’re dealing with a distributed system, logging alone quickly falls short.

Imagine a typical request in a microservices setup:

  • The API receives a request.

  • It saves data to a database.

  • It publishes an event to a message bus.

  • A downstream service picks up the event and performs additional work.

You may log each of these actions, but if something goes wrong, how do you know where it went wrong? Or why a request took longer than usual?

Why Logging Alone Isn’t Enough

Logs are great for showing what happened, but they don’t give you the full picture in two key areas:

  1. Cross-service behavior:
    You need to trace a request as it hops across services, databases, queues, etc. Logs alone don’t provide correlation between components.

  2. Metrics and aggregate insights:
    Logs won’t tell you things like:

    • How many requests are coming in per minute?

    • What are peak traffic hours?

    • Which endpoints are the slowest?

    • Is latency caused by the database, an internal service call, or external dependency?

    • How many orders per day do we process?

    • What’s the average order size?

These are fundamental questions for both technical and business observability.

The Three Pillars of Observability

To build real observability into your systems, you need three distinct tools working together:

  • Logging – tells you what happened

  • Metrics – tell you how often and how badly

  • Tracing – tells you where it happened (end-to-end path of a request)

Together, they let you understand, diagnose, and improve your system — even under pressure.

Observability = Turning Runtime Chaos Into Diagnosable Insights

Observability is what lets you go from "Something is wrong" to "Here’s what’s wrong and how to fix it", with speed and confidence.

Now let’s see how to add observability to a .NET API using tools like OpenTelemetry.

Setting up Observability in .NET with OpenTelemetry

1. Install OpenTelemetry NuGet Packages

These give you the core SDK, along with various instrumentation libraries for HTTP, SQL, etc., and exporters that send data somewhere (like Jaeger or the console).

dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package OpenTelemetry.Instrumentation.SqlClient
dotnet add package OpenTelemetry.Metrics
dotnet add package OpenTelemetry.Exporter.Console
dotnet add package OpenTelemetry.Exporter.Jaeger

2. Add OpenTelemetry to the Dependency Injection Container

In your Program.cs, you use .AddOpenTelemetry() to register everything in the DI system.

var builder = WebApplication.CreateBuilder(args);

// Add OpenTelemetry to DI
builder.Services.AddOpenTelemetry()
    .WithTracing(tracerProviderBuilder =>
    {
        tracerProviderBuilder
            .AddAspNetCoreInstrumentation()     // Tracks incoming HTTP requests
            .AddHttpClientInstrumentation()     // Tracks outgoing HTTP calls
            .AddSqlClientInstrumentation()      // Tracks SQL queries
            .AddConsoleExporter()               // Outputs data to console (dev)
            .AddJaegerExporter(options =>       // Sends traces to Jaeger
            {
                options.AgentHost = "localhost";
                options.AgentPort = 6831;
            });
    })
    .WithMetrics(metricsBuilder =>
    {
        metricsBuilder
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddRuntimeInstrumentation()        // GC, threads, etc.
            .AddProcessInstrumentation()        // CPU/memory stats
            .AddConsoleExporter();
    });
    
builder.Services.AddControllers();

var app = builder.Build();
app.MapControllers();
app.Run();

Note: There are many instrumentation options available, and you can pick what makes sense for your app:

  • .AddGrpcClientInstrumentation()
  • .AddRedisInstrumentation()
  • .AddEntityFrameworkCoreInstrumentation()
  • etc.

3. What Is Jaeger?

Jaeger is a popular open-source distributed tracing backend. It collects spans (units of work) and visualizes full traces of requests flowing through your services.

For local testing, you can spin up Jaeger with Docker:

docker run --rm --name jaeger \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318 \
  -p 5778:5778 \
  -p 9411:9411 \
  jaegertracing/jaeger:2.7.0

Then open http://localhost:16686 to view your traces.

You can also use exporters for:

  • Azure Monitor (Application Insights)
  • Prometheus
  • OTLP Platforms like Honeycomb, New Relic, Datadog, etc

4.Tracking Custom Metrics

For business-specific metrics like "orders placed," you can create your own Meter:

var meter = new Meter("MyApp.Metrics", "1.0");
builder.Services.AddSingleton(meter);

Then wire it up in the metrics pipeline:

metricsBuilder.AddMeter("MyApp.Metrics");

And finally, use it in your controllers:

[ApiController]
[Route("[controller]")]
public class OrdersController(Meter meter) : ControllerBase
{
	private readonly Counter<int> _ordersPlaced = meter.CreateCounter<int>("orders_placed");
	
    [HttpPost]
    public async Task<IActionResult> Place(Order order)
    {
		_ordersPlaced.Add(1);
		// Your business logic here...
		return Ok();
    }
}

You can also create histograms, observable gauges, and more.

Recap

With just a few lines of setup, you now have:

✅ Distributed tracing across services and dependencies
✅ System and business metrics
✅ A foundation for logs, dashboards, and alerts in your observability stack

You’ve taken a big step beyond just “adding logging”, you’ve made your system observable.

A Note on Scope

While we’ve set up basic observability using mostly auto-instrumented libraries and a simple custom metric, it’s important to recognize that observability is a deep, nuanced topic, one that could easily become its own dedicated series.

In this post, we intentionally kept it simple:

  • We didn’t dive into custom spans or trace enrichment

  • We skipped trace sampling strategies

  • We didn’t touch on structured logging correlation or advanced exporter setups

These are all powerful tools that help you go beyond “it’s working” to “why is it working that way?”

Our goal here was to show that getting started isn’t hard, and even lightweight observability can make a huge impact in real systems.