# Logging

Course: [C# Advanced](https://www.devtutorials.ai/learn/advanced-csharp) · Section: Advanced Development
Source: https://www.devtutorials.ai/learn/advanced-csharp/advanced-development/logging

> Record what your app does: ILogger, log levels, structured logging, providers, console and file logging, exception logging, diagnostics, debugging, tracing and metrics.

## Introduction to Logging

Think about the **black box** on an aeroplane.

During a normal flight, nobody looks at it. But if something goes wrong, the black box tells investigators exactly what happened, in what order, and why.

**Logging** is the black box for your program. While it runs, it writes short messages about what it is doing:

```text
09:14:02 info  Order ORD-2201 placed by Asha, total Rs. 1,299
09:14:03 warn  Payment gateway slow: 2,450 ms
09:14:05 error Email to asha@example.com failed: SMTP timeout
```

On your own machine you can use a debugger. On a server at 3 a.m., with real users, **logs are all you have**.

**In simple words:** logging is your program keeping a diary, so you can find out later what happened.

**Easy Way to Remember:** **if it is not logged, it did not happen** (as far as anyone can prove).

## What You Will Learn

In this lesson, you will learn:

1. How `ILogger` and `ILogger<T>` work.
2. The six log levels and when to use each.
3. Structured logging with message templates.
4. What logging providers are, including console and file logging.
5. How to log exceptions properly.
6. Diagnostic tools and debugging techniques.
7. Tracing with `Activity` and performance monitoring with metrics.
8. How to add diagnostics to a real application.

The examples run in your browser with `Microsoft.Extensions.Logging`, the same logging library ASP.NET Core uses. The ready-made **console** and **file** provider packages are not part of the browser runtime, so the examples include a small provider written by hand. The real package code is shown as plain text next to it.

## Start With a Working Example

Press **Run**.

```csharp runnable title="Your first logger"
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var services = new ServiceCollection();
services.AddLogging(logging =>
{
    logging.AddProvider(new SimpleConsoleProvider());   // in a real app: logging.AddConsole();
    logging.SetMinimumLevel(LogLevel.Debug);
});
services.AddTransient<OrderService>();

var provider = services.BuildServiceProvider();
provider.GetRequiredService<OrderService>().Place("ORD-2201", "Asha", 1299m);

class OrderService
{
    private readonly ILogger<OrderService> _logger;
    public OrderService(ILogger<OrderService> logger) => _logger = logger;

    public void Place(string orderId, string customer, decimal total)
    {
        _logger.LogDebug("Validating order {OrderId}", orderId);
        _logger.LogInformation("Order {OrderId} placed by {Customer}, total {Total}", orderId, customer, total);
    }
}

// A tiny provider that writes to the console. Real apps use the
// Microsoft.Extensions.Logging.Console package instead.
class SimpleConsoleProvider : ILoggerProvider
{
    public ILogger CreateLogger(string category) => new SimpleConsoleLogger(category);
    public void Dispose() { }
}

class SimpleConsoleLogger : ILogger
{
    private readonly string _category;
    public SimpleConsoleLogger(string category) => _category = category;
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
    public bool IsEnabled(LogLevel level) => level != LogLevel.None;

    public void Log<TState>(LogLevel level, EventId id, TState state, Exception? error,
        Func<TState, Exception?, string> format)
    {
        Console.WriteLine($"{level,-11} {_category}: {format(state, error)}");
        if (error != null) Console.WriteLine($"            {error.GetType().Name}: {error.Message}");
    }
}
```

`OrderService` asks for an `ILogger<OrderService>` and writes two messages. It does not know or care **where** those messages go. That is decided once, at startup.

## ILogger

**`ILogger`** is the interface you write log messages to. It lives in `Microsoft.Extensions.Logging`. You almost always use **`ILogger<T>`**, where `T` is your class.

The `T` sets the logger's **category**: the full class name, such as `OrderService` or `MyShop.Payments.PaymentService`. The category shows up in every message, so you can see **which class** wrote it, and you can filter by it.

You get a logger through **dependency injection**, from lesson 17:

```text
public class PaymentService
{
    private readonly ILogger<PaymentService> _logger;

    public PaymentService(ILogger<PaymentService> logger) => _logger = logger;
}
```

`ILogger` has convenient extension methods, one per level:

| Method | Level |
|---|---|
| `LogTrace(...)` | Trace |
| `LogDebug(...)` | Debug |
| `LogInformation(...)` | Information |
| `LogWarning(...)` | Warning |
| `LogError(...)` | Error |
| `LogCritical(...)` | Critical |

Outside a dependency injection container, you can build loggers with **`LoggerFactory.Create`**:

```csharp runnable title="LoggerFactory without dependency injection"
using System;
using Microsoft.Extensions.Logging;

using ILoggerFactory factory = LoggerFactory.Create(builder =>
    builder.AddProvider(new SimpleConsoleProvider()));

ILogger<Program> programLog = factory.CreateLogger<Program>();
ILogger importLog = factory.CreateLogger("Shop.Import");

programLog.LogInformation("App started");
importLog.LogWarning("File {FileName} has {Count} empty rows", "customers.csv", 3);

class SimpleConsoleProvider : ILoggerProvider
{
    public ILogger CreateLogger(string category) => new SimpleConsoleLogger(category);
    public void Dispose() { }
}

class SimpleConsoleLogger : ILogger
{
    private readonly string _category;
    public SimpleConsoleLogger(string category) => _category = category;
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
    public bool IsEnabled(LogLevel level) => true;
    public void Log<TState>(LogLevel level, EventId id, TState state, Exception? error, Func<TState, Exception?, string> format)
        => Console.WriteLine($"{level,-11} [{_category}] {format(state, error)}");
}
```

## Log Levels

Not every message is equally important. A **log level** describes how serious a message is.

| Level | Number | Use it for | Example |
|---|---|---|---|
| **Trace** | 0 | The tiniest details; off almost always | "Entering CalculateTax with 3 items" |
| **Debug** | 1 | Details useful while developing | "Cache miss for product 42" |
| **Information** | 2 | Normal, important events | "Order ORD-2201 placed" |
| **Warning** | 3 | Something odd that did not stop the work | "Payment gateway took 2.4 s" |
| **Error** | 4 | An operation failed | "Could not send email to Asha" |
| **Critical** | 5 | The whole app is in trouble | "Database unreachable; shutting down" |

You choose a **minimum level**. Messages below it are skipped, and cost almost nothing. A typical setup is:

- **Development**: `Debug`, to see more details.
- **Production**: `Information` or `Warning`, to keep logs smaller.

```csharp runnable title="The minimum level filters messages"
using System;
using Microsoft.Extensions.Logging;

foreach (LogLevel minimum in new[] { LogLevel.Debug, LogLevel.Warning })
{
    Console.WriteLine($"--- Minimum level: {minimum} ---");
    using var factory = LoggerFactory.Create(b => b
        .AddProvider(new SimpleConsoleProvider())
        .SetMinimumLevel(minimum));

    var log = factory.CreateLogger("Shop");
    log.LogTrace("Trace: entering checkout");
    log.LogDebug("Debug: cart has 3 items");
    log.LogInformation("Information: order placed");
    log.LogWarning("Warning: stock is low");
    log.LogError("Error: payment failed");
}

class SimpleConsoleProvider : ILoggerProvider
{
    public ILogger CreateLogger(string category) => new SimpleConsoleLogger();
    public void Dispose() { }
}

class SimpleConsoleLogger : ILogger
{
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
    public bool IsEnabled(LogLevel level) => true;
    public void Log<TState>(LogLevel level, EventId id, TState state, Exception? error, Func<TState, Exception?, string> format)
        => Console.WriteLine($"  {format(state, error)}");
}
```

### Setting Levels in Configuration

In a real app, levels live in `appsettings.json` (lesson 18), so you can change them without rebuilding the app. You can set a different level for each category:

```text
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "MyShop.Payments": "Debug"
    }
  }
}
```

## Structured Logging

Look at these two lines. They print the same text:

```text
_logger.LogInformation($"Order {orderId} placed by {customer}");        // string interpolation
_logger.LogInformation("Order {OrderId} placed by {Customer}", orderId, customer);   // message template
```

The second one is **structured logging**. The text in quotes is a **message template**. `{OrderId}` and `{Customer}` are **named placeholders**, and the values are passed separately.

The logger keeps the values **as data**, with their names. A log system such as Seq, Elasticsearch or Azure Application Insights can then answer questions like:

- "Show every log where `Customer` = `Asha`."
- "Count orders per hour where `Total` > 1000."

With interpolation, you only have one long text string, and searching for specific values becomes difficult.

```csharp runnable title="Structured logging keeps the values as data"
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;

using var factory = LoggerFactory.Create(b => b.AddProvider(new InspectingProvider()));
var log = factory.CreateLogger("Orders");

string orderId = "ORD-2201", customer = "Asha";
decimal total = 1299.50m;

log.LogInformation("Order {OrderId} placed by {Customer}, total {Total}", orderId, customer, total);

class InspectingProvider : ILoggerProvider
{
    public ILogger CreateLogger(string category) => new InspectingLogger();
    public void Dispose() { }
}

class InspectingLogger : ILogger
{
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
    public bool IsEnabled(LogLevel level) => true;

    public void Log<TState>(LogLevel level, EventId id, TState state, Exception? error, Func<TState, Exception?, string> format)
    {
        Console.WriteLine($"Message: {format(state, error)}");
        Console.WriteLine("Data a log system can search:");
        if (state is IReadOnlyList<KeyValuePair<string, object?>> fields)
            foreach (var (name, value) in fields)
                Console.WriteLine($"  {name,-18} = {value} ({value?.GetType().Name})");
    }
}
```

The logger received the **template**, the **names** and the **typed values**. `Total` is still a `decimal`, not text.

Use **PascalCase names** for placeholders, such as `{OrderId}`, and keep the same name for the same thing everywhere. Then one search for `OrderId` finds every message about that order. Never use `$"..."` interpolation in a log call: it throws the structure away and builds the string even when the level is turned off.

### Log Scopes

A **scope** attaches extra data to **every** message logged inside it, such as the current request ID. You do not have to repeat it in each message.

```text
using (_logger.BeginScope("Request {RequestId} for user {UserId}", requestId, userId))
{
    _logger.LogInformation("Loading cart");      // carries RequestId and UserId
    _logger.LogInformation("Applying coupon");   // carries them too
}
```

ASP.NET Core opens a scope for each web request automatically, with the request ID and path.

## Logging Providers

A **logging provider** decides **where** log messages go. You can add several, and every message goes to all of them.

| Provider | Package | Writes to |
|---|---|---|
| Console | `Microsoft.Extensions.Logging.Console` | The terminal |
| Debug | `Microsoft.Extensions.Logging.Debug` | The Visual Studio Output window |
| EventSource | `Microsoft.Extensions.Logging.EventSource` | .NET tracing tools such as `dotnet-trace` |
| EventLog | `Microsoft.Extensions.Logging.EventLog` | Windows Event Log |
| Application Insights | `Microsoft.Extensions.Logging.ApplicationInsights` | Azure Monitor |
| Serilog, NLog | Third-party | Files, databases, Seq, Elasticsearch and more |

Your classes only depend on `ILogger<T>`. Switching from the console to a cloud service is a **one-line change at startup**. That is dependency inversion again (lesson 17).

A provider has two parts:

- **`ILoggerProvider`** creates a logger for each category.
- **`ILogger`** writes the messages.

That is exactly what the small `SimpleConsoleProvider` in the examples implements.

## Console Logging

Console logging shows what your program writes while you develop, and in containers, where the platform collects everything the app writes to the console.

With the real package installed, setup is one line. This cannot run in the browser, so it is plain code:

```text
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddConsole();                 // human-readable text
// builder.Logging.AddJsonConsole();          // one JSON object per line, for log collectors
// builder.Logging.AddSimpleConsole(o => { o.SingleLine = true; o.TimestampFormat = "HH:mm:ss "; });
```

The console formatter prints the level, the category and the message, with colours:

```text
info: MyShop.OrderService[0]
      Order ORD-2201 placed by Asha, total 1299.50
warn: MyShop.PaymentService[0]
      Payment gateway slow: 2450 ms
```

**`AddJsonConsole()`** is popular in containers and Kubernetes, because tools like Fluent Bit and Azure Monitor read JSON lines easily.

## File Logging

Microsoft does not provide a built-in file provider. Most teams use **Serilog** or **NLog** for files, which handle log file rotation, size limits and retention:

```text
// dotnet add package Serilog.AspNetCore
// dotnet add package Serilog.Sinks.File
Log.Logger = new LoggerConfiguration()
    .WriteTo.File("logs/shop-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 14)
    .CreateLogger();

builder.Host.UseSerilog();
```

To understand how file logging works, here is a small file provider. The browser has an in-memory file system, so it runs here:

```csharp runnable title="A simple file logging provider"
using System;
using System.IO;
using Microsoft.Extensions.Logging;

using (var fileProvider = new FileLoggerProvider("shop.log"))
using (var factory = LoggerFactory.Create(b => b.AddProvider(fileProvider)))
{
    var log = factory.CreateLogger("Checkout");
    log.LogInformation("Cart opened by {Customer}", "Ravi");
    log.LogWarning("Coupon {Code} has expired", "DIWALI10");
    log.LogInformation("Order {OrderId} placed", "ORD-3310");
}   // disposing the provider flushes and closes the file

Console.WriteLine("Contents of shop.log:");
Console.Write(File.ReadAllText("shop.log"));

class FileLoggerProvider : ILoggerProvider
{
    private readonly StreamWriter _writer;
    public FileLoggerProvider(string path) => _writer = new StreamWriter(path, append: false);
    public ILogger CreateLogger(string category) => new FileLogger(category, _writer);
    public void Dispose() => _writer.Dispose();
}

class FileLogger : ILogger
{
    private readonly string _category;
    private readonly StreamWriter _writer;
    public FileLogger(string category, StreamWriter writer) { _category = category; _writer = writer; }
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
    public bool IsEnabled(LogLevel level) => level >= LogLevel.Information;

    public void Log<TState>(LogLevel level, EventId id, TState state, Exception? error, Func<TState, Exception?, string> format)
    {
        if (!IsEnabled(level)) return;
        lock (_writer)       // many threads may log at once
            _writer.WriteLine($"{level.ToString().ToUpper()[..4]} {_category}: {format(state, error)}");
    }
}
```

Notice the `lock`. Many threads log at the same time in a real app, and a file can only be written by one thread at a time. Real file loggers also write on a **background thread**, so logging never slows down a request.

## Exception Logging

When you log an error, **pass the exception object itself** as the first argument. Do not just log `ex.Message`.

```text
catch (Exception ex)
{
    _logger.LogError(ex, "Payment failed for order {OrderId}", orderId);   // good
    // _logger.LogError("Payment failed: " + ex.Message);                 // loses the stack trace
}
```

Passing `ex` keeps the **exception type, the stack trace and every inner exception**. Those tell you exactly which line failed and why.

```csharp runnable title="Log the exception, not just its message"
using System;
using Microsoft.Extensions.Logging;

using var factory = LoggerFactory.Create(b => b.AddProvider(new ErrorDetailsProvider()));
var log = factory.CreateLogger("Payments");

try
{
    ChargeCard("ORD-4410", 2499m);
}
catch (Exception ex)
{
    log.LogError(ex, "Payment failed for order {OrderId}", "ORD-4410");
}

static void ChargeCard(string orderId, decimal amount)
{
    try
    {
        throw new TimeoutException("Gateway did not answer in 30 seconds");
    }
    catch (TimeoutException inner)
    {
        throw new InvalidOperationException($"Could not charge Rs. {amount}", inner);
    }
}

class ErrorDetailsProvider : ILoggerProvider
{
    public ILogger CreateLogger(string category) => new ErrorDetailsLogger();
    public void Dispose() { }
}

class ErrorDetailsLogger : ILogger
{
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
    public bool IsEnabled(LogLevel level) => true;

    public void Log<TState>(LogLevel level, EventId id, TState state, Exception? error, Func<TState, Exception?, string> format)
    {
        Console.WriteLine($"{level}: {format(state, error)}");
        for (Exception? e = error; e != null; e = e.InnerException)
            Console.WriteLine($"  {e.GetType().Name}: {e.Message}");
    }
}
```

The log shows the outer error **and** its cause, the timeout. With only `ex.Message`, you would see "Could not charge Rs. 2499" and have to guess why.

Logs are read by many people and kept for a long time. Never write passwords, tokens, card numbers or full personal details into a log message, even at `Debug` level. Log an ID instead, such as `{CustomerId}`, and look the details up when you need them.

### Exception Logging Rules

- **Log an exception once**, where you handle it. Logging and re-throwing at every level fills the logs with copies.
- **Do not log and swallow** without a good reason. If the operation failed, the caller usually needs to know.
- **Never log secrets**: passwords, card numbers, tokens, or full personal details.
- In ASP.NET Core, **unhandled exceptions are logged automatically** by the framework.

## Diagnostic Tools

When logs are not enough, .NET has tools that look inside a running app. These tools run on your machine or server, not in a browser:

| Tool | What it shows |
|---|---|
| **Visual Studio Diagnostic Tools** | CPU, memory and events while debugging |
| **`dotnet-counters`** | Live numbers: CPU, garbage collection, requests per second, exceptions |
| **`dotnet-trace`** | A detailed recording of what the app did, for performance analysis |
| **`dotnet-dump`** | A snapshot of the app's memory, to analyse a hang or crash |
| **`dotnet-gcdump`** | A snapshot of the managed heap, for memory leaks (lesson 19) |
| **Application Insights, Grafana, Seq** | Logs, traces and metrics from production, searchable in one place |

```text
dotnet tool install -g dotnet-counters
dotnet-counters monitor --name MyShop.Api

dotnet tool install -g dotnet-trace
dotnet-trace collect --name MyShop.Api --duration 00:00:30
```

## Debugging Techniques

Logging is one tool. Here are the others every developer uses:

### Breakpoints and Stepping

In Visual Studio or VS Code, click in the margin to set a **breakpoint**. Run with **F5**. The app stops on that line, and you can inspect every variable.

- **F10** steps over a line; **F11** steps into a method.
- **Conditional breakpoints** stop only when a condition is true, such as `orderId == "ORD-4410"`.
- **Tracepoints** print a message instead of stopping.

### Debug and Trace Output

`System.Diagnostics.Debug.WriteLine` writes to the debugger's **Output** window. It is removed entirely from Release builds, because its methods are marked `[Conditional("DEBUG")]` (lesson 14).

```text
Debug.WriteLine($"Cart total before discount: {total}");
Debug.Assert(total >= 0, "Total should never be negative");   // stops the debugger if false
```

### Assertions and Guard Clauses

Check your assumptions in code, so a bug shows up **where it starts**, not three methods later:

```csharp runnable title="Fail early with clear messages"
using System;

try
{
    ApplyDiscount(price: 500m, percent: 120);
}
catch (ArgumentOutOfRangeException ex)
{
    Console.WriteLine($"Caught early: {ex.ParamName} = {ex.ActualValue}");
    Console.WriteLine(ex.Message.Split('\n')[0]);
}

static decimal ApplyDiscount(decimal price, int percent)
{
    ArgumentOutOfRangeException.ThrowIfNegative(price);
    ArgumentOutOfRangeException.ThrowIfGreaterThan(percent, 100);
    return price * (100 - percent) / 100;
}
```

### Reproduce, Then Fix

The most reliable debugging method is simple:

1. **Reproduce** the bug with the smallest possible input.
2. **Write a failing test** that shows it.
3. **Fix** the code until the test passes.
4. **Keep the test**, so the bug can never come back silently.

## Tracing

In modern apps, one user action often passes through several services: a web API, a payment service, a database, a message queue. A log line from each service is not enough. You need to see **one request's whole journey**.

**Distributed tracing** gives each request a **trace ID**, and each step a **span**. In .NET, a span is an **`Activity`**, and you create activities from an **`ActivitySource`**.

```csharp runnable title="Trace the steps of a checkout"
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;

var source = new ActivitySource("MyShop.Checkout");

var traceIds = new HashSet<ActivityTraceId>();

// A listener collects finished activities. OpenTelemetry does this in a real app.
using var listener = new ActivityListener
{
    ShouldListenTo = s => s.Name == "MyShop.Checkout",
    Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData,
    ActivityStopped = a =>
    {
        traceIds.Add(a.TraceId);
        Console.WriteLine($"{a.DisplayName,-12} parent: {a.Parent?.DisplayName ?? "-",-9} order: {a.GetTagItem("order.id")}");
    },
};
ActivitySource.AddActivityListener(listener);

using (var checkout = source.StartActivity("Checkout"))
{
    checkout?.SetTag("order.id", "ORD-5120");

    using (var pay = source.StartActivity("ChargeCard"))
    {
        pay?.SetTag("order.id", "ORD-5120");
        await Task.Delay(30);
    }

    using (var mail = source.StartActivity("SendReceipt"))
    {
        mail?.SetTag("order.id", "ORD-5120");
        await Task.Delay(10);
    }
}

Console.WriteLine($"All steps share one trace ID: {traceIds.Count == 1}");
```

`ChargeCard` and `SendReceipt` are **children** of `Checkout`, and all three share one trace ID. A tracing tool such as Jaeger, Zipkin or Application Insights draws them as a timeline, so you can see which step was slow.

In a real app you add **OpenTelemetry**, which listens to activities and sends them to your tracing tool:

```text
builder.Services.AddOpenTelemetry()
    .WithTracing(t => t
        .AddSource("MyShop.Checkout")
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddOtlpExporter());
```

## Performance Monitoring

Logs say **what happened**. **Metrics** say **how much and how fast**: requests per second, average response time, orders per minute, error rate.

.NET has two simple tools for this.

### Stopwatch for One Operation

```csharp runnable title="Time an operation and warn when it is slow"
using System;
using System.Diagnostics;
using System.Threading.Tasks;

foreach (int delay in new[] { 20, 120 })
{
    var watch = Stopwatch.StartNew();
    await Task.Delay(delay);                    // pretend to call a payment gateway
    watch.Stop();

    string level = watch.ElapsedMilliseconds > 100 ? "WARN" : "info";
    Console.WriteLine($"{level}: gateway call took about {(watch.ElapsedMilliseconds >= 100 ? "100+" : "<100")} ms");
}
```

### Meters for Ongoing Numbers

**`System.Diagnostics.Metrics`** lets you publish numbers that monitoring tools read continuously. A **`Meter`** creates **instruments**:

- **`Counter<T>`**: a total that only goes up, such as orders placed.
- **`Histogram<T>`**: a spread of values, such as response times.
- **`ObservableGauge<T>`**: a value read on demand, such as queue length.

The browser runtime switches metrics off, so this example is shown as plain code with its desktop .NET output:

```text
using System;
using System.Diagnostics.Metrics;

using var meter = new Meter("MyShop.Orders");
Counter<int> ordersPlaced = meter.CreateCounter<int>("orders_placed");
Histogram<double> orderValue = meter.CreateHistogram<double>("order_value", unit: "INR");

// A listener stands in for dotnet-counters or OpenTelemetry.
using var listener = new MeterListener();
listener.InstrumentPublished = (instrument, l) =>
{
    if (instrument.Meter.Name == "MyShop.Orders") l.EnableMeasurementEvents(instrument);
};
listener.SetMeasurementEventCallback<int>((inst, value, tags, _) =>
    Console.WriteLine($"{inst.Name}: +{value}"));
listener.SetMeasurementEventCallback<double>((inst, value, tags, _) =>
    Console.WriteLine($"{inst.Name}: {value} {inst.Unit}"));
listener.Start();

foreach (double total in new[] { 1299.0, 499.0, 2450.0 })
{
    ordersPlaced.Add(1);
    orderValue.Record(total);
}

// Output on desktop .NET:
// orders_placed: +1
// order_value: 1299 INR
// orders_placed: +1
// order_value: 499 INR
// orders_placed: +1
// order_value: 2450 INR
```

On a server, you watch these live with `dotnet-counters monitor --counters MyShop.Orders`, or export them to Prometheus and Grafana with OpenTelemetry.

**Easy Way to Remember:** **logs** tell the **story**, **traces** show the **journey**, **metrics** count the **numbers**. Together they are called **observability**.

## Application Diagnostics

Putting it all together, a well-observed .NET application has:

1. **Structured logs** with `ILogger<T>`, sensible levels and no secrets.
2. **Configured levels** in `appsettings.json`, more detailed in Development.
3. **Exceptions logged once**, with the exception object.
4. **Scopes** carrying request and user IDs.
5. **Tracing** with `ActivitySource` and OpenTelemetry across services.
6. **Metrics** with `Meter` for business and performance numbers.
7. **Health checks**, so the hosting platform knows if the app is alive:

```text
builder.Services.AddHealthChecks()
    .AddCheck("database", () => HealthCheckResult.Healthy());
app.MapHealthChecks("/health");
```

8. **High-performance logging** in hot paths with source-generated log methods:

```text
public static partial class Log
{
    [LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} placed by {Customer}")]
    public static partial void OrderPlaced(ILogger logger, string orderId, string customer);
}

Log.OrderPlaced(_logger, orderId, customer);   // no boxing, no parsing at runtime
```

The `[LoggerMessage]` attribute (lesson 14) makes the compiler generate the fastest possible logging code.

## Where Logging Is Used

- **Web APIs**: every request, error and slow call.
- **Background jobs**: start, finish, progress and failures.
- **Payments and orders**: an audit trail of what happened and when.
- **Microservices**: traces that follow a request through many services.
- **Production support**: the first place to look when a user reports a problem.

## Common mistakes

| Mistake | Symptom | Fix |
|---|---|---|
| `$"..."` interpolation in log calls | Logs cannot be searched by field; work done even when the level is off | Use a message template with placeholders |
| `LogError("Failed: " + ex.Message)` | No stack trace or inner exception | `LogError(ex, "Failed for {OrderId}", id)` |
| Logging secrets or personal data | Security and privacy problems | Log IDs, not the sensitive values |
| Everything at `Information` | Logs too noisy to read | Use `Debug` for details, `Warning` and `Error` for problems |
| Logging the same exception at every layer | One error appears five times | Log once, where it is handled |
| No provider configured | Nothing appears anywhere | Add a provider such as `AddConsole()` |
| Minimum level set too high | Expected messages are missing | Lower the level for that category in configuration |
| Different names for the same field | `{OrderId}` here, `{Id}` there; searches miss logs | Use one name per concept everywhere |

## What to Remember

- Logging records what your app does, so you can understand problems later.
- Inject `ILogger<T>`; the type sets the category.
- Six levels: Trace, Debug, Information, Warning, Error, Critical. Set the minimum per environment.
- Use message templates with named placeholders for structured, searchable logs.
- Providers decide where logs go: console, debug, files, cloud services.
- Pass the exception object to `LogError`, and log each exception once.
- Debug with breakpoints, guard clauses, assertions and reproducible tests.
- `ActivitySource` and `Activity` trace a request's journey; `Meter` publishes metrics.
- Logs, traces and metrics together give you observability.

Next: HttpClient and JSON
