Compare commits

..

2 Commits

Author SHA1 Message Date
daf3639038 Добавил correlation id
Some checks failed
Create and publish a Docker image / Publish image (push) Failing after 1m10s
2025-11-21 16:03:14 +03:00
8ba1aea46a Вынес httpclient в отдельный сервис 2025-11-21 03:56:04 +03:00
6 changed files with 218 additions and 108 deletions

View File

@@ -11,7 +11,7 @@ namespace SfeduSchedule.Controllers;
[ApiController]
[Route("api/proxy")]
[EnableRateLimiting("throttle")]
public class ProxyController(ModeusService modeusService, ILogger<ScheduleController> logger) : ControllerBase
public class ProxyController(ModeusService modeusService, ILogger<ProxyController> logger) : ControllerBase
{
/// <summary>
/// Получить расписание по пользовательскому запросу.

View File

@@ -8,6 +8,7 @@ public class UpdateJwtJob(
IConfiguration configuration,
ILogger<UpdateJwtJob> logger,
IHttpClientFactory httpClientFactory,
ModeusHttpClient modeusHttpClient,
ModeusService modeusService) : IJob
{
private const int MaxAttempts = 5; // Максимальное число попыток
@@ -70,7 +71,7 @@ public class UpdateJwtJob(
}
configuration["TOKEN"] = body.Jwt;
modeusService.SetToken(body.Jwt);
modeusHttpClient.SetToken(body.Jwt);
await File.WriteAllTextAsync(GlobalConsts.JwtFilePath,
body.Jwt + "\n" + DateTime.Now.ToString("O"), cts.Token);
logger.LogInformation("JWT успешно обновлён");

View File

@@ -0,0 +1,34 @@
using System.Diagnostics;
namespace SfeduSchedule.Middleware;
/// <summary>
/// Middleware для добавления и обработки Correlation ID в HTTP запросах.
/// Нужно для трассировки запросов.
/// </summary>
public sealed class CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger)
{
private const string HeaderName = "X-Correlation-ID";
public async Task InvokeAsync(HttpContext context)
{
// 1. Берём из заголовка, если клиент прислал
if (!context.Request.Headers.TryGetValue(HeaderName, out var correlationId) ||
string.IsNullOrWhiteSpace(correlationId))
{
// 2. Иначе используем Activity TraceId или TraceIdentifier
var activityId = Activity.Current?.TraceId.ToString();
correlationId = !string.IsNullOrEmpty(activityId)
? activityId
: context.TraceIdentifier;
}
// Положим в Items, чтобы можно было достать из сервисов
// context.Items[HeaderName] = correlationId.ToString();
// 3. Прокинем в ответ
context.Response.Headers[HeaderName] = correlationId.ToString();
await next(context);
}
}

View File

@@ -1,4 +1,5 @@
using System.Net;
using System.Diagnostics;
using System.Reflection;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Authentication;
@@ -15,10 +16,13 @@ using SfeduSchedule.Auth;
using SfeduSchedule.Jobs;
using SfeduSchedule.Middleware;
using SfeduSchedule.Services;
using SfeduSchedule.Logging;
using X.Extensions.Logging.Telegram.Extensions;
using Microsoft.Extensions.Logging.Console;
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
var builder = WebApplication.CreateBuilder(args);
#region Работа с конфигурацией
var configuration = builder.Configuration;
var preinstalledJwtToken = configuration["TOKEN"];
@@ -36,15 +40,25 @@ if (string.IsNullOrEmpty(configuration["MODEUS_URL"]))
var permitLimit = int.TryParse(configuration["PERMIT_LIMIT"], out var parsedPermitLimit) ? parsedPermitLimit : 40;
var timeLimit = int.TryParse(configuration["TIME_LIMIT"], out var parsedTimeLimit) ? parsedTimeLimit : 10;
#endregion
#region Работа с папкой данных
// создать папку data если не существует
var dataDirectory = Path.Combine(AppContext.BaseDirectory, "data");
if (!Directory.Exists(dataDirectory)) Directory.CreateDirectory(dataDirectory);
GlobalConsts.JwtFilePath = Path.Combine(dataDirectory, "jwt.txt");
var pluginsPath = Path.Combine(dataDirectory, "Plugins");
#endregion
#region Работа с логированием
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
// Configure the console logger to include logging scopes so TraceId from the CorrelationIdMiddleware is visible
builder.Logging.AddSimpleConsole(options =>
{
options.SingleLine = true;
options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff ";
});
builder.Logging.AddFilter("Quartz", LogLevel.Warning);
if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken))
builder.Logging.AddTelegram(options =>
@@ -52,7 +66,7 @@ if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken))
options.ChatId = tgChatId;
options.AccessToken = tgToken;
options.FormatterConfiguration.UseEmoji = true;
options.FormatterConfiguration.ReadableApplicationName = "Sfedu Schedule";
options.FormatterConfiguration.ReadableApplicationName = "Modeus Schedule Proxy";
options.LogLevel = new Dictionary<string, LogLevel>
{
{ "Default", LogLevel.Error },
@@ -61,6 +75,7 @@ if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken))
{ "Quartz", LogLevel.Warning }
};
});
#endregion
// Включаем MVC контроллеры
var mvcBuilder = builder.Services.AddControllers();
@@ -68,7 +83,8 @@ builder.Services.AddHttpClient("modeus", client =>
{
client.BaseAddress = new Uri(configuration["MODEUS_URL"]!);
});
builder.Services.AddSingleton<ModeusService>();
builder.Services.AddSingleton<ModeusHttpClient>();
builder.Services.AddScoped<ModeusService>();
builder.Services.AddHttpClient("authClient");
builder.Services.AddAuthentication()
@@ -225,6 +241,9 @@ var logger = app.Services.GetRequiredService<ILogger<Program>>();
// Используем настройки из DI (Configure<ForwardedHeadersOptions>)
app.UseForwardedHeaders();
// Корреляция логов по запросам
app.UseMiddleware<CorrelationIdMiddleware>();
if (string.IsNullOrEmpty(preinstalledJwtToken))
{
var schedulerFactory = app.Services.GetRequiredService<ISchedulerFactory>();

View File

@@ -0,0 +1,131 @@
using System.Text;
using System.Text.Json;
using Microsoft.Net.Http.Headers;
using ModeusSchedule.Abstractions;
using ModeusSchedule.Abstractions.DTO;
namespace SfeduSchedule.Services;
public class ModeusHttpClient
{
private readonly HttpClient _httpClient;
private readonly IConfiguration _configuration;
private readonly ILogger<ModeusHttpClient> _logger;
public ModeusHttpClient(IHttpClientFactory httpClientFactory,
ILogger<ModeusHttpClient> logger,
IConfiguration configuration)
{
_httpClient = httpClientFactory.CreateClient("modeus");
_logger = logger;
_configuration = configuration;
SetToken(_configuration["TOKEN"]); // Установка предустановленного токена при инициализации, на случай если нет возможности связи с AUTH сервисом
}
public void SetToken(string? token)
{
if (string.IsNullOrWhiteSpace(token)) {
_logger.LogError("SetToken: Предоставленный токен пустой.");
return;
}
_httpClient.DefaultRequestHeaders.Remove(HeaderNames.Authorization);
_httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, $"Bearer {token}");
}
public async Task<string?> GetScheduleAsync(ModeusScheduleRequest msr)
{
var request = new HttpRequestMessage(HttpMethod.Post,
$"schedule-calendar-v2/api/calendar/events/search?tz={_configuration["TZ"]!}");
request.Content = new StringContent(JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions),
Encoding.UTF8, "application/json");
var response = await _httpClient.SendAsync(request);
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
_logger.LogError("GetScheduleAsync: Неуспешный статус при получении расписания: {StatusCode}, Request: {msr}",
response.StatusCode, msr);
return null;
}
return await response.Content.ReadAsStringAsync();
}
public async Task<List<Attendees>> GetAttendeesAsync(Guid eventId)
{
var request = new HttpRequestMessage(HttpMethod.Get,
$"schedule-calendar-v2/api/calendar/events/{eventId}/attendees");
var response = await _httpClient.SendAsync(request);
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
_logger.LogError("GetAttendeesAsync: Неуспешный статус при получении расписания: {StatusCode}, eventId: {eventId}",
response.StatusCode, eventId);
}
List<Attendees>? attendees;
try
{
attendees = Attendees.FromJson(await response.Content.ReadAsStringAsync());
return attendees;
}
catch (Exception ex)
{
_logger.LogError(ex, "GetAttendeesAsync: Deserialization failed.");
}
return new List<Attendees>();
}
public async Task<string?> SearchRoomsAsync(RoomSearchRequest requestDto)
{
var request = new HttpRequestMessage(HttpMethod.Post, "schedule-calendar-v2/api/campus/rooms/search");
request.Content =
new StringContent(JsonSerializer.Serialize(requestDto, GlobalConsts.JsonSerializerOptions),
Encoding.UTF8, "application/json");
var response = await _httpClient.SendAsync(request);
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
_logger.LogError("GetScheduleAsync: Неуспешный статус при получении расписания: {StatusCode}, Request: {requestDto}",
response.StatusCode, requestDto);
return null;
}
return await response.Content.ReadAsStringAsync();
}
public async Task<string?> GetGuidAsync(string fullName)
{
var request = new HttpRequestMessage(HttpMethod.Post, "schedule-calendar-v2/api/people/persons/search");
request.Content = new StringContent(JsonSerializer.Serialize(new
{
fullName,
sort = "+fullName",
size = 10,
page = 0
}), Encoding.UTF8, "application/json");
var response = await _httpClient.SendAsync(request);
_logger.LogInformation("GetGuidAsync: Ответ получен: {StatusCode}", response.StatusCode);
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
_logger.LogError("GetGuidAsync: Неуспешный статус при получении расписания: {StatusCode}, Name: {fullName}",
response.StatusCode, fullName);
return null;
}
var json = await response.Content.ReadAsStringAsync();
string? personId;
try
{
personId = JsonDocument.Parse(json).RootElement
.GetProperty("_embedded")
.GetProperty("persons")[0]
.GetProperty("id")
.GetString();
}
catch
{
_logger.LogWarning(
"GetGuidAsync: не удалось получить идентификатор пользователя. FullName={FullName}", fullName);
return null;
}
return personId;
}
}

View File

@@ -1,10 +1,8 @@
using System.Text;
using System.Text.Json;
using Ical.Net;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
using Ical.Net.Serialization;
using Microsoft.Net.Http.Headers;
using ModeusSchedule.Abstractions;
using ModeusSchedule.Abstractions.DTO;
@@ -13,110 +11,18 @@ namespace SfeduSchedule.Services;
public class ModeusService
{
private readonly IConfiguration _configuration;
private readonly HttpClient _httpClient;
private readonly ILogger<ModeusService> _logger;
public ModeusService(IHttpClientFactory httpClientFactory,
private readonly ILogger<ModeusService> _logger;
private readonly ModeusHttpClient _modeusHttpClient;
public ModeusService(
ILogger<ModeusService> logger,
IConfiguration configuration)
IConfiguration configuration,
ModeusHttpClient modeusHttpClient)
{
_httpClient = httpClientFactory.CreateClient("modeus");
_modeusHttpClient = modeusHttpClient;
_logger = logger;
_configuration = configuration;
SetToken(_configuration["TOKEN"]); // Установка предустановленного токена при инициализации, на случай если нет возможности связи с AUTH сервисом
}
public void SetToken(string? token)
{
if (string.IsNullOrWhiteSpace(token)) {
_logger.LogError("SetToken: Предоставленный токен пустой.");
return;
}
_httpClient.DefaultRequestHeaders.Remove(HeaderNames.Authorization);
_httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, $"Bearer {token}");
}
public async Task<string?> GetScheduleAsync(ModeusScheduleRequest msr)
{
var request = new HttpRequestMessage(HttpMethod.Post,
$"schedule-calendar-v2/api/calendar/events/search?tz={_configuration["TZ"]!}");
request.Content = new StringContent(JsonSerializer.Serialize(msr, GlobalConsts.JsonSerializerOptions),
Encoding.UTF8, "application/json");
var response = await _httpClient.SendAsync(request);
_logger.LogInformation("GetScheduleAsync: Ответ получен: {StatusCode}", response.StatusCode);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
public async Task<List<Attendees>> GetAttendeesAsync(Guid eventId)
{
var request = new HttpRequestMessage(HttpMethod.Get,
$"schedule-calendar-v2/api/calendar/events/{eventId}/attendees");
var response = await _httpClient.SendAsync(request);
_logger.LogInformation("GetAttendeesAsync: Ответ получен: {StatusCode}", response.StatusCode);
response.EnsureSuccessStatusCode();
List<Attendees>? attendees;
try
{
attendees = Attendees.FromJson(await response.Content.ReadAsStringAsync());
return attendees;
}
catch (Exception ex)
{
_logger.LogError(ex, "GetAttendeesAsync: Deserialization failed.");
}
return new List<Attendees>();
}
public async Task<string?> SearchRoomsAsync(RoomSearchRequest requestDto)
{
var request = new HttpRequestMessage(HttpMethod.Post, "schedule-calendar-v2/api/campus/rooms/search");
request.Content =
new StringContent(JsonSerializer.Serialize(requestDto, GlobalConsts.JsonSerializerOptions),
Encoding.UTF8, "application/json");
var response = await _httpClient.SendAsync(request);
_logger.LogInformation("SearchRoomsAsync: Ответ получен: {StatusCode}", response.StatusCode);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
public async Task<string?> GetGuidAsync(string fullName)
{
var request = new HttpRequestMessage(HttpMethod.Post, "schedule-calendar-v2/api/people/persons/search");
request.Content = new StringContent(JsonSerializer.Serialize(new
{
fullName,
sort = "+fullName",
size = 10,
page = 0
}), Encoding.UTF8, "application/json");
var response = await _httpClient.SendAsync(request);
_logger.LogInformation("GetGuidAsync: Ответ получен: {StatusCode}", response.StatusCode);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
string? personId;
try
{
personId = JsonDocument.Parse(json).RootElement
.GetProperty("_embedded")
.GetProperty("persons")[0]
.GetProperty("id")
.GetString();
}
catch
{
_logger.LogWarning(
"GetGuidAsync: Не удалось получить идентификатор пользователя, {FullName}, json: {Json}", fullName,
json);
return null;
}
return personId;
}
public async Task<Schedule?> GetScheduleJsonAsync(ModeusScheduleRequest msr)
@@ -124,7 +30,7 @@ public class ModeusService
var schedule = await GetScheduleAsync(msr);
if (schedule == null)
{
_logger.LogError("GetScheduleJsonAsync: Schedule is null. Request: {@msr}", msr);
_logger.LogError("GetScheduleJsonAsync: schedule is null. {@Request}", msr);
throw new Exception("Schedule is null");
}
@@ -283,8 +189,27 @@ public class ModeusService
var serializer = new CalendarSerializer();
var serializedCalendar = serializer.SerializeToString(calendar);
_logger.LogInformation("GetIcsAsync: Serialized calendar created. Length: {Length}",
_logger.LogInformation("GetIcsAsync: serialized calendar created. Length: {Length}",
serializedCalendar?.Length ?? 0);
return serializedCalendar;
}
#region Проксирование методов из ModeusHttpClient
public async Task<string?> SearchRoomsAsync(RoomSearchRequest request)
{
return await _modeusHttpClient.SearchRoomsAsync(request);
}
public async Task<string?> GetScheduleAsync(ModeusScheduleRequest msr)
{
return await _modeusHttpClient.GetScheduleAsync(msr);
}
public async Task<string?> GetGuidAsync(string fullname)
{
return await _modeusHttpClient.GetGuidAsync(fullname);
}
#endregion
}