Skip to content

Commit 066a9b9

Browse files
Serilogs added for request audibility and exception middleware. Xmlcomments added in fetch application by id. Endpoint boom added to test exception middleware. Instance name added for in docker MySQL connection string in appsettings.production.json.
1 parent dc8f55c commit 066a9b9

File tree

10 files changed

+314
-29
lines changed

10 files changed

+314
-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/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
}
Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
{
2-
"Logging": {
3-
"LogLevel": {
4-
"Default": "Information",
5-
"Microsoft.AspNetCore": "Warning"
6-
}
7-
},
8-
"ConnectionStrings": {
9-
"DefaultConnection": "Server=localhost,1433;Database=CentralizedLoggingDB;User=sa;Password=Bisp@123;MultipleActiveResultSets=true;TrustServerCertificate=True;Encrypt=False;"
10-
},
11-
"AllowedHosts": "*"
2+
"ConnectionStrings": {
3+
"DefaultConnection": "Server=db,1433;Database=CentralizedLoggingDB;User=sa;Password=Bisp@123;MultipleActiveResultSets=true;TrustServerCertificate=True;Encrypt=False;"
4+
}
125
}

0 commit comments

Comments
 (0)