ASP.NET Core Logging Integration
Add structured logging to ASP.NET Core applications with ILogger integration, Entity Framework tracing, and Docker deployment.
LogTide’s C# SDK integrates with ASP.NET Core’s built-in ILogger infrastructure, providing structured logging with automatic request tracing, Entity Framework query logging, and dependency injection support. This guide covers middleware setup, the ILogger provider, background services, and production deployment.
Why use LogTide with ASP.NET Core?
- ILogger integration: Drop-in provider that works with the standard
ILogger<T>interface - Request middleware: Automatic HTTP request/response logging with timing and trace IDs
- Dependency injection: Register once, inject everywhere via ASP.NET Core DI
- Entity Framework: Log slow queries and database operations with execution time
- Background services: Trace
IHostedServiceand worker processes - Structured logging: First-class support for scopes, message templates, and semantic values
- Non-blocking: Async batching keeps request latency unaffected
Prerequisites
- .NET 8.0+ (.NET 9.0 supported)
- ASP.NET Core
- LogTide instance with API key
Installation
Install the LogTide.Client NuGet package:
dotnet add package LogTide.Client
Or via the Package Manager Console:
Install-Package LogTide.Client
Or add directly to your .csproj:
<PackageReference Include="LogTide.Client" Version="0.4.0" />
Quick Start
1. Configure Services
// Program.cs
using LogTide.Client;
var builder = WebApplication.CreateBuilder(args);
// Register LogTide
builder.Services.AddLogTide(options =>
{
options.ApiUrl = builder.Configuration["LogTide:ApiUrl"]!;
options.ApiKey = builder.Configuration["LogTide:ApiKey"]!;
options.DefaultService = "dotnet-app";
options.BatchSize = 50;
options.FlushIntervalMs = 5000;
options.GlobalMetadata = new Dictionary<string, object>
{
["environment"] = builder.Environment.EnvironmentName,
["version"] = typeof(Program).Assembly.GetName().Version?.ToString() ?? "unknown"
};
});
// Add the LogTide ILogger provider
builder.Logging.AddLogTide();
builder.Services.AddControllers();
var app = builder.Build();
app.UseLogTideMiddleware();
app.MapControllers();
app.Run();
2. Configure App Settings
// appsettings.json
{
"LogTide": {
"ApiUrl": "https://your-logtide-instance.example.com",
"ApiKey": "lp_your_api_key_here"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
}
}
For production, use environment variables or user secrets:
export LogTide__ApiUrl="https://your-logtide-instance.example.com"
export LogTide__ApiKey="lp_your_api_key_here"
3. Use in Controllers
// Controllers/UsersController.cs
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly ILogger<UsersController> _logger;
private readonly UserService _userService;
public UsersController(ILogger<UsersController> logger, UserService userService)
{
_logger = logger;
_userService = userService;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
_logger.LogInformation("Fetching user profile for {UserId}", id);
var user = await _userService.GetByIdAsync(id);
if (user is null)
{
_logger.LogWarning("User {UserId} not found", id);
return NotFound();
}
return Ok(user);
}
[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request)
{
_logger.LogInformation("Creating user with email {Email}", request.Email);
try
{
var user = await _userService.CreateAsync(request);
_logger.LogInformation("User {UserId} created successfully", user.Id);
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}
catch (DuplicateEmailException)
{
_logger.LogWarning("Duplicate email address: {Email}", request.Email);
return Conflict(new { error = "Email already exists" });
}
}
}
ASP.NET Core Middleware
Request Logging Middleware
Create middleware that logs every request with timing, trace IDs, and user context:
// Middleware/LogTideRequestMiddleware.cs
using System.Diagnostics;
using LogTide.Client;
public class LogTideRequestMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogTideClient _client;
private static readonly HashSet<string> SkipPaths = new()
{
"/health", "/ready", "/metrics", "/swagger"
};
public LogTideRequestMiddleware(RequestDelegate next, ILogTideClient client)
{
_next = next;
_client = client;
}
public async Task InvokeAsync(HttpContext context)
{
// Generate or extract trace ID
var traceId = context.Request.Headers["X-Trace-ID"].FirstOrDefault()
?? Activity.Current?.TraceId.ToString()
?? Guid.NewGuid().ToString();
context.Items["TraceId"] = traceId;
context.Response.Headers["X-Trace-Id"] = traceId;
var stopwatch = Stopwatch.StartNew();
try
{
await _next(context);
}
catch (Exception ex)
{
stopwatch.Stop();
LogRequest(context, stopwatch.ElapsedMilliseconds, traceId, ex);
throw;
}
stopwatch.Stop();
if (!ShouldSkip(context.Request.Path))
{
LogRequest(context, stopwatch.ElapsedMilliseconds, traceId);
}
}
private void LogRequest(HttpContext context, long durationMs, string traceId, Exception? exception = null)
{
var statusCode = exception != null ? 500 : context.Response.StatusCode;
var level = statusCode >= 500 ? "error" : statusCode >= 400 ? "warning" : "info";
var metadata = new Dictionary<string, object>
{
["method"] = context.Request.Method,
["path"] = context.Request.Path.ToString(),
["statusCode"] = statusCode,
["durationMs"] = durationMs,
["traceId"] = traceId,
["ip"] = GetClientIp(context),
["userAgent"] = context.Request.Headers.UserAgent.ToString(),
};
if (context.User.Identity?.IsAuthenticated == true)
{
metadata["userId"] = context.User.FindFirst("sub")?.Value ?? "unknown";
}
if (exception != null)
{
metadata["error"] = exception.Message;
metadata["errorType"] = exception.GetType().Name;
metadata["stackTrace"] = exception.StackTrace?.Substring(0, Math.Min(exception.StackTrace.Length, 1000)) ?? "";
}
_client.Log(level, $"{context.Request.Method} {context.Request.Path} {statusCode}", metadata);
}
private static string GetClientIp(HttpContext context)
{
var forwarded = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
if (!string.IsNullOrEmpty(forwarded))
return forwarded.Split(',')[0].Trim();
return context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
}
private static bool ShouldSkip(PathString path)
{
return SkipPaths.Any(p => path.StartsWithSegments(p));
}
}
// Extension method for clean registration
public static class LogTideMiddlewareExtensions
{
public static IApplicationBuilder UseLogTideMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<LogTideRequestMiddleware>();
}
}
Request Logging Output
Each request generates a structured log:
{
"level": "info",
"message": "GET /api/users/42 200",
"service": "dotnet-app",
"metadata": {
"method": "GET",
"path": "/api/users/42",
"statusCode": 200,
"durationMs": 15,
"traceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"ip": "192.168.1.1",
"userId": "42",
"environment": "Production"
}
}
ILogger Provider Integration
The LogTide ILogger provider routes all ILogger<T> calls to LogTide while preserving scopes and message templates:
// Services/OrderService.cs
public class OrderService
{
private readonly ILogger<OrderService> _logger;
private readonly AppDbContext _db;
public OrderService(ILogger<OrderService> logger, AppDbContext db)
{
_logger = logger;
_db = db;
}
public async Task<Order> ProcessOrderAsync(int orderId)
{
using (_logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = orderId,
["Operation"] = "ProcessOrder"
}))
{
_logger.LogInformation("Starting order processing");
var order = await _db.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == orderId);
if (order is null)
{
_logger.LogWarning("Order {OrderId} not found", orderId);
throw new OrderNotFoundException(orderId);
}
try
{
await ChargePaymentAsync(order);
order.Status = OrderStatus.Completed;
await _db.SaveChangesAsync();
_logger.LogInformation(
"Order {OrderId} processed. Total: {Total:C}, Items: {ItemCount}",
orderId, order.Total, order.Items.Count);
return order;
}
catch (PaymentFailedException ex)
{
_logger.LogError(ex,
"Payment failed for order {OrderId}. Amount: {Amount:C}",
orderId, order.Total);
throw;
}
}
}
}
Dependency Injection
Register LogTide services and inject them throughout the application:
// Program.cs
builder.Services.AddLogTide(options =>
{
options.ApiUrl = builder.Configuration["LogTide:ApiUrl"]!;
options.ApiKey = builder.Configuration["LogTide:ApiKey"]!;
options.DefaultService = "dotnet-app";
});
// Register application services
builder.Services.AddScoped<OrderService>();
builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<AuditService>();
Direct Client Injection
For cases where you need the LogTide client directly (outside ILogger):
// Services/AuditService.cs
public class AuditService
{
private readonly ILogTideClient _logtide;
public AuditService(ILogTideClient logtide)
{
_logtide = logtide;
}
public void LogAdminAction(string action, string userId, string resourceType, string resourceId)
{
_logtide.Log("info", $"Admin action: {action}", new Dictionary<string, object>
{
["action"] = action,
["userId"] = userId,
["resourceType"] = resourceType,
["resourceId"] = resourceId,
["timestamp"] = DateTimeOffset.UtcNow.ToString("o"),
["source"] = "admin-panel"
});
}
}
Entity Framework Logging
Log Slow Queries
Intercept Entity Framework queries to log slow or problematic operations:
// Data/LogTideDbInterceptor.cs
using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Data.Common;
using System.Diagnostics;
using LogTide.Client;
public class LogTideDbInterceptor : DbCommandInterceptor
{
private readonly ILogTideClient _client;
private readonly int _slowQueryThresholdMs;
public LogTideDbInterceptor(ILogTideClient client, int slowQueryThresholdMs = 100)
{
_client = client;
_slowQueryThresholdMs = slowQueryThresholdMs;
}
public override DbDataReader ReaderExecuted(
DbCommand command, CommandExecutedEventData eventData, DbDataReader result)
{
LogQuery(command, eventData.Duration);
return base.ReaderExecuted(command, eventData, result);
}
public override ValueTask<DbDataReader> ReaderExecutedAsync(
DbCommand command, CommandExecutedEventData eventData,
DbDataReader result, CancellationToken cancellationToken = default)
{
LogQuery(command, eventData.Duration);
return base.ReaderExecutedAsync(command, eventData, result, cancellationToken);
}
public override void CommandFailed(DbCommand command, CommandErrorEventData eventData)
{
_client.Log("error", "Database query failed", new Dictionary<string, object>
{
["query"] = command.CommandText[..Math.Min(command.CommandText.Length, 500)],
["durationMs"] = eventData.Duration.TotalMilliseconds,
["error"] = eventData.Exception.Message,
["errorType"] = eventData.Exception.GetType().Name
});
base.CommandFailed(command, eventData);
}
private void LogQuery(DbCommand command, TimeSpan duration)
{
var durationMs = duration.TotalMilliseconds;
if (durationMs >= _slowQueryThresholdMs)
{
_client.Log("warning", "Slow SQL query detected", new Dictionary<string, object>
{
["query"] = command.CommandText[..Math.Min(command.CommandText.Length, 500)],
["durationMs"] = Math.Round(durationMs, 2),
["thresholdMs"] = _slowQueryThresholdMs
});
}
}
}
Register the Interceptor
// Program.cs
builder.Services.AddDbContext<AppDbContext>((serviceProvider, options) =>
{
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"));
var logtideClient = serviceProvider.GetRequiredService<ILogTideClient>();
options.AddInterceptors(new LogTideDbInterceptor(logtideClient, slowQueryThresholdMs: 100));
});
Background Service Logging
Log from IHostedService and background workers:
// Services/OrderProcessingService.cs
public class OrderProcessingService : BackgroundService
{
private readonly ILogger<OrderProcessingService> _logger;
private readonly IServiceScopeFactory _scopeFactory;
public OrderProcessingService(
ILogger<OrderProcessingService> logger,
IServiceScopeFactory scopeFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Order processing service started");
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var pendingOrders = await db.Orders
.Where(o => o.Status == OrderStatus.Pending)
.Take(10)
.ToListAsync(stoppingToken);
if (pendingOrders.Count > 0)
{
_logger.LogInformation(
"Processing {Count} pending orders", pendingOrders.Count);
foreach (var order in pendingOrders)
{
using (_logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = order.Id,
["Task"] = "BackgroundProcessing"
}))
{
try
{
await ProcessSingleOrder(order, db);
_logger.LogInformation("Order {OrderId} processed", order.Id);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Failed to process order {OrderId}", order.Id);
order.Status = OrderStatus.Failed;
}
}
}
await db.SaveChangesAsync(stoppingToken);
}
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Order processing service stopping");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Order processing service encountered an error");
await Task.Delay(TimeSpan.FromSeconds(60), stoppingToken);
}
}
_logger.LogInformation("Order processing service stopped");
}
private async Task ProcessSingleOrder(Order order, AppDbContext db)
{
order.Status = OrderStatus.Processing;
await db.SaveChangesAsync();
// ... payment, notifications, etc.
order.Status = OrderStatus.Completed;
}
}
Register the Background Service
// Program.cs
builder.Services.AddHostedService<OrderProcessingService>();
Health Check Integration
Add a LogTide connectivity health check:
// HealthChecks/LogTideHealthCheck.cs
using Microsoft.Extensions.Diagnostics.HealthChecks;
using LogTide.Client;
public class LogTideHealthCheck : IHealthCheck
{
private readonly ILogTideClient _client;
public LogTideHealthCheck(ILogTideClient client)
{
_client = client;
}
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken cancellationToken = default)
{
var isHealthy = _client.IsConnected;
return Task.FromResult(isHealthy
? HealthCheckResult.Healthy("LogTide connection is active")
: HealthCheckResult.Degraded("LogTide connection is unavailable"));
}
}
// Program.cs
builder.Services.AddHealthChecks()
.AddCheck<LogTideHealthCheck>("logtide");
Docker Deployment
Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish --no-restore
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime
WORKDIR /app
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
COPY --from=build /app/publish .
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -q --spider http://localhost:8080/health || exit 1
ENTRYPOINT ["dotnet", "MyApp.dll"]
Docker Compose
# docker-compose.yml
services:
api:
build: .
ports:
- "8080:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8080
- LogTide__ApiUrl=${LOGTIDE_API_URL}
- LogTide__ApiKey=${LOGTIDE_API_KEY}
- ConnectionStrings__DefaultConnection=Host=db;Database=myapp;Username=user;Password=pass
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
interval: 10s
timeout: 5s
retries: 5
volumes:
pgdata:
Graceful Shutdown
ASP.NET Core handles shutdown automatically when LogTide is registered via AddLogTide(). The SDK’s IDisposable implementation flushes pending logs on host shutdown. For manual control:
// Program.cs
var app = builder.Build();
app.Lifetime.ApplicationStopping.Register(() =>
{
var client = app.Services.GetRequiredService<ILogTideClient>();
client.Flush();
});
Performance
| Metric | Value |
|---|---|
| Middleware overhead | <1ms per request |
| Memory overhead | ~12MB |
| Network calls | 1 per batch (50 logs) |
| EF interceptor overhead | <0.5ms per query |
| Background service overhead | Negligible |
| ILogger provider overhead | <0.2ms per log entry |
Troubleshooting
Logs not appearing
- Verify configuration values are loaded correctly:
var config = builder.Configuration.GetSection("LogTide"); Console.WriteLine($"ApiUrl: {config["ApiUrl"]}"); - Ensure
AddLogTide()is called on bothServicesandLogging - Check that the middleware is registered before
MapControllers()
ILogger scopes not appearing in LogTide
Ensure you are using BeginScope with a Dictionary<string, object>:
using (_logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = orderId
}))
{
_logger.LogInformation("Processing order");
}
Entity Framework logs too verbose
Raise the EF Core log level in appsettings.json:
{
"Logging": {
"LogLevel": {
"Microsoft.EntityFrameworkCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
}
}
High memory usage in background services
When processing large batches, create a new DI scope per iteration and dispose it properly to release the LogTide batch buffer:
using var scope = _scopeFactory.CreateScope();
// ... process batch
// Scope disposes here, flushing logs
Next Steps
- Docker Integration - Container deployment patterns
- Kubernetes Integration - Production K8s deployment
- PostgreSQL Integration - Database logging correlation
- Compliance Audit Trail - Regulatory logging
- Security Monitoring - Threat detection and alerting