Skip to content

Commit a85a400

Browse files
Merge branch 'phase-2-logging' of https://github.com/HasanJaved-Developer/CentralizedLoggingMonitoring into phase-2-logging
2 parents da41428 + f019703 commit a85a400

11 files changed

+311
-29
lines changed

CentralizedLoggingApi/CentralizedLoggingApi.csproj

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
<TargetFramework>net9.0</TargetFramework>
55
<Nullable>enable</Nullable>
66
<ImplicitUsings>enable</ImplicitUsings>
7+
<GenerateDocumentationFile>True</GenerateDocumentationFile>
8+
<NoWarn>$(NoWarn);1591</NoWarn> <!-- suppress missing XML comment warnings -->
9+
<DocumentationFile>$(AssemblyName).xml</DocumentationFile>
710
</PropertyGroup>
811

912
<ItemGroup>
@@ -18,6 +21,14 @@
1821
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1922
<PrivateAssets>all</PrivateAssets>
2023
</PackageReference>
24+
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
25+
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
26+
<PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0" />
27+
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
28+
<PackageReference Include="Serilog.Filters.Expressions" Version="2.1.0" />
29+
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
30+
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
31+
<PackageReference Include="Serilog.Sinks.MSSqlServer" Version="8.2.2" />
2132
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
2233
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.3" />
2334
</ItemGroup>

CentralizedLoggingApi/CentralizedLoggingApi.xml

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CentralizedLoggingApi/Controllers/ApplicationsController.cs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ namespace CentralizedLoggingApi.Controllers
99
[Route("api/[controller]")]
1010
public class ApplicationsController : ControllerBase
1111
{
12+
private readonly ILogger<ApplicationsController> _logger;
13+
1214
private readonly LoggingDbContext _context;
1315

14-
public ApplicationsController(LoggingDbContext context)
16+
public ApplicationsController(LoggingDbContext context, ILogger<ApplicationsController> logger)
1517
{
1618
_context = context;
19+
_logger = logger;
1720
}
1821

1922
[HttpPost]
@@ -24,14 +27,60 @@ public async Task<IActionResult> Create([FromBody] Application app)
2427
return CreatedAtAction(nameof(GetById), new { id = app.Id }, app);
2528
}
2629

30+
/// <summary>
31+
/// Fetches an application by its unique Id.
32+
/// </summary>
33+
/// <remarks>
34+
/// This endpoint retrieves application details from the database.
35+
///
36+
/// **Logging:**
37+
/// - On every call, Serilog writes an entry to the log file (e.g., `dev-app-.clef`).
38+
/// - If an error occurs (e.g., record not found or DB exception), the error is also recorded in the same log file.
39+
///
40+
/// **Usage:**
41+
/// `GET /api/applications/{id}`
42+
///
43+
/// Note:
44+
/// The logging setup ensures that each request is auditable and errors can be traced via structured log files.
45+
/// </remarks>
46+
/// <param name="id">Unique application Id.</param>
47+
/// <returns>Returns the application details if found, otherwise NotFound (404).</returns>
48+
2749
[HttpGet("{id}")]
2850
public async Task<IActionResult> GetById(int id)
29-
{
51+
{
52+
3053
var app = await _context.Applications.FindAsync(id);
31-
if (app == null) return NotFound();
54+
if (app == null)
55+
{
56+
_logger.LogWarning("Application not found. Id={Id}", id);
57+
return NotFound();
58+
}
59+
3260
return Ok(app);
3361
}
3462

63+
/// <summary>
64+
/// Test endpoint that always throws an <see cref="InvalidOperationException"/>.
65+
/// </summary>
66+
/// <remarks>
67+
/// <para>
68+
/// This endpoint exists only for verifying the application's global exception
69+
/// handling middleware and Serilog logging.
70+
/// </para>
71+
/// <para>
72+
/// When called, it will throw an unhandled exception. The request should be
73+
/// intercepted by <c>ExceptionHandlingMiddleware</c>, which will log the error
74+
/// and return a consistent JSON error response.
75+
/// </para>
76+
/// </remarks>
77+
/// <response code="500">Always returned, because the endpoint throws an exception.</response>
78+
[HttpGet("boom")]
79+
public Task<IActionResult> Boom()
80+
{
81+
throw new InvalidOperationException("Boom!");
82+
}
83+
3584
[HttpGet]
3685
public async Task<IActionResult> GetAll()
3786
{
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System.Net.Mime;
2+
3+
namespace CentralizedLoggingApi.Middlewares
4+
{
5+
public class ExceptionHandlingMiddleware
6+
{
7+
private readonly RequestDelegate _next;
8+
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
9+
10+
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
11+
{
12+
_next = next;
13+
_logger = logger;
14+
}
15+
16+
public async Task Invoke(HttpContext context)
17+
{
18+
try
19+
{
20+
await _next(context); // continue pipeline
21+
}
22+
catch (OperationCanceledException oce) when (context.RequestAborted.IsCancellationRequested)
23+
{
24+
// Client aborted; don’t try to write a response body.
25+
_logger.LogWarning(oce, "Request aborted by client {Path} ({TraceId})",
26+
context.Request.Path, context.TraceIdentifier);
27+
// Let it bubble or just return; here we just return.
28+
}
29+
catch (Exception ex)
30+
{
31+
// Log using Serilog (through ILogger)
32+
_logger.LogError(ex, "Unhandled exception occurred while processing request {Path}", context.Request.Path);
33+
34+
// If the server already committed headers/body, we can't change it.
35+
if (context.Response.HasStarted)
36+
{
37+
_logger.LogWarning("The response has already started; cannot write error body. Rethrowing.");
38+
throw; // Let server infrastructure terminate the connection appropriately.
39+
}
40+
41+
// Clear headers/status that may have been set
42+
context.Response.Clear();
43+
context.Response.ContentType = "application/json";
44+
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
45+
46+
// If a previous component wrote to the (buffered) body, wipe it safely.
47+
try
48+
{
49+
if (context.Response.Body.CanSeek)
50+
{
51+
context.Response.Body.SetLength(0);
52+
context.Response.Body.Position = 0;
53+
}
54+
}
55+
catch
56+
{
57+
// If the body stream isn't seekable/writable (rare with your buffer), ignore.
58+
}
59+
// Let the server recalc Content-Length
60+
context.Response.Headers.ContentLength = null;
61+
62+
63+
var result = new
64+
{
65+
error = "An unexpected error occurred",
66+
details = ex.Message // optional, avoid exposing internals in production
67+
};
68+
69+
await context.Response.WriteAsJsonAsync(result);
70+
}
71+
}
72+
}
73+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
namespace CentralizedLoggingApi.Middlewares
2+
{
3+
public class RequestAudibilityMiddleware
4+
{
5+
private readonly RequestDelegate _next;
6+
private readonly ILogger<RequestAudibilityMiddleware> _logger;
7+
8+
public RequestAudibilityMiddleware(RequestDelegate next, ILogger<RequestAudibilityMiddleware> logger)
9+
{
10+
_next = next;
11+
_logger = logger;
12+
}
13+
14+
public async Task InvokeAsync(HttpContext context)
15+
{
16+
var requestId = context.TraceIdentifier;
17+
18+
// Log request info
19+
_logger.LogInformation("Incoming request {RequestId} {Method} {Path} from {RemoteIp}",
20+
requestId,
21+
context.Request.Method,
22+
context.Request.Path,
23+
context.Connection.RemoteIpAddress?.ToString());
24+
25+
26+
var originalBodyStream = context.Response.Body; // Save original response stream
27+
28+
await using var responseBody = new MemoryStream();
29+
context.Response.Body = responseBody;
30+
31+
try
32+
{
33+
await _next(context); // continue pipeline
34+
}
35+
finally
36+
{
37+
// Capture response
38+
context.Response.Body.Seek(0, SeekOrigin.Begin);
39+
var responseText = await new StreamReader(context.Response.Body).ReadToEndAsync();
40+
context.Response.Body.Seek(0, SeekOrigin.Begin);
41+
42+
_logger.LogInformation("Outgoing response {RequestId} with status {StatusCode}, length {Length}",
43+
requestId,
44+
context.Response.StatusCode,
45+
responseText?.Length);
46+
47+
// Copy back to original response body
48+
await responseBody.CopyToAsync(originalBodyStream);
49+
}
50+
}
51+
}
52+
}

CentralizedLoggingApi/Migrations/LoggingDbContextModelSnapshot.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace CentralizedLoggingApi.Migrations
1313
[DbContext(typeof(LoggingDbContext))]
1414
partial class LoggingDbContextModelSnapshot : ModelSnapshot
1515
{
16+
1617
protected override void BuildModel(ModelBuilder modelBuilder)
1718
{
1819
#pragma warning disable 612, 618

CentralizedLoggingApi/Program.cs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
1+
using CentralizedLoggingApi;
2+
using CentralizedLoggingApi.Data;
3+
using CentralizedLoggingApi.Middlewares;
14
using Microsoft.EntityFrameworkCore;
25
using Microsoft.OpenApi.Models;
3-
using CentralizedLoggingApi.Data;
4-
using CentralizedLoggingApi;
6+
using Serilog;
7+
using System.Reflection;
58

69

710

811
var builder = WebApplication.CreateBuilder(args);
912

13+
Log.Logger = new LoggerConfiguration()
14+
.ReadFrom.Configuration(builder.Configuration)
15+
.Enrich.WithProperty("Application", "CentralizedLogging") // change if needed
16+
.CreateLogger();
17+
18+
builder.Host.UseSerilog();
19+
1020
Console.WriteLine($"Environment: {builder.Environment.EnvironmentName}");
1121
Console.WriteLine($"Connection: {builder.Configuration.GetConnectionString("DefaultConnection")}");
1222

1323
// DB connection string (SQL Server example)
1424
builder.Services.AddDbContext<LoggingDbContext>(options =>
1525
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
1626

27+
28+
builder.Services.AddHttpContextAccessor();
29+
1730
// Add services to the container.
1831

1932
builder.Services.AddControllers();
@@ -28,10 +41,28 @@
2841
Version = "v1",
2942
Description = "API for centralized error logging and monitoring"
3043
});
44+
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
45+
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
46+
c.IncludeXmlComments(xmlPath, includeControllerXmlComments: true); // ok if your Swashbuckle version supports the bool
47+
3148
});
3249

3350
var app = builder.Build();
3451

52+
app.Use(async (ctx, next) =>
53+
{
54+
using (Serilog.Context.LogContext.PushProperty("Environment", app.Environment.EnvironmentName))
55+
using (Serilog.Context.LogContext.PushProperty("Service", "CoreAPI"))
56+
using (Serilog.Context.LogContext.PushProperty("CorrelationId", ctx.TraceIdentifier))
57+
{
58+
await next();
59+
}
60+
});
61+
62+
// Middleware should be early in the pipeline
63+
app.UseMiddleware<RequestAudibilityMiddleware>();
64+
app.UseMiddleware<ExceptionHandlingMiddleware>();
65+
3566
// Configure the HTTP request pipeline.
3667
if (app.Environment.IsDevelopment())
3768
{
@@ -48,6 +79,9 @@
4879

4980
app.UseAuthorization();
5081

82+
83+
84+
5185
app.MapControllers();
5286

5387
// Seed sample data
Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
{
2-
"Logging": {
3-
"LogLevel": {
4-
"Default": "Information",
5-
"Microsoft.AspNetCore": "Warning"
6-
}
7-
},
82
"ConnectionStrings": {
9-
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=CentralizedLoggingDB;Trusted_Connection=True;MultipleActiveResultSets=true"
3+
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=CentralizedLoggingDB;Trusted_Connection=True;MultipleActiveResultSets=true"
104
}
115
}

0 commit comments

Comments
 (0)