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 async Task<HttpClient> CreateAuthenticatedClientAsync(
|
||||
public static Task<HttpClient> CreateAuthenticatedClientAsync(
|
||||
WebApplicationFactory<Program> factory,
|
||||
User testUser)
|
||||
{
|
||||
@ -34,7 +34,7 @@ public static class AuthenticationHelper
|
||||
client.DefaultRequestHeaders.Add("TestUserEmail", testUser.Email);
|
||||
client.DefaultRequestHeaders.Add("TestUserName", testUser.Name);
|
||||
|
||||
return client;
|
||||
return Task.FromResult(client);
|
||||
}
|
||||
|
||||
public static ClaimsPrincipal CreateTestClaimsPrincipal(User user)
|
||||
|
||||
@ -161,9 +161,9 @@ public class PuppeteerTestHelper : IAsyncDisposable
|
||||
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)
|
||||
|
||||
@ -20,6 +20,15 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.ResponseCaching" Version="2.2.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.54" />
|
||||
<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>
|
||||
|
||||
@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace BCards.Web.Controllers;
|
||||
|
||||
@ -197,6 +198,11 @@ public class AdminController : Controller
|
||||
ModelState.Remove<ManagePageViewModel>(x => x.TwitterUrl);
|
||||
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
|
||||
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;
|
||||
|
||||
//[Route("[controller]")]
|
||||
public class UserPageController : Controller
|
||||
{
|
||||
private readonly IUserPageService _userPageService;
|
||||
@ -28,9 +27,6 @@ public class UserPageController : Controller
|
||||
_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)
|
||||
{
|
||||
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.Services;
|
||||
using BCards.Web.Repositories;
|
||||
using BCards.Web.HealthChecks;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.Google;
|
||||
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
|
||||
@ -14,9 +15,74 @@ using SendGrid;
|
||||
using BCards.Web.Middleware;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
|
||||
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
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
@ -208,6 +274,52 @@ builder.Services.AddMemoryCache();
|
||||
|
||||
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();
|
||||
|
||||
// 🔥 PRIMEIRA COISA APÓS BUILD - FORWARDED HEADERS + BASE URL OVERRIDE
|
||||
@ -360,7 +472,21 @@ using (var scope = app.Services.CreateScope())
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
public partial class Program { }
|
||||
@ -7,6 +7,10 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Serilog": {
|
||||
"SeqUrl": "http://192.168.0.100:5341",
|
||||
"ApiKey": ""
|
||||
},
|
||||
"DetailedErrors": true,
|
||||
"MongoDb": {
|
||||
"ConnectionString": "mongodb://localhost:27017",
|
||||
|
||||
@ -5,6 +5,10 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Serilog": {
|
||||
"SeqUrl": "http://seq.internal:5341",
|
||||
"ApiKey": "YOUR_PRODUCTION_API_KEY"
|
||||
},
|
||||
"MongoDb": {
|
||||
"ConnectionString": "mongodb://192.168.0.100:27017",
|
||||
"DatabaseName": "BCardsDB_Staging"
|
||||
|
||||
@ -6,6 +6,10 @@
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"Serilog": {
|
||||
"SeqUrl": "http://localhost:5341",
|
||||
"ApiKey": ""
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Stripe": {
|
||||
"PublishableKey": "pk_test_51RjUmIBMIadsOxJVP4bWc54pHEOSf5km1hpOkOBSoGVoKxI46N4KSWtevpXCSq68OjFazBuXmPJGBwZ1KDN5MNJy003lj1YmAS",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user