feat: heath checks, seq e logs
All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 1s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 15m39s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Successful in 1m17s
BCards Deployment Pipeline / Deploy to Staging (x86 - Local) (push) Has been skipped
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 0s
All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 1s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 15m39s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Successful in 1m17s
BCards Deployment Pipeline / Deploy to Staging (x86 - Local) (push) Has been skipped
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 0s
This commit is contained in:
parent
2d901708b8
commit
90cc01d7cf
@ -12,7 +12,7 @@ namespace BCards.IntegrationTests.Helpers;
|
|||||||
|
|
||||||
public static class AuthenticationHelper
|
public static class AuthenticationHelper
|
||||||
{
|
{
|
||||||
public static async Task<HttpClient> CreateAuthenticatedClientAsync(
|
public static Task<HttpClient> CreateAuthenticatedClientAsync(
|
||||||
WebApplicationFactory<Program> factory,
|
WebApplicationFactory<Program> factory,
|
||||||
User testUser)
|
User testUser)
|
||||||
{
|
{
|
||||||
@ -34,7 +34,7 @@ public static class AuthenticationHelper
|
|||||||
client.DefaultRequestHeaders.Add("TestUserEmail", testUser.Email);
|
client.DefaultRequestHeaders.Add("TestUserEmail", testUser.Email);
|
||||||
client.DefaultRequestHeaders.Add("TestUserName", testUser.Name);
|
client.DefaultRequestHeaders.Add("TestUserName", testUser.Name);
|
||||||
|
|
||||||
return client;
|
return Task.FromResult(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ClaimsPrincipal CreateTestClaimsPrincipal(User user)
|
public static ClaimsPrincipal CreateTestClaimsPrincipal(User user)
|
||||||
|
|||||||
@ -161,9 +161,9 @@ public class PuppeteerTestHelper : IAsyncDisposable
|
|||||||
await Page.ScreenshotAsync(fileName);
|
await Page.ScreenshotAsync(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GetCurrentUrlAsync()
|
public Task<string> GetCurrentUrlAsync()
|
||||||
{
|
{
|
||||||
return Page.Url;
|
return Task.FromResult(Page.Url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<string>> GetAllElementTextsAsync(string selector)
|
public async Task<List<string>> GetAllElementTextsAsync(string selector)
|
||||||
|
|||||||
@ -20,6 +20,15 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.ResponseCaching" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.ResponseCaching" Version="2.2.0" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.54" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.54" />
|
||||||
<PackageReference Include="SendGrid" Version="9.29.3" />
|
<PackageReference Include="SendGrid" Version="9.29.3" />
|
||||||
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Seq" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.0" />
|
||||||
|
<PackageReference Include="AspNetCore.HealthChecks.MongoDb" Version="7.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace BCards.Web.Controllers;
|
namespace BCards.Web.Controllers;
|
||||||
|
|
||||||
@ -197,6 +198,11 @@ public class AdminController : Controller
|
|||||||
ModelState.Remove<ManagePageViewModel>(x => x.TwitterUrl);
|
ModelState.Remove<ManagePageViewModel>(x => x.TwitterUrl);
|
||||||
ModelState.Remove<ManagePageViewModel>(x => x.WhatsAppNumber);
|
ModelState.Remove<ManagePageViewModel>(x => x.WhatsAppNumber);
|
||||||
|
|
||||||
|
_logger.LogInformation($"ManagePage POST: IsNewPage={model.IsNewPage}, DisplayName={model.DisplayName}, Category={model.Category}, Links={model.Links?.Count ?? 0}");
|
||||||
|
|
||||||
|
//Logar modelstate em information
|
||||||
|
_logger.LogInformation($"ModelState: {JsonSerializer.Serialize(ModelState)}");
|
||||||
|
|
||||||
// Processar upload de imagem se fornecida
|
// Processar upload de imagem se fornecida
|
||||||
if (model.ProfileImageFile != null && model.ProfileImageFile.Length > 0)
|
if (model.ProfileImageFile != null && model.ProfileImageFile.Length > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
278
src/BCards.Web/Controllers/HealthController.cs
Normal file
278
src/BCards.Web/Controllers/HealthController.cs
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace BCards.Web.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("health")]
|
||||||
|
public class HealthController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly HealthCheckService _healthCheckService;
|
||||||
|
private readonly ILogger<HealthController> _logger;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private static readonly DateTime _startTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public HealthController(HealthCheckService healthCheckService, ILogger<HealthController> logger, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_healthCheckService = healthCheckService;
|
||||||
|
_logger = logger;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Health check simples - retorna apenas status geral
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetHealth()
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var healthReport = await _healthCheckService.CheckHealthAsync();
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
status = healthReport.Status.ToString().ToLower(),
|
||||||
|
timestamp = DateTime.UtcNow,
|
||||||
|
duration = $"{stopwatch.ElapsedMilliseconds}ms"
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("Simple health check completed: {Status} in {Duration}ms",
|
||||||
|
response.status, stopwatch.ElapsedMilliseconds);
|
||||||
|
|
||||||
|
return healthReport.Status == HealthStatus.Healthy
|
||||||
|
? Ok(response)
|
||||||
|
: StatusCode(503, response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
_logger.LogError(ex, "Health check failed after {Duration}ms", stopwatch.ElapsedMilliseconds);
|
||||||
|
|
||||||
|
return StatusCode(503, new
|
||||||
|
{
|
||||||
|
status = "unhealthy",
|
||||||
|
timestamp = DateTime.UtcNow,
|
||||||
|
duration = $"{stopwatch.ElapsedMilliseconds}ms",
|
||||||
|
error = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Health check detalhado - formato completo com métricas
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("detailed")]
|
||||||
|
public async Task<IActionResult> GetDetailedHealth()
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var healthReport = await _healthCheckService.CheckHealthAsync();
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
var checks = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
foreach (var entry in healthReport.Entries)
|
||||||
|
{
|
||||||
|
checks[entry.Key] = new
|
||||||
|
{
|
||||||
|
status = entry.Value.Status.ToString().ToLower(),
|
||||||
|
duration = entry.Value.Duration.TotalMilliseconds + "ms",
|
||||||
|
description = entry.Value.Description,
|
||||||
|
data = entry.Value.Data,
|
||||||
|
exception = entry.Value.Exception?.Message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var uptime = DateTime.UtcNow - _startTime;
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
applicationName = _configuration["ApplicationName"] ?? "BCards",
|
||||||
|
status = healthReport.Status.ToString().ToLower(),
|
||||||
|
timestamp = DateTime.UtcNow,
|
||||||
|
uptime = FormatUptime(uptime),
|
||||||
|
totalDuration = $"{stopwatch.ElapsedMilliseconds}ms",
|
||||||
|
checks = checks,
|
||||||
|
version = "1.0.0",
|
||||||
|
environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("Detailed health check completed: {Status} in {Duration}ms - {HealthyCount}/{TotalCount} services healthy",
|
||||||
|
response.status, stopwatch.ElapsedMilliseconds,
|
||||||
|
healthReport.Entries.Count(e => e.Value.Status == HealthStatus.Healthy),
|
||||||
|
healthReport.Entries.Count);
|
||||||
|
|
||||||
|
return healthReport.Status == HealthStatus.Unhealthy
|
||||||
|
? StatusCode(503, response)
|
||||||
|
: Ok(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
_logger.LogError(ex, "Detailed health check failed after {Duration}ms", stopwatch.ElapsedMilliseconds);
|
||||||
|
|
||||||
|
return StatusCode(503, new
|
||||||
|
{
|
||||||
|
applicationName = "BCards",
|
||||||
|
status = "unhealthy",
|
||||||
|
timestamp = DateTime.UtcNow,
|
||||||
|
uptime = FormatUptime(DateTime.UtcNow - _startTime),
|
||||||
|
totalDuration = $"{stopwatch.ElapsedMilliseconds}ms",
|
||||||
|
error = ex.Message,
|
||||||
|
version = "1.0.0",
|
||||||
|
environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Health check para Uptime Kuma - formato específico
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("uptime-kuma")]
|
||||||
|
public async Task<IActionResult> GetUptimeKumaHealth()
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var healthReport = await _healthCheckService.CheckHealthAsync();
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
var isHealthy = healthReport.Status == HealthStatus.Healthy;
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
status = isHealthy ? "up" : "down",
|
||||||
|
message = isHealthy ? "All services operational" : $"Issues detected: {healthReport.Status}",
|
||||||
|
timestamp = DateTime.UtcNow.ToString("O"),
|
||||||
|
responseTime = stopwatch.ElapsedMilliseconds,
|
||||||
|
services = healthReport.Entries.ToDictionary(
|
||||||
|
e => e.Key,
|
||||||
|
e => new {
|
||||||
|
status = e.Value.Status.ToString().ToLower(),
|
||||||
|
responseTime = e.Value.Duration.TotalMilliseconds
|
||||||
|
}
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("Uptime Kuma health check: {Status} in {Duration}ms",
|
||||||
|
response.status, stopwatch.ElapsedMilliseconds);
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
_logger.LogError(ex, "Uptime Kuma health check failed after {Duration}ms", stopwatch.ElapsedMilliseconds);
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
status = "down",
|
||||||
|
message = $"Health check failed: {ex.Message}",
|
||||||
|
timestamp = DateTime.UtcNow.ToString("O"),
|
||||||
|
responseTime = stopwatch.ElapsedMilliseconds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Health checks específicos por serviço
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("mongodb")]
|
||||||
|
public async Task<IActionResult> GetMongoDbHealth()
|
||||||
|
{
|
||||||
|
return await GetSpecificServiceHealth("mongodb");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("stripe")]
|
||||||
|
public async Task<IActionResult> GetStripeHealth()
|
||||||
|
{
|
||||||
|
return await GetSpecificServiceHealth("stripe");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("sendgrid")]
|
||||||
|
public async Task<IActionResult> GetSendGridHealth()
|
||||||
|
{
|
||||||
|
return await GetSpecificServiceHealth("sendgrid");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("external")]
|
||||||
|
public async Task<IActionResult> GetExternalServicesHealth()
|
||||||
|
{
|
||||||
|
return await GetSpecificServiceHealth("external_services");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("resources")]
|
||||||
|
public async Task<IActionResult> GetSystemResourcesHealth()
|
||||||
|
{
|
||||||
|
return await GetSpecificServiceHealth("resources");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IActionResult> GetSpecificServiceHealth(string serviceName)
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var healthReport = await _healthCheckService.CheckHealthAsync(check => check.Name == serviceName);
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
if (!healthReport.Entries.Any())
|
||||||
|
{
|
||||||
|
return NotFound(new { error = $"Service '{serviceName}' not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry = healthReport.Entries.First().Value;
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
service = serviceName,
|
||||||
|
status = entry.Status.ToString().ToLower(),
|
||||||
|
timestamp = DateTime.UtcNow,
|
||||||
|
duration = $"{entry.Duration.TotalMilliseconds}ms",
|
||||||
|
description = entry.Description,
|
||||||
|
data = entry.Data,
|
||||||
|
exception = entry.Exception?.Message
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("Service {ServiceName} health check: {Status} in {Duration}ms",
|
||||||
|
serviceName, response.status, entry.Duration.TotalMilliseconds);
|
||||||
|
|
||||||
|
return entry.Status == HealthStatus.Unhealthy
|
||||||
|
? StatusCode(503, response)
|
||||||
|
: Ok(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
_logger.LogError(ex, "Service {ServiceName} health check failed after {Duration}ms",
|
||||||
|
serviceName, stopwatch.ElapsedMilliseconds);
|
||||||
|
|
||||||
|
return StatusCode(503, new
|
||||||
|
{
|
||||||
|
service = serviceName,
|
||||||
|
status = "unhealthy",
|
||||||
|
timestamp = DateTime.UtcNow,
|
||||||
|
duration = $"{stopwatch.ElapsedMilliseconds}ms",
|
||||||
|
error = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatUptime(TimeSpan uptime)
|
||||||
|
{
|
||||||
|
if (uptime.TotalDays >= 1)
|
||||||
|
return $"{(int)uptime.TotalDays}d {uptime.Hours}h {uptime.Minutes}m";
|
||||||
|
if (uptime.TotalHours >= 1)
|
||||||
|
return $"{uptime.Hours}h {uptime.Minutes}m";
|
||||||
|
if (uptime.TotalMinutes >= 1)
|
||||||
|
return $"{uptime.Minutes}m {uptime.Seconds}s";
|
||||||
|
return $"{uptime.Seconds}s";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
|
|
||||||
namespace BCards.Web.Controllers;
|
namespace BCards.Web.Controllers;
|
||||||
|
|
||||||
//[Route("[controller]")]
|
|
||||||
public class UserPageController : Controller
|
public class UserPageController : Controller
|
||||||
{
|
{
|
||||||
private readonly IUserPageService _userPageService;
|
private readonly IUserPageService _userPageService;
|
||||||
@ -28,9 +27,6 @@ public class UserPageController : Controller
|
|||||||
_moderationService = moderationService;
|
_moderationService = moderationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
//[Route("{category}/{slug}")]
|
|
||||||
//VOltar a linha abaixo em prod
|
|
||||||
//[ResponseCache(Duration = 300, VaryByQueryKeys = new[] { "category", "slug" })]
|
|
||||||
public async Task<IActionResult> Display(string category, string slug)
|
public async Task<IActionResult> Display(string category, string slug)
|
||||||
{
|
{
|
||||||
var userPage = await _userPageService.GetPageAsync(category, slug);
|
var userPage = await _userPageService.GetPageAsync(category, slug);
|
||||||
|
|||||||
125
src/BCards.Web/HealthChecks/ExternalServicesHealthCheck.cs
Normal file
125
src/BCards.Web/HealthChecks/ExternalServicesHealthCheck.cs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace BCards.Web.HealthChecks;
|
||||||
|
|
||||||
|
public class ExternalServicesHealthCheck : IHealthCheck
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<ExternalServicesHealthCheck> _logger;
|
||||||
|
|
||||||
|
public ExternalServicesHealthCheck(HttpClient httpClient, ILogger<ExternalServicesHealthCheck> logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||||
|
HealthCheckContext context,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
var results = new Dictionary<string, object>();
|
||||||
|
var allHealthy = true;
|
||||||
|
var hasUnhealthy = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Lista de serviços externos para verificar
|
||||||
|
var services = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "google_oauth", "https://accounts.google.com/.well-known/openid_configuration" },
|
||||||
|
{ "microsoft_oauth", "https://login.microsoftonline.com/common/v2.0/.well-known/openid_configuration" }
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var service in services)
|
||||||
|
{
|
||||||
|
var serviceStopwatch = Stopwatch.StartNew();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var response = await _httpClient.GetAsync(service.Value, cancellationToken);
|
||||||
|
serviceStopwatch.Stop();
|
||||||
|
|
||||||
|
var serviceResult = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "status", response.IsSuccessStatusCode ? "healthy" : "unhealthy" },
|
||||||
|
{ "duration", $"{serviceStopwatch.ElapsedMilliseconds}ms" },
|
||||||
|
{ "status_code", (int)response.StatusCode },
|
||||||
|
{ "url", service.Value }
|
||||||
|
};
|
||||||
|
|
||||||
|
results[service.Key] = serviceResult;
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
allHealthy = false;
|
||||||
|
if (response.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable ||
|
||||||
|
response.StatusCode == System.Net.HttpStatusCode.InternalServerError)
|
||||||
|
{
|
||||||
|
hasUnhealthy = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("External service {Service} health check: {Status} in {Duration}ms",
|
||||||
|
service.Key, response.StatusCode, serviceStopwatch.ElapsedMilliseconds);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
serviceStopwatch.Stop();
|
||||||
|
allHealthy = false;
|
||||||
|
hasUnhealthy = true;
|
||||||
|
|
||||||
|
results[service.Key] = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "status", "unhealthy" },
|
||||||
|
{ "duration", $"{serviceStopwatch.ElapsedMilliseconds}ms" },
|
||||||
|
{ "error", ex.Message },
|
||||||
|
{ "url", service.Value }
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogError(ex, "External service {Service} health check failed", service.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
var totalDuration = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
var data = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "status", hasUnhealthy ? "unhealthy" : (allHealthy ? "healthy" : "degraded") },
|
||||||
|
{ "duration", $"{totalDuration}ms" },
|
||||||
|
{ "services", results },
|
||||||
|
{ "total_services", services.Count },
|
||||||
|
{ "healthy_services", results.Values.Count(r => ((Dictionary<string, object>)r)["status"].ToString() == "healthy") }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (hasUnhealthy)
|
||||||
|
{
|
||||||
|
return HealthCheckResult.Unhealthy("One or more external services are unhealthy", data: data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allHealthy)
|
||||||
|
{
|
||||||
|
return HealthCheckResult.Degraded("Some external services have issues", data: data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HealthCheckResult.Healthy($"All external services are responsive ({totalDuration}ms)", data: data);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
var duration = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
_logger.LogError(ex, "External services health check failed after {Duration}ms", duration);
|
||||||
|
|
||||||
|
var data = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "status", "unhealthy" },
|
||||||
|
{ "duration", $"{duration}ms" },
|
||||||
|
{ "error", ex.Message }
|
||||||
|
};
|
||||||
|
|
||||||
|
return HealthCheckResult.Unhealthy($"External services check failed: {ex.Message}", ex, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/BCards.Web/HealthChecks/MongoDbHealthCheck.cs
Normal file
73
src/BCards.Web/HealthChecks/MongoDbHealthCheck.cs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace BCards.Web.HealthChecks;
|
||||||
|
|
||||||
|
public class MongoDbHealthCheck : IHealthCheck
|
||||||
|
{
|
||||||
|
private readonly IMongoDatabase _database;
|
||||||
|
private readonly ILogger<MongoDbHealthCheck> _logger;
|
||||||
|
|
||||||
|
public MongoDbHealthCheck(IMongoDatabase database, ILogger<MongoDbHealthCheck> logger)
|
||||||
|
{
|
||||||
|
_database = database;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||||
|
HealthCheckContext context,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Executa ping no MongoDB
|
||||||
|
var command = new BsonDocument("ping", 1);
|
||||||
|
await _database.RunCommandAsync<BsonDocument>(command, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
var duration = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
_logger.LogInformation("MongoDB health check completed successfully in {Duration}ms", duration);
|
||||||
|
|
||||||
|
var data = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "status", "healthy" },
|
||||||
|
{ "duration", $"{duration}ms" },
|
||||||
|
{ "database", _database.DatabaseNamespace.DatabaseName },
|
||||||
|
{ "connection_state", "connected" },
|
||||||
|
{ "latency", duration }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Status baseado na latência
|
||||||
|
if (duration > 5000) // > 5s
|
||||||
|
return HealthCheckResult.Unhealthy($"MongoDB response time too high: {duration}ms", data: data);
|
||||||
|
|
||||||
|
if (duration > 2000) // > 2s
|
||||||
|
return HealthCheckResult.Degraded($"MongoDB response time elevated: {duration}ms", data: data);
|
||||||
|
|
||||||
|
return HealthCheckResult.Healthy($"MongoDB is responsive ({duration}ms)", data: data);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
var duration = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
_logger.LogError(ex, "MongoDB health check failed after {Duration}ms", duration);
|
||||||
|
|
||||||
|
var data = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "status", "unhealthy" },
|
||||||
|
{ "duration", $"{duration}ms" },
|
||||||
|
{ "database", _database.DatabaseNamespace.DatabaseName },
|
||||||
|
{ "connection_state", "disconnected" },
|
||||||
|
{ "error", ex.Message }
|
||||||
|
};
|
||||||
|
|
||||||
|
return HealthCheckResult.Unhealthy($"MongoDB connection failed: {ex.Message}", ex, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/BCards.Web/HealthChecks/SendGridHealthCheck.cs
Normal file
95
src/BCards.Web/HealthChecks/SendGridHealthCheck.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using SendGrid;
|
||||||
|
using SendGrid.Helpers.Mail;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace BCards.Web.HealthChecks;
|
||||||
|
|
||||||
|
public class SendGridHealthCheck : IHealthCheck
|
||||||
|
{
|
||||||
|
private readonly ISendGridClient _sendGridClient;
|
||||||
|
private readonly ILogger<SendGridHealthCheck> _logger;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public SendGridHealthCheck(ISendGridClient sendGridClient, ILogger<SendGridHealthCheck> logger, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_sendGridClient = sendGridClient;
|
||||||
|
_logger = logger;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||||
|
HealthCheckContext context,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Testa a API do SendGrid fazendo uma validação de API key
|
||||||
|
// Usando endpoint de templates que não requer parâmetros específicos
|
||||||
|
var response = await _sendGridClient.RequestAsync(
|
||||||
|
method: SendGridClient.Method.GET,
|
||||||
|
urlPath: "templates",
|
||||||
|
queryParams: "{\"generations\":\"legacy,dynamic\",\"page_size\":1}",
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
var duration = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
var apiKey = _configuration["SendGrid:ApiKey"];
|
||||||
|
var apiKeyPrefix = string.IsNullOrEmpty(apiKey) ? "not_configured" :
|
||||||
|
apiKey.Substring(0, Math.Min(8, apiKey.Length)) + "...";
|
||||||
|
|
||||||
|
_logger.LogInformation("SendGrid health check completed with status {StatusCode} in {Duration}ms",
|
||||||
|
response.StatusCode, duration);
|
||||||
|
|
||||||
|
var data = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "status", response.IsSuccessStatusCode ? "healthy" : "unhealthy" },
|
||||||
|
{ "duration", $"{duration}ms" },
|
||||||
|
{ "status_code", (int)response.StatusCode },
|
||||||
|
{ "api_key_prefix", apiKeyPrefix },
|
||||||
|
{ "latency", duration }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verifica se a resposta foi bem-sucedida
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
// Status baseado na latência
|
||||||
|
if (duration > 8000) // > 8s
|
||||||
|
return HealthCheckResult.Unhealthy($"SendGrid response time too high: {duration}ms", data: data);
|
||||||
|
|
||||||
|
if (duration > 4000) // > 4s
|
||||||
|
return HealthCheckResult.Degraded($"SendGrid response time elevated: {duration}ms", data: data);
|
||||||
|
|
||||||
|
return HealthCheckResult.Healthy($"SendGrid API is responsive ({duration}ms)", data: data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data["error"] = $"HTTP {response.StatusCode}";
|
||||||
|
data["response_body"] = response.Body;
|
||||||
|
|
||||||
|
return HealthCheckResult.Unhealthy(
|
||||||
|
$"SendGrid API returned {response.StatusCode}",
|
||||||
|
data: data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
var duration = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
_logger.LogError(ex, "SendGrid health check failed after {Duration}ms", duration);
|
||||||
|
|
||||||
|
var data = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "status", "unhealthy" },
|
||||||
|
{ "duration", $"{duration}ms" },
|
||||||
|
{ "error", ex.Message }
|
||||||
|
};
|
||||||
|
|
||||||
|
return HealthCheckResult.Unhealthy($"SendGrid connection failed: {ex.Message}", ex, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
src/BCards.Web/HealthChecks/StripeHealthCheck.cs
Normal file
94
src/BCards.Web/HealthChecks/StripeHealthCheck.cs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using BCards.Web.Configuration;
|
||||||
|
using Stripe;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace BCards.Web.HealthChecks;
|
||||||
|
|
||||||
|
public class StripeHealthCheck : IHealthCheck
|
||||||
|
{
|
||||||
|
private readonly StripeSettings _stripeSettings;
|
||||||
|
private readonly ILogger<StripeHealthCheck> _logger;
|
||||||
|
|
||||||
|
public StripeHealthCheck(IOptions<StripeSettings> stripeSettings, ILogger<StripeHealthCheck> logger)
|
||||||
|
{
|
||||||
|
_stripeSettings = stripeSettings.Value;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||||
|
HealthCheckContext context,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Configura Stripe temporariamente para o teste
|
||||||
|
StripeConfiguration.ApiKey = _stripeSettings.SecretKey;
|
||||||
|
|
||||||
|
// Testa conectividade listando produtos (limite 1 para ser rápido)
|
||||||
|
var productService = new ProductService();
|
||||||
|
var options = new ProductListOptions { Limit = 1 };
|
||||||
|
|
||||||
|
await productService.ListAsync(options, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
var duration = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
_logger.LogInformation("Stripe health check completed successfully in {Duration}ms", duration);
|
||||||
|
|
||||||
|
var data = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "status", "healthy" },
|
||||||
|
{ "duration", $"{duration}ms" },
|
||||||
|
{ "api_key_prefix", _stripeSettings.SecretKey?.Substring(0, Math.Min(12, _stripeSettings.SecretKey.Length)) + "..." },
|
||||||
|
{ "latency", duration }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Status baseado na latência
|
||||||
|
if (duration > 10000) // > 10s
|
||||||
|
return HealthCheckResult.Unhealthy($"Stripe response time too high: {duration}ms", data: data);
|
||||||
|
|
||||||
|
if (duration > 5000) // > 5s
|
||||||
|
return HealthCheckResult.Degraded($"Stripe response time elevated: {duration}ms", data: data);
|
||||||
|
|
||||||
|
return HealthCheckResult.Healthy($"Stripe API is responsive ({duration}ms)", data: data);
|
||||||
|
}
|
||||||
|
catch (StripeException stripeEx)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
var duration = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
_logger.LogError(stripeEx, "Stripe health check failed after {Duration}ms: {Error}", duration, stripeEx.Message);
|
||||||
|
|
||||||
|
var data = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "status", "unhealthy" },
|
||||||
|
{ "duration", $"{duration}ms" },
|
||||||
|
{ "error", stripeEx.Message },
|
||||||
|
{ "error_code", stripeEx.StripeError?.Code ?? "unknown" },
|
||||||
|
{ "error_type", stripeEx.StripeError?.Type ?? "unknown" }
|
||||||
|
};
|
||||||
|
|
||||||
|
return HealthCheckResult.Unhealthy($"Stripe API error: {stripeEx.Message}", stripeEx, data);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
var duration = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
_logger.LogError(ex, "Stripe health check failed after {Duration}ms", duration);
|
||||||
|
|
||||||
|
var data = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "status", "unhealthy" },
|
||||||
|
{ "duration", $"{duration}ms" },
|
||||||
|
{ "error", ex.Message }
|
||||||
|
};
|
||||||
|
|
||||||
|
return HealthCheckResult.Unhealthy($"Stripe connection failed: {ex.Message}", ex, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
src/BCards.Web/HealthChecks/SystemResourcesHealthCheck.cs
Normal file
134
src/BCards.Web/HealthChecks/SystemResourcesHealthCheck.cs
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace BCards.Web.HealthChecks;
|
||||||
|
|
||||||
|
public class SystemResourcesHealthCheck : IHealthCheck
|
||||||
|
{
|
||||||
|
private readonly ILogger<SystemResourcesHealthCheck> _logger;
|
||||||
|
private static readonly DateTime _startTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public SystemResourcesHealthCheck(ILogger<SystemResourcesHealthCheck> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<HealthCheckResult> CheckHealthAsync(
|
||||||
|
HealthCheckContext context,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Informações de memória
|
||||||
|
var totalMemory = GC.GetTotalMemory(false);
|
||||||
|
var workingSet = Environment.WorkingSet;
|
||||||
|
|
||||||
|
// Informações do processo atual
|
||||||
|
using var currentProcess = Process.GetCurrentProcess();
|
||||||
|
var cpuUsage = GetCpuUsage(currentProcess);
|
||||||
|
|
||||||
|
// Uptime
|
||||||
|
var uptime = DateTime.UtcNow - _startTime;
|
||||||
|
var uptimeString = FormatUptime(uptime);
|
||||||
|
|
||||||
|
// Thread count
|
||||||
|
var threadCount = currentProcess.Threads.Count;
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
var duration = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
var data = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "status", "healthy" },
|
||||||
|
{ "duration", $"{duration}ms" },
|
||||||
|
{ "memory", new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "total_managed_mb", Math.Round(totalMemory / 1024.0 / 1024.0, 2) },
|
||||||
|
{ "working_set_mb", Math.Round(workingSet / 1024.0 / 1024.0, 2) },
|
||||||
|
{ "gc_generation_0", GC.CollectionCount(0) },
|
||||||
|
{ "gc_generation_1", GC.CollectionCount(1) },
|
||||||
|
{ "gc_generation_2", GC.CollectionCount(2) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ "process", new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "id", currentProcess.Id },
|
||||||
|
{ "threads", threadCount },
|
||||||
|
{ "handles", currentProcess.HandleCount },
|
||||||
|
{ "uptime", uptimeString },
|
||||||
|
{ "uptime_seconds", (int)uptime.TotalSeconds }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ "system", new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "processor_count", Environment.ProcessorCount },
|
||||||
|
{ "os_version", Environment.OSVersion.ToString() },
|
||||||
|
{ "machine_name", Environment.MachineName },
|
||||||
|
{ "user_name", Environment.UserName }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("System resources health check completed in {Duration}ms - Memory: {Memory}MB, Threads: {Threads}",
|
||||||
|
duration, Math.Round(totalMemory / 1024.0 / 1024.0, 1), threadCount);
|
||||||
|
|
||||||
|
// Definir thresholds para status
|
||||||
|
var memoryMb = totalMemory / 1024.0 / 1024.0;
|
||||||
|
|
||||||
|
if (memoryMb > 1000) // > 1GB
|
||||||
|
{
|
||||||
|
data["status"] = "degraded";
|
||||||
|
return Task.FromResult(HealthCheckResult.Degraded($"High memory usage: {memoryMb:F1}MB", data: data));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threadCount > 500)
|
||||||
|
{
|
||||||
|
data["status"] = "degraded";
|
||||||
|
return Task.FromResult(HealthCheckResult.Degraded($"High thread count: {threadCount}", data: data));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(HealthCheckResult.Healthy($"System resources normal (Memory: {memoryMb:F1}MB, Threads: {threadCount})", data: data));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
var duration = stopwatch.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
_logger.LogError(ex, "System resources health check failed after {Duration}ms", duration);
|
||||||
|
|
||||||
|
var data = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "status", "unhealthy" },
|
||||||
|
{ "duration", $"{duration}ms" },
|
||||||
|
{ "error", ex.Message }
|
||||||
|
};
|
||||||
|
|
||||||
|
return Task.FromResult(HealthCheckResult.Unhealthy($"System resources check failed: {ex.Message}", ex, data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double GetCpuUsage(Process process)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return process.TotalProcessorTime.TotalMilliseconds;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatUptime(TimeSpan uptime)
|
||||||
|
{
|
||||||
|
if (uptime.TotalDays >= 1)
|
||||||
|
return $"{(int)uptime.TotalDays}d {uptime.Hours}h {uptime.Minutes}m";
|
||||||
|
if (uptime.TotalHours >= 1)
|
||||||
|
return $"{uptime.Hours}h {uptime.Minutes}m";
|
||||||
|
if (uptime.TotalMinutes >= 1)
|
||||||
|
return $"{uptime.Minutes}m {uptime.Seconds}s";
|
||||||
|
return $"{uptime.Seconds}s";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
using BCards.Web.Configuration;
|
using BCards.Web.Configuration;
|
||||||
using BCards.Web.Services;
|
using BCards.Web.Services;
|
||||||
using BCards.Web.Repositories;
|
using BCards.Web.Repositories;
|
||||||
|
using BCards.Web.HealthChecks;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authentication.Google;
|
using Microsoft.AspNetCore.Authentication.Google;
|
||||||
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
|
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
|
||||||
@ -14,9 +15,74 @@ using SendGrid;
|
|||||||
using BCards.Web.Middleware;
|
using BCards.Web.Middleware;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.HttpOverrides;
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
using Serilog;
|
||||||
|
using Serilog.Events;
|
||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Configure Serilog with environment-specific settings
|
||||||
|
var isDevelopment = builder.Environment.IsDevelopment();
|
||||||
|
var hostname = Environment.MachineName;
|
||||||
|
|
||||||
|
var loggerConfig = new LoggerConfiguration()
|
||||||
|
.ReadFrom.Configuration(builder.Configuration)
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
.Enrich.WithEnvironmentName()
|
||||||
|
.Enrich.WithProcessId()
|
||||||
|
.Enrich.WithThreadId()
|
||||||
|
.Enrich.WithProperty("ApplicationName", builder.Configuration["ApplicationName"] ?? "BCards")
|
||||||
|
.Enrich.WithProperty("Environment", builder.Environment.EnvironmentName)
|
||||||
|
.Enrich.WithProperty("Hostname", hostname);
|
||||||
|
|
||||||
|
if (isDevelopment)
|
||||||
|
{
|
||||||
|
// Development: Log EVERYTHING to console with detailed formatting
|
||||||
|
loggerConfig
|
||||||
|
.MinimumLevel.Debug()
|
||||||
|
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||||
|
.MinimumLevel.Override("System", LogEventLevel.Information)
|
||||||
|
.WriteTo.Async(a => a.Console(
|
||||||
|
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{Hostname}] {Message:lj} {Properties:j}{NewLine}{Exception}"));
|
||||||
|
|
||||||
|
// Also send to Seq if configured (for local development with Seq)
|
||||||
|
var seqUrl = builder.Configuration["Serilog:SeqUrl"];
|
||||||
|
if (!string.IsNullOrEmpty(seqUrl))
|
||||||
|
{
|
||||||
|
var apiKey = builder.Configuration["Serilog:ApiKey"];
|
||||||
|
loggerConfig.WriteTo.Async(a => a.Seq(seqUrl, apiKey: string.IsNullOrEmpty(apiKey) ? null : apiKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Production: Only errors to console, everything to Seq
|
||||||
|
loggerConfig
|
||||||
|
.MinimumLevel.Information()
|
||||||
|
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||||
|
.MinimumLevel.Override("System", LogEventLevel.Warning)
|
||||||
|
.WriteTo.Async(a => a.Console(
|
||||||
|
restrictedToMinimumLevel: LogEventLevel.Error,
|
||||||
|
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] [{Hostname}] {Message:lj}{NewLine}{Exception}"));
|
||||||
|
|
||||||
|
// Production: Send detailed logs to Seq
|
||||||
|
var seqUrl = builder.Configuration["Serilog:SeqUrl"];
|
||||||
|
if (!string.IsNullOrEmpty(seqUrl))
|
||||||
|
{
|
||||||
|
var apiKey = builder.Configuration["Serilog:ApiKey"];
|
||||||
|
loggerConfig.WriteTo.Async(a => a.Seq(seqUrl,
|
||||||
|
apiKey: string.IsNullOrEmpty(apiKey) ? null : apiKey,
|
||||||
|
restrictedToMinimumLevel: LogEventLevel.Information));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Logger = loggerConfig.CreateLogger();
|
||||||
|
|
||||||
|
// Use Serilog for the host
|
||||||
|
builder.Host.UseSerilog();
|
||||||
|
|
||||||
|
// Log startup information
|
||||||
|
Log.Information("Starting BCards application on {Hostname} in {Environment} mode", hostname, builder.Environment.EnvironmentName);
|
||||||
|
|
||||||
// 🔥 CONFIGURAR FORWARDED HEADERS NO BUILDER
|
// 🔥 CONFIGURAR FORWARDED HEADERS NO BUILDER
|
||||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||||
{
|
{
|
||||||
@ -208,6 +274,52 @@ builder.Services.AddMemoryCache();
|
|||||||
|
|
||||||
builder.Services.AddRazorPages();
|
builder.Services.AddRazorPages();
|
||||||
|
|
||||||
|
// ===== CONFIGURAÇÃO DOS HEALTH CHECKS =====
|
||||||
|
builder.Services.AddHealthChecks()
|
||||||
|
// MongoDB Health Check usando configuração existente
|
||||||
|
.AddCheck<MongoDbHealthCheck>(
|
||||||
|
name: "mongodb",
|
||||||
|
failureStatus: HealthStatus.Unhealthy,
|
||||||
|
timeout: TimeSpan.FromSeconds(10))
|
||||||
|
|
||||||
|
// Stripe Health Check
|
||||||
|
.AddCheck<StripeHealthCheck>(
|
||||||
|
name: "stripe",
|
||||||
|
failureStatus: HealthStatus.Degraded, // Stripe não é crítico para funcionalidade básica
|
||||||
|
timeout: TimeSpan.FromSeconds(15))
|
||||||
|
|
||||||
|
// SendGrid Health Check
|
||||||
|
.AddCheck<SendGridHealthCheck>(
|
||||||
|
name: "sendgrid",
|
||||||
|
failureStatus: HealthStatus.Degraded, // Email não é crítico para funcionalidade básica
|
||||||
|
timeout: TimeSpan.FromSeconds(10))
|
||||||
|
|
||||||
|
// External Services (OAuth providers)
|
||||||
|
.AddCheck<ExternalServicesHealthCheck>(
|
||||||
|
name: "external_services",
|
||||||
|
failureStatus: HealthStatus.Degraded, // OAuth pode estar indisponível temporariamente
|
||||||
|
timeout: TimeSpan.FromSeconds(20))
|
||||||
|
|
||||||
|
// System Resources
|
||||||
|
.AddCheck<SystemResourcesHealthCheck>(
|
||||||
|
name: "resources",
|
||||||
|
failureStatus: HealthStatus.Degraded,
|
||||||
|
timeout: TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
// Registrar health checks customizados no DI
|
||||||
|
builder.Services.AddTransient<MongoDbHealthCheck>();
|
||||||
|
builder.Services.AddTransient<StripeHealthCheck>();
|
||||||
|
builder.Services.AddTransient<SendGridHealthCheck>();
|
||||||
|
builder.Services.AddTransient<ExternalServicesHealthCheck>();
|
||||||
|
builder.Services.AddTransient<SystemResourcesHealthCheck>();
|
||||||
|
|
||||||
|
// HttpClient para External Services Health Check
|
||||||
|
builder.Services.AddHttpClient<ExternalServicesHealthCheck>(client =>
|
||||||
|
{
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(10);
|
||||||
|
client.DefaultRequestHeaders.Add("User-Agent", "BCards-HealthCheck/1.0");
|
||||||
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// 🔥 PRIMEIRA COISA APÓS BUILD - FORWARDED HEADERS + BASE URL OVERRIDE
|
// 🔥 PRIMEIRA COISA APÓS BUILD - FORWARDED HEADERS + BASE URL OVERRIDE
|
||||||
@ -360,7 +472,21 @@ using (var scope = app.Services.CreateScope())
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Run();
|
try
|
||||||
|
{
|
||||||
|
Log.Information("BCards application started successfully on {Hostname}", hostname);
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Fatal(ex, "BCards application terminated unexpectedly on {Hostname}", hostname);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Log.Information("BCards application shutting down on {Hostname}", hostname);
|
||||||
|
Log.CloseAndFlush();
|
||||||
|
}
|
||||||
|
|
||||||
// Make Program accessible for integration tests
|
// Make Program accessible for integration tests
|
||||||
public partial class Program { }
|
public partial class Program { }
|
||||||
@ -7,6 +7,10 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Serilog": {
|
||||||
|
"SeqUrl": "http://192.168.0.100:5341",
|
||||||
|
"ApiKey": ""
|
||||||
|
},
|
||||||
"DetailedErrors": true,
|
"DetailedErrors": true,
|
||||||
"MongoDb": {
|
"MongoDb": {
|
||||||
"ConnectionString": "mongodb://localhost:27017",
|
"ConnectionString": "mongodb://localhost:27017",
|
||||||
|
|||||||
@ -5,6 +5,10 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Serilog": {
|
||||||
|
"SeqUrl": "http://seq.internal:5341",
|
||||||
|
"ApiKey": "YOUR_PRODUCTION_API_KEY"
|
||||||
|
},
|
||||||
"MongoDb": {
|
"MongoDb": {
|
||||||
"ConnectionString": "mongodb://192.168.0.100:27017",
|
"ConnectionString": "mongodb://192.168.0.100:27017",
|
||||||
"DatabaseName": "BCardsDB_Staging"
|
"DatabaseName": "BCardsDB_Staging"
|
||||||
|
|||||||
@ -6,6 +6,10 @@
|
|||||||
"Microsoft.Hosting.Lifetime": "Information"
|
"Microsoft.Hosting.Lifetime": "Information"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Serilog": {
|
||||||
|
"SeqUrl": "http://localhost:5341",
|
||||||
|
"ApiKey": ""
|
||||||
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"Stripe": {
|
"Stripe": {
|
||||||
"PublishableKey": "pk_test_51RjUmIBMIadsOxJVP4bWc54pHEOSf5km1hpOkOBSoGVoKxI46N4KSWtevpXCSq68OjFazBuXmPJGBwZ1KDN5MNJy003lj1YmAS",
|
"PublishableKey": "pk_test_51RjUmIBMIadsOxJVP4bWc54pHEOSf5km1hpOkOBSoGVoKxI46N4KSWtevpXCSq68OjFazBuXmPJGBwZ1KDN5MNJy003lj1YmAS",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user