diff --git a/Dockerfile b/Dockerfile index 6593d17..70e8cfa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src COPY ./src ./src @@ -6,10 +6,16 @@ COPY ./src ./src RUN dotnet restore "src/Telegram2VkBot/Telegram2VkBot.csproj" RUN dotnet publish "src/Telegram2VkBot/Telegram2VkBot.csproj" -c Release -o /app/publish --no-restore -FROM mcr.microsoft.com/dotnet/runtime:8.0 AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY --from=build /app/publish . +ENV ASPNETCORE_URLS=http://+:8080 +EXPOSE 8080 + ENTRYPOINT ["dotnet", "Telegram2VkBot.dll"] diff --git a/docker-compose.yml b/docker-compose.yml index 5372880..c0a2846 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,15 @@ services: #VK__GroupId: ${VK_GROUP_ID} #VK__ApiVersion: ${VK_API_VERSION:-5.199} DB__Path: /data/mapping.sqlite + ASPNETCORE_URLS: http://+:8080 + ports: + - "8080:8080" volumes: - ./data:/data + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/health >/dev/null || exit 1"] + interval: 30s + timeout: 12s + retries: 3 + start_period: 45s diff --git a/src/Telegram2VkBot/Program.cs b/src/Telegram2VkBot/Program.cs index f1646af..a93f0ff 100644 --- a/src/Telegram2VkBot/Program.cs +++ b/src/Telegram2VkBot/Program.cs @@ -1,13 +1,10 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - namespace Telegram2VkBot; public static class Program { public static async Task Main(string[] args) { - var builder = Host.CreateApplicationBuilder(args); + var builder = WebApplication.CreateBuilder(args); builder.Services.Configure(builder.Configuration.GetSection("TELEGRAM")); builder.Services.Configure(builder.Configuration.GetSection("VK")); @@ -17,9 +14,20 @@ public static class Program { client.Timeout = TimeSpan.FromSeconds(60); }); + + builder.Services.AddHttpClient(TelegramApiHealthCheck.HttpClientName, client => + { + client.Timeout = TimeSpan.FromSeconds(15); + }); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddHealthChecks() + .AddCheck( + name: "telegram", + tags: new[] { "telegram" }); + builder.Services.AddHostedService(); builder.Logging.AddSimpleConsole(options => @@ -28,8 +36,11 @@ public static class Program options.TimestampFormat = "yyyy-MM-dd HH:mm:ss "; }); - var host = builder.Build(); - await host.RunAsync(); + var app = builder.Build(); + + app.MapHealthChecks("/health"); + app.MapGet("/", () => Results.Redirect("/health")); + + await app.RunAsync().ConfigureAwait(false); } } - diff --git a/src/Telegram2VkBot/Telegram2VkBot.csproj b/src/Telegram2VkBot/Telegram2VkBot.csproj index 9ad51d4..371109a 100644 --- a/src/Telegram2VkBot/Telegram2VkBot.csproj +++ b/src/Telegram2VkBot/Telegram2VkBot.csproj @@ -1,6 +1,6 @@ - + - net8.0 + net10.0 enable enable @@ -9,10 +9,7 @@ PreserveNewest - - - - + diff --git a/src/Telegram2VkBot/TelegramApiHealthCheck.cs b/src/Telegram2VkBot/TelegramApiHealthCheck.cs new file mode 100644 index 0000000..bcb9bdd --- /dev/null +++ b/src/Telegram2VkBot/TelegramApiHealthCheck.cs @@ -0,0 +1,76 @@ +using System.Text.Json; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; + +namespace Telegram2VkBot; + +/// +/// Проверка доступности Telegram Bot API (getMe). +/// +public sealed class TelegramApiHealthCheck : IHealthCheck +{ + public const string HttpClientName = "TelegramHealth"; + + private readonly TelegramOptions _telegram; + private readonly IHttpClientFactory _httpFactory; + + public TelegramApiHealthCheck(IOptions telegram, IHttpClientFactory httpFactory) + { + _telegram = telegram.Value; + _httpFactory = httpFactory; + } + + public async Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(_telegram.BotToken)) + return HealthCheckResult.Unhealthy("TELEGRAM__BotToken не задан."); + + var url = $"https://api.telegram.org/bot{_telegram.BotToken}/getMe"; + var http = _httpFactory.CreateClient(HttpClientName); + + try + { + using var response = await http.GetAsync(url, cancellationToken).ConfigureAwait(false); + var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + { + return HealthCheckResult.Unhealthy( + $"Telegram API HTTP {(int)response.StatusCode}", + data: new Dictionary { ["body"] = Truncate(json, 500) }); + } + + using var doc = JsonDocument.Parse(json); + if (!doc.RootElement.TryGetProperty("ok", out var ok) || ok.ValueKind != JsonValueKind.True) + { + var desc = doc.RootElement.TryGetProperty("description", out var d) ? d.GetString() : json; + return HealthCheckResult.Unhealthy($"Telegram getMe: ok=false, {desc}"); + } + + var username = doc.RootElement.TryGetProperty("result", out var result) + && result.TryGetProperty("username", out var un) + ? un.GetString() + : null; + + return HealthCheckResult.Healthy( + username is null ? "Telegram API доступен." : $"Telegram API: @{username}"); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception ex) + { + return HealthCheckResult.Unhealthy("Не удалось связаться с Telegram API.", ex); + } + } + + private static string Truncate(string s, int max) + { + if (string.IsNullOrEmpty(s) || s.Length <= max) + return s; + return string.Concat(s.AsSpan(0, max), "…"); + } +}